mirror of
https://github.com/RPCS3/discord-bot.git
synced 2026-01-31 01:25:22 +01:00
260 lines
12 KiB
C#
260 lines
12 KiB
C#
using System;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net.Http;
|
|
using System.Threading.Tasks;
|
|
using CompatApiClient.Compression;
|
|
using CompatApiClient.Utils;
|
|
using CompatBot.Commands.Attributes;
|
|
using CompatBot.Commands.Converters;
|
|
using CompatBot.Database;
|
|
using CompatBot.Utils;
|
|
using DSharpPlus;
|
|
using DSharpPlus.CommandsNext;
|
|
using DSharpPlus.CommandsNext.Attributes;
|
|
using DSharpPlus.Entities;
|
|
using DSharpPlus.Interactivity.Extensions;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using SharpCompress.Common;
|
|
using SharpCompress.Compressors;
|
|
using SharpCompress.Compressors.Deflate;
|
|
using SharpCompress.Writers;
|
|
using SharpCompress.Writers.Zip;
|
|
|
|
namespace CompatBot.Commands;
|
|
|
|
[Group("sudo"), RequiresBotSudoerRole]
|
|
[Description("Used to manage bot moderators and sudoers")]
|
|
internal sealed partial class Sudo : BaseCommandModuleCustom
|
|
{
|
|
[Command("say")]
|
|
[Description("Make bot say things. Specify #channel or put message link in the beginning to specify where to reply")]
|
|
public async Task Say(CommandContext ctx, [RemainingText, Description("Message text to send")] string message)
|
|
{
|
|
var msgParts = message.Split(' ', 2, StringSplitOptions.TrimEntries);
|
|
var channel = ctx.Channel;
|
|
DiscordMessage? ogMsg = null;
|
|
if (msgParts.Length > 1)
|
|
{
|
|
if (await ctx.GetMessageAsync(msgParts[0]).ConfigureAwait(false) is DiscordMessage lnk)
|
|
{
|
|
ogMsg = lnk;
|
|
channel = ogMsg.Channel;
|
|
message = msgParts[1];
|
|
}
|
|
else if (await TextOnlyDiscordChannelConverter.ConvertAsync(msgParts[0], ctx).ConfigureAwait(false) is {HasValue: true} ch)
|
|
{
|
|
channel = ch.Value;
|
|
message = msgParts[1];
|
|
}
|
|
}
|
|
|
|
var typingTask = channel.TriggerTypingAsync();
|
|
// simulate bot typing the message at 300 cps
|
|
await Task.Delay(message.Length * 10 / 3).ConfigureAwait(false);
|
|
var msgBuilder = new DiscordMessageBuilder().WithContent(message);
|
|
if (ogMsg is not null)
|
|
msgBuilder.WithReply(ogMsg.Id);
|
|
if (ctx.Message.Attachments.Any())
|
|
{
|
|
try
|
|
{
|
|
await using var memStream = Config.MemoryStreamManager.GetStream();
|
|
using var client = HttpClientFactory.Create(new CompressionMessageHandler());
|
|
await using var requestStream = await client.GetStreamAsync(ctx.Message.Attachments[0].Url!).ConfigureAwait(false);
|
|
await requestStream.CopyToAsync(memStream).ConfigureAwait(false);
|
|
memStream.Seek(0, SeekOrigin.Begin);
|
|
msgBuilder.AddFile(ctx.Message.Attachments[0].FileName, memStream);
|
|
await channel.SendMessageAsync(msgBuilder).ConfigureAwait(false);
|
|
}
|
|
catch { }
|
|
}
|
|
else
|
|
await channel.SendMessageAsync(msgBuilder).ConfigureAwait(false);
|
|
await typingTask.ConfigureAwait(false);
|
|
}
|
|
|
|
[Command("react")]
|
|
[Description("Add reactions to the specified message")]
|
|
public async Task React(
|
|
CommandContext ctx,
|
|
[Description("Message link")] string messageLink,
|
|
[RemainingText, Description("List of reactions to add")]string emojis
|
|
)
|
|
{
|
|
try
|
|
{
|
|
var message = await ctx.GetMessageAsync(messageLink).ConfigureAwait(false);
|
|
if (message is null)
|
|
{
|
|
await ctx.ReactWithAsync(Config.Reactions.Failure, "Couldn't find the message").ConfigureAwait(false);
|
|
return;
|
|
}
|
|
|
|
string emoji = "";
|
|
for (var i = 0; i < emojis.Length; i++)
|
|
{
|
|
try
|
|
{
|
|
var c = emojis[i];
|
|
if (char.IsHighSurrogate(c))
|
|
emoji += c;
|
|
else
|
|
{
|
|
DiscordEmoji de;
|
|
if (c == '<')
|
|
{
|
|
var endIdx = emojis.IndexOf('>', i);
|
|
if (endIdx < i)
|
|
endIdx = emojis.Length;
|
|
emoji = emojis[i..endIdx];
|
|
i = endIdx - 1;
|
|
var emojiId = ulong.Parse(emoji[(emoji.LastIndexOf(':') + 1)..]);
|
|
de = DiscordEmoji.FromGuildEmote(ctx.Client, emojiId);
|
|
}
|
|
else
|
|
de = DiscordEmoji.FromUnicode(emoji + c);
|
|
emoji = "";
|
|
await message.ReactWithAsync(de).ConfigureAwait(false);
|
|
}
|
|
}
|
|
catch { }
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Config.Log.Debug(e);
|
|
}
|
|
}
|
|
|
|
[Command("log"), RequiresDm]
|
|
[Description("Uploads current log file as an attachment")]
|
|
public async Task Log(CommandContext ctx, [Description("Specific date")]string date = "")
|
|
{
|
|
try
|
|
{
|
|
Config.Log.Factory.Flush();
|
|
var logPath = Config.CurrentLogPath;
|
|
if (DateTime.TryParse(date, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var logDate))
|
|
logPath = Path.Combine(Config.LogPath, $"bot.{logDate:yyyyMMdd}.*.log");
|
|
if (!File.Exists(logPath))
|
|
{
|
|
await ctx.ReactWithAsync(Config.Reactions.Failure, "Log file does not exist for specified day", true).ConfigureAwait(false);
|
|
return;
|
|
}
|
|
|
|
await using var result = Config.MemoryStreamManager.GetStream();
|
|
using (var zip = new ZipWriter(result, new(CompressionType.LZMA){DeflateCompressionLevel = CompressionLevel.Default}))
|
|
foreach (var fname in Directory.EnumerateFiles(Config.LogPath, Path.GetFileName(logPath), new EnumerationOptions { IgnoreInaccessible = true, RecurseSubdirectories = false, }))
|
|
{
|
|
await using var log = File.Open(fname, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
|
zip.Write(Path.GetFileName(fname), log);
|
|
}
|
|
|
|
if (result.Length <= ctx.GetAttachmentSizeLimit())
|
|
{
|
|
result.Seek(0, SeekOrigin.Begin);
|
|
await ctx.Channel.SendMessageAsync(new DiscordMessageBuilder().AddFile(Path.GetFileName(logPath) + ".zip", result)).ConfigureAwait(false);
|
|
}
|
|
else
|
|
await ctx.ReactWithAsync(Config.Reactions.Failure, "Compressed log size is too large, ask 13xforever for help :(", true).ConfigureAwait(false);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Config.Log.Warn(e, "Failed to upload current log");
|
|
await ctx.ReactWithAsync(Config.Reactions.Failure, $"Failed to send the log\n{e}".Trim(EmbedPager.MaxMessageLength), true).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
[Command("dbbackup"), Aliases("dbb"), TriggersTyping]
|
|
[Description("Uploads current Thumbs.db and Hardware.db files as an attachments")]
|
|
public async Task DbBackup(CommandContext ctx, [Description("Name of the database")]string name = "")
|
|
{
|
|
name = name.ToLower();
|
|
if (name.EndsWith(".db"))
|
|
name = name[..^3];
|
|
if (name != "hw")
|
|
await using (var db = new ThumbnailDb())
|
|
await BackupDb(ctx, db).ConfigureAwait(false);
|
|
if (name != "thumbs")
|
|
await using (var db = new HardwareDb())
|
|
await BackupDb(ctx, db).ConfigureAwait(false);
|
|
}
|
|
|
|
private static async Task BackupDb(CommandContext ctx, DbContext db)
|
|
{
|
|
string? dbName = null;
|
|
try
|
|
{
|
|
await using var botDb = new BotDb();
|
|
string dbPath, dbDir;
|
|
await using (var connection = db.Database.GetDbConnection())
|
|
{
|
|
dbPath = connection.DataSource;
|
|
dbDir = Path.GetDirectoryName(dbPath) ?? ".";
|
|
dbName = Path.GetFileNameWithoutExtension(dbPath);
|
|
|
|
var tsName = "db-vacuum-" + dbName;
|
|
var vacuumTs = await botDb.BotState.FirstOrDefaultAsync(v => v.Key == tsName).ConfigureAwait(false);
|
|
if (vacuumTs?.Value is null
|
|
|| (long.TryParse(vacuumTs.Value, out var vtsTicks)
|
|
&& vtsTicks < DateTime.UtcNow.AddDays(-30).Ticks))
|
|
{
|
|
await db.Database.ExecuteSqlRawAsync("VACUUM;").ConfigureAwait(false);
|
|
|
|
var newTs = DateTime.UtcNow.Ticks.ToString();
|
|
if (vacuumTs is null)
|
|
botDb.BotState.Add(new() { Key = tsName, Value = newTs });
|
|
else
|
|
vacuumTs.Value = newTs;
|
|
await botDb.SaveChangesAsync().ConfigureAwait(false);
|
|
}
|
|
}
|
|
await using var result = Config.MemoryStreamManager.GetStream();
|
|
using (var zip = new ZipWriter(result, new(CompressionType.LZMA){DeflateCompressionLevel = CompressionLevel.Default}))
|
|
foreach (var fname in Directory.EnumerateFiles(dbDir, $"{dbName}.*", new EnumerationOptions { IgnoreInaccessible = true, RecurseSubdirectories = false, }))
|
|
{
|
|
await using var dbData = File.Open(fname, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
|
zip.Write(Path.GetFileName(fname), dbData);
|
|
}
|
|
if (result.Length <= ctx.GetAttachmentSizeLimit())
|
|
{
|
|
result.Seek(0, SeekOrigin.Begin);
|
|
await ctx.Channel.SendMessageAsync(new DiscordMessageBuilder().AddFile(Path.GetFileName(dbName) + ".zip", result)).ConfigureAwait(false);
|
|
}
|
|
else
|
|
await ctx.ReactWithAsync(Config.Reactions.Failure, $"Compressed {dbName}.db size is too large, ask 13xforever for help :(", true).ConfigureAwait(false);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Config.Log.Warn(e, $"Failed to upload {(dbName is null? "DB": dbName + ".db")} backup");
|
|
await ctx.ReactWithAsync(Config.Reactions.Failure, $"Failed to send {(dbName is null? "DB": dbName + ".db")} backup", true).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
[Command("gen-salt")]
|
|
[Description("Regenerates salt for data anonymization purposes. This WILL affect Hardware DB deduplication.")]
|
|
public async Task ResetCryptoSalt(CommandContext ctx)
|
|
{
|
|
var btnYes = new DiscordButtonComponent(ButtonStyle.Danger, "gen-salt:yes", "Yes, regenerate salt");
|
|
var btnNo = new DiscordButtonComponent(ButtonStyle.Primary, "gen-salt:no", "No, I do not fully understand the consequences");
|
|
var b = new DiscordMessageBuilder()
|
|
.WithContent("This will affect hardware DB data deduplication. Are you sure?")
|
|
.AddComponents(btnYes, btnNo);
|
|
var msg = await ctx.RespondAsync(b).ConfigureAwait(false);
|
|
var interactivity = ctx.Client.GetInteractivity();
|
|
var (txt, reaction) = await interactivity.WaitForMessageOrButtonAsync(msg, ctx.User, TimeSpan.FromMinutes(1)).ConfigureAwait(false);
|
|
if (txt?.Content?.ToLowerInvariant() is "y" or "yes" || reaction?.Id == btnYes.CustomId)
|
|
{
|
|
var salt = new byte[256 / 8];
|
|
System.Security.Cryptography.RandomNumberGenerator.Fill(salt);
|
|
await new Bot.Configuration().Set(ctx, nameof(Config.CryptoSalt), Convert.ToBase64String(salt)).ConfigureAwait(false);
|
|
await msg.UpdateOrCreateMessageAsync(ctx.Channel, "Regenerated salt.").ConfigureAwait(false);
|
|
}
|
|
else
|
|
{
|
|
await msg.UpdateOrCreateMessageAsync(ctx.Channel, "Operation cancelled.").ConfigureAwait(false);
|
|
}
|
|
}
|
|
} |