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; namespace Discord.WebSocket { /// /// Represents a WebSocket-based guild object. /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class SocketGuild : SocketEntity, IGuild, IDisposable { #pragma warning disable IDISP002, IDISP006 private readonly SemaphoreSlim _audioLock; private TaskCompletionSource _syncPromise, _downloaderPromise; private TaskCompletionSource _audioConnectPromise; private ConcurrentHashSet _channels; private ConcurrentDictionary _members; private ConcurrentDictionary _roles; private ConcurrentDictionary _voiceStates; private ImmutableArray _emotes; private ImmutableArray _features; private AudioClient _audioClient; #pragma warning restore IDISP002, IDISP006 /// public string Name { get; private set; } /// public int AFKTimeout { get; private set; } /// public bool IsEmbeddable { 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? EmbedChannelId { get; private set; } internal ulong? SystemChannelId { 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 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 CultureInfo PreferredCulture { 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 BannerUrl => CDN.GetGuildBannerUrl(Id, BannerId); /// 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) .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; null if none is set. /// public SocketVoiceChannel AFKChannel { get { var id = AFKChannelId; return id.HasValue ? GetVoiceChannel(id.Value) : null; } } /// /// Gets the embed channel (i.e. the channel set in the guild's widget settings) in this guild. /// /// /// A channel set within the server's widget settings; null if none is set. /// public SocketGuildChannel EmbedChannel { get { var id = EmbedChannelId; 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; null if none is set. /// public SocketTextChannel SystemChannel { get { var id = SystemChannelId; 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 category channels in this guild. /// /// /// A read-only collection of category channels found within this guild. /// public IReadOnlyCollection CategoryChannels => 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 => state.GetChannel(x) as SocketGuildChannel).Where(x => x != null).ToReadOnlyCollection(channels); } } /// public IReadOnlyCollection Emotes => _emotes; /// public IReadOnlyCollection Features => _features; /// /// 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(); internal SocketGuild(DiscordSocketClient client, ulong id) : base(client, id) { _audioLock = new SemaphoreSlim(1, 1); _emotes = ImmutableArray.Create(); _features = 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 (_channels == null) _channels = new ConcurrentHashSet(); 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(); if (Discord.ApiClient.AuthTokenType != TokenType.User) { _syncPromise.TrySetResultAsync(true); /*if (!model.Large) _ = _downloaderPromise.TrySetResultAsync(true);*/ } return; } Update(state, model as Model); var channels = new ConcurrentHashSet(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); } } _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]); 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; 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; _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; EmbedChannelId = model.EmbedChannelId; SystemChannelId = model.SystemChannelId; AFKTimeout = model.AFKTimeout; IsEmbeddable = model.EmbedEnabled; IconId = model.Icon; Name = model.Name; OwnerId = model.OwnerId; VoiceRegionId = model.Region; SplashId = model.Splash; 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(); PreferredLocale = model.PreferredLocale; PreferredCulture = new CultureInfo(PreferredLocale); 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(); if (model.Features != null) _features = model.Features.ToImmutableArray(); else _features = ImmutableArray.Create(); var roles = new ConcurrentDictionary(ConcurrentHashSet.DefaultConcurrencyLevel, (int)(model.Roles.Length * 1.05)); if (model.Roles != null) { 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; } internal void Update(ClientState state, GuildSyncModel model) { 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(); } //General /// public Task DeleteAsync(RequestOptions options = null) => GuildHelper.DeleteAsync(this, Discord, options); /// /// is null. public Task ModifyAsync(Action func, RequestOptions options = null) => GuildHelper.ModifyAsync(this, Discord, func, options); /// /// is null. public Task ModifyEmbedAsync(Action func, RequestOptions options = null) => GuildHelper.ModifyEmbedAsync(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); //Bans /// /// Gets a collection of all users banned 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 /// ban objects that this guild currently possesses, with each object containing the user banned and reason /// behind the ban. /// public Task> GetBansAsync(RequestOptions options = null) => GuildHelper.GetBansAsync(this, Discord, 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; null 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; null 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); //Channels /// /// Gets a channel in this guild. /// /// The snowflake identifier for the channel. /// /// A generic channel associated with the specified ; null 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 ; null if none is found. /// public SocketTextChannel GetTextChannel(ulong id) => GetChannel(id) as SocketTextChannel; /// /// Gets a voice channel in this guild. /// /// The snowflake identifier for the voice channel. /// /// A voice channel associated with the specified ; null if none is found. /// public SocketVoiceChannel GetVoiceChannel(ulong id) => GetChannel(id) as SocketVoiceChannel; /// /// Gets a category channel in this guild. /// /// The snowflake identifier for the category channel. /// /// A category channel associated with the specified ; null 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 null. /// /// 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 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 null. /// /// 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); internal SocketGuildChannel AddChannel(ClientState state, ChannelModel model) { var channel = SocketGuildChannel.Create(this, state, model); _channels.TryAdd(model.Id); state.AddChannel(channel); return channel; } internal SocketGuildChannel RemoveChannel(ClientState state, ulong id) { if (_channels.TryRemove(id)) return state.RemoveChannel(id) as SocketGuildChannel; return null; } //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); //Integrations public Task> GetIntegrationsAsync(RequestOptions options = null) => GuildHelper.GetIntegrationsAsync(this, Discord, options); public Task CreateIntegrationAsync(ulong id, string type, RequestOptions options = null) => GuildHelper.CreateIntegrationAsync(this, Discord, id, type, options); //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; null if none is found. /// public Task GetVanityInviteAsync(RequestOptions options = null) => GuildHelper.GetVanityInviteAsync(this, Discord, options); //Roles /// /// Gets a role in this guild. /// /// The snowflake identifier for the role. /// /// A role that is associated with the specified ; null if none is found. /// public SocketRole GetRole(ulong id) { if (_roles.TryGetValue(id, out SocketRole value)) return value; return null; } /// public Task CreateRoleAsync(string name, GuildPermissions? permissions = default(GuildPermissions?), Color? color = default(Color?), bool isHoisted = false, RequestOptions options = null) => GuildHelper.CreateRoleAsync(this, Discord, name, permissions, color, isHoisted, false, options); /// /// 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 null. /// /// 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; } //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 null 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 ; null 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) => GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options); 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); if (member == null) throw new InvalidOperationException("SocketGuildUser.Create failed to produce a member"); // TODO 2.2rel: delete this if (member.GlobalUser == null) throw new InvalidOperationException("Member was created without global user"); // TODO 2.2rel: delete this member.GlobalUser.AddRef(); _members[member.Id] = member; DownloadedMemberCount++; } if (member == null) throw new InvalidOperationException("AddOrUpdateUser failed to produce a user"); // TODO 2.2rel: delete this 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; } /// public async Task DownloadUsersAsync() { await Discord.DownloadUsersAsync(new[] { this }).ConfigureAwait(false); } internal void CompleteDownloadUsers() { _downloaderPromise.TrySetResultAsync(true); } //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); //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 ; null 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); //Emotes /// 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 null. 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); //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; } //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; if (external) { #pragma warning disable IDISP001 var _ = promise.TrySetResultAsync(null); await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, channelId, selfDeaf, selfMute).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(Id, channelId, selfDeaf, selfMute).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; } internal async Task FinishConnectAudio(string url, string token) { //TODO: Mem Leak: Disconnected/Connected handlers arent 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; //IGuild /// ulong? IGuild.AFKChannelId => AFKChannelId; /// IAudioClient IGuild.AudioClient => null; /// bool IGuild.Available => true; /// ulong IGuild.DefaultChannelId => DefaultChannel?.Id ?? 0; /// ulong? IGuild.EmbedChannelId => EmbedChannelId; /// ulong? IGuild.SystemChannelId => SystemChannelId; /// IRole IGuild.EveryoneRole => EveryoneRole; /// IReadOnlyCollection IGuild.Roles => Roles; /// async Task> IGuild.GetBansAsync(RequestOptions options) => await GetBansAsync(options).ConfigureAwait(false); /// 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.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.GetAFKChannelAsync(CacheMode mode, RequestOptions options) => Task.FromResult(AFKChannel); /// Task IGuild.GetDefaultChannelAsync(CacheMode mode, RequestOptions options) => Task.FromResult(DefaultChannel); /// Task IGuild.GetEmbedChannelAsync(CacheMode mode, RequestOptions options) => Task.FromResult(EmbedChannel); /// Task IGuild.GetSystemChannelAsync(CacheMode mode, RequestOptions options) => Task.FromResult(SystemChannel); /// 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.CreateCategoryAsync(string name, Action func, RequestOptions options) => await CreateCategoryChannelAsync(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.CreateIntegrationAsync(ulong id, string type, RequestOptions options) => await CreateIntegrationAsync(id, type, 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); /// Task> IGuild.GetUsersAsync(CacheMode mode, RequestOptions options) => Task.FromResult>(Users); /// async Task IGuild.AddGuildUserAsync(ulong userId, string accessToken, Action func, RequestOptions options) => await AddGuildUserAsync(userId, accessToken, func, options); /// Task IGuild.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); /// Task IGuild.GetCurrentUserAsync(CacheMode mode, RequestOptions options) => Task.FromResult(CurrentUser); /// Task IGuild.GetOwnerAsync(CacheMode mode, RequestOptions options) => Task.FromResult(Owner); /// 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); void IDisposable.Dispose() { DisconnectAudioAsync().GetAwaiter().GetResult(); _audioLock?.Dispose(); _audioClient?.Dispose(); } } }