diff --git a/DisCatSharp/Entities/Guild/DiscordMember.cs b/DisCatSharp/Entities/Guild/DiscordMember.cs
index 4d4a59dbb..3a5096f26 100644
--- a/DisCatSharp/Entities/Guild/DiscordMember.cs
+++ b/DisCatSharp/Entities/Guild/DiscordMember.cs
@@ -1,807 +1,722 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using DisCatSharp.Enums;
using DisCatSharp.Exceptions;
using DisCatSharp.Net;
using DisCatSharp.Net.Abstractions;
using DisCatSharp.Net.Models;
using Newtonsoft.Json;
namespace DisCatSharp.Entities;
///
/// Represents a Discord guild member.
///
public class DiscordMember : DiscordUser, IEquatable
{
///
/// Initializes a new instance of the class.
///
internal DiscordMember()
{
this._roleIdsLazy = new Lazy>(() => new ReadOnlyCollection(this.RoleIdsInternal));
}
///
/// Initializes a new instance of the class.
///
/// The user.
internal DiscordMember(DiscordUser user)
{
this.Discord = user.Discord;
this.Id = user.Id;
this.RoleIdsInternal = new List();
this._roleIdsLazy = new Lazy>(() => new ReadOnlyCollection(this.RoleIdsInternal));
}
///
/// Initializes a new instance of the class.
///
/// The mbr.
internal DiscordMember(TransportMember mbr)
{
this.Id = mbr.User.Id;
this.IsDeafened = mbr.IsDeafened;
this.IsMuted = mbr.IsMuted;
this.JoinedAt = mbr.JoinedAt;
this.Nickname = mbr.Nickname;
this.PremiumSince = mbr.PremiumSince;
this.IsPending = mbr.IsPending;
this.GuildAvatarHash = mbr.GuildAvatarHash;
this.GuildBannerHash = mbr.GuildBannerHash;
this.GuildBio = mbr.GuildBio;
this.GuildPronouns = mbr.GuildPronouns;
this.CommunicationDisabledUntil = mbr.CommunicationDisabledUntil;
this.AvatarHashInternal = mbr.AvatarHash;
this.RoleIdsInternal = mbr.Roles ?? new List();
this._roleIdsLazy = new Lazy>(() => new ReadOnlyCollection(this.RoleIdsInternal));
this.MemberFlags = mbr.MemberFlags;
}
///
/// Gets the members avatar hash.
///
[JsonProperty("avatar", NullValueHandling = NullValueHandling.Ignore)]
public virtual string GuildAvatarHash { get; internal set; }
///
/// Gets the members avatar URL.
///
[JsonIgnore]
public string GuildAvatarUrl
=> string.IsNullOrWhiteSpace(this.GuildAvatarHash) ? this.User.AvatarUrl : $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.GUILDS}/{this.GuildId.ToString(CultureInfo.InvariantCulture)}{Endpoints.USERS}/{this.Id.ToString(CultureInfo.InvariantCulture)}{Endpoints.AVATARS}/{this.GuildAvatarHash}.{(this.GuildAvatarHash.StartsWith("a_") ? "gif" : "png")}?size=1024";
///
/// Gets the members banner hash.
///
[JsonProperty("banner", NullValueHandling = NullValueHandling.Ignore)]
public virtual string GuildBannerHash { get; internal set; }
///
/// Gets the members banner URL.
///
[JsonIgnore]
public string GuildBannerUrl
=> string.IsNullOrWhiteSpace(this.GuildBannerHash) ? this.User.BannerUrl : $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.GUILDS}/{this.GuildId.ToString(CultureInfo.InvariantCulture)}{Endpoints.USERS}/{this.Id.ToString(CultureInfo.InvariantCulture)}{Endpoints.BANNERS}/{this.GuildBannerHash}.{(this.GuildBannerHash.StartsWith("a_") ? "gif" : "png")}?size=1024";
///
/// The color of this member's banner. Mutually exclusive with .
///
[JsonIgnore]
public override DiscordColor? BannerColor
=> this.User.BannerColor;
///
/// Gets this member's nickname.
///
[JsonProperty("nick", NullValueHandling = NullValueHandling.Ignore)]
public string Nickname { get; internal set; }
///
/// Gets the members guild bio.
/// This is not available to bots tho.
///
[JsonProperty("bio", NullValueHandling = NullValueHandling.Ignore)]
public string GuildBio { get; internal set; }
///
/// Gets the members's pronouns.
///
[JsonProperty("pronouns", NullValueHandling = NullValueHandling.Ignore)]
public string GuildPronouns { get; internal set; }
///
/// Gets the members flags.
///
[JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)]
public MemberFlags MemberFlags { get; internal set; }
[JsonIgnore]
internal string AvatarHashInternal;
///
/// Gets this member's display name.
///
[JsonIgnore]
public string DisplayName
=> this.Nickname ?? this.Username;
///
/// List of role ids
///
[JsonIgnore]
internal IReadOnlyList RoleIds
=> this._roleIdsLazy.Value;
[JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)]
internal List RoleIdsInternal;
[JsonIgnore]
private readonly Lazy> _roleIdsLazy;
///
/// Gets the list of roles associated with this member.
///
[JsonIgnore]
public IReadOnlyList Roles
=> this.RoleIds.Select(id => this.Guild.GetRole(id)).Where(x => x != null).ToList();
///
/// Gets the color associated with this user's top color-giving role, otherwise 0 (no color).
///
[JsonIgnore]
public DiscordColor Color
{
get
{
var role = this.Roles.OrderByDescending(xr => xr.Position).FirstOrDefault(xr => xr.Color.Value != 0);
return role != null ? role.Color : new DiscordColor();
}
}
///
/// Date the user joined the guild
///
[JsonProperty("joined_at", NullValueHandling = NullValueHandling.Ignore)]
public DateTimeOffset JoinedAt { get; internal set; }
///
/// Date the user started boosting this server
///
[JsonProperty("premium_since", NullValueHandling = NullValueHandling.Ignore)]
public DateTimeOffset? PremiumSince { get; internal set; }
///
/// Date until the can communicate again.
///
[JsonProperty("communication_disabled_until", NullValueHandling = NullValueHandling.Include)]
public DateTime? CommunicationDisabledUntil { get; internal set; }
///
/// If the user is deafened
///
[JsonProperty("is_deafened", NullValueHandling = NullValueHandling.Ignore)]
public bool IsDeafened { get; internal set; }
///
/// If the user is muted
///
[JsonProperty("is_muted", NullValueHandling = NullValueHandling.Ignore)]
public bool IsMuted { get; internal set; }
///
/// Whether the user has not passed the guild's Membership Screening requirements yet.
///
[JsonProperty("pending", NullValueHandling = NullValueHandling.Ignore)]
public bool? IsPending { get; internal set; }
///
/// Gets this member's voice state.
///
[JsonIgnore]
public DiscordVoiceState VoiceState
=> this.Discord.Guilds[this.GuildId].VoiceStates.TryGetValue(this.Id, out var voiceState) ? voiceState : null;
[JsonIgnore]
internal ulong GuildId = 0;
///
/// Gets the guild of which this member is a part of.
///
[JsonIgnore]
public DiscordGuild Guild
=> this.Discord.Guilds[this.GuildId];
///
/// Gets whether this member is the Guild owner.
///
[JsonIgnore]
public bool IsOwner
=> this.Id == this.Guild.OwnerId;
///
/// Gets the member's position in the role hierarchy, which is the member's highest role's position. Returns for the guild's owner.
///
[JsonIgnore]
public int Hierarchy
=> this.IsOwner ? int.MaxValue : this.RoleIds.Count == 0 ? 0 : this.Roles.Max(x => x.Position);
///
/// Gets the permissions for the current member.
///
[JsonIgnore]
public Permissions Permissions
=> this.GetPermissions();
#region Overridden user properties
///
/// Gets the user.
///
[JsonIgnore]
internal DiscordUser User
=> this.Discord.UserCache[this.Id];
///
/// Gets this member's username.
///
[JsonIgnore]
public override string Username
{
get => this.User.Username;
internal set => this.User.Username = value;
}
///
/// Gets the member's 4-digit discriminator.
///
[JsonIgnore]
public override string Discriminator
{
get => this.User.Discriminator;
internal set => this.User.Discriminator = value;
}
///
/// Gets the member's avatar hash.
///
[JsonIgnore]
public override string AvatarHash
{
get => this.User.AvatarHash;
internal set => this.User.AvatarHash = value;
}
///
/// Gets the member's banner hash.
///
[JsonIgnore]
public override string BannerHash
{
get => this.User.BannerHash;
internal set => this.User.BannerHash = value;
}
///
/// Gets whether the member is a bot.
///
[JsonIgnore]
public override bool IsBot
{
get => this.User.IsBot;
internal set => this.User.IsBot = value;
}
///
/// Gets the member's email address.
/// This is only present in OAuth.
///
[JsonIgnore]
public override string Email
{
get => this.User.Email;
internal set => this.User.Email = value;
}
///
/// Gets whether the member has multi-factor authentication enabled.
///
[JsonIgnore]
public override bool? MfaEnabled
{
get => this.User.MfaEnabled;
internal set => this.User.MfaEnabled = value;
}
///
/// Gets whether the member is verified.
/// This is only present in OAuth.
///
[JsonIgnore]
public override bool? Verified
{
get => this.User.Verified;
internal set => this.User.Verified = value;
}
///
/// Gets the member's chosen language
///
[JsonIgnore]
public override string Locale
{
get => this.User.Locale;
internal set => this.User.Locale = value;
}
///
/// Gets the user's flags.
///
[JsonIgnore]
public override UserFlags? OAuthFlags
{
get => this.User.OAuthFlags;
internal set => this.User.OAuthFlags = value;
}
///
/// Gets the member's flags for OAuth.
///
[JsonIgnore]
public override UserFlags? Flags
{
get => this.User.Flags;
internal set => this.User.Flags = value;
}
#endregion
-
- ///
- /// Creates a direct message channel to this member.
- ///
- /// Direct message channel to this member.
- /// Thrown when the member has the bot blocked, the member is no longer in the guild, or if the member has Allow DM from server members off.
- /// Thrown when the member does not exist.
- /// Thrown when an invalid parameter was provided.
- /// Thrown when Discord is unable to process the request.
- public Task CreateDmChannelAsync()
- => this.Discord.ApiClient.CreateDmAsync(this.Id);
-
- ///
- /// Sends a direct message to this member. Creates a direct message channel if one does not exist already.
- ///
- /// Content of the message to send.
- /// The sent message.
- /// Thrown when the member has the bot blocked, the member is no longer in the guild, or if the member has Allow DM from server members off.
- /// Thrown when the member does not exist.
- /// Thrown when an invalid parameter was provided.
- /// Thrown when Discord is unable to process the request.
- public async Task SendMessageAsync(string content)
- {
- if (this.IsBot && this.Discord.CurrentUser.IsBot)
- throw new ArgumentException("Bots cannot DM each other.");
-
- var chn = await this.CreateDmChannelAsync().ConfigureAwait(false);
- return await chn.SendMessageAsync(content).ConfigureAwait(false);
- }
-
- ///
- /// Sends a direct message to this member. Creates a direct message channel if one does not exist already.
- ///
- /// Embed to attach to the message.
- /// The sent message.
- /// Thrown when the member has the bot blocked, the member is no longer in the guild, or if the member has Allow DM from server members off.
- /// Thrown when the member does not exist.
- /// Thrown when an invalid parameter was provided.
- /// Thrown when Discord is unable to process the request.
- public async Task SendMessageAsync(DiscordEmbed embed)
- {
- if (this.IsBot && this.Discord.CurrentUser.IsBot)
- throw new ArgumentException("Bots cannot DM each other.");
-
- var chn = await this.CreateDmChannelAsync().ConfigureAwait(false);
- return await chn.SendMessageAsync(embed).ConfigureAwait(false);
- }
-
- ///
- /// Sends a direct message to this member. Creates a direct message channel if one does not exist already.
- ///
- /// Content of the message to send.
- /// Embed to attach to the message.
- /// The sent message.
- /// Thrown when the member has the bot blocked, the member is no longer in the guild, or if the member has Allow DM from server members off.
- /// Thrown when the member does not exist.
- /// Thrown when an invalid parameter was provided.
- /// Thrown when Discord is unable to process the request.
- public async Task SendMessageAsync(string content, DiscordEmbed embed)
- {
- if (this.IsBot && this.Discord.CurrentUser.IsBot)
- throw new ArgumentException("Bots cannot DM each other.");
-
- var chn = await this.CreateDmChannelAsync().ConfigureAwait(false);
- return await chn.SendMessageAsync(content, embed).ConfigureAwait(false);
- }
-
- ///
- /// Sends a direct message to this member. Creates a direct message channel if one does not exist already.
- ///
- /// Builder to with the message.
- /// The sent message.
- /// Thrown when the member has the bot blocked, the member is no longer in the guild, or if the member has Allow DM from server members off.
- /// Thrown when the member does not exist.
- /// Thrown when an invalid parameter was provided.
- /// Thrown when Discord is unable to process the request.
- public async Task SendMessageAsync(DiscordMessageBuilder message)
- {
- if (this.IsBot && this.Discord.CurrentUser.IsBot)
- throw new ArgumentException("Bots cannot DM each other.");
-
- var chn = await this.CreateDmChannelAsync().ConfigureAwait(false);
- return await chn.SendMessageAsync(message).ConfigureAwait(false);
- }
-
///
/// Sets this member's voice mute status.
///
/// Whether the member is to be muted.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task SetMuteAsync(bool mute, string reason = null)
=> this.Discord.ApiClient.ModifyGuildMemberAsync(this.GuildId, this.Id, default, default, mute, default, default, default, this.MemberFlags, reason);
public Task VerifyAsync(string reason = null)
=> this.Discord.ApiClient.ModifyGuildMemberAsync(this.GuildId, this.Id, default, default, default, default, default, true, this.MemberFlags, reason);
public Task UnverifyAsync(string reason = null)
=> this.Discord.ApiClient.ModifyGuildMemberAsync(this.GuildId, this.Id, default, default, default, default, default, false, this.MemberFlags, reason);
///
/// Sets this member's voice deaf status.
///
/// Whether the member is to be deafened.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task SetDeafAsync(bool deaf, string reason = null)
=> this.Discord.ApiClient.ModifyGuildMemberAsync(this.GuildId, this.Id, default, default, default, deaf, default, default, this.MemberFlags, reason);
///
/// Modifies this member.
///
/// Action to perform on this member.
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task ModifyAsync(Action action)
{
var mdl = new MemberEditModel();
action(mdl);
if (mdl.VoiceChannel.HasValue && mdl.VoiceChannel.Value != null && mdl.VoiceChannel.Value.Type != ChannelType.Voice && mdl.VoiceChannel.Value.Type != ChannelType.Stage)
throw new ArgumentException("Given channel is not a voice or stage channel.", nameof(action));
if (mdl.Nickname.HasValue && this.Discord.CurrentUser.Id == this.Id)
{
await this.Discord.ApiClient.ModifyCurrentMemberNicknameAsync(this.Guild.Id, mdl.Nickname.Value,
mdl.AuditLogReason).ConfigureAwait(false);
await this.Discord.ApiClient.ModifyGuildMemberAsync(this.Guild.Id, this.Id, Optional.None,
mdl.Roles.Map(e => e.Select(xr => xr.Id)), mdl.Muted, mdl.Deafened,
mdl.VoiceChannel.Map(e => e?.Id), default, this.MemberFlags, mdl.AuditLogReason).ConfigureAwait(false);
}
else
{
await this.Discord.ApiClient.ModifyGuildMemberAsync(this.Guild.Id, this.Id, mdl.Nickname,
mdl.Roles.Map(e => e.Select(xr => xr.Id)), mdl.Muted, mdl.Deafened,
mdl.VoiceChannel.Map(e => e?.Id), default, this.MemberFlags, mdl.AuditLogReason).ConfigureAwait(false);
}
}
///
/// Disconnects the member from their current voice channel.
///
public async Task DisconnectFromVoiceAsync()
=> await this.ModifyAsync(x => x.VoiceChannel = null);
///
/// Adds a timeout to a member.
///
/// The datetime offset to time out the user. Up to 28 days.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task TimeoutAsync(DateTimeOffset until, string reason = null)
=> until.Subtract(DateTimeOffset.UtcNow).Days > 28 ? throw new ArgumentException("Timeout can not be longer than 28 days") : this.Discord.ApiClient.ModifyTimeoutAsync(this.Guild.Id, this.Id, until, reason);
///
/// Adds a timeout to a member.
///
/// The timespan to time out the user. Up to 28 days.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task TimeoutAsync(TimeSpan until, string reason = null)
=> this.TimeoutAsync(DateTimeOffset.UtcNow + until, reason);
///
/// Adds a timeout to a member.
///
/// The datetime to time out the user. Up to 28 days.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task TimeoutAsync(DateTime until, string reason = null)
=> this.TimeoutAsync(until.ToUniversalTime() - DateTime.UtcNow, reason);
///
/// Removes the timeout from a member.
///
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task RemoveTimeoutAsync(string reason = null)
=> this.Discord.ApiClient.ModifyTimeoutAsync(this.Guild.Id, this.Id, null, reason);
///
/// Grants a role to the member.
///
/// Role to grant.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task GrantRoleAsync(DiscordRole role, string reason = null)
=> this.Discord.ApiClient.AddGuildMemberRoleAsync(this.Guild.Id, this.Id, role.Id, reason);
///
/// Revokes a role from a member.
///
/// Role to revoke.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task RevokeRoleAsync(DiscordRole role, string reason = null)
=> this.Discord.ApiClient.RemoveGuildMemberRoleAsync(this.Guild.Id, this.Id, role.Id, reason);
///
/// Sets the member's roles to ones specified.
///
/// Roles to set.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task ReplaceRolesAsync(IEnumerable roles, string reason = null)
=> this.Discord.ApiClient.ModifyGuildMemberAsync(this.Guild.Id, this.Id, default,
Optional.Some(roles.Select(xr => xr.Id)), default, default, default, default, this.MemberFlags, reason);
///
/// Bans this member from their guild.
///
/// How many days to remove messages from.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task BanAsync(int deleteMessageDays = 0, string reason = null)
=> this.Guild.BanMemberAsync(this, deleteMessageDays, reason);
///
/// Unbans this member from their guild.
///
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task UnbanAsync(string reason = null)
=> this.Guild.UnbanMemberAsync(this, reason);
///
/// Kicks this member from their guild.
///
/// Reason for audit logs.
///
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task RemoveAsync(string reason = null)
=> this.Discord.ApiClient.RemoveGuildMemberAsync(this.GuildId, this.Id, reason);
///
/// Moves this member to the specified voice channel
///
///
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task PlaceInAsync(DiscordChannel channel)
=> channel.PlaceMemberAsync(this);
///
/// Updates the member's suppress state in a stage channel.
///
/// The channel the member is currently in.
/// Toggles the member's suppress state.
/// Thrown when the channel in not a voice channel.
public async Task UpdateVoiceStateAsync(DiscordChannel channel, bool? suppress)
{
if (channel.Type != ChannelType.Stage)
throw new ArgumentException("Voice state can only be updated in a stage channel.");
await this.Discord.ApiClient.UpdateUserVoiceStateAsync(this.Guild.Id, this.Id, channel.Id, suppress).ConfigureAwait(false);
}
///
/// Makes the user a speaker.
///
/// Thrown when the user is not inside an stage channel.
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task MakeSpeakerAsync()
{
var vs = this.VoiceState;
if (vs == null || vs.Channel.Type != ChannelType.Stage)
throw new ArgumentException("Voice state can only be updated when the user is inside an stage channel.");
await this.Discord.ApiClient.UpdateUserVoiceStateAsync(this.Guild.Id, this.Id, vs.Channel.Id, false).ConfigureAwait(false);
}
///
/// Moves the user to audience.
///
/// Thrown when the user is not inside an stage channel.
/// Thrown when the client does not have the permission.
/// Thrown when the member does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task MoveToAudienceAsync()
{
var vs = this.VoiceState;
if (vs == null || vs.Channel.Type != ChannelType.Stage)
throw new ArgumentException("Voice state can only be updated when the user is inside an stage channel.");
await this.Discord.ApiClient.UpdateUserVoiceStateAsync(this.Guild.Id, this.Id, vs.Channel.Id, true).ConfigureAwait(false);
}
///
/// Calculates permissions in a given channel for this member.
///
/// Channel to calculate permissions for.
/// Calculated permissions for this member in the channel.
public Permissions PermissionsIn(DiscordChannel channel)
=> channel.PermissionsFor(this);
///
/// Get's the current member's roles based on the sum of the permissions of their given roles.
///
private Permissions GetPermissions()
{
if (this.Guild.OwnerId == this.Id)
return PermissionMethods.FullPerms;
Permissions perms;
// assign @everyone permissions
var everyoneRole = this.Guild.EveryoneRole;
perms = everyoneRole.Permissions;
// assign permissions from member's roles (in order)
perms |= this.Roles.Aggregate(Permissions.None, (c, role) => c | role.Permissions);
// Administrator grants all permissions and cannot be overridden
return (perms & Permissions.Administrator) == Permissions.Administrator ? PermissionMethods.FullPerms : perms;
}
///
/// Returns a string representation of this member.
///
/// String representation of this member.
public override string ToString()
=> $"Member {this.Id}; {this.Username}#{this.Discriminator} ({this.DisplayName})";
///
/// Checks whether this is equal to another object.
///
/// Object to compare to.
/// Whether the object is equal to this .
public override bool Equals(object obj)
=> this.Equals(obj as DiscordMember);
///
/// Checks whether this is equal to another .
///
/// to compare to.
/// Whether the is equal to this .
public bool Equals(DiscordMember e)
=> e is not null && (ReferenceEquals(this, e) || (this.Id == e.Id && this.GuildId == e.GuildId));
///
/// Gets the hash code for this .
///
/// The hash code for this .
public override int GetHashCode()
{
var hash = 13;
hash = (hash * 7) + this.Id.GetHashCode();
hash = (hash * 7) + this.GuildId.GetHashCode();
return hash;
}
///
/// Gets whether the two objects are equal.
///
/// First member to compare.
/// Second member to compare.
/// Whether the two members are equal.
public static bool operator ==(DiscordMember e1, DiscordMember e2)
{
var o1 = e1 as object;
var o2 = e2 as object;
return (o1 != null || o2 == null) && (o1 == null || o2 != null) && ((o1 == null && o2 == null) || (e1.Id == e2.Id && e1.GuildId == e2.GuildId));
}
///
/// Gets whether the two objects are not equal.
///
/// First member to compare.
/// Second member to compare.
/// Whether the two members are not equal.
public static bool operator !=(DiscordMember e1, DiscordMember e2)
=> !(e1 == e2);
}
diff --git a/DisCatSharp/Entities/User/DiscordUser.cs b/DisCatSharp/Entities/User/DiscordUser.cs
index 13b69e8d7..3510dbacf 100644
--- a/DisCatSharp/Entities/User/DiscordUser.cs
+++ b/DisCatSharp/Entities/User/DiscordUser.cs
@@ -1,523 +1,607 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using DisCatSharp.Enums;
using DisCatSharp.Exceptions;
using DisCatSharp.Net;
using DisCatSharp.Net.Abstractions;
using Newtonsoft.Json;
namespace DisCatSharp.Entities;
///
/// Represents a Discord user.
///
public class DiscordUser : SnowflakeObject, IEquatable
{
///
/// Initializes a new instance of the class.
///
internal DiscordUser()
{ }
///
/// Initializes a new instance of the class.
///
/// The transport user.
internal DiscordUser(TransportUser transport)
{
this.Id = transport.Id;
this.Username = transport.Username;
this.Discriminator = transport.Discriminator;
this.AvatarHash = transport.AvatarHash;
this.AvatarDecorationHash = transport.AvatarDecorationHash;
this.BannerHash = transport.BannerHash;
this.BannerColorInternal = transport.BannerColor;
this.ThemeColorsInternal = (transport.ThemeColors ?? Array.Empty()).ToList();
this.IsBot = transport.IsBot;
this.MfaEnabled = transport.MfaEnabled;
this.Verified = transport.Verified;
this.Email = transport.Email;
this.PremiumType = transport.PremiumType;
this.Locale = transport.Locale;
this.Flags = transport.Flags;
this.OAuthFlags = transport.OAuthFlags;
this.Bio = transport.Bio;
this.Pronouns = transport.Pronouns;
}
///
/// Gets this user's username.
///
[JsonProperty("username", NullValueHandling = NullValueHandling.Ignore)]
public virtual string Username { get; internal set; }
///
/// Gets this user's username with the discriminator.
/// Example: Discord#0000
///
[JsonIgnore]
public virtual string UsernameWithDiscriminator
=> $"{this.Username}#{this.Discriminator}";
///
/// Gets the user's 4-digit discriminator.
///
[JsonProperty("discriminator", NullValueHandling = NullValueHandling.Ignore)]
public virtual string Discriminator { get; internal set; }
///
/// Gets the discriminator integer.
///
[JsonIgnore]
internal int DiscriminatorInt
=> int.Parse(this.Discriminator, NumberStyles.Integer, CultureInfo.InvariantCulture);
///
/// Gets the user's banner color, if set. Mutually exclusive with .
///
[JsonIgnore]
public virtual DiscordColor? BannerColor
=> !this.BannerColorInternal.HasValue ? null : new DiscordColor(this.BannerColorInternal.Value);
///
/// Gets the user's theme colors, if set.
///
[JsonIgnore]
#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
public virtual IReadOnlyList? ThemeColors
#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
=> !(this.ThemeColorsInternal is not null && this.ThemeColorsInternal.Count != 0) ? null : this.ThemeColorsInternal.Select(x => new DiscordColor(x)).ToList();
///
/// Gets the user's banner color integer.
///
[JsonProperty("accent_color")]
internal int? BannerColorInternal;
///
/// Gets the user's theme color integers.
///
[JsonProperty("theme_colors", NullValueHandling = NullValueHandling.Ignore)]
#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
internal List? ThemeColorsInternal;
#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
///
/// Gets the user's banner url
///
[JsonIgnore]
public string BannerUrl
=> string.IsNullOrWhiteSpace(this.BannerHash) ? null : $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.BANNERS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.BannerHash}.{(this.BannerHash.StartsWith("a_") ? "gif" : "png")}?size=4096";
///
/// Gets the user's profile banner hash. Mutually exclusive with .
///
[JsonProperty("banner", NullValueHandling = NullValueHandling.Ignore)]
public virtual string BannerHash { get; internal set; }
///
/// Gets the users bio.
/// This is not available to bots tho.
///
[JsonProperty("bio", NullValueHandling = NullValueHandling.Ignore)]
public virtual string Bio { get; internal set; }
///
/// Gets the user's avatar hash.
///
[JsonProperty("avatar", NullValueHandling = NullValueHandling.Ignore)]
public virtual string AvatarHash { get; internal set; }
///
/// Gets the user's avatar decoration hash.
///
[JsonProperty("avatar_decoration", NullValueHandling = NullValueHandling.Ignore)]
public virtual string AvatarDecorationHash { get; internal set; }
///
/// Returns a uri to this users profile.
///
[JsonIgnore]
public Uri ProfileUri => new($"{DiscordDomain.GetDomain(CoreDomain.Discord).Url}{Endpoints.USERS}/{this.Id}");
///
/// Returns a string representing the direct URL to this users profile.
///
/// The URL of this users profile.
[JsonIgnore]
public string ProfileUrl => this.ProfileUri.AbsoluteUri;
///
/// Gets the user's avatar url.
///
[JsonIgnore]
public string AvatarUrl
=> string.IsNullOrWhiteSpace(this.AvatarHash) ? this.DefaultAvatarUrl : $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.AVATARS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.AvatarHash}.{(this.AvatarHash.StartsWith("a_") ? "gif" : "png")}?size=1024";
///
/// Gets the user's avatar decoration url.
///
[JsonIgnore]
#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
public string? AvatarDecorationUrl
#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
=> string.IsNullOrWhiteSpace(this.AvatarDecorationHash) ? null : $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.AVATARS_DECORATIONS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.AvatarDecorationHash}.{(this.AvatarDecorationHash.StartsWith("a_") ? "gif" : "png")}?size=1024";
///
/// Gets the URL of default avatar for this user.
///
[JsonIgnore]
public string DefaultAvatarUrl
=> $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.EMBED}{Endpoints.AVATARS}/{(this.DiscriminatorInt % 5).ToString(CultureInfo.InvariantCulture)}.png?size=1024";
///
/// Gets whether the user is a bot.
///
[JsonProperty("bot", NullValueHandling = NullValueHandling.Ignore)]
public virtual bool IsBot { get; internal set; }
///
/// Gets whether the user has multi-factor authentication enabled.
///
[JsonProperty("mfa_enabled", NullValueHandling = NullValueHandling.Ignore)]
public virtual bool? MfaEnabled { get; internal set; }
///
/// Gets whether the user is an official Discord system user.
///
[JsonProperty("system", NullValueHandling = NullValueHandling.Ignore)]
public bool? IsSystem { get; internal set; }
///
/// Gets whether the user is verified.
/// This is only present in OAuth.
///
[JsonProperty("verified", NullValueHandling = NullValueHandling.Ignore)]
public virtual bool? Verified { get; internal set; }
///
/// Gets the user's email address.
/// This is only present in OAuth.
///
[JsonProperty("email", NullValueHandling = NullValueHandling.Ignore)]
public virtual string Email { get; internal set; }
///
/// Gets the user's premium type.
///
[JsonProperty("premium_type", NullValueHandling = NullValueHandling.Ignore)]
public virtual PremiumType? PremiumType { get; internal set; }
///
/// Gets the user's chosen language
///
[JsonProperty("locale", NullValueHandling = NullValueHandling.Ignore)]
public virtual string Locale { get; internal set; }
///
/// Gets the user's flags for OAuth.
///
[JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)]
public virtual UserFlags? OAuthFlags { get; internal set; }
///
/// Gets the user's flags.
///
[JsonProperty("public_flags", NullValueHandling = NullValueHandling.Ignore)]
public virtual UserFlags? Flags { get; internal set; }
///
/// Gets the user's pronouns.
///
[JsonProperty("pronouns", NullValueHandling = NullValueHandling.Ignore)]
public virtual string Pronouns { get; internal set; }
///
/// Gets the user's mention string.
///
[JsonIgnore]
public string Mention
=> Formatter.Mention(this, this is DiscordMember);
///
/// Gets whether this user is the Client which created this object.
///
[JsonIgnore]
public bool IsCurrent
=> this.Id == this.Discord.CurrentUser.Id;
#region Extension of DiscordUser
///
/// Whether this member is a
///
///
[JsonIgnore]
public bool IsMod
=> this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.CertifiedModerator);
///
/// Whether this member is a
///
///
[JsonIgnore]
public bool IsPartner
=> this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.Partner);
///
/// Whether this member is a
///
///
[JsonIgnore]
public bool IsVerifiedBot
=> this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.VerifiedBot);
///
/// Whether this member is a
///
///
[JsonIgnore]
public bool IsBotDev
=> this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.VerifiedDeveloper);
///
/// Whether this member is a
///
///
[JsonIgnore]
public bool IsActiveDeveloper
=> this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.ActiveDeveloper);
///
/// Whether this member is a
///
///
[JsonIgnore]
public bool IsStaff
=> this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.Staff);
#endregion
///
/// Fetches the user from the API.
///
/// The user with fresh data from the API.
public async Task GetFromApiAsync()
=> await this.Discord.ApiClient.GetUserAsync(this.Id);
///
/// Gets additional information about an application if the user is an bot.
///
/// The rpc info or
/// Thrown when the application does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
public async Task GetRpcInfoAsync()
#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
=> this.IsBot ? await this.Discord.ApiClient.GetApplicationInfoAsync(this.Id) : await Task.FromResult(null);
#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
///
/// Whether this user is in a
///
///
///
/// DiscordGuild guild = await Client.GetGuildAsync(806675511555915806);
/// DiscordUser user = await Client.GetUserAsync(469957180968271873);
/// Console.WriteLine($"{user.Username} {(user.IsInGuild(guild) ? "is a" : "is not a")} member of {guild.Name}");
///
/// results to J_M_Lutra is a member of Project Nyaw~.
///
///
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "")]
public async Task IsInGuild(DiscordGuild guild)
{
try
{
var member = await guild.GetMemberAsync(this.Id);
return member is not null;
}
catch (NotFoundException)
{
return false;
}
}
///
/// Whether this user is not in a
///
///
///
public async Task IsNotInGuild(DiscordGuild guild)
=> !await this.IsInGuild(guild);
///
/// Returns the DiscordMember in the specified
///
/// The to get this user on.
/// The .
/// Thrown when the user is not part of the guild.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public async Task ConvertToMember(DiscordGuild guild)
=> await guild.GetMemberAsync(this.Id);
///
/// Unbans this user from a guild.
///
/// Guild to unban this user from.
/// Reason for audit logs.
/// Thrown when the client does not have the permission.
/// Thrown when the user does not exist.
/// Thrown when an invalid parameter was provided.
/// Thrown when Discord is unable to process the request.
public Task UnbanAsync(DiscordGuild guild, string reason = null)
=> guild.UnbanMemberAsync(this, reason);
///
/// Gets this user's presence.
///
[JsonIgnore]
public DiscordPresence Presence
=> this.Discord is DiscordClient dc && dc.Presences.TryGetValue(this.Id, out var presence) ? presence : null;
///
/// Gets the user's avatar URL, in requested format and size.
///
/// Format of the avatar to get.
/// Maximum size of the avatar. Must be a power of two, minimum 16, maximum 2048.
/// URL of the user's avatar.
public string GetAvatarUrl(ImageFormat fmt, ushort size = 1024)
{
if (fmt == ImageFormat.Unknown)
throw new ArgumentException("You must specify valid image format.", nameof(fmt));
if (size < 16 || size > 2048)
throw new ArgumentOutOfRangeException(nameof(size));
var log = Math.Log(size, 2);
if (log < 4 || log > 11 || log % 1 != 0)
throw new ArgumentOutOfRangeException(nameof(size));
var sfmt = "";
sfmt = fmt switch
{
ImageFormat.Gif => "gif",
ImageFormat.Jpeg => "jpg",
ImageFormat.Png => "png",
ImageFormat.WebP => "webp",
ImageFormat.Auto => !string.IsNullOrWhiteSpace(this.AvatarHash) ? this.AvatarHash.StartsWith("a_") ? "gif" : "png" : "png",
_ => throw new ArgumentOutOfRangeException(nameof(fmt)),
};
var ssize = size.ToString(CultureInfo.InvariantCulture);
if (!string.IsNullOrWhiteSpace(this.AvatarHash))
{
var id = this.Id.ToString(CultureInfo.InvariantCulture);
return $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.AVATARS}/{id}/{this.AvatarHash}.{sfmt}?size={ssize}";
}
else
{
var type = (this.DiscriminatorInt % 5).ToString(CultureInfo.InvariantCulture);
return $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.EMBED}{Endpoints.AVATARS}/{type}.{sfmt}?size={ssize}";
}
}
+ ///
+ /// Creates a direct message channel to this user.
+ ///
+ /// Direct message channel to this user.
+ /// Thrown when the user has the bot blocked, the member shares no guild with the bot, or if the member has Allow DM from server members off.
+ /// Thrown when the user does not exist.
+ /// Thrown when an invalid parameter was provided.
+ /// Thrown when Discord is unable to process the request.
+ public Task CreateDmChannelAsync()
+ => this.Discord.ApiClient.CreateDmAsync(this.Id);
+
+ ///
+ /// Sends a direct message to this user. Creates a direct message channel if one does not exist already.
+ ///
+ /// Content of the message to send.
+ /// The sent message.
+ /// Thrown when the user has the bot blocked, the member shares no guild with the bot, or if the member has Allow DM from server members off.
+ /// Thrown when the user does not exist.
+ /// Thrown when an invalid parameter was provided.
+ /// Thrown when Discord is unable to process the request.
+ public async Task SendMessageAsync(string content)
+ {
+ if (this.IsBot && this.Discord.CurrentUser.IsBot)
+ throw new ArgumentException("Bots cannot DM each other.");
+
+ var chn = await this.CreateDmChannelAsync().ConfigureAwait(false);
+ return await chn.SendMessageAsync(content).ConfigureAwait(false);
+ }
+
+ ///
+ /// Sends a direct message to this user. Creates a direct message channel if one does not exist already.
+ ///
+ /// Embed to attach to the message.
+ /// The sent message.
+ /// Thrown when the user has the bot blocked, the member shares no guild with the bot, or if the member has Allow DM from server members off.
+ /// Thrown when the user does not exist.
+ /// Thrown when an invalid parameter was provided.
+ /// Thrown when Discord is unable to process the request.
+ public async Task SendMessageAsync(DiscordEmbed embed)
+ {
+ if (this.IsBot && this.Discord.CurrentUser.IsBot)
+ throw new ArgumentException("Bots cannot DM each other.");
+
+ var chn = await this.CreateDmChannelAsync().ConfigureAwait(false);
+ return await chn.SendMessageAsync(embed).ConfigureAwait(false);
+ }
+
+ ///
+ /// Sends a direct message to this user. Creates a direct message channel if one does not exist already.
+ ///
+ /// Content of the message to send.
+ /// Embed to attach to the message.
+ /// The sent message.
+ /// Thrown when the user has the bot blocked, the member shares no guild with the bot, or if the member has Allow DM from server members off.
+ /// Thrown when the user does not exist.
+ /// Thrown when an invalid parameter was provided.
+ /// Thrown when Discord is unable to process the request.
+ public async Task SendMessageAsync(string content, DiscordEmbed embed)
+ {
+ if (this.IsBot && this.Discord.CurrentUser.IsBot)
+ throw new ArgumentException("Bots cannot DM each other.");
+
+ var chn = await this.CreateDmChannelAsync().ConfigureAwait(false);
+ return await chn.SendMessageAsync(content, embed).ConfigureAwait(false);
+ }
+
+ ///
+ /// Sends a direct message to this user. Creates a direct message channel if one does not exist already.
+ ///
+ /// Builder to with the message.
+ /// The sent message.
+ /// Thrown when the user has the bot blocked, the member shares no guild with the bot, or if the member has Allow DM from server members off.
+ /// Thrown when the user does not exist.
+ /// Thrown when an invalid parameter was provided.
+ /// Thrown when Discord is unable to process the request.
+ public async Task SendMessageAsync(DiscordMessageBuilder message)
+ {
+ if (this.IsBot && this.Discord.CurrentUser.IsBot)
+ throw new ArgumentException("Bots cannot DM each other.");
+
+ var chn = await this.CreateDmChannelAsync().ConfigureAwait(false);
+ return await chn.SendMessageAsync(message).ConfigureAwait(false);
+ }
+
///
/// Returns a string representation of this user.
///
/// String representation of this user.
public override string ToString() => $"User {this.Id}; {this.Username}#{this.Discriminator}";
///
/// Checks whether this is equal to another object.
///
/// Object to compare to.
/// Whether the object is equal to this .
public override bool Equals(object obj) => this.Equals(obj as DiscordUser);
///
/// Checks whether this is equal to another .
///
/// to compare to.
/// Whether the is equal to this .
public bool Equals(DiscordUser e) => e is not null && (ReferenceEquals(this, e) || this.Id == e.Id);
///
/// Gets the hash code for this .
///
/// The hash code for this .
public override int GetHashCode() => this.Id.GetHashCode();
///
/// Gets whether the two objects are equal.
///
/// First user to compare.
/// Second user to compare.
/// Whether the two users are equal.
public static bool operator ==(DiscordUser e1, DiscordUser e2)
{
var o1 = e1 as object;
var o2 = e2 as object;
return (o1 != null || o2 == null) && (o1 == null || o2 != null) && ((o1 == null && o2 == null) || e1.Id == e2.Id);
}
///
/// Gets whether the two objects are not equal.
///
/// First user to compare.
/// Second user to compare.
/// Whether the two users are not equal.
public static bool operator !=(DiscordUser e1, DiscordUser e2)
=> !(e1 == e2);
}
///
/// Represents a user comparer.
///
internal class DiscordUserComparer : IEqualityComparer
{
///
/// Whether the users are equal.
///
/// The first user
/// The second user.
public bool Equals(DiscordUser x, DiscordUser y) => x.Equals(y);
///
/// Gets the hash code.
///
/// The user.
public int GetHashCode(DiscordUser obj) => obj.Id.GetHashCode();
}