using Discord.API.Gateway; using Discord.Audio; using Discord.Rest; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; using ChannelModel = Discord.API.Channel; using EmojiUpdateModel = Discord.API.Gateway.GuildEmojiUpdateEvent; using ExtendedModel = Discord.API.Gateway.ExtendedGuild; using GuildSyncModel = Discord.API.Gateway.GuildSyncEvent; using MemberModel = Discord.API.GuildMember; using Model = Discord.API.Guild; using PresenceModel = Discord.API.Presence; using RoleModel = Discord.API.Role; using UserModel = Discord.API.User; using VoiceStateModel = Discord.API.VoiceState; using StickerModel = Discord.API.Sticker; using EventModel = Discord.API.GuildScheduledEvent; using System.IO; namespace Discord.WebSocket { /// /// Represents a WebSocket-based guild object. /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketGuild : SocketEntity, IGuild, IDisposable { #region SocketGuild #pragma warning disable IDISP002, IDISP006 private readonly SemaphoreSlim _audioLock; private TaskCompletionSource _syncPromise, _downloaderPromise; private TaskCompletionSource _audioConnectPromise; private ConcurrentDictionary _channels; private ConcurrentDictionary _members; private ConcurrentDictionary _roles; private ConcurrentDictionary _voiceStates; private ConcurrentDictionary _stickers; private ConcurrentDictionary _events; private ImmutableArray _emotes; private AudioClient _audioClient; private VoiceStateUpdateParams _voiceStateUpdateParams; #pragma warning restore IDISP002, IDISP006 /// public string Name { get; private set; } /// public int AFKTimeout { get; private set; } /// public bool IsWidgetEnabled { get; private set; } /// public VerificationLevel VerificationLevel { get; private set; } /// public MfaLevel MfaLevel { get; private set; } /// public DefaultMessageNotifications DefaultMessageNotifications { get; private set; } /// public ExplicitContentFilterLevel ExplicitContentFilter { get; private set; } /// /// Gets the number of members. /// /// /// This property retrieves the number of members returned by Discord. /// /// /// Due to how this property is returned by Discord instead of relying on the WebSocket cache, the /// number here is the most accurate in terms of counting the number of users within this guild. /// /// /// Use this instead of enumerating the count of the /// collection, as you may see discrepancy /// between that and this property. /// /// /// public int MemberCount { get; internal set; } /// Gets the number of members downloaded to the local guild cache. public int DownloadedMemberCount { get; private set; } internal bool IsAvailable { get; private set; } /// Indicates whether the client is connected to this guild. public bool IsConnected { get; internal set; } /// public ulong? ApplicationId { get; internal set; } internal ulong? AFKChannelId { get; private set; } internal ulong? WidgetChannelId { get; private set; } internal ulong? SystemChannelId { get; private set; } internal ulong? RulesChannelId { get; private set; } internal ulong? PublicUpdatesChannelId { get; private set; } /// public ulong OwnerId { get; private set; } /// Gets the user that owns this guild. public SocketGuildUser Owner => GetUser(OwnerId); /// public string VoiceRegionId { get; private set; } /// public string IconId { get; private set; } /// public string SplashId { get; private set; } /// public string DiscoverySplashId { get; private set; } /// public PremiumTier PremiumTier { get; private set; } /// public string BannerId { get; private set; } /// public string VanityURLCode { get; private set; } /// public SystemChannelMessageDeny SystemChannelFlags { get; private set; } /// public string Description { get; private set; } /// public int PremiumSubscriptionCount { get; private set; } /// public string PreferredLocale { get; private set; } /// public int? MaxPresences { get; private set; } /// public int? MaxMembers { get; private set; } /// public int? MaxVideoChannelUsers { get; private set; } /// public NsfwLevel NsfwLevel { get; private set; } /// public CultureInfo PreferredCulture { get; private set; } /// public bool IsBoostProgressBarEnabled { get; private set; } /// public GuildFeatures Features { get; private set; } /// public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); /// public string IconUrl => CDN.GetGuildIconUrl(Id, IconId); /// public string SplashUrl => CDN.GetGuildSplashUrl(Id, SplashId); /// public string DiscoverySplashUrl => CDN.GetGuildDiscoverySplashUrl(Id, DiscoverySplashId); /// public string BannerUrl => CDN.GetGuildBannerUrl(Id, BannerId, ImageFormat.Auto); /// Indicates whether the client has all the members downloaded to the local guild cache. public bool HasAllMembers => MemberCount <= DownloadedMemberCount;// _downloaderPromise.Task.IsCompleted; /// Indicates whether the guild cache is synced to this guild. public bool IsSynced => _syncPromise.Task.IsCompleted; public Task SyncPromise => _syncPromise.Task; public Task DownloaderPromise => _downloaderPromise.Task; /// /// Gets the associated with this guild. /// public IAudioClient AudioClient => _audioClient; /// /// Gets the default channel in this guild. /// /// /// This property retrieves the first viewable text channel for this guild. /// /// This channel does not guarantee the user can send message to it, as it only looks for the first viewable /// text channel. /// /// /// /// A representing the first viewable channel that the user has access to. /// public SocketTextChannel DefaultChannel => TextChannels .Where(c => CurrentUser.GetPermissions(c).ViewChannel && c is not IThreadChannel) .OrderBy(c => c.Position) .FirstOrDefault(); /// /// Gets the AFK voice channel in this guild. /// /// /// A that the AFK users will be moved to after they have idled for too /// long; if none is set. /// public SocketVoiceChannel AFKChannel { get { var id = AFKChannelId; return id.HasValue ? GetVoiceChannel(id.Value) : null; } } /// public int MaxBitrate { get { return PremiumTier switch { PremiumTier.Tier1 => 128000, PremiumTier.Tier2 => 256000, PremiumTier.Tier3 => 384000, _ => 96000, }; } } /// public ulong MaxUploadLimit => GuildHelper.GetUploadLimit(this); /// /// Gets the widget channel (i.e. the channel set in the guild's widget settings) in this guild. /// /// /// A channel set within the server's widget settings; if none is set. /// public SocketGuildChannel WidgetChannel { get { var id = WidgetChannelId; return id.HasValue ? GetChannel(id.Value) : null; } } /// /// Gets the system channel where randomized welcome messages are sent in this guild. /// /// /// A text channel where randomized welcome messages will be sent to; if none is set. /// public SocketTextChannel SystemChannel { get { var id = SystemChannelId; return id.HasValue ? GetTextChannel(id.Value) : null; } } /// /// Gets the channel with the guild rules. /// /// /// A text channel with the guild rules; if none is set. /// public SocketTextChannel RulesChannel { get { var id = RulesChannelId; return id.HasValue ? GetTextChannel(id.Value) : null; } } /// /// Gets the channel where admins and moderators of Community guilds receive /// notices from Discord. /// /// /// A text channel where admins and moderators of Community guilds receive /// notices from Discord; if none is set. /// public SocketTextChannel PublicUpdatesChannel { get { var id = PublicUpdatesChannelId; return id.HasValue ? GetTextChannel(id.Value) : null; } } /// /// Gets a collection of all text channels in this guild. /// /// /// A read-only collection of message channels found within this guild. /// public IReadOnlyCollection TextChannels => Channels.OfType().ToImmutableArray(); /// /// Gets a collection of all voice channels in this guild. /// /// /// A read-only collection of voice channels found within this guild. /// public IReadOnlyCollection VoiceChannels => Channels.OfType().ToImmutableArray(); /// /// Gets a collection of all stage channels in this guild. /// /// /// A read-only collection of stage channels found within this guild. /// public IReadOnlyCollection StageChannels => Channels.OfType().ToImmutableArray(); /// /// Gets a collection of all category channels in this guild. /// /// /// A read-only collection of category channels found within this guild. /// public IReadOnlyCollection CategoryChannels => Channels.OfType().ToImmutableArray(); /// /// Gets a collection of all thread channels in this guild. /// /// /// A read-only collection of thread channels found within this guild. /// public IReadOnlyCollection ThreadChannels => Channels.OfType().ToImmutableArray(); /// /// Gets a collection of all forum channels in this guild. /// /// /// A read-only collection of forum channels found within this guild. /// public IReadOnlyCollection ForumChannels => Channels.OfType().ToImmutableArray(); /// /// Gets the current logged-in user. /// public SocketGuildUser CurrentUser => _members.TryGetValue(Discord.CurrentUser.Id, out SocketGuildUser member) ? member : null; /// /// Gets the built-in role containing all users in this guild. /// /// /// A role object that represents an @everyone role in this guild. /// public SocketRole EveryoneRole => GetRole(Id); /// /// Gets a collection of all channels in this guild. /// /// /// A read-only collection of generic channels found within this guild. /// public IReadOnlyCollection Channels { get { var channels = _channels; var state = Discord.State; return channels.Select(x => x.Value).Where(x => x != null).ToReadOnlyCollection(channels); } } /// public IReadOnlyCollection Emotes => _emotes; /// /// Gets a collection of all custom stickers for this guild. /// public IReadOnlyCollection Stickers => _stickers.Select(x => x.Value).ToImmutableArray(); /// /// Gets a collection of users in this guild. /// /// /// This property retrieves all users found within this guild. /// /// /// This property may not always return all the members for large guilds (i.e. guilds containing /// 100+ users). If you are simply looking to get the number of users present in this guild, /// consider using instead. /// /// /// Otherwise, you may need to enable to fetch /// the full user list upon startup, or use to manually download /// the users. /// /// /// /// /// A collection of guild users found within this guild. /// public IReadOnlyCollection Users => _members.ToReadOnlyCollection(); /// /// Gets a collection of all roles in this guild. /// /// /// A read-only collection of roles found within this guild. /// public IReadOnlyCollection Roles => _roles.ToReadOnlyCollection(); /// /// Gets a collection of all events within this guild. /// /// /// This field is based off of caching alone, since there is no events returned on the guild model. /// /// /// A read-only collection of guild events found within this guild. /// public IReadOnlyCollection Events => _events.ToReadOnlyCollection(); internal SocketGuild(DiscordSocketClient client, ulong id) : base(client, id) { _audioLock = new SemaphoreSlim(1, 1); _emotes = ImmutableArray.Create(); } internal static SocketGuild Create(DiscordSocketClient discord, ClientState state, ExtendedModel model) { var entity = new SocketGuild(discord, model.Id); entity.Update(state, model); return entity; } internal void Update(ClientState state, ExtendedModel model) { IsAvailable = !(model.Unavailable ?? false); if (!IsAvailable) { if(_events == null) _events = new ConcurrentDictionary(); if (_channels == null) _channels = new ConcurrentDictionary(); if (_members == null) _members = new ConcurrentDictionary(); if (_roles == null) _roles = new ConcurrentDictionary(); /*if (Emojis == null) _emojis = ImmutableArray.Create(); if (Features == null) _features = ImmutableArray.Create();*/ _syncPromise = new TaskCompletionSource(); _downloaderPromise = new TaskCompletionSource(); return; } Update(state, model as Model); var channels = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Channels.Length * 1.05)); { for (int i = 0; i < model.Channels.Length; i++) { var channel = SocketGuildChannel.Create(this, state, model.Channels[i]); state.AddChannel(channel); channels.TryAdd(channel.Id, channel); } for(int i = 0; i < model.Threads.Length; i++) { var threadChannel = SocketThreadChannel.Create(this, state, model.Threads[i]); state.AddChannel(threadChannel); channels.TryAdd(threadChannel.Id, threadChannel); } } _channels = channels; var members = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Members.Length * 1.05)); { for (int i = 0; i < model.Members.Length; i++) { var member = SocketGuildUser.Create(this, state, model.Members[i]); if (members.TryAdd(member.Id, member)) member.GlobalUser.AddRef(); } DownloadedMemberCount = members.Count; for (int i = 0; i < model.Presences.Length; i++) { if (members.TryGetValue(model.Presences[i].User.Id, out SocketGuildUser member)) member.Update(state, model.Presences[i], true); } } _members = members; MemberCount = model.MemberCount; var voiceStates = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.VoiceStates.Length * 1.05)); { for (int i = 0; i < model.VoiceStates.Length; i++) { SocketVoiceChannel channel = null; if (model.VoiceStates[i].ChannelId.HasValue) channel = state.GetChannel(model.VoiceStates[i].ChannelId.Value) as SocketVoiceChannel; var voiceState = SocketVoiceState.Create(channel, model.VoiceStates[i]); voiceStates.TryAdd(model.VoiceStates[i].UserId, voiceState); } } _voiceStates = voiceStates; var events = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.GuildScheduledEvents.Length * 1.05)); { for (int i = 0; i < model.GuildScheduledEvents.Length; i++) { var guildEvent = SocketGuildEvent.Create(Discord, this, model.GuildScheduledEvents[i]); events.TryAdd(guildEvent.Id, guildEvent); } } _events = events; _syncPromise = new TaskCompletionSource(); _downloaderPromise = new TaskCompletionSource(); var _ = _syncPromise.TrySetResultAsync(true); /*if (!model.Large) _ = _downloaderPromise.TrySetResultAsync(true);*/ } internal void Update(ClientState state, Model model) { AFKChannelId = model.AFKChannelId; if (model.WidgetChannelId.IsSpecified) WidgetChannelId = model.WidgetChannelId.Value; SystemChannelId = model.SystemChannelId; RulesChannelId = model.RulesChannelId; PublicUpdatesChannelId = model.PublicUpdatesChannelId; AFKTimeout = model.AFKTimeout; if (model.WidgetEnabled.IsSpecified) IsWidgetEnabled = model.WidgetEnabled.Value; IconId = model.Icon; Name = model.Name; OwnerId = model.OwnerId; VoiceRegionId = model.Region; SplashId = model.Splash; DiscoverySplashId = model.DiscoverySplash; VerificationLevel = model.VerificationLevel; MfaLevel = model.MfaLevel; DefaultMessageNotifications = model.DefaultMessageNotifications; ExplicitContentFilter = model.ExplicitContentFilter; ApplicationId = model.ApplicationId; PremiumTier = model.PremiumTier; VanityURLCode = model.VanityURLCode; BannerId = model.Banner; SystemChannelFlags = model.SystemChannelFlags; Description = model.Description; PremiumSubscriptionCount = model.PremiumSubscriptionCount.GetValueOrDefault(); NsfwLevel = model.NsfwLevel; if (model.MaxPresences.IsSpecified) MaxPresences = model.MaxPresences.Value ?? 25000; if (model.MaxMembers.IsSpecified) MaxMembers = model.MaxMembers.Value; if (model.MaxVideoChannelUsers.IsSpecified) MaxVideoChannelUsers = model.MaxVideoChannelUsers.Value; PreferredLocale = model.PreferredLocale; PreferredCulture = PreferredLocale == null ? null : new CultureInfo(PreferredLocale); if (model.IsBoostProgressBarEnabled.IsSpecified) IsBoostProgressBarEnabled = model.IsBoostProgressBarEnabled.Value; if (model.Emojis != null) { var emojis = ImmutableArray.CreateBuilder(model.Emojis.Length); for (int i = 0; i < model.Emojis.Length; i++) emojis.Add(model.Emojis[i].ToEntity()); _emotes = emojis.ToImmutable(); } else _emotes = ImmutableArray.Create(); Features = model.Features; var roles = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Roles.Length * 1.05)); for (int i = 0; i < model.Roles.Length; i++) { var role = SocketRole.Create(this, state, model.Roles[i]); roles.TryAdd(role.Id, role); } _roles = roles; if (model.Stickers != null) { var stickers = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Stickers.Length * 1.05)); for (int i = 0; i < model.Stickers.Length; i++) { var sticker = model.Stickers[i]; if (sticker.User.IsSpecified) AddOrUpdateUser(sticker.User.Value); var entity = SocketCustomSticker.Create(Discord, sticker, this, sticker.User.IsSpecified ? sticker.User.Value.Id : null); stickers.TryAdd(sticker.Id, entity); } _stickers = stickers; } else _stickers = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, 7); } /*internal void Update(ClientState state, GuildSyncModel model) //TODO remove? userbot related { var members = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Members.Length * 1.05)); { for (int i = 0; i < model.Members.Length; i++) { var member = SocketGuildUser.Create(this, state, model.Members[i]); members.TryAdd(member.Id, member); } DownloadedMemberCount = members.Count; for (int i = 0; i < model.Presences.Length; i++) { if (members.TryGetValue(model.Presences[i].User.Id, out SocketGuildUser member)) member.Update(state, model.Presences[i], true); } } _members = members; var _ = _syncPromise.TrySetResultAsync(true); //if (!model.Large) // _ = _downloaderPromise.TrySetResultAsync(true); }*/ internal void Update(ClientState state, EmojiUpdateModel model) { var emotes = ImmutableArray.CreateBuilder(model.Emojis.Length); for (int i = 0; i < model.Emojis.Length; i++) emotes.Add(model.Emojis[i].ToEntity()); _emotes = emotes.ToImmutable(); } #endregion #region General /// public Task DeleteAsync(RequestOptions options = null) => GuildHelper.DeleteAsync(this, Discord, options); /// /// is . public Task ModifyAsync(Action func, RequestOptions options = null) => GuildHelper.ModifyAsync(this, Discord, func, options); /// /// is . public Task ModifyWidgetAsync(Action func, RequestOptions options = null) => GuildHelper.ModifyWidgetAsync(this, Discord, func, options); /// public Task ReorderChannelsAsync(IEnumerable args, RequestOptions options = null) => GuildHelper.ReorderChannelsAsync(this, Discord, args, options); /// public Task ReorderRolesAsync(IEnumerable args, RequestOptions options = null) => GuildHelper.ReorderRolesAsync(this, Discord, args, options); /// public Task LeaveAsync(RequestOptions options = null) => GuildHelper.LeaveAsync(this, Discord, options); #endregion #region Bans /// public IAsyncEnumerable> GetBansAsync(int limit = DiscordConfig.MaxBansPerBatch, RequestOptions options = null) => GuildHelper.GetBansAsync(this, Discord, null, Direction.Before, limit, options); /// public IAsyncEnumerable> GetBansAsync(ulong fromUserId, Direction dir, int limit = DiscordConfig.MaxBansPerBatch, RequestOptions options = null) => GuildHelper.GetBansAsync(this, Discord, fromUserId, dir, limit, options); /// public IAsyncEnumerable> GetBansAsync(IUser fromUser, Direction dir, int limit = DiscordConfig.MaxBansPerBatch, RequestOptions options = null) => GuildHelper.GetBansAsync(this, Discord, fromUser.Id, dir, limit, options); /// /// Gets a ban object for a banned user. /// /// The banned user. /// The options to be used when sending the request. /// /// A task that represents the asynchronous get operation. The task result contains a ban object, which /// contains the user information and the reason for the ban; if the ban entry cannot be found. /// public Task GetBanAsync(IUser user, RequestOptions options = null) => GuildHelper.GetBanAsync(this, Discord, user.Id, options); /// /// Gets a ban object for a banned user. /// /// The snowflake identifier for the banned user. /// The options to be used when sending the request. /// /// A task that represents the asynchronous get operation. The task result contains a ban object, which /// contains the user information and the reason for the ban; if the ban entry cannot be found. /// public Task GetBanAsync(ulong userId, RequestOptions options = null) => GuildHelper.GetBanAsync(this, Discord, userId, options); /// public Task AddBanAsync(IUser user, int pruneDays = 0, string reason = null, RequestOptions options = null) => GuildHelper.AddBanAsync(this, Discord, user.Id, pruneDays, reason, options); /// public Task AddBanAsync(ulong userId, int pruneDays = 0, string reason = null, RequestOptions options = null) => GuildHelper.AddBanAsync(this, Discord, userId, pruneDays, reason, options); /// public Task RemoveBanAsync(IUser user, RequestOptions options = null) => GuildHelper.RemoveBanAsync(this, Discord, user.Id, options); /// public Task RemoveBanAsync(ulong userId, RequestOptions options = null) => GuildHelper.RemoveBanAsync(this, Discord, userId, options); #endregion #region Channels /// /// Gets a channel in this guild. /// /// The snowflake identifier for the channel. /// /// A generic channel associated with the specified ; if none is found. /// public SocketGuildChannel GetChannel(ulong id) { var channel = Discord.State.GetChannel(id) as SocketGuildChannel; if (channel?.Guild.Id == Id) return channel; return null; } /// /// Gets a text channel in this guild. /// /// The snowflake identifier for the text channel. /// /// A text channel associated with the specified ; if none is found. /// public SocketTextChannel GetTextChannel(ulong id) => GetChannel(id) as SocketTextChannel; /// /// Gets a thread in this guild. /// /// The snowflake identifier for the thread. /// /// A thread channel associated with the specified ; if none is found. /// public SocketThreadChannel GetThreadChannel(ulong id) => GetChannel(id) as SocketThreadChannel; /// /// Gets a forum channel in this guild. /// /// The snowflake identifier for the forum channel. /// /// A forum channel associated with the specified ; if none is found. /// public SocketForumChannel GetForumChannel(ulong id) => GetChannel(id) as SocketForumChannel; /// /// Gets a voice channel in this guild. /// /// The snowflake identifier for the voice channel. /// /// A voice channel associated with the specified ; if none is found. /// public SocketVoiceChannel GetVoiceChannel(ulong id) => GetChannel(id) as SocketVoiceChannel; /// /// Gets a stage channel in this guild. /// /// The snowflake identifier for the stage channel. /// /// A stage channel associated with the specified ; if none is found. /// public SocketStageChannel GetStageChannel(ulong id) => GetChannel(id) as SocketStageChannel; /// /// Gets a category channel in this guild. /// /// The snowflake identifier for the category channel. /// /// A category channel associated with the specified ; if none is found. /// public SocketCategoryChannel GetCategoryChannel(ulong id) => GetChannel(id) as SocketCategoryChannel; /// /// Creates a new text channel in this guild. /// /// /// The following example creates a new text channel under an existing category named Wumpus with a set topic. /// /// var categories = await guild.GetCategoriesAsync(); /// var targetCategory = categories.FirstOrDefault(x => x.Name == "wumpus"); /// if (targetCategory == null) return; /// await Context.Guild.CreateTextChannelAsync(name, x => /// { /// x.CategoryId = targetCategory.Id; /// x.Topic = $"This channel was created at {DateTimeOffset.UtcNow} by {user}."; /// }); /// /// /// The new name for the text channel. /// The delegate containing the properties to be applied to the channel upon its creation. /// The options to be used when sending the request. /// /// A task that represents the asynchronous creation operation. The task result contains the newly created /// text channel. /// public Task CreateTextChannelAsync(string name, Action func = null, RequestOptions options = null) => GuildHelper.CreateTextChannelAsync(this, Discord, name, options, func); /// /// Creates a new voice channel in this guild. /// /// The new name for the voice channel. /// The delegate containing the properties to be applied to the channel upon its creation. /// The options to be used when sending the request. /// is . /// /// A task that represents the asynchronous creation operation. The task result contains the newly created /// voice channel. /// public Task CreateVoiceChannelAsync(string name, Action func = null, RequestOptions options = null) => GuildHelper.CreateVoiceChannelAsync(this, Discord, name, options, func); /// /// Creates a new stage channel in this guild. /// /// The new name for the stage channel. /// The delegate containing the properties to be applied to the channel upon its creation. /// The options to be used when sending the request. /// /// A task that represents the asynchronous creation operation. The task result contains the newly created /// stage channel. /// public Task CreateStageChannelAsync(string name, Action func = null, RequestOptions options = null) => GuildHelper.CreateStageChannelAsync(this, Discord, name, options, func); /// /// Creates a new channel category in this guild. /// /// The new name for the category. /// The delegate containing the properties to be applied to the channel upon its creation. /// The options to be used when sending the request. /// is . /// /// A task that represents the asynchronous creation operation. The task result contains the newly created /// category channel. /// public Task CreateCategoryChannelAsync(string name, Action func = null, RequestOptions options = null) => GuildHelper.CreateCategoryChannelAsync(this, Discord, name, options, func); /// /// Creates a new channel forum in this guild. /// /// The new name for the forum. /// The delegate containing the properties to be applied to the channel upon its creation. /// The options to be used when sending the request. /// is . /// /// A task that represents the asynchronous creation operation. The task result contains the newly created /// forum channel. /// public Task CreateForumChannelAsync(string name, Action func = null, RequestOptions options = null) => GuildHelper.CreateForumChannelAsync(this, Discord, name, options, func); internal SocketGuildChannel AddChannel(ClientState state, ChannelModel model) { var channel = SocketGuildChannel.Create(this, state, model); _channels.TryAdd(model.Id, channel); state.AddChannel(channel); return channel; } internal SocketGuildChannel AddOrUpdateChannel(ClientState state, ChannelModel model) { if (_channels.TryGetValue(model.Id, out SocketGuildChannel channel)) channel.Update(Discord.State, model); else { channel = SocketGuildChannel.Create(this, Discord.State, model); _channels[channel.Id] = channel; state.AddChannel(channel); } return channel; } internal SocketGuildChannel RemoveChannel(ClientState state, ulong id) { if (_channels.TryRemove(id, out var _)) return state.RemoveChannel(id) as SocketGuildChannel; return null; } internal void PurgeChannelCache(ClientState state) { foreach (var channelId in _channels) state.RemoveChannel(channelId.Key); _channels.Clear(); } #endregion #region Voice Regions /// /// Gets a collection of all the voice regions this guild can access. /// /// The options to be used when sending the request. /// /// A task that represents the asynchronous get operation. The task result contains a read-only collection of /// voice regions the guild can access. /// public Task> GetVoiceRegionsAsync(RequestOptions options = null) => GuildHelper.GetVoiceRegionsAsync(this, Discord, options); #endregion #region Integrations public Task> GetIntegrationsAsync(RequestOptions options = null) => GuildHelper.GetIntegrationsAsync(this, Discord, options); public Task DeleteIntegrationAsync(ulong id, RequestOptions options = null) => GuildHelper.DeleteIntegrationAsync(this, Discord, id, options); #endregion #region Interactions /// /// Deletes all application commands in the current guild. /// /// The options to be used when sending the request. /// /// A task that represents the asynchronous delete operation. /// public Task DeleteApplicationCommandsAsync(RequestOptions options = null) => InteractionHelper.DeleteAllGuildCommandsAsync(Discord, Id, options); /// /// Gets a collection of slash commands created by the current user in this guild. /// /// Whether to include full localization dictionaries in the returned objects, instead of the name localized and description localized fields. /// The target locale of the localized name and description fields. Sets X-Discord-Locale header, which takes precedence over Accept-Language. /// The options to be used when sending the request. /// /// A task that represents the asynchronous get operation. The task result contains a read-only collection of /// slash commands created by the current user. /// public async Task> GetApplicationCommandsAsync(bool withLocalizations = false, string locale = null, RequestOptions options = null) { var commands = (await Discord.ApiClient.GetGuildApplicationCommandsAsync(Id, withLocalizations, locale, options)) .Select(x => SocketApplicationCommand.Create(Discord, x, Id)); foreach (var command in commands) { Discord.State.AddCommand(command); } return commands.ToImmutableArray(); } /// /// Gets an application command within this guild with the specified id. /// /// The id of the application command to get. /// The that determines whether the object should be fetched from cache. /// The options to be used when sending the request. /// /// A ValueTask that represents the asynchronous get operation. The task result contains a /// if found, otherwise . /// public async ValueTask GetApplicationCommandAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) { var command = Discord.State.GetCommand(id); if (command != null) return command; if (mode == CacheMode.CacheOnly) return null; var model = await Discord.ApiClient.GetGlobalApplicationCommandAsync(id, options); if (model == null) return null; command = SocketApplicationCommand.Create(Discord, model, Id); Discord.State.AddCommand(command); return command; } /// /// Creates an application command within this guild. /// /// The properties to use when creating the command. /// The options to be used when sending the request. /// /// A task that represents the asynchronous creation operation. The task result contains the command that was created. /// public async Task CreateApplicationCommandAsync(ApplicationCommandProperties properties, RequestOptions options = null) { var model = await InteractionHelper.CreateGuildCommandAsync(Discord, Id, properties, options); var entity = Discord.State.GetOrAddCommand(model.Id, (id) => SocketApplicationCommand.Create(Discord, model)); entity.Update(model); return entity; } /// /// Overwrites the application commands within this guild. /// /// A collection of properties to use when creating the commands. /// The options to be used when sending the request. /// /// A task that represents the asynchronous creation operation. The task result contains a collection of commands that was created. /// public async Task> BulkOverwriteApplicationCommandAsync(ApplicationCommandProperties[] properties, RequestOptions options = null) { var models = await InteractionHelper.BulkOverwriteGuildCommandsAsync(Discord, Id, properties, options); var entities = models.Select(x => SocketApplicationCommand.Create(Discord, x)); Discord.State.PurgeCommands(x => !x.IsGlobalCommand && x.Guild.Id == Id); foreach(var entity in entities) { Discord.State.AddCommand(entity); } return entities.ToImmutableArray(); } #endregion #region Invites /// /// Gets a collection of all invites in this guild. /// /// The options to be used when sending the request. /// /// A task that represents the asynchronous get operation. The task result contains a read-only collection of /// invite metadata, each representing information for an invite found within this guild. /// public Task> GetInvitesAsync(RequestOptions options = null) => GuildHelper.GetInvitesAsync(this, Discord, options); /// /// Gets the vanity invite URL of this guild. /// /// The options to be used when sending the request. /// /// A task that represents the asynchronous get operation. The task result contains the partial metadata of /// the vanity invite found within this guild; if none is found. /// public Task GetVanityInviteAsync(RequestOptions options = null) => GuildHelper.GetVanityInviteAsync(this, Discord, options); #endregion #region Roles /// /// Gets a role in this guild. /// /// The snowflake identifier for the role. /// /// A role that is associated with the specified ; if none is found. /// public SocketRole GetRole(ulong id) { if (_roles.TryGetValue(id, out SocketRole value)) return value; return null; } /// /// Creates a new role with the provided name. /// /// The new name for the role. /// The guild permission that the role should possess. /// The color of the role. /// Whether the role is separated from others on the sidebar. /// Whether the role can be mentioned. /// The options to be used when sending the request. /// is . /// /// A task that represents the asynchronous creation operation. The task result contains the newly created /// role. /// public Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), bool isHoisted = false, bool isMentionable = false, RequestOptions options = null) => GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, isMentionable, options); internal SocketRole AddRole(RoleModel model) { var role = SocketRole.Create(this, Discord.State, model); _roles[model.Id] = role; return role; } internal SocketRole RemoveRole(ulong id) { if (_roles.TryRemove(id, out SocketRole role)) return role; return null; } internal SocketRole AddOrUpdateRole(RoleModel model) { if (_roles.TryGetValue(model.Id, out SocketRole role)) _roles[model.Id].Update(Discord.State, model); else role = AddRole(model); return role; } internal SocketCustomSticker AddSticker(StickerModel model) { if (model.User.IsSpecified) AddOrUpdateUser(model.User.Value); var sticker = SocketCustomSticker.Create(Discord, model, this, model.User.IsSpecified ? model.User.Value.Id : null); _stickers[model.Id] = sticker; return sticker; } internal SocketCustomSticker AddOrUpdateSticker(StickerModel model) { if (_stickers.TryGetValue(model.Id, out SocketCustomSticker sticker)) _stickers[model.Id].Update(model); else sticker = AddSticker(model); return sticker; } internal SocketCustomSticker RemoveSticker(ulong id) { if (_stickers.TryRemove(id, out SocketCustomSticker sticker)) return sticker; return null; } #endregion #region Users /// public Task AddGuildUserAsync(ulong id, string accessToken, Action func = null, RequestOptions options = null) => GuildHelper.AddGuildUserAsync(this, Discord, id, accessToken, func, options); /// /// Gets a user from this guild. /// /// /// This method retrieves a user found within this guild. /// /// This may return in the WebSocket implementation due to incomplete user collection in /// large guilds. /// /// /// The snowflake identifier of the user. /// /// A guild user associated with the specified ; if none is found. /// public SocketGuildUser GetUser(ulong id) { if (_members.TryGetValue(id, out SocketGuildUser member)) return member; return null; } /// public Task PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null, IEnumerable includeRoleIds = null) => GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options, includeRoleIds); internal SocketGuildUser AddOrUpdateUser(UserModel model) { if (_members.TryGetValue(model.Id, out SocketGuildUser member)) member.GlobalUser?.Update(Discord.State, model); else { member = SocketGuildUser.Create(this, Discord.State, model); member.GlobalUser.AddRef(); _members[member.Id] = member; DownloadedMemberCount++; } return member; } internal SocketGuildUser AddOrUpdateUser(MemberModel model) { if (_members.TryGetValue(model.User.Id, out SocketGuildUser member)) member.Update(Discord.State, model); else { member = SocketGuildUser.Create(this, Discord.State, model); member.GlobalUser.AddRef(); _members[member.Id] = member; DownloadedMemberCount++; } return member; } internal SocketGuildUser AddOrUpdateUser(PresenceModel model) { if (_members.TryGetValue(model.User.Id, out SocketGuildUser member)) member.Update(Discord.State, model, false); else { member = SocketGuildUser.Create(this, Discord.State, model); member.GlobalUser.AddRef(); _members[member.Id] = member; DownloadedMemberCount++; } return member; } internal SocketGuildUser RemoveUser(ulong id) { if (_members.TryRemove(id, out SocketGuildUser member)) { DownloadedMemberCount--; member.GlobalUser.RemoveRef(Discord); return member; } return null; } /// /// Purges this guild's user cache. /// public void PurgeUserCache() => PurgeUserCache(_ => true); /// /// Purges this guild's user cache. /// /// The predicate used to select which users to clear. public void PurgeUserCache(Func predicate) { var membersToPurge = Users.Where(x => predicate.Invoke(x) && x?.Id != Discord.CurrentUser.Id); var membersToKeep = Users.Where(x => !predicate.Invoke(x) || x?.Id == Discord.CurrentUser.Id); foreach (var member in membersToPurge) if(_members.TryRemove(member.Id, out _)) member.GlobalUser.RemoveRef(Discord); foreach (var member in membersToKeep) _members.TryAdd(member.Id, member); _downloaderPromise = new TaskCompletionSource(); DownloadedMemberCount = _members.Count; } /// /// Gets a collection of all users in this guild. /// /// /// This method retrieves all users found within this guild throught REST. /// Users returned by this method are not cached. /// /// The options to be used when sending the request. /// /// A task that represents the asynchronous get operation. The task result contains a collection of guild /// users found within this guild. /// public IAsyncEnumerable> GetUsersAsync(RequestOptions options = null) { if (HasAllMembers) return ImmutableArray.Create(Users).ToAsyncEnumerable>(); return GuildHelper.GetUsersAsync(this, Discord, null, null, options); } /// public async Task DownloadUsersAsync() { await Discord.DownloadUsersAsync(new[] { this }).ConfigureAwait(false); } internal void CompleteDownloadUsers() { _downloaderPromise.TrySetResultAsync(true); } /// /// Gets a collection of users in this guild that the name or nickname starts with the /// provided at . /// /// /// The can not be higher than . /// /// The partial name or nickname to search. /// The maximum number of users to be gotten. /// The options to be used when sending the request. /// /// A task that represents the asynchronous get operation. The task result contains a collection of guild /// users that the name or nickname starts with the provided at . /// public Task> SearchUsersAsync(string query, int limit = DiscordConfig.MaxUsersPerBatch, RequestOptions options = null) => GuildHelper.SearchUsersAsync(this, Discord, query, limit, options); #endregion #region Guild Events /// /// Gets an event in this guild. /// /// The snowflake identifier for the event. /// /// An event that is associated with the specified ; if none is found. /// public SocketGuildEvent GetEvent(ulong id) { if (_events.TryGetValue(id, out SocketGuildEvent value)) return value; return null; } internal SocketGuildEvent RemoveEvent(ulong id) { if (_events.TryRemove(id, out SocketGuildEvent value)) return value; return null; } internal SocketGuildEvent AddOrUpdateEvent(EventModel model) { if (_events.TryGetValue(model.Id, out SocketGuildEvent value)) value.Update(model); else { value = SocketGuildEvent.Create(Discord, this, model); _events[model.Id] = value; } return value; } /// /// Gets an event within this guild. /// /// The snowflake identifier for the event. /// The options to be used when sending the request. /// /// A task that represents the asynchronous get operation. /// public Task GetEventAsync(ulong id, RequestOptions options = null) => GuildHelper.GetGuildEventAsync(Discord, id, this, options); /// /// Gets all active events within this guild. /// /// The options to be used when sending the request. /// /// A task that represents the asynchronous get operation. /// public Task> GetEventsAsync(RequestOptions options = null) => GuildHelper.GetGuildEventsAsync(Discord, this, options); /// /// Creates an event within this guild. /// /// The name of the event. /// The privacy level of the event. /// The start time of the event. /// The type of the event. /// The description of the event. /// The end time of the event. /// /// The channel id of the event. /// /// The event must have a type of or /// in order to use this property. /// /// /// The location of the event; links are supported /// The optional banner image for the event. /// The options to be used when sending the request. /// /// A task that represents the asynchronous create operation. /// public Task CreateEventAsync( string name, DateTimeOffset startTime, GuildScheduledEventType type, GuildScheduledEventPrivacyLevel privacyLevel = GuildScheduledEventPrivacyLevel.Private, string description = null, DateTimeOffset? endTime = null, ulong? channelId = null, string location = null, Image? coverImage = null, RequestOptions options = null) { // requirements taken from https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-permissions-requirements switch (type) { case GuildScheduledEventType.Stage: CurrentUser.GuildPermissions.Ensure(GuildPermission.ManageEvents | GuildPermission.ManageChannels | GuildPermission.MuteMembers | GuildPermission.MoveMembers); break; case GuildScheduledEventType.Voice: CurrentUser.GuildPermissions.Ensure(GuildPermission.ManageEvents | GuildPermission.ViewChannel | GuildPermission.Connect); break; case GuildScheduledEventType.External: CurrentUser.GuildPermissions.Ensure(GuildPermission.ManageEvents); break; } return GuildHelper.CreateGuildEventAsync(Discord, this, name, privacyLevel, startTime, type, description, endTime, channelId, location, coverImage, options); } #endregion #region Audit logs /// /// Gets the specified number of audit log entries for this guild. /// /// The number of audit log entries to fetch. /// The options to be used when sending the request. /// The audit log entry ID to filter entries before. /// The type of actions to filter. /// The user ID to filter entries for. /// /// A task that represents the asynchronous get operation. The task result contains a read-only collection /// of the requested audit log entries. /// public IAsyncEnumerable> GetAuditLogsAsync(int limit, RequestOptions options = null, ulong? beforeId = null, ulong? userId = null, ActionType? actionType = null) => GuildHelper.GetAuditLogsAsync(this, Discord, beforeId, limit, options, userId: userId, actionType: actionType); #endregion #region Webhooks /// /// Gets a webhook found within this guild. /// /// The identifier for the webhook. /// The options to be used when sending the request. /// /// A task that represents the asynchronous get operation. The task result contains the webhook with the /// specified ; if none is found. /// public Task GetWebhookAsync(ulong id, RequestOptions options = null) => GuildHelper.GetWebhookAsync(this, Discord, id, options); /// /// Gets a collection of all webhook from this guild. /// /// The options to be used when sending the request. /// /// A task that represents the asynchronous get operation. The task result contains a read-only collection /// of webhooks found within the guild. /// public Task> GetWebhooksAsync(RequestOptions options = null) => GuildHelper.GetWebhooksAsync(this, Discord, options); #endregion #region Emotes /// public Task> GetEmotesAsync(RequestOptions options = null) => GuildHelper.GetEmotesAsync(this, Discord, options); /// public Task GetEmoteAsync(ulong id, RequestOptions options = null) => GuildHelper.GetEmoteAsync(this, Discord, id, options); /// public Task CreateEmoteAsync(string name, Image image, Optional> roles = default(Optional>), RequestOptions options = null) => GuildHelper.CreateEmoteAsync(this, Discord, name, image, roles, options); /// /// is . public Task ModifyEmoteAsync(GuildEmote emote, Action func, RequestOptions options = null) => GuildHelper.ModifyEmoteAsync(this, Discord, emote.Id, func, options); /// public Task DeleteEmoteAsync(GuildEmote emote, RequestOptions options = null) => GuildHelper.DeleteEmoteAsync(this, Discord, emote.Id, options); /// /// Moves the user to the voice channel. /// /// The user to move. /// the channel where the user gets moved to. /// A task that represents the asynchronous operation for moving a user. public Task MoveAsync(IGuildUser user, IVoiceChannel targetChannel) => user.ModifyAsync(x => x.Channel = new Optional(targetChannel)); /// /// Disconnects the user from its current voice channel /// /// The user to disconnect. /// A task that represents the asynchronous operation for disconnecting a user. async Task IGuild.DisconnectAsync(IGuildUser user) => await user.ModifyAsync(x => x.Channel = null); #endregion #region Stickers /// /// Gets a specific sticker within this guild. /// /// The id of the sticker to get. /// The that determines whether the object should be fetched from cache. /// The options to be used when sending the request. /// /// A task that represents the asynchronous get operation. The task result contains the sticker found with the /// specified ; if none is found. /// public async ValueTask GetStickerAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) { var sticker = _stickers?.FirstOrDefault(x => x.Key == id); if (sticker?.Value != null) return sticker?.Value; if (mode == CacheMode.CacheOnly) return null; var model = await Discord.ApiClient.GetGuildStickerAsync(Id, id, options).ConfigureAwait(false); if (model == null) return null; return AddOrUpdateSticker(model); } /// /// Gets a specific sticker within this guild. /// /// The id of the sticker to get. /// A sticker, if none is found then . public SocketCustomSticker GetSticker(ulong id) => GetStickerAsync(id, CacheMode.CacheOnly).GetAwaiter().GetResult(); /// /// Gets a collection of all stickers within this guild. /// /// The that determines whether the object should be fetched from cache. /// The options to be used when sending the request. /// /// A task that represents the asynchronous get operation. The task result contains a read-only collection /// of stickers found within the guild. /// public async ValueTask> GetStickersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null) { if (Stickers.Count > 0) return Stickers; if (mode == CacheMode.CacheOnly) return ImmutableArray.Create(); var models = await Discord.ApiClient.ListGuildStickersAsync(Id, options).ConfigureAwait(false); List stickers = new(); foreach (var model in models) { stickers.Add(AddOrUpdateSticker(model)); } return stickers; } /// /// Creates a new sticker in this guild. /// /// The name of the sticker. /// The description of the sticker. /// The tags of the sticker. /// The image of the new emote. /// The options to be used when sending the request. /// /// A task that represents the asynchronous creation operation. The task result contains the created sticker. /// public async Task CreateStickerAsync(string name, string description, IEnumerable tags, Image image, RequestOptions options = null) { var model = await GuildHelper.CreateStickerAsync(Discord, this, name, description, tags, image, options).ConfigureAwait(false); return AddOrUpdateSticker(model); } /// /// Creates a new sticker in this guild /// /// The name of the sticker. /// The description of the sticker. /// The tags of the sticker. /// The path of the file to upload. /// The options to be used when sending the request. /// /// A task that represents the asynchronous creation operation. The task result contains the created sticker. /// public Task CreateStickerAsync(string name, string description, IEnumerable tags, string path, RequestOptions options = null) { var fs = File.OpenRead(path); return CreateStickerAsync(name, description, tags, fs, Path.GetFileName(fs.Name), options); } /// /// Creates a new sticker in this guild /// /// The name of the sticker. /// The description of the sticker. /// The tags of the sticker. /// The stream containing the file data. /// The name of the file with the extension, ex: image.png. /// The options to be used when sending the request. /// /// A task that represents the asynchronous creation operation. The task result contains the created sticker. /// public async Task CreateStickerAsync(string name, string description, IEnumerable tags, Stream stream, string filename, RequestOptions options = null) { var model = await GuildHelper.CreateStickerAsync(Discord, this, name, description, tags, stream, filename, options).ConfigureAwait(false); return AddOrUpdateSticker(model); } /// /// Deletes a sticker within this guild. /// /// The sticker to delete. /// The options to be used when sending the request. /// /// A task that represents the asynchronous removal operation. /// public Task DeleteStickerAsync(SocketCustomSticker sticker, RequestOptions options = null) => sticker.DeleteAsync(options); #endregion #region Voice States internal async Task AddOrUpdateVoiceStateAsync(ClientState state, VoiceStateModel model) { var voiceChannel = state.GetChannel(model.ChannelId.Value) as SocketVoiceChannel; var before = GetVoiceState(model.UserId) ?? SocketVoiceState.Default; var after = SocketVoiceState.Create(voiceChannel, model); _voiceStates[model.UserId] = after; if (_audioClient != null && before.VoiceChannel?.Id != after.VoiceChannel?.Id) { if (model.UserId == CurrentUser.Id) { if (after.VoiceChannel != null && _audioClient.ChannelId != after.VoiceChannel?.Id) { _audioClient.ChannelId = after.VoiceChannel.Id; await RepopulateAudioStreamsAsync().ConfigureAwait(false); } } else { await _audioClient.RemoveInputStreamAsync(model.UserId).ConfigureAwait(false); //User changed channels, end their stream if (CurrentUser.VoiceChannel != null && after.VoiceChannel?.Id == CurrentUser.VoiceChannel?.Id) await _audioClient.CreateInputStreamAsync(model.UserId).ConfigureAwait(false); } } return after; } internal SocketVoiceState? GetVoiceState(ulong id) { if (_voiceStates.TryGetValue(id, out SocketVoiceState voiceState)) return voiceState; return null; } internal async Task RemoveVoiceStateAsync(ulong id) { if (_voiceStates.TryRemove(id, out SocketVoiceState voiceState)) { if (_audioClient != null) await _audioClient.RemoveInputStreamAsync(id).ConfigureAwait(false); //User changed channels, end their stream return voiceState; } return null; } #endregion #region Audio internal AudioInStream GetAudioStream(ulong userId) { return _audioClient?.GetInputStream(userId); } internal async Task ConnectAudioAsync(ulong channelId, bool selfDeaf, bool selfMute, bool external) { TaskCompletionSource promise; await _audioLock.WaitAsync().ConfigureAwait(false); try { await DisconnectAudioInternalAsync().ConfigureAwait(false); promise = new TaskCompletionSource(); _audioConnectPromise = promise; _voiceStateUpdateParams = new VoiceStateUpdateParams { GuildId = Id, ChannelId = channelId, SelfDeaf = selfDeaf, SelfMute = selfMute }; if (external) { #pragma warning disable IDISP001 var _ = promise.TrySetResultAsync(null); await Discord.ApiClient.SendVoiceStateUpdateAsync(_voiceStateUpdateParams).ConfigureAwait(false); return null; #pragma warning restore IDISP001 } if (_audioClient == null) { var audioClient = new AudioClient(this, Discord.GetAudioId(), channelId); audioClient.Disconnected += async ex => { if (!promise.Task.IsCompleted) { try { audioClient.Dispose(); } catch { } _audioClient = null; if (ex != null) await promise.TrySetExceptionAsync(ex); else await promise.TrySetCanceledAsync(); return; } }; audioClient.Connected += () => { #pragma warning disable IDISP001 var _ = promise.TrySetResultAsync(_audioClient); #pragma warning restore IDISP001 return Task.Delay(0); }; #pragma warning disable IDISP003 _audioClient = audioClient; #pragma warning restore IDISP003 } await Discord.ApiClient.SendVoiceStateUpdateAsync(_voiceStateUpdateParams).ConfigureAwait(false); } catch { await DisconnectAudioInternalAsync().ConfigureAwait(false); throw; } finally { _audioLock.Release(); } try { var timeoutTask = Task.Delay(15000); if (await Task.WhenAny(promise.Task, timeoutTask).ConfigureAwait(false) == timeoutTask) throw new TimeoutException(); return await promise.Task.ConfigureAwait(false); } catch { await DisconnectAudioAsync().ConfigureAwait(false); throw; } } internal async Task DisconnectAudioAsync() { await _audioLock.WaitAsync().ConfigureAwait(false); try { await DisconnectAudioInternalAsync().ConfigureAwait(false); } finally { _audioLock.Release(); } } private async Task DisconnectAudioInternalAsync() { _audioConnectPromise?.TrySetCanceledAsync(); //Cancel any previous audio connection _audioConnectPromise = null; if (_audioClient != null) await _audioClient.StopAsync().ConfigureAwait(false); await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, null, false, false).ConfigureAwait(false); _audioClient?.Dispose(); _audioClient = null; _voiceStateUpdateParams = null; } internal async Task ModifyAudioAsync(ulong channelId, Action func, RequestOptions options) { await _audioLock.WaitAsync().ConfigureAwait(false); try { await ModifyAudioInternalAsync(channelId, func, options).ConfigureAwait(false); } finally { _audioLock.Release(); } } private async Task ModifyAudioInternalAsync(ulong channelId, Action func, RequestOptions options) { if (_voiceStateUpdateParams == null || _voiceStateUpdateParams.ChannelId != channelId) throw new InvalidOperationException("Cannot modify properties of not connected audio channel"); var props = new AudioChannelProperties(); func(props); if (props.SelfDeaf.IsSpecified) _voiceStateUpdateParams.SelfDeaf = props.SelfDeaf.Value; if (props.SelfMute.IsSpecified) _voiceStateUpdateParams.SelfMute = props.SelfMute.Value; await Discord.ApiClient.SendVoiceStateUpdateAsync(_voiceStateUpdateParams, options).ConfigureAwait(false); } internal async Task FinishConnectAudio(string url, string token) { //TODO: Mem Leak: Disconnected/Connected handlers aren't cleaned up var voiceState = GetVoiceState(Discord.CurrentUser.Id).Value; await _audioLock.WaitAsync().ConfigureAwait(false); try { if (_audioClient != null) { await RepopulateAudioStreamsAsync().ConfigureAwait(false); await _audioClient.StartAsync(url, Discord.CurrentUser.Id, voiceState.VoiceSessionId, token).ConfigureAwait(false); } } catch (OperationCanceledException) { await DisconnectAudioInternalAsync().ConfigureAwait(false); } catch (Exception e) { await _audioConnectPromise.SetExceptionAsync(e).ConfigureAwait(false); await DisconnectAudioInternalAsync().ConfigureAwait(false); } finally { _audioLock.Release(); } } internal async Task RepopulateAudioStreamsAsync() { await _audioClient.ClearInputStreamsAsync().ConfigureAwait(false); //We changed channels, end all current streams if (CurrentUser.VoiceChannel != null) { foreach (var pair in _voiceStates) { if (pair.Value.VoiceChannel?.Id == CurrentUser.VoiceChannel?.Id && pair.Key != CurrentUser.Id) await _audioClient.CreateInputStreamAsync(pair.Key).ConfigureAwait(false); } } } /// /// Gets the name of the guild. /// /// /// A string that resolves to . /// public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Id})"; internal SocketGuild Clone() => MemberwiseClone() as SocketGuild; #endregion #region IGuild /// ulong? IGuild.AFKChannelId => AFKChannelId; /// IAudioClient IGuild.AudioClient => AudioClient; /// bool IGuild.Available => true; /// ulong? IGuild.WidgetChannelId => WidgetChannelId; /// ulong? IGuild.SystemChannelId => SystemChannelId; /// ulong? IGuild.RulesChannelId => RulesChannelId; /// ulong? IGuild.PublicUpdatesChannelId => PublicUpdatesChannelId; /// IRole IGuild.EveryoneRole => EveryoneRole; /// IReadOnlyCollection IGuild.Roles => Roles; /// int? IGuild.ApproximateMemberCount => null; /// int? IGuild.ApproximatePresenceCount => null; /// IReadOnlyCollection IGuild.Stickers => Stickers; /// async Task IGuild.CreateEventAsync(string name, DateTimeOffset startTime, GuildScheduledEventType type, GuildScheduledEventPrivacyLevel privacyLevel, string description, DateTimeOffset? endTime, ulong? channelId, string location, Image? coverImage, RequestOptions options) => await CreateEventAsync(name, startTime, type, privacyLevel, description, endTime, channelId, location, coverImage, options).ConfigureAwait(false); /// async Task IGuild.GetEventAsync(ulong id, RequestOptions options) => await GetEventAsync(id, options).ConfigureAwait(false); /// async Task> IGuild.GetEventsAsync(RequestOptions options) => await GetEventsAsync(options).ConfigureAwait(false); /// IAsyncEnumerable> IGuild.GetBansAsync(int limit, RequestOptions options) => GetBansAsync(limit, options); /// IAsyncEnumerable> IGuild.GetBansAsync(ulong fromUserId, Direction dir, int limit, RequestOptions options) => GetBansAsync(fromUserId, dir, limit, options); /// IAsyncEnumerable> IGuild.GetBansAsync(IUser fromUser, Direction dir, int limit, RequestOptions options) => GetBansAsync(fromUser, dir, limit, options); /// async Task IGuild.GetBanAsync(IUser user, RequestOptions options) => await GetBanAsync(user, options).ConfigureAwait(false); /// async Task IGuild.GetBanAsync(ulong userId, RequestOptions options) => await GetBanAsync(userId, options).ConfigureAwait(false); /// Task> IGuild.GetChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(Channels); /// Task IGuild.GetChannelAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetChannel(id)); /// Task> IGuild.GetTextChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(TextChannels); /// Task IGuild.GetTextChannelAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetTextChannel(id)); /// Task IGuild.GetThreadChannelAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetThreadChannel(id)); /// Task> IGuild.GetThreadChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(ThreadChannels); /// Task> IGuild.GetVoiceChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(VoiceChannels); /// Task> IGuild.GetCategoriesAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(CategoryChannels); /// Task IGuild.GetVoiceChannelAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetVoiceChannel(id)); /// Task IGuild.GetStageChannelAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetStageChannel(id)); /// Task> IGuild.GetStageChannelsAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(StageChannels); /// Task IGuild.GetAFKChannelAsync(CacheMode mode, RequestOptions options) => Task.FromResult(AFKChannel); /// Task IGuild.GetDefaultChannelAsync(CacheMode mode, RequestOptions options) => Task.FromResult(DefaultChannel); /// Task IGuild.GetWidgetChannelAsync(CacheMode mode, RequestOptions options) => Task.FromResult(WidgetChannel); /// Task IGuild.GetSystemChannelAsync(CacheMode mode, RequestOptions options) => Task.FromResult(SystemChannel); /// Task IGuild.GetRulesChannelAsync(CacheMode mode, RequestOptions options) => Task.FromResult(RulesChannel); /// Task IGuild.GetPublicUpdatesChannelAsync(CacheMode mode, RequestOptions options) => Task.FromResult(PublicUpdatesChannel); /// async Task IGuild.CreateTextChannelAsync(string name, Action func, RequestOptions options) => await CreateTextChannelAsync(name, func, options).ConfigureAwait(false); /// async Task IGuild.CreateVoiceChannelAsync(string name, Action func, RequestOptions options) => await CreateVoiceChannelAsync(name, func, options).ConfigureAwait(false); /// async Task IGuild.CreateStageChannelAsync(string name, Action func, RequestOptions options) => await CreateStageChannelAsync(name, func, options).ConfigureAwait(false); /// async Task IGuild.CreateCategoryAsync(string name, Action func, RequestOptions options) => await CreateCategoryChannelAsync(name, func, options).ConfigureAwait(false); /// async Task IGuild.CreateForumChannelAsync(string name, Action func, RequestOptions options) => await CreateForumChannelAsync(name, func, options).ConfigureAwait(false); /// async Task> IGuild.GetVoiceRegionsAsync(RequestOptions options) => await GetVoiceRegionsAsync(options).ConfigureAwait(false); /// async Task> IGuild.GetIntegrationsAsync(RequestOptions options) => await GetIntegrationsAsync(options).ConfigureAwait(false); /// async Task IGuild.DeleteIntegrationAsync(ulong id, RequestOptions options) => await DeleteIntegrationAsync(id, options).ConfigureAwait(false); /// async Task> IGuild.GetInvitesAsync(RequestOptions options) => await GetInvitesAsync(options).ConfigureAwait(false); /// async Task IGuild.GetVanityInviteAsync(RequestOptions options) => await GetVanityInviteAsync(options).ConfigureAwait(false); /// IRole IGuild.GetRole(ulong id) => GetRole(id); /// async Task IGuild.CreateRoleAsync(string name, GuildPermissions? permissions, Color? color, bool isHoisted, RequestOptions options) => await CreateRoleAsync(name, permissions, color, isHoisted, false, options).ConfigureAwait(false); /// async Task IGuild.CreateRoleAsync(string name, GuildPermissions? permissions, Color? color, bool isHoisted, bool isMentionable, RequestOptions options) => await CreateRoleAsync(name, permissions, color, isHoisted, isMentionable, options).ConfigureAwait(false); /// async Task> IGuild.GetUsersAsync(CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload && !HasAllMembers) return (await GetUsersAsync(options).FlattenAsync().ConfigureAwait(false)).ToImmutableArray(); else return Users; } /// async Task IGuild.AddGuildUserAsync(ulong userId, string accessToken, Action func, RequestOptions options) => await AddGuildUserAsync(userId, accessToken, func, options); /// async Task IGuild.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) { var user = GetUser(id); if (user is not null || mode == CacheMode.CacheOnly) return user; return await GuildHelper.GetUserAsync(this, Discord, id, options).ConfigureAwait(false); } /// Task IGuild.GetCurrentUserAsync(CacheMode mode, RequestOptions options) => Task.FromResult(CurrentUser); /// Task IGuild.GetOwnerAsync(CacheMode mode, RequestOptions options) => Task.FromResult(Owner); /// async Task> IGuild.SearchUsersAsync(string query, int limit, CacheMode mode, RequestOptions options) { if (mode == CacheMode.AllowDownload) return await SearchUsersAsync(query, limit, options).ConfigureAwait(false); else return ImmutableArray.Create(); } /// async Task> IGuild.GetAuditLogsAsync(int limit, CacheMode cacheMode, RequestOptions options, ulong? beforeId, ulong? userId, ActionType? actionType) { if (cacheMode == CacheMode.AllowDownload) return (await GetAuditLogsAsync(limit, options, beforeId: beforeId, userId: userId, actionType: actionType).FlattenAsync().ConfigureAwait(false)).ToImmutableArray(); else return ImmutableArray.Create(); } /// async Task IGuild.GetWebhookAsync(ulong id, RequestOptions options) => await GetWebhookAsync(id, options).ConfigureAwait(false); /// async Task> IGuild.GetWebhooksAsync(RequestOptions options) => await GetWebhooksAsync(options).ConfigureAwait(false); /// async Task> IGuild.GetApplicationCommandsAsync (bool withLocalizations, string locale, RequestOptions options) => await GetApplicationCommandsAsync(withLocalizations, locale, options).ConfigureAwait(false); /// async Task IGuild.CreateStickerAsync(string name, string description, IEnumerable tags, Image image, RequestOptions options) => await CreateStickerAsync(name, description, tags, image, options); /// async Task IGuild.CreateStickerAsync(string name, string description, IEnumerable tags, Stream stream, string filename, RequestOptions options) => await CreateStickerAsync(name, description, tags, stream, filename, options); /// async Task IGuild.CreateStickerAsync(string name, string description, IEnumerable tags, string path, RequestOptions options) => await CreateStickerAsync(name, description, tags, path, options); /// async Task IGuild.GetStickerAsync(ulong id, CacheMode mode, RequestOptions options) => await GetStickerAsync(id, mode, options); /// async Task> IGuild.GetStickersAsync(CacheMode mode, RequestOptions options) => await GetStickersAsync(mode, options); /// Task IGuild.DeleteStickerAsync(ICustomSticker sticker, RequestOptions options) => DeleteStickerAsync(_stickers[sticker.Id], options); /// async Task IGuild.GetApplicationCommandAsync(ulong id, CacheMode mode, RequestOptions options) => await GetApplicationCommandAsync(id, mode, options); /// async Task IGuild.CreateApplicationCommandAsync(ApplicationCommandProperties properties, RequestOptions options) => await CreateApplicationCommandAsync(properties, options); /// async Task> IGuild.BulkOverwriteApplicationCommandsAsync(ApplicationCommandProperties[] properties, RequestOptions options) => await BulkOverwriteApplicationCommandAsync(properties, options); void IDisposable.Dispose() { DisconnectAudioAsync().GetAwaiter().GetResult(); _audioLock?.Dispose(); _audioClient?.Dispose(); } #endregion } }