Compare commits

..

No commits in common. "v5" and "5.1.10" have entirely different histories.
v5 ... 5.1.10

87 changed files with 580 additions and 1682 deletions

View file

@ -2,54 +2,6 @@
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
### Fixed

View file

@ -30,8 +30,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ellie.Marmalade", "src\Elli
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EllieBot.Voice", "src\EllieBot.Voice\EllieBot.Voice.csproj", "{1D93CE3C-80B4-49C7-A9A2-99988920AAEC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EllieBot.GrpcApiBase", "src\EllieBot.GrpcApiBase\EllieBot.GrpcApiBase.csproj", "{3B71F0BF-AE6C-480C-AB88-FCE23EDC7D91}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -66,10 +64,6 @@ Global
{1D93CE3C-80B4-49C7-A9A2-99988920AAEC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1D93CE3C-80B4-49C7-A9A2-99988920AAEC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1D93CE3C-80B4-49C7-A9A2-99988920AAEC}.Release|Any CPU.Build.0 = Release|Any CPU
{3B71F0BF-AE6C-480C-AB88-FCE23EDC7D91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3B71F0BF-AE6C-480C-AB88-FCE23EDC7D91}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3B71F0BF-AE6C-480C-AB88-FCE23EDC7D91}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3B71F0BF-AE6C-480C-AB88-FCE23EDC7D91}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -82,7 +76,6 @@ Global
{F1A77F56-71B0-430E-AE46-94CDD7D43874} = {B28FB883-9688-41EB-BF5A-945F4A4EB628}
{76AC715D-12FF-4CBE-9585-A861139A2D0C} = {B28FB883-9688-41EB-BF5A-945F4A4EB628}
{1D93CE3C-80B4-49C7-A9A2-99988920AAEC} = {B28FB883-9688-41EB-BF5A-945F4A4EB628}
{3B71F0BF-AE6C-480C-AB88-FCE23EDC7D91} = {B28FB883-9688-41EB-BF5A-945F4A4EB628}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {79F61C2C-CDBB-4361-A234-91A0B334CFE4}

View file

@ -9,7 +9,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" 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" />
</ItemGroup>

View file

@ -1,147 +0,0 @@
#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

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

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

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

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

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

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

View file

@ -26,6 +26,17 @@ public static class UserXpExtensions
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)
=> await xps.ToLinqToDBTable()
.Where(x => x.GuildId == guildId)

View file

@ -4,7 +4,7 @@
<Nullable>enable</Nullable>
<ImplicitUsings>true</ImplicitUsings>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<Version>5.1.14</Version>
<Version>5.1.10</Version>
<!-- Output/build -->
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
@ -34,12 +34,13 @@
<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.Customsearch.v1" Version="1.49.0.2084" />
<PackageReference Include="Google.Protobuf" Version="3.28.2" />
<PackageReference Include="Grpc" Version="2.46.6" />
<PackageReference Include="Grpc.Net.Client" Version="2.62.0" />
<PackageReference Include="Grpc.Tools" Version="2.66.0" PrivateAssets="All" />
<!-- <PackageReference Include="Grpc.AspNetCore" Version="2.62.0" />-->
<PackageReference Include="Google.Protobuf" Version="3.26.1" />
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.62.0" />
<PackageReference Include="Grpc.Tools" Version="2.63.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.5.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
@ -102,7 +103,6 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\EllieBot.GrpcApiBase\EllieBot.GrpcApiBase.csproj" />
<ProjectReference Include="..\Ellie.Marmalade\Ellie.Marmalade.csproj" />
<ProjectReference Include="..\EllieBot.Voice\EllieBot.Voice.csproj" />
<ProjectReference Include="..\EllieBot.Generators\EllieBot.Generators.csproj" OutputItemType="Analyzer" />
@ -113,6 +113,9 @@
</ItemGroup>
<ItemGroup>
<Protobuf Include="..\EllieBot.Coordinator\Protos\coordinator.proto" GrpcServices="Client">
<Link>Protos\coordinator.proto</Link>
</Protobuf>
<None Update="data\**\*">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@ -128,10 +131,7 @@
</ItemGroup>
<ItemGroup>
<Protobuf Include="..\EllieBot.Coordinator\Protos\coordinator.proto">
<Link>_common\CoordinatorProtos\coordinator.proto</Link>
<!-- <GrpcServices>Client</GrpcServices>-->
</Protobuf>
<Folder Include="Grpc\" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'GlobalEllie' ">

View file

@ -93,9 +93,6 @@ public class GreetService : IEService, IReadyExecutor
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
// or boosted again the same server
if ((optOldUser.Value is { PremiumSince: null } && newUser is { PremiumSince: not null })
@ -137,63 +134,21 @@ public class GreetService : IEService, IReadyExecutor
.DeleteAsync();
}
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;
}
private Task OnUserLeft(SocketGuild guild, SocketUser user)
{
_ = Task.Run(async () =>
{
if (!_enabled[GreetType.Bye].Contains(guild.Id))
return;
try
{
var conf = await GetGreetSettingsAsync(guild.Id, GreetType.Bye);
if (conf?.ChannelId is not { } cid)
if (conf is null)
return;
var channel = guild.GetChannel(cid) as ITextChannel;
var channel = guild.TextChannels.FirstOrDefault(c => c.Id == conf.ChannelId);
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);
return;
}
@ -208,11 +163,11 @@ public class GreetService : IEService, IReadyExecutor
return Task.CompletedTask;
}
private TypedKey<GreetSettings?> GreetSettingsKey(ulong gid, GreetType type)
=> new($"greet_settings:{gid}:{type}");
private TypedKey<GreetSettings?> GreetSettingsKey(GreetType type)
=> new($"greet_settings:{type}");
public async Task<GreetSettings?> GetGreetSettingsAsync(ulong gid, GreetType type)
=> await _cache.GetOrAddAsync<GreetSettings?>(GreetSettingsKey(gid, type),
=> await _cache.GetOrAddAsync<GreetSettings?>(GreetSettingsKey(type),
() => InternalGetGreetSettingsAsync(gid, type),
TimeSpan.FromSeconds(3));
@ -261,10 +216,9 @@ public class GreetService : IEService, IReadyExecutor
or DiscordErrorCode.UnknownChannel)
{
Log.Warning(ex,
"Missing permissions to send a {GreetType} message, it will be disabled on server: {GuildId}",
conf.GreetType,
"Missing permissions to send a bye message, the greet message will be disabled on server: {GuildId}",
channel.GuildId);
await SetGreet(channel.GuildId, channel.Id, conf.GreetType, false);
await SetGreet(channel.GuildId, channel.Id, GreetType.Greet, false);
}
catch (Exception ex)
{
@ -353,6 +307,43 @@ public class GreetService : IEService, IReadyExecutor
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)
=> greetType switch

View file

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

View file

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

View file

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

View file

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

View file

@ -6,6 +6,49 @@ namespace EllieBot.Modules.EllieExpressions;
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)]
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
}
private readonly IBotCreds _creds;
private readonly IBotCredentials _creds;
private readonly IHttpClientFactory _clientFactory;
public EllieExpressions(IBotCreds creds, IHttpClientFactory clientFactory)
public EllieExpressions(IBotCredentials creds, IHttpClientFactory clientFactory)
{
_creds = creds;
_clientFactory = clientFactory;

View file

@ -249,9 +249,8 @@ public sealed class EllieExpressionsService : IExecOnMessage, IReadyExecutor
try
{
if (guild is not SocketGuild sg)
return false;
if (guild is SocketGuild sg)
{
var result = await _permChecker.CheckPermsAsync(
guild,
msg.Channel,
@ -287,16 +286,9 @@ public sealed class EllieExpressionsService : IExecOnMessage, IReadyExecutor
return true;
}
}
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 sentMsg = await expr.Send(msg, _repSvc, _client, _sender);
var reactions = expr.GetReactions();
foreach (var reaction in reactions)
@ -344,47 +336,6 @@ public sealed class EllieExpressionsService : IExecOnMessage, IReadyExecutor
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)
{
EllieExpression expr;
@ -838,7 +789,7 @@ public sealed class EllieExpressionsService : IExecOnMessage, IReadyExecutor
if (newguildExpressions.TryGetValue(guildId, out var exprs))
{
return (exprs.Where(x => x.Trigger.Contains(query) || x.Response.Contains(query))
return (exprs.Where(x => x.Trigger.Contains(query))
.Skip(page * 9)
.Take(9)
.ToArray(), exprs.Length);

View file

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

View file

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

View file

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

View file

@ -15,7 +15,7 @@ public class WaifuService : IEService, IReadyExecutor
private readonly ICurrencyService _cs;
private readonly IBotCache _cache;
private readonly GamblingConfigService _gss;
private readonly IBotCreds _creds;
private readonly IBotCredentials _creds;
private readonly DiscordSocketClient _client;
public WaifuService(
@ -23,7 +23,7 @@ public class WaifuService : IEService, IReadyExecutor
ICurrencyService cs,
IBotCache cache,
GamblingConfigService gss,
IBotCreds creds,
IBotCredentials creds,
DiscordSocketClient client)
{
_db = db;
@ -300,10 +300,10 @@ public class WaifuService : IEService, IReadyExecutor
return (oldAff, success, remaining);
}
public async Task<IReadOnlyList<WaifuLbResult>> GetTopWaifusAtPage(int page, int perPage = 9)
public IEnumerable<WaifuLbResult> GetTopWaifusAtPage(int page, int perPage = 9)
{
await using var uow = _db.GetDbContext();
return await uow.Set<WaifuInfo>().GetTop(perPage, page * perPage);
using var uow = _db.GetDbContext();
return uow.Set<WaifuInfo>().GetTop(perPage, page * perPage);
}
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);
}
public static async Task<IReadOnlyList<WaifuLbResult>> GetTop(this DbSet<WaifuInfo> waifus, int count, int skip = 0)
public static IEnumerable<WaifuLbResult> GetTop(this DbSet<WaifuInfo> waifus, int count, int skip = 0)
{
ArgumentOutOfRangeException.ThrowIfNegative(count);
if (count == 0)
return [];
return await waifus.Include(wi => wi.Waifu)
return waifus.Include(wi => wi.Waifu)
.Include(wi => wi.Affinity)
.Include(wi => wi.Claimer)
.OrderByDescending(wi => wi.Price)
@ -48,7 +48,7 @@ public static class WaifuExtensions
Discrim = x.Waifu.Discriminator,
Price = x.Price
})
.ToListAsyncEF();
.ToList();
}
public static decimal GetTotalValue(this DbSet<WaifuInfo> waifus)

View file

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

View file

@ -77,7 +77,7 @@ public sealed partial class Help : EllieModule<HelpService>
m.Name,
null);
#if GLOBAL_ELLIE
#if GLOBAL_NADEKO
if (m.Preconditions.Any(x => x is NoPublicBotAttribute))
continue;
#endif

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

@ -0,0 +1,34 @@
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>
{
private readonly IBotCreds _creds;
private readonly IBotCredentials _creds;
private readonly IGoogleApiService _google;
private readonly IHttpClientFactory _httpFactory;
private readonly IMemoryCache _cache;
private readonly ITimezoneService _tzSvc;
public Searches(
IBotCreds creds,
IBotCredentials creds,
IGoogleApiService google,
IHttpClientFactory factory,
IMemoryCache cache,

View file

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

View file

@ -47,11 +47,19 @@ public class SearchesConfigService : ConfigServiceBase<SearchesConfig>
});
}
if (data.Version < 4)
if (data.Version < 2)
{
ModifyConfig(c =>
{
c.Version = 4;
c.Version = 2;
});
}
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 = "🎉";
private readonly DbService _db;
private readonly IBotCreds _creds;
private readonly IBotCredentials _creds;
private readonly DiscordSocketClient _client;
private readonly IMessageSenderService _sender;
private readonly IBotStrings _strings;
@ -20,7 +20,7 @@ public sealed class GiveawayService : IEService, IReadyExecutor
private SortedSet<GiveawayModel> _giveawayCache = new SortedSet<GiveawayModel>();
private readonly EllieRandom _rng;
public GiveawayService(DbService db, IBotCreds creds, DiscordSocketClient client,
public GiveawayService(DbService db, IBotCredentials creds, DiscordSocketClient client,
IMessageSenderService sender, IBotStrings strings, ILocalization localization, IMemoryCache cache)
{
_db = db;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,6 @@
var shardId = 0;
var pid = Environment.ProcessId;
var shardId = 0;
int? totalShards = null; // 0 to read from creds.yml
if (args.Length > 0 && args[0] != "run")
{
@ -20,5 +22,7 @@ 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();

View file

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

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

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

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

@ -119,7 +119,7 @@ public sealed class BotCredsProvider : IBotCredsProvider
}
}
public void ModifyCredsFile(Action<IBotCreds> func)
public void ModifyCredsFile(Action<IBotCredentials> func)
{
var ymlData = File.ReadAllText(CREDS_FILE_NAME);
var creds = Yaml.Deserializer.Deserialize<Creds>(ymlData);
@ -137,18 +137,24 @@ public sealed class BotCredsProvider : IBotCredsProvider
var creds = Yaml.Deserializer.Deserialize<Creds>(File.ReadAllText(CREDS_FILE_NAME));
if (creds.Version <= 5)
{
creds.BotCache = BotCacheImplemenation.Memory;
creds.BotCache = BotCacheImplemenation.Redis;
}
if (creds.Version < 12)
if (creds.Version <= 6)
{
creds.Version = 12;
creds.Version = 7;
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));
}
}
}
public IBotCreds GetCreds()
public IBotCredentials GetCreds()
{
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);
}
public async Task<IReadOnlyList<string>> GetVideoLinksByKeywordAsync(string keywords, int count = 1)
public async Task<IEnumerable<string>> GetVideoLinksByKeywordAsync(string keywords, int count = 1)
{
if (string.IsNullOrWhiteSpace(keywords))
throw new ArgumentNullException(nameof(keywords));
@ -87,7 +87,7 @@ public sealed partial class GoogleApiService : IGoogleApiService, IEService
query.Q = keywords;
query.Type = "video";
query.SafeSearch = SearchResource.ListRequest.SafeSearchEnum.Strict;
return (await query.ExecuteAsync()).Items.Select(i => "https://www.youtube.com/watch?v=" + i.Id.VideoId).ToArray();
return (await query.ExecuteAsync()).Items.Select(i => "https://www.youtube.com/watch?v=" + i.Id.VideoId);
}
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
{
private readonly IBotCreds _creds;
private readonly IBotCredentials _creds;
private readonly ConnectionMultiplexer _multi;
private readonly ISeria _serializer;
public RedisPubSub(ConnectionMultiplexer multi, ISeria serializer, IBotCreds creds)
public RedisPubSub(ConnectionMultiplexer multi, ISeria serializer, IBotCredentials creds)
{
_multi = multi;
_serializer = serializer;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,10 +3,10 @@ using EllieBot.Common.Yml;
namespace EllieBot.Common;
public sealed class Creds : IBotCreds
public sealed class Creds : IBotCredentials
{
[Comment("""DO NOT CHANGE""")]
public int Version { get; set; } = 12;
public int Version { get; set; }
[Comment("""Bot token. Do not share with anyone ever -> https://discordapp.com/developers/applications/""")]
public string Token { get; set; }
@ -17,8 +17,7 @@ public sealed class Creds : IBotCreds
""")]
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; }
[Comment("""
@ -156,21 +155,9 @@ public sealed class Creds : IBotCreds
""")]
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()
{
Version = 9;
Token = string.Empty;
UsePrivilegedIntents = true;
OwnerIds = new List<ulong>();
@ -193,9 +180,6 @@ public sealed class Creds : IBotCreds
RestartCommand = new RestartConfig();
Google = new GoogleApiConfig();
GrpcApi = new();
Seq = new();
}
public class DbOptions
@ -289,21 +273,6 @@ public sealed class Creds : IBotCreds
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
@ -311,3 +280,6 @@ public class GoogleApiConfig : IGoogleApiConfig
public string SearchId { get; init; }
public string ImageSearchId { get; init; }
}

View file

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

View file

@ -1,3 +1,4 @@
#nullable disable
using System.Text.RegularExpressions;
namespace EllieBot.Common;
@ -65,15 +66,13 @@ public sealed partial class ReplacementPatternStore
Register("%user.mention%", static (IUser user) => user.Mention);
Register("%user.fullname%", static (IUser user) => user.ToString()!);
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.avatar%", static (IUser user) => user.RealAvatarUrl().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_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_date%",
static (IGuildUser user) => user.JoinedAt?.ToString("dd.MM.yyyy") ?? "??.??.????");
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%",
static (IUser[] users) => string.Join(" ", users.Select(user => user.Mention)));

View file

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

View file

@ -5,7 +5,7 @@ public interface IGoogleApiService
{
IReadOnlyDictionary<string, string> Languages { get; }
Task<IReadOnlyList<string>> GetVideoLinksByKeywordAsync(string keywords, int count = 1);
Task<IEnumerable<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>> GetPlaylistIdsByKeywordsAsync(string keywords, int count = 1);
Task<IEnumerable<string>> GetRelatedVideosAsync(string id, int count = 1, string user = null);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
# DO NOT CHANGE
version: 4
version: 3
# Which engine should .search command
# '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
@ -11,12 +11,14 @@ webSearchEngine: Google_Scrape
imgSearchEngine: Google
# 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. `.q` is not supported for this setting. It will fallback to yt-dlp.
# - `ytDataApiv3` - uses google's official youtube data api. Requires `GoogleApiKey` set in creds and youtube data api enabled in developers console
#
# - `ytdlp` - default, recommended easy, uses `yt-dlp`. Requires `yt-dlp` to be installed and it's path added to env variables
# - `ytdl` - default, uses youtube-dl. Requires `youtube-dl` to be installed and it's path added to env variables. Slow.
#
# - `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
ytProvider: Ytdl
ytProvider: Ytdlp
# 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.
# Use a fully qualified url. Example: `https://my-searx-instance.mydomain.com`