-
Notifications
You must be signed in to change notification settings - Fork 3
/
Program.cs
305 lines (260 loc) · 12.9 KB
/
Program.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
namespace MechanicalMilkshake;
public class GatewayController : IGatewayController
{
public async Task HeartbeatedAsync(IGatewayClient client) => await HeartbeatEvent.Heartbeated(client);
public async Task ResumeAttemptedAsync(IGatewayClient _) { }
public async Task ZombiedAsync(IGatewayClient _) { }
public async Task ReconnectRequestedAsync(IGatewayClient _) { }
public async Task ReconnectFailedAsync(IGatewayClient _) { }
public async Task SessionInvalidatedAsync(IGatewayClient _) { }
}
public class Program
{
public static DiscordClient Discord;
private static readonly string[] Prefixes = ["pls"];
public static MinioClient Minio;
public static readonly List<string> DisabledCommands = [];
public static readonly Random Random = new();
public static DateTime ConnectTime;
public static readonly HttpClient HttpClient = new();
public static ConfigJson ConfigJson;
public static readonly string ProcessStartTime = DateTime.Now.ToString(CultureInfo.CurrentCulture);
public static readonly DiscordColor BotColor = new("#FAA61A");
public static DiscordChannel HomeChannel;
public static DiscordGuild HomeServer;
public static List<DiscordApplicationCommand> ApplicationCommands;
public static EventId BotEventId { get; } = new(1000, "MechanicalMilkshake");
#if DEBUG
private static readonly ConnectionMultiplexer Redis = ConnectionMultiplexer.Connect("localhost:6379");
#else
private static readonly ConnectionMultiplexer Redis = ConnectionMultiplexer.Connect("redis");
#endif
public static readonly IDatabase Db = Redis.GetDatabase();
public static bool RedisExceptionsSuppressed;
public static readonly Entities.MessageCaching.MessageCache MessageCache = new();
public static string LastUptimeKumaHeartbeatStatus = "N/A";
public static bool GuildDownloadCompleted = false;
public static readonly Dictionary<string, ulong> UserFlagEmoji = new()
{
{ "earlyVerifiedBotDeveloper", 1000168738970144779 },
{ "discordStaff", 1000168738022228088 },
{ "hypesquadBalance", 1000168740073242756 },
{ "hypesquadBravery", 1000168740991811704 },
{ "hypesquadBrilliance", 1000168741973266462 },
{ "hypesquadEvents", 1000168742535303312 },
{ "bugHunterLevelOne", 1000168734666793001 },
{ "bugHunterLevelTwo", 1000168735740526732 },
{ "certifiedModerator", 1000168736789118976 },
{ "partneredServerOwner", 1000168744192053298 },
{ "verifiedBot1", 1000229381563744397 },
{ "verifiedBot2", 1000229382431977582 },
{ "earlySupporter", 1001317583124971582 }
};
internal static async Task Main()
{
// Read config.json, or config.dev.json if running in development mode
string json;
#if DEBUG
const string configFile = "config.dev.json";
#else
const string configFile = "config.json";
#endif
await using (var fs = File.OpenRead(configFile))
using (StreamReader sr = new(fs, new UTF8Encoding(false)))
{
json = await sr.ReadToEndAsync();
}
ConfigJson = JsonConvert.DeserializeObject<ConfigJson>(json);
if (ConfigJson?.Base is null)
{
Discord.Logger.LogCritical(BotEventId,
// ReSharper disable once LogMessageIsSentenceProblem
"Your config.json file is malformed. Please be sure it has all of the required values.");
Environment.Exit(1);
}
if (string.IsNullOrWhiteSpace(ConfigJson.Base.HomeChannel) ||
string.IsNullOrWhiteSpace(ConfigJson.Base.HomeServer) ||
string.IsNullOrWhiteSpace(ConfigJson.Base.BotToken))
{
Discord.Logger.LogError(BotEventId,
// ReSharper disable once LogMessageIsSentenceProblem
"You are missing required values in your config.json file. Please make sure you have values for all of the keys under \"base\".");
Environment.Exit(1);
}
var clientBuilder = DiscordClientBuilder.CreateDefault(ConfigJson.Base.BotToken, DiscordIntents.All.RemoveIntent(DiscordIntents.GuildPresences));
#if DEBUG
clientBuilder.SetLogLevel(LogLevel.Debug);
#else
clientBuilder.SetLogLevel(LogLevel.Information);
#endif
clientBuilder.ConfigureServices(services =>
{
services.Replace<IGatewayController, GatewayController>();
});
clientBuilder.ConfigureExtraFeatures(config =>
{
config.LogUnknownEvents = false;
config.LogUnknownAuditlogs = false;
});
clientBuilder.ConfigureEventHandlers(builder =>
builder.HandleSessionCreated(ReadyEvent.OnReady)
.HandleMessageCreated(MessageEvents.MessageCreated)
.HandleMessageUpdated(MessageEvents.MessageUpdated)
.HandleMessageDeleted(MessageEvents.MessageDeleted)
.HandleChannelDeleted(ChannelEvents.ChannelDeleted)
.HandleComponentInteractionCreated(ComponentInteractionEvent.ComponentInteractionCreated)
.HandleGuildCreated(GuildEvents.GuildCreated)
.HandleGuildDeleted(GuildEvents.GuildDeleted)
.HandleGuildMemberUpdated(GuildEvents.GuildMemberUpdated)
.HandleGuildDownloadCompleted(GuildEvents.GuildDownloadCompleted)
);
clientBuilder.UseInteractivity(new InteractivityConfiguration
{
PollBehaviour = PollBehaviour.KeepEmojis,
Timeout = TimeSpan.FromSeconds(30)
});
clientBuilder.UseCommands((_, extension) =>
{
// Use custom TextCommandProcessor to set custom prefixes & disable CommandNotFoundExceptions
TextCommandProcessor textCommandProcessor = new(new()
{
PrefixResolver = new DefaultPrefixResolver(true, Prefixes).ResolvePrefixAsync,
EnableCommandNotFoundException = false
});
extension.AddProcessor(textCommandProcessor);
// Register context checks
extension.AddCheck<RequireAuthCheck>();
// Register error handling
extension.CommandErrored += ErrorEvents.CommandErrored;
// Register logging
extension.CommandExecuted += Events.InteractionEvents.CommandExecuted;
// Register interaction commands
CommandHelpers.RegisterCommands(extension, HomeServer.Id);
}, new CommandsConfiguration
{
UseDefaultCommandErrorHandler = false
});
// Build the client
Discord = clientBuilder.Build();
// Set home channel & guild for later reference
ulong homeChanId = default;
ulong homeServerId = default;
try
{
homeChanId = Convert.ToUInt64(ConfigJson.Base.HomeChannel);
homeServerId = Convert.ToUInt64(ConfigJson.Base.HomeServer);
}
catch
{
Discord.Logger.LogError(BotEventId,
// ReSharper disable once LogMessageIsSentenceProblem
"\"homeChannel\" or \"homeServer\" in config.json are misconfigured. Please make sure you have a valid ID for both of these values.");
Environment.Exit(1);
}
HomeChannel = await Discord.GetChannelAsync(homeChanId);
HomeServer = await Discord.GetGuildAsync(homeServerId);
// Set up Minio (used for some Owner commands)
if (ConfigJson.S3 is null || ConfigJson.S3.Bucket == "" || ConfigJson.S3.CdnBaseUrl == "" || ConfigJson.S3.Endpoint == "" ||
ConfigJson.S3.AccessKey == "" || ConfigJson.S3.SecretKey == "" || ConfigJson.S3.Region == "" ||
ConfigJson.Cloudflare.UrlPrefix == "" || ConfigJson.Cloudflare.ZoneId == "" ||
ConfigJson.Cloudflare.Token == "")
{
Discord.Logger.LogWarning(BotEventId,
// ReSharper disable once LogMessageIsSentenceProblem
"CDN commands disabled due to missing S3 or Cloudflare information.");
DisabledCommands.Add("cdn");
}
else
{
Minio = new MinioClient()
.WithEndpoint(ConfigJson.S3.Endpoint)
.WithCredentials(ConfigJson.S3.AccessKey, ConfigJson.S3.SecretKey)
.WithRegion(ConfigJson.S3.Region)
.WithSSL();
}
if (ConfigJson.WorkerLinks is null || ConfigJson.Cloudflare is null
|| ConfigJson.WorkerLinks.BaseUrl == "" || ConfigJson.WorkerLinks.Secret == ""
|| ConfigJson.WorkerLinks.NamespaceId == "" || ConfigJson.WorkerLinks.ApiKey == ""
|| ConfigJson.Cloudflare.AccountId == "" || ConfigJson.WorkerLinks.Email == "")
{
Discord.Logger.LogWarning(BotEventId,
// ReSharper disable once LogMessageIsSentenceProblem
"Short-link commands disabled due to missing WorkerLinks information.");
DisabledCommands.Add("wl");
}
if (ConfigJson.Cloudflare is null || ConfigJson.Hastebin is null
|| ConfigJson.Cloudflare.AccountId == "" || ConfigJson.Hastebin.NamespaceId == ""
|| ConfigJson.Hastebin.Url == "")
{
Discord.Logger.LogWarning(BotEventId,
// ReSharper disable once LogMessageIsSentenceProblem
"Hastebin commands disabled due to missing Cloudflare or Hastebin information.");
DisabledCommands.Add("haste");
}
if (ConfigJson.Base is null || ConfigJson.Base.WolframAlphaAppId == "")
{
Discord.Logger.LogWarning(BotEventId,
// ReSharper disable once LogMessageIsSentenceProblem
"WolframAlpha commands disabled due to missing App ID.");
DisabledCommands.Add("wa");
}
if (ConfigJson.Ids is null || ConfigJson.Ids.FeedbackChannel == "")
{
Discord.Logger.LogWarning(BotEventId,
// ReSharper disable once LogMessageIsSentenceProblem
"Feedback command disabled due to missing channel ID.");
DisabledCommands.Add("feedback");
}
if (ConfigJson.WakeOnLan is null || ConfigJson.WakeOnLan.MacAddress == "" || ConfigJson.WakeOnLan.IpAddress == "" ||
ConfigJson.WakeOnLan.Port == 0 || ConfigJson.Err.SshUsername == "" || ConfigJson.Err.SshHost == "")
{
Discord.Logger.LogWarning(BotEventId,
// ReSharper disable once LogMessageIsSentenceProblem
"Error lookup command disabled due to missing Wake-on-LAN information.");
DisabledCommands.Add("err");
}
if (ConfigJson.Base.UptimeKumaHeartbeatUrl is null or "")
{
Discord.Logger.LogWarning(BotEventId, "Uptime Kuma heartbeats disabled due to missing push URL.");
}
await Discord.ConnectAsync();
/* Fix SSH key permissions at bot startup.
I wanted to be able to do this somewhere else, but for now it seems
like this is the best way of doing it that I'm aware of, and it works. */
#if !DEBUG
await EvalCommands.RunCommand("cat /app/id_ed25519 > ~/.ssh/id_ed25519 && chmod 700 ~/.ssh/id_ed25519");
#endif
// Run tasks
// Delay to give bot time to connect
await Task.Delay(TimeSpan.FromSeconds(1));
// Start tasks
// Populate ApplicationCommands
await Task.Run(async () => CommandTasks.ExecuteAsync());
// Package update check
await Task.Run(async () => PackageUpdateTasks.ExecuteAsync());
// Reminder check
await Task.Run(async () => ReminderTasks.ExecuteAsync());
// Database connection check
await Task.Run(async () => DatabaseTasks.ExecuteAsync());
// Custom status update
await Task.Run(async () => ActivityTasks.ExecuteAsync());
// DBots stats update
await Task.Run(async () => DBotsTasks.ExecuteAsync());
// Send startup message
await Program.HomeChannel.SendMessageAsync(await DebugInfoHelpers.GenerateDebugInfoEmbed(true));
// Wait indefinitely, let tasks continue running in async threads
await Task.Delay(System.Threading.Timeout.InfiniteTimeSpan);
}
}
// Set custom command attributes
// [RequireAuth] - used instead of [RequireOwner] or [SlashRequireOwner] to allow owners set in config.json
// to use commands instead of restricting them to the bot account owner.
public class RequireAuthAttribute : ContextCheckAttribute;
public class RequireAuthCheck : IContextCheck<RequireAuthAttribute>
{
#nullable enable
public ValueTask<string?> ExecuteCheckAsync(RequireAuthAttribute _, CommandContext ctx) =>
ValueTask.FromResult(Program.ConfigJson.Base.AuthorizedUsers.Contains(ctx.User.Id.ToString())
? null
: "The user is not authorized to use this command.");
}