Compare commits

...

14 commits
5.1.10 ... v5

Author SHA1 Message Date
8176cdbf96
Version upped to 5.1.14, updated CHANGELOG.md 2024-10-05 14:16:57 +13:00
113dc3748a
improved .xplb -c, it will now correctly work only on users who are still in the server, isntead of only top 1k
Fixed marmalade error on bot startup
2024-10-05 14:14:33 +13:00
5c72c6562f
Version upped to 5.1.13, updated CHANGELOG.md
Fixed seq comment in creds
2024-10-05 13:29:09 +13:00
dd939ce55a
Grpc api will no longer start unless it's enabled in creds 2024-10-05 13:24:40 +13:00
1a52085340
Forgot a entry in CHANGELOG.md 2024-10-05 12:53:07 +13:00
487c7865cb
Fixed greet/bye messages showing wrong message in the wrong server sometimes
Fixed the check for updates service
Version upped to 5.1.12. Updated CHANGELOG.md
2024-10-05 11:44:44 +13:00
3ba1d06fd0
expressions will no longer cause exceptions if the bot doesn't have perms to write in the target channel
Cleaned up expr code a little bit
2024-10-05 11:17:12 +13:00
4338df0b38
Updated Changelog, version upped to 5.1.11
Ellie might grumble about the ngrpc marmalade not being able to load but it should be fine
2024-10-03 19:14:24 +13:00
a321cdbe55
fixed build warnings 2024-10-03 18:51:55 +13:00
391d2e43e8
Added grpc api, perm system
grpc api config in creds
2024-10-03 18:46:10 +13:00
de97213046
Possible fixes for buggy .bye behavior 2024-10-03 17:51:01 +13:00
b506b4461b
Updated Help.cs 2024-10-03 17:39:47 +13:00
a62a26091f
Fixed a build warning 2024-10-03 17:38:49 +13:00
c0cd161c90
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
2024-10-03 17:24:13 +13:00
87 changed files with 1683 additions and 581 deletions

View file

@ -2,6 +2,54 @@
Mostly based on [keepachangelog](https://keepachangelog.com/en/1.1.0/) except date format. a-c-f-r-o Mostly based on [keepachangelog](https://keepachangelog.com/en/1.1.0/) except date format. a-c-f-r-o
## [5.1.14] - 03.10.2024
## Changed
- Improved `.xplb -c`, it will now correctly only show users who are still in the server with no count limit
## Fixed
- Fixed marmalade load error on startup
## [5.1.13] - 03.10.2024
### Fixed
- Grpc api server will no longer start unless enabled in creds
- Seq comment in creds fixed
## [5.1.12] - 03.10.2024
### Added
- Added support for `seq` for logging. If you fill in seq url and apiKey in creds.yml, bot will sends logs to it
### Fixed
- Fixed the Check for updates service not using the right URL and spitting an error in the console.
- Fixed another bug in `.greet` / `.bye` system, which caused it to show wrong message on a wrong server occasionally
## [5.1.11] - 03.10.2024
### Added
- Added `%user.displayname%` placeholder. It will show users nickname, if there is one, otherwise it will show the username.
- Nickname won't be shown in bye messages.
- Added initial version of grpc api. Beta
### Fixed
- Fixed a bug which caused `.bye` and `.greet` messages to be randomly disabled
- Fixed `.lb -c` breaking sometimes, and fixed pagination
### Changed
- Youtube now always uses `yt-dlp`. Dropped support for `youtube-dl`
- If you've previously renamed your yt-dlp file to youtube-dl, please rename it back.
- ytProvider in data/searches.yml now also controls where you're getting your song streams from.
- (Invidious support added for .q)
## [5.1.10] - 24.09.2024 ## [5.1.10] - 24.09.2024
### Fixed ### Fixed

View file

@ -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}

View file

@ -9,7 +9,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" PrivateAssets="all" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" /> <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" PrivateAssets="all" GeneratePathProperty="true" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" PrivateAssets="all" GeneratePathProperty="true" />
</ItemGroup> </ItemGroup>

View file

@ -0,0 +1,147 @@
#nullable enable
using System.CodeDom.Compiler;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using Newtonsoft.Json;
namespace EllieBot.Generators
{
public readonly record struct MethodPermData
{
public readonly string Name;
public readonly string Value;
public MethodPermData(string name, string value)
{
Name = name;
Value = value;
}
}
[Generator]
public class GrpcApiPermGenerator : IIncrementalGenerator
{
public const string Attribute =
"""
namespace EllieBot.GrpcApi;
[System.AttributeUsage(System.AttributeTargets.Method)]
public class GrpcApiPermAttribute : System.Attribute
{
public GuildPerm Value { get; }
public GrpcApiPermAttribute(GuildPerm value) => Value = value;
}
""";
public void Initialize(IncrementalGeneratorInitializationContext context)
{
context.RegisterPostInitializationOutput(ctx => ctx.AddSource("GrpcApiPermAttribute.cs",
SourceText.From(Attribute, Encoding.UTF8)));
var enumsToGenerate = context.SyntaxProvider
.ForAttributeWithMetadataName(
"EllieBot.GrpcApi.GrpcApiPermAttribute",
predicate: static (s, _) => s is MethodDeclarationSyntax,
transform: static (ctx, _) => GetMethodSemanticTargets(ctx.SemanticModel, ctx.TargetNode))
.Where(static m => m is not null)
.Select(static (x, _) => x!.Value)
.Collect();
context.RegisterSourceOutput(enumsToGenerate,
static (spc, source) => Execute(source, spc));
}
private static MethodPermData? GetMethodSemanticTargets(SemanticModel model, SyntaxNode node)
{
var method = (MethodDeclarationSyntax)node;
var name = method.Identifier.Text;
var attr = method.AttributeLists
.SelectMany(x => x.Attributes)
.FirstOrDefault();
// .FirstOrDefault(x => x.Name.ToString() == "GrpcApiPermAttribute");
if (attr is null)
return null;
// if (model.GetSymbolInfo(attr).Symbol is not IMethodSymbol attrSymbol)
// return null;
return new MethodPermData(name, attr.ArgumentList.Arguments[0].ToString() ?? "__missing_perm__");
// return new MethodPermData(name, attrSymbol.Parameters[0].ContainingType.ToDisplayString() + "." + attrSymbol.Parameters[0].Name);
}
private static void Execute(ImmutableArray<MethodPermData> fields, SourceProductionContext ctx)
{
using (var stringWriter = new StringWriter())
using (var sw = new IndentedTextWriter(stringWriter))
{
sw.WriteLine("using System.Collections.Frozen;");
sw.WriteLine();
sw.WriteLine("namespace EllieBot.GrpcApi;");
sw.WriteLine();
sw.WriteLine("public partial class PermsInterceptor");
sw.WriteLine("{");
sw.Indent++;
sw.WriteLine("public static FrozenDictionary<string, GuildPerm> perms = new Dictionary<string, GuildPerm>()");
sw.WriteLine("{");
sw.Indent++;
foreach (var field in fields)
{
sw.WriteLine("{{ \"{0}\", {1} }},", field.Name, field.Value);
}
sw.Indent--;
sw.WriteLine("}.ToFrozenDictionary();");
sw.Indent--;
sw.WriteLine("}");
sw.Flush();
ctx.AddSource("GrpcApiInterceptor.g.cs", stringWriter.ToString());
}
}
private List<TranslationPair> GetFields(string? dataText)
{
if (string.IsNullOrWhiteSpace(dataText))
return new();
Dictionary<string, string> data;
try
{
var output = JsonConvert.DeserializeObject<Dictionary<string, string>>(dataText!);
if (output is null)
return new();
data = output;
}
catch
{
Debug.WriteLine("Failed parsing responses file.");
return new();
}
var list = new List<TranslationPair>();
foreach (var entry in data)
{
list.Add(new(
entry.Key,
entry.Value
));
}
return list;
}
}
}

View file

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

View file

@ -0,0 +1,26 @@
syntax = "proto3";
option csharp_namespace = "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;
}

View 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;
}

View 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;
}

View file

@ -0,0 +1,137 @@
syntax = "proto3";
option csharp_namespace = "EllieBot.GrpcApi";
import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
package other;
service GrpcOther {
rpc GetGuilds(google.protobuf.Empty) returns (GetGuildsReply);
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);
rpc GetServerInfo(ServerInfoRequest) returns (GetServerInfoReply);
}
message GetGuildsReply {
repeated GuildReply guilds = 1;
}
message GuildReply {
uint64 id = 1;
string name = 2;
string iconUrl = 3;
}
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;
}
message ServerInfoRequest {
uint64 guildId = 1;
}
message GetServerInfoReply {
uint64 id = 1;
string name = 2;
string iconUrl = 3;
uint64 ownerId = 4;
string ownerName = 5;
repeated RoleReply roles = 6;
repeated EmojiReply emojis = 7;
repeated string features = 8;
int32 textChannels = 9;
int32 voiceChannels = 10;
int32 memberCount = 11;
int64 createdAt = 12;
}
message RoleReply {
uint64 id = 1;
string name = 2;
string iconUrl = 3;
string color = 4;
}
message EmojiReply {
string name = 1;
string url = 2;
string code = 3;
}
message ChannelReply {
uint64 id = 1;
string name = 2;
ChannelType type = 3;
}
enum ChannelType {
Text = 0;
Voice = 1;
}

View file

@ -0,0 +1,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;
}

View file

@ -25,7 +25,7 @@ public sealed class Bot : IBot
public bool IsReady { get; private set; } public bool IsReady { get; private set; }
public int ShardId { get; set; } public int ShardId { get; set; }
private readonly IBotCredentials _creds; private readonly IBotCreds _creds;
private readonly CommandService _commandService; private readonly CommandService _commandService;
private readonly DbService _db; private readonly DbService _db;
@ -42,6 +42,9 @@ public sealed class Bot : IBot
_credsProvider = new BotCredsProvider(totalShards, credPath); _credsProvider = new BotCredsProvider(totalShards, credPath);
_creds = _credsProvider.GetCreds(); _creds = _credsProvider.GetCreds();
LogSetup.SetupLogger(shardId, _creds);
Log.Information("Pid: {ProcessId}", Environment.ProcessId);
_db = new EllieDbService(_credsProvider); _db = new EllieDbService(_credsProvider);
var messageCacheSize = var messageCacheSize =
@ -115,7 +118,7 @@ public sealed class Bot : IBot
// svcs.Components.Remove<IPlanner, Planner>(); // svcs.Components.Remove<IPlanner, Planner>();
// svcs.Components.Add<IPlanner, RemovablePlanner>(); // svcs.Components.Add<IPlanner, RemovablePlanner>();
svcs.AddSingleton<IBotCredentials>(_ => _credsProvider.GetCreds()); svcs.AddSingleton<IBotCreds>(_ => _credsProvider.GetCreds());
svcs.AddSingleton<DbService, DbService>(_db); svcs.AddSingleton<DbService, DbService>(_db);
svcs.AddSingleton<IBotCredsProvider>(_credsProvider); svcs.AddSingleton<IBotCredsProvider>(_credsProvider);
svcs.AddSingleton<DiscordSocketClient>(Client); svcs.AddSingleton<DiscordSocketClient>(Client);

View file

@ -26,17 +26,6 @@ public static class UserXpExtensions
return usr; return usr;
} }
public static async Task<IReadOnlyCollection<UserXpStats>> GetUsersFor(
this DbSet<UserXpStats> xps,
ulong guildId,
int page)
=> await xps.ToLinqToDBTable()
.Where(x => x.GuildId == guildId)
.OrderByDescending(x => x.Xp + x.AwardedXp)
.Skip(page * 9)
.Take(9)
.ToArrayAsyncLinqToDB();
public static async Task<List<UserXpStats>> GetTopUserXps(this DbSet<UserXpStats> xps, ulong guildId, int count) public static async Task<List<UserXpStats>> GetTopUserXps(this DbSet<UserXpStats> xps, ulong guildId, int count)
=> await xps.ToLinqToDBTable() => await xps.ToLinqToDBTable()
.Where(x => x.GuildId == guildId) .Where(x => x.GuildId == guildId)

View file

@ -4,7 +4,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>true</ImplicitUsings> <ImplicitUsings>true</ImplicitUsings>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages> <SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<Version>5.1.10</Version> <Version>5.1.14</Version>
<!-- Output/build --> <!-- Output/build -->
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory> <RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
@ -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' ">

View file

@ -93,6 +93,9 @@ public class GreetService : IEService, IReadyExecutor
private Task ClientOnGuildMemberUpdated(Cacheable<SocketGuildUser, ulong> optOldUser, SocketGuildUser newUser) private Task ClientOnGuildMemberUpdated(Cacheable<SocketGuildUser, ulong> optOldUser, SocketGuildUser newUser)
{ {
if (!_enabled[GreetType.Boost].Contains(newUser.Guild.Id))
return Task.CompletedTask;
// if user is a new booster // if user is a new booster
// or boosted again the same server // or boosted again the same server
if ((optOldUser.Value is { PremiumSince: null } && newUser is { PremiumSince: not null }) if ((optOldUser.Value is { PremiumSince: null } && newUser is { PremiumSince: not null })
@ -134,21 +137,63 @@ public class GreetService : IEService, IReadyExecutor
.DeleteAsync(); .DeleteAsync();
} }
private Task OnUserLeft(SocketGuild guild, SocketUser user) private Task OnUserJoined(IGuildUser user)
{ {
_ = Task.Run(async () => _ = Task.Run(async () =>
{ {
try try
{ {
var conf = await GetGreetSettingsAsync(guild.Id, GreetType.Bye); if (_enabled[GreetType.Greet].Contains(user.GuildId))
{
var conf = await GetGreetSettingsAsync(user.GuildId, GreetType.Greet);
if (conf?.ChannelId is ulong cid)
{
var channel = await user.Guild.GetTextChannelAsync(cid);
if (channel is not null)
{
await _greetQueue.Writer.WriteAsync((conf, user, channel));
}
}
}
if (conf is null)
if (_enabled[GreetType.GreetDm].Contains(user.GuildId))
{
var confDm = await GetGreetSettingsAsync(user.GuildId, GreetType.GreetDm);
if (confDm is not null)
{
await _greetQueue.Writer.WriteAsync((confDm, user, null));
}
}
}
catch (Exception ex)
{
Log.Error(ex, "Error in GreetService.OnUserJoined. This should not happen. Please report it");
}
});
return Task.CompletedTask;
}
private Task OnUserLeft(SocketGuild guild, SocketUser user)
{
_ = Task.Run(async () =>
{
if (!_enabled[GreetType.Bye].Contains(guild.Id))
return; return;
var channel = guild.TextChannels.FirstOrDefault(c => c.Id == conf.ChannelId); try
{
var conf = await GetGreetSettingsAsync(guild.Id, GreetType.Bye);
if (conf?.ChannelId is not { } cid)
return;
var channel = guild.GetChannel(cid) as ITextChannel;
if (channel is null) //maybe warn the server owner that the channel is missing if (channel is null) //maybe warn the server owner that the channel is missing
{ {
Log.Warning("Channel {ChannelId} in {GuildId} was not found. Bye message will be disabled",
conf.ChannelId,
conf.GuildId);
await SetGreet(guild.Id, null, GreetType.Bye, false); await SetGreet(guild.Id, null, GreetType.Bye, false);
return; return;
} }
@ -163,11 +208,11 @@ public class GreetService : IEService, IReadyExecutor
return Task.CompletedTask; return Task.CompletedTask;
} }
private TypedKey<GreetSettings?> GreetSettingsKey(GreetType type) private TypedKey<GreetSettings?> GreetSettingsKey(ulong gid, GreetType type)
=> new($"greet_settings:{type}"); => new($"greet_settings:{gid}:{type}");
public async Task<GreetSettings?> GetGreetSettingsAsync(ulong gid, GreetType type) public async Task<GreetSettings?> GetGreetSettingsAsync(ulong gid, GreetType type)
=> await _cache.GetOrAddAsync<GreetSettings?>(GreetSettingsKey(type), => await _cache.GetOrAddAsync<GreetSettings?>(GreetSettingsKey(gid, type),
() => InternalGetGreetSettingsAsync(gid, type), () => InternalGetGreetSettingsAsync(gid, type),
TimeSpan.FromSeconds(3)); TimeSpan.FromSeconds(3));
@ -216,9 +261,10 @@ public class GreetService : IEService, IReadyExecutor
or DiscordErrorCode.UnknownChannel) or DiscordErrorCode.UnknownChannel)
{ {
Log.Warning(ex, Log.Warning(ex,
"Missing permissions to send a bye message, the greet message will be disabled on server: {GuildId}", "Missing permissions to send a {GreetType} message, it will be disabled on server: {GuildId}",
conf.GreetType,
channel.GuildId); channel.GuildId);
await SetGreet(channel.GuildId, channel.Id, GreetType.Greet, false); await SetGreet(channel.GuildId, channel.Id, conf.GreetType, false);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -307,43 +353,6 @@ public class GreetService : IEService, IReadyExecutor
IconUrl = user.Guild.IconUrl IconUrl = user.Guild.IconUrl
}; };
private Task OnUserJoined(IGuildUser user)
{
_ = Task.Run(async () =>
{
try
{
if (_enabled[GreetType.Greet].Contains(user.GuildId))
{
var conf = await GetGreetSettingsAsync(user.GuildId, GreetType.Greet);
if (conf?.ChannelId is ulong cid)
{
var channel = await user.Guild.GetTextChannelAsync(cid);
if (channel is not null)
{
await _greetQueue.Writer.WriteAsync((conf, user, channel));
}
}
}
if (_enabled[GreetType.GreetDm].Contains(user.GuildId))
{
var confDm = await GetGreetSettingsAsync(user.GuildId, GreetType.GreetDm);
if (confDm is not null)
{
await _greetQueue.Writer.WriteAsync((confDm, user, null));
}
}
}
catch(Exception ex)
{
Log.Error(ex, "Error in GreetService.OnUserJoined. This should not happen. Please report it");
}
});
return Task.CompletedTask;
}
public static string GetDefaultGreet(GreetType greetType) public static string GetDefaultGreet(GreetType greetType)
=> greetType switch => greetType switch

View file

@ -13,7 +13,7 @@ public sealed class ReactionRolesService : IReadyExecutor, IEService, IReactionR
{ {
private readonly DbService _db; private readonly DbService _db;
private readonly DiscordSocketClient _client; private readonly DiscordSocketClient _client;
private readonly IBotCredentials _creds; private readonly IBotCreds _creds;
private ConcurrentDictionary<ulong, List<ReactionRoleV2>> _cache; private ConcurrentDictionary<ulong, List<ReactionRoleV2>> _cache;
private readonly object _cacheLock = new(); private readonly object _cacheLock = new();
@ -24,7 +24,7 @@ public sealed class ReactionRolesService : IReadyExecutor, IEService, IReactionR
DiscordSocketClient client, DiscordSocketClient client,
IPatronageService ps, IPatronageService ps,
DbService db, DbService db,
IBotCredentials creds) IBotCreds creds)
{ {
_db = db; _db = db;
_client = client; _client = client;

View file

@ -9,13 +9,13 @@ namespace EllieBot.Modules.Administration;
public sealed class StickyRolesService : IEService, IReadyExecutor public sealed class StickyRolesService : IEService, IReadyExecutor
{ {
private readonly DiscordSocketClient _client; private readonly DiscordSocketClient _client;
private readonly IBotCredentials _creds; private readonly IBotCreds _creds;
private readonly DbService _db; private readonly DbService _db;
private HashSet<ulong> _stickyRoles = new(); private HashSet<ulong> _stickyRoles = new();
public StickyRolesService( public StickyRolesService(
DiscordSocketClient client, DiscordSocketClient client,
IBotCredentials creds, IBotCreds creds,
DbService db) DbService db)
{ {
_client = client; _client = client;

View file

@ -19,7 +19,7 @@ public sealed class CheckForUpdatesService : IEService, IReadyExecutor
private readonly IMessageSenderService _sender; private readonly IMessageSenderService _sender;
private const string RELEASES_URL = "https://toastielab.dev/Emotions-stuff/elliebot/releases"; private const string RELEASES_URL = "https://toastielab.dev/api/v1/repos/Emotions-stuff/elliebot/releases";
public CheckForUpdatesService( public CheckForUpdatesService(
BotConfigService bcs, BotConfigService bcs,

View file

@ -15,7 +15,7 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, IEService
private readonly IBotStrings _strings; private readonly IBotStrings _strings;
private readonly DiscordSocketClient _client; private readonly DiscordSocketClient _client;
private readonly IBotCredentials _creds; private readonly IBotCreds _creds;
private ImmutableDictionary<ulong, IDMChannel> ownerChannels = private ImmutableDictionary<ulong, IDMChannel> ownerChannels =
new Dictionary<ulong, IDMChannel>().ToImmutableDictionary(); new Dictionary<ulong, IDMChannel>().ToImmutableDictionary();
@ -36,7 +36,7 @@ public sealed class SelfService : IExecNoCommand, IReadyExecutor, IEService
CommandHandler cmdHandler, CommandHandler cmdHandler,
DbService db, DbService db,
IBotStrings strings, IBotStrings strings,
IBotCredentials creds, IBotCreds creds,
IHttpClientFactory factory, IHttpClientFactory factory,
BotConfigService bss, BotConfigService bss,
IPubSub pubSub, IPubSub pubSub,

View file

@ -6,49 +6,6 @@ namespace EllieBot.Modules.EllieExpressions;
public static class EllieExpressionExtensions public static class EllieExpressionExtensions
{ {
private static string ResolveTriggerString(this string str, DiscordSocketClient client)
=> str.Replace("%bot.mention%", client.CurrentUser.Mention, StringComparison.Ordinal);
public static async Task<IUserMessage> Send(
this EllieExpression cr,
IUserMessage ctx,
IReplacementService repSvc,
DiscordSocketClient client,
IMessageSenderService sender)
{
var channel = cr.DmResponse ? await ctx.Author.CreateDMChannelAsync() : ctx.Channel;
var trigger = cr.Trigger.ResolveTriggerString(client);
var substringIndex = trigger.Length;
if (cr.ContainsAnywhere)
{
var pos = ctx.Content.AsSpan().GetWordPosition(trigger);
if (pos == WordPosition.Start)
substringIndex += 1;
else if (pos == WordPosition.End)
substringIndex = ctx.Content.Length;
else if (pos == WordPosition.Middle)
substringIndex += ctx.Content.IndexOf(trigger, StringComparison.InvariantCulture);
}
var canMentionEveryone = (ctx.Author as IGuildUser)?.GuildPermissions.MentionEveryone ?? true;
var repCtx = new ReplacementContext(client: client,
guild: (ctx.Channel as ITextChannel)?.Guild as SocketGuild,
channel: ctx.Channel,
user: ctx.Author
)
.WithOverride("%target%",
() => canMentionEveryone
? ctx.Content[substringIndex..].Trim()
: ctx.Content[substringIndex..].Trim().SanitizeMentions(true));
var text = SmartText.CreateFrom(cr.Response);
text = await repSvc.ReplaceAsync(text, repCtx);
return await sender.Response(channel).Text(text).Sanitize(false).SendAsync();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static WordPosition GetWordPosition(this ReadOnlySpan<char> str, in ReadOnlySpan<char> word) public static WordPosition GetWordPosition(this ReadOnlySpan<char> str, in ReadOnlySpan<char> word)
{ {

View file

@ -11,10 +11,10 @@ public partial class EllieExpressions : EllieModule<EllieExpressionsService>
All All
} }
private readonly IBotCredentials _creds; private readonly IBotCreds _creds;
private readonly IHttpClientFactory _clientFactory; private readonly IHttpClientFactory _clientFactory;
public EllieExpressions(IBotCredentials creds, IHttpClientFactory clientFactory) public EllieExpressions(IBotCreds creds, IHttpClientFactory clientFactory)
{ {
_creds = creds; _creds = creds;
_clientFactory = clientFactory; _clientFactory = clientFactory;

View file

@ -249,8 +249,9 @@ public sealed class EllieExpressionsService : IExecOnMessage, IReadyExecutor
try try
{ {
if (guild is SocketGuild sg) if (guild is not SocketGuild sg)
{ return false;
var result = await _permChecker.CheckPermsAsync( var result = await _permChecker.CheckPermsAsync(
guild, guild,
msg.Channel, msg.Channel,
@ -286,9 +287,16 @@ public sealed class EllieExpressionsService : IExecOnMessage, IReadyExecutor
return true; return true;
} }
}
var sentMsg = await expr.Send(msg, _repSvc, _client, _sender); var cu = sg.CurrentUser;
var channel = expr.DmResponse ? await msg.Author.CreateDMChannelAsync() : msg.Channel;
// have no perms to speak in that channel
if (channel is ITextChannel tc && !cu.GetPermissions(tc).SendMessages)
return false;
var sentMsg = await Send(expr, msg, channel);
var reactions = expr.GetReactions(); var reactions = expr.GetReactions();
foreach (var reaction in reactions) foreach (var reaction in reactions)
@ -336,6 +344,47 @@ public sealed class EllieExpressionsService : IExecOnMessage, IReadyExecutor
return false; return false;
} }
public string ResolveTriggerString(string str)
=> str.Replace("%bot.mention%", _client.CurrentUser.Mention, StringComparison.Ordinal);
public async Task<IUserMessage> Send(
EllieExpression cr,
IUserMessage ctx,
IMessageChannel channel
)
{
var trigger = ResolveTriggerString(cr.Trigger);
var substringIndex = trigger.Length;
if (cr.ContainsAnywhere)
{
var pos = ctx.Content.AsSpan().GetWordPosition(trigger);
if (pos == WordPosition.Start)
substringIndex += 1;
else if (pos == WordPosition.End)
substringIndex = ctx.Content.Length;
else if (pos == WordPosition.Middle)
substringIndex += ctx.Content.IndexOf(trigger, StringComparison.InvariantCulture);
}
var canMentionEveryone = (ctx.Author as IGuildUser)?.GuildPermissions.MentionEveryone ?? true;
var repCtx = new ReplacementContext(client: _client,
guild: (ctx.Channel as ITextChannel)?.Guild as SocketGuild,
channel: ctx.Channel,
user: ctx.Author
)
.WithOverride("%target%",
() => canMentionEveryone
? ctx.Content[substringIndex..].Trim()
: ctx.Content[substringIndex..].Trim().SanitizeMentions(true));
var text = SmartText.CreateFrom(cr.Response);
text = await _repSvc.ReplaceAsync(text, repCtx);
return await _sender.Response(channel).Text(text).Sanitize(false).SendAsync();
}
public async Task ResetExprReactions(ulong? maybeGuildId, int id) public async Task ResetExprReactions(ulong? maybeGuildId, int id)
{ {
EllieExpression expr; EllieExpression expr;
@ -789,7 +838,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);

View file

@ -1,6 +1,7 @@
#nullable disable #nullable disable
using LinqToDB; using LinqToDB;
using LinqToDB.EntityFrameworkCore; using LinqToDB.EntityFrameworkCore;
using LinqToDB.Tools;
using EllieBot.Db.Models; using EllieBot.Db.Models;
using EllieBot.Modules.Gambling.Bank; using EllieBot.Modules.Gambling.Bank;
using EllieBot.Modules.Gambling.Common; using EllieBot.Modules.Gambling.Common;
@ -625,8 +626,6 @@ public partial class Gambling : GamblingModule<GamblingService>
var (opts, _) = OptionsParser.ParseFrom(new LbOpts(), args); var (opts, _) = OptionsParser.ParseFrom(new LbOpts(), args);
// List<DiscordUser> cleanRichest;
// it's pointless to have clean on dm context
if (ctx.Guild is null) if (ctx.Guild is null)
{ {
opts.Clean = false; opts.Clean = false;
@ -640,13 +639,18 @@ public partial class Gambling : GamblingModule<GamblingService>
await ctx.Channel.TriggerTypingAsync(); await ctx.Channel.TriggerTypingAsync();
await _tracker.EnsureUsersDownloadedAsync(ctx.Guild); await _tracker.EnsureUsersDownloadedAsync(ctx.Guild);
var users = ((SocketGuild)ctx.Guild).Users.Map(x => x.Id);
var perPage = 9;
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
var cleanRichest = await uow.GetTable<DiscordUser>()
.Where(x => x.UserId.In(users))
.OrderByDescending(x => x.CurrencyAmount)
.Skip(curPage * perPage)
.Take(perPage)
.ToListAsync();
var cleanRichest = await uow.Set<DiscordUser>() return cleanRichest;
.GetTopRichest(_client.CurrentUser.Id, 0, 1000);
var sg = (SocketGuild)ctx.Guild!;
return cleanRichest.Where(x => sg.GetUser(x.UserId) is not null).ToList();
} }
else else
{ {
@ -655,13 +659,9 @@ public partial class Gambling : GamblingModule<GamblingService>
} }
} }
var res = Response()
.Paginated();
await Response() await Response()
.Paginated() .Paginated()
.PageItems(GetTopRichest) .PageItems(GetTopRichest)
.TotalElements(900)
.PageSize(9) .PageSize(9)
.CurrentPage(page) .CurrentPage(page)
.Page((toSend, curPage) => .Page((toSend, curPage) =>

View file

@ -14,13 +14,13 @@ public class VoteModel
public class VoteRewardService : IEService, IReadyExecutor public class VoteRewardService : IEService, IReadyExecutor
{ {
private readonly DiscordSocketClient _client; private readonly DiscordSocketClient _client;
private readonly IBotCredentials _creds; private readonly IBotCreds _creds;
private readonly ICurrencyService _currencyService; private readonly ICurrencyService _currencyService;
private readonly GamblingConfigService _gamb; private readonly GamblingConfigService _gamb;
public VoteRewardService( public VoteRewardService(
DiscordSocketClient client, DiscordSocketClient client,
IBotCredentials creds, IBotCreds creds,
ICurrencyService currencyService, ICurrencyService currencyService,
GamblingConfigService gamb) GamblingConfigService gamb)
{ {

View file

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

View file

@ -15,7 +15,7 @@ public class WaifuService : IEService, IReadyExecutor
private readonly ICurrencyService _cs; private readonly ICurrencyService _cs;
private readonly IBotCache _cache; private readonly IBotCache _cache;
private readonly GamblingConfigService _gss; private readonly GamblingConfigService _gss;
private readonly IBotCredentials _creds; private readonly IBotCreds _creds;
private readonly DiscordSocketClient _client; private readonly DiscordSocketClient _client;
public WaifuService( public WaifuService(
@ -23,7 +23,7 @@ public class WaifuService : IEService, IReadyExecutor
ICurrencyService cs, ICurrencyService cs,
IBotCache cache, IBotCache cache,
GamblingConfigService gss, GamblingConfigService gss,
IBotCredentials creds, IBotCreds creds,
DiscordSocketClient client) DiscordSocketClient client)
{ {
_db = db; _db = db;
@ -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)

View file

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

View file

@ -19,7 +19,7 @@ public class ChatterBotService : IExecOnMessage
private readonly DiscordSocketClient _client; private readonly DiscordSocketClient _client;
private readonly IPermissionChecker _perms; private readonly IPermissionChecker _perms;
private readonly IBotCredentials _creds; private readonly IBotCreds _creds;
private readonly IHttpClientFactory _httpFactory; private readonly IHttpClientFactory _httpFactory;
private readonly GamesConfigService _gcs; private readonly GamesConfigService _gcs;
private readonly IMessageSenderService _sender; private readonly IMessageSenderService _sender;
@ -32,7 +32,7 @@ public class ChatterBotService : IExecOnMessage
IBot bot, IBot bot,
IPatronageService ps, IPatronageService ps,
IHttpClientFactory factory, IHttpClientFactory factory,
IBotCredentials creds, IBotCreds creds,
GamesConfigService gcs, GamesConfigService gcs,
IMessageSenderService sender, IMessageSenderService sender,
DbService db) DbService db)

View file

@ -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

View file

@ -12,9 +12,9 @@ public sealed partial class Music
{ {
private static readonly SemaphoreSlim _playlistLock = new(1, 1); private static readonly SemaphoreSlim _playlistLock = new(1, 1);
private readonly DbService _db; private readonly DbService _db;
private readonly IBotCredentials _creds; private readonly IBotCreds _creds;
public PlaylistCommands(DbService db, IBotCredentials creds) public PlaylistCommands(DbService db, IBotCreds creds)
{ {
_db = db; _db = db;
_creds = creds; _creds = creds;

View file

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

View file

@ -16,11 +16,11 @@ public class CryptoService : IEService
{ {
private readonly IBotCache _cache; private readonly IBotCache _cache;
private readonly IHttpClientFactory _httpFactory; private readonly IHttpClientFactory _httpFactory;
private readonly IBotCredentials _creds; private readonly IBotCreds _creds;
private readonly SemaphoreSlim _getCryptoLock = new(1, 1); private readonly SemaphoreSlim _getCryptoLock = new(1, 1);
public CryptoService(IBotCache cache, IHttpClientFactory httpFactory, IBotCredentials creds) public CryptoService(IBotCache cache, IHttpClientFactory httpFactory, IBotCreds creds)
{ {
_cache = cache; _cache = cache;
_httpFactory = httpFactory; _httpFactory = httpFactory;

View file

@ -9,10 +9,10 @@ public partial class Searches
[Group] [Group]
public partial class OsuCommands : EllieModule<OsuService> public partial class OsuCommands : EllieModule<OsuService>
{ {
private readonly IBotCredentials _creds; private readonly IBotCreds _creds;
private readonly IHttpClientFactory _httpFactory; private readonly IHttpClientFactory _httpFactory;
public OsuCommands(IBotCredentials creds, IHttpClientFactory factory) public OsuCommands(IBotCreds creds, IHttpClientFactory factory)
{ {
_creds = creds; _creds = creds;
_httpFactory = factory; _httpFactory = factory;

View file

@ -7,9 +7,9 @@ namespace EllieBot.Modules.Searches;
public sealed class OsuService : IEService public sealed class OsuService : IEService
{ {
private readonly IHttpClientFactory _httpFactory; private readonly IHttpClientFactory _httpFactory;
private readonly IBotCredentials _creds; private readonly IBotCreds _creds;
public OsuService(IHttpClientFactory httpFactory, IBotCredentials creds) public OsuService(IHttpClientFactory httpFactory, IBotCreds creds)
{ {
_httpFactory = httpFactory; _httpFactory = httpFactory;
_creds = creds; _creds = creds;

View file

@ -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()
}; };
} }

View file

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

View file

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

View file

@ -18,7 +18,7 @@ public sealed class InvidiousYtSearchService : IYoutubeSearchService, 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));
} }
} }

View file

@ -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
};
} }
} }

View file

@ -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));
}
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -13,14 +13,14 @@ namespace EllieBot.Modules.Searches;
public partial class Searches : EllieModule<SearchesService> public partial class Searches : EllieModule<SearchesService>
{ {
private readonly IBotCredentials _creds; private readonly IBotCreds _creds;
private readonly IGoogleApiService _google; private readonly IGoogleApiService _google;
private readonly IHttpClientFactory _httpFactory; private readonly IHttpClientFactory _httpFactory;
private readonly IMemoryCache _cache; private readonly IMemoryCache _cache;
private readonly ITimezoneService _tzSvc; private readonly ITimezoneService _tzSvc;
public Searches( public Searches(
IBotCredentials creds, IBotCreds creds,
IGoogleApiService google, IGoogleApiService google,
IHttpClientFactory factory, IHttpClientFactory factory,
IMemoryCache cache, IMemoryCache cache,

View file

@ -28,11 +28,9 @@ public partial class SearchesConfig : ICloneable<SearchesConfig>
[Comment(""" [Comment("""
Which search provider will be used for the `.youtube` and `.q` commands. Which search provider will be used for the `.youtube` and `.q` commands.
- `ytDataApiv3` - uses google's official youtube data api. Requires `GoogleApiKey` set in creds and youtube data api enabled in developers console - `ytDataApiv3` - uses google's official youtube data api. Requires `GoogleApiKey` set in creds and youtube data api enabled in developers console. `.q` is not supported for this setting. It will fallback to yt-dlp.
- `ytdl` - default, uses youtube-dl. Requires `youtube-dl` to be installed and it's path added to env variables. Slow. - `ytdlp` - default, recommended easy, uses `yt-dlp`. Requires `yt-dlp` to be installed and it's path added to env variables
- `ytdlp` - recommended easy, uses `yt-dlp`. Requires `yt-dlp` to be installed and it's path added to env variables
- `invidious` - recommended advanced, uses invidious api. Requires at least one invidious instance specified in the `invidiousInstances` property - `invidious` - recommended advanced, uses invidious api. Requires at least one invidious instance specified in the `invidiousInstances` property
""")] """)]
@ -77,9 +75,9 @@ public sealed class FollowedStreamConfig
public enum YoutubeSearcher public enum YoutubeSearcher
{ {
YtDataApiv3, YtDataApiv3 = 0,
Ytdl, Ytdl = 1,
Ytdlp, Ytdlp = 1,
Invid, Invid = 3,
Invidious = 3 Invidious = 3
} }

View file

@ -47,19 +47,11 @@ public class SearchesConfigService : ConfigServiceBase<SearchesConfig>
}); });
} }
if (data.Version < 2) if (data.Version < 4)
{ {
ModifyConfig(c => ModifyConfig(c =>
{ {
c.Version = 2; c.Version = 4;
});
}
if (data.Version < 3)
{
ModifyConfig(c =>
{
c.Version = 3;
}); });
} }
} }

View file

@ -11,7 +11,7 @@ public sealed class GiveawayService : IEService, IReadyExecutor
public static string GiveawayEmoji = "🎉"; public static string GiveawayEmoji = "🎉";
private readonly DbService _db; private readonly DbService _db;
private readonly IBotCredentials _creds; private readonly IBotCreds _creds;
private readonly DiscordSocketClient _client; private readonly DiscordSocketClient _client;
private readonly IMessageSenderService _sender; private readonly IMessageSenderService _sender;
private readonly IBotStrings _strings; private readonly IBotStrings _strings;
@ -20,7 +20,7 @@ public sealed class GiveawayService : IEService, IReadyExecutor
private SortedSet<GiveawayModel> _giveawayCache = new SortedSet<GiveawayModel>(); private SortedSet<GiveawayModel> _giveawayCache = new SortedSet<GiveawayModel>();
private readonly EllieRandom _rng; private readonly EllieRandom _rng;
public GiveawayService(DbService db, IBotCredentials creds, DiscordSocketClient client, public GiveawayService(DbService db, IBotCreds creds, DiscordSocketClient client,
IMessageSenderService sender, IBotStrings strings, ILocalization localization, IMemoryCache cache) IMessageSenderService sender, IBotStrings strings, ILocalization localization, IMemoryCache cache)
{ {
_db = db; _db = db;

View file

@ -17,14 +17,14 @@ public class RemindService : IEService, IReadyExecutor, IRemindService
private readonly DiscordSocketClient _client; private readonly DiscordSocketClient _client;
private readonly DbService _db; private readonly DbService _db;
private readonly IBotCredentials _creds; private readonly IBotCreds _creds;
private readonly IMessageSenderService _sender; private readonly IMessageSenderService _sender;
private readonly CultureInfo _culture; private readonly CultureInfo _culture;
public RemindService( public RemindService(
DiscordSocketClient client, DiscordSocketClient client,
DbService db, DbService db,
IBotCredentials creds, IBotCreds creds,
IMessageSenderService sender) IMessageSenderService sender)
{ {
_client = client; _client = client;

View file

@ -12,7 +12,7 @@ public sealed class RepeaterService : IReadyExecutor, IEService
private readonly DbService _db; private readonly DbService _db;
private readonly IReplacementService _repSvc; private readonly IReplacementService _repSvc;
private readonly IBotCredentials _creds; private readonly IBotCreds _creds;
private readonly DiscordSocketClient _client; private readonly DiscordSocketClient _client;
private readonly LinkedList<RunningRepeater> _repeaterQueue; private readonly LinkedList<RunningRepeater> _repeaterQueue;
private readonly ConcurrentHashSet<int> _noRedundant; private readonly ConcurrentHashSet<int> _noRedundant;
@ -25,7 +25,7 @@ public sealed class RepeaterService : IReadyExecutor, IEService
DiscordSocketClient client, DiscordSocketClient client,
DbService db, DbService db,
IReplacementService repSvc, IReplacementService repSvc,
IBotCredentials creds, IBotCreds creds,
IMessageSenderService sender) IMessageSenderService sender)
{ {
_db = db; _db = db;

View file

@ -34,7 +34,7 @@ public partial class Utility : EllieModule
private readonly DiscordSocketClient _client; private readonly DiscordSocketClient _client;
private readonly ICoordinator _coord; private readonly ICoordinator _coord;
private readonly IStatsService _stats; private readonly IStatsService _stats;
private readonly IBotCredentials _creds; private readonly IBotCreds _creds;
private readonly DownloadTracker _tracker; private readonly DownloadTracker _tracker;
private readonly IHttpClientFactory _httpFactory; private readonly IHttpClientFactory _httpFactory;
private readonly VerboseErrorsService _veService; private readonly VerboseErrorsService _veService;
@ -45,7 +45,7 @@ public partial class Utility : EllieModule
DiscordSocketClient client, DiscordSocketClient client,
ICoordinator coord, ICoordinator coord,
IStatsService stats, IStatsService stats,
IBotCredentials creds, IBotCreds creds,
DownloadTracker tracker, DownloadTracker tracker,
IHttpClientFactory httpFactory, IHttpClientFactory httpFactory,
VerboseErrorsService veService, VerboseErrorsService veService,

View file

@ -183,27 +183,26 @@ public partial class Xp : EllieModule<XpService>
await ctx.Channel.TriggerTypingAsync(); await ctx.Channel.TriggerTypingAsync();
async Task<IReadOnlyCollection<UserXpStats>> GetPageItems(int curPage)
{
var socketGuild = (SocketGuild)ctx.Guild; var socketGuild = (SocketGuild)ctx.Guild;
var allCleanUsers = new List<UserXpStats>();
if (opts.Clean) if (opts.Clean)
{ {
await ctx.Channel.TriggerTypingAsync(); await ctx.Channel.TriggerTypingAsync();
await _tracker.EnsureUsersDownloadedAsync(ctx.Guild); await _tracker.EnsureUsersDownloadedAsync(ctx.Guild);
allCleanUsers = (await _service.GetTopUserXps(ctx.Guild.Id, 1000)) return await _service.GetTopUserXps(ctx.Guild.Id,
.Where(user => socketGuild.GetUser(user.UserId) is not null) socketGuild.Users.Select(x => x.Id).ToList(),
.ToList(); curPage);
} }
var res = opts.Clean return await _service.GetUserXps(ctx.Guild.Id, curPage);
? Response() }
.Paginated()
.Items(allCleanUsers)
: Response()
.Paginated()
.PageItems((curPage) => _service.GetUserXps(ctx.Guild.Id, curPage));
await res await Response()
.Paginated()
.PageItems(GetPageItems)
.PageSize(9) .PageSize(9)
.CurrentPage(page) .CurrentPage(page)
.Page((users, curPage) => .Page((users, curPage) =>

View file

@ -12,6 +12,7 @@ using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
using System.Threading.Channels; using System.Threading.Channels;
using LinqToDB.EntityFrameworkCore; using LinqToDB.EntityFrameworkCore;
using LinqToDB.Tools;
using EllieBot.Modules.Patronage; using EllieBot.Modules.Patronage;
using Color = SixLabors.ImageSharp.Color; using Color = SixLabors.ImageSharp.Color;
using Exception = System.Exception; using Exception = System.Exception;
@ -25,7 +26,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
private readonly IImageCache _images; private readonly IImageCache _images;
private readonly IBotStrings _strings; private readonly IBotStrings _strings;
private readonly FontProvider _fonts; private readonly FontProvider _fonts;
private readonly IBotCredentials _creds; private readonly IBotCreds _creds;
private readonly ICurrencyService _cs; private readonly ICurrencyService _cs;
private readonly IHttpClientFactory _httpFactory; private readonly IHttpClientFactory _httpFactory;
private readonly XpConfigService _xpConfig; private readonly XpConfigService _xpConfig;
@ -55,7 +56,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
IImageCache images, IImageCache images,
IBotCache c, IBotCache c,
FontProvider fonts, FontProvider fonts,
IBotCredentials creds, IBotCreds creds,
ICurrencyService cs, ICurrencyService cs,
IHttpClientFactory http, IHttpClientFactory http,
XpConfigService xpConfig, XpConfigService xpConfig,
@ -566,13 +567,24 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
public async Task<IReadOnlyCollection<UserXpStats>> GetUserXps(ulong guildId, int page) public async Task<IReadOnlyCollection<UserXpStats>> GetUserXps(ulong guildId, int page)
{ {
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
return await uow.Set<UserXpStats>().GetUsersFor(guildId, page); return await uow
.UserXpStats
.Where(x => x.GuildId == guildId)
.OrderByDescending(x => x.Xp + x.AwardedXp)
.Skip(page * 9)
.Take(9)
.ToArrayAsyncLinqToDB();
} }
public async Task<IReadOnlyCollection<UserXpStats>> GetTopUserXps(ulong guildId, int count) public async Task<IReadOnlyCollection<UserXpStats>> GetTopUserXps(ulong guildId, List<ulong> users, int curPage)
{ {
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
return await uow.Set<UserXpStats>().GetTopUserXps(guildId, count); return await uow.Set<UserXpStats>()
.Where(x => x.GuildId == guildId && x.UserId.In(users))
.OrderByDescending(x => x.Xp + x.AwardedXp)
.Skip(curPage * 9)
.Take(9)
.ToArrayAsyncLinqToDB();
} }
public Task<IReadOnlyCollection<DiscordUser>> GetUserXps(int page, int perPage = 9) public Task<IReadOnlyCollection<DiscordUser>> GetUserXps(int page, int perPage = 9)

View file

@ -1,6 +1,4 @@
var pid = Environment.ProcessId; var shardId = 0;
var shardId = 0;
int? totalShards = null; // 0 to read from creds.yml int? totalShards = null; // 0 to read from creds.yml
if (args.Length > 0 && args[0] != "run") if (args.Length > 0 && args[0] != "run")
{ {
@ -22,7 +20,5 @@ if (args.Length > 0 && args[0] != "run")
} }
} }
LogSetup.SetupLogger(shardId);
Log.Information("Pid: {ProcessId}", pid);
await new Bot(shardId, totalShards, Environment.GetEnvironmentVariable("EllieBot__creds")).RunAndBlockAsync(); await new Bot(shardId, totalShards, Environment.GetEnvironmentVariable("EllieBot__creds")).RunAndBlockAsync();

View file

@ -0,0 +1,76 @@
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;
}
[GrpcApiPerm(GuildPerm.Administrator)]
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,
};
}
[GrpcApiPerm(GuildPerm.Administrator)]
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;
}
[GrpcApiPerm(GuildPerm.Administrator)]
public override async Task<Empty> DeleteExpr(DeleteExprRequest request, ServerCallContext context)
{
await _svc.DeleteAsync(request.GuildId, new kwum(request.Id));
return new Empty();
}
}

View file

@ -0,0 +1,124 @@
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,
};
}
[GrpcApiPerm(GuildPerm.Administrator)]
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)
};
}
[GrpcApiPerm(GuildPerm.Administrator)]
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
};
}
[GrpcApiPerm(GuildPerm.Administrator)]
public override Task<TestGreetReply> TestGreet(TestGreetRequest request, ServerCallContext context)
=> TestGreet(request.GuildId, request.ChannelId, request.UserId, request.Type);
private async Task<TestGreetReply> TestGreet(
ulong guildId,
ulong channelId,
ulong userId,
GrpcGreetType gtDto)
{
var g = _client.GetGuild(guildId) as IGuild;
if (g is null)
{
return new()
{
Error = "Guild doesn't exist",
Success = false,
};
}
var gu = await g.GetUserAsync(userId);
var ch = await g.GetTextChannelAsync(channelId);
if (gu is null || ch is null)
return new TestGreetReply()
{
Error = "Guild or channel doesn't exist",
Success = false,
};
var gt = GetGreetType(gtDto);
await _gs.Test(guildId, gt, ch, gu);
return new TestGreetReply()
{
Success = true
};
}
private static GreetType GetGreetType(GrpcGreetType gtDto)
{
return gtDto switch
{
GrpcGreetType.Greet => GreetType.Greet,
GrpcGreetType.GreetDm => GreetType.GreetDm,
GrpcGreetType.Bye => GreetType.Bye,
GrpcGreetType.Boost => GreetType.Boost,
_ => throw new ArgumentOutOfRangeException(nameof(gtDto), gtDto, null)
};
}
}

View file

@ -0,0 +1,199 @@
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using EllieBot.Modules.Gambling.Services;
using EllieBot.Modules.Xp.Services;
namespace EllieBot.GrpcApi;
public static class GrpcApiExtensions
{
public static ulong GetUserId(this ServerCallContext context)
=> ulong.Parse(context.RequestHeaders.FirstOrDefault(x => x.Key == "userid")!.Value);
}
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;
private readonly IStatsService _stats;
public OtherSvc(
DiscordSocketClient client,
XpService xp,
ICurrencyService cur,
WaifuService waifus,
ICoordinator coord,
IStatsService stats)
{
_client = client;
_xp = xp;
_cur = cur;
_waifus = waifus;
_coord = coord;
_stats = stats;
}
public override async Task<GetGuildsReply> GetGuilds(Empty request, ServerCallContext context)
{
var guilds = await _client.GetGuildsAsync(CacheMode.CacheOnly);
var reply = new GetGuildsReply();
var userId = context.GetUserId();
var toReturn = new List<IGuild>();
foreach (var g in guilds)
{
var user = await g.GetUserAsync(userId, CacheMode.AllowDownload);
if (user.GuildPermissions.Has(GuildPermission.Administrator))
toReturn.Add(g);
}
reply.Guilds.AddRange(toReturn
.Select(x => new GuildReply()
{
Id = x.Id,
Name = x.Name,
IconUrl = x.IconUrl
}));
return reply;
}
[GrpcApiPerm(GuildPerm.Administrator)]
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() ?? x.Username,
UserId = x.UserId,
Avatar = user?.RealAvatarUrl().ToString() ?? x.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 ?? string.Empty,
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();
// todo cache
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);
}
[GrpcApiPerm(GuildPerm.Administrator)]
public override async Task<GetServerInfoReply> GetServerInfo(ServerInfoRequest request, ServerCallContext context)
{
var info = await _stats.GetGuildInfoAsync(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 reply;
}
}

View file

@ -0,0 +1,69 @@
using Grpc.Core;
using Grpc.Core.Interceptors;
using EllieBot.Common.ModuleBehaviors;
namespace EllieBot.GrpcApi;
public class GrpcApiService : IEService, IReadyExecutor
{
private Server? _app;
private readonly DiscordSocketClient _client;
private readonly OtherSvc _other;
private readonly ExprsSvc _exprs;
private readonly GreetByeSvc _greet;
private readonly IBotCredsProvider _creds;
public GrpcApiService(
DiscordSocketClient client,
OtherSvc other,
ExprsSvc exprs,
GreetByeSvc greet,
IBotCredsProvider creds)
{
_client = client;
_other = other;
_exprs = exprs;
_greet = greet;
_creds = creds;
}
public Task OnReadyAsync()
{
var creds = _creds.GetCreds();
if (creds.GrpcApi is null || !creds.GrpcApi.Enabled)
return Task.CompletedTask;
try
{
var host = creds.GrpcApi.Host;
var port = creds.GrpcApi.Port + _client.ShardId;
var interceptor = new PermsInterceptor(_client);
_app = new Server()
{
Services =
{
GrpcOther.BindService(_other).Intercept(interceptor),
GrpcExprs.BindService(_exprs).Intercept(interceptor),
GrpcGreet.BindService(_greet).Intercept(interceptor),
},
Ports =
{
new(host, port, ServerCredentials.Insecure),
}
};
_app.Start();
Log.Information("Grpc Api Server started on port {Host}:{Port}", host, port);
}
catch
{
_app?.ShutdownAsync().GetAwaiter().GetResult();
}
return Task.CompletedTask;
}
}

View file

@ -0,0 +1,67 @@
using Grpc.Core;
using Grpc.Core.Interceptors;
namespace EllieBot.GrpcApi;
public sealed partial class PermsInterceptor : Interceptor
{
private readonly DiscordSocketClient _client;
public PermsInterceptor(DiscordSocketClient client)
{
_client = client;
Log.Information("interceptor created");
}
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
try
{
Log.Information("Starting receiving call. Type/Method: {Type} / {Method}",
MethodType.Unary,
context.Method);
// get metadata
var metadata = context
.RequestHeaders
.ToDictionary(x => x.Key, x => x.Value);
var method = context.Method[(context.Method.LastIndexOf('/') + 1)..];
if (perms.TryGetValue(method, out var perm))
{
Log.Information("Required permission for {Method} is {Perm}",
method,
perm);
var userId = ulong.Parse(metadata["userid"]);
var guildId = ulong.Parse(metadata["guildid"]);
IGuild guild = _client.GetGuild(guildId);
var user = guild is null ? null : await guild.GetUserAsync(userId);
if (user is null)
throw new RpcException(new Status(StatusCode.NotFound, "User not found"));
if (!user.GuildPermissions.Has(perm))
throw new RpcException(new Status(StatusCode.PermissionDenied,
$"You need {perm} permission to use this method"));
}
else
{
Log.Information("No permission required for {Method}", method);
}
return await continuation(request, context);
}
catch (Exception ex)
{
Log.Error(ex, "Error thrown by {ContextMethod}", context.Method);
throw;
}
}
}

View file

@ -6,9 +6,9 @@ namespace Ellie.Common;
public static class LogSetup public static class LogSetup
{ {
public static void SetupLogger(object source) public static void SetupLogger(object source, IBotCreds creds)
{ {
Log.Logger = new LoggerConfiguration().MinimumLevel.Override("Microsoft", LogEventLevel.Information) var config = new LoggerConfiguration().MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.MinimumLevel.Override("System", LogEventLevel.Information) .MinimumLevel.Override("System", LogEventLevel.Information)
.MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning) .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
.Enrich.FromLogContext() .Enrich.FromLogContext()
@ -16,7 +16,12 @@ public static class LogSetup
theme: GetTheme(), theme: GetTheme(),
outputTemplate: outputTemplate:
"[{Timestamp:HH:mm:ss} {Level:u3}] | #{LogSource} | {Message:lj}{NewLine}{Exception}") "[{Timestamp:HH:mm:ss} {Level:u3}] | #{LogSource} | {Message:lj}{NewLine}{Exception}")
.Enrich.WithProperty("LogSource", source) .Enrich.WithProperty("LogSource", source);
if (!string.IsNullOrWhiteSpace(creds.Seq.Url))
config = config.WriteTo.Seq(creds.Seq.Url, apiKey: creds.Seq.ApiKey);
Log.Logger = config
.CreateLogger(); .CreateLogger();
Console.OutputEncoding = Encoding.UTF8; Console.OutputEncoding = Encoding.UTF8;

View file

@ -1,7 +1,7 @@
#nullable disable #nullable disable
namespace EllieBot; namespace EllieBot;
public interface IBotCredentials public interface IBotCreds
{ {
string Token { get; } string Token { get; }
string EllieAiToken { get; } string EllieAiToken { get; }
@ -29,6 +29,8 @@ public interface IBotCredentials
string TwitchClientSecret { get; set; } string TwitchClientSecret { get; set; }
GoogleApiConfig Google { get; set; } GoogleApiConfig Google { get; set; }
BotCacheImplemenation BotCache { get; set; } BotCacheImplemenation BotCache { get; set; }
Creds.GrpcApiConfig GrpcApi { get; set; }
SeqConfig Seq { get; set; }
} }
public interface IVotesSettings public interface IVotesSettings

View file

@ -3,6 +3,6 @@
public interface IBotCredsProvider public interface IBotCredsProvider
{ {
public void Reload(); public void Reload();
public IBotCredentials GetCreds(); public IBotCreds GetCreds();
public void ModifyCredsFile(Action<IBotCredentials> func); public void ModifyCredsFile(Action<IBotCreds> func);
} }

View file

@ -28,7 +28,7 @@ public sealed partial class BotConfig : ICloneable<BotConfig>
public CultureInfo DefaultLocale { get; set; } public CultureInfo DefaultLocale { get; set; }
[Comment(""" [Comment("""
Style in which executed commands will show up in the console. Style in which executed commands will show up in the logs.
Allowed values: Simple, Normal, None Allowed values: Simple, Normal, None
""")] """)]
public ConsoleOutputType ConsoleOutputType { get; set; } public ConsoleOutputType ConsoleOutputType { get; set; }

View file

@ -3,10 +3,10 @@ using EllieBot.Common.Yml;
namespace EllieBot.Common; namespace EllieBot.Common;
public sealed class Creds : IBotCredentials public sealed class Creds : IBotCreds
{ {
[Comment("""DO NOT CHANGE""")] [Comment("""DO NOT CHANGE""")]
public int Version { get; set; } public int Version { get; set; } = 12;
[Comment("""Bot token. Do not share with anyone ever -> https://discordapp.com/developers/applications/""")] [Comment("""Bot token. Do not share with anyone ever -> https://discordapp.com/developers/applications/""")]
public string Token { get; set; } public string Token { get; set; }
@ -17,7 +17,8 @@ public sealed class Creds : IBotCredentials
""")] """)]
public ICollection<ulong> OwnerIds { get; set; } public ICollection<ulong> OwnerIds { get; set; }
[Comment("Keep this on 'true' unless you're sure your bot shouldn't use privileged intents or you're waiting to be accepted")] [Comment(
"Keep this on 'true' unless you're sure your bot shouldn't use privileged intents or you're waiting to be accepted")]
public bool UsePrivilegedIntents { get; set; } public bool UsePrivilegedIntents { get; set; }
[Comment(""" [Comment("""
@ -155,9 +156,21 @@ public sealed class Creds : IBotCredentials
""")] """)]
public RestartConfig RestartCommand { get; set; } public RestartConfig RestartCommand { get; set; }
[Comment("""
Settings for the grpc api.
We don't provide support for this.
If you leave certPath empty, the api will run on http.
""")]
public GrpcApiConfig GrpcApi { get; set; }
[Comment("""
Url and api key to a seq server. If url is set, bot will try to send logs to it.
""")]
public SeqConfig Seq { 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,6 +193,9 @@ public sealed class Creds : IBotCredentials
RestartCommand = new RestartConfig(); RestartCommand = new RestartConfig();
Google = new GoogleApiConfig(); Google = new GoogleApiConfig();
GrpcApi = new();
Seq = new();
} }
public class DbOptions public class DbOptions
@ -273,6 +289,21 @@ public sealed class Creds : IBotCredentials
DiscordsKey = discordsKey; DiscordsKey = discordsKey;
} }
} }
public sealed record GrpcApiConfig
{
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 sealed class SeqConfig
{
public string Url { get; init; }
public string ApiKey { get; init; }
} }
public class GoogleApiConfig : IGoogleApiConfig public class GoogleApiConfig : IGoogleApiConfig
@ -280,6 +311,3 @@ public class GoogleApiConfig : IGoogleApiConfig
public string SearchId { get; init; } public string SearchId { get; init; }
public string ImageSearchId { get; init; } public string ImageSearchId { get; init; }
} }

View file

@ -14,6 +14,7 @@ public class DownloadTracker : IEService
public async Task EnsureUsersDownloadedAsync(IGuild guild) public async Task EnsureUsersDownloadedAsync(IGuild guild)
{ {
#if GLOBAL_ELLIE #if GLOBAL_ELLIE
await Task.CompletedTask;
return; return;
#endif #endif
await _downloadUsersSemaphore.WaitAsync(); await _downloadUsersSemaphore.WaitAsync();

View file

@ -119,7 +119,7 @@ public sealed class BotCredsProvider : IBotCredsProvider
} }
} }
public void ModifyCredsFile(Action<IBotCredentials> func) public void ModifyCredsFile(Action<IBotCreds> func)
{ {
var ymlData = File.ReadAllText(CREDS_FILE_NAME); var ymlData = File.ReadAllText(CREDS_FILE_NAME);
var creds = Yaml.Deserializer.Deserialize<Creds>(ymlData); var creds = Yaml.Deserializer.Deserialize<Creds>(ymlData);
@ -137,24 +137,18 @@ public sealed class BotCredsProvider : IBotCredsProvider
var creds = Yaml.Deserializer.Deserialize<Creds>(File.ReadAllText(CREDS_FILE_NAME)); var creds = Yaml.Deserializer.Deserialize<Creds>(File.ReadAllText(CREDS_FILE_NAME));
if (creds.Version <= 5) if (creds.Version <= 5)
{ {
creds.BotCache = BotCacheImplemenation.Redis; creds.BotCache = BotCacheImplemenation.Memory;
} }
if (creds.Version <= 6) if (creds.Version < 12)
{ {
creds.Version = 7; creds.Version = 12;
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));
} }
} }
} }
public IBotCredentials GetCreds() public IBotCreds GetCreds()
{ {
lock (_reloadLock) lock (_reloadLock)
{ {

View file

@ -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(

View file

@ -4,11 +4,11 @@ namespace EllieBot.Common;
public sealed class RedisPubSub : IPubSub public sealed class RedisPubSub : IPubSub
{ {
private readonly IBotCredentials _creds; private readonly IBotCreds _creds;
private readonly ConnectionMultiplexer _multi; private readonly ConnectionMultiplexer _multi;
private readonly ISeria _serializer; private readonly ISeria _serializer;
public RedisPubSub(ConnectionMultiplexer multi, ISeria serializer, IBotCredentials creds) public RedisPubSub(ConnectionMultiplexer multi, ISeria serializer, IBotCreds creds)
{ {
_multi = multi; _multi = multi;
_serializer = serializer; _serializer = serializer;

View file

@ -15,13 +15,13 @@ public class RedisBotStringsProvider : IBotStringsProvider
private readonly ConnectionMultiplexer _redis; private readonly ConnectionMultiplexer _redis;
private readonly IStringsSource _source; private readonly IStringsSource _source;
private readonly IBotCredentials _creds; private readonly IBotCreds _creds;
public RedisBotStringsProvider( public RedisBotStringsProvider(
ConnectionMultiplexer redis, ConnectionMultiplexer redis,
DiscordSocketClient discordClient, DiscordSocketClient discordClient,
IStringsSource source, IStringsSource source,
IBotCredentials creds) IBotCreds creds)
{ {
_redis = redis; _redis = redis;
_source = source; _source = source;

View file

@ -11,7 +11,7 @@ public class RemoteGrpcCoordinator : ICoordinator, IReadyExecutor
private readonly Coordinator.Coordinator.CoordinatorClient _coordClient; private readonly Coordinator.Coordinator.CoordinatorClient _coordClient;
private readonly DiscordSocketClient _client; private readonly DiscordSocketClient _client;
public RemoteGrpcCoordinator(IBotCredentials creds, DiscordSocketClient client) public RemoteGrpcCoordinator(IBotCreds creds, DiscordSocketClient client)
{ {
var coordUrl = string.IsNullOrWhiteSpace(creds.CoordinatorUrl) ? "http://localhost:3442" : creds.CoordinatorUrl; var coordUrl = string.IsNullOrWhiteSpace(creds.CoordinatorUrl) ? "http://localhost:3442" : creds.CoordinatorUrl;
@ -90,8 +90,7 @@ public class RemoteGrpcCoordinator : ICoordinator, IReadyExecutor
{ {
if (!gracefulImminent) if (!gracefulImminent)
{ {
Log.Warning(ex, Log.Warning(ex, "Hearbeat failed and graceful shutdown was not expected: {Message}",
"Hearbeat failed and graceful shutdown was not expected: {Message}",
ex.Message); ex.Message);
break; break;
} }

View file

@ -1,4 +1,3 @@
#nullable disable
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace EllieBot.Common; namespace EllieBot.Common;
@ -66,13 +65,15 @@ public sealed partial class ReplacementPatternStore
Register("%user.mention%", static (IUser user) => user.Mention); Register("%user.mention%", static (IUser user) => user.Mention);
Register("%user.fullname%", static (IUser user) => user.ToString()!); Register("%user.fullname%", static (IUser user) => user.ToString()!);
Register("%user.name%", static (IUser user) => user.Username); Register("%user.name%", static (IUser user) => user.Username);
Register("%user.displayname%", static (IUser user) => user is IGuildUser gu ? gu.DisplayName : user.Username);
Register("%user.discrim%", static (IUser user) => user.Discriminator); Register("%user.discrim%", static (IUser user) => user.Discriminator);
Register("%user.avatar%", static (IUser user) => user.RealAvatarUrl().ToString()); Register("%user.avatar%", static (IUser user) => user.RealAvatarUrl().ToString());
Register("%user.id%", static (IUser user) => user.Id.ToString()); Register("%user.id%", static (IUser user) => user.Id.ToString());
Register("%user.created_time%", static (IUser user) => user.CreatedAt.ToString("HH:mm")); Register("%user.created_time%", static (IUser user) => user.CreatedAt.ToString("HH:mm"));
Register("%user.created_date%", static (IUser user) => user.CreatedAt.ToString("dd.MM.yyyy")); Register("%user.created_date%", static (IUser user) => user.CreatedAt.ToString("dd.MM.yyyy"));
Register("%user.joined_time%", static (IGuildUser user) => user.JoinedAt?.ToString("HH:mm")); Register("%user.joined_time%", static (IGuildUser user) => user.JoinedAt?.ToString("HH:mm") ?? "??:??");
Register("%user.joined_date%", static (IGuildUser user) => user.JoinedAt?.ToString("dd.MM.yyyy")); Register("%user.joined_date%",
static (IGuildUser user) => user.JoinedAt?.ToString("dd.MM.yyyy") ?? "??.??.????");
Register("%user%", Register("%user%",
static (IUser[] users) => string.Join(" ", users.Select(user => user.Mention))); static (IUser[] users) => string.Join(" ", users.Select(user => user.Mention)));

View file

@ -61,7 +61,7 @@ public static class ServiceCollectionExtensions
return svcs; return svcs;
} }
public static IContainer AddCache(this IContainer cont, IBotCredentials creds) public static IContainer AddCache(this IContainer cont, IBotCreds creds)
{ {
if (creds.BotCache == BotCacheImplemenation.Redis) if (creds.BotCache == BotCacheImplemenation.Redis)
{ {

View file

@ -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);

View file

@ -49,8 +49,8 @@ public interface IStatsService
/// </summary> /// </summary>
double GetPrivateMemoryMegabytes(); double GetPrivateMemoryMegabytes();
GuildInfo GetGuildInfo(string name); GuildInfo GetGuildInfoAsync(string name);
GuildInfo GetGuildInfo(ulong id); Task<GuildInfo> GetGuildInfoAsync(ulong id);
} }
public record struct GuildInfo public record struct GuildInfo

View file

@ -14,12 +14,12 @@ public sealed class BlacklistService : IExecOnMessage
private readonly DbService _db; private readonly DbService _db;
private readonly IPubSub _pubSub; private readonly IPubSub _pubSub;
private readonly IBotCredentials _creds; private readonly IBotCreds _creds;
private IReadOnlyList<BlacklistEntry> blacklist; private IReadOnlyList<BlacklistEntry> blacklist;
private readonly TypedKey<BlacklistEntry[]> _blPubKey = new("blacklist.reload"); private readonly TypedKey<BlacklistEntry[]> _blPubKey = new("blacklist.reload");
public BlacklistService(DbService db, IPubSub pubSub, IBotCredentials creds) public BlacklistService(DbService db, IPubSub pubSub, IBotCreds creds)
{ {
_db = db; _db = db;
_pubSub = pubSub; _pubSub = pubSub;

View file

@ -5,10 +5,10 @@ namespace EllieBot.Services;
public class SingleProcessCoordinator : ICoordinator public class SingleProcessCoordinator : ICoordinator
{ {
private readonly IBotCredentials _creds; private readonly IBotCreds _creds;
private readonly DiscordSocketClient _client; private readonly DiscordSocketClient _client;
public SingleProcessCoordinator(IBotCredentials creds, DiscordSocketClient client) public SingleProcessCoordinator(IBotCreds creds, DiscordSocketClient client)
{ {
_creds = creds; _creds = creds;
_client = client; _client = client;

View file

@ -29,7 +29,7 @@ public sealed class StatsService : IStatsService, IReadyExecutor, IEService
private readonly Process _currentProcess = Process.GetCurrentProcess(); private readonly Process _currentProcess = Process.GetCurrentProcess();
private readonly DiscordSocketClient _client; private readonly DiscordSocketClient _client;
private readonly IBotCredentials _creds; private readonly IBotCreds _creds;
private readonly DateTime _started; private readonly DateTime _started;
private long textChannels; private long textChannels;
@ -42,7 +42,7 @@ public sealed class StatsService : IStatsService, IReadyExecutor, IEService
public StatsService( public StatsService(
DiscordSocketClient client, DiscordSocketClient client,
CommandHandler cmdHandler, CommandHandler cmdHandler,
IBotCredentials creds, IBotCreds creds,
IHttpClientFactory factory) IHttpClientFactory factory)
{ {
_client = client; _client = client;
@ -180,19 +180,20 @@ public sealed class StatsService : IStatsService, IReadyExecutor, IEService
return _currentProcess.PrivateMemorySize64 / 1.Megabytes(); return _currentProcess.PrivateMemorySize64 / 1.Megabytes();
} }
public GuildInfo GetGuildInfo(string name) public GuildInfo GetGuildInfoAsync(string name)
=> throw new NotImplementedException(); => throw new NotImplementedException();
public GuildInfo GetGuildInfo(ulong id) public async Task<GuildInfo> GetGuildInfoAsync(ulong id)
{ {
var g = _client.GetGuild(id); var g = _client.GetGuild(id);
var ig = (IGuild)g;
return new GuildInfo() return new GuildInfo()
{ {
Id = g.Id, Id = g.Id,
IconUrl = g.IconUrl, IconUrl = g.IconUrl,
Name = g.Name, Name = g.Name,
Owner = g.Owner.Username, Owner = (await ig.GetUserAsync(g.OwnerId))?.Username ?? "Unknown",
OwnerId = g.OwnerId, OwnerId = g.OwnerId,
CreatedAt = g.CreatedAt.UtcDateTime, CreatedAt = g.CreatedAt.UtcDateTime,
VoiceChannels = g.VoiceChannels.Count, VoiceChannels = g.VoiceChannels.Count,

View file

@ -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;
} }
} }

View file

@ -2,9 +2,9 @@ namespace EllieBot.Extensions;
public static class BotCredentialsExtensions public static class BotCredentialsExtensions
{ {
public static bool IsOwner(this IBotCredentials creds, IUser user) public static bool IsOwner(this IBotCreds creds, IUser user)
=> creds.IsOwner(user.Id); => creds.IsOwner(user.Id);
public static bool IsOwner(this IBotCredentials creds, ulong userId) public static bool IsOwner(this IBotCreds creds, ulong userId)
=> creds.OwnerIds.Contains(userId); => creds.OwnerIds.Contains(userId);
} }

View file

@ -103,7 +103,7 @@ public static class Extensions
/// <summary> /// <summary>
/// First 10 characters of teh bot token. /// First 10 characters of teh bot token.
/// </summary> /// </summary>
public static string RedisKey(this IBotCredentials bc) public static string RedisKey(this IBotCreds bc)
=> bc.Token[..10]; => bc.Token[..10];
public static bool IsAuthor(this IMessage msg, IDiscordClient client) public static bool IsAuthor(this IMessage msg, IDiscordClient client)

View file

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

View file

@ -1,5 +1,5 @@
# DO NOT CHANGE # DO NOT CHANGE
version: 3 version: 4
# Which engine should .search command # Which engine should .search command
# 'google_scrape' - default. Scrapes the webpage for results. May break. Requires no api keys. # 'google_scrape' - default. Scrapes the webpage for results. May break. Requires no api keys.
# 'google' - official google api. Requires googleApiKey and google.searchId set in creds.yml # 'google' - official google api. Requires googleApiKey and google.searchId set in creds.yml
@ -11,14 +11,12 @@ webSearchEngine: Google_Scrape
imgSearchEngine: Google imgSearchEngine: Google
# Which search provider will be used for the `.youtube` and `.q` commands. # Which search provider will be used for the `.youtube` and `.q` commands.
# #
# - `ytDataApiv3` - uses google's official youtube data api. Requires `GoogleApiKey` set in creds and youtube data api enabled in developers console # - `ytDataApiv3` - uses google's official youtube data api. Requires `GoogleApiKey` set in creds and youtube data api enabled in developers console. `.q` is not supported for this setting. It will fallback to yt-dlp.
# #
# - `ytdl` - default, uses youtube-dl. Requires `youtube-dl` to be installed and it's path added to env variables. Slow. # - `ytdlp` - default, recommended easy, uses `yt-dlp`. Requires `yt-dlp` to be installed and it's path added to env variables
#
# - `ytdlp` - recommended easy, uses `yt-dlp`. Requires `yt-dlp` to be installed and it's path added to env variables
# #
# - `invidious` - recommended advanced, uses invidious api. Requires at least one invidious instance specified in the `invidiousInstances` property # - `invidious` - recommended advanced, uses invidious api. Requires at least one invidious instance specified in the `invidiousInstances` property
ytProvider: Ytdlp ytProvider: Ytdl
# Set the searx instance urls in case you want to use 'searx' for either img or web search. # Set the searx instance urls in case you want to use 'searx' for either img or web search.
# Ellie will use a random one for each request. # Ellie will use a random one for each request.
# Use a fully qualified url. Example: `https://my-searx-instance.mydomain.com` # Use a fully qualified url. Example: `https://my-searx-instance.mydomain.com`