diff --git a/DisCatSharp/Clients/DiscordClient.Dispatch.cs b/DisCatSharp/Clients/DiscordClient.Dispatch.cs index a15535f20..55c8429fd 100644 --- a/DisCatSharp/Clients/DiscordClient.Dispatch.cs +++ b/DisCatSharp/Clients/DiscordClient.Dispatch.cs @@ -1,3430 +1,3406 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 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_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 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(); await this.OnMessageReactionAddAsync((ulong)dat["user_id"], (ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"], mbr, dat["emoji"].ToObject()).ConfigureAwait(false); break; case "message_reaction_remove": await this.OnMessageReactionRemoveAsync((ulong)dat["user_id"], (ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"], dat["emoji"].ToObject()).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": 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; default: await this.OnUnknownEventAsync(payload).ConfigureAwait(false); this.Logger.LogWarning(LoggerEvents.WebSocketReceive, "Unknown event: {0}\npayload: {1}", payload.EventName, payload.Data); 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.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; 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 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); } #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; 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; 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; 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; 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; 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; 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; 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. internal async Task OnMessageReactionAddAsync(ulong userId, ulong messageId, ulong channelId, ulong? guildId, TransportMember mbr, DiscordEmoji emoji) { 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 }; 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. internal async Task OnMessageReactionRemoveAsync(ulong userId, ulong messageId, ulong channelId, ulong? guildId, DiscordEmoji emoji) { var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId); 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 }; 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); 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); } } - if (this.UserCache.TryGetValue(uid, out var usr)) - { - if (old != null) - { - old.InternalUser.Username = usr.Username; - old.InternalUser.Discriminator = usr.Discriminator; - old.InternalUser.AvatarHash = usr.AvatarHash; - } - - if (rawUser["username"] is object) - usr.Username = (string)rawUser["username"]; - if (rawUser["discriminator"] is object) - usr.Discriminator = (string)rawUser["discriminator"]; - if (rawUser["avatar"] is object) - usr.AvatarHash = (string)rawUser["avatar"]; - - presence.InternalUser.Username = usr.Username; - presence.InternalUser.Discriminator = usr.Discriminator; - presence.InternalUser.AvatarHash = usr.AvatarHash; - } - - var usrafter = usr ?? new DiscordUser(presence.InternalUser); var ea = new PresenceUpdateEventArgs(this.ServiceProvider) { Status = presence.Status, Activity = presence.Activity, - User = usr, + User = presence.User, PresenceBefore = old, - PresenceAfter = presence, - UserBefore = old != null ? new DiscordUser(old.InternalUser) : usrafter, - UserAfter = usrafter + 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); } 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 b54008221..97fef0683 100644 --- a/DisCatSharp/Clients/DiscordClient.WebSocket.cs +++ b/DisCatSharp/Clients/DiscordClient.WebSocket.cs @@ -1,592 +1,589 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 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.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; 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() { 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); } 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 TransportUser + InternalUser = new UserWithIdOnly() { - Id = this.CurrentUser.Id, - Username = this.CurrentUser.Username, - Discriminator = this.CurrentUser.Discriminator, - AvatarHash = this.CurrentUser.AvatarHash + 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"); } } 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 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 TransportUser { Id = this.CurrentUser.Id } + 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/Entities/User/DiscordPresence.cs b/DisCatSharp/Entities/User/DiscordPresence.cs index e31a8210d..8f04b82d5 100644 --- a/DisCatSharp/Entities/User/DiscordPresence.cs +++ b/DisCatSharp/Entities/User/DiscordPresence.cs @@ -1,150 +1,160 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 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.Collections.Generic; using DisCatSharp.Net.Abstractions; using Newtonsoft.Json; namespace DisCatSharp.Entities; /// /// Represents a user presence. /// public sealed class DiscordPresence { /// /// Gets the discord client. /// [JsonIgnore] internal DiscordClient Discord { get; set; } /// /// Gets the internal user. /// - [JsonProperty("user", NullValueHandling = NullValueHandling.Ignore)] - internal TransportUser InternalUser { get; set; } + [JsonProperty("user")] + internal UserWithIdOnly InternalUser { get; set; } /// /// Gets the user that owns this presence. /// [JsonIgnore] public DiscordUser User => this.Discord.GetCachedOrEmptyUserInternal(this.InternalUser.Id); /// /// Gets the user's current activity. /// [JsonIgnore] public DiscordActivity Activity { get; internal set; } /// /// Gets the raw activity. /// internal TransportActivity RawActivity { get; set; } /// /// Gets the user's current activities. /// [JsonIgnore] - public IReadOnlyList Activities => this.InternalActivities; + public IReadOnlyList Activities + => this.InternalActivities; [JsonIgnore] internal DiscordActivity[] InternalActivities; /// /// Gets the raw activities. /// [JsonProperty("activities", NullValueHandling = NullValueHandling.Ignore)] internal TransportActivity[] RawActivities { get; set; } /// /// Gets this user's status. /// [JsonProperty("status", NullValueHandling = NullValueHandling.Ignore)] public UserStatus Status { get; internal set; } /// /// Gets the guild id for which this presence was set. /// [JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)] internal ulong GuildId { get; set; } /// /// Gets the guild for which this presence was set. /// [JsonIgnore] public DiscordGuild Guild => this.GuildId != 0 ? this.Discord.GuildsInternal[this.GuildId] : null; /// /// Gets this user's platform-dependent status. /// [JsonProperty("client_status", NullValueHandling = NullValueHandling.Ignore)] public DiscordClientStatus ClientStatus { get; internal set; } /// /// Initializes a new instance of the class. /// internal DiscordPresence() { } /// /// Initializes a new instance of the class. /// /// The other. internal DiscordPresence(DiscordPresence other) { this.Discord = other.Discord; this.Activity = other.Activity; this.RawActivity = other.RawActivity; this.InternalActivities = (DiscordActivity[])other.InternalActivities?.Clone(); this.RawActivities = (TransportActivity[])other.RawActivities?.Clone(); this.Status = other.Status; - this.InternalUser = new TransportUser(other.InternalUser); + this.InternalUser = other.InternalUser; } } +/// +/// Represents a user with only its id. +/// +public sealed class UserWithIdOnly +{ + [JsonProperty("id")] + public ulong Id { get; internal set; } +} + /// /// Represents a client status. /// public sealed class DiscordClientStatus { /// /// Gets the user's status set for an active desktop (Windows, Linux, Mac) application session. /// [JsonProperty("desktop", NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Populate)] public Optional Desktop { get; internal set; } = UserStatus.Offline; /// /// Gets the user's status set for an active mobile (iOS, Android) application session. /// [JsonProperty("mobile", NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Populate)] public Optional Mobile { get; internal set; } = UserStatus.Offline; /// /// Gets the user's status set for an active web (browser, bot account) application session. /// [JsonProperty("web", NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Populate)] public Optional Web { get; internal set; } = UserStatus.Offline; } diff --git a/DisCatSharp/EventArgs/User/PresenceUpdateEventArgs.cs b/DisCatSharp/EventArgs/User/PresenceUpdateEventArgs.cs index 475763a90..5c295710d 100644 --- a/DisCatSharp/EventArgs/User/PresenceUpdateEventArgs.cs +++ b/DisCatSharp/EventArgs/User/PresenceUpdateEventArgs.cs @@ -1,73 +1,63 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 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 DisCatSharp.Entities; namespace DisCatSharp.EventArgs; /// /// Represents arguments for event. /// public class PresenceUpdateEventArgs : DiscordEventArgs { /// /// Gets the user whose presence was updated. /// public DiscordUser User { get; internal set; } /// /// Gets the user's new game. /// public DiscordActivity Activity { get; internal set; } /// /// Gets the user's status. /// public UserStatus Status { get; internal set; } /// /// Gets the user's old presence. /// public DiscordPresence PresenceBefore { get; internal set; } /// /// Gets the user's new presence. /// public DiscordPresence PresenceAfter { get; internal set; } - /// - /// Gets the user prior to presence update. - /// - public DiscordUser UserBefore { get; internal set; } - - /// - /// Gets the user after the presence update. - /// - public DiscordUser UserAfter { get; internal set; } - /// /// Initializes a new instance of the class. /// internal PresenceUpdateEventArgs(IServiceProvider provider) : base(provider) { } } diff --git a/DisCatSharp/Net/Abstractions/Transport/TransportUser.cs b/DisCatSharp/Net/Abstractions/Transport/TransportUser.cs index d1530c96b..14eccfc90 100644 --- a/DisCatSharp/Net/Abstractions/Transport/TransportUser.cs +++ b/DisCatSharp/Net/Abstractions/Transport/TransportUser.cs @@ -1,177 +1,178 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 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 DisCatSharp.Enums; using Newtonsoft.Json; namespace DisCatSharp.Net.Abstractions; /// /// Represents a transport user. /// internal class TransportUser { /// /// Gets the id. /// [JsonProperty("id")] public ulong Id { get; internal set; } /// /// Gets the username. /// [JsonProperty("username", NullValueHandling = NullValueHandling.Ignore)] public string Username { get; internal set; } /// /// Gets or sets the discriminator. /// [JsonProperty("discriminator", NullValueHandling = NullValueHandling.Ignore)] internal string Discriminator { get; set; } /// /// Gets the username with discriminator. /// + [JsonIgnore] internal string UsernameWithDiscriminator => $"{this.Username}#{this.Discriminator}"; /// /// Gets the avatar hash. /// [JsonProperty("avatar", NullValueHandling = NullValueHandling.Ignore)] public string AvatarHash { get; internal set; } /// /// Gets the avatar decoration hash. /// [JsonProperty("avatar_decoration", NullValueHandling = NullValueHandling.Ignore)] public string AvatarDecorationHash { get; internal set; } /// /// Gets the banner hash. /// [JsonProperty("banner", NullValueHandling = NullValueHandling.Ignore)] public string BannerHash { get; internal set; } /// /// Gets the banner color. /// - [JsonProperty("accent_color")] + [JsonProperty("accent_color", NullValueHandling = NullValueHandling.Ignore)] public int? BannerColor { get; internal set; } /// /// Gets the users theme colors. /// - [JsonProperty("theme_colors")] + [JsonProperty("theme_colors", NullValueHandling = NullValueHandling.Ignore)] public int[]? ThemeColors { get; internal set; } /// /// Gets a value indicating whether is bot. /// [JsonProperty("bot", NullValueHandling = NullValueHandling.Ignore)] public bool IsBot { get; internal set; } /// /// Gets a value indicating whether mfa enabled. /// [JsonProperty("mfa_enabled", NullValueHandling = NullValueHandling.Ignore)] public bool? MfaEnabled { get; internal set; } /// /// Gets a value indicating whether verified. /// [JsonProperty("verified", NullValueHandling = NullValueHandling.Ignore)] public bool? Verified { get; internal set; } /// /// Gets the email. /// [JsonProperty("email", NullValueHandling = NullValueHandling.Ignore)] public string Email { get; internal set; } /// /// Gets the premium type. /// [JsonProperty("premium_type", NullValueHandling = NullValueHandling.Ignore)] public PremiumType? PremiumType { get; internal set; } /// /// Gets the locale. /// [JsonProperty("locale", NullValueHandling = NullValueHandling.Ignore)] public string Locale { get; internal set; } /// /// Gets the OAuth flags. /// [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] public UserFlags? OAuthFlags { get; internal set; } /// /// Gets the flags. /// [JsonProperty("public_flags", NullValueHandling = NullValueHandling.Ignore)] public UserFlags? Flags { get; internal set; } /// /// Gets the users bio. /// This is not available to bots tho. /// [JsonProperty("bio", NullValueHandling = NullValueHandling.Ignore)] public string Bio { get; internal set; } /// /// Gets the users pronouns. /// [JsonProperty("pronouns", NullValueHandling = NullValueHandling.Ignore)] public string Pronouns { get; internal set; } /// /// Initializes a new instance of the class. /// internal TransportUser() { } /// /// Initializes a new instance of the class from an existing . /// /// The other transport user. internal TransportUser(TransportUser other) { this.Id = other.Id; this.Username = other.Username; this.Discriminator = other.Discriminator; this.AvatarHash = other.AvatarHash; this.BannerHash = other.BannerHash; this.BannerColor = other.BannerColor; this.IsBot = other.IsBot; this.MfaEnabled = other.MfaEnabled; this.Verified = other.Verified; this.Email = other.Email; this.PremiumType = other.PremiumType; this.Locale = other.Locale; this.Flags = other.Flags; this.OAuthFlags = other.OAuthFlags; this.Bio = other.Bio; this.Pronouns = other.Pronouns; } } diff --git a/DisCatSharp/Net/Rest/DiscordApiClient.cs b/DisCatSharp/Net/Rest/DiscordApiClient.cs index 0175f94d3..9187780b6 100644 --- a/DisCatSharp/Net/Rest/DiscordApiClient.cs +++ b/DisCatSharp/Net/Rest/DiscordApiClient.cs @@ -1,5581 +1,5581 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 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.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Net; using System.Text; using System.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.Enums; using DisCatSharp.Net.Abstractions; using DisCatSharp.Net.Serialization; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace DisCatSharp.Net; /// /// Represents a discord api client. /// public sealed class DiscordApiClient { /// /// The audit log reason header name. /// private const string REASON_HEADER_NAME = "X-Audit-Log-Reason"; /// /// Gets the discord client. /// internal BaseDiscordClient Discord { get; } /// /// Gets the rest client. /// internal RestClient Rest { get; } /// /// Initializes a new instance of the class. /// /// The client. internal DiscordApiClient(BaseDiscordClient client) { this.Discord = client; this.Rest = new RestClient(client); } /// /// Initializes a new instance of the class. /// /// The proxy. /// The timeout. /// If true, use relative rate limit. /// The logger. internal DiscordApiClient(IWebProxy proxy, TimeSpan timeout, bool useRelativeRateLimit, ILogger logger) // This is for meta-clients, such as the webhook client { this.Rest = new RestClient(proxy, timeout, useRelativeRateLimit, logger); } /// /// Builds the query string. /// /// The values. /// Whether this query will be transmitted via POST. private static string BuildQueryString(IDictionary values, bool post = false) { if (values == null || values.Count == 0) return string.Empty; var valsCollection = values.Select(xkvp => $"{WebUtility.UrlEncode(xkvp.Key)}={WebUtility.UrlEncode(xkvp.Value)}"); var vals = string.Join("&", valsCollection); return !post ? $"?{vals}" : vals; } /// /// Prepares the message. /// /// The msg_raw. /// A DiscordMessage. private DiscordMessage PrepareMessage(JToken msgRaw) { var author = msgRaw["author"].ToObject(); var ret = msgRaw.ToDiscordObject(); ret.Discord = this.Discord; this.PopulateMessage(author, ret); var referencedMsg = msgRaw["referenced_message"]; if (ret.MessageType == MessageType.Reply && !string.IsNullOrWhiteSpace(referencedMsg?.ToString())) { author = referencedMsg["author"].ToObject(); ret.ReferencedMessage.Discord = this.Discord; this.PopulateMessage(author, ret.ReferencedMessage); } if (ret.Channel != null) return ret; var channel = !ret.GuildId.HasValue ? new DiscordDmChannel { Id = ret.ChannelId, Discord = this.Discord, Type = ChannelType.Private } : new DiscordChannel { Id = ret.ChannelId, GuildId = ret.GuildId, Discord = this.Discord }; ret.Channel = channel; return ret; } /// /// Populates the message. /// /// The author. /// The message. private void PopulateMessage(TransportUser author, DiscordMessage ret) { var guild = ret.Channel?.Guild; //If this is a webhook, it shouldn't be in the user cache. if (author.IsBot && int.Parse(author.Discriminator) == 0) { ret.Author = new DiscordUser(author) { Discord = this.Discord }; } else { if (!this.Discord.UserCache.TryGetValue(author.Id, out var usr)) { this.Discord.UserCache[author.Id] = usr = new DiscordUser(author) { Discord = this.Discord }; } if (guild != null) { if (!guild.Members.TryGetValue(author.Id, out var mbr)) mbr = new DiscordMember(usr) { Discord = this.Discord, GuildId = guild.Id }; ret.Author = mbr; } else { ret.Author = usr; } } ret.PopulateMentions(); ret.ReactionsInternal ??= new List(); foreach (var xr in ret.ReactionsInternal) xr.Emoji.Discord = this.Discord; } /// /// Executes a rest request. /// /// The client. /// The bucket. /// The url. /// The method. /// The route. /// The headers. /// The payload. /// The ratelimit wait override. internal Task DoRequestAsync(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, string route, IReadOnlyDictionary headers = null, string payload = null, double? ratelimitWaitOverride = null) { var req = new RestRequest(client, bucket, url, method, route, headers, payload, ratelimitWaitOverride); if (this.Discord != null) this.Rest.ExecuteRequestAsync(req).LogTaskFault(this.Discord.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while executing request"); else _ = this.Rest.ExecuteRequestAsync(req); return req.WaitForCompletionAsync(); } /// /// Executes a multipart rest request for stickers. /// /// The client. /// The bucket. /// The url. /// The method. /// The route. /// The headers. /// The file. /// The sticker name. /// The sticker tag. /// The sticker description. /// The ratelimit wait override. private Task DoStickerMultipartAsync(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, string route, IReadOnlyDictionary headers = null, DiscordMessageFile file = null, string name = "", string tags = "", string description = "", double? ratelimitWaitOverride = null) { var req = new MultipartStickerWebRequest(client, bucket, url, method, route, headers, file, name, tags, description, ratelimitWaitOverride); if (this.Discord != null) this.Rest.ExecuteRequestAsync(req).LogTaskFault(this.Discord.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while executing request"); else _ = this.Rest.ExecuteRequestAsync(req); return req.WaitForCompletionAsync(); } /// /// Executes a multipart request. /// /// The client. /// The bucket. /// The url. /// The method. /// The route. /// The headers. /// The values. /// The files. /// The ratelimit wait override. private Task DoMultipartAsync(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, string route, IReadOnlyDictionary headers = null, IReadOnlyDictionary values = null, IReadOnlyCollection files = null, double? ratelimitWaitOverride = null) { var req = new MultipartWebRequest(client, bucket, url, method, route, headers, values, files, ratelimitWaitOverride); if (this.Discord != null) this.Rest.ExecuteRequestAsync(req).LogTaskFault(this.Discord.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while executing request"); else _ = this.Rest.ExecuteRequestAsync(req); return req.WaitForCompletionAsync(); } #region Guild /// /// Searches the members async. /// /// The guild_id. /// The name. /// The limit. internal async Task> SearchMembersAsync(ulong guildId, string name, int? limit) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}{Endpoints.SEARCH}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId }, out var path); var querydict = new Dictionary { ["query"] = name, ["limit"] = limit.ToString() }; var url = Utilities.GetApiUriFor(path, BuildQueryString(querydict), this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var json = JArray.Parse(res.Response); var tms = json.ToObject>(); var mbrs = new List(); foreach (var xtm in tms) { var usr = new DiscordUser(xtm.User) { Discord = this.Discord }; this.Discord.UserCache.AddOrUpdate(xtm.User.Id, usr, (id, old) => { old.Username = usr.Username; old.Discord = usr.Discord; old.AvatarHash = usr.AvatarHash; return old; }); mbrs.Add(new DiscordMember(xtm) { Discord = this.Discord, GuildId = guildId }); } return mbrs; } /// /// Gets the guild ban async. /// /// The guild_id. /// The user_id. internal async Task GetGuildBanAsync(ulong guildId, ulong userId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.BANS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId, user_id = userId}, out var path); var uri = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, uri, RestRequestMethod.GET, route).ConfigureAwait(false); var json = JObject.Parse(res.Response); var ban = json.ToObject(); return ban; } /// /// Creates the guild async. /// /// The name. /// The region_id. /// The iconb64. /// The verification_level. /// The default_message_notifications. /// The system_channel_flags. internal async Task CreateGuildAsync(string name, string regionId, Optional iconb64, VerificationLevel? verificationLevel, DefaultMessageNotifications? defaultMessageNotifications, SystemChannelFlags? systemChannelFlags) { var pld = new RestGuildCreatePayload { Name = name, RegionId = regionId, DefaultMessageNotifications = defaultMessageNotifications, VerificationLevel = verificationLevel, IconBase64 = iconb64, SystemChannelFlags = systemChannelFlags }; var route = $"{Endpoints.GUILDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var json = JObject.Parse(res.Response); var rawMembers = (JArray)json["members"]; var guild = json.ToDiscordObject(); if (this.Discord is DiscordClient dc) await dc.OnGuildCreateEventAsync(guild, rawMembers, null).ConfigureAwait(false); return guild; } /// /// Creates the guild from template async. /// /// The template_code. /// The name. /// The iconb64. internal async Task CreateGuildFromTemplateAsync(string templateCode, string name, Optional iconb64) { var pld = new RestGuildCreateFromTemplatePayload { Name = name, IconBase64 = iconb64 }; var route = $"{Endpoints.GUILDS}{Endpoints.TEMPLATES}/:template_code"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {template_code = templateCode }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var json = JObject.Parse(res.Response); var rawMembers = (JArray)json["members"]; var guild = json.ToDiscordObject(); if (this.Discord is DiscordClient dc) await dc.OnGuildCreateEventAsync(guild, rawMembers, null).ConfigureAwait(false); return guild; } /// /// Deletes the guild async. /// /// The guild_id. internal async Task DeleteGuildAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route).ConfigureAwait(false); if (this.Discord is DiscordClient dc) { var gld = dc.GuildsInternal[guildId]; await dc.OnGuildDeleteEventAsync(gld).ConfigureAwait(false); } } /// /// Modifies the guild. /// /// The guild id. /// The name. /// The verification level. /// The default message notifications. /// The mfa level. /// The explicit content filter. /// The afk channel id. /// The afk timeout. /// The iconb64. /// The owner id. /// The splashb64. /// The system channel id. /// The system channel flags. /// The public updates channel id. /// The rules channel id. /// The description. /// The banner base64. /// The discovery base64. /// The preferred locale. /// Whether the premium progress bar should be enabled. /// The reason. internal async Task ModifyGuildAsync(ulong guildId, Optional name, Optional verificationLevel, Optional defaultMessageNotifications, Optional mfaLevel, Optional explicitContentFilter, Optional afkChannelId, Optional afkTimeout, Optional iconb64, Optional ownerId, Optional splashb64, Optional systemChannelId, Optional systemChannelFlags, Optional publicUpdatesChannelId, Optional rulesChannelId, Optional description, Optional bannerb64, Optional discoverySplashb64, Optional preferredLocale, Optional premiumProgressBarEnabled, string reason) { var pld = new RestGuildModifyPayload { Name = name, VerificationLevel = verificationLevel, DefaultMessageNotifications = defaultMessageNotifications, MfaLevel = mfaLevel, ExplicitContentFilter = explicitContentFilter, AfkChannelId = afkChannelId, AfkTimeout = afkTimeout, IconBase64 = iconb64, SplashBase64 = splashb64, BannerBase64 = bannerb64, DiscoverySplashBase64 = discoverySplashb64, OwnerId = ownerId, SystemChannelId = systemChannelId, SystemChannelFlags = systemChannelFlags, RulesChannelId = rulesChannelId, PublicUpdatesChannelId = publicUpdatesChannelId, PreferredLocale = preferredLocale, Description = description, PremiumProgressBarEnabled = premiumProgressBarEnabled }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var json = JObject.Parse(res.Response); var rawMembers = (JArray)json["members"]; var guild = json.ToDiscordObject(); foreach (var r in guild.RolesInternal.Values) r.GuildId = guild.Id; if (this.Discord is DiscordClient dc) await dc.OnGuildUpdateEventAsync(guild, rawMembers).ConfigureAwait(false); return guild; } /// /// Modifies the guild community settings. /// /// The guild id. /// The guild features. /// The rules channel id. /// The public updates channel id. /// The preferred locale. /// The description. /// The default message notifications. /// The explicit content filter. /// The verification level. /// The reason. internal async Task ModifyGuildCommunitySettingsAsync(ulong guildId, List features, Optional rulesChannelId, Optional publicUpdatesChannelId, string preferredLocale, string description, DefaultMessageNotifications defaultMessageNotifications, ExplicitContentFilter explicitContentFilter, VerificationLevel verificationLevel, string reason) { var pld = new RestGuildCommunityModifyPayload { VerificationLevel = verificationLevel, DefaultMessageNotifications = defaultMessageNotifications, ExplicitContentFilter = explicitContentFilter, RulesChannelId = rulesChannelId, PublicUpdatesChannelId = publicUpdatesChannelId, PreferredLocale = preferredLocale, Description = Optional.FromNullable(description), Features = features }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var json = JObject.Parse(res.Response); var rawMembers = (JArray)json["members"]; var guild = json.ToDiscordObject(); foreach (var r in guild.RolesInternal.Values) r.GuildId = guild.Id; if (this.Discord is DiscordClient dc) await dc.OnGuildUpdateEventAsync(guild, rawMembers).ConfigureAwait(false); return guild; } /// /// Modifies the guild features. /// /// The guild id. /// The guild features. /// The reason. /// internal async Task ModifyGuildFeaturesAsync(ulong guildId, List features, string reason) { var pld = new RestGuildFeatureModifyPayload { Features = features }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var json = JObject.Parse(res.Response); var rawMembers = (JArray)json["members"]; var guild = json.ToDiscordObject(); foreach (var r in guild.RolesInternal.Values) r.GuildId = guild.Id; if (this.Discord is DiscordClient dc) await dc.OnGuildUpdateEventAsync(guild, rawMembers).ConfigureAwait(false); return guild; } /// /// Enables the guilds mfa requirement. /// /// The guild id. /// The reason. internal async Task EnableGuildMfaAsync(ulong guildId, string reason) { var pld = new RestGuildMfaLevelModifyPayload { Level = MfaLevel.Enabled }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MFA}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); } /// /// Disables the guilds mfa requirement. /// /// The guild id. /// The reason. internal async Task DisableGuildMfaAsync(ulong guildId, string reason) { var pld = new RestGuildMfaLevelModifyPayload { Level = MfaLevel.Disabled }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MFA}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); } /// /// Implements https://discord.com/developers/docs/resources/guild#get-guild-bans. /// internal async Task> GetGuildBansAsync(ulong guildId, int? limit, ulong? before, ulong? after) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.BANS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId }, out var path); var urlParams = new Dictionary(); if (limit != null) urlParams["limit"] = limit.Value.ToString(CultureInfo.InvariantCulture); if (before != null) urlParams["before"] = before.Value.ToString(CultureInfo.InvariantCulture); if (after != null) urlParams["after"] = after.Value.ToString(CultureInfo.InvariantCulture); var url = Utilities.GetApiUriFor(path, BuildQueryString(urlParams), this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var bansRaw = JsonConvert.DeserializeObject>(res.Response).Select(xb => { if (!this.Discord.TryGetCachedUserInternal(xb.RawUser.Id, out var usr)) { usr = new DiscordUser(xb.RawUser) { Discord = this.Discord }; usr = this.Discord.UserCache.AddOrUpdate(usr.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); } xb.User = usr; return xb; }); var bans = new ReadOnlyCollection(new List(bansRaw)); return bans; } /// /// Creates the guild ban async. /// /// The guild_id. /// The user_id. /// The delete_message_days. /// The reason. internal Task CreateGuildBanAsync(ulong guildId, ulong userId, int deleteMessageDays, string reason) { if (deleteMessageDays < 0 || deleteMessageDays > 7) throw new ArgumentException("Delete message days must be a number between 0 and 7.", nameof(deleteMessageDays)); var urlParams = new Dictionary { ["delete_message_days"] = deleteMessageDays.ToString(CultureInfo.InvariantCulture) }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.BANS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new {guild_id = guildId, user_id = userId }, out var path); var url = Utilities.GetApiUriFor(path, BuildQueryString(urlParams), this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, headers); } /// /// Removes the guild ban async. /// /// The guild_id. /// The user_id. /// The reason. internal Task RemoveGuildBanAsync(ulong guildId, ulong userId, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.BANS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {guild_id = guildId, user_id = userId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Leaves the guild async. /// /// The guild_id. internal Task LeaveGuildAsync(ulong guildId) { var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.GUILDS}/:guild_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Adds the guild member async. /// /// The guild_id. /// The user_id. /// The access_token. /// The nick. /// The roles. /// If true, muted. /// If true, deafened. internal async Task AddGuildMemberAsync(ulong guildId, ulong userId, string accessToken, string nick, IEnumerable roles, bool muted, bool deafened) { var pld = new RestGuildMemberAddPayload { AccessToken = accessToken, Nickname = nick ?? "", Roles = roles ?? new List(), Deaf = deafened, Mute = muted }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new {guild_id = guildId, user_id = userId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var tm = JsonConvert.DeserializeObject(res.Response); return new DiscordMember(tm) { Discord = this.Discord, GuildId = guildId }; } /// /// Lists the guild members async. /// /// The guild_id. /// The limit. /// The after. internal async Task> ListGuildMembersAsync(ulong guildId, int? limit, ulong? after) { var urlParams = new Dictionary(); if (limit != null && limit > 0) urlParams["limit"] = limit.Value.ToString(CultureInfo.InvariantCulture); if (after != null) urlParams["after"] = after.Value.ToString(CultureInfo.InvariantCulture); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, urlParams.Any() ? BuildQueryString(urlParams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var membersRaw = JsonConvert.DeserializeObject>(res.Response); return new ReadOnlyCollection(membersRaw); } /// /// Adds the guild member role async. /// /// The guild_id. /// The user_id. /// The role_id. /// The reason. internal Task AddGuildMemberRoleAsync(ulong guildId, ulong userId, ulong roleId, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id{Endpoints.ROLES}/:role_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new {guild_id = guildId, user_id = userId, role_id = roleId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, headers); } /// /// Removes the guild member role async. /// /// The guild_id. /// The user_id. /// The role_id. /// The reason. internal Task RemoveGuildMemberRoleAsync(ulong guildId, ulong userId, ulong roleId, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id{Endpoints.ROLES}/:role_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {guild_id = guildId, user_id = userId, role_id = roleId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Modifies the guild channel position async. /// /// The guild_id. /// The pld. /// The reason. internal Task ModifyGuildChannelPositionAsync(ulong guildId, IEnumerable pld, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CHANNELS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Modifies the guild channel parent async. /// /// The guild_id. /// The pld. /// The reason. internal Task ModifyGuildChannelParentAsync(ulong guildId, IEnumerable pld, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CHANNELS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Detaches the guild channel parent async. /// /// The guild_id. /// The pld. /// The reason. internal Task DetachGuildChannelParentAsync(ulong guildId, IEnumerable pld, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CHANNELS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Modifies the guild role position async. /// /// The guild_id. /// The pld. /// The reason. internal Task ModifyGuildRolePositionAsync(ulong guildId, IEnumerable pld, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.ROLES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Gets the audit logs async. /// /// The guild_id. /// The limit. /// The after. /// The before. /// The responsible. /// The action_type. internal async Task GetAuditLogsAsync(ulong guildId, int limit, ulong? after, ulong? before, ulong? responsible, int? actionType) { var urlParams = new Dictionary { ["limit"] = limit.ToString(CultureInfo.InvariantCulture) }; if (after != null) urlParams["after"] = after?.ToString(CultureInfo.InvariantCulture); if (before != null) urlParams["before"] = before?.ToString(CultureInfo.InvariantCulture); if (responsible != null) urlParams["user_id"] = responsible?.ToString(CultureInfo.InvariantCulture); if (actionType != null) urlParams["action_type"] = actionType?.ToString(CultureInfo.InvariantCulture); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.AUDIT_LOGS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, urlParams.Any() ? BuildQueryString(urlParams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var auditLogDataRaw = JsonConvert.DeserializeObject(res.Response); return auditLogDataRaw; } /// /// Gets the guild vanity url async. /// /// The guild_id. internal async Task GetGuildVanityUrlAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.VANITY_URL}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var invite = JsonConvert.DeserializeObject(res.Response); return invite; } /// /// Gets the guild widget async. /// /// The guild_id. internal async Task GetGuildWidgetAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WIDGET_JSON}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var json = JObject.Parse(res.Response); var rawChannels = (JArray)json["channels"]; var ret = json.ToDiscordObject(); ret.Discord = this.Discord; ret.Guild = (this.Discord.Guilds.ContainsKey(guildId)) ? this.Discord.Guilds[guildId] : null; ret.Channels = ret.Guild == null ? rawChannels.Select(r => new DiscordChannel { Id = (ulong)r["id"], Name = r["name"].ToString(), Position = (int)r["position"] }).ToList() : rawChannels.Select(r => { var c = ret.Guild.GetChannel((ulong)r["id"]); c.Position = (int)r["position"]; return c; }).ToList(); return ret; } /// /// Gets the guild widget settings async. /// /// The guild_id. internal async Task GetGuildWidgetSettingsAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WIDGET}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Guild = this.Discord.Guilds[guildId]; return ret; } /// /// Modifies the guild widget settings async. /// /// The guild_id. /// If true, is enabled. /// The channel id. /// The reason. internal async Task ModifyGuildWidgetSettingsAsync(ulong guildId, bool? isEnabled, ulong? channelId, string reason) { var pld = new RestGuildWidgetSettingsPayload { Enabled = isEnabled, ChannelId = channelId }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WIDGET}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Guild = this.Discord.Guilds[guildId]; return ret; } /// /// Gets the guild templates async. /// /// The guild_id. internal async Task> GetGuildTemplatesAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.TEMPLATES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var templatesRaw = JsonConvert.DeserializeObject>(res.Response); return new ReadOnlyCollection(new List(templatesRaw)); } /// /// Creates the guild template async. /// /// The guild_id. /// The name. /// The description. internal async Task CreateGuildTemplateAsync(ulong guildId, string name, string description) { var pld = new RestGuildTemplateCreateOrModifyPayload { Name = name, Description = description }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.TEMPLATES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); return ret; } /// /// Syncs the guild template async. /// /// The guild_id. /// The template_code. internal async Task SyncGuildTemplateAsync(ulong guildId, string templateCode) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.TEMPLATES}/:template_code"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new {guild_id = guildId, template_code = templateCode }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route).ConfigureAwait(false); var templateRaw = JsonConvert.DeserializeObject(res.Response); return templateRaw; } /// /// Modifies the guild template async. /// /// The guild_id. /// The template_code. /// The name. /// The description. internal async Task ModifyGuildTemplateAsync(ulong guildId, string templateCode, string name, string description) { var pld = new RestGuildTemplateCreateOrModifyPayload { Name = name, Description = description }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.TEMPLATES}/:template_code"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {guild_id = guildId, template_code = templateCode }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var templateRaw = JsonConvert.DeserializeObject(res.Response); return templateRaw; } /// /// Deletes the guild template async. /// /// The guild_id. /// The template_code. internal async Task DeleteGuildTemplateAsync(ulong guildId, string templateCode) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.TEMPLATES}/:template_code"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {guild_id = guildId, template_code = templateCode }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route).ConfigureAwait(false); var templateRaw = JsonConvert.DeserializeObject(res.Response); return templateRaw; } /// /// Gets the guild membership screening form async. /// /// The guild_id. internal async Task GetGuildMembershipScreeningFormAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBER_VERIFICATION}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var screeningRaw = JsonConvert.DeserializeObject(res.Response); return screeningRaw; } /// /// Modifies the guild membership screening form async. /// /// The guild_id. /// The enabled. /// The fields. /// The description. internal async Task ModifyGuildMembershipScreeningFormAsync(ulong guildId, Optional enabled, Optional fields, Optional description) { var pld = new RestGuildMembershipScreeningFormModifyPayload { Enabled = enabled, Description = description, Fields = fields }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBER_VERIFICATION}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var screeningRaw = JsonConvert.DeserializeObject(res.Response); return screeningRaw; } /// /// Gets the guild welcome screen async. /// /// The guild_id. internal async Task GetGuildWelcomeScreenAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WELCOME_SCREEN}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var ret = JsonConvert.DeserializeObject(res.Response); return ret; } /// /// Modifies the guild welcome screen async. /// /// The guild_id. /// The enabled. /// The welcome channels. /// The description. internal async Task ModifyGuildWelcomeScreenAsync(ulong guildId, Optional enabled, Optional> welcomeChannels, Optional description) { var pld = new RestGuildWelcomeScreenModifyPayload { Enabled = enabled, WelcomeChannels = welcomeChannels, Description = description }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WELCOME_SCREEN}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)); var ret = JsonConvert.DeserializeObject(res.Response); return ret; } /// /// Updates the current user voice state async. /// /// The guild_id. /// The channel id. /// If true, suppress. /// The request to speak timestamp. internal async Task UpdateCurrentUserVoiceStateAsync(ulong guildId, ulong channelId, bool? suppress, DateTimeOffset? requestToSpeakTimestamp) { var pld = new RestGuildUpdateCurrentUserVoiceStatePayload { ChannelId = channelId, Suppress = suppress, RequestToSpeakTimestamp = requestToSpeakTimestamp }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.VOICE_STATES}/@me"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)); } /// /// Updates the user voice state async. /// /// The guild_id. /// The user_id. /// The channel id. /// If true, suppress. internal async Task UpdateUserVoiceStateAsync(ulong guildId, ulong userId, ulong channelId, bool? suppress) { var pld = new RestGuildUpdateUserVoiceStatePayload { ChannelId = channelId, Suppress = suppress }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.VOICE_STATES}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {guild_id = guildId, user_id = userId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)); } #endregion #region Guild Scheduled Events /// /// Creates a scheduled event. /// internal async Task CreateGuildScheduledEventAsync(ulong guildId, ulong? channelId, DiscordScheduledEventEntityMetadata metadata, string name, DateTimeOffset scheduledStartTime, DateTimeOffset? scheduledEndTime, string description, ScheduledEventEntityType type, Optional coverb64, string reason = null) { var pld = new RestGuildScheduledEventCreatePayload { ChannelId = channelId, EntityMetadata = metadata, Name = name, ScheduledStartTime = scheduledStartTime, ScheduledEndTime = scheduledEndTime, Description = description, EntityType = type, CoverBase64 = coverb64 }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)); var scheduledEvent = JsonConvert.DeserializeObject(res.Response); var guild = this.Discord.Guilds[guildId]; scheduledEvent.Discord = this.Discord; if (scheduledEvent.Creator != null) scheduledEvent.Creator.Discord = this.Discord; if (this.Discord is DiscordClient dc) await dc.OnGuildScheduledEventCreateEventAsync(scheduledEvent, guild); return scheduledEvent; } /// /// Modifies a scheduled event. /// internal async Task ModifyGuildScheduledEventAsync(ulong guildId, ulong scheduledEventId, Optional channelId, Optional metadata, Optional name, Optional scheduledStartTime, Optional scheduledEndTime, Optional description, Optional type, Optional status, Optional coverb64, string reason = null) { var pld = new RestGuildScheduledEventModifyPayload { ChannelId = channelId, EntityMetadata = metadata, Name = name, ScheduledStartTime = scheduledStartTime, ScheduledEndTime = scheduledEndTime, Description = description, EntityType = type, Status = status, CoverBase64 = coverb64 }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}/:scheduled_event_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {guild_id = guildId, scheduled_event_id = scheduledEventId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); var scheduledEvent = JsonConvert.DeserializeObject(res.Response); var guild = this.Discord.Guilds[guildId]; scheduledEvent.Discord = this.Discord; if (scheduledEvent.Creator != null) { scheduledEvent.Creator.Discord = this.Discord; this.Discord.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; return old; }); } if (this.Discord is DiscordClient dc) await dc.OnGuildScheduledEventUpdateEventAsync(scheduledEvent, guild); return scheduledEvent; } /// /// Modifies a scheduled event. /// internal async Task ModifyGuildScheduledEventStatusAsync(ulong guildId, ulong scheduledEventId, ScheduledEventStatus status, string reason = null) { var pld = new RestGuildScheduledEventModifyPayload { Status = status }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}/:scheduled_event_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {guild_id = guildId, scheduled_event_id = scheduledEventId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); var scheduledEvent = JsonConvert.DeserializeObject(res.Response); var guild = this.Discord.Guilds[guildId]; scheduledEvent.Discord = this.Discord; if (scheduledEvent.Creator != null) { scheduledEvent.Creator.Discord = this.Discord; this.Discord.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; return old; }); } if (this.Discord is DiscordClient dc) await dc.OnGuildScheduledEventUpdateEventAsync(scheduledEvent, guild); return scheduledEvent; } /// /// Gets a scheduled event. /// /// The guild_id. /// The event id. /// Whether to include user count. internal async Task GetGuildScheduledEventAsync(ulong guildId, ulong scheduledEventId, bool? withUserCount) { var urlParams = new Dictionary(); if (withUserCount.HasValue) urlParams["with_user_count"] = withUserCount?.ToString(); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}/:scheduled_event_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId, scheduled_event_id = scheduledEventId }, out var path); var url = Utilities.GetApiUriFor(path, urlParams.Any() ? BuildQueryString(urlParams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var scheduledEvent = JsonConvert.DeserializeObject(res.Response); var guild = this.Discord.Guilds[guildId]; scheduledEvent.Discord = this.Discord; if (scheduledEvent.Creator != null) { scheduledEvent.Creator.Discord = this.Discord; this.Discord.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; return old; }); } return scheduledEvent; } /// /// Gets the guilds scheduled events. /// /// The guild_id. /// Whether to include the count of users subscribed to the scheduled event. internal async Task> ListGuildScheduledEventsAsync(ulong guildId, bool? withUserCount) { var urlParams = new Dictionary(); if (withUserCount.HasValue) urlParams["with_user_count"] = withUserCount?.ToString(); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, urlParams.Any() ? BuildQueryString(urlParams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var events = new Dictionary(); var eventsRaw = JsonConvert.DeserializeObject>(res.Response); var guild = this.Discord.Guilds[guildId]; foreach (var ev in eventsRaw) { ev.Discord = this.Discord; if (ev.Creator != null) { ev.Creator.Discord = this.Discord; this.Discord.UserCache.AddOrUpdate(ev.Creator.Id, ev.Creator, (id, old) => { old.Username = ev.Creator.Username; old.Discriminator = ev.Creator.Discriminator; old.AvatarHash = ev.Creator.AvatarHash; old.Flags = ev.Creator.Flags; return old; }); } events.Add(ev.Id, ev); } return new ReadOnlyDictionary(new Dictionary(events)); } /// /// Deletes a guild scheduled event. /// /// The guild_id. /// The scheduled event id. /// The reason. internal Task DeleteGuildScheduledEventAsync(ulong guildId, ulong scheduledEventId, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}/:scheduled_event_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {guild_id = guildId, scheduled_event_id = scheduledEventId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Gets the users who RSVP'd to a scheduled event. /// Optional with member objects. /// This endpoint is paginated. /// /// The guild_id. /// The scheduled event id. /// The limit how many users to receive from the event. /// Get results before the given id. /// Get results after the given id. /// Whether to include guild member data. attaches guild_member property to the user object. internal async Task> GetGuildScheduledEventRspvUsersAsync(ulong guildId, ulong scheduledEventId, int? limit, ulong? before, ulong? after, bool? withMember) { var urlParams = new Dictionary(); if (limit != null && limit > 0) urlParams["limit"] = limit.Value.ToString(CultureInfo.InvariantCulture); if (before != null) urlParams["before"] = before.Value.ToString(CultureInfo.InvariantCulture); if (after != null) urlParams["after"] = after.Value.ToString(CultureInfo.InvariantCulture); if (withMember != null) urlParams["with_member"] = withMember.Value.ToString(CultureInfo.InvariantCulture); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}/:scheduled_event_id{Endpoints.USERS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId, scheduled_event_id = scheduledEventId }, out var path); var url = Utilities.GetApiUriFor(path, urlParams.Any() ? BuildQueryString(urlParams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var rspvUsers = JsonConvert.DeserializeObject>(res.Response); Dictionary rspv = new(); foreach (var rspvUser in rspvUsers) { rspvUser.Discord = this.Discord; rspvUser.GuildId = guildId; rspvUser.User.Discord = this.Discord; rspvUser.User = this.Discord.UserCache.AddOrUpdate(rspvUser.User.Id, rspvUser.User, (id, old) => { old.Username = rspvUser.User.Username; old.Discriminator = rspvUser.User.Discriminator; old.AvatarHash = rspvUser.User.AvatarHash; old.BannerHash = rspvUser.User.BannerHash; old.BannerColorInternal = rspvUser.User.BannerColorInternal; return old; }); /*if (with_member.HasValue && with_member.Value && rspv_user.Member != null) { rspv_user.Member.Discord = this.Discord; }*/ rspv.Add(rspvUser.User.Id, rspvUser); } return new ReadOnlyDictionary(new Dictionary(rspv)); } #endregion #region Channel /// /// Creates a guild channel. /// /// The guild_id. /// The name. /// The type. /// The parent. /// The topic. /// The bitrate. /// The user_limit. /// The overwrites. /// If true, nsfw. /// The per user rate limit. /// The quality mode. /// The default auto archive duration. /// The reason. internal async Task CreateGuildChannelAsync(ulong guildId, string name, ChannelType type, ulong? parent, Optional topic, int? bitrate, int? userLimit, IEnumerable overwrites, bool? nsfw, Optional perUserRateLimit, VideoQualityMode? qualityMode, ThreadAutoArchiveDuration? defaultAutoArchiveDuration, Optional flags, string reason) { var restOverwrites = new List(); if (overwrites != null) foreach (var ow in overwrites) restOverwrites.Add(ow.Build()); var pld = new RestChannelCreatePayload { Name = name, Type = type, Parent = parent, Topic = topic, Bitrate = bitrate, UserLimit = userLimit, PermissionOverwrites = restOverwrites, Nsfw = nsfw, PerUserRateLimit = perUserRateLimit, QualityMode = qualityMode, DefaultAutoArchiveDuration = defaultAutoArchiveDuration, Flags = flags }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CHANNELS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Initialize(this.Discord); return ret; } /// /// Creates a guild forum channel. /// /// The guild_id. /// The name. /// The parent. /// The topic. /// The template. /// The default reaction emoji. /// The overwrites. /// If true, nsfw. /// The per user rate limit. /// The per user post create rate limit. /// The default auto archive duration. /// The reason. internal async Task CreateForumChannelAsync(ulong guildId, string name, ulong? parent, Optional topic, Optional template, bool? nsfw, Optional defaultReactionEmoji, Optional perUserRateLimit, Optional postCreateUserRateLimit, Optional defaultSortOrder, ThreadAutoArchiveDuration? defaultAutoArchiveDuration, IEnumerable permissionOverwrites, Optional flags, string reason) { List restoverwrites = null; if (permissionOverwrites != null) { restoverwrites = new List(); foreach (var ow in permissionOverwrites) restoverwrites.Add(ow.Build()); } var pld = new RestChannelCreatePayload { Name = name, Topic = topic, //Template = template, Nsfw = nsfw, Parent = parent, PerUserRateLimit = perUserRateLimit, PostCreateUserRateLimit = postCreateUserRateLimit, DefaultAutoArchiveDuration = defaultAutoArchiveDuration, DefaultReactionEmoji = defaultReactionEmoji, PermissionOverwrites = restoverwrites, DefaultSortOrder = defaultSortOrder, Flags = flags, Type = ChannelType.Forum }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CHANNELS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Initialize(this.Discord); return ret; } /// /// Modifies the channel async. /// /// The channel_id. /// The name. /// The position. /// The topic. /// If true, nsfw. /// The parent. /// The bitrate. /// The user_limit. /// The per user rate limit. /// The rtc region. /// The quality mode. /// The default auto archive duration. /// The type. /// The permission overwrites. /// The reason. internal Task ModifyChannelAsync(ulong channelId, string name, int? position, Optional topic, bool? nsfw, Optional parent, int? bitrate, int? userLimit, Optional perUserRateLimit, Optional rtcRegion, VideoQualityMode? qualityMode, ThreadAutoArchiveDuration? autoArchiveDuration, Optional type, IEnumerable permissionOverwrites, Optional flags, string reason) { List restoverwrites = null; if (permissionOverwrites != null) { restoverwrites = new List(); foreach (var ow in permissionOverwrites) restoverwrites.Add(ow.Build()); } var pld = new RestChannelModifyPayload { Name = name, Position = position, Topic = topic, Nsfw = nsfw, Parent = parent, Bitrate = bitrate, UserLimit = userLimit, PerUserRateLimit = perUserRateLimit, RtcRegion = rtcRegion, QualityMode = qualityMode, DefaultAutoArchiveDuration = autoArchiveDuration, Type = type, Flags = flags, PermissionOverwrites = restoverwrites }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } internal Task ModifyForumChannelAsync(ulong channelId, string name, int? position, Optional topic, Optional template, bool? nsfw, Optional parent, Optional defaultReactionEmoji, Optional perUserRateLimit, Optional postCreateUserRateLimit, Optional defaultSortOrder, ThreadAutoArchiveDuration? defaultAutoArchiveDuration, IEnumerable permissionOverwrites, Optional flags, string reason) { List restoverwrites = null; if (permissionOverwrites != null) { restoverwrites = new List(); foreach (var ow in permissionOverwrites) restoverwrites.Add(ow.Build()); } var pld = new RestChannelModifyPayload { Name = name, Position = position, Topic = topic, //Template = template, Nsfw = nsfw, Parent = parent, PerUserRateLimit = perUserRateLimit, PostCreateUserRateLimit = postCreateUserRateLimit, DefaultAutoArchiveDuration = defaultAutoArchiveDuration, DefaultReactionEmoji = defaultReactionEmoji, PermissionOverwrites = restoverwrites, DefaultSortOrder = defaultSortOrder, Flags = flags }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } internal async Task CreateForumTagAsync(ulong channelId, string name, DiscordEmoji emoji, bool moderated, string reason) { var pld = new RestForumPostTagPayloads { Name = name, Moderated = moderated }; if (emoji != null) if (emoji.Id == 0) { pld.Emoji = null; pld.UnicodeEmojiString = emoji.Name; } else { pld.Emoji = emoji.Id; pld.UnicodeEmojiString = null; } else { pld.Emoji = null; pld.UnicodeEmojiString = null; } var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.TAGS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)); var channel = JsonConvert.DeserializeObject(res.Response); channel.Discord = this.Discord; var tag = channel.InternalAvailableTags.First(x => x.Name == name); tag.Discord = this.Discord; return tag; } internal async Task ModifyForumTagAsync(ulong id, ulong channelId, string name, DiscordEmoji emoji = null, bool moderated = false, string reason = null) { var pld = new RestForumPostTagPayloads { Name = name, Moderated = moderated }; if (emoji != null) if (emoji.Id == 0) { pld.Emoji = null; pld.UnicodeEmojiString = emoji.Name; } else { pld.Emoji = emoji.Id; pld.UnicodeEmojiString = null; } else { pld.Emoji = null; pld.UnicodeEmojiString = null; } var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.TAGS}/:tag_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { channel_id = channelId, tag_id = id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, headers, DiscordJson.SerializeObject(pld)); var channel = JsonConvert.DeserializeObject(res.Response); channel.Discord = this.Discord; var tag = channel.InternalAvailableTags.First(x => x.Name == name); tag.Discord = this.Discord; return tag; } internal Task DeleteForumTagAsync(ulong id, ulong channelId, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.TAGS}/:tag_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id = channelId, tag_id = id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Gets the channel async. /// /// The channel_id. internal async Task GetChannelAsync(ulong channelId) { var route = $"{Endpoints.CHANNELS}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Initialize(this.Discord); return ret; } /// /// Deletes the channel async. /// /// The channel_id. /// The reason. internal Task DeleteChannelAsync(ulong channelId, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Gets the message async. /// /// The channel_id. /// The message_id. internal async Task GetMessageAsync(ulong channelId, ulong messageId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {channel_id = channelId, message_id = messageId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = this.PrepareMessage(JObject.Parse(res.Response)); return ret; } /// /// Creates the message async. /// /// The channel_id. /// The content. /// The embeds. /// The sticker. /// The reply message id. /// If true, mention reply. /// If true, fail on invalid reply. internal async Task CreateMessageAsync(ulong channelId, string content, IEnumerable embeds, DiscordSticker sticker, ulong? replyMessageId, bool mentionReply, bool failOnInvalidReply) { if (content != null && content.Length > 2000) throw new ArgumentException("Message content length cannot exceed 2000 characters."); if (!embeds?.Any() ?? true) { if (content == null && sticker == null) throw new ArgumentException("You must specify message content, a sticker or an embed."); if (content.Length == 0) throw new ArgumentException("Message content must not be empty."); } if (embeds != null) foreach (var embed in embeds) if (embed.Timestamp != null) embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); var pld = new RestChannelMessageCreatePayload { HasContent = content != null, Content = content, StickersIds = sticker is null ? Array.Empty() : new[] {sticker.Id}, IsTts = false, HasEmbed = embeds?.Any() ?? false, Embeds = embeds }; if (replyMessageId != null) pld.MessageReference = new InternalDiscordMessageReference { MessageId = replyMessageId, FailIfNotExists = failOnInvalidReply }; if (replyMessageId != null) pld.Mentions = new DiscordMentions(Mentions.All, true, mentionReply); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = this.PrepareMessage(JObject.Parse(res.Response)); return ret; } /// /// Creates the message async. /// /// The channel_id. /// The builder. internal async Task CreateMessageAsync(ulong channelId, DiscordMessageBuilder builder) { builder.Validate(); if (builder.Embeds != null) foreach (var embed in builder.Embeds) if (embed?.Timestamp != null) embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); var pld = new RestChannelMessageCreatePayload { HasContent = builder.Content != null, Content = builder.Content, StickersIds = builder.Sticker is null ? Array.Empty() : new[] {builder.Sticker.Id}, IsTts = builder.IsTts, HasEmbed = builder.Embeds != null, Embeds = builder.Embeds, Components = builder.Components }; if (builder.ReplyId != null) pld.MessageReference = new InternalDiscordMessageReference { MessageId = builder.ReplyId, FailIfNotExists = builder.FailOnInvalidReply }; pld.Mentions = new DiscordMentions(builder.Mentions ?? Mentions.All, builder.Mentions?.Any() ?? false, builder.MentionOnReply); if (builder.Files.Count == 0) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = this.PrepareMessage(JObject.Parse(res.Response)); return ret; } else { ulong fileId = 0; List attachments = new(builder.Files.Count); foreach (var file in builder.Files) { DiscordAttachment att = new() { Id = fileId, Discord = this.Discord, Description = file.Description, FileName = file.FileName }; attachments.Add(att); fileId++; } pld.Attachments = attachments; var values = new Dictionary { ["payload_json"] = DiscordJson.SerializeObject(pld) }; var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoMultipartAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, values: values, files: builder.Files).ConfigureAwait(false); var ret = this.PrepareMessage(JObject.Parse(res.Response)); foreach (var file in builder.FilesInternal.Where(x => x.ResetPositionTo.HasValue)) { file.Stream.Position = file.ResetPositionTo.Value; } return ret; } } /// /// Gets the guild channels async. /// /// The guild_id. internal async Task> GetGuildChannelsAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CHANNELS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var channelsRaw = JsonConvert.DeserializeObject>(res.Response).Select(xc => { xc.Discord = this.Discord; return xc; }); foreach (var ret in channelsRaw) ret.Initialize(this.Discord); return new ReadOnlyCollection(new List(channelsRaw)); } /// /// Creates the stage instance async. /// /// The channel_id. /// The topic. /// Whether everyone should be notified about the stage. /// The privacy_level. /// The reason. internal async Task CreateStageInstanceAsync(ulong channelId, string topic, bool sendStartNotification, StagePrivacyLevel privacyLevel, string reason) { var pld = new RestStageInstanceCreatePayload { ChannelId = channelId, Topic = topic, PrivacyLevel = privacyLevel, SendStartNotification = sendStartNotification }; var route = $"{Endpoints.STAGE_INSTANCES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { }, out var path); var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var stageInstance = JsonConvert.DeserializeObject(res.Response); return stageInstance; } /// /// Gets the stage instance async. /// /// The channel_id. internal async Task GetStageInstanceAsync(ulong channelId) { var route = $"{Endpoints.STAGE_INSTANCES}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var stageInstance = JsonConvert.DeserializeObject(res.Response); return stageInstance; } /// /// Modifies the stage instance async. /// /// The channel_id. /// The topic. /// The privacy_level. /// The reason. internal Task ModifyStageInstanceAsync(ulong channelId, Optional topic, Optional privacyLevel, string reason) { var pld = new RestStageInstanceModifyPayload { Topic = topic, PrivacyLevel = privacyLevel }; var route = $"{Endpoints.STAGE_INSTANCES}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {channel_id = channelId }, out var path); var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Deletes the stage instance async. /// /// The channel_id. /// The reason. internal Task DeleteStageInstanceAsync(ulong channelId, string reason) { var route = $"{Endpoints.STAGE_INSTANCES}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {channel_id = channelId }, out var path); var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Gets the channel messages async. /// /// The channel id. /// The limit. /// The before. /// The after. /// The around. internal async Task> GetChannelMessagesAsync(ulong channelId, int limit, ulong? before, ulong? after, ulong? around) { var urlParams = new Dictionary(); if (around != null) urlParams["around"] = around?.ToString(CultureInfo.InvariantCulture); if (before != null) urlParams["before"] = before?.ToString(CultureInfo.InvariantCulture); if (after != null) urlParams["after"] = after?.ToString(CultureInfo.InvariantCulture); if (limit > 0) urlParams["limit"] = limit.ToString(CultureInfo.InvariantCulture); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, urlParams.Any() ? BuildQueryString(urlParams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var msgsRaw = JArray.Parse(res.Response); var msgs = new List(); foreach (var xj in msgsRaw) msgs.Add(this.PrepareMessage(xj)); return new ReadOnlyCollection(new List(msgs)); } /// /// Gets the channel message async. /// /// The channel_id. /// The message_id. internal async Task GetChannelMessageAsync(ulong channelId, ulong messageId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {channel_id = channelId, message_id = messageId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = this.PrepareMessage(JObject.Parse(res.Response)); return ret; } /// /// Edits the message async. /// /// The channel_id. /// The message_id. /// The content. /// The embeds. /// The mentions. /// The components. /// The suppress_embed. /// The files. /// The attachments to keep. internal async Task EditMessageAsync(ulong channelId, ulong messageId, Optional content, Optional> embeds, Optional> mentions, IReadOnlyList components, Optional suppressEmbed, IReadOnlyCollection files, Optional> attachments) { if (embeds.HasValue && embeds.Value != null) foreach (var embed in embeds.Value) if (embed.Timestamp != null) embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); var pld = new RestChannelMessageEditPayload { HasContent = content.HasValue, Content = content.ValueOrDefault(), HasEmbed = embeds.HasValue && (embeds.Value?.Any() ?? false), Embeds = embeds.HasValue && (embeds.Value?.Any() ?? false) ? embeds.Value : null, Components = components, Flags = suppressEmbed.HasValue && (bool)suppressEmbed ? MessageFlags.SuppressedEmbeds : null, Mentions = mentions .Map(m => new DiscordMentions(m ?? Mentions.None, false, mentions.Value?.OfType().Any() ?? false)) .ValueOrDefault() }; if (files?.Count > 0) { ulong fileId = 0; List attachmentsNew = new(); foreach (var file in files) { DiscordAttachment att = new() { Id = fileId, Discord = this.Discord, Description = file.Description, FileName = file.FileName }; attachmentsNew.Add(att); fileId++; } if (attachments.HasValue && attachments.Value.Any()) attachmentsNew.AddRange(attachments.Value); pld.Attachments = attachmentsNew; var values = new Dictionary { ["payload_json"] = DiscordJson.SerializeObject(pld) }; var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {channel_id = channelId, message_id = messageId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoMultipartAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, values: values, files: files).ConfigureAwait(false); var ret = this.PrepareMessage(JObject.Parse(res.Response)); foreach (var file in files.Where(x => x.ResetPositionTo.HasValue)) { file.Stream.Position = file.ResetPositionTo.Value; } return ret; } else { pld.Attachments = attachments.ValueOrDefault(); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {channel_id = channelId, message_id = messageId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = this.PrepareMessage(JObject.Parse(res.Response)); return ret; } } /// /// Deletes the message async. /// /// The channel_id. /// The message_id. /// The reason. internal Task DeleteMessageAsync(ulong channelId, ulong messageId, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {channel_id = channelId, message_id = messageId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Deletes the messages async. /// /// The channel_id. /// The message_ids. /// The reason. internal Task DeleteMessagesAsync(ulong channelId, IEnumerable messageIds, string reason) { var pld = new RestChannelMessageBulkDeletePayload { Messages = messageIds }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}{Endpoints.BULK_DELETE}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Gets the channel invites async. /// /// The channel_id. internal async Task> GetChannelInvitesAsync(ulong channelId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.INVITES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var invitesRaw = JsonConvert.DeserializeObject>(res.Response).Select(xi => { xi.Discord = this.Discord; return xi; }); return new ReadOnlyCollection(new List(invitesRaw)); } /// /// Creates the channel invite async. /// /// The channel_id. /// The max_age. /// The max_uses. /// The target_type. /// The target_application. /// The target_user. /// If true, temporary. /// If true, unique. /// The reason. internal async Task CreateChannelInviteAsync(ulong channelId, int maxAge, int maxUses, TargetType? targetType, ulong? targetApplicationId, ulong? targetUser, bool temporary, bool unique, string reason) { var pld = new RestChannelInviteCreatePayload { MaxAge = maxAge, MaxUses = maxUses, TargetType = targetType, TargetApplicationId = targetApplicationId, TargetUserId = targetUser, Temporary = temporary, Unique = unique }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.INVITES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Deletes the channel permission async. /// /// The channel_id. /// The overwrite_id. /// The reason. internal Task DeleteChannelPermissionAsync(ulong channelId, ulong overwriteId, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.PERMISSIONS}/:overwrite_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {channel_id = channelId, overwrite_id = overwriteId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Edits the channel permissions async. /// /// The channel_id. /// The overwrite_id. /// The allow. /// The deny. /// The type. /// The reason. internal Task EditChannelPermissionsAsync(ulong channelId, ulong overwriteId, Permissions allow, Permissions deny, string type, string reason) { var pld = new RestChannelPermissionEditPayload { Type = type, Allow = allow & PermissionMethods.FullPerms, Deny = deny & PermissionMethods.FullPerms }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.PERMISSIONS}/:overwrite_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new {channel_id = channelId, overwrite_id = overwriteId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Triggers the typing async. /// /// The channel_id. internal Task TriggerTypingAsync(ulong channelId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.TYPING}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route); } /// /// Gets the pinned messages async. /// /// The channel_id. internal async Task> GetPinnedMessagesAsync(ulong channelId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.PINS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var msgsRaw = JArray.Parse(res.Response); var msgs = new List(); foreach (var xj in msgsRaw) msgs.Add(this.PrepareMessage(xj)); return new ReadOnlyCollection(new List(msgs)); } /// /// Pins the message async. /// /// The channel_id. /// The message_id. internal Task PinMessageAsync(ulong channelId, ulong messageId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.PINS}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new {channel_id = channelId, message_id = messageId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route); } /// /// Unpins the message async. /// /// The channel_id. /// The message_id. internal Task UnpinMessageAsync(ulong channelId, ulong messageId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.PINS}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {channel_id = channelId, message_id = messageId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Adds the group dm recipient async. /// /// The channel_id. /// The user_id. /// The access_token. /// The nickname. internal Task AddGroupDmRecipientAsync(ulong channelId, ulong userId, string accessToken, string nickname) { var pld = new RestChannelGroupDmRecipientAddPayload { AccessToken = accessToken, Nickname = nickname }; var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.CHANNELS}/:channel_id{Endpoints.RECIPIENTS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new {channel_id = channelId, user_id = userId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(pld)); } /// /// Removes the group dm recipient async. /// /// The channel_id. /// The user_id. internal Task RemoveGroupDmRecipientAsync(ulong channelId, ulong userId) { var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.CHANNELS}/:channel_id{Endpoints.RECIPIENTS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {channel_id = channelId, user_id = userId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Creates the group dm async. /// /// The access_tokens. /// The nicks. internal async Task CreateGroupDmAsync(IEnumerable accessTokens, IDictionary nicks) { var pld = new RestUserGroupDmCreatePayload { AccessTokens = accessTokens, Nicknames = nicks }; var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.CHANNELS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Creates the dm async. /// /// The recipient_id. internal async Task CreateDmAsync(ulong recipientId) { var pld = new RestUserDmCreatePayload { Recipient = recipientId }; var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.CHANNELS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Follows the channel async. /// /// The channel_id. /// The webhook_channel_id. internal async Task FollowChannelAsync(ulong channelId, ulong webhookChannelId) { var pld = new FollowedChannelAddPayload { WebhookChannelId = webhookChannelId }; var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.FOLLOWERS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var response = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); return JsonConvert.DeserializeObject(response.Response); } /// /// Crossposts the message async. /// /// The channel_id. /// The message_id. internal async Task CrosspostMessageAsync(ulong channelId, ulong messageId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.CROSSPOST}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {channel_id = channelId, message_id = messageId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var response = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route).ConfigureAwait(false); return JsonConvert.DeserializeObject(response.Response); } #endregion #region Member /// /// Gets the current user async. /// internal Task GetCurrentUserAsync() => this.GetUserAsync("@me"); /// /// Gets the user async. /// /// The user_id. internal Task GetUserAsync(ulong userId) => this.GetUserAsync(userId.ToString(CultureInfo.InvariantCulture)); /// /// Gets the user async. /// /// The user_id. internal async Task GetUserAsync(string userId) { var route = $"{Endpoints.USERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {user_id = userId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var userRaw = JsonConvert.DeserializeObject(res.Response); var duser = new DiscordUser(userRaw) { Discord = this.Discord }; if (this.Discord.Configuration.Intents.HasIntent(DiscordIntents.GuildPresences) && duser.Presence == null && this.Discord is DiscordClient dc) dc.PresencesInternal[duser.Id] = new DiscordPresence { Discord = dc, RawActivity = new TransportActivity(), Activity = new DiscordActivity(), Status = UserStatus.Offline, - InternalUser = userRaw + InternalUser = new UserWithIdOnly { Id = duser.Id} }; return duser; } /// /// Gets the guild member async. /// /// The guild_id. /// The user_id. internal async Task GetGuildMemberAsync(ulong guildId, ulong userId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId, user_id = userId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var tm = JsonConvert.DeserializeObject(res.Response); var usr = new DiscordUser(tm.User) { Discord = this.Discord }; usr = this.Discord.UserCache.AddOrUpdate(tm.User.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); if (this.Discord.Configuration.Intents.HasIntent(DiscordIntents.GuildPresences) && usr.Presence == null && this.Discord is DiscordClient dc) dc.PresencesInternal[usr.Id] = new DiscordPresence { Discord = dc, RawActivity = new TransportActivity(), Activity = new DiscordActivity(), Status = UserStatus.Offline }; return new DiscordMember(tm) { Discord = this.Discord, GuildId = guildId }; } /// /// Removes the guild member async. /// /// The guild_id. /// The user_id. /// The reason. internal Task RemoveGuildMemberAsync(ulong guildId, ulong userId, string reason) { var urlParams = new Dictionary(); if (reason != null) urlParams["reason"] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {guild_id = guildId, user_id = userId }, out var path); var url = Utilities.GetApiUriFor(path, BuildQueryString(urlParams), this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Modifies the current user async. /// /// The username. /// The base64_avatar. internal async Task ModifyCurrentUserAsync(string username, Optional base64Avatar) { var pld = new RestUserUpdateCurrentPayload { Username = username, AvatarBase64 = base64Avatar.ValueOrDefault(), AvatarSet = base64Avatar.HasValue }; var route = $"{Endpoints.USERS}{Endpoints.ME}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var userRaw = JsonConvert.DeserializeObject(res.Response); return userRaw; } /// /// Gets the current user guilds async. /// /// The limit. /// The before. /// The after. internal async Task> GetCurrentUserGuildsAsync(int limit = 100, ulong? before = null, ulong? after = null) { var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.GUILDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); var url = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration) .AddParameter($"limit", limit.ToString(CultureInfo.InvariantCulture)); if (before != null) url.AddParameter("before", before.Value.ToString(CultureInfo.InvariantCulture)); if (after != null) url.AddParameter("after", after.Value.ToString(CultureInfo.InvariantCulture)); var res = await this.DoRequestAsync(this.Discord, bucket, url.Build(), RestRequestMethod.GET, route).ConfigureAwait(false); if (this.Discord is DiscordClient) { var guildsRaw = JsonConvert.DeserializeObject>(res.Response); var glds = guildsRaw.Select(xug => (this.Discord as DiscordClient)?.GuildsInternal[xug.Id]); return new ReadOnlyCollection(new List(glds)); } else { return new ReadOnlyCollection(JsonConvert.DeserializeObject>(res.Response)); } } /// /// Modifies the guild member async. /// /// The guild_id. /// The user_id. /// The nick. /// The role_ids. /// The mute. /// The deaf. /// The voice_channel_id. /// The reason. internal Task ModifyGuildMemberAsync(ulong guildId, ulong userId, Optional nick, Optional> roleIds, Optional mute, Optional deaf, Optional voiceChannelId, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var pld = new RestGuildMemberModifyPayload { Nickname = nick, RoleIds = roleIds, Deafen = deaf, Mute = mute, VoiceChannelId = voiceChannelId }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {guild_id = guildId, user_id = userId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, payload: DiscordJson.SerializeObject(pld)); } /// /// Modifies the time out of a guild member. /// /// The guild_id. /// The user_id. /// Datetime offset. /// The reason. internal Task ModifyTimeoutAsync(ulong guildId, ulong userId, DateTimeOffset? until, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var pld = new RestGuildMemberTimeoutModifyPayload { CommunicationDisabledUntil = until }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {guild_id = guildId, user_id = userId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, payload: DiscordJson.SerializeObject(pld)); } /// /// Modifies the current member nickname async. /// /// The guild_id. /// The nick. /// The reason. internal Task ModifyCurrentMemberNicknameAsync(ulong guildId, string nick, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var pld = new RestGuildMemberModifyPayload { Nickname = nick }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}{Endpoints.ME}{Endpoints.NICK}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, payload: DiscordJson.SerializeObject(pld)); } #endregion #region Roles /// /// Gets the guild roles async. /// /// The guild_id. internal async Task> GetGuildRolesAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.ROLES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var rolesRaw = JsonConvert.DeserializeObject>(res.Response).Select(xr => { xr.Discord = this.Discord; xr.GuildId = guildId; return xr; }); return new ReadOnlyCollection(new List(rolesRaw)); } /// /// Gets the guild async. /// /// The guild id. /// If true, with_counts. internal async Task GetGuildAsync(ulong guildId, bool? withCounts) { var urlParams = new Dictionary(); if (withCounts.HasValue) urlParams["with_counts"] = withCounts?.ToString(); var route = $"{Endpoints.GUILDS}/:guild_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, urlParams.Any() ? BuildQueryString(urlParams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route, urlParams).ConfigureAwait(false); var json = JObject.Parse(res.Response); var rawMembers = (JArray)json["members"]; var guildRest = json.ToDiscordObject(); foreach (var r in guildRest.RolesInternal.Values) r.GuildId = guildRest.Id; if (this.Discord is DiscordClient dc) { await dc.OnGuildUpdateEventAsync(guildRest, rawMembers).ConfigureAwait(false); return dc.GuildsInternal[guildRest.Id]; } else { guildRest.Discord = this.Discord; return guildRest; } } /// /// Modifies the guild role async. /// /// The guild_id. /// The role_id. /// The name. /// The permissions. /// The color. /// If true, hoist. /// If true, mentionable. /// The icon. /// The unicode emoji icon. /// The reason. internal async Task ModifyGuildRoleAsync(ulong guildId, ulong roleId, string name, Permissions? permissions, int? color, bool? hoist, bool? mentionable, Optional iconb64, Optional emoji, string reason) { var pld = new RestGuildRolePayload { Name = name, Permissions = permissions & PermissionMethods.FullPerms, Color = color, Hoist = hoist, Mentionable = mentionable, }; if (emoji.HasValue && !iconb64.HasValue) pld.UnicodeEmoji = emoji; if (emoji.HasValue && iconb64.HasValue) { pld.IconBase64 = null; pld.UnicodeEmoji = emoji; } if (iconb64.HasValue) pld.IconBase64 = iconb64; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.ROLES}/:role_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {guild_id = guildId, role_id = roleId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; ret.GuildId = guildId; return ret; } /// /// Deletes the role async. /// /// The guild_id. /// The role_id. /// The reason. internal Task DeleteRoleAsync(ulong guildId, ulong roleId, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.ROLES}/:role_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {guild_id = guildId, role_id = roleId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Creates the guild role async. /// /// The guild_id. /// The name. /// The permissions. /// The color. /// If true, hoist. /// If true, mentionable. /// The reason. internal async Task CreateGuildRoleAsync(ulong guildId, string name, Permissions? permissions, int? color, bool? hoist, bool? mentionable, string reason) { var pld = new RestGuildRolePayload { Name = name, Permissions = permissions & PermissionMethods.FullPerms, Color = color, Hoist = hoist, Mentionable = mentionable }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.ROLES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; ret.GuildId = guildId; return ret; } #endregion #region Prune /// /// Gets the guild prune count async. /// /// The guild_id. /// The days. /// The include_roles. internal async Task GetGuildPruneCountAsync(ulong guildId, int days, IEnumerable includeRoles) { if (days < 0 || days > 30) throw new ArgumentException("Prune inactivity days must be a number between 0 and 30.", nameof(days)); var urlParams = new Dictionary { ["days"] = days.ToString(CultureInfo.InvariantCulture) }; var sb = includeRoles?.Aggregate(new StringBuilder(), (sb, id) => sb.Append($"&include_roles={id}")) ?? new StringBuilder(); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.PRUNE}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, $"{BuildQueryString(urlParams)}{sb}", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var pruned = JsonConvert.DeserializeObject(res.Response); return pruned.Pruned.Value; } /// /// Begins the guild prune async. /// /// The guild_id. /// The days. /// If true, compute_prune_count. /// The include_roles. /// The reason. internal async Task BeginGuildPruneAsync(ulong guildId, int days, bool computePruneCount, IEnumerable includeRoles, string reason) { if (days < 0 || days > 30) throw new ArgumentException("Prune inactivity days must be a number between 0 and 30.", nameof(days)); var urlParams = new Dictionary { ["days"] = days.ToString(CultureInfo.InvariantCulture), ["compute_prune_count"] = computePruneCount.ToString() }; var sb = includeRoles?.Aggregate(new StringBuilder(), (sb, id) => sb.Append($"&include_roles={id}")) ?? new StringBuilder(); var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.PRUNE}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, $"{BuildQueryString(urlParams)}{sb}", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers).ConfigureAwait(false); var pruned = JsonConvert.DeserializeObject(res.Response); return pruned.Pruned; } #endregion #region GuildVarious /// /// Gets the template async. /// /// The code. internal async Task GetTemplateAsync(string code) { var route = $"{Endpoints.GUILDS}{Endpoints.TEMPLATES}/:code"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { code }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var templatesRaw = JsonConvert.DeserializeObject(res.Response); return templatesRaw; } /// /// Gets the guild integrations async. /// /// The guild_id. internal async Task> GetGuildIntegrationsAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INTEGRATIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var integrationsRaw = JsonConvert.DeserializeObject>(res.Response).Select(xi => { xi.Discord = this.Discord; return xi; }); return new ReadOnlyCollection(new List(integrationsRaw)); } /// /// Gets the guild preview async. /// /// The guild_id. internal async Task GetGuildPreviewAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.PREVIEW}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Creates the guild integration async. /// /// The guild_id. /// The type. /// The id. internal async Task CreateGuildIntegrationAsync(ulong guildId, string type, ulong id) { var pld = new RestGuildIntegrationAttachPayload { Type = type, Id = id }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INTEGRATIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Modifies the guild integration async. /// /// The guild_id. /// The integration_id. /// The expire_behaviour. /// The expire_grace_period. /// If true, enable_emoticons. internal async Task ModifyGuildIntegrationAsync(ulong guildId, ulong integrationId, int expireBehaviour, int expireGracePeriod, bool enableEmoticons) { var pld = new RestGuildIntegrationModifyPayload { ExpireBehavior = expireBehaviour, ExpireGracePeriod = expireGracePeriod, EnableEmoticons = enableEmoticons }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INTEGRATIONS}/:integration_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {guild_id = guildId, integration_id = integrationId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Deletes the guild integration async. /// /// The guild_id. /// The integration. internal Task DeleteGuildIntegrationAsync(ulong guildId, DiscordIntegration integration) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INTEGRATIONS}/:integration_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {guild_id = guildId, integration_id = integration.Id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, payload: DiscordJson.SerializeObject(integration)); } /// /// Syncs the guild integration async. /// /// The guild_id. /// The integration_id. internal Task SyncGuildIntegrationAsync(ulong guildId, ulong integrationId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INTEGRATIONS}/:integration_id{Endpoints.SYNC}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {guild_id = guildId, integration_id = integrationId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route); } /// /// Gets the guild voice regions async. /// /// The guild_id. internal async Task> GetGuildVoiceRegionsAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.REGIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var regionsRaw = JsonConvert.DeserializeObject>(res.Response); return new ReadOnlyCollection(new List(regionsRaw)); } /// /// Gets the guild invites async. /// /// The guild_id. internal async Task> GetGuildInvitesAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INVITES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var invitesRaw = JsonConvert.DeserializeObject>(res.Response).Select(xi => { xi.Discord = this.Discord; return xi; }); return new ReadOnlyCollection(new List(invitesRaw)); } #endregion #region Invite /// /// Gets the invite async. /// /// The invite_code. /// If true, with_counts. /// If true, with_expiration. /// The scheduled event id to get. internal async Task GetInviteAsync(string inviteCode, bool? withCounts, bool? withExpiration, ulong? guildScheduledEventId) { var urlParams = new Dictionary(); if (withCounts.HasValue) urlParams["with_counts"] = withCounts?.ToString(); if (withExpiration.HasValue) urlParams["with_expiration"] = withExpiration?.ToString(); if (guildScheduledEventId.HasValue) urlParams["guild_scheduled_event_id"] = guildScheduledEventId?.ToString(); var route = $"{Endpoints.INVITES}/:invite_code"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {invite_code = inviteCode }, out var path); var url = Utilities.GetApiUriFor(path, urlParams.Any() ? BuildQueryString(urlParams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Deletes the invite async. /// /// The invite_code. /// The reason. internal async Task DeleteInviteAsync(string inviteCode, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.INVITES}/:invite_code"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {invite_code = inviteCode }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /* * Disabled due to API restrictions * * internal async Task InternalAcceptInvite(string invite_code) * { * this.Discord.DebugLogger.LogMessage(LogLevel.Warning, "REST API", "Invite accept endpoint was used; this account is now likely unverified", DateTime.Now); * * var url = new Uri($"{Utils.GetApiBaseUri(this.Configuration), Endpoints.INVITES}/{invite_code)); * var bucket = this.Rest.GetBucket(0, MajorParameterType.Unbucketed, url, HttpRequestMethod.POST); * var res = await this.DoRequestAsync(this.Discord, bucket, url, HttpRequestMethod.POST).ConfigureAwait(false); * * var ret = JsonConvert.DeserializeObject(res.Response); * ret.Discord = this.Discord; * * return ret; * } */ #endregion #region Connections /// /// Gets the users connections async. /// internal async Task> GetUserConnectionsAsync() { var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.CONNECTIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var connectionsRaw = JsonConvert.DeserializeObject>(res.Response).Select(xc => { xc.Discord = this.Discord; return xc; }); return new ReadOnlyCollection(new List(connectionsRaw)); } #endregion #region Voice /// /// Lists the voice regions async. /// internal async Task> ListVoiceRegionsAsync() { var route = $"{Endpoints.VOICE}{Endpoints.REGIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var regions = JsonConvert.DeserializeObject>(res.Response); return new ReadOnlyCollection(new List(regions)); } #endregion #region Webhooks /// /// Creates the webhook async. /// /// The channel_id. /// The name. /// The base64_avatar. /// The reason. internal async Task CreateWebhookAsync(ulong channelId, string name, Optional base64Avatar, string reason) { var pld = new RestWebhookPayload { Name = name, AvatarBase64 = base64Avatar.ValueOrDefault(), AvatarSet = base64Avatar.HasValue }; var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.WEBHOOKS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; ret.ApiClient = this; return ret; } /// /// Gets the channel webhooks async. /// /// The channel_id. internal async Task> GetChannelWebhooksAsync(ulong channelId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.WEBHOOKS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var webhooksRaw = JsonConvert.DeserializeObject>(res.Response).Select(xw => { xw.Discord = this.Discord; xw.ApiClient = this; return xw; }); return new ReadOnlyCollection(new List(webhooksRaw)); } /// /// Gets the guild webhooks async. /// /// The guild_id. internal async Task> GetGuildWebhooksAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WEBHOOKS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var webhooksRaw = JsonConvert.DeserializeObject>(res.Response).Select(xw => { xw.Discord = this.Discord; xw.ApiClient = this; return xw; }); return new ReadOnlyCollection(new List(webhooksRaw)); } /// /// Gets the webhook async. /// /// The webhook_id. internal async Task GetWebhookAsync(ulong webhookId) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {webhook_id = webhookId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; ret.ApiClient = this; return ret; } /// /// Gets the webhook with token async. /// /// The webhook_id. /// The webhook_token. internal async Task GetWebhookWithTokenAsync(ulong webhookId, string webhookToken) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {webhook_id = webhookId, webhook_token = webhookToken }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Token = webhookToken; ret.Id = webhookId; ret.Discord = this.Discord; ret.ApiClient = this; return ret; } /// /// Modifies the webhook async. /// /// The webhook_id. /// The channel id. /// The name. /// The base64_avatar. /// The reason. internal async Task ModifyWebhookAsync(ulong webhookId, ulong channelId, string name, Optional base64Avatar, string reason) { var pld = new RestWebhookPayload { Name = name, AvatarBase64 = base64Avatar.ValueOrDefault(), AvatarSet = base64Avatar.HasValue, ChannelId = channelId }; var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.WEBHOOKS}/:webhook_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {webhook_id = webhookId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; ret.ApiClient = this; return ret; } /// /// Modifies the webhook async. /// /// The webhook_id. /// The name. /// The base64_avatar. /// The webhook_token. /// The reason. internal async Task ModifyWebhookAsync(ulong webhookId, string name, string base64Avatar, string webhookToken, string reason) { var pld = new RestWebhookPayload { Name = name, AvatarBase64 = base64Avatar }; var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {webhook_id = webhookId, webhook_token = webhookToken }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; ret.ApiClient = this; return ret; } /// /// Deletes the webhook async. /// /// The webhook_id. /// The reason. internal Task DeleteWebhookAsync(ulong webhookId, string reason) { var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.WEBHOOKS}/:webhook_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {webhook_id = webhookId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Deletes the webhook async. /// /// The webhook_id. /// The webhook_token. /// The reason. internal Task DeleteWebhookAsync(ulong webhookId, string webhookToken, string reason) { var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {webhook_id = webhookId, webhook_token = webhookToken }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Executes the webhook async. /// /// The webhook_id. /// The webhook_token. /// The builder. /// The thread_id. internal async Task ExecuteWebhookAsync(ulong webhookId, string webhookToken, DiscordWebhookBuilder builder, string threadId) { builder.Validate(); if (builder.Embeds != null) foreach (var embed in builder.Embeds) if (embed.Timestamp != null) embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); var values = new Dictionary(); var pld = new RestWebhookExecutePayload { Content = builder.Content, Username = builder.Username.ValueOrDefault(), AvatarUrl = builder.AvatarUrl.ValueOrDefault(), IsTts = builder.IsTts, Embeds = builder.Embeds, Components = builder.Components, ThreadName = builder.ThreadName }; if (builder.Mentions != null) pld.Mentions = new DiscordMentions(builder.Mentions, builder.Mentions.Any()); if (builder.Files?.Count > 0) { ulong fileId = 0; List attachments = new(); foreach (var file in builder.Files) { DiscordAttachment att = new() { Id = fileId, Discord = this.Discord, Description = file.Description, FileName = file.FileName, FileSize = null }; attachments.Add(att); fileId++; } pld.Attachments = attachments; } if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count > 0 || builder.Files?.Count > 0 || builder.IsTts == true || builder.Mentions != null) values["payload_json"] = DiscordJson.SerializeObject(pld); var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {webhook_id = webhookId, webhook_token = webhookToken }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration).AddParameter("wait", "true"); if (threadId != null) qub.AddParameter("thread_id", threadId); var url = qub.Build(); var res = await this.DoMultipartAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, values: values, files: builder.Files).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); foreach (var att in ret.Attachments) att.Discord = this.Discord; foreach (var file in builder.Files.Where(x => x.ResetPositionTo.HasValue)) { file.Stream.Position = file.ResetPositionTo.Value; } ret.Discord = this.Discord; return ret; } /// /// Executes the webhook slack async. /// /// The webhook_id. /// The webhook_token. /// The json_payload. /// The thread_id. internal async Task ExecuteWebhookSlackAsync(ulong webhookId, string webhookToken, string jsonPayload, string threadId) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.SLACK}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {webhook_id = webhookId, webhook_token = webhookToken }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration).AddParameter("wait", "true"); if (threadId != null) qub.AddParameter("thread_id", threadId); var url = qub.Build(); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: jsonPayload).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Executes the webhook github async. /// /// The webhook_id. /// The webhook_token. /// The json_payload. /// The thread_id. internal async Task ExecuteWebhookGithubAsync(ulong webhookId, string webhookToken, string jsonPayload, string threadId) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.GITHUB}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {webhook_id = webhookId, webhook_token = webhookToken }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration).AddParameter("wait", "true"); if (threadId != null) qub.AddParameter("thread_id", threadId); var url = qub.Build(); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: jsonPayload).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Edits the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// The builder. /// The thread_id. internal async Task EditWebhookMessageAsync(ulong webhookId, string webhookToken, string messageId, DiscordWebhookBuilder builder, string threadId) { builder.Validate(true); var pld = new RestWebhookMessageEditPayload { Content = builder.Content, Embeds = builder.Embeds, Mentions = builder.Mentions, Components = builder.Components, }; if (builder.Files?.Count > 0) { ulong fileId = 0; List attachments = new(); foreach (var file in builder.Files) { DiscordAttachment att = new() { Id = fileId, Discord = this.Discord, Description = file.Description, FileName = file.FileName, FileSize = null }; attachments.Add(att); fileId++; } if (builder.Attachments != null && builder.Attachments?.Count() > 0) attachments.AddRange(builder.Attachments); pld.Attachments = attachments; var values = new Dictionary { ["payload_json"] = DiscordJson.SerializeObject(pld) }; var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {webhook_id = webhookId, webhook_token = webhookToken, message_id = messageId }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration); if (threadId != null) qub.AddParameter("thread_id", threadId); var url = qub.Build(); var res = await this.DoMultipartAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, values: values, files: builder.Files); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; foreach (var att in ret.AttachmentsInternal) att.Discord = this.Discord; foreach (var file in builder.Files.Where(x => x.ResetPositionTo.HasValue)) { file.Stream.Position = file.ResetPositionTo.Value; } return ret; } else { pld.Attachments = builder.Attachments; var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {webhook_id = webhookId, webhook_token = webhookToken, message_id = messageId }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration); if (threadId != null) qub.AddParameter("thread_id", threadId); var url = qub.Build(); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; foreach (var att in ret.AttachmentsInternal) att.Discord = this.Discord; return ret; } } /// /// Edits the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// The builder. /// The thread_id. internal Task EditWebhookMessageAsync(ulong webhookId, string webhookToken, ulong messageId, DiscordWebhookBuilder builder, ulong threadId) => this.EditWebhookMessageAsync(webhookId, webhookToken, messageId.ToString(), builder, threadId.ToString()); /// /// Gets the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// The thread_id. internal async Task GetWebhookMessageAsync(ulong webhookId, string webhookToken, string messageId, string threadId) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {webhook_id = webhookId, webhook_token = webhookToken, message_id = messageId }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration); if (threadId != null) qub.AddParameter("thread_id", threadId); var url = qub.Build(); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Gets the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. internal Task GetWebhookMessageAsync(ulong webhookId, string webhookToken, ulong messageId) => this.GetWebhookMessageAsync(webhookId, webhookToken, messageId.ToString(), null); /// /// Gets the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// The thread_id. internal Task GetWebhookMessageAsync(ulong webhookId, string webhookToken, ulong messageId, ulong threadId) => this.GetWebhookMessageAsync(webhookId, webhookToken, messageId.ToString(), threadId.ToString()); /// /// Deletes the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// The thread_id. internal async Task DeleteWebhookMessageAsync(ulong webhookId, string webhookToken, string messageId, string threadId) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {webhook_id = webhookId, webhook_token = webhookToken, message_id = messageId }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration); if (threadId != null) qub.AddParameter("thread_id", threadId); var url = qub.Build(); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Deletes the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. internal Task DeleteWebhookMessageAsync(ulong webhookId, string webhookToken, ulong messageId) => this.DeleteWebhookMessageAsync(webhookId, webhookToken, messageId.ToString(), null); /// /// Deletes the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// The thread_id. internal Task DeleteWebhookMessageAsync(ulong webhookId, string webhookToken, ulong messageId, ulong threadId) => this.DeleteWebhookMessageAsync(webhookId, webhookToken, messageId.ToString(), threadId.ToString()); #endregion #region Reactions /// /// Creates the reaction async. /// /// The channel_id. /// The message_id. /// The emoji. internal Task CreateReactionAsync(ulong channelId, ulong messageId, string emoji) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.REACTIONS}/:emoji{Endpoints.ME}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new {channel_id = channelId, message_id = messageId, emoji }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, ratelimitWaitOverride: this.Discord.Configuration.UseRelativeRatelimit ? null : 0.26); } /// /// Deletes the own reaction async. /// /// The channel_id. /// The message_id. /// The emoji. internal Task DeleteOwnReactionAsync(ulong channelId, ulong messageId, string emoji) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.REACTIONS}/:emoji{Endpoints.ME}"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {channel_id = channelId, message_id = messageId, emoji }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, ratelimitWaitOverride: this.Discord.Configuration.UseRelativeRatelimit ? null : 0.26); } /// /// Deletes the user reaction async. /// /// The channel_id. /// The message_id. /// The user_id. /// The emoji. /// The reason. internal Task DeleteUserReactionAsync(ulong channelId, ulong messageId, ulong userId, string emoji, string reason) { var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.REACTIONS}/:emoji/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {channel_id = channelId, message_id = messageId, emoji, user_id = userId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers, ratelimitWaitOverride: this.Discord.Configuration.UseRelativeRatelimit ? null : 0.26); } /// /// Gets the reactions async. /// /// The channel_id. /// The message_id. /// The emoji. /// The after_id. /// The limit. internal async Task> GetReactionsAsync(ulong channelId, ulong messageId, string emoji, ulong? afterId = null, int limit = 25) { var urlParams = new Dictionary(); if (afterId.HasValue) urlParams["after"] = afterId.Value.ToString(CultureInfo.InvariantCulture); urlParams["limit"] = limit.ToString(CultureInfo.InvariantCulture); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.REACTIONS}/:emoji"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {channel_id = channelId, message_id = messageId, emoji }, out var path); var url = Utilities.GetApiUriFor(path, BuildQueryString(urlParams), this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var reactersRaw = JsonConvert.DeserializeObject>(res.Response); var reacters = new List(); foreach (var xr in reactersRaw) { var usr = new DiscordUser(xr) { Discord = this.Discord }; usr = this.Discord.UserCache.AddOrUpdate(xr.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); reacters.Add(usr); } return new ReadOnlyCollection(new List(reacters)); } /// /// Deletes the all reactions async. /// /// The channel_id. /// The message_id. /// The reason. internal Task DeleteAllReactionsAsync(ulong channelId, ulong messageId, string reason) { var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.REACTIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {channel_id = channelId, message_id = messageId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers, ratelimitWaitOverride: this.Discord.Configuration.UseRelativeRatelimit ? null : 0.26); } /// /// Deletes the reactions emoji async. /// /// The channel_id. /// The message_id. /// The emoji. internal Task DeleteReactionsEmojiAsync(ulong channelId, ulong messageId, string emoji) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.REACTIONS}/:emoji"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {channel_id = channelId, message_id = messageId, emoji }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, ratelimitWaitOverride: this.Discord.Configuration.UseRelativeRatelimit ? null : 0.26); } #endregion #region Threads /// /// Creates the thread. /// /// The channel id to create the thread in. /// The optional message id to create the thread from. /// The name of the thread. /// The auto_archive_duration for the thread. /// Can be either or . /// The rate limit per user. /// The tags to add on creation. /// The reason. internal async Task CreateThreadAsync(ulong channelId, ulong? messageId, string name, ThreadAutoArchiveDuration? autoArchiveDuration, ChannelType? type, int? rateLimitPerUser, IEnumerable? appliedTags = null, DiscordMessageBuilder builder = null, bool isForum = false, string reason = null) { var pld = new RestThreadChannelCreatePayload { Name = name, AutoArchiveDuration = autoArchiveDuration, PerUserRateLimit = rateLimitPerUser }; if (isForum) { pld.Message = new RestChannelMessageCreatePayload { Content = builder.Content, Attachments = builder.Attachments, Components = builder.Components, HasContent = true, Embeds = builder.Embeds, //Flags = builder.Flags, //Mentions = builder.Mentions, StickersIds = builder.Sticker != null ? new List(1) { builder.Sticker.Id } : null }; if (appliedTags != null && appliedTags.Any()) { List tags = new(); foreach (var b in appliedTags) tags.Add(b.Id); pld.AppliedTags = tags; pld.Type = null; } } else { pld.Type = type; } var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id"; if (messageId is not null) route += $"{Endpoints.MESSAGES}/:message_id"; route += Endpoints.THREADS; object param = messageId is null ? new {channel_id = channelId} : new {channel_id = channelId, message_id = messageId}; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, param, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)); var threadChannel = JsonConvert.DeserializeObject(res.Response); threadChannel.Discord = this.Discord; return threadChannel; } /// /// Gets the thread. /// /// The thread id. internal async Task GetThreadAsync(ulong threadId) { var route = $"{Endpoints.CHANNELS}/:thread_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {thread_id = threadId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Joins the thread. /// /// The channel id. internal async Task JoinThreadAsync(ulong channelId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREAD_MEMBERS}{Endpoints.ME}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new {channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route); } /// /// Leaves the thread. /// /// The channel id. internal async Task LeaveThreadAsync(ulong channelId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREAD_MEMBERS}{Endpoints.ME}"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Adds a thread member. /// /// The channel id to add the member to. /// The user id to add. internal async Task AddThreadMemberAsync(ulong channelId, ulong userId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREAD_MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new {channel_id = channelId, user_id = userId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route); } /// /// Gets a thread member. /// /// The channel id to get the member from. /// The user id to get. internal async Task GetThreadMemberAsync(ulong channelId, ulong userId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREAD_MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {channel_id = channelId, user_id = userId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var threadMember = JsonConvert.DeserializeObject(res.Response); return threadMember; } /// /// Removes a thread member. /// /// The channel id to remove the member from. /// The user id to remove. internal async Task RemoveThreadMemberAsync(ulong channelId, ulong userId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREAD_MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {channel_id = channelId, user_id = userId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Gets the thread members. /// /// The thread id. internal async Task> GetThreadMembersAsync(ulong threadId) { var route = $"{Endpoints.CHANNELS}/:thread_id{Endpoints.THREAD_MEMBERS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {thread_id = threadId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var threadMembersRaw = JsonConvert.DeserializeObject>(res.Response); return new ReadOnlyCollection(threadMembersRaw); } /// /// Gets the active threads in a guild. /// /// The guild id. internal async Task GetActiveThreadsAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.THREADS}{Endpoints.THREAD_ACTIVE}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var threadReturn = JsonConvert.DeserializeObject(res.Response); threadReturn.Threads.ForEach(x => x.Discord = this.Discord); return threadReturn; } /// /// Gets the joined private archived threads in a channel. /// /// The channel id. /// Get threads before snowflake. /// Limit the results. internal async Task GetJoinedPrivateArchivedThreadsAsync(ulong channelId, ulong? before, int? limit) { var urlParams = new Dictionary(); if (before != null) urlParams["before"] = before.Value.ToString(CultureInfo.InvariantCulture); if (limit != null && limit > 0) urlParams["limit"] = limit.Value.ToString(CultureInfo.InvariantCulture); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.USERS}{Endpoints.ME}{Endpoints.THREADS}{Endpoints.THREAD_ARCHIVED}{Endpoints.THREAD_PRIVATE}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, urlParams.Any() ? BuildQueryString(urlParams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var threadReturn = JsonConvert.DeserializeObject(res.Response); return threadReturn; } /// /// Gets the public archived threads in a channel. /// /// The channel id. /// Get threads before snowflake. /// Limit the results. internal async Task GetPublicArchivedThreadsAsync(ulong channelId, ulong? before, int? limit) { var urlParams = new Dictionary(); if (before != null) urlParams["before"] = before.Value.ToString(CultureInfo.InvariantCulture); if (limit != null && limit > 0) urlParams["limit"] = limit.Value.ToString(CultureInfo.InvariantCulture); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREADS}{Endpoints.THREAD_ARCHIVED}{Endpoints.THREAD_PUBLIC}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, urlParams.Any() ? BuildQueryString(urlParams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var threadReturn = JsonConvert.DeserializeObject(res.Response); return threadReturn; } /// /// Gets the private archived threads in a channel. /// /// The channel id. /// Get threads before snowflake. /// Limit the results. internal async Task GetPrivateArchivedThreadsAsync(ulong channelId, ulong? before, int? limit) { var urlParams = new Dictionary(); if (before != null) urlParams["before"] = before.Value.ToString(CultureInfo.InvariantCulture); if (limit != null && limit > 0) urlParams["limit"] = limit.Value.ToString(CultureInfo.InvariantCulture); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREADS}{Endpoints.THREAD_ARCHIVED}{Endpoints.THREAD_PRIVATE}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {channel_id = channelId }, out var path); var url = Utilities.GetApiUriFor(path, urlParams.Any() ? BuildQueryString(urlParams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var threadReturn = JsonConvert.DeserializeObject(res.Response); return threadReturn; } /// /// Modifies a thread. /// /// The thread to modify. /// The parent channels type as failback to ignore forum fields. /// The new name. /// The new locked state. /// The new archived state. /// The new per user rate limit. /// The new auto archive duration. /// The new user invitable state. /// The tags to add on creation. /// The reason for the modification. internal Task ModifyThreadAsync(ulong threadId, ChannelType parentType, string name, Optional locked, Optional archived, Optional perUserRateLimit, Optional autoArchiveDuration, Optional invitable, Optional> appliedTags, string reason) { var pld = new RestThreadChannelModifyPayload { Name = name, Archived = archived, AutoArchiveDuration = autoArchiveDuration, Locked = locked, PerUserRateLimit = perUserRateLimit, Invitable = invitable }; // TODO: Pinned threads in forum if (parentType == ChannelType.Forum) { if (appliedTags.HasValue && appliedTags.Value != null) { List tags = new(appliedTags.Value.Count()); foreach (var b in appliedTags.Value) tags.Add(b.Id); pld.AppliedTags = tags; } } var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:thread_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {thread_id = threadId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } #endregion #region Emoji /// /// Gets the guild emojis async. /// /// The guild_id. internal async Task> GetGuildEmojisAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EMOJIS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var emojisRaw = JsonConvert.DeserializeObject>(res.Response); this.Discord.Guilds.TryGetValue(guildId, out var gld); var users = new Dictionary(); var emojis = new List(); foreach (var rawEmoji in emojisRaw) { var xge = rawEmoji.ToObject(); xge.Guild = gld; var xtu = rawEmoji["user"]?.ToObject(); if (xtu != null) { if (!users.ContainsKey(xtu.Id)) { var user = gld != null && gld.Members.TryGetValue(xtu.Id, out var member) ? member : new DiscordUser(xtu); users[user.Id] = user; } xge.User = users[xtu.Id]; } emojis.Add(xge); } return new ReadOnlyCollection(emojis); } /// /// Gets the guild emoji async. /// /// The guild_id. /// The emoji_id. internal async Task GetGuildEmojiAsync(ulong guildId, ulong emojiId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EMOJIS}/:emoji_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId, emoji_id = emojiId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); this.Discord.Guilds.TryGetValue(guildId, out var gld); var emojiRaw = JObject.Parse(res.Response); var emoji = emojiRaw.ToObject(); emoji.Guild = gld; var xtu = emojiRaw["user"]?.ToObject(); if (xtu != null) emoji.User = gld != null && gld.Members.TryGetValue(xtu.Id, out var member) ? member : new DiscordUser(xtu); return emoji; } /// /// Creates the guild emoji async. /// /// The guild_id. /// The name. /// The imageb64. /// The roles. /// The reason. internal async Task CreateGuildEmojiAsync(ulong guildId, string name, string imageb64, IEnumerable roles, string reason) { var pld = new RestGuildEmojiCreatePayload { Name = name, ImageB64 = imageb64, Roles = roles?.ToArray() }; var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EMOJIS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); this.Discord.Guilds.TryGetValue(guildId, out var gld); var emojiRaw = JObject.Parse(res.Response); var emoji = emojiRaw.ToObject(); emoji.Guild = gld; var xtu = emojiRaw["user"]?.ToObject(); emoji.User = xtu != null ? gld != null && gld.Members.TryGetValue(xtu.Id, out var member) ? member : new DiscordUser(xtu) : this.Discord.CurrentUser; return emoji; } /// /// Modifies the guild emoji async. /// /// The guild_id. /// The emoji_id. /// The name. /// The roles. /// The reason. internal async Task ModifyGuildEmojiAsync(ulong guildId, ulong emojiId, string name, IEnumerable roles, string reason) { var pld = new RestGuildEmojiModifyPayload { Name = name, Roles = roles?.ToArray() }; var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EMOJIS}/:emoji_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {guild_id = guildId, emoji_id = emojiId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); this.Discord.Guilds.TryGetValue(guildId, out var gld); var emojiRaw = JObject.Parse(res.Response); var emoji = emojiRaw.ToObject(); emoji.Guild = gld; var xtu = emojiRaw["user"]?.ToObject(); if (xtu != null) emoji.User = gld != null && gld.Members.TryGetValue(xtu.Id, out var member) ? member : new DiscordUser(xtu); return emoji; } /// /// Deletes the guild emoji async. /// /// The guild_id. /// The emoji_id. /// The reason. internal Task DeleteGuildEmojiAsync(ulong guildId, ulong emojiId, string reason) { var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EMOJIS}/:emoji_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {guild_id = guildId, emoji_id = emojiId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } #endregion #region Stickers /// /// Gets a sticker. /// /// The sticker id. internal async Task GetStickerAsync(ulong stickerId) { var route = $"{Endpoints.STICKERS}/:sticker_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {sticker_id = stickerId}, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = JObject.Parse(res.Response).ToDiscordObject(); ret.Discord = this.Discord; return ret; } /// /// Gets the sticker packs. /// internal async Task> GetStickerPacksAsync() { var route = $"{Endpoints.STICKERPACKS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var json = JObject.Parse(res.Response)["sticker_packs"] as JArray; var ret = json.ToDiscordObject(); return ret.ToList(); } /// /// Gets the guild stickers. /// /// The guild id. internal async Task> GetGuildStickersAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.STICKERS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId}, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var json = JArray.Parse(res.Response); var ret = json.ToDiscordObject(); for (var i = 0; i < ret.Length; i++) { var stkr = ret[i]; stkr.Discord = this.Discord; if (json[i]["user"] is JObject obj) // Null = Missing stickers perm // { var tsr = obj.ToDiscordObject(); var usr = new DiscordUser(tsr) {Discord = this.Discord}; usr = this.Discord.UserCache.AddOrUpdate(tsr.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); stkr.User = usr; } } return ret.ToList(); } /// /// Gets a guild sticker. /// /// The guild id. /// The sticker id. internal async Task GetGuildStickerAsync(ulong guildId, ulong stickerId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.STICKERS}/:sticker_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId, sticker_id = stickerId}, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var json = JObject.Parse(res.Response); var ret = json.ToDiscordObject(); if (json["user"] is not null) // Null = Missing stickers perm // { var tsr = json["user"].ToDiscordObject(); var usr = new DiscordUser(tsr) {Discord = this.Discord}; usr = this.Discord.UserCache.AddOrUpdate(tsr.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); ret.User = usr; } ret.Discord = this.Discord; return ret; } /// /// Creates the guild sticker. /// /// The guild id. /// The name. /// The description. /// The tags. /// The file. /// The reason. internal async Task CreateGuildStickerAsync(ulong guildId, string name, string description, string tags, DiscordMessageFile file, string reason) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.STICKERS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {guild_id = guildId}, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var res = await this.DoStickerMultipartAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, file, name, tags, description); var ret = JObject.Parse(res.Response).ToDiscordObject(); ret.Discord = this.Discord; return ret; } /// /// Modifies the guild sticker. /// /// The guild id. /// The sticker id. /// The name. /// The description. /// The tags. /// The reason. internal async Task ModifyGuildStickerAsync(ulong guildId, ulong stickerId, Optional name, Optional description, Optional tags, string reason) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.STICKERS}/:sticker_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {guild_id = guildId, sticker_id = stickerId}, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var pld = new RestStickerModifyPayload() { Name = name, Description = description, Tags = tags }; var values = new Dictionary { ["payload_json"] = DiscordJson.SerializeObject(pld) }; var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route); var ret = JObject.Parse(res.Response).ToDiscordObject(); ret.Discord = this.Discord; return null; } /// /// Deletes the guild sticker async. /// /// The guild id. /// The sticker id. /// The reason. internal async Task DeleteGuildStickerAsync(ulong guildId, ulong stickerId, string reason) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.STICKERS}/:sticker_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {guild_id = guildId, sticker_id = stickerId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } #endregion #region Application Commands /// /// Gets the global application commands. /// /// The application id. /// Whether to get the full localization dict. internal async Task> GetGlobalApplicationCommandsAsync(ulong applicationId, bool withLocalizations = false) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {application_id = applicationId }, out var path); var querydict = new Dictionary { ["with_localizations"] = withLocalizations.ToString().ToLower() }; var url = Utilities.GetApiUriFor(path, BuildQueryString(querydict), this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var ret = JsonConvert.DeserializeObject>(res.Response); foreach (var app in ret) app.Discord = this.Discord; return ret.ToList(); } /// /// Bulk overwrites the global application commands. /// /// The application id. /// The commands. internal async Task> BulkOverwriteGlobalApplicationCommandsAsync(ulong applicationId, IEnumerable commands) { var pld = new List(); foreach (var command in commands) { pld.Add(new RestApplicationCommandCreatePayload { Type = command.Type, Name = command.Name, Description = command.Description, Options = command.Options, NameLocalizations = command.NameLocalizations?.GetKeyValuePairs(), DescriptionLocalizations = command.DescriptionLocalizations?.GetKeyValuePairs(), DefaultMemberPermission = command.DefaultMemberPermissions, DmPermission = command.DmPermission/*, Nsfw = command.IsNsfw*/ }); } this.Discord.Logger.LogDebug(DiscordJson.SerializeObject(pld)); var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new {application_id = applicationId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(pld)); var ret = JsonConvert.DeserializeObject>(res.Response); foreach (var app in ret) app.Discord = this.Discord; return ret.ToList(); } /// /// Creates a global application command. /// /// The applicationid. /// The command. internal async Task CreateGlobalApplicationCommandAsync(ulong applicationId, DiscordApplicationCommand command) { var pld = new RestApplicationCommandCreatePayload { Type = command.Type, Name = command.Name, Description = command.Description, Options = command.Options, NameLocalizations = command.NameLocalizations.GetKeyValuePairs(), DescriptionLocalizations = command.DescriptionLocalizations.GetKeyValuePairs(), DefaultMemberPermission = command.DefaultMemberPermissions, DmPermission = command.DmPermission/*, Nsfw = command.IsNsfw*/ }; var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {application_id = applicationId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Gets a global application command. /// /// The application id. /// The command id. internal async Task GetGlobalApplicationCommandAsync(ulong applicationId, ulong commandId) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}/:command_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {application_id = applicationId, command_id = commandId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Edits a global application command. /// /// The application id. /// The command id. /// The name. /// The description. /// The options. /// The localizations of the name. /// The localizations of the description. /// The default member permissions. /// The dm permission. /// Whether this command is marked as NSFW. internal async Task EditGlobalApplicationCommandAsync(ulong applicationId, ulong commandId, Optional name, Optional description, Optional?> options, Optional nameLocalization, Optional descriptionLocalization, Optional defaultMemberPermission, Optional dmPermission, Optional isNsfw) { var pld = new RestApplicationCommandEditPayload { Name = name, Description = description, Options = options, DefaultMemberPermission = defaultMemberPermission, DmPermission = dmPermission, NameLocalizations = nameLocalization.Map(l => l.GetKeyValuePairs()).ValueOrDefault(), DescriptionLocalizations = descriptionLocalization.Map(l => l.GetKeyValuePairs()).ValueOrDefault()/*, Nsfw = isNsfw*/ }; var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}/:command_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {application_id = applicationId, command_id = commandId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Deletes a global application command. /// /// The application_id. /// The command_id. internal async Task DeleteGlobalApplicationCommandAsync(ulong applicationId, ulong commandId) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}/:command_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {application_id = applicationId, command_id = commandId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Gets the guild application commands. /// /// The application id. /// The guild id. /// Whether to get the full localization dict. internal async Task> GetGuildApplicationCommandsAsync(ulong applicationId, ulong guildId, bool withLocalizations = false) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {application_id = applicationId, guild_id = guildId }, out var path); var querydict = new Dictionary { ["with_localizations"] = withLocalizations.ToString().ToLower() }; var url = Utilities.GetApiUriFor(path, BuildQueryString(querydict), this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var ret = JsonConvert.DeserializeObject>(res.Response); foreach (var app in ret) app.Discord = this.Discord; return ret.ToList(); } /// /// Bulk overwrites the guild application commands. /// /// The application id. /// The guild id. /// The commands. internal async Task> BulkOverwriteGuildApplicationCommandsAsync(ulong applicationId, ulong guildId, IEnumerable commands) { var pld = new List(); foreach (var command in commands) { pld.Add(new RestApplicationCommandCreatePayload { Type = command.Type, Name = command.Name, Description = command.Description, Options = command.Options, NameLocalizations = command.NameLocalizations?.GetKeyValuePairs(), DescriptionLocalizations = command.DescriptionLocalizations?.GetKeyValuePairs(), DefaultMemberPermission = command.DefaultMemberPermissions, DmPermission = command.DmPermission/*, Nsfw = command.IsNsfw*/ }); } this.Discord.Logger.LogDebug(DiscordJson.SerializeObject(pld)); var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new {application_id = applicationId, guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(pld)); var ret = JsonConvert.DeserializeObject>(res.Response); foreach (var app in ret) app.Discord = this.Discord; return ret.ToList(); } /// /// Creates a guild application command. /// /// The application id. /// The guild id. /// The command. internal async Task CreateGuildApplicationCommandAsync(ulong applicationId, ulong guildId, DiscordApplicationCommand command) { var pld = new RestApplicationCommandCreatePayload { Type = command.Type, Name = command.Name, Description = command.Description, Options = command.Options, NameLocalizations = command.NameLocalizations.GetKeyValuePairs(), DescriptionLocalizations = command.DescriptionLocalizations.GetKeyValuePairs(), DefaultMemberPermission = command.DefaultMemberPermissions, DmPermission = command.DmPermission/*, Nsfw = command.IsNsfw*/ }; var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {application_id = applicationId, guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Gets a guild application command. /// /// The application id. /// The guild id. /// The command id. internal async Task GetGuildApplicationCommandAsync(ulong applicationId, ulong guildId, ulong commandId) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}/:command_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {application_id = applicationId, guild_id = guildId, command_id = commandId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Edits a guild application command. /// /// The application id. /// The guild id. /// The command id. /// The name. /// The description. /// The options. /// The localizations of the name. /// The localizations of the description. /// The default member permissions. /// The dm permission. /// Whether this command is marked as NSFW. internal async Task EditGuildApplicationCommandAsync(ulong applicationId, ulong guildId, ulong commandId, Optional name, Optional description, Optional?> options, Optional nameLocalization, Optional descriptionLocalization, Optional defaultMemberPermission, Optional dmPermission, Optional isNsfw) { var pld = new RestApplicationCommandEditPayload { Name = name, Description = description, Options = options, DefaultMemberPermission = defaultMemberPermission, DmPermission = dmPermission, NameLocalizations = nameLocalization.Map(l => l.GetKeyValuePairs()).ValueOrDefault(), DescriptionLocalizations = descriptionLocalization.Map(l => l.GetKeyValuePairs()).ValueOrDefault()/*, Nsfw = isNsfw*/ }; var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}/:command_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {application_id = applicationId, guild_id = guildId, command_id = commandId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Deletes a guild application command. /// /// The application id. /// The guild id. /// The command id. internal async Task DeleteGuildApplicationCommandAsync(ulong applicationId, ulong guildId, ulong commandId) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}/:command_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {application_id = applicationId, guild_id = guildId, command_id = commandId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } #region Permissions 2.1 /// /// Gets the guild application command permissions. /// /// The target application id. /// The target guild id. internal async Task> GetGuildApplicationCommandPermissionsAsync(ulong applicationId, ulong guildId) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}{Endpoints.PERMISSIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {application_id = applicationId, guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var ret = JsonConvert.DeserializeObject>(res.Response); foreach (var app in ret) app.Discord = this.Discord; return ret.ToList(); } /// /// Gets a guild application command permission. /// /// The target application id. /// The target guild id. /// The target command id. internal async Task GetGuildApplicationCommandPermissionAsync(ulong applicationId, ulong guildId, ulong commandId) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}/:command_id{Endpoints.PERMISSIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {application_id = applicationId, guild_id = guildId, command_id = commandId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } #endregion /// /// Creates the interaction response. /// /// The interaction id. /// The interaction token. /// The type. /// The builder. internal async Task CreateInteractionResponseAsync(ulong interactionId, string interactionToken, InteractionResponseType type, DiscordInteractionResponseBuilder builder) { if (builder?.Embeds != null) foreach (var embed in builder.Embeds) if (embed.Timestamp != null) embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); RestInteractionResponsePayload pld; if (type != InteractionResponseType.AutoCompleteResult) { var data = builder != null ? new DiscordInteractionApplicationCommandCallbackData { Content = builder.Content ?? null, Embeds = builder.Embeds ?? null, IsTts = builder.IsTts, Mentions = builder.Mentions ?? null, Flags = builder.IsEphemeral ? MessageFlags.Ephemeral : null, Components = builder.Components ?? null, Choices = null } : null; pld = new RestInteractionResponsePayload { Type = type, Data = data }; if (builder != null && builder.Files != null && builder.Files.Count > 0) { ulong fileId = 0; List attachments = new(); foreach (var file in builder.Files) { DiscordAttachment att = new() { Id = fileId, Discord = this.Discord, Description = file.Description, FileName = file.FileName, FileSize = null }; attachments.Add(att); fileId++; } pld.Attachments = attachments; pld.Data.Attachments = attachments; } } else { pld = new RestInteractionResponsePayload { Type = type, Data = new DiscordInteractionApplicationCommandCallbackData { Content = null, Embeds = null, IsTts = null, Mentions = null, Flags = null, Components = null, Choices = builder.Choices, Attachments = null }, Attachments = null }; } var values = new Dictionary(); if (builder != null) if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count > 0 || builder.IsTts == true || builder.Mentions != null || builder.Files?.Count > 0) values["payload_json"] = DiscordJson.SerializeObject(pld); var route = $"{Endpoints.INTERACTIONS}/:interaction_id/:interaction_token{Endpoints.CALLBACK}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {interaction_id = interactionId, interaction_token = interactionToken }, out var path); var url = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration).AddParameter("wait", "false").Build(); if (builder != null) { await this.DoMultipartAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, values: values, files: builder.Files); foreach (var file in builder.Files.Where(x => x.ResetPositionTo.HasValue)) { file.Stream.Position = file.ResetPositionTo.Value; } } else { await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)); } } /// /// Creates the interaction response. /// /// The interaction id. /// The interaction token. /// The type. /// The builder. internal async Task CreateInteractionModalResponseAsync(ulong interactionId, string interactionToken, InteractionResponseType type, DiscordInteractionModalBuilder builder) { if (builder.ModalComponents.Any(mc => mc.Components.Any(c => c.Type != ComponentType.InputText && c.Type != ComponentType.Select))) throw new NotSupportedException("Can't send any other type then Input Text or Select as Modal Component."); var pld = new RestInteractionModalResponsePayload { Type = type, Data = new DiscordInteractionApplicationCommandModalCallbackData { Title = builder.Title, CustomId = builder.CustomId, ModalComponents = builder.ModalComponents } }; var values = new Dictionary(); var route = $"{Endpoints.INTERACTIONS}/:interaction_id/:interaction_token{Endpoints.CALLBACK}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {interaction_id = interactionId, interaction_token = interactionToken }, out var path); var url = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration).AddParameter("wait", "true").Build(); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)); } /// /// Gets the original interaction response. /// /// The application id. /// The interaction token. internal Task GetOriginalInteractionResponseAsync(ulong applicationId, string interactionToken) => this.GetWebhookMessageAsync(applicationId, interactionToken, Endpoints.ORIGINAL, null); /// /// Edits the original interaction response. /// /// The application id. /// The interaction token. /// The builder. internal Task EditOriginalInteractionResponseAsync(ulong applicationId, string interactionToken, DiscordWebhookBuilder builder) => this.EditWebhookMessageAsync(applicationId, interactionToken, Endpoints.ORIGINAL, builder, null); /// /// Deletes the original interaction response. /// /// The application id. /// The interaction token. internal Task DeleteOriginalInteractionResponseAsync(ulong applicationId, string interactionToken) => this.DeleteWebhookMessageAsync(applicationId, interactionToken, Endpoints.ORIGINAL, null); /// /// Creates the followup message. /// /// The application id. /// The interaction token. /// The builder. internal async Task CreateFollowupMessageAsync(ulong applicationId, string interactionToken, DiscordFollowupMessageBuilder builder) { builder.Validate(); if (builder.Embeds != null) foreach (var embed in builder.Embeds) if (embed.Timestamp != null) embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); var values = new Dictionary(); var pld = new RestFollowupMessageCreatePayload { Content = builder.Content, IsTts = builder.IsTts, Embeds = builder.Embeds, Flags = builder.Flags, Components = builder.Components }; if (builder.Files != null && builder.Files.Count > 0) { ulong fileId = 0; List attachments = new(); foreach (var file in builder.Files) { DiscordAttachment att = new() { Id = fileId, Discord = this.Discord, Description = file.Description, FileName = file.FileName, FileSize = null }; attachments.Add(att); fileId++; } pld.Attachments = attachments; } if (builder.Mentions != null) pld.Mentions = new DiscordMentions(builder.Mentions, builder.Mentions.Any()); if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count > 0 || builder.IsTts == true || builder.Mentions != null || builder.Files?.Count > 0) values["payload_json"] = DiscordJson.SerializeObject(pld); var route = $"{Endpoints.WEBHOOKS}/:application_id/:interaction_token"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {application_id = applicationId, interaction_token = interactionToken }, out var path); var url = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration).AddParameter("wait", "true").Build(); var res = await this.DoMultipartAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, values: values, files: builder.Files).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); foreach (var att in ret.AttachmentsInternal) { att.Discord = this.Discord; } foreach (var file in builder.Files.Where(x => x.ResetPositionTo.HasValue)) { file.Stream.Position = file.ResetPositionTo.Value; } ret.Discord = this.Discord; return ret; } /// /// Gets the followup message. /// /// The application id. /// The interaction token. /// The message id. internal Task GetFollowupMessageAsync(ulong applicationId, string interactionToken, ulong messageId) => this.GetWebhookMessageAsync(applicationId, interactionToken, messageId); /// /// Edits the followup message. /// /// The application id. /// The interaction token. /// The message id. /// The builder. internal Task EditFollowupMessageAsync(ulong applicationId, string interactionToken, ulong messageId, DiscordWebhookBuilder builder) => this.EditWebhookMessageAsync(applicationId, interactionToken, messageId.ToString(), builder, null); /// /// Deletes the followup message. /// /// The application id. /// The interaction token. /// The message id. internal Task DeleteFollowupMessageAsync(ulong applicationId, string interactionToken, ulong messageId) => this.DeleteWebhookMessageAsync(applicationId, interactionToken, messageId); #endregion #region Misc /// /// Gets the current application info async. /// internal Task GetCurrentApplicationInfoAsync() => this.GetApplicationInfoAsync("@me"); /// /// Gets the application info async. /// /// The application_id. internal Task GetApplicationInfoAsync(ulong applicationId) => this.GetApplicationInfoAsync(applicationId.ToString(CultureInfo.InvariantCulture)); /// /// Gets the application info async. /// /// The application_id. private async Task GetApplicationInfoAsync(string applicationId) { var route = $"{Endpoints.OAUTH2}{Endpoints.APPLICATIONS}/:application_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {application_id = applicationId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); return JsonConvert.DeserializeObject(res.Response); } /// /// Gets the application assets async. /// /// The application. internal async Task> GetApplicationAssetsAsync(DiscordApplication application) { var route = $"{Endpoints.OAUTH2}{Endpoints.APPLICATIONS}/:application_id{Endpoints.ASSETS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { application_id = application.Id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var assets = JsonConvert.DeserializeObject>(res.Response); foreach (var asset in assets) { asset.Discord = application.Discord; asset.Application = application; } return new ReadOnlyCollection(new List(assets)); } /// /// Gets the gateway info async. /// internal async Task GetGatewayInfoAsync() { var headers = Utilities.GetBaseHeaders(); var route = Endpoints.GATEWAY; if (this.Discord.Configuration.TokenType == TokenType.Bot) route += Endpoints.BOT; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route, headers).ConfigureAwait(false); var info = JObject.Parse(res.Response).ToObject(); info.SessionBucket.ResetAfter = DateTimeOffset.UtcNow + TimeSpan.FromMilliseconds(info.SessionBucket.ResetAfterInternal); return info; } #endregion }