This commit is contained in:
Toastie (DCS Team) 2024-08-02 00:22:19 +12:00
commit 430052a446
Signed by: toastie_t0ast
GPG key ID: 27F3B6855AFD40A4
52 changed files with 1199 additions and 686 deletions

3
.gitignore vendored
View file

@ -20,6 +20,9 @@ src/EllieBot/credentials.json
src/EllieBot/old_credentials.json src/EllieBot/old_credentials.json
src/EllieBot/credentials.json.bak src/EllieBot/credentials.json.bak
src/EllieBot/data/EllieBot.db src/EllieBot/data/EllieBot.db
build.ps1
build.sh
test.ps1
# Created by https://www.gitignore.io/api/visualstudio,visualstudiocode,windows,linux,macos # Created by https://www.gitignore.io/api/visualstudio,visualstudiocode,windows,linux,macos

View file

@ -2,6 +2,15 @@
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
## Unreleased
### Added
- Added: Added a `'afk <msg>?` command which sets an afk message which will trigger whenever someone pings you
- Message will when you type a message in any channel that the bot sees, or after 8 hours, whichever comes first
- The specified message will be prefixed with "The user is afk: "
- The afk message will disappear 30 seconds after being triggered
## [5.1.4] - 15.07.2024 ## [5.1.4] - 15.07.2024
### Added ### Added

View file

@ -8,11 +8,10 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6C633450-E6C2-47ED-A7AA-7367232F703A}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6C633450-E6C2-47ED-A7AA-7367232F703A}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
CHANGELOG.md = CHANGELOG.md CHANGELOG.md = CHANGELOG.md
LICENSE = LICENSE
README.md = README.md
Dockerfile = Dockerfile Dockerfile = Dockerfile
NuGet.Config = NuGet.Config LICENSE = LICENSE
migrate.ps1 = migrate.ps1 migrate.ps1 = migrate.ps1
README.md = README.md
remove-migrations.ps1 = remove-migrations.ps1 remove-migrations.ps1 = remove-migrations.ps1
TODO.md = TODO.md TODO.md = TODO.md
EndProjectSection EndProjectSection

View file

@ -1,6 +0,0 @@
<configuration>
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
<add key="toastielab.dev" value="https://toastielab.dev/api/packages/ellie/nuget/index.json" protocolVersion="3" />
</packageSources>
</configuration>

View file

@ -9,7 +9,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Discord.Net.Core" Version="3.204.0" /> <PackageReference Include="Discord.Net.Core" Version="3.15.3" />
<PackageReference Include="Serilog" Version="3.1.1" /> <PackageReference Include="Serilog" Version="3.1.1" />
<PackageReference Include="YamlDotNet" Version="15.1.4" /> <PackageReference Include="YamlDotNet" Version="15.1.4" />
</ItemGroup> </ItemGroup>

View file

@ -77,7 +77,6 @@ csharp_style_var_when_type_is_apparent = true:suggestion
# Expression-bodied members # Expression-bodied members
csharp_style_expression_bodied_accessors = true:suggestion csharp_style_expression_bodied_accessors = true:suggestion
csharp_style_expression_bodied_constructors = when_on_single_line:suggestion
csharp_style_expression_bodied_indexers = true:suggestion csharp_style_expression_bodied_indexers = true:suggestion
csharp_style_expression_bodied_lambdas = true:suggestion csharp_style_expression_bodied_lambdas = true:suggestion
csharp_style_expression_bodied_local_functions = true:suggestion csharp_style_expression_bodied_local_functions = true:suggestion

View file

@ -182,16 +182,6 @@ public static class GuildConfigExtensions
.SelectMany(gc => gc.FollowedStreams) .SelectMany(gc => gc.FollowedStreams)
.ToList(); .ToList();
public static void SetCleverbotEnabled(this DbSet<GuildConfig> configs, ulong id, bool cleverbotEnabled)
{
var conf = configs.FirstOrDefault(gc => gc.GuildId == id);
if (conf is null)
return;
conf.CleverbotEnabled = cleverbotEnabled;
}
public static XpSettings XpSettingsFor(this DbContext ctx, ulong guildId) public static XpSettings XpSettingsFor(this DbContext ctx, ulong guildId)
{ {
var gc = ctx.GuildConfigsForId(guildId, var gc = ctx.GuildConfigsForId(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.4</Version> <Version>5.1.5</Version>
<!-- Output/build --> <!-- Output/build -->
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory> <RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
@ -29,7 +29,7 @@
</PackageReference> </PackageReference>
<PackageReference Include="CodeHollow.FeedReader" Version="1.2.6" /> <PackageReference Include="CodeHollow.FeedReader" Version="1.2.6" />
<PackageReference Include="CommandLineParser" Version="2.9.1" /> <PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="Discord.Net" Version="3.204.0" /> <PackageReference Include="Discord.Net" Version="3.15.3" />
<PackageReference Include="CoreCLR-NCalc" Version="3.1.246" /> <PackageReference Include="CoreCLR-NCalc" Version="3.1.246" />
<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" />
@ -70,7 +70,7 @@
<PackageReference Include="Serilog.Sinks.Seq" Version="7.0.1" /> <PackageReference Include="Serilog.Sinks.Seq" Version="7.0.1" />
<PackageReference Include="SixLabors.Fonts" Version="1.0.0-beta17" /> <PackageReference Include="SixLabors.Fonts" Version="1.0.0-beta17" />
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.8" /> <PackageReference Include="SixLabors.ImageSharp" Version="2.1.9" />
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta14" /> <PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta14" />
<PackageReference Include="SixLabors.Shapes" Version="1.0.0-beta0009" /> <PackageReference Include="SixLabors.Shapes" Version="1.0.0-beta0009" />
<PackageReference Include="StackExchange.Redis" Version="2.8.0" /> <PackageReference Include="StackExchange.Redis" Version="2.8.0" />

View file

@ -27,5 +27,15 @@ public partial class Administration
.Confirm($"{result.GuildCount} guilds' data remain in the database.") .Confirm($"{result.GuildCount} guilds' data remain in the database.")
.SendAsync(); .SendAsync();
} }
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
public async Task Keep()
{
var result = await _svc.KeepGuild(Context.Guild.Id);
await Response().Text("This guild's bot data will be saved.").SendAsync();
}
} }
} }

View file

@ -1,6 +1,7 @@
using LinqToDB; using LinqToDB;
using LinqToDB.Data; using LinqToDB.Data;
using LinqToDB.EntityFrameworkCore; using LinqToDB.EntityFrameworkCore;
using LinqToDB.Mapping;
using EllieBot.Common.ModuleBehaviors; using EllieBot.Common.ModuleBehaviors;
using EllieBot.Db.Models; using EllieBot.Db.Models;
@ -66,67 +67,88 @@ public sealed class CleanupService : ICleanupService, IReadyExecutor, IEService
.Where(x => !tempTable.Select(x => x.GuildId) .Where(x => !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId)) .Contains(x.GuildId))
.DeleteAsync(); .DeleteAsync();
// delete guild xp // delete guild xp
await ctx.GetTable<UserXpStats>() await ctx.GetTable<UserXpStats>()
.Where(x => !tempTable.Select(x => x.GuildId) .Where(x => !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId)) .Contains(x.GuildId))
.DeleteAsync(); .DeleteAsync();
// delete expressions // delete expressions
await ctx.GetTable<EllieExpression>() await ctx.GetTable<EllieExpression>()
.Where(x => x.GuildId != null && !tempTable.Select(x => x.GuildId) .Where(x => x.GuildId != null
.Contains(x.GuildId.Value)) && !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId.Value))
.DeleteAsync(); .DeleteAsync();
// delete quotes // delete quotes
await ctx.GetTable<Quote>() await ctx.GetTable<Quote>()
.Where(x => !tempTable.Select(x => x.GuildId) .Where(x => !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId)) .Contains(x.GuildId))
.DeleteAsync(); .DeleteAsync();
// delete planted currencies // delete planted currencies
await ctx.GetTable<PlantedCurrency>() await ctx.GetTable<PlantedCurrency>()
.Where(x => !tempTable.Select(x => x.GuildId) .Where(x => !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId)) .Contains(x.GuildId))
.DeleteAsync(); .DeleteAsync();
// delete image only channels // delete image only channels
await ctx.GetTable<ImageOnlyChannel>() await ctx.GetTable<ImageOnlyChannel>()
.Where(x => !tempTable.Select(x => x.GuildId) .Where(x => !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId)) .Contains(x.GuildId))
.DeleteAsync(); .DeleteAsync();
// delete reaction roles // delete reaction roles
await ctx.GetTable<ReactionRoleV2>() await ctx.GetTable<ReactionRoleV2>()
.Where(x => !tempTable.Select(x => x.GuildId) .Where(x => !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId)) .Contains(x.GuildId))
.DeleteAsync(); .DeleteAsync();
// delete ignored users // delete ignored users
await ctx.GetTable<DiscordPermOverride>() await ctx.GetTable<DiscordPermOverride>()
.Where(x => x.GuildId != null && !tempTable.Select(x => x.GuildId) .Where(x => x.GuildId != null
.Contains(x.GuildId.Value)) && !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId.Value))
.DeleteAsync(); .DeleteAsync();
// delete perm overrides // delete perm overrides
await ctx.GetTable<DiscordPermOverride>() await ctx.GetTable<DiscordPermOverride>()
.Where(x => x.GuildId != null && !tempTable.Select(x => x.GuildId) .Where(x => x.GuildId != null
.Contains(x.GuildId.Value)) && !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId.Value))
.DeleteAsync(); .DeleteAsync();
// delete repeaters // delete repeaters
await ctx.GetTable<Repeater>() await ctx.GetTable<Repeater>()
.Where(x => !tempTable.Select(x => x.GuildId) .Where(x => !tempTable.Select(x => x.GuildId)
.Contains(x.GuildId)) .Contains(x.GuildId))
.DeleteAsync(); .DeleteAsync();
return new() return new()
{ {
GuildCount = guildIds.Keys.Count, GuildCount = guildIds.Keys.Count,
}; };
} }
public async Task<bool> KeepGuild(ulong guildId)
{
await using var db = _db.GetDbContext();
await using var ctx = db.CreateLinqToDBContext();
var table = ctx.CreateTable<KeptGuilds>(tableOptions: TableOptions.CheckExistence);
if (await table.AnyAsyncLinqToDB(x => x.GuildId == guildId))
return false;
await table.InsertAsync(() => new()
{
GuildId = guildId
});
return true;
}
private ValueTask OnKeepReport(KeepReport report) private ValueTask OnKeepReport(KeepReport report)
{ {
guildIds[report.ShardId] = report.GuildIds; guildIds[report.ShardId] = report.GuildIds;
@ -137,10 +159,17 @@ public sealed class CleanupService : ICleanupService, IReadyExecutor, IEService
{ {
await _pubSub.Sub(_keepTriggerKey, OnKeepTrigger); await _pubSub.Sub(_keepTriggerKey, OnKeepTrigger);
_client.JoinedGuild += ClientOnJoinedGuild;
if (_client.ShardId == 0) if (_client.ShardId == 0)
await _pubSub.Sub(_keepReportKey, OnKeepReport); await _pubSub.Sub(_keepReportKey, OnKeepReport);
} }
private async Task ClientOnJoinedGuild(SocketGuild arg)
{
await KeepGuild(arg.Id);
}
private ValueTask OnKeepTrigger(bool arg) private ValueTask OnKeepTrigger(bool arg)
{ {
_pubSub.Pub(_keepReportKey, _pubSub.Pub(_keepReportKey,
@ -152,4 +181,10 @@ public sealed class CleanupService : ICleanupService, IReadyExecutor, IEService
return default; return default;
} }
}
public class KeptGuilds
{
[PrimaryKey]
public ulong GuildId { get; set; }
} }

View file

@ -3,4 +3,5 @@
public interface ICleanupService public interface ICleanupService
{ {
Task<KeepResult?> DeleteMissingGuildDataAsync(); Task<KeepResult?> DeleteMissingGuildDataAsync();
Task<bool> KeepGuild(ulong guildId);
} }

View file

@ -65,23 +65,6 @@ public partial class Administration
await progressMsg.DeleteAsync(); await progressMsg.DeleteAsync();
} }
private async Task SendResult(PruneResult result)
{
switch (result)
{
case PruneResult.Success:
break;
case PruneResult.AlreadyRunning:
break;
case PruneResult.FeatureLimit:
await Response().Pending(strs.feature_limit_reached_owner).SendAsync();
break;
default:
throw new ArgumentOutOfRangeException(nameof(result), result, null);
}
}
// prune x // prune x
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
@ -218,5 +201,27 @@ public partial class Administration
await Response().Confirm(strs.prune_cancelled).SendAsync(); await Response().Confirm(strs.prune_cancelled).SendAsync();
} }
private async Task SendResult(PruneResult result)
{
switch (result)
{
case PruneResult.Success:
break;
case PruneResult.AlreadyRunning:
var msg = await Response().Pending(strs.prune_already_running).SendAsync();
msg.DeleteAfter(5);
break;
case PruneResult.FeatureLimit:
var msg2 = await Response().Pending(strs.feature_limit_reached_owner).SendAsync();
msg2.DeleteAfter(10);
break;
default:
Log.Error("Unhandled result received in prune: {Result}", result);
await Response().Error(strs.error_occured).SendAsync();
break;
}
}
} }
} }

View file

@ -26,21 +26,21 @@ public class PruneService : IEService
) )
{ {
ArgumentNullException.ThrowIfNull(channel, nameof(channel)); ArgumentNullException.ThrowIfNull(channel, nameof(channel));
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(amount);
var originalAmount = amount; var originalAmount = amount;
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(amount);
using var cancelSource = new CancellationTokenSource(); using var cancelSource = new CancellationTokenSource();
if (!_pruningGuilds.TryAdd(channel.GuildId, cancelSource)) if (!_pruningGuilds.TryAdd(channel.GuildId, cancelSource))
return PruneResult.AlreadyRunning; return PruneResult.AlreadyRunning;
if (!await _ps.LimitHitAsync(LimitedFeatureName.Prune, channel.Guild.OwnerId))
{
return PruneResult.FeatureLimit;
}
try try
{ {
if (!await _ps.LimitHitAsync(LimitedFeatureName.Prune, channel.Guild.OwnerId))
{
return PruneResult.FeatureLimit;
}
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
IMessage[] msgs; IMessage[] msgs;
IMessage lastMessage = null; IMessage lastMessage = null;

View file

@ -33,7 +33,7 @@ public partial class Administration
var msg = await ctx.Channel.GetMessageAsync(messageId); var msg = await ctx.Channel.GetMessageAsync(messageId);
if (msg is null) if (msg is null)
{ {
await Response().Error(strs.not_found).SendAsync(); await Response().Error(strs.rero_message_not_found).SendAsync();
return; return;
} }

View file

@ -273,6 +273,31 @@ public partial class Administration
.SendAsync(); .SendAsync();
} }
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)]
public Task WarnDelete(IGuildUser user, int index)
=> WarnDelete(user.Id, index);
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPermission.Administrator)]
public async Task WarnDelete(ulong userId, int index)
{
if (--index < 0)
return;
var warn = await _service.WarnDelete(userId, index);
if (warn is null)
{
await Response().Error(strs.warning_not_found).SendAsync();
return;
}
await Response().Confirm(strs.warning_deleted(Format.Bold(index.ToString()))).SendAsync();
}
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.BanMembers)] [UserPerm(GuildPerm.BanMembers)]
@ -286,6 +311,7 @@ public partial class Administration
{ {
if (index < 0) if (index < 0)
return; return;
var success = await _service.WarnClearAsync(ctx.Guild.Id, userId, index, ctx.User.ToString()); var success = await _service.WarnClearAsync(ctx.Guild.Id, userId, index, ctx.User.ToString());
var userStr = Format.Bold((ctx.Guild as SocketGuild)?.GetUser(userId)?.ToString() ?? userId.ToString()); var userStr = Format.Bold((ctx.Guild as SocketGuild)?.GetUser(userId)?.ToString() ?? userId.ToString());
if (index == 0) if (index == 0)

View file

@ -89,9 +89,10 @@ public class UserPunishService : IEService, IReadyExecutor
{ {
ps = uow.GuildConfigsForId(guildId, set => set.Include(x => x.WarnPunishments)).WarnPunishments; ps = uow.GuildConfigsForId(guildId, set => set.Include(x => x.WarnPunishments)).WarnPunishments;
previousCount = uow.Set<Warning>().ForId(guildId, userId) previousCount = uow.Set<Warning>()
.Where(w => !w.Forgiven && w.UserId == userId) .ForId(guildId, userId)
.Sum(x => x.Weight); .Where(w => !w.Forgiven && w.UserId == userId)
.Sum(x => x.Weight);
uow.Set<Warning>().Add(warn); uow.Set<Warning>().Add(warn);
@ -103,7 +104,7 @@ public class UserPunishService : IEService, IReadyExecutor
var totalCount = previousCount + weight; var totalCount = previousCount + weight;
var p = ps.Where(x => x.Count > previousCount && x.Count <= totalCount) var p = ps.Where(x => x.Count > previousCount && x.Count <= totalCount)
.MaxBy(x => x.Count); .MaxBy(x => x.Count);
if (p is not null) if (p is not null)
{ {
@ -244,33 +245,33 @@ public class UserPunishService : IEService, IReadyExecutor
{ {
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
var cleared = await uow.Set<Warning>() var cleared = await uow.Set<Warning>()
.Where(x => uow.Set<GuildConfig>() .Where(x => uow.Set<GuildConfig>()
.Any(y => y.GuildId == x.GuildId .Any(y => y.GuildId == x.GuildId
&& y.WarnExpireHours > 0 && y.WarnExpireHours > 0
&& y.WarnExpireAction == WarnExpireAction.Clear) && y.WarnExpireAction == WarnExpireAction.Clear)
&& x.Forgiven == false && x.Forgiven == false
&& x.DateAdded && x.DateAdded
< DateTime.UtcNow.AddHours(-uow.Set<GuildConfig>() < DateTime.UtcNow.AddHours(-uow.Set<GuildConfig>()
.Where(y => x.GuildId == y.GuildId) .Where(y => x.GuildId == y.GuildId)
.Select(y => y.WarnExpireHours) .Select(y => y.WarnExpireHours)
.First())) .First()))
.UpdateAsync(_ => new() .UpdateAsync(_ => new()
{ {
Forgiven = true, Forgiven = true,
ForgivenBy = "expiry" ForgivenBy = "expiry"
}); });
var deleted = await uow.Set<Warning>() var deleted = await uow.Set<Warning>()
.Where(x => uow.Set<GuildConfig>() .Where(x => uow.Set<GuildConfig>()
.Any(y => y.GuildId == x.GuildId .Any(y => y.GuildId == x.GuildId
&& y.WarnExpireHours > 0 && y.WarnExpireHours > 0
&& y.WarnExpireAction == WarnExpireAction.Delete) && y.WarnExpireAction == WarnExpireAction.Delete)
&& x.DateAdded && x.DateAdded
< DateTime.UtcNow.AddHours(-uow.Set<GuildConfig>() < DateTime.UtcNow.AddHours(-uow.Set<GuildConfig>()
.Where(y => x.GuildId == y.GuildId) .Where(y => x.GuildId == y.GuildId)
.Select(y => y.WarnExpireHours) .Select(y => y.WarnExpireHours)
.First())) .First()))
.DeleteAsync(); .DeleteAsync();
if (cleared > 0 || deleted > 0) if (cleared > 0 || deleted > 0)
{ {
@ -293,21 +294,21 @@ public class UserPunishService : IEService, IReadyExecutor
if (config.WarnExpireAction == WarnExpireAction.Clear) if (config.WarnExpireAction == WarnExpireAction.Clear)
{ {
await uow.Set<Warning>() await uow.Set<Warning>()
.Where(x => x.GuildId == guildId .Where(x => x.GuildId == guildId
&& x.Forgiven == false && x.Forgiven == false
&& x.DateAdded < DateTime.UtcNow.AddHours(-config.WarnExpireHours)) && x.DateAdded < DateTime.UtcNow.AddHours(-config.WarnExpireHours))
.UpdateAsync(_ => new() .UpdateAsync(_ => new()
{ {
Forgiven = true, Forgiven = true,
ForgivenBy = "expiry" ForgivenBy = "expiry"
}); });
} }
else if (config.WarnExpireAction == WarnExpireAction.Delete) else if (config.WarnExpireAction == WarnExpireAction.Delete)
{ {
await uow.Set<Warning>() await uow.Set<Warning>()
.Where(x => x.GuildId == guildId .Where(x => x.GuildId == guildId
&& x.DateAdded < DateTime.UtcNow.AddHours(-config.WarnExpireHours)) && x.DateAdded < DateTime.UtcNow.AddHours(-config.WarnExpireHours))
.DeleteAsync(); .DeleteAsync();
} }
await uow.SaveChangesAsync(); await uow.SaveChangesAsync();
@ -425,8 +426,8 @@ public class UserPunishService : IEService, IReadyExecutor
{ {
using var uow = _db.GetDbContext(); using var uow = _db.GetDbContext();
return uow.GuildConfigsForId(guildId, gc => gc.Include(x => x.WarnPunishments)) return uow.GuildConfigsForId(guildId, gc => gc.Include(x => x.WarnPunishments))
.WarnPunishments.OrderBy(x => x.Count) .WarnPunishments.OrderBy(x => x.Count)
.ToArray(); .ToArray();
} }
public (IReadOnlyCollection<(string Original, ulong? Id, string Reason)> Bans, int Missing) MassKill( public (IReadOnlyCollection<(string Original, ulong? Id, string Reason)> Bans, int Missing) MassKill(
@ -436,20 +437,20 @@ public class UserPunishService : IEService, IReadyExecutor
var gusers = guild.Users; var gusers = guild.Users;
//get user objects and reasons //get user objects and reasons
var bans = people.Split("\n") var bans = people.Split("\n")
.Select(x => .Select(x =>
{ {
var split = x.Trim().Split(" "); var split = x.Trim().Split(" ");
var reason = string.Join(" ", split.Skip(1)); var reason = string.Join(" ", split.Skip(1));
if (ulong.TryParse(split[0], out var id)) if (ulong.TryParse(split[0], out var id))
return (Original: split[0], Id: id, Reason: reason); return (Original: split[0], Id: id, Reason: reason);
return (Original: split[0], return (Original: split[0],
gusers.FirstOrDefault(u => u.ToString().ToLowerInvariant() == x)?.Id, gusers.FirstOrDefault(u => u.ToString().ToLowerInvariant() == x)?.Id,
Reason: reason); Reason: reason);
}) })
.ToArray(); .ToArray();
//if user is null, means that person couldn't be found //if user is null, means that person couldn't be found
var missing = bans.Count(x => !x.Id.HasValue); var missing = bans.Count(x => !x.Id.HasValue);
@ -483,11 +484,12 @@ public class UserPunishService : IEService, IReadyExecutor
} }
else if (template is null) else if (template is null)
{ {
uow.Set<BanTemplate>().Add(new() uow.Set<BanTemplate>()
{ .Add(new()
GuildId = guildId, {
Text = text GuildId = guildId,
}); Text = text
});
} }
else else
template.Text = text; template.Text = text;
@ -499,31 +501,31 @@ public class UserPunishService : IEService, IReadyExecutor
{ {
await using var ctx = _db.GetDbContext(); await using var ctx = _db.GetDbContext();
await ctx.Set<BanTemplate>() await ctx.Set<BanTemplate>()
.ToLinqToDBTable() .ToLinqToDBTable()
.InsertOrUpdateAsync(() => new() .InsertOrUpdateAsync(() => new()
{ {
GuildId = guildId, GuildId = guildId,
Text = null, Text = null,
DateAdded = DateTime.UtcNow, DateAdded = DateTime.UtcNow,
PruneDays = pruneDays PruneDays = pruneDays
}, },
old => new() old => new()
{ {
PruneDays = pruneDays PruneDays = pruneDays
}, },
() => new() () => new()
{ {
GuildId = guildId GuildId = guildId
}); });
} }
public async Task<int?> GetBanPruneAsync(ulong guildId) public async Task<int?> GetBanPruneAsync(ulong guildId)
{ {
await using var ctx = _db.GetDbContext(); await using var ctx = _db.GetDbContext();
return await ctx.Set<BanTemplate>() return await ctx.Set<BanTemplate>()
.Where(x => x.GuildId == guildId) .Where(x => x.GuildId == guildId)
.Select(x => x.PruneDays) .Select(x => x.PruneDays)
.FirstOrDefaultAsyncLinqToDB(); .FirstOrDefaultAsyncLinqToDB();
} }
public Task<SmartText> GetBanUserDmEmbed( public Task<SmartText> GetBanUserDmEmbed(
@ -554,18 +556,18 @@ public class UserPunishService : IEService, IReadyExecutor
banReason = string.IsNullOrWhiteSpace(banReason) ? "-" : banReason; banReason = string.IsNullOrWhiteSpace(banReason) ? "-" : banReason;
var repCtx = new ReplacementContext(client, guild) var repCtx = new ReplacementContext(client, guild)
.WithOverride("%ban.mod%", () => moderator.ToString()) .WithOverride("%ban.mod%", () => moderator.ToString())
.WithOverride("%ban.mod.fullname%", () => moderator.ToString()) .WithOverride("%ban.mod.fullname%", () => moderator.ToString())
.WithOverride("%ban.mod.name%", () => moderator.Username) .WithOverride("%ban.mod.name%", () => moderator.Username)
.WithOverride("%ban.mod.discrim%", () => moderator.Discriminator) .WithOverride("%ban.mod.discrim%", () => moderator.Discriminator)
.WithOverride("%ban.user%", () => target.ToString()) .WithOverride("%ban.user%", () => target.ToString())
.WithOverride("%ban.user.fullname%", () => target.ToString()) .WithOverride("%ban.user.fullname%", () => target.ToString())
.WithOverride("%ban.user.name%", () => target.Username) .WithOverride("%ban.user.name%", () => target.Username)
.WithOverride("%ban.user.discrim%", () => target.Discriminator) .WithOverride("%ban.user.discrim%", () => target.Discriminator)
.WithOverride("%reason%", () => banReason) .WithOverride("%reason%", () => banReason)
.WithOverride("%ban.reason%", () => banReason) .WithOverride("%ban.reason%", () => banReason)
.WithOverride("%ban.duration%", .WithOverride("%ban.duration%",
() => duration?.ToString(@"d\.hh\:mm") ?? "perma"); () => duration?.ToString(@"d\.hh\:mm") ?? "perma");
// if template isn't set, use the old message style // if template isn't set, use the old message style
@ -594,4 +596,24 @@ public class UserPunishService : IEService, IReadyExecutor
var output = SmartText.CreateFrom(template); var output = SmartText.CreateFrom(template);
return await _repSvc.ReplaceAsync(output, repCtx); return await _repSvc.ReplaceAsync(output, repCtx);
} }
public async Task<Warning> WarnDelete(ulong userId, int index)
{
await using var uow = _db.GetDbContext();
var warn = await uow.GetTable<Warning>()
.Where(x => x.UserId == userId)
.OrderByDescending(x => x.DateAdded)
.Skip(index)
.FirstOrDefaultAsyncLinqToDB();
if (warn is not null)
{
await uow.GetTable<Warning>()
.Where(x => x.Id == warn.Id)
.DeleteAsync();
}
return warn;
}
} }

View file

@ -397,15 +397,14 @@ public partial class EllieExpressions : EllieModule<EllieExpressionsService>
var serialized = _service.ExportExpressions(ctx.Guild?.Id); var serialized = _service.ExportExpressions(ctx.Guild?.Id);
await using var stream = await serialized.ToStream(); await using var stream = await serialized.ToStream();
await ctx.Channel.SendFileAsync(stream, "exprs-export.yml"); await ctx.User.SendFileAsync(stream, $"exprs-export_{DateTime.UtcNow:yyyy-MM-dd-HH-mm-ss}_{(ctx.Guild?.Id.ToString() ?? "global")}.yml");
} }
[Cmd] [Cmd]
#if GLOBAL_ELLIE
[OwnerOnly]
#endif
public async Task ExprsImport([Leftover] string input = null) public async Task ExprsImport([Leftover] string input = null)
{ {
// todo cooldown on public bot for 1 day, limit 100
if (!AdminInGuildOrOwnerInDm()) if (!AdminInGuildOrOwnerInDm())
{ {
await Response().Error(strs.expr_insuff_perms).SendAsync(); await Response().Error(strs.expr_insuff_perms).SendAsync();

View file

@ -18,31 +18,18 @@ public partial class Games
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageMessages)] [UserPerm(GuildPerm.ManageMessages)]
[NoPublicBot]
public async Task CleverBot() public async Task CleverBot()
{ {
var channel = (ITextChannel)ctx.Channel; var channel = (ITextChannel)ctx.Channel;
if (_service.ChatterBotGuilds.TryRemove(channel.Guild.Id, out _)) var newState = await _service.ToggleChatterBotAsync(ctx.Guild.Id);
{
await using (var uow = _db.GetDbContext())
{
uow.Set<GuildConfig>().SetCleverbotEnabled(ctx.Guild.Id, false);
await uow.SaveChangesAsync();
}
if (!newState)
{
await Response().Confirm(strs.chatbot_disabled).SendAsync(); await Response().Confirm(strs.chatbot_disabled).SendAsync();
return; return;
} }
_service.ChatterBotGuilds.TryAdd(channel.Guild.Id, new(() => _service.CreateSession(), true));
await using (var uow = _db.GetDbContext())
{
uow.Set<GuildConfig>().SetCleverbotEnabled(ctx.Guild.Id, true);
await uow.SaveChangesAsync();
}
await Response().Confirm(strs.chatbot_enabled).SendAsync(); await Response().Confirm(strs.chatbot_enabled).SendAsync();
} }
} }

View file

@ -1,5 +1,8 @@
#nullable disable #nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using EllieBot.Common.ModuleBehaviors; using EllieBot.Common.ModuleBehaviors;
using EllieBot.Db.Models;
using EllieBot.Modules.Games.Common; using EllieBot.Modules.Games.Common;
using EllieBot.Modules.Games.Common.ChatterBot; using EllieBot.Modules.Games.Common.ChatterBot;
using EllieBot.Modules.Patronage; using EllieBot.Modules.Patronage;
@ -9,7 +12,7 @@ namespace EllieBot.Modules.Games.Services;
public class ChatterBotService : IExecOnMessage public class ChatterBotService : IExecOnMessage
{ {
public ConcurrentDictionary<ulong, Lazy<IChatterBotSession>> ChatterBotGuilds { get; } private ConcurrentDictionary<ulong, Lazy<IChatterBotSession>> ChatterBotGuilds { get; }
public int Priority public int Priority
=> 1; => 1;
@ -20,6 +23,7 @@ public class ChatterBotService : IExecOnMessage
private readonly IHttpClientFactory _httpFactory; private readonly IHttpClientFactory _httpFactory;
private readonly GamesConfigService _gcs; private readonly GamesConfigService _gcs;
private readonly IMessageSenderService _sender; private readonly IMessageSenderService _sender;
private readonly DbService _db;
public readonly IPatronageService _ps; public readonly IPatronageService _ps;
public ChatterBotService( public ChatterBotService(
@ -30,12 +34,14 @@ public class ChatterBotService : IExecOnMessage
IHttpClientFactory factory, IHttpClientFactory factory,
IBotCredentials creds, IBotCredentials creds,
GamesConfigService gcs, GamesConfigService gcs,
IMessageSenderService sender) IMessageSenderService sender,
DbService db)
{ {
_client = client; _client = client;
_perms = perms; _perms = perms;
_creds = creds; _creds = creds;
_sender = sender; _sender = sender;
_db = db;
_httpFactory = factory; _httpFactory = factory;
_perms = perms; _perms = perms;
_gcs = gcs; _gcs = gcs;
@ -196,4 +202,38 @@ public class ChatterBotService : IExecOnMessage
return false; return false;
} }
public async Task<bool> ToggleChatterBotAsync(ulong guildId)
{
if (ChatterBotGuilds.TryRemove(guildId, out _))
{
await using var uow = _db.GetDbContext();
await uow.Set<GuildConfig>()
.ToLinqToDBTable()
.Where(x => x.GuildId == guildId)
.UpdateAsync((gc) => new GuildConfig()
{
CleverbotEnabled = false
});
await uow.SaveChangesAsync();
return false;
}
ChatterBotGuilds.TryAdd(guildId, new(() => CreateSession(), true));
await using (var uow = _db.GetDbContext())
{
await uow.Set<GuildConfig>()
.ToLinqToDBTable()
.Where(x => x.GuildId == guildId)
.UpdateAsync((gc) => new GuildConfig()
{
CleverbotEnabled = true
});
await uow.SaveChangesAsync();
}
return true;
}
} }

View file

@ -5,5 +5,5 @@ namespace EllieBot.Modules.Games.Common.ChatterBot;
public class Choice public class Choice
{ {
[JsonPropertyName("message")] [JsonPropertyName("message")]
public Message Message { get; init; } public required Message Message { get; init; }
} }

View file

@ -5,5 +5,5 @@ namespace EllieBot.Modules.Games.Common.ChatterBot;
public class Message public class Message
{ {
[JsonPropertyName("content")] [JsonPropertyName("content")]
public string Content { get; init; } public required string Content { get; init; }
} }

View file

@ -5,11 +5,11 @@ namespace EllieBot.Modules.Games.Common.ChatterBot;
public class OpenAiApiMessage public class OpenAiApiMessage
{ {
[JsonPropertyName("role")] [JsonPropertyName("role")]
public string Role { get; init; } public required string Role { get; init; }
[JsonPropertyName("content")] [JsonPropertyName("content")]
public string Content { get; init; } public required string Content { get; init; }
[JsonPropertyName("name")] [JsonPropertyName("name")]
public string Name { get; init; } public required string Name { get; init; }
} }

View file

@ -5,14 +5,14 @@ namespace EllieBot.Modules.Games.Common.ChatterBot;
public class OpenAiApiRequest public class OpenAiApiRequest
{ {
[JsonPropertyName("model")] [JsonPropertyName("model")]
public string Model { get; init; } public required string Model { get; init; }
[JsonPropertyName("messages")] [JsonPropertyName("messages")]
public List<OpenAiApiMessage> Messages { get; init; } public required List<OpenAiApiMessage> Messages { get; init; }
[JsonPropertyName("temperature")] [JsonPropertyName("temperature")]
public int Temperature { get; init; } public required int Temperature { get; init; }
[JsonPropertyName("max_tokens")] [JsonPropertyName("max_tokens")]
public int MaxTokens { get; init; } public required int MaxTokens { get; init; }
} }

View file

@ -238,6 +238,6 @@ public partial class Searches
=> value > 0 ? "▲" : "▼"; => value > 0 ? "▲" : "▼";
private static string GetSign(decimal value) private static string GetSign(decimal value)
=> value >= 0 ? "+" : "-"; => value >= 0 ? "+" : "";
} }
} }

View file

@ -0,0 +1,13 @@
using System.Text.Json.Serialization;
namespace EllieBot.Modules.Searches;
public class BibleVerse
{
[JsonPropertyName("book_name")]
public required string BookName { get; set; }
public required int Chapter { get; set; }
public required int Verse { get; set; }
public required string Text { get; set; }
}

View file

@ -0,0 +1,7 @@
namespace EllieBot.Modules.Searches;
public class BibleVerses
{
public string? Error { get; set; }
public BibleVerse[]? Verses { get; set; }
}

View file

@ -0,0 +1,20 @@
#nullable disable
using System.Text.Json.Serialization;
namespace EllieBot.Modules.Searches;
public sealed class QuranAyah
{
[JsonPropertyName("number")]
public int Number { get; set; }
[JsonPropertyName("audio")]
public string Audio { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("text")]
public string Text { get; set; }
}

View file

@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
namespace EllieBot.Modules.Searches;
public sealed class QuranResponse<T>
{
[JsonPropertyName("code")]
public required int Code { get; set; }
[JsonPropertyName("status")]
public required string Status { get; set; }
[JsonPropertyName("data")]
public required T[] Data { get; set; }
}

View file

@ -0,0 +1,63 @@
using EllieBot.Modules.Searches.Common;
using OneOf;
using OneOf.Types;
using System.Net;
using System.Net.Http.Json;
namespace EllieBot.Modules.Searches;
public sealed class ReligiousApiService : IEService
{
private readonly IHttpClientFactory _httpFactory;
public ReligiousApiService(IHttpClientFactory httpFactory)
{
_httpFactory = httpFactory;
}
public async Task<OneOf<BibleVerse, Error<string>>> GetBibleVerseAsync(string book, string chapterAndVerse)
{
if (string.IsNullOrWhiteSpace(book) || string.IsNullOrWhiteSpace(chapterAndVerse))
return new Error<string>("Invalid input.");
book = Uri.EscapeDataString(book);
chapterAndVerse = Uri.EscapeDataString(chapterAndVerse);
using var http = _httpFactory.CreateClient();
try
{
var res = await http.GetFromJsonAsync<BibleVerses>($"https://bible-api.com/{book} {chapterAndVerse}");
if (res is null || res.Error is not null || res.Verses is null || res.Verses.Length == 0)
{
return new Error<string>(res?.Error ?? "No verse found.");
}
return res.Verses[0];
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
return new Error<string>("No verse found.");
}
}
public async Task<OneOf<QuranResponse<QuranAyah>, Error<LocStr>>> GetQuranVerseAsync(string ayah)
{
if (string.IsNullOrWhiteSpace(ayah))
return new Error<LocStr>(strs.invalid_input);
ayah = Uri.EscapeDataString(ayah);
using var http = _httpFactory.CreateClient();
var res = await http.GetFromJsonAsync<QuranResponse<QuranAyah>>(
$"https://api.alquran.cloud/v1/ayah/{ayah}/editions/en.asad,ar.alafasy");
if (res is null or not { Code: 200 })
{
return new Error<LocStr>(strs.not_found);
}
return res;
}
}

View file

@ -0,0 +1,60 @@
namespace EllieBot.Modules.Searches;
public partial class Searches
{
public partial class ReligiousCommands : EllieModule<ReligiousApiService>
{
private readonly IHttpClientFactory _httpFactory;
public ReligiousCommands(IHttpClientFactory httpFactory)
=> _httpFactory = httpFactory;
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task Bible(string book, string chapterAndVerse)
{
var res = await _service.GetBibleVerseAsync(book, chapterAndVerse);
if (!res.TryPickT0(out var verse, out var error))
{
await Response().Error(error.Value).SendAsync();
return;
}
await Response()
.Embed(_sender.CreateEmbed()
.WithOkColor()
.WithTitle($"{verse.BookName} {verse.Chapter}:{verse.Verse}")
.WithDescription(verse.Text))
.SendAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task Quran(string ayah)
{
var res = await _service.GetQuranVerseAsync(ayah);
if (!res.TryPickT0(out var qr, out var error))
{
await Response().Error(error.Value).SendAsync();
return;
}
var english = qr.Data[0];
var arabic = qr.Data[1];
using var http = _httpFactory.CreateClient();
await using var audio = await http.GetStreamAsync(arabic.Audio);
await Response()
.Embed(_sender.CreateEmbed()
.WithOkColor()
.AddField("Arabic", arabic.Text)
.AddField("English", english.Text)
.WithFooter(arabic.Number.ToString()))
.File(audio, Uri.EscapeDataString(ayah) + ".mp3")
.SendAsync();
}
}
}

View file

@ -1,102 +0,0 @@
using EllieBot.Modules.Searches.Common;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
namespace EllieBot.Modules.Searches;
public partial class Searches
{
public partial class ReligiousCommands : EllieModule
{
private readonly IHttpClientFactory _httpFactory;
public ReligiousCommands(IHttpClientFactory httpFactory)
{
_httpFactory = httpFactory;
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task Bible(string book, string chapterAndVerse)
{
var obj = new BibleVerses();
try
{
using var http = _httpFactory.CreateClient();
obj = await http.GetFromJsonAsync<BibleVerses>($"https://bible-api.com/{book} {chapterAndVerse}");
}
catch
{
}
if (obj.Error is not null || obj.Verses is null || obj.Verses.Length == 0)
await Response().Error(obj.Error ?? "No verse found.").SendAsync();
else
{
var v = obj.Verses[0];
await Response()
.Embed(_sender.CreateEmbed()
.WithOkColor()
.WithTitle($"{v.BookName} {v.Chapter}:{v.Verse}")
.WithDescription(v.Text))
.SendAsync();
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
public async Task Quran(string ayah)
{
using var http = _httpFactory.CreateClient();
var obj = await http.GetFromJsonAsync<QuranResponse<QuranAyah>>($"https://api.alquran.cloud/v1/ayah/{Uri.EscapeDataString(ayah)}/editions/en.asad,ar.alafasy");
if(obj is null or not { Code: 200 })
{
await Response().Error("No verse found.").SendAsync();
return;
}
var english = obj.Data[0];
var arabic = obj.Data[1];
await using var audio = await http.GetStreamAsync(arabic.Audio);
await Response()
.Embed(_sender.CreateEmbed()
.WithOkColor()
.AddField("Arabic", arabic.Text)
.AddField("English", english.Text)
.WithFooter(arabic.Number.ToString()))
.File(audio, Uri.EscapeDataString(ayah) + ".mp3")
.SendAsync();
}
}
}
public sealed class QuranResponse<T>
{
[JsonPropertyName("code")]
public int Code { get; set; }
[JsonPropertyName("status")]
public string Status { get; set; }
[JsonPropertyName("data")]
public T[] Data { get; set; }
}
public sealed class QuranAyah
{
[JsonPropertyName("number")]
public int Number { get; set; }
[JsonPropertyName("audio")]
public string Audio { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("text")]
public string Text { get; set; }
}

View file

@ -2,16 +2,12 @@
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using EllieBot.Modules.Searches.Common; using EllieBot.Modules.Searches.Common;
using EllieBot.Modules.Searches.Services; using EllieBot.Modules.Searches.Services;
using EllieBot.Modules.Utility;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Net.Http.Json;
using Color = SixLabors.ImageSharp.Color; using Color = SixLabors.ImageSharp.Color;
namespace EllieBot.Modules.Searches; namespace EllieBot.Modules.Searches;
@ -39,16 +35,6 @@ public partial class Searches : EllieModule<SearchesService>
_tzSvc = tzSvc; _tzSvc = tzSvc;
} }
[Cmd]
public async Task Rip([Leftover] IGuildUser usr)
{
var av = usr.RealAvatarUrl();
await using var picStream = await _service.GetRipPictureAsync(usr.Nickname ?? usr.Username, av);
await ctx.Channel.SendFileAsync(picStream,
"rip.png",
$"Rip {Format.Bold(usr.ToString())} \n\t- " + Format.Italics(ctx.User.ToString()));
}
[Cmd] [Cmd]
public async Task Weather([Leftover] string query) public async Task Weather([Leftover] string query)
{ {
@ -108,24 +94,7 @@ public partial class Searches : EllieModule<SearchesService>
var (data, err) = await _service.GetTimeDataAsync(query); var (data, err) = await _service.GetTimeDataAsync(query);
if (err is not null) if (err is not null)
{ {
LocStr errorKey; await HandleErrorAsync(err.Value);
switch (err)
{
case TimeErrors.ApiKeyMissing:
errorKey = strs.api_key_missing;
break;
case TimeErrors.InvalidInput:
errorKey = strs.invalid_input;
break;
case TimeErrors.NotFound:
errorKey = strs.not_found;
break;
default:
errorKey = strs.error_occured;
break;
}
await Response().Error(errorKey).SendAsync();
return; return;
} }
@ -136,11 +105,11 @@ public partial class Searches : EllieModule<SearchesService>
} }
var eb = _sender.CreateEmbed() var eb = _sender.CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle(GetText(strs.time_new)) .WithTitle(GetText(strs.time_new))
.WithDescription(Format.Code(data.Time.ToString(Culture))) .WithDescription(Format.Code(data.Time.ToString(Culture)))
.AddField(GetText(strs.location), string.Join('\n', data.Address.Split(", ")), true) .AddField(GetText(strs.location), string.Join('\n', data.Address.Split(", ")), true)
.AddField(GetText(strs.timezone), data.TimeZoneName, true); .AddField(GetText(strs.timezone), data.TimeZoneName, true);
await Response().Embed(eb).SendAsync(); await Response().Embed(eb).SendAsync();
} }
@ -162,14 +131,16 @@ public partial class Searches : EllieModule<SearchesService>
await Response() await Response()
.Embed(_sender.CreateEmbed() .Embed(_sender.CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle(movie.Title) .WithTitle(movie.Title)
.WithUrl($"https://www.imdb.com/title/{movie.ImdbId}/") .WithUrl($"https://www.imdb.com/title/{movie.ImdbId}/")
.WithDescription(movie.Plot.TrimTo(1000)) .WithDescription(movie.Plot.TrimTo(1000))
.AddField("Rating", movie.ImdbRating, true) .AddField("Rating", movie.ImdbRating, true)
.AddField("Genre", movie.Genre, true) .AddField("Genre", movie.Genre, true)
.AddField("Year", movie.Year, true) .AddField("Year", movie.Year, true)
.WithImageUrl(Uri.IsWellFormedUriString(movie.Poster, UriKind.Absolute) ? movie.Poster : null)) .WithImageUrl(Uri.IsWellFormedUriString(movie.Poster, UriKind.Absolute)
? movie.Poster
: null))
.SendAsync(); .SendAsync();
} }
@ -196,12 +167,12 @@ public partial class Searches : EllieModule<SearchesService>
} }
[Cmd] [Cmd]
public async Task Lmgtfy([Leftover] string ffs = null) public async Task Lmgtfy([Leftover] string smh = null)
{ {
if (!await ValidateQuery(ffs)) if (!await ValidateQuery(smh))
return; return;
var shortenedUrl = await _google.ShortenUrl($"https://letmegooglethat.com/?q={Uri.EscapeDataString(ffs)}"); var shortenedUrl = await _google.ShortenUrl($"https://letmegooglethat.com/?q={Uri.EscapeDataString(smh)}");
await Response().Confirm($"<{shortenedUrl}>").SendAsync(); await Response().Confirm($"<{shortenedUrl}>").SendAsync();
} }
@ -244,9 +215,9 @@ public partial class Searches : EllieModule<SearchesService>
await Response() await Response()
.Embed(_sender.CreateEmbed() .Embed(_sender.CreateEmbed()
.WithOkColor() .WithOkColor()
.AddField(GetText(strs.original_url), $"<{query}>") .AddField(GetText(strs.original_url), $"<{query}>")
.AddField(GetText(strs.short_url), $"<{shortLink}>")) .AddField(GetText(strs.short_url), $"<{shortLink}>"))
.SendAsync(); .SendAsync();
} }
@ -266,13 +237,13 @@ public partial class Searches : EllieModule<SearchesService>
} }
var embed = _sender.CreateEmbed() var embed = _sender.CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle(card.Name) .WithTitle(card.Name)
.WithDescription(card.Description) .WithDescription(card.Description)
.WithImageUrl(card.ImageUrl) .WithImageUrl(card.ImageUrl)
.AddField(GetText(strs.store_url), card.StoreUrl, true) .AddField(GetText(strs.store_url), card.StoreUrl, true)
.AddField(GetText(strs.cost), card.ManaCost, true) .AddField(GetText(strs.cost), card.ManaCost, true)
.AddField(GetText(strs.types), card.Types, true); .AddField(GetText(strs.types), card.Types, true);
await Response().Embed(embed).SendAsync(); await Response().Embed(embed).SendAsync();
} }
@ -331,10 +302,10 @@ public partial class Searches : EllieModule<SearchesService>
{ {
var item = items[0]; var item = items[0];
return _sender.CreateEmbed() return _sender.CreateEmbed()
.WithOkColor() .WithOkColor()
.WithUrl(item.Permalink) .WithUrl(item.Permalink)
.WithTitle(item.Word) .WithTitle(item.Word)
.WithDescription(item.Definition); .WithDescription(item.Definition);
}) })
.SendAsync(); .SendAsync();
return; return;
@ -354,100 +325,82 @@ public partial class Searches : EllieModule<SearchesService>
if (!await ValidateQuery(word)) if (!await ValidateQuery(word))
return; return;
using var http = _httpFactory.CreateClient();
string res; var maybeItems = await _service.GetDefinitionsAsync(word);
try
if (!maybeItems.TryPickT0(out var defs, out var error))
{ {
res = await _cache.GetOrCreateAsync($"define_{word}", await HandleErrorAsync(error);
e => return;
{
e.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(12);
return http.GetStringAsync("https://api.pearson.com/v2/dictionaries/entries?headword="
+ WebUtility.UrlEncode(word));
});
var responseModel = JsonConvert.DeserializeObject<DefineModel>(res);
var data = responseModel.Results
.Where(x => x.Senses is not null
&& x.Senses.Count > 0
&& x.Senses[0].Definition is not null)
.Select(x => (Sense: x.Senses[0], x.PartOfSpeech))
.ToList();
if (!data.Any())
{
Log.Warning("Definition not found: {Word}", word);
await Response().Error(strs.define_unknown).SendAsync();
}
var col = data.Select(x => (
Definition: x.Sense.Definition is string
? x.Sense.Definition.ToString()
: ((JArray)JToken.Parse(x.Sense.Definition.ToString())).First.ToString(),
Example: x.Sense.Examples is null || x.Sense.Examples.Count == 0
? string.Empty
: x.Sense.Examples[0].Text, Word: word,
WordType: string.IsNullOrWhiteSpace(x.PartOfSpeech) ? "-" : x.PartOfSpeech))
.ToList();
Log.Information("Sending {Count} definition for: {Word}", col.Count, word);
await Response()
.Paginated()
.Items(col)
.PageSize(1)
.Page((items, _) =>
{
var model = items.First();
var embed = _sender.CreateEmbed()
.WithDescription(ctx.User.Mention)
.AddField(GetText(strs.word), model.Word, true)
.AddField(GetText(strs._class), model.WordType, true)
.AddField(GetText(strs.definition), model.Definition)
.WithOkColor();
if (!string.IsNullOrWhiteSpace(model.Example))
embed.AddField(GetText(strs.example), model.Example);
return embed;
})
.SendAsync();
}
catch (Exception ex)
{
Log.Error(ex, "Error retrieving definition data for: {Word}", word);
} }
await Response()
.Paginated()
.Items(defs)
.PageSize(1)
.Page((items, _) =>
{
var model = items.First();
var embed = _sender.CreateEmbed()
.WithDescription(ctx.User.Mention)
.AddField(GetText(strs.word), model.Word, true)
.AddField(GetText(strs._class), model.WordType, true)
.AddField(GetText(strs.definition), model.Definition)
.WithOkColor();
if (!string.IsNullOrWhiteSpace(model.Example))
embed.AddField(GetText(strs.example), model.Example);
return embed;
})
.SendAsync();
} }
[Cmd] [Cmd]
public async Task Catfact() public async Task Catfact()
{ {
using var http = _httpFactory.CreateClient(); var maybeFact = await _service.GetCatFactAsync();
var response = await http.GetStringAsync("https://catfact.ninja/fact");
if (!maybeFact.TryPickT0(out var fact, out var error))
{
await HandleErrorAsync(error);
return;
}
var fact = JObject.Parse(response)["fact"].ToString();
await Response().Confirm("🐈" + GetText(strs.catfact), fact).SendAsync(); await Response().Confirm("🐈" + GetText(strs.catfact), fact).SendAsync();
} }
[Cmd] [Cmd]
public async Task Wiki([Leftover] string query = null) public async Task Wiki([Leftover] string query)
{ {
query = query?.Trim(); query = query?.Trim();
if (!await ValidateQuery(query)) if (!await ValidateQuery(query))
return; return;
using var http = _httpFactory.CreateClient(); var maybeRes = await _service.GetWikipediaPageAsync(query);
var result = await http.GetStringAsync( if (!maybeRes.TryPickT0(out var res, out var error))
"https://en.wikipedia.org//w/api.php?action=query&format=json&prop=info&redirects=1&formatversion=2&inprop=url&titles=" {
+ Uri.EscapeDataString(query)); await HandleErrorAsync(error);
var data = JsonConvert.DeserializeObject<WikipediaApiModel>(result); return;
if (data.Query.Pages[0].Missing || string.IsNullOrWhiteSpace(data.Query.Pages[0].FullUrl)) }
await Response().Error(strs.wiki_page_not_found).SendAsync();
else var data = res.Data;
await Response().Text(data.Query.Pages[0].FullUrl).SendAsync(); await Response().Text(data.Url).SendAsync();
}
public Task<IUserMessage> HandleErrorAsync(ErrorType error)
{
var errorKey = error switch
{
ErrorType.ApiKeyMissing => strs.api_key_missing,
ErrorType.InvalidInput => strs.invalid_input,
ErrorType.NotFound => strs.not_found,
ErrorType.Unknown => strs.error_occured,
_ => strs.error_occured,
};
return Response().Error(errorKey).SendAsync();
} }
[Cmd] [Cmd]
@ -473,18 +426,17 @@ public partial class Searches : EllieModule<SearchesService>
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
public async Task Avatar([Leftover] IGuildUser usr = null) public async Task Avatar([Leftover] IGuildUser usr = null)
{ {
if (usr is null) usr ??= (IGuildUser)ctx.User;
usr = (IGuildUser)ctx.User;
var avatarUrl = usr.RealAvatarUrl(2048); var avatarUrl = usr.RealAvatarUrl(2048);
await Response() await Response()
.Embed( .Embed(
_sender.CreateEmbed() _sender.CreateEmbed()
.WithOkColor() .WithOkColor()
.AddField("Username", usr.ToString()) .AddField("Username", usr.ToString())
.AddField("Avatar Url", avatarUrl) .AddField("Avatar Url", avatarUrl)
.WithThumbnailUrl(avatarUrl.ToString())) .WithThumbnailUrl(avatarUrl.ToString()))
.SendAsync(); .SendAsync();
} }
@ -497,35 +449,16 @@ public partial class Searches : EllieModule<SearchesService>
return; return;
} }
await ctx.Channel.TriggerTypingAsync(); var maybeRes = await _service.GetWikiaPageAsync(target, query);
using var http = _httpFactory.CreateClient();
http.DefaultRequestHeaders.Clear();
try
{
var res = await http.GetStringAsync($"https://{Uri.EscapeDataString(target)}.fandom.com/api.php"
+ "?action=query"
+ "&format=json"
+ "&list=search"
+ $"&srsearch={Uri.EscapeDataString(query)}"
+ "&srlimit=1");
var items = JObject.Parse(res);
var title = items["query"]?["search"]?.FirstOrDefault()?["title"]?.ToString();
if (string.IsNullOrWhiteSpace(title)) if (!maybeRes.TryPickT0(out var res, out var error))
{
await Response().Error(strs.wikia_error).SendAsync();
return;
}
var url = Uri.EscapeDataString($"https://{target}.fandom.com/wiki/{title}");
var response = $@"`{GetText(strs.title)}` {title.SanitizeMentions()}
`{GetText(strs.url)}:` {url}";
await Response().Text(response).SendAsync();
}
catch
{ {
await Response().Error(strs.wikia_error).SendAsync(); await HandleErrorAsync(error);
return;
} }
var response = $"### {res.Title}\n{res.Url}";
await Response().Text(response).Sanitize().SendAsync();
} }
[Cmd] [Cmd]
@ -543,16 +476,6 @@ public partial class Searches : EllieModule<SearchesService>
return; return;
} }
//var embed = _sender.CreateEmbed()
// .WithOkColor()
// .WithDescription(gameData.ShortDescription)
// .WithTitle(gameData.Name)
// .WithUrl(gameData.Link)
// .WithImageUrl(gameData.HeaderImage)
// .AddField(GetText(strs.genres), gameData.TotalEpisodes.ToString(), true)
// .AddField(GetText(strs.price), gameData.IsFree ? GetText(strs.FREE) : game, true)
// .AddField(GetText(strs.links), gameData.GetGenresString(), true)
// .WithFooter(GetText(strs.recommendations(gameData.TotalRecommendations)));
await Response().Text($"https://store.steampowered.com/app/{appId}").SendAsync(); await Response().Text($"https://store.steampowered.com/app/{appId}").SendAsync();
} }

View file

@ -2,13 +2,8 @@
using EllieBot.Modules.Searches.Common; using EllieBot.Modules.Searches.Common;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using SixLabors.Fonts; using System.Text.Json;
using SixLabors.ImageSharp; using OneOf;
using SixLabors.ImageSharp.Drawing.Processing;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using Color = SixLabors.ImageSharp.Color;
using Image = SixLabors.ImageSharp.Image;
namespace EllieBot.Modules.Searches.Services; namespace EllieBot.Modules.Searches.Services;
@ -72,56 +67,6 @@ public class SearchesService : IEService
} }
} }
public async Task<Stream> GetRipPictureAsync(string text, Uri imgUrl)
=> (await GetRipPictureFactory(text, imgUrl)).ToStream();
private void DrawAvatar(Image bg, Image avatarImage)
=> bg.Mutate(x => x.Grayscale().DrawImage(avatarImage, new(83, 139), new GraphicsOptions()));
public async Task<byte[]> GetRipPictureFactory(string text, Uri avatarUrl)
{
using var bg = Image.Load<Rgba32>(await _imgs.GetRipBgAsync());
var result = await _c.GetImageDataAsync(avatarUrl);
if (!result.TryPickT0(out var data, out _))
{
using var http = _httpFactory.CreateClient();
data = await http.GetByteArrayAsync(avatarUrl);
using (var avatarImg = Image.Load<Rgba32>(data))
{
avatarImg.Mutate(x => x.Resize(85, 85).ApplyRoundedCorners(42));
await using var avStream = await avatarImg.ToStreamAsync();
data = avStream.ToArray();
DrawAvatar(bg, avatarImg);
}
await _c.SetImageDataAsync(avatarUrl, data);
}
else
{
using var avatarImg = Image.Load<Rgba32>(data);
DrawAvatar(bg, avatarImg);
}
bg.Mutate(x => x.DrawText(
new TextOptions(_fonts.RipFont)
{
HorizontalAlignment = HorizontalAlignment.Center,
FallbackFontFamilies = _fonts.FallBackFonts,
Origin = new(bg.Width / 2, 225),
},
text,
Color.Black));
//flowa
using (var flowers = Image.Load(await _imgs.GetRipOverlayAsync()))
{
bg.Mutate(x => x.DrawImage(flowers, new(0, 0), new GraphicsOptions()));
}
await using var stream = bg.ToStream();
return stream.ToArray();
}
public async Task<WeatherData> GetWeatherDataAsync(string query) public async Task<WeatherData> GetWeatherDataAsync(string query)
{ {
query = query.Trim().ToLowerInvariant(); query = query.Trim().ToLowerInvariant();
@ -153,26 +98,26 @@ public class SearchesService : IEService
} }
} }
public Task<((string Address, DateTime Time, string TimeZoneName), TimeErrors?)> GetTimeDataAsync(string arg) public Task<((string Address, DateTime Time, string TimeZoneName), ErrorType?)> GetTimeDataAsync(string arg)
=> GetTimeDataFactory(arg); => GetTimeDataFactory(arg);
//return _cache.GetOrAddCachedDataAsync($"ellie_time_{arg}", //return _cache.GetOrAddCachedDataAsync($"ellie_time_{arg}",
// GetTimeDataFactory, // GetTimeDataFactory,
// arg, // arg,
// TimeSpan.FromMinutes(1)); // TimeSpan.FromMinutes(1));
private async Task<((string Address, DateTime Time, string TimeZoneName), TimeErrors?)> GetTimeDataFactory( private async Task<((string Address, DateTime Time, string TimeZoneName), ErrorType?)> GetTimeDataFactory(
string query) string query)
{ {
query = query.Trim(); query = query.Trim();
if (string.IsNullOrEmpty(query)) if (string.IsNullOrEmpty(query))
return (default, TimeErrors.InvalidInput); return (default, ErrorType.InvalidInput);
var locIqKey = _creds.GetCreds().LocationIqApiKey; var locIqKey = _creds.GetCreds().LocationIqApiKey;
var tzDbKey = _creds.GetCreds().TimezoneDbApiKey; var tzDbKey = _creds.GetCreds().TimezoneDbApiKey;
if (string.IsNullOrWhiteSpace(locIqKey) || string.IsNullOrWhiteSpace(tzDbKey)) if (string.IsNullOrWhiteSpace(locIqKey) || string.IsNullOrWhiteSpace(tzDbKey))
return (default, TimeErrors.ApiKeyMissing); return (default, ErrorType.ApiKeyMissing);
try try
{ {
@ -196,7 +141,7 @@ public class SearchesService : IEService
if (responses is null || responses.Length == 0) if (responses is null || responses.Length == 0)
{ {
Log.Warning("Geocode lookup failed for: {Query}", query); Log.Warning("Geocode lookup failed for: {Query}", query);
return (default, TimeErrors.NotFound); return (default, ErrorType.NotFound);
} }
var geoData = responses[0]; var geoData = responses[0];
@ -220,7 +165,7 @@ public class SearchesService : IEService
catch (Exception ex) catch (Exception ex)
{ {
Log.Error(ex, "Weather error: {Message}", ex.Message); Log.Error(ex, "Weather error: {Message}", ex.Message);
return (default, TimeErrors.NotFound); return (default, ErrorType.NotFound);
} }
} }
@ -395,12 +340,11 @@ public class SearchesService : IEService
private async Task<OmdbMovie> GetMovieDataFactory(string name) private async Task<OmdbMovie> GetMovieDataFactory(string name)
{ {
using var http = _httpFactory.CreateClient(); using var http = _httpFactory.CreateClient();
var res = await http.GetStringAsync(string.Format("https://omdbapi.nadeko.bot/" var res = await http.GetStringAsync("https://omdbapi.nadeko.bot/"
+ "?t={0}" + $"?t={name.Trim().Replace(' ', '+')}"
+ "&y=" + "&y="
+ "&plot=full" + "&plot=full"
+ "&r=json", + "&r=json");
name.Trim().Replace(' ', '+')));
var movie = JsonConvert.DeserializeObject<OmdbMovie>(res); var movie = JsonConvert.DeserializeObject<OmdbMovie>(res);
if (movie?.Title is null) if (movie?.Title is null)
return null; return null;
@ -454,4 +398,184 @@ public class SearchesService : IEService
return gamesMap[key]; return gamesMap[key];
} }
public async Task<OneOf<WikipediaReply, ErrorType>> GetWikipediaPageAsync(string query)
{
query = query.Trim();
if (string.IsNullOrEmpty(query))
{
return ErrorType.InvalidInput;
}
try
{
var result = await _c.GetOrAddAsync($"wikipedia_{query}",
async () =>
{
using var http = _httpFactory.CreateClient();
http.DefaultRequestHeaders.Clear();
return await http.GetStringAsync(
"https://en.wikipedia.org/w/api.php?action=query"
+ "&format=json"
+ "&prop=info"
+ "&redirects=1"
+ "&formatversion=2"
+ "&inprop=url"
+ "&titles="
+ Uri.EscapeDataString(query));
},
TimeSpan.FromHours(1))
.ConfigureAwait(false);
var data = JsonConvert.DeserializeObject<WikipediaApiModel>(result);
if (data.Query.Pages is null || !data.Query.Pages.Any() || data.Query.Pages.First().Missing)
{
return ErrorType.NotFound;
}
Log.Information("Sending wikipedia url for: {Query}", query);
return new WikipediaReply
{
Data = new()
{
Url = data.Query.Pages[0].FullUrl,
}
};
}
catch (Exception ex)
{
Log.Error(ex, "Error retrieving wikipedia data for: '{Query}'", query);
return ErrorType.Unknown;
}
}
public async Task<OneOf<string, ErrorType>> GetCatFactAsync()
{
using var http = _httpFactory.CreateClient();
var response = await http.GetStringAsync("https://catfact.ninja/fact").ConfigureAwait(false);
var doc = JsonDocument.Parse(response);
if (!doc.RootElement.TryGetProperty("fact", out var factElement))
{
return ErrorType.Unknown;
}
return factElement.ToString();
}
public async Task<OneOf<WikiaResponse, ErrorType>> GetWikiaPageAsync(string target, string query)
{
if (string.IsNullOrWhiteSpace(target) || string.IsNullOrWhiteSpace(query))
{
return ErrorType.InvalidInput;
}
query = Uri.EscapeDataString(query.Trim());
target = Uri.EscapeDataString(target.Trim());
if (string.IsNullOrEmpty(query))
{
return ErrorType.InvalidInput;
}
using var http = _httpFactory.CreateClient();
http.DefaultRequestHeaders.Clear();
try
{
var res = await http.GetStringAsync($"https://{Uri.EscapeDataString(target)}.fandom.com/api.php"
+ "?action=query"
+ "&format=json"
+ "&list=search"
+ $"&srsearch={Uri.EscapeDataString(query)}"
+ "&srlimit=1");
var items = JObject.Parse(res);
var title = items["query"]?["search"]?.FirstOrDefault()?["title"]?.ToString();
if (string.IsNullOrWhiteSpace(title))
{
return ErrorType.NotFound;
}
var url = $"https://{target}.fandom.com/wiki/{title}";
return new WikiaResponse()
{
Url = url,
Title = title,
};
}
catch (Exception ex)
{
Log.Warning(ex, "Error getting wikia page: {Message}", ex.Message);
return ErrorType.Unknown;
}
}
private static TypedKey<string> GetDefineKey(string query)
=> new TypedKey<string>($"define_{query}");
public async Task<OneOf<List<DefineData>, ErrorType>> GetDefinitionsAsync(string query)
{
if (string.IsNullOrEmpty(query))
{
return ErrorType.InvalidInput;
}
query = Uri.EscapeDataString(query);
using var http = _httpFactory.CreateClient();
string res;
try
{
res = await _c.GetOrAddAsync(GetDefineKey(query),
async () => await http.GetStringAsync(
$"https://api.pearson.com/v2/dictionaries/entries?headword={query}"),
TimeSpan.FromHours(12));
var responseModel = JsonConvert.DeserializeObject<DefineModel>(res);
var data = responseModel.Results
.Where(x => x.Senses is not null
&& x.Senses.Count > 0
&& x.Senses[0].Definition is not null)
.Select(x => (Sense: x.Senses[0], x.PartOfSpeech))
.ToList();
if (!data.Any())
{
Log.Warning("Definition not found: {Word}", query);
return ErrorType.NotFound;
}
var items = new List<DefineData>();
foreach (var d in data)
{
items.Add(new DefineData
{
Definition = d.Sense.Definition is JArray { Count: > 0 } defs
? defs[0].ToString()
: d.Sense.Definition.ToString(),
Example = d.Sense.Examples is null || d.Sense.Examples.Count == 0
? string.Empty
: d.Sense.Examples[0].Text,
WordType = string.IsNullOrWhiteSpace(d.PartOfSpeech) ? "-" : d.PartOfSpeech,
Word = query,
});
}
return items.OrderByDescending(x => !string.IsNullOrWhiteSpace(x.Example)).ToList();
}
catch (Exception ex)
{
Log.Error(ex, "Error retrieving definition data for: {Word}", query);
return ErrorType.Unknown;
}
}
} }

View file

@ -1,20 +0,0 @@
#nullable disable
using System.Text.Json.Serialization;
namespace EllieBot.Modules.Searches.Common;
public class BibleVerses
{
public string Error { get; set; }
public BibleVerse[] Verses { get; set; }
}
public class BibleVerse
{
[JsonPropertyName("book_name")]
public string BookName { get; set; }
public int Chapter { get; set; }
public int Verse { get; set; }
public string Text { get; set; }
}

View file

@ -0,0 +1,10 @@
#nullable disable
namespace EllieBot.Modules.Searches.Services;
public sealed class DefineData
{
public required string Definition { get; init; }
public required string Example { get; init; }
public required string WordType { get; init; }
public required string Word { get; init; }
}

View file

@ -0,0 +1,9 @@
namespace EllieBot.Modules.Searches.Services;
public enum ErrorType
{
InvalidInput,
NotFound,
Unknown,
ApiKeyMissing
}

View file

@ -0,0 +1,7 @@
namespace EllieBot.Modules.Searches.Services;
public sealed class WikiaResponse
{
public required string Url { get; init; }
public required string Title { get; init; }
}

View file

@ -0,0 +1,11 @@
namespace EllieBot.Modules.Searches.Services;
public class WikipediaReply
{
public class Info
{
public required string Url { get; init; }
}
public required Info Data { get; init; }
}

View file

@ -0,0 +1,148 @@
using EllieBot.Common.ModuleBehaviors;
namespace EllieBot.Modules.Utility;
public sealed class AfkService : IEService, IReadyExecutor
{
private readonly IBotCache _cache;
private readonly DiscordSocketClient _client;
private readonly MessageSenderService _mss;
private static readonly TimeSpan _maxAfkDuration = 8.Hours();
public AfkService(IBotCache cache, DiscordSocketClient client, MessageSenderService mss)
{
_cache = cache;
_client = client;
_mss = mss;
}
private static TypedKey<string> GetKey(ulong userId)
=> new($"afk:msg:{userId}");
public async Task<bool> SetAfkAsync(ulong userId, string text)
{
var added = await _cache.AddAsync(GetKey(userId), text, _maxAfkDuration, overwrite: true);
async Task StopAfk(SocketMessage socketMessage)
{
try
{
if (socketMessage.Author?.Id == userId)
{
await _cache.RemoveAsync(GetKey(userId));
_client.MessageReceived -= StopAfk;
// write the message saying afk status cleared
if (socketMessage.Channel is ITextChannel tc)
{
_ = Task.Run(async () =>
{
var msg = await _mss.Response(tc).Confirm("AFK message cleared!").SendAsync();
msg.DeleteAfter(5);
});
}
}
}
catch (Exception ex)
{
Log.Warning("Unexpected error occurred while trying to stop afk: {Message}", ex.Message);
}
}
_client.MessageReceived += StopAfk;
_ = Task.Run(async () =>
{
await Task.Delay(_maxAfkDuration);
_client.MessageReceived -= StopAfk;
});
return added;
}
public Task OnReadyAsync()
{
_client.MessageReceived += TryTriggerAfkMessage;
return Task.CompletedTask;
}
private Task TryTriggerAfkMessage(SocketMessage arg)
{
if (arg.Author.IsBot)
return Task.CompletedTask;
if (arg is not IUserMessage uMsg || uMsg.Channel is not ITextChannel tc)
return Task.CompletedTask;
if ((arg.MentionedUsers.Count is 0 or > 3) && uMsg.ReferencedMessage is null)
return Task.CompletedTask;
_ = Task.Run(async () =>
{
var botUser = await tc.Guild.GetCurrentUserAsync();
var perms = botUser.GetPermissions(tc);
if (!perms.SendMessages)
return;
ulong mentionedUserId = 0;
if (arg.MentionedUsers.Count <= 3)
{
foreach (var uid in uMsg.MentionedUserIds)
{
if (uid == arg.Author.Id)
continue;
if (arg.Content.StartsWith($"<@{uid}>") || arg.Content.StartsWith($"<@!{uid}>"))
{
mentionedUserId = uid;
break;
}
}
}
if (mentionedUserId == 0)
{
if (uMsg.ReferencedMessage?.Author?.Id is not ulong repliedUserId)
{
return;
}
mentionedUserId = repliedUserId;
}
try
{
var result = await _cache.GetAsync(GetKey(mentionedUserId));
if (result.TryPickT0(out var msg, out _))
{
var st = SmartText.CreateFrom(msg);
st = "The user is AFK: " + st;
var toDelete = await _mss.Response(arg.Channel)
.Message(uMsg)
.Text(st)
.Sanitize(false)
.SendAsync();
toDelete.DeleteAfter(30);
}
}
catch (HttpException ex)
{
Log.Warning("Error in afk service: {Message}", ex.Message);
}
});
return Task.CompletedTask;
}
}

View file

@ -1,4 +1,4 @@
#nullable disable using LinqToDB.Reflection;
using EllieBot.Modules.Utility.Services; using EllieBot.Modules.Utility.Services;
using Newtonsoft.Json; using Newtonsoft.Json;
using System.Diagnostics; using System.Diagnostics;
@ -7,6 +7,7 @@ using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Microsoft.CodeAnalysis.CSharp.Scripting; using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting; using Microsoft.CodeAnalysis.Scripting;
using EllieBot.Modules.Games.Hangman;
using EllieBot.Modules.Searches.Common; using EllieBot.Modules.Searches.Common;
namespace EllieBot.Modules.Utility; namespace EllieBot.Modules.Utility;
@ -41,6 +42,7 @@ public partial class Utility : EllieModule
private readonly IHttpClientFactory _httpFactory; private readonly IHttpClientFactory _httpFactory;
private readonly VerboseErrorsService _veService; private readonly VerboseErrorsService _veService;
private readonly IServiceProvider _services; private readonly IServiceProvider _services;
private readonly AfkService _afkService;
public Utility( public Utility(
DiscordSocketClient client, DiscordSocketClient client,
@ -50,7 +52,8 @@ public partial class Utility : EllieModule
DownloadTracker tracker, DownloadTracker tracker,
IHttpClientFactory httpFactory, IHttpClientFactory httpFactory,
VerboseErrorsService veService, VerboseErrorsService veService,
IServiceProvider services) IServiceProvider services,
AfkService afkService)
{ {
_client = client; _client = client;
_coord = coord; _coord = coord;
@ -60,6 +63,7 @@ public partial class Utility : EllieModule
_httpFactory = httpFactory; _httpFactory = httpFactory;
_veService = veService; _veService = veService;
_services = services; _services = services;
_afkService = afkService;
} }
[Cmd] [Cmd]
@ -99,7 +103,7 @@ public partial class Utility : EllieModule
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
public async Task WhosPlaying([Leftover] string game) public async Task WhosPlaying([Leftover] string? game)
{ {
game = game?.Trim().ToUpperInvariant(); game = game?.Trim().ToUpperInvariant();
if (string.IsNullOrWhiteSpace(game)) if (string.IsNullOrWhiteSpace(game))
@ -140,7 +144,7 @@ public partial class Utility : EllieModule
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[Priority(0)] [Priority(0)]
public async Task InRole(int page, [Leftover] IRole role = null) public async Task InRole(int page, [Leftover] IRole? role = null)
{ {
if (--page < 0) if (--page < 0)
return; return;
@ -178,7 +182,7 @@ public partial class Utility : EllieModule
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[Priority(1)] [Priority(1)]
public Task InRole([Leftover] IRole role = null) public Task InRole([Leftover] IRole? role = null)
=> InRole(1, role); => InRole(1, role);
[Cmd] [Cmd]
@ -218,7 +222,7 @@ public partial class Utility : EllieModule
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
public async Task UserId([Leftover] IGuildUser target = null) public async Task UserId([Leftover] IGuildUser? target = null)
{ {
var usr = target ?? ctx.User; var usr = target ?? ctx.User;
await Response() await Response()
@ -248,7 +252,7 @@ public partial class Utility : EllieModule
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
public async Task Roles(IGuildUser target, int page = 1) public async Task Roles(IGuildUser? target, int page = 1)
{ {
var guild = ctx.Guild; var guild = ctx.Guild;
@ -301,7 +305,7 @@ public partial class Utility : EllieModule
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
public async Task ChannelTopic([Leftover] ITextChannel channel = null) public async Task ChannelTopic([Leftover] ITextChannel? channel = null)
{ {
if (channel is null) if (channel is null)
channel = (ITextChannel)ctx.Channel; channel = (ITextChannel)ctx.Channel;
@ -382,7 +386,7 @@ public partial class Utility : EllieModule
[BotPerm(GuildPerm.ManageEmojisAndStickers)] [BotPerm(GuildPerm.ManageEmojisAndStickers)]
[UserPerm(GuildPerm.ManageEmojisAndStickers)] [UserPerm(GuildPerm.ManageEmojisAndStickers)]
[Priority(0)] [Priority(0)]
public async Task EmojiAdd(string name, string url = null) public async Task EmojiAdd(string name, string? url = null)
{ {
name = name.Trim(':'); name = name.Trim(':');
@ -456,10 +460,10 @@ public partial class Utility : EllieModule
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[BotPerm(GuildPerm.ManageEmojisAndStickers)] [BotPerm(GuildPerm.ManageEmojisAndStickers)]
[UserPerm(GuildPerm.ManageEmojisAndStickers)] [UserPerm(GuildPerm.ManageEmojisAndStickers)]
public async Task StickerAdd(string name = null, string description = null, params string[] tags) public async Task StickerAdd(string? name = null, string? description = null, params string[] tags)
{ {
string format; string format;
Stream stream = null; Stream? stream = null;
try try
{ {
@ -696,6 +700,19 @@ public partial class Utility : EllieModule
await Response().Confirm(strs.verbose_errors_disabled).SendAsync(); await Response().Confirm(strs.verbose_errors_disabled).SendAsync();
} }
[Cmd]
public async Task Afk([Leftover] string text = "No reason specified.")
{
var succ = await _afkService.SetAfkAsync(ctx.User.Id, text);
if (succ)
{
await Response()
.Confirm(strs.afk_set)
.SendAsync();
}
}
[Cmd] [Cmd]
[NoPublicBot] [NoPublicBot]
[OwnerOnly] [OwnerOnly]

View file

@ -74,10 +74,4 @@ public sealed class ImageCache : IImageCache, IEService
public Task<byte[]?> GetSlotBgAsync() public Task<byte[]?> GetSlotBgAsync()
=> GetImageDataAsync(_ic.Data.Slots.Bg); => GetImageDataAsync(_ic.Data.Slots.Bg);
public Task<byte[]?> GetRipBgAsync()
=> GetImageDataAsync(_ic.Data.Rip.Bg);
public Task<byte[]?> GetRipOverlayAsync()
=> GetImageDataAsync(_ic.Data.Rip.Overlay);
} }

View file

@ -6,5 +6,5 @@ namespace EllieBot.Modules.Permissions;
[StructLayout(LayoutKind.Sequential, Size = 1)] [StructLayout(LayoutKind.Sequential, Size = 1)]
public readonly struct CleverBotResponseStr public readonly struct CleverBotResponseStr
{ {
public const string CLEVERBOT_RESPONSE = "cleverbot:response"; public const string CLEVERBOT_RESPONSE = "CLEVERBOT:RESPONSE";
} }

View file

@ -15,9 +15,11 @@ public sealed class DoAsUserMessage : IUserMessage
_message = message; _message = message;
} }
public ulong Id => _msg.Id; public ulong Id
=> _msg.Id;
public DateTimeOffset CreatedAt => _msg.CreatedAt; public DateTimeOffset CreatedAt
=> _msg.CreatedAt;
public Task DeleteAsync(RequestOptions? options = null) public Task DeleteAsync(RequestOptions? options = null)
{ {
@ -56,67 +58,104 @@ public sealed class DoAsUserMessage : IUserMessage
ReactionType type = ReactionType.Normal) ReactionType type = ReactionType.Normal)
=> _msg.GetReactionUsersAsync(emoji, limit, options, type); => _msg.GetReactionUsersAsync(emoji, limit, options, type);
public IAsyncEnumerable<IReadOnlyCollection<IUser>> GetReactionUsersAsync(IEmote emoji, int limit, public IAsyncEnumerable<IReadOnlyCollection<IUser>> GetReactionUsersAsync(
IEmote emoji,
int limit,
RequestOptions? options = null) RequestOptions? options = null)
{ {
return _msg.GetReactionUsersAsync(emoji, limit, options); return _msg.GetReactionUsersAsync(emoji, limit, options);
} }
public MessageType Type => _msg.Type; public MessageType Type
=> _msg.Type;
public MessageSource Source => _msg.Source; public MessageSource Source
=> _msg.Source;
public bool IsTTS => _msg.IsTTS; public bool IsTTS
=> _msg.IsTTS;
public bool IsPinned => _msg.IsPinned; public bool IsPinned
=> _msg.IsPinned;
public bool IsSuppressed => _msg.IsSuppressed; public bool IsSuppressed
=> _msg.IsSuppressed;
public bool MentionedEveryone => _msg.MentionedEveryone; public bool MentionedEveryone
=> _msg.MentionedEveryone;
public string Content => _message; public string Content
=> _message;
public string CleanContent => _msg.CleanContent; public string CleanContent
=> _msg.CleanContent;
public DateTimeOffset Timestamp => _msg.Timestamp; public DateTimeOffset Timestamp
=> _msg.Timestamp;
public DateTimeOffset? EditedTimestamp => _msg.EditedTimestamp; public DateTimeOffset? EditedTimestamp
=> _msg.EditedTimestamp;
public IMessageChannel Channel => _msg.Channel; public IMessageChannel Channel
=> _msg.Channel;
public IUser Author => _user; public IUser Author
=> _user;
public IThreadChannel Thread => _msg.Thread; public IThreadChannel Thread
=> _msg.Thread;
public IReadOnlyCollection<IAttachment> Attachments => _msg.Attachments; public IReadOnlyCollection<IAttachment> Attachments
=> _msg.Attachments;
public IReadOnlyCollection<IEmbed> Embeds => _msg.Embeds; public IReadOnlyCollection<IEmbed> Embeds
=> _msg.Embeds;
public IReadOnlyCollection<ITag> Tags => _msg.Tags; public IReadOnlyCollection<ITag> Tags
=> _msg.Tags;
public IReadOnlyCollection<ulong> MentionedChannelIds => _msg.MentionedChannelIds; public IReadOnlyCollection<ulong> MentionedChannelIds
=> _msg.MentionedChannelIds;
public IReadOnlyCollection<ulong> MentionedRoleIds => _msg.MentionedRoleIds; public IReadOnlyCollection<ulong> MentionedRoleIds
=> _msg.MentionedRoleIds;
public IReadOnlyCollection<ulong> MentionedUserIds => _msg.MentionedUserIds; public IReadOnlyCollection<ulong> MentionedUserIds
=> _msg.MentionedUserIds;
public MessageActivity Activity => _msg.Activity; public MessageActivity Activity
=> _msg.Activity;
public MessageApplication Application => _msg.Application; public MessageApplication Application
=> _msg.Application;
public MessageReference Reference => _msg.Reference; public MessageReference Reference
=> _msg.Reference;
public IReadOnlyDictionary<IEmote, ReactionMetadata> Reactions => _msg.Reactions; public IReadOnlyDictionary<IEmote, ReactionMetadata> Reactions
=> _msg.Reactions;
public IReadOnlyCollection<IMessageComponent> Components => _msg.Components; public IReadOnlyCollection<IMessageComponent> Components
=> _msg.Components;
public IReadOnlyCollection<IStickerItem> Stickers => _msg.Stickers; public IReadOnlyCollection<IStickerItem> Stickers
=> _msg.Stickers;
public MessageFlags? Flags => _msg.Flags; public MessageFlags? Flags
=> _msg.Flags;
[Obsolete("Obsolete in favor of InteractionMetadata")] [Obsolete("Obsolete in favor of InteractionMetadata")]
public IMessageInteraction Interaction => _msg.Interaction; public IMessageInteraction Interaction
public MessageRoleSubscriptionData RoleSubscriptionData => _msg.RoleSubscriptionData; => _msg.Interaction;
public MessageRoleSubscriptionData RoleSubscriptionData
=> _msg.RoleSubscriptionData;
public PurchaseNotification PurchaseNotification
=> _msg.PurchaseNotification;
public MessageCallData? CallData
=> _msg.CallData;
public Task ModifyAsync(Action<MessageProperties> func, RequestOptions? options = null) public Task ModifyAsync(Action<MessageProperties> func, RequestOptions? options = null)
{ {
@ -138,17 +177,39 @@ public sealed class DoAsUserMessage : IUserMessage
return _msg.CrosspostAsync(options); return _msg.CrosspostAsync(options);
} }
public string Resolve(TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name, public string Resolve(
TagHandling userHandling = TagHandling.Name,
TagHandling channelHandling = TagHandling.Name,
TagHandling roleHandling = TagHandling.Name, TagHandling roleHandling = TagHandling.Name,
TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name) TagHandling everyoneHandling = TagHandling.Ignore,
TagHandling emojiHandling = TagHandling.Name)
{ {
return _msg.Resolve(userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); return _msg.Resolve(userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling);
} }
public MessageResolvedData ResolvedData => _msg.ResolvedData; public Task EndPollAsync(RequestOptions options)
=> _msg.EndPollAsync(options);
public IUserMessage ReferencedMessage => _msg.ReferencedMessage; public IAsyncEnumerable<IReadOnlyCollection<IUser>> GetPollAnswerVotersAsync(
uint answerId,
int? limit = null,
ulong? afterId = null,
RequestOptions? options = null)
=> _msg.GetPollAnswerVotersAsync(
answerId,
limit,
afterId,
options);
public MessageResolvedData ResolvedData
=> _msg.ResolvedData;
public IUserMessage ReferencedMessage
=> _msg.ReferencedMessage;
public IMessageInteractionMetadata InteractionMetadata public IMessageInteractionMetadata InteractionMetadata
=> _msg.InteractionMetadata; => _msg.InteractionMetadata;
public Poll? Poll
=> _msg.Poll;
} }

View file

@ -8,7 +8,7 @@ namespace EllieBot.Common;
public partial class ImageUrls : ICloneable<ImageUrls> public partial class ImageUrls : ICloneable<ImageUrls>
{ {
[Comment("DO NOT CHANGE")] [Comment("DO NOT CHANGE")]
public int Version { get; set; } = 3; public int Version { get; set; } = 4;
public CoinData Coins { get; set; } public CoinData Coins { get; set; }
public Uri[] Currency { get; set; } public Uri[] Currency { get; set; }
@ -16,16 +16,8 @@ public partial class ImageUrls : ICloneable<ImageUrls>
public RategirlData Rategirl { get; set; } public RategirlData Rategirl { get; set; }
public XpData Xp { get; set; } public XpData Xp { get; set; }
//new
public RipData Rip { get; set; }
public SlotData Slots { get; set; } public SlotData Slots { get; set; }
public class RipData
{
public Uri Bg { get; set; }
public Uri Overlay { get; set; }
}
public class SlotData public class SlotData
{ {
public Uri[] Emojis { get; set; } public Uri[] Emojis { get; set; }

View file

@ -70,6 +70,7 @@ public abstract class EllieInteractionBase
{ {
if (_singleUse) if (_singleUse)
_interactionCompletedSource.TrySetResult(true); _interactionCompletedSource.TrySetResult(true);
await ExecuteOnActionAsync(smc); await ExecuteOnActionAsync(smc);
if (!smc.HasResponded) if (!smc.HasResponded)

View file

@ -12,11 +12,6 @@ public class FontProvider : IEService
public FontFamily NotoSans { get; } public FontFamily NotoSans { get; }
//public FontFamily Emojis { get; } //public FontFamily Emojis { get; }
/// <summary>
/// Font used for .rip command
/// </summary>
public Font RipFont { get; }
public List<FontFamily> FallBackFonts { get; } public List<FontFamily> FallBackFonts { get; }
private readonly FontCollection _fonts; private readonly FontCollection _fonts;
@ -54,7 +49,6 @@ public class FontProvider : IEService
FallBackFonts.AddRange(_fonts.AddCollection(font)); FallBackFonts.AddRange(_fonts.AddCollection(font));
} }
RipFont = NotoSans.CreateFont(20, FontStyle.Bold);
DottyFont = FallBackFonts.First(x => x.Name == "dotty"); DottyFont = FallBackFonts.First(x => x.Name == "dotty");
} }
} }

View file

@ -11,7 +11,5 @@ public interface IImageCache
Task<byte[]?> GetDiceAsync(int num); Task<byte[]?> GetDiceAsync(int num);
Task<byte[]?> GetSlotEmojiAsync(int number); Task<byte[]?> GetSlotEmojiAsync(int number);
Task<byte[]?> GetSlotBgAsync(); Task<byte[]?> GetSlotBgAsync();
Task<byte[]?> GetRipBgAsync();
Task<byte[]?> GetRipOverlayAsync();
Task<byte[]?> GetImageDataAsync(Uri url); Task<byte[]?> GetImageDataAsync(Uri url);
} }

View file

@ -1,5 +1,5 @@
# DO NOT CHANGE # DO NOT CHANGE
version: 7 version: 9
# Bot token. Do not share with anyone ever -> https://discordapp.com/developers/applications/ # Bot token. Do not share with anyone ever -> https://discordapp.com/developers/applications/
token: "" token: ""
# List of Ids of the users who have bot owner permissions # List of Ids of the users who have bot owner permissions

View file

@ -947,6 +947,10 @@ warnexpire:
warnclear: warnclear:
- warnclear - warnclear
- warnc - warnc
warndelete:
- warndelete
- warnrm
- warnd
warnpunishlist: warnpunishlist:
- warnpunishlist - warnpunishlist
- warnpl - warnpl
@ -1146,8 +1150,6 @@ discordpermoverridereset:
- dpor - dpor
rafflecur: rafflecur:
- rafflecur - rafflecur
rip:
- rip
timelyset: timelyset:
- timelyset - timelyset
timely: timely:
@ -1410,4 +1412,8 @@ honeypot:
coins: coins:
- coins - coins
- crypto - crypto
- cryptos - cryptos
afk:
- afk
keep:
- keep

View file

@ -57,18 +57,26 @@ raceAnimals:
# Which chatbot API should bot use. # Which chatbot API should bot use.
# 'cleverbot' - bot will use Cleverbot API. # 'cleverbot' - bot will use Cleverbot API.
# 'gpt' - bot will use GPT API # 'gpt' - bot will use GPT API
chatBot: Gpt chatBot: OpenAi
chatGpt: chatGpt:
# Url to any openai api compatible url.
# Make sure to modify the modelName appropriately
# DO NOT add /v1/chat/completions suffix to the url
apiUrl: https://api.openai.com
# Which GPT Model should bot use. # Which GPT Model should bot use.
# gpt35turbo - cheapest # gpt-3.5-turbo - cheapest
# gpt4o - more expensive, higher quality # gpt-4o - more expensive, higher quality
# #
modelName: Gpt35Turbo # If you are using another openai compatible api, you may use any of the models supported by that api
# How should the chat bot behave, what's its personality? (Usage of this counts towards the max tokens) modelName: gpt-3.5-turbo
# How should the chatbot behave, what's its personality?
# This will be sent as a system message.
# Usage of this counts towards the max tokens.
personalityPrompt: You are a chat bot willing to have a conversation with anyone about anything. personalityPrompt: You are a chat bot willing to have a conversation with anyone about anything.
# The maximum number of messages in a conversation that can be remembered. (This will increase the number of tokens used) # The maximum number of messages in a conversation that can be remembered.
# This will increase the number of tokens used.
chatHistory: 5 chatHistory: 5
# The maximum number of tokens to use per GPT API call # The maximum number of tokens to use per OpenAi API call
maxTokens: 100 maxTokens: 100
# The minimum number of tokens to use per GPT API call, such that chat history is removed to make room. # The minimum number of tokens to use per GPT API call, such that chat history is removed to make room.
minTokens: 30 minTokens: 30

View file

@ -576,7 +576,7 @@ deleterole:
- Awesome Role - Awesome Role
params: params:
- role: - role:
desc: "The role being deleted, as identified by its unique identifier." desc: "The role being deleted, as identified by its id."
rolecolor: rolecolor:
desc: Set a role's color using its hex value. Provide no color in order to see the hex value of the color of the specified role. The role you specify has to be lower in the role hierarchy than your highest role. desc: Set a role's color using its hex value. Provide no color in order to see the hex value of the color of the specified role. The role you specify has to be lower in the role hierarchy than your highest role.
ex: ex:
@ -605,11 +605,11 @@ ban:
- time: - time:
desc: "The duration of the temporary ban." desc: "The duration of the temporary ban."
userId: userId:
desc: "The unique identifier of the user being banned." desc: "The id of the user being banned."
msg: msg:
desc: "The reason for the ban is provided in this message." desc: "The reason for the ban is provided in this message."
- userId: - userId:
desc: "The unique identifier of the user being banned." desc: "The id of the user being banned."
msg: msg:
desc: "The reason for the ban is provided in this message." desc: "The reason for the ban is provided in this message."
- user: - user:
@ -627,7 +627,7 @@ softban:
msg: msg:
desc: "The reason for the ban is described in this string." desc: "The reason for the ban is described in this string."
- userId: - userId:
desc: "The unique identifier for the user being banned and then unbanned." desc: "The id of the user being banned and then unbanned."
msg: msg:
desc: "The reason for the ban is described in this string." desc: "The reason for the ban is described in this string."
kick: kick:
@ -843,7 +843,12 @@ setservericon:
- img: - img:
desc: "The URL of the image file to be displayed as the bot's banner." desc: "The URL of the image file to be displayed as the bot's banner."
send: send:
desc: 'Sends a message to a channel or user. Channel or user can be ' desc: |-
Sends a message to a channel or user.
You can write "channel" (literally word 'channel') first followed by the channel id or channel mention, or
You can write "user" (literally word 'user') first followed by the user id or user mention.
After either one of those, specify the message to be sent.
This command can only be used by the Bot Owner.
ex: ex:
- channel 123123123132312 Stop spamming commands plz - channel 123123123132312 Stop spamming commands plz
- user 1231231232132 I can see in the console what you're doing. - user 1231231232132 I can see in the console what you're doing.
@ -1198,7 +1203,7 @@ userblacklist:
- action: - action:
desc: "The type of operation to perform on the user, either adding or removing them from the blacklist." desc: "The type of operation to perform on the user, either adding or removing them from the blacklist."
id: id:
desc: "The unique identifier of the user to be added, removed, or listed." desc: "The id of the user to be added, removed, or listed."
- action: - action:
desc: "The type of operation to perform on the user, either adding or removing them from the blacklist." desc: "The type of operation to perform on the user, either adding or removing them from the blacklist."
usr: usr:
@ -1218,7 +1223,7 @@ channelblacklist:
- action: - action:
desc: "The type of operation to perform on the channel, either adding it to the blacklist or removing it from it." desc: "The type of operation to perform on the channel, either adding it to the blacklist or removing it from it."
id: id:
desc: "The unique identifier of the channel being added, removed, or listed." desc: "The id of the channel being added, removed, or listed."
serverblacklist: serverblacklist:
desc: |- desc: |-
Either [add]s or [rem]oves a server, or servers specified by an ID from a blacklist. Either [add]s or [rem]oves a server, or servers specified by an ID from a blacklist.
@ -1234,7 +1239,7 @@ serverblacklist:
- action: - action:
desc: "The type of operation to perform on the server(s). It can be either adding or removing them from the blacklist." desc: "The type of operation to perform on the server(s). It can be either adding or removing them from the blacklist."
id: id:
desc: "The unique identifier of the server being added, removed, or listed." desc: "The id of the server being added, removed, or listed."
- action: - action:
desc: "The type of operation to perform on the server(s). It can be either adding or removing them from the blacklist." desc: "The type of operation to perform on the server(s). It can be either adding or removing them from the blacklist."
guild: guild:
@ -1291,7 +1296,7 @@ quoteshow:
- 123 - 123
params: params:
- quoteId: - quoteId:
desc: "The unique identifier for the quote being queried." desc: "The id of the quote being queried."
quotesearch: quotesearch:
desc: 'Shows a random quote given a search query. Partially matches in several ways: 1) Only content of any quote, 2) only by author, 3) keyword and content, 3) or keyword and author' desc: 'Shows a random quote given a search query. Partially matches in several ways: 1) Only content of any quote, 2) only by author, 3) keyword and content, 3) or keyword and author'
ex: ex:
@ -1312,14 +1317,14 @@ quoteid:
- 123456 - 123456
params: params:
- quoteId: - quoteId:
desc: "The unique identifier for the quote to be displayed." desc: "The id of the quote to be displayed."
quotedelete: quotedelete:
desc: Deletes a quote with the specified ID. You have to either have the Manage Messages permission or be the creator of the quote to delete it. desc: Deletes a quote with the specified ID. You have to either have the Manage Messages permission or be the creator of the quote to delete it.
ex: ex:
- 123456 - 123456
params: params:
- quoteId: - quoteId:
desc: "The unique identifier for the quote being deleted." desc: "The id of the quote being deleted."
quotedeleteauthor: quotedeleteauthor:
desc: Deletes all quotes by the specified author. If the author is not you, then ManageMessage server permission is required. desc: Deletes all quotes by the specified author. If the author is not you, then ManageMessage server permission is required.
ex: ex:
@ -1361,10 +1366,11 @@ flip:
desc: "The number of times the coin is flipped." desc: "The number of times the coin is flipped."
betflip: betflip:
desc: |- desc: |-
Bet to guess will the result be heads or tails. Bet on the coin flip.
Guessing awards you 1.95x the currency you've bet (rounded up). The result can be heads or tails.
Guessing correctly rewards you with 1.95x of the currency you've bet (rounded up).
Multiplier can be changed by the bot owner. Multiplier can be changed by the bot owner.
You can specify 'all', 'half' or 'X%' instead of the amount to bet that part of your current balance.
ex: ex:
- 5 heads - 5 heads
- 3 t - 3 t
@ -1513,7 +1519,7 @@ betroll:
desc: |- desc: |-
Bets the specified amount of currency and rolls a dice. Bets the specified amount of currency and rolls a dice.
Rolling over 66 yields x2 of your currency, over 90 - x4 and 100 x10. Rolling over 66 yields x2 of your currency, over 90 - x4 and 100 x10.
You can type 'all', 'half' or 'X%' to bet that part of your current balance. You can specify 'all', 'half' or 'X%' instead of the amount to bet that part of your current balance.
ex: ex:
- 5 - 5
params: params:
@ -1524,7 +1530,7 @@ luckyladder:
Bets the specified amount of currency on the lucky ladder. Bets the specified amount of currency on the lucky ladder.
You can stop on one of many different multipliers. You can stop on one of many different multipliers.
The won amount is rounded down to the nearest whole number. The won amount is rounded down to the nearest whole number.
You can type 'all', 'half' or 'X%' to bet that part of your current balance. You can specify 'all', 'half' or 'X%' instead of the amount to bet that part of your current balance.
ex: ex:
- 10 - 10
params: params:
@ -1629,8 +1635,8 @@ choose:
rps: rps:
desc: |- desc: |-
Play a game of Rocket-Paperclip-Scissors with Ellie. Play a game of Rocket-Paperclip-Scissors with Ellie.
You can bet on it. Multiplier is the same as on betflip. You can bet on it. Winning awards you 1.95x of the bet.
You can type 'all', 'half' or 'X%' to bet that part of your current balance. You can specify 'all', 'half' or 'X%' instead of the amount to bet that part of your current balance.
ex: ex:
- r 100 - r 100
- scissors - scissors
@ -1816,7 +1822,7 @@ load:
- 5 - 5
params: params:
- id: - id:
desc: "The unique identifier of the playlist to be loaded." desc: "The id of the playlist to be loaded."
playlists: playlists:
desc: Lists all playlists. Paginated, 20 per page. desc: Lists all playlists. Paginated, 20 per page.
ex: ex:
@ -1830,7 +1836,7 @@ playlistshow:
- 1 - 1
params: params:
- id: - id:
desc: "The unique identifier for the playlist to retrieve songs from." desc: "The id of the playlist to retrieve songs from."
page: page:
desc: "The current page number for the pagination." desc: "The current page number for the pagination."
deleteplaylist: deleteplaylist:
@ -2072,11 +2078,11 @@ image:
- query: - query:
desc: "The search term used to retrieve the desired image." desc: "The search term used to retrieve the desired image."
lmgtfy: lmgtfy:
desc: Google something for an idiot. desc: Google something for a baka.
ex: ex:
- query - How to eat a banana
params: params:
- ffs: - smh:
desc: "The search query to be entered into the search engine." desc: "The search query to be entered into the search engine."
google: google:
desc: Get a Google search link for some terms. desc: Get a Google search link for some terms.
@ -2226,7 +2232,7 @@ currencytransaction:
- 3yvd - 3yvd
params: params:
- id: - id:
desc: "The unique identifier for the transaction being queried." desc: "The id of the transaction being queried."
listperms: listperms:
desc: Lists whole permission chain with their indexes. You can specify an optional page number if there are a lot of permissions. desc: Lists whole permission chain with their indexes. You can specify an optional page number if there are a lot of permissions.
ex: ex:
@ -2712,9 +2718,10 @@ betstats:
slot: slot:
desc: |- desc: |-
Play Ellie slots by placing your bet. Play Ellie slots by placing your bet.
You can type 'all', 'half' or 'X%' to bet that part of your current balance. You can specify 'all', 'half' or 'X%' instead of the amount to bet that part of your current balance.
ex: ex:
- 5 - 5
- 'all'
params: params:
- amount: - amount:
desc: "The amount of currency to bet." desc: "The amount of currency to bet."
@ -2755,7 +2762,7 @@ waifutransfer:
newOwner: newOwner:
desc: "The user to whom ownership of the waifu is being transferred." desc: "The user to whom ownership of the waifu is being transferred."
waifugift: waifugift:
desc: -| desc: |-
Gift an item to a waifu user. Gift an item to a waifu user.
The waifu's value will be increased by the percentage of the gift's value. The waifu's value will be increased by the percentage of the gift's value.
You can optionally prefix the gift with a multiplier to gift the item that many times. You can optionally prefix the gift with a multiplier to gift the item that many times.
@ -3171,6 +3178,13 @@ warnclear:
desc: "The ID of the user whose warnings are being cleared." desc: "The ID of the user whose warnings are being cleared."
index: index:
desc: "The index of the warning to be cleared, or 0 to clear all warnings." desc: "The index of the warning to be cleared, or 0 to clear all warnings."
warndelete:
desc: Deletes a warning from a user by its index.
ex:
- 3
params:
- index:
desc: "The index of the warning to be deleted."
warnpunishlist: warnpunishlist:
desc: Lists punishments for warnings. desc: Lists punishments for warnings.
ex: ex:
@ -3761,11 +3775,15 @@ expredit:
- 123 I'm a magical girl - 123 I'm a magical girl
params: params:
- id: - id:
desc: "The unique identifier for the expression being edited." desc: "The id of the expression being edited."
message: message:
desc: "The text that will replace the original response in the expression's output." desc: "The text that will replace the original response in the expression's output."
say: say:
desc: Bot will send the message you typed in the specified channel. If you omit the channel name, it will send the message in the current channel. Supports embeds. desc: |-
Make the bot say something, or in other words, make the bot send the message.
You can optionally specify the channel where the bot will send the message.
If you omit the channel name, it will send the message in the current channel.
Supports embeds.
ex: ex:
- hi - hi
- '#chat hi' - '#chat hi'
@ -3867,13 +3885,6 @@ rafflecur:
desc: "The minimum or maximum amount of currency that can be used for betting." desc: "The minimum or maximum amount of currency that can be used for betting."
mixed: mixed:
desc: "The parameter determines whether the raffle operates in \"fixed\" or \"proportional\" mode." desc: "The parameter determines whether the raffle operates in \"fixed\" or \"proportional\" mode."
rip:
desc: Shows the inevitable fate of someone.
ex:
- '@Someone'
params:
- usr:
desc: "The user whose fate is being revealed."
autodisconnect: autodisconnect:
desc: Toggles whether the bot should disconnect from the voice channel once it's done playing all of the songs and queue repeat option is set to `none`. desc: Toggles whether the bot should disconnect from the voice channel once it's done playing all of the songs and queue repeat option is set to `none`.
ex: ex:
@ -4070,7 +4081,7 @@ xpshopbuy:
- type: - type:
desc: "The type of item to purchase, such as a skill or a cosmetic." desc: "The type of item to purchase, such as a skill or a cosmetic."
key: key:
desc: "The unique identifier for the item being purchased." desc: "The id of the item being purchased."
xpshopuse: xpshopuse:
desc: Use a previously purchased item from the xp shop by specifying the type and the key of the item. desc: Use a previously purchased item from the xp shop by specifying the type and the key of the item.
ex: ex:
@ -4080,7 +4091,7 @@ xpshopuse:
- type: - type:
desc: "The type of item to be used, such as an experience point or a skill upgrade." desc: "The type of item to be used, such as an experience point or a skill upgrade."
key: key:
desc: "The unique identifier for the item in the XP shop that you want to use." desc: "The id of the item in the XP shop that you want to use."
bible: bible:
desc: Shows bible verse. You need to supply book name and chapter:verse desc: Shows bible verse. You need to supply book name and chapter:verse
ex: ex:
@ -4108,13 +4119,13 @@ edit:
- '#other-channel 771562360594628608 {{"description":"hello"}}' - '#other-channel 771562360594628608 {{"description":"hello"}}'
params: params:
- messageId: - messageId:
desc: "The unique identifier of the message being edited." desc: "The id of the message being edited."
text: text:
desc: "The new text content of the edited message." desc: "The new text content of the edited message."
- channel: - channel:
desc: "The target channel where the edited message will be sent or updated in." desc: "The target channel where the edited message will be sent or updated in."
messageId: messageId:
desc: "The unique identifier of the message being edited." desc: "The id of the message being edited."
text: text:
desc: "The new text content of the edited message." desc: "The new text content of the edited message."
delete: delete:
@ -4125,13 +4136,13 @@ delete:
- 771562360594628608 5m - 771562360594628608 5m
params: params:
- messageId: - messageId:
desc: "The unique identifier of a specific message within a channel, used to target the deletion operation." desc: "The id of a specific message within a channel, used to target the deletion operation."
time: time:
desc: "The duration after which the message should be automatically deleted." desc: "The duration after which the message should be automatically deleted."
- channel: - channel:
desc: "The channel where the message is located or should be searched for." desc: "The channel where the message is located or should be searched for."
messageId: messageId:
desc: "The unique identifier of a specific message within a channel, used to target the deletion operation." desc: "The id of a specific message within a channel, used to target the deletion operation."
time: time:
desc: "The duration after which the message should be automatically deleted." desc: "The duration after which the message should be automatically deleted."
roleid: roleid:
@ -4269,7 +4280,7 @@ bankbalance:
Bot Owner can also check another user's bank balance. Bot Owner can also check another user's bank balance.
ex: ex:
- '' - ''
- '@User' - '@User'
params: params:
- {} - {}
banktake: banktake:
@ -4284,7 +4295,7 @@ banktake:
- amount: - amount:
desc: "The total value of funds being withdrawn." desc: "The total value of funds being withdrawn."
userId: userId:
desc: "The unique identifier for the user whose account is being accessed." desc: "The id of the user whose account is being accessed."
bankaward: bankaward:
desc: Award the specified amount of currency to a user's bank desc: Award the specified amount of currency to a user's bank
ex: ex:
@ -4334,6 +4345,7 @@ betdraw:
You can specify `r` or `b` for red or black, and `h` or `l` for high or low. You can specify `r` or `b` for red or black, and `h` or `l` for high or low.
You can specify only h/l or only r/b or both. You can specify only h/l or only r/b or both.
Returns are high but **7 always loses**. Returns are high but **7 always loses**.
You can specify 'all', 'half' or 'X%' instead of the amount to bet that part of your current balance.
ex: ex:
- 50 r - 50 r
- 200 b l - 200 b l
@ -4474,7 +4486,7 @@ todoedit:
- abc This is an updated entry - abc This is an updated entry
params: params:
- todoId: - todoId:
desc: "The unique identifier for the todo item being edited." desc: "The id of the todo item being edited."
newMessage: newMessage:
desc: "The text of a new task description or update to an existing one." desc: "The text of a new task description or update to an existing one."
todocomplete: todocomplete:
@ -4483,14 +4495,14 @@ todocomplete:
- 4a - 4a
params: params:
- todoId: - todoId:
desc: "The unique identifier for the todo item being marked as completed." desc: "The id of the todo item being marked as completed."
tododelete: tododelete:
desc: Deletes a todo with the specified ID. desc: Deletes a todo with the specified ID.
ex: ex:
- abc - abc
params: params:
- todoId: - todoId:
desc: "The unique identifier for the todo item being deleted." desc: "The id of the todo item being deleted."
todoclear: todoclear:
desc: Deletes all unarchived todos. desc: Deletes all unarchived todos.
ex: ex:
@ -4524,7 +4536,7 @@ todoshow:
- 4a - 4a
params: params:
- todoId: - todoId:
desc: "The unique identifier for the todo item being displayed." desc: "The id of the todo item being displayed."
todoarchivedelete: todoarchivedelete:
desc: Deletes the archived todo list with the specified ID. desc: Deletes the archived todo list with the specified ID.
ex: ex:
@ -4569,3 +4581,21 @@ coins:
params: params:
- page: - page:
desc: "Page number to show. Starts at 1." desc: "Page number to show. Starts at 1."
afk:
desc: |-
Toggles AFK status for yourself with the specified message.
If you don't provide a message it default to a generic one.
Anyone @ mentioning you in any server will receive the afk message.
This will only work if the other user's message starts with the mention.
ex:
- ''
params:
- msg:
desc: "The message to send when someone pings you."
keep:
desc: |-
The current serve, won't be deleted from Ellie's database during the purge.
ex:
- ''
params:
- {}

View file

@ -38,6 +38,7 @@
"prune_cancelled": "Pruning was cancelled.", "prune_cancelled": "Pruning was cancelled.",
"prune_not_found": "No active prune was found on this server.", "prune_not_found": "No active prune was found on this server.",
"prune_progress": "Pruning... {0}/{1} messages deleted.", "prune_progress": "Pruning... {0}/{1} messages deleted.",
"prune_already_running": "A prune is already running on this server.",
"timeoutdm": "You have been timed out in {0} server.\nReason: {1}", "timeoutdm": "You have been timed out in {0} server.\nReason: {1}",
"timedout_user": "User Timed Out", "timedout_user": "User Timed Out",
"remove_roles_pl": "have had their roles removed", "remove_roles_pl": "have had their roles removed",
@ -701,6 +702,7 @@
"warn_count": "{0} current, {1} total", "warn_count": "{0} current, {1} total",
"warnlog_for": "Warnlog for {0}", "warnlog_for": "Warnlog for {0}",
"warnpl_none": "No punishments set.", "warnpl_none": "No punishments set.",
"warning_not_found": "Warning not found.",
"warn_expire_set_delete": "Warnings will be deleted after {0} days.", "warn_expire_set_delete": "Warnings will be deleted after {0} days.",
"warn_expire_set_clear": "Warnings will be cleared after {0} days.", "warn_expire_set_clear": "Warnings will be cleared after {0} days.",
"warn_expire_reset": "Warnings will no longer expire.", "warn_expire_reset": "Warnings will no longer expire.",
@ -711,6 +713,7 @@
"warn_punish_rem": "Having {0} warnings will no longer trigger a punishment.", "warn_punish_rem": "Having {0} warnings will no longer trigger a punishment.",
"warn_punish_set": "I will apply {0} punishment to users with {1} warnings.", "warn_punish_set": "I will apply {0} punishment to users with {1} warnings.",
"warn_punish_set_timed": "I will apply {0} punishment for {2} to users with {1} warnings.", "warn_punish_set_timed": "I will apply {0} punishment for {2} to users with {1} warnings.",
"warning_deleted": "Warning {0} has been deleted.",
"time_new": "Time", "time_new": "Time",
"timezone": "Timezone", "timezone": "Timezone",
"timezone_db_api_key": "You need to activate your TimezoneDB API key. You can do so by clicking on the link you've received in the email with your API key.", "timezone_db_api_key": "You need to activate your TimezoneDB API key. You can do so by clicking on the link you've received in the email with your API key.",
@ -1104,5 +1107,7 @@
"queue_search_results": "Type the number of the search result to queue up that track.", "queue_search_results": "Type the number of the search result to queue up that track.",
"overloads": "Overloads", "overloads": "Overloads",
"honeypot_on": "Honeypot enabled on this channel.", "honeypot_on": "Honeypot enabled on this channel.",
"honeypot_off": "Honeypot disabled." "honeypot_off": "Honeypot disabled.",
"afk_set": "AFK message set. Type a message in any channel to clear.",
"rero_message_not_found": "The specified message wasn't found. Make sure you've specified the message from this channel."
} }