From c0cd161c904786892af626cdc5342adc8a5a58a7 Mon Sep 17 00:00:00 2001 From: Toastie Date: Thu, 3 Oct 2024 17:24:13 +1300 Subject: [PATCH] Added initial version of the grpc api. Added relevant dummy settings to creds (they have no effect rn) Yt searches now INTERNALLY return multiple results but there is no way right now to paginate plain text results moved some stuff around --- EllieBot.sln | 7 + .../EllieBot.GrpcApiBase.csproj | 21 ++ src/EllieBot.GrpcApiBase/protos/econ.proto | 26 +++ src/EllieBot.GrpcApiBase/protos/exprs.proto | 50 ++++ src/EllieBot.GrpcApiBase/protos/greet.proto | 57 +++++ src/EllieBot.GrpcApiBase/protos/info.proto | 52 +++++ src/EllieBot.GrpcApiBase/protos/other.proto | 81 +++++++ src/EllieBot.GrpcApiBase/protos/warn.proto | 83 +++++++ src/EllieBot/EllieBot.csproj | 22 +- .../Expressions/EllieExpressionsService.cs | 2 +- .../Gambling/Waifus/WaifuClaimCommands.cs | 2 +- .../Modules/Gambling/Waifus/WaifuService.cs | 6 +- .../Gambling/Waifus/db/WaifuExtensions.cs | 36 +-- .../_common/Resolvers/YtdlYoutubeResolver.cs | 9 +- .../Search/DefaultSearchServiceFactory.cs | 14 +- .../Modules/Searches/Search/SearchCommands.cs | 101 ++++---- .../Search/Youtube/IYoutubeSearchService.cs | 2 +- .../Youtube/InvidiousYtSearchService.cs | 5 +- .../Youtube/YoutubeDataApiSearchService.cs | 11 +- .../Search/Youtube/YtDlpSearchService.cs | 26 +++ .../Youtube/YtdlYoutubeSearchService.cs | 7 - .../Youtube/YtdlpYoutubeSearchService.cs | 7 - .../Search/Youtube/YtdlxServiceBase.cs | 34 --- .../Searches/_common/Config/SearchesConfig.cs | 8 +- src/EllieBot/Services/GrpcApi/ExprsSvc.cs | 73 ++++++ src/EllieBot/Services/GrpcApi/GreetByeSvc.cs | 121 ++++++++++ src/EllieBot/Services/GrpcApi/OtherSvc.cs | 123 ++++++++++ .../Services/GrpcApi/ServerInfoSvc.cs | 50 ++++ src/EllieBot/Services/GrpcApiService.cs | 63 +++++ src/EllieBot/_common/Creds.cs | 216 ++++++++++-------- .../Impl/BotCredsProvider.cs | 10 +- .../Impl/GoogleApiService.cs | 4 +- .../GoogleApiService_SupportedLanguages.cs | 0 .../{Services => _common}/Impl/ImageCache.cs | 0 .../Impl/LocalDataCache.cs | 0 .../Impl/Localization.cs | 0 .../Impl/PubSub/JsonSeria.cs | 0 .../Impl/PubSub/RedisPubSub.cs | 0 .../Impl/PubSub/YamlSeria.cs | 0 .../Impl/RedisBotCache.cs | 0 .../Impl/RedisBotStringsProvider.cs | 0 .../Impl/RemoteGrpcCoordinator.cs | 0 .../_common/Services/IGoogleApiService.cs | 2 +- .../_common/Services/Impl/YtdlOperation.cs | 11 +- src/EllieBot/data/marmalades/marmalade.yml | 1 + 45 files changed, 1060 insertions(+), 283 deletions(-) create mode 100644 src/EllieBot.GrpcApiBase/EllieBot.GrpcApiBase.csproj create mode 100644 src/EllieBot.GrpcApiBase/protos/econ.proto create mode 100644 src/EllieBot.GrpcApiBase/protos/exprs.proto create mode 100644 src/EllieBot.GrpcApiBase/protos/greet.proto create mode 100644 src/EllieBot.GrpcApiBase/protos/info.proto create mode 100644 src/EllieBot.GrpcApiBase/protos/other.proto create mode 100644 src/EllieBot.GrpcApiBase/protos/warn.proto create mode 100644 src/EllieBot/Modules/Searches/Search/Youtube/YtDlpSearchService.cs delete mode 100644 src/EllieBot/Modules/Searches/Search/Youtube/YtdlYoutubeSearchService.cs delete mode 100644 src/EllieBot/Modules/Searches/Search/Youtube/YtdlpYoutubeSearchService.cs delete mode 100644 src/EllieBot/Modules/Searches/Search/Youtube/YtdlxServiceBase.cs create mode 100644 src/EllieBot/Services/GrpcApi/ExprsSvc.cs create mode 100644 src/EllieBot/Services/GrpcApi/GreetByeSvc.cs create mode 100644 src/EllieBot/Services/GrpcApi/OtherSvc.cs create mode 100644 src/EllieBot/Services/GrpcApi/ServerInfoSvc.cs create mode 100644 src/EllieBot/Services/GrpcApiService.cs rename src/EllieBot/{Services => _common}/Impl/BotCredsProvider.cs (94%) rename src/EllieBot/{Services => _common}/Impl/GoogleApiService.cs (97%) rename src/EllieBot/{Services => _common}/Impl/GoogleApiService_SupportedLanguages.cs (100%) rename src/EllieBot/{Services => _common}/Impl/ImageCache.cs (100%) rename src/EllieBot/{Services => _common}/Impl/LocalDataCache.cs (100%) rename src/EllieBot/{Services => _common}/Impl/Localization.cs (100%) rename src/EllieBot/{Services => _common}/Impl/PubSub/JsonSeria.cs (100%) rename src/EllieBot/{Services => _common}/Impl/PubSub/RedisPubSub.cs (100%) rename src/EllieBot/{Services => _common}/Impl/PubSub/YamlSeria.cs (100%) rename src/EllieBot/{Services => _common}/Impl/RedisBotCache.cs (100%) rename src/EllieBot/{Services => _common}/Impl/RedisBotStringsProvider.cs (100%) rename src/EllieBot/{Services => _common}/Impl/RemoteGrpcCoordinator.cs (100%) diff --git a/EllieBot.sln b/EllieBot.sln index 306d678..0d4b7ec 100644 --- a/EllieBot.sln +++ b/EllieBot.sln @@ -30,6 +30,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ellie.Marmalade", "src\Elli EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EllieBot.Voice", "src\EllieBot.Voice\EllieBot.Voice.csproj", "{1D93CE3C-80B4-49C7-A9A2-99988920AAEC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EllieBot.GrpcApiBase", "src\EllieBot.GrpcApiBase\EllieBot.GrpcApiBase.csproj", "{3B71F0BF-AE6C-480C-AB88-FCE23EDC7D91}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -64,6 +66,10 @@ Global {1D93CE3C-80B4-49C7-A9A2-99988920AAEC}.Debug|Any CPU.Build.0 = Debug|Any CPU {1D93CE3C-80B4-49C7-A9A2-99988920AAEC}.Release|Any CPU.ActiveCfg = Release|Any CPU {1D93CE3C-80B4-49C7-A9A2-99988920AAEC}.Release|Any CPU.Build.0 = Release|Any CPU + {3B71F0BF-AE6C-480C-AB88-FCE23EDC7D91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3B71F0BF-AE6C-480C-AB88-FCE23EDC7D91}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3B71F0BF-AE6C-480C-AB88-FCE23EDC7D91}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3B71F0BF-AE6C-480C-AB88-FCE23EDC7D91}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -76,6 +82,7 @@ Global {F1A77F56-71B0-430E-AE46-94CDD7D43874} = {B28FB883-9688-41EB-BF5A-945F4A4EB628} {76AC715D-12FF-4CBE-9585-A861139A2D0C} = {B28FB883-9688-41EB-BF5A-945F4A4EB628} {1D93CE3C-80B4-49C7-A9A2-99988920AAEC} = {B28FB883-9688-41EB-BF5A-945F4A4EB628} + {3B71F0BF-AE6C-480C-AB88-FCE23EDC7D91} = {B28FB883-9688-41EB-BF5A-945F4A4EB628} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {79F61C2C-CDBB-4361-A234-91A0B334CFE4} diff --git a/src/EllieBot.GrpcApiBase/EllieBot.GrpcApiBase.csproj b/src/EllieBot.GrpcApiBase/EllieBot.GrpcApiBase.csproj new file mode 100644 index 0000000..c941779 --- /dev/null +++ b/src/EllieBot.GrpcApiBase/EllieBot.GrpcApiBase.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + Server + + + + \ No newline at end of file diff --git a/src/EllieBot.GrpcApiBase/protos/econ.proto b/src/EllieBot.GrpcApiBase/protos/econ.proto new file mode 100644 index 0000000..2d6f2f1 --- /dev/null +++ b/src/EllieBot.GrpcApiBase/protos/econ.proto @@ -0,0 +1,26 @@ +syntax = "proto3"; + +option csharp_namespace = "EllieBot.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; +} \ No newline at end of file diff --git a/src/EllieBot.GrpcApiBase/protos/exprs.proto b/src/EllieBot.GrpcApiBase/protos/exprs.proto new file mode 100644 index 0000000..2d4528e --- /dev/null +++ b/src/EllieBot.GrpcApiBase/protos/exprs.proto @@ -0,0 +1,50 @@ +syntax = "proto3"; + +option csharp_namespace = "EllieBot.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; +} diff --git a/src/EllieBot.GrpcApiBase/protos/greet.proto b/src/EllieBot.GrpcApiBase/protos/greet.proto new file mode 100644 index 0000000..f2eeec4 --- /dev/null +++ b/src/EllieBot.GrpcApiBase/protos/greet.proto @@ -0,0 +1,57 @@ +syntax = "proto3"; + +option csharp_namespace = "EllieBot.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; +} diff --git a/src/EllieBot.GrpcApiBase/protos/info.proto b/src/EllieBot.GrpcApiBase/protos/info.proto new file mode 100644 index 0000000..463f112 --- /dev/null +++ b/src/EllieBot.GrpcApiBase/protos/info.proto @@ -0,0 +1,52 @@ +syntax = "proto3"; + +option csharp_namespace = "EllieBot.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; +} diff --git a/src/EllieBot.GrpcApiBase/protos/other.proto b/src/EllieBot.GrpcApiBase/protos/other.proto new file mode 100644 index 0000000..3a7aecf --- /dev/null +++ b/src/EllieBot.GrpcApiBase/protos/other.proto @@ -0,0 +1,81 @@ +syntax = "proto3"; + +option csharp_namespace = "EllieBot.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; +} diff --git a/src/EllieBot.GrpcApiBase/protos/warn.proto b/src/EllieBot.GrpcApiBase/protos/warn.proto new file mode 100644 index 0000000..5dc82ee --- /dev/null +++ b/src/EllieBot.GrpcApiBase/protos/warn.proto @@ -0,0 +1,83 @@ +syntax = "proto3"; + +option csharp_namespace = "EllieBot.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; +} diff --git a/src/EllieBot/EllieBot.csproj b/src/EllieBot/EllieBot.csproj index 61cc0a7..1ef7da7 100644 --- a/src/EllieBot/EllieBot.csproj +++ b/src/EllieBot/EllieBot.csproj @@ -34,13 +34,12 @@ - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + + + + + @@ -103,6 +102,7 @@ + @@ -113,9 +113,6 @@ - - Protos\coordinator.proto - true PreserveNewest @@ -131,7 +128,10 @@ - + + _common\CoordinatorProtos\coordinator.proto + + diff --git a/src/EllieBot/Modules/Expressions/EllieExpressionsService.cs b/src/EllieBot/Modules/Expressions/EllieExpressionsService.cs index a660b4d..783e3ab 100644 --- a/src/EllieBot/Modules/Expressions/EllieExpressionsService.cs +++ b/src/EllieBot/Modules/Expressions/EllieExpressionsService.cs @@ -789,7 +789,7 @@ public sealed class EllieExpressionsService : IExecOnMessage, IReadyExecutor 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) .Take(9) .ToArray(), exprs.Length); diff --git a/src/EllieBot/Modules/Gambling/Waifus/WaifuClaimCommands.cs b/src/EllieBot/Modules/Gambling/Waifus/WaifuClaimCommands.cs index a25bdd9..8afb886 100644 --- a/src/EllieBot/Modules/Gambling/Waifus/WaifuClaimCommands.cs +++ b/src/EllieBot/Modules/Gambling/Waifus/WaifuClaimCommands.cs @@ -227,7 +227,7 @@ public partial class Gambling if (page > 100) page = 100; - var waifus = _service.GetTopWaifusAtPage(page).ToList(); + var waifus = await _service.GetTopWaifusAtPage(page); if (waifus.Count == 0) { diff --git a/src/EllieBot/Modules/Gambling/Waifus/WaifuService.cs b/src/EllieBot/Modules/Gambling/Waifus/WaifuService.cs index 5a00ab5..9bbce79 100644 --- a/src/EllieBot/Modules/Gambling/Waifus/WaifuService.cs +++ b/src/EllieBot/Modules/Gambling/Waifus/WaifuService.cs @@ -300,10 +300,10 @@ public class WaifuService : IEService, IReadyExecutor return (oldAff, success, remaining); } - public IEnumerable GetTopWaifusAtPage(int page, int perPage = 9) + public async Task> GetTopWaifusAtPage(int page, int perPage = 9) { - using var uow = _db.GetDbContext(); - return uow.Set().GetTop(perPage, page * perPage); + await using var uow = _db.GetDbContext(); + return await uow.Set().GetTop(perPage, page * perPage); } public ulong GetWaifuUserId(ulong ownerId, string name) diff --git a/src/EllieBot/Modules/Gambling/Waifus/db/WaifuExtensions.cs b/src/EllieBot/Modules/Gambling/Waifus/db/WaifuExtensions.cs index 126b760..d0615eb 100644 --- a/src/EllieBot/Modules/Gambling/Waifus/db/WaifuExtensions.cs +++ b/src/EllieBot/Modules/Gambling/Waifus/db/WaifuExtensions.cs @@ -25,30 +25,30 @@ public static class WaifuExtensions return includes(waifus).AsQueryable().FirstOrDefault(wi => wi.Waifu.UserId == userId); } - public static IEnumerable GetTop(this DbSet waifus, int count, int skip = 0) + public static async Task> GetTop(this DbSet waifus, int count, int skip = 0) { ArgumentOutOfRangeException.ThrowIfNegative(count); if (count == 0) return []; - return waifus.Include(wi => wi.Waifu) - .Include(wi => wi.Affinity) - .Include(wi => wi.Claimer) - .OrderByDescending(wi => wi.Price) - .Skip(skip) - .Take(count) - .Select(x => new WaifuLbResult - { - Affinity = x.Affinity == null ? null : x.Affinity.Username, - AffinityDiscrim = x.Affinity == null ? null : x.Affinity.Discriminator, - Claimer = x.Claimer == null ? null : x.Claimer.Username, - ClaimerDiscrim = x.Claimer == null ? null : x.Claimer.Discriminator, - Username = x.Waifu.Username, - Discrim = x.Waifu.Discriminator, - Price = x.Price - }) - .ToList(); + return await waifus.Include(wi => wi.Waifu) + .Include(wi => wi.Affinity) + .Include(wi => wi.Claimer) + .OrderByDescending(wi => wi.Price) + .Skip(skip) + .Take(count) + .Select(x => new WaifuLbResult + { + Affinity = x.Affinity == null ? null : x.Affinity.Username, + AffinityDiscrim = x.Affinity == null ? null : x.Affinity.Discriminator, + Claimer = x.Claimer == null ? null : x.Claimer.Username, + ClaimerDiscrim = x.Claimer == null ? null : x.Claimer.Discriminator, + Username = x.Waifu.Username, + Discrim = x.Waifu.Discriminator, + Price = x.Price + }) + .ToListAsyncEF(); } public static decimal GetTotalValue(this DbSet waifus) diff --git a/src/EllieBot/Modules/Music/_common/Resolvers/YtdlYoutubeResolver.cs b/src/EllieBot/Modules/Music/_common/Resolvers/YtdlYoutubeResolver.cs index eeb3a1c..4b0af89 100644 --- a/src/EllieBot/Modules/Music/_common/Resolvers/YtdlYoutubeResolver.cs +++ b/src/EllieBot/Modules/Music/_common/Resolvers/YtdlYoutubeResolver.cs @@ -43,8 +43,7 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver + "--no-check-certificate " + "-i " + "--yes-playlist " - + "-- \"{0}\"", - scs.Data.YtProvider != YoutubeSearcher.Ytdl); + + "-- \"{0}\""); _ytdlIdOperation = new("-4 " + "--geo-bypass " @@ -56,8 +55,7 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver + "--get-thumbnail " + "--get-duration " + "--no-check-certificate " - + "-- \"{0}\"", - scs.Data.YtProvider != YoutubeSearcher.Ytdl); + + "-- \"{0}\""); _ytdlSearchOperation = new("-4 " + "--geo-bypass " @@ -70,8 +68,7 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver + "--get-duration " + "--no-check-certificate " + "--default-search " - + "\"ytsearch:\" -- \"{0}\"", - scs.Data.YtProvider != YoutubeSearcher.Ytdl); + + "\"ytsearch:\" -- \"{0}\""); } private YtTrackData ResolveYtdlData(string ytdlOutputString) diff --git a/src/EllieBot/Modules/Searches/Search/DefaultSearchServiceFactory.cs b/src/EllieBot/Modules/Searches/Search/DefaultSearchServiceFactory.cs index fa3c634..40f802a 100644 --- a/src/EllieBot/Modules/Searches/Search/DefaultSearchServiceFactory.cs +++ b/src/EllieBot/Modules/Searches/Search/DefaultSearchServiceFactory.cs @@ -7,10 +7,9 @@ public sealed class DefaultSearchServiceFactory : ISearchServiceFactory, IEServi { private readonly SearchesConfigService _scs; private readonly SearxSearchService _sss; + private readonly YtDlpSearchService _ytdlp; private readonly GoogleSearchService _gss; - private readonly YtdlpYoutubeSearchService _ytdlp; - private readonly YtdlYoutubeSearchService _ytdl; private readonly YoutubeDataApiSearchService _ytdata; private readonly InvidiousYtSearchService _iYtSs; private readonly GoogleScrapeService _gscs; @@ -20,19 +19,17 @@ public sealed class DefaultSearchServiceFactory : ISearchServiceFactory, IEServi GoogleSearchService gss, GoogleScrapeService gscs, SearxSearchService sss, - YtdlpYoutubeSearchService ytdlp, - YtdlYoutubeSearchService ytdl, + YtDlpSearchService ytdlp, YoutubeDataApiSearchService ytdata, InvidiousYtSearchService iYtSs) { _scs = scs; _sss = sss; + _ytdlp = ytdlp; _gss = gss; _gscs = gscs; _iYtSs = iYtSs; - _ytdlp = ytdlp; - _ytdl = ytdl; _ytdata = ytdata; } @@ -57,9 +54,8 @@ public sealed class DefaultSearchServiceFactory : ISearchServiceFactory, IEServi => _scs.Data.YtProvider switch { YoutubeSearcher.YtDataApiv3 => _ytdata, - YoutubeSearcher.Ytdlp => _ytdlp, - YoutubeSearcher.Ytdl => _ytdl, YoutubeSearcher.Invidious => _iYtSs, - _ => _ytdl + YoutubeSearcher.Ytdlp => _ytdlp, + _ => throw new ArgumentOutOfRangeException() }; } \ No newline at end of file diff --git a/src/EllieBot/Modules/Searches/Search/SearchCommands.cs b/src/EllieBot/Modules/Searches/Search/SearchCommands.cs index 63e0821..4bd4d2c 100644 --- a/src/EllieBot/Modules/Searches/Search/SearchCommands.cs +++ b/src/EllieBot/Modules/Searches/Search/SearchCommands.cs @@ -93,16 +93,12 @@ public partial class Searches return; } - var embeds = new List(4); - - EmbedBuilder CreateEmbed(IImageSearchResultEntry entry) { return _sender.CreateEmbed() .WithOkColor() .WithAuthor(ctx.User) .WithTitle(query) - .WithUrl("https://google.com") .WithImageUrl(entry.Link); } @@ -120,86 +116,81 @@ public partial class Searches .WithDescription(GetText(strs.no_search_results)); var embed = CreateEmbed(item); - embeds.Add(embed); return embed; }) .SendAsync(); } - private TypedKey GetYtCacheKey(string query) - => new($"search:youtube:{query}"); + private TypedKey GetYtCacheKey(string 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()); - private async Task GetYoutubeUrlFromCacheAsync(string query) + private async Task GetYoutubeUrlFromCacheAsync(string 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 new VideoInfo() + return urls.Map(url => new VideoInfo() { Url = url - }; + }); } [Cmd] - public async Task Youtube([Leftover] string? query = null) + public async Task Youtube([Leftover] string query) { - query = query?.Trim(); - - if (string.IsNullOrWhiteSpace(query)) - { - await Response().Error(strs.specify_search_params).SendAsync(); - return; - } + query = query.Trim(); _ = ctx.Channel.TriggerTypingAsync(); - var maybeResult = await GetYoutubeUrlFromCacheAsync(query) - ?? await _searchFactory.GetYoutubeSearchService().SearchAsync(query); - if (maybeResult is not { } result || result is { Url: null }) + var maybeResults = await GetYoutubeUrlFromCacheAsync(query) + ?? await _searchFactory.GetYoutubeSearchService().SearchAsync(query); + + if (maybeResults is not { } result || result.Length == 0) { await Response().Error(strs.no_results).SendAsync(); return; } - await AddYoutubeUrlToCacheAsync(query, result.Url); - await Response().Text(result.Url).SendAsync(); + await AddYoutubeUrlToCacheAsync(query, result.Map(x => x.Url)); + + await Response().Text(result[0].Url).SendAsync(); } -// [Cmd] -// public async Task DuckDuckGo([Leftover] string query = null) -// { -// query = query?.Trim(); -// if (!await ValidateQuery(query)) -// return; -// -// _ = ctx.Channel.TriggerTypingAsync(); -// -// var data = await _service.DuckDuckGoSearchAsync(query); -// if (data is null) -// { -// await Response().Error(strs.no_results).SendAsync(); -// return; -// } -// -// var desc = data.Results.Take(5) -// .Select(res => $@"[**{res.Title}**]({res.Link}) -// {res.Text.TrimTo(380 - res.Title.Length - res.Link.Length)}"); -// -// var descStr = string.Join("\n\n", desc); -// -// var embed = _sender.CreateEmbed() -// .WithAuthor(ctx.User.ToString(), -// "https://upload.wikimedia.org/wikipedia/en/9/90/The_DuckDuckGo_Duck.png") -// .WithDescription($"{GetText(strs.search_for)} **{query}**\n\n" + descStr) -// .WithOkColor(); -// -// await Response().Embed(embed).SendAsync(); -// } + // [Cmd] + // public async Task DuckDuckGo([Leftover] string query = null) + // { + // query = query?.Trim(); + // if (!await ValidateQuery(query)) + // return; + // + // _ = ctx.Channel.TriggerTypingAsync(); + // + // var data = await _service.DuckDuckGoSearchAsync(query); + // if (data is null) + // { + // await Response().Error(strs.no_results).SendAsync(); + // return; + // } + // + // var desc = data.Results.Take(5) + // .Select(res => $@"[**{res.Title}**]({res.Link}) + // {res.Text.TrimTo(380 - res.Title.Length - res.Link.Length)}"); + // + // var descStr = string.Join("\n\n", desc); + // + // var embed = _sender.CreateEmbed() + // .WithAuthor(ctx.User.ToString(), + // "https://upload.wikimedia.org/wikipedia/en/9/90/The_DuckDuckGo_Duck.png") + // .WithDescription($"{GetText(strs.search_for)} **{query}**\n\n" + descStr) + // .WithOkColor(); + // + // await Response().Embed(embed).SendAsync(); + // } } } \ No newline at end of file diff --git a/src/EllieBot/Modules/Searches/Search/Youtube/IYoutubeSearchService.cs b/src/EllieBot/Modules/Searches/Search/Youtube/IYoutubeSearchService.cs index 5b9bfab..bbfca62 100644 --- a/src/EllieBot/Modules/Searches/Search/Youtube/IYoutubeSearchService.cs +++ b/src/EllieBot/Modules/Searches/Search/Youtube/IYoutubeSearchService.cs @@ -2,5 +2,5 @@ public interface IYoutubeSearchService { - Task SearchAsync(string query); + Task SearchAsync(string query); } \ No newline at end of file diff --git a/src/EllieBot/Modules/Searches/Search/Youtube/InvidiousYtSearchService.cs b/src/EllieBot/Modules/Searches/Search/Youtube/InvidiousYtSearchService.cs index 17bd519..99ae4c2 100644 --- a/src/EllieBot/Modules/Searches/Search/Youtube/InvidiousYtSearchService.cs +++ b/src/EllieBot/Modules/Searches/Search/Youtube/InvidiousYtSearchService.cs @@ -18,7 +18,7 @@ public sealed class InvidiousYtSearchService : IYoutubeSearchService, IEService _rng = new(); } - public async Task SearchAsync(string query) + public async Task SearchAsync(string query) { ArgumentNullException.ThrowIfNull(query); @@ -35,6 +35,7 @@ public sealed class InvidiousYtSearchService : IYoutubeSearchService, IEService var url = $"{instance}/api/v1/search" + $"?q={query}" + $"&type=video"; + using var http = _http.CreateClient(); var res = await http.GetFromJsonAsync>( url); @@ -42,6 +43,6 @@ public sealed class InvidiousYtSearchService : IYoutubeSearchService, IEService if (res is null or { Count: 0 }) return null; - return new VideoInfo(res[0].VideoId); + return res.Map(r => new VideoInfo(r.VideoId)); } } \ No newline at end of file diff --git a/src/EllieBot/Modules/Searches/Search/Youtube/YoutubeDataApiSearchService.cs b/src/EllieBot/Modules/Searches/Search/Youtube/YoutubeDataApiSearchService.cs index e8bfcd2..9c69a5e 100644 --- a/src/EllieBot/Modules/Searches/Search/Youtube/YoutubeDataApiSearchService.cs +++ b/src/EllieBot/Modules/Searches/Search/Youtube/YoutubeDataApiSearchService.cs @@ -9,18 +9,15 @@ public sealed class YoutubeDataApiSearchService : IYoutubeSearchService, IEServi _gapi = gapi; } - public async Task SearchAsync(string query) + public async Task SearchAsync(string query) { ArgumentNullException.ThrowIfNull(query); var results = await _gapi.GetVideoLinksByKeywordAsync(query); - var first = results.FirstOrDefault(); - if (first is null) + + if (results.Count == 0) return null; - return new() - { - Url = first - }; + return results.Map(r => new VideoInfo(r)); } } \ No newline at end of file diff --git a/src/EllieBot/Modules/Searches/Search/Youtube/YtDlpSearchService.cs b/src/EllieBot/Modules/Searches/Search/Youtube/YtDlpSearchService.cs new file mode 100644 index 0000000..b36a726 --- /dev/null +++ b/src/EllieBot/Modules/Searches/Search/Youtube/YtDlpSearchService.cs @@ -0,0 +1,26 @@ +namespace EllieBot.Modules.Searches.Youtube; + +public class YtDlpSearchService : IYoutubeSearchService, IEService +{ + 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 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)); + } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Searches/Search/Youtube/YtdlYoutubeSearchService.cs b/src/EllieBot/Modules/Searches/Search/Youtube/YtdlYoutubeSearchService.cs deleted file mode 100644 index 3ac59f8..0000000 --- a/src/EllieBot/Modules/Searches/Search/Youtube/YtdlYoutubeSearchService.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace EllieBot.Modules.Searches.Youtube; - -public sealed class YtdlYoutubeSearchService : YoutubedlxServiceBase, IEService -{ - public override async Task SearchAsync(string query) - => await InternalGetInfoAsync(query, false); -} \ No newline at end of file diff --git a/src/EllieBot/Modules/Searches/Search/Youtube/YtdlpYoutubeSearchService.cs b/src/EllieBot/Modules/Searches/Search/Youtube/YtdlpYoutubeSearchService.cs deleted file mode 100644 index d7e66fa..0000000 --- a/src/EllieBot/Modules/Searches/Search/Youtube/YtdlpYoutubeSearchService.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace EllieBot.Modules.Searches.Youtube; - -public sealed class YtdlpYoutubeSearchService : YoutubedlxServiceBase, IEService -{ - public override async Task SearchAsync(string query) - => await InternalGetInfoAsync(query, true); -} \ No newline at end of file diff --git a/src/EllieBot/Modules/Searches/Search/Youtube/YtdlxServiceBase.cs b/src/EllieBot/Modules/Searches/Search/Youtube/YtdlxServiceBase.cs deleted file mode 100644 index 6239bdd..0000000 --- a/src/EllieBot/Modules/Searches/Search/Youtube/YtdlxServiceBase.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace EllieBot.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 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 SearchAsync(string query); -} \ No newline at end of file diff --git a/src/EllieBot/Modules/Searches/_common/Config/SearchesConfig.cs b/src/EllieBot/Modules/Searches/_common/Config/SearchesConfig.cs index 8cb0227..fb872aa 100644 --- a/src/EllieBot/Modules/Searches/_common/Config/SearchesConfig.cs +++ b/src/EllieBot/Modules/Searches/_common/Config/SearchesConfig.cs @@ -77,9 +77,9 @@ public sealed class FollowedStreamConfig public enum YoutubeSearcher { - YtDataApiv3, - Ytdl, - Ytdlp, - Invid, + YtDataApiv3 = 0, + Ytdl = 1, + Ytdlp = 1, + Invid = 3, Invidious = 3 } \ No newline at end of file diff --git a/src/EllieBot/Services/GrpcApi/ExprsSvc.cs b/src/EllieBot/Services/GrpcApi/ExprsSvc.cs new file mode 100644 index 0000000..57c49d9 --- /dev/null +++ b/src/EllieBot/Services/GrpcApi/ExprsSvc.cs @@ -0,0 +1,73 @@ +using Google.Protobuf.WellKnownTypes; +using Grpc.Core; +using EllieBot.Db.Models; +using EllieBot.Modules.EllieExpressions; + +namespace EllieBot.GrpcApi; + +public class ExprsSvc : GrpcExprs.GrpcExprsBase, IEService +{ + private readonly EllieExpressionsService _svc; + + public ExprsSvc(EllieExpressionsService svc) + { + _svc = svc; + } + + public override async Task AddExpr(AddExprRequest request, ServerCallContext context) + { + EllieExpression 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 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 DeleteExpr(DeleteExprRequest request, ServerCallContext context) + { + await _svc.DeleteAsync(request.GuildId, new kwum(request.Id)); + + return new Empty(); + } +} diff --git a/src/EllieBot/Services/GrpcApi/GreetByeSvc.cs b/src/EllieBot/Services/GrpcApi/GreetByeSvc.cs new file mode 100644 index 0000000..c0fec41 --- /dev/null +++ b/src/EllieBot/Services/GrpcApi/GreetByeSvc.cs @@ -0,0 +1,121 @@ +using Grpc.Core; +using GreetType = EllieBot.Services.GreetType; + +namespace EllieBot.GrpcApi; + +public sealed class GreetByeSvc : GrpcGreet.GrpcGreetBase, IEService +{ + 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 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 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 TestGreet(TestGreetRequest request, ServerCallContext context) + => TestGreet(request.GuildId, request.ChannelId, request.UserId, request.Type); + + private async Task 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) + }; + } +} diff --git a/src/EllieBot/Services/GrpcApi/OtherSvc.cs b/src/EllieBot/Services/GrpcApi/OtherSvc.cs new file mode 100644 index 0000000..f720803 --- /dev/null +++ b/src/EllieBot/Services/GrpcApi/OtherSvc.cs @@ -0,0 +1,123 @@ +using Google.Protobuf.WellKnownTypes; +using Grpc.Core; +using EllieBot.Modules.Gambling.Services; +using EllieBot.Modules.Xp.Services; + +namespace EllieBot.GrpcApi; + +public sealed class OtherSvc : GrpcOther.GrpcOtherBase, IEService +{ + 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 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 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 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 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 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); + } +} diff --git a/src/EllieBot/Services/GrpcApi/ServerInfoSvc.cs b/src/EllieBot/Services/GrpcApi/ServerInfoSvc.cs new file mode 100644 index 0000000..2bf3089 --- /dev/null +++ b/src/EllieBot/Services/GrpcApi/ServerInfoSvc.cs @@ -0,0 +1,50 @@ +using EllieBot.GrpcApi; +using Grpc.Core; + +namespace EllieBot.GrpcApi; + +public sealed class ServerInfoSvc : GrpcInfo.GrpcInfoBase, IEService +{ + private readonly IStatsService _stats; + + public ServerInfoSvc(IStatsService stats) + { + _stats = stats; + } + + public override Task 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); + } +} diff --git a/src/EllieBot/Services/GrpcApiService.cs b/src/EllieBot/Services/GrpcApiService.cs new file mode 100644 index 0000000..fb2c8d4 --- /dev/null +++ b/src/EllieBot/Services/GrpcApiService.cs @@ -0,0 +1,63 @@ +using Grpc; +using Grpc.Core; +using EllieBot.Common.ModuleBehaviors; + +namespace EllieBot.GrpcApi; + +public class GrpcApiService : IEService, 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); + } +} \ No newline at end of file diff --git a/src/EllieBot/_common/Creds.cs b/src/EllieBot/_common/Creds.cs index 65de7e6..30ad6b7 100644 --- a/src/EllieBot/_common/Creds.cs +++ b/src/EllieBot/_common/Creds.cs @@ -6,27 +6,28 @@ namespace EllieBot.Common; public sealed class Creds : IBotCredentials { [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/""")] public string Token { get; set; } [Comment(""" - List of Ids of the users who have bot owner permissions - **DO NOT ADD PEOPLE YOU DON'T TRUST** - """)] + List of Ids of the users who have bot owner permissions + **DO NOT ADD PEOPLE YOU DON'T TRUST** + """)] public ICollection 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; } [Comment(""" - The number of shards that the bot will be running on. - Leave at 1 if you don't know what you're doing. - - note: If you are planning to have more than one shard, then you must change botCache to 'redis'. - Also, in that case you should be using EllieBot.Coordinator to start the bot, and it will correctly override this value. - """)] + The number of shards that the bot will be running on. + Leave at 1 if you don't know what you're doing. + + note: If you are planning to have more than one shard, then you must change botCache to 'redis'. + Also, in that case you should be using EllieBot.Coordinator to start the bot, and it will correctly override this value. + """)] public int TotalShards { get; set; } [Comment(""" @@ -38,34 +39,34 @@ public sealed class Creds : IBotCredentials ⚠ This does not currently work and is a work in progress. """)] public string EllieAiToken { get; set; } - - [Comment( + + [Comment( """ - Login to https://console.cloud.google.com, create a new project, go to APIs & Services -> Library -> YouTube Data API and enable it. - Then, go to APIs and Services -> Credentials and click Create credentials -> API key. - Used only for Youtube Data Api (at the moment). - """)] + Login to https://console.cloud.google.com, create a new project, go to APIs & Services -> Library -> YouTube Data API and enable it. + Then, go to APIs and Services -> Credentials and click Create credentials -> API key. + Used only for Youtube Data Api (at the moment). + """)] public string GoogleApiKey { get; set; } - - [Comment( + + [Comment( """ - Create a new custom search here https://programmablesearchengine.google.com/cse/create/new - Enable SafeSearch - Remove all Sites to Search - Enable Search the entire web - Copy the 'Search Engine ID' to the SearchId field - - Do all steps again but enable image search for the ImageSearchId - """)] + Create a new custom search here https://programmablesearchengine.google.com/cse/create/new + Enable SafeSearch + Remove all Sites to Search + Enable Search the entire web + Copy the 'Search Engine ID' to the SearchId field + + Do all steps again but enable image search for the ImageSearchId + """)] public GoogleApiConfig Google { get; set; } [Comment("""Settings for voting system for discordbots. Meant for use on global Ellie.""")] public VotesSettings Votes { get; set; } [Comment(""" - Patreon auto reward system settings. - go to https://www.patreon.com/portal -> my clients -> create client - """)] + Patreon auto reward system settings. + go to https://www.patreon.com/portal -> my clients -> create client + """)] public PatreonSettings Patreon { get; set; } [Comment("""Api key for sending stats to DiscordBotList.""")] @@ -76,27 +77,27 @@ public sealed class Creds : IBotCredentials [Comment(@"OpenAi api key.")] public string Gpt3ApiKey { get; set; } - + [Comment(""" - Which cache implementation should bot use. - 'memory' - Cache will be in memory of the bot's process itself. Only use this on bots with a single shard. When the bot is restarted the cache is reset. - 'redis' - Uses redis (which needs to be separately downloaded and installed). The cache will persist through bot restarts. You can configure connection string in creds.yml - """)] + Which cache implementation should bot use. + 'memory' - Cache will be in memory of the bot's process itself. Only use this on bots with a single shard. When the bot is restarted the cache is reset. + 'redis' - Uses redis (which needs to be separately downloaded and installed). The cache will persist through bot restarts. You can configure connection string in creds.yml + """)] public BotCacheImplemenation BotCache { get; set; } - + [Comment(""" - Redis connection string. Don't change if you don't know what you're doing. - Only used if botCache is set to 'redis' - """)] + Redis connection string. Don't change if you don't know what you're doing. + Only used if botCache is set to 'redis' + """)] public string RedisOptions { get; set; } [Comment("""Database options. Don't change if you don't know what you're doing. Leave null for default values""")] public DbOptions Db { get; set; } [Comment(""" - Address and port of the coordinator endpoint. Leave empty for default. - Change only if you've changed the coordinator address or port. - """)] + Address and port of the coordinator endpoint. Leave empty for default. + Change only if you've changed the coordinator address or port. + """)] public string CoordinatorUrl { get; set; } [Comment( @@ -104,34 +105,34 @@ public sealed class Creds : IBotCredentials public string RapidApiKey { get; set; } [Comment(""" - https://locationiq.com api key (register and you will receive the token in the email). - Used only for .time command. - """)] + https://locationiq.com api key (register and you will receive the token in the email). + Used only for .time command. + """)] public string LocationIqApiKey { get; set; } [Comment(""" - https://timezonedb.com api key (register and you will receive the token in the email). - Used only for .time command - """)] + https://timezonedb.com api key (register and you will receive the token in the email). + Used only for .time command + """)] public string TimezoneDbApiKey { get; set; } [Comment(""" - https://pro.coinmarketcap.com/account/ api key. There is a free plan for personal use. - Used for cryptocurrency related commands. - """)] + https://pro.coinmarketcap.com/account/ api key. There is a free plan for personal use. + Used for cryptocurrency related commands. + """)] public string CoinmarketcapApiKey { get; set; } - -// [Comment(@"https://polygon.io/dashboard/api-keys api key. Free plan allows for 5 queries per minute. -// Used for stocks related commands.")] -// public string PolygonIoApiKey { get; set; } + + // [Comment(@"https://polygon.io/dashboard/api-keys api key. Free plan allows for 5 queries per minute. + // Used for stocks related commands.")] + // public string PolygonIoApiKey { get; set; } [Comment("""Api key used for Osu related commands. Obtain this key at https://osu.ppy.sh/p/api""")] public string OsuApiKey { get; set; } [Comment(""" - Optional Trovo client id. - You should use this if Trovo stream notifications stopped working or you're getting ratelimit errors. - """)] + Optional Trovo client id. + You should use this if Trovo stream notifications stopped working or you're getting ratelimit errors. + """)] public string TrovoClientId { get; set; } [Comment("""Obtain by creating an application at https://dev.twitch.tv/console/apps""")] @@ -141,23 +142,30 @@ public sealed class Creds : IBotCredentials public string TwitchClientSecret { get; set; } [Comment(""" - Command and args which will be used to restart the bot. - Only used if bot is executed directly (NOT through the coordinator) - placeholders: - {0} -> shard id - {1} -> total shards - Linux default - cmd: dotnet - args: "EllieBot.dll -- {0}" - Windows default - cmd: EllieBot.exe - args: "{0}" - """)] + Command and args which will be used to restart the bot. + Only used if bot is executed directly (NOT through the coordinator) + placeholders: + {0} -> shard id + {1} -> total shards + Linux default + cmd: dotnet + args: "EllieBot.dll -- {0}" + Windows default + cmd: EllieBot.exe + args: "{0}" + """)] 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() { - Version = 9; Token = string.Empty; UsePrivilegedIntents = true; OwnerIds = new List(); @@ -180,24 +188,26 @@ public sealed class Creds : IBotCredentials RestartCommand = new RestartConfig(); Google = new GoogleApiConfig(); + + Api = new ApiConfig(); } - + public class DbOptions : IDbOptions { [Comment(""" - Database type. "sqlite", "mysql" and "postgresql" are supported. - Default is "sqlite" - """)] + Database type. "sqlite", "mysql" and "postgresql" are supported. + Default is "sqlite" + """)] public string Type { get; set; } [Comment(""" - Database connection string. - You MUST change this if you're not using "sqlite" type. - Default is "Data Source=data/EllieBot.db" - Example for mysql: "Server=localhost;Port=3306;Uid=root;Pwd=my_super_secret_mysql_password;Database=ellie" - Example for postgresql: "Server=localhost;Port=5432;User Id=postgres;Password=my_super_secret_postgres_password;Database=ellie;" - """)] + Database connection string. + You MUST change this if you're not using "sqlite" type. + Default is "Data Source=data/EllieBot.db" + Example for mysql: "Server=localhost;Port=3306;Uid=root;Pwd=my_super_secret_mysql_password;Database=ellie" + Example for postgresql: "Server=localhost;Port=5432;User Id=postgres;Password=my_super_secret_postgres_password;Database=ellie;" + """)] public string ConnectionString { get; set; } } @@ -232,29 +242,29 @@ public sealed class Creds : IBotCredentials public sealed record VotesSettings : IVotesSettings { [Comment(""" - top.gg votes service url - This is the url of your instance of the EllieBot.Votes api - Example: https://votes.my.cool.bot.com - """)] + top.gg votes service url + This is the url of your instance of the EllieBot.Votes api + Example: https://votes.my.cool.bot.com + """)] public string TopggServiceUrl { get; set; } [Comment(""" - Authorization header value sent to the TopGG service url with each request - This should be equivalent to the TopggKey in your EllieBot.Votes api appsettings.json file - """)] + Authorization header value sent to the TopGG service url with each request + This should be equivalent to the TopggKey in your EllieBot.Votes api appsettings.json file + """)] public string TopggKey { get; set; } [Comment(""" - discords.com votes service url - This is the url of your instance of the EllieBot.Votes api - Example: https://votes.my.cool.bot.com - """)] + discords.com votes service url + This is the url of your instance of the EllieBot.Votes api + Example: https://votes.my.cool.bot.com + """)] public string DiscordsServiceUrl { get; set; } [Comment(""" - Authorization header value sent to the Discords service url with each request - This should be equivalent to the DiscordsKey in your EllieBot.Votes api appsettings.json file - """)] + Authorization header value sent to the Discords service url with each request + This should be equivalent to the DiscordsKey in your EllieBot.Votes api appsettings.json file + """)] public string DiscordsKey { get; set; } public VotesSettings() @@ -273,13 +283,19 @@ public sealed class Creds : IBotCredentials 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 string SearchId { get; init; } public string ImageSearchId { get; init; } -} - - - +} \ No newline at end of file diff --git a/src/EllieBot/Services/Impl/BotCredsProvider.cs b/src/EllieBot/_common/Impl/BotCredsProvider.cs similarity index 94% rename from src/EllieBot/Services/Impl/BotCredsProvider.cs rename to src/EllieBot/_common/Impl/BotCredsProvider.cs index d28925d..5d9a83d 100644 --- a/src/EllieBot/Services/Impl/BotCredsProvider.cs +++ b/src/EllieBot/_common/Impl/BotCredsProvider.cs @@ -140,15 +140,9 @@ public sealed class BotCredsProvider : IBotCredsProvider creds.BotCache = BotCacheImplemenation.Redis; } - if (creds.Version <= 6) + if (creds.Version <= 9) { - creds.Version = 7; - File.WriteAllText(CREDS_FILE_NAME, Yaml.Serializer.Serialize(creds)); - } - - if (creds.Version <= 8) - { - creds.Version = 9; + creds.Version = 10; File.WriteAllText(CREDS_FILE_NAME, Yaml.Serializer.Serialize(creds)); } } diff --git a/src/EllieBot/Services/Impl/GoogleApiService.cs b/src/EllieBot/_common/Impl/GoogleApiService.cs similarity index 97% rename from src/EllieBot/Services/Impl/GoogleApiService.cs rename to src/EllieBot/_common/Impl/GoogleApiService.cs index 9d4a494..972a228 100644 --- a/src/EllieBot/Services/Impl/GoogleApiService.cs +++ b/src/EllieBot/_common/Impl/GoogleApiService.cs @@ -75,7 +75,7 @@ public sealed partial class GoogleApiService : IGoogleApiService, IEService return (await query.ExecuteAsync()).Items.Select(i => "https://www.youtube.com/watch?v=" + i.Id.VideoId).Skip(1); } - public async Task> GetVideoLinksByKeywordAsync(string keywords, int count = 1) + public async Task> GetVideoLinksByKeywordAsync(string keywords, int count = 1) { if (string.IsNullOrWhiteSpace(keywords)) throw new ArgumentNullException(nameof(keywords)); @@ -87,7 +87,7 @@ public sealed partial class GoogleApiService : IGoogleApiService, IEService query.Q = keywords; query.Type = "video"; 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> GetVideoInfosByKeywordAsync( diff --git a/src/EllieBot/Services/Impl/GoogleApiService_SupportedLanguages.cs b/src/EllieBot/_common/Impl/GoogleApiService_SupportedLanguages.cs similarity index 100% rename from src/EllieBot/Services/Impl/GoogleApiService_SupportedLanguages.cs rename to src/EllieBot/_common/Impl/GoogleApiService_SupportedLanguages.cs diff --git a/src/EllieBot/Services/Impl/ImageCache.cs b/src/EllieBot/_common/Impl/ImageCache.cs similarity index 100% rename from src/EllieBot/Services/Impl/ImageCache.cs rename to src/EllieBot/_common/Impl/ImageCache.cs diff --git a/src/EllieBot/Services/Impl/LocalDataCache.cs b/src/EllieBot/_common/Impl/LocalDataCache.cs similarity index 100% rename from src/EllieBot/Services/Impl/LocalDataCache.cs rename to src/EllieBot/_common/Impl/LocalDataCache.cs diff --git a/src/EllieBot/Services/Impl/Localization.cs b/src/EllieBot/_common/Impl/Localization.cs similarity index 100% rename from src/EllieBot/Services/Impl/Localization.cs rename to src/EllieBot/_common/Impl/Localization.cs diff --git a/src/EllieBot/Services/Impl/PubSub/JsonSeria.cs b/src/EllieBot/_common/Impl/PubSub/JsonSeria.cs similarity index 100% rename from src/EllieBot/Services/Impl/PubSub/JsonSeria.cs rename to src/EllieBot/_common/Impl/PubSub/JsonSeria.cs diff --git a/src/EllieBot/Services/Impl/PubSub/RedisPubSub.cs b/src/EllieBot/_common/Impl/PubSub/RedisPubSub.cs similarity index 100% rename from src/EllieBot/Services/Impl/PubSub/RedisPubSub.cs rename to src/EllieBot/_common/Impl/PubSub/RedisPubSub.cs diff --git a/src/EllieBot/Services/Impl/PubSub/YamlSeria.cs b/src/EllieBot/_common/Impl/PubSub/YamlSeria.cs similarity index 100% rename from src/EllieBot/Services/Impl/PubSub/YamlSeria.cs rename to src/EllieBot/_common/Impl/PubSub/YamlSeria.cs diff --git a/src/EllieBot/Services/Impl/RedisBotCache.cs b/src/EllieBot/_common/Impl/RedisBotCache.cs similarity index 100% rename from src/EllieBot/Services/Impl/RedisBotCache.cs rename to src/EllieBot/_common/Impl/RedisBotCache.cs diff --git a/src/EllieBot/Services/Impl/RedisBotStringsProvider.cs b/src/EllieBot/_common/Impl/RedisBotStringsProvider.cs similarity index 100% rename from src/EllieBot/Services/Impl/RedisBotStringsProvider.cs rename to src/EllieBot/_common/Impl/RedisBotStringsProvider.cs diff --git a/src/EllieBot/Services/Impl/RemoteGrpcCoordinator.cs b/src/EllieBot/_common/Impl/RemoteGrpcCoordinator.cs similarity index 100% rename from src/EllieBot/Services/Impl/RemoteGrpcCoordinator.cs rename to src/EllieBot/_common/Impl/RemoteGrpcCoordinator.cs diff --git a/src/EllieBot/_common/Services/IGoogleApiService.cs b/src/EllieBot/_common/Services/IGoogleApiService.cs index 856bd51..d9fa0c7 100644 --- a/src/EllieBot/_common/Services/IGoogleApiService.cs +++ b/src/EllieBot/_common/Services/IGoogleApiService.cs @@ -5,7 +5,7 @@ public interface IGoogleApiService { IReadOnlyDictionary Languages { get; } - Task> GetVideoLinksByKeywordAsync(string keywords, int count = 1); + Task> GetVideoLinksByKeywordAsync(string keywords, int count = 1); Task> GetVideoInfosByKeywordAsync(string keywords, int count = 1); Task> GetPlaylistIdsByKeywordsAsync(string keywords, int count = 1); Task> GetRelatedVideosAsync(string id, int count = 1, string user = null); diff --git a/src/EllieBot/_common/Services/Impl/YtdlOperation.cs b/src/EllieBot/_common/Services/Impl/YtdlOperation.cs index 3813b80..95e8846 100644 --- a/src/EllieBot/_common/Services/Impl/YtdlOperation.cs +++ b/src/EllieBot/_common/Services/Impl/YtdlOperation.cs @@ -10,10 +10,9 @@ public class YtdlOperation private readonly string _baseArgString; private readonly bool _isYtDlp; - public YtdlOperation(string baseArgString, bool isYtDlp = false) + public YtdlOperation(string baseArgString) { _baseArgString = baseArgString; - _isYtDlp = isYtDlp; } private Process CreateProcess(string[] args) @@ -23,7 +22,7 @@ public class YtdlOperation { StartInfo = new() { - FileName = _isYtDlp ? "yt-dlp" : "youtube-dl", + FileName = "yt-dlp", Arguments = string.Format(_baseArgString, newArgs), UseShellExecute = false, RedirectStandardError = true, @@ -47,18 +46,18 @@ public class YtdlOperation var str = await process.StandardOutput.ReadToEndAsync(); var err = await process.StandardError.ReadToEndAsync(); if (!string.IsNullOrEmpty(err)) - Log.Warning("YTDL warning: {YtdlWarning}", err); + Log.Warning("yt-dlp warning: {YtdlWarning}", err); return str; } 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; } 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; } } diff --git a/src/EllieBot/data/marmalades/marmalade.yml b/src/EllieBot/data/marmalades/marmalade.yml index 34bfa71..4eb7a5c 100644 --- a/src/EllieBot/data/marmalades/marmalade.yml +++ b/src/EllieBot/data/marmalades/marmalade.yml @@ -2,3 +2,4 @@ version: 1 # List of marmalades automatically loaded at startup loaded: + - ngrpc