dev: Added initial version of the grpc api. Added relevant dummy settings to creds (they have no effect rn)

dev: Yt searches now INTERNALLY return multiple results but there is no way right now to paginate plain text results
dev: moved some stuff around
This commit is contained in:
Kwoth
2024-09-26 07:26:18 +00:00
parent c473669cbc
commit 3c108e531e
45 changed files with 1039 additions and 261 deletions

View File

@@ -30,6 +30,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nadeko.Medusa", "src\Nadeko
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NadekoBot.Generators", "src\NadekoBot.Generators\NadekoBot.Generators.csproj", "{92770AF3-83EE-49F1-A0BB-79124D19A13D}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NadekoBot.Generators", "src\NadekoBot.Generators\NadekoBot.Generators.csproj", "{92770AF3-83EE-49F1-A0BB-79124D19A13D}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NadekoBot.GrpcApiBase", "src\NadekoBot.GrpcApiBase\NadekoBot.GrpcApiBase.csproj", "{FB74B9EA-10B9-4542-ACB1-35523A95A587}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -79,6 +81,12 @@ Global
{92770AF3-83EE-49F1-A0BB-79124D19A13D}.GlobalNadeko|Any CPU.Build.0 = Debug|Any CPU {92770AF3-83EE-49F1-A0BB-79124D19A13D}.GlobalNadeko|Any CPU.Build.0 = Debug|Any CPU
{92770AF3-83EE-49F1-A0BB-79124D19A13D}.Release|Any CPU.ActiveCfg = Release|Any CPU {92770AF3-83EE-49F1-A0BB-79124D19A13D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{92770AF3-83EE-49F1-A0BB-79124D19A13D}.Release|Any CPU.Build.0 = Release|Any CPU {92770AF3-83EE-49F1-A0BB-79124D19A13D}.Release|Any CPU.Build.0 = Release|Any CPU
{FB74B9EA-10B9-4542-ACB1-35523A95A587}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FB74B9EA-10B9-4542-ACB1-35523A95A587}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FB74B9EA-10B9-4542-ACB1-35523A95A587}.GlobalNadeko|Any CPU.ActiveCfg = Debug|Any CPU
{FB74B9EA-10B9-4542-ACB1-35523A95A587}.GlobalNadeko|Any CPU.Build.0 = Debug|Any CPU
{FB74B9EA-10B9-4542-ACB1-35523A95A587}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FB74B9EA-10B9-4542-ACB1-35523A95A587}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -91,6 +99,7 @@ Global
{E685977E-31A4-46F4-A5D7-4E3E39E82E43} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2} {E685977E-31A4-46F4-A5D7-4E3E39E82E43} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2}
{92770AF3-83EE-49F1-A0BB-79124D19A13D} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2} {92770AF3-83EE-49F1-A0BB-79124D19A13D} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2}
{2F4CF6D6-0C2F-4944-B204-9508CDA53195} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2} {2F4CF6D6-0C2F-4944-B204-9508CDA53195} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2}
{FB74B9EA-10B9-4542-ACB1-35523A95A587} = {04929013-5BAB-42B0-B9B2-8F2BB8F16AF2}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5F3F555C-855F-4BE8-B526-D062D3E8ACA4} SolutionGuid = {5F3F555C-855F-4BE8-B526-D062D3E8ACA4}

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.28.2" />
<PackageReference Include="Grpc" Version="2.46.6" />
<PackageReference Include="Grpc.Tools" Version="2.66.0" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<Protobuf Include="protos/*.proto">
<GrpcServices>Server</GrpcServices>
</Protobuf>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,26 @@
syntax = "proto3";
option csharp_namespace = "NadekoBot.GrpcApi";
package econ;
service GrpcEcon {
rpc GetEconomy(EconomyRequest) returns (EconomyReply);
}
message EconomyRequest {
string guildId = 1;
}
message EconomyReply {
uint64 totalOwned = 1;
uint64 byTopOnePercent = 2;
uint64 plantedAmount = 3;
uint64 ownedByTheBot = 4;
uint64 inTheBank = 5;
uint64 totalEconomy = 6;
}
message CurrencyLbRequest {
int32 page = 1;
}

View File

@@ -0,0 +1,50 @@
syntax = "proto3";
option csharp_namespace = "NadekoBot.GrpcApi";
import "google/protobuf/empty.proto";
package exprs;
service GrpcExprs {
rpc GetExprs(GetExprsRequest) returns (GetExprsReply);
rpc AddExpr(AddExprRequest) returns (AddExprReply);
rpc DeleteExpr(DeleteExprRequest) returns (google.protobuf.Empty);
}
message DeleteExprRequest {
string id = 1;
uint64 guildId = 2;
}
message GetExprsRequest {
uint64 guildId = 1;
string query = 2;
int32 page = 3;
}
message GetExprsReply {
repeated ExprDto expressions = 1;
int32 totalCount = 2;
}
message ExprDto {
string id = 1;
string trigger = 2;
string response = 3;
bool ca = 4;
bool ad = 5;
bool dm = 6;
bool at = 7;
}
message AddExprRequest {
uint64 guildId = 1;
ExprDto expr = 2;
}
message AddExprReply {
string id = 1;
bool success = 2;
}

View File

@@ -0,0 +1,57 @@
syntax = "proto3";
option csharp_namespace = "NadekoBot.GrpcApi";
package greet;
service GrpcGreet {
rpc GetGreetSettings (GetGreetRequest) returns (GetGreetReply);
rpc UpdateGreet (UpdateGreetRequest) returns (UpdateGreetReply);
rpc TestGreet (TestGreetRequest) returns (TestGreetReply);
}
message GetGreetReply {
GrpcGreetSettings greet = 1;
GrpcGreetSettings greetDm = 2;
GrpcGreetSettings bye = 3;
GrpcGreetSettings boost = 4;
}
message GrpcGreetSettings {
optional uint64 channelId = 1;
string message = 2;
bool isEnabled = 3;
GrpcGreetType type = 4;
}
message GetGreetRequest {
uint64 guildId = 1;
}
message UpdateGreetRequest {
uint64 guildId = 1;
GrpcGreetSettings settings = 2;
}
enum GrpcGreetType {
Greet = 0;
GreetDm = 1;
Bye = 2;
Boost = 3;
}
message UpdateGreetReply {
bool success = 1;
}
message TestGreetRequest {
uint64 guildId = 1;
uint64 channelId = 2;
uint64 userId = 3;
GrpcGreetType type = 4;
}
message TestGreetReply {
bool success = 1;
string error = 2;
}

View File

@@ -0,0 +1,52 @@
syntax = "proto3";
option csharp_namespace = "NadekoBot.GrpcApi";
package info;
service GrpcInfo {
rpc GetServerInfo(ServerInfoRequest) returns (GetServerInfoReply);
}
message ServerInfoRequest {
uint64 guildId = 1;
}
message GetServerInfoReply {
uint64 id = 1;
string name = 2;
string iconUrl = 3;
uint64 ownerId = 4;
string ownerName = 5;
repeated RoleReply roles = 6;
repeated EmojiReply emojis = 7;
repeated string features = 8;
int32 textChannels = 9;
int32 voiceChannels = 10;
int32 memberCount = 11;
int64 createdAt = 12;
}
message RoleReply {
uint64 id = 1;
string name = 2;
string iconUrl = 3;
string color = 4;
}
message EmojiReply {
string name = 1;
string url = 2;
string code = 3;
}
message ChannelReply {
uint64 id = 1;
string name = 2;
ChannelType type = 3;
}
enum ChannelType {
Text = 0;
Voice = 1;
}

View File

@@ -0,0 +1,81 @@
syntax = "proto3";
option csharp_namespace = "NadekoBot.GrpcApi";
import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
package other;
service GrpcOther {
rpc GetTextChannels(GetTextChannelsRequest) returns (GetTextChannelsReply);
rpc GetCurrencyLb(GetLbRequest) returns (CurrencyLbReply);
rpc GetXpLb(GetLbRequest) returns (XpLbReply);
rpc GetWaifuLb(GetLbRequest) returns (WaifuLbReply);
rpc GetShardStatuses(google.protobuf.Empty) returns (GetShardStatusesReply);
}
message GetShardStatusesReply {
repeated ShardStatusReply shards = 1;
}
message ShardStatusReply {
int32 id = 1;
string status = 2;
int32 guildCount = 3;
google.protobuf.Timestamp lastUpdate = 4;
}
message GetTextChannelsRequest{
uint64 guildId = 1;
}
message GetTextChannelsReply {
repeated TextChannelReply textChannels = 1;
}
message TextChannelReply {
uint64 id = 1;
string name = 2;
}
message CurrencyLbReply {
repeated CurrencyLbEntryReply entries = 1;
}
message CurrencyLbEntryReply {
string user = 1;
uint64 userId = 2;
int64 amount = 3;
string avatar = 4;
}
message GetLbRequest {
int32 page = 1;
int32 perPage = 2;
}
message XpLbReply {
repeated XpLbEntryReply entries = 1;
}
message XpLbEntryReply {
string user = 1;
uint64 userId = 2;
int64 totalXp = 3;
int64 level = 4;
}
message WaifuLbReply {
repeated WaifuLbEntry entries = 1;
}
message WaifuLbEntry {
string user = 1;
string claimedBy = 2;
int64 value = 3;
bool isMutual = 4;
}

View File

@@ -0,0 +1,83 @@
syntax = "proto3";
option csharp_namespace = "NadekoBot.GrpcApi";
package warn;
service GrpcWarn {
rpc GetWarnSettings (WarnSettingsRequest) returns (WarnSettingsReply);
rpc AddWarnp (AddWarnpRequest) returns (AddWarnpReply);
rpc DeleteWarnp (DeleteWarnpRequest) returns (DeleteWarnpReply);
rpc GetUserWarnings(GetUserWarningsRequest) returns (GetUserWarningsReply);
rpc ClearWarning(ClearWarningRequest) returns (ClearWarningReply);
rpc SetWarnExpiry(SetWarnExpiryRequest) returns (SetWarnExpiryReply);
}
message WarnSettingsRequest {
uint64 guildId = 1;
}
message WarnPunishment {
int32 threshold = 1;
string action = 2;
int64 duration = 3;
}
message WarnSettingsReply {
repeated WarnPunishment punishments = 1;
int32 expiryDays = 2;
}
message AddWarnpRequest {
uint64 guildId = 1;
WarnPunishment punishment = 2;
}
message AddWarnpReply {
bool success = 1;
}
message DeleteWarnpRequest {
uint64 guildId = 1;
int32 warnpIndex = 2;
}
message DeleteWarnpReply {
bool success = 1;
}
message GetUserWarningsRequest {
uint64 guildId = 1;
uint64 user_id = 2;
}
message GetUserWarningsReply {
repeated Warning warnings = 1;
}
message Warning {
int32 id = 1;
string reason = 2;
int64 timestamp = 3;
int64 expiry_timestamp = 4;
bool cleared = 5;
string clearedBy = 6;
}
message ClearWarningRequest {
uint64 guildId = 1;
uint64 userId = 2;
optional int32 warnId = 3;
}
message ClearWarningReply {
bool success = 1;
}
message SetWarnExpiryRequest {
uint64 guildId = 1;
int32 expiryDays = 2;
}
message SetWarnExpiryReply {
bool success = 1;
}

View File

@@ -789,7 +789,7 @@ public sealed class NadekoExpressionsService : IExecOnMessage, IReadyExecutor
if (newguildExpressions.TryGetValue(guildId, out var exprs)) if (newguildExpressions.TryGetValue(guildId, out var exprs))
{ {
return (exprs.Where(x => x.Trigger.Contains(query)) return (exprs.Where(x => x.Trigger.Contains(query) || x.Response.Contains(query))
.Skip(page * 9) .Skip(page * 9)
.Take(9) .Take(9)
.ToArray(), exprs.Length); .ToArray(), exprs.Length);

View File

@@ -227,7 +227,7 @@ public partial class Gambling
if (page > 100) if (page > 100)
page = 100; page = 100;
var waifus = _service.GetTopWaifusAtPage(page).ToList(); var waifus = await _service.GetTopWaifusAtPage(page);
if (waifus.Count == 0) if (waifus.Count == 0)
{ {

View File

@@ -300,10 +300,10 @@ public class WaifuService : INService, IReadyExecutor
return (oldAff, success, remaining); return (oldAff, success, remaining);
} }
public IEnumerable<WaifuLbResult> GetTopWaifusAtPage(int page, int perPage = 9) public async Task<IReadOnlyList<WaifuLbResult>> GetTopWaifusAtPage(int page, int perPage = 9)
{ {
using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
return uow.Set<WaifuInfo>().GetTop(perPage, page * perPage); return await uow.Set<WaifuInfo>().GetTop(perPage, page * perPage);
} }
public ulong GetWaifuUserId(ulong ownerId, string name) public ulong GetWaifuUserId(ulong ownerId, string name)

View File

@@ -25,14 +25,14 @@ public static class WaifuExtensions
return includes(waifus).AsQueryable().FirstOrDefault(wi => wi.Waifu.UserId == userId); return includes(waifus).AsQueryable().FirstOrDefault(wi => wi.Waifu.UserId == userId);
} }
public static IEnumerable<WaifuLbResult> GetTop(this DbSet<WaifuInfo> waifus, int count, int skip = 0) public static async Task<IReadOnlyList<WaifuLbResult>> GetTop(this DbSet<WaifuInfo> waifus, int count, int skip = 0)
{ {
ArgumentOutOfRangeException.ThrowIfNegative(count); ArgumentOutOfRangeException.ThrowIfNegative(count);
if (count == 0) if (count == 0)
return []; return [];
return waifus.Include(wi => wi.Waifu) return await waifus.Include(wi => wi.Waifu)
.Include(wi => wi.Affinity) .Include(wi => wi.Affinity)
.Include(wi => wi.Claimer) .Include(wi => wi.Claimer)
.OrderByDescending(wi => wi.Price) .OrderByDescending(wi => wi.Price)
@@ -48,7 +48,7 @@ public static class WaifuExtensions
Discrim = x.Waifu.Discriminator, Discrim = x.Waifu.Discriminator,
Price = x.Price Price = x.Price
}) })
.ToList(); .ToListAsyncEF();
} }
public static decimal GetTotalValue(this DbSet<WaifuInfo> waifus) public static decimal GetTotalValue(this DbSet<WaifuInfo> waifus)

View File

@@ -43,8 +43,7 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver
+ "--no-check-certificate " + "--no-check-certificate "
+ "-i " + "-i "
+ "--yes-playlist " + "--yes-playlist "
+ "-- \"{0}\"", + "-- \"{0}\"");
scs.Data.YtProvider != YoutubeSearcher.Ytdl);
_ytdlIdOperation = new("-4 " _ytdlIdOperation = new("-4 "
+ "--geo-bypass " + "--geo-bypass "
@@ -56,8 +55,7 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver
+ "--get-thumbnail " + "--get-thumbnail "
+ "--get-duration " + "--get-duration "
+ "--no-check-certificate " + "--no-check-certificate "
+ "-- \"{0}\"", + "-- \"{0}\"");
scs.Data.YtProvider != YoutubeSearcher.Ytdl);
_ytdlSearchOperation = new("-4 " _ytdlSearchOperation = new("-4 "
+ "--geo-bypass " + "--geo-bypass "
@@ -70,8 +68,7 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver
+ "--get-duration " + "--get-duration "
+ "--no-check-certificate " + "--no-check-certificate "
+ "--default-search " + "--default-search "
+ "\"ytsearch:\" -- \"{0}\"", + "\"ytsearch:\" -- \"{0}\"");
scs.Data.YtProvider != YoutubeSearcher.Ytdl);
} }
private YtTrackData ResolveYtdlData(string ytdlOutputString) private YtTrackData ResolveYtdlData(string ytdlOutputString)
@@ -291,6 +288,7 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver
public Task<string?> GetStreamUrl(string videoId) public Task<string?> GetStreamUrl(string videoId)
=> CreateCacherFactory(videoId)(); => CreateCacherFactory(videoId)();
private readonly struct YtTrackData private readonly struct YtTrackData
{ {
public readonly string Title; public readonly string Title;

View File

@@ -7,10 +7,9 @@ public sealed class DefaultSearchServiceFactory : ISearchServiceFactory, INServi
{ {
private readonly SearchesConfigService _scs; private readonly SearchesConfigService _scs;
private readonly SearxSearchService _sss; private readonly SearxSearchService _sss;
private readonly YtDlpSearchService _ytdlp;
private readonly GoogleSearchService _gss; private readonly GoogleSearchService _gss;
private readonly YtdlpYoutubeSearchService _ytdlp;
private readonly YtdlYoutubeSearchService _ytdl;
private readonly YoutubeDataApiSearchService _ytdata; private readonly YoutubeDataApiSearchService _ytdata;
private readonly InvidiousYtSearchService _iYtSs; private readonly InvidiousYtSearchService _iYtSs;
private readonly GoogleScrapeService _gscs; private readonly GoogleScrapeService _gscs;
@@ -20,19 +19,17 @@ public sealed class DefaultSearchServiceFactory : ISearchServiceFactory, INServi
GoogleSearchService gss, GoogleSearchService gss,
GoogleScrapeService gscs, GoogleScrapeService gscs,
SearxSearchService sss, SearxSearchService sss,
YtdlpYoutubeSearchService ytdlp, YtDlpSearchService ytdlp,
YtdlYoutubeSearchService ytdl,
YoutubeDataApiSearchService ytdata, YoutubeDataApiSearchService ytdata,
InvidiousYtSearchService iYtSs) InvidiousYtSearchService iYtSs)
{ {
_scs = scs; _scs = scs;
_sss = sss; _sss = sss;
_ytdlp = ytdlp;
_gss = gss; _gss = gss;
_gscs = gscs; _gscs = gscs;
_iYtSs = iYtSs; _iYtSs = iYtSs;
_ytdlp = ytdlp;
_ytdl = ytdl;
_ytdata = ytdata; _ytdata = ytdata;
} }
@@ -57,9 +54,8 @@ public sealed class DefaultSearchServiceFactory : ISearchServiceFactory, INServi
=> _scs.Data.YtProvider switch => _scs.Data.YtProvider switch
{ {
YoutubeSearcher.YtDataApiv3 => _ytdata, YoutubeSearcher.YtDataApiv3 => _ytdata,
YoutubeSearcher.Ytdlp => _ytdlp,
YoutubeSearcher.Ytdl => _ytdl,
YoutubeSearcher.Invidious => _iYtSs, YoutubeSearcher.Invidious => _iYtSs,
_ => _ytdl YoutubeSearcher.Ytdlp => _ytdlp,
_ => throw new ArgumentOutOfRangeException()
}; };
} }

View File

@@ -93,16 +93,12 @@ public partial class Searches
return; return;
} }
var embeds = new List<EmbedBuilder>(4);
EmbedBuilder CreateEmbed(IImageSearchResultEntry entry) EmbedBuilder CreateEmbed(IImageSearchResultEntry entry)
{ {
return _sender.CreateEmbed() return _sender.CreateEmbed()
.WithOkColor() .WithOkColor()
.WithAuthor(ctx.User) .WithAuthor(ctx.User)
.WithTitle(query) .WithTitle(query)
.WithUrl("https://google.com")
.WithImageUrl(entry.Link); .WithImageUrl(entry.Link);
} }
@@ -120,55 +116,50 @@ public partial class Searches
.WithDescription(GetText(strs.no_search_results)); .WithDescription(GetText(strs.no_search_results));
var embed = CreateEmbed(item); var embed = CreateEmbed(item);
embeds.Add(embed);
return embed; return embed;
}) })
.SendAsync(); .SendAsync();
} }
private TypedKey<string> GetYtCacheKey(string query) private TypedKey<string[]> GetYtCacheKey(string query)
=> new($"search:youtube:{query}"); => new($"search:yt:{query}");
private async Task AddYoutubeUrlToCacheAsync(string query, string url) private async Task AddYoutubeUrlToCacheAsync(string query, string[] url)
=> await _cache.AddAsync(GetYtCacheKey(query), url, expiry: 1.Hours()); => await _cache.AddAsync(GetYtCacheKey(query), url, expiry: 1.Hours());
private async Task<VideoInfo?> GetYoutubeUrlFromCacheAsync(string query) private async Task<VideoInfo[]?> GetYoutubeUrlFromCacheAsync(string query)
{ {
var result = await _cache.GetAsync(GetYtCacheKey(query)); var result = await _cache.GetAsync(GetYtCacheKey(query));
if (!result.TryGetValue(out var url) || string.IsNullOrWhiteSpace(url)) if (!result.TryGetValue(out var urls) || urls.Length == 0)
return null; return null;
return new VideoInfo() return urls.Map(url => new VideoInfo()
{ {
Url = url Url = url
}; });
} }
[Cmd] [Cmd]
public async Task Youtube([Leftover] string? query = null) public async Task Youtube([Leftover] string query)
{ {
query = query?.Trim(); query = query.Trim();
if (string.IsNullOrWhiteSpace(query))
{
await Response().Error(strs.specify_search_params).SendAsync();
return;
}
_ = ctx.Channel.TriggerTypingAsync(); _ = ctx.Channel.TriggerTypingAsync();
var maybeResult = await GetYoutubeUrlFromCacheAsync(query) var maybeResults = await GetYoutubeUrlFromCacheAsync(query)
?? await _searchFactory.GetYoutubeSearchService().SearchAsync(query); ?? await _searchFactory.GetYoutubeSearchService().SearchAsync(query);
if (maybeResult is not { } result || result is { Url: null })
if (maybeResults is not { } result || result.Length == 0)
{ {
await Response().Error(strs.no_results).SendAsync(); await Response().Error(strs.no_results).SendAsync();
return; return;
} }
await AddYoutubeUrlToCacheAsync(query, result.Url); await AddYoutubeUrlToCacheAsync(query, result.Map(x => x.Url));
await Response().Text(result.Url).SendAsync();
await Response().Text(result[0].Url).SendAsync();
} }
// [Cmd] // [Cmd]

View File

@@ -2,5 +2,5 @@
public interface IYoutubeSearchService public interface IYoutubeSearchService
{ {
Task<VideoInfo?> SearchAsync(string query); Task<VideoInfo[]?> SearchAsync(string query);
} }

View File

@@ -18,7 +18,7 @@ public sealed class InvidiousYtSearchService : IYoutubeSearchService, INService
_rng = new(); _rng = new();
} }
public async Task<VideoInfo?> SearchAsync(string query) public async Task<VideoInfo[]?> SearchAsync(string query)
{ {
ArgumentNullException.ThrowIfNull(query); ArgumentNullException.ThrowIfNull(query);
@@ -35,6 +35,7 @@ public sealed class InvidiousYtSearchService : IYoutubeSearchService, INService
var url = $"{instance}/api/v1/search" var url = $"{instance}/api/v1/search"
+ $"?q={query}" + $"?q={query}"
+ $"&type=video"; + $"&type=video";
using var http = _http.CreateClient(); using var http = _http.CreateClient();
var res = await http.GetFromJsonAsync<List<InvidiousSearchResponse>>( var res = await http.GetFromJsonAsync<List<InvidiousSearchResponse>>(
url); url);
@@ -42,6 +43,6 @@ public sealed class InvidiousYtSearchService : IYoutubeSearchService, INService
if (res is null or { Count: 0 }) if (res is null or { Count: 0 })
return null; return null;
return new VideoInfo(res[0].VideoId); return res.Map(r => new VideoInfo(r.VideoId));
} }
} }

View File

@@ -9,18 +9,15 @@ public sealed class YoutubeDataApiSearchService : IYoutubeSearchService, INServi
_gapi = gapi; _gapi = gapi;
} }
public async Task<VideoInfo?> SearchAsync(string query) public async Task<VideoInfo[]?> SearchAsync(string query)
{ {
ArgumentNullException.ThrowIfNull(query); ArgumentNullException.ThrowIfNull(query);
var results = await _gapi.GetVideoLinksByKeywordAsync(query); var results = await _gapi.GetVideoLinksByKeywordAsync(query);
var first = results.FirstOrDefault();
if (first is null) if(results.Count == 0)
return null; return null;
return new() return results.Map(r => new VideoInfo(r));
{
Url = first
};
} }
} }

View File

@@ -0,0 +1,26 @@
namespace NadekoBot.Modules.Searches.Youtube;
public class YtDlpSearchService : IYoutubeSearchService, INService
{
private YtdlOperation CreateYtdlOp(int count)
=> new YtdlOperation("-4 "
+ "--ignore-errors --flat-playlist --skip-download --quiet "
+ "--geo-bypass "
+ "--encoding UTF8 "
+ "--get-id "
+ "--no-check-certificate "
+ "--default-search "
+ $"\"ytsearch{count}:\" -- \"{{0}}\"");
public async Task<VideoInfo[]?> SearchAsync(string query)
{
var op = CreateYtdlOp(5);
var data = await op.GetDataAsync(query);
var items = data?.Split('\n');
if (items is null or { Length: 0 })
return null;
return items
.Map(x => new VideoInfo(x));
}
}

View File

@@ -1,7 +0,0 @@
namespace NadekoBot.Modules.Searches.Youtube;
public sealed class YtdlYoutubeSearchService : YoutubedlxServiceBase, INService
{
public override async Task<VideoInfo?> SearchAsync(string query)
=> await InternalGetInfoAsync(query, false);
}

View File

@@ -1,7 +0,0 @@
namespace NadekoBot.Modules.Searches.Youtube;
public sealed class YtdlpYoutubeSearchService : YoutubedlxServiceBase, INService
{
public override async Task<VideoInfo?> SearchAsync(string query)
=> await InternalGetInfoAsync(query, true);
}

View File

@@ -1,34 +0,0 @@
namespace NadekoBot.Modules.Searches.Youtube;
public abstract class YoutubedlxServiceBase : IYoutubeSearchService
{
private YtdlOperation CreateYtdlOp(bool isYtDlp)
=> new YtdlOperation("-4 "
+ "--geo-bypass "
+ "--encoding UTF8 "
+ "--get-id "
+ "--no-check-certificate "
+ "--default-search "
+ "\"ytsearch:\" -- \"{0}\"",
isYtDlp: isYtDlp);
protected async Task<VideoInfo?> InternalGetInfoAsync(string query, bool isYtDlp)
{
var op = CreateYtdlOp(isYtDlp);
var data = await op.GetDataAsync(query);
var items = data?.Split('\n');
if (items is null or { Length: 0 })
return null;
var id = items.FirstOrDefault(x => x.Length is > 5 and < 15);
if (id is null)
return null;
return new VideoInfo()
{
Url = $"https://youtube.com/watch?v={id}"
};
}
public abstract Task<VideoInfo?> SearchAsync(string query);
}

View File

@@ -77,9 +77,9 @@ public sealed class FollowedStreamConfig
public enum YoutubeSearcher public enum YoutubeSearcher
{ {
YtDataApiv3, YtDataApiv3 = 0,
Ytdl, Ytdl = 1,
Ytdlp, Ytdlp = 1,
Invid, Invid = 3,
Invidious = 3 Invidious = 3
} }

View File

@@ -34,13 +34,12 @@
<PackageReference Include="Google.Apis.Urlshortener.v1" Version="1.41.1.138"/> <PackageReference Include="Google.Apis.Urlshortener.v1" Version="1.41.1.138"/>
<PackageReference Include="Google.Apis.YouTube.v3" Version="1.68.0.3414"/> <PackageReference Include="Google.Apis.YouTube.v3" Version="1.68.0.3414"/>
<PackageReference Include="Google.Apis.Customsearch.v1" Version="1.49.0.2084"/> <PackageReference Include="Google.Apis.Customsearch.v1" Version="1.49.0.2084"/>
<!-- <PackageReference Include="Grpc.AspNetCore" Version="2.62.0" />-->
<PackageReference Include="Google.Protobuf" Version="3.26.1"/> <PackageReference Include="Google.Protobuf" Version="3.28.2" />
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.62.0"/> <PackageReference Include="Grpc" Version="2.46.6" />
<PackageReference Include="Grpc.Tools" Version="2.63.0"> <PackageReference Include="Grpc.Net.Client" Version="2.62.0" />
<PrivateAssets>all</PrivateAssets> <PackageReference Include="Grpc.Tools" Version="2.66.0" PrivateAssets="All" />
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.5.0"/> <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.5.0"/>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0"/> <PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0"/>
@@ -69,19 +68,19 @@
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1"/> <PackageReference Include="Serilog.Sinks.Console" Version="5.0.1"/>
<PackageReference Include="Serilog.Sinks.Seq" Version="7.0.1"/> <PackageReference Include="Serilog.Sinks.Seq" Version="7.0.1"/>
<PackageReference Include="SixLabors.Fonts" Version="2.0.4" /> <PackageReference Include="SixLabors.Fonts" Version="2.0.4"/>
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.5"/>
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.4" /> <PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.4"/>
<PackageReference Include="SixLabors.Shapes" Version="1.0.0-beta0009"/> <PackageReference Include="SixLabors.Shapes" Version="1.0.0-beta0009"/>
<PackageReference Include="StackExchange.Redis" Version="2.8.0" /> <PackageReference Include="StackExchange.Redis" Version="2.8.0"/>
<PackageReference Include="YamlDotNet" Version="15.1.4"/> <PackageReference Include="YamlDotNet" Version="15.1.4"/>
<PackageReference Include="SharpToken" Version="2.0.3" /> <PackageReference Include="SharpToken" Version="2.0.3"/>
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0"/> <PackageReference Include="JetBrains.Annotations" Version="2023.3.0"/>
<!-- Db-related packages --> <!-- Db-related packages -->
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.4"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.4">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -89,7 +88,7 @@
<PackageReference Include="linq2db.EntityFrameworkCore" Version="8.1.0"/> <PackageReference Include="linq2db.EntityFrameworkCore" Version="8.1.0"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.8" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.8"/>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4"/> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4"/>
<PackageReference Include="EFCore.NamingConventions" Version="8.0.3"/> <PackageReference Include="EFCore.NamingConventions" Version="8.0.3"/>
@@ -103,20 +102,17 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\NadekoBot.GrpcApiBase\NadekoBot.GrpcApiBase.csproj"/>
<ProjectReference Include="..\Nadeko.Medusa\Nadeko.Medusa.csproj"/> <ProjectReference Include="..\Nadeko.Medusa\Nadeko.Medusa.csproj"/>
<ProjectReference Include="..\NadekoBot.Voice\NadekoBot.Voice.csproj"/> <ProjectReference Include="..\NadekoBot.Voice\NadekoBot.Voice.csproj"/>
<ProjectReference Include="..\NadekoBot.Generators\NadekoBot.Generators.csproj" OutputItemType="Analyzer"/> <ProjectReference Include="..\NadekoBot.Generators\NadekoBot.Generators.csproj" OutputItemType="Analyzer"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<AdditionalFiles Include="data\strings\responses\responses.en-US.json"/> <AdditionalFiles Include="data\strings\responses\responses.en-US.json"/>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Protobuf Include="..\NadekoBot.Coordinator\Protos\coordinator.proto" GrpcServices="Client">
<Link>Protos\coordinator.proto</Link>
</Protobuf>
<None Update="data\**\*"> <None Update="data\**\*">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile> <ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@@ -132,7 +128,10 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Grpc\" /> <Protobuf Include="..\NadekoBot.Coordinator\Protos\coordinator.proto">
<Link>_common\CoordinatorProtos\coordinator.proto</Link>
<!-- <GrpcServices>Client</GrpcServices>-->
</Protobuf>
</ItemGroup> </ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'GlobalNadeko' "> <PropertyGroup Condition=" '$(Configuration)' == 'GlobalNadeko' ">

View File

@@ -0,0 +1,73 @@
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using NadekoBot.Db.Models;
using NadekoBot.Modules.NadekoExpressions;
namespace NadekoBot.GrpcApi;
public class ExprsSvc : GrpcExprs.GrpcExprsBase, INService
{
private readonly NadekoExpressionsService _svc;
public ExprsSvc(NadekoExpressionsService svc)
{
_svc = svc;
}
public override async Task<AddExprReply> AddExpr(AddExprRequest request, ServerCallContext context)
{
NadekoExpression expr;
if (!string.IsNullOrWhiteSpace(request.Expr.Id))
{
expr = await _svc.EditAsync(request.GuildId,
new kwum(request.Expr.Id),
request.Expr.Response,
request.Expr.Ca,
request.Expr.Ad,
request.Expr.Dm);
}
else
{
expr = await _svc.AddAsync(request.GuildId,
request.Expr.Trigger,
request.Expr.Response,
request.Expr.Ca,
request.Expr.Ad,
request.Expr.Dm);
}
return new AddExprReply()
{
Id = new kwum(expr.Id).ToString(),
Success = true,
};
}
public override async Task<GetExprsReply> GetExprs(GetExprsRequest request, ServerCallContext context)
{
var (exprs, totalCount) = await _svc.FindExpressionsAsync(request.GuildId, request.Query, request.Page);
var reply = new GetExprsReply();
reply.TotalCount = totalCount;
reply.Expressions.AddRange(exprs.Select(x => new ExprDto()
{
Ad = x.AutoDeleteTrigger,
At = x.AllowTarget,
Ca = x.ContainsAnywhere,
Dm = x.DmResponse,
Response = x.Response,
Id = new kwum(x.Id).ToString(),
Trigger = x.Trigger,
}));
return reply;
}
public override async Task<Empty> DeleteExpr(DeleteExprRequest request, ServerCallContext context)
{
await _svc.DeleteAsync(request.GuildId, new kwum(request.Id));
return new Empty();
}
}

View File

@@ -0,0 +1,121 @@
using Grpc.Core;
using GreetType = NadekoBot.Services.GreetType;
namespace NadekoBot.GrpcApi;
public sealed class GreetByeSvc : GrpcGreet.GrpcGreetBase, INService
{
private readonly GreetService _gs;
private readonly DiscordSocketClient _client;
public GreetByeSvc(GreetService gs, DiscordSocketClient client)
{
_gs = gs;
_client = client;
}
public GreetSettings GetDefaultGreet(GreetType type)
=> new GreetSettings()
{
GreetType = type
};
private static GrpcGreetSettings ToConf(GreetSettings? conf)
{
if (conf is null)
return new GrpcGreetSettings();
return new GrpcGreetSettings()
{
Message = conf.MessageText,
Type = (GrpcGreetType)conf.GreetType,
ChannelId = conf.ChannelId ?? 0,
IsEnabled = conf.IsEnabled
};
}
public override async Task<GetGreetReply> GetGreetSettings(GetGreetRequest request, ServerCallContext context)
{
var guildId = request.GuildId;
var greetConf = await _gs.GetGreetSettingsAsync(guildId, GreetType.Greet);
var byeConf = await _gs.GetGreetSettingsAsync(guildId, GreetType.Bye);
var boostConf = await _gs.GetGreetSettingsAsync(guildId, GreetType.Boost);
var greetDmConf = await _gs.GetGreetSettingsAsync(guildId, GreetType.GreetDm);
// todo timer
return new GetGreetReply()
{
Greet = ToConf(greetConf),
Bye = ToConf(byeConf),
Boost = ToConf(boostConf),
GreetDm = ToConf(greetDmConf)
};
}
public override async Task<UpdateGreetReply> UpdateGreet(UpdateGreetRequest request, ServerCallContext context)
{
var gid = request.GuildId;
var s = request.Settings;
var msg = s.Message;
await _gs.SetMessage(gid, GetGreetType(s.Type), msg);
await _gs.SetGreet(gid, s.ChannelId, GetGreetType(s.Type), s.IsEnabled);
return new()
{
Success = true
};
}
public override Task<TestGreetReply> TestGreet(TestGreetRequest request, ServerCallContext context)
=> TestGreet(request.GuildId, request.ChannelId, request.UserId, request.Type);
private async Task<TestGreetReply> TestGreet(
ulong guildId,
ulong channelId,
ulong userId,
GrpcGreetType gtDto)
{
var g = _client.GetGuild(guildId) as IGuild;
if (g is null)
{
return new()
{
Error = "Guild doesn't exist",
Success = false,
};
}
var gu = await g.GetUserAsync(userId);
var ch = await g.GetTextChannelAsync(channelId);
if (gu is null || ch is null)
return new TestGreetReply()
{
Error = "Guild or channel doesn't exist",
Success = false,
};
var gt = GetGreetType(gtDto);
await _gs.Test(guildId, gt, ch, gu);
return new TestGreetReply()
{
Success = true
};
}
private static GreetType GetGreetType(GrpcGreetType gtDto)
{
return gtDto switch
{
GrpcGreetType.Greet => GreetType.Greet,
GrpcGreetType.GreetDm => GreetType.GreetDm,
GrpcGreetType.Bye => GreetType.Bye,
GrpcGreetType.Boost => GreetType.Boost,
_ => throw new ArgumentOutOfRangeException(nameof(gtDto), gtDto, null)
};
}
}

View File

@@ -0,0 +1,123 @@
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using NadekoBot.Modules.Gambling.Services;
using NadekoBot.Modules.Xp.Services;
namespace NadekoBot.GrpcApi;
public sealed class OtherSvc : GrpcOther.GrpcOtherBase, INService
{
private readonly IDiscordClient _client;
private readonly XpService _xp;
private readonly ICurrencyService _cur;
private readonly WaifuService _waifus;
private readonly ICoordinator _coord;
public OtherSvc(
DiscordSocketClient client,
XpService xp,
ICurrencyService cur,
WaifuService waifus,
ICoordinator coord)
{
_client = client;
_xp = xp;
_cur = cur;
_waifus = waifus;
_coord = coord;
}
public override async Task<GetTextChannelsReply> GetTextChannels(GetTextChannelsRequest request, ServerCallContext context)
{
var g = await _client.GetGuildAsync(request.GuildId);
var reply = new GetTextChannelsReply();
var chs = await g.GetTextChannelsAsync();
reply.TextChannels.AddRange(chs.Select(x => new TextChannelReply()
{
Id = x.Id,
Name = x.Name,
}));
return reply;
}
public override async Task<CurrencyLbReply> GetCurrencyLb(GetLbRequest request, ServerCallContext context)
{
var users = await _cur.GetTopRichest(_client.CurrentUser.Id, request.Page, request.PerPage);
var reply = new CurrencyLbReply();
var entries = users.Select(async x =>
{
var user = await _client.GetUserAsync(x.UserId, CacheMode.CacheOnly);
return new CurrencyLbEntryReply()
{
Amount = x.CurrencyAmount,
User = user.ToString(),
UserId = x.UserId,
Avatar = user.RealAvatarUrl().ToString()
};
});
reply.Entries.AddRange(await entries.WhenAll());
return reply;
}
public override async Task<XpLbReply> GetXpLb(GetLbRequest request, ServerCallContext context)
{
var users = await _xp.GetUserXps(request.Page, request.PerPage);
var reply = new XpLbReply();
var entries = users.Select(x =>
{
var lvl = new LevelStats(x.TotalXp);
return new XpLbEntryReply()
{
Level = lvl.Level,
TotalXp = x.TotalXp,
User = x.Username,
UserId = x.UserId
};
});
reply.Entries.AddRange(entries);
return reply;
}
public override async Task<WaifuLbReply> GetWaifuLb(GetLbRequest request, ServerCallContext context)
{
var waifus = await _waifus.GetTopWaifusAtPage(request.Page, request.PerPage);
var reply = new WaifuLbReply();
reply.Entries.AddRange(waifus.Select(x => new WaifuLbEntry()
{
ClaimedBy = x.Claimer,
IsMutual = x.Claimer == x.Affinity,
Value = x.Price,
User = x.Username,
}));
return reply;
}
public override Task<GetShardStatusesReply> GetShardStatuses(Empty request, ServerCallContext context)
{
var reply = new GetShardStatusesReply();
var shards = _coord.GetAllShardStatuses();
reply.Shards.AddRange(shards.Select(x => new ShardStatusReply()
{
Id = x.ShardId,
Status = x.ConnectionState.ToString(),
GuildCount = x.GuildCount,
LastUpdate = Timestamp.FromDateTime(x.LastUpdate),
}));
return Task.FromResult(reply);
}
}

View File

@@ -0,0 +1,49 @@
using Grpc.Core;
namespace NadekoBot.GrpcApi;
public sealed class ServerInfoSvc : GrpcInfo.GrpcInfoBase, INService
{
private readonly IStatsService _stats;
public ServerInfoSvc(IStatsService stats)
{
_stats = stats;
}
public override Task<GetServerInfoReply> GetServerInfo(ServerInfoRequest request, ServerCallContext context)
{
var info = _stats.GetGuildInfo(request.GuildId);
var reply = new GetServerInfoReply()
{
Id = info.Id,
Name = info.Name,
IconUrl = info.IconUrl,
OwnerId = info.OwnerId,
OwnerName = info.Owner,
TextChannels = info.TextChannels,
VoiceChannels = info.VoiceChannels,
MemberCount = info.MemberCount,
CreatedAt = info.CreatedAt.Ticks,
};
reply.Features.AddRange(info.Features);
reply.Emojis.AddRange(info.Emojis.Select(x => new EmojiReply()
{
Name = x.Name,
Url = x.Url,
Code = x.ToString()
}));
reply.Roles.AddRange(info.Roles.Select(x => new RoleReply()
{
Id = x.Id,
Name = x.Name,
IconUrl = x.GetIconUrl() ?? string.Empty,
Color = x.Color.ToString()
}));
return Task.FromResult(reply);
}
}

View File

@@ -0,0 +1,63 @@
using Grpc;
using Grpc.Core;
using NadekoBot.Common.ModuleBehaviors;
namespace NadekoBot.GrpcApi;
public class GrpcApiService : INService, IReadyExecutor
{
private Server? _app;
private static readonly bool _isEnabled = true;
private readonly string _host = "localhost";
private readonly int _port = 5030;
private readonly ServerCredentials _creds = ServerCredentials.Insecure;
private readonly OtherSvc _other;
private readonly ExprsSvc _exprs;
private readonly ServerInfoSvc _info;
private readonly GreetByeSvc _greet;
public GrpcApiService(
OtherSvc other,
ExprsSvc exprs,
ServerInfoSvc info,
GreetByeSvc greet)
{
_other = other;
_exprs = exprs;
_info = info;
_greet = greet;
}
public async Task OnReadyAsync()
{
if (!_isEnabled)
return;
try
{
_app = new()
{
Services =
{
GrpcOther.BindService(_other),
GrpcExprs.BindService(_exprs),
GrpcInfo.BindService(_info),
GrpcGreet.BindService(_greet)
},
Ports =
{
new(_host, _port, _creds),
}
};
_app.Start();
}
finally
{
_app?.ShutdownAsync().GetAwaiter().GetResult();
}
Log.Information("Grpc Api Server started on port {Host}:{Port}", _host, _port);
}
}

View File

@@ -6,7 +6,7 @@ namespace NadekoBot.Common;
public sealed class Creds : IBotCredentials public sealed class Creds : IBotCredentials
{ {
[Comment("""DO NOT CHANGE""")] [Comment("""DO NOT CHANGE""")]
public int Version { get; set; } public int Version { get; set; } = 10;
[Comment("""Bot token. Do not share with anyone ever -> https://discordapp.com/developers/applications/""")] [Comment("""Bot token. Do not share with anyone ever -> https://discordapp.com/developers/applications/""")]
public string Token { get; set; } public string Token { get; set; }
@@ -17,7 +17,8 @@ public sealed class Creds : IBotCredentials
""")] """)]
public ICollection<ulong> OwnerIds { get; set; } public ICollection<ulong> OwnerIds { get; set; }
[Comment("Keep this on 'true' unless you're sure your bot shouldn't use privileged intents or you're waiting to be accepted")] [Comment(
"Keep this on 'true' unless you're sure your bot shouldn't use privileged intents or you're waiting to be accepted")]
public bool UsePrivilegedIntents { get; set; } public bool UsePrivilegedIntents { get; set; }
[Comment(""" [Comment("""
@@ -154,9 +155,16 @@ public sealed class Creds : IBotCredentials
""")] """)]
public RestartConfig RestartCommand { get; set; } public RestartConfig RestartCommand { get; set; }
[Comment("""
Settings for the grpc api.
We don't provide support for this.
If you leave certPath empty, the api will run on http.
""")]
public ApiConfig Api { get; set; }
public Creds() public Creds()
{ {
Version = 9;
Token = string.Empty; Token = string.Empty;
UsePrivilegedIntents = true; UsePrivilegedIntents = true;
OwnerIds = new List<ulong>(); OwnerIds = new List<ulong>();
@@ -179,6 +187,8 @@ public sealed class Creds : IBotCredentials
RestartCommand = new RestartConfig(); RestartCommand = new RestartConfig();
Google = new GoogleApiConfig(); Google = new GoogleApiConfig();
Api = new ApiConfig();
} }
public class DbOptions public class DbOptions
@@ -272,6 +282,15 @@ public sealed class Creds : IBotCredentials
DiscordsKey = discordsKey; DiscordsKey = discordsKey;
} }
} }
public sealed record ApiConfig
{
public bool Enabled { get; set; } = false;
public string CertPath { get; set; } = string.Empty;
public string CertPassword { get; set; } = string.Empty;
public string Host { get; set; } = "localhost";
public int Port { get; set; } = 43120;
}
} }
public class GoogleApiConfig : IGoogleApiConfig public class GoogleApiConfig : IGoogleApiConfig
@@ -279,6 +298,3 @@ public class GoogleApiConfig : IGoogleApiConfig
public string SearchId { get; init; } public string SearchId { get; init; }
public string ImageSearchId { get; init; } public string ImageSearchId { get; init; }
} }

View File

@@ -140,15 +140,9 @@ public sealed class BotCredsProvider : IBotCredsProvider
creds.BotCache = BotCacheImplemenation.Redis; creds.BotCache = BotCacheImplemenation.Redis;
} }
if (creds.Version <= 6) if (creds.Version <= 9)
{ {
creds.Version = 7; creds.Version = 10;
File.WriteAllText(CREDS_FILE_NAME, Yaml.Serializer.Serialize(creds));
}
if (creds.Version <= 8)
{
creds.Version = 9;
File.WriteAllText(CREDS_FILE_NAME, Yaml.Serializer.Serialize(creds)); File.WriteAllText(CREDS_FILE_NAME, Yaml.Serializer.Serialize(creds));
} }
} }

View File

@@ -75,7 +75,7 @@ public sealed partial class GoogleApiService : IGoogleApiService, INService
return (await query.ExecuteAsync()).Items.Select(i => "https://www.youtube.com/watch?v=" + i.Id.VideoId).Skip(1); return (await query.ExecuteAsync()).Items.Select(i => "https://www.youtube.com/watch?v=" + i.Id.VideoId).Skip(1);
} }
public async Task<IEnumerable<string>> GetVideoLinksByKeywordAsync(string keywords, int count = 1) public async Task<IReadOnlyList<string>> GetVideoLinksByKeywordAsync(string keywords, int count = 1)
{ {
if (string.IsNullOrWhiteSpace(keywords)) if (string.IsNullOrWhiteSpace(keywords))
throw new ArgumentNullException(nameof(keywords)); throw new ArgumentNullException(nameof(keywords));
@@ -87,7 +87,7 @@ public sealed partial class GoogleApiService : IGoogleApiService, INService
query.Q = keywords; query.Q = keywords;
query.Type = "video"; query.Type = "video";
query.SafeSearch = SearchResource.ListRequest.SafeSearchEnum.Strict; query.SafeSearch = SearchResource.ListRequest.SafeSearchEnum.Strict;
return (await query.ExecuteAsync()).Items.Select(i => "https://www.youtube.com/watch?v=" + i.Id.VideoId); return (await query.ExecuteAsync()).Items.Select(i => "https://www.youtube.com/watch?v=" + i.Id.VideoId).ToArray();
} }
public async Task<IEnumerable<(string Name, string Id, string Url, string Thumbnail)>> GetVideoInfosByKeywordAsync( public async Task<IEnumerable<(string Name, string Id, string Url, string Thumbnail)>> GetVideoInfosByKeywordAsync(

View File

@@ -6,7 +6,7 @@ public interface IGoogleApiService
{ {
IReadOnlyDictionary<string, string> Languages { get; } IReadOnlyDictionary<string, string> Languages { get; }
Task<IEnumerable<string>> GetVideoLinksByKeywordAsync(string keywords, int count = 1); Task<IReadOnlyList<string>> GetVideoLinksByKeywordAsync(string keywords, int count = 1);
Task<IEnumerable<(string Name, string Id, string Url, string Thumbnail)>> GetVideoInfosByKeywordAsync(string keywords, int count = 1); Task<IEnumerable<(string Name, string Id, string Url, string Thumbnail)>> GetVideoInfosByKeywordAsync(string keywords, int count = 1);
Task<IEnumerable<string>> GetPlaylistIdsByKeywordsAsync(string keywords, int count = 1); Task<IEnumerable<string>> GetPlaylistIdsByKeywordsAsync(string keywords, int count = 1);
Task<IEnumerable<string>> GetRelatedVideosAsync(string id, int count = 1, string user = null); Task<IEnumerable<string>> GetRelatedVideosAsync(string id, int count = 1, string user = null);

View File

@@ -10,10 +10,9 @@ public class YtdlOperation
private readonly string _baseArgString; private readonly string _baseArgString;
private readonly bool _isYtDlp; private readonly bool _isYtDlp;
public YtdlOperation(string baseArgString, bool isYtDlp = false) public YtdlOperation(string baseArgString)
{ {
_baseArgString = baseArgString; _baseArgString = baseArgString;
_isYtDlp = isYtDlp;
} }
private Process CreateProcess(string[] args) private Process CreateProcess(string[] args)
@@ -23,7 +22,7 @@ public class YtdlOperation
{ {
StartInfo = new() StartInfo = new()
{ {
FileName = _isYtDlp ? "yt-dlp" : "youtube-dl", FileName = "yt-dlp",
Arguments = string.Format(_baseArgString, newArgs), Arguments = string.Format(_baseArgString, newArgs),
UseShellExecute = false, UseShellExecute = false,
RedirectStandardError = true, RedirectStandardError = true,
@@ -47,18 +46,18 @@ public class YtdlOperation
var str = await process.StandardOutput.ReadToEndAsync(); var str = await process.StandardOutput.ReadToEndAsync();
var err = await process.StandardError.ReadToEndAsync(); var err = await process.StandardError.ReadToEndAsync();
if (!string.IsNullOrEmpty(err)) if (!string.IsNullOrEmpty(err))
Log.Warning("YTDL warning: {YtdlWarning}", err); Log.Warning("yt-dlp warning: {YtdlWarning}", err);
return str; return str;
} }
catch (Win32Exception) catch (Win32Exception)
{ {
Log.Error("youtube-dl is likely not installed. Please install it before running the command again"); Log.Error("yt-dlp is likely not installed. Please install it before running the command again");
return default; return default;
} }
catch (Exception ex) catch (Exception ex)
{ {
Log.Error(ex, "Exception running youtube-dl: {ErrorMessage}", ex.Message); Log.Error(ex, "Exception running yt-dlp: {ErrorMessage}", ex.Message);
return default; return default;
} }
} }

View File

@@ -1,4 +1,5 @@
# DO NOT CHANGE # DO NOT CHANGE
version: 1 version: 1
# List of medusae automatically loaded at startup # List of medusae automatically loaded at startup
loaded: [] loaded:
- ngrpc