Files
archived-discord-bot/CompatBot/Commands/Sudo.cs
2023-04-21 02:05:59 +05:00

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