diff --git a/DisCatSharp/Clients/BaseDiscordClient.cs b/DisCatSharp/Clients/BaseDiscordClient.cs
index 522589127..9cce61e61 100644
--- a/DisCatSharp/Clients/BaseDiscordClient.cs
+++ b/DisCatSharp/Clients/BaseDiscordClient.cs
@@ -1,352 +1,354 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2023 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#pragma warning disable CS0618
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Threading.Tasks;
using DisCatSharp.Entities;
using DisCatSharp.Enums;
using DisCatSharp.Exceptions;
using DisCatSharp.Net;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace DisCatSharp;
///
/// Represents a common base for various Discord Client implementations.
///
public abstract class BaseDiscordClient : IDisposable
{
///
/// Gets the api client.
///
internal protected DiscordApiClient ApiClient { get; }
///
/// Gets the configuration.
///
internal protected DiscordConfiguration Configuration { get; }
///
/// Gets the instance of the logger for this client.
///
public ILogger Logger { get; internal set; }
///
/// Gets the string representing the version of bot lib.
///
public string VersionString { get; }
///
/// Gets the bot library name.
///
public string BotLibrary { get; }
[Obsolete("Use GetLibraryDeveloperTeamAsync")]
public DisCatSharpTeam LibraryDeveloperTeamAsync
=> this.GetLibraryDevelopmentTeamAsync().Result;
///
/// Gets the current user.
///
public DiscordUser CurrentUser { get; internal set; }
///
/// Gets the current application.
///
public DiscordApplication CurrentApplication { get; internal set; }
///
/// Exposes a Http Client for custom operations.
///
public HttpClient RestClient { get; internal set; }
///
/// Gets the cached guilds for this client.
///
public abstract IReadOnlyDictionary Guilds { get; }
///
/// Gets the cached users for this client.
///
public ConcurrentDictionary UserCache { get; internal set; }
///
/// Gets the service provider.
/// This allows passing data around without resorting to static members.
/// Defaults to null.
///
internal IServiceProvider ServiceProvider { get; set; }
///
/// Gets the list of available voice regions. Note that this property will not contain VIP voice regions.
///
public IReadOnlyDictionary VoiceRegions
=> this.VoiceRegionsLazy.Value;
///
/// Gets the list of available voice regions. This property is meant as a way to modify .
///
protected internal ConcurrentDictionary InternalVoiceRegions { get; set; }
internal Lazy> VoiceRegionsLazy;
///
/// Initializes this Discord API client.
///
/// Configuration for this client.
protected BaseDiscordClient(DiscordConfiguration config)
{
this.Configuration = new DiscordConfiguration(config);
this.ServiceProvider = config.ServiceProvider;
if (this.ServiceProvider != null)
{
this.Configuration.LoggerFactory ??= config.ServiceProvider.GetService();
this.Logger = config.ServiceProvider.GetService>();
}
if (this.Configuration.LoggerFactory == null)
{
this.Configuration.LoggerFactory = new DefaultLoggerFactory();
this.Configuration.LoggerFactory.AddProvider(new DefaultLoggerProvider(this));
+ if (this.Configuration.EnableSentry)
+ this.Configuration.LoggerFactory.AddSentry(x => x.DiagnosticLevel = Sentry.SentryLevel.Error);
}
this.Logger ??= this.Configuration.LoggerFactory.CreateLogger();
this.ApiClient = new DiscordApiClient(this);
this.UserCache = new ConcurrentDictionary();
this.InternalVoiceRegions = new ConcurrentDictionary();
this.VoiceRegionsLazy = new Lazy>(() => new ReadOnlyDictionary(this.InternalVoiceRegions));
this.RestClient = new();
this.RestClient.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", Utilities.GetUserAgent());
this.RestClient.DefaultRequestHeaders.TryAddWithoutValidation("X-Discord-Locale", this.Configuration.Locale);
var a = typeof(DiscordClient).GetTypeInfo().Assembly;
var iv = a.GetCustomAttribute();
if (iv != null)
{
this.VersionString = iv.InformationalVersion;
}
else
{
var v = a.GetName().Version;
var vs = v.ToString(3);
if (v.Revision > 0)
this.VersionString = $"{vs}, CI build {v.Revision}";
}
this.BotLibrary = "DisCatSharp";
}
///
/// Gets the current API application.
///
public async Task GetCurrentApplicationAsync()
{
var tapp = await this.ApiClient.GetCurrentApplicationOauth2InfoAsync().ConfigureAwait(false);
var app = new DiscordApplication
{
Discord = this,
Id = tapp.Id,
Name = tapp.Name,
Description = tapp.Description,
Summary = tapp.Summary,
IconHash = tapp.IconHash,
RpcOrigins = tapp.RpcOrigins != null ? new ReadOnlyCollection(tapp.RpcOrigins) : null,
Flags = tapp.Flags,
IsHook = tapp.IsHook,
Type = tapp.Type,
PrivacyPolicyUrl = tapp.PrivacyPolicyUrl,
TermsOfServiceUrl = tapp.TermsOfServiceUrl,
CustomInstallUrl = tapp.CustomInstallUrl,
InstallParams = tapp.InstallParams,
RoleConnectionsVerificationUrl = tapp.RoleConnectionsVerificationUrl,
Tags = (tapp.Tags ?? Enumerable.Empty()).ToArray()
};
if (tapp.Team == null)
{
app.Owners = new List(new[] { new DiscordUser(tapp.Owner) });
app.Team = null;
app.TeamName = null;
}
else
{
app.Team = new DiscordTeam(tapp.Team);
var members = tapp.Team.Members
.Select(x => new DiscordTeamMember(x) { TeamId = app.Team.Id, TeamName = app.Team.Name, User = new DiscordUser(x.User) })
.ToArray();
var owners = members
.Where(x => x.MembershipStatus == DiscordTeamMembershipStatus.Accepted)
.Select(x => x.User)
.ToArray();
app.Owners = new List(owners);
app.Team.Owner = owners.FirstOrDefault(x => x.Id == tapp.Team.OwnerId);
app.Team.Members = new List(members);
app.TeamName = app.Team.Name;
}
app.GuildId = tapp.GuildId.ValueOrDefault();
app.Slug = tapp.Slug.ValueOrDefault();
app.PrimarySkuId = tapp.PrimarySkuId.ValueOrDefault();
app.VerifyKey = tapp.VerifyKey.ValueOrDefault();
app.CoverImageHash = tapp.CoverImageHash.ValueOrDefault();
app.Guild = tapp.Guild.ValueOrDefault();
app.ApproximateGuildCount = tapp.ApproximateGuildCount.ValueOrDefault();
app.RequiresCodeGrant = tapp.BotRequiresCodeGrant.ValueOrDefault();
app.IsPublic = tapp.IsPublicBot.ValueOrDefault();
app.RedirectUris = tapp.RedirectUris.ValueOrDefault();
app.InteractionsEndpointUrl = tapp.InteractionsEndpointUrl.ValueOrDefault();
return app;
}
///
/// Updates the current API application.
///
/// The new description.
/// The new interactions endpoint url.
/// The new role connections verification url.
/// The new tags.
/// The new application icon.
/// The updated application.
public async Task UpdateCurrentApplicationInfoAsync(Optional description, Optional interactionsEndpointUrl, Optional roleConnectionsVerificationUrl, Optional?> tags, Optional icon)
{
var iconb64 = ImageTool.Base64FromStream(icon);
if (tags != null && tags.HasValue && tags.Value != null)
if (tags.Value.Any(x => x.Length > 20))
throw new InvalidOperationException("Tags can not exceed 20 chars.");
_ = await this.ApiClient.ModifyCurrentApplicationInfoAsync(description, interactionsEndpointUrl, roleConnectionsVerificationUrl, tags, iconb64);
// We use GetCurrentApplicationAsync because modify returns internal data not meant for developers.
var app = await this.GetCurrentApplicationAsync();
this.CurrentApplication = app;
return app;
}
///
/// Gets a list of voice regions.
///
/// Thrown when Discord is unable to process the request.
public Task> ListVoiceRegionsAsync()
=> this.ApiClient.ListVoiceRegionsAsync();
///
/// Initializes this client. This method fetches information about current user, application, and voice regions.
///
public virtual async Task InitializeAsync()
{
if (this.CurrentUser == null)
{
this.CurrentUser = await this.ApiClient.GetCurrentUserAsync().ConfigureAwait(false);
this.UserCache.AddOrUpdate(this.CurrentUser.Id, this.CurrentUser, (id, xu) => this.CurrentUser);
}
if (this.Configuration.TokenType == TokenType.Bot && this.CurrentApplication == null)
this.CurrentApplication = await this.GetCurrentApplicationAsync().ConfigureAwait(false);
if (this.Configuration.TokenType != TokenType.Bearer && this.InternalVoiceRegions.IsEmpty)
{
var vrs = await this.ListVoiceRegionsAsync().ConfigureAwait(false);
foreach (var xvr in vrs)
this.InternalVoiceRegions.TryAdd(xvr.Id, xvr);
}
}
///
/// Gets the current gateway info for the provided token.
/// If no value is provided, the configuration value will be used instead.
///
/// A gateway info object.
public async Task GetGatewayInfoAsync(string token = null)
{
if (this.Configuration.TokenType != TokenType.Bot)
throw new InvalidOperationException("Only bot tokens can access this info.");
if (string.IsNullOrEmpty(this.Configuration.Token))
{
if (string.IsNullOrEmpty(token))
throw new InvalidOperationException("Could not locate a valid token.");
this.Configuration.Token = token;
var res = await this.ApiClient.GetGatewayInfoAsync().ConfigureAwait(false);
this.Configuration.Token = null;
return res;
}
return await this.ApiClient.GetGatewayInfoAsync().ConfigureAwait(false);
}
///
/// Gets some information about the development team behind DisCatSharp.
/// Can be used for crediting etc.
/// Note: This call contacts servers managed by the DCS team, no information is collected.
/// The team, or null with errors being logged on failure.
///
[Obsolete("Don't use this right now, inactive")]
public async Task GetLibraryDevelopmentTeamAsync()
=> await DisCatSharpTeam.Get(this.RestClient, this.Logger, this.ApiClient).ConfigureAwait(false);
///
/// Gets a cached user.
///
/// The user id.
internal DiscordUser GetCachedOrEmptyUserInternal(ulong userId)
{
this.TryGetCachedUserInternal(userId, out var user);
return user;
}
///
/// Tries the get a cached user.
///
/// The user id.
/// The user.
internal bool TryGetCachedUserInternal(ulong userId, out DiscordUser user)
{
if (this.UserCache.TryGetValue(userId, out user))
return true;
user = new DiscordUser { Id = userId, Discord = this };
return false;
}
///
/// Disposes this client.
///
public abstract void Dispose();
}
diff --git a/DisCatSharp/Clients/DiscordClient.Dispatch.cs b/DisCatSharp/Clients/DiscordClient.Dispatch.cs
index c65184610..59d3f85ee 100644
--- a/DisCatSharp/Clients/DiscordClient.Dispatch.cs
+++ b/DisCatSharp/Clients/DiscordClient.Dispatch.cs
@@ -1,3583 +1,3586 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2023 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using DisCatSharp.Common;
using DisCatSharp.Entities;
using DisCatSharp.Enums;
using DisCatSharp.EventArgs;
using DisCatSharp.Exceptions;
using DisCatSharp.Net.Abstractions;
using DisCatSharp.Net.Serialization;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
namespace DisCatSharp;
///
/// Represents a discord Logger.ent.L
///
public sealed partial class DiscordClient
{
#region Private Fields
private string _resumeGatewayUrl;
private string _sessionId;
private bool _guildDownloadCompleted;
private readonly Dictionary> _tempTimers = new();
///
/// Represents a timeout handler.
///
internal class TimeoutHandler
{
///
/// Gets the member.
///
internal readonly DiscordMember Member;
///
/// Gets the guild.
///
internal readonly DiscordGuild Guild;
///
/// Gets the old timeout value.
///
internal DateTime? TimeoutUntilOld;
///
/// Gets the new timeout value.
///
internal DateTime? TimeoutUntilNew;
///
/// Constructs a new .
///
/// The affected member.
/// The affected guild.
/// The old timeout value.
/// The new timeout value.
internal TimeoutHandler(DiscordMember mbr, DiscordGuild guild, DateTime? too, DateTime? ton)
{
this.Guild = guild;
this.Member = mbr;
this.TimeoutUntilOld = too;
this.TimeoutUntilNew = ton;
}
}
#endregion
#region Dispatch Handler
///
/// Handles the dispatch payloads.
///
/// The payload.
internal async Task HandleDispatchAsync(GatewayPayload payload)
{
if (payload.Data is not JObject dat)
{
this.Logger.LogWarning(LoggerEvents.WebSocketReceive, "Invalid payload body (this message is probably safe to ignore); opcode: {0} event: {1}; payload: {2}", payload.OpCode, payload.EventName, payload.Data);
return;
}
await this._payloadReceived.InvokeAsync(this, new PayloadReceivedEventArgs(this.ServiceProvider)
{
EventName = payload.EventName,
PayloadObject = dat
}).ConfigureAwait(false);
#region Default objects
DiscordChannel chn;
ulong gid;
ulong cid;
ulong uid;
DiscordStageInstance stg = default;
DiscordIntegration itg = default;
DiscordThreadChannel trd = default;
DiscordThreadChannelMember trdm = default;
DiscordScheduledEvent gse = default;
TransportUser usr = default;
TransportMember mbr = default;
TransportUser refUsr = default;
TransportMember refMbr = default;
JToken rawMbr = default;
var rawRefMsg = dat["referenced_message"];
#endregion
switch (payload.EventName.ToLowerInvariant())
{
#region Gateway Status
case "ready":
var glds = (JArray)dat["guilds"];
await this.OnReadyEventAsync(dat.ToObject(), glds).ConfigureAwait(false);
break;
case "resumed":
await this.OnResumedAsync().ConfigureAwait(false);
break;
#endregion
#region Channel
case "channel_create":
chn = dat.ToObject();
await this.OnChannelCreateEventAsync(chn).ConfigureAwait(false);
break;
case "channel_update":
await this.OnChannelUpdateEventAsync(dat.ToObject()).ConfigureAwait(false);
break;
case "channel_delete":
chn = dat.ToObject();
await this.OnChannelDeleteEventAsync(chn.IsPrivate ? dat.ToObject() : chn).ConfigureAwait(false);
break;
case "channel_pins_update":
cid = (ulong)dat["channel_id"];
var ts = (string)dat["last_pin_timestamp"];
await this.OnChannelPinsUpdateAsync((ulong?)dat["guild_id"], cid, ts != null ? DateTimeOffset.Parse(ts, CultureInfo.InvariantCulture) : default(DateTimeOffset?)).ConfigureAwait(false);
break;
#endregion
#region Guild
case "guild_create":
await this.OnGuildCreateEventAsync(dat.ToDiscordObject(), (JArray)dat["members"], dat["presences"].ToDiscordObject>()).ConfigureAwait(false);
break;
case "guild_update":
await this.OnGuildUpdateEventAsync(dat.ToDiscordObject(), (JArray)dat["members"]).ConfigureAwait(false);
break;
case "guild_delete":
await this.OnGuildDeleteEventAsync(dat.ToDiscordObject()).ConfigureAwait(false);
break;
case "guild_audit_log_entry_create":
gid = (ulong)dat["guild_id"];
dat.Remove("guild_id");
await this.OnGuildAuditLogEntryCreateEventAsync(this.GuildsInternal[gid], dat).ConfigureAwait(false);
break;
case "guild_sync":
gid = (ulong)dat["id"];
await this.OnGuildSyncEventAsync(this.GuildsInternal[gid], (bool)dat["large"], (JArray)dat["members"], dat["presences"].ToDiscordObject>()).ConfigureAwait(false);
break;
case "guild_emojis_update":
gid = (ulong)dat["guild_id"];
var ems = dat["emojis"].ToObject>();
await this.OnGuildEmojisUpdateEventAsync(this.GuildsInternal[gid], ems).ConfigureAwait(false);
break;
case "guild_stickers_update":
gid = (ulong)dat["guild_id"];
var strs = dat["stickers"].ToDiscordObject>();
await this.OnStickersUpdatedAsync(strs, gid).ConfigureAwait(false);
break;
case "guild_integrations_update":
gid = (ulong)dat["guild_id"];
// discord fires this event inconsistently if the current user leaves a guild.
if (!this.GuildsInternal.ContainsKey(gid))
return;
await this.OnGuildIntegrationsUpdateEventAsync(this.GuildsInternal[gid]).ConfigureAwait(false);
break;
/*
case "guild_join_request_create":
break;
case "guild_join_request_update":
break;
case "guild_join_request_delete":
break;
*/
#endregion
#region Guild Automod
case "auto_moderation_rule_create":
await this.OnAutomodRuleCreated(dat.ToDiscordObject());
break;
case "auto_moderation_rule_update":
await this.OnAutomodRuleUpdated(dat.ToDiscordObject());
break;
case "auto_moderation_rule_delete":
await this.OnAutomodRuleDeleted(dat.ToDiscordObject());
break;
case "auto_moderation_action_execution":
gid = (ulong)dat["guild_id"];
await this.OnAutomodActionExecuted(this.GuildsInternal[gid], dat);
break;
#endregion
#region Guild Ban
case "guild_ban_add":
usr = dat["user"].ToObject();
gid = (ulong)dat["guild_id"];
await this.OnGuildBanAddEventAsync(usr, this.GuildsInternal[gid]).ConfigureAwait(false);
break;
case "guild_ban_remove":
usr = dat["user"].ToObject();
gid = (ulong)dat["guild_id"];
await this.OnGuildBanRemoveEventAsync(usr, this.GuildsInternal[gid]).ConfigureAwait(false);
break;
#endregion
#region Guild Event
case "guild_scheduled_event_create":
gse = dat.ToObject();
gid = (ulong)dat["guild_id"];
await this.OnGuildScheduledEventCreateEventAsync(gse, this.GuildsInternal[gid]).ConfigureAwait(false);
break;
case "guild_scheduled_event_update":
gse = dat.ToObject();
gid = (ulong)dat["guild_id"];
await this.OnGuildScheduledEventUpdateEventAsync(gse, this.GuildsInternal[gid]).ConfigureAwait(false);
break;
case "guild_scheduled_event_delete":
gse = dat.ToObject();
gid = (ulong)dat["guild_id"];
await this.OnGuildScheduledEventDeleteEventAsync(gse, this.GuildsInternal[gid]).ConfigureAwait(false);
break;
case "guild_scheduled_event_user_add":
gid = (ulong)dat["guild_id"];
uid = (ulong)dat["user_id"];
await this.OnGuildScheduledEventUserAddedEventAsync((ulong)dat["guild_scheduled_event_id"], uid, this.GuildsInternal[gid]).ConfigureAwait(false);
break;
case "guild_scheduled_event_user_remove":
gid = (ulong)dat["guild_id"];
uid = (ulong)dat["user_id"];
await this.OnGuildScheduledEventUserRemovedEventAsync((ulong)dat["guild_scheduled_event_id"], uid, this.GuildsInternal[gid]).ConfigureAwait(false);
break;
#endregion
#region Guild Integration
case "integration_create":
gid = (ulong)dat["guild_id"];
itg = dat.ToObject();
// discord fires this event inconsistently if the current user leaves a guild.
if (!this.GuildsInternal.ContainsKey(gid))
return;
await this.OnGuildIntegrationCreateEventAsync(this.GuildsInternal[gid], itg).ConfigureAwait(false);
break;
case "integration_update":
gid = (ulong)dat["guild_id"];
itg = dat.ToObject();
// discord fires this event inconsistently if the current user leaves a guild.
if (!this.GuildsInternal.ContainsKey(gid))
return;
await this.OnGuildIntegrationUpdateEventAsync(this.GuildsInternal[gid], itg).ConfigureAwait(false);
break;
case "integration_delete":
gid = (ulong)dat["guild_id"];
// discord fires this event inconsistently if the current user leaves a guild.
if (!this.GuildsInternal.ContainsKey(gid))
return;
await this.OnGuildIntegrationDeleteEventAsync(this.GuildsInternal[gid], (ulong)dat["id"], (ulong?)dat["application_id"]).ConfigureAwait(false);
break;
#endregion
#region Guild Member
case "guild_member_add":
gid = (ulong)dat["guild_id"];
await this.OnGuildMemberAddEventAsync(dat.ToObject(), this.GuildsInternal[gid]).ConfigureAwait(false);
break;
case "guild_member_remove":
gid = (ulong)dat["guild_id"];
usr = dat["user"].ToObject();
if (!this.GuildsInternal.ContainsKey(gid))
{
// discord fires this event inconsistently if the current user leaves a guild.
if (usr.Id != this.CurrentUser.Id)
this.Logger.LogError(LoggerEvents.WebSocketReceive, "Could not find {0} in guild cache", gid);
return;
}
await this.OnGuildMemberRemoveEventAsync(usr, this.GuildsInternal[gid]).ConfigureAwait(false);
break;
case "guild_member_update":
gid = (ulong)dat["guild_id"];
await this.OnGuildMemberUpdateEventAsync(dat.ToDiscordObject(), this.GuildsInternal[gid], dat["roles"].ToObject>(), (string)dat["nick"], (bool?)dat["pending"]).ConfigureAwait(false);
break;
case "guild_members_chunk":
await this.OnGuildMembersChunkEventAsync(dat).ConfigureAwait(false);
break;
#endregion
#region Guild Role
case "guild_role_create":
gid = (ulong)dat["guild_id"];
await this.OnGuildRoleCreateEventAsync(dat["role"].ToObject(), this.GuildsInternal[gid]).ConfigureAwait(false);
break;
case "guild_role_update":
gid = (ulong)dat["guild_id"];
await this.OnGuildRoleUpdateEventAsync(dat["role"].ToObject(), this.GuildsInternal[gid]).ConfigureAwait(false);
break;
case "guild_role_delete":
gid = (ulong)dat["guild_id"];
await this.OnGuildRoleDeleteEventAsync((ulong)dat["role_id"], this.GuildsInternal[gid]).ConfigureAwait(false);
break;
#endregion
#region Invite
case "invite_create":
gid = (ulong)dat["guild_id"];
cid = (ulong)dat["channel_id"];
await this.OnInviteCreateEventAsync(cid, gid, dat.ToObject()).ConfigureAwait(false);
break;
case "invite_delete":
gid = (ulong)dat["guild_id"];
cid = (ulong)dat["channel_id"];
await this.OnInviteDeleteEventAsync(cid, gid, dat).ConfigureAwait(false);
break;
#endregion
#region Message
case "message_ack":
cid = (ulong)dat["channel_id"];
var mid = (ulong)dat["message_id"];
await this.OnMessageAckEventAsync(this.InternalGetCachedChannel(cid), mid).ConfigureAwait(false);
break;
case "message_create":
rawMbr = dat["member"];
if (rawMbr != null)
mbr = rawMbr.ToObject();
if (rawRefMsg != null && rawRefMsg.HasValues)
{
if (rawRefMsg.SelectToken("author") != null)
{
refUsr = rawRefMsg.SelectToken("author").ToObject();
}
if (rawRefMsg.SelectToken("member") != null)
{
refMbr = rawRefMsg.SelectToken("member").ToObject();
}
}
await this.OnMessageCreateEventAsync(dat.ToDiscordObject(), dat["author"].ToObject(), mbr, refUsr, refMbr).ConfigureAwait(false);
break;
case "message_update":
rawMbr = dat["member"];
if (rawMbr != null)
mbr = rawMbr.ToObject();
if (rawRefMsg != null && rawRefMsg.HasValues)
{
if (rawRefMsg.SelectToken("author") != null)
{
refUsr = rawRefMsg.SelectToken("author").ToObject();
}
if (rawRefMsg.SelectToken("member") != null)
{
refMbr = rawRefMsg.SelectToken("member").ToObject();
}
}
await this.OnMessageUpdateEventAsync(dat.ToDiscordObject(), dat["author"]?.ToObject(), mbr, refUsr, refMbr).ConfigureAwait(false);
break;
// delete event does *not* include message object
case "message_delete":
await this.OnMessageDeleteEventAsync((ulong)dat["id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"]).ConfigureAwait(false);
break;
case "message_delete_bulk":
await this.OnMessageBulkDeleteEventAsync(dat["ids"].ToObject(), (ulong)dat["channel_id"], (ulong?)dat["guild_id"]).ConfigureAwait(false);
break;
#endregion
#region Message Reaction
case "message_reaction_add":
rawMbr = dat["member"];
if (rawMbr != null)
mbr = rawMbr.ToObject();
// TODO: Add burst stuff
+ this.Logger.LogDebug("Reaction add: {object}", dat.ToString(Newtonsoft.Json.Formatting.Indented));
await this.OnMessageReactionAddAsync((ulong)dat["user_id"], (ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"], mbr, dat["emoji"].ToObject(), (bool)dat["burst"]).ConfigureAwait(false);
break;
case "message_reaction_remove":
+ this.Logger.LogDebug("Reaction removed: {object}", dat.ToString(Newtonsoft.Json.Formatting.Indented));
await this.OnMessageReactionRemoveAsync((ulong)dat["user_id"], (ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"], dat["emoji"].ToObject(), (bool)dat["burst"]).ConfigureAwait(false);
break;
case "message_reaction_remove_all":
await this.OnMessageReactionRemoveAllAsync((ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"]).ConfigureAwait(false);
break;
case "message_reaction_remove_emoji":
+ this.Logger.LogDebug(dat.ToString(Newtonsoft.Json.Formatting.Indented));
await this.OnMessageReactionRemoveEmojiAsync((ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong)dat["guild_id"], dat["emoji"]).ConfigureAwait(false);
break;
#endregion
#region Stage Instance
case "stage_instance_create":
stg = dat.ToObject();
await this.OnStageInstanceCreateEventAsync(stg).ConfigureAwait(false);
break;
case "stage_instance_update":
stg = dat.ToObject();
await this.OnStageInstanceUpdateEventAsync(stg).ConfigureAwait(false);
break;
case "stage_instance_delete":
stg = dat.ToObject();
await this.OnStageInstanceDeleteEventAsync(stg).ConfigureAwait(false);
break;
#endregion
#region Thread
case "thread_create":
trd = dat.ToObject();
await this.OnThreadCreateEventAsync(trd).ConfigureAwait(false);
break;
case "thread_update":
trd = dat.ToObject();
await this.OnThreadUpdateEventAsync(trd).ConfigureAwait(false);
break;
case "thread_delete":
trd = dat.ToObject();
await this.OnThreadDeleteEventAsync(trd).ConfigureAwait(false);
break;
case "thread_list_sync":
gid = (ulong)dat["guild_id"]; //get guild
await this.OnThreadListSyncEventAsync(this.GuildsInternal[gid], dat["channel_ids"].ToObject>(), dat["threads"].ToObject>(), dat["members"].ToObject>()).ConfigureAwait(false);
break;
case "thread_member_update":
trdm = dat.ToObject();
await this.OnThreadMemberUpdateEventAsync(trdm).ConfigureAwait(false);
break;
case "thread_members_update":
gid = (ulong)dat["guild_id"];
await this.OnThreadMembersUpdateEventAsync(this.GuildsInternal[gid], (ulong)dat["id"], (JArray)dat["added_members"], (JArray)dat["removed_member_ids"], (int)dat["member_count"]).ConfigureAwait(false);
break;
#endregion
#region Activities
case "embedded_activity_update":
gid = (ulong)dat["guild_id"];
cid = (ulong)dat["channel_id"];
await this.OnEmbeddedActivityUpdateAsync((JObject)dat["embedded_activity"], this.GuildsInternal[gid], cid, (JArray)dat["users"], (ulong)dat["embedded_activity"]["application_id"]).ConfigureAwait(false);
break;
#endregion
#region User/Presence Update
case "presence_update":
await this.OnPresenceUpdateEventAsync(dat, (JObject)dat["user"]).ConfigureAwait(false);
break;
case "user_settings_update":
await this.OnUserSettingsUpdateEventAsync(dat.ToObject()).ConfigureAwait(false);
break;
case "user_update":
await this.OnUserUpdateEventAsync(dat.ToObject()).ConfigureAwait(false);
break;
#endregion
#region Voice
case "voice_state_update":
await this.OnVoiceStateUpdateEventAsync(dat).ConfigureAwait(false);
break;
case "voice_server_update":
gid = (ulong)dat["guild_id"];
await this.OnVoiceServerUpdateEventAsync((string)dat["endpoint"], (string)dat["token"], this.GuildsInternal[gid]).ConfigureAwait(false);
break;
#endregion
#region Interaction/Integration/Application
case "interaction_create":
rawMbr = dat["member"];
if (rawMbr != null)
{
mbr = dat["member"].ToObject();
usr = mbr.User;
}
else
{
usr = dat["user"].ToObject();
}
cid = (ulong)dat["channel_id"];
// Console.WriteLine(dat.ToString()); // Get raw interaction payload.
await this.OnInteractionCreateAsync((ulong?)dat["guild_id"], cid, usr, mbr, dat.ToDiscordObject(), dat.ToString()).ConfigureAwait(false);
break;
case "application_command_create":
await this.OnApplicationCommandCreateAsync(dat.ToObject(), (ulong?)dat["guild_id"]).ConfigureAwait(false);
break;
case "application_command_update":
await this.OnApplicationCommandUpdateAsync(dat.ToObject(), (ulong?)dat["guild_id"]).ConfigureAwait(false);
break;
case "application_command_delete":
await this.OnApplicationCommandDeleteAsync(dat.ToObject(), (ulong?)dat["guild_id"]).ConfigureAwait(false);
break;
case "guild_application_command_counts_update":
var counts = dat["application_command_counts"];
await this.OnGuildApplicationCommandCountsUpdateAsync((int)counts["1"], (int)counts["2"], (int)counts["3"], (ulong)dat["guild_id"]).ConfigureAwait(false);
break;
case "guild_application_command_index_update":
// TODO: Implement.
break;
case "application_command_permissions_update":
var aid = (ulong)dat["application_id"];
if (aid != this.CurrentApplication.Id)
return;
var pms = dat["permissions"].ToObject>();
gid = (ulong)dat["guild_id"];
await this.OnApplicationCommandPermissionsUpdateAsync(pms, (ulong)dat["id"], gid, aid).ConfigureAwait(false);
break;
#endregion
#region Misc
case "gift_code_update": //Not supposed to be dispatched to bots
break;
case "typing_start":
cid = (ulong)dat["channel_id"];
rawMbr = dat["member"];
if (rawMbr != null)
mbr = rawMbr.ToObject();
await this.OnTypingStartEventAsync((ulong)dat["user_id"], cid, this.InternalGetCachedChannel(cid), (ulong?)dat["guild_id"], Utilities.GetDateTimeOffset((long)dat["timestamp"]), mbr).ConfigureAwait(false);
break;
case "webhooks_update":
gid = (ulong)dat["guild_id"];
cid = (ulong)dat["channel_id"];
await this.OnWebhooksUpdateAsync(this.GuildsInternal[gid].GetChannel(cid), this.GuildsInternal[gid]).ConfigureAwait(false);
break;
case "guild_join_request_update":
case "guild_join_request_create":
case "guild_join_request_delete": // Deprecated
break;
default:
await this.OnUnknownEventAsync(payload).ConfigureAwait(false);
this.Logger.LogWarning(LoggerEvents.WebSocketReceive, "Unknown event: {0}\npayload: {1}", payload.EventName, dat.ToString(Newtonsoft.Json.Formatting.Indented));
break;
#endregion
}
}
#endregion
#region Events
#region Gateway
///
/// Handles the ready event.
///
/// The ready payload.
/// The raw guilds.
internal async Task OnReadyEventAsync(ReadyPayload ready, JArray rawGuilds)
{
//ready.CurrentUser.Discord = this;
var rusr = ready.CurrentUser;
this.CurrentUser.Username = rusr.Username;
this.CurrentUser.Discriminator = rusr.Discriminator;
this.CurrentUser.AvatarHash = rusr.AvatarHash;
this.CurrentUser.MfaEnabled = rusr.MfaEnabled;
this.CurrentUser.Verified = rusr.Verified;
this.CurrentUser.IsBot = rusr.IsBot;
this.CurrentUser.Flags = rusr.Flags;
this.CurrentUser.GlobalName = rusr.GlobalName;
this.GatewayVersion = ready.GatewayVersion;
this._sessionId = ready.SessionId;
this._resumeGatewayUrl = ready.ResumeGatewayUrl;
var rawGuildIndex = rawGuilds.ToDictionary(xt => (ulong)xt["id"], xt => (JObject)xt);
this.GuildsInternal.Clear();
foreach (var guild in ready.Guilds)
{
guild.Discord = this;
guild.ChannelsInternal ??= new ConcurrentDictionary();
foreach (var xc in guild.Channels.Values)
{
xc.GuildId = guild.Id;
xc.Initialize(this);
}
guild.RolesInternal ??= new ConcurrentDictionary();
foreach (var xr in guild.Roles.Values)
{
xr.Discord = this;
xr.GuildId = guild.Id;
}
var rawGuild = rawGuildIndex[guild.Id];
var rawMembers = (JArray)rawGuild["members"];
if (guild.MembersInternal != null)
guild.MembersInternal.Clear();
else
guild.MembersInternal = new ConcurrentDictionary();
if (rawMembers != null)
{
foreach (var xj in rawMembers)
{
var xtm = xj.ToObject();
var xu = new DiscordUser(xtm.User) { Discord = this };
xu = this.UserCache.AddOrUpdate(xtm.User.Id, xu, (id, old) =>
{
old.Username = xu.Username;
old.Discriminator = xu.Discriminator;
old.AvatarHash = xu.AvatarHash;
old.GlobalName = xu.GlobalName;
return old;
});
guild.MembersInternal[xtm.User.Id] = new DiscordMember(xtm) { Discord = this, GuildId = guild.Id };
}
}
guild.EmojisInternal ??= new ConcurrentDictionary();
foreach (var xe in guild.Emojis.Values)
xe.Discord = this;
guild.StickersInternal ??= new ConcurrentDictionary();
foreach (var xs in guild.Stickers.Values)
xs.Discord = this;
guild.VoiceStatesInternal ??= new ConcurrentDictionary();
foreach (var xvs in guild.VoiceStates.Values)
xvs.Discord = this;
guild.ThreadsInternal ??= new ConcurrentDictionary();
foreach (var xt in guild.ThreadsInternal.Values)
xt.Discord = this;
guild.StageInstancesInternal ??= new ConcurrentDictionary();
foreach (var xsi in guild.StageInstancesInternal.Values)
xsi.Discord = this;
guild.ScheduledEventsInternal ??= new ConcurrentDictionary();
foreach (var xse in guild.ScheduledEventsInternal.Values)
xse.Discord = this;
this.GuildsInternal[guild.Id] = guild;
}
await this._ready.InvokeAsync(this, new ReadyEventArgs(this.ServiceProvider)).ConfigureAwait(false);
}
///
/// Handles the resumed event.
///
internal Task OnResumedAsync()
{
this.Logger.LogInformation(LoggerEvents.SessionUpdate, "Session resumed");
return this._resumed.InvokeAsync(this, new ReadyEventArgs(this.ServiceProvider));
}
#endregion
#region Channel
///
/// Handles the channel create event.
///
/// The channel.
internal async Task OnChannelCreateEventAsync(DiscordChannel channel)
{
channel.Initialize(this);
this.GuildsInternal[channel.GuildId.Value].ChannelsInternal[channel.Id] = channel;
/*if (this.Configuration.AutoRefreshChannelCache)
{
await this.RefreshChannelsAsync(channel.Guild.Id);
}*/
await this._channelCreated.InvokeAsync(this, new ChannelCreateEventArgs(this.ServiceProvider) { Channel = channel, Guild = channel.Guild }).ConfigureAwait(false);
}
///
/// Handles the channel update event.
///
/// The channel.
internal async Task OnChannelUpdateEventAsync(DiscordChannel channel)
{
if (channel == null)
return;
channel.Discord = this;
var gld = channel.Guild;
var channelNew = this.InternalGetCachedChannel(channel.Id);
DiscordChannel channelOld = null;
if (channelNew != null)
{
channelOld = new DiscordChannel
{
Bitrate = channelNew.Bitrate,
Discord = this,
GuildId = channelNew.GuildId,
Id = channelNew.Id,
LastMessageId = channelNew.LastMessageId,
Name = channelNew.Name,
PermissionOverwritesInternal = new List(channelNew.PermissionOverwritesInternal),
Position = channelNew.Position,
Topic = channelNew.Topic,
Type = channelNew.Type,
UserLimit = channelNew.UserLimit,
ParentId = channelNew.ParentId,
IsNsfw = channelNew.IsNsfw,
PerUserRateLimit = channelNew.PerUserRateLimit,
RtcRegionId = channelNew.RtcRegionId,
QualityMode = channelNew.QualityMode,
DefaultAutoArchiveDuration = channelNew.DefaultAutoArchiveDuration,
};
channelNew.Bitrate = channel.Bitrate;
channelNew.Name = channel.Name;
channelNew.Position = channel.Position;
channelNew.Topic = channel.Topic;
channelNew.UserLimit = channel.UserLimit;
channelNew.ParentId = channel.ParentId;
channelNew.IsNsfw = channel.IsNsfw;
channelNew.PerUserRateLimit = channel.PerUserRateLimit;
channelNew.Type = channel.Type;
channelNew.RtcRegionId = channel.RtcRegionId;
channelNew.QualityMode = channel.QualityMode;
channelNew.DefaultAutoArchiveDuration = channel.DefaultAutoArchiveDuration;
channelNew.PermissionOverwritesInternal.Clear();
channel.Initialize(this);
channelNew.PermissionOverwritesInternal.AddRange(channel.PermissionOverwritesInternal);
if (channel.Type == ChannelType.Forum)
{
channelOld.PostCreateUserRateLimit = channelNew.PostCreateUserRateLimit;
channelOld.InternalAvailableTags = channelNew.InternalAvailableTags;
channelOld.Template = channelNew.Template;
channelOld.DefaultReactionEmoji = channelNew.DefaultReactionEmoji;
channelOld.DefaultSortOrder = channelNew.DefaultSortOrder;
channelNew.PostCreateUserRateLimit = channel.PostCreateUserRateLimit;
channelNew.Template = channel.Template;
channelNew.DefaultReactionEmoji = channel.DefaultReactionEmoji;
channelNew.DefaultSortOrder = channel.DefaultSortOrder;
if (channelNew.InternalAvailableTags != null && channelNew.InternalAvailableTags.Any())
channelNew.InternalAvailableTags.Clear();
if (channel.InternalAvailableTags != null && channel.InternalAvailableTags.Any())
channelNew.InternalAvailableTags.AddRange(channel.InternalAvailableTags);
}
else
{
channelOld.PostCreateUserRateLimit = null;
channelOld.InternalAvailableTags = null;
channelOld.Template = null;
channelOld.DefaultReactionEmoji = null;
channelOld.DefaultSortOrder = null;
channelNew.PostCreateUserRateLimit = null;
channelNew.InternalAvailableTags = null;
channelNew.Template = null;
channelNew.DefaultReactionEmoji = null;
channelNew.DefaultSortOrder = null;
}
channelOld.Initialize(this);
channelNew.Initialize(this);
if (this.Configuration.AutoRefreshChannelCache && gld != null)
{
await this.RefreshChannelsAsync(channel.Guild.Id);
}
}
else if (gld != null)
{
gld.ChannelsInternal[channel.Id] = channel;
if (this.Configuration.AutoRefreshChannelCache)
{
await this.RefreshChannelsAsync(channel.Guild.Id);
}
}
await this._channelUpdated.InvokeAsync(this, new ChannelUpdateEventArgs(this.ServiceProvider) { ChannelAfter = channelNew, Guild = gld, ChannelBefore = channelOld }).ConfigureAwait(false);
}
///
/// Handles the channel delete event.
///
/// The channel.
internal async Task OnChannelDeleteEventAsync(DiscordChannel channel)
{
if (channel == null)
return;
channel.Discord = this;
//if (channel.IsPrivate)
if (channel.Type == ChannelType.Group || channel.Type == ChannelType.Private)
{
var dmChannel = channel as DiscordDmChannel;
await this._dmChannelDeleted.InvokeAsync(this, new DmChannelDeleteEventArgs(this.ServiceProvider) { Channel = dmChannel }).ConfigureAwait(false);
}
else
{
var gld = channel.Guild;
if (gld.ChannelsInternal.TryRemove(channel.Id, out var cachedChannel)) channel = cachedChannel;
if (this.Configuration.AutoRefreshChannelCache)
{
await this.RefreshChannelsAsync(channel.Guild.Id);
}
await this._channelDeleted.InvokeAsync(this, new ChannelDeleteEventArgs(this.ServiceProvider) { Channel = channel, Guild = gld }).ConfigureAwait(false);
}
}
///
/// Refreshes the channels.
///
/// The guild id.
internal async Task RefreshChannelsAsync(ulong guildId)
{
var guild = this.InternalGetCachedGuild(guildId);
var channels = await this.ApiClient.GetGuildChannelsAsync(guildId);
guild.ChannelsInternal.Clear();
foreach (var channel in channels.ToList())
{
channel.Initialize(this);
guild.ChannelsInternal[channel.Id] = channel;
}
}
///
/// Handles the channel pins update event.
///
/// The optional guild id.
/// The channel id.
/// The optional last pin timestamp.
internal async Task OnChannelPinsUpdateAsync(ulong? guildId, ulong channelId, DateTimeOffset? lastPinTimestamp)
{
var guild = this.InternalGetCachedGuild(guildId);
var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId);
var ea = new ChannelPinsUpdateEventArgs(this.ServiceProvider)
{
Guild = guild,
Channel = channel,
LastPinTimestamp = lastPinTimestamp
};
await this._channelPinsUpdated.InvokeAsync(this, ea).ConfigureAwait(false);
}
#endregion
#region Guild
///
/// Handles the guild create event.
///
/// The guild.
/// The raw members.
/// The presences.
internal async Task OnGuildCreateEventAsync(DiscordGuild guild, JArray rawMembers, IEnumerable presences)
{
if (presences != null)
{
foreach (var xp in presences)
{
xp.Discord = this;
xp.GuildId = guild.Id;
xp.Activity = new DiscordActivity(xp.RawActivity);
if (xp.RawActivities != null)
{
xp.InternalActivities = xp.RawActivities
.Select(x => new DiscordActivity(x)).ToArray();
}
this.PresencesInternal[xp.InternalUser.Id] = xp;
}
}
var exists = this.GuildsInternal.TryGetValue(guild.Id, out var foundGuild);
guild.Discord = this;
guild.IsUnavailable = false;
var eventGuild = guild;
if (exists)
guild = foundGuild;
guild.ChannelsInternal ??= new ConcurrentDictionary();
guild.ThreadsInternal ??= new ConcurrentDictionary();
guild.RolesInternal ??= new ConcurrentDictionary();
guild.ThreadsInternal ??= new ConcurrentDictionary();
guild.StickersInternal ??= new ConcurrentDictionary();
guild.EmojisInternal ??= new ConcurrentDictionary();
guild.VoiceStatesInternal ??= new ConcurrentDictionary();
guild.MembersInternal ??= new ConcurrentDictionary();
guild.ScheduledEventsInternal ??= new ConcurrentDictionary();
this.UpdateCachedGuild(eventGuild, rawMembers);
guild.JoinedAt = eventGuild.JoinedAt;
guild.IsLarge = eventGuild.IsLarge;
guild.MemberCount = Math.Max(eventGuild.MemberCount, guild.MembersInternal.Count);
guild.IsUnavailable = eventGuild.IsUnavailable;
guild.PremiumSubscriptionCount = eventGuild.PremiumSubscriptionCount;
guild.PremiumTier = eventGuild.PremiumTier;
guild.BannerHash = eventGuild.BannerHash;
guild.VanityUrlCode = eventGuild.VanityUrlCode;
guild.Description = eventGuild.Description;
guild.IsNsfw = eventGuild.IsNsfw;
foreach (var kvp in eventGuild.VoiceStatesInternal) guild.VoiceStatesInternal[kvp.Key] = kvp.Value;
foreach (var kvp in eventGuild.ChannelsInternal) guild.ChannelsInternal[kvp.Key] = kvp.Value;
foreach (var kvp in eventGuild.RolesInternal) guild.RolesInternal[kvp.Key] = kvp.Value;
foreach (var kvp in eventGuild.EmojisInternal) guild.EmojisInternal[kvp.Key] = kvp.Value;
foreach (var kvp in eventGuild.ThreadsInternal) guild.ThreadsInternal[kvp.Key] = kvp.Value;
foreach (var kvp in eventGuild.StickersInternal) guild.StickersInternal[kvp.Key] = kvp.Value;
foreach (var kvp in eventGuild.StageInstancesInternal) guild.StageInstancesInternal[kvp.Key] = kvp.Value;
foreach (var kvp in eventGuild.ScheduledEventsInternal) guild.ScheduledEventsInternal[kvp.Key] = kvp.Value;
foreach (var xc in guild.ChannelsInternal.Values)
{
xc.GuildId = guild.Id;
xc.Initialize(this);
}
foreach (var xt in guild.ThreadsInternal.Values)
{
xt.GuildId = guild.Id;
xt.Discord = this;
}
foreach (var xe in guild.EmojisInternal.Values)
xe.Discord = this;
foreach (var xs in guild.StickersInternal.Values)
xs.Discord = this;
foreach (var xvs in guild.VoiceStatesInternal.Values)
xvs.Discord = this;
foreach (var xsi in guild.StageInstancesInternal.Values)
{
xsi.Discord = this;
xsi.GuildId = guild.Id;
}
foreach (var xr in guild.RolesInternal.Values)
{
xr.Discord = this;
xr.GuildId = guild.Id;
}
foreach (var xse in guild.ScheduledEventsInternal.Values)
{
xse.Discord = this;
xse.GuildId = guild.Id;
if (xse.Creator != null)
xse.Creator.Discord = this;
}
var old = Volatile.Read(ref this._guildDownloadCompleted);
var dcompl = this.GuildsInternal.Values.All(xg => !xg.IsUnavailable);
Volatile.Write(ref this._guildDownloadCompleted, dcompl);
if (exists)
await this._guildAvailable.InvokeAsync(this, new GuildCreateEventArgs(this.ServiceProvider) { Guild = guild }).ConfigureAwait(false);
else
await this._guildCreated.InvokeAsync(this, new GuildCreateEventArgs(this.ServiceProvider) { Guild = guild }).ConfigureAwait(false);
if (dcompl && !old)
await this._guildDownloadCompletedEv.InvokeAsync(this, new GuildDownloadCompletedEventArgs(this.Guilds, this.ServiceProvider)).ConfigureAwait(false);
}
///
/// Handles the guild update event.
///
/// The guild.
/// The raw members.
internal async Task OnGuildUpdateEventAsync(DiscordGuild guild, JArray rawMembers)
{
DiscordGuild oldGuild;
if (!this.GuildsInternal.ContainsKey(guild.Id))
{
this.GuildsInternal[guild.Id] = guild;
oldGuild = null;
}
else
{
var gld = this.GuildsInternal[guild.Id];
oldGuild = new DiscordGuild
{
Discord = gld.Discord,
Name = gld.Name,
AfkChannelId = gld.AfkChannelId,
AfkTimeout = gld.AfkTimeout,
ApplicationId = gld.ApplicationId,
DefaultMessageNotifications = gld.DefaultMessageNotifications,
ExplicitContentFilter = gld.ExplicitContentFilter,
RawFeatures = gld.RawFeatures,
IconHash = gld.IconHash,
Id = gld.Id,
IsLarge = gld.IsLarge,
IsSynced = gld.IsSynced,
IsUnavailable = gld.IsUnavailable,
JoinedAt = gld.JoinedAt,
MemberCount = gld.MemberCount,
MaxMembers = gld.MaxMembers,
MaxPresences = gld.MaxPresences,
ApproximateMemberCount = gld.ApproximateMemberCount,
ApproximatePresenceCount = gld.ApproximatePresenceCount,
MaxVideoChannelUsers = gld.MaxVideoChannelUsers,
DiscoverySplashHash = gld.DiscoverySplashHash,
PreferredLocale = gld.PreferredLocale,
MfaLevel = gld.MfaLevel,
OwnerId = gld.OwnerId,
SplashHash = gld.SplashHash,
SystemChannelId = gld.SystemChannelId,
SystemChannelFlags = gld.SystemChannelFlags,
Description = gld.Description,
WidgetEnabled = gld.WidgetEnabled,
WidgetChannelId = gld.WidgetChannelId,
VerificationLevel = gld.VerificationLevel,
RulesChannelId = gld.RulesChannelId,
PublicUpdatesChannelId = gld.PublicUpdatesChannelId,
VoiceRegionId = gld.VoiceRegionId,
IsNsfw = gld.IsNsfw,
PremiumProgressBarEnabled = gld.PremiumProgressBarEnabled,
PremiumSubscriptionCount = gld.PremiumSubscriptionCount,
PremiumTier = gld.PremiumTier,
ChannelsInternal = new ConcurrentDictionary(),
ThreadsInternal = new ConcurrentDictionary(),
EmojisInternal = new ConcurrentDictionary(),
StickersInternal = new ConcurrentDictionary(),
MembersInternal = new ConcurrentDictionary(),
RolesInternal = new ConcurrentDictionary(),
StageInstancesInternal = new ConcurrentDictionary(),
VoiceStatesInternal = new ConcurrentDictionary(),
ScheduledEventsInternal = new ConcurrentDictionary()
};
foreach (var kvp in gld.ChannelsInternal) oldGuild.ChannelsInternal[kvp.Key] = kvp.Value;
foreach (var kvp in gld.ThreadsInternal) oldGuild.ThreadsInternal[kvp.Key] = kvp.Value;
foreach (var kvp in gld.EmojisInternal) oldGuild.EmojisInternal[kvp.Key] = kvp.Value;
foreach (var kvp in gld.StickersInternal) oldGuild.StickersInternal[kvp.Key] = kvp.Value;
foreach (var kvp in gld.RolesInternal) oldGuild.RolesInternal[kvp.Key] = kvp.Value;
foreach (var kvp in gld.VoiceStatesInternal) oldGuild.VoiceStatesInternal[kvp.Key] = kvp.Value;
foreach (var kvp in gld.MembersInternal) oldGuild.MembersInternal[kvp.Key] = kvp.Value;
foreach (var kvp in gld.StageInstancesInternal) oldGuild.StageInstancesInternal[kvp.Key] = kvp.Value;
foreach (var kvp in gld.ScheduledEventsInternal) oldGuild.ScheduledEventsInternal[kvp.Key] = kvp.Value;
}
guild.Discord = this;
guild.IsUnavailable = false;
var eventGuild = guild;
guild = this.GuildsInternal[eventGuild.Id];
guild.ChannelsInternal ??= new ConcurrentDictionary();
guild.ThreadsInternal ??= new ConcurrentDictionary();
guild.RolesInternal ??= new ConcurrentDictionary();
guild.EmojisInternal ??= new ConcurrentDictionary();
guild.StickersInternal ??= new ConcurrentDictionary();
guild.VoiceStatesInternal ??= new ConcurrentDictionary();
guild.StageInstancesInternal ??= new ConcurrentDictionary();
guild.MembersInternal ??= new ConcurrentDictionary();
guild.ScheduledEventsInternal ??= new ConcurrentDictionary();
this.UpdateCachedGuild(eventGuild, rawMembers);
foreach (var xc in guild.ChannelsInternal.Values)
{
xc.GuildId = guild.Id;
xc.Initialize(this);
}
foreach (var xc in guild.ThreadsInternal.Values)
{
xc.GuildId = guild.Id;
xc.Discord = this;
}
foreach (var xe in guild.EmojisInternal.Values)
xe.Discord = this;
foreach (var xs in guild.StickersInternal.Values)
xs.Discord = this;
foreach (var xvs in guild.VoiceStatesInternal.Values)
xvs.Discord = this;
foreach (var xr in guild.RolesInternal.Values)
{
xr.Discord = this;
xr.GuildId = guild.Id;
}
foreach (var xsi in guild.StageInstancesInternal.Values)
{
xsi.Discord = this;
xsi.GuildId = guild.Id;
}
foreach (var xse in guild.ScheduledEventsInternal.Values)
{
xse.Discord = this;
xse.GuildId = guild.Id;
if (xse.Creator != null)
xse.Creator.Discord = this;
}
await this._guildUpdated.InvokeAsync(this, new GuildUpdateEventArgs(this.ServiceProvider) { GuildBefore = oldGuild, GuildAfter = guild }).ConfigureAwait(false);
}
///
/// Handles the guild delete event.
///
/// The guild.
internal async Task OnGuildDeleteEventAsync(DiscordGuild guild)
{
if (guild.IsUnavailable)
{
if (!this.GuildsInternal.TryGetValue(guild.Id, out var gld))
return;
gld.IsUnavailable = true;
await this._guildUnavailable.InvokeAsync(this, new GuildDeleteEventArgs(this.ServiceProvider) { Guild = guild, Unavailable = true }).ConfigureAwait(false);
}
else
{
if (!this.GuildsInternal.TryRemove(guild.Id, out var gld))
return;
await this._guildDeleted.InvokeAsync(this, new GuildDeleteEventArgs(this.ServiceProvider) { Guild = gld }).ConfigureAwait(false);
}
}
///
/// Handles the guild audit log entry create event.
///
/// The guild where the audit log entry was created.
/// The auditlog event.
internal async Task OnGuildAuditLogEntryCreateEventAsync(DiscordGuild guild, JObject auditLogCreateEntry)
{
try
{
var auditLogAction = DiscordJson.ToDiscordObject(auditLogCreateEntry);
List workaroundAuditLogEntryList = new()
{
new AuditLog()
{
Entries = new List()
{
auditLogAction
}
}
};
var dataList = await guild.ProcessAuditLog(workaroundAuditLogEntryList);
var data = dataList.First();
await this._guildAuditLogEntryCreated.InvokeAsync(this, new(this.ServiceProvider) { Guild = guild, AuditLogEntry = data });
}
catch (Exception)
{ }
}
///
/// Handles the guild sync event.
///
/// The guild.
/// Whether the guild is a large guild..
/// The raw members.
/// The presences.
internal async Task OnGuildSyncEventAsync(DiscordGuild guild, bool isLarge, JArray rawMembers, IEnumerable presences)
{
presences = presences.Select(xp => { xp.Discord = this; xp.Activity = new DiscordActivity(xp.RawActivity); return xp; });
foreach (var xp in presences)
this.PresencesInternal[xp.InternalUser.Id] = xp;
guild.IsSynced = true;
guild.IsLarge = isLarge;
this.UpdateCachedGuild(guild, rawMembers);
await this._guildAvailable.InvokeAsync(this, new GuildCreateEventArgs(this.ServiceProvider) { Guild = guild }).ConfigureAwait(false);
}
///
/// Handles the guild emojis update event.
///
/// The guild.
/// The new emojis.
internal async Task OnGuildEmojisUpdateEventAsync(DiscordGuild guild, IEnumerable newEmojis)
{
var oldEmojis = new ConcurrentDictionary(guild.EmojisInternal);
guild.EmojisInternal.Clear();
foreach (var emoji in newEmojis)
{
emoji.Discord = this;
guild.EmojisInternal[emoji.Id] = emoji;
}
var ea = new GuildEmojisUpdateEventArgs(this.ServiceProvider)
{
Guild = guild,
EmojisAfter = guild.Emojis,
EmojisBefore = new ReadOnlyConcurrentDictionary(oldEmojis)
};
await this._guildEmojisUpdated.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the stickers updated.
///
/// The new stickers.
/// The guild id.
internal async Task OnStickersUpdatedAsync(IEnumerable newStickers, ulong guildId)
{
var guild = this.InternalGetCachedGuild(guildId);
var oldStickers = new ConcurrentDictionary(guild.StickersInternal);
guild.StickersInternal.Clear();
foreach (var nst in newStickers)
{
if (nst.User is not null)
{
nst.User.Discord = this;
this.UserCache.AddOrUpdate(nst.User.Id, nst.User, (old, @new) => @new);
}
nst.Discord = this;
guild.StickersInternal[nst.Id] = nst;
}
var sea = new GuildStickersUpdateEventArgs(this.ServiceProvider)
{
Guild = guild,
StickersBefore = oldStickers,
StickersAfter = guild.Stickers
};
await this._guildStickersUpdated.InvokeAsync(this, sea).ConfigureAwait(false);
}
///
/// Handles the created rule.
///
/// The new added rule.
internal async Task OnAutomodRuleCreated(AutomodRule newRule)
{
var sea = new AutomodRuleCreateEventArgs(this.ServiceProvider)
{
Rule = newRule
};
await this._automodRuleCreated.InvokeAsync(this, sea).ConfigureAwait(false);
}
///
/// Handles the updated rule.
///
/// The updated rule.
internal async Task OnAutomodRuleUpdated(AutomodRule updatedRule)
{
var sea = new AutomodRuleUpdateEventArgs(this.ServiceProvider)
{
Rule = updatedRule
};
await this._automodRuleUpdated.InvokeAsync(this, sea).ConfigureAwait(false);
}
///
/// Handles the deleted rule.
///
/// The deleted rule.
internal async Task OnAutomodRuleDeleted(AutomodRule deletedRule)
{
var sea = new AutomodRuleDeleteEventArgs(this.ServiceProvider)
{
Rule = deletedRule
};
await this._automodRuleDeleted.InvokeAsync(this, sea).ConfigureAwait(false);
}
///
/// Handles the rule action execution.
///
/// The guild.
/// The raw payload.
internal async Task OnAutomodActionExecuted(DiscordGuild guild, JObject rawPayload)
{
var executedAction = rawPayload["action"].ToObject();
var ruleId = (ulong)rawPayload["rule_id"];
var triggerType = rawPayload["rule_trigger_type"].ToObject();
var userId = (ulong)rawPayload["user_id"];
var channelId = rawPayload.ContainsKey("channel_id") ? (ulong?)rawPayload["channel_id"] : null;
var messageId = rawPayload.ContainsKey("message_id") ? (ulong?)rawPayload["message_id"] : null;
var alertMessageId = rawPayload.ContainsKey("alert_system_message_id") ? (ulong?)rawPayload["alert_system_message_id"] : null;
#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
string? content = rawPayload.ContainsKey("content") ?(string?)rawPayload["content"] : null;
#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
string? matchedKeyword = rawPayload.ContainsKey("matched_keyword") ? (string?)rawPayload["matched_keyword"] : null;
#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
string? matchedContent = rawPayload.ContainsKey("matched_content") ? (string?)rawPayload["matched_content"] : null;
#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
var ea = new AutomodActionExecutedEventArgs(this.ServiceProvider)
{
Guild = guild,
Action = executedAction,
RuleId = ruleId,
TriggerType = triggerType,
UserId = userId,
ChannelId = channelId,
MessageId = messageId,
AlertMessageId = alertMessageId,
MessageContent = content,
MatchedKeyword = matchedKeyword,
MatchedContent = matchedContent
};
await this._automodActionExecuted.InvokeAsync(this, ea).ConfigureAwait(false);
}
#endregion
#region Guild Ban
///
/// Handles the guild ban add event.
///
/// The transport user.
/// The guild.
internal async Task OnGuildBanAddEventAsync(TransportUser user, DiscordGuild guild)
{
var usr = new DiscordUser(user) { Discord = this };
usr = this.UserCache.AddOrUpdate(user.Id, usr, (id, old) =>
{
old.Username = usr.Username;
old.Discriminator = usr.Discriminator;
old.AvatarHash = usr.AvatarHash;
old.GlobalName = usr.GlobalName;
return old;
});
if (!guild.Members.TryGetValue(user.Id, out var mbr))
mbr = new DiscordMember(usr) { Discord = this, GuildId = guild.Id };
var ea = new GuildBanAddEventArgs(this.ServiceProvider)
{
Guild = guild,
Member = mbr
};
await this._guildBanAdded.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the guild ban remove event.
///
/// The transport user.
/// The guild.
internal async Task OnGuildBanRemoveEventAsync(TransportUser user, DiscordGuild guild)
{
var usr = new DiscordUser(user) { Discord = this };
usr = this.UserCache.AddOrUpdate(user.Id, usr, (id, old) =>
{
old.Username = usr.Username;
old.Discriminator = usr.Discriminator;
old.AvatarHash = usr.AvatarHash;
old.GlobalName = usr.GlobalName;
return old;
});
if (!guild.Members.TryGetValue(user.Id, out var mbr))
mbr = new DiscordMember(usr) { Discord = this, GuildId = guild.Id };
var ea = new GuildBanRemoveEventArgs(this.ServiceProvider)
{
Guild = guild,
Member = mbr
};
await this._guildBanRemoved.InvokeAsync(this, ea).ConfigureAwait(false);
}
#endregion
#region Guild Scheduled Event
///
/// Handles the scheduled event create event.
///
/// The created event.
/// The guild.
internal async Task OnGuildScheduledEventCreateEventAsync(DiscordScheduledEvent scheduledEvent, DiscordGuild guild)
{
scheduledEvent.Discord = this;
guild.ScheduledEventsInternal.AddOrUpdate(scheduledEvent.Id, scheduledEvent, (old, newScheduledEvent) => newScheduledEvent);
if (scheduledEvent.Creator != null)
{
scheduledEvent.Creator.Discord = this;
this.UserCache.AddOrUpdate(scheduledEvent.Creator.Id, scheduledEvent.Creator, (id, old) =>
{
old.Username = scheduledEvent.Creator.Username;
old.Discriminator = scheduledEvent.Creator.Discriminator;
old.AvatarHash = scheduledEvent.Creator.AvatarHash;
old.Flags = scheduledEvent.Creator.Flags;
old.GlobalName = scheduledEvent.Creator.GlobalName;
return old;
});
}
await this._guildScheduledEventCreated.InvokeAsync(this, new GuildScheduledEventCreateEventArgs(this.ServiceProvider) { ScheduledEvent = scheduledEvent, Guild = scheduledEvent.Guild }).ConfigureAwait(false);
}
///
/// Handles the scheduled event update event.
///
/// The updated event.
/// The guild.
internal async Task OnGuildScheduledEventUpdateEventAsync(DiscordScheduledEvent scheduledEvent, DiscordGuild guild)
{
if (guild == null)
return;
DiscordScheduledEvent oldEvent;
if (!guild.ScheduledEventsInternal.ContainsKey(scheduledEvent.Id))
{
oldEvent = null;
}
else
{
var ev = guild.ScheduledEventsInternal[scheduledEvent.Id];
oldEvent = new DiscordScheduledEvent
{
Id = ev.Id,
ChannelId = ev.ChannelId,
EntityId = ev.EntityId,
EntityMetadata = ev.EntityMetadata,
CreatorId = ev.CreatorId,
Creator = ev.Creator,
Discord = this,
Description = ev.Description,
EntityType = ev.EntityType,
ScheduledStartTimeRaw = ev.ScheduledStartTimeRaw,
ScheduledEndTimeRaw = ev.ScheduledEndTimeRaw,
GuildId = ev.GuildId,
Status = ev.Status,
Name = ev.Name,
UserCount = ev.UserCount,
CoverImageHash = ev.CoverImageHash
};
}
if (scheduledEvent.Creator != null)
{
scheduledEvent.Creator.Discord = this;
this.UserCache.AddOrUpdate(scheduledEvent.Creator.Id, scheduledEvent.Creator, (id, old) =>
{
old.Username = scheduledEvent.Creator.Username;
old.Discriminator = scheduledEvent.Creator.Discriminator;
old.AvatarHash = scheduledEvent.Creator.AvatarHash;
old.Flags = scheduledEvent.Creator.Flags;
old.GlobalName = scheduledEvent.Creator.GlobalName;
return old;
});
}
if (scheduledEvent.Status == ScheduledEventStatus.Completed)
{
guild.ScheduledEventsInternal.TryRemove(scheduledEvent.Id, out var deletedEvent);
await this._guildScheduledEventDeleted.InvokeAsync(this, new GuildScheduledEventDeleteEventArgs(this.ServiceProvider) { ScheduledEvent = scheduledEvent, Guild = guild, Reason = ScheduledEventStatus.Completed }).ConfigureAwait(false);
}
else if (scheduledEvent.Status == ScheduledEventStatus.Canceled)
{
guild.ScheduledEventsInternal.TryRemove(scheduledEvent.Id, out var deletedEvent);
scheduledEvent.Status = ScheduledEventStatus.Canceled;
await this._guildScheduledEventDeleted.InvokeAsync(this, new GuildScheduledEventDeleteEventArgs(this.ServiceProvider) { ScheduledEvent = scheduledEvent, Guild = guild, Reason = ScheduledEventStatus.Canceled }).ConfigureAwait(false);
}
else
{
this.UpdateScheduledEvent(scheduledEvent, guild);
await this._guildScheduledEventUpdated.InvokeAsync(this, new GuildScheduledEventUpdateEventArgs(this.ServiceProvider) { ScheduledEventBefore = oldEvent, ScheduledEventAfter = scheduledEvent, Guild = guild }).ConfigureAwait(false);
}
}
///
/// Handles the scheduled event delete event.
///
/// The deleted event.
/// The guild.
internal async Task OnGuildScheduledEventDeleteEventAsync(DiscordScheduledEvent scheduledEvent, DiscordGuild guild)
{
scheduledEvent.Discord = this;
if (scheduledEvent.Status == ScheduledEventStatus.Scheduled)
scheduledEvent.Status = ScheduledEventStatus.Canceled;
if (scheduledEvent.Creator != null)
{
scheduledEvent.Creator.Discord = this;
this.UserCache.AddOrUpdate(scheduledEvent.Creator.Id, scheduledEvent.Creator, (id, old) =>
{
old.Username = scheduledEvent.Creator.Username;
old.Discriminator = scheduledEvent.Creator.Discriminator;
old.AvatarHash = scheduledEvent.Creator.AvatarHash;
old.Flags = scheduledEvent.Creator.Flags;
old.GlobalName = scheduledEvent.Creator.GlobalName;
return old;
});
}
await this._guildScheduledEventDeleted.InvokeAsync(this, new GuildScheduledEventDeleteEventArgs(this.ServiceProvider) { ScheduledEvent = scheduledEvent, Guild = scheduledEvent.Guild, Reason = scheduledEvent.Status }).ConfigureAwait(false);
guild.ScheduledEventsInternal.TryRemove(scheduledEvent.Id, out var deletedEvent);
}
///
/// Handles the scheduled event user add event.
/// The event.
/// The added user id.
/// The guild.
///
internal async Task OnGuildScheduledEventUserAddedEventAsync(ulong guildScheduledEventId, ulong userId, DiscordGuild guild)
{
var scheduledEvent = this.InternalGetCachedScheduledEvent(guildScheduledEventId) ?? this.UpdateScheduledEvent(new DiscordScheduledEvent
{
Id = guildScheduledEventId,
GuildId = guild.Id,
Discord = this,
UserCount = 0
}, guild);
scheduledEvent.UserCount++;
scheduledEvent.Discord = this;
guild.Discord = this;
var user = this.GetUserAsync(userId, true).Result;
user.Discord = this;
var member = guild.Members.TryGetValue(userId, out var mem) ? mem : guild.GetMemberAsync(userId).Result;
member.Discord = this;
await this._guildScheduledEventUserAdded.InvokeAsync(this, new GuildScheduledEventUserAddEventArgs(this.ServiceProvider) { ScheduledEvent = scheduledEvent, Guild = guild, User = user, Member = member }).ConfigureAwait(false);
}
///
/// Handles the scheduled event user remove event.
/// The event.
/// The removed user id.
/// The guild.
///
internal async Task OnGuildScheduledEventUserRemovedEventAsync(ulong guildScheduledEventId, ulong userId, DiscordGuild guild)
{
var scheduledEvent = this.InternalGetCachedScheduledEvent(guildScheduledEventId) ?? this.UpdateScheduledEvent(new DiscordScheduledEvent
{
Id = guildScheduledEventId,
GuildId = guild.Id,
Discord = this,
UserCount = 0
}, guild);
scheduledEvent.UserCount = scheduledEvent.UserCount == 0 ? 0 : scheduledEvent.UserCount - 1;
scheduledEvent.Discord = this;
guild.Discord = this;
var user = this.GetUserAsync(userId, true).Result;
user.Discord = this;
var member = guild.Members.TryGetValue(userId, out var mem) ? mem : guild.GetMemberAsync(userId).Result;
member.Discord = this;
await this._guildScheduledEventUserRemoved.InvokeAsync(this, new GuildScheduledEventUserRemoveEventArgs(this.ServiceProvider) { ScheduledEvent = scheduledEvent, Guild = guild, User = user, Member = member }).ConfigureAwait(false);
}
#endregion
#region Guild Integration
///
/// Handles the guild integration create event.
///
/// The guild.
/// The integration.
internal async Task OnGuildIntegrationCreateEventAsync(DiscordGuild guild, DiscordIntegration integration)
{
integration.Discord = this;
await this._guildIntegrationCreated.InvokeAsync(this, new GuildIntegrationCreateEventArgs(this.ServiceProvider) { Integration = integration, Guild = guild }).ConfigureAwait(false);
}
///
/// Handles the guild integration update event.
///
/// The guild.
/// The integration.
internal async Task OnGuildIntegrationUpdateEventAsync(DiscordGuild guild, DiscordIntegration integration)
{
integration.Discord = this;
await this._guildIntegrationUpdated.InvokeAsync(this, new GuildIntegrationUpdateEventArgs(this.ServiceProvider) { Integration = integration, Guild = guild }).ConfigureAwait(false);
}
///
/// Handles the guild integrations update event.
///
/// The guild.
internal async Task OnGuildIntegrationsUpdateEventAsync(DiscordGuild guild)
{
var ea = new GuildIntegrationsUpdateEventArgs(this.ServiceProvider)
{
Guild = guild
};
await this._guildIntegrationsUpdated.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the guild integration delete event.
///
/// The guild.
/// The integration id.
/// The optional application id.
internal async Task OnGuildIntegrationDeleteEventAsync(DiscordGuild guild, ulong integrationId, ulong? applicationId)
=> await this._guildIntegrationDeleted.InvokeAsync(this, new GuildIntegrationDeleteEventArgs(this.ServiceProvider) { Guild = guild, IntegrationId = integrationId, ApplicationId = applicationId }).ConfigureAwait(false);
#endregion
#region Guild Member
///
/// Handles the guild member add event.
///
/// The transport member.
/// The guild.
internal async Task OnGuildMemberAddEventAsync(TransportMember member, DiscordGuild guild)
{
var usr = new DiscordUser(member.User) { Discord = this };
usr = this.UserCache.AddOrUpdate(member.User.Id, usr, (id, old) =>
{
old.Username = usr.Username;
old.Discriminator = usr.Discriminator;
old.AvatarHash = usr.AvatarHash;
old.GlobalName = usr.GlobalName;
return old;
});
var mbr = new DiscordMember(member)
{
Discord = this,
GuildId = guild.Id
};
guild.MembersInternal[mbr.Id] = mbr;
guild.MemberCount++;
var ea = new GuildMemberAddEventArgs(this.ServiceProvider)
{
Guild = guild,
Member = mbr
};
await this._guildMemberAdded.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the guild member remove event.
///
/// The transport user.
/// The guild.
internal async Task OnGuildMemberRemoveEventAsync(TransportUser user, DiscordGuild guild)
{
var usr = new DiscordUser(user);
if (!guild.MembersInternal.TryRemove(user.Id, out var mbr))
mbr = new DiscordMember(usr) { Discord = this, GuildId = guild.Id };
guild.MemberCount--;
_ = this.UserCache.AddOrUpdate(user.Id, usr, (old, @new) => @new);
var ea = new GuildMemberRemoveEventArgs(this.ServiceProvider)
{
Guild = guild,
Member = mbr
};
await this._guildMemberRemoved.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the guild member update event.
///
/// The transport member.
/// The guild.
/// The roles.
/// The nick.
/// Whether the member is pending.
internal async Task OnGuildMemberUpdateEventAsync(TransportMember member, DiscordGuild guild, IEnumerable roles, string nick, bool? pending)
{
var usr = new DiscordUser(member.User) { Discord = this };
usr = this.UserCache.AddOrUpdate(usr.Id, usr, (id, old) =>
{
old.Username = usr.Username;
old.Discriminator = usr.Discriminator;
old.AvatarHash = usr.AvatarHash;
old.GlobalName = usr.GlobalName;
return old;
});
if (!guild.Members.TryGetValue(member.User.Id, out var mbr))
mbr = new DiscordMember(usr) { Discord = this, GuildId = guild.Id };
var old = mbr;
var gAvOld = old.GuildAvatarHash;
var avOld = old.AvatarHash;
var nickOld = mbr.Nickname;
var pendingOld = mbr.IsPending;
var rolesOld = new ReadOnlyCollection(new List(mbr.Roles));
var cduOld = mbr.CommunicationDisabledUntil;
mbr.MemberFlags = member.MemberFlags;
mbr.AvatarHashInternal = member.AvatarHash;
mbr.GuildAvatarHash = member.GuildAvatarHash;
mbr.Nickname = nick;
mbr.GuildPronouns = member.GuildPronouns;
mbr.IsPending = pending;
mbr.CommunicationDisabledUntil = member.CommunicationDisabledUntil;
mbr.RoleIdsInternal.Clear();
mbr.RoleIdsInternal.AddRange(roles);
guild.MembersInternal.AddOrUpdate(member.User.Id, mbr, (id, oldMbr) => oldMbr);
var timeoutUntil = member.CommunicationDisabledUntil;
/*this.Logger.LogTrace($"Timeout:\nBefore - {cduOld}\nAfter - {timeoutUntil}");
if ((timeoutUntil.HasValue && cduOld.HasValue) || (timeoutUntil == null && cduOld.HasValue) || (timeoutUntil.HasValue && cduOld == null))
{
// We are going to add a scheduled timer to assure that we get a auditlog entry.
var id = $"tt-{mbr.Id}-{guild.Id}-{DateTime.Now.ToLongTimeString()}";
this._tempTimers.Add(
id,
new(
new TimeoutHandler(
mbr,
guild,
cduOld,
timeoutUntil
),
new Timer(
this.TimeoutTimer,
id,
2000,
Timeout.Infinite
)
)
);
this.Logger.LogTrace("Scheduling timeout event.");
return;
}*/
//this.Logger.LogTrace("No timeout detected. Continuing on normal operation.");
var eargs = new GuildMemberUpdateEventArgs(this.ServiceProvider)
{
Guild = guild,
Member = mbr,
NicknameAfter = mbr.Nickname,
RolesAfter = new ReadOnlyCollection(new List(mbr.Roles)),
PendingAfter = mbr.IsPending,
TimeoutAfter = mbr.CommunicationDisabledUntil,
AvatarHashAfter = mbr.AvatarHash,
GuildAvatarHashAfter = mbr.GuildAvatarHash,
NicknameBefore = nickOld,
RolesBefore = rolesOld,
PendingBefore = pendingOld,
TimeoutBefore = cduOld,
AvatarHashBefore = avOld,
GuildAvatarHashBefore = gAvOld
};
await this._guildMemberUpdated.InvokeAsync(this, eargs).ConfigureAwait(false);
}
///
/// Handles timeout events.
///
/// Internally used as uid for the timer data.
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "")]
private async void TimeoutTimer(object state)
{
var tid = (string)state;
var data = this._tempTimers.First(x=> x.Key == tid).Value.Key;
var timer = this._tempTimers.First(x=> x.Key == tid).Value.Value;
IReadOnlyList auditlog = null;
DiscordAuditLogMemberUpdateEntry filtered = null;
try
{
auditlog = await data.Guild.GetAuditLogsAsync(10, null, AuditLogActionType.MemberUpdate);
var preFiltered = auditlog.Select(x => x as DiscordAuditLogMemberUpdateEntry).Where(x => x.Target.Id == data.Member.Id);
filtered = preFiltered.First();
}
catch (UnauthorizedException) { }
catch (Exception)
{
this.Logger.LogTrace("Failing timeout event.");
await timer.DisposeAsync();
this._tempTimers.Remove(tid);
return;
}
var actor = filtered?.UserResponsible as DiscordMember;
this.Logger.LogTrace("Trying to execute timeout event.");
if (data.TimeoutUntilOld.HasValue && data.TimeoutUntilNew.HasValue)
{
// A timeout was updated.
if (filtered != null && auditlog == null)
{
this.Logger.LogTrace("Re-scheduling timeout event.");
timer.Change(2000, Timeout.Infinite);
return;
}
var ea = new GuildMemberTimeoutUpdateEventArgs(this.ServiceProvider)
{
Guild = data.Guild,
Target = data.Member,
TimeoutBefore = data.TimeoutUntilOld.Value,
TimeoutAfter = data.TimeoutUntilNew.Value,
Actor = actor,
AuditLogId = filtered?.Id,
AuditLogReason = filtered?.Reason
};
await this._guildMemberTimeoutChanged.InvokeAsync(this, ea).ConfigureAwait(false);
}
else if (!data.TimeoutUntilOld.HasValue && data.TimeoutUntilNew.HasValue)
{
// A timeout was added.
if (filtered != null && auditlog == null)
{
this.Logger.LogTrace("Re-scheduling timeout event.");
timer.Change(2000, Timeout.Infinite);
return;
}
var ea = new GuildMemberTimeoutAddEventArgs(this.ServiceProvider)
{
Guild = data.Guild,
Target = data.Member,
Timeout = data.TimeoutUntilNew.Value,
Actor = actor,
AuditLogId = filtered?.Id,
AuditLogReason = filtered?.Reason
};
await this._guildMemberTimeoutAdded.InvokeAsync(this, ea).ConfigureAwait(false);
}
else if (data.TimeoutUntilOld.HasValue && !data.TimeoutUntilNew.HasValue)
{
// A timeout was removed.
if (filtered != null && auditlog == null)
{
this.Logger.LogTrace("Re-scheduling timeout event.");
timer.Change(2000, Timeout.Infinite);
return;
}
var ea = new GuildMemberTimeoutRemoveEventArgs(this.ServiceProvider)
{
Guild = data.Guild,
Target = data.Member,
TimeoutBefore = data.TimeoutUntilOld.Value,
Actor = actor,
AuditLogId = filtered?.Id,
AuditLogReason = filtered?.Reason
};
await this._guildMemberTimeoutRemoved.InvokeAsync(this, ea).ConfigureAwait(false);
}
// Ending timer because it worked.
this.Logger.LogTrace("Removing timeout event.");
await timer.DisposeAsync();
this._tempTimers.Remove(tid);
}
///
/// Handles the guild members chunk event.
///
/// The raw chunk data.
internal async Task OnGuildMembersChunkEventAsync(JObject dat)
{
var guild = this.Guilds[(ulong)dat["guild_id"]];
var chunkIndex = (int)dat["chunk_index"];
var chunkCount = (int)dat["chunk_count"];
var nonce = (string)dat["nonce"];
var mbrs = new HashSet();
var pres = new HashSet();
var members = dat["members"].ToObject();
foreach (var member in members)
{
var mbr = new DiscordMember(member) { Discord = this, GuildId = guild.Id };
if (!this.UserCache.ContainsKey(mbr.Id))
this.UserCache[mbr.Id] = new DiscordUser(member.User) { Discord = this };
guild.MembersInternal[mbr.Id] = mbr;
mbrs.Add(mbr);
}
guild.MemberCount = guild.MembersInternal.Count;
var ea = new GuildMembersChunkEventArgs(this.ServiceProvider)
{
Guild = guild,
Members = new ReadOnlySet(mbrs),
ChunkIndex = chunkIndex,
ChunkCount = chunkCount,
Nonce = nonce,
};
if (dat["presences"] != null)
{
var presences = dat["presences"].ToObject();
var presCount = presences.Length;
foreach (var presence in presences)
{
presence.Discord = this;
presence.Activity = new DiscordActivity(presence.RawActivity);
if (presence.RawActivities != null)
{
presence.InternalActivities = presence.RawActivities
.Select(x => new DiscordActivity(x)).ToArray();
}
pres.Add(presence);
}
ea.Presences = new ReadOnlySet(pres);
}
if (dat["not_found"] != null)
{
var nf = dat["not_found"].ToObject>();
ea.NotFound = new ReadOnlySet(nf);
}
await this._guildMembersChunked.InvokeAsync(this, ea).ConfigureAwait(false);
}
#endregion
#region Guild Role
///
/// Handles the guild role create event.
///
/// The role.
/// The guild.
internal async Task OnGuildRoleCreateEventAsync(DiscordRole role, DiscordGuild guild)
{
role.Discord = this;
role.GuildId = guild.Id;
guild.RolesInternal[role.Id] = role;
var ea = new GuildRoleCreateEventArgs(this.ServiceProvider)
{
Guild = guild,
Role = role
};
await this._guildRoleCreated.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the guild role update event.
///
/// The role.
/// The guild.
internal async Task OnGuildRoleUpdateEventAsync(DiscordRole role, DiscordGuild guild)
{
var newRole = guild.GetRole(role.Id);
var oldRole = new DiscordRole
{
GuildId = guild.Id,
ColorInternal = newRole.ColorInternal,
Discord = this,
IsHoisted = newRole.IsHoisted,
Id = newRole.Id,
IsManaged = newRole.IsManaged,
IsMentionable = newRole.IsMentionable,
Name = newRole.Name,
Permissions = newRole.Permissions,
Position = newRole.Position,
IconHash = newRole.IconHash,
Tags = newRole.Tags ?? null,
UnicodeEmojiString = newRole.UnicodeEmojiString
};
newRole.GuildId = guild.Id;
newRole.ColorInternal = role.ColorInternal;
newRole.IsHoisted = role.IsHoisted;
newRole.IsManaged = role.IsManaged;
newRole.IsMentionable = role.IsMentionable;
newRole.Name = role.Name;
newRole.Permissions = role.Permissions;
newRole.Position = role.Position;
newRole.IconHash = role.IconHash;
newRole.Tags = role.Tags ?? null;
newRole.UnicodeEmojiString = role.UnicodeEmojiString;
var ea = new GuildRoleUpdateEventArgs(this.ServiceProvider)
{
Guild = guild,
RoleAfter = newRole,
RoleBefore = oldRole
};
await this._guildRoleUpdated.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the guild role delete event.
///
/// The role id.
/// The guild.
internal async Task OnGuildRoleDeleteEventAsync(ulong roleId, DiscordGuild guild)
{
if (!guild.RolesInternal.TryRemove(roleId, out var role))
this.Logger.LogWarning($"Attempted to delete a nonexistent role ({roleId}) from guild ({guild}).");
var ea = new GuildRoleDeleteEventArgs(this.ServiceProvider)
{
Guild = guild,
Role = role
};
await this._guildRoleDeleted.InvokeAsync(this, ea).ConfigureAwait(false);
}
#endregion
#region Invite
///
/// Handles the invite create event.
///
/// The channel id.
/// The guild id.
/// The invite.
internal async Task OnInviteCreateEventAsync(ulong channelId, ulong guildId, DiscordInvite invite)
{
var guild = this.InternalGetCachedGuild(guildId);
var channel = this.InternalGetCachedChannel(channelId);
invite.Discord = this;
if (invite.Inviter is not null)
{
invite.Inviter.Discord = this;
this.UserCache.AddOrUpdate(invite.Inviter.Id, invite.Inviter, (old, @new) => @new);
}
guild.Invites[invite.Code] = invite;
var ea = new InviteCreateEventArgs(this.ServiceProvider)
{
Channel = channel,
Guild = guild,
Invite = invite
};
await this._inviteCreated.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the invite delete event.
///
/// The channel id.
/// The guild id.
/// The raw invite.
internal async Task OnInviteDeleteEventAsync(ulong channelId, ulong guildId, JToken dat)
{
var guild = this.InternalGetCachedGuild(guildId);
var channel = this.InternalGetCachedChannel(channelId);
if (!guild.Invites.TryRemove(dat["code"].ToString(), out var invite))
{
invite = dat.ToObject();
invite.Discord = this;
}
invite.IsRevoked = true;
var ea = new InviteDeleteEventArgs(this.ServiceProvider)
{
Channel = channel,
Guild = guild,
Invite = invite
};
await this._inviteDeleted.InvokeAsync(this, ea).ConfigureAwait(false);
}
#endregion
#region Message
///
/// Handles the message acknowledge event.
///
/// The channel.
/// The message id.
internal async Task OnMessageAckEventAsync(DiscordChannel chn, ulong messageId)
{
if (this.MessageCache == null || !this.MessageCache.TryGet(xm => xm.Id == messageId && xm.ChannelId == chn.Id, out var msg))
{
msg = new DiscordMessage
{
Id = messageId,
ChannelId = chn.Id,
Discord = this,
};
}
await this._messageAcknowledged.InvokeAsync(this, new MessageAcknowledgeEventArgs(this.ServiceProvider) { Message = msg }).ConfigureAwait(false);
}
///
/// Handles the message create event.
///
/// The message.
/// The transport user (author).
/// The transport member.
/// The reference transport user (author).
/// The reference transport member.
internal async Task OnMessageCreateEventAsync(DiscordMessage message, TransportUser author, TransportMember member, TransportUser referenceAuthor, TransportMember referenceMember)
{
message.Discord = this;
this.PopulateMessageReactionsAndCache(message, author, member);
message.PopulateMentions();
if (message.Channel == null && message.ChannelId == default)
this.Logger.LogWarning(LoggerEvents.WebSocketReceive, "Channel which the last message belongs to is not in cache - cache state might be invalid!");
if (message.ReferencedMessage != null)
{
message.ReferencedMessage.Discord = this;
this.PopulateMessageReactionsAndCache(message.ReferencedMessage, referenceAuthor, referenceMember);
message.ReferencedMessage.PopulateMentions();
}
foreach (var sticker in message.Stickers)
sticker.Discord = this;
var ea = new MessageCreateEventArgs(this.ServiceProvider)
{
Message = message,
MentionedUsers = new ReadOnlyCollection(message.MentionedUsersInternal),
MentionedRoles = message.MentionedRolesInternal != null ? new ReadOnlyCollection(message.MentionedRolesInternal) : null,
MentionedChannels = message.MentionedChannelsInternal != null ? new ReadOnlyCollection(message.MentionedChannelsInternal) : null
};
await this._messageCreated.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the message update event.
///
/// The message.
/// The transport user (author).
/// The transport member.
/// The reference transport user (author).
/// The reference transport member.
internal async Task OnMessageUpdateEventAsync(DiscordMessage message, TransportUser author, TransportMember member, TransportUser referenceAuthor, TransportMember referenceMember)
{
DiscordGuild guild;
message.Discord = this;
var eventMessage = message;
DiscordMessage oldmsg = null;
if (this.Configuration.MessageCacheSize == 0
|| this.MessageCache == null
|| !this.MessageCache.TryGet(xm => xm.Id == eventMessage.Id && xm.ChannelId == eventMessage.ChannelId, out message))
{
message = eventMessage;
this.PopulateMessageReactionsAndCache(message, author, member);
guild = message.Channel?.Guild;
if (message.ReferencedMessage != null)
{
message.ReferencedMessage.Discord = this;
this.PopulateMessageReactionsAndCache(message.ReferencedMessage, referenceAuthor, referenceMember);
message.ReferencedMessage.PopulateMentions();
}
}
else
{
oldmsg = new DiscordMessage(message);
guild = message.Channel?.Guild;
message.EditedTimestampRaw = eventMessage.EditedTimestampRaw;
if (eventMessage.Content != null)
message.Content = eventMessage.Content;
message.EmbedsInternal.Clear();
message.EmbedsInternal.AddRange(eventMessage.EmbedsInternal);
message.Pinned = eventMessage.Pinned;
message.IsTts = eventMessage.IsTts;
}
message.PopulateMentions();
var ea = new MessageUpdateEventArgs(this.ServiceProvider)
{
Message = message,
MessageBefore = oldmsg,
MentionedUsers = new ReadOnlyCollection(message.MentionedUsersInternal),
MentionedRoles = message.MentionedRolesInternal != null ? new ReadOnlyCollection(message.MentionedRolesInternal) : null,
MentionedChannels = message.MentionedChannelsInternal != null ? new ReadOnlyCollection(message.MentionedChannelsInternal) : null
};
await this._messageUpdated.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the message delete event.
///
/// The message id.
/// The channel id.
/// The optional guild id.
internal async Task OnMessageDeleteEventAsync(ulong messageId, ulong channelId, ulong? guildId)
{
var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId);
var guild = this.InternalGetCachedGuild(guildId);
if (channel == null
|| this.Configuration.MessageCacheSize == 0
|| this.MessageCache == null
|| !this.MessageCache.TryGet(xm => xm.Id == messageId && xm.ChannelId == channelId, out var msg))
{
msg = new DiscordMessage
{
Id = messageId,
ChannelId = channelId,
Discord = this,
};
}
if (this.Configuration.MessageCacheSize > 0)
this.MessageCache?.Remove(xm => xm.Id == msg.Id && xm.ChannelId == channelId);
var ea = new MessageDeleteEventArgs(this.ServiceProvider)
{
Channel = channel,
Message = msg,
Guild = guild
};
await this._messageDeleted.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the message bulk delete event.
///
/// The message ids.
/// The channel id.
/// The optional guild id.
internal async Task OnMessageBulkDeleteEventAsync(ulong[] messageIds, ulong channelId, ulong? guildId)
{
var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId);
var msgs = new List(messageIds.Length);
foreach (var messageId in messageIds)
{
if (channel == null
|| this.Configuration.MessageCacheSize == 0
|| this.MessageCache == null
|| !this.MessageCache.TryGet(xm => xm.Id == messageId && xm.ChannelId == channelId, out var msg))
{
msg = new DiscordMessage
{
Id = messageId,
ChannelId = channelId,
Discord = this,
};
}
if (this.Configuration.MessageCacheSize > 0)
this.MessageCache?.Remove(xm => xm.Id == msg.Id && xm.ChannelId == channelId);
msgs.Add(msg);
}
var guild = this.InternalGetCachedGuild(guildId);
var ea = new MessageBulkDeleteEventArgs(this.ServiceProvider)
{
Channel = channel,
Messages = new ReadOnlyCollection(msgs),
Guild = guild
};
await this._messagesBulkDeleted.InvokeAsync(this, ea).ConfigureAwait(false);
}
#endregion
#region Message Reaction
///
/// Handles the message reaction add event.
///
/// The user id.
/// The message id.
/// The channel id.
/// The optional guild id.
/// The transport member.
/// The emoji.
/// Whether a burst reaction was added.
internal async Task OnMessageReactionAddAsync(ulong userId, ulong messageId, ulong channelId, ulong? guildId, TransportMember mbr, DiscordEmoji emoji, bool isBurst)
{
var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId);
var guild = this.InternalGetCachedGuild(guildId);
emoji.Discord = this;
var usr = this.UpdateUser(new DiscordUser { Id = userId, Discord = this }, guildId, guild, mbr);
if (channel == null
|| this.Configuration.MessageCacheSize == 0
|| this.MessageCache == null
|| !this.MessageCache.TryGet(xm => xm.Id == messageId && xm.ChannelId == channelId, out var msg))
{
msg = new DiscordMessage
{
Id = messageId,
ChannelId = channelId,
Discord = this,
ReactionsInternal = new List()
};
}
var react = msg.ReactionsInternal.FirstOrDefault(xr => xr.Emoji == emoji);
if (react == null)
{
msg.ReactionsInternal.Add(react = new DiscordReaction
{
Count = 1,
Emoji = emoji,
IsMe = this.CurrentUser.Id == userId
});
}
else
{
react.Count++;
react.IsMe |= this.CurrentUser.Id == userId;
}
var ea = new MessageReactionAddEventArgs(this.ServiceProvider)
{
Message = msg,
User = usr,
Guild = guild,
Emoji = emoji,
IsBurst = isBurst,
Channel = channel,
ChannelId = channelId
};
await this._messageReactionAdded.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the message reaction remove event.
///
/// The user id.
/// The message id.
/// The channel id.
/// The guild id.
/// The emoji.
/// Whether a burst reaction was added.
internal async Task OnMessageReactionRemoveAsync(ulong userId, ulong messageId, ulong channelId, ulong? guildId, DiscordEmoji emoji, bool isBurst)
{
var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId) ?? new DiscordChannel()
{
Type = ChannelType.Unknown,
Id = channelId,
GuildId = guildId,
Discord = this
};
emoji.Discord = this;
if (!this.UserCache.TryGetValue(userId, out var usr))
usr = new DiscordUser { Id = userId, Discord = this };
if (channel?.Guild != null)
usr = channel.Guild.Members.TryGetValue(userId, out var member)
? member
: new DiscordMember(usr) { Discord = this, GuildId = channel.GuildId.Value };
if (channel == null
|| this.Configuration.MessageCacheSize == 0
|| this.MessageCache == null
|| !this.MessageCache.TryGet(xm => xm.Id == messageId && xm.ChannelId == channelId, out var msg))
{
msg = new DiscordMessage
{
Id = messageId,
ChannelId = channelId,
Discord = this
};
}
var react = msg.ReactionsInternal?.FirstOrDefault(xr => xr.Emoji == emoji);
if (react != null)
{
react.Count--;
react.IsMe &= this.CurrentUser.Id != userId;
if (msg.ReactionsInternal != null && react.Count <= 0) // shit happens
msg.ReactionsInternal.RemoveFirst(x => x.Emoji == emoji);
}
var guild = this.InternalGetCachedGuild(guildId);
var ea = new MessageReactionRemoveEventArgs(this.ServiceProvider)
{
Message = msg,
User = usr,
Guild = guild,
Emoji = emoji,
IsBurst = isBurst,
Channel = channel,
ChannelId = channelId
};
await this._messageReactionRemoved.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the message reaction remove event.
/// Fired when all message reactions were removed.
///
/// The message id.
/// The channel id.
/// The optional guild id.
internal async Task OnMessageReactionRemoveAllAsync(ulong messageId, ulong channelId, ulong? guildId)
{
var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId) ?? new DiscordChannel()
{
Type = ChannelType.Unknown,
Id = channelId,
GuildId = guildId,
Discord = this
};
if (channel == null
|| this.Configuration.MessageCacheSize == 0
|| this.MessageCache == null
|| !this.MessageCache.TryGet(xm => xm.Id == messageId && xm.ChannelId == channelId, out var msg))
{
msg = new DiscordMessage
{
Id = messageId,
ChannelId = channelId,
Discord = this
};
}
msg.ReactionsInternal?.Clear();
var guild = this.InternalGetCachedGuild(guildId);
var ea = new MessageReactionsClearEventArgs(this.ServiceProvider)
{
Message = msg
};
await this._messageReactionsCleared.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the message reaction remove event.
/// Fired when a emoji got removed.
///
/// The message id.
/// The channel id.
/// The guild id.
/// The raw discord emoji.
internal async Task OnMessageReactionRemoveEmojiAsync(ulong messageId, ulong channelId, ulong guildId, JToken dat)
{
var guild = this.InternalGetCachedGuild(guildId);
var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId);
if (channel == null
|| this.Configuration.MessageCacheSize == 0
|| this.MessageCache == null
|| !this.MessageCache.TryGet(xm => xm.Id == messageId && xm.ChannelId == channelId, out var msg))
{
msg = new DiscordMessage
{
Id = messageId,
ChannelId = channelId,
Discord = this
};
}
var partialEmoji = dat.ToObject();
if (!guild.EmojisInternal.TryGetValue(partialEmoji.Id, out var emoji))
{
emoji = partialEmoji;
emoji.Discord = this;
}
msg.ReactionsInternal?.RemoveAll(r => r.Emoji.Equals(emoji));
var ea = new MessageReactionRemoveEmojiEventArgs(this.ServiceProvider)
{
Channel = channel,
Guild = guild,
Message = msg,
Emoji = emoji
};
await this._messageReactionRemovedEmoji.InvokeAsync(this, ea).ConfigureAwait(false);
}
#endregion
#region Stage Instance
///
/// Handles the stage instance create event.
///
/// The created stage instance.
internal async Task OnStageInstanceCreateEventAsync(DiscordStageInstance stage)
{
stage.Discord = this;
var guild = this.InternalGetCachedGuild(stage.GuildId);
guild.StageInstancesInternal[stage.Id] = stage;
await this._stageInstanceCreated.InvokeAsync(this, new StageInstanceCreateEventArgs(this.ServiceProvider) { StageInstance = stage, Guild = stage.Guild }).ConfigureAwait(false);
}
///
/// Handles the stage instance update event.
///
/// The updated stage instance.
internal async Task OnStageInstanceUpdateEventAsync(DiscordStageInstance stage)
{
stage.Discord = this;
var guild = this.InternalGetCachedGuild(stage.GuildId);
guild.StageInstancesInternal[stage.Id] = stage;
await this._stageInstanceUpdated.InvokeAsync(this, new StageInstanceUpdateEventArgs(this.ServiceProvider) { StageInstance = stage, Guild = stage.Guild }).ConfigureAwait(false);
}
///
/// Handles the stage instance delete event.
///
/// The deleted stage instance.
internal async Task OnStageInstanceDeleteEventAsync(DiscordStageInstance stage)
{
stage.Discord = this;
var guild = this.InternalGetCachedGuild(stage.GuildId);
guild.StageInstancesInternal[stage.Id] = stage;
await this._stageInstanceDeleted.InvokeAsync(this, new StageInstanceDeleteEventArgs(this.ServiceProvider) { StageInstance = stage, Guild = stage.Guild }).ConfigureAwait(false);
}
#endregion
#region Thread
///
/// Handles the thread create event.
///
/// The created thread.
internal async Task OnThreadCreateEventAsync(DiscordThreadChannel thread)
{
thread.Discord = this;
this.InternalGetCachedGuild(thread.GuildId).ThreadsInternal.AddOrUpdate(thread.Id, thread, (oldThread, newThread) => newThread);
await this._threadCreated.InvokeAsync(this, new ThreadCreateEventArgs(this.ServiceProvider) { Thread = thread, Guild = thread.Guild, Parent = thread.Parent }).ConfigureAwait(false);
}
///
/// Handles the thread update event.
///
/// The updated thread.
internal async Task OnThreadUpdateEventAsync(DiscordThreadChannel thread)
{
if (thread == null)
return;
thread.Discord = this;
var guild = thread.Guild;
var threadNew = this.InternalGetCachedThread(thread.Id);
DiscordThreadChannel threadOld = null;
ThreadUpdateEventArgs updateEvent;
if (threadNew != null)
{
threadOld = new DiscordThreadChannel
{
Discord = this,
Type = threadNew.Type,
ThreadMetadata = thread.ThreadMetadata,
ThreadMembersInternal = threadNew.ThreadMembersInternal,
ParentId = thread.ParentId,
OwnerId = thread.OwnerId,
Name = thread.Name,
LastMessageId = threadNew.LastMessageId,
MessageCount = thread.MessageCount,
MemberCount = thread.MemberCount,
GuildId = thread.GuildId,
LastPinTimestampRaw = threadNew.LastPinTimestampRaw,
PerUserRateLimit = threadNew.PerUserRateLimit,
CurrentMember = threadNew.CurrentMember,
TotalMessagesSent = threadNew.TotalMessagesSent
};
if (this.Guilds != null)
{
if (thread.ParentId.HasValue && this.InternalGetCachedChannel(thread.ParentId.Value).Type == ChannelType.Forum)
{
threadOld.AppliedTagIdsInternal = threadNew.AppliedTagIdsInternal;
threadNew.AppliedTagIdsInternal = thread.AppliedTagIdsInternal;
}
else
{
threadOld.AppliedTagIdsInternal = null;
threadNew.AppliedTagIdsInternal = null;
}
}
else
{
threadOld.AppliedTagIdsInternal = threadNew.AppliedTagIdsInternal;
threadNew.AppliedTagIdsInternal = thread.AppliedTagIdsInternal;
}
threadNew.ThreadMetadata = thread.ThreadMetadata;
threadNew.ParentId = thread.ParentId;
threadNew.OwnerId = thread.OwnerId;
threadNew.Name = thread.Name;
threadNew.LastMessageId = thread.LastMessageId.HasValue ? thread.LastMessageId : threadOld.LastMessageId;
threadNew.MessageCount = thread.MessageCount;
threadNew.MemberCount = thread.MemberCount;
threadNew.GuildId = thread.GuildId;
threadNew.Discord = this;
threadNew.TotalMessagesSent = thread.TotalMessagesSent;
updateEvent = new ThreadUpdateEventArgs(this.ServiceProvider)
{
ThreadAfter = thread,
ThreadBefore = threadOld,
Guild = thread.Guild,
Parent = thread.Parent
};
}
else
{
updateEvent = new ThreadUpdateEventArgs(this.ServiceProvider)
{
ThreadAfter = thread,
Guild = thread.Guild,
Parent = thread.Parent
};
guild.ThreadsInternal[thread.Id] = thread;
}
await this._threadUpdated.InvokeAsync(this, updateEvent).ConfigureAwait(false);
}
///
/// Handles the thread delete event.
///
/// The deleted thread.
internal async Task OnThreadDeleteEventAsync(DiscordThreadChannel thread)
{
if (thread == null)
return;
thread.Discord = this;
var gld = thread.Guild;
if (gld.ThreadsInternal.TryRemove(thread.Id, out var cachedThread))
thread = cachedThread;
await this._threadDeleted.InvokeAsync(this, new ThreadDeleteEventArgs(this.ServiceProvider) { Thread = thread, Guild = thread.Guild, Parent = thread.Parent, Type = thread.Type }).ConfigureAwait(false);
}
///
/// Handles the thread list sync event.
///
/// The synced guild.
/// The synced channel ids.
/// The synced threads.
/// The synced thread members.
internal async Task OnThreadListSyncEventAsync(DiscordGuild guild, IReadOnlyList channelIds, IReadOnlyList threads, IReadOnlyList members)
{
guild.Discord = this;
var channels = channelIds.Select(x => guild.GetChannel(x.Value)); //getting channel objects
foreach (var chan in channels)
{
chan.Discord = this;
}
_ = threads.Select(x => x.Discord = this);
await this._threadListSynced.InvokeAsync(this, new ThreadListSyncEventArgs(this.ServiceProvider) { Guild = guild, Channels = channels.ToList().AsReadOnly(), Threads = threads, Members = members.ToList().AsReadOnly() }).ConfigureAwait(false);
}
///
/// Handles the thread member update event.
///
/// The updated member.
internal async Task OnThreadMemberUpdateEventAsync(DiscordThreadChannelMember member)
{
member.Discord = this;
var thread = this.InternalGetCachedThread(member.Id);
if (thread == null)
{
var tempThread = await this.ApiClient.GetThreadAsync(member.Id);
thread = this.GuildsInternal[member.GuildId].ThreadsInternal.AddOrUpdate(member.Id, tempThread, (old, newThread) => newThread);
}
thread.CurrentMember = member;
thread.Guild.ThreadsInternal.AddOrUpdate(member.Id, thread, (oldThread, newThread) => newThread);
await this._threadMemberUpdated.InvokeAsync(this, new ThreadMemberUpdateEventArgs(this.ServiceProvider) { ThreadMember = member, Thread = thread }).ConfigureAwait(false);
}
///
/// Handles the thread members update event.
///
/// The target guild.
/// The thread id of the target thread this update belongs to.
/// The added members.
/// The ids of the removed members.
/// The new member count.
internal async Task OnThreadMembersUpdateEventAsync(DiscordGuild guild, ulong threadId, JArray membersAdded, JArray membersRemoved, int memberCount)
{
var thread = this.InternalGetCachedThread(threadId);
if (thread == null)
{
var tempThread = await this.ApiClient.GetThreadAsync(threadId);
thread = guild.ThreadsInternal.AddOrUpdate(threadId, tempThread, (old, newThread) => newThread);
}
thread.Discord = this;
guild.Discord = this;
List addedMembers = new();
List removedMemberIds = new();
if (membersAdded != null)
{
foreach (var xj in membersAdded)
{
var xtm = xj.ToDiscordObject();
xtm.Discord = this;
xtm.GuildId = guild.Id;
if (xtm != null)
addedMembers.Add(xtm);
if (xtm.Id == this.CurrentUser.Id)
thread.CurrentMember = xtm;
}
}
var removedMembers = new List();
if (membersRemoved != null)
{
foreach (var removedId in membersRemoved)
{
removedMembers.Add(guild.MembersInternal.TryGetValue((ulong)removedId, out var member) ? member : new DiscordMember { Id = (ulong)removedId, GuildId = guild.Id, Discord = this });
}
}
if (removedMemberIds.Contains(this.CurrentUser.Id)) //indicates the bot was removed from the thread
thread.CurrentMember = null;
thread.MemberCount = memberCount;
var threadMembersUpdateArg = new ThreadMembersUpdateEventArgs(this.ServiceProvider)
{
Guild = guild,
Thread = thread,
AddedMembers = addedMembers,
RemovedMembers = removedMembers,
MemberCount = memberCount
};
await this._threadMembersUpdated.InvokeAsync(this, threadMembersUpdateArg).ConfigureAwait(false);
}
#endregion
#region Activities
///
/// Dispatches the event.
///
/// The transport activity.
/// The guild.
/// The channel id.
/// The users in the activity.
/// The application id.
internal async Task OnEmbeddedActivityUpdateAsync(JObject trActivity, DiscordGuild guild, ulong channelId, JArray jUsers, ulong appId)
=> await Task.Delay(20);
/*{
try
{
var users = j_users?.ToObject>();
DiscordActivity old = null;
var uid = $"{guild.Id}_{channel_id}_{app_id}";
if (this._embeddedActivities.TryGetValue(uid, out var activity))
{
old = new DiscordActivity(activity);
DiscordJson.PopulateObject(tr_activity, activity);
}
else
{
activity = tr_activity.ToObject();
this._embeddedActivities[uid] = activity;
}
var activity_users = new List();
var channel = this.InternalGetCachedChannel(channel_id) ?? await this.ApiClient.GetChannelAsync(channel_id);
if (users != null)
{
foreach (var user in users)
{
var activity_user = guild._members.TryGetValue(user, out var member) ? member : new DiscordMember { Id = user, _guild_id = guild.Id, Discord = this };
activity_users.Add(activity_user);
}
}
else
activity_users = null;
var ea = new EmbeddedActivityUpdateEventArgs(this.ServiceProvider)
{
Guild = guild,
Users = activity_users,
Channel = channel
};
await this._embeddedActivityUpdated.InvokeAsync(this, ea).ConfigureAwait(false);
} catch (Exception ex)
{
this.Logger.LogError(ex, ex.Message);
}
}*/
#endregion
#region User/Presence Update
///
/// Handles the presence update event.
///
/// The raw presence.
/// The raw user.
internal async Task OnPresenceUpdateEventAsync(JObject rawPresence, JObject rawUser)
{
var uid = (ulong)rawUser["id"];
DiscordPresence old = null;
if (this.PresencesInternal.TryGetValue(uid, out var presence))
{
old = new DiscordPresence(presence);
DiscordJson.PopulateObject(rawPresence, presence);
}
else
{
presence = rawPresence.ToObject();
presence.Discord = this;
presence.Activity = new DiscordActivity(presence.RawActivity);
this.PresencesInternal[presence.InternalUser.Id] = presence;
}
// reuse arrays / avoid linq (this is a hot zone)
if (presence.Activities == null || rawPresence["activities"] == null)
{
presence.InternalActivities = Array.Empty();
}
else
{
if (presence.InternalActivities.Length != presence.RawActivities.Length)
presence.InternalActivities = new DiscordActivity[presence.RawActivities.Length];
for (var i = 0; i < presence.InternalActivities.Length; i++)
presence.InternalActivities[i] = new DiscordActivity(presence.RawActivities[i]);
if (presence.InternalActivities.Length > 0)
{
presence.RawActivity = presence.RawActivities[0];
if (presence.Activity != null)
presence.Activity.UpdateWith(presence.RawActivity);
else
presence.Activity = new DiscordActivity(presence.RawActivity);
}
}
var ea = new PresenceUpdateEventArgs(this.ServiceProvider)
{
Status = presence.Status,
Activity = presence.Activity,
User = presence.User,
PresenceBefore = old,
PresenceAfter = presence
};
await this._presenceUpdated.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the user settings update event.
///
/// The transport user.
internal async Task OnUserSettingsUpdateEventAsync(TransportUser user)
{
var usr = new DiscordUser(user) { Discord = this };
var ea = new UserSettingsUpdateEventArgs(this.ServiceProvider)
{
User = usr
};
await this._userSettingsUpdated.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the user update event.
///
/// The transport user.
internal async Task OnUserUpdateEventAsync(TransportUser user)
{
var usrOld = new DiscordUser
{
AvatarHash = this.CurrentUser.AvatarHash,
Discord = this,
Discriminator = this.CurrentUser.Discriminator,
Email = this.CurrentUser.Email,
Id = this.CurrentUser.Id,
IsBot = this.CurrentUser.IsBot,
MfaEnabled = this.CurrentUser.MfaEnabled,
Username = this.CurrentUser.Username,
Verified = this.CurrentUser.Verified
};
this.CurrentUser.AvatarHash = user.AvatarHash;
this.CurrentUser.Discriminator = user.Discriminator;
this.CurrentUser.Email = user.Email;
this.CurrentUser.Id = user.Id;
this.CurrentUser.IsBot = user.IsBot;
this.CurrentUser.MfaEnabled = user.MfaEnabled;
this.CurrentUser.Username = user.Username;
this.CurrentUser.Verified = user.Verified;
var ea = new UserUpdateEventArgs(this.ServiceProvider)
{
UserAfter = this.CurrentUser,
UserBefore = usrOld
};
await this._userUpdated.InvokeAsync(this, ea).ConfigureAwait(false);
}
#endregion
#region Voice
///
/// Handles the voice state update event.
///
/// The raw voice state update object.
internal async Task OnVoiceStateUpdateEventAsync(JObject raw)
{
var gid = (ulong)raw["guild_id"];
var uid = (ulong)raw["user_id"];
var gld = this.GuildsInternal[gid];
var vstateNew = raw.ToObject();
vstateNew.Discord = this;
gld.VoiceStatesInternal.TryRemove(uid, out var vstateOld);
if (vstateNew.Channel != null)
{
gld.VoiceStatesInternal[vstateNew.UserId] = vstateNew;
}
if (gld.MembersInternal.TryGetValue(uid, out var mbr))
{
mbr.IsMuted = vstateNew.IsServerMuted;
mbr.IsDeafened = vstateNew.IsServerDeafened;
}
else
{
var transportMbr = vstateNew.TransportMember;
this.UpdateUser(new DiscordUser(transportMbr.User) { Discord = this }, gid, gld, transportMbr);
}
var ea = new VoiceStateUpdateEventArgs(this.ServiceProvider)
{
Guild = vstateNew.Guild,
Channel = vstateNew.Channel,
User = vstateNew.User,
SessionId = vstateNew.SessionId,
Before = vstateOld,
After = vstateNew
};
await this._voiceStateUpdated.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the voice server update event.
///
/// The new endpoint.
/// The new token.
/// The guild.
internal async Task OnVoiceServerUpdateEventAsync(string endpoint, string token, DiscordGuild guild)
{
var ea = new VoiceServerUpdateEventArgs(this.ServiceProvider)
{
Endpoint = endpoint,
VoiceToken = token,
Guild = guild
};
await this._voiceServerUpdated.InvokeAsync(this, ea).ConfigureAwait(false);
}
#endregion
#region Commands
///
/// Handles the application command create event.
///
/// The application command.
/// The optional guild id.
internal async Task OnApplicationCommandCreateAsync(DiscordApplicationCommand cmd, ulong? guildId)
{
cmd.Discord = this;
var guild = this.InternalGetCachedGuild(guildId);
if (guild == null && guildId.HasValue)
{
guild = new DiscordGuild
{
Id = guildId.Value,
Discord = this
};
}
var ea = new ApplicationCommandEventArgs(this.ServiceProvider)
{
Guild = guild,
Command = cmd
};
await this._applicationCommandCreated.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the application command update event.
///
/// The application command.
/// The optional guild id.
internal async Task OnApplicationCommandUpdateAsync(DiscordApplicationCommand cmd, ulong? guildId)
{
cmd.Discord = this;
var guild = this.InternalGetCachedGuild(guildId);
if (guild == null && guildId.HasValue)
{
guild = new DiscordGuild
{
Id = guildId.Value,
Discord = this
};
}
var ea = new ApplicationCommandEventArgs(this.ServiceProvider)
{
Guild = guild,
Command = cmd
};
await this._applicationCommandUpdated.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the application command delete event.
///
/// The application command.
/// The optional guild id.
internal async Task OnApplicationCommandDeleteAsync(DiscordApplicationCommand cmd, ulong? guildId)
{
cmd.Discord = this;
var guild = this.InternalGetCachedGuild(guildId);
if (guild == null && guildId.HasValue)
{
guild = new DiscordGuild
{
Id = guildId.Value,
Discord = this
};
}
var ea = new ApplicationCommandEventArgs(this.ServiceProvider)
{
Guild = guild,
Command = cmd
};
await this._applicationCommandDeleted.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the guild application command counts update event.
///
/// The count.
/// The count.
/// The count.
/// The guild id.
/// Count of application commands.
internal async Task OnGuildApplicationCommandCountsUpdateAsync(int chatInputCommandCount, int userContextMenuCommandCount, int messageContextMenuCount, ulong guildId)
{
var guild = this.InternalGetCachedGuild(guildId);
if (guild == null)
{
guild = new DiscordGuild
{
Id = guildId,
Discord = this
};
}
var ea = new GuildApplicationCommandCountEventArgs(this.ServiceProvider)
{
SlashCommands = chatInputCommandCount,
UserContextMenuCommands = userContextMenuCommandCount,
MessageContextMenuCommands = messageContextMenuCount,
Guild = guild
};
await this._guildApplicationCommandCountUpdated.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the application command permissions update event.
///
/// The new permissions.
/// The command id.
/// The guild id.
/// The application id.
internal async Task OnApplicationCommandPermissionsUpdateAsync(IEnumerable perms, ulong channelId, ulong guildId, ulong applicationId)
{
if (applicationId != this.CurrentApplication.Id)
return;
var guild = this.InternalGetCachedGuild(guildId);
DiscordApplicationCommand cmd;
try
{
cmd = await this.GetGuildApplicationCommandAsync(guildId, channelId);
}
catch (NotFoundException)
{
cmd = await this.GetGlobalApplicationCommandAsync(channelId);
}
if (guild == null)
{
guild = new DiscordGuild
{
Id = guildId,
Discord = this
};
}
var ea = new ApplicationCommandPermissionsUpdateEventArgs(this.ServiceProvider)
{
Permissions = perms.ToList(),
Command = cmd,
ApplicationId = applicationId,
Guild = guild
};
await this._applicationCommandPermissionsUpdated.InvokeAsync(this, ea).ConfigureAwait(false);
}
#endregion
#region Interaction
///
/// Handles the interaction create event.
///
/// The guild id.
/// The channel id.
/// The transport user.
/// The transport member.
/// The interaction.
/// Debug.
internal async Task OnInteractionCreateAsync(ulong? guildId, ulong channelId, TransportUser user, TransportMember member, DiscordInteraction interaction, string rawInteraction)
{
this.Logger.LogTrace("Interaction from {guild} on shard {shard}", guildId.HasValue ? guildId.Value : "dm", this.ShardId);
this.Logger.LogTrace("Interaction: {interaction}", rawInteraction);
var usr = new DiscordUser(user) { Discord = this };
interaction.ChannelId = channelId;
interaction.GuildId = guildId;
interaction.Discord = this;
interaction.Data.Discord = this;
if (member != null)
{
usr = new DiscordMember(member) { GuildId = guildId.Value, Discord = this };
this.UpdateUser(usr, guildId, interaction.Guild, member);
}
else
{
this.UserCache.AddOrUpdate(usr.Id, usr, (old, @new) => @new);
}
usr.Locale = interaction.Locale;
interaction.User = usr;
var resolved = interaction.Data.Resolved;
if (resolved != null)
{
if (resolved.Users != null)
{
foreach (var c in resolved.Users)
{
c.Value.Discord = this;
this.UserCache.AddOrUpdate(c.Value.Id, c.Value, (old, @new) => @new);
}
}
if (resolved.Members != null)
{
foreach (var c in resolved.Members)
{
c.Value.Discord = this;
c.Value.Id = c.Key;
c.Value.GuildId = guildId.Value;
c.Value.User.Discord = this;
this.UserCache.AddOrUpdate(c.Value.User.Id, c.Value.User, (old, @new) => @new);
}
}
if (resolved.Channels != null)
{
foreach (var c in resolved.Channels)
{
c.Value.Discord = this;
if (guildId.HasValue)
{
c.Value.GuildId = guildId.Value;
try
{
if (this.Guilds.TryGetValue(guildId.Value, out var guild))
if (guild.ChannelsInternal.TryGetValue(c.Key, out var channel) && channel.PermissionOverwritesInternal != null && channel.PermissionOverwritesInternal.Any())
c.Value.PermissionOverwritesInternal = channel.PermissionOverwritesInternal;
}
catch (Exception) { }
}
}
}
if (resolved.Roles != null)
{
foreach (var c in resolved.Roles)
{
c.Value.Discord = this;
if (guildId.HasValue)
c.Value.GuildId = guildId.Value;
}
}
if (resolved.Messages != null)
{
foreach (var m in resolved.Messages)
{
m.Value.Discord = this;
if (guildId.HasValue)
m.Value.GuildId = guildId.Value;
}
}
if (resolved.Attachments != null)
foreach (var a in resolved.Attachments)
a.Value.Discord = this;
}
if (interaction.Type is InteractionType.Component || interaction.Type is InteractionType.ModalSubmit)
{
if (interaction.Message != null)
{
interaction.Message.Discord = this;
interaction.Message.ChannelId = interaction.ChannelId;
}
var cea = new ComponentInteractionCreateEventArgs(this.ServiceProvider)
{
Message = interaction.Message,
Interaction = interaction
};
await this._componentInteractionCreated.InvokeAsync(this, cea).ConfigureAwait(false);
}
else
{
if (interaction.Data.Target.HasValue) // Context-Menu. //
{
var targetId = interaction.Data.Target.Value;
DiscordUser targetUser = null;
DiscordMember targetMember = null;
DiscordMessage targetMessage = null;
interaction.Data.Resolved.Messages?.TryGetValue(targetId, out targetMessage);
interaction.Data.Resolved.Members?.TryGetValue(targetId, out targetMember);
interaction.Data.Resolved.Users?.TryGetValue(targetId, out targetUser);
var ea = new ContextMenuInteractionCreateEventArgs(this.ServiceProvider)
{
Interaction = interaction,
TargetUser = targetMember ?? targetUser,
TargetMessage = targetMessage,
Type = interaction.Data.Type
};
await this._contextMenuInteractionCreated.InvokeAsync(this, ea);
}
else
{
var ea = new InteractionCreateEventArgs(this.ServiceProvider)
{
Interaction = interaction
};
await this._interactionCreated.InvokeAsync(this, ea);
}
}
}
#endregion
#region Misc
///
/// Handles the typing start event.
///
/// The user id.
/// The channel id.
/// The channel.
/// The optional guild id.
/// The time when the user started typing.
/// The transport member.
internal async Task OnTypingStartEventAsync(ulong userId, ulong channelId, DiscordChannel channel, ulong? guildId, DateTimeOffset started, TransportMember mbr)
{
if (channel == null)
{
channel = new DiscordChannel
{
Discord = this,
Id = channelId,
GuildId = guildId ?? default,
};
}
var guild = this.InternalGetCachedGuild(guildId);
var usr = this.UpdateUser(new DiscordUser { Id = userId, Discord = this }, guildId, guild, mbr);
var ea = new TypingStartEventArgs(this.ServiceProvider)
{
Channel = channel,
User = usr,
Guild = guild,
StartedAt = started
};
await this._typingStarted.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles the webhooks update.
///
/// The channel.
/// The guild.
internal async Task OnWebhooksUpdateAsync(DiscordChannel channel, DiscordGuild guild)
{
var ea = new WebhooksUpdateEventArgs(this.ServiceProvider)
{
Channel = channel,
Guild = guild
};
await this._webhooksUpdated.InvokeAsync(this, ea).ConfigureAwait(false);
}
///
/// Handles all unknown events.
///
/// The payload.
internal async Task OnUnknownEventAsync(GatewayPayload payload)
{
var ea = new UnknownEventArgs(this.ServiceProvider) { EventName = payload.EventName, Json = (payload.Data as JObject)?.ToString() };
await this._unknownEvent.InvokeAsync(this, ea).ConfigureAwait(false);
}
#endregion
#endregion
}
diff --git a/DisCatSharp/Clients/DiscordClient.WebSocket.cs b/DisCatSharp/Clients/DiscordClient.WebSocket.cs
index 0c16772ae..daa68d7ec 100644
--- a/DisCatSharp/Clients/DiscordClient.WebSocket.cs
+++ b/DisCatSharp/Clients/DiscordClient.WebSocket.cs
@@ -1,589 +1,634 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2023 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Concurrent;
using System.IO;
+using System.Linq;
+using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using DisCatSharp.Entities;
using DisCatSharp.Enums;
using DisCatSharp.EventArgs;
using DisCatSharp.Net.Abstractions;
using DisCatSharp.Net.WebSocket;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
+using Sentry;
+
namespace DisCatSharp;
///
/// Represents a discord websocket client.
///
public sealed partial class DiscordClient
{
#region Private Fields
private int _heartbeatInterval;
private DateTimeOffset _lastHeartbeat;
private Task _heartbeatTask;
internal static DateTimeOffset DiscordEpoch = new(2015, 1, 1, 0, 0, 0, TimeSpan.Zero);
private int _skippedHeartbeats;
private long _lastSequence;
internal IWebSocketClient WebSocketClient;
private PayloadDecompressor _payloadDecompressor;
private CancellationTokenSource _cancelTokenSource;
private CancellationToken _cancelToken;
#endregion
#region Connection Semaphore
///
/// Gets the socket locks.
///
private static ConcurrentDictionary s_socketLocks { get; } = new();
///
/// Gets the session lock.
///
private readonly ManualResetEventSlim _sessionLock = new(true);
#endregion
#region Internal Connection Methods
///
/// Reconnects the websocket client.
///
/// Whether to start a new session.
/// The reconnect code.
/// The reconnect message.
private Task InternalReconnectAsync(bool startNewSession = false, int code = 1000, string message = "")
{
if (startNewSession)
this._sessionId = null;
_ = this.WebSocketClient.DisconnectAsync(code, message);
return Task.CompletedTask;
}
///
/// Connects the websocket client.
///
internal async Task InternalConnectAsync()
{
+ using (SentrySdk.Init(o => {
+ o.DetectStartupTime = StartupTimeDetectionMode.Fast;
+ o.DiagnosticLevel = SentryLevel.Debug;
+ o.Environment = "prod";
+ o.IsGlobalModeEnabled = true;
+ o.TracesSampleRate = 1.0;
+ o.ReportAssembliesMode = ReportAssembliesMode.InformationalVersion;
+ o.Dsn = "https://1da216e26a2741b99e8ccfccea1b7ac8@o1113828.ingest.sentry.io/4504901362515968";
+ o.AddInAppInclude("DisCatSharp");
+ o.AttachStacktrace = true;
+ o.StackTraceMode = StackTraceMode.Enhanced;
+ //o.BeforeSend = sentryEvent => !sentryEvent.Modules.Keys.Contains("DisCatSharp") ? null : sentryEvent;
+ var a = typeof(DiscordClient).GetTypeInfo().Assembly;
+ var vs = "";
+ var iv = a.GetCustomAttribute();
+ if (iv != null)
+ vs = iv.InformationalVersion;
+ else
+ {
+ var v = a.GetName().Version;
+ vs = v.ToString(3);
+ }
+ o.Release = $"{this.BotLibrary}@{vs}";
+ o.SendClientReports = true;
+ }))
+ {
+ if (this.Configuration.EnableSentry)
+ SentrySdk.StartSession();
+
+
SocketLock socketLock = null;
try
{
if (this.GatewayInfo == null)
await this.InternalUpdateGatewayAsync().ConfigureAwait(false);
await this.InitializeAsync().ConfigureAwait(false);
socketLock = this.GetSocketLock();
await socketLock.LockAsync().ConfigureAwait(false);
- }
+ SentrySdk.ConfigureScope(o => o.User = new User()
+ {
+ Id = this.CurrentApplication.Id.ToString(),
+ Username = this.CurrentUser.UsernameWithDiscriminator
+ });
+ //SentrySdk.CaptureMessage($"Testing {DateTime.UtcNow.Ticks}");
+ }
catch
{
socketLock?.UnlockAfter(TimeSpan.Zero);
throw;
}
if (!this.Presences.ContainsKey(this.CurrentUser.Id))
{
this.PresencesInternal[this.CurrentUser.Id] = new DiscordPresence
{
Discord = this,
RawActivity = new TransportActivity(),
Activity = new DiscordActivity(),
Status = UserStatus.Online,
InternalUser = new UserWithIdOnly()
{
Id = this.CurrentUser.Id
}
};
}
else
{
var pr = this.PresencesInternal[this.CurrentUser.Id];
pr.RawActivity = new TransportActivity();
pr.Activity = new DiscordActivity();
pr.Status = UserStatus.Online;
}
Volatile.Write(ref this._skippedHeartbeats, 0);
this.WebSocketClient = this.Configuration.WebSocketClientFactory(this.Configuration.Proxy, this.ServiceProvider);
this._payloadDecompressor = this.Configuration.GatewayCompressionLevel != GatewayCompressionLevel.None
? new PayloadDecompressor(this.Configuration.GatewayCompressionLevel)
: null;
this._cancelTokenSource = new CancellationTokenSource();
this._cancelToken = this._cancelTokenSource.Token;
this.WebSocketClient.Connected += SocketOnConnect;
this.WebSocketClient.Disconnected += SocketOnDisconnect;
this.WebSocketClient.MessageReceived += SocketOnMessage;
this.WebSocketClient.ExceptionThrown += SocketOnException;
var gwuri = new QueryUriBuilder(this.GatewayUri)
.AddParameter("v", this.Configuration.ApiVersion)
.AddParameter("encoding", "json");
if (this.Configuration.GatewayCompressionLevel == GatewayCompressionLevel.Stream)
gwuri.AddParameter("compress", "zlib-stream");
this.Logger.LogDebug(LoggerEvents.Startup, "Connecting to {gw}", this.GatewayUri.AbsoluteUri);
await this.WebSocketClient.ConnectAsync(gwuri.Build()).ConfigureAwait(false);
+
+
Task SocketOnConnect(IWebSocketClient sender, SocketEventArgs e)
=> this._socketOpened.InvokeAsync(this, e);
async Task SocketOnMessage(IWebSocketClient sender, SocketMessageEventArgs e)
{
string msg = null;
if (e is SocketTextMessageEventArgs etext)
{
msg = etext.Message;
}
else if (e is SocketBinaryMessageEventArgs ebin)
{
using var ms = new MemoryStream();
if (!this._payloadDecompressor.TryDecompress(new ArraySegment(ebin.Message), ms))
{
this.Logger.LogError(LoggerEvents.WebSocketReceiveFailure, "Payload decompression failed");
return;
}
ms.Position = 0;
using var sr = new StreamReader(ms, Utilities.UTF8);
msg = await sr.ReadToEndAsync().ConfigureAwait(false);
}
try
{
this.Logger.LogTrace(LoggerEvents.GatewayWsRx, msg);
await this.HandleSocketMessageAsync(msg).ConfigureAwait(false);
}
catch (Exception ex)
{
this.Logger.LogError(LoggerEvents.WebSocketReceiveFailure, ex, "Socket handler suppressed an exception");
+ if (this.Configuration.EnableSentry)
+ SentrySdk.CaptureException(ex);
}
}
Task SocketOnException(IWebSocketClient sender, SocketErrorEventArgs e)
=> this._socketErrored.InvokeAsync(this, e);
async Task SocketOnDisconnect(IWebSocketClient sender, SocketCloseEventArgs e)
{
// release session and connection
this._connectionLock.Set();
this._sessionLock.Set();
if (!this._disposed)
this._cancelTokenSource.Cancel();
this.Logger.LogDebug(LoggerEvents.ConnectionClose, "Connection closed ({0}, '{1}')", e.CloseCode, e.CloseMessage);
await this._socketClosed.InvokeAsync(this, e).ConfigureAwait(false);
if (this.Configuration.AutoReconnect && (e.CloseCode < 4001 || e.CloseCode >= 5000))
{
this.Logger.LogCritical(LoggerEvents.ConnectionClose, "Connection terminated ({0}, '{1}'), reconnecting", e.CloseCode, e.CloseMessage);
if (this._status == null)
await this.ConnectAsync().ConfigureAwait(false);
else
if (this._status.IdleSince.HasValue)
await this.ConnectAsync(this._status.ActivityInternal, this._status.Status, Utilities.GetDateTimeOffsetFromMilliseconds(this._status.IdleSince.Value)).ConfigureAwait(false);
else
await this.ConnectAsync(this._status.ActivityInternal, this._status.Status).ConfigureAwait(false);
}
else
{
this.Logger.LogCritical(LoggerEvents.ConnectionClose, "Connection terminated ({0}, '{1}')", e.CloseCode, e.CloseMessage);
}
+ }
}
}
#endregion
#region WebSocket (Events)
///
/// Handles the socket message.
///
/// The data.
internal async Task HandleSocketMessageAsync(string data)
{
var payload = JsonConvert.DeserializeObject(data);
this._lastSequence = payload.Sequence ?? this._lastSequence;
switch (payload.OpCode)
{
case GatewayOpCode.Dispatch:
await Task.Run(async () => await this.HandleDispatchAsync(payload).ConfigureAwait(false));
break;
case GatewayOpCode.Heartbeat:
await this.OnHeartbeatAsync((long)payload.Data).ConfigureAwait(false);
break;
case GatewayOpCode.Reconnect:
await this.OnReconnectAsync().ConfigureAwait(false);
break;
case GatewayOpCode.InvalidSession:
await this.OnInvalidateSessionAsync((bool)payload.Data).ConfigureAwait(false);
break;
case GatewayOpCode.Hello:
await this.OnHelloAsync((payload.Data as JObject).ToObject()).ConfigureAwait(false);
break;
case GatewayOpCode.HeartbeatAck:
await this.OnHeartbeatAckAsync().ConfigureAwait(false);
break;
default:
this.Logger.LogWarning(LoggerEvents.WebSocketReceive, "Unknown Discord opcode: {0}\nPayload: {1}", payload.OpCode, payload.Data);
break;
}
}
///
/// Handles the heartbeat.
///
/// The sequence.
internal async Task OnHeartbeatAsync(long seq)
{
this.Logger.LogTrace(LoggerEvents.WebSocketReceive, "Received HEARTBEAT (OP1)");
await this.SendHeartbeatAsync(seq).ConfigureAwait(false);
}
///
/// Handles the reconnect event.
///
internal async Task OnReconnectAsync()
{
this.Logger.LogTrace(LoggerEvents.WebSocketReceive, "Received RECONNECT (OP7)");
await this.InternalReconnectAsync(code: 4000, message: "OP7 acknowledged").ConfigureAwait(false);
}
///
/// Handles the invalidate session event
///
/// Unknown. Please fill documentation.
internal async Task OnInvalidateSessionAsync(bool data)
{
// begin a session if one is not open already
if (this._sessionLock.Wait(0))
this._sessionLock.Reset();
// we are sending a fresh resume/identify, so lock the socket
var socketLock = this.GetSocketLock();
await socketLock.LockAsync().ConfigureAwait(false);
socketLock.UnlockAfter(TimeSpan.FromSeconds(5));
if (data)
{
this.Logger.LogTrace(LoggerEvents.WebSocketReceive, "Received INVALID_SESSION (OP9, true)");
await Task.Delay(6000).ConfigureAwait(false);
await this.SendResumeAsync().ConfigureAwait(false);
}
else
{
this.Logger.LogTrace(LoggerEvents.WebSocketReceive, "Received INVALID_SESSION (OP9, false)");
this._sessionId = null;
await this.SendIdentifyAsync(this._status).ConfigureAwait(false);
}
}
///
/// Handles the hello event.
///
/// The gateway hello payload.
internal async Task OnHelloAsync(GatewayHello hello)
{
this.Logger.LogTrace(LoggerEvents.WebSocketReceive, "Received HELLO (OP10)");
if (this._sessionLock.Wait(0))
{
this._sessionLock.Reset();
this.GetSocketLock().UnlockAfter(TimeSpan.FromSeconds(5));
}
else
{
this.Logger.LogWarning(LoggerEvents.SessionUpdate, "Attempt to start a session while another session is active");
return;
}
Interlocked.CompareExchange(ref this._skippedHeartbeats, 0, 0);
this._heartbeatInterval = hello.HeartbeatInterval;
this._heartbeatTask = Task.Run(this.HeartbeatLoopAsync, this._cancelToken);
if (string.IsNullOrEmpty(this._sessionId))
await this.SendIdentifyAsync(this._status).ConfigureAwait(false);
else
await this.SendResumeAsync().ConfigureAwait(false);
}
///
/// Handles the heartbeat acknowledge event.
///
internal async Task OnHeartbeatAckAsync()
{
Interlocked.Decrement(ref this._skippedHeartbeats);
var ping = (int)(DateTime.Now - this._lastHeartbeat).TotalMilliseconds;
this.Logger.LogTrace(LoggerEvents.WebSocketReceive, "Received HEARTBEAT_ACK (OP11, {0}ms)", ping);
Volatile.Write(ref this._ping, ping);
var args = new HeartbeatEventArgs(this.ServiceProvider)
{
Ping = this.Ping,
Timestamp = DateTimeOffset.Now
};
await this._heartbeated.InvokeAsync(this, args).ConfigureAwait(false);
}
///
/// Handles the heartbeat loop.
///
internal async Task HeartbeatLoopAsync()
{
this.Logger.LogDebug(LoggerEvents.Heartbeat, "Heartbeat task started");
var token = this._cancelToken;
try
{
while (true)
{
await this.SendHeartbeatAsync(this._lastSequence).ConfigureAwait(false);
await Task.Delay(this._heartbeatInterval, token).ConfigureAwait(false);
token.ThrowIfCancellationRequested();
}
}
catch (OperationCanceledException) { }
}
#endregion
#region Internal Gateway Methods
///
/// Updates the status.
///
/// The activity.
/// The optional user status.
/// Since when is the client performing the specified activity.
internal async Task InternalUpdateStatusAsync(DiscordActivity activity, UserStatus? userStatus, DateTimeOffset? idleSince)
{
if (activity != null && activity.Name != null && activity.Name.Length > 128)
throw new Exception("Game name can't be longer than 128 characters!");
var sinceUnix = idleSince != null ? (long?)Utilities.GetUnixTime(idleSince.Value) : null;
var act = activity ?? new DiscordActivity();
var status = new StatusUpdate
{
Activity = new TransportActivity(act),
IdleSince = sinceUnix,
IsAfk = idleSince != null,
Status = userStatus ?? UserStatus.Online
};
// Solution to have status persist between sessions
this._status = status;
var statusUpdate = new GatewayPayload
{
OpCode = GatewayOpCode.StatusUpdate,
Data = status
};
var statusstr = JsonConvert.SerializeObject(statusUpdate);
await this.WsSendAsync(statusstr).ConfigureAwait(false);
if (!this.PresencesInternal.ContainsKey(this.CurrentUser.Id))
{
this.PresencesInternal[this.CurrentUser.Id] = new DiscordPresence
{
Discord = this,
Activity = act,
Status = userStatus ?? UserStatus.Online,
InternalUser = new UserWithIdOnly { Id = this.CurrentUser.Id }
};
}
else
{
var pr = this.PresencesInternal[this.CurrentUser.Id];
pr.Activity = act;
pr.Status = userStatus ?? pr.Status;
}
}
///
/// Sends the heartbeat.
///
/// The sequenze.
internal async Task SendHeartbeatAsync(long seq)
{
var moreThan5 = Volatile.Read(ref this._skippedHeartbeats) > 5;
var guildsComp = Volatile.Read(ref this._guildDownloadCompleted);
if (guildsComp && moreThan5)
{
this.Logger.LogCritical(LoggerEvents.HeartbeatFailure, "Server failed to acknowledge more than 5 heartbeats - connection is zombie");
var args = new ZombiedEventArgs(this.ServiceProvider)
{
Failures = Volatile.Read(ref this._skippedHeartbeats),
GuildDownloadCompleted = true
};
await this._zombied.InvokeAsync(this, args).ConfigureAwait(false);
await this.InternalReconnectAsync(code: 4001, message: "Too many heartbeats missed").ConfigureAwait(false);
return;
}
else if (!guildsComp && moreThan5)
{
var args = new ZombiedEventArgs(this.ServiceProvider)
{
Failures = Volatile.Read(ref this._skippedHeartbeats),
GuildDownloadCompleted = false
};
await this._zombied.InvokeAsync(this, args).ConfigureAwait(false);
this.Logger.LogWarning(LoggerEvents.HeartbeatFailure, "Server failed to acknowledge more than 5 heartbeats, but the guild download is still running - check your connection speed");
}
Volatile.Write(ref this._lastSequence, seq);
this.Logger.LogTrace(LoggerEvents.Heartbeat, "Sending heartbeat");
var heartbeat = new GatewayPayload
{
OpCode = GatewayOpCode.Heartbeat,
Data = seq
};
var heartbeatStr = JsonConvert.SerializeObject(heartbeat);
await this.WsSendAsync(heartbeatStr).ConfigureAwait(false);
this._lastHeartbeat = DateTimeOffset.Now;
Interlocked.Increment(ref this._skippedHeartbeats);
}
///
/// Sends the identify payload.
///
/// The status update payload.
internal async Task SendIdentifyAsync(StatusUpdate status)
{
var identify = new GatewayIdentify
{
Token = Utilities.GetFormattedToken(this),
Compress = this.Configuration.GatewayCompressionLevel == GatewayCompressionLevel.Payload,
LargeThreshold = this.Configuration.LargeThreshold,
ShardInfo = new ShardInfo
{
ShardId = this.Configuration.ShardId,
ShardCount = this.Configuration.ShardCount
},
Presence = status,
Intents = this.Configuration.Intents,
Discord = this
};
var payload = new GatewayPayload
{
OpCode = GatewayOpCode.Identify,
Data = identify
};
var payloadstr = JsonConvert.SerializeObject(payload);
await this.WsSendAsync(payloadstr).ConfigureAwait(false);
this.Logger.LogDebug(LoggerEvents.Intents, "Registered gateway intents ({0})", this.Configuration.Intents);
}
///
/// Sends the resume payload.
///
internal async Task SendResumeAsync()
{
var resume = new GatewayResume
{
Token = Utilities.GetFormattedToken(this),
SessionId = this._sessionId,
SequenceNumber = Volatile.Read(ref this._lastSequence)
};
var resumePayload = new GatewayPayload
{
OpCode = GatewayOpCode.Resume,
Data = resume
};
var resumestr = JsonConvert.SerializeObject(resumePayload);
this.GatewayUri = new Uri(this._resumeGatewayUrl);
this.Logger.LogDebug(LoggerEvents.ConnectionClose, "Request to resume via {gw}", this.GatewayUri.AbsoluteUri);
await this.WsSendAsync(resumestr).ConfigureAwait(false);
}
///
/// Internals the update gateway async.
///
/// A Task.
internal async Task InternalUpdateGatewayAsync()
{
var info = await this.GetGatewayInfoAsync().ConfigureAwait(false);
this.GatewayInfo = info;
this.GatewayUri = new Uri(info.Url);
}
///
/// Sends a websocket message.
///
/// The payload to send.
internal async Task WsSendAsync(string payload)
{
this.Logger.LogTrace(LoggerEvents.GatewayWsTx, payload);
await this.WebSocketClient.SendMessageAsync(payload).ConfigureAwait(false);
}
#endregion
#region Semaphore Methods
///
/// Gets the socket lock.
///
/// The added socket lock.
private SocketLock GetSocketLock()
=> s_socketLocks.GetOrAdd(this.CurrentApplication.Id, appId => new SocketLock(appId, this.GatewayInfo.SessionBucket.MaxConcurrency));
#endregion
}
diff --git a/DisCatSharp/DisCatSharp.csproj b/DisCatSharp/DisCatSharp.csproj
index d790add00..a8965d405 100644
--- a/DisCatSharp/DisCatSharp.csproj
+++ b/DisCatSharp/DisCatSharp.csproj
@@ -1,72 +1,72 @@
DisCatSharp
DisCatSharp
DisCatSharp
DisCatSharp
Your library to write discord bots in C# with a focus on always providing access to the latest discord features.
Written with love and for everyone.
DisCatSharp,Discord API Wrapper,Discord,Bots,Discord Bots,AITSYS,Net6,Net7
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
+
+
+
True
True
Resources.resx
ResXFileCodeGenerator
Resources.Designer.cs
diff --git a/DisCatSharp/DiscordConfiguration.cs b/DisCatSharp/DiscordConfiguration.cs
index d315a49b9..0ea857acf 100644
--- a/DisCatSharp/DiscordConfiguration.cs
+++ b/DisCatSharp/DiscordConfiguration.cs
@@ -1,325 +1,327 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2023 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Net;
using DisCatSharp.Attributes;
using DisCatSharp.Entities;
using DisCatSharp.Enums;
using DisCatSharp.Net.Udp;
using DisCatSharp.Net.WebSocket;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace DisCatSharp;
///
/// Represents configuration for and .
///
public sealed class DiscordConfiguration
{
///
/// Sets the token used to identify the client.
///
public string Token
{
internal get => this._token;
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentNullException(nameof(value), "Token cannot be null, empty, or all whitespace.");
this._token = value.Trim();
}
}
private string _token = "";
///
/// Sets the type of the token used to identify the client.
/// Defaults to .
///
public TokenType TokenType { internal get; set; } = TokenType.Bot;
///
/// Sets the minimum logging level for messages.
/// Typically, the default value of is ok for most uses.
///
public LogLevel MinimumLogLevel { internal get; set; } = LogLevel.Information;
///
/// Overwrites the api version.
/// Defaults to 10.
///
public string ApiVersion { internal get; set; } = "10";
///
/// Sets whether to rely on Discord for NTP (Network Time Protocol) synchronization with the "X-Ratelimit-Reset-After" header.
/// If the system clock is unsynced, setting this to true will ensure ratelimits are synced with Discord and reduce the risk of hitting one.
/// This should only be set to false if the system clock is synced with NTP.
/// Defaults to .
///
public bool UseRelativeRatelimit { internal get; set; } = true;
///
/// Allows you to overwrite the time format used by the internal debug logger.
/// Only applicable when is set left at default value. Defaults to ISO 8601-like format.
///
public string LogTimestampFormat { internal get; set; } = "yyyy-MM-dd HH:mm:ss zzz";
///
/// Sets the member count threshold at which guilds are considered large.
/// Defaults to 250.
///
public int LargeThreshold { internal get; set; } = 250;
///
/// Sets whether to automatically reconnect in case a connection is lost.
/// Defaults to .
///
public bool AutoReconnect { internal get; set; } = true;
///
/// Sets the ID of the shard to connect to.
/// If not sharding, or sharding automatically, this value should be left with the default value of 0.
///
public int ShardId { internal get; set; }
///
/// Sets the total number of shards the bot is on. If not sharding, this value should be left with a default value of 1.
/// If sharding automatically, this value will indicate how many shards to boot. If left default for automatic sharding, the client will determine the shard count automatically.
///
public int ShardCount { internal get; set; } = 1;
///
/// Sets the level of compression for WebSocket traffic.
/// Disabling this option will increase the amount of traffic sent via WebSocket. Setting will enable compression for READY and GUILD_CREATE payloads. Setting will enable compression for the entire WebSocket stream, drastically reducing amount of traffic.
/// Defaults to .
///
public GatewayCompressionLevel GatewayCompressionLevel { internal get; set; } = GatewayCompressionLevel.Stream;
///
/// Sets the size of the global message cache.
/// Setting this to 0 will disable message caching entirely.
/// Defaults to 1024.
///
public int MessageCacheSize { internal get; set; } = 1024;
///
/// Sets the proxy to use for HTTP and WebSocket connections to Discord.
/// Defaults to .
///
public IWebProxy Proxy { internal get; set; }
///
/// Sets the timeout for HTTP requests.
/// Set to to disable timeouts.
/// Defaults to 20 seconds.
///
public TimeSpan HttpTimeout { internal get; set; } = TimeSpan.FromSeconds(20);
///
/// Defines that the client should attempt to reconnect indefinitely.
/// This is typically a very bad idea to set to true, as it will swallow all connection errors.
/// Defaults to .
///
public bool ReconnectIndefinitely { internal get; set; }
///
/// Sets whether the client should attempt to cache members if exclusively using unprivileged intents.
///
/// This will only take effect if there are no or
/// intents specified. Otherwise, this will always be overwritten to true.
///
/// Defaults to .
///
public bool AlwaysCacheMembers { internal get; set; } = true;
///
/// Sets the gateway intents for this client.
/// If set, the client will only receive events that they specify with intents.
/// Defaults to .
///
public DiscordIntents Intents { internal get; set; } = DiscordIntents.AllUnprivileged;
///
/// Sets the factory method used to create instances of WebSocket clients.
/// Use and equivalents on other implementations to switch out client implementations.
/// Defaults to .
///
public WebSocketClientFactoryDelegate WebSocketClientFactory
{
internal get => this._webSocketClientFactory;
set
{
if (value == null)
throw new InvalidOperationException("You need to supply a valid WebSocket client factory method.");
this._webSocketClientFactory = value;
}
}
private WebSocketClientFactoryDelegate _webSocketClientFactory = WebSocketClient.CreateNew;
///
/// Sets the factory method used to create instances of UDP clients.
/// Use and equivalents on other implementations to switch out client implementations.
/// Defaults to .
///
public UdpClientFactoryDelegate UdpClientFactory
{
internal get => this._udpClientFactory;
set => this._udpClientFactory = value ?? throw new InvalidOperationException("You need to supply a valid UDP client factory method.");
}
private UdpClientFactoryDelegate _udpClientFactory = DcsUdpClient.CreateNew;
///
/// Sets the logger implementation to use.
/// To create your own logger, implement the instance.
/// Defaults to built-in implementation.
///
public ILoggerFactory LoggerFactory { internal get; set; }
///
/// Sets if the bot's status should show the mobile icon.
/// Defaults to .
///
public bool MobileStatus { internal get; set; }
///
/// Whether to use canary. has to be false.
/// Defaults to .
///
[Deprecated("Use ApiChannel instead.")]
public bool UseCanary
{
internal get => this.ApiChannel == ApiChannel.Canary;
set
{
if (value)
this.ApiChannel = ApiChannel.Canary;
}
}
///
/// Whether to use ptb. has to be false.
/// Defaults to .
///
[Deprecated("Use ApiChannel instead.")]
public bool UsePtb
{
internal get => this.ApiChannel == ApiChannel.PTB;
set
{
if (value)
this.ApiChannel = ApiChannel.PTB;
}
}
///
/// Which api channel to use.
/// Defaults to .
///
public ApiChannel ApiChannel { internal get; set; } = ApiChannel.Stable;
///
/// Refresh full guild channel cache.
/// Defaults to .
///
public bool AutoRefreshChannelCache { internal get; set; }
///
/// Do not use, this is meant for DisCatSharp Devs.
/// Defaults to .
///
public string Override { internal get; set; }
///
/// Sets your preferred API language. See for valid locales.
///
public string Locale { internal get; set; } = DiscordLocales.AMERICAN_ENGLISH;
///
/// Whether to report missing fields for discord object.
/// Useful for library development.
/// Defaults to .
///
public bool ReportMissingFields { internal get; set; } = false;
///
/// Sets the service provider.
/// This allows passing data around without resorting to static members.
/// Defaults to an empty service provider.
///
public IServiceProvider ServiceProvider { internal get; set; } = new ServiceCollection().BuildServiceProvider(true);
+ public bool EnableSentry { get; internal set; } = false;
///
/// Creates a new configuration with default values.
///
public DiscordConfiguration()
{ }
///
/// Utilized via Dependency Injection Pipeline
///
///
[ActivatorUtilitiesConstructor]
public DiscordConfiguration(IServiceProvider provider)
{
this.ServiceProvider = provider;
}
///
/// Creates a clone of another discord configuration.
///
/// Client configuration to clone.
public DiscordConfiguration(DiscordConfiguration other)
{
this.Token = other.Token;
this.TokenType = other.TokenType;
this.MinimumLogLevel = other.MinimumLogLevel;
this.UseRelativeRatelimit = other.UseRelativeRatelimit;
this.LogTimestampFormat = other.LogTimestampFormat;
this.LargeThreshold = other.LargeThreshold;
this.AutoReconnect = other.AutoReconnect;
this.ShardId = other.ShardId;
this.ShardCount = other.ShardCount;
this.GatewayCompressionLevel = other.GatewayCompressionLevel;
this.MessageCacheSize = other.MessageCacheSize;
this.WebSocketClientFactory = other.WebSocketClientFactory;
this.UdpClientFactory = other.UdpClientFactory;
this.Proxy = other.Proxy;
this.HttpTimeout = other.HttpTimeout;
this.ReconnectIndefinitely = other.ReconnectIndefinitely;
this.Intents = other.Intents;
this.LoggerFactory = other.LoggerFactory;
this.MobileStatus = other.MobileStatus;
this.UseCanary = other.UseCanary;
this.UsePtb = other.UsePtb;
this.AutoRefreshChannelCache = other.AutoRefreshChannelCache;
this.ApiVersion = other.ApiVersion;
this.ServiceProvider = other.ServiceProvider;
this.Override = other.Override;
this.Locale = other.Locale;
this.ReportMissingFields = other.ReportMissingFields;
+ this.EnableSentry = other.EnableSentry;
}
}
diff --git a/DisCatSharp/Net/Rest/RestClient.cs b/DisCatSharp/Net/Rest/RestClient.cs
index d515d55bc..bf6d0683e 100644
--- a/DisCatSharp/Net/Rest/RestClient.cs
+++ b/DisCatSharp/Net/Rest/RestClient.cs
@@ -1,870 +1,888 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2023 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
+using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using DisCatSharp.Exceptions;
using Microsoft.Extensions.Logging;
+using Sentry;
+
namespace DisCatSharp.Net;
///
/// Represents a client used to make REST requests.
///
internal sealed class RestClient : IDisposable
{
///
/// Gets the route argument regex.
///
private static Regex s_routeArgumentRegex { get; } = new(@":([a-z_]+)");
///
/// Gets the http client.
///
internal HttpClient HttpClient { get; }
///
/// Gets the discord client.
///
private readonly BaseDiscordClient _discord;
///
/// Gets a value indicating whether debug is enabled.
///
internal bool Debug { get; set; }
///
/// Gets the logger.
///
private readonly ILogger _logger;
///
/// Gets the routes to hashes.
///
private readonly ConcurrentDictionary _routesToHashes;
///
/// Gets the hashes to buckets.
///
private readonly ConcurrentDictionary _hashesToBuckets;
///
/// Gets the request queue.
///
private readonly ConcurrentDictionary _requestQueue;
///
/// Gets the global rate limit event.
///
private readonly AsyncManualResetEvent _globalRateLimitEvent;
///
/// Gets a value indicating whether use reset after.
///
private readonly bool _useResetAfter;
private CancellationTokenSource _bucketCleanerTokenSource;
private readonly TimeSpan _bucketCleanupDelay = TimeSpan.FromSeconds(60);
private volatile bool _cleanerRunning;
private Task _cleanerTask;
private volatile bool _disposed;
///
/// Initializes a new instance of the class.
///
/// The client.
internal RestClient(BaseDiscordClient client)
: this(client.Configuration.Proxy, client.Configuration.HttpTimeout, client.Configuration.UseRelativeRatelimit, client.Logger)
{
this._discord = client;
this.HttpClient.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", Utilities.GetFormattedToken(client));
if (client.Configuration.Override != null)
{
this.HttpClient.DefaultRequestHeaders.TryAddWithoutValidation("x-super-properties", client.Configuration.Override);
}
}
///
/// Initializes a new instance of the class.
/// This is for meta-clients, such as the webhook client.
///
/// The proxy.
/// The timeout.
/// Whether to use relative ratelimit.
/// The logger.
internal RestClient(IWebProxy proxy, TimeSpan timeout, bool useRelativeRatelimit,
ILogger logger)
{
this._logger = logger;
var httphandler = new HttpClientHandler
{
UseCookies = false,
AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip,
UseProxy = proxy != null,
Proxy = proxy
};
this.HttpClient = new HttpClient(httphandler)
{
BaseAddress = new Uri(Utilities.GetApiBaseUri(this._discord?.Configuration)),
Timeout = timeout
};
this.HttpClient.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", Utilities.GetUserAgent());
if (this._discord != null && this._discord.Configuration != null && this._discord.Configuration.Override != null)
this.HttpClient.DefaultRequestHeaders.TryAddWithoutValidation("x-super-properties", this._discord.Configuration.Override);
this.HttpClient.DefaultRequestHeaders.TryAddWithoutValidation("X-Discord-Locale", this._discord?.Configuration?.Locale ?? "en-US");
this._routesToHashes = new ConcurrentDictionary();
this._hashesToBuckets = new ConcurrentDictionary();
this._requestQueue = new ConcurrentDictionary();
this._globalRateLimitEvent = new AsyncManualResetEvent(true);
this._useResetAfter = useRelativeRatelimit;
}
///
/// Gets a ratelimit bucket.
///
/// The method.
/// The route.
/// The route parameters.
/// The url.
/// A ratelimit bucket.
public RateLimitBucket GetBucket(RestRequestMethod method, string route, object routeParams, out string url)
{
var rparamsProps = routeParams.GetType()
.GetTypeInfo()
.DeclaredProperties;
var rparams = new Dictionary();
foreach (var xp in rparamsProps)
{
var val = xp.GetValue(routeParams);
rparams[xp.Name] = val is string xs
? xs
: val is DateTime dt
? dt.ToString("yyyy-MM-ddTHH:mm:sszzz", CultureInfo.InvariantCulture)
: val is DateTimeOffset dto
? dto.ToString("yyyy-MM-ddTHH:mm:sszzz", CultureInfo.InvariantCulture)
: val is IFormattable xf ? xf.ToString(null, CultureInfo.InvariantCulture) : val.ToString();
}
var guildId = rparams.ContainsKey("guild_id") ? rparams["guild_id"] : "";
var channelId = rparams.ContainsKey("channel_id") ? rparams["channel_id"] : "";
var webhookId = rparams.ContainsKey("webhook_id") ? rparams["webhook_id"] : "";
// Create a generic route (minus major params) key
// ex: POST:/channels/channel_id/messages
var hashKey = RateLimitBucket.GenerateHashKey(method, route);
// We check if the hash is present, using our generic route (without major params)
// ex: in POST:/channels/channel_id/messages, out 80c17d2f203122d936070c88c8d10f33
// If it doesn't exist, we create an unlimited hash as our initial key in the form of the hash key + the unlimited constant
// and assign this to the route to hash cache
// ex: this.RoutesToHashes[POST:/channels/channel_id/messages] = POST:/channels/channel_id/messages:unlimited
var hash = this._routesToHashes.GetOrAdd(hashKey, RateLimitBucket.GenerateUnlimitedHash(method, route));
// Next we use the hash to generate the key to obtain the bucket.
// ex: 80c17d2f203122d936070c88c8d10f33:guild_id:506128773926879242:webhook_id
// or if unlimited: POST:/channels/channel_id/messages:unlimited:guild_id:506128773926879242:webhook_id
var bucketId = RateLimitBucket.GenerateBucketId(hash, guildId, channelId, webhookId);
// If it's not in cache, create a new bucket and index it by its bucket id.
var bucket = this._hashesToBuckets.GetOrAdd(bucketId, new RateLimitBucket(hash, guildId, channelId, webhookId));
bucket.LastAttemptAt = DateTimeOffset.UtcNow;
// Cache the routes for each bucket so it can be used for GC later.
if (!bucket.RouteHashes.Contains(bucketId))
bucket.RouteHashes.Add(bucketId);
// Add the current route to the request queue, which indexes the amount
// of requests occurring to the bucket id.
_ = this._requestQueue.TryGetValue(bucketId, out var count);
// Increment by one atomically due to concurrency
this._requestQueue[bucketId] = Interlocked.Increment(ref count);
// Start bucket cleaner if not already running.
if (!this._cleanerRunning)
{
this._cleanerRunning = true;
this._bucketCleanerTokenSource = new CancellationTokenSource();
this._cleanerTask = Task.Run(this.CleanupBucketsAsync, this._bucketCleanerTokenSource.Token);
this._logger.LogDebug(LoggerEvents.RestCleaner, "Bucket cleaner task started.");
}
url = s_routeArgumentRegex.Replace(route, xm => rparams[xm.Groups[1].Value]);
return bucket;
}
///
/// Executes the request.
///
/// The request to be executed.
public Task ExecuteRequestAsync(BaseRestRequest request)
=> request == null ? throw new ArgumentNullException(nameof(request)) : this.ExecuteRequestAsync(request, null, null);
///
/// Executes the request.
/// This is to allow proper rescheduling of the first request from a bucket.
///
/// The request to be executed.
/// The bucket.
/// The ratelimit task completion source.
private async Task ExecuteRequestAsync(BaseRestRequest request, RateLimitBucket bucket, TaskCompletionSource ratelimitTcs)
{
if (this._disposed)
return;
HttpResponseMessage res = default;
try
{
await this._globalRateLimitEvent.WaitAsync().ConfigureAwait(false);
bucket ??= request.RateLimitBucket;
ratelimitTcs ??= await this.WaitForInitialRateLimit(bucket).ConfigureAwait(false);
if (ratelimitTcs == null) // check rate limit only if we are not the probe request
{
var now = DateTimeOffset.UtcNow;
await bucket.TryResetLimitAsync(now).ConfigureAwait(false);
// Decrement the remaining number of requests as there can be other concurrent requests before this one finishes and has a chance to update the bucket
if (Interlocked.Decrement(ref bucket.RemainingInternal) < 0)
{
this._logger.LogWarning(LoggerEvents.RatelimitDiag, "Request for {bucket} is blocked. Url: {url}", bucket.ToString(), request.Url.AbsoluteUri);
var delay = bucket.Reset - now;
var resetDate = bucket.Reset;
if (this._useResetAfter)
{
delay = bucket.ResetAfter.Value;
resetDate = bucket.ResetAfterOffset;
}
if (delay < new TimeSpan(-TimeSpan.TicksPerMinute))
{
this._logger.LogError(LoggerEvents.RatelimitDiag, "Failed to retrieve ratelimits - giving up and allowing next request for bucket");
bucket.RemainingInternal = 1;
}
if (delay < TimeSpan.Zero)
delay = TimeSpan.FromMilliseconds(100);
this._logger.LogWarning(LoggerEvents.RatelimitPreemptive, "Preemptive ratelimit triggered - waiting until {0:yyyy-MM-dd HH:mm:ss zzz} ({1:c}).", resetDate, delay);
Task.Delay(delay)
.ContinueWith(_ => this.ExecuteRequestAsync(request, null, null))
.LogTaskFault(this._logger, LogLevel.Error, LoggerEvents.RestError, "Error while executing request");
return;
}
this._logger.LogDebug(LoggerEvents.RatelimitDiag, "Request for {bucket} is allowed. Url: {url}", bucket.ToString(), request.Url.AbsoluteUri);
}
else
this._logger.LogDebug(LoggerEvents.RatelimitDiag, "Initial request for {bucket} is allowed. Url: {url}", bucket.ToString(), request.Url.AbsoluteUri);
var req = this.BuildRequest(request);
if (this.Debug)
this._logger.LogTrace(LoggerEvents.Misc, await req.Content.ReadAsStringAsync());
var response = new RestResponse();
try
{
if (this._disposed)
return;
res = await this.HttpClient.SendAsync(req, HttpCompletionOption.ResponseContentRead, CancellationToken.None).ConfigureAwait(false);
var bts = await res.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
var txt = Utilities.UTF8.GetString(bts, 0, bts.Length);
this._logger.LogTrace(LoggerEvents.RestRx, txt);
response.Headers = res.Headers.ToDictionary(xh => xh.Key, xh => string.Join("\n", xh.Value), StringComparer.OrdinalIgnoreCase);
response.Response = txt;
response.ResponseCode = (int)res.StatusCode;
}
catch (HttpRequestException httpex)
{
this._logger.LogError(LoggerEvents.RestError, httpex, "Request to {0} triggered an HttpException", request.Url.AbsoluteUri);
request.SetFaulted(httpex);
this.FailInitialRateLimitTest(request, ratelimitTcs);
return;
}
this.UpdateBucket(request, response, ratelimitTcs);
Exception ex = null;
+ Exception senex = null;
switch (response.ResponseCode)
{
case 400:
case 405:
ex = new BadRequestException(request, response);
+ senex = new Exception(ex.Message + "\nJson Response: " + (ex as BadRequestException).JsonMessage ?? "null", ex);
break;
case 401:
case 403:
ex = new UnauthorizedException(request, response);
break;
case 404:
ex = new NotFoundException(request, response);
break;
case 413:
ex = new RequestSizeException(request, response);
break;
case 429:
ex = new RateLimitException(request, response);
// check the limit info and requeue
this.Handle429(response, out var wait, out var global);
if (wait != null)
{
if (global)
{
bucket.IsGlobal = true;
this._logger.LogError(LoggerEvents.RatelimitHit, "Global ratelimit hit, cooling down for {uri}", request.Url.AbsoluteUri);
try
{
this._globalRateLimitEvent.Reset();
await wait.ConfigureAwait(false);
}
finally
{
// we don't want to wait here until all the blocked requests have been run, additionally Set can never throw an exception that could be suppressed here
_ = this._globalRateLimitEvent.SetAsync();
}
this.ExecuteRequestAsync(request, bucket, ratelimitTcs)
.LogTaskFault(this._logger, LogLevel.Error, LoggerEvents.RestError, "Error while retrying request");
}
else
{
if (this._discord is DiscordClient)
{
await (this._discord as DiscordClient)._rateLimitHit.InvokeAsync(this._discord as DiscordClient, new EventArgs.RateLimitExceptionEventArgs(this._discord.ServiceProvider)
{
Exception = ex as RateLimitException,
ApiEndpoint = request.Url.AbsoluteUri
});
}
this._logger.LogError(LoggerEvents.RatelimitHit, "Ratelimit hit, requeuing request to {url}", request.Url.AbsoluteUri);
await wait.ConfigureAwait(false);
this.ExecuteRequestAsync(request, bucket, ratelimitTcs)
.LogTaskFault(this._logger, LogLevel.Error, LoggerEvents.RestError, "Error while retrying request");
}
return;
}
break;
case 500:
case 502:
case 503:
case 504:
ex = new ServerErrorException(request, response);
+ senex = new Exception(ex.Message + "\nJson Response: " + (ex as ServerErrorException).JsonMessage ?? "null", ex);
break;
}
if (ex != null)
+ {
+ if (senex != null)
+ {
+ Dictionary debugInfo = new()
+ {
+ { "route", request.Route },
+ { "time", DateTimeOffset.UtcNow }
+ };
+ senex.AddSentryContext("Request", debugInfo);
+ SentrySdk.CaptureException(senex);
+ }
request.SetFaulted(ex);
+ }
else
request.SetCompleted(response);
}
catch (Exception ex)
{
this._logger.LogError(LoggerEvents.RestError, ex, "Request to {0} triggered an exception", request.Url.AbsoluteUri);
// if something went wrong and we couldn't get rate limits for the first request here, allow the next request to run
if (bucket != null && ratelimitTcs != null && bucket.LimitTesting != 0)
this.FailInitialRateLimitTest(request, ratelimitTcs);
if (!request.TrySetFaulted(ex))
throw;
}
finally
{
res?.Dispose();
// Get and decrement active requests in this bucket by 1.
_ = this._requestQueue.TryGetValue(bucket.BucketId, out var count);
this._requestQueue[bucket.BucketId] = Interlocked.Decrement(ref count);
// If it's 0 or less, we can remove the bucket from the active request queue,
// along with any of its past routes.
if (count <= 0)
{
foreach (var r in bucket.RouteHashes)
{
if (this._requestQueue.ContainsKey(r))
{
_ = this._requestQueue.TryRemove(r, out _);
}
}
}
}
}
///
/// Fails the initial rate limit test.
///
/// The request.
/// The ratelimit task completion source.
/// Whether to reset to initial values.
private void FailInitialRateLimitTest(BaseRestRequest request, TaskCompletionSource ratelimitTcs, bool resetToInitial = false)
{
if (ratelimitTcs == null && !resetToInitial)
return;
var bucket = request.RateLimitBucket;
bucket.LimitValid = false;
bucket.LimitTestFinished = null;
bucket.LimitTesting = 0;
//Reset to initial values.
if (resetToInitial)
{
this.UpdateHashCaches(request, bucket);
bucket.Maximum = 0;
bucket.RemainingInternal = 0;
return;
}
// no need to wait on all the potentially waiting tasks
_ = Task.Run(() => ratelimitTcs.TrySetResult(false));
}
///
/// Waits for the initial rate limit.
///
/// The bucket.
private async Task> WaitForInitialRateLimit(RateLimitBucket bucket)
{
while (!bucket.LimitValid)
{
if (bucket.LimitTesting == 0)
{
if (Interlocked.CompareExchange(ref bucket.LimitTesting, 1, 0) == 0)
{
// if we got here when the first request was just finishing, we must not create the waiter task as it would signal ExecuteRequestAsync to bypass rate limiting
if (bucket.LimitValid)
return null;
// allow exactly one request to go through without having rate limits available
var ratelimitsTcs = new TaskCompletionSource();
bucket.LimitTestFinished = ratelimitsTcs.Task;
return ratelimitsTcs;
}
}
// it can take a couple of cycles for the task to be allocated, so wait until it happens or we are no longer probing for the limits
Task waitTask = null;
while (bucket.LimitTesting != 0 && (waitTask = bucket.LimitTestFinished) == null)
await Task.Yield();
if (waitTask != null)
await waitTask.ConfigureAwait(false);
// if the request failed and the response did not have rate limit headers we have allow the next request and wait again, thus this is a loop here
}
return null;
}
///
/// Builds the request.
///
/// The request.
/// A http request message.
private HttpRequestMessage BuildRequest(BaseRestRequest request)
{
var req = new HttpRequestMessage(new HttpMethod(request.Method.ToString()), request.Url);
if (request.Headers != null && request.Headers.Any())
foreach (var kvp in request.Headers)
req.Headers.Add(kvp.Key, kvp.Value);
if (request is RestRequest nmprequest && !string.IsNullOrWhiteSpace(nmprequest.Payload))
{
this._logger.LogTrace(LoggerEvents.RestTx, nmprequest.Payload);
req.Content = new StringContent(nmprequest.Payload);
req.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
}
if (request is MultipartWebRequest mprequest)
{
this._logger.LogTrace(LoggerEvents.RestTx, "");
var boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x");
req.Headers.Add("Connection", "keep-alive");
req.Headers.Add("Keep-Alive", "600");
var content = new MultipartFormDataContent(boundary);
if (mprequest.Values != null && mprequest.Values.Any())
foreach (var kvp in mprequest.Values)
content.Add(new StringContent(kvp.Value), kvp.Key);
var fileId = mprequest.OverwriteFileIdStart ?? 0;
if (mprequest.Files != null && mprequest.Files.Any())
{
foreach (var f in mprequest.Files)
{
var name = $"files[{fileId.ToString(CultureInfo.InvariantCulture)}]";
content.Add(new StreamContent(f.Value), name, f.Key);
fileId++;
}
}
req.Content = content;
}
if (request is MultipartStickerWebRequest mpsrequest)
{
this._logger.LogTrace(LoggerEvents.RestTx, "");
var boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x");
req.Headers.Add("Connection", "keep-alive");
req.Headers.Add("Keep-Alive", "600");
var sc = new StreamContent(mpsrequest.File.Stream);
if (mpsrequest.File.ContentType != null)
sc.Headers.ContentType = new MediaTypeHeaderValue(mpsrequest.File.ContentType);
var fileName = mpsrequest.File.FileName;
if (mpsrequest.File.FileType != null)
fileName += '.' + mpsrequest.File.FileType;
var content = new MultipartFormDataContent(boundary)
{
{ new StringContent(mpsrequest.Name), "name" },
{ new StringContent(mpsrequest.Tags), "tags" },
{ new StringContent(mpsrequest.Description), "description" },
{ sc, "file", fileName }
};
req.Content = content;
}
return req;
}
///
/// Handles the HTTP 429 status.
///
/// The response.
/// The wait task.
/// If true, global.
private void Handle429(RestResponse response, out Task waitTask, out bool global)
{
waitTask = null;
global = false;
if (response.Headers == null)
return;
var hs = response.Headers;
// handle the wait
if (hs.TryGetValue("Retry-After", out var retryAfterRaw))
{
var retryAfter = TimeSpan.FromSeconds(int.Parse(retryAfterRaw, CultureInfo.InvariantCulture));
waitTask = Task.Delay(retryAfter);
}
// check if global b1nzy
if (hs.TryGetValue("X-RateLimit-Global", out var isGlobal) && isGlobal.ToLowerInvariant() == "true")
{
// global
global = true;
}
}
///
/// Updates the bucket.
///
/// The request.
/// The response.
/// The ratelimit task completion source.
private void UpdateBucket(BaseRestRequest request, RestResponse response, TaskCompletionSource ratelimitTcs)
{
var bucket = request.RateLimitBucket;
if (response.Headers == null)
{
if (response.ResponseCode != 429) // do not fail when ratelimit was or the next request will be scheduled hitting the rate limit again
this.FailInitialRateLimitTest(request, ratelimitTcs);
return;
}
var hs = response.Headers;
if (hs.TryGetValue("X-RateLimit-Scope", out var scope))
{
bucket.Scope = scope;
}
if (hs.TryGetValue("X-RateLimit-Global", out var isGlobal) && isGlobal.ToLowerInvariant() == "true")
{
if (response.ResponseCode != 429)
{
bucket.IsGlobal = true;
this.FailInitialRateLimitTest(request, ratelimitTcs);
}
return;
}
var r1 = hs.TryGetValue("X-RateLimit-Limit", out var usesMax);
var r2 = hs.TryGetValue("X-RateLimit-Remaining", out var usesLeft);
var r3 = hs.TryGetValue("X-RateLimit-Reset", out var reset);
var r4 = hs.TryGetValue("X-Ratelimit-Reset-After", out var resetAfter);
var r5 = hs.TryGetValue("X-Ratelimit-Bucket", out var hash);
if (!r1 || !r2 || !r3 || !r4)
{
//If the limits were determined before this request, make the bucket initial again.
if (response.ResponseCode != 429)
this.FailInitialRateLimitTest(request, ratelimitTcs, ratelimitTcs == null);
return;
}
var clientTime = DateTimeOffset.UtcNow;
var resetTime = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero).AddSeconds(double.Parse(reset, CultureInfo.InvariantCulture));
var serverTime = clientTime;
if (hs.TryGetValue("Date", out var rawDate))
serverTime = DateTimeOffset.Parse(rawDate, CultureInfo.InvariantCulture).ToUniversalTime();
var resetDelta = resetTime - serverTime;
//var difference = clientTime - serverTime;
//if (Math.Abs(difference.TotalSeconds) >= 1)
//// this.Logger.LogMessage(LogLevel.DebugBaseDiscordClient.RestEventId, $"Difference between machine and server time: {difference.TotalMilliseconds.ToString("#,##0.00", CultureInfo.InvariantCulture)}ms", DateTime.Now);
//else
// difference = TimeSpan.Zero;
if (request.RateLimitWaitOverride.HasValue)
resetDelta = TimeSpan.FromSeconds(request.RateLimitWaitOverride.Value);
var newReset = clientTime + resetDelta;
if (this._useResetAfter)
{
bucket.ResetAfter = TimeSpan.FromSeconds(double.Parse(resetAfter, CultureInfo.InvariantCulture));
newReset = clientTime + bucket.ResetAfter.Value + (request.RateLimitWaitOverride.HasValue
? resetDelta
: TimeSpan.Zero);
bucket.ResetAfterOffset = newReset;
}
else
bucket.Reset = newReset;
var maximum = int.Parse(usesMax, CultureInfo.InvariantCulture);
var remaining = int.Parse(usesLeft, CultureInfo.InvariantCulture);
if (ratelimitTcs != null)
{
// initial population of the ratelimit data
bucket.SetInitialValues(maximum, remaining, newReset);
_ = Task.Run(() => ratelimitTcs.TrySetResult(true));
}
else
{
// only update the bucket values if this request was for a newer interval than the one
// currently in the bucket, to avoid issues with concurrent requests in one bucket
// remaining is reset by TryResetLimit and not the response, just allow that to happen when it is time
if (bucket.NextReset == 0)
bucket.NextReset = newReset.UtcTicks;
}
this.UpdateHashCaches(request, bucket, hash);
}
///
/// Updates the hash caches.
///
/// The request.
/// The bucket.
/// The new hash.
private void UpdateHashCaches(BaseRestRequest request, RateLimitBucket bucket, string newHash = null)
{
var hashKey = RateLimitBucket.GenerateHashKey(request.Method, request.Route);
if (!this._routesToHashes.TryGetValue(hashKey, out var oldHash))
return;
// This is an unlimited bucket, which we don't need to keep track of.
if (newHash == null)
{
_ = this._routesToHashes.TryRemove(hashKey, out _);
_ = this._hashesToBuckets.TryRemove(bucket.BucketId, out _);
return;
}
// Only update the hash once, due to a bug on Discord's end.
// This will cause issues if the bucket hashes are dynamically changed from the API while running,
// in which case, Dispose will need to be called to clear the caches.
if (bucket.IsUnlimited && newHash != oldHash)
{
this._logger.LogDebug(LoggerEvents.RestHashMover, "Updating hash in {0}: \"{1}\" -> \"{2}\"", hashKey, oldHash, newHash);
var bucketId = RateLimitBucket.GenerateBucketId(newHash, bucket.GuildId, bucket.ChannelId, bucket.WebhookId);
_ = this._routesToHashes.AddOrUpdate(hashKey, newHash, (key, oldHash) =>
{
bucket.Hash = newHash;
var oldBucketId = RateLimitBucket.GenerateBucketId(oldHash, bucket.GuildId, bucket.ChannelId, bucket.WebhookId);
// Remove the old unlimited bucket.
_ = this._hashesToBuckets.TryRemove(oldBucketId, out _);
_ = this._hashesToBuckets.AddOrUpdate(bucketId, bucket, (key, oldBucket) => bucket);
return newHash;
});
}
return;
}
///
/// Cleans the buckets.
///
private async Task CleanupBucketsAsync()
{
while (!this._bucketCleanerTokenSource.IsCancellationRequested)
{
try
{
await Task.Delay(this._bucketCleanupDelay, this._bucketCleanerTokenSource.Token).ConfigureAwait(false);
}
catch { }
if (this._disposed)
return;
//Check and clean request queue first in case it wasn't removed properly during requests.
foreach (var key in this._requestQueue.Keys)
{
var bucket = this._hashesToBuckets.Values.FirstOrDefault(x => x.RouteHashes.Contains(key));
if (bucket == null || (bucket != null && bucket.LastAttemptAt.AddSeconds(5) < DateTimeOffset.UtcNow))
_ = this._requestQueue.TryRemove(key, out _);
}
var removedBuckets = 0;
StringBuilder bucketIdStrBuilder = default;
foreach (var kvp in this._hashesToBuckets)
{
bucketIdStrBuilder ??= new StringBuilder();
var key = kvp.Key;
var value = kvp.Value;
// Don't remove the bucket if it's currently being handled by the rest client, unless it's an unlimited bucket.
if (this._requestQueue.ContainsKey(value.BucketId) && !value.IsUnlimited)
continue;
var resetOffset = this._useResetAfter ? value.ResetAfterOffset : value.Reset;
// Don't remove the bucket if it's reset date is less than now + the additional wait time, unless it's an unlimited bucket.
if (!value.IsUnlimited && (resetOffset > DateTimeOffset.UtcNow || DateTimeOffset.UtcNow - resetOffset < this._bucketCleanupDelay))
continue;
_ = this._hashesToBuckets.TryRemove(key, out _);
removedBuckets++;
bucketIdStrBuilder.Append(value.BucketId + ", ");
}
if (removedBuckets > 0)
this._logger.LogDebug(LoggerEvents.RestCleaner, "Removed {0} unused bucket{1}: [{2}]", removedBuckets, removedBuckets > 1 ? "s" : string.Empty, bucketIdStrBuilder.ToString().TrimEnd(',', ' '));
if (this._hashesToBuckets.IsEmpty)
break;
}
if (!this._bucketCleanerTokenSource.IsCancellationRequested)
this._bucketCleanerTokenSource.Cancel();
this._cleanerRunning = false;
this._logger.LogDebug(LoggerEvents.RestCleaner, "Bucket cleaner task stopped.");
}
~RestClient()
{
this.Dispose();
}
///
/// Disposes the rest client.
///
public void Dispose()
{
if (this._disposed)
return;
this._disposed = true;
this._globalRateLimitEvent.Reset();
if (this._bucketCleanerTokenSource?.IsCancellationRequested == false)
{
this._bucketCleanerTokenSource?.Cancel();
this._logger.LogDebug(LoggerEvents.RestCleaner, "Bucket cleaner task stopped.");
}
try
{
this._cleanerTask?.Dispose();
this._bucketCleanerTokenSource?.Dispose();
this.HttpClient?.Dispose();
}
catch { }
this._routesToHashes.Clear();
this._hashesToBuckets.Clear();
this._requestQueue.Clear();
GC.SuppressFinalize(this);
}
}
diff --git a/DisCatSharp/Net/Serialization/DiscordJson.cs b/DisCatSharp/Net/Serialization/DiscordJson.cs
index d8c3f2d81..08ed91aed 100644
--- a/DisCatSharp/Net/Serialization/DiscordJson.cs
+++ b/DisCatSharp/Net/Serialization/DiscordJson.cs
@@ -1,106 +1,130 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2023 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
+using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Runtime.CompilerServices;
using System.Text;
using DisCatSharp.Entities;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
+using Sentry;
+
namespace DisCatSharp.Net.Serialization;
///
/// Represents discord json.
///
public static class DiscordJson
{
private static readonly JsonSerializer s_serializer = JsonSerializer.CreateDefault(new JsonSerializerSettings
{
ContractResolver = new OptionalJsonContractResolver()
});
/// Serializes the specified object to a JSON string.
/// The object to serialize.
/// A JSON string representation of the object.
public static string SerializeObject(object value)
=> SerializeObjectInternal(value, null, s_serializer);
public static T DeserializeObject(string json, BaseDiscordClient discord) where T : ObservableApiObject
=> DeserializeObjectInternal(json, discord);
/// Populates an object with the values from a JSON node.
/// The token to populate the object with.
/// The object to populate.
public static void PopulateObject(JToken value, object target)
{
using var reader = value.CreateReader();
s_serializer.Populate(reader, target);
}
///
/// Converts this token into an object, passing any properties through extra s if needed.
///
/// The token to convert
/// Type to convert to
/// The converted token
public static T ToDiscordObject(this JToken token)
=> token.ToObject(s_serializer);
///
/// Serializes the object.
///
/// The value.
/// The type.
/// The json serializer.
private static string SerializeObjectInternal(object value, Type type, JsonSerializer jsonSerializer)
{
var stringWriter = new StringWriter(new StringBuilder(256), CultureInfo.InvariantCulture);
using (var jsonTextWriter = new JsonTextWriter(stringWriter))
{
jsonTextWriter.Formatting = jsonSerializer.Formatting;
jsonSerializer.Serialize(jsonTextWriter, value, type);
}
return stringWriter.ToString();
}
private static T DeserializeObjectInternal(string json, BaseDiscordClient discord) where T : ObservableApiObject
{
var obj = JsonConvert.DeserializeObject(json);
obj.Discord = discord;
if (discord.Configuration.ReportMissingFields && obj.AdditionalProperties.Any())
{
- discord.Logger.LogInformation("Found missing properties in api response");
+ var sentryMessage = "Found missing properties in api response for " + obj.GetType().Name;
+ List sentryFields = new();
+ discord.Logger.LogInformation(sentryMessage);
foreach (var ap in obj.AdditionalProperties)
+ {
+ sentryFields.Add(ap.Key);
discord.Logger.LogInformation("Found field {field} on {object}", ap.Key, obj.GetType().Name);
+ }
+
+ if (discord.Configuration.EnableSentry)
+ {
+ var sentryJson = JsonConvert.SerializeObject(sentryFields);
+ sentryMessage += "\n\nNew fields: " + sentryJson;
+ SentryEvent sevnt = new()
+ {
+ Level = SentryLevel.Warning,
+ Logger = nameof(DiscordJson),
+ Message = sentryMessage
+ };
+ sevnt.SetExtra("Found Fields", sentryJson);
+ var sid = SentrySdk.CaptureEvent(sevnt);
+ discord.Logger.LogInformation("Reported to sentry with id {sid}", sid.ToString());
+ }
}
return obj;
}
}