-
Notifications
You must be signed in to change notification settings - Fork 2
/
Config.cs
411 lines (354 loc) · 19.1 KB
/
Config.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
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
using DSharpPlus;
using DSharpPlus.Entities;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection;
using Tomlet;
using Tomlet.Attributes;
using Tomlet.Models;
namespace Voidway_Bot {
internal static class Config {
internal class ConfigValues
{
[TomlProperty("token")] // property-ize because otherwise it throws a shitfit
public string DiscordToken { get; set; } = "";
public string modioToken = "";
[TomlPrecedingComment("Can be left blank if you only use an API key w/o OAuth2")]
public string modioOAuth = "";
public string logPath = "./logs/";
public int maxLogFiles = 5;
public int auditLogRetryCount = 5;
public bool logDiscordDebug = false;
[TomlPrecedingComment("Allows thread creators to PIN messages in threads they created.")]
public bool threadCreatorPinMessages = true;
[TomlPrecedingComment("Allows thread creators to DELETE messages in threads they created.")]
public bool threadCreatorDeleteMessages = false;
[TomlPrecedingComment("Key=ServerID -> Value=ChannelID; Will be used for logging actions taken by moderators.")]
public Dictionary<string, ulong> moderationChannels = new() { { "0", 1 }, { "2", 3 } }; // init w/ default values so the user knows how its formatted
[TomlPrecedingComment("Key=ServerID -> Value=ChannelID; Will be used for logging message actions by users.")]
public Dictionary<string, ulong> messageChannels = new() { { "4", 5 } }; // <string,ulong> because otherwise tomlet shits itself and refuses to deserialize
[TomlPrecedingComment("Where the bot will log suspicious joins. (<1d old & acc creation time within 1h of join time)")]
public Dictionary<string, ulong> newAccountChannels = new() { { "6", 7 } }; // <string,ulong> because otherwise tomlet shits itself and refuses to deserialize
[TomlPrecedingComment("Where the bot will log flagged message from OpenAI.")]
public Dictionary<string, ulong> openAiDiscordMonitorLogChannels = new() { { "8", 9 } }; // <string,ulong> because otherwise tomlet shits itself and refuses to deserialize
[TomlPrecedingComment("Key=ServerID -> Value=ChannelID; Where ALL mod uploads get posted to, useful for seeing an entire list for moderation")]
public Dictionary<string, ulong> allModUploads = new() { { "10", 11 } }; // <string,ulong> because otherwise tomlet shits itself and refuses to deserialize
[TomlPrecedingComment("Key=ServerID -> Value=ChannelID; Where announcements of malformed uploads are sent (A staff-only channel)")]
public Dictionary<string, ulong> malformedUploadChannels = new() { { "12", 13 } };
[TomlPrecedingComment("ServerID -> Upload Type -> ChannelID; Will be used for announcing recent mod.io uploads (Upload types: 'Avatar', 'Level', 'Spawnable', 'Utility').")]
public Dictionary<string, Dictionary<string, ulong>> modUploadChannels = new()
{
{
"14", new() { { nameof(ModUploads.UploadType.Avatar), 15 } }
}
};
[TomlPrecedingComment("Will hide image & desc of mod announcements when posted in these servers AS LONG AS THEY MATCH THE SPECIFIED CRITERIA")]
public ulong[] censorModAnnouncementsIn = [16];
[TomlPrecedingComment("Determines if 'All' criteria, or just 'One' criterion must be met before a mod's announcement is censored. All criteria are in LOWERCASE, and can be set to '*' to match every mod (for censorCriteriaBehavior = All)")]
public ModUploads.CensorCriteriaBehavior censorCriteriaBehavior = ModUploads.CensorCriteriaBehavior.One;
public string[] censorModsWithSummaryContaining = ["ten point five"];
public string[] censorModsWithTitlesContaining = ["eleven and no fraction", "This will never be hit because T is uppercase."];
public string[] censorModsWithTag = ["ELEVEN POINT FIVE THAT WONT BE HIT CUZ CAPS", "adult 18+", "other tag"];
public bool ignoreTagspamMods = true;
[TomlPrecedingComment("Renames users to 'hoist' if their nick/name starts with one of these characterss (and is in a specified server). Backslash escape char FYI.")]
public string hoistCharacters = @"()-+=_][\|;',.<>/?!@#$%^&*"; // literal string literal ftw
public ulong[] hoistServers = [14];
[TomlPrecedingComment("Deletes activity join invites in these servers.")]
public ulong[] msgFilterServers = [15];
[TomlPrecedingComment("Allows invites in these channels, even if they're in a filtering server.")]
public ulong[] msgFilterExceptions = [16];
[TomlPrecedingComment("The invites to send when filtering someone's message.")]
public string[] sendWhenFilterMessage = ["discord.gg/real"];
[TomlPrecedingComment("The time between sending a message filter response to sending another message filter response, if someone else posts a new invite, and the time to leave the message up.")]
public int msgFilterMessageTimeout = 60;
public int msgFilterMessageStayTime = 10;
[TomlPrecedingComment("Not necessarily able to bypass permissions (like Slash Commands) checks, just able to access debug commands/")]
public ulong[] owners = [17];
public string[] ignoreDSharpPlusLogsWith = ["Unknown event:"]; // "GUILD_JOIN_REQUEST_UPDATE" SHUT THE FUCK UP
public Dictionary<string, ulong> modioCommentModerationNotifChannels = new() { { "18", 19 } };
[TomlPrecedingComment("Doesn't run these channels' messages through OpenAI's Moderation endpoint.")]
public ulong[] openAiModerationExceptions = [16];
[TomlPrecedingComment("If populated, will use the *free* OpenAI Moderation endpoint to flag discord messages and/or mod.io comments given their content.")]
public string openAiApiKey = "";
public bool openAiModerateDiscord = false;
public bool openAiModerateModio = true;
public Dictionary<string, ulong> serverModNotesChannels = new() { { "20", 21 } };
}
const string FILE_NAME = "config.toml";
static readonly FileSystemWatcher watcher;
static readonly string activePath;
static ConfigValues values;
// static ctor
static Config()
{
activePath = Path.Combine(AppContext.BaseDirectory, FILE_NAME);
Console.WriteLine("Attempting to read config from " + activePath);
if (!File.Exists(activePath))
{
WriteConfig(new ConfigValues()).Wait();
Console.WriteLine("Config file wasn't found! An empty one was created, fill it out.");
Console.ReadKey();
Environment.Exit(0);
}
LoadConfig();
WriteConfig(values).GetAwaiter().GetResult();
// write new cfg to add new fields
Logger.Put("Updated config.");
Logger.Put("(Updating config is harmless, just in case things changed between versions, this adds the new fields)", Logger.Reason.Trace);
Logger.Put("Starting config watcher.");
watcher = new FileSystemWatcher(AppContext.BaseDirectory)
{
Filter = "*.toml",
IncludeSubdirectories = false,
EnableRaisingEvents = true,
};
watcher.Changed += WatcherChanged;
Logger.Put("Watcher started successfully.");
}
private static async void WatcherChanged(object sender, FileSystemEventArgs e)
{
// wait 25ms to avoid race conditions about reading while another process has access
await Task.Delay(25);
try
{
LoadConfig();
}
catch (Exception ex)
{
Logger.Warn("Exception while live-reloading config file.", ex);
}
}
[MemberNotNull(nameof(values))]
private static void LoadConfig()
{
string fileContents = File.ReadAllText(activePath);
values = TomletMain.To<ConfigValues>(fileContents);
Logger.Put("Retrieved config values.");
}
internal static int GetAuditLogRetryCount() => values.auditLogRetryCount;
internal static int GetMaxLogFiles() => values.maxLogFiles;
internal static bool GetLogDiscordDebug() => values.logDiscordDebug;
internal static bool GetThreadCreatorPinMessages() => values.threadCreatorPinMessages;
internal static bool GetThreadCreatorDeleteMessages() => values.threadCreatorDeleteMessages;
internal static string GetLogPath() => Path.GetFullPath(values.logPath);
internal static ModUploads.CensorCriteriaBehavior GetCriteriaBehavior() => values.censorCriteriaBehavior;
internal static bool GetIgnoreTagspam() => values.ignoreTagspamMods;
internal static int GetFilterResponseTimeout() => values.msgFilterMessageTimeout;
internal static string[] GetFilterInvites() => values.sendWhenFilterMessage;
internal static int GetFilterResponseStayTime() => values.msgFilterMessageStayTime;
internal static string GetDiscordToken() => values.DiscordToken;
internal static (string, string) GetModioTokens() => (values.modioToken, values.modioOAuth);
internal static string GetOpenAiToken() => values.openAiApiKey;
internal static ulong FetchModerationChannel(ulong guild) {
if (values.moderationChannels.TryGetValue(guild.ToString(), out ulong channel)) return channel;
else
{
Logger.Warn("Config values don't have a moderation log channel for the given guild ID: " + guild);
return default;
}
}
internal static ulong FetchMessagesChannel(ulong guild) {
if (values.messageChannels.TryGetValue(guild.ToString(), out ulong channel)) return channel;
else
{
Logger.Warn("Config values don't have a messages channel for the given guild ID: " + guild);
return default;
}
}
internal static ulong FetchAllModsChannel(ulong guild) {
if(values.allModUploads.TryGetValue(guild.ToString(), out ulong channel)) return channel;
else {
Logger.Warn("Config values don't have an all mod uploads channel for the given guild ID: " + guild);
return default;
}
}
internal static ulong FetchUploadChannel(ulong guild, ModUploads.UploadType uploadType) {
if (!values.modUploadChannels.TryGetValue(guild.ToString(), out var uploadTypeToChannel))
return default;
if (!uploadTypeToChannel.TryGetValue(uploadType.ToString(), out ulong channel))
return default;
return channel;
}
internal static ulong FetchMalformedUploadChannel(ulong guild)
{
if (values.malformedUploadChannels.TryGetValue((guild.ToString()), out var channel))
return channel;
return default;
}
internal static ulong FetchCommentModerationChannel(ulong guild)
{
if (values.modioCommentModerationNotifChannels.TryGetValue((guild.ToString()), out var channel))
return channel;
return default;
}
internal static ulong FetchNewAccountLogChannel(ulong guild)
{
if (values.newAccountChannels.TryGetValue(guild.ToString(), out ulong channel)) return channel;
else
{
// don't log, because some servers wont want to log new users (like the SLZ server)
// Logger.Warn("Config values don't have a messages channel for the given guild ID: " + guild);
return default;
}
}
internal static ulong FetchOpenAiModerationChannel(ulong guild)
{
if (values.openAiDiscordMonitorLogChannels.TryGetValue(guild.ToString(), out ulong channel)) return channel;
else
{
return default;
}
}
internal static bool IsHoistServer(ulong guild)
{
return values.hoistServers.Contains(guild);
}
internal static bool IsFilterMessageServer(ulong guild)
{
return values.msgFilterServers.Contains(guild);
}
internal static bool IsJoinMessageAllowedIn(ulong channel)
{
return values.msgFilterExceptions.Contains(channel);
}
internal static bool IsHoistMember(char firstChar)
{
return values.hoistCharacters.Contains(firstChar);
}
internal static bool IsDSharpPlusMessageIgnored(string message)
{
foreach (string ignoreWith in values.ignoreDSharpPlusLogsWith)
{
if (message.Contains(ignoreWith)) return true; // this may be a bit wasteful, speed-wise, but oh well it prevents logspam.
}
return false;
}
internal static bool IsServerCensoringMods(ulong guild)
{
return values.censorModAnnouncementsIn.Contains(guild);
}
internal static bool IsModSummaryCensored(string? description)
{
string? desc = description?.ToLower();
if (desc is null) return false;
foreach (string censorModsWith in values.censorModsWithSummaryContaining)
{
if (desc.Contains(censorModsWith)) return true;
else if (censorModsWith == "*") return true;
}
return false;
}
internal static bool IsModTitleCensored(string? title)
{
string? modTitle = title?.ToLower(); // so tempted to name this local "tit" for lols
if (modTitle is null) return false;
foreach (string censorModsWith in values.censorModsWithTitlesContaining)
{
if (modTitle.Contains(censorModsWith)) return true;
else if (censorModsWith == "*") return true;
}
return false;
}
internal static bool IsModTagsCensored(string[] modTags) // grammatically should be AreModTagsCensored but ive got a naming convention going on
{
string[] tags = modTags.Select(s => s.ToLower()).ToArray();
foreach (string censorModsWith in values.censorModsWithTag)
{
if (tags.Contains(censorModsWith)) return true;
else if (censorModsWith == "*") return true;
}
return false;
}
internal static bool IsExemptFromOpenAiScanning(ulong discordChannel)
{
return values.openAiModerationExceptions.Contains(discordChannel);
}
internal static bool IsModeratingModioComments() => values.openAiModerateModio;
internal static bool IsModeratingDiscordMessages() => values.openAiModerateDiscord;
internal static bool IsUserOwner(ulong id) => values.owners.Contains(id);
internal static ulong GetModNotesChannel(ulong guildId)
{
if (values.serverModNotesChannels.TryGetValue(guildId.ToString(), out ulong channelId))
{
return channelId;
}
return 0;
}
internal static async Task<DiscordChannel?> GetModNotesChannel(DiscordClient client, ulong guildId)
{
if (values.serverModNotesChannels.TryGetValue(guildId.ToString(), out ulong channelId))
{
DiscordChannel? channel = await FetchChannelFromJumpLink(client, $"https://discord.com/channels/{guildId}/{channelId}");
return channel;
}
return null;
}
internal static Task ModifyConfig(Action<ConfigValues> changeVia)
{
StackTrace trace = new(1);
MethodBase? mb = trace.GetFrame(0)?.GetMethod();
Logger.Put("Config being modified from: " + (mb?.DeclaringType?.FullName ?? "<Unknown type>") + (mb?.Name ?? "<Unknown method>"), Logger.Reason.Trace);
changeVia(values);
return WriteConfig(values);
}
internal static async Task WriteConfig(ConfigValues cfg)
{
string fileContents = TomletMain.DocumentFrom(cfg).SerializedValue;
await File.WriteAllTextAsync(activePath, fileContents);
Logger.Put("Wrote config to disk.");
}
private static async Task<DiscordMessage?> FetchMessageFromJumpLink(DiscordClient client, string jumpLink)
{
string[] split = jumpLink.Split('/');
if (split.Length < 3) return null;
ulong guildId = ulong.Parse(split[4]);
ulong channelId = ulong.Parse(split[5]);
ulong messageId = ulong.Parse(split[6]);
// wrap in try-catch because dsharpplus will throw if the guild or channel is not found (EPICK WIN!!!)
try
{
DiscordGuild guild = await client.GetGuildAsync(guildId);
if (guild is null) return null;
DiscordChannel channel = guild.GetChannel(channelId);
if (channel is null) return null;
return await channel.GetMessageAsync(messageId);
}
catch (Exception ex)
{
Logger.Warn("Exception while fetching message from jump link.", ex);
return null;
}
}
private static async Task<DiscordChannel?> FetchChannelFromJumpLink(DiscordClient client, string jumpLink)
{
string[] split = jumpLink.Split('/');
if (split.Length < 3) return null;
ulong guildId = ulong.Parse(split[4]);
ulong channelId = ulong.Parse(split[5]);
// wrap in try-catch because dsharpplus will throw if the guild or channel is not found (EPICK WIN!!!)
try
{
DiscordGuild guild = await client.GetGuildAsync(guildId);
if (guild is null) return null;
if (guild.Channels.TryGetValue(channelId, out DiscordChannel? channel))
{
return channel;
}
static IEnumerable<DiscordChannel> GetThreads(DiscordChannel ch)
{
return ch.Type == ChannelType.Text || ch.Type == ChannelType.News || ch.Type == ChannelType.GuildForum
? ch.Threads // avoids an exception
: Enumerable.Empty<DiscordChannel>();
}
IEnumerable<DiscordChannel> channelsAndThreads = guild.Channels.Values.Concat(guild.Channels.Values.SelectMany(GetThreads));
return channelsAndThreads.FirstOrDefault(c => c.Id == channelId);
}
catch (Exception ex)
{
Logger.Warn("Exception while fetching channel from jump link.", ex);
return null;
}
}
}
}