-
Notifications
You must be signed in to change notification settings - Fork 45
/
Config.cs
343 lines (305 loc) · 17.5 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
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using CompatBot.Utils;
using DSharpPlus.Entities;
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.DependencyCollector;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.ApplicationInsights.Extensibility.PerfCounterCollector;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.UserSecrets;
using Microsoft.Extensions.Logging;
using Microsoft.IO;
using Microsoft.TeamFoundation.Build.WebApi;
using Microsoft.VisualStudio.Services.Common;
using Microsoft.VisualStudio.Services.WebApi;
using NLog;
using NLog.Extensions.Logging;
using NLog.Filters;
using NLog.Targets;
using NLog.Targets.Wrappers;
using ILogger = NLog.ILogger;
using LogLevel = NLog.LogLevel;
namespace CompatBot;
internal static class Config
{
private static IConfigurationRoot config = null!;
private static TelemetryClient? telemetryClient;
private static readonly DependencyTrackingTelemetryModule DependencyTrackingTelemetryModule = new();
private static readonly PerformanceCollectorModule PerformanceCollectorModule = new();
internal static readonly ILogger Log;
internal static readonly ILoggerFactory LoggerFactory;
internal static readonly ConcurrentDictionary<string, string?> InMemorySettings = new();
internal static readonly RecyclableMemoryStreamManager MemoryStreamManager = new();
public static readonly CancellationTokenSource Cts = new();
public static readonly Stopwatch Uptime = Stopwatch.StartNew();
public static string? GitRevision { get; private set; }
// these settings could be configured either through `$ dotnet user-secrets`, or through environment variables (e.g. launchSettings.json, etc)
public static string CommandPrefix => config.GetValue(nameof(CommandPrefix), "!")!;
public static string AutoRemoveCommandPrefix => config.GetValue(nameof(AutoRemoveCommandPrefix), ".")!;
public static ulong BotGuildId => config.GetValue(nameof(BotGuildId), 272035812277878785ul); // discord server where the bot is supposed to be
public static ulong BotGeneralChannelId => config.GetValue(nameof(BotGeneralChannelId), 272035812277878785ul);// #rpcs3; main or general channel where noobs come first thing
public static ulong BotChannelId => config.GetValue(nameof(BotChannelId), 291679908067803136ul); // #build-updates; this is used for new build announcements
public static ulong BotSpamId => config.GetValue(nameof(BotSpamId), 319224795785068545ul); // #bot-spam; this is a dedicated channel for bot abuse
public static ulong BotLogId => config.GetValue(nameof(BotLogId), 436972161572536329ul); // #bot-log; a private channel for admin mod queue
public static ulong BotRulesChannelId => config.GetValue(nameof(BotRulesChannelId), 311894275015049216ul); // #rules-info; used to give links to rules
public static ulong ThumbnailSpamId => config.GetValue(nameof(ThumbnailSpamId), 475678410098606100ul); // #bot-data; used for whatever bot needs to keep (cover embeds, etc)
public static ulong DeletedMessagesLogChannelId => config.GetValue(nameof(DeletedMessagesLogChannelId), 0ul);
public static TimeSpan ModerationBacklogThresholdInHours => TimeSpan.FromHours(config.GetValue(nameof(ModerationBacklogThresholdInHours), 1));
public static TimeSpan DefaultTimeoutInSec => TimeSpan.FromSeconds(config.GetValue(nameof(DefaultTimeoutInSec), 30));
public static TimeSpan SocketDisconnectCheckIntervalInSec => TimeSpan.FromSeconds(config.GetValue(nameof(SocketDisconnectCheckIntervalInSec), 10));
public static TimeSpan LogParsingTimeoutInSec => TimeSpan.FromSeconds(config.GetValue(nameof(LogParsingTimeoutInSec), 30));
public static TimeSpan BuildTimeDifferenceForOutdatedBuildsInDays => TimeSpan.FromDays(config.GetValue(nameof(BuildTimeDifferenceForOutdatedBuildsInDays), 3));
public static TimeSpan ShutupTimeLimitInMin => TimeSpan.FromMinutes(config.GetValue(nameof(ShutupTimeLimitInMin), 5));
public static TimeSpan ForcedNicknamesRecheckTimeInHours => TimeSpan.FromHours(config.GetValue(nameof(ForcedNicknamesRecheckTimeInHours), 3));
public static TimeSpan IncomingMessageCheckIntervalInMin => TimeSpan.FromMinutes(config.GetValue(nameof(IncomingMessageCheckIntervalInMin), 10));
public static TimeSpan MetricsIntervalInSec => TimeSpan.FromSeconds(config.GetValue(nameof(MetricsIntervalInSec), 10));
public static int ProductCodeLookupHistoryThrottle => config.GetValue(nameof(ProductCodeLookupHistoryThrottle), 7);
public static int TopLimit => config.GetValue(nameof(TopLimit), 15);
public static int AttachmentSizeLimit => config.GetValue(nameof(AttachmentSizeLimit), 25 * 1024 * 1024);
public static int LogSizeLimit => config.GetValue(nameof(LogSizeLimit), 64 * 1024 * 1024);
public static int MinimumBufferSize => config.GetValue(nameof(MinimumBufferSize), 512);
public static int MessageCacheSize => config.GetValue(nameof(MessageCacheSize), 1024);
public static int BuildNumberDifferenceForOutdatedBuilds => config.GetValue(nameof(BuildNumberDifferenceForOutdatedBuilds), 10);
public static int MinimumPiracyTriggerLength => config.GetValue(nameof(MinimumPiracyTriggerLength), 4);
public static int MaxSyscallResultLines => config.GetValue(nameof(MaxSyscallResultLines), 13);
public static int ChannelMessageHistorySize => config.GetValue(nameof(ChannelMessageHistorySize), 100);
public static int FunMultiplier => config.GetValue(nameof(FunMultiplier), 1);
public static int MaxPositionsForHwSurveyResults => config.GetValue(nameof(MaxPositionsForHwSurveyResults), 10);
public static string Token => config.GetValue(nameof(Token), "")!;
public static string AzureDevOpsToken => config.GetValue(nameof(AzureDevOpsToken), "")!;
public static string AzureComputerVisionKey => config.GetValue(nameof(AzureComputerVisionKey), "")!;
public static string AzureComputerVisionEndpoint => config.GetValue(nameof(AzureComputerVisionEndpoint), "https://westeurope.api.cognitive.microsoft.com/")!;
public static Guid AzureDevOpsProjectId => config.GetValue(nameof(AzureDevOpsProjectId), new Guid("3598951b-4d39-4fad-ad3b-ff2386a649de"));
public static string AzureAppInsightsConnectionString => config.GetValue(nameof(AzureAppInsightsConnectionString), "")!;
public static string GithubToken => config.GetValue(nameof(GithubToken), "")!;
public static string GoogleApiCredentials => config.GetValue(nameof(GoogleApiCredentials), "")!;
public static string PreferredFontFamily => config.GetValue(nameof(PreferredFontFamily), "")!;
public static string LogPath => config.GetValue(nameof(LogPath), "./logs/")!; // paths are relative to the working directory
public static string IrdCachePath => config.GetValue(nameof(IrdCachePath), "./ird/")!;
public static double GameTitleMatchThreshold => config.GetValue(nameof(GameTitleMatchThreshold), 0.57);
public static byte[] CryptoSalt => Convert.FromBase64String(config.GetValue(nameof(CryptoSalt), "")!);
public static string RenameNameSuffix => config.GetValue(nameof(RenameNameSuffix), " (Rule 7)")!;
internal static class AllowedMentions
{
internal static readonly IMention[] UsersOnly = [UserMention.All];
internal static readonly IMention[] Nothing = [];
}
internal static string CurrentLogPath => Path.GetFullPath(Path.Combine(LogPath, "bot.log"));
public static string GoogleApiConfigPath
{
get
{
if (SandboxDetector.Detect() == SandboxType.Docker)
return "/bot-config/credentials.json";
if (Assembly.GetEntryAssembly()?.GetCustomAttribute<UserSecretsIdAttribute>() is UserSecretsIdAttribute attribute
&& Path.GetDirectoryName(PathHelper.GetSecretsPathFromSecretsId(attribute.UserSecretsId)) is string path)
{
path = Path.Combine(path, "credentials.json");
if (File.Exists(path))
return path;
}
return "Properties/credentials.json";
}
}
public static class Colors
{
public static readonly DiscordColor Help = DiscordColor.Azure;
public static readonly DiscordColor DownloadLinks = new(0x3b88c3);
public static readonly DiscordColor Maintenance = new(0xffff00);
public static readonly DiscordColor CompatStatusNothing = new(0x455556); // colors mimic compat list statuses
public static readonly DiscordColor CompatStatusLoadable = new(0xe74c3c);
public static readonly DiscordColor CompatStatusIntro = new(0xe08a1e);
public static readonly DiscordColor CompatStatusIngame = new(0xf9b32f);
public static readonly DiscordColor CompatStatusPlayable = new(0x1ebc61);
public static readonly DiscordColor CompatStatusUnknown = new(0x3198ff);
public static readonly DiscordColor LogResultFailed = DiscordColor.Gray;
public static readonly DiscordColor LogAlert = new(0xf04747); // colors mimic discord statuses
public static readonly DiscordColor LogNotice = new(0xfaa61a);
public static readonly DiscordColor LogInfo = new(0x43b581);
public static readonly DiscordColor LogUnknown = new(0x747f8d);
public static readonly DiscordColor PrOpen = new(0x2cbe4e);
public static readonly DiscordColor PrMerged = new(0x6f42c1);
public static readonly DiscordColor PrClosed = new(0xcb2431);
public static readonly DiscordColor UpdateStatusGood = new(0x3b88c3);
public static readonly DiscordColor UpdateStatusBad = DiscordColor.Yellow;
}
public static class Reactions
{
public static readonly DiscordEmoji Success = DiscordEmoji.FromUnicode("👌");
public static readonly DiscordEmoji Failure = DiscordEmoji.FromUnicode("⛔");
public static readonly DiscordEmoji Denied = DiscordEmoji.FromUnicode("👮");
public static readonly DiscordEmoji Starbucks = DiscordEmoji.FromUnicode("☕");
public static readonly DiscordEmoji Moderated = DiscordEmoji.FromUnicode("🔨");
public static readonly DiscordEmoji No = DiscordEmoji.FromUnicode("😐");
public static readonly DiscordEmoji PleaseWait = DiscordEmoji.FromUnicode("👀");
public static readonly DiscordEmoji PiracyCheck = DiscordEmoji.FromUnicode("🔨");
public static readonly DiscordEmoji ShutUp = DiscordEmoji.FromUnicode("🔇");
public static readonly DiscordEmoji BadUpdate = DiscordEmoji.FromUnicode("⚠️");
}
public static class Moderation
{
public const int StarbucksThreshold = 5;
public static readonly IReadOnlyCollection<ulong> MediaChannels = new List<ulong>
{
272875751773306881, // #media
319224795785068545,
}.AsReadOnly();
public static readonly IReadOnlyCollection<ulong> OcrChannels = new HashSet<ulong>(MediaChannels)
{
272035812277878785, // #rpcs3
277227681836302338, // #help
272875751773306881, // #media
};
public static readonly IReadOnlyCollection<ulong> LogParsingChannels = new HashSet<ulong>
{
277227681836302338, // #help
272081036316377088, // #donors
319224795785068545, // #bot-spam
442667232489897997, // #testers
// test server
988392381118308394,
};
public static readonly IReadOnlyCollection<string> RoleWhiteList = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase)
{
"Administrator",
"Community Manager",
"Web Developer",
"Moderator",
"Lead Graphics Developer",
"Lead Core Developer",
"Developers",
"Affiliated",
};
public static readonly IReadOnlyCollection<string> RoleSmartList = new HashSet<string>(RoleWhiteList, StringComparer.InvariantCultureIgnoreCase)
{
"Testers",
"Helpers",
"Contributors",
};
public static readonly IReadOnlyCollection<string> SupporterRoleList = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase)
{
"Fans",
"Supporters",
"Spectators",
"Nitro Booster",
};
}
static Config()
{
try
{
RebuildConfiguration();
Log = GetLog();
LoggerFactory = new NLogLoggerFactory();
Log.Info("Log path: " + CurrentLogPath);
}
catch (Exception e)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("Error initializing settings: " + e.Message);
Console.ResetColor();
throw;
}
}
internal static void RebuildConfiguration()
{
config = new ConfigurationBuilder()
.AddUserSecrets(Assembly.GetExecutingAssembly()) // lower priority
.AddEnvironmentVariables()
.AddInMemoryCollection(InMemorySettings) // higher priority
.Build();
}
private static ILogger GetLog()
{
var loggingConfig = new NLog.Config.LoggingConfiguration();
var fileTarget = new FileTarget("logfile") {
FileName = CurrentLogPath,
ArchiveEvery = FileArchivePeriod.Day,
ArchiveNumbering = ArchiveNumberingMode.DateAndSequence,
KeepFileOpen = true,
ConcurrentWrites = false,
AutoFlush = false,
OpenFileFlushTimeout = 1,
Layout = "${longdate} ${sequenceid:padding=6} ${level:uppercase=true:padding=-5} ${message} ${onexception:" +
"${newline}${exception:format=ToString}" +
":when=not contains('${exception:format=ShortType}','TaskCanceledException')}",
};
var asyncFileTarget = new AsyncTargetWrapper(fileTarget)
{
TimeToSleepBetweenBatches = 0,
OverflowAction = AsyncTargetWrapperOverflowAction.Block,
BatchSize = 500,
};
var consoleTarget = new ColoredConsoleTarget("logconsole") {
Layout = "${longdate} ${level:uppercase=true:padding=-5} ${message} ${onexception:" +
"${newline}${exception:format=Message}" +
":when=not contains('${exception:format=ShortType}','TaskCanceledException')}",
};
var watchdogTarget = new MethodCallTarget("watchdog")
{
ClassName = typeof(Watchdog).AssemblyQualifiedName,
MethodName = nameof(Watchdog.OnLogHandler),
};
watchdogTarget.Parameters.AddRange(new[]
{
new MethodCallParameter("${level}"),
new MethodCallParameter("${message}"),
});
#if DEBUG
loggingConfig.AddRule(LogLevel.Trace, LogLevel.Fatal, consoleTarget, "default"); // only echo messages from default logger to the console
#else
loggingConfig.AddRule(LogLevel.Info, LogLevel.Fatal, consoleTarget, "default");
#endif
loggingConfig.AddRule(LogLevel.Debug, LogLevel.Fatal, asyncFileTarget);
loggingConfig.AddRule(LogLevel.Info, LogLevel.Fatal, watchdogTarget);
var ignoreFilter1 = new ConditionBasedFilter { Condition = "contains('${message}','TaskCanceledException')", Action = FilterResult.Ignore, };
var ignoreFilter2 = new ConditionBasedFilter { Condition = "contains('${message}','One or more pre-execution checks failed')", Action = FilterResult.Ignore, };
foreach (var rule in loggingConfig.LoggingRules)
{
rule.Filters.Add(ignoreFilter1);
rule.Filters.Add(ignoreFilter2);
rule.FilterDefaultAction = FilterResult.Log;
}
LogManager.Configuration = loggingConfig;
return LogManager.GetLogger("default");
}
public static BuildHttpClient? GetAzureDevOpsClient()
{
if (string.IsNullOrEmpty(AzureDevOpsToken))
return null;
var azureCreds = new VssBasicCredential("bot", AzureDevOpsToken);
var azureConnection = new VssConnection(new Uri("https://dev.azure.com/nekotekina"), azureCreds);
return azureConnection.GetClient<BuildHttpClient>();
}
public static TelemetryClient? TelemetryClient
{
get
{
if (string.IsNullOrEmpty(AzureAppInsightsConnectionString))
return null;
if (telemetryClient?.InstrumentationKey == AzureAppInsightsConnectionString)
return telemetryClient;
var telemetryConfig = TelemetryConfiguration.CreateDefault();
telemetryConfig.ConnectionString = AzureAppInsightsConnectionString;
telemetryConfig.TelemetryInitializers.Add(new HttpDependenciesParsingTelemetryInitializer());
DependencyTrackingTelemetryModule.Initialize(telemetryConfig);
PerformanceCollectorModule.Initialize(telemetryConfig);
return telemetryClient = new TelemetryClient(telemetryConfig);
}
}
public static async Task GetCurrentGitRevisionAsync(CancellationToken cancellationToken)
{
if (GitRevision is not null)
return;
var commit = await GitRunner.Exec("log -1 --pretty=format:%h", cancellationToken);
var branch = await GitRunner.Exec("rev-parse --abbrev-ref HEAD", cancellationToken);
GitRevision = $"{commit} on {branch}";
}
}