Compare commits
3 commits
564ae52291
...
b506b4461b
Author | SHA1 | Date | |
---|---|---|---|
b506b4461b | |||
a62a26091f | |||
c0cd161c90 |
46 changed files with 1061 additions and 285 deletions
|
@ -30,6 +30,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ellie.Marmalade", "src\Elli
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EllieBot.Voice", "src\EllieBot.Voice\EllieBot.Voice.csproj", "{1D93CE3C-80B4-49C7-A9A2-99988920AAEC}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EllieBot.Voice", "src\EllieBot.Voice\EllieBot.Voice.csproj", "{1D93CE3C-80B4-49C7-A9A2-99988920AAEC}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EllieBot.GrpcApiBase", "src\EllieBot.GrpcApiBase\EllieBot.GrpcApiBase.csproj", "{3B71F0BF-AE6C-480C-AB88-FCE23EDC7D91}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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}.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.ActiveCfg = Release|Any CPU
|
||||||
{1D93CE3C-80B4-49C7-A9A2-99988920AAEC}.Release|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
@ -76,6 +82,7 @@ Global
|
||||||
{F1A77F56-71B0-430E-AE46-94CDD7D43874} = {B28FB883-9688-41EB-BF5A-945F4A4EB628}
|
{F1A77F56-71B0-430E-AE46-94CDD7D43874} = {B28FB883-9688-41EB-BF5A-945F4A4EB628}
|
||||||
{76AC715D-12FF-4CBE-9585-A861139A2D0C} = {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}
|
{1D93CE3C-80B4-49C7-A9A2-99988920AAEC} = {B28FB883-9688-41EB-BF5A-945F4A4EB628}
|
||||||
|
{3B71F0BF-AE6C-480C-AB88-FCE23EDC7D91} = {B28FB883-9688-41EB-BF5A-945F4A4EB628}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {79F61C2C-CDBB-4361-A234-91A0B334CFE4}
|
SolutionGuid = {79F61C2C-CDBB-4361-A234-91A0B334CFE4}
|
||||||
|
|
21
src/EllieBot.GrpcApiBase/EllieBot.GrpcApiBase.csproj
Normal file
21
src/EllieBot.GrpcApiBase/EllieBot.GrpcApiBase.csproj
Normal 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>
|
26
src/EllieBot.GrpcApiBase/protos/econ.proto
Normal file
26
src/EllieBot.GrpcApiBase/protos/econ.proto
Normal file
|
@ -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;
|
||||||
|
}
|
50
src/EllieBot.GrpcApiBase/protos/exprs.proto
Normal file
50
src/EllieBot.GrpcApiBase/protos/exprs.proto
Normal file
|
@ -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;
|
||||||
|
}
|
57
src/EllieBot.GrpcApiBase/protos/greet.proto
Normal file
57
src/EllieBot.GrpcApiBase/protos/greet.proto
Normal file
|
@ -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;
|
||||||
|
}
|
52
src/EllieBot.GrpcApiBase/protos/info.proto
Normal file
52
src/EllieBot.GrpcApiBase/protos/info.proto
Normal file
|
@ -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;
|
||||||
|
}
|
81
src/EllieBot.GrpcApiBase/protos/other.proto
Normal file
81
src/EllieBot.GrpcApiBase/protos/other.proto
Normal file
|
@ -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;
|
||||||
|
}
|
83
src/EllieBot.GrpcApiBase/protos/warn.proto
Normal file
83
src/EllieBot.GrpcApiBase/protos/warn.proto
Normal file
|
@ -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;
|
||||||
|
}
|
|
@ -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" />
|
||||||
|
@ -103,6 +102,7 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\EllieBot.GrpcApiBase\EllieBot.GrpcApiBase.csproj" />
|
||||||
<ProjectReference Include="..\Ellie.Marmalade\Ellie.Marmalade.csproj" />
|
<ProjectReference Include="..\Ellie.Marmalade\Ellie.Marmalade.csproj" />
|
||||||
<ProjectReference Include="..\EllieBot.Voice\EllieBot.Voice.csproj" />
|
<ProjectReference Include="..\EllieBot.Voice\EllieBot.Voice.csproj" />
|
||||||
<ProjectReference Include="..\EllieBot.Generators\EllieBot.Generators.csproj" OutputItemType="Analyzer" />
|
<ProjectReference Include="..\EllieBot.Generators\EllieBot.Generators.csproj" OutputItemType="Analyzer" />
|
||||||
|
@ -113,9 +113,6 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Protobuf Include="..\EllieBot.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>
|
||||||
|
@ -131,7 +128,10 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Grpc\" />
|
<Protobuf Include="..\EllieBot.Coordinator\Protos\coordinator.proto">
|
||||||
|
<Link>_common\CoordinatorProtos\coordinator.proto</Link>
|
||||||
|
<!-- <GrpcServices>Client</GrpcServices>-->
|
||||||
|
</Protobuf>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)' == 'GlobalEllie' ">
|
<PropertyGroup Condition=" '$(Configuration)' == 'GlobalEllie' ">
|
||||||
|
|
|
@ -789,7 +789,7 @@ public sealed class EllieExpressionsService : 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);
|
||||||
|
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
@ -300,10 +300,10 @@ public class WaifuService : IEService, 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)
|
||||||
|
|
|
@ -25,30 +25,30 @@ 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)
|
||||||
.Skip(skip)
|
.Skip(skip)
|
||||||
.Take(count)
|
.Take(count)
|
||||||
.Select(x => new WaifuLbResult
|
.Select(x => new WaifuLbResult
|
||||||
{
|
{
|
||||||
Affinity = x.Affinity == null ? null : x.Affinity.Username,
|
Affinity = x.Affinity == null ? null : x.Affinity.Username,
|
||||||
AffinityDiscrim = x.Affinity == null ? null : x.Affinity.Discriminator,
|
AffinityDiscrim = x.Affinity == null ? null : x.Affinity.Discriminator,
|
||||||
Claimer = x.Claimer == null ? null : x.Claimer.Username,
|
Claimer = x.Claimer == null ? null : x.Claimer.Username,
|
||||||
ClaimerDiscrim = x.Claimer == null ? null : x.Claimer.Discriminator,
|
ClaimerDiscrim = x.Claimer == null ? null : x.Claimer.Discriminator,
|
||||||
Username = x.Waifu.Username,
|
Username = x.Waifu.Username,
|
||||||
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)
|
||||||
|
|
|
@ -77,7 +77,7 @@ public sealed partial class Help : EllieModule<HelpService>
|
||||||
m.Name,
|
m.Name,
|
||||||
null);
|
null);
|
||||||
|
|
||||||
#if GLOBAL_NADEKO
|
#if GLOBAL_ELLIE
|
||||||
if (m.Preconditions.Any(x => x is NoPublicBotAttribute))
|
if (m.Preconditions.Any(x => x is NoPublicBotAttribute))
|
||||||
continue;
|
continue;
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -7,10 +7,9 @@ public sealed class DefaultSearchServiceFactory : ISearchServiceFactory, IEServi
|
||||||
{
|
{
|
||||||
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, IEServi
|
||||||
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, IEServi
|
||||||
=> _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()
|
||||||
};
|
};
|
||||||
}
|
}
|
|
@ -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,86 +116,81 @@ 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]
|
||||||
// public async Task DuckDuckGo([Leftover] string query = null)
|
// public async Task DuckDuckGo([Leftover] string query = null)
|
||||||
// {
|
// {
|
||||||
// query = query?.Trim();
|
// query = query?.Trim();
|
||||||
// if (!await ValidateQuery(query))
|
// if (!await ValidateQuery(query))
|
||||||
// return;
|
// return;
|
||||||
//
|
//
|
||||||
// _ = ctx.Channel.TriggerTypingAsync();
|
// _ = ctx.Channel.TriggerTypingAsync();
|
||||||
//
|
//
|
||||||
// var data = await _service.DuckDuckGoSearchAsync(query);
|
// var data = await _service.DuckDuckGoSearchAsync(query);
|
||||||
// if (data is null)
|
// if (data is null)
|
||||||
// {
|
// {
|
||||||
// await Response().Error(strs.no_results).SendAsync();
|
// await Response().Error(strs.no_results).SendAsync();
|
||||||
// return;
|
// return;
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// var desc = data.Results.Take(5)
|
// var desc = data.Results.Take(5)
|
||||||
// .Select(res => $@"[**{res.Title}**]({res.Link})
|
// .Select(res => $@"[**{res.Title}**]({res.Link})
|
||||||
// {res.Text.TrimTo(380 - res.Title.Length - res.Link.Length)}");
|
// {res.Text.TrimTo(380 - res.Title.Length - res.Link.Length)}");
|
||||||
//
|
//
|
||||||
// var descStr = string.Join("\n\n", desc);
|
// var descStr = string.Join("\n\n", desc);
|
||||||
//
|
//
|
||||||
// var embed = _sender.CreateEmbed()
|
// var embed = _sender.CreateEmbed()
|
||||||
// .WithAuthor(ctx.User.ToString(),
|
// .WithAuthor(ctx.User.ToString(),
|
||||||
// "https://upload.wikimedia.org/wikipedia/en/9/90/The_DuckDuckGo_Duck.png")
|
// "https://upload.wikimedia.org/wikipedia/en/9/90/The_DuckDuckGo_Duck.png")
|
||||||
// .WithDescription($"{GetText(strs.search_for)} **{query}**\n\n" + descStr)
|
// .WithDescription($"{GetText(strs.search_for)} **{query}**\n\n" + descStr)
|
||||||
// .WithOkColor();
|
// .WithOkColor();
|
||||||
//
|
//
|
||||||
// await Response().Embed(embed).SendAsync();
|
// await Response().Embed(embed).SendAsync();
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,5 +2,5 @@
|
||||||
|
|
||||||
public interface IYoutubeSearchService
|
public interface IYoutubeSearchService
|
||||||
{
|
{
|
||||||
Task<VideoInfo?> SearchAsync(string query);
|
Task<VideoInfo[]?> SearchAsync(string query);
|
||||||
}
|
}
|
|
@ -18,7 +18,7 @@ public sealed class InvidiousYtSearchService : IYoutubeSearchService, IEService
|
||||||
_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, IEService
|
||||||
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, IEService
|
||||||
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -9,18 +9,15 @@ public sealed class YoutubeDataApiSearchService : IYoutubeSearchService, IEServi
|
||||||
_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
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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<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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +0,0 @@
|
||||||
namespace EllieBot.Modules.Searches.Youtube;
|
|
||||||
|
|
||||||
public sealed class YtdlYoutubeSearchService : YoutubedlxServiceBase, IEService
|
|
||||||
{
|
|
||||||
public override async Task<VideoInfo?> SearchAsync(string query)
|
|
||||||
=> await InternalGetInfoAsync(query, false);
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
namespace EllieBot.Modules.Searches.Youtube;
|
|
||||||
|
|
||||||
public sealed class YtdlpYoutubeSearchService : YoutubedlxServiceBase, IEService
|
|
||||||
{
|
|
||||||
public override async Task<VideoInfo?> SearchAsync(string query)
|
|
||||||
=> await InternalGetInfoAsync(query, true);
|
|
||||||
}
|
|
|
@ -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<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);
|
|
||||||
}
|
|
|
@ -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
|
||||||
}
|
}
|
73
src/EllieBot/Services/GrpcApi/ExprsSvc.cs
Normal file
73
src/EllieBot/Services/GrpcApi/ExprsSvc.cs
Normal file
|
@ -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<AddExprReply> 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<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();
|
||||||
|
}
|
||||||
|
}
|
121
src/EllieBot/Services/GrpcApi/GreetByeSvc.cs
Normal file
121
src/EllieBot/Services/GrpcApi/GreetByeSvc.cs
Normal file
|
@ -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<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)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
123
src/EllieBot/Services/GrpcApi/OtherSvc.cs
Normal file
123
src/EllieBot/Services/GrpcApi/OtherSvc.cs
Normal file
|
@ -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<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);
|
||||||
|
}
|
||||||
|
}
|
50
src/EllieBot/Services/GrpcApi/ServerInfoSvc.cs
Normal file
50
src/EllieBot/Services/GrpcApi/ServerInfoSvc.cs
Normal file
|
@ -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<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);
|
||||||
|
}
|
||||||
|
}
|
63
src/EllieBot/Services/GrpcApiService.cs
Normal file
63
src/EllieBot/Services/GrpcApiService.cs
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,27 +6,28 @@ namespace EllieBot.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; }
|
||||||
|
|
||||||
[Comment("""
|
[Comment("""
|
||||||
List of Ids of the users who have bot owner permissions
|
List of Ids of the users who have bot owner permissions
|
||||||
**DO NOT ADD PEOPLE YOU DON'T TRUST**
|
**DO NOT ADD PEOPLE YOU DON'T TRUST**
|
||||||
""")]
|
""")]
|
||||||
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("""
|
||||||
The number of shards that the bot will be running on.
|
The number of shards that the bot will be running on.
|
||||||
Leave at 1 if you don't know what you're doing.
|
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'.
|
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.
|
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; }
|
public int TotalShards { get; set; }
|
||||||
|
|
||||||
[Comment("""
|
[Comment("""
|
||||||
|
@ -38,34 +39,34 @@ public sealed class Creds : IBotCredentials
|
||||||
⚠ This does not currently work and is a work in progress.
|
⚠ This does not currently work and is a work in progress.
|
||||||
""")]
|
""")]
|
||||||
public string EllieAiToken { get; set; }
|
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.
|
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.
|
Then, go to APIs and Services -> Credentials and click Create credentials -> API key.
|
||||||
Used only for Youtube Data Api (at the moment).
|
Used only for Youtube Data Api (at the moment).
|
||||||
""")]
|
""")]
|
||||||
public string GoogleApiKey { get; set; }
|
public string GoogleApiKey { get; set; }
|
||||||
|
|
||||||
[Comment(
|
[Comment(
|
||||||
"""
|
"""
|
||||||
Create a new custom search here https://programmablesearchengine.google.com/cse/create/new
|
Create a new custom search here https://programmablesearchengine.google.com/cse/create/new
|
||||||
Enable SafeSearch
|
Enable SafeSearch
|
||||||
Remove all Sites to Search
|
Remove all Sites to Search
|
||||||
Enable Search the entire web
|
Enable Search the entire web
|
||||||
Copy the 'Search Engine ID' to the SearchId field
|
Copy the 'Search Engine ID' to the SearchId field
|
||||||
|
|
||||||
Do all steps again but enable image search for the ImageSearchId
|
Do all steps again but enable image search for the ImageSearchId
|
||||||
""")]
|
""")]
|
||||||
public GoogleApiConfig Google { get; set; }
|
public GoogleApiConfig Google { get; set; }
|
||||||
|
|
||||||
[Comment("""Settings for voting system for discordbots. Meant for use on global Ellie.""")]
|
[Comment("""Settings for voting system for discordbots. Meant for use on global Ellie.""")]
|
||||||
public VotesSettings Votes { get; set; }
|
public VotesSettings Votes { get; set; }
|
||||||
|
|
||||||
[Comment("""
|
[Comment("""
|
||||||
Patreon auto reward system settings.
|
Patreon auto reward system settings.
|
||||||
go to https://www.patreon.com/portal -> my clients -> create client
|
go to https://www.patreon.com/portal -> my clients -> create client
|
||||||
""")]
|
""")]
|
||||||
public PatreonSettings Patreon { get; set; }
|
public PatreonSettings Patreon { get; set; }
|
||||||
|
|
||||||
[Comment("""Api key for sending stats to DiscordBotList.""")]
|
[Comment("""Api key for sending stats to DiscordBotList.""")]
|
||||||
|
@ -76,27 +77,27 @@ public sealed class Creds : IBotCredentials
|
||||||
|
|
||||||
[Comment(@"OpenAi api key.")]
|
[Comment(@"OpenAi api key.")]
|
||||||
public string Gpt3ApiKey { get; set; }
|
public string Gpt3ApiKey { get; set; }
|
||||||
|
|
||||||
[Comment("""
|
[Comment("""
|
||||||
Which cache implementation should bot use.
|
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.
|
'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
|
'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; }
|
public BotCacheImplemenation BotCache { get; set; }
|
||||||
|
|
||||||
[Comment("""
|
[Comment("""
|
||||||
Redis connection string. Don't change if you don't know what you're doing.
|
Redis connection string. Don't change if you don't know what you're doing.
|
||||||
Only used if botCache is set to 'redis'
|
Only used if botCache is set to 'redis'
|
||||||
""")]
|
""")]
|
||||||
public string RedisOptions { get; set; }
|
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""")]
|
[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; }
|
public DbOptions Db { get; set; }
|
||||||
|
|
||||||
[Comment("""
|
[Comment("""
|
||||||
Address and port of the coordinator endpoint. Leave empty for default.
|
Address and port of the coordinator endpoint. Leave empty for default.
|
||||||
Change only if you've changed the coordinator address or port.
|
Change only if you've changed the coordinator address or port.
|
||||||
""")]
|
""")]
|
||||||
public string CoordinatorUrl { get; set; }
|
public string CoordinatorUrl { get; set; }
|
||||||
|
|
||||||
[Comment(
|
[Comment(
|
||||||
|
@ -104,34 +105,34 @@ public sealed class Creds : IBotCredentials
|
||||||
public string RapidApiKey { get; set; }
|
public string RapidApiKey { get; set; }
|
||||||
|
|
||||||
[Comment("""
|
[Comment("""
|
||||||
https://locationiq.com api key (register and you will receive the token in the email).
|
https://locationiq.com api key (register and you will receive the token in the email).
|
||||||
Used only for .time command.
|
Used only for .time command.
|
||||||
""")]
|
""")]
|
||||||
public string LocationIqApiKey { get; set; }
|
public string LocationIqApiKey { get; set; }
|
||||||
|
|
||||||
[Comment("""
|
[Comment("""
|
||||||
https://timezonedb.com api key (register and you will receive the token in the email).
|
https://timezonedb.com api key (register and you will receive the token in the email).
|
||||||
Used only for .time command
|
Used only for .time command
|
||||||
""")]
|
""")]
|
||||||
public string TimezoneDbApiKey { get; set; }
|
public string TimezoneDbApiKey { get; set; }
|
||||||
|
|
||||||
[Comment("""
|
[Comment("""
|
||||||
https://pro.coinmarketcap.com/account/ api key. There is a free plan for personal use.
|
https://pro.coinmarketcap.com/account/ api key. There is a free plan for personal use.
|
||||||
Used for cryptocurrency related commands.
|
Used for cryptocurrency related commands.
|
||||||
""")]
|
""")]
|
||||||
public string CoinmarketcapApiKey { get; set; }
|
public string CoinmarketcapApiKey { get; set; }
|
||||||
|
|
||||||
// [Comment(@"https://polygon.io/dashboard/api-keys api key. Free plan allows for 5 queries per minute.
|
// [Comment(@"https://polygon.io/dashboard/api-keys api key. Free plan allows for 5 queries per minute.
|
||||||
// Used for stocks related commands.")]
|
// Used for stocks related commands.")]
|
||||||
// public string PolygonIoApiKey { get; set; }
|
// public string PolygonIoApiKey { get; set; }
|
||||||
|
|
||||||
[Comment("""Api key used for Osu related commands. Obtain this key at https://osu.ppy.sh/p/api""")]
|
[Comment("""Api key used for Osu related commands. Obtain this key at https://osu.ppy.sh/p/api""")]
|
||||||
public string OsuApiKey { get; set; }
|
public string OsuApiKey { get; set; }
|
||||||
|
|
||||||
[Comment("""
|
[Comment("""
|
||||||
Optional Trovo client id.
|
Optional Trovo client id.
|
||||||
You should use this if Trovo stream notifications stopped working or you're getting ratelimit errors.
|
You should use this if Trovo stream notifications stopped working or you're getting ratelimit errors.
|
||||||
""")]
|
""")]
|
||||||
public string TrovoClientId { get; set; }
|
public string TrovoClientId { get; set; }
|
||||||
|
|
||||||
[Comment("""Obtain by creating an application at https://dev.twitch.tv/console/apps""")]
|
[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; }
|
public string TwitchClientSecret { get; set; }
|
||||||
|
|
||||||
[Comment("""
|
[Comment("""
|
||||||
Command and args which will be used to restart the bot.
|
Command and args which will be used to restart the bot.
|
||||||
Only used if bot is executed directly (NOT through the coordinator)
|
Only used if bot is executed directly (NOT through the coordinator)
|
||||||
placeholders:
|
placeholders:
|
||||||
{0} -> shard id
|
{0} -> shard id
|
||||||
{1} -> total shards
|
{1} -> total shards
|
||||||
Linux default
|
Linux default
|
||||||
cmd: dotnet
|
cmd: dotnet
|
||||||
args: "EllieBot.dll -- {0}"
|
args: "EllieBot.dll -- {0}"
|
||||||
Windows default
|
Windows default
|
||||||
cmd: EllieBot.exe
|
cmd: EllieBot.exe
|
||||||
args: "{0}"
|
args: "{0}"
|
||||||
""")]
|
""")]
|
||||||
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>();
|
||||||
|
@ -180,24 +188,26 @@ 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
|
||||||
: IDbOptions
|
: IDbOptions
|
||||||
{
|
{
|
||||||
[Comment("""
|
[Comment("""
|
||||||
Database type. "sqlite", "mysql" and "postgresql" are supported.
|
Database type. "sqlite", "mysql" and "postgresql" are supported.
|
||||||
Default is "sqlite"
|
Default is "sqlite"
|
||||||
""")]
|
""")]
|
||||||
public string Type { get; set; }
|
public string Type { get; set; }
|
||||||
|
|
||||||
[Comment("""
|
[Comment("""
|
||||||
Database connection string.
|
Database connection string.
|
||||||
You MUST change this if you're not using "sqlite" type.
|
You MUST change this if you're not using "sqlite" type.
|
||||||
Default is "Data Source=data/EllieBot.db"
|
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 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;"
|
Example for postgresql: "Server=localhost;Port=5432;User Id=postgres;Password=my_super_secret_postgres_password;Database=ellie;"
|
||||||
""")]
|
""")]
|
||||||
public string ConnectionString { get; set; }
|
public string ConnectionString { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -232,29 +242,29 @@ public sealed class Creds : IBotCredentials
|
||||||
public sealed record VotesSettings : IVotesSettings
|
public sealed record VotesSettings : IVotesSettings
|
||||||
{
|
{
|
||||||
[Comment("""
|
[Comment("""
|
||||||
top.gg votes service url
|
top.gg votes service url
|
||||||
This is the url of your instance of the EllieBot.Votes api
|
This is the url of your instance of the EllieBot.Votes api
|
||||||
Example: https://votes.my.cool.bot.com
|
Example: https://votes.my.cool.bot.com
|
||||||
""")]
|
""")]
|
||||||
public string TopggServiceUrl { get; set; }
|
public string TopggServiceUrl { get; set; }
|
||||||
|
|
||||||
[Comment("""
|
[Comment("""
|
||||||
Authorization header value sent to the TopGG service url with each request
|
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
|
This should be equivalent to the TopggKey in your EllieBot.Votes api appsettings.json file
|
||||||
""")]
|
""")]
|
||||||
public string TopggKey { get; set; }
|
public string TopggKey { get; set; }
|
||||||
|
|
||||||
[Comment("""
|
[Comment("""
|
||||||
discords.com votes service url
|
discords.com votes service url
|
||||||
This is the url of your instance of the EllieBot.Votes api
|
This is the url of your instance of the EllieBot.Votes api
|
||||||
Example: https://votes.my.cool.bot.com
|
Example: https://votes.my.cool.bot.com
|
||||||
""")]
|
""")]
|
||||||
public string DiscordsServiceUrl { get; set; }
|
public string DiscordsServiceUrl { get; set; }
|
||||||
|
|
||||||
[Comment("""
|
[Comment("""
|
||||||
Authorization header value sent to the Discords service url with each request
|
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
|
This should be equivalent to the DiscordsKey in your EllieBot.Votes api appsettings.json file
|
||||||
""")]
|
""")]
|
||||||
public string DiscordsKey { get; set; }
|
public string DiscordsKey { get; set; }
|
||||||
|
|
||||||
public VotesSettings()
|
public VotesSettings()
|
||||||
|
@ -273,13 +283,19 @@ 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
|
||||||
{
|
{
|
||||||
public string SearchId { get; init; }
|
public string SearchId { get; init; }
|
||||||
public string ImageSearchId { get; init; }
|
public string ImageSearchId { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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);
|
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, IEService
|
||||||
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(
|
|
@ -5,7 +5,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);
|
||||||
|
|
|
@ -8,12 +8,10 @@ namespace EllieBot.Services;
|
||||||
public class YtdlOperation
|
public class YtdlOperation
|
||||||
{
|
{
|
||||||
private readonly string _baseArgString;
|
private readonly string _baseArgString;
|
||||||
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 +21,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 +45,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,3 +2,4 @@
|
||||||
version: 1
|
version: 1
|
||||||
# List of marmalades automatically loaded at startup
|
# List of marmalades automatically loaded at startup
|
||||||
loaded:
|
loaded:
|
||||||
|
- ngrpc
|
||||||
|
|
Loading…
Reference in a new issue