-
Notifications
You must be signed in to change notification settings - Fork 45
/
Sudo.cs
264 lines (251 loc) · 11.7 KB
/
Sudo.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
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 NLog;
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
{
LogManager.LogFactory.Flush();
string[] logPaths = [Config.CurrentLogPath];
if (DateTime.TryParse(date, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var logDate))
{
var enumOptions = new EnumerationOptions { IgnoreInaccessible = true, RecurseSubdirectories = false, };
logPaths = Directory.GetFiles(Config.LogPath, $"bot.{logDate:yyyyMMdd}.*.log", enumOptions);
}
if (logPaths.Length is 0)
{
await ctx.ReactWithAsync(Config.Reactions.Failure, "Log files do 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 logPaths)
{
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(logPaths[0]) + ".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);
}
}
}