diff --git a/DisCatSharp/Clients/DiscordClient.Dispatch.cs b/DisCatSharp/Clients/DiscordClient.Dispatch.cs index 93b47c66c..337866de7 100644 --- a/DisCatSharp/Clients/DiscordClient.Dispatch.cs +++ b/DisCatSharp/Clients/DiscordClient.Dispatch.cs @@ -1,3049 +1,3051 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Utilities; 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 client. /// public sealed partial class DiscordClient { #region Private Fields private string _sessionId; private bool _guildDownloadCompleted = false; #endregion #region Dispatch Handler /// /// Handles the dispatch. /// /// 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(this.ServiceProvider) { EventName = payload.EventName, PayloadObject = dat }).ConfigureAwait(false); DiscordChannel chn; ulong gid; ulong cid; DiscordStageInstance stg = default; DiscordIntegration itg = default; DiscordThreadChannel trd = default; DiscordThreadChannelMember trdm = default; DiscordEvent gse = default; TransportUser usr = default; TransportMember mbr = default; TransportUser refUsr = default; TransportMember refMbr = default; JToken rawMbr = default; var rawRefMsg = dat["referenced_message"]; 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._guilds[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._guilds[gid], ems).ConfigureAwait(false); break; case "guild_stickers_update": var strs = dat["stickers"].ToDiscordObject>(); await this.OnStickersUpdatedAsync(strs, dat).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._guilds.ContainsKey(gid)) return; await this.OnGuildIntegrationsUpdateEventAsync(this._guilds[gid]).ConfigureAwait(false); break; /* Ok soooo.. this isn't documented yet It seems to be part of the next version of membership screening (https://discord.com/channels/641574644578648068/689591708962652289/845836910991507486) Previews: https://github.com/DSharpPlus/DSharpPlus/pull/890#issuecomment-846464105 advaith said the following (https://discord.com/channels/641574644578648068/689591708962652289/845838160047112202): > iirc it happens when a user leaves a server where they havent completed screening yet We have to wait till it's documented, but the fields are: { "user_id": "snowflake_user", "guild_id": "snowflake_guild" } We could handle it rn, but due to the fact that it isn't documented, it's not an good idea. */ 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._guilds[gid]).ConfigureAwait(false); break; case "guild_ban_remove": usr = dat["user"].ToObject(); gid = (ulong)dat["guild_id"]; await this.OnGuildBanRemoveEventAsync(usr, this._guilds[gid]).ConfigureAwait(false); break; #endregion #region Guild Event case "guild_scheduled_event_create": gse = dat.ToObject(); await this.OnGuildScheduledEventCreateEventAsync(gse).ConfigureAwait(false); break; case "guild_scheduled_event_update": gse = dat.ToObject(); await this.OnGuildScheduledEventUpdateEventAsync(gse).ConfigureAwait(false); break; case "guild_scheduled_event_delete": gse = dat.ToObject(); await this.OnGuildScheduledEventDeleteEventAsync(gse).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._guilds.ContainsKey(gid)) return; await this.OnGuildIntegrationCreateEventAsync(this._guilds[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._guilds.ContainsKey(gid)) return; await this.OnGuildIntegrationUpdateEventAsync(this._guilds[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._guilds.ContainsKey(gid)) return; await this.OnGuildIntegrationDeleteEventAsync(this._guilds[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._guilds[gid]).ConfigureAwait(false); break; case "guild_member_remove": gid = (ulong)dat["guild_id"]; usr = dat["user"].ToObject(); if (!this._guilds.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._guilds[gid]).ConfigureAwait(false); break; case "guild_member_update": gid = (ulong)dat["guild_id"]; await this.OnGuildMemberUpdateEventAsync(dat.ToDiscordObject(), this._guilds[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._guilds[gid]).ConfigureAwait(false); break; case "guild_role_update": gid = (ulong)dat["guild_id"]; await this.OnGuildRoleUpdateEventAsync(dat["role"].ToObject(), this._guilds[gid]).ConfigureAwait(false); break; case "guild_role_delete": gid = (ulong)dat["guild_id"]; await this.OnGuildRoleDeleteEventAsync((ulong)dat["role_id"], this._guilds[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._guilds[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._guilds[gid], (ulong)dat["id"], dat["added_members"]?.ToObject>(), dat["removed_member_ids"]?.ToObject>(), (int)dat["member_count"]).ConfigureAwait(false); + await this.OnThreadMembersUpdateEventAsync(this._guilds[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._guilds[gid], cid, (JObject)dat["users"], (ulong)dat["embedded_activity"]["application_id"]).ConfigureAwait(false); + await this.OnEmbeddedActivityUpdateAsync((JObject)dat["embedded_activity"], this._guilds[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._guilds[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"]; await this.OnInteractionCreateAsync((ulong?)dat["guild_id"], cid, usr, mbr, dat.ToDiscordObject()).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 "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._guilds[gid].GetChannel(cid), this._guilds[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. /// 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.GatewayVersion = ready.GatewayVersion; this._sessionId = ready.SessionId; var raw_guild_index = rawGuilds.ToDictionary(xt => (ulong)xt["id"], xt => (JObject)xt); this._guilds.Clear(); foreach (var guild in ready.Guilds) { guild.Discord = this; if (guild._channels == null) guild._channels = new ConcurrentDictionary(); foreach (var xc in guild.Channels.Values) { xc.GuildId = guild.Id; xc.Discord = this; foreach (var xo in xc._permissionOverwrites) { xo.Discord = this; xo._channel_id = xc.Id; } } if (guild._roles == null) guild._roles = new ConcurrentDictionary(); foreach (var xr in guild.Roles.Values) { xr.Discord = this; xr._guild_id = guild.Id; } var raw_guild = raw_guild_index[guild.Id]; var raw_members = (JArray)raw_guild["members"]; if (guild._members != null) guild._members.Clear(); else guild._members = new ConcurrentDictionary(); if (raw_members != null) { foreach (var xj in raw_members) { 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._members[xtm.User.Id] = new DiscordMember(xtm) { Discord = this, _guild_id = guild.Id }; } } if (guild._emojis == null) guild._emojis = new ConcurrentDictionary(); foreach (var xe in guild.Emojis.Values) xe.Discord = this; if (guild._voiceStates == null) guild._voiceStates = new ConcurrentDictionary(); foreach (var xvs in guild.VoiceStates.Values) xvs.Discord = this; this._guilds[guild.Id] = guild; } await this._ready.InvokeAsync(this, new ReadyEventArgs(this.ServiceProvider)).ConfigureAwait(false); } /// /// Handles the resumed. /// 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.Discord = this; foreach (var xo in channel._permissionOverwrites) { xo.Discord = this; xo._channel_id = channel.Id; } this._guilds[channel.GuildId.Value]._channels[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 channel_new = this.InternalGetCachedChannel(channel.Id); DiscordChannel channel_old = null; if (channel_new != null) { channel_old = new DiscordChannel { Bitrate = channel_new.Bitrate, Discord = this, GuildId = channel_new.GuildId, Id = channel_new.Id, //IsPrivate = channel_new.IsPrivate, LastMessageId = channel_new.LastMessageId, Name = channel_new.Name, _permissionOverwrites = new List(channel_new._permissionOverwrites), Position = channel_new.Position, Topic = channel_new.Topic, Type = channel_new.Type, UserLimit = channel_new.UserLimit, ParentId = channel_new.ParentId, IsNSFW = channel_new.IsNSFW, PerUserRateLimit = channel_new.PerUserRateLimit, RtcRegionId = channel_new.RtcRegionId, QualityMode = channel_new.QualityMode, DefaultAutoArchiveDuration = channel_new.DefaultAutoArchiveDuration }; channel_new.Bitrate = channel.Bitrate; channel_new.Name = channel.Name; channel_new.Position = channel.Position; channel_new.Topic = channel.Topic; channel_new.UserLimit = channel.UserLimit; channel_new.ParentId = channel.ParentId; channel_new.IsNSFW = channel.IsNSFW; channel_new.PerUserRateLimit = channel.PerUserRateLimit; channel_new.Type = channel.Type; channel_new.RtcRegionId = channel.RtcRegionId; channel_new.QualityMode = channel.QualityMode; channel_new.DefaultAutoArchiveDuration = channel.DefaultAutoArchiveDuration; channel_new._permissionOverwrites.Clear(); foreach (var po in channel._permissionOverwrites) { po.Discord = this; po._channel_id = channel.Id; } channel_new._permissionOverwrites.AddRange(channel._permissionOverwrites); if (this.Configuration.AutoRefreshChannelCache && gld != null) { await this.RefreshChannelsAsync(channel.Guild.Id); } } else if (gld != null) { gld._channels[channel.Id] = channel; if (this.Configuration.AutoRefreshChannelCache) { await this.RefreshChannelsAsync(channel.Guild.Id); } } await this._channelUpdated.InvokeAsync(this, new ChannelUpdateEventArgs(this.ServiceProvider) { ChannelAfter = channel_new, Guild = gld, ChannelBefore = channel_old }).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._channels.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._channels.Clear(); foreach (var channel in channels.ToList()) { channel.Discord = this; foreach (var xo in channel._permissionOverwrites) { xo.Discord = this; xo._channel_id = channel.Id; } guild._channels[channel.Id] = channel; } } /// /// Handles the channel pins update. /// /// The guild id. /// The channel id. /// The 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 = new DiscordActivity[xp.RawActivities.Length]; for (var i = 0; i < xp.RawActivities.Length; i++) xp._internalActivities[i] = new DiscordActivity(xp.RawActivities[i]); } this._presences[xp.InternalUser.Id] = xp; } } var exists = this._guilds.TryGetValue(guild.Id, out var foundGuild); guild.Discord = this; guild.IsUnavailable = false; var eventGuild = guild; if (exists) guild = foundGuild; if (guild._channels == null) guild._channels = new ConcurrentDictionary(); if(guild._threads == null) guild._threads = new ConcurrentDictionary(); if (guild._roles == null) guild._roles = new ConcurrentDictionary(); if (guild._threads == null) guild._threads = new ConcurrentDictionary(); if (guild._stickers == null) guild._stickers = new ConcurrentDictionary(); if (guild._emojis == null) guild._emojis = new ConcurrentDictionary(); if (guild._voiceStates == null) guild._voiceStates = new ConcurrentDictionary(); if (guild._members == null) guild._members = new ConcurrentDictionary(); if (guild._scheduledEvents == null) guild._scheduledEvents = new ConcurrentDictionary(); this.UpdateCachedGuild(eventGuild, rawMembers); guild.JoinedAt = eventGuild.JoinedAt; guild.IsLarge = eventGuild.IsLarge; guild.MemberCount = Math.Max(eventGuild.MemberCount, guild._members.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._voiceStates) guild._voiceStates[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild._channels) guild._channels[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild._roles) guild._roles[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild._emojis) guild._emojis[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild._threads) guild._threads[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild._stickers) guild._stickers[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild._stageInstances) guild._stageInstances[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild._scheduledEvents) guild._scheduledEvents[kvp.Key] = kvp.Value; foreach (var xc in guild._channels.Values) { xc.GuildId = guild.Id; xc.Discord = this; foreach (var xo in xc._permissionOverwrites) { xo.Discord = this; xo._channel_id = xc.Id; } } foreach(var xt in guild._threads.Values) { xt.GuildId = guild.Id; xt.Discord = this; } foreach (var xe in guild._emojis.Values) xe.Discord = this; foreach (var xs in guild._stickers.Values) xs.Discord = this; foreach (var xvs in guild._voiceStates.Values) xvs.Discord = this; foreach (var xsi in guild._stageInstances.Values) { xsi.Discord = this; xsi.GuildId = guild.Id; } foreach (var xr in guild._roles.Values) { xr.Discord = this; xr._guild_id = guild.Id; } foreach (var xse in guild._scheduledEvents.Values) { xse.Discord = this; xse.GuildId = guild.Id; } var old = Volatile.Read(ref this._guildDownloadCompleted); var dcompl = this._guilds.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._guilds.ContainsKey(guild.Id)) { this._guilds[guild.Id] = guild; oldGuild = null; } else { var gld = this._guilds[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, _channels = new ConcurrentDictionary(), _threads = new ConcurrentDictionary(), _emojis = new ConcurrentDictionary(), _stickers = new ConcurrentDictionary(), _members = new ConcurrentDictionary(), _roles = new ConcurrentDictionary(), _stageInstances = new ConcurrentDictionary(), _voiceStates = new ConcurrentDictionary(), _scheduledEvents = new ConcurrentDictionary() }; foreach (var kvp in gld._channels) oldGuild._channels[kvp.Key] = kvp.Value; foreach (var kvp in gld._threads) oldGuild._threads[kvp.Key] = kvp.Value; foreach (var kvp in gld._emojis) oldGuild._emojis[kvp.Key] = kvp.Value; foreach (var kvp in gld._stickers) oldGuild._stickers[kvp.Key] = kvp.Value; foreach (var kvp in gld._roles) oldGuild._roles[kvp.Key] = kvp.Value; foreach (var kvp in gld._voiceStates) oldGuild._voiceStates[kvp.Key] = kvp.Value; foreach (var kvp in gld._members) oldGuild._members[kvp.Key] = kvp.Value; foreach (var kvp in gld._stageInstances) oldGuild._stageInstances[kvp.Key] = kvp.Value; foreach (var kvp in gld._scheduledEvents) oldGuild._scheduledEvents[kvp.Key] = kvp.Value; } guild.Discord = this; guild.IsUnavailable = false; var eventGuild = guild; guild = this._guilds[eventGuild.Id]; if (guild._channels == null) guild._channels = new ConcurrentDictionary(); if (guild._threads == null) guild._threads = new ConcurrentDictionary(); if (guild._roles == null) guild._roles = new ConcurrentDictionary(); if (guild._emojis == null) guild._emojis = new ConcurrentDictionary(); if (guild._stickers == null) guild._stickers = new ConcurrentDictionary(); if (guild._voiceStates == null) guild._voiceStates = new ConcurrentDictionary(); if (guild._stageInstances == null) guild._stageInstances = new ConcurrentDictionary(); if (guild._members == null) guild._members = new ConcurrentDictionary(); if (guild._scheduledEvents == null) guild._scheduledEvents = new ConcurrentDictionary(); this.UpdateCachedGuild(eventGuild, rawMembers); foreach (var xc in guild._channels.Values) { xc.GuildId = guild.Id; xc.Discord = this; foreach (var xo in xc._permissionOverwrites) { xo.Discord = this; xo._channel_id = xc.Id; } } foreach (var xc in guild._threads.Values) { xc.GuildId = guild.Id; xc.Discord = this; } foreach (var xe in guild._emojis.Values) xe.Discord = this; foreach (var xs in guild._stickers.Values) xs.Discord = this; foreach (var xvs in guild._voiceStates.Values) xvs.Discord = this; foreach (var xr in guild._roles.Values) { xr.Discord = this; xr._guild_id = guild.Id; } foreach (var xsi in guild._stageInstances.Values) { xsi.Discord = this; xsi.GuildId = guild.Id; } foreach (var xse in guild._scheduledEvents.Values) { xse.Discord = this; xse.GuildId = guild.Id; } 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._guilds.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._guilds.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. /// If true, is large. /// 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._presences[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._emojis); guild._emojis.Clear(); foreach (var emoji in newEmojis) { emoji.Discord = this; guild._emojis[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 raw. internal async Task OnStickersUpdatedAsync(IEnumerable newStickers, JObject raw) { var guild = this.InternalGetCachedGuild((ulong)raw["guild_id"]); var oldStickers = new ConcurrentDictionary(guild._stickers); guild._stickers.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._stickers[nst.Id] = nst; } var sea = new GuildStickersUpdateEventArgs(this.ServiceProvider) { Guild = guild, StickersBefore = oldStickers, StickersAfter = guild.Stickers }; await this._guildStickersUpdated.InvokeAsync(this, sea).ConfigureAwait(false); } /// /// Handles the 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); } #endregion #region Guild Ban /// /// Handles the guild ban add event. /// /// The 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, _guild_id = 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 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, _guild_id = guild.Id }; var ea = new GuildBanRemoveEventArgs(this.ServiceProvider) { Guild = guild, Member = mbr }; await this._guildBanRemoved.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Guild Event /// /// Dispatches the event. /// /// The created event. internal async Task OnGuildScheduledEventCreateEventAsync(DiscordEvent scheduled_event) { scheduled_event.Discord = this; var guild = this.InternalGetCachedGuild(scheduled_event.GuildId); guild._scheduledEvents[scheduled_event.Id] = scheduled_event; await this._guildScheduledEventCreated.InvokeAsync(this, new GuildScheduledEventCreateEventArgs(this.ServiceProvider) { ScheduledEvent = scheduled_event, Guild = scheduled_event.Guild }).ConfigureAwait(false); } /// /// Dispatches the event. /// /// The updated event. internal async Task OnGuildScheduledEventUpdateEventAsync(DiscordEvent scheduled_event) { scheduled_event.Discord = this; var guild = this.InternalGetCachedGuild(scheduled_event.GuildId); guild._scheduledEvents[scheduled_event.Id] = scheduled_event; await this._guildScheduledEventUpdated.InvokeAsync(this, new GuildScheduledEventUpdateEventArgs(this.ServiceProvider) { ScheduledEvent = scheduled_event, Guild = scheduled_event.Guild }).ConfigureAwait(false); } /// /// Dispatches the event. /// /// The deleted event. internal async Task OnGuildScheduledEventDeleteEventAsync(DiscordEvent scheduled_event) { scheduled_event.Discord = this; var guild = this.InternalGetCachedGuild(scheduled_event.GuildId); guild._scheduledEvents[scheduled_event.Id] = scheduled_event; await this._guildScheduledEventDeleted.InvokeAsync(this, new GuildScheduledEventDeleteEventArgs(this.ServiceProvider) { ScheduledEvent = scheduled_event, Guild = scheduled_event.Guild }).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 integration delete event. /// /// The guild. /// The integration_id. /// The application_id. internal async Task OnGuildIntegrationDeleteEventAsync(DiscordGuild guild, ulong integration_id, ulong? application_id) => await this._guildIntegrationDeleted.InvokeAsync(this, new GuildIntegrationDeleteEventArgs(this.ServiceProvider) { Guild = guild, IntegrationId = integration_id, ApplicationId = application_id }).ConfigureAwait(false); #endregion #region Guild Member /// /// Handles the guild member add event. /// /// The 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, _guild_id = guild.Id }; guild._members[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 user. /// The guild. internal async Task OnGuildMemberRemoveEventAsync(TransportUser user, DiscordGuild guild) { var usr = new DiscordUser(user); if (!guild._members.TryRemove(user.Id, out var mbr)) mbr = new DiscordMember(usr) { Discord = this, _guild_id = 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 member. /// The guild. /// The roles. /// The nick. /// If true, 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, _guild_id = guild.Id }; var nick_old = mbr.Nickname; var pending_old = mbr.IsPending; var roles_old = new ReadOnlyCollection(new List(mbr.Roles)); mbr._avatarHash = member.AvatarHash; mbr.GuildAvatarHash = member.GuildAvatarHash; mbr.Nickname = nick; mbr.IsPending = pending; mbr._role_ids.Clear(); mbr._role_ids.AddRange(roles); var ea = new GuildMemberUpdateEventArgs(this.ServiceProvider) { Guild = guild, Member = mbr, NicknameAfter = mbr.Nickname, RolesAfter = new ReadOnlyCollection(new List(mbr.Roles)), PendingAfter = mbr.IsPending, NicknameBefore = nick_old, RolesBefore = roles_old, PendingBefore = pending_old, }; await this._guildMemberUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the guild members chunk event. /// /// The dat. 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(); var memCount = members.Count(); for (var i = 0; i < memCount; i++) { var mbr = new DiscordMember(members[i]) { Discord = this, _guild_id = guild.Id }; if (!this.UserCache.ContainsKey(mbr.Id)) this.UserCache[mbr.Id] = new DiscordUser(members[i].User) { Discord = this }; guild._members[mbr.Id] = mbr; mbrs.Add(mbr); } guild.MemberCount = guild._members.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.Count(); for (var i = 0; i < presCount; i++) { var xp = presences[i]; xp.Discord = this; xp.Activity = new DiscordActivity(xp.RawActivity); if (xp.RawActivities != null) { xp._internalActivities = new DiscordActivity[xp.RawActivities.Length]; for (var j = 0; j < xp.RawActivities.Length; j++) xp._internalActivities[j] = new DiscordActivity(xp.RawActivities[j]); } pres.Add(xp); } 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._guild_id = guild.Id; guild._roles[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 { _guild_id = guild.Id, _color = newRole._color, Discord = this, IsHoisted = newRole.IsHoisted, Id = newRole.Id, IsManaged = newRole.IsManaged, IsMentionable = newRole.IsMentionable, Name = newRole.Name, Permissions = newRole.Permissions, Position = newRole.Position }; newRole._guild_id = guild.Id; newRole._color = role._color; newRole.IsHoisted = role.IsHoisted; newRole.IsManaged = role.IsManaged; newRole.IsMentionable = role.IsMentionable; newRole.Name = role.Name; newRole.Permissions = role.Permissions; newRole.Position = role.Position; 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._roles.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 dat. 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 ack event. /// /// The chn. /// 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 author. /// The member. /// The reference author. /// The reference 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._mentionedUsers), MentionedRoles = message._mentionedRoles != null ? new ReadOnlyCollection(message._mentionedRoles) : null, MentionedChannels = message._mentionedChannels != null ? new ReadOnlyCollection(message._mentionedChannels) : null }; await this._messageCreated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the message update event. /// /// The message. /// The author. /// The member. /// The reference author. /// The reference member. internal async Task OnMessageUpdateEventAsync(DiscordMessage message, TransportUser author, TransportMember member, TransportUser referenceAuthor, TransportMember referenceMember) { DiscordGuild guild; message.Discord = this; var event_message = message; DiscordMessage oldmsg = null; if (this.Configuration.MessageCacheSize == 0 || this.MessageCache == null || !this.MessageCache.TryGet(xm => xm.Id == event_message.Id && xm.ChannelId == event_message.ChannelId, out message)) { message = event_message; 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 = event_message.EditedTimestampRaw; if (event_message.Content != null) message.Content = event_message.Content; message._embeds.Clear(); message._embeds.AddRange(event_message._embeds); message.Pinned = event_message.Pinned; message.IsTTS = event_message.IsTTS; } message.PopulateMentions(); var ea = new MessageUpdateEventArgs(this.ServiceProvider) { Message = message, MessageBefore = oldmsg, MentionedUsers = new ReadOnlyCollection(message._mentionedUsers), MentionedRoles = message._mentionedRoles != null ? new ReadOnlyCollection(message._mentionedRoles) : null, MentionedChannels = message._mentionedChannels != null ? new ReadOnlyCollection(message._mentionedChannels) : null }; await this._messageUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the message delete event. /// /// The message id. /// The channel id. /// The 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 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. /// /// The user id. /// The message id. /// The channel id. /// The guild id. /// The mbr. /// 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, _reactions = new List() }; } var react = msg._reactions.FirstOrDefault(xr => xr.Emoji == emoji); if (react == null) { msg._reactions.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. /// /// 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, _guild_id = 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._reactions?.FirstOrDefault(xr => xr.Emoji == emoji); if (react != null) { react.Count--; react.IsMe &= this.CurrentUser.Id != userId; if (msg._reactions != null && react.Count <= 0) // shit happens for (var i = 0; i < msg._reactions.Count; i++) if (msg._reactions[i].Emoji == emoji) { msg._reactions.RemoveAt(i); break; } } 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 all. /// /// The message id. /// The channel id. /// The 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._reactions?.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 emoji. /// /// The message id. /// The channel id. /// The guild id. /// The dat. 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._emojis.TryGetValue(partialEmoji.Id, out var emoji)) { emoji = partialEmoji; emoji.Discord = this; } msg._reactions?.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 /// /// Dispatches the event. /// /// The created stage instance. internal async Task OnStageInstanceCreateEventAsync(DiscordStageInstance stage) { stage.Discord = this; var guild = this.InternalGetCachedGuild(stage.GuildId); guild._stageInstances[stage.Id] = stage; await this._stageInstanceCreated.InvokeAsync(this, new StageInstanceCreateEventArgs(this.ServiceProvider) { StageInstance = stage, Guild = stage.Guild }).ConfigureAwait(false); } /// /// Dispatches the event. /// /// The updated stage instance. internal async Task OnStageInstanceUpdateEventAsync(DiscordStageInstance stage) { stage.Discord = this; var guild = this.InternalGetCachedGuild(stage.GuildId); guild._stageInstances[stage.Id] = stage; await this._stageInstanceUpdated.InvokeAsync(this, new StageInstanceUpdateEventArgs(this.ServiceProvider) { StageInstance = stage, Guild = stage.Guild }).ConfigureAwait(false); } /// /// Dispatches the event. /// /// The deleted stage instance. internal async Task OnStageInstanceDeleteEventAsync(DiscordStageInstance stage) { stage.Discord = this; var guild = this.InternalGetCachedGuild(stage.GuildId); guild._stageInstances[stage.Id] = stage; await this._stageInstanceDeleted.InvokeAsync(this, new StageInstanceDeleteEventArgs(this.ServiceProvider) { StageInstance = stage, Guild = stage.Guild }).ConfigureAwait(false); } #endregion #region Thread /// /// Dispatches the event. /// /// The created thread. internal async Task OnThreadCreateEventAsync(DiscordThreadChannel thread) { thread.Discord = this; this.InternalGetCachedGuild(thread.GuildId)._threads.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); } /// /// Dispatches the 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, _threadMembers = threadNew._threadMembers, 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 }; 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; 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._threads[thread.Id] = thread; } await this._threadUpdated.InvokeAsync(this, updateEvent).ConfigureAwait(false); } /// /// Dispatches the event. /// /// The deleted thread. internal async Task OnThreadDeleteEventAsync(DiscordThreadChannel thread) { if (thread == null) return; thread.Discord = this; var gld = thread.Guild; if (gld._threads.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); } /// /// Dispatches the event. /// /// The synced guild. /// The synced channel ids. /// The synced threads. /// The synced members. internal async Task OnThreadListSyncEventAsync(DiscordGuild guild, IReadOnlyList channel_ids, IReadOnlyList threads, IReadOnlyList members) { guild.Discord = this; var channels = channel_ids.Select(x => guild.GetChannel(x.Value)); //getting channel objects foreach (var chan in channels) { chan.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); } /// /// Dispatches the event. /// /// The updated member. internal async Task OnThreadMemberUpdateEventAsync(DiscordThreadChannelMember member) { member.Discord = this; var thread = this.InternalGetCachedThread(member.Id); thread.CurrentMember = member; thread.Guild._threads.AddOrUpdate(member.Id, thread, (oldThread, newThread) => newThread); await this._threadMemberUpdated.InvokeAsync(this, new ThreadMemberUpdateEventArgs(this.ServiceProvider) { ThreadMember = member, Thread = thread }).ConfigureAwait(false); } /// /// Dispatches the 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 added members. + /// The ids of the removed members. /// The new member count. - internal async Task OnThreadMembersUpdateEventAsync(DiscordGuild guild, ulong thread_id, IReadOnlyList addedMembers, IReadOnlyList removed_member_ids, int member_count) + internal async Task OnThreadMembersUpdateEventAsync(DiscordGuild guild, ulong thread_id, JArray added_members, JArray removed_members, int member_count) { var thread = this.InternalGetCachedThread(thread_id); thread.Discord = this; guild.Discord = this; + List addedMembers = null; + List removed_member_ids = null; - var removedMembers = new List(); - if (removed_member_ids != null) + if (added_members != null) { - foreach (var removedId in removed_member_ids) + foreach (var xj in added_members) { - removedMembers.Add(guild._members.TryGetValue(removedId.Value, out var member) ? member : new DiscordMember { Id = removedId.Value, _guild_id = guild.Id, Discord = this }); + var xtm = xj.ToDiscordObject(); + xtm.Discord = this; + xtm._guild_id = guild.Id; + xtm.Member = guild._members.TryGetValue(xtm.Id, out var member) ? member : new DiscordMember { Id = xtm.Id, _guild_id = guild.Id, Discord = this }; + addedMembers.Add(xtm); + + if (xtm.Id == this.CurrentUser.Id) + thread.CurrentMember = xtm; } } - else - removed_member_ids = Array.Empty(); - if (addedMembers != null) + + var removedMembers = new List(); + if (removed_members != null) { - foreach (var threadMember in addedMembers) + foreach (var removedId in removed_members) { - threadMember.Discord = this; - threadMember._guild_id = guild.Id; - - if (threadMember.Id == this.CurrentUser.Id) - thread.CurrentMember = threadMember; + removedMembers.Add(guild._members.TryGetValue((ulong)removedId, out var member) ? member : new DiscordMember { Id = (ulong)removedId, _guild_id = guild.Id, Discord = this }); } } - else - addedMembers = Array.Empty(); if (removed_member_ids.Contains(this.CurrentUser.Id)) //indicates the bot was removed from the thread thread.CurrentMember = null; + thread.MemberCount = member_count; var threadMembersUpdateArg = new ThreadMembersUpdateEventArgs(this.ServiceProvider) { Guild = guild, Thread = thread, AddedMembers = addedMembers, RemovedMembers = removedMembers, MemberCount = member_count }; 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. /// A Task. - internal async Task OnEmbeddedActivityUpdateAsync(JObject tr_activity, DiscordGuild guild, ulong channel_id, JObject j_users, ulong app_id) + internal async Task OnEmbeddedActivityUpdateAsync(JObject tr_activity, DiscordGuild guild, ulong channel_id, JArray j_users, ulong app_id) { - try + /*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, - EmbeddedActivityBefore = old, - EmbeddedActivityAfter = activity, Users = activity_users, Channel = channel }; await this._embeddedActivityUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } catch (Exception ex) { this.Logger.LogError(ex, ex.Message); - } + }*/ + await Task.Delay(20); } #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._presences.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._presences[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, PresenceBefore = old, PresenceAfter = presence, UserBefore = old != null ? new DiscordUser(old.InternalUser) : usrafter, UserAfter = usrafter }; await this._presenceUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the user settings update event. /// /// The 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 user. internal async Task OnUserUpdateEventAsync(TransportUser user) { var usr_old = 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 = usr_old }; await this._userUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Voice /// /// Handles the voice state update event. /// /// The raw. internal async Task OnVoiceStateUpdateEventAsync(JObject raw) { var gid = (ulong)raw["guild_id"]; var uid = (ulong)raw["user_id"]; var gld = this._guilds[gid]; var vstateNew = raw.ToObject(); vstateNew.Discord = this; gld._voiceStates.TryRemove(uid, out var vstateOld); if (vstateNew.Channel != null) { gld._voiceStates[vstateNew.UserId] = vstateNew; } if (gld._members.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 endpoint. /// The 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. /// /// The cmd. /// The guild_id. internal async Task OnApplicationCommandCreateAsync(DiscordApplicationCommand cmd, ulong? guild_id) { cmd.Discord = this; var guild = this.InternalGetCachedGuild(guild_id); if (guild == null && guild_id.HasValue) { guild = new DiscordGuild { Id = guild_id.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. /// /// The cmd. /// The guild_id. internal async Task OnApplicationCommandUpdateAsync(DiscordApplicationCommand cmd, ulong? guild_id) { cmd.Discord = this; var guild = this.InternalGetCachedGuild(guild_id); if (guild == null && guild_id.HasValue) { guild = new DiscordGuild { Id = guild_id.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. /// /// The cmd. /// The guild_id. internal async Task OnApplicationCommandDeleteAsync(DiscordApplicationCommand cmd, ulong? guild_id) { cmd.Discord = this; var guild = this.InternalGetCachedGuild(guild_id); if (guild == null && guild_id.HasValue) { guild = new DiscordGuild { Id = guild_id.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. /// /// The count. /// The count. /// The count. /// The guild_id. /// Count of application commands. internal async Task OnGuildApplicationCommandCountsUpdateAsync(int sc, int ucmc, int mcmc, ulong guild_id) { var guild = this.InternalGetCachedGuild(guild_id); if (guild == null) { guild = new DiscordGuild { Id = guild_id, Discord = this }; } var ea = new GuildApplicationCommandCountEventArgs(this.ServiceProvider) { SlashCommands = sc, UserContextMenuCommands = ucmc, MessageContextMenuCommands = mcmc, Guild = guild }; await this._guildApplicationCommandCountUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the application command permissions update. /// /// The new permissions. /// The command id. /// The guild id. /// The application id. internal async Task OnApplicationCommandPermissionsUpdateAsync(IEnumerable perms, ulong c_id, ulong guild_id, ulong a_id) { if (a_id != this.CurrentApplication.Id) return; var guild = this.InternalGetCachedGuild(guild_id); DiscordApplicationCommand cmd; try { cmd = await this.GetGuildApplicationCommandAsync(guild_id, c_id); } catch(NotFoundException) { cmd = await this.GetGlobalApplicationCommandAsync(c_id); } if (guild == null) { guild = new DiscordGuild { Id = guild_id, Discord = this }; } var ea = new ApplicationCommandPermissionsUpdateEventArgs(this.ServiceProvider) { Permissions = perms.ToList(), Command = cmd, ApplicationId = a_id, Guild = guild }; await this._applicationCommandPermissionsUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Interaction /// /// Handles the interaction create. /// /// The guild id. /// The channel id. /// The user. /// The member. /// The interaction. internal async Task OnInteractionCreateAsync(ulong? guildId, ulong channelId, TransportUser user, TransportMember member, DiscordInteraction interaction) { 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) { _guild_id = 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._guild_id = 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; } } if (resolved.Roles != null) { foreach (var c in resolved.Roles) { c.Value.Discord = this; if (guildId.HasValue) c.Value._guild_id = 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 (interaction.Type is InteractionType.Component) { 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 ctea = new ContextMenuInteractionCreateEventArgs(this.ServiceProvider) { Interaction = interaction, TargetUser = targetMember ?? targetUser, TargetMessage = targetMessage, Type = interaction.Data.Type, }; await this._contextMenuInteractionCreated.InvokeAsync(this, ctea).ConfigureAwait(false); } else { var ea = new InteractionCreateEventArgs(this.ServiceProvider) { Interaction = interaction }; await this._interactionCreated.InvokeAsync(this, ea).ConfigureAwait(false); } } } #endregion #region Misc /// /// Handles the typing start event. /// /// The user id. /// The channel id. /// The channel. /// The guild id. /// The started. /// The mbr. 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 the unknown event. /// /// 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/Entities/Channel/DiscordChannel.cs b/DisCatSharp/Entities/Channel/DiscordChannel.cs index d10fa7deb..0a7a9f3c6 100644 --- a/DisCatSharp/Entities/Channel/DiscordChannel.cs +++ b/DisCatSharp/Entities/Channel/DiscordChannel.cs @@ -1,1382 +1,1382 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.IO; using System.Linq; using System.Threading.Tasks; using DisCatSharp.Enums; using DisCatSharp.Exceptions; using DisCatSharp.Net; using DisCatSharp.Net.Abstractions; using DisCatSharp.Net.Models; using Newtonsoft.Json; namespace DisCatSharp.Entities { /// /// Represents a discord channel. /// public class DiscordChannel : SnowflakeObject, IEquatable { /// /// Gets ID of the guild to which this channel belongs. /// [JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)] public ulong? GuildId { get; internal set; } /// /// Gets ID of the category that contains this channel. /// [JsonProperty("parent_id", NullValueHandling = NullValueHandling.Include)] public ulong? ParentId { get; internal set; } /// /// Gets the category that contains this channel. /// [JsonIgnore] public DiscordChannel Parent => this.ParentId.HasValue ? this.Guild.GetChannel(this.ParentId.Value) : null; /// /// Gets the name of this channel. /// [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; internal set; } /// /// Gets the type of this channel. /// [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] public ChannelType Type { get; internal set; } /// /// Gets this channels's banner hash, when applicable. /// [JsonProperty("banner")] public string BannerHash { get; internal set; } /// /// Gets this channels's banner in url form. /// [JsonIgnore] public string BannerUrl - => !string.IsNullOrWhiteSpace(this.BannerHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Uri}{Endpoints.BANNERS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.BannerHash}.{(this.BannerHash.StartsWith("a_") ? "gif" : "png")}" : null; + => !string.IsNullOrWhiteSpace(this.BannerHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Uri}{Endpoints.CHANNELS}/{this.Id.ToString(CultureInfo.InvariantCulture)}{Endpoints.BANNERS}/{this.BannerHash}.{(this.BannerHash.StartsWith("a_") ? "gif" : "png")}" : null; /// /// Gets the position of this channel. /// [JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)] public int Position { get; internal set; } /// /// Gets the maximum available position to move the channel to. /// This can contain outdated informations. /// public int GetMaxPosition() { var channels = this.Guild.Channels.Values; return this.ParentId != null ? this.Type == ChannelType.Text || this.Type == ChannelType.News ? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Text || xc.Type == ChannelType.News)).OrderBy(xc => xc.Position).ToArray().Last().Position : this.Type == ChannelType.Voice || this.Type == ChannelType.Stage ? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Voice || xc.Type == ChannelType.Stage)).OrderBy(xc => xc.Position).ToArray().Last().Position : channels.Where(xc => xc.ParentId == this.ParentId && xc.Type == this.Type).OrderBy(xc => xc.Position).ToArray().Last().Position : channels.Where(xc => xc.ParentId == null && xc.Type == this.Type).OrderBy(xc => xc.Position).ToArray().Last().Position; } /// /// Gets the minimum available position to move the channel to. /// public int GetMinPosition() { var channels = this.Guild.Channels.Values; return this.ParentId != null ? this.Type == ChannelType.Text || this.Type == ChannelType.News ? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Text || xc.Type == ChannelType.News)).OrderBy(xc => xc.Position).ToArray().First().Position : this.Type == ChannelType.Voice || this.Type == ChannelType.Stage ? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Voice || xc.Type == ChannelType.Stage)).OrderBy(xc => xc.Position).ToArray().First().Position : channels.Where(xc => xc.ParentId == this.ParentId && xc.Type == this.Type).OrderBy(xc => xc.Position).ToArray().First().Position : channels.Where(xc => xc.ParentId == null && xc.Type == this.Type).OrderBy(xc => xc.Position).ToArray().First().Position; } /// /// Gets whether this channel is a DM channel. /// [JsonIgnore] public bool IsPrivate => this.Type == ChannelType.Private || this.Type == ChannelType.Group; /// /// Gets whether this channel is a channel category. /// [JsonIgnore] public bool IsCategory => this.Type == ChannelType.Category; /// /// Gets whether this channel is a stage channel. /// [JsonIgnore] public bool IsStage => this.Type == ChannelType.Stage; /// /// Gets the guild to which this channel belongs. /// [JsonIgnore] public DiscordGuild Guild => this.GuildId.HasValue && this.Discord.Guilds.TryGetValue(this.GuildId.Value, out var guild) ? guild : null; /// /// Gets a collection of permission overwrites for this channel. /// [JsonIgnore] public IReadOnlyList PermissionOverwrites => this._permissionOverwritesLazy.Value; [JsonProperty("permission_overwrites", NullValueHandling = NullValueHandling.Ignore)] internal List _permissionOverwrites = new(); [JsonIgnore] private readonly Lazy> _permissionOverwritesLazy; /// /// Gets the channel's topic. This is applicable to text channels only. /// [JsonProperty("topic", NullValueHandling = NullValueHandling.Ignore)] public string Topic { get; internal set; } /// /// Gets the ID of the last message sent in this channel. This is applicable to text channels only. /// [JsonProperty("last_message_id", NullValueHandling = NullValueHandling.Ignore)] public ulong? LastMessageId { get; internal set; } /// /// Gets this channel's bitrate. This is applicable to voice channels only. /// [JsonProperty("bitrate", NullValueHandling = NullValueHandling.Ignore)] public int? Bitrate { get; internal set; } /// /// Gets this channel's user limit. This is applicable to voice channels only. /// [JsonProperty("user_limit", NullValueHandling = NullValueHandling.Ignore)] public int? UserLimit { get; internal set; } /// /// Gets the slow mode delay configured for this channel. /// All bots, as well as users with or permissions in the channel are exempt from slow mode. /// [JsonProperty("rate_limit_per_user")] public int? PerUserRateLimit { get; internal set; } /// /// Gets this channel's video quality mode. This is applicable to voice channels only. /// [JsonProperty("video_quality_mode", NullValueHandling = NullValueHandling.Ignore)] public VideoQualityMode? QualityMode { get; internal set; } /// /// Gets when the last pinned message was pinned. /// [JsonIgnore] public DateTimeOffset? LastPinTimestamp => !string.IsNullOrWhiteSpace(this.LastPinTimestampRaw) && DateTimeOffset.TryParse(this.LastPinTimestampRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto) ? dto : null; /// /// Gets when the last pinned message was pinned as raw string. /// [JsonProperty("last_pin_timestamp", NullValueHandling = NullValueHandling.Ignore)] internal string LastPinTimestampRaw { get; set; } /// /// Gets this channel's default duration for newly created threads, in minutes, to automatically archive the thread after recent activity. /// [JsonProperty("default_auto_archive_duration", NullValueHandling = NullValueHandling.Ignore)] public ThreadAutoArchiveDuration? DefaultAutoArchiveDuration { get; internal set; } /// /// Gets this channel's mention string. /// [JsonIgnore] public string Mention => Formatter.Mention(this); /// /// Gets this channel's children. This applies only to channel categories. /// [JsonIgnore] public IReadOnlyList Children { get { return !this.IsCategory ? throw new ArgumentException("Only channel categories contain children.") : this.Guild._channels.Values.Where(e => e.ParentId == this.Id).ToList(); } } /// /// Gets the list of members currently in the channel (if voice channel), or members who can see the channel (otherwise). /// [JsonIgnore] public virtual IReadOnlyList Users { get { return this.Guild == null ? throw new InvalidOperationException("Cannot query users outside of guild channels.") : this.IsVoiceJoinable() ? this.Guild.Members.Values.Where(x => x.VoiceState?.ChannelId == this.Id).ToList() : this.Guild.Members.Values.Where(x => (this.PermissionsFor(x) & Permissions.AccessChannels) == Permissions.AccessChannels).ToList(); } } /// /// Gets whether this channel is an NSFW channel. /// [JsonProperty("nsfw")] public bool IsNSFW { get; internal set; } /// /// Gets this channel's region id (if voice channel). /// [JsonProperty("rtc_region", NullValueHandling = NullValueHandling.Ignore)] internal string RtcRegionId { get; set; } /// /// Gets this channel's region override (if voice channel). /// [JsonIgnore] public DiscordVoiceRegion RtcRegion => this.RtcRegionId != null ? this.Discord.VoiceRegions[this.RtcRegionId] : null; /// /// Only sent on the resolved channels of interaction responses for application commands. Gets the permissions of the user in this channel who invoked the command. /// [JsonProperty("permissions")] public Permissions? UserPermissions { get; internal set; } /// /// Initializes a new instance of the class. /// internal DiscordChannel() { this._permissionOverwritesLazy = new Lazy>(() => new ReadOnlyCollection(this._permissionOverwrites)); } #region Methods /// /// Sends a message to this channel. /// /// Content of the message to send. /// The sent message. /// Thrown when the client does not have the permission if TTS is true and if TTS is true. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task SendMessageAsync(string content) { return !this.IsWriteable() ? throw new ArgumentException("Cannot send a text message to a non-text channel.") : this.Discord.ApiClient.CreateMessageAsync(this.Id, content, null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false); } /// /// Sends a message to this channel. /// /// Embed to attach to the message. /// The sent message. /// Thrown when the client does not have the permission and if TTS is true. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task SendMessageAsync(DiscordEmbed embed) { return !this.IsWriteable() ? throw new ArgumentException("Cannot send a text message to a non-text channel.") : this.Discord.ApiClient.CreateMessageAsync(this.Id, null, embed != null ? new[] { embed } : null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false); } /// /// Sends a message to this channel. /// /// Embed to attach to the message. /// Content of the message to send. /// The sent message. /// Thrown when the client does not have the permission if TTS is true and if TTS is true. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task SendMessageAsync(string content, DiscordEmbed embed) { return !this.IsWriteable() ? throw new ArgumentException("Cannot send a text message to a non-text channel.") : this.Discord.ApiClient.CreateMessageAsync(this.Id, content, embed != null ? new[] { embed } : null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false); } /// /// Sends a message to this channel. /// /// The builder with all the items to send. /// The sent message. /// Thrown when the client does not have the permission TTS is true and if TTS is true. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task SendMessageAsync(DiscordMessageBuilder builder) => this.Discord.ApiClient.CreateMessageAsync(this.Id, builder); /// /// Sends a message to this channel. /// /// The builder with all the items to send. /// The sent message. /// Thrown when the client does not have the permission TTS is true and if TTS is true. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task SendMessageAsync(Action action) { var builder = new DiscordMessageBuilder(); action(builder); return !this.IsWriteable() ? throw new ArgumentException("Cannot send a text message to a non-text channel.") : this.Discord.ApiClient.CreateMessageAsync(this.Id, builder); } /// /// Deletes a guild channel /// /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task DeleteAsync(string reason = null) => this.Discord.ApiClient.DeleteChannelAsync(this.Id, reason); /// /// Clones this channel. This operation will create a channel with identical settings to this one. Note that this will not copy messages. /// /// Reason for audit logs. /// Newly-created channel. /// Thrown when the client does not have the permission. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task CloneAsync(string reason = null) { if (this.Guild == null) throw new InvalidOperationException("Non-guild channels cannot be cloned."); var ovrs = new List(); foreach (var ovr in this._permissionOverwrites) #pragma warning disable CS0618 // Type or member is obsolete ovrs.Add(await new DiscordOverwriteBuilder().FromAsync(ovr).ConfigureAwait(false)); #pragma warning restore CS0618 // Type or member is obsolete var bitrate = this.Bitrate; var userLimit = this.UserLimit; Optional perUserRateLimit = this.PerUserRateLimit; if(!this.IsVoiceJoinable()) { bitrate = null; userLimit = null; } if (this.Type == ChannelType.Stage) { userLimit = null; } if (!this.IsWriteable()) { perUserRateLimit = Optional.FromNoValue(); } return await this.Guild.CreateChannelAsync(this.Name, this.Type, this.Parent, this.Topic, bitrate, userLimit, ovrs, this.IsNSFW, perUserRateLimit, this.QualityMode, reason).ConfigureAwait(false); } /// /// Returns a specific message /// /// The id of the message /// Thrown when the client does not have the permission. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task GetMessageAsync(ulong id) { return this.Discord.Configuration.MessageCacheSize > 0 && this.Discord is DiscordClient dc && dc.MessageCache != null && dc.MessageCache.TryGet(xm => xm.Id == id && xm.ChannelId == this.Id, out var msg) ? msg : await this.Discord.ApiClient.GetMessageAsync(this.Id, id).ConfigureAwait(false); } /// /// Modifies the current channel. /// /// Action to perform on this channel /// Thrown when the client does not have the . /// Thrown when the client does not have the correct for modifying the . /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task ModifyAsync(Action action) { var mdl = new ChannelEditModel(); action(mdl); if (mdl.DefaultAutoArchiveDuration.HasValue) { if (!Utilities.CheckThreadAutoArchiveDurationFeature(this.Guild, mdl.DefaultAutoArchiveDuration.Value)) throw new NotSupportedException($"Cannot modify DefaultAutoArchiveDuration. Guild needs boost tier {(mdl.DefaultAutoArchiveDuration.Value == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}."); } if (mdl.Banner.HasValue) { if (!this.Guild.Features.CanSetChannelBanner) throw new NotSupportedException($"Cannot modify Banner. Guild needs boost tier three."); } var bannerb64 = Optional.FromNoValue(); if (mdl.Banner.HasValue && mdl.Banner.Value != null) using (var imgtool = new ImageTool(mdl.Banner.Value)) bannerb64 = imgtool.GetBase64(); else if (mdl.Banner.HasValue) bannerb64 = null; return this.Discord.ApiClient.ModifyChannelAsync(this.Id, mdl.Name, mdl.Position, mdl.Topic, mdl.Nsfw, mdl.Parent.HasValue ? mdl.Parent.Value?.Id : default(Optional), mdl.Bitrate, mdl.Userlimit, mdl.PerUserRateLimit, mdl.RtcRegion.IfPresent(r => r?.Id), mdl.QualityMode, mdl.DefaultAutoArchiveDuration, mdl.Type, mdl.PermissionOverwrites, bannerb64, mdl.AuditLogReason); } /// /// Updates the channel position when it doesn't have a category. /// /// Use for moving to other categories. /// Use to move out of a category. /// Use for moving within a category. /// /// Position the channel should be moved to. /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. [Obsolete("This will be replaced by ModifyPositionInCategoryAsync. Use it instead.")] public Task ModifyPositionAsync(int position, string reason = null) { if (this.Guild == null) throw new ArgumentException("Cannot modify order of non-guild channels."); if (!this.IsMovable()) throw new NotSupportedException("You can't move this type of channel in categories."); if (this.ParentId != null) throw new ArgumentException("Cannot modify order of channels within a category. Use ModifyPositionInCategoryAsync instead."); var chns = this.Guild._channels.Values.Where(xc => xc.Type == this.Type).OrderBy(xc => xc.Position).ToArray(); var pmds = new RestGuildChannelReorderPayload[chns.Length]; for (var i = 0; i < chns.Length; i++) { pmds[i] = new RestGuildChannelReorderPayload { ChannelId = chns[i].Id, }; pmds[i].Position = chns[i].Id == this.Id ? position : chns[i].Position >= position ? chns[i].Position + 1 : chns[i].Position; } return this.Discord.ApiClient.ModifyGuildChannelPositionAsync(this.Guild.Id, pmds, reason); } /// /// Updates the channel position within it's own category. /// /// Use for moving to other categories. /// Use to move out of a category. /// Use to move channels outside a category. /// /// The position. /// The reason. /// Thrown when the client does not have the permission. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. /// Thrown when is out of range. /// Thrown when function is called on a channel without a parent channel. public async Task ModifyPositionInCategoryAsync(int position, string reason = null) { //if (this.ParentId == null) // throw new ArgumentException("You can call this function only on channels in categories."); if (!this.IsMovableInParent()) throw new NotSupportedException("You can't move this type of channel in categories."); var isUp = position > this.Position; var channels = await this.InternalRefreshChannelsAsync(); var chns = this.ParentId != null ? this.Type == ChannelType.Text || this.Type == ChannelType.News ? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Text || xc.Type == ChannelType.News)) : this.Type == ChannelType.Voice || this.Type == ChannelType.Stage ? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Voice || xc.Type == ChannelType.Stage)) : channels.Where(xc => xc.ParentId == this.ParentId && xc.Type == this.Type) : this.Type == ChannelType.Text || this.Type == ChannelType.News ? channels.Where(xc => xc.ParentId == null && (xc.Type == ChannelType.Text || xc.Type == ChannelType.News)) : this.Type == ChannelType.Voice || this.Type == ChannelType.Stage ? channels.Where(xc => xc.ParentId == null && (xc.Type == ChannelType.Voice || xc.Type == ChannelType.Stage)) : channels.Where(xc => xc.ParentId == null && xc.Type == this.Type); var ochns = chns.OrderBy(xc => xc.Position).ToArray(); var min = ochns.First().Position; var max = ochns.Last().Position; if (position > max || position < min) throw new IndexOutOfRangeException($"Position is not in range. {position} is {(position > max ? "greater then the maximal" : "lower then the minimal")} position."); var pmds = new RestGuildChannelReorderPayload[ochns.Length]; for (var i = 0; i < ochns.Length; i++) { pmds[i] = new RestGuildChannelReorderPayload { ChannelId = ochns[i].Id, }; if (ochns[i].Id == this.Id) { pmds[i].Position = position; } else { if (isUp) { if (ochns[i].Position <= position && ochns[i].Position > this.Position) { pmds[i].Position = ochns[i].Position - 1; } else if (ochns[i].Position < this.Position || ochns[i].Position > position) { pmds[i].Position = ochns[i].Position; } } else { if (ochns[i].Position >= position && ochns[i].Position < this.Position) { pmds[i].Position = ochns[i].Position + 1; } else if (ochns[i].Position > this.Position || ochns[i].Position < position) { pmds[i].Position = ochns[i].Position; } } } } await this.Discord.ApiClient.ModifyGuildChannelPositionAsync(this.Guild.Id, pmds, reason).ConfigureAwait(false); } /// /// Internaly refreshes the channel list. /// private async Task> InternalRefreshChannelsAsync() { await this.RefreshPositionsAsync(); return this.Guild.Channels.Values.ToList().AsReadOnly(); } /// /// Refreshes the positions. /// public async Task RefreshPositionsAsync() { var channels = await this.Discord.ApiClient.GetGuildChannelsAsync(this.Guild.Id); this.Guild._channels.Clear(); foreach (var channel in channels.ToList()) { channel.Discord = this.Discord; foreach (var xo in channel._permissionOverwrites) { xo.Discord = this.Discord; xo._channel_id = channel.Id; } this.Guild._channels[channel.Id] = channel; } } /// /// Updates the channel position within it's own category. /// Valid modes: '+' or 'down' to move a channel down | '-' or 'up' to move a channel up. /// /// Use for moving to other categories. /// Use to move out of a category. /// Use to move channels outside a category. /// /// The mode. Valid: '+' or 'down' to move a channel down | '-' or 'up' to move a channel up /// The position. /// The reason. /// Thrown when the client does not have the permission. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. /// Thrown when is out of range. /// Thrown when function is called on a channel without a parent channel, a wrong mode is givven or given position is zero. public Task ModifyPositionInCategorySmartAsync(string mode, int position, string reason = null) { if (!this.IsMovableInParent()) throw new NotSupportedException("You can't move this type of channel in categories."); if (mode != "+" && mode != "-" && mode != "down" && mode != "up") throw new ArgumentException("Error with the selected mode: Valid is '+' or 'down' to move a channel down and '-' or 'up' to move a channel up"); var positive = mode == "+" || mode == "positive" || mode == "down"; var negative = mode == "-" || mode == "negative" || mode == "up"; return positive ? position < this.GetMaxPosition() ? this.ModifyPositionInCategoryAsync(this.Position + position, reason) : throw new IndexOutOfRangeException($"Position is not in range of category.") : negative ? position > this.GetMinPosition() ? this.ModifyPositionInCategoryAsync(this.Position - position, reason) : throw new IndexOutOfRangeException($"Position is not in range of category.") : throw new ArgumentException("You can only modify with +X or -X. 0 is not valid."); } /// /// Updates the channel parent, moving the channel to the bottom of the new category. /// /// New parent for channel. Will move out of parent if null. /// Sync permissions with parent. Defaults to null. /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. #pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. public Task ModifyParentAsync(DiscordChannel? newParent = null, bool? lock_permissions = null, string reason = null) #pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. { if (this.Guild == null) throw new ArgumentException("Cannot modify parent of non-guild channels."); if (!this.IsMovableInParent()) throw new NotSupportedException("You can't move this type of channel in categories."); if (newParent.Type is not ChannelType.Category) throw new ArgumentException("Only category type channels can be parents."); var position = this.Guild._channels.Values.Where(xc => xc.Type == this.Type && xc.ParentId == newParent.Id) // gets list same type channels in parent .Select(xc => xc.Position).DefaultIfEmpty(-1).Max() + 1; // returns highest position of list +1, default val: 0 var chns = this.Guild._channels.Values.Where(xc => xc.Type == this.Type) .OrderBy(xc => xc.Position).ToArray(); var pmds = new RestGuildChannelNewParentPayload[chns.Length]; for (var i = 0; i < chns.Length; i++) { pmds[i] = new RestGuildChannelNewParentPayload { ChannelId = chns[i].Id, Position = chns[i].Position >= position ? chns[i].Position + 1 : chns[i].Position, }; if (chns[i].Id == this.Id) { pmds[i].Position = position; pmds[i].ParentId = newParent is not null ? newParent.Id : null; pmds[i].LockPermissions = lock_permissions; } } return this.Discord.ApiClient.ModifyGuildChannelParentAsync(this.Guild.Id, pmds, reason); } /// /// Moves the channel out of a category. /// /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task RemoveParentAsync(string reason = null) { if (this.Guild == null) throw new ArgumentException("Cannot modify parent of non-guild channels."); if (!this.IsMovableInParent()) throw new NotSupportedException("You can't move this type of channel in categories."); var position = this.Guild._channels.Values.Where(xc => xc.Type == this.Type && xc.Parent is null) //gets list of same type channels with no parent .Select(xc => xc.Position).DefaultIfEmpty(-1).Max() + 1; // returns highest position of list +1, default val: 0 var chns = this.Guild._channels.Values.Where(xc => xc.Type == this.Type) .OrderBy(xc => xc.Position).ToArray(); var pmds = new RestGuildChannelNoParentPayload[chns.Length]; for (var i = 0; i < chns.Length; i++) { pmds[i] = new RestGuildChannelNoParentPayload { ChannelId = chns[i].Id, }; if (chns[i].Id == this.Id) { pmds[i].Position = 1; pmds[i].ParentId = null; } else { pmds[i].Position = chns[i].Position < this.Position ? chns[i].Position + 1 : chns[i].Position; } } return this.Discord.ApiClient.DetachGuildChannelParentAsync(this.Guild.Id, pmds, reason); } /// /// Returns a list of messages before a certain message. /// The amount of messages to fetch. /// Message to fetch before from. /// /// Thrown when the client does not have the permission. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task> GetMessagesBeforeAsync(ulong before, int limit = 100) => this.GetMessagesInternalAsync(limit, before, null, null); /// /// Returns a list of messages after a certain message. /// The amount of messages to fetch. /// Message to fetch after from. /// /// Thrown when the client does not have the permission. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task> GetMessagesAfterAsync(ulong after, int limit = 100) => this.GetMessagesInternalAsync(limit, null, after, null); /// /// Returns a list of messages around a certain message. /// The amount of messages to fetch. /// Message to fetch around from. /// /// Thrown when the client does not have the permission. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task> GetMessagesAroundAsync(ulong around, int limit = 100) => this.GetMessagesInternalAsync(limit, null, null, around); /// /// Returns a list of messages from the last message in the channel. /// The amount of messages to fetch. /// /// Thrown when the client does not have the permission. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task> GetMessagesAsync(int limit = 100) => this.GetMessagesInternalAsync(limit, null, null, null); /// /// Returns a list of messages /// /// How many messages should be returned. /// Get messages before snowflake. /// Get messages after snowflake. /// Get messages around snowflake. private async Task> GetMessagesInternalAsync(int limit = 100, ulong? before = null, ulong? after = null, ulong? around = null) { if (!this.IsWriteable()) throw new ArgumentException("Cannot get the messages of a non-text channel."); if (limit < 0) throw new ArgumentException("Cannot get a negative number of messages."); if (limit == 0) return Array.Empty(); //return this.Discord.ApiClient.GetChannelMessagesAsync(this.Id, limit, before, after, around); if (limit > 100 && around != null) throw new InvalidOperationException("Cannot get more than 100 messages around the specified ID."); var msgs = new List(limit); var remaining = limit; ulong? last = null; var isAfter = after != null; int lastCount; do { var fetchSize = remaining > 100 ? 100 : remaining; var fetch = await this.Discord.ApiClient.GetChannelMessagesAsync(this.Id, fetchSize, !isAfter ? last ?? before : null, isAfter ? last ?? after : null, around).ConfigureAwait(false); lastCount = fetch.Count; remaining -= lastCount; if (!isAfter) { msgs.AddRange(fetch); last = fetch.LastOrDefault()?.Id; } else { msgs.InsertRange(0, fetch); last = fetch.FirstOrDefault()?.Id; } } while (remaining > 0 && lastCount > 0); return new ReadOnlyCollection(msgs); } /// /// Deletes multiple messages if they are less than 14 days old. If they are older, none of the messages will be deleted and you will receive a error. /// /// A collection of messages to delete. /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task DeleteMessagesAsync(IEnumerable messages, string reason = null) { // don't enumerate more than once var msgs = messages.Where(x => x.Channel.Id == this.Id).Select(x => x.Id).ToArray(); if (messages == null || !msgs.Any()) throw new ArgumentException("You need to specify at least one message to delete."); if (msgs.Count() < 2) { await this.Discord.ApiClient.DeleteMessageAsync(this.Id, msgs.Single(), reason).ConfigureAwait(false); return; } for (var i = 0; i < msgs.Count(); i += 100) await this.Discord.ApiClient.DeleteMessagesAsync(this.Id, msgs.Skip(i).Take(100), reason).ConfigureAwait(false); } /// /// Deletes a message /// /// The message to be deleted. /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task DeleteMessageAsync(DiscordMessage message, string reason = null) => this.Discord.ApiClient.DeleteMessageAsync(this.Id, message.Id, reason); /// /// Returns a list of invite objects /// /// Thrown when the client does not have the permission. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task> GetInvitesAsync() { return this.Guild == null ? throw new ArgumentException("Cannot get the invites of a channel that does not belong to a guild.") : this.Discord.ApiClient.GetChannelInvitesAsync(this.Id); } /// /// Create a new invite object /// /// Duration of invite in seconds before expiry, or 0 for never. Defaults to 86400. /// Max number of uses or 0 for unlimited. Defaults to 0 /// Whether this invite should be temporary. Defaults to false. /// Whether this invite should be unique. Defaults to false. /// The target type. Defaults to null. /// The target activity. Defaults to null. /// The target user id. Defaults to null. /// The audit log reason. /// Thrown when the client does not have the permission. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task CreateInviteAsync(int max_age = 86400, int max_uses = 0, bool temporary = false, bool unique = false, TargetType? target_type = null, TargetActivity? target_application = null, ulong? target_user = null, string reason = null) => this.Discord.ApiClient.CreateChannelInviteAsync(this.Id, max_age, max_uses, target_type, target_application, target_user, temporary, unique, reason); #region Stage /// /// Opens a stage. /// /// Topic of the stage. /// Whether @everyone should be notified. /// Privacy level of the stage (Defaults to . /// Audit log reason. /// Stage instance /// Thrown when the client does not have the permission. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task OpenStageAsync(string topic, bool send_start_notification = false, StagePrivacyLevel privacy_level = StagePrivacyLevel.GUILD_ONLY, string reason = null) => await this.Discord.ApiClient.CreateStageInstanceAsync(this.Id, topic, send_start_notification, privacy_level, reason); /// /// Modifies a stage topic. /// /// New topic of the stage. /// New privacy level of the stage. /// Audit log reason. /// Thrown when the client does not have the permission. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task ModifyStageAsync(Optional topic, Optional privacy_level, string reason = null) => await this.Discord.ApiClient.ModifyStageInstanceAsync(this.Id, topic, privacy_level, reason); /// /// Closes a stage. /// /// Audit log reason. /// Thrown when the client does not have the permission. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task CloseStageAsync(string reason = null) => await this.Discord.ApiClient.DeleteStageInstanceAsync(this.Id, reason); /// /// Gets a stage. /// /// The requested stage. /// Thrown when the client does not have the or permission. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task GetStageAsync() => await this.Discord.ApiClient.GetStageInstanceAsync(this.Id); #endregion #region Threads /// /// Creates a thread. /// Depending on whether it is created inside an or an it is either an or an . /// Depending on whether the is set to it is either an or an (default). /// /// The name of the thread. /// till it gets archived. Defaults to . /// Can be either an , or an . /// Audit log reason. /// The created thread. /// Thrown when the client does not have the or or if creating a private thread the permission. /// Thrown when the guild hasn't enabled threads atm. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. /// Thrown when the cannot be modified. This happens, when the guild hasn't reached a certain boost . Or if is not enabled for guild. This happens, if the guild does not have public async Task CreateThreadAsync(string name, ThreadAutoArchiveDuration auto_archive_duration = ThreadAutoArchiveDuration.OneHour, ChannelType type = ChannelType.PublicThread, string reason = null) { return (type != ChannelType.NewsThread && type != ChannelType.PublicThread && type != ChannelType.PrivateThread) ? throw new NotSupportedException("Wrong thread type given.") : (!this.IsThreadHolder()) ? throw new NotSupportedException("Parent channel can't have threads") : type == ChannelType.PrivateThread ? Utilities.CheckThreadPrivateFeature(this.Guild) ? Utilities.CheckThreadAutoArchiveDurationFeature(this.Guild, auto_archive_duration) ? await this.Discord.ApiClient.CreateThreadWithoutMessageAsync(this.Id, name, auto_archive_duration, type, reason) : throw new NotSupportedException($"Cannot modify ThreadAutoArchiveDuration. Guild needs boost tier {(auto_archive_duration == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}.") : throw new NotSupportedException($"Cannot create a private thread. Guild needs to be boost tier two.") : Utilities.CheckThreadAutoArchiveDurationFeature(this.Guild, auto_archive_duration) ? await this.Discord.ApiClient.CreateThreadWithoutMessageAsync(this.Id, name, auto_archive_duration, this.Type == ChannelType.News ? ChannelType.NewsThread : ChannelType.PublicThread, reason) : throw new NotSupportedException($"Cannot modify ThreadAutoArchiveDuration. Guild needs boost tier {(auto_archive_duration == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}."); } /// /// Gets joined archived private threads. Can contain more threads. /// If the result's value 'HasMore' is true, you need to recall this function to get older threads. /// /// Get threads created before this thread id. /// Defines the limit of returned . /// Thrown when the client does not have the permission. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task GetJoinedPrivateArchivedThreadsAsync(ulong? before, int? limit) => await this.Discord.ApiClient.GetJoinedPrivateArchivedThreadsAsync(this.Id, before, limit); /// /// Gets archived public threads. Can contain more threads. /// If the result's value 'HasMore' is true, you need to recall this function to get older threads. /// /// Get threads created before this thread id. /// Defines the limit of returned . /// Thrown when the client does not have the permission. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task GetPublicArchivedThreadsAsync(ulong? before, int? limit) => await this.Discord.ApiClient.GetPublicArchivedThreadsAsync(this.Id, before, limit); /// /// Gets archived private threads. Can contain more threads. /// If the result's value 'HasMore' is true, you need to recall this function to get older threads. /// /// Get threads created before this thread id. /// Defines the limit of returned . /// Thrown when the client does not have the or permission. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task GetPrivateArchivedThreadsAsync(ulong? before, int? limit) => await this.Discord.ApiClient.GetPrivateArchivedThreadsAsync(this.Id, before, limit); #endregion /// /// Adds a channel permission overwrite for specified role. /// /// The role to have the permission added. /// The permissions to allow. /// The permissions to deny. /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task AddOverwriteAsync(DiscordRole role, Permissions allow = Permissions.None, Permissions deny = Permissions.None, string reason = null) => this.Discord.ApiClient.EditChannelPermissionsAsync(this.Id, role.Id, allow, deny, "role", reason); /// /// Adds a channel permission overwrite for specified member. /// /// The member to have the permission added. /// The permissions to allow. /// The permissions to deny. /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task AddOverwriteAsync(DiscordMember member, Permissions allow = Permissions.None, Permissions deny = Permissions.None, string reason = null) => this.Discord.ApiClient.EditChannelPermissionsAsync(this.Id, member.Id, allow, deny, "member", reason); /// /// Deletes a channel permission overwrite for specified member. /// /// The member to have the permission deleted. /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task DeleteOverwriteAsync(DiscordMember member, string reason = null) => this.Discord.ApiClient.DeleteChannelPermissionAsync(this.Id, member.Id, reason); /// /// Deletes a channel permission overwrite for specified role. /// /// The role to have the permission deleted. /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task DeleteOverwriteAsync(DiscordRole role, string reason = null) => this.Discord.ApiClient.DeleteChannelPermissionAsync(this.Id, role.Id, reason); /// /// Post a typing indicator /// /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task TriggerTypingAsync() { return !this.IsWriteable() ? throw new ArgumentException("Cannot start typing in a non-text channel.") : this.Discord.ApiClient.TriggerTypingAsync(this.Id); } /// /// Returns all pinned messages /// /// Thrown when the client does not have the permission. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task> GetPinnedMessagesAsync() { return !this.IsWriteable() ? throw new ArgumentException("A non-text channel does not have pinned messages.") : this.Discord.ApiClient.GetPinnedMessagesAsync(this.Id); } /// /// Create a new webhook /// /// The name of the webhook. /// The image for the default webhook avatar. /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task CreateWebhookAsync(string name, Optional avatar = default, string reason = null) { var av64 = Optional.FromNoValue(); if (avatar.HasValue && avatar.Value != null) using (var imgtool = new ImageTool(avatar.Value)) av64 = imgtool.GetBase64(); else if (avatar.HasValue) av64 = null; return await this.Discord.ApiClient.CreateWebhookAsync(this.Id, name, av64, reason).ConfigureAwait(false); } /// /// Returns a list of webhooks /// /// Thrown when the client does not have the permission. /// Thrown when the channel does not exist. /// Thrown when Discord is unable to process the request. public Task> GetWebhooksAsync() => this.Discord.ApiClient.GetChannelWebhooksAsync(this.Id); /// /// Moves a member to this voice channel /// /// The member to be moved. /// Thrown when the client does not have the permission. /// Thrown when the channel does not exists or if the Member does not exists. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task PlaceMemberAsync(DiscordMember member) { if (!this.IsVoiceJoinable()) throw new ArgumentException("Cannot place a member in a non-voice channel."); await this.Discord.ApiClient.ModifyGuildMemberAsync(this.Guild.Id, member.Id, default, default, default, default, this.Id, null).ConfigureAwait(false); } /// /// Follows a news channel /// /// Channel to crosspost messages to /// Thrown when trying to follow a non-news channel /// Thrown when the current user doesn't have on the target channel public Task FollowAsync(DiscordChannel targetChannel) { return this.Type != ChannelType.News ? throw new ArgumentException("Cannot follow a non-news channel.") : this.Discord.ApiClient.FollowChannelAsync(this.Id, targetChannel.Id); } /// /// Publishes a message in a news channel to following channels /// /// Message to publish /// Thrown when the message has already been crossposted /// /// Thrown when the current user doesn't have and/or /// public Task CrosspostMessageAsync(DiscordMessage message) { return (message.Flags & MessageFlags.Crossposted) == MessageFlags.Crossposted ? throw new ArgumentException("Message is already crossposted.") : this.Discord.ApiClient.CrosspostMessageAsync(this.Id, message.Id); } /// /// Updates the current user's suppress state in this channel, if stage channel. /// /// Toggles the suppress state. /// Sets the time the user requested to speak. /// Thrown when the channel is not a stage channel. public async Task UpdateCurrentUserVoiceStateAsync(bool? suppress, DateTimeOffset? requestToSpeakTimestamp = null) { if (this.Type != ChannelType.Stage) throw new ArgumentException("Voice state can only be updated in a stage channel."); await this.Discord.ApiClient.UpdateCurrentUserVoiceStateAsync(this.GuildId.Value, this.Id, suppress, requestToSpeakTimestamp).ConfigureAwait(false); } /// /// Calculates permissions for a given member. /// /// Member to calculate permissions for. /// Calculated permissions for a given member. public Permissions PermissionsFor(DiscordMember mbr) { // future note: might be able to simplify @everyone role checks to just check any role ... but I'm not sure // xoxo, ~uwx // // you should use a single tilde // ~emzi // user > role > everyone // allow > deny > undefined // => // user allow > user deny > role allow > role deny > everyone allow > everyone deny // thanks to meew0 if (this.IsPrivate || this.Guild == null) return Permissions.None; if (this.Guild.OwnerId == mbr.Id) return PermissionMethods.FULL_PERMS; Permissions perms; // assign @everyone permissions var everyoneRole = this.Guild.EveryoneRole; perms = everyoneRole.Permissions; // roles that member is in var mbRoles = mbr.Roles.Where(xr => xr.Id != everyoneRole.Id).ToArray(); // assign permissions from member's roles (in order) perms |= mbRoles.Aggregate(Permissions.None, (c, role) => c | role.Permissions); // Adminstrator grants all permissions and cannot be overridden if ((perms & Permissions.Administrator) == Permissions.Administrator) return PermissionMethods.FULL_PERMS; // channel overrides for roles that member is in var mbRoleOverrides = mbRoles .Select(xr => this._permissionOverwrites.FirstOrDefault(xo => xo.Id == xr.Id)) .Where(xo => xo != null) .ToList(); // assign channel permission overwrites for @everyone pseudo-role var everyoneOverwrites = this._permissionOverwrites.FirstOrDefault(xo => xo.Id == everyoneRole.Id); if (everyoneOverwrites != null) { perms &= ~everyoneOverwrites.Denied; perms |= everyoneOverwrites.Allowed; } // assign channel permission overwrites for member's roles (explicit deny) perms &= ~mbRoleOverrides.Aggregate(Permissions.None, (c, overs) => c | overs.Denied); // assign channel permission overwrites for member's roles (explicit allow) perms |= mbRoleOverrides.Aggregate(Permissions.None, (c, overs) => c | overs.Allowed); // channel overrides for just this member var mbOverrides = this._permissionOverwrites.FirstOrDefault(xo => xo.Id == mbr.Id); if (mbOverrides == null) return perms; // assign channel permission overwrites for just this member perms &= ~mbOverrides.Denied; perms |= mbOverrides.Allowed; return perms; } /// /// Returns a string representation of this channel. /// /// String representation of this channel. public override string ToString() { return this.Type == ChannelType.Category ? $"Channel Category {this.Name} ({this.Id})" : this.Type == ChannelType.Text || this.Type == ChannelType.News || this.IsThread() ? $"Channel #{this.Name} ({this.Id})" : this.IsVoiceJoinable() ? $"Channel #!{this.Name} ({this.Id})" : !string.IsNullOrWhiteSpace(this.Name) ? $"Channel {this.Name} ({this.Id})" : $"Channel {this.Id}"; } #endregion /// /// Checks whether this is equal to another object. /// /// Object to compare to. /// Whether the object is equal to this . public override bool Equals(object obj) => this.Equals(obj as DiscordChannel); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(DiscordChannel e) => e is not null && (ReferenceEquals(this, e) || this.Id == e.Id); /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() => this.Id.GetHashCode(); /// /// Gets whether the two objects are equal. /// /// First channel to compare. /// Second channel to compare. /// Whether the two channels are equal. public static bool operator ==(DiscordChannel e1, DiscordChannel e2) { var o1 = e1 as object; var o2 = e2 as object; return (o1 != null || o2 == null) && (o1 == null || o2 != null) && ((o1 == null && o2 == null) || e1.Id == e2.Id); } /// /// Gets whether the two objects are not equal. /// /// First channel to compare. /// Second channel to compare. /// Whether the two channels are not equal. public static bool operator !=(DiscordChannel e1, DiscordChannel e2) => !(e1 == e2); } } diff --git a/DisCatSharp/Entities/Thread/DiscordThreadChannelMember.cs b/DisCatSharp/Entities/Thread/DiscordThreadChannelMember.cs index 2630d82ac..ba6574707 100644 --- a/DisCatSharp/Entities/Thread/DiscordThreadChannelMember.cs +++ b/DisCatSharp/Entities/Thread/DiscordThreadChannelMember.cs @@ -1,138 +1,137 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 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.Globalization; using Newtonsoft.Json; namespace DisCatSharp.Entities { /// /// Represents a discord thread member object. /// public class DiscordThreadChannelMember : SnowflakeObject, IEquatable { /// /// Gets the id of the user. /// [JsonProperty("user_id", NullValueHandling = NullValueHandling.Ignore)] public ulong UserId { get; internal set; } /// /// Gets the member object of the user. /// [JsonIgnore] - public DiscordMember Member - => this.Guild != null ? (this.Guild._members.TryGetValue(this.UserId, out var member) ? member : this.Discord.ApiClient.GetGuildMemberAsync(this._guild_id, this.UserId).Result ?? new DiscordMember { Id = this.UserId, _guild_id = this._guild_id, Discord = this.Discord }) : null; + public DiscordMember Member { get; internal set; } /// /// Gets the presence of the user. /// [JsonProperty("presence", NullValueHandling = NullValueHandling.Ignore)] public DiscordPresence Presence { get; internal set; } /// /// Gets the timestamp when the user joined the thread. /// [JsonIgnore] public DateTimeOffset? JoinTimeStamp => !string.IsNullOrWhiteSpace(this.JoinTimeStampRaw) && DateTimeOffset.TryParse(this.JoinTimeStampRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto) ? dto : null; /// /// Gets the timestamp when the user joined the thread as raw string. /// [JsonProperty("join_timestamp", NullValueHandling = NullValueHandling.Ignore)] internal string JoinTimeStampRaw { get; set; } /// /// Gets the thread member flags. /// [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] public ThreadMemberFlags Flags { get; internal set; } /// /// Gets the category that contains this channel. For threads, gets the channel this thread was created in. /// [JsonIgnore] public DiscordChannel Thread => this.Guild != null ? (this.Guild._threads.TryGetValue(this.Id, out var thread) ? thread : null) : null; /// /// Gets the guild to which this channel belongs. /// [JsonIgnore] public DiscordGuild Guild => this.Discord.Guilds.TryGetValue(this._guild_id, out var guild) ? guild : null; [JsonIgnore] internal ulong _guild_id; /// /// Checks whether this is equal to another object. /// /// Object to compare to. /// Whether the object is equal to this . public override bool Equals(object obj) => this.Equals(obj as DiscordThreadChannelMember); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(DiscordThreadChannelMember e) => e is not null && (ReferenceEquals(this, e) || (this.Id == e.Id && this.UserId == e.UserId)); /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() => HashCode.Combine(this.Id.GetHashCode(), this.UserId.GetHashCode()); /// /// Gets whether the two objects are equal. /// /// First channel to compare. /// Second channel to compare. /// Whether the two channels are equal. public static bool operator ==(DiscordThreadChannelMember e1, DiscordThreadChannelMember e2) { var o1 = e1 as object; var o2 = e2 as object; return (o1 != null || o2 == null) && (o1 == null || o2 != null) && ((o1 == null && o2 == null) || (e1.Id == e2.Id && e1.UserId == e2.UserId)); } /// /// Gets whether the two objects are not equal. /// /// First channel to compare. /// Second channel to compare. /// Whether the two channels are not equal. public static bool operator !=(DiscordThreadChannelMember e1, DiscordThreadChannelMember e2) => !(e1 == e2); /// /// Initializes a new instance of the class. /// internal DiscordThreadChannelMember() { } } }