Initial commit, basic proxying working

This commit is contained in:
Ske
2020-12-22 13:15:26 +01:00
parent c3f6becea4
commit a6fbd869be
109 changed files with 3539 additions and 359 deletions

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.WebSockets;
@@ -9,10 +10,10 @@ using App.Metrics;
using Autofac;
using DSharpPlus;
using DSharpPlus.Entities;
using DSharpPlus.EventArgs;
using DSharpPlus.Exceptions;
using Myriad.Cache;
using Myriad.Gateway;
using Myriad.Rest;
using Myriad.Types;
using NodaTime;
@@ -27,47 +28,38 @@ namespace PluralKit.Bot
{
public class Bot
{
private readonly DiscordShardedClient _client;
private readonly ConcurrentDictionary<ulong, GuildMemberPartial> _guildMembers = new();
private readonly Cluster _cluster;
private readonly DiscordApiClient _rest;
private readonly ILogger _logger;
private readonly ILifetimeScope _services;
private readonly PeriodicStatCollector _collector;
private readonly IMetrics _metrics;
private readonly ErrorMessageService _errorMessageService;
private readonly CommandMessageService _commandMessageService;
private readonly IDiscordCache _cache;
private bool _hasReceivedReady = false;
private Timer _periodicTask; // Never read, just kept here for GC reasons
public Bot(DiscordShardedClient client, ILifetimeScope services, ILogger logger, PeriodicStatCollector collector, IMetrics metrics,
ErrorMessageService errorMessageService, CommandMessageService commandMessageService)
public Bot(ILifetimeScope services, ILogger logger, PeriodicStatCollector collector, IMetrics metrics,
ErrorMessageService errorMessageService, CommandMessageService commandMessageService, Cluster cluster, DiscordApiClient rest, IDiscordCache cache)
{
_client = client;
_logger = logger.ForContext<Bot>();
_services = services;
_collector = collector;
_metrics = metrics;
_errorMessageService = errorMessageService;
_commandMessageService = commandMessageService;
_cluster = cluster;
_rest = rest;
_cache = cache;
}
public void Init()
{
// HandleEvent takes a type parameter, automatically inferred by the event type
// It will then look up an IEventHandler<TypeOfEvent> in the DI container and call that object's handler method
// For registering new ones, see Modules.cs
_client.MessageCreated += HandleEvent;
_client.MessageDeleted += HandleEvent;
_client.MessageUpdated += HandleEvent;
_client.MessagesBulkDeleted += HandleEvent;
_client.MessageReactionAdded += HandleEvent;
// Update shard status for shards immediately on connect
_client.Ready += (client, _) =>
{
_hasReceivedReady = true;
return UpdateBotStatus(client);
};
_client.Resumed += (client, _) => UpdateBotStatus(client);
_cluster.EventReceived += OnEventReceived;
// Init the shard stuff
_services.Resolve<ShardInfoService>().Init();
@@ -83,6 +75,58 @@ namespace PluralKit.Bot
}, null, timeTillNextWholeMinute, TimeSpan.FromMinutes(1));
}
public GuildMemberPartial? BotMemberIn(ulong guildId) => _guildMembers.GetValueOrDefault(guildId);
private async Task OnEventReceived(Shard shard, IGatewayEvent evt)
{
await _cache.HandleGatewayEvent(evt);
TryUpdateSelfMember(shard, evt);
// HandleEvent takes a type parameter, automatically inferred by the event type
// It will then look up an IEventHandler<TypeOfEvent> in the DI container and call that object's handler method
// For registering new ones, see Modules.cs
if (evt is MessageCreateEvent mc)
await HandleEvent(shard, mc);
if (evt is MessageUpdateEvent mu)
await HandleEvent(shard, mu);
if (evt is MessageDeleteEvent md)
await HandleEvent(shard, md);
if (evt is MessageDeleteBulkEvent mdb)
await HandleEvent(shard, mdb);
if (evt is MessageReactionAddEvent mra)
await HandleEvent(shard, mra);
// Update shard status for shards immediately on connect
if (evt is ReadyEvent re)
await HandleReady(shard, re);
if (evt is ResumedEvent)
await HandleResumed(shard);
}
private void TryUpdateSelfMember(Shard shard, IGatewayEvent evt)
{
if (evt is GuildCreateEvent gc)
_guildMembers[gc.Id] = gc.Members.FirstOrDefault(m => m.User.Id == shard.User?.Id);
if (evt is MessageCreateEvent mc && mc.Member != null && mc.Author.Id == shard.User?.Id)
_guildMembers[mc.GuildId!.Value] = mc.Member;
if (evt is GuildMemberAddEvent gma && gma.User.Id == shard.User?.Id)
_guildMembers[gma.GuildId] = gma;
if (evt is GuildMemberUpdateEvent gmu && gmu.User.Id == shard.User?.Id)
_guildMembers[gmu.GuildId] = gmu;
}
private Task HandleResumed(Shard shard)
{
return UpdateBotStatus(shard);
}
private Task HandleReady(Shard shard, ReadyEvent _)
{
_hasReceivedReady = true;
return UpdateBotStatus(shard);
}
public async Task Shutdown()
{
// This will stop the timer and prevent any subsequent invocations
@@ -92,10 +136,24 @@ namespace PluralKit.Bot
// We're not actually properly disconnecting from the gateway (lol) so it'll linger for a few minutes
// Should be plenty of time for the bot to connect again next startup and set the real status
if (_hasReceivedReady)
await _client.UpdateStatusAsync(new DiscordActivity("Restarting... (please wait)"), UserStatus.Idle);
{
await Task.WhenAll(_cluster.Shards.Values.Select(shard =>
shard.UpdateStatus(new GatewayStatusUpdate
{
Activities = new[]
{
new ActivityPartial
{
Name = "Restarting... (please wait)",
Type = ActivityType.Game
}
},
Status = GatewayStatusUpdate.UserStatus.Idle
})));
}
}
private Task HandleEvent<T>(DiscordClient shard, T evt) where T: DiscordEventArgs
private Task HandleEvent<T>(Shard shard, T evt) where T: IGatewayEvent
{
// We don't want to stall the event pipeline, so we'll "fork" inside here
var _ = HandleEventInner();
@@ -121,7 +179,7 @@ namespace PluralKit.Bot
try
{
using var timer = _metrics.Measure.Timer.Time(BotMetrics.EventsHandled,
new MetricTags("event", typeof(T).Name.Replace("EventArgs", "")));
new MetricTags("event", typeof(T).Name.Replace("Event", "")));
// Delegate to the queue to see if it wants to handle this event
// the TryHandle call returns true if it's handled the event
@@ -131,13 +189,13 @@ namespace PluralKit.Bot
}
catch (Exception exc)
{
await HandleError(handler, evt, serviceScope, exc);
await HandleError(shard, handler, evt, serviceScope, exc);
}
}
}
private async Task HandleError<T>(IEventHandler<T> handler, T evt, ILifetimeScope serviceScope, Exception exc)
where T: DiscordEventArgs
private async Task HandleError<T>(Shard shard, IEventHandler<T> handler, T evt, ILifetimeScope serviceScope, Exception exc)
where T: IGatewayEvent
{
_metrics.Measure.Meter.Mark(BotMetrics.BotErrors, exc.GetType().FullName);
@@ -149,7 +207,7 @@ namespace PluralKit.Bot
.Error(exc, "Exception in event handler: {SentryEventId}", sentryEvent.EventId);
// If the event is us responding to our own error messages, don't bother logging
if (evt is MessageCreateEventArgs mc && mc.Author.Id == _client.CurrentUser.Id)
if (evt is MessageCreateEvent mc && mc.Author.Id == shard.User?.Id)
return;
var shouldReport = exc.IsOurProblem();
@@ -160,19 +218,21 @@ namespace PluralKit.Bot
var sentryScope = serviceScope.Resolve<Scope>();
// Add some specific info about Discord error responses, as a breadcrumb
if (exc is BadRequestException bre)
sentryScope.AddBreadcrumb(bre.WebResponse.Response, "response.error", data: new Dictionary<string, string>(bre.WebResponse.Headers));
if (exc is NotFoundException nfe)
sentryScope.AddBreadcrumb(nfe.WebResponse.Response, "response.error", data: new Dictionary<string, string>(nfe.WebResponse.Headers));
if (exc is UnauthorizedException ue)
sentryScope.AddBreadcrumb(ue.WebResponse.Response, "response.error", data: new Dictionary<string, string>(ue.WebResponse.Headers));
// TODO: headers to dict
// if (exc is BadRequestException bre)
// sentryScope.AddBreadcrumb(bre.Response, "response.error", data: new Dictionary<string, string>(bre.Response.Headers));
// if (exc is NotFoundException nfe)
// sentryScope.AddBreadcrumb(nfe.Response, "response.error", data: new Dictionary<string, string>(nfe.Response.Headers));
// if (exc is UnauthorizedException ue)
// sentryScope.AddBreadcrumb(ue.Response, "response.error", data: new Dictionary<string, string>(ue.Response.Headers));
SentrySdk.CaptureEvent(sentryEvent, sentryScope);
// Once we've sent it to Sentry, report it to the user (if we have permission to)
var reportChannel = handler.ErrorChannelFor(evt);
if (reportChannel != null && reportChannel.BotHasAllPermissions(Permissions.SendMessages | Permissions.EmbedLinks))
await _errorMessageService.SendErrorMessage(reportChannel, sentryEvent.EventId.ToString());
// TODO: ID lookup
// if (reportChannel != null && reportChannel.BotHasAllPermissions(Permissions.SendMessages | Permissions.EmbedLinks))
// await _errorMessageService.SendErrorMessage(reportChannel, sentryEvent.EventId.ToString());
}
}
@@ -191,23 +251,38 @@ namespace PluralKit.Bot
_logger.Debug("Submitted metrics to backend");
}
private async Task UpdateBotStatus(DiscordClient specificShard = null)
private async Task UpdateBotStatus(Shard specificShard = null)
{
// If we're not on any shards, don't bother (this happens if the periodic timer fires before the first Ready)
if (!_hasReceivedReady) return;
var totalGuilds = _client.ShardClients.Values.Sum(c => c.Guilds.Count);
var totalGuilds = await _cache.GetAllGuilds().CountAsync();
try // DiscordClient may throw an exception if the socket is closed (e.g just after OP 7 received)
{
Task UpdateStatus(DiscordClient shard) =>
shard.UpdateStatusAsync(new DiscordActivity($"pk;help | in {totalGuilds} servers | shard #{shard.ShardId}"));
Task UpdateStatus(Shard shard) =>
shard.UpdateStatus(new GatewayStatusUpdate
{
Activities = new[]
{
new ActivityPartial
{
Name = $"pk;help | in {totalGuilds} servers | shard #{shard.ShardInfo?.ShardId}",
Type = ActivityType.Game,
Url = "https://pluralkit.me/"
}
}
});
if (specificShard != null)
await UpdateStatus(specificShard);
else // Run shard updates concurrently
await Task.WhenAll(_client.ShardClients.Values.Select(UpdateStatus));
await Task.WhenAll(_cluster.Shards.Values.Select(UpdateStatus));
}
catch (WebSocketException)
{
// TODO: this still thrown?
}
catch (WebSocketException) { }
}
}
}