| @@ -3,6 +3,6 @@ | |||||
| public static class CommandExtensions | public static class CommandExtensions | ||||
| { | { | ||||
| public static CommandService Commands(this DiscordClient client, bool required = true) | public static CommandService Commands(this DiscordClient client, bool required = true) | ||||
| => client.GetService<CommandService>(required); | |||||
| => client.Services.Get<CommandService>(required); | |||||
| } | } | ||||
| } | } | ||||
| @@ -28,7 +28,6 @@ namespace Discord | |||||
| private readonly ConcurrentDictionary<ulong, Channel> _channels; | private readonly ConcurrentDictionary<ulong, Channel> _channels; | ||||
| private readonly ConcurrentDictionary<ulong, Channel> _privateChannels; //Key = RecipientId | private readonly ConcurrentDictionary<ulong, Channel> _privateChannels; //Key = RecipientId | ||||
| private readonly JsonSerializer _serializer; | private readonly JsonSerializer _serializer; | ||||
| private readonly Region _unknownRegion; | |||||
| private Dictionary<string, Region> _regions; | private Dictionary<string, Region> _regions; | ||||
| private CancellationTokenSource _cancelTokenSource; | private CancellationTokenSource _cancelTokenSource; | ||||
| @@ -42,6 +41,8 @@ namespace Discord | |||||
| public RestClient StatusAPI { get; } | public RestClient StatusAPI { get; } | ||||
| /// <summary> Gets the internal WebSocket for the Gateway event stream. </summary> | /// <summary> Gets the internal WebSocket for the Gateway event stream. </summary> | ||||
| public GatewaySocket GatewaySocket { get; } | public GatewaySocket GatewaySocket { get; } | ||||
| /// <summary> Gets the service manager used for adding extensions to this client. </summary> | |||||
| public ServiceManager Services { get; } | |||||
| /// <summary> Gets the queue used for outgoing messages, if enabled. </summary> | /// <summary> Gets the queue used for outgoing messages, if enabled. </summary> | ||||
| internal MessageQueue MessageQueue { get; } | internal MessageQueue MessageQueue { get; } | ||||
| /// <summary> Gets the logger used for this client. </summary> | /// <summary> Gets the logger used for this client. </summary> | ||||
| @@ -66,6 +67,8 @@ namespace Discord | |||||
| // public IEnumerable<Channel> Channels => _servers.Select(x => x.Value); | // public IEnumerable<Channel> Channels => _servers.Select(x => x.Value); | ||||
| /// <summary> Gets a collection of all private channels this client is a member of. </summary> | /// <summary> Gets a collection of all private channels this client is a member of. </summary> | ||||
| public IEnumerable<Channel> PrivateChannels => _channels.Select(x => x.Value); | public IEnumerable<Channel> PrivateChannels => _channels.Select(x => x.Value); | ||||
| /// <summary> Gets a collection of all voice regions currently offered by Discord. </summary> | |||||
| public IEnumerable<Region> Regions => _regions.Select(x => x.Value); | |||||
| /// <summary> Initializes a new instance of the DiscordClient class. </summary> | /// <summary> Initializes a new instance of the DiscordClient class. </summary> | ||||
| public DiscordClient(DiscordConfig config = null) | public DiscordClient(DiscordConfig config = null) | ||||
| @@ -75,8 +78,8 @@ namespace Discord | |||||
| State = (int)ConnectionState.Disconnected; | State = (int)ConnectionState.Disconnected; | ||||
| Status = UserStatus.Online; | Status = UserStatus.Online; | ||||
| //Services | |||||
| //Logging | |||||
| Log = new LogManager(this); | Log = new LogManager(this); | ||||
| Logger = Log.CreateLogger("Discord"); | Logger = Log.CreateLogger("Discord"); | ||||
| @@ -91,7 +94,6 @@ namespace Discord | |||||
| _servers = new ConcurrentDictionary<ulong, Server>(); | _servers = new ConcurrentDictionary<ulong, Server>(); | ||||
| _channels = new ConcurrentDictionary<ulong, Channel>(); | _channels = new ConcurrentDictionary<ulong, Channel>(); | ||||
| _privateChannels = new ConcurrentDictionary<ulong, Channel>(); | _privateChannels = new ConcurrentDictionary<ulong, Channel>(); | ||||
| _unknownRegion = new Region("", "Unknown", "", 0); | |||||
| //Serialization | //Serialization | ||||
| _serializer = new JsonSerializer(); | _serializer = new JsonSerializer(); | ||||
| @@ -126,10 +128,13 @@ namespace Discord | |||||
| ClientAPI.CancelToken = CancelToken; | ClientAPI.CancelToken = CancelToken; | ||||
| await SendStatus(); | await SendStatus(); | ||||
| }; | }; | ||||
| //Import/Export | |||||
| //_messageImporter = new JsonSerializer(); | |||||
| //_messageImporter.ContractResolver = new Message.ImportResolver(); | |||||
| //Extensibility | |||||
| Services = new ServiceManager(this); | |||||
| //Import/Export | |||||
| //_messageImporter = new JsonSerializer(); | |||||
| //_messageImporter.ContractResolver = new Message.ImportResolver(); | |||||
| } | } | ||||
| /// <summary> Connects to the Discord server with the provided email and password. </summary> | /// <summary> Connects to the Discord server with the provided email and password. </summary> | ||||
| @@ -167,7 +172,8 @@ namespace Discord | |||||
| await Login(email, password, token).ConfigureAwait(false); | await Login(email, password, token).ConfigureAwait(false); | ||||
| ClientAPI.Token = token; | ClientAPI.Token = token; | ||||
| await GatewaySocket.Connect(token).ConfigureAwait(false); | |||||
| GatewaySocket.Token = token; | |||||
| await GatewaySocket.Connect().ConfigureAwait(false); | |||||
| List<Task> tasks = new List<Task>(); | List<Task> tasks = new List<Task>(); | ||||
| tasks.Add(CancelToken.Wait()); | tasks.Add(CancelToken.Wait()); | ||||
| @@ -265,7 +271,10 @@ namespace Discord | |||||
| await ClientAPI.Send(new LogoutRequest()).ConfigureAwait(false); | await ClientAPI.Send(new LogoutRequest()).ConfigureAwait(false); | ||||
| _servers.Clear(); | |||||
| ClientAPI.Token = null; | |||||
| GatewaySocket.Token = null; | |||||
| _servers.Clear(); | |||||
| _channels.Clear(); | _channels.Clear(); | ||||
| _privateChannels.Clear(); | _privateChannels.Clear(); | ||||
| @@ -276,45 +285,197 @@ namespace Discord | |||||
| _connectedEvent.Reset(); | _connectedEvent.Reset(); | ||||
| _disconnectedEvent.Set(); | _disconnectedEvent.Set(); | ||||
| } | } | ||||
| private void OnReceivedEvent(WebSocketEventEventArgs e) | |||||
| { | |||||
| try | |||||
| { | |||||
| switch (e.Type) | |||||
| { | |||||
| //Global | |||||
| case "READY": //Resync | |||||
| { | |||||
| var data = e.Payload.ToObject<ReadyEvent>(_serializer); | |||||
| public Task SetStatus(UserStatus status) | |||||
| { | |||||
| if (status == null) throw new ArgumentNullException(nameof(status)); | |||||
| if (status != UserStatus.Online && status != UserStatus.Idle) | |||||
| throw new ArgumentException($"Invalid status, must be {UserStatus.Online} or {UserStatus.Idle}", nameof(status)); | |||||
| Status = status; | |||||
| return SendStatus(); | |||||
| } | |||||
| public Task SetGame(int? gameId) | |||||
| { | |||||
| CurrentGameId = gameId; | |||||
| return SendStatus(); | |||||
| } | |||||
| private Task SendStatus() | |||||
| { | |||||
| GatewaySocket.SendUpdateStatus(Status == UserStatus.Idle ? EpochTime.GetMilliseconds() - (10 * 60 * 1000) : (long?)null, CurrentGameId); | |||||
| return TaskHelper.CompletedTask; | |||||
| } | |||||
| #region Channels | |||||
| private Channel AddChannel(ulong id, ulong? guildId, ulong? recipientId) | |||||
| { | |||||
| Channel channel; | |||||
| if (recipientId != null) | |||||
| { | |||||
| channel = _privateChannels.GetOrAdd(recipientId.Value, | |||||
| x => new Channel(this, x, new User(this, recipientId.Value, null))); | |||||
| } | |||||
| else | |||||
| { | |||||
| var server = GetServer(guildId.Value); | |||||
| channel = server.AddChannel(id); | |||||
| } | |||||
| _channels[channel.Id] = channel; | |||||
| return channel; | |||||
| } | |||||
| private Channel RemoveChannel(ulong id) | |||||
| { | |||||
| Channel channel; | |||||
| if (_channels.TryRemove(id, out channel)) | |||||
| { | |||||
| if (channel.IsPrivate) | |||||
| _privateChannels.TryRemove(channel.Recipient.Id, out channel); | |||||
| else | |||||
| channel.Server.RemoveChannel(id); | |||||
| } | |||||
| return channel; | |||||
| } | |||||
| internal Channel GetChannel(ulong id) | |||||
| { | |||||
| Channel channel; | |||||
| _channels.TryGetValue(id, out channel); | |||||
| return channel; | |||||
| } | |||||
| internal Channel GetPrivateChannel(ulong recipientId) | |||||
| { | |||||
| Channel channel; | |||||
| _privateChannels.TryGetValue(recipientId, out channel); | |||||
| return channel; | |||||
| } | |||||
| internal async Task<Channel> CreatePrivateChannel(User user) | |||||
| { | |||||
| var channel = GetPrivateChannel(user.Id); | |||||
| if (channel != null) return channel; | |||||
| var request = new CreatePrivateChannelRequest() { RecipientId = user.Id }; | |||||
| var response = await ClientAPI.Send(request).ConfigureAwait(false); | |||||
| channel = AddChannel(response.Id, null, response.Recipient.Id); | |||||
| channel.Update(response); | |||||
| return channel; | |||||
| } | |||||
| #endregion | |||||
| #region Invites | |||||
| /// <summary> Gets more info about the provided invite code. </summary> | |||||
| /// <remarks> Supported formats: inviteCode, xkcdCode, https://discord.gg/inviteCode, https://discord.gg/xkcdCode </remarks> | |||||
| public async Task<Invite> GetInvite(string inviteIdOrXkcd) | |||||
| { | |||||
| if (inviteIdOrXkcd == null) throw new ArgumentNullException(nameof(inviteIdOrXkcd)); | |||||
| //Remove trailing slash | |||||
| if (inviteIdOrXkcd.Length > 0 && inviteIdOrXkcd[inviteIdOrXkcd.Length - 1] == '/') | |||||
| inviteIdOrXkcd = inviteIdOrXkcd.Substring(0, inviteIdOrXkcd.Length - 1); | |||||
| //Remove leading URL | |||||
| int index = inviteIdOrXkcd.LastIndexOf('/'); | |||||
| if (index >= 0) | |||||
| inviteIdOrXkcd = inviteIdOrXkcd.Substring(index + 1); | |||||
| var response = await ClientAPI.Send(new GetInviteRequest(inviteIdOrXkcd)).ConfigureAwait(false); | |||||
| var invite = new Invite(this, response.Code, response.XkcdPass); | |||||
| invite.Update(response); | |||||
| return invite; | |||||
| } | |||||
| #endregion | |||||
| #region Regions | |||||
| public Region GetRegion(string id) | |||||
| { | |||||
| Region region; | |||||
| if (_regions.TryGetValue(id, out region)) | |||||
| return region; | |||||
| else | |||||
| return new Region(id, id, "", 0); | |||||
| } | |||||
| #endregion | |||||
| #region Servers | |||||
| private Server AddServer(ulong id) | |||||
| => _servers.GetOrAdd(id, x => new Server(this, x)); | |||||
| private Server RemoveServer(ulong id) | |||||
| { | |||||
| Server server; | |||||
| _servers.TryRemove(id, out server); | |||||
| return server; | |||||
| } | |||||
| public Server GetServer(ulong id) | |||||
| { | |||||
| Server server; | |||||
| _servers.TryGetValue(id, out server); | |||||
| return server; | |||||
| } | |||||
| public IEnumerable<Server> FindServers(string name) | |||||
| { | |||||
| if (name == null) throw new ArgumentNullException(nameof(name)); | |||||
| return _servers.Select(x => x.Value).Find(name); | |||||
| } | |||||
| /// <summary> Creates a new server with the provided name and region. </summary> | |||||
| public async Task<Server> CreateServer(string name, Region region, ImageType iconType = ImageType.None, Stream icon = null) | |||||
| { | |||||
| if (name == null) throw new ArgumentNullException(nameof(name)); | |||||
| if (region == null) throw new ArgumentNullException(nameof(region)); | |||||
| var request = new CreateGuildRequest() | |||||
| { | |||||
| Name = name, | |||||
| Region = region.Id, | |||||
| IconBase64 = icon.Base64(iconType, null) | |||||
| }; | |||||
| var response = await ClientAPI.Send(request).ConfigureAwait(false); | |||||
| var server = AddServer(response.Id); | |||||
| server.Update(response); | |||||
| return server; | |||||
| } | |||||
| #endregion | |||||
| private void OnReceivedEvent(WebSocketEventEventArgs e) | |||||
| { | |||||
| try | |||||
| { | |||||
| switch (e.Type) | |||||
| { | |||||
| //Global | |||||
| case "READY": //Resync | |||||
| { | |||||
| var data = e.Payload.ToObject<ReadyEvent>(_serializer); | |||||
| //SessionId = data.SessionId; | //SessionId = data.SessionId; | ||||
| PrivateUser = new User(data.User.Id, null); | |||||
| PrivateUser = new User(this, data.User.Id, null); | |||||
| PrivateUser.Update(data.User); | PrivateUser.Update(data.User); | ||||
| CurrentUser.Update(data.User); | CurrentUser.Update(data.User); | ||||
| foreach (var model in data.Guilds) | foreach (var model in data.Guilds) | ||||
| { | |||||
| if (model.Unavailable != true) | |||||
| { | |||||
| var server = AddServer(model.Id); | |||||
| server.Update(model); | |||||
| } | |||||
| } | |||||
| foreach (var model in data.PrivateChannels) | |||||
| { | |||||
| if (model.Unavailable != true) | |||||
| { | |||||
| var server = AddServer(model.Id); | |||||
| server.Update(model); | |||||
| } | |||||
| } | |||||
| foreach (var model in data.PrivateChannels) | |||||
| { | { | ||||
| var channel = AddChannel(model.Id, null, model.Recipient.Id); | var channel = AddChannel(model.Id, null, model.Recipient.Id); | ||||
| channel.Update(model); | |||||
| } | |||||
| } | |||||
| break; | |||||
| //Servers | |||||
| case "GUILD_CREATE": | |||||
| { | |||||
| var data = e.Payload.ToObject<GuildCreateEvent>(_serializer); | |||||
| if (data.Unavailable != true) | |||||
| { | |||||
| var server = AddServer(data.Id); | |||||
| server.Update(data); | |||||
| channel.Update(model); | |||||
| } | |||||
| } | |||||
| break; | |||||
| //Servers | |||||
| case "GUILD_CREATE": | |||||
| { | |||||
| var data = e.Payload.ToObject<GuildCreateEvent>(_serializer); | |||||
| if (data.Unavailable != true) | |||||
| { | |||||
| var server = AddServer(data.Id); | |||||
| server.Update(data); | |||||
| if (data.Unavailable != false) | if (data.Unavailable != false) | ||||
| { | { | ||||
| Logger.Info($"Server Created: {server.Name}"); | Logger.Info($"Server Created: {server.Name}"); | ||||
| @@ -324,26 +485,26 @@ namespace Discord | |||||
| Logger.Info($"Server Available: {server.Name}"); | Logger.Info($"Server Available: {server.Name}"); | ||||
| OnServerAvailable(server); | OnServerAvailable(server); | ||||
| } | } | ||||
| } | |||||
| break; | |||||
| case "GUILD_UPDATE": | |||||
| { | |||||
| var data = e.Payload.ToObject<GuildUpdateEvent>(_serializer); | |||||
| var server = GetServer(data.Id); | |||||
| } | |||||
| break; | |||||
| case "GUILD_UPDATE": | |||||
| { | |||||
| var data = e.Payload.ToObject<GuildUpdateEvent>(_serializer); | |||||
| var server = GetServer(data.Id); | |||||
| if (server != null) | if (server != null) | ||||
| { | |||||
| server.Update(data); | |||||
| { | |||||
| server.Update(data); | |||||
| Logger.Info($"Server Updated: {server.Name}"); | Logger.Info($"Server Updated: {server.Name}"); | ||||
| OnServerUpdated(server); | OnServerUpdated(server); | ||||
| } | |||||
| } | |||||
| break; | |||||
| case "GUILD_DELETE": | |||||
| { | |||||
| var data = e.Payload.ToObject<GuildDeleteEvent>(_serializer); | |||||
| } | |||||
| } | |||||
| break; | |||||
| case "GUILD_DELETE": | |||||
| { | |||||
| var data = e.Payload.ToObject<GuildDeleteEvent>(_serializer); | |||||
| Server server = RemoveServer(data.Id); | Server server = RemoveServer(data.Id); | ||||
| if (server != null) | |||||
| { | |||||
| if (server != null) | |||||
| { | |||||
| if (data.Unavailable != true) | if (data.Unavailable != true) | ||||
| Logger.Info($"Server Destroyed: {server.Name}"); | Logger.Info($"Server Destroyed: {server.Name}"); | ||||
| else | else | ||||
| @@ -353,61 +514,61 @@ namespace Discord | |||||
| if (data.Unavailable != true) | if (data.Unavailable != true) | ||||
| OnLeftServer(server); | OnLeftServer(server); | ||||
| } | } | ||||
| } | |||||
| break; | |||||
| } | |||||
| break; | |||||
| //Channels | |||||
| case "CHANNEL_CREATE": | |||||
| { | |||||
| var data = e.Payload.ToObject<ChannelCreateEvent>(_serializer); | |||||
| //Channels | |||||
| case "CHANNEL_CREATE": | |||||
| { | |||||
| var data = e.Payload.ToObject<ChannelCreateEvent>(_serializer); | |||||
| Channel channel = AddChannel(data.Id, data.GuildId, data.Recipient.Id); | Channel channel = AddChannel(data.Id, data.GuildId, data.Recipient.Id); | ||||
| channel.Update(data); | |||||
| channel.Update(data); | |||||
| Logger.Info($"Channel Created: {channel.Server?.Name ?? "[Private]"}/{channel.Name}"); | Logger.Info($"Channel Created: {channel.Server?.Name ?? "[Private]"}/{channel.Name}"); | ||||
| OnChannelCreated(channel); | OnChannelCreated(channel); | ||||
| } | |||||
| break; | |||||
| case "CHANNEL_UPDATE": | |||||
| { | |||||
| var data = e.Payload.ToObject<ChannelUpdateEvent>(_serializer); | |||||
| } | |||||
| break; | |||||
| case "CHANNEL_UPDATE": | |||||
| { | |||||
| var data = e.Payload.ToObject<ChannelUpdateEvent>(_serializer); | |||||
| var channel = GetChannel(data.Id); | var channel = GetChannel(data.Id); | ||||
| if (channel != null) | |||||
| { | |||||
| channel.Update(data); | |||||
| if (channel != null) | |||||
| { | |||||
| channel.Update(data); | |||||
| Logger.Info($"Channel Updated: {channel.Server?.Name ?? "[Private]"}/{channel.Name}"); | Logger.Info($"Channel Updated: {channel.Server?.Name ?? "[Private]"}/{channel.Name}"); | ||||
| OnChannelUpdated(channel); | OnChannelUpdated(channel); | ||||
| } | |||||
| } | |||||
| break; | |||||
| case "CHANNEL_DELETE": | |||||
| { | |||||
| var data = e.Payload.ToObject<ChannelDeleteEvent>(_serializer); | |||||
| } | |||||
| } | |||||
| break; | |||||
| case "CHANNEL_DELETE": | |||||
| { | |||||
| var data = e.Payload.ToObject<ChannelDeleteEvent>(_serializer); | |||||
| var channel = RemoveChannel(data.Id); | var channel = RemoveChannel(data.Id); | ||||
| if (channel != null) | if (channel != null) | ||||
| { | { | ||||
| Logger.Info($"Channel Destroyed: {channel.Server?.Name ?? "[Private]"}/{channel.Name}"); | Logger.Info($"Channel Destroyed: {channel.Server?.Name ?? "[Private]"}/{channel.Name}"); | ||||
| OnChannelDestroyed(channel); | OnChannelDestroyed(channel); | ||||
| } | } | ||||
| } | |||||
| break; | |||||
| } | |||||
| break; | |||||
| //Members | |||||
| case "GUILD_MEMBER_ADD": | |||||
| { | |||||
| var data = e.Payload.ToObject<GuildMemberAddEvent>(_serializer); | |||||
| //Members | |||||
| case "GUILD_MEMBER_ADD": | |||||
| { | |||||
| var data = e.Payload.ToObject<GuildMemberAddEvent>(_serializer); | |||||
| var server = GetServer(data.GuildId.Value); | var server = GetServer(data.GuildId.Value); | ||||
| if (server != null) | if (server != null) | ||||
| { | { | ||||
| var user = server.AddMember(data.User.Id); | |||||
| var user = server.AddUser(data.User.Id); | |||||
| user.Update(data); | user.Update(data); | ||||
| user.UpdateActivity(); | user.UpdateActivity(); | ||||
| Logger.Info($"User Joined: {server.Name}/{user.Name}"); | Logger.Info($"User Joined: {server.Name}/{user.Name}"); | ||||
| OnUserJoined(user); | OnUserJoined(user); | ||||
| } | } | ||||
| } | |||||
| break; | |||||
| case "GUILD_MEMBER_UPDATE": | |||||
| { | |||||
| var data = e.Payload.ToObject<GuildMemberUpdateEvent>(_serializer); | |||||
| } | |||||
| break; | |||||
| case "GUILD_MEMBER_UPDATE": | |||||
| { | |||||
| var data = e.Payload.ToObject<GuildMemberUpdateEvent>(_serializer); | |||||
| var server = GetServer(data.GuildId.Value); | var server = GetServer(data.GuildId.Value); | ||||
| if (server != null) | if (server != null) | ||||
| { | { | ||||
| @@ -419,43 +580,43 @@ namespace Discord | |||||
| OnUserUpdated(user); | OnUserUpdated(user); | ||||
| } | } | ||||
| } | } | ||||
| } | |||||
| break; | |||||
| case "GUILD_MEMBER_REMOVE": | |||||
| { | |||||
| var data = e.Payload.ToObject<GuildMemberRemoveEvent>(_serializer); | |||||
| } | |||||
| break; | |||||
| case "GUILD_MEMBER_REMOVE": | |||||
| { | |||||
| var data = e.Payload.ToObject<GuildMemberRemoveEvent>(_serializer); | |||||
| var server = GetServer(data.GuildId.Value); | var server = GetServer(data.GuildId.Value); | ||||
| if (server != null) | if (server != null) | ||||
| { | { | ||||
| var user = server.RemoveMember(data.User.Id); | |||||
| var user = server.RemoveUser(data.User.Id); | |||||
| if (user != null) | if (user != null) | ||||
| { | { | ||||
| Logger.Info($"User Left: {server.Name}/{user.Name}"); | Logger.Info($"User Left: {server.Name}/{user.Name}"); | ||||
| OnUserLeft(user); | OnUserLeft(user); | ||||
| } | } | ||||
| } | } | ||||
| } | |||||
| break; | |||||
| case "GUILD_MEMBERS_CHUNK": | |||||
| { | |||||
| var data = e.Payload.ToObject<GuildMembersChunkEvent>(_serializer); | |||||
| } | |||||
| break; | |||||
| case "GUILD_MEMBERS_CHUNK": | |||||
| { | |||||
| var data = e.Payload.ToObject<GuildMembersChunkEvent>(_serializer); | |||||
| foreach (var memberData in data.Members) | foreach (var memberData in data.Members) | ||||
| { | { | ||||
| var server = GetServer(memberData.GuildId.Value); | var server = GetServer(memberData.GuildId.Value); | ||||
| if (server != null) | if (server != null) | ||||
| { | { | ||||
| var user = server.AddMember(memberData.User.Id); | |||||
| var user = server.AddUser(memberData.User.Id); | |||||
| user.Update(memberData); | user.Update(memberData); | ||||
| //OnUserAdded(user); | //OnUserAdded(user); | ||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| break; | |||||
| break; | |||||
| //Roles | |||||
| case "GUILD_ROLE_CREATE": | |||||
| { | |||||
| var data = e.Payload.ToObject<GuildRoleCreateEvent>(_serializer); | |||||
| //Roles | |||||
| case "GUILD_ROLE_CREATE": | |||||
| { | |||||
| var data = e.Payload.ToObject<GuildRoleCreateEvent>(_serializer); | |||||
| var server = GetServer(data.GuildId); | var server = GetServer(data.GuildId); | ||||
| if (server != null) | if (server != null) | ||||
| { | { | ||||
| @@ -464,11 +625,11 @@ namespace Discord | |||||
| Logger.Info($"Role Created: {server.Name}/{role.Name}"); | Logger.Info($"Role Created: {server.Name}/{role.Name}"); | ||||
| OnRoleUpdated(role); | OnRoleUpdated(role); | ||||
| } | } | ||||
| } | |||||
| break; | |||||
| case "GUILD_ROLE_UPDATE": | |||||
| { | |||||
| var data = e.Payload.ToObject<GuildRoleUpdateEvent>(_serializer); | |||||
| } | |||||
| break; | |||||
| case "GUILD_ROLE_UPDATE": | |||||
| { | |||||
| var data = e.Payload.ToObject<GuildRoleUpdateEvent>(_serializer); | |||||
| var server = GetServer(data.GuildId); | var server = GetServer(data.GuildId); | ||||
| if (server != null) | if (server != null) | ||||
| { | { | ||||
| @@ -480,11 +641,11 @@ namespace Discord | |||||
| OnRoleUpdated(role); | OnRoleUpdated(role); | ||||
| } | } | ||||
| } | } | ||||
| } | |||||
| break; | |||||
| case "GUILD_ROLE_DELETE": | |||||
| { | |||||
| var data = e.Payload.ToObject<GuildRoleDeleteEvent>(_serializer); | |||||
| } | |||||
| break; | |||||
| case "GUILD_ROLE_DELETE": | |||||
| { | |||||
| var data = e.Payload.ToObject<GuildRoleDeleteEvent>(_serializer); | |||||
| var server = GetServer(data.GuildId); | var server = GetServer(data.GuildId); | ||||
| if (server != null) | if (server != null) | ||||
| { | { | ||||
| @@ -495,41 +656,41 @@ namespace Discord | |||||
| OnRoleDeleted(role); | OnRoleDeleted(role); | ||||
| } | } | ||||
| } | } | ||||
| } | |||||
| break; | |||||
| //Bans | |||||
| case "GUILD_BAN_ADD": | |||||
| { | |||||
| var data = e.Payload.ToObject<GuildBanAddEvent>(_serializer); | |||||
| var server = GetServer(data.GuildId); | |||||
| } | |||||
| break; | |||||
| //Bans | |||||
| case "GUILD_BAN_ADD": | |||||
| { | |||||
| var data = e.Payload.ToObject<GuildBanAddEvent>(_serializer); | |||||
| var server = GetServer(data.GuildId); | |||||
| if (server != null) | if (server != null) | ||||
| { | |||||
| { | |||||
| server.AddBan(data.UserId); | server.AddBan(data.UserId); | ||||
| Logger.Info($"User Banned: {server.Name}/{data.UserId}"); | Logger.Info($"User Banned: {server.Name}/{data.UserId}"); | ||||
| OnUserBanned(server, data.UserId); | OnUserBanned(server, data.UserId); | ||||
| } | |||||
| } | |||||
| break; | |||||
| case "GUILD_BAN_REMOVE": | |||||
| { | |||||
| var data = e.Payload.ToObject<GuildBanRemoveEvent>(_serializer); | |||||
| var server = GetServer(data.GuildId); | |||||
| } | |||||
| } | |||||
| break; | |||||
| case "GUILD_BAN_REMOVE": | |||||
| { | |||||
| var data = e.Payload.ToObject<GuildBanRemoveEvent>(_serializer); | |||||
| var server = GetServer(data.GuildId); | |||||
| if (server != null) | if (server != null) | ||||
| { | |||||
| { | |||||
| if (server.RemoveBan(data.UserId)) | if (server.RemoveBan(data.UserId)) | ||||
| { | { | ||||
| Logger.Info($"User Unbanned: {server.Name}/{data.UserId}"); | Logger.Info($"User Unbanned: {server.Name}/{data.UserId}"); | ||||
| OnUserUnbanned(server, data.UserId); | OnUserUnbanned(server, data.UserId); | ||||
| } | } | ||||
| } | |||||
| } | |||||
| break; | |||||
| } | |||||
| } | |||||
| break; | |||||
| //Messages | |||||
| case "MESSAGE_CREATE": | |||||
| { | |||||
| var data = e.Payload.ToObject<MessageCreateEvent>(_serializer); | |||||
| //Messages | |||||
| case "MESSAGE_CREATE": | |||||
| { | |||||
| var data = e.Payload.ToObject<MessageCreateEvent>(_serializer); | |||||
| Channel channel = GetChannel(data.ChannelId); | Channel channel = GetChannel(data.ChannelId); | ||||
| if (channel != null) | if (channel != null) | ||||
| @@ -548,7 +709,7 @@ namespace Discord | |||||
| msg = channel.AddMessage(data.Id, data.Author.Id, data.Timestamp.Value); | msg = channel.AddMessage(data.Id, data.Author.Id, data.Timestamp.Value); | ||||
| //nonce = 0; | //nonce = 0; | ||||
| } | } | ||||
| msg.Update(data); | msg.Update(data); | ||||
| var user = msg.User; | var user = msg.User; | ||||
| if (user != null) | if (user != null) | ||||
| @@ -566,11 +727,11 @@ namespace Discord | |||||
| Logger.Info($"Message Received: {channel.Server?.Name ?? "[Private]"}/{channel.Name}/{msg.Id}"); | Logger.Info($"Message Received: {channel.Server?.Name ?? "[Private]"}/{channel.Name}/{msg.Id}"); | ||||
| OnMessageReceived(msg); | OnMessageReceived(msg); | ||||
| } | } | ||||
| } | |||||
| break; | |||||
| case "MESSAGE_UPDATE": | |||||
| { | |||||
| var data = e.Payload.ToObject<MessageUpdateEvent>(_serializer); | |||||
| } | |||||
| break; | |||||
| case "MESSAGE_UPDATE": | |||||
| { | |||||
| var data = e.Payload.ToObject<MessageUpdateEvent>(_serializer); | |||||
| var channel = GetChannel(data.ChannelId); | var channel = GetChannel(data.ChannelId); | ||||
| if (channel != null) | if (channel != null) | ||||
| { | { | ||||
| @@ -583,11 +744,11 @@ namespace Discord | |||||
| OnMessageUpdated(msg); | OnMessageUpdated(msg); | ||||
| } | } | ||||
| } | } | ||||
| } | |||||
| break; | |||||
| case "MESSAGE_DELETE": | |||||
| { | |||||
| var data = e.Payload.ToObject<MessageDeleteEvent>(_serializer); | |||||
| } | |||||
| break; | |||||
| case "MESSAGE_DELETE": | |||||
| { | |||||
| var data = e.Payload.ToObject<MessageDeleteEvent>(_serializer); | |||||
| var channel = GetChannel(data.ChannelId); | var channel = GetChannel(data.ChannelId); | ||||
| if (channel != null) | if (channel != null) | ||||
| { | { | ||||
| @@ -598,11 +759,11 @@ namespace Discord | |||||
| OnMessageDeleted(msg); | OnMessageDeleted(msg); | ||||
| } | } | ||||
| } | } | ||||
| } | |||||
| break; | |||||
| case "MESSAGE_ACK": | |||||
| { | |||||
| var data = e.Payload.ToObject<MessageAckEvent>(_serializer); | |||||
| } | |||||
| break; | |||||
| case "MESSAGE_ACK": | |||||
| { | |||||
| var data = e.Payload.ToObject<MessageAckEvent>(_serializer); | |||||
| var channel = GetChannel(data.ChannelId); | var channel = GetChannel(data.ChannelId); | ||||
| if (channel != null) | if (channel != null) | ||||
| { | { | ||||
| @@ -613,13 +774,13 @@ namespace Discord | |||||
| OnMessageAcknowledged(msg); | OnMessageAcknowledged(msg); | ||||
| } | } | ||||
| } | } | ||||
| } | |||||
| break; | |||||
| } | |||||
| break; | |||||
| //Statuses | |||||
| case "PRESENCE_UPDATE": | |||||
| { | |||||
| var data = e.Payload.ToObject<PresenceUpdateEvent>(_serializer); | |||||
| //Statuses | |||||
| case "PRESENCE_UPDATE": | |||||
| { | |||||
| var data = e.Payload.ToObject<PresenceUpdateEvent>(_serializer); | |||||
| User user; | User user; | ||||
| Server server; | Server server; | ||||
| if (data.GuildId == null) | if (data.GuildId == null) | ||||
| @@ -633,20 +794,20 @@ namespace Discord | |||||
| user = server?.GetUser(data.User.Id); | user = server?.GetUser(data.User.Id); | ||||
| } | } | ||||
| if (user != null) | |||||
| { | |||||
| user.Update(data); | |||||
| if (user != null) | |||||
| { | |||||
| user.Update(data); | |||||
| Logger.Verbose($"Presence Updated: {server.Name}/{user.Name}"); | Logger.Verbose($"Presence Updated: {server.Name}/{user.Name}"); | ||||
| OnUserPresenceUpdated(user); | OnUserPresenceUpdated(user); | ||||
| } | |||||
| } | |||||
| break; | |||||
| case "TYPING_START": | |||||
| { | |||||
| var data = e.Payload.ToObject<TypingStartEvent>(_serializer); | |||||
| var channel = GetChannel(data.ChannelId); | |||||
| if (channel != null) | |||||
| { | |||||
| } | |||||
| } | |||||
| break; | |||||
| case "TYPING_START": | |||||
| { | |||||
| var data = e.Payload.ToObject<TypingStartEvent>(_serializer); | |||||
| var channel = GetChannel(data.ChannelId); | |||||
| if (channel != null) | |||||
| { | |||||
| User user; | User user; | ||||
| if (channel.IsPrivate) | if (channel.IsPrivate) | ||||
| { | { | ||||
| @@ -657,20 +818,20 @@ namespace Discord | |||||
| } | } | ||||
| else | else | ||||
| user = channel.Server.GetUser(data.UserId); | user = channel.Server.GetUser(data.UserId); | ||||
| if (user != null) | |||||
| if (user != null) | |||||
| { | { | ||||
| Logger.Verbose($"Is Typing: {channel.Server?.Name ?? "[Private]"}/{channel.Name}/{user.Name}"); | Logger.Verbose($"Is Typing: {channel.Server?.Name ?? "[Private]"}/{channel.Name}/{user.Name}"); | ||||
| OnUserIsTypingUpdated(channel, user); | |||||
| user.UpdateActivity(); | |||||
| OnUserIsTypingUpdated(channel, user); | |||||
| user.UpdateActivity(); | |||||
| } | } | ||||
| } | } | ||||
| } | |||||
| break; | |||||
| } | |||||
| break; | |||||
| //Voice | |||||
| case "VOICE_STATE_UPDATE": | |||||
| { | |||||
| var data = e.Payload.ToObject<VoiceStateUpdateEvent>(_serializer); | |||||
| //Voice | |||||
| case "VOICE_STATE_UPDATE": | |||||
| { | |||||
| var data = e.Payload.ToObject<VoiceStateUpdateEvent>(_serializer); | |||||
| var server = GetServer(data.GuildId); | var server = GetServer(data.GuildId); | ||||
| if (server != null) | if (server != null) | ||||
| { | { | ||||
| @@ -682,126 +843,46 @@ namespace Discord | |||||
| OnUserVoiceStateUpdated(user); | OnUserVoiceStateUpdated(user); | ||||
| } | } | ||||
| } | } | ||||
| } | |||||
| break; | |||||
| } | |||||
| break; | |||||
| //Settings | |||||
| case "USER_UPDATE": | |||||
| { | |||||
| var data = e.Payload.ToObject<UserUpdateEvent>(_serializer); | |||||
| //Settings | |||||
| case "USER_UPDATE": | |||||
| { | |||||
| var data = e.Payload.ToObject<UserUpdateEvent>(_serializer); | |||||
| if (data.Id == CurrentUser.Id) | if (data.Id == CurrentUser.Id) | ||||
| { | { | ||||
| CurrentUser.Update(data); | CurrentUser.Update(data); | ||||
| PrivateUser.Update(data); | PrivateUser.Update(data); | ||||
| foreach (var server in _servers) | |||||
| server.Value.CurrentUser.Update(data); | |||||
| foreach (var server in _servers) | |||||
| server.Value.CurrentUser.Update(data); | |||||
| Logger.Info("Profile Updated"); | Logger.Info("Profile Updated"); | ||||
| OnProfileUpdated(CurrentUser); | OnProfileUpdated(CurrentUser); | ||||
| } | |||||
| } | |||||
| break; | |||||
| //Ignored | |||||
| case "USER_SETTINGS_UPDATE": | |||||
| case "GUILD_INTEGRATIONS_UPDATE": | |||||
| case "VOICE_SERVER_UPDATE": | |||||
| break; | |||||
| case "RESUMED": //Handled in DataWebSocket | |||||
| break; | |||||
| //Others | |||||
| default: | |||||
| Logger.Warning($"Unknown message type: {e.Type}"); | |||||
| break; | |||||
| } | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| Logger.Error($"Error handling {e.Type} event", ex); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| break; | |||||
| public Task SetStatus(UserStatus status) | |||||
| { | |||||
| if (status == null) throw new ArgumentNullException(nameof(status)); | |||||
| if (status != UserStatus.Online && status != UserStatus.Idle) | |||||
| throw new ArgumentException($"Invalid status, must be {UserStatus.Online} or {UserStatus.Idle}", nameof(status)); | |||||
| CheckReady(); | |||||
| //Ignored | |||||
| case "USER_SETTINGS_UPDATE": | |||||
| case "GUILD_INTEGRATIONS_UPDATE": | |||||
| case "VOICE_SERVER_UPDATE": | |||||
| break; | |||||
| Status = status; | |||||
| return SendStatus(); | |||||
| } | |||||
| public Task SetGame(int? gameId) | |||||
| { | |||||
| CheckReady(); | |||||
| case "RESUMED": //Handled in DataWebSocket | |||||
| break; | |||||
| CurrentGameId = gameId; | |||||
| return SendStatus(); | |||||
| } | |||||
| private Task SendStatus() | |||||
| { | |||||
| GatewaySocket.SendUpdateStatus(Status == UserStatus.Idle ? EpochTime.GetMilliseconds() - (10 * 60 * 1000) : (long?)null, CurrentGameId); | |||||
| return TaskHelper.CompletedTask; | |||||
| } | |||||
| private Server AddServer(ulong id) | |||||
| => _servers.GetOrAdd(id, x => new Server(this, x)); | |||||
| private Server RemoveServer(ulong id) | |||||
| { | |||||
| Server server; | |||||
| _servers.TryRemove(id, out server); | |||||
| return server; | |||||
| } | |||||
| public Server GetServer(ulong id) | |||||
| { | |||||
| Server server; | |||||
| _servers.TryGetValue(id, out server); | |||||
| return server; | |||||
| } | |||||
| private Channel AddChannel(ulong id, ulong? guildId, ulong? recipientId) | |||||
| { | |||||
| Channel channel; | |||||
| if (recipientId != null) | |||||
| { | |||||
| channel = _privateChannels.GetOrAdd(recipientId.Value, | |||||
| x => new Channel(this, x, new User(recipientId.Value, null))); | |||||
| } | |||||
| else | |||||
| { | |||||
| var server = GetServer(guildId.Value); | |||||
| channel = server.AddChannel(id); | |||||
| //Others | |||||
| default: | |||||
| Logger.Warning($"Unknown message type: {e.Type}"); | |||||
| break; | |||||
| } | |||||
| } | } | ||||
| _channels[channel.Id] = channel; | |||||
| return channel; | |||||
| } | |||||
| private Channel RemoveChannel(ulong id) | |||||
| { | |||||
| Channel channel; | |||||
| if (_channels.TryRemove(id, out channel)) | |||||
| catch (Exception ex) | |||||
| { | { | ||||
| if (channel.IsPrivate) | |||||
| _privateChannels.TryRemove(channel.Recipient.Id, out channel); | |||||
| else | |||||
| channel.Server.RemoveChannel(id); | |||||
| Logger.Error($"Error handling {e.Type} event", ex); | |||||
| } | } | ||||
| return channel; | |||||
| } | |||||
| internal Channel GetChannel(ulong id) | |||||
| { | |||||
| Channel channel; | |||||
| _channels.TryGetValue(id, out channel); | |||||
| return channel; | |||||
| } | |||||
| internal Channel GetPrivateChannel(ulong recipientId) | |||||
| { | |||||
| Channel channel; | |||||
| _privateChannels.TryGetValue(recipientId, out channel); | |||||
| return channel; | |||||
| } | } | ||||
| #region Async Wrapper | #region Async Wrapper | ||||
| /// <summary> Blocking call that will not return until client has been stopped. This is mainly intended for use in console applications. </summary> | /// <summary> Blocking call that will not return until client has been stopped. This is mainly intended for use in console applications. </summary> | ||||
| public void Run(Func<Task> asyncAction) | public void Run(Func<Task> asyncAction) | ||||
| @@ -843,19 +924,6 @@ namespace Discord | |||||
| #endregion | #endregion | ||||
| //Helpers | //Helpers | ||||
| private void CheckReady() | |||||
| { | |||||
| switch (State) | |||||
| { | |||||
| case ConnectionState.Disconnecting: | |||||
| throw new InvalidOperationException("The client is disconnecting."); | |||||
| case ConnectionState.Disconnected: | |||||
| throw new InvalidOperationException("The client is not connected to Discord"); | |||||
| case ConnectionState.Connecting: | |||||
| throw new InvalidOperationException("The client is connecting."); | |||||
| } | |||||
| } | |||||
| private string GetTokenCachePath(string email) | private string GetTokenCachePath(string email) | ||||
| { | { | ||||
| using (var md5 = MD5.Create()) | using (var md5 = MD5.Create()) | ||||
| @@ -922,29 +990,7 @@ namespace Discord | |||||
| } | } | ||||
| } | } | ||||
| private static string Base64Image(ImageType type, Stream stream, string existingId) | |||||
| { | |||||
| if (type == ImageType.None) | |||||
| return null; | |||||
| else if (stream != null) | |||||
| { | |||||
| byte[] bytes = new byte[stream.Length - stream.Position]; | |||||
| stream.Read(bytes, 0, bytes.Length); | |||||
| string base64 = Convert.ToBase64String(bytes); | |||||
| string imageType = type == ImageType.Jpeg ? "image/jpeg;base64" : "image/png;base64"; | |||||
| return $"data:{imageType},{base64}"; | |||||
| } | |||||
| return existingId; | |||||
| } | |||||
| public Region GetRegion(string regionName) | |||||
| { | |||||
| Region region; | |||||
| if (_regions.TryGetValue(regionName, out region)) | |||||
| return region; | |||||
| else | |||||
| return _unknownRegion; | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -1,6 +1,9 @@ | |||||
| using System; | using System; | ||||
| using System.Collections.Concurrent; | using System.Collections.Concurrent; | ||||
| using System.Collections.Generic; | |||||
| using System.Globalization; | using System.Globalization; | ||||
| using System.IO; | |||||
| using System.Linq; | |||||
| using System.Runtime.CompilerServices; | using System.Runtime.CompilerServices; | ||||
| namespace Discord | namespace Discord | ||||
| @@ -13,8 +16,10 @@ namespace Discord | |||||
| => ulong.Parse(value, NumberStyles.None, _format); | => ulong.Parse(value, NumberStyles.None, _format); | ||||
| public static ulong? ToNullableId(this string value) | public static ulong? ToNullableId(this string value) | ||||
| => value == null ? (ulong?)null : ulong.Parse(value, NumberStyles.None, _format); | => value == null ? (ulong?)null : ulong.Parse(value, NumberStyles.None, _format); | ||||
| public static string ToIdString(this ulong value) | |||||
| public static bool TryToId(this string value, out ulong result) | |||||
| => ulong.TryParse(value, NumberStyles.None, _format, out result); | |||||
| public static string ToIdString(this ulong value) | |||||
| => value.ToString(_format); | => value.ToString(_format); | ||||
| public static string ToIdString(this ulong? value) | public static string ToIdString(this ulong? value) | ||||
| => value?.ToString(_format); | => value?.ToString(_format); | ||||
| @@ -33,5 +38,100 @@ namespace Discord | |||||
| return true; | return true; | ||||
| } | } | ||||
| } | } | ||||
| public static IEnumerable<Channel> Find(this IEnumerable<Channel> channels, string name, ChannelType type = null, bool exactMatch = false) | |||||
| { | |||||
| //Search by name | |||||
| var query = channels | |||||
| .Where(x => string.Equals(x.Name, name, exactMatch ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)); | |||||
| if (!exactMatch) | |||||
| { | |||||
| if (name.Length >= 2 && name[0] == '<' && name[1] == '#' && name[name.Length - 1] == '>') //Search by raw mention | |||||
| { | |||||
| ulong id; | |||||
| if (name.Substring(2, name.Length - 3).TryToId(out id)) | |||||
| { | |||||
| var channel = channels.Where(x => x.Id == id).FirstOrDefault(); | |||||
| if (channel != null) | |||||
| query = query.Concat(new Channel[] { channel }); | |||||
| } | |||||
| } | |||||
| if (name.Length >= 1 && name[0] == '#' && (type == null || type == ChannelType.Text)) //Search by clean mention | |||||
| { | |||||
| string name2 = name.Substring(1); | |||||
| query = query.Concat(channels | |||||
| .Where(x => x.Type == ChannelType.Text && string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase))); | |||||
| } | |||||
| } | |||||
| if (type != null) | |||||
| query = query.Where(x => x.Type == type); | |||||
| return query; | |||||
| } | |||||
| public static IEnumerable<User> Find(this IEnumerable<User> users, | |||||
| string name, ushort? discriminator = null, bool exactMatch = false) | |||||
| { | |||||
| //Search by name | |||||
| var query = users | |||||
| .Where(x => string.Equals(x.Name, name, exactMatch ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)); | |||||
| if (!exactMatch) | |||||
| { | |||||
| if (name.Length >= 2 && name[0] == '<' && name[1] == '@' && name[name.Length - 1] == '>') //Search by raw mention | |||||
| { | |||||
| ulong id; | |||||
| if (name.Substring(2, name.Length - 3).TryToId(out id)) | |||||
| { | |||||
| var user = users.Where(x => x.Id == id).FirstOrDefault(); | |||||
| if (user != null) | |||||
| query = query.Concat(new User[] { user }); | |||||
| } | |||||
| } | |||||
| if (name.Length >= 1 && name[0] == '@') //Search by clean mention | |||||
| { | |||||
| string name2 = name.Substring(1); | |||||
| query = query.Concat(users | |||||
| .Where(x => string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase))); | |||||
| } | |||||
| } | |||||
| if (discriminator != null) | |||||
| query = query.Where(x => x.Discriminator == discriminator.Value); | |||||
| return query; | |||||
| } | |||||
| public static IEnumerable<Role> Find(this IEnumerable<Role> roles, string name, bool exactMatch = false) | |||||
| { | |||||
| // if (name.StartsWith("@")) | |||||
| // { | |||||
| // string name2 = name.Substring(1); | |||||
| // return _roles.Where(x => x.Server.Id == server.Id && | |||||
| // string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) || | |||||
| // string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase)); | |||||
| // } | |||||
| // else | |||||
| return roles.Where(x => string.Equals(x.Name, name, exactMatch ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)); | |||||
| } | |||||
| public static IEnumerable<Server> Find(this IEnumerable<Server> servers, string name, bool exactMatch = false) | |||||
| => servers.Where(x => string.Equals(x.Name, name, exactMatch ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase)); | |||||
| public static string Base64(this Stream stream, ImageType type, string existingId) | |||||
| { | |||||
| if (type == ImageType.None) | |||||
| return null; | |||||
| else if (stream != null) | |||||
| { | |||||
| byte[] bytes = new byte[stream.Length - stream.Position]; | |||||
| stream.Read(bytes, 0, bytes.Length); | |||||
| string base64 = Convert.ToBase64String(bytes); | |||||
| string imageType = type == ImageType.Jpeg ? "image/jpeg;base64" : "image/png;base64"; | |||||
| return $"data:{imageType},{base64}"; | |||||
| } | |||||
| return existingId; | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -0,0 +1,7 @@ | |||||
| namespace Discord | |||||
| { | |||||
| public interface IService | |||||
| { | |||||
| void Install(DiscordClient client); | |||||
| } | |||||
| } | |||||
| @@ -1,95 +0,0 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Text.RegularExpressions; | |||||
| namespace Discord | |||||
| { | |||||
| public static class Mention | |||||
| { | |||||
| private static readonly Regex _userRegex = new Regex(@"<@([0-9]+)>", RegexOptions.Compiled); | |||||
| private static readonly Regex _channelRegex = new Regex(@"<#([0-9]+)>", RegexOptions.Compiled); | |||||
| private static readonly Regex _roleRegex = new Regex(@"@everyone", RegexOptions.Compiled); | |||||
| /// <summary> Returns the string used to create a user mention. </summary> | |||||
| [Obsolete("Use User.Mention instead")] | |||||
| public static string User(User user) | |||||
| => $"<@{user.Id}>"; | |||||
| /// <summary> Returns the string used to create a channel mention. </summary> | |||||
| [Obsolete("Use Channel.Mention instead")] | |||||
| public static string Channel(Channel channel) | |||||
| => $"<#{channel.Id}>"; | |||||
| /// <summary> Returns the string used to create a mention to everyone in a channel. </summary> | |||||
| [Obsolete("Use Server.EveryoneRole.Mention instead")] | |||||
| public static string Everyone() | |||||
| => $"@everyone"; | |||||
| internal static string CleanUserMentions(DiscordClient client, Channel channel, string text, List<User> users = null) | |||||
| { | |||||
| return _userRegex.Replace(text, new MatchEvaluator(e => | |||||
| { | |||||
| var id = e.Value.Substring(2, e.Value.Length - 3).ToId(); | |||||
| var user = channel.GetUser(id); | |||||
| if (user != null) | |||||
| { | |||||
| if (users != null) | |||||
| users.Add(user); | |||||
| return '@' + user.Name; | |||||
| } | |||||
| else //User not found | |||||
| return '@' + e.Value; | |||||
| })); | |||||
| } | |||||
| internal static string CleanChannelMentions(DiscordClient client, Channel channel, string text, List<Channel> channels = null) | |||||
| { | |||||
| var server = channel.Server; | |||||
| if (server == null) return text; | |||||
| return _channelRegex.Replace(text, new MatchEvaluator(e => | |||||
| { | |||||
| var id = e.Value.Substring(2, e.Value.Length - 3).ToId(); | |||||
| var mentionedChannel = server.GetChannel(id); | |||||
| if (mentionedChannel != null && mentionedChannel.Server.Id == server.Id) | |||||
| { | |||||
| if (channels != null) | |||||
| channels.Add(mentionedChannel); | |||||
| return '#' + mentionedChannel.Name; | |||||
| } | |||||
| else //Channel not found | |||||
| return '#' + e.Value; | |||||
| })); | |||||
| } | |||||
| /*internal static string CleanRoleMentions(DiscordClient client, User user, Channel channel, string text, List<Role> roles = null) | |||||
| { | |||||
| var server = channel.Server; | |||||
| if (server == null) return text; | |||||
| return _roleRegex.Replace(text, new MatchEvaluator(e => | |||||
| { | |||||
| if (roles != null && user.GetPermissions(channel).MentionEveryone) | |||||
| roles.Add(server.EveryoneRole); | |||||
| return e.Value; | |||||
| })); | |||||
| }*/ | |||||
| /// <summary>Resolves all mentions in a provided string to those users, channels or roles' names.</summary> | |||||
| public static string Resolve(Message source, string text) | |||||
| { | |||||
| if (source == null) throw new ArgumentNullException(nameof(source)); | |||||
| if (text == null) throw new ArgumentNullException(nameof(text)); | |||||
| return Resolve(source.Channel, text); | |||||
| } | |||||
| /// <summary>Resolves all mentions in a provided string to those users, channels or roles' names.</summary> | |||||
| public static string Resolve(Channel channel, string text) | |||||
| { | |||||
| if (text == null) throw new ArgumentNullException(nameof(text)); | |||||
| var client = channel.Client; | |||||
| text = CleanUserMentions(client, channel, text); | |||||
| text = CleanChannelMentions(client, channel, text); | |||||
| //text = CleanRoleMentions(_client, channel, text); | |||||
| return text; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -1,8 +1,13 @@ | |||||
| using Discord.API.Client; | using Discord.API.Client; | ||||
| using Discord.API.Client.Rest; | |||||
| using Discord.Net; | |||||
| using System; | using System; | ||||
| using System.Collections.Concurrent; | using System.Collections.Concurrent; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.IO; | |||||
| using System.Linq; | using System.Linq; | ||||
| using System.Net; | |||||
| using System.Threading.Tasks; | |||||
| using APIChannel = Discord.API.Client.Channel; | using APIChannel = Discord.API.Client.Channel; | ||||
| namespace Discord | namespace Discord | ||||
| @@ -38,9 +43,9 @@ namespace Discord | |||||
| private readonly ConcurrentDictionary<ulong, Member> _users; | private readonly ConcurrentDictionary<ulong, Member> _users; | ||||
| private readonly ConcurrentDictionary<ulong, Message> _messages; | private readonly ConcurrentDictionary<ulong, Message> _messages; | ||||
| private Dictionary<ulong, PermissionOverwrite> _permissionOverwrites; | private Dictionary<ulong, PermissionOverwrite> _permissionOverwrites; | ||||
| /// <summary> Gets the client that generated this channel object. </summary> | |||||
| internal DiscordClient Client { get; } | internal DiscordClient Client { get; } | ||||
| /// <summary> Gets the unique identifier for this channel. </summary> | /// <summary> Gets the unique identifier for this channel. </summary> | ||||
| public ulong Id { get; } | public ulong Id { get; } | ||||
| /// <summary> Gets the server owning this channel, if this is a public chat. </summary> | /// <summary> Gets the server owning this channel, if this is a public chat. </summary> | ||||
| @@ -149,32 +154,85 @@ namespace Discord | |||||
| } | } | ||||
| } | } | ||||
| //Members | |||||
| internal void AddUser(User user) | |||||
| /// <summary> Edits this channel, changing only non-null attributes. </summary> | |||||
| public async Task Edit(string name = null, string topic = null, int? position = null) | |||||
| { | { | ||||
| if (!Client.Config.UsePermissionsCache) | |||||
| return; | |||||
| if (name != null || topic != null) | |||||
| { | |||||
| var request = new UpdateChannelRequest(Id) | |||||
| { | |||||
| Name = name ?? Name, | |||||
| Topic = topic ?? Topic, | |||||
| Position = Position | |||||
| }; | |||||
| await Client.ClientAPI.Send(request).ConfigureAwait(false); | |||||
| } | |||||
| var member = new Member(user); | |||||
| if (_users.TryAdd(user.Id, member)) | |||||
| UpdatePermissions(user, member.Permissions); | |||||
| if (position != null) | |||||
| { | |||||
| Channel[] channels = Server.Channels.Where(x => x.Type == Type).OrderBy(x => x.Position).ToArray(); | |||||
| int oldPos = Array.IndexOf(channels, this); | |||||
| var newPosChannel = channels.Where(x => x.Position > position).FirstOrDefault(); | |||||
| int newPos = (newPosChannel != null ? Array.IndexOf(channels, newPosChannel) : channels.Length) - 1; | |||||
| if (newPos < 0) | |||||
| newPos = 0; | |||||
| int minPos; | |||||
| if (oldPos < newPos) //Moving Down | |||||
| { | |||||
| minPos = oldPos; | |||||
| for (int i = oldPos; i < newPos; i++) | |||||
| channels[i] = channels[i + 1]; | |||||
| channels[newPos] = this; | |||||
| } | |||||
| else //(oldPos > newPos) Moving Up | |||||
| { | |||||
| minPos = newPos; | |||||
| for (int i = oldPos; i > newPos; i--) | |||||
| channels[i] = channels[i - 1]; | |||||
| channels[newPos] = this; | |||||
| } | |||||
| Channel after = minPos > 0 ? channels.Skip(minPos - 1).FirstOrDefault() : null; | |||||
| await Server.ReorderChannels(channels.Skip(minPos), after).ConfigureAwait(false); | |||||
| } | |||||
| } | } | ||||
| internal void RemoveUser(ulong id) | |||||
| public async Task Delete() | |||||
| { | { | ||||
| if (!Client.Config.UsePermissionsCache) | |||||
| return; | |||||
| Member ignored; | |||||
| _users.TryRemove(id, out ignored); | |||||
| try { await Client.ClientAPI.Send(new DeleteChannelRequest(Id)).ConfigureAwait(false); } | |||||
| catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } | |||||
| } | } | ||||
| public User GetUser(ulong id) | |||||
| #region Invites | |||||
| /// <summary> Gets all active (non-expired) invites to this server. </summary> | |||||
| public async Task<IEnumerable<Invite>> GetInvites() | |||||
| => (await Server.GetInvites()).Where(x => x.Channel.Id == Id); | |||||
| /// <summary> Creates a new invite to this channel. </summary> | |||||
| /// <param name="maxAge"> Time (in seconds) until the invite expires. Set to null to never expire. </param> | |||||
| /// <param name="maxUses"> The max amount of times this invite may be used. Set to null to have unlimited uses. </param> | |||||
| /// <param name="tempMembership"> If true, a user accepting this invite will be kicked from the server after closing their client. </param> | |||||
| /// <param name="withXkcd"> If true, creates a human-readable link. Not supported if maxAge is set to null. </param> | |||||
| public async Task<Invite> CreateInvite(int? maxAge = 1800, int? maxUses = null, bool tempMembership = false, bool withXkcd = false) | |||||
| { | { | ||||
| Member result; | |||||
| _users.TryGetValue(id, out result); | |||||
| return result.User; | |||||
| if (maxAge < 0) throw new ArgumentOutOfRangeException(nameof(maxAge)); | |||||
| if (maxUses < 0) throw new ArgumentOutOfRangeException(nameof(maxUses)); | |||||
| var request = new CreateInviteRequest(Id) | |||||
| { | |||||
| MaxAge = maxAge ?? 0, | |||||
| MaxUses = maxUses ?? 0, | |||||
| IsTemporary = tempMembership, | |||||
| WithXkcdPass = withXkcd | |||||
| }; | |||||
| var response = await Client.ClientAPI.Send(request).ConfigureAwait(false); | |||||
| var invite = new Invite(Client, response.Code, response.XkcdPass); | |||||
| return invite; | |||||
| } | } | ||||
| #endregion | |||||
| //Messages | |||||
| #region Messages | |||||
| internal Message AddMessage(ulong id, ulong userId, DateTime timestamp) | internal Message AddMessage(ulong id, ulong userId, DateTime timestamp) | ||||
| { | { | ||||
| Message message = new Message(id, this, userId); | Message message = new Message(id, this, userId); | ||||
| @@ -202,14 +260,117 @@ namespace Discord | |||||
| } | } | ||||
| return null; | return null; | ||||
| } | } | ||||
| public Message GetMessage(ulong id) | public Message GetMessage(ulong id) | ||||
| { | { | ||||
| Message result; | Message result; | ||||
| _messages.TryGetValue(id, out result); | _messages.TryGetValue(id, out result); | ||||
| return result; | return result; | ||||
| } | } | ||||
| public async Task<Message[]> DownloadMessages(int limit = 100, ulong? relativeMessageId = null, | |||||
| RelativeDirection relativeDir = RelativeDirection.Before, bool useCache = true) | |||||
| { | |||||
| if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit)); | |||||
| if (limit == 0 || Type != ChannelType.Text) return new Message[0]; | |||||
| try | |||||
| { | |||||
| var request = new GetMessagesRequest(Id) | |||||
| { | |||||
| Limit = limit, | |||||
| RelativeDir = relativeMessageId.HasValue ? relativeDir == RelativeDirection.Before ? "before" : "after" : null, | |||||
| RelativeId = relativeMessageId ?? 0 | |||||
| }; | |||||
| var msgs = await Client.ClientAPI.Send(request).ConfigureAwait(false); | |||||
| return msgs.Select(x => | |||||
| { | |||||
| Message msg = null; | |||||
| if (useCache) | |||||
| { | |||||
| msg = AddMessage(x.Id, x.Author.Id, x.Timestamp.Value); | |||||
| var user = msg.User; | |||||
| if (user != null) | |||||
| user.UpdateActivity(msg.EditedTimestamp ?? msg.Timestamp); | |||||
| } | |||||
| else | |||||
| msg = new Message(x.Id, this, x.Author.Id); | |||||
| msg.Update(x); | |||||
| return msg; | |||||
| }) | |||||
| .ToArray(); | |||||
| } | |||||
| catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.Forbidden) | |||||
| { | |||||
| return new Message[0]; | |||||
| } | |||||
| } | |||||
| /// <summary> Returns all members of this channel with the specified name. </summary> | |||||
| /// <remarks> Name formats supported: Name, @Name and <@Id>. Search is case-insensitive if exactMatch is false.</remarks> | |||||
| public IEnumerable<User> FindUsers(string name, bool exactMatch = false) | |||||
| { | |||||
| if (name == null) throw new ArgumentNullException(nameof(name)); | |||||
| return _users.Select(x => x.Value.User).Find(name, exactMatch: exactMatch); | |||||
| } | |||||
| public Task<Message> SendMessage(string text) | |||||
| { | |||||
| if (text == null) throw new ArgumentNullException(nameof(text)); | |||||
| return SendMessageInternal(text, false); | |||||
| } | |||||
| public Task<Message> SendTTSMessage(string text) | |||||
| { | |||||
| if (text == null) throw new ArgumentNullException(nameof(text)); | |||||
| return SendMessageInternal(text, true); | |||||
| } | |||||
| private async Task<Message> SendMessageInternal(string text, bool isTTS) | |||||
| { | |||||
| Message msg = null; | |||||
| var mentionedUsers = new List<User>(); | |||||
| text = Message.CleanUserMentions(this, text, mentionedUsers); | |||||
| if (text.Length > DiscordConfig.MaxMessageSize) | |||||
| throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {DiscordConfig.MaxMessageSize} characters or less."); | |||||
| if (Client.Config.UseMessageQueue) | |||||
| Client.MessageQueue.QueueSend(Id, text, mentionedUsers.Select(x => x.Id).ToArray(), isTTS); | |||||
| else | |||||
| { | |||||
| var request = new SendMessageRequest(Id) | |||||
| { | |||||
| Content = text, | |||||
| MentionedUserIds = mentionedUsers.Select(x => x.Id).ToArray(), | |||||
| Nonce = null, | |||||
| IsTTS = isTTS | |||||
| }; | |||||
| var model = await Client.ClientAPI.Send(request).ConfigureAwait(false); | |||||
| msg = AddMessage(model.Id, model.Author.Id, model.Timestamp.Value); | |||||
| msg.Update(model); | |||||
| } | |||||
| return msg; | |||||
| } | |||||
| //Permissions | |||||
| public Task<Message> SendFile(string filePath) | |||||
| => SendFile(Path.GetFileName(filePath), File.OpenRead(filePath)); | |||||
| public async Task<Message> SendFile(string filename, Stream stream) | |||||
| { | |||||
| if (filename == null) throw new ArgumentNullException(nameof(filename)); | |||||
| if (stream == null) throw new ArgumentNullException(nameof(stream)); | |||||
| var request = new SendFileRequest(Id) | |||||
| { | |||||
| Filename = filename, | |||||
| Stream = stream | |||||
| }; | |||||
| var model = await Client.ClientAPI.Send(request).ConfigureAwait(false); | |||||
| var msg = AddMessage(model.Id, model.Author.Id, model.Timestamp.Value); | |||||
| msg.Update(model); | |||||
| return msg; | |||||
| } | |||||
| #endregion | |||||
| #region Permissions | |||||
| internal void UpdatePermissions() | internal void UpdatePermissions() | ||||
| { | { | ||||
| if (!Client.Config.UsePermissionsCache) | if (!Client.Config.UsePermissionsCache) | ||||
| @@ -291,6 +452,116 @@ namespace Discord | |||||
| } | } | ||||
| } | } | ||||
| [Obsolete("Use Channel.GetPermissions")] | |||||
| public DualChannelPermissions GetPermissionsRule(User user) | |||||
| { | |||||
| if (user == null) throw new ArgumentNullException(nameof(user)); | |||||
| return PermissionOverwrites | |||||
| .Where(x => x.TargetType == PermissionTarget.User && x.TargetId == user.Id) | |||||
| .Select(x => x.Permissions) | |||||
| .FirstOrDefault(); | |||||
| } | |||||
| [Obsolete("Use Channel.GetPermissions")] | |||||
| public DualChannelPermissions GetPermissionsRule(Role role) | |||||
| { | |||||
| if (role == null) throw new ArgumentNullException(nameof(role)); | |||||
| return PermissionOverwrites | |||||
| .Where(x => x.TargetType == PermissionTarget.Role && x.TargetId == role.Id) | |||||
| .Select(x => x.Permissions) | |||||
| .FirstOrDefault(); | |||||
| } | |||||
| [Obsolete("Use Channel.SetPermissions")] | |||||
| public Task AddPermissionsRule(User user, ChannelPermissions allow = null, ChannelPermissions deny = null) | |||||
| { | |||||
| if (user == null) throw new ArgumentNullException(nameof(user)); | |||||
| return AddPermissionsRule(user.Id, PermissionTarget.User, allow, deny); | |||||
| } | |||||
| [Obsolete("Use Channel.SetPermissions")] | |||||
| public Task AddPermissionsRule(User user, DualChannelPermissions permissions = null) | |||||
| { | |||||
| if (user == null) throw new ArgumentNullException(nameof(user)); | |||||
| return AddPermissionsRule(user.Id, PermissionTarget.User, permissions?.Allow, permissions?.Deny); | |||||
| } | |||||
| [Obsolete("Use Channel.SetPermissions")] | |||||
| public Task AddPermissionsRule(Role role, ChannelPermissions allow = null, ChannelPermissions deny = null) | |||||
| { | |||||
| if (role == null) throw new ArgumentNullException(nameof(role)); | |||||
| return AddPermissionsRule(role.Id, PermissionTarget.Role, allow, deny); | |||||
| } | |||||
| [Obsolete("Use Channel.SetPermissions")] | |||||
| public Task AddPermissionsRule(Role role, DualChannelPermissions permissions = null) | |||||
| { | |||||
| if (role == null) throw new ArgumentNullException(nameof(role)); | |||||
| return AddPermissionsRule(role.Id, PermissionTarget.Role, permissions?.Allow, permissions?.Deny); | |||||
| } | |||||
| private Task AddPermissionsRule(ulong targetId, PermissionTarget targetType, ChannelPermissions allow = null, ChannelPermissions deny = null) | |||||
| { | |||||
| var request = new AddChannelPermissionsRequest(Id) | |||||
| { | |||||
| TargetId = targetId, | |||||
| TargetType = targetType.Value, | |||||
| Allow = allow?.RawValue ?? 0, | |||||
| Deny = deny?.RawValue ?? 0 | |||||
| }; | |||||
| return Client.ClientAPI.Send(request); | |||||
| } | |||||
| [Obsolete("Use Channel.RemovePermissions")] | |||||
| public Task RemovePermissionsRule(User user) | |||||
| { | |||||
| if (user == null) throw new ArgumentNullException(nameof(user)); | |||||
| return RemovePermissionsRule(user.Id, PermissionTarget.User); | |||||
| } | |||||
| [Obsolete("Use Channel.RemovePermissions")] | |||||
| public Task RemovePermissionsRule(Role role) | |||||
| { | |||||
| if (role == null) throw new ArgumentNullException(nameof(role)); | |||||
| return RemovePermissionsRule(role.Id, PermissionTarget.Role); | |||||
| } | |||||
| private async Task RemovePermissionsRule(ulong userOrRoleId, PermissionTarget targetType) | |||||
| { | |||||
| try | |||||
| { | |||||
| var perms = PermissionOverwrites.Where(x => x.TargetType != targetType || x.TargetId != userOrRoleId).FirstOrDefault(); | |||||
| await Client.ClientAPI.Send(new RemoveChannelPermissionsRequest(Id, userOrRoleId)).ConfigureAwait(false); | |||||
| } | |||||
| catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } | |||||
| } | |||||
| #endregion | |||||
| #region Users | |||||
| internal void AddUser(User user) | |||||
| { | |||||
| if (!Client.Config.UsePermissionsCache) | |||||
| return; | |||||
| var member = new Member(user); | |||||
| if (_users.TryAdd(user.Id, member)) | |||||
| UpdatePermissions(user, member.Permissions); | |||||
| } | |||||
| internal void RemoveUser(ulong id) | |||||
| { | |||||
| if (!Client.Config.UsePermissionsCache) | |||||
| return; | |||||
| Member ignored; | |||||
| _users.TryRemove(id, out ignored); | |||||
| } | |||||
| public User GetUser(ulong id) | |||||
| { | |||||
| Member result; | |||||
| _users.TryGetValue(id, out result); | |||||
| return result.User; | |||||
| } | |||||
| #endregion | |||||
| public override bool Equals(object obj) => obj is Channel && (obj as Channel).Id == Id; | public override bool Equals(object obj) => obj is Channel && (obj as Channel).Id == Id; | ||||
| public override int GetHashCode() => unchecked(Id.GetHashCode() + 5658); | public override int GetHashCode() => unchecked(Id.GetHashCode() + 5658); | ||||
| public override string ToString() => Name ?? Id.ToIdString(); | public override string ToString() => Name ?? Id.ToIdString(); | ||||
| @@ -1,5 +1,9 @@ | |||||
| using Discord.API.Client; | using Discord.API.Client; | ||||
| using Discord.API.Client.Rest; | |||||
| using Discord.Net; | |||||
| using System; | using System; | ||||
| using System.Net; | |||||
| using System.Threading.Tasks; | |||||
| using APIInvite = Discord.API.Client.Invite; | using APIInvite = Discord.API.Client.Invite; | ||||
| namespace Discord | namespace Discord | ||||
| @@ -55,6 +59,10 @@ namespace Discord | |||||
| } | } | ||||
| } | } | ||||
| private ulong _serverId, _channelId; | |||||
| internal DiscordClient Client { get; } | |||||
| /// <summary> Gets the unique code for this invite. </summary> | /// <summary> Gets the unique code for this invite. </summary> | ||||
| public string Code { get; } | public string Code { get; } | ||||
| /// <summary> Gets, if enabled, an alternative human-readable invite code. </summary> | /// <summary> Gets, if enabled, an alternative human-readable invite code. </summary> | ||||
| @@ -63,9 +71,9 @@ namespace Discord | |||||
| /// <summary> Gets information about the server this invite is attached to. </summary> | /// <summary> Gets information about the server this invite is attached to. </summary> | ||||
| public ServerInfo Server { get; private set; } | public ServerInfo Server { get; private set; } | ||||
| /// <summary> Gets information about the channel this invite is attached to. </summary> | /// <summary> Gets information about the channel this invite is attached to. </summary> | ||||
| public ChannelInfo Channel { get; private set; } | |||||
| /// <summary> Gets the time (in seconds) until the invite expires. </summary> | |||||
| public int? MaxAge { get; private set; } | |||||
| public ChannelInfo Channel { get; private set; } | |||||
| /// <summary> Gets the time (in seconds) until the invite expires. </summary> | |||||
| public int? MaxAge { get; private set; } | |||||
| /// <summary> Gets the amount of times this invite has been used. </summary> | /// <summary> Gets the amount of times this invite has been used. </summary> | ||||
| public int Uses { get; private set; } | public int Uses { get; private set; } | ||||
| /// <summary> Gets the max amount of times this invite may be used. </summary> | /// <summary> Gets the max amount of times this invite may be used. </summary> | ||||
| @@ -80,8 +88,9 @@ namespace Discord | |||||
| /// <summary> Returns a URL for this invite using XkcdCode if available or Id if not. </summary> | /// <summary> Returns a URL for this invite using XkcdCode if available or Id if not. </summary> | ||||
| public string Url => $"{DiscordConfig.InviteUrl}/{Code}"; | public string Url => $"{DiscordConfig.InviteUrl}/{Code}"; | ||||
| internal Invite(string code, string xkcdPass) | |||||
| internal Invite(DiscordClient client, string code, string xkcdPass) | |||||
| { | { | ||||
| Client = client; | |||||
| Code = code; | Code = code; | ||||
| XkcdCode = xkcdPass; | XkcdCode = xkcdPass; | ||||
| } | } | ||||
| @@ -110,8 +119,16 @@ namespace Discord | |||||
| if (model.CreatedAt != null) | if (model.CreatedAt != null) | ||||
| CreatedAt = model.CreatedAt.Value; | CreatedAt = model.CreatedAt.Value; | ||||
| } | } | ||||
| public async Task Delete() | |||||
| { | |||||
| try { await Client.ClientAPI.Send(new DeleteInviteRequest(Code)).ConfigureAwait(false); } | |||||
| catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } | |||||
| } | |||||
| public Task Accept() | |||||
| => Client.ClientAPI.Send(new AcceptInviteRequest(Code)); | |||||
| public override bool Equals(object obj) => obj is Invite && (obj as Invite).Code == Code; | |||||
| public override bool Equals(object obj) => obj is Invite && (obj as Invite).Code == Code; | |||||
| public override int GetHashCode() => unchecked(Code.GetHashCode() + 9980); | public override int GetHashCode() => unchecked(Code.GetHashCode() + 9980); | ||||
| public override string ToString() => XkcdCode ?? Code; | public override string ToString() => XkcdCode ?? Code; | ||||
| } | } | ||||
| @@ -1,9 +1,14 @@ | |||||
| using Newtonsoft.Json; | |||||
| using Discord.API.Client.Rest; | |||||
| using Discord.Net; | |||||
| using Newtonsoft.Json; | |||||
| using Newtonsoft.Json.Serialization; | using Newtonsoft.Json.Serialization; | ||||
| using System; | using System; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Linq; | using System.Linq; | ||||
| using System.Net; | |||||
| using System.Reflection; | using System.Reflection; | ||||
| using System.Text.RegularExpressions; | |||||
| using System.Threading.Tasks; | |||||
| using APIMessage = Discord.API.Client.Message; | using APIMessage = Discord.API.Client.Message; | ||||
| namespace Discord | namespace Discord | ||||
| @@ -16,8 +21,73 @@ namespace Discord | |||||
| } | } | ||||
| public sealed class Message | public sealed class Message | ||||
| { | |||||
| /*internal class ImportResolver : DefaultContractResolver | |||||
| { | |||||
| private static readonly Regex _userRegex = new Regex(@"<@([0-9]+)>", RegexOptions.Compiled); | |||||
| private static readonly Regex _channelRegex = new Regex(@"<#([0-9]+)>", RegexOptions.Compiled); | |||||
| private static readonly Regex _roleRegex = new Regex(@"@everyone", RegexOptions.Compiled); | |||||
| private static readonly Attachment[] _initialAttachments = new Attachment[0]; | |||||
| private static readonly Embed[] _initialEmbeds = new Embed[0]; | |||||
| internal static string CleanUserMentions(Channel channel, string text, List<User> users = null) | |||||
| { | |||||
| return _userRegex.Replace(text, new MatchEvaluator(e => | |||||
| { | |||||
| var id = e.Value.Substring(2, e.Value.Length - 3).ToId(); | |||||
| var user = channel.GetUser(id); | |||||
| if (user != null) | |||||
| { | |||||
| if (users != null) | |||||
| users.Add(user); | |||||
| return '@' + user.Name; | |||||
| } | |||||
| else //User not found | |||||
| return '@' + e.Value; | |||||
| })); | |||||
| } | |||||
| internal static string CleanChannelMentions(Channel channel, string text, List<Channel> channels = null) | |||||
| { | |||||
| var server = channel.Server; | |||||
| if (server == null) return text; | |||||
| return _channelRegex.Replace(text, new MatchEvaluator(e => | |||||
| { | |||||
| var id = e.Value.Substring(2, e.Value.Length - 3).ToId(); | |||||
| var mentionedChannel = server.GetChannel(id); | |||||
| if (mentionedChannel != null && mentionedChannel.Server.Id == server.Id) | |||||
| { | |||||
| if (channels != null) | |||||
| channels.Add(mentionedChannel); | |||||
| return '#' + mentionedChannel.Name; | |||||
| } | |||||
| else //Channel not found | |||||
| return '#' + e.Value; | |||||
| })); | |||||
| } | |||||
| /*internal static string CleanRoleMentions(User user, Channel channel, string text, List<Role> roles = null) | |||||
| { | |||||
| var server = channel.Server; | |||||
| if (server == null) return text; | |||||
| return _roleRegex.Replace(text, new MatchEvaluator(e => | |||||
| { | |||||
| if (roles != null && user.GetPermissions(channel).MentionEveryone) | |||||
| roles.Add(server.EveryoneRole); | |||||
| return e.Value; | |||||
| })); | |||||
| }*/ | |||||
| private static string Resolve(Channel channel, string text) | |||||
| { | |||||
| if (text == null) throw new ArgumentNullException(nameof(text)); | |||||
| var client = channel.Client; | |||||
| text = CleanUserMentions(channel, text); | |||||
| text = CleanChannelMentions(channel, text); | |||||
| //text = CleanRoleMentions(Channel, text); | |||||
| return text; | |||||
| } | |||||
| /*internal class ImportResolver : DefaultContractResolver | |||||
| { | { | ||||
| protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) | protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) | ||||
| { | { | ||||
| @@ -33,7 +103,7 @@ namespace Discord | |||||
| } | } | ||||
| }*/ | }*/ | ||||
| public sealed class Attachment : File | |||||
| public sealed class Attachment : File | |||||
| { | { | ||||
| /// <summary> Unique identifier for this file. </summary> | /// <summary> Unique identifier for this file. </summary> | ||||
| public string Id { get; internal set; } | public string Id { get; internal set; } | ||||
| @@ -89,11 +159,10 @@ namespace Discord | |||||
| internal File() { } | internal File() { } | ||||
| } | } | ||||
| private static readonly Attachment[] _initialAttachments = new Attachment[0]; | |||||
| private static readonly Embed[] _initialEmbeds = new Embed[0]; | |||||
| private readonly ulong _userId; | private readonly ulong _userId; | ||||
| internal DiscordClient Client => Channel.Client; | |||||
| /// <summary> Returns the unique identifier for this message. </summary> | /// <summary> Returns the unique identifier for this message. </summary> | ||||
| public ulong Id { get; } | public ulong Id { get; } | ||||
| /// <summary> Returns the channel this message was sent to. </summary> | /// <summary> Returns the channel this message was sent to. </summary> | ||||
| @@ -117,7 +186,7 @@ namespace Discord | |||||
| public Attachment[] Attachments { get; private set; } | public Attachment[] Attachments { get; private set; } | ||||
| /// <summary> Returns a collection of all embeded content in this message. </summary> | /// <summary> Returns a collection of all embeded content in this message. </summary> | ||||
| public Embed[] Embeds { get; private set; } | public Embed[] Embeds { get; private set; } | ||||
| /// <summary> Returns a collection of all users mentioned in this message. </summary> | /// <summary> Returns a collection of all users mentioned in this message. </summary> | ||||
| public IEnumerable<User> MentionedUsers { get; internal set; } | public IEnumerable<User> MentionedUsers { get; internal set; } | ||||
| /// <summary> Returns a collection of all channels mentioned in this message. </summary> | /// <summary> Returns a collection of all channels mentioned in this message. </summary> | ||||
| @@ -210,11 +279,11 @@ namespace Discord | |||||
| //var mentionedUsers = new List<User>(); | //var mentionedUsers = new List<User>(); | ||||
| var mentionedChannels = new List<Channel>(); | var mentionedChannels = new List<Channel>(); | ||||
| //var mentionedRoles = new List<Role>(); | //var mentionedRoles = new List<Role>(); | ||||
| text = Mention.CleanUserMentions(Channel.Client, channel, text/*, mentionedUsers*/); | |||||
| text = CleanUserMentions(Channel, text/*, mentionedUsers*/); | |||||
| if (server != null) | if (server != null) | ||||
| { | { | ||||
| text = Mention.CleanChannelMentions(Channel.Client, channel, text, mentionedChannels); | |||||
| //text = Mention.CleanRoleMentions(_client, User, channel, text, mentionedRoles); | |||||
| text = CleanChannelMentions(Channel, text, mentionedChannels); | |||||
| //text = CleanRoleMentions(_client, User, channel, text, mentionedRoles); | |||||
| } | } | ||||
| Text = text; | Text = text; | ||||
| @@ -236,7 +305,54 @@ namespace Discord | |||||
| } | } | ||||
| } | } | ||||
| public override bool Equals(object obj) => obj is Message && (obj as Message).Id == Id; | |||||
| public async Task Edit(string text) | |||||
| { | |||||
| if (text == null) throw new ArgumentNullException(nameof(text)); | |||||
| var channel = Channel; | |||||
| var mentionedUsers = new List<User>(); | |||||
| if (!channel.IsPrivate) | |||||
| text = CleanUserMentions(channel, text, mentionedUsers); | |||||
| if (text.Length > DiscordConfig.MaxMessageSize) | |||||
| throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {DiscordConfig.MaxMessageSize} characters or less."); | |||||
| if (Client.Config.UseMessageQueue) | |||||
| Client.MessageQueue.QueueEdit(channel.Id, Id, text, mentionedUsers.Select(x => x.Id).ToArray()); | |||||
| else | |||||
| { | |||||
| var request = new UpdateMessageRequest(Channel.Id, Id) | |||||
| { | |||||
| Content = text, | |||||
| MentionedUserIds = mentionedUsers.Select(x => x.Id).ToArray() | |||||
| }; | |||||
| await Client.ClientAPI.Send(request).ConfigureAwait(false); | |||||
| } | |||||
| } | |||||
| public async Task Delete() | |||||
| { | |||||
| var request = new DeleteMessageRequest(Channel.Id, Id); | |||||
| try { await Client.ClientAPI.Send(request).ConfigureAwait(false); } | |||||
| catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } | |||||
| } | |||||
| public Task Acknowledge() | |||||
| { | |||||
| if (_userId != Client.CurrentUser.Id) | |||||
| return Client.ClientAPI.Send(new AckMessageRequest(Channel.Id, Id)); | |||||
| else | |||||
| return TaskHelper.CompletedTask; | |||||
| } | |||||
| /// <summary>Resolves all mentions in a provided string to those users, channels or roles' names.</summary> | |||||
| public string Resolve(string text) | |||||
| { | |||||
| if (text == null) throw new ArgumentNullException(nameof(text)); | |||||
| return Resolve(Channel, text); | |||||
| } | |||||
| public override bool Equals(object obj) => obj is Message && (obj as Message).Id == Id; | |||||
| public override int GetHashCode() => unchecked(Id.GetHashCode() + 9979); | public override int GetHashCode() => unchecked(Id.GetHashCode() + 9979); | ||||
| public override string ToString() => $"{User}: {RawText}"; | public override string ToString() => $"{User}: {RawText}"; | ||||
| } | } | ||||
| @@ -6,8 +6,8 @@ namespace Discord | |||||
| { | { | ||||
| //General | //General | ||||
| CreateInstantInvite = 0, | CreateInstantInvite = 0, | ||||
| BanMembers = 1, | |||||
| KickMembers = 2, | |||||
| KickMembers = 1, | |||||
| BanMembers = 2, | |||||
| ManageRolesOrPermissions = 3, | ManageRolesOrPermissions = 3, | ||||
| ManageChannel = 4, | ManageChannel = 4, | ||||
| ManageServer = 5, | ManageServer = 5, | ||||
| @@ -1,10 +1,15 @@ | |||||
| using Newtonsoft.Json; | |||||
| using Discord.API.Client.Rest; | |||||
| using System; | |||||
| using System.IO; | |||||
| using System.Threading.Tasks; | |||||
| using APIUser = Discord.API.Client.User; | using APIUser = Discord.API.Client.User; | ||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| public sealed class Profile | public sealed class Profile | ||||
| { | { | ||||
| internal DiscordClient Client { get; } | |||||
| /// <summary> Gets the unique identifier for this user. </summary> | /// <summary> Gets the unique identifier for this user. </summary> | ||||
| public ulong Id { get; private set; } | public ulong Id { get; private set; } | ||||
| /// <summary> Gets the email for this user. </summary> | /// <summary> Gets the email for this user. </summary> | ||||
| @@ -12,7 +17,10 @@ namespace Discord | |||||
| /// <summary> Gets if the email for this user has been verified. </summary> | /// <summary> Gets if the email for this user has been verified. </summary> | ||||
| public bool? IsVerified { get; private set; } | public bool? IsVerified { get; private set; } | ||||
| internal Profile() { } | |||||
| internal Profile(DiscordClient client) | |||||
| { | |||||
| Client = client; | |||||
| } | |||||
| internal void Update(APIUser model) | internal void Update(APIUser model) | ||||
| { | { | ||||
| @@ -21,6 +29,36 @@ namespace Discord | |||||
| IsVerified = model.IsVerified; | IsVerified = model.IsVerified; | ||||
| } | } | ||||
| public async Task Edit(string currentPassword = "", | |||||
| string username = null, string email = null, string password = null, | |||||
| Stream avatar = null, ImageType avatarType = ImageType.Png) | |||||
| { | |||||
| if (currentPassword == null) throw new ArgumentNullException(nameof(currentPassword)); | |||||
| var request = new UpdateProfileRequest() | |||||
| { | |||||
| CurrentPassword = currentPassword, | |||||
| Email = email ?? Email, | |||||
| Password = password, | |||||
| Username = username ?? Client.PrivateUser.Name, | |||||
| AvatarBase64 = avatar.Base64(avatarType, Client.PrivateUser.AvatarId) | |||||
| }; | |||||
| await Client.ClientAPI.Send(request).ConfigureAwait(false); | |||||
| if (password != null) | |||||
| { | |||||
| var loginRequest = new LoginRequest() | |||||
| { | |||||
| Email = Email, | |||||
| Password = password | |||||
| }; | |||||
| var loginResponse = await Client.ClientAPI.Send(loginRequest).ConfigureAwait(false); | |||||
| Client.ClientAPI.Token = loginResponse.Token; | |||||
| Client.GatewaySocket.Token = loginResponse.Token; | |||||
| } | |||||
| } | |||||
| public override bool Equals(object obj) | public override bool Equals(object obj) | ||||
| => (obj is Profile && (obj as Profile).Id == Id) || (obj is User && (obj as User).Id == Id); | => (obj is Profile && (obj as Profile).Id == Id) || (obj is User && (obj as User).Id == Id); | ||||
| public override int GetHashCode() => unchecked(Id.GetHashCode() + 2061); | public override int GetHashCode() => unchecked(Id.GetHashCode() + 2061); | ||||
| @@ -1,13 +1,17 @@ | |||||
| using System; | |||||
| using Discord.API.Client.Rest; | |||||
| using Discord.Net; | |||||
| using System; | |||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Linq; | using System.Linq; | ||||
| using System.Net; | |||||
| using System.Threading.Tasks; | |||||
| using APIRole = Discord.API.Client.Role; | using APIRole = Discord.API.Client.Role; | ||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| public sealed class Role | public sealed class Role | ||||
| { | { | ||||
| private readonly DiscordClient _client; | |||||
| internal DiscordClient Client => Server.Client; | |||||
| /// <summary> Gets the unique identifier for this role. </summary> | /// <summary> Gets the unique identifier for this role. </summary> | ||||
| public ulong Id { get; } | public ulong Id { get; } | ||||
| @@ -72,8 +76,57 @@ namespace Discord | |||||
| foreach (var member in Members) | foreach (var member in Members) | ||||
| Server.UpdatePermissions(member); | Server.UpdatePermissions(member); | ||||
| } | } | ||||
| public async Task Edit(string name = null, ServerPermissions permissions = null, Color color = null, bool? isHoisted = null, int? position = null) | |||||
| { | |||||
| var updateRequest = new UpdateRoleRequest(Server.Id, Id) | |||||
| { | |||||
| Name = name ?? Name, | |||||
| Permissions = (permissions ?? Permissions).RawValue, | |||||
| Color = (color ?? Color).RawValue, | |||||
| IsHoisted = isHoisted ?? IsHoisted | |||||
| }; | |||||
| var updateResponse = await Client.ClientAPI.Send(updateRequest).ConfigureAwait(false); | |||||
| if (position != null) | |||||
| { | |||||
| int oldPos = Position; | |||||
| int newPos = position.Value; | |||||
| int minPos; | |||||
| Role[] roles = Server.Roles.OrderBy(x => x.Position).ToArray(); | |||||
| if (oldPos < newPos) //Moving Down | |||||
| { | |||||
| minPos = oldPos; | |||||
| for (int i = oldPos; i < newPos; i++) | |||||
| roles[i] = roles[i + 1]; | |||||
| roles[newPos] = this; | |||||
| } | |||||
| else //(oldPos > newPos) Moving Up | |||||
| { | |||||
| minPos = newPos; | |||||
| for (int i = oldPos; i > newPos; i--) | |||||
| roles[i] = roles[i - 1]; | |||||
| roles[newPos] = this; | |||||
| } | |||||
| var reorderRequest = new ReorderRolesRequest(Server.Id) | |||||
| { | |||||
| RoleIds = roles.Skip(minPos).Select(x => x.Id).ToArray(), | |||||
| StartPos = minPos | |||||
| }; | |||||
| await Client.ClientAPI.Send(reorderRequest).ConfigureAwait(false); | |||||
| } | |||||
| } | |||||
| public async Task Delete() | |||||
| { | |||||
| try { await Client.ClientAPI.Send(new DeleteRoleRequest(Server.Id, Id)).ConfigureAwait(false); } | |||||
| catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } | |||||
| } | |||||
| public override bool Equals(object obj) => obj is Role && (obj as Role).Id == Id; | |||||
| public override bool Equals(object obj) => obj is Role && (obj as Role).Id == Id; | |||||
| public override int GetHashCode() => unchecked(Id.GetHashCode() + 6653); | public override int GetHashCode() => unchecked(Id.GetHashCode() + 6653); | ||||
| public override string ToString() => Name ?? Id.ToIdString(); | public override string ToString() => Name ?? Id.ToIdString(); | ||||
| } | } | ||||
| @@ -1,14 +1,22 @@ | |||||
| using Discord.API.Client; | using Discord.API.Client; | ||||
| using Discord.API.Client.Rest; | |||||
| using Discord.Net; | |||||
| using System; | using System; | ||||
| using System.Collections.Concurrent; | using System.Collections.Concurrent; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.IO; | |||||
| using System.Linq; | using System.Linq; | ||||
| using System.Net; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| /// <summary> Represents a Discord server (also known as a guild). </summary> | /// <summary> Represents a Discord server (also known as a guild). </summary> | ||||
| public sealed class Server | public sealed class Server | ||||
| { | { | ||||
| internal static string GetIconUrl(ulong serverId, string iconId) | |||||
| => iconId != null ? $"{DiscordConfig.CDNUrl}/icons/{serverId}/{iconId}.jpg" : null; | |||||
| private struct Member | private struct Member | ||||
| { | { | ||||
| public readonly User User; | public readonly User User; | ||||
| @@ -19,7 +27,7 @@ namespace Discord | |||||
| Permissions = new ServerPermissions(); | Permissions = new ServerPermissions(); | ||||
| Permissions.Lock(); | Permissions.Lock(); | ||||
| } | } | ||||
| } | |||||
| } | |||||
| private readonly ConcurrentDictionary<ulong, Role> _roles; | private readonly ConcurrentDictionary<ulong, Role> _roles; | ||||
| private readonly ConcurrentDictionary<ulong, Member> _users; | private readonly ConcurrentDictionary<ulong, Member> _users; | ||||
| @@ -27,9 +35,9 @@ namespace Discord | |||||
| private readonly ConcurrentDictionary<ulong, bool> _bans; | private readonly ConcurrentDictionary<ulong, bool> _bans; | ||||
| private ulong _ownerId; | private ulong _ownerId; | ||||
| private ulong? _afkChannelId; | private ulong? _afkChannelId; | ||||
| /// <summary> Gets the client that generated this server object. </summary> | |||||
| internal DiscordClient Client { get; } | internal DiscordClient Client { get; } | ||||
| /// <summary> Gets the unique identifier for this server. </summary> | /// <summary> Gets the unique identifier for this server. </summary> | ||||
| public ulong Id { get; } | public ulong Id { get; } | ||||
| /// <summary> Gets the default channel for this server. </summary> | /// <summary> Gets the default channel for this server. </summary> | ||||
| @@ -39,19 +47,14 @@ namespace Discord | |||||
| /// <summary> Gets the name of this server. </summary> | /// <summary> Gets the name of this server. </summary> | ||||
| public string Name { get; private set; } | public string Name { get; private set; } | ||||
| /// <summary> Gets the amount of time (in seconds) a user must be inactive for until they are automatically moved to the AFK channel, if one is set. </summary> | |||||
| public int AFKTimeout { get; private set; } | |||||
| /// <summary> Gets the date and time you joined this server. </summary> | |||||
| public DateTime JoinedAt { get; private set; } | |||||
| /// <summary> Gets the voice region for this server. </summary> | /// <summary> Gets the voice region for this server. </summary> | ||||
| public Region Region { get; private set; } | public Region Region { get; private set; } | ||||
| /// <summary> Gets the unique identifier for this user's current avatar. </summary> | /// <summary> Gets the unique identifier for this user's current avatar. </summary> | ||||
| public string IconId { get; private set; } | public string IconId { get; private set; } | ||||
| /// <summary> Gets the URL to this user's current avatar. </summary> | |||||
| public string IconUrl => GetIconUrl(Id, IconId); | |||||
| internal static string GetIconUrl(ulong serverId, string iconId) | |||||
| => iconId != null ? $"{DiscordConfig.CDNUrl}/icons/{serverId}/{iconId}.jpg" : null; | |||||
| /// <summary> Gets the amount of time (in seconds) a user must be inactive for until they are automatically moved to the AFK voice channel, if one is set. </summary> | |||||
| public int AFKTimeout { get; private set; } | |||||
| /// <summary> Gets the date and time you joined this server. </summary> | |||||
| public DateTime JoinedAt { get; private set; } | |||||
| /// <summary> Gets the user that created this server. </summary> | /// <summary> Gets the user that created this server. </summary> | ||||
| public User Owner => GetUser(_ownerId); | public User Owner => GetUser(_ownerId); | ||||
| @@ -59,16 +62,18 @@ namespace Discord | |||||
| public Channel AFKChannel => _afkChannelId != null ? GetChannel(_afkChannelId.Value) : null; | public Channel AFKChannel => _afkChannelId != null ? GetChannel(_afkChannelId.Value) : null; | ||||
| /// <summary> Gets the current user in this server. </summary> | /// <summary> Gets the current user in this server. </summary> | ||||
| public User CurrentUser => GetUser(Client.CurrentUser.Id); | public User CurrentUser => GetUser(Client.CurrentUser.Id); | ||||
| /// <summary> Gets the URL to this user's current avatar. </summary> | |||||
| public string IconUrl => GetIconUrl(Id, IconId); | |||||
| /// <summary> Gets a collection of the ids of all users banned on this server. </summary> | /// <summary> Gets a collection of the ids of all users banned on this server. </summary> | ||||
| public IEnumerable<ulong> BannedUserIds => _bans.Select(x => x.Key); | public IEnumerable<ulong> BannedUserIds => _bans.Select(x => x.Key); | ||||
| /// <summary> Gets a collection of all channels within this server. </summary> | |||||
| /// <summary> Gets a collection of all channels in this server. </summary> | |||||
| public IEnumerable<Channel> Channels => _channels.Select(x => x.Value); | public IEnumerable<Channel> Channels => _channels.Select(x => x.Value); | ||||
| /// <summary> Gets a collection of all users within this server with their server-specific data. </summary> | |||||
| /// <summary> Gets a collection of all members in this server. </summary> | |||||
| public IEnumerable<User> Users => _users.Select(x => x.Value.User); | public IEnumerable<User> Users => _users.Select(x => x.Value.User); | ||||
| /// <summary> Gets a collection of all roles within this server. </summary> | |||||
| /// <summary> Gets a collection of all roles in this server. </summary> | |||||
| public IEnumerable<Role> Roles => _roles.Select(x => x.Value); | public IEnumerable<Role> Roles => _roles.Select(x => x.Value); | ||||
| internal Server(DiscordClient client, ulong id) | internal Server(DiscordClient client, ulong id) | ||||
| { | { | ||||
| Client = client; | Client = client; | ||||
| @@ -80,7 +85,7 @@ namespace Discord | |||||
| DefaultChannel = AddChannel(id); | DefaultChannel = AddChannel(id); | ||||
| EveryoneRole = AddRole(id); | EveryoneRole = AddRole(id); | ||||
| } | } | ||||
| internal void Update(GuildReference model) | internal void Update(GuildReference model) | ||||
| { | { | ||||
| if (model.Name != null) | if (model.Name != null) | ||||
| @@ -120,7 +125,7 @@ namespace Discord | |||||
| if (model.Members != null) | if (model.Members != null) | ||||
| { | { | ||||
| foreach (var subModel in model.Members) | foreach (var subModel in model.Members) | ||||
| AddMember(subModel.User.Id).Update(subModel); | |||||
| AddUser(subModel.User.Id).Update(subModel); | |||||
| } | } | ||||
| if (model.VoiceStates != null) | if (model.VoiceStates != null) | ||||
| { | { | ||||
| @@ -133,8 +138,39 @@ namespace Discord | |||||
| GetUser(subModel.User.Id)?.Update(subModel); | GetUser(subModel.User.Id)?.Update(subModel); | ||||
| } | } | ||||
| } | } | ||||
| /// <summary> Edits this server, changing only non-null attributes. </summary> | |||||
| public Task Edit(string name = null, string region = null, Stream icon = null, ImageType iconType = ImageType.Png) | |||||
| { | |||||
| var request = new UpdateGuildRequest(Id) | |||||
| { | |||||
| Name = name ?? Name, | |||||
| Region = region ?? Region.Id, | |||||
| IconBase64 = icon.Base64(iconType, IconId), | |||||
| AFKChannelId = AFKChannel?.Id, | |||||
| AFKTimeout = AFKTimeout | |||||
| }; | |||||
| return Client.ClientAPI.Send(request); | |||||
| } | |||||
| //Bans | |||||
| /// <summary> Leaves this server. This function will fail if you're the owner - use Delete instead. </summary> | |||||
| public async Task Leave() | |||||
| { | |||||
| if (_ownerId == CurrentUser.Id) | |||||
| throw new InvalidOperationException("Unable to leave a server you own, use Server.Delete instead"); | |||||
| try { await Client.ClientAPI.Send(new LeaveGuildRequest(Id)).ConfigureAwait(false); } | |||||
| catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } | |||||
| } | |||||
| /// <summary> Deletes this server. This function will fail if you're not the owner - use Leave instead. </summary> | |||||
| public async Task Delete() | |||||
| { | |||||
| if (_ownerId != CurrentUser.Id) | |||||
| throw new InvalidOperationException("Unable to delete a server you don't own, use Server.Leave instead"); | |||||
| try { await Client.ClientAPI.Send(new LeaveGuildRequest(Id)).ConfigureAwait(false); } | |||||
| catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } | |||||
| } | |||||
| #region Bans | |||||
| internal void AddBan(ulong banId) | internal void AddBan(ulong banId) | ||||
| => _bans.TryAdd(banId, true); | => _bans.TryAdd(banId, true); | ||||
| internal bool RemoveBan(ulong banId) | internal bool RemoveBan(ulong banId) | ||||
| @@ -143,7 +179,22 @@ namespace Discord | |||||
| return _bans.TryRemove(banId, out ignored); | return _bans.TryRemove(banId, out ignored); | ||||
| } | } | ||||
| //Channels | |||||
| public Task Ban(User user, int pruneDays = 0) | |||||
| { | |||||
| var request = new AddGuildBanRequest(user.Server.Id, user.Id); | |||||
| request.PruneDays = pruneDays; | |||||
| return Client.ClientAPI.Send(request); | |||||
| } | |||||
| public Task Unban(User user, int pruneDays = 0) | |||||
| => Unban(user.Id); | |||||
| public async Task Unban(ulong userId) | |||||
| { | |||||
| try { await Client.ClientAPI.Send(new RemoveGuildBanRequest(Id, userId)).ConfigureAwait(false); } | |||||
| catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } | |||||
| } | |||||
| #endregion | |||||
| #region Channels | |||||
| internal Channel AddChannel(ulong id) | internal Channel AddChannel(ulong id) | ||||
| => _channels.GetOrAdd(id, x => new Channel(Client, x, this)); | => _channels.GetOrAdd(id, x => new Channel(Client, x, this)); | ||||
| internal Channel RemoveChannel(ulong id) | internal Channel RemoveChannel(ulong id) | ||||
| @@ -152,6 +203,8 @@ namespace Discord | |||||
| _channels.TryRemove(id, out channel); | _channels.TryRemove(id, out channel); | ||||
| return channel; | return channel; | ||||
| } | } | ||||
| /// <summary> Gets the channel with the provided id and owned by this server, or null if not found. </summary> | |||||
| public Channel GetChannel(ulong id) | public Channel GetChannel(ulong id) | ||||
| { | { | ||||
| Channel result; | Channel result; | ||||
| @@ -159,36 +212,67 @@ namespace Discord | |||||
| return result; | return result; | ||||
| } | } | ||||
| //Members | |||||
| internal User AddMember(ulong id) | |||||
| /// <summary> Returns all channels with the specified server and name. </summary> | |||||
| /// <remarks> Name formats supported: Name, #Name and <#Id>. Search is case-insensitive if exactMatch is false.</remarks> | |||||
| public IEnumerable<Channel> FindChannels(string name, ChannelType type = null, bool exactMatch = false) | |||||
| { | { | ||||
| User newUser = null; | |||||
| var user = _users.GetOrAdd(id, x => new Member(new User(id, this))); | |||||
| if (user.User == newUser) | |||||
| { | |||||
| foreach (var channel in Channels) | |||||
| channel.AddUser(newUser); | |||||
| } | |||||
| return user.User; | |||||
| if (name == null) throw new ArgumentNullException(nameof(name)); | |||||
| if (type == null) throw new ArgumentNullException(nameof(type)); | |||||
| return _channels.Select(x => x.Value).Find(name, type, exactMatch); | |||||
| } | } | ||||
| internal User RemoveMember(ulong id) | |||||
| { | |||||
| Member member; | |||||
| if (_users.TryRemove(id, out member)) | |||||
| { | |||||
| foreach (var channel in Channels) | |||||
| channel.RemoveUser(id); | |||||
| } | |||||
| return member.User; | |||||
| /// <summary> Creates a new channel. </summary> | |||||
| public async Task<Channel> CreateChannel(string name, ChannelType type) | |||||
| { | |||||
| if (name == null) throw new ArgumentNullException(nameof(name)); | |||||
| if (type == null) throw new ArgumentNullException(nameof(type)); | |||||
| var request = new CreateChannelRequest(Id) { Name = name, Type = type.Value }; | |||||
| var response = await Client.ClientAPI.Send(request).ConfigureAwait(false); | |||||
| var channel = AddChannel(response.Id); | |||||
| channel.Update(response); | |||||
| return channel; | |||||
| } | } | ||||
| public User GetUser(ulong id) | |||||
| /// <summary> Reorders the provided channels and optionally places them after a certain channel. </summary> | |||||
| public Task ReorderChannels(IEnumerable<Channel> channels, Channel after = null) | |||||
| { | { | ||||
| Member result; | |||||
| _users.TryGetValue(id, out result); | |||||
| return result.User; | |||||
| if (channels == null) throw new ArgumentNullException(nameof(channels)); | |||||
| var request = new ReorderChannelsRequest(Id) | |||||
| { | |||||
| ChannelIds = channels.Select(x => x.Id).ToArray(), | |||||
| StartPos = after != null ? after.Position + 1 : channels.Min(x => x.Position) | |||||
| }; | |||||
| return Client.ClientAPI.Send(request); | |||||
| } | } | ||||
| #endregion | |||||
| //Roles | |||||
| #region Invites | |||||
| /// <summary> Gets all active (non-expired) invites to this server. </summary> | |||||
| public async Task<IEnumerable<Invite>> GetInvites() | |||||
| { | |||||
| var response = await Client.ClientAPI.Send(new GetInvitesRequest(Id)).ConfigureAwait(false); | |||||
| return response.Select(x => | |||||
| { | |||||
| var invite = new Invite(Client, x.Code, x.XkcdPass); | |||||
| invite.Update(x); | |||||
| return invite; | |||||
| }); | |||||
| } | |||||
| /// <summary> Creates a new invite to the default channel of this server. </summary> | |||||
| /// <param name="maxAge"> Time (in seconds) until the invite expires. Set to null to never expire. </param> | |||||
| /// <param name="maxUses"> The max amount of times this invite may be used. Set to null to have unlimited uses. </param> | |||||
| /// <param name="tempMembership"> If true, a user accepting this invite will be kicked from the server after closing their client. </param> | |||||
| /// <param name="withXkcd"> If true, creates a human-readable link. Not supported if maxAge is set to null. </param> | |||||
| public Task<Invite> CreateInvite(int? maxAge = 1800, int? maxUses = null, bool tempMembership = false, bool withXkcd = false) | |||||
| => DefaultChannel.CreateInvite(maxAge, maxUses, tempMembership, withXkcd); | |||||
| #endregion | |||||
| #region Roles | |||||
| internal Role AddRole(ulong id) | internal Role AddRole(ulong id) | ||||
| => _roles.GetOrAdd(id, x => new Role(x, this)); | => _roles.GetOrAdd(id, x => new Role(x, this)); | ||||
| internal Role RemoveRole(ulong id) | internal Role RemoveRole(ulong id) | ||||
| @@ -197,14 +281,59 @@ namespace Discord | |||||
| _roles.TryRemove(id, out role); | _roles.TryRemove(id, out role); | ||||
| return role; | return role; | ||||
| } | } | ||||
| /// <summary> Gets the role with the provided id and owned by this server, or null if not found. </summary> | |||||
| public Role GetRole(ulong id) | public Role GetRole(ulong id) | ||||
| { | { | ||||
| Role result; | Role result; | ||||
| _roles.TryGetValue(id, out result); | _roles.TryGetValue(id, out result); | ||||
| return result; | return result; | ||||
| } | } | ||||
| /// <summary> Returns all roles with the specified server and name. </summary> | |||||
| /// <remarks> Search is case-insensitive if exactMatch is false.</remarks> | |||||
| public IEnumerable<Role> FindRoles(string name, bool exactMatch = false) | |||||
| { | |||||
| if (name == null) throw new ArgumentNullException(nameof(name)); | |||||
| return _roles.Select(x => x.Value).Find(name, exactMatch); | |||||
| } | |||||
| /// <summary> Creates a new role. </summary> | |||||
| public async Task<Role> CreateRole(string name, ServerPermissions permissions = null, Color color = null, bool isHoisted = false) | |||||
| { | |||||
| if (name == null) throw new ArgumentNullException(nameof(name)); | |||||
| //Permissions | |||||
| var createRequest = new CreateRoleRequest(Id); | |||||
| var createResponse = await Client.ClientAPI.Send(createRequest).ConfigureAwait(false); | |||||
| var role = AddRole(createResponse.Id); | |||||
| role.Update(createResponse); | |||||
| var editRequest = new UpdateRoleRequest(role.Server.Id, role.Id) | |||||
| { | |||||
| Name = name, | |||||
| Permissions = (permissions ?? role.Permissions).RawValue, | |||||
| Color = (color ?? Color.Default).RawValue, | |||||
| IsHoisted = isHoisted | |||||
| }; | |||||
| var editResponse = await Client.ClientAPI.Send(editRequest).ConfigureAwait(false); | |||||
| role.Update(editResponse); | |||||
| return role; | |||||
| } | |||||
| /// <summary> Reorders the provided roles and optionally places them after a certain role. </summary> | |||||
| public Task ReorderRoles(IEnumerable<Role> roles, Role after = null) | |||||
| { | |||||
| if (roles == null) throw new ArgumentNullException(nameof(roles)); | |||||
| return Client.ClientAPI.Send(new ReorderRolesRequest(Id) | |||||
| { | |||||
| RoleIds = roles.Select(x => x.Id).ToArray(), | |||||
| StartPos = after != null ? after.Position + 1 : roles.Min(x => x.Position) | |||||
| }); | |||||
| } | |||||
| #endregion | |||||
| #region Permissions | |||||
| internal ServerPermissions GetPermissions(User user) | internal ServerPermissions GetPermissions(User user) | ||||
| { | { | ||||
| Member member; | Member member; | ||||
| @@ -213,12 +342,14 @@ namespace Discord | |||||
| else | else | ||||
| return null; | return null; | ||||
| } | } | ||||
| internal void UpdatePermissions(User user) | internal void UpdatePermissions(User user) | ||||
| { | { | ||||
| Member member; | Member member; | ||||
| if (_users.TryGetValue(user.Id, out member)) | if (_users.TryGetValue(user.Id, out member)) | ||||
| UpdatePermissions(member.User, member.Permissions); | UpdatePermissions(member.User, member.Permissions); | ||||
| } | } | ||||
| private void UpdatePermissions(User user, ServerPermissions permissions) | private void UpdatePermissions(User user, ServerPermissions permissions) | ||||
| { | { | ||||
| uint newPermissions = 0; | uint newPermissions = 0; | ||||
| @@ -241,8 +372,75 @@ namespace Discord | |||||
| channel.Value.UpdatePermissions(user); | channel.Value.UpdatePermissions(user); | ||||
| } | } | ||||
| } | } | ||||
| #endregion | |||||
| #region Users | |||||
| internal User AddUser(ulong id) | |||||
| { | |||||
| User newUser = null; | |||||
| var user = _users.GetOrAdd(id, x => new Member(new User(Client, id, this))); | |||||
| if (user.User == newUser) | |||||
| { | |||||
| foreach (var channel in Channels) | |||||
| channel.AddUser(newUser); | |||||
| } | |||||
| return user.User; | |||||
| } | |||||
| internal User RemoveUser(ulong id) | |||||
| { | |||||
| Member member; | |||||
| if (_users.TryRemove(id, out member)) | |||||
| { | |||||
| foreach (var channel in Channels) | |||||
| channel.RemoveUser(id); | |||||
| } | |||||
| return member.User; | |||||
| } | |||||
| /// <summary> Gets the user with the provided id and is a member of this server, or null if not found. </summary> | |||||
| public User GetUser(ulong id) | |||||
| { | |||||
| Member result; | |||||
| _users.TryGetValue(id, out result); | |||||
| return result.User; | |||||
| } | |||||
| /// <summary> Gets the user with the provided username and discriminator, that is a member of this server, or null if not found. </summary> | |||||
| public User GetUser(string name, ushort discriminator) | |||||
| { | |||||
| if (name == null) throw new ArgumentNullException(nameof(name)); | |||||
| return _users.Select(x => x.Value.User).Find(name, discriminator: discriminator, exactMatch: false).FirstOrDefault(); | |||||
| } | |||||
| /// <summary> Returns all members of this server with the specified name. </summary> | |||||
| /// <remarks> Name formats supported: Name, @Name and <@Id>. Search is case-insensitive if exactMatch is false.</remarks> | |||||
| public IEnumerable<User> FindUsers(string name, bool exactMatch = false) | |||||
| { | |||||
| if (name == null) throw new ArgumentNullException(nameof(name)); | |||||
| return _users.Select(x => x.Value.User).Find(name, exactMatch: exactMatch); | |||||
| } | |||||
| /// <summary> Kicks all users with an inactivity greater or equal to the provided number of days. </summary> | |||||
| /// <param name="simulate">If true, no pruning will actually be done but instead return the number of users that would be pruned. </param> | |||||
| public async Task<int> PruneUsers(int days = 30, bool simulate = false) | |||||
| { | |||||
| if (days <= 0) throw new ArgumentOutOfRangeException(nameof(days)); | |||||
| var request = new PruneMembersRequest(Id) | |||||
| { | |||||
| Days = days, | |||||
| IsSimulation = simulate | |||||
| }; | |||||
| var response = await Client.ClientAPI.Send(request).ConfigureAwait(false); | |||||
| return response.Pruned; | |||||
| } | |||||
| /// <summary>When Config.UseLargeThreshold is enabled, running this command will request the Discord server to provide you with all offline users for this server.</summary> | |||||
| public void RequestOfflineUsers() | |||||
| => Client.GatewaySocket.SendRequestMembers(Id, "", 0); | |||||
| #endregion | |||||
| public override bool Equals(object obj) => obj is Server && (obj as Server).Id == Id; | |||||
| public override bool Equals(object obj) => obj is Server && (obj as Server).Id == Id; | |||||
| public override int GetHashCode() => unchecked(Id.GetHashCode() + 5175); | public override int GetHashCode() => unchecked(Id.GetHashCode() + 5175); | ||||
| public override string ToString() => Name ?? Id.ToIdString(); | public override string ToString() => Name ?? Id.ToIdString(); | ||||
| } | } | ||||
| @@ -1,13 +1,19 @@ | |||||
| using Discord.API.Client; | using Discord.API.Client; | ||||
| using Discord.API.Client.Rest; | |||||
| using System; | using System; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.IO; | |||||
| using System.Linq; | using System.Linq; | ||||
| using System.Threading.Tasks; | |||||
| using APIMember = Discord.API.Client.Member; | using APIMember = Discord.API.Client.Member; | ||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| public class User | public class User | ||||
| { | |||||
| { | |||||
| internal static string GetAvatarUrl(ulong userId, string avatarId) | |||||
| => avatarId != null ? $"{DiscordConfig.CDNUrl}/avatars/{userId}/{avatarId}.jpg" : null; | |||||
| [Flags] | [Flags] | ||||
| private enum VoiceState : byte | private enum VoiceState : byte | ||||
| { | { | ||||
| @@ -34,15 +40,13 @@ namespace Discord | |||||
| => unchecked(ServerId.GetHashCode() + UserId.GetHashCode() + 23); | => unchecked(ServerId.GetHashCode() + UserId.GetHashCode() + 23); | ||||
| } | } | ||||
| internal static string GetAvatarUrl(ulong userId, string avatarId) => avatarId != null ? $"{DiscordConfig.CDNUrl}/avatars/{userId}/{avatarId}.jpg" : null; | |||||
| private VoiceState _voiceState; | private VoiceState _voiceState; | ||||
| private DateTime? _lastOnline; | private DateTime? _lastOnline; | ||||
| private ulong? _voiceChannelId; | private ulong? _voiceChannelId; | ||||
| private Dictionary<ulong, Role> _roles; | private Dictionary<ulong, Role> _roles; | ||||
| /// <summary> Gets the client that generated this user object. </summary> | |||||
| internal DiscordClient Client { get; } | internal DiscordClient Client { get; } | ||||
| /// <summary> Gets the unique identifier for this user. </summary> | /// <summary> Gets the unique identifier for this user. </summary> | ||||
| public ulong Id { get; } | public ulong Id { get; } | ||||
| /// <summary> Gets the server this user is a member of. </summary> | /// <summary> Gets the server this user is a member of. </summary> | ||||
| @@ -129,9 +133,9 @@ namespace Discord | |||||
| } | } | ||||
| } | } | ||||
| internal User(ulong id, Server server) | |||||
| internal User(DiscordClient client, ulong id, Server server) | |||||
| { | { | ||||
| Client = client; | |||||
| Server = server; | Server = server; | ||||
| _roles = new Dictionary<ulong, Role>(); | _roles = new Dictionary<ulong, Role>(); | ||||
| @@ -220,51 +224,109 @@ namespace Discord | |||||
| _voiceChannelId = model.ChannelId; //Allows null | _voiceChannelId = model.ChannelId; //Allows null | ||||
| } | } | ||||
| private void UpdateRoles(IEnumerable<Role> roles) | |||||
| { | |||||
| var newRoles = new Dictionary<ulong, Role>(); | |||||
| if (roles != null) | |||||
| { | |||||
| foreach (var r in roles) | |||||
| { | |||||
| if (r != null) | |||||
| newRoles[r.Id] = r; | |||||
| } | |||||
| } | |||||
| if (Server != null) | |||||
| { | |||||
| var everyone = Server.EveryoneRole; | |||||
| newRoles[everyone.Id] = everyone; | |||||
| } | |||||
| _roles = newRoles; | |||||
| if (Server != null) | |||||
| Server.UpdatePermissions(this); | |||||
| } | |||||
| internal void UpdateActivity(DateTime? activity = null) | internal void UpdateActivity(DateTime? activity = null) | ||||
| { | { | ||||
| if (LastActivityAt == null || activity > LastActivityAt.Value) | if (LastActivityAt == null || activity > LastActivityAt.Value) | ||||
| LastActivityAt = activity ?? DateTime.UtcNow; | LastActivityAt = activity ?? DateTime.UtcNow; | ||||
| } | } | ||||
| public ServerPermissions ServerPermissions => Server.GetPermissions(this); | |||||
| public Task Edit(bool? isMuted = null, bool? isDeafened = null, Channel voiceChannel = null, IEnumerable<Role> roles = null) | |||||
| { | |||||
| if (Server == null) throw new InvalidOperationException("Unable to edit users in a private channel"); | |||||
| //Modify the roles collection and filter out the everyone role | |||||
| var roleIds = roles == null ? null : Roles.Where(x => !x.IsEveryone).Select(x => x.Id); | |||||
| var request = new UpdateMemberRequest(Server.Id, Id) | |||||
| { | |||||
| IsMuted = isMuted ?? IsServerMuted, | |||||
| IsDeafened = isDeafened ?? IsServerDeafened, | |||||
| VoiceChannelId = voiceChannel?.Id, | |||||
| RoleIds = roleIds.ToArray() | |||||
| }; | |||||
| return Client.ClientAPI.Send(request); | |||||
| } | |||||
| public Task Kick() | |||||
| { | |||||
| if (Server == null) throw new InvalidOperationException("Unable to kick users from a private channel"); | |||||
| var request = new KickMemberRequest(Server.Id, Id); | |||||
| return Client.ClientAPI.Send(request); | |||||
| } | |||||
| #region Permissions | |||||
| public ServerPermissions ServerPermissions => Server.GetPermissions(this); | |||||
| public ChannelPermissions GetPermissions(Channel channel) | public ChannelPermissions GetPermissions(Channel channel) | ||||
| { | { | ||||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | if (channel == null) throw new ArgumentNullException(nameof(channel)); | ||||
| return channel.GetPermissions(this); | return channel.GetPermissions(this); | ||||
| } | } | ||||
| #endregion | |||||
| public bool HasRole(Role role) | |||||
| { | |||||
| if (role == null) throw new ArgumentNullException(nameof(role)); | |||||
| return _roles.ContainsKey(role.Id); | |||||
| } | |||||
| #region Channels | |||||
| public Task<Channel> CreateChannel() | |||||
| => Client.CreatePrivateChannel(this); | |||||
| #endregion | |||||
| #region Messages | |||||
| public async Task<Message> SendMessage(string text) | |||||
| { | |||||
| if (text == null) throw new ArgumentNullException(nameof(text)); | |||||
| var channel = await CreateChannel().ConfigureAwait(false); | |||||
| return await channel.SendMessage(text).ConfigureAwait(false); | |||||
| } | |||||
| public async Task<Message> SendFile(string filePath) | |||||
| { | |||||
| if (filePath == null) throw new ArgumentNullException(nameof(filePath)); | |||||
| var channel = await CreateChannel().ConfigureAwait(false); | |||||
| return await channel.SendFile(filePath).ConfigureAwait(false); | |||||
| } | |||||
| public async Task<Message> SendFile(string filename, Stream stream) | |||||
| { | |||||
| if (filename == null) throw new ArgumentNullException(nameof(filename)); | |||||
| if (stream == null) throw new ArgumentNullException(nameof(stream)); | |||||
| var channel = await CreateChannel().ConfigureAwait(false); | |||||
| return await channel.SendFile(filename, stream).ConfigureAwait(false); | |||||
| } | |||||
| #endregion | |||||
| #region Roles | |||||
| private void UpdateRoles(IEnumerable<Role> roles) | |||||
| { | |||||
| var newRoles = new Dictionary<ulong, Role>(); | |||||
| if (roles != null) | |||||
| { | |||||
| foreach (var r in roles) | |||||
| { | |||||
| if (r != null) | |||||
| newRoles[r.Id] = r; | |||||
| } | |||||
| } | |||||
| if (Server != null) | |||||
| { | |||||
| var everyone = Server.EveryoneRole; | |||||
| newRoles[everyone.Id] = everyone; | |||||
| } | |||||
| _roles = newRoles; | |||||
| if (Server != null) | |||||
| Server.UpdatePermissions(this); | |||||
| } | |||||
| public bool HasRole(Role role) | |||||
| { | |||||
| if (role == null) throw new ArgumentNullException(nameof(role)); | |||||
| return _roles.ContainsKey(role.Id); | |||||
| } | |||||
| #endregion | |||||
| public override bool Equals(object obj) => obj is User && (obj as User).Id == Id; | |||||
| public override bool Equals(object obj) => obj is User && (obj as User).Id == Id; | |||||
| public override int GetHashCode() => unchecked(Id.GetHashCode() + 7230); | public override int GetHashCode() => unchecked(Id.GetHashCode() + 7230); | ||||
| public override string ToString() => Name != null ? $"{Name}#{Discriminator}" : Id.ToIdString(); | public override string ToString() => Name != null ? $"{Name}#{Discriminator}" : Id.ToIdString(); | ||||
| } | } | ||||
| @@ -14,7 +14,7 @@ namespace Discord.Net.WebSockets | |||||
| private int _lastSequence; | private int _lastSequence; | ||||
| private string _sessionId; | private string _sessionId; | ||||
| public string Token { get; private set; } | |||||
| public string Token { get; internal set; } | |||||
| public GatewaySocket(DiscordClient client, JsonSerializer serializer, Logger logger) | public GatewaySocket(DiscordClient client, JsonSerializer serializer, Logger logger) | ||||
| : base(client, serializer, logger) | : base(client, serializer, logger) | ||||
| @@ -26,11 +26,10 @@ namespace Discord.Net.WebSockets | |||||
| }; | }; | ||||
| } | } | ||||
| public async Task Connect(string token) | |||||
| public async Task Connect() | |||||
| { | { | ||||
| Token = token; | |||||
| await BeginConnect().ConfigureAwait(false); | await BeginConnect().ConfigureAwait(false); | ||||
| SendIdentify(token); | |||||
| SendIdentify(Token); | |||||
| } | } | ||||
| private async Task Redirect() | private async Task Redirect() | ||||
| { | { | ||||
| @@ -47,7 +46,7 @@ namespace Discord.Net.WebSockets | |||||
| { | { | ||||
| try | try | ||||
| { | { | ||||
| await Connect(Token).ConfigureAwait(false); | |||||
| await Connect().ConfigureAwait(false); | |||||
| break; | break; | ||||
| } | } | ||||
| catch (OperationCanceledException) { throw; } | catch (OperationCanceledException) { throw; } | ||||
| @@ -1,28 +0,0 @@ | |||||
| using System.Collections.Generic; | |||||
| namespace Discord | |||||
| { | |||||
| /*public struct Optional<T> | |||||
| { | |||||
| public bool HasValue { get; } | |||||
| public T Value { get; } | |||||
| public Optional(T value) | |||||
| { | |||||
| HasValue = true; | |||||
| Value = value; | |||||
| } | |||||
| public static implicit operator Optional<T>(T value) => new Optional<T>(value); | |||||
| public static bool operator ==(Optional<T> a, Optional<T> b) => | |||||
| a.HasValue == b.HasValue && EqualityComparer<T>.Default.Equals(a.Value, b.Value); | |||||
| public static bool operator !=(Optional<T> a, Optional<T> b) => | |||||
| a.HasValue != b.HasValue || EqualityComparer<T>.Default.Equals(a.Value, b.Value); | |||||
| public override bool Equals(object obj) => | |||||
| this == ((Optional<T>)obj); | |||||
| public override int GetHashCode() => | |||||
| unchecked(HasValue.GetHashCode() + Value?.GetHashCode() ?? 0); | |||||
| public override string ToString() => Value?.ToString() ?? "null"; | |||||
| }*/ | |||||
| } | |||||
| @@ -0,0 +1,12 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord | |||||
| { | |||||
| public enum RelativeDirection | |||||
| { | |||||
| Before, After | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,39 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| namespace Discord | |||||
| { | |||||
| public class ServiceManager | |||||
| { | |||||
| private readonly Dictionary<Type, IService> _services; | |||||
| internal DiscordClient Client { get; } | |||||
| internal ServiceManager(DiscordClient client) | |||||
| { | |||||
| Client = client; | |||||
| _services = new Dictionary<Type, IService>(); | |||||
| } | |||||
| public void Add<T>(T service) | |||||
| where T : class, IService | |||||
| { | |||||
| _services.Add(typeof(T), service); | |||||
| service.Install(Client); | |||||
| } | |||||
| public T Get<T>(bool isRequired = true) | |||||
| where T : class, IService | |||||
| { | |||||
| IService service; | |||||
| T singletonT = null; | |||||
| if (_services.TryGetValue(typeof(T), out service)) | |||||
| singletonT = service as T; | |||||
| if (singletonT == null && isRequired) | |||||
| throw new InvalidOperationException($"This operation requires {typeof(T).Name} to be added to {nameof(DiscordClient)}."); | |||||
| return singletonT; | |||||
| } | |||||
| } | |||||
| } | |||||