├── .github └── workflows │ └── aspnetcore.yml ├── .gitignore ├── .nuget └── icon.png ├── .travis.yml ├── Examples ├── DependencyInjection │ ├── DependencyInjection.csproj │ └── Program.cs ├── SimpleGateway │ ├── Program.cs │ └── SimpleGateway.csproj └── helpers │ └── Example.Helpers │ ├── Example.Helpers.csproj │ └── ExampleHelper.cs ├── LICENSE ├── Miki.Discord.Common ├── Color.cs ├── DiscordToken.cs ├── EmbedBuilder.cs ├── Extensions │ ├── EventExtensions.cs │ └── ReactiveExtensions.cs ├── Gateway │ ├── GatewayConnectionObject.cs │ ├── GatewayMessage.cs │ ├── GatewayOpCode.cs │ ├── GatewaySessionLimitsPacket.cs │ └── IGatewayApiClient.cs ├── Helpers │ ├── CacheHelpers.cs │ └── DiscordHelpers.cs ├── IDiscordClient.cs ├── IDiscordEvents.cs ├── IGateway.cs ├── IGatewayEvents.cs ├── Mention.cs ├── Miki.Discord.Common.csproj ├── Miki.Discord.Common.csproj.DotSettings ├── Miki.Discord.Common.xml ├── Models │ ├── IApiClient.cs │ ├── IContainsGuild.cs │ ├── IDiscordAttachment.cs │ ├── IDiscordChannel.cs │ ├── IDiscordGuild.cs │ ├── IDiscordGuildMessage.cs │ ├── IDiscordGuildUser.cs │ ├── IDiscordMessage.cs │ ├── IDiscordPresence.cs │ ├── IDiscordReaction.cs │ ├── IDiscordRole.cs │ ├── IDiscordSelfUser.cs │ ├── IDiscordUser.cs │ ├── IGuildUser.cs │ ├── ISnowflake.cs │ └── ITextChannel.cs ├── Packets │ ├── API │ │ ├── ActivityType.cs │ │ ├── DiscordActivity.cs │ │ ├── DiscordAttachmentPacket.cs │ │ ├── DiscordChannelPacket.cs │ │ ├── DiscordEmbed.cs │ │ ├── DiscordEmoji.cs │ │ ├── DiscordGuildMemberPacket.cs │ │ ├── DiscordGuildPacket.cs │ │ ├── DiscordGuildUnavailablePacket.cs │ │ ├── DiscordMessagePacket.cs │ │ ├── DiscordPresencePacket.cs │ │ ├── DiscordReactionEventContent.cs │ │ ├── DiscordReactionPacket.cs │ │ ├── DiscordRolePacket.cs │ │ ├── DiscordStatus.cs │ │ ├── DiscordUserPacket.cs │ │ ├── DiscordVoiceStatePacket.cs │ │ ├── GuildPermissions.cs │ │ ├── ImageSize.cs │ │ ├── ImageType.cs │ │ ├── Ratelimit.cs │ │ ├── RichPresenceAssets.cs │ │ ├── RichPresenceParty.cs │ │ ├── TimeStampsObject.cs │ │ └── UserStatus.cs │ ├── Arguments │ │ ├── ChannelBulkDeleteArgs.cs │ │ ├── CreateRoleArgs.cs │ │ ├── EmojiCreationArgs.cs │ │ ├── EmojiModifyArgs.cs │ │ ├── MessageArgs.cs │ │ └── UserModifyArgs.cs │ └── Events │ │ ├── GatewayReadyPacket.cs │ │ ├── GuildEmojisUpdateEventArgs.cs │ │ ├── GuildIdUserArgs.cs │ │ ├── GuildMemberUpdateArgs.cs │ │ ├── GuildMemberUpdateEventArgs.cs │ │ ├── MessageBulkDeleteEventArgs.cs │ │ ├── MessageDeleteArgs.cs │ │ ├── RoleDeleteEventArgs.cs │ │ ├── RoleEventArgs.cs │ │ └── TypingStartEventArgs.cs └── TokenType.cs ├── Miki.Discord.Extensions ├── DependencyInjection │ └── ServiceCollectionExtensions.cs ├── DiscordConfiguration.cs └── Miki.Discord.Extensions.csproj ├── Miki.Discord.Gateway ├── Connection │ ├── GatewayConnection.cs │ ├── GatewayConstants.cs │ ├── GatewayEncoding.cs │ └── Models │ │ ├── GatewayEventType.cs │ │ ├── GatewayHelloPacket.cs │ │ ├── GatewayIdentifyPacket.cs │ │ └── GatewayResumePacket.cs ├── Exceptions │ └── GatewayException.cs ├── Extensions │ └── JsonElementExtensions.cs ├── GatewayCluster.cs ├── GatewayEventHandler.cs ├── GatewayIntents.cs ├── GatewayShard.cs ├── Miki.Discord.Gateway.csproj ├── Miki.Discord.Gateway.csproj.DotSettings ├── Miki.Discord.Gateway.xml ├── Models │ └── GatewayConfiguration.cs ├── Ratelimiting │ ├── CacheBasedRatelimiter.cs │ ├── DefaultGatewayRatelimiter.cs │ └── IGatewayRatelimiter.cs ├── Utils │ └── WebsocketUrlBuilder.cs └── WebSocket │ ├── DefaultWebSocketClient.cs │ └── IWebsocketClient.cs ├── Miki.Discord.Rest ├── Converters │ ├── StringToEnumConverter.cs │ ├── StringToShortConverter.cs │ ├── StringToUlongConverter.cs │ └── UserAvatarConverter.cs ├── DiscordApiClient.cs ├── DiscordApiRoutes.cs ├── Exceptions │ └── DiscordRestException.cs ├── Http │ └── DiscordRateLimiter.cs ├── Miki.Discord.Rest.csproj ├── Miki.Discord.Rest.csproj.DotSettings ├── Miki.Discord.Rest.xml └── Models │ ├── DiscordPruneObject.cs │ └── DiscordRestError.cs ├── Miki.Discord.sln ├── Miki.Discord.sln.DotSettings ├── Miki.Discord ├── Cache │ ├── CacheHandler.cs │ ├── DefaultCacheHandler.cs │ └── ICacheHandler.cs ├── DiscordClient.cs ├── DiscordClientConfiguration.cs ├── Events │ └── DiscordEventHandler.cs ├── Exceptions │ └── Discord │ │ └── DiscordPermissionException.cs ├── Helpers │ ├── AbstractionHelpers.cs │ └── DiscordChannelHelper.cs ├── Internal │ ├── Data │ │ ├── DiscordAttachment.cs │ │ ├── DiscordChannel.cs │ │ ├── DiscordGuild.cs │ │ ├── DiscordGuildChannel.cs │ │ ├── DiscordGuildMessage.cs │ │ ├── DiscordGuildTextChannel.cs │ │ ├── DiscordGuildUser.cs │ │ ├── DiscordMessage.cs │ │ ├── DiscordPresence.cs │ │ ├── DiscordReaction.cs │ │ ├── DiscordRole.cs │ │ ├── DiscordSelfUser.cs │ │ ├── DiscordTextChannel.cs │ │ └── DiscordUser.cs │ └── Repositories │ │ ├── BaseCacheRepository.cs │ │ ├── CacheRepositoryHelpers.cs │ │ ├── DiscordChannelCacheRepository.cs │ │ ├── DiscordGuildCacheRepository.cs │ │ ├── DiscordMemberCacheRepository.cs │ │ ├── DiscordRoleCacheRepository.cs │ │ └── DiscordUserCacheRepository.cs ├── Miki.Discord.csproj └── Miki.Discord.xml ├── README.md ├── Tests └── Miki.Discord.Tests │ ├── Cache │ └── CacheRepositoryTests.cs │ ├── Gateway │ ├── Converters │ │ └── StringToEnumConverterTests.cs │ └── GatewayConnectionTests.cs │ ├── Helpers.cs │ ├── Miki.Discord.Tests.csproj │ ├── Ratelimits.cs │ └── Utils │ ├── MentionParserTests.cs │ └── MockWebsocketClient.cs └── azure-pipelines.yml /.github/workflows/aspnetcore.yml: -------------------------------------------------------------------------------- 1 | name: build+test+analyze 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Setup .NET Core 13 | uses: actions/setup-dotnet@v1 14 | with: 15 | dotnet-version: 3.1.101 16 | - name: Build with dotnet 17 | run: dotnet build --configuration Release 18 | - name: Test with dotnet 19 | run: dotnet test Miki.Discord.Tests 20 | -------------------------------------------------------------------------------- /.nuget/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veldtech/Miki.Discord/58985363b9e4a61ae7c89c27201cd2b1ba197df7/.nuget/icon.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: csharp 2 | solution: Miki.Discord.sln 3 | mono: none 4 | dotnet: 2.1.402 5 | services: 6 | - redis 7 | script: 8 | - dotnet restore 9 | - dotnet build 10 | -------------------------------------------------------------------------------- /Examples/DependencyInjection/DependencyInjection.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Examples/DependencyInjection/Program.cs: -------------------------------------------------------------------------------- 1 | using Example.Helpers; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Miki.Cache; 4 | using Miki.Cache.InMemory; 5 | using Miki.Discord.Common; 6 | using Miki.Discord.Extensions.DependencyInjection; 7 | using Miki.Logging; 8 | using Miki.Serialization; 9 | using Miki.Serialization.Protobuf; 10 | using System; 11 | using System.Threading.Tasks; 12 | 13 | namespace DependencyInjection 14 | { 15 | class Program 16 | { 17 | static async Task Main() 18 | { 19 | ExampleHelper.InitLog(LogLevel.Debug); 20 | 21 | var token = ExampleHelper.GetTokenFromEnv(); 22 | 23 | ServiceCollection collection = new ServiceCollection(); 24 | collection.AddSingleton(); 25 | collection.AddSingleton(); 26 | collection.AddSingleton(); 27 | 28 | collection.UseDiscord(x => 29 | { 30 | x.Token = token; 31 | }); 32 | 33 | var serviceProvider = collection.BuildServiceProvider(); 34 | 35 | var client = serviceProvider.GetService(); 36 | 37 | client.Events.MessageCreate.Subscribe(x => 38 | { 39 | Console.WriteLine($"{x.Author.Username}: {x.Content}"); 40 | }); 41 | 42 | await client.StartAsync(default); 43 | 44 | await Task.Delay(-1); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Examples/SimpleGateway/Program.cs: -------------------------------------------------------------------------------- 1 | using Example.Helpers; 2 | using Miki.Cache.InMemory; 3 | using Miki.Discord; 4 | using Miki.Discord.Common; 5 | using Miki.Discord.Gateway; 6 | using Miki.Discord.Rest; 7 | using Miki.Logging; 8 | using Miki.Serialization.Protobuf; 9 | using System.Reactive.Linq; 10 | using System.Threading.Tasks; 11 | 12 | namespace SimpleGateway 13 | { 14 | // This is a test app for Miki.Discord's current interfacing. The goal of this app is to explain how 15 | // the system works and why these steps are necessary. If you're new to bot programming, this might 16 | // not be the most suitable API for you, but it does promiseScaling and control over certain aspects 17 | // whenever needed. For questions join the Miki Stack discord (link in README.md) or tweet me 18 | // @velddev 19 | internal static class Program 20 | { 21 | static async Task Main() 22 | { 23 | // Sets up Miki.Logging for internal library logging. Can be removed if you do not want to 24 | // see internal logs. 25 | ExampleHelper.InitLog(LogLevel.Information); 26 | 27 | // Fetches your token from environment values. 28 | var token = ExampleHelper.GetTokenFromEnv(); 29 | 30 | var memCache = new InMemoryCacheClient(new ProtobufSerializer()); 31 | 32 | var apiClient = new DiscordApiClient(token, memCache); 33 | 34 | // Discord direct gateway implementation. 35 | var gateway = new GatewayCluster( 36 | new GatewayProperties 37 | { 38 | ShardCount = 1, 39 | ShardId = 0, 40 | Token = token.ToString(), 41 | }); 42 | 43 | var discordClient = new DiscordClient(apiClient, gateway, memCache); 44 | 45 | // Subscribe to ready event. 46 | discordClient.Events.MessageCreate.SubscribeTask(OnMessageReceived); 47 | 48 | // Start the connection to the gateway. 49 | await gateway.StartAsync(); 50 | 51 | // Wait, else the application will close. 52 | await Task.Delay(-1); 53 | } 54 | 55 | static async Task OnMessageReceived(IDiscordMessage message) 56 | { 57 | if (message.Content == "ping") 58 | { 59 | var channel = await message.GetChannelAsync(); 60 | await channel.SendMessageAsync("pong!"); 61 | } 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /Examples/SimpleGateway/SimpleGateway.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Examples/helpers/Example.Helpers/Example.Helpers.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Examples/helpers/Example.Helpers/ExampleHelper.cs: -------------------------------------------------------------------------------- 1 | using Miki.Discord.Common; 2 | using Miki.Logging; 3 | using System; 4 | 5 | namespace Example.Helpers 6 | { 7 | public static class ExampleHelper 8 | { 9 | /// 10 | /// Enables library wide logging for all Miki libraries. Consider using this logging for 11 | /// debugging or general information. 12 | /// 13 | public static void InitLog(LogLevel level) 14 | { 15 | new LogBuilder().AddLogEvent(((message, thisLevel) => 16 | { 17 | if (thisLevel >= level) 18 | { 19 | Console.WriteLine(level + " | " + message); 20 | } 21 | })).Apply(); 22 | } 23 | 24 | public static DiscordToken GetTokenFromEnv() 25 | { 26 | return Environment.GetEnvironmentVariable("TOKEN") 27 | ?? throw new InvalidOperationException("Token environment value was not passed."); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Miki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Miki.Discord.Common/Color.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Miki.Discord.Common 4 | { 5 | public struct Color : IEquatable 6 | { 7 | public uint Value { get; } 8 | 9 | public byte R => (byte)(Value >> 16); 10 | public byte G => (byte)(Value >> 8); 11 | public byte B => (byte)Value; 12 | 13 | public Color(uint baseValue) 14 | { 15 | Value = baseValue; 16 | } 17 | public Color(byte r, byte g, byte b) 18 | : this(((uint)r << 16) | ((uint)g << 8) | (uint)b) 19 | { 20 | } 21 | public Color(int r, int g, int b) 22 | : this(((uint)r << 16) | ((uint)g << 8) | (uint)b) 23 | { 24 | } 25 | /// 26 | /// Creates a color from floats ranging from 0.0 to 1.0. 27 | /// 28 | public Color(float r, float g, float b) 29 | : this((byte)(r * byte.MaxValue), (byte)(g * byte.MaxValue), (byte)(b * byte.MaxValue)) 30 | { 31 | } 32 | 33 | /// 34 | public override bool Equals(object obj) 35 | { 36 | if (obj == null) 37 | { 38 | return false; 39 | } 40 | return obj.GetType() == GetType() 41 | && Equals((Color)obj); 42 | } 43 | 44 | /// 45 | public bool Equals(Color other) 46 | { 47 | return other.Value == Value; 48 | } 49 | 50 | /// 51 | public override int GetHashCode() 52 | { 53 | return (int)Value; 54 | } 55 | 56 | public Color Lerp(Color c, float t) 57 | { 58 | return Lerp(this, c, t); 59 | } 60 | 61 | public static Color Lerp(Color colorA, Color ColorB, float time) 62 | { 63 | int newR = (int)(colorA.R + (ColorB.R - colorA.R) * time); 64 | int newG = (int)(colorA.G + (ColorB.G - colorA.G) * time); 65 | int newB = (int)(colorA.B + (ColorB.B - colorA.B) * time); 66 | return new Color(newR, newG, newB); 67 | } 68 | 69 | /// 70 | public override string ToString() 71 | { 72 | return $"#{R:X2}{G:X2}{B:X2}"; 73 | } 74 | 75 | public static bool operator ==(Color c, int value) 76 | => value == c.Value; 77 | 78 | public static bool operator !=(Color c, int value) 79 | => value != c.Value; 80 | } 81 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/DiscordToken.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Miki.Discord.Common 4 | { 5 | /// 6 | /// Discord Token wrapper object to abstractify the bare token away. 7 | /// 8 | public struct DiscordToken 9 | { 10 | /// 11 | /// Parses token for multiple kinds of tokens. 12 | /// 13 | /// 14 | /// Bearer {{TOKEN}} 15 | /// For a user login token. 16 | /// 17 | /// 18 | /// Bot {{TOKEN}} 19 | /// For a bot API token. 20 | /// 21 | /// 22 | /// {{TOKEN}} 23 | /// Defaults to Bot. 24 | /// 25 | /// 26 | /// 27 | public DiscordToken(string tokenSource) 28 | { 29 | if(tokenSource.ToLowerInvariant().StartsWith("bearer ")) 30 | { 31 | Token = tokenSource.Substring(7); 32 | Type = TokenType.BEARER; 33 | } 34 | else if(tokenSource.ToLowerInvariant().StartsWith("bot ")) 35 | { 36 | Token = tokenSource.Substring(4); 37 | Type = TokenType.BOT; 38 | } 39 | else 40 | { 41 | Token = tokenSource; 42 | Type = TokenType.BOT; 43 | } 44 | } 45 | 46 | /// 47 | /// Raw token source 48 | /// 49 | public string Token { get; } 50 | 51 | /// 52 | /// Token type 53 | /// 54 | public TokenType Type { get; } 55 | 56 | /// 57 | /// Gets the formatted type. e.g. Bot, Bearer. 58 | /// 59 | public string GetOAuthType() 60 | { 61 | var x = Type.ToString() 62 | .ToLowerInvariant() 63 | .ToCharArray(); 64 | x[0] = char.ToUpperInvariant(x[0]); 65 | return new string(x); 66 | } 67 | 68 | /// 69 | /// Verifies if the token is somewhat 70 | /// 71 | /// 72 | public bool IsValidToken() 73 | { 74 | if (Token == null) 75 | { 76 | return false; 77 | } 78 | 79 | var segments = Token.Split('.'); 80 | if (segments.Length != 3) 81 | { 82 | return false; 83 | } 84 | 85 | return true; 86 | } 87 | 88 | /// 89 | public override string ToString() 90 | { 91 | return GetOAuthType() + " " + Token; 92 | } 93 | 94 | /// 95 | /// Implicitely takes a string and parses it into a token. 96 | /// 97 | public static implicit operator DiscordToken(string token) 98 | => new DiscordToken(token); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Miki.Discord.Common/EmbedBuilder.cs: -------------------------------------------------------------------------------- 1 | using Miki.Discord.Common; 2 | using Miki.Discord.Rest; 3 | using System.Collections.Generic; 4 | 5 | namespace Miki.Discord 6 | { 7 | public class EmbedBuilder 8 | { 9 | private readonly DiscordEmbed embed = new DiscordEmbed(); 10 | 11 | public EmbedAuthor Author 12 | { 13 | get => embed.Author; 14 | set => embed.Author = value; 15 | } 16 | 17 | public Color Color 18 | { 19 | get => new Color(embed.Color ?? 0U); 20 | set => embed.Color = value.Value; 21 | } 22 | 23 | public string Description 24 | { 25 | get => embed.Description; 26 | set => embed.Description = value; 27 | } 28 | 29 | public EmbedFooter Footer 30 | { 31 | get => embed.Footer; 32 | set => embed.Footer = value; 33 | } 34 | 35 | public string ImageUrl 36 | { 37 | get => embed.Image?.Url ?? null; 38 | set => embed.Image = new EmbedImage() { Url = value }; 39 | } 40 | 41 | public string ThumbnailUrl 42 | { 43 | get => embed.Thumbnail?.Url ?? null; 44 | set => embed.Thumbnail = new EmbedImage() { Url = value }; 45 | } 46 | 47 | public string Title 48 | { 49 | get => embed.Title; 50 | set => embed.Title = value; 51 | } 52 | 53 | public EmbedBuilder AddField(string title, object content, bool isInline = false) 54 | { 55 | if(embed.Fields == null) 56 | { 57 | embed.Fields = new List(); 58 | } 59 | 60 | embed.Fields.Add(new EmbedField() 61 | { 62 | Title = title, 63 | Content = content.ToString(), 64 | Inline = isInline 65 | }); 66 | 67 | return this; 68 | } 69 | 70 | public EmbedBuilder AddInlineField(string title, string content) 71 | => AddField(title, content, true); 72 | 73 | public EmbedBuilder SetAuthor(string name, string iconUrl = null, string url = null) 74 | { 75 | embed.Author = new EmbedAuthor() 76 | { 77 | Name = name, 78 | IconUrl = iconUrl, 79 | Url = url 80 | }; 81 | return this; 82 | } 83 | 84 | public EmbedBuilder SetColor(int r, int g, int b) 85 | { 86 | embed.Color = new Color(r, g, b).Value; 87 | return this; 88 | } 89 | 90 | public EmbedBuilder SetColor(float r, float g, float b) 91 | { 92 | return SetColor(new Color(r, g, b)); 93 | } 94 | 95 | public EmbedBuilder SetColor(Color color) 96 | { 97 | embed.Color = color.Value; 98 | return this; 99 | } 100 | 101 | public EmbedBuilder SetDescription(string description) 102 | { 103 | if(!string.IsNullOrWhiteSpace(description)) 104 | { 105 | embed.Description = description; 106 | } 107 | return this; 108 | } 109 | 110 | public EmbedBuilder SetFooter(string text, string url = "") 111 | { 112 | embed.Footer = new EmbedFooter() 113 | { 114 | Text = text, 115 | IconUrl = url 116 | }; 117 | 118 | return this; 119 | } 120 | 121 | public EmbedBuilder SetImage(string url) 122 | { 123 | embed.Image = new EmbedImage() 124 | { 125 | Url = url 126 | }; 127 | 128 | return this; 129 | } 130 | 131 | public EmbedBuilder SetTitle(string title) 132 | { 133 | embed.Title = title; 134 | return this; 135 | } 136 | 137 | public EmbedBuilder SetThumbnail(string url) 138 | { 139 | embed.Thumbnail = new EmbedImage() 140 | { 141 | Url = url 142 | }; 143 | 144 | return this; 145 | } 146 | 147 | public DiscordEmbed ToEmbed() 148 | { 149 | return embed; 150 | } 151 | } 152 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Extensions/EventExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using System.Threading.Tasks; 4 | 5 | namespace Miki.Discord.Common 6 | { 7 | public static class EventExtensions 8 | { 9 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 10 | public static Task InvokeAsync(this Func func, T arg) 11 | { 12 | return func != null ? func(arg) : Task.CompletedTask; 13 | } 14 | 15 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 16 | public static Task InvokeAsync(this Func func, T1 arg1, T2 arg2) 17 | { 18 | return func != null ? func(arg1, arg2) : Task.CompletedTask; 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Extensions/ReactiveExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reactive; 3 | using System.Reactive.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace Miki.Discord.Common 7 | { 8 | public static class ReactiveExtensions 9 | { 10 | /// 11 | /// Subscribes with an asynchronous task and handles errors in case of error. 12 | /// 13 | public static IDisposable SubscribeTask( 14 | this IObservable source, 15 | Func onNext, 16 | Func onError = default) 17 | { 18 | return source 19 | .Select(e => Observable.FromAsync(x => onNext(e)) 20 | .Catch(err => 21 | { 22 | if (onError != default) 23 | { 24 | onError(err); 25 | } 26 | return Observable.Return(Unit.Default); 27 | })) 28 | .Concat() 29 | .Subscribe(); 30 | } 31 | 32 | /// 33 | /// Filters all members that are null or default. 34 | /// 35 | /// 36 | /// 37 | /// 38 | public static IObservable WhereNotNull(this IObservable source) 39 | => source.Where(x => !x.Equals(default)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Miki.Discord.Common/Gateway/GatewayConnectionObject.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | 3 | namespace Miki.Discord.Common.Gateway 4 | { 5 | /// 6 | /// Gateway bot connection recommended amount. 7 | /// 8 | public class GatewayConnectionPacket 9 | { 10 | /// 11 | /// Websocket URL to connect to. 12 | /// 13 | [DataMember(Name = "url")] 14 | public string Url; 15 | 16 | /// 17 | /// Recommended amount of shards to connect. 18 | /// 19 | [DataMember(Name = "shards")] 20 | public int ShardCount; 21 | 22 | /// 23 | /// Session limits. 24 | /// 25 | [DataMember(Name = "session_start_limit")] 26 | public GatewaySessionLimitsPacket SessionLimit; 27 | } 28 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Gateway/GatewayMessage.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Miki.Discord.Common.Gateway 5 | { 6 | /// 7 | /// A message payload wrapping events received from the Discord gateway. 8 | /// 9 | [DataContract] 10 | public struct GatewayMessage 11 | { 12 | /// 13 | /// Gateway message type. Can be instructions for the gateway to follow, or events. 14 | /// 15 | [JsonPropertyName("op")] 16 | [DataMember(Name = "op", Order = 1)] 17 | public GatewayOpcode? OpCode { get; set; } 18 | 19 | /// 20 | /// Data modelled for each . 21 | /// 22 | [JsonPropertyName("d")] 23 | [DataMember(Name = "d", Order = 2)] 24 | public object Data { get; set; } 25 | 26 | /// 27 | /// Sequence number, should increase linearly. 28 | /// 29 | [JsonPropertyName("s")] 30 | [DataMember(Name = "s", Order = 3)] 31 | public int? SequenceNumber { get; set; } 32 | 33 | /// 34 | /// If is , an event name is attached 35 | /// for the user to parse with. 36 | /// 37 | [JsonPropertyName("t")] 38 | [DataMember(Name = "t", Order = 4)] 39 | public string EventName { get; set; } 40 | } 41 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Gateway/GatewayOpCode.cs: -------------------------------------------------------------------------------- 1 | namespace Miki.Discord.Common.Gateway 2 | { 3 | /// 4 | /// Gateway payload opcodes. Discord documentation page: 5 | /// 6 | /// Gateway - Opcodes and status codes. 7 | /// 8 | public enum GatewayOpcode 9 | { 10 | /// 11 | /// Discord events being dispatched to the bot. 12 | /// Receive only 13 | /// 14 | Dispatch = 0, 15 | 16 | /// 17 | /// Opcode to send discord's gateway a heartbeat. If properly sent, the gateway returns with 18 | /// . Is required to keep the connection alive. 19 | /// Send/Receive 20 | /// 21 | Heartbeat = 1, 22 | 23 | Identify = 2, 24 | 25 | StatusUpdate = 3, 26 | 27 | VoiceStateUpdate = 4, 28 | 29 | VoiceServerPing = 5, 30 | 31 | Resume = 6, 32 | 33 | /// 34 | /// Opcode to instruct the gateway implementation to reconnect to the gateway immediately. You 35 | /// are allowed to afterwards. 36 | /// Receive only 37 | /// 38 | Reconnect = 7, 39 | 40 | RequestGuildMembers = 8, 41 | 42 | InvalidSession = 9, 43 | 44 | Hello = 10, 45 | 46 | /// 47 | /// Opcode for the Discord gateway to acknowledge your latest heartbeat. 48 | /// Receive only 49 | /// 50 | HeartbeatAcknowledge = 11 51 | } 52 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Gateway/GatewaySessionLimitsPacket.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Miki.Discord.Common.Gateway 5 | { 6 | /// 7 | /// Session limits 8 | /// 9 | [DataContract] 10 | public class GatewaySessionLimitsPacket 11 | { 12 | /// 13 | /// Total amount of reconnects for the time until . 14 | /// 15 | [JsonPropertyName("total")] 16 | [DataMember(Name = "total")] 17 | public int Total { get; set; } 18 | 19 | /// 20 | /// Total sum of reconnect calls available to use. 21 | /// 22 | [JsonPropertyName("remaining")] 23 | [DataMember(Name = "remaining")] 24 | public int Remaining { get; set; } 25 | 26 | /// 27 | /// Milliseconds until this session refreshes. 28 | /// 29 | [JsonPropertyName("reset_after")] 30 | [DataMember(Name = "reset_after")] 31 | public int ResetAfter { get; set; } 32 | } 33 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Gateway/IGatewayApiClient.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Miki.Discord.Common.Gateway 4 | { 5 | public interface IGatewayApiClient 6 | { 7 | Task GetGatewayAsync(); 8 | 9 | Task GetGatewayBotAsync(); 10 | } 11 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Helpers/CacheHelpers.cs: -------------------------------------------------------------------------------- 1 | namespace Miki.Discord.Common 2 | { 3 | /// 4 | /// Helper class for cache related operations in Miki.Discord. 5 | /// 6 | public static class CacheHelpers 7 | { 8 | /// 9 | /// Returns a DM channel cache key collection 10 | /// 11 | /// 12 | public static string ChannelsKey(ulong? guildId = null) 13 | { 14 | if(guildId.HasValue) 15 | { 16 | return $"{GuildsCacheKey}:channels:{guildId}"; 17 | } 18 | 19 | return $"discord:dmchannels"; 20 | } 21 | 22 | /// 23 | /// Returns a user collection cache key 24 | /// 25 | public const string UsersCacheKey = "discord:users"; 26 | 27 | /// 28 | /// Guilds collection cache key. 29 | /// 30 | public const string GuildsCacheKey = "discord:guilds"; 31 | 32 | /// 33 | /// Guild members cache key, indexes all members per guild. 34 | /// 35 | public static string GuildMembersKey(ulong guildId) 36 | => $"{GuildsCacheKey}:members:{guildId}"; 37 | 38 | /// 39 | /// Guild roles cache key, indexes all roles per guild. 40 | /// 41 | public static string GuildRolesKey(ulong guildId) 42 | => $"{GuildsCacheKey}:roles:{guildId}"; 43 | 44 | /// 45 | /// Guild presences cache key, indexes all presences per guild. 46 | /// 47 | public static string GuildPresencesKey() 48 | => $"{UsersCacheKey}:presences"; 49 | } 50 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Helpers/DiscordHelpers.cs: -------------------------------------------------------------------------------- 1 | using Miki.Discord.Common.Packets; 2 | using System; 3 | 4 | namespace Miki.Discord.Common 5 | { 6 | /// 7 | /// Helper methods and properties for Discord related objects. 8 | /// 9 | public static class DiscordHelpers 10 | { 11 | /// 12 | /// API base path. 13 | /// 14 | public const string BasePath = "/api/v6"; 15 | 16 | /// 17 | /// Discord's snowflake start epoch. Used to find the creation date of a snowflake. 18 | /// 19 | public const long DiscordEpoch = 1420070400000; 20 | 21 | /// 22 | /// Discord base URL. Links to the website. 23 | /// 24 | public const string DiscordUrl = "https://discord.com"; 25 | 26 | /// 27 | /// CDN base URL. Used to fetch resources from Discord's CDN. 28 | /// 29 | public const string CdnUrl = "https://cdn.discordapp.com"; 30 | 31 | /// 32 | /// Helper function to automatically get either custom or default avatar based on the 33 | /// values received from a . 34 | /// 35 | public static string GetAvatarUrl( 36 | DiscordUserPacket packet, 37 | ImageType type = ImageType.AUTO, 38 | ImageSize size = ImageSize.x256) 39 | { 40 | if(packet.Avatar != null) 41 | { 42 | return GetAvatarUrl(packet.Id, packet.Avatar, type, size); 43 | } 44 | return GetAvatarUrl(packet.Discriminator); 45 | } 46 | 47 | /// 48 | /// Gets user's custom avatar URL. 49 | /// 50 | public static string GetAvatarUrl( 51 | ulong id, 52 | string hash, 53 | ImageType imageType = ImageType.AUTO, 54 | ImageSize size = ImageSize.x256) 55 | { 56 | if(imageType == ImageType.AUTO) 57 | { 58 | imageType = hash.StartsWith("a_") 59 | ? ImageType.GIF 60 | : ImageType.PNG; 61 | } 62 | 63 | return $"{CdnUrl}/avatars/{id}/{hash}.{imageType.ToString().ToLower()}?size={(int)size}"; 64 | } 65 | 66 | /// 67 | /// Gets the default Discord avatars based on the user's discriminator. 68 | /// 69 | public static string GetAvatarUrl(short discriminator) 70 | => $"{CdnUrl}/embed/avatars/{discriminator % 5}.png"; 71 | 72 | /// 73 | /// Get a guild icon URL 74 | /// 75 | public static string GetIconUrl( 76 | DiscordGuildPacket packet, 77 | ImageType type = ImageType.AUTO, 78 | ImageSize size = ImageSize.x128) 79 | { 80 | if (type == ImageType.AUTO) 81 | { 82 | type = packet.Icon.StartsWith("a_") 83 | ? ImageType.GIF 84 | : ImageType.PNG; 85 | } 86 | 87 | var imgType = type.ToString().ToLowerInvariant(); 88 | return $"{CdnUrl}/icons/{packet.Id}/{packet.Icon}.{imgType}?size={(int)size}"; 89 | } 90 | 91 | /// 92 | /// Gets the time of creation for a snowflake. 93 | /// 94 | public static DateTime GetCreationTime(this ISnowflake snowflake) 95 | { 96 | return DateTimeOffset.FromUnixTimeSeconds(DiscordEpoch) 97 | .AddMilliseconds(snowflake.Id >> 22) 98 | .UtcDateTime; 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/IDiscordClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Threading.Tasks; 5 | using Microsoft.Extensions.Hosting; 6 | using Miki.Discord.Events; 7 | 8 | namespace Miki.Discord.Common 9 | { 10 | public interface IDiscordClient : IDisposable, IHostedService 11 | { 12 | /// 13 | /// The api client used in the discord client and was given in at the beginning. 14 | /// 15 | IApiClient ApiClient { get; } 16 | 17 | /// 18 | /// The gateway client used in the discord client and was given in at the beginning. 19 | /// 20 | IGateway Gateway { get; } 21 | 22 | /// 23 | /// Object containing all Discord gateway events. 24 | /// 25 | IDiscordEvents Events { get; } 26 | 27 | Task EditMessageAsync(ulong channelId, ulong messageId, string text, DiscordEmbed embed = null); 28 | 29 | Task CreateDMAsync(ulong userid); 30 | 31 | Task CreateRoleAsync(ulong guildId, CreateRoleArgs args = null); 32 | 33 | Task EditRoleAsync(ulong guildId, DiscordRolePacket role); 34 | 35 | Task GetUserPresence(ulong userId, ulong? guildId = null); 36 | 37 | Task GetRoleAsync(ulong guildId, ulong roleId); 38 | 39 | Task> GetRolesAsync(ulong guildId); 40 | 41 | Task> GetChannelsAsync(ulong guildId); 42 | 43 | Task GetChannelAsync(ulong id, ulong? guildId = null); 44 | 45 | Task GetSelfAsync(); 46 | 47 | Task GetGuildAsync(ulong id); 48 | 49 | Task GetGuildUserAsync(ulong id, ulong guildId); 50 | 51 | Task> GetGuildUsersAsync(ulong guildId); 52 | 53 | Task> GetReactionsAsync(ulong channelId, ulong messageId, DiscordEmoji emoji); 54 | 55 | Task GetUserAsync(ulong id); 56 | 57 | Task SetGameAsync(int shardId, DiscordStatus status); 58 | 59 | /// 60 | /// Sends a file from containing . 61 | /// 62 | Task SendFileAsync(ulong channelId, Stream stream, string fileName, MessageArgs message = null); 63 | 64 | /// 65 | /// Sends a message to . 66 | /// 67 | Task SendMessageAsync(ulong channelId, MessageArgs message); 68 | 69 | /// 70 | /// Sends a message to . 71 | /// 72 | Task SendMessageAsync(ulong channelId, string text, DiscordEmbed embed = null); 73 | } 74 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/IDiscordEvents.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Miki.Discord.Common; 3 | using Miki.Discord.Common.Packets.Events; 4 | 5 | namespace Miki.Discord.Events 6 | { 7 | public interface IDiscordEvents : IDisposable 8 | { 9 | IObservable ChannelCreate { get; } 10 | IObservable ChannelDelete { get; } 11 | IObservable ChannelUpdate { get; } 12 | 13 | /// 14 | /// Emits events for guilds that were previously unavailable that have been loaded now. 15 | /// 16 | IObservable GuildAvailable { get; } 17 | 18 | /// 19 | /// Raw guild create call, will respond with every server. 20 | /// 21 | IObservable GuildCreate { get; } 22 | 23 | /// 24 | /// Emits events when the bot user joins a new guild. 25 | /// 26 | IObservable GuildJoin { get; } 27 | IObservable GuildLeave { get; } 28 | IObservable GuildUpdate { get; } 29 | IObservable GuildUnavailable { get; } 30 | IObservable GuildEmojiUpdate { get; } 31 | IObservable GuildMemberCreate { get; } 32 | IObservable GuildMemberDelete { get; } 33 | IObservable GuildMemberUpdate { get; } 34 | IObservable GuildRoleCreate { get; } 35 | IObservable GuildRoleDelete { get; } 36 | IObservable GuildRoleUpdate { get; } 37 | IObservable MessageCreate { get; } 38 | IObservable MessageDelete { get; } 39 | IObservable MessageUpdate { get; } 40 | IObservable MessageReactionCreate { get; } 41 | IObservable MessageReactionDelete { get; } 42 | IObservable PresenceUpdate { get; } 43 | IObservable TypingStart { get; } 44 | IObservable UserUpdate { get; } 45 | 46 | void SubscribeTo(IGateway gateway); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Miki.Discord.Common/IGateway.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Miki.Discord.Common.Gateway; 3 | using System.Threading.Tasks; 4 | using Microsoft.Extensions.Hosting; 5 | 6 | namespace Miki.Discord.Common 7 | { 8 | public interface IGateway : IHostedService 9 | { 10 | IObservable PacketReceived { get; } 11 | 12 | IGatewayEvents Events { get; } 13 | 14 | Task RestartAsync(); 15 | 16 | Task SendAsync(int shardId, GatewayOpcode opcode, object payload); 17 | } 18 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/IGatewayEvents.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Miki.Discord.Common.Events; 3 | using Miki.Discord.Common.Gateway; 4 | using Miki.Discord.Common.Packets; 5 | using Miki.Discord.Common.Packets.API; 6 | using Miki.Discord.Common.Packets.Events; 7 | 8 | namespace Miki.Discord.Common 9 | { 10 | public interface IGatewayEvents 11 | { 12 | IObservable ChannelCreate { get; } 13 | IObservable ChannelDelete { get; } 14 | IObservable ChannelUpdate { get; } 15 | IObservable GuildCreate { get; } 16 | IObservable GuildDelete { get; } 17 | IObservable GuildUpdate { get; } 18 | IObservable GuildEmojiUpdate { get; } 19 | IObservable GuildMemberCreate { get; } 20 | IObservable GuildMemberDelete { get; } 21 | IObservable GuildMemberUpdate { get; } 22 | IObservable GuildRoleCreate { get; } 23 | IObservable GuildRoleDelete { get; } 24 | IObservable GuildRoleUpdate { get; } 25 | IObservable MessageCreate { get; } 26 | IObservable MessageDelete { get; } 27 | IObservable MessageUpdate { get; } 28 | IObservable MessageReactionCreate { get; } 29 | IObservable MessageReactionDelete { get; } 30 | IObservable PresenceUpdate { get; } 31 | IObservable Ready { get; } 32 | IObservable TypingStart { get; } 33 | IObservable UserUpdate { get; } 34 | } 35 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Miki.Discord.Common.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1 5 | false 6 | 4.0.0-rc.6 7 | Velddev 8 | false 9 | velddev.pfx 10 | Common type and helper library for Miki.Discord.* libraries 11 | 12 | Velddev 13 | Miki 14 | https://github.com/mikibot/miki.discord 15 | Discord Bot Miki 16 | 17 | LICENSE 18 | mikibot 19 | icon.png 20 | 21 | 22 | 23 | latest 24 | 25 | 26 | 27 | 28 | latest 29 | D:\Projects\Miki.Discord\Miki.Discord.Common\Miki.Discord.Common.xml 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | True 38 | 39 | 40 | 41 | True 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /Miki.Discord.Common/Miki.Discord.Common.csproj.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True 3 | True 4 | True 5 | True 6 | True -------------------------------------------------------------------------------- /Miki.Discord.Common/Models/IContainsGuild.cs: -------------------------------------------------------------------------------- 1 | namespace Miki.Discord.Common.Models 2 | { 3 | using System.Threading.Tasks; 4 | 5 | /// 6 | /// This member has a guild connection. 7 | /// 8 | public interface IContainsGuild 9 | { 10 | ulong GuildId { get; } 11 | 12 | Task GetGuildAsync(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Miki.Discord.Common/Models/IDiscordAttachment.cs: -------------------------------------------------------------------------------- 1 | namespace Miki.Discord.Common 2 | { 3 | public interface IDiscordAttachment 4 | { 5 | /// 6 | /// Full name of the file attached. 7 | /// 8 | string FileName { get; } 9 | 10 | /// 11 | /// The height of the file (if the attachment is an image). 12 | /// 13 | int? Height { get; } 14 | 15 | /// 16 | /// The attachment Id. 17 | /// 18 | ulong Id { get; } 19 | 20 | /// 21 | /// The proxy version of the Url. 22 | /// 23 | string ProxyUrl { get; } 24 | 25 | /// 26 | /// The size of the file in bytes. 27 | /// 28 | int Size { get; } 29 | 30 | /// 31 | /// The source url of the attachment. 32 | /// 33 | string Url { get; } 34 | 35 | /// 36 | /// The width of the file (if the attachment is an image). 37 | /// 38 | int? Width { get; } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Miki.Discord.Common/Models/IDiscordChannel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | 4 | namespace Miki.Discord.Common 5 | { 6 | public interface IDiscordChannel : ISnowflake 7 | { 8 | bool IsNsfw { get; } 9 | 10 | string Name { get; } 11 | 12 | Task DeleteAsync(); 13 | 14 | Task ModifyAsync(object todo); 15 | } 16 | 17 | public interface IDiscordGuildChannel : IDiscordChannel 18 | { 19 | IEnumerable PermissionOverwrites { get; } 20 | 21 | ulong GuildId { get; } 22 | 23 | ChannelType Type { get; } 24 | 25 | Task GetPermissionsAsync(IDiscordGuildUser user = null); 26 | 27 | Task GetUserAsync(ulong id); 28 | 29 | /// 30 | /// Gets the current user in the guild. 31 | /// 32 | Task GetSelfAsync(); 33 | 34 | Task GetGuildAsync(); 35 | } 36 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Models/IDiscordGuildMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Miki.Discord.Common.Models 6 | { 7 | public interface IDiscordGuildMessage : IDiscordMessage, IContainsGuild 8 | { 9 | 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Miki.Discord.Common/Models/IDiscordGuildUser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Miki.Discord.Common.Models; 5 | 6 | namespace Miki.Discord.Common 7 | { 8 | public interface IDiscordGuildUser : IDiscordUser, IContainsGuild 9 | { 10 | string Nickname { get; } 11 | 12 | IReadOnlyCollection RoleIds { get; } 13 | 14 | DateTimeOffset JoinedAt { get; } 15 | 16 | /// 17 | /// This user nitro boosting current 18 | /// 19 | DateTimeOffset? PremiumSince { get; } 20 | 21 | Task AddRoleAsync(IDiscordRole role); 22 | 23 | Task GetHierarchyAsync(); 24 | 25 | Task> GetRolesAsync(); 26 | 27 | Task HasPermissionsAsync(GuildPermission permissions); 28 | 29 | Task KickAsync(string reason = ""); 30 | 31 | Task RemoveRoleAsync(IDiscordRole role); 32 | } 33 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Models/IDiscordMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Miki.Discord.Common.Packets.API; 5 | 6 | namespace Miki.Discord.Common 7 | { 8 | public interface IDiscordMessage : ISnowflake 9 | { 10 | /// 11 | /// All attachments attached to this message. 12 | /// 13 | IReadOnlyList Attachments { get; } 14 | 15 | /// 16 | /// The creator of the message. 17 | /// 18 | IDiscordUser Author { get; } 19 | 20 | string Content { get; } 21 | 22 | /// 23 | /// The channel this message was created in. 24 | /// 25 | ulong ChannelId { get; } 26 | 27 | IReadOnlyList MentionedUserIds { get; } 28 | 29 | DateTimeOffset Timestamp { get; } 30 | 31 | DiscordMessageType Type { get; } 32 | 33 | Task CreateReactionAsync(DiscordEmoji emoji); 34 | 35 | Task DeleteReactionAsync(DiscordEmoji emoji); 36 | 37 | Task DeleteReactionAsync(DiscordEmoji emoji, IDiscordUser user); 38 | 39 | Task DeleteReactionAsync(DiscordEmoji emoji, ulong userId); 40 | 41 | Task DeleteAllReactionsAsync(); 42 | 43 | Task EditAsync(EditMessageArgs args); 44 | 45 | /// 46 | /// Deletes this message. 47 | /// 48 | Task DeleteAsync(); 49 | 50 | Task GetChannelAsync(); 51 | 52 | Task> GetReactionsAsync(DiscordEmoji emoji); 53 | } 54 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Models/IDiscordPresence.cs: -------------------------------------------------------------------------------- 1 | using Miki.Discord.Common.Packets; 2 | 3 | namespace Miki.Discord.Common 4 | { 5 | public interface IDiscordPresence 6 | { 7 | DiscordActivity Activity { get; } 8 | UserStatus Status { get; } 9 | } 10 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Models/IDiscordReaction.cs: -------------------------------------------------------------------------------- 1 | using Miki.Discord.Common.Models; 2 | using System.Threading.Tasks; 3 | 4 | namespace Miki.Discord.Common 5 | { 6 | public interface IDiscordGuildReaction : IDiscordReaction, IContainsGuild 7 | {} 8 | 9 | public interface IDiscordReaction 10 | { 11 | /// 12 | /// Channel where the message contained into. 13 | /// 14 | ulong ChannelId { get; } 15 | 16 | /// 17 | /// The emoji information. 18 | /// 19 | DiscordEmoji Emoji { get; } 20 | 21 | /// 22 | /// ID of the message that was reacted on. 23 | /// 24 | ulong MessageId { get; } 25 | 26 | /// 27 | /// User who reacted. 28 | /// 29 | ValueTask GetUserAsync(); 30 | 31 | /// 32 | /// Gets the channel where the message appeared. 33 | /// 34 | ValueTask GetChannelAsync(); 35 | } 36 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Models/IDiscordRole.cs: -------------------------------------------------------------------------------- 1 | using Miki.Discord.Rest; 2 | 3 | namespace Miki.Discord.Common 4 | { 5 | public interface IDiscordRole : ISnowflake 6 | { 7 | /// 8 | /// Name of the role. 9 | /// 10 | string Name { get; } 11 | /// 12 | /// The color attached to the role. 13 | /// 14 | Color Color { get; } 15 | /// 16 | /// The position of the role compared to other roles. 17 | /// 18 | int Position { get; } 19 | /// 20 | /// Permissions attached to the role. 21 | /// 22 | GuildPermission Permissions { get; } 23 | /// 24 | /// Is this role managed by an external service? 25 | /// 26 | bool IsManaged { get; } 27 | /// 28 | /// Is this role hoisted up in the user list? 29 | /// 30 | bool IsHoisted { get; } 31 | /// 32 | /// Can this role be mentioned in a ? 33 | /// 34 | bool IsMentionable { get; } 35 | } 36 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Models/IDiscordSelfUser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Miki.Discord.Common.Packets; 4 | 5 | namespace Miki.Discord.Common 6 | { 7 | public interface IDiscordSelfUser : IDiscordUser 8 | { 9 | /// 10 | /// Gets recent DM channels for the current user. This function does not work on a BOT account. 11 | /// 12 | /// Does not work on a BOT account. 13 | Task GetDMChannelsAsync(); 14 | 15 | /// 16 | /// Modify the current user. 17 | /// 18 | /// 19 | Task ModifyAsync(Action modifyArgs); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Miki.Discord.Common/Models/IDiscordUser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Miki.Discord.Common 5 | { 6 | public interface IDiscordUser : ISnowflake 7 | { 8 | string AvatarId { get; } 9 | 10 | string Mention { get; } 11 | 12 | string Username { get; } 13 | 14 | short Discriminator { get; } 15 | 16 | DateTimeOffset CreatedAt { get; } 17 | 18 | bool IsBot { get; } 19 | 20 | Task GetPresenceAsync(); 21 | 22 | Task GetDMChannelAsync(); 23 | 24 | string GetAvatarUrl(ImageType type = ImageType.AUTO, ImageSize size = ImageSize.x256); 25 | } 26 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Models/IGuildUser.cs: -------------------------------------------------------------------------------- 1 | namespace Miki.Discord.Common 2 | { 3 | internal interface IGuildUser 4 | { 5 | } 6 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Models/ISnowflake.cs: -------------------------------------------------------------------------------- 1 | namespace Miki.Discord.Common 2 | { 3 | public interface ISnowflake 4 | { 5 | ulong Id { get; } 6 | } 7 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Models/ITextChannel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | 5 | namespace Miki.Discord.Common 6 | { 7 | public interface IDiscordTextChannel : IDiscordChannel 8 | { 9 | Task DeleteMessagesAsync(params ulong[] id); 10 | 11 | Task DeleteMessagesAsync(params IDiscordMessage[] id); 12 | 13 | Task> GetMessagesAsync(int amount = 100); 14 | 15 | Task GetMessageAsync(ulong id); 16 | 17 | Task SendMessageAsync(string content, bool isTTS = false, DiscordEmbed embed = null); 18 | 19 | Task SendFileAsync(Stream file, string fileName, string content = null, bool isTTs = false, DiscordEmbed embed = null); 20 | 21 | Task TriggerTypingAsync(); 22 | } 23 | 24 | public enum GetMessageType 25 | { 26 | Around, Before, After 27 | } 28 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/API/ActivityType.cs: -------------------------------------------------------------------------------- 1 | namespace Miki.Discord.Common.Packets 2 | { 3 | /// 4 | /// Current activity type of an 5 | /// 6 | public enum ActivityType 7 | { 8 | Playing, 9 | Streaming, 10 | Listening, 11 | Watching 12 | } 13 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/API/DiscordActivity.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | using System.Text.Json.Serialization; 3 | using Miki.Discord.Common.Packets; 4 | 5 | namespace Miki.Discord.Common 6 | { 7 | [DataContract] 8 | public class DiscordActivity 9 | { 10 | [JsonPropertyName("name")] 11 | [DataMember(Name = "name", Order = 1)] 12 | public string Name { get; set; } 13 | 14 | [JsonPropertyName("type")] 15 | [DataMember(Name = "type", Order = 2)] 16 | public ActivityType Type { get; set; } 17 | 18 | [JsonPropertyName("url")] 19 | [DataMember(Name = "url", Order = 3)] 20 | public string Url { get; set; } 21 | 22 | [JsonPropertyName("timestamps")] 23 | [DataMember(Name = "timestamps", Order = 4)] 24 | public TimeStampsObject Timestamps { get; set; } 25 | 26 | [JsonPropertyName("application_id")] 27 | [DataMember(Name = "application_id", Order = 5)] 28 | public ulong? ApplicationId { get; set; } 29 | 30 | [JsonPropertyName("state")] 31 | [DataMember(Name = "state", Order = 6)] 32 | public string State { get; set; } 33 | 34 | [JsonPropertyName("details")] 35 | [DataMember(Name = "details", Order = 7)] 36 | public string Details { get; set; } 37 | 38 | [JsonPropertyName("party")] 39 | [DataMember(Name = "party", Order = 8)] 40 | public RichPresenceParty Party { get; set; } 41 | 42 | [JsonPropertyName("assets")] 43 | [DataMember(Name = "assets", Order = 9)] 44 | public RichPresenceAssets Assets { get; set; } 45 | } 46 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/API/DiscordAttachmentPacket.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Miki.Discord.Common 5 | { 6 | [DataContract] 7 | public class DiscordAttachmentPacket 8 | { 9 | [JsonPropertyName("id")] 10 | [DataMember(Name = "id", Order = 1)] 11 | public ulong Id { get; set; } 12 | 13 | [JsonPropertyName("filename")] 14 | [DataMember(Name = "filename", Order = 2)] 15 | public string FileName { get; set; } 16 | 17 | [JsonPropertyName("size")] 18 | [DataMember(Name = "size", Order = 3)] 19 | public int Size { get; set; } 20 | 21 | [JsonPropertyName("url")] 22 | [DataMember(Name = "url", Order = 4)] 23 | public string Url { get; set; } 24 | 25 | [JsonPropertyName("proxy_url")] 26 | [DataMember(Name = "proxy_url", Order = 5)] 27 | public string ProxyUrl { get; set; } 28 | 29 | [JsonPropertyName("height")] 30 | [DataMember(Name = "height", Order = 6)] 31 | public int? Height { get; set; } 32 | 33 | [JsonPropertyName("width")] 34 | [DataMember(Name = "width", Order = 7)] 35 | public int? Width { get; set; } 36 | 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/API/DiscordChannelPacket.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace Miki.Discord.Common 6 | { 7 | [Serializable] 8 | [DataContract] 9 | public sealed class DiscordChannelPacket 10 | { 11 | [JsonPropertyName("id")] 12 | [DataMember(Name = "id", Order = 1)] 13 | public ulong Id { get; set; } 14 | 15 | [JsonPropertyName("type")] 16 | [DataMember(Name = "type", Order = 2)] 17 | public ChannelType Type { get; set; } 18 | 19 | [JsonPropertyName("created_at")] 20 | [DataMember(Name = "created_at", Order = 3)] 21 | public long CreatedAt { get; set; } 22 | 23 | [JsonPropertyName("name")] 24 | [DataMember(Name = "name", Order = 4)] 25 | public string Name { get; set; } 26 | 27 | [JsonPropertyName("guild_id")] 28 | [DataMember(Name = "guild_id", Order = 5)] 29 | public ulong? GuildId { get; set; } 30 | 31 | [JsonPropertyName("position")] 32 | [DataMember(Name = "position", Order = 6)] 33 | public int? Position { get; set; } 34 | 35 | [JsonPropertyName("permission_overwrites")] 36 | [DataMember(Name = "permission_overwrites", Order = 7)] 37 | public PermissionOverwrite[] PermissionOverwrites { get; set; } 38 | 39 | [JsonPropertyName("parent_id")] 40 | [DataMember(Name = "parent_id", Order = 8)] 41 | public ulong? ParentId { get; set; } 42 | 43 | [JsonPropertyName("nsfw")] 44 | [DataMember(Name = "nsfw", Order = 9)] 45 | public bool? IsNsfw { get; set; } 46 | 47 | [JsonPropertyName("topic")] 48 | [DataMember(Name = "topic", Order = 10)] 49 | public string Topic { get; set; } 50 | } 51 | 52 | public enum ChannelType 53 | { 54 | /// 55 | /// A text channel within a Discord server. 56 | /// 57 | GuildText = 0, 58 | 59 | /// 60 | /// A Direct Message channel with another user. 61 | /// 62 | DirectText, 63 | 64 | /// 65 | /// A voice channel. 66 | /// 67 | GuildVoice, 68 | 69 | /// 70 | /// A Group Direct Message channel with multiple users. 71 | /// 72 | GroupDirect, 73 | 74 | /// 75 | /// A server category 76 | /// 77 | GuildCategory, 78 | 79 | /// 80 | /// A news channel which allows users to cross-post their message. 81 | /// 82 | GuildNews, 83 | 84 | /// 85 | /// A game store channel to sell games on Discord. 86 | /// 87 | GuildStore 88 | } 89 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/API/DiscordEmbed.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Runtime.Serialization; 4 | using System.Text.Json.Serialization; 5 | 6 | namespace Miki.Discord.Common 7 | { 8 | [Serializable] 9 | [DataContract] 10 | public sealed class DiscordEmbed 11 | { 12 | [JsonPropertyName("title")] 13 | [DataMember(Name = "title")] 14 | public string Title { get; set; } 15 | 16 | [JsonPropertyName("description")] 17 | [DataMember(Name = "description")] 18 | public string Description { get; set; } 19 | 20 | [JsonPropertyName("color")] 21 | [DataMember(Name = "color")] 22 | public uint? Color { get; set; } 23 | 24 | [JsonPropertyName("fields")] 25 | [DataMember(Name = "fields")] 26 | public List Fields { get; set; } 27 | 28 | [JsonPropertyName("author")] 29 | [DataMember(Name = "author")] 30 | public EmbedAuthor Author { get; set; } 31 | 32 | [JsonPropertyName("footer")] 33 | [DataMember(Name = "footer")] 34 | public EmbedFooter Footer { get; set; } 35 | 36 | [JsonPropertyName("thumbnail")] 37 | [DataMember(Name = "thumbnail")] 38 | public EmbedImage Thumbnail { get; set; } 39 | 40 | [JsonPropertyName("image")] 41 | [DataMember(Name = "image")] 42 | public EmbedImage Image { get; set; } 43 | } 44 | 45 | [DataContract] 46 | public class EmbedAuthor 47 | { 48 | [JsonPropertyName("name")] 49 | [DataMember(Name = "name")] 50 | public string Name { get; set; } 51 | 52 | [JsonPropertyName("icon_url")] 53 | [DataMember(Name = "icon_url")] 54 | public string IconUrl { get; set; } 55 | 56 | [JsonPropertyName("url")] 57 | [DataMember(Name = "url")] 58 | public string Url { get; set; } 59 | } 60 | 61 | [DataContract] 62 | public class EmbedFooter 63 | { 64 | [JsonPropertyName("icon_url")] 65 | [DataMember(Name = "icon_url")] 66 | public string IconUrl { get; set; } 67 | 68 | [JsonPropertyName("text")] 69 | [DataMember(Name = "text")] 70 | public string Text { get; set; } 71 | } 72 | 73 | [DataContract] 74 | public class EmbedImage 75 | { 76 | [JsonPropertyName("url")] 77 | [DataMember(Name = "url")] 78 | public string Url { get; set; } 79 | } 80 | 81 | [DataContract] 82 | public class EmbedField 83 | { 84 | [JsonPropertyName("name")] 85 | [DataMember(Name = "name")] 86 | public string Title { get; set; } 87 | 88 | [JsonPropertyName("value")] 89 | [DataMember(Name = "value")] 90 | public string Content { get; set; } 91 | 92 | [JsonPropertyName("inline")] 93 | [DataMember(Name = "inline")] 94 | public bool Inline { get; set; } = false; 95 | } 96 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/API/DiscordEmoji.cs: -------------------------------------------------------------------------------- 1 | using Miki.Discord.Common.Packets; 2 | using System.Collections.Generic; 3 | using System.Runtime.Serialization; 4 | using System.Text.Json.Serialization; 5 | 6 | namespace Miki.Discord.Common 7 | { 8 | /// 9 | /// Discord Emoji payload. 10 | /// 11 | [DataContract] 12 | public class DiscordEmoji 13 | { 14 | /// 15 | /// Empty constructor for modern builder pattern. 16 | /// 17 | public DiscordEmoji() { } 18 | 19 | /// 20 | /// Creates an unicode emoji. 21 | /// new DiscordEmoji("🚀"); 22 | /// 23 | public DiscordEmoji(string unicode) 24 | { 25 | Name = unicode; 26 | } 27 | 28 | /// 29 | /// The Discord ID belonging to this emoji, if this value is set it means it's a custom Discord 30 | /// emoji. 31 | /// 32 | [JsonPropertyName("id")] 33 | [DataMember(Name = "id", Order = 1)] 34 | public ulong? Id { get; set; } 35 | 36 | /// 37 | /// Emoji name or unicode. 38 | /// 39 | [JsonPropertyName("name")] 40 | [DataMember(Name = "name", Order = 2)] 41 | public string Name { get; set; } 42 | 43 | [JsonPropertyName("roles")] 44 | [DataMember(Name = "roles", Order = 3)] 45 | public List WhitelistedRoles { get; set; } 46 | 47 | [JsonPropertyName("user")] 48 | [DataMember(Name = "user", Order = 4)] 49 | public DiscordUserPacket Creator { get; set; } 50 | 51 | [JsonPropertyName("require_colons")] 52 | [DataMember(Name = "require_colons", Order = 5)] 53 | public bool? RequireColons { get; set; } 54 | 55 | [JsonPropertyName("managed")] 56 | [DataMember(Name = "managed", Order = 6)] 57 | public bool? Managed { get; set; } 58 | 59 | /// 60 | /// Checks if the emoji is animated. 61 | /// 62 | [JsonPropertyName("animated")] 63 | [DataMember(Name = "animated", Order = 7)] 64 | public bool? Animated { get; set; } 65 | 66 | /// 67 | /// Parses an discord emoji from either a mention. 68 | /// 69 | /// 70 | /// 71 | /// 72 | public static bool TryParse(string text, out DiscordEmoji emoji) 73 | { 74 | if(Mention.TryParse(text, out Mention mention)) 75 | { 76 | if(mention.Type == MentionType.EMOJI 77 | || mention.Type == MentionType.ANIMATED_EMOJI) 78 | { 79 | emoji = new DiscordEmoji 80 | { 81 | Id = mention.Id, 82 | Name = mention.Data, 83 | Animated = mention.Type == MentionType.ANIMATED_EMOJI 84 | }; 85 | return true; 86 | } 87 | } 88 | 89 | emoji = null; 90 | return false; 91 | } 92 | 93 | /// 94 | public override string ToString() 95 | { 96 | if(Id.HasValue) 97 | { 98 | return $"<{(Animated ?? false ? "a" : "")}:{Name}:{Id}>"; 99 | } 100 | return Name; 101 | } 102 | } 103 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/API/DiscordGuildMemberPacket.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Runtime.Serialization; 5 | using System.Text.Json.Serialization; 6 | using Miki.Discord.Common.Packets; 7 | 8 | namespace Miki.Discord.Common 9 | { 10 | [DataContract] 11 | public class DiscordGuildMemberPacket 12 | { 13 | [JsonPropertyName("user")] 14 | [DataMember(Name = "user", Order = 1)] 15 | public DiscordUserPacket User { get; set; } 16 | 17 | [JsonPropertyName("guild_id")] 18 | [DataMember(Name = "guild_id", Order = 2)] 19 | public ulong GuildId { get; set; } 20 | 21 | [JsonPropertyName("nick")] 22 | [DataMember(Name = "nick", Order = 3)] 23 | public string Nickname { get; set; } 24 | 25 | [JsonPropertyName("roles")] 26 | [DataMember(Name = "roles", Order = 4)] 27 | public List Roles { get; set; } = new List(); 28 | 29 | [JsonPropertyName("joined_at")] 30 | [DataMember(Name = "joined_at", Order = 5)] 31 | public DateTime JoinedAt { get; set; } 32 | 33 | [JsonPropertyName("deaf")] 34 | [DataMember(Name = "deaf", Order = 6)] 35 | public bool Deafened { get; set; } 36 | 37 | [JsonPropertyName("mute")] 38 | [DataMember(Name = "mute", Order = 7)] 39 | public bool Muted { get; set; } 40 | 41 | [JsonPropertyName("premium_since")] 42 | [DataMember(Name = "premium_since", Order = 8)] 43 | public DateTime? PremiumSince { get; set; } 44 | } 45 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/API/DiscordGuildUnavailablePacket.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Miki.Discord.Common.Packets 5 | { 6 | [DataContract] 7 | public class DiscordGuildUnavailablePacket 8 | { 9 | [JsonPropertyName("id")] 10 | [DataMember(Name = "id")] 11 | public ulong GuildId { get; set; } 12 | 13 | [JsonPropertyName("unavailable")] 14 | [DataMember(Name = "unavailable")] 15 | public bool? IsUnavailable { get; set; } 16 | 17 | /// 18 | /// A converter method to avoid protocol buffer serialization complexion 19 | /// 20 | /// A converted DiscordGuildPacket 21 | public DiscordGuildPacket ToGuildPacket() 22 | { 23 | return new DiscordGuildPacket 24 | { 25 | Id = GuildId, 26 | Unavailable = IsUnavailable 27 | }; 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/API/DiscordMessagePacket.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Runtime.Serialization; 4 | using System.Text.Json.Serialization; 5 | 6 | namespace Miki.Discord.Common.Packets.API 7 | { 8 | [Serializable] 9 | [DataContract] 10 | public class DiscordMessagePacket 11 | { 12 | [JsonPropertyName("id")] 13 | [DataMember(Name = "id")] 14 | public ulong Id { get; set; } 15 | 16 | [JsonPropertyName("channel_id")] 17 | [DataMember(Name = "channel_id")] 18 | public ulong ChannelId { get; set; } 19 | 20 | [JsonPropertyName("guild_id")] 21 | [DataMember(Name = "guild_id")] 22 | public ulong? GuildId { get; set; } 23 | 24 | [JsonPropertyName("author")] 25 | [DataMember(Name = "author")] 26 | public DiscordUserPacket Author { get; set; } 27 | 28 | [JsonPropertyName("member")] 29 | [DataMember(Name = "member")] 30 | public DiscordGuildMemberPacket Member { get; set; } 31 | 32 | [JsonPropertyName("type")] 33 | [DataMember(Name = "type")] 34 | public DiscordMessageType Type { get; set; } 35 | 36 | [JsonPropertyName("content")] 37 | [DataMember(Name = "content")] 38 | public string Content { get; set; } 39 | 40 | [JsonPropertyName("timestamp")] 41 | [DataMember(Name = "timestamp")] 42 | public DateTimeOffset Timestamp { get; set; } 43 | 44 | [JsonPropertyName("tts")] 45 | [DataMember(Name = "tts")] 46 | public bool IsTTS { get; set; } 47 | 48 | [JsonPropertyName("mention_everyone")] 49 | [DataMember(Name = "mention_everyone")] 50 | public bool MentionsEveryone { get; set; } 51 | 52 | [JsonPropertyName("mentions")] 53 | [DataMember(Name = "mentions")] 54 | public List Mentions { get; set; } 55 | 56 | [JsonPropertyName("attachments")] 57 | [DataMember(Name = "attachments")] 58 | public List Attachments { get; set; } 59 | } 60 | 61 | /// 62 | /// Type of message received from the Discord API. 63 | /// 64 | public enum DiscordMessageType 65 | { 66 | /// 67 | /// Default text message from a user. 68 | /// 69 | DEFAULT = 0, 70 | /// 71 | /// Recipent added to a DM group. 72 | /// 73 | RECIPIENT_ADD, 74 | /// 75 | /// Recipent removed from a DM group. 76 | /// 77 | RECIPIENT_REMOVE, 78 | /// 79 | /// Receiving a voice call request from another user. 80 | /// 81 | CALL, 82 | /// 83 | /// DM channel name change event 84 | /// 85 | CHANNEL_NAME_CHANGE, 86 | /// 87 | /// DM channel icon change event 88 | /// 89 | CHANNEL_ICON_CHANGE, 90 | /// 91 | /// Message has been pinned in a channel 92 | /// 93 | CHANNEL_PINNED_MESSAGE, 94 | /// 95 | /// A guild member joined, and the discord guild has default join events turned on. 96 | /// 97 | GUILD_MEMBER_JOIN, 98 | USER_PREMIUM_GUILD_SUBSCRIPTION, 99 | USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1, 100 | USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2, 101 | USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3 102 | } 103 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/API/DiscordPresencePacket.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Runtime.Serialization; 3 | using System.Text.Json.Serialization; 4 | using Miki.Discord.Common.Packets; 5 | 6 | namespace Miki.Discord.Common 7 | { 8 | [DataContract] 9 | public class DiscordPresencePacket 10 | { 11 | [JsonPropertyName("user")] 12 | [DataMember(Name = "user", Order = 1)] 13 | public DiscordUserPacket User { get; set; } 14 | 15 | [JsonPropertyName("roles")] 16 | [DataMember(Name = "roles", Order = 2)] 17 | public List RoleIds { get; set; } 18 | 19 | [JsonPropertyName("game")] 20 | [DataMember(Name = "game", Order = 3)] 21 | public DiscordActivity Game { get; set; } 22 | 23 | [JsonPropertyName("guild_id")] 24 | [DataMember(Name = "guild_id", Order = 4)] 25 | public ulong? GuildId { get; set; } 26 | 27 | [JsonPropertyName("status")] 28 | [DataMember(Name = "status", Order = 5)] 29 | public UserStatus Status { get; set; } 30 | } 31 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/API/DiscordReactionEventContent.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | using System.Text.Json.Serialization; 3 | 4 | #nullable enable 5 | 6 | namespace Miki.Discord.Common 7 | { 8 | [DataContract] 9 | public class DiscordReactionPacket 10 | { 11 | /// 12 | /// Id of the user reacting. 13 | /// 14 | [DataMember(Name = "user_id", Order = 1)] 15 | [JsonPropertyName("user_id")] 16 | public ulong UserId { get; set; } 17 | 18 | /// 19 | /// Channel where the message was posted. 20 | /// 21 | [DataMember(Name = "channel_id", Order = 2)] 22 | [JsonPropertyName("channel_id")] 23 | public ulong ChannelId { get; set; } 24 | 25 | /// 26 | /// Message ID of the reaction added. 27 | /// 28 | [DataMember(Name = "message_id", Order = 3)] 29 | [JsonPropertyName("message_id")] 30 | public ulong MessageId { get; set; } 31 | 32 | /// 33 | /// Guild Id if the channel is in a guild. 34 | /// 35 | [DataMember(Name = "guild_id", Order = 4)] 36 | [JsonPropertyName("guild_id")] 37 | public ulong? GuildId { get; set; } 38 | 39 | /// 40 | /// Guild member if the message was in a guild. 41 | /// 42 | [DataMember(Name = "member", Order = 5)] 43 | [JsonPropertyName("member")] 44 | public DiscordGuildMemberPacket? Member { get; set; } 45 | 46 | /// 47 | /// The emoji which was reacted. 48 | /// 49 | [DataMember(Name = "emoji", Order = 6)] 50 | [JsonPropertyName("emoji")] 51 | public DiscordEmoji Emoji { get; set; } 52 | } 53 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/API/DiscordReactionPacket.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Miki.Discord.Common 5 | { 6 | [DataContract] 7 | public class DiscordReactionCountPacket 8 | { 9 | [JsonPropertyName("count")] 10 | [DataMember(Name = "count")] 11 | public int Count { get; set; } 12 | 13 | [JsonPropertyName("me")] 14 | [DataMember(Name = "me")] 15 | public bool Me { get; set; } 16 | 17 | [JsonPropertyName("emoji")] 18 | [DataMember(Name = "emoji")] 19 | public DiscordEmoji Emoji { get; set; } 20 | } 21 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/API/DiscordRolePacket.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Miki.Discord.Common 5 | { 6 | [DataContract] 7 | public class DiscordRolePacket 8 | { 9 | [JsonPropertyName("id")] 10 | [DataMember(Name = "id", Order = 1)] 11 | public ulong Id { get; set; } 12 | 13 | [JsonPropertyName("name")] 14 | [DataMember(Name = "name", Order = 2)] 15 | public string Name { get; set; } 16 | 17 | [JsonPropertyName("color")] 18 | [DataMember(Name = "color", Order = 3)] 19 | public int Color { get; set; } 20 | 21 | [JsonPropertyName("hoist")] 22 | [DataMember(Name = "hoist", Order = 4)] 23 | public bool IsHoisted { get; set; } 24 | 25 | [JsonPropertyName("position")] 26 | [DataMember(Name = "position", Order = 5)] 27 | public int Position { get; set; } 28 | 29 | [JsonPropertyName("permissions")] 30 | [DataMember(Name = "permissions", Order = 6)] 31 | public GuildPermission Permissions { get; set; } 32 | 33 | [JsonPropertyName("managed")] 34 | [DataMember(Name = "managed", Order = 7)] 35 | public bool Managed { get; set; } 36 | 37 | [JsonPropertyName("mentionable")] 38 | [DataMember(Name = "mentionable", Order = 8)] 39 | public bool Mentionable { get; set; } 40 | 41 | public ulong GuildId { get; set; } 42 | } 43 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/API/DiscordStatus.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Miki.Discord.Common 5 | { 6 | [DataContract] 7 | public class DiscordStatus 8 | { 9 | [JsonPropertyName("since")] 10 | [DataMember(Name = "since", Order = 1)] 11 | public int? Since { get; set; } 12 | 13 | [JsonPropertyName("game")] 14 | [DataMember(Name = "game", Order = 2)] 15 | public DiscordActivity Game { get; set; } 16 | 17 | [JsonPropertyName("status")] 18 | [DataMember(Name = "status", Order = 3)] 19 | public string Status { get; set; } 20 | 21 | [JsonPropertyName("afk")] 22 | [DataMember(Name = "afk", Order = 4)] 23 | public bool IsAFK { get; set; } 24 | } 25 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/API/DiscordUserPacket.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Miki.Discord.Common.Packets 5 | { 6 | [DataContract] 7 | public class DiscordUserPacket 8 | { 9 | /// 10 | /// Internal Discord ID. 11 | /// 12 | [JsonPropertyName("id")] 13 | [DataMember(Name = "id", Order = 1)] 14 | public ulong Id { get; set; } 15 | 16 | /// 17 | /// User's name. 18 | /// 19 | [JsonPropertyName("username")] 20 | [DataMember(Name = "username", Order = 2)] 21 | public string Username { get; set; } 22 | 23 | /// 24 | /// User discriminator, aka the #1234 after someone's name. 25 | /// 26 | [JsonPropertyName("discriminator")] 27 | [DataMember(Name = "discriminator", Order = 3)] 28 | public short Discriminator { get; set; } 29 | 30 | /// 31 | /// Is the user a bot? 32 | /// 33 | [JsonPropertyName("bot")] 34 | [DataMember(Name = "bot", Order = 4)] 35 | public bool IsBot { get; set; } 36 | 37 | /// 38 | /// Avatar MD5 hash. 39 | /// 40 | [JsonPropertyName("avatar")] 41 | [DataMember(Name = "avatar", Order = 5)] 42 | public string Avatar { get; set; } 43 | 44 | /// 45 | /// User verified their phone? 46 | /// 47 | [JsonPropertyName("verified")] 48 | [DataMember(Name = "verified", Order = 6)] 49 | public bool Verified { get; set; } 50 | 51 | /// 52 | /// Email address user signed up with, only available in OAuth. 53 | /// 54 | [JsonPropertyName("email")] 55 | [DataMember(Name = "email", Order = 7)] 56 | public string Email { get; set; } 57 | 58 | /// 59 | /// Multi-factor authentication enabled. 60 | /// 61 | [JsonPropertyName("mfa_enabled")] 62 | [DataMember(Name = "mfa_enabled", Order = 8)] 63 | public bool MfaEnabled { get; set; } 64 | } 65 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/API/DiscordVoiceStatePacket.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Miki.Discord.Common 5 | { 6 | [DataContract] 7 | public class DiscordVoiceStatePacket 8 | { 9 | [JsonPropertyName("guild_id")] 10 | [DataMember(Name = "guild_id", Order = 1)] 11 | public ulong? GuildId { get; set; } 12 | 13 | [JsonPropertyName("channel_id")] 14 | [DataMember(Name = "channel_id", Order = 2)] 15 | public ulong ChannelId { get; set; } 16 | 17 | [JsonPropertyName("user_id")] 18 | [DataMember(Name = "user_id", Order = 3)] 19 | public ulong UserId { get; set; } 20 | 21 | [JsonPropertyName("session_id")] 22 | [DataMember(Name = "session_id", Order = 4)] 23 | public string SessionId { get; set; } 24 | 25 | [JsonPropertyName("deaf")] 26 | [DataMember(Name = "deaf", Order = 5)] 27 | public bool Deafened { get; set; } 28 | 29 | [JsonPropertyName("mute")] 30 | [DataMember(Name = "mute", Order = 6)] 31 | public bool Muted { get; set; } 32 | 33 | [JsonPropertyName("self_deaf")] 34 | [DataMember(Name = "self_deaf", Order = 7)] 35 | public bool SelfDeafened { get; set; } 36 | 37 | [JsonPropertyName("self_mute")] 38 | [DataMember(Name = "self_mute", Order = 8)] 39 | public bool SelfMuted { get; set; } 40 | 41 | [JsonPropertyName("suppress")] 42 | [DataMember(Name = "suppress", Order = 9)] 43 | public bool Suppressed { get; set; } 44 | } 45 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/API/GuildPermissions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace Miki.Discord.Common 6 | { 7 | [DataContract] 8 | public class PermissionOverwrite 9 | { 10 | [JsonPropertyName("id")] 11 | [DataMember(Name = "id")] 12 | public ulong Id { get; set; } 13 | 14 | /// 15 | /// Either 0 (role) or 1 (member) 16 | /// 17 | [JsonPropertyName("type")] 18 | [DataMember(Name = "type")] 19 | public int Type { get; set; } 20 | 21 | [JsonPropertyName("allow")] 22 | [DataMember(Name = "allow")] 23 | public GuildPermission AllowedPermissions { get; set; } 24 | 25 | [JsonPropertyName("deny")] 26 | [DataMember(Name = "deny")] 27 | public GuildPermission DeniedPermissions { get; set; } 28 | } 29 | 30 | [Flags] 31 | public enum GuildPermission : ulong 32 | { 33 | // General 34 | CreateInstantInvite = 0x00_00_00_01, 35 | 36 | KickMembers = 0x00_00_00_02, 37 | BanMembers = 0x00_00_00_04, 38 | Administrator = 0x00_00_00_08, 39 | ManageChannels = 0x00_00_00_10, 40 | ManageGuild = 0x00_00_00_20, 41 | 42 | // Text 43 | AddReactions = 0x00_00_00_40, 44 | 45 | ViewAuditLog = 0x00_00_00_80, 46 | ViewChannel = 0x00_00_04_00, 47 | SendMessages = 0x00_00_08_00, 48 | SendTTSMessages = 0x00_00_10_00, 49 | ManageMessages = 0x00_00_20_00, 50 | EmbedLinks = 0x00_00_40_00, 51 | AttachFiles = 0x00_00_80_00, 52 | ReadMessageHistory = 0x00_01_00_00, 53 | MentionEveryone = 0x00_02_00_00, 54 | UseExternalEmojis = 0x00_04_00_00, 55 | 56 | // Voice 57 | Connect = 0x00_10_00_00, 58 | 59 | Speak = 0x00_20_00_00, 60 | MuteMembers = 0x00_40_00_00, 61 | DeafenMembers = 0x00_80_00_00, 62 | MoveMembers = 0x01_00_00_00, 63 | UseVAD = 0x02_00_00_00, 64 | 65 | // General 2 66 | ChangeNickname = 0x04_00_00_00, 67 | 68 | ManageNicknames = 0x08_00_00_00, 69 | ManageRoles = 0x10_00_00_00, 70 | ManageWebhooks = 0x20_00_00_00, 71 | ManageEmojis = 0x40_00_00_00, 72 | 73 | All = 0xff_ff_ff_ff, 74 | None = 0x00_00_00_00 75 | } 76 | 77 | [Flags] 78 | public enum TextOverrides : ulong 79 | { 80 | // General 81 | CreateInstantInvite = 0x00_00_00_01, 82 | 83 | ManageChannels = 0x00_00_00_10, 84 | 85 | // Text 86 | AddReactions = 0x00_00_00_40, 87 | 88 | ViewChannel = 0x00_00_04_00, 89 | SendMessages = 0x00_00_08_00, 90 | SendTTSMessages = 0x00_00_10_00, 91 | ManageMessages = 0x00_00_20_00, 92 | EmbedLinks = 0x00_00_40_00, 93 | AttachFiles = 0x00_00_80_00, 94 | ReadMessageHistory = 0x00_01_00_00, 95 | MentionEveryone = 0x00_02_00_00, 96 | UseExternalEmojis = 0x00_04_00_00, 97 | 98 | ManageRoles = 0x10_00_00_00, 99 | ManageWebhooks = 0x20_00_00_00, 100 | 101 | All = 0xff_ff_ff_ff, 102 | None = 0x00_00_00_00 103 | } 104 | 105 | [Flags] 106 | public enum VoiceOverrides : ulong 107 | { 108 | // General 109 | CreateInstantInvite = 0x00_00_00_01, 110 | 111 | ManageChannels = 0x00_00_00_10, 112 | 113 | // Text 114 | ViewChannel = 0x00_00_04_00, 115 | 116 | // Voice 117 | Connect = 0x00_10_00_00, 118 | 119 | Speak = 0x00_20_00_00, 120 | MuteMembers = 0x00_40_00_00, 121 | DeafenMembers = 0x00_80_00_00, 122 | MoveMembers = 0x01_00_00_00, 123 | UseVAD = 0x02_00_00_00, 124 | 125 | // General 2 126 | ManageRoles = 0x10_00_00_00, 127 | 128 | ManageWebhooks = 0x20_00_00_00, 129 | 130 | All = 0xff_ff_ff_ff, 131 | None = 0x00_00_00_00 132 | } 133 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/API/ImageSize.cs: -------------------------------------------------------------------------------- 1 | namespace Miki.Discord.Common 2 | { 3 | /// 4 | /// Determines the size of your avatar image. 5 | /// 6 | public enum ImageSize 7 | { 8 | x16 = 16, 9 | x32 = 32, 10 | x64 = 64, 11 | x128 = 128, 12 | x256 = 256, 13 | x512 = 512, 14 | x1024 = 1024, 15 | x2048 = 2048 16 | } 17 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/API/ImageType.cs: -------------------------------------------------------------------------------- 1 | namespace Miki.Discord.Common 2 | { 3 | /// 4 | /// Declares what type you want your image is. 5 | /// 6 | public enum ImageType 7 | { 8 | PNG, 9 | WEBP, 10 | GIF, 11 | JPEG, 12 | AUTO 13 | } 14 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/API/Ratelimit.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace Miki.Discord.Rest 6 | { 7 | /// 8 | /// General ratelimit struct used to verify ratelimits and block potentially ratelimited requests. 9 | /// 10 | [DataContract] 11 | public struct Ratelimit 12 | { 13 | /// 14 | /// Remaining amount of entities that can be sent on this route. 15 | /// 16 | [JsonPropertyName("remaining")] 17 | [DataMember(Name = "remaining", Order = 1)] 18 | public int Remaining { get; set; } 19 | 20 | /// 21 | /// Total limit of entities that can be sent until occurs. 22 | /// 23 | [JsonPropertyName("limit")] 24 | [DataMember(Name = "limit", Order = 2)] 25 | public int Limit { get; set; } 26 | 27 | /// 28 | /// Epoch until ratelimit resets values. 29 | /// 30 | [JsonPropertyName("reset")] 31 | [DataMember(Name = "reset", Order = 3)] 32 | public long Reset { get; set; } 33 | 34 | /// 35 | /// An optional global value for a shared ratelimit value. 36 | /// 37 | [JsonPropertyName("global")] 38 | [DataMember(Name = "global", Order = 4)] 39 | public int? Global { get; set; } 40 | 41 | /// 42 | /// Checks if the current ratelimit is valid and/or is expired. 43 | /// 44 | /// Whether the current instance is being ratelimited 45 | public bool IsRatelimited() 46 | => IsRatelimited(this); 47 | 48 | /// 49 | /// Checks if the ratelimit is valid and/or is expired. 50 | /// 51 | /// The instance that is being checked. 52 | /// Whether the instance is being ratelimited 53 | public static bool IsRatelimited(Ratelimit rl) 54 | { 55 | return (rl.Global <= 0 || rl.Remaining <= 0) 56 | && DateTime.UtcNow <= DateTimeOffset.FromUnixTimeSeconds(rl.Reset); 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/API/RichPresenceAssets.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Miki.Discord.Common 5 | { 6 | [DataContract] 7 | public class RichPresenceAssets 8 | { 9 | [JsonPropertyName("large_image")] 10 | [DataMember(Name = "large_image", Order = 1)] 11 | public string LargeImage { get; set; } 12 | 13 | [JsonPropertyName("large_text")] 14 | [DataMember(Name = "large_text", Order = 2)] 15 | public string LargeText { get; set; } 16 | 17 | [JsonPropertyName("small_image")] 18 | [DataMember(Name = "small_image", Order = 3)] 19 | public string SmallImage { get; set; } 20 | 21 | [JsonPropertyName("small_text")] 22 | [DataMember(Name = "small_text", Order = 4)] 23 | public string SmallText { get; set; } 24 | } 25 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/API/RichPresenceParty.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Miki.Discord.Common 5 | { 6 | [DataContract] 7 | public class RichPresenceParty 8 | { 9 | [JsonPropertyName("id")] 10 | [DataMember(Name = "id", Order = 1)] 11 | public string Id { get; set; } 12 | 13 | [JsonPropertyName("size")] 14 | [DataMember(Name = "size", Order = 2)] 15 | public int[] Size { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/API/TimeStampsObject.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Miki.Discord.Common 5 | { 6 | [DataContract] 7 | public class TimeStampsObject 8 | { 9 | [JsonPropertyName("start")] 10 | [DataMember(Name = "start", Order = 1)] 11 | public long Start { get; set; } 12 | 13 | [JsonPropertyName("end")] 14 | [DataMember(Name = "end", Order = 2)] 15 | public long End { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/API/UserStatus.cs: -------------------------------------------------------------------------------- 1 | namespace Miki.Discord.Common 2 | { 3 | public enum UserStatus 4 | { 5 | ONLINE, 6 | IDLE, 7 | DND, 8 | OFFLINE 9 | } 10 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/Arguments/ChannelBulkDeleteArgs.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Miki.Discord.Rest.Arguments 5 | { 6 | [DataContract] 7 | public class ChannelBulkDeleteArgs 8 | { 9 | [JsonPropertyName("messages")] 10 | [DataMember(Name = "messages")] 11 | public ulong[] Messages { get; set; } 12 | 13 | public ChannelBulkDeleteArgs(ulong[] messages) 14 | { 15 | Messages = messages; 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/Arguments/CreateRoleArgs.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Miki.Discord.Common 5 | { 6 | [DataContract] 7 | public class CreateRoleArgs 8 | { 9 | [JsonPropertyName("name")] 10 | [DataMember(Name = "name")] 11 | public string Name { get; set; } 12 | 13 | [JsonPropertyName("permissions")] 14 | [DataMember(Name = "permissions")] 15 | public GuildPermission? Permissions { get; set; } 16 | 17 | [JsonPropertyName("color")] 18 | [DataMember(Name = "color")] 19 | public int? Color { get; set; } 20 | 21 | [JsonPropertyName("hoist")] 22 | [DataMember(Name = "hoist")] 23 | public bool? Hoisted { get; set; } 24 | 25 | [JsonPropertyName("mentionable")] 26 | [DataMember(Name = "mentionable")] 27 | public bool? Mentionable { get; set; } 28 | } 29 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/Arguments/EmojiCreationArgs.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Runtime.Serialization; 3 | using System.Text.Json.Serialization; 4 | using Miki.Discord.Rest; 5 | 6 | namespace Miki.Discord.Common 7 | { 8 | /// 9 | /// Data structure to create an emoji. 10 | /// 11 | [DataContract] 12 | public class EmojiCreationArgs : EmojiModifyArgs 13 | { 14 | [JsonPropertyName("image")] 15 | [DataMember(Name = "image")] 16 | public Stream Image { get; set; } 17 | 18 | public EmojiCreationArgs(string name, Stream image, params ulong[] roles) 19 | : base(name, roles) 20 | { 21 | Image = image; 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/Arguments/EmojiModifyArgs.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Miki.Discord.Rest 5 | { 6 | [DataContract] 7 | public class EmojiModifyArgs 8 | { 9 | [JsonPropertyName("name")] 10 | [DataMember(Name = "name")] 11 | public string Name { get; set; } 12 | 13 | [JsonPropertyName("roles")] 14 | [DataMember(Name = "roles")] 15 | public ulong[] Roles { get; set; } 16 | 17 | public EmojiModifyArgs(string name, params ulong[] roles) 18 | { 19 | Name = name; 20 | Roles = roles; 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/Arguments/MessageArgs.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Miki.Discord.Common 5 | { 6 | [DataContract] 7 | public class EditMessageArgs 8 | { 9 | public EditMessageArgs(string content = null, DiscordEmbed embed = null) 10 | { 11 | Content = content; 12 | Embed = embed; 13 | } 14 | 15 | [JsonPropertyName("content")] 16 | [DataMember(Name = "content")] 17 | public string Content { get; set; } 18 | 19 | [JsonPropertyName("embed")] 20 | [DataMember(Name = "embed")] 21 | public DiscordEmbed Embed { get; set; } 22 | } 23 | 24 | [DataContract] 25 | public class MessageArgs : EditMessageArgs 26 | { 27 | public MessageArgs(string content = null, DiscordEmbed embed = null, bool tts = false) 28 | : base(content, embed) 29 | { 30 | TextToSpeech = tts; 31 | } 32 | 33 | [JsonPropertyName("tts")] 34 | [DataMember(Name = "tts")] 35 | public bool TextToSpeech { get; set; } 36 | } 37 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/Arguments/UserModifyArgs.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Runtime.Serialization; 5 | using System.Text.Json.Serialization; 6 | 7 | namespace Miki.Discord.Common.Packets 8 | { 9 | public class UserAvatar 10 | { 11 | private static readonly byte[] JpegHeader = { 0xff, 0xd8 }; 12 | private static readonly byte[] Gif89aHeader = { 0x47, 0x49, 0x46, 0x38, 0x39, 0x61 }; 13 | private static readonly byte[] Gif87aHeader = { 0x47, 0x49, 0x46, 0x38, 0x37, 0x61 }; 14 | private static readonly byte[] PngHeader = { 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a }; 15 | 16 | public MemoryStream Stream { get; set; } 17 | 18 | public ImageType Type { get; set; } 19 | 20 | public UserAvatar(Stream stream, ImageType type = ImageType.AUTO) 21 | { 22 | Stream = new MemoryStream(); 23 | stream.CopyTo(Stream); 24 | 25 | if(type != ImageType.AUTO) 26 | { 27 | return; 28 | } 29 | 30 | var buffer = Stream.GetBuffer(); 31 | 32 | if(Validate(buffer.Take(JpegHeader.Length), JpegHeader)) 33 | { 34 | Type = ImageType.JPEG; 35 | return; 36 | } 37 | 38 | if(Validate(buffer.Take(Gif89aHeader.Length), Gif89aHeader) 39 | || Validate(buffer.Take(Gif87aHeader.Length), Gif87aHeader)) 40 | { 41 | Type = ImageType.GIF; 42 | return; 43 | } 44 | 45 | if(Validate(buffer.Take(PngHeader.Length), PngHeader)) 46 | { 47 | Type = ImageType.PNG; 48 | return; 49 | } 50 | } 51 | 52 | public static implicit operator UserAvatar(Stream s) 53 | { 54 | return new UserAvatar(s); 55 | } 56 | 57 | private bool Validate(IEnumerable a, IEnumerable b) 58 | { 59 | if(a.Count() != b.Count()) 60 | { 61 | return false; 62 | } 63 | 64 | for(var i = 0; i < a.Count(); i++) 65 | { 66 | if(a.ElementAt(i) != b.ElementAt(i)) 67 | { 68 | return false; 69 | } 70 | } 71 | return true; 72 | } 73 | } 74 | 75 | [DataContract] 76 | public class UserModifyArgs 77 | { 78 | [JsonPropertyName("avatar")] 79 | [DataMember(Name = "avatar")] 80 | public UserAvatar Avatar { get; set; } 81 | 82 | [JsonPropertyName("username")] 83 | [DataMember(Name = "username")] 84 | public string Username { get; set; } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/Events/GatewayReadyPacket.cs: -------------------------------------------------------------------------------- 1 | using Miki.Discord.Common.Packets; 2 | using System.Runtime.Serialization; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace Miki.Discord.Common.Gateway 6 | { 7 | [DataContract] 8 | public class GatewayReadyPacket 9 | { 10 | [JsonPropertyName("v")] 11 | [DataMember(Name = "v")] 12 | public int ProtocolVersion { get; set; } 13 | 14 | [JsonPropertyName("user")] 15 | [DataMember(Name = "user")] 16 | public DiscordUserPacket CurrentUser { get; set; } 17 | 18 | [JsonPropertyName("private_channels")] 19 | [DataMember(Name = "private_channels")] 20 | public DiscordChannelPacket[] PrivateChannels { get; set; } 21 | 22 | [JsonPropertyName("guilds")] 23 | [DataMember(Name = "guilds")] 24 | public DiscordGuildPacket[] Guilds { get; set; } 25 | 26 | [JsonPropertyName("session_id")] 27 | [DataMember(Name = "session_id")] 28 | public string SessionId { get; set; } 29 | 30 | [JsonPropertyName("_trace")] 31 | [DataMember(Name = "_trace")] 32 | public string[] TraceGuilds { get; set; } 33 | 34 | [JsonPropertyName("shard")] 35 | [DataMember(Name = "shard")] 36 | public int[] Shard { get; set; } 37 | 38 | public int CurrentShard 39 | => Shard[0]; 40 | 41 | public int TotalShards 42 | => Shard[1]; 43 | } 44 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/Events/GuildEmojisUpdateEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Runtime.Serialization; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace Miki.Discord.Common.Events 6 | { 7 | [DataContract] 8 | public class GuildEmojisUpdateEventArgs 9 | { 10 | [JsonPropertyName("guild_id")] 11 | [DataMember(Name = "guild_id")] 12 | public ulong GuildId { get; set; } 13 | 14 | [JsonPropertyName("emojis")] 15 | [DataMember(Name = "emojis")] 16 | public IReadOnlyList Emojis { get; set; } 17 | } 18 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/Events/GuildIdUserArgs.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | using Miki.Discord.Common.Packets; 3 | 4 | namespace Miki.Discord.Common.Events 5 | { 6 | [DataContract] 7 | public class GuildIdUserArgs 8 | { 9 | [DataMember(Name = "user")] 10 | public DiscordUserPacket User { get; set; } 11 | 12 | [DataMember(Name = "guild_id")] 13 | public ulong GuildId { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/Events/GuildMemberUpdateArgs.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | 3 | namespace Miki.Discord.Common.Events 4 | { 5 | [DataContract] 6 | public class ModifyGuildMemberArgs 7 | { 8 | [DataMember(Name = "nick")] 9 | public string Nickname; 10 | 11 | [DataMember(Name = "roles")] 12 | public ulong[] RoleIds; 13 | 14 | [DataMember(Name = "mute")] 15 | public bool? Muted; 16 | 17 | [DataMember(Name = "deaf")] 18 | public bool? Deafened; 19 | 20 | [DataMember(Name = "channel_id")] 21 | public ulong? MoveToChannelId; 22 | } 23 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/Events/GuildMemberUpdateEventArgs.cs: -------------------------------------------------------------------------------- 1 | using Miki.Discord.Common.Packets; 2 | using System.Runtime.Serialization; 3 | 4 | namespace Miki.Discord.Common.Events 5 | { 6 | [DataContract] 7 | public class GuildMemberUpdateEventArgs 8 | { 9 | [DataMember(Name = "guild_id")] 10 | public ulong GuildId; 11 | 12 | [DataMember(Name = "roles")] 13 | public ulong[] RoleIds; 14 | 15 | [DataMember(Name = "user")] 16 | public DiscordUserPacket User; 17 | 18 | [DataMember(Name = "nick")] 19 | public string Nickname; 20 | } 21 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/Events/MessageBulkDeleteEventArgs.cs: -------------------------------------------------------------------------------- 1 |  2 | using System.Runtime.Serialization; 3 | 4 | namespace Miki.Discord.Common.Packets.Events 5 | { 6 | [DataContract] 7 | public class MessageBulkDeleteEventArgs 8 | { 9 | [DataMember(Name = "guild_id")] 10 | public ulong guildId; 11 | 12 | [DataMember(Name = "channel_id")] 13 | public ulong channelId; 14 | 15 | [DataMember(Name = "ids")] 16 | public ulong[] messagesDeleted; 17 | } 18 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/Events/MessageDeleteArgs.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | 3 | namespace Miki.Discord.Common.Events 4 | { 5 | [DataContract] 6 | public class DiscordMessageDeleteArgs 7 | { 8 | [DataMember(Name = "id")] 9 | public ulong MessageId { get; set; } 10 | 11 | [DataMember(Name = "channel_id")] 12 | public ulong ChannelId { get; set; } 13 | } 14 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/Events/RoleDeleteEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | 3 | namespace Miki.Discord.Common.Events 4 | { 5 | [DataContract] 6 | public class RoleDeleteEventArgs 7 | { 8 | [DataMember(Name = "guild_id")] 9 | public ulong GuildId { get; set; } 10 | 11 | [DataMember(Name = "role_id")] 12 | public ulong RoleId { get; set; } 13 | } 14 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/Events/RoleEventArgs.cs: -------------------------------------------------------------------------------- 1 | using Miki.Discord.Common.Packets; 2 | using System.Runtime.Serialization; 3 | 4 | namespace Miki.Discord.Common.Events 5 | { 6 | [DataContract] 7 | public class RoleEventArgs 8 | { 9 | [DataMember(Name = "guild_id")] public ulong GuildId; 10 | 11 | [DataMember(Name = "role")] public DiscordRolePacket Role; 12 | } 13 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/Packets/Events/TypingStartEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | 3 | namespace Miki.Discord.Common.Packets.Events 4 | { 5 | [DataContract] 6 | public class TypingStartEventArgs 7 | { 8 | [DataMember(Name = "channel_id")] 9 | public ulong channelId; 10 | 11 | [DataMember(Name = "guild_id")] 12 | public ulong guildId; 13 | 14 | [DataMember(Name = "member")] 15 | public DiscordGuildMemberPacket member; 16 | } 17 | } -------------------------------------------------------------------------------- /Miki.Discord.Common/TokenType.cs: -------------------------------------------------------------------------------- 1 | namespace Miki.Discord.Common 2 | { 3 | /// 4 | /// Token type used to differentiate bot users from normal users. 5 | /// 6 | public enum TokenType 7 | { 8 | /// 9 | /// API bot user. 10 | /// 11 | BOT, 12 | 13 | /// 14 | /// Discord user account. 15 | /// 16 | BEARER 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Miki.Discord.Extensions/DependencyInjection/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Miki.Cache; 3 | using Miki.Discord.Common; 4 | using Miki.Discord.Gateway; 5 | using Miki.Discord.Rest; 6 | using System; 7 | 8 | namespace Miki.Discord.Extensions.DependencyInjection 9 | { 10 | public static class ServiceCollectionExtensions 11 | { 12 | public static IServiceCollection UseDiscord( 13 | this IServiceCollection collection, Action configFactory) 14 | { 15 | var config = new DiscordConfiguration(); 16 | configFactory(config); 17 | 18 | if (!config.Token.IsValidToken()) 19 | { 20 | throw new ArgumentException("Invalid Token"); 21 | } 22 | 23 | if(config.GatewayProperties.Token == null) 24 | { 25 | config.GatewayProperties.Token = config.Token.ToString(); 26 | } 27 | 28 | collection.AddSingleton(config); 29 | collection.UseDiscord(); 30 | 31 | return collection; 32 | } 33 | public static IServiceCollection UseDiscord( 34 | this IServiceCollection collection) 35 | { 36 | collection.AddSingleton( 37 | x => new DiscordApiClient( 38 | x.GetRequiredService().Token, 39 | x.GetRequiredService())); 40 | collection.AddSingleton( 41 | x => new GatewayCluster( 42 | x.GetRequiredService().GatewayProperties)); 43 | collection.AddSingleton(); 44 | return collection; 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /Miki.Discord.Extensions/DiscordConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Miki.Discord.Common; 2 | using Miki.Discord.Gateway; 3 | 4 | namespace Miki.Discord.Extensions 5 | { 6 | public class DiscordConfiguration 7 | { 8 | public DiscordToken Token { get; set; } 9 | 10 | public GatewayProperties GatewayProperties { get; set; } = new GatewayProperties(); 11 | } 12 | } -------------------------------------------------------------------------------- /Miki.Discord.Extensions/Miki.Discord.Extensions.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1 5 | 4.0.0-rc.5 6 | velddev 7 | mikibot 8 | Extension usage for Miki.Discord for domain-related changes such as dependency injection. 9 | icon.png 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | True 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Miki.Discord.Gateway/Connection/GatewayConstants.cs: -------------------------------------------------------------------------------- 1 | namespace Miki.Discord.Gateway.Connection 2 | { 3 | public static class GatewayConstants 4 | { 5 | /// 6 | /// Default Gateway version supported by Miki.Discord. 7 | /// 8 | public const int DefaultVersion = 8; 9 | 10 | /// 11 | /// Default websocket receive payload size. 12 | /// 13 | public const int WebSocketReceiveSize = 16 * 1024; 14 | 15 | /// 16 | /// Default websocket send payload size. 17 | /// 18 | public const int WebSocketSendSize = 4 * 1024; 19 | } 20 | } -------------------------------------------------------------------------------- /Miki.Discord.Gateway/Connection/GatewayEncoding.cs: -------------------------------------------------------------------------------- 1 | namespace Miki.Discord.Gateway.Connection 2 | { 3 | /// 4 | /// Discord supported Gateway encoding formats 5 | /// 6 | public enum GatewayEncoding 7 | { 8 | /// 9 | /// Plain-text Json 10 | /// 11 | Json, 12 | 13 | /// 14 | /// Erlang binary format 15 | /// 16 | ETF 17 | } 18 | } -------------------------------------------------------------------------------- /Miki.Discord.Gateway/Connection/Models/GatewayEventType.cs: -------------------------------------------------------------------------------- 1 | namespace Miki.Discord.Common.Gateway 2 | { 3 | /// 4 | /// All Gateway Events. 5 | /// 6 | public enum GatewayEventType 7 | { 8 | /// 9 | /// A new channel was created. 10 | /// 11 | ChannelCreate, 12 | 13 | /// 14 | /// A channel was deleted. 15 | /// 16 | ChannelDelete, 17 | 18 | /// 19 | /// A channel was edited. 20 | /// 21 | ChannelUpdate, 22 | 23 | /// 24 | /// A message was pinned on this channel. 25 | /// 26 | ChannelPinsUpdate, 27 | 28 | /// 29 | /// A member got banned. 30 | /// 31 | GuildBanAdd, 32 | 33 | /// 34 | /// A member got unbanned. 35 | /// 36 | GuildBanRemove, 37 | 38 | /// 39 | /// The bot joined a new guild. 40 | /// 41 | GuildCreate, 42 | 43 | /// 44 | /// The bot left a guild. 45 | /// 46 | GuildDelete, 47 | 48 | /// 49 | /// A new emoji got added/removed. 50 | /// 51 | GuildEmojisUpdate, 52 | 53 | /// 54 | /// A new integration got added/removed. 55 | /// 56 | GuildIntegrationsUpdate, 57 | 58 | /// 59 | /// A member joined a guild. 60 | /// 61 | GuildMemberAdd, 62 | 63 | /// 64 | /// A former member left a guild. 65 | /// 66 | GuildMemberRemove, 67 | 68 | /// 69 | /// A guild member updated their profile/roles/nickname. 70 | /// 71 | GuildMemberUpdate, 72 | 73 | /// 74 | /// Gateway requested members from this guild. 75 | /// 76 | GuildMembersChunk, 77 | GuildRoleCreate, 78 | GuildRoleDelete, 79 | GuildRoleUpdate, 80 | GuildUpdate, 81 | InviteCreate, 82 | InviteDelete, 83 | MessageCreate, 84 | MessageDelete, 85 | MessageDeleteBulk, 86 | MessageUpdate, 87 | MessageReactionAdd, 88 | MessageReactionRemove, 89 | MessageReactionRemoveAll, 90 | PresenceUpdate, 91 | 92 | /// 93 | /// Gateway is ready to go. 94 | /// 95 | Ready, 96 | 97 | /// 98 | /// The connection was cut and got resumed. 99 | /// 100 | Resumed, 101 | TypingStart, 102 | UserUpdate, 103 | VoiceServerUpdate, 104 | VoiceStateUpdate, 105 | WebhooksUpdate, 106 | 107 | /// 108 | /// Catch-all for undefined event types. 109 | /// 110 | Undefined, 111 | } 112 | } -------------------------------------------------------------------------------- /Miki.Discord.Gateway/Connection/Models/GatewayHelloPacket.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Miki.Discord.Common.Gateway 5 | { 6 | [DataContract] 7 | public class GatewayHelloPacket 8 | { 9 | [JsonPropertyName("heartbeat_interval")] 10 | [DataMember(Name = "heartbeat_interval")] 11 | public int HeartbeatInterval { get; set; } 12 | 13 | [JsonPropertyName("_trace")] 14 | [DataMember(Name = "_trace")] 15 | public string[] TraceServers { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /Miki.Discord.Gateway/Connection/Models/GatewayIdentifyPacket.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace Miki.Discord.Common.Gateway 6 | { 7 | [DataContract] 8 | public class GatewayIdentifyPacket 9 | { 10 | [JsonPropertyName("token")] 11 | [DataMember(Name = "token")] 12 | public string Token { get; set; } 13 | 14 | [JsonPropertyName("properties")] 15 | [DataMember(Name = "properties")] 16 | public GatewayIdentifyConnectionProperties ConnectionProperties { get; set; } 17 | = new GatewayIdentifyConnectionProperties(); 18 | 19 | [JsonPropertyName("compress")] 20 | [DataMember(Name = "compress")] 21 | public bool Compressed { get; set; } 22 | 23 | [JsonPropertyName("large_threshold")] 24 | [DataMember(Name = "large_threshold")] 25 | public int LargeThreshold { get; set; } 26 | 27 | [JsonPropertyName("presence")] 28 | [DataMember(Name = "presence")] 29 | public DiscordStatus Presence { get; set; } 30 | 31 | [JsonPropertyName("shard")] 32 | [DataMember(Name = "shard")] 33 | public int[] Shard { get; set; } 34 | 35 | [JsonPropertyName("intents")] 36 | [DataMember(Name = "shard")] 37 | public int Intent { get; set; } 38 | } 39 | 40 | [DataContract] 41 | public class GatewayIdentifyConnectionProperties 42 | { 43 | [JsonPropertyName("$os")] 44 | [DataMember(Name = "$os")] 45 | public string OperatingSystem { get; set; } 46 | = Environment.OSVersion.ToString(); 47 | 48 | [JsonPropertyName("$browser")] 49 | [DataMember(Name = "$browser")] 50 | public string Browser { get; set; } = "Miki.Discord"; 51 | 52 | [JsonPropertyName("$device")] 53 | [DataMember(Name = "$device")] 54 | public string Device { get; set; } = "Miki.Discord"; 55 | } 56 | } -------------------------------------------------------------------------------- /Miki.Discord.Gateway/Connection/Models/GatewayResumePacket.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | using Newtonsoft.Json; 3 | 4 | namespace Miki.Discord.Gateway.Connection 5 | { 6 | public class GatewayResumePacket 7 | { 8 | [JsonProperty("token")] 9 | [JsonPropertyName("token")] 10 | public string Token { get; set; } 11 | 12 | [JsonProperty("session_id")] 13 | [JsonPropertyName("session_id")] 14 | public string SessionId { get; set; } 15 | 16 | [JsonProperty("seq")] 17 | [JsonPropertyName("seq")] 18 | public int Sequence { get; set; } 19 | } 20 | } -------------------------------------------------------------------------------- /Miki.Discord.Gateway/Exceptions/GatewayException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Miki.Discord.Gateway 4 | { 5 | public class GatewayException : Exception 6 | { 7 | public GatewayException(string message, Exception innerException) 8 | : base(message, innerException) 9 | { 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Miki.Discord.Gateway/Extensions/JsonElementExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | 3 | namespace Miki.Discord.Gateway 4 | { 5 | public static class JsonElementExtensions 6 | { 7 | public static T ToObject(this JsonElement element, JsonSerializerOptions options = null) 8 | { 9 | return JsonSerializer.Deserialize(element.GetRawText(), options); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Miki.Discord.Gateway/GatewayCluster.cs: -------------------------------------------------------------------------------- 1 | using System.Reactive.Subjects; 2 | using Miki.Discord.Common; 3 | using Miki.Discord.Common.Gateway; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | using System.Threading; 9 | using Microsoft.Extensions.Hosting; 10 | 11 | namespace Miki.Discord.Gateway 12 | { 13 | public class GatewayCluster : IGateway, IHostedService 14 | { 15 | /// 16 | public IObservable PacketReceived => messageSubject; 17 | 18 | /// 19 | public IGatewayEvents Events { get; } 20 | 21 | /// 22 | /// Currently running shards in this cluster. 23 | /// 24 | public Dictionary Shards { get; } = new Dictionary(); 25 | 26 | private Subject messageSubject; 27 | 28 | /// 29 | /// Spawn all shards in a single cluster 30 | /// 31 | /// general gateway properties 32 | public GatewayCluster(GatewayProperties properties) 33 | : this(properties, Enumerable.Range(0, properties.ShardCount)) 34 | { 35 | } 36 | 37 | /// 38 | /// Used to spawn specific shards only 39 | /// 40 | /// general gateway properties 41 | /// Which shards should this cluster spawn 42 | public GatewayCluster(GatewayProperties properties, IEnumerable shards) 43 | { 44 | if(shards == null) 45 | { 46 | throw new ArgumentException("shards cannot be null."); 47 | } 48 | 49 | messageSubject = new Subject(); 50 | 51 | foreach(var i in shards) 52 | { 53 | var shardProperties = new GatewayProperties 54 | { 55 | Encoding = properties.Encoding, 56 | Compressed = properties.Compressed, 57 | Ratelimiter = properties.Ratelimiter, 58 | ShardCount = properties.ShardCount, 59 | ShardId = i, 60 | Token = properties.Token, 61 | Version = properties.Version, 62 | Intents = properties.Intents, 63 | AllowNonDispatchEvents = properties.AllowNonDispatchEvents, 64 | GatewayFactory = properties.GatewayFactory, 65 | SerializerOptions = properties.SerializerOptions, 66 | UseGatewayEvents = false 67 | }; 68 | 69 | var shard = properties.GatewayFactory(shardProperties); 70 | Shards.Add(i, shard); 71 | shard.PacketReceived.Subscribe(messageSubject.OnNext); 72 | } 73 | 74 | Events = new GatewayEventHandler(messageSubject, properties.SerializerOptions); 75 | } 76 | 77 | /// 78 | public async Task SendAsync(int shardId, GatewayOpcode opcode, object payload) 79 | { 80 | if(Shards.TryGetValue(shardId, out var shard)) 81 | { 82 | await shard.SendAsync(shardId, opcode, payload); 83 | } 84 | } 85 | 86 | /// 87 | public async Task RestartAsync() 88 | { 89 | foreach(var shard in Shards.Values) 90 | { 91 | await shard.RestartAsync(); 92 | } 93 | } 94 | 95 | /// 96 | public async Task StartAsync(CancellationToken token = default) 97 | { 98 | foreach(var shard in Shards.Values) 99 | { 100 | await shard.StartAsync(token) 101 | .ConfigureAwait(false); 102 | } 103 | } 104 | 105 | /// 106 | public async Task StopAsync(CancellationToken token = default) 107 | { 108 | foreach(var shard in Shards.Values) 109 | { 110 | await shard.StopAsync(token) 111 | .ConfigureAwait(false); 112 | } 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /Miki.Discord.Gateway/GatewayShard.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Hosting; 2 | using Miki.Discord.Common; 3 | using Miki.Discord.Common.Gateway; 4 | using Miki.Discord.Gateway.Connection; 5 | using System; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace Miki.Discord.Gateway 10 | { 11 | public class GatewayShard : IDisposable, IGateway, IHostedService 12 | { 13 | private readonly GatewayConnection connection; 14 | private readonly GatewayEventHandler eventHandler; 15 | private readonly CancellationTokenSource tokenSource; 16 | private bool isRunning; 17 | 18 | /// 19 | public IGatewayEvents Events => eventHandler; 20 | 21 | /// 22 | /// Index of the shard. 23 | /// 24 | public int ShardId => connection.ShardId; 25 | 26 | /// 27 | /// Current status of the connection. 28 | /// 29 | public ConnectionStatus Status => connection.ConnectionStatus; 30 | 31 | public IObservable PacketReceived => connection.OnPacketReceived; 32 | 33 | public GatewayShard(GatewayProperties configuration) 34 | { 35 | tokenSource = new CancellationTokenSource(); 36 | connection = new GatewayConnection(configuration); 37 | 38 | if (configuration.UseGatewayEvents) 39 | { 40 | eventHandler = new GatewayEventHandler( 41 | PacketReceived, configuration.SerializerOptions); 42 | } 43 | } 44 | 45 | /// 46 | public async Task RestartAsync() 47 | { 48 | await connection.ReconnectAsync(); 49 | } 50 | 51 | /// 52 | public async Task StartAsync(CancellationToken token) 53 | { 54 | if(isRunning) 55 | { 56 | return; 57 | } 58 | 59 | await connection.StartAsync(token); 60 | isRunning = true; 61 | } 62 | 63 | /// 64 | public async Task StopAsync(CancellationToken token) 65 | { 66 | if(!isRunning) 67 | { 68 | return; 69 | } 70 | 71 | tokenSource.Cancel(); 72 | await connection.StopAsync(token); 73 | isRunning = false; 74 | } 75 | 76 | /// 77 | public async Task SendAsync(int shardId, GatewayOpcode opcode, object payload) 78 | { 79 | if(payload == null) 80 | { 81 | throw new ArgumentNullException(nameof(payload)); 82 | } 83 | 84 | await connection.SendCommandAsync(opcode, payload, tokenSource.Token); 85 | } 86 | 87 | /// 88 | public void Dispose() 89 | { 90 | tokenSource.Dispose(); 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /Miki.Discord.Gateway/Miki.Discord.Gateway.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1 5 | 4.0.0-rc.5 6 | Velddev 7 | Velddev 8 | Discord gateway implementation 9 | false 10 | Miki.Discord.Gateway.Centralized.snk 11 | Discord Gateway Miki 12 | Miki 13 | latest 14 | https://github.com/mikibot/miki.discord 15 | 16 | LICENSE 17 | icon.png 18 | 19 | 20 | 21 | false 22 | D:\Projects\Miki.Discord\Miki.Discord.Gateway\Miki.Discord.Gateway.xml 23 | 24 | 25 | 26 | D:\Projects\Miki.Discord\Miki.Discord.Gateway\Miki.Discord.Gateway.xml 27 | 28 | 29 | 30 | 31 | 32 | True 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | True 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /Miki.Discord.Gateway/Miki.Discord.Gateway.csproj.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True 3 | True -------------------------------------------------------------------------------- /Miki.Discord.Gateway/Models/GatewayConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Miki.Discord.Common; 2 | using Miki.Discord.Gateway.Connection; 3 | using Miki.Discord.Gateway.Ratelimiting; 4 | using System; 5 | using System.Text.Json; 6 | using Miki.Discord.Rest.Converters; 7 | using Miki.Discord.Gateway.WebSocket; 8 | 9 | namespace Miki.Discord.Gateway 10 | { 11 | /// 12 | /// Configurable properties for the gateway client. 13 | /// 14 | public class GatewayProperties 15 | { 16 | /// 17 | /// Discord token 18 | /// 19 | public string Token { get; set; } 20 | 21 | /// 22 | /// Whether the gateway should receive zlib-compressed packets. 23 | /// Warning: this is not supported in this library as of now. Please check the github 24 | /// page. 25 | /// 26 | public bool Compressed { get; set; } 27 | 28 | /// 29 | /// What kind of encoding do you want receive. 30 | /// 31 | public GatewayEncoding Encoding { get; set; } = GatewayEncoding.Json; 32 | 33 | /// 34 | /// If you are unsure what this should be, keep it null or GatewayConstants.DefaultVersion. 35 | /// 36 | public int Version { get; set; } = GatewayConstants.DefaultVersion; 37 | 38 | /// 39 | /// Total shards running on this token 40 | /// 41 | public int ShardCount { get; set; } = 1; 42 | 43 | /// 44 | /// The current shard's Id 45 | /// 46 | public int ShardId { get; set; } 47 | 48 | /// 49 | /// The gateway factory used for spawning shards in . 50 | /// 51 | public Func GatewayFactory { get; set; } 52 | = p => new GatewayShard(p); 53 | 54 | /// 55 | /// The websocket that will be used to connect to discord. 56 | /// 57 | public Func WebSocketFactory { get; set; } 58 | = () => new DefaultWebSocketClient(); 59 | 60 | /// 61 | /// 62 | /// 63 | public IGatewayRatelimiter Ratelimiter { get; set; } = new DefaultGatewayRatelimiter(); 64 | 65 | /// 66 | /// Json serializer options. 67 | /// 68 | public JsonSerializerOptions SerializerOptions { get; set; } = new JsonSerializerOptions 69 | { 70 | Converters = 71 | { 72 | new StringToEnumConverter(), 73 | new StringToShortConverter(), 74 | new StringToUlongConverter() 75 | } 76 | }; 77 | 78 | /// 79 | /// Allow events other than dispatch to be received in raw events? 80 | /// 81 | public bool AllowNonDispatchEvents { get; set; } = false; 82 | 83 | /// 84 | /// Initializes for rich events. 85 | /// 86 | public bool UseGatewayEvents { get; set; } = true; 87 | 88 | /// 89 | /// to subscribe to events. If passed null, you'll subscribe to all events. 90 | /// 91 | public GatewayIntents Intents { get; set; } = GatewayIntents.AllDefault; 92 | } 93 | } -------------------------------------------------------------------------------- /Miki.Discord.Gateway/Ratelimiting/CacheBasedRatelimiter.cs: -------------------------------------------------------------------------------- 1 | using Miki.Cache; 2 | using System; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace Miki.Discord.Gateway.Ratelimiting 7 | { 8 | public class CacheBasedRatelimiter : IGatewayRatelimiter 9 | { 10 | private const string CacheKey = "miki:gateway:identify:ratelimit"; 11 | 12 | private readonly IDistributedLockProvider cache; 13 | private readonly bool largeBot; 14 | 15 | public CacheBasedRatelimiter(IDistributedLockProvider cache, bool largeBot = false) 16 | { 17 | this.cache = cache; 18 | this.largeBot = largeBot; 19 | } 20 | 21 | /// 22 | public async Task CanIdentifyAsync(int shardId, CancellationToken token) 23 | { 24 | token.ThrowIfCancellationRequested(); 25 | 26 | try 27 | { 28 | await cache.AcquireLockAsync(GetCacheKey(shardId), token); 29 | } 30 | catch(OperationCanceledException) 31 | { 32 | return false; 33 | } 34 | 35 | await cache.ExpiresAsync(GetCacheKey(shardId), TimeSpan.FromSeconds(5)); 36 | return true; 37 | } 38 | 39 | private string GetCacheKey(int shardId) 40 | { 41 | if(largeBot) 42 | { 43 | return $"{CacheKey}:{shardId % 16}"; 44 | } 45 | return CacheKey; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Miki.Discord.Gateway/Ratelimiting/DefaultGatewayRatelimiter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Threading; 4 | using System.Linq; 5 | 6 | namespace Miki.Discord.Gateway.Ratelimiting 7 | { 8 | public class DefaultGatewayRatelimiter : IGatewayRatelimiter 9 | { 10 | private readonly DateTime[] lastIdentifyAccepted; 11 | private readonly bool largeBot; 12 | 13 | public DefaultGatewayRatelimiter(bool largeBot = false) 14 | { 15 | lastIdentifyAccepted = new DateTime[largeBot ? 16 : 1]; 16 | this.largeBot = largeBot; 17 | } 18 | 19 | /// 20 | public Task CanIdentifyAsync(int shardId, CancellationToken token) 21 | { 22 | if(GetLastIdentify(shardId).AddSeconds(5) > DateTime.UtcNow) 23 | { 24 | return Task.FromResult(false); 25 | } 26 | UpdateLastIdentify(shardId); 27 | return Task.FromResult(true); 28 | } 29 | 30 | private DateTime GetLastIdentify(int shardId) 31 | { 32 | if(largeBot) 33 | { 34 | return lastIdentifyAccepted[shardId % 16]; 35 | } 36 | return lastIdentifyAccepted.First(); 37 | } 38 | 39 | private void UpdateLastIdentify(int shardId) 40 | { 41 | if (largeBot) 42 | { 43 | lastIdentifyAccepted[shardId % 16] = DateTime.UtcNow; 44 | return; 45 | } 46 | lastIdentifyAccepted[0] = DateTime.UtcNow; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Miki.Discord.Gateway/Ratelimiting/IGatewayRatelimiter.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | 4 | namespace Miki.Discord.Gateway.Ratelimiting 5 | { 6 | /// 7 | /// Ratelimits identify calls on websocket. 8 | /// 9 | public interface IGatewayRatelimiter 10 | { 11 | /// 12 | /// Returns whether it can identify or not. 13 | /// 14 | Task CanIdentifyAsync(int shardId, CancellationToken token); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Miki.Discord.Gateway/Utils/WebsocketUrlBuilder.cs: -------------------------------------------------------------------------------- 1 | using Miki.Discord.Gateway.Connection; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Miki.Discord.Gateway.Utils 6 | { 7 | public static class DictionaryUtils 8 | { 9 | public static void AddOrUpdate( 10 | this Dictionary dict, TKey key, TValue value) 11 | { 12 | if(dict.ContainsKey(key)) 13 | { 14 | dict[key] = value; 15 | } 16 | else 17 | { 18 | dict.Add(key, value); 19 | } 20 | } 21 | 22 | public static bool TryRemove(this Dictionary dict, TKey key) 23 | { 24 | if(dict.ContainsKey(key)) 25 | { 26 | dict.Remove(key); 27 | return true; 28 | } 29 | return false; 30 | } 31 | } 32 | 33 | public class WebSocketUrlBuilder 34 | { 35 | private readonly string url; 36 | private readonly Dictionary arguments = new Dictionary(); 37 | 38 | public WebSocketUrlBuilder(string baseUrl) 39 | { 40 | url = baseUrl; 41 | } 42 | 43 | public WebSocketUrlBuilder SetVersion(int version = GatewayConstants.DefaultVersion) 44 | { 45 | arguments.AddOrUpdate("v", version); 46 | return this; 47 | } 48 | 49 | public WebSocketUrlBuilder SetEncoding(GatewayEncoding encoding) 50 | { 51 | arguments.AddOrUpdate("encoding", encoding.ToString().ToLower()); 52 | return this; 53 | } 54 | 55 | public WebSocketUrlBuilder SetCompression(bool compressed) 56 | { 57 | if(compressed) 58 | { 59 | arguments.AddOrUpdate("compress", "zlib-stream"); 60 | } 61 | else 62 | { 63 | arguments.TryRemove("compress"); 64 | } 65 | return this; 66 | } 67 | 68 | public string Build() 69 | { 70 | if(arguments.Count == 0) 71 | { 72 | return url; 73 | } 74 | return url + "?" + string.Join("&", arguments.Select(x => $"{x.Key}={x.Value}")); 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /Miki.Discord.Gateway/WebSocket/DefaultWebSocketClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.WebSockets; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace Miki.Discord.Gateway.WebSocket 7 | { 8 | /// 9 | /// Default WebSocket implementation with . 10 | /// 11 | public class DefaultWebSocketClient : IWebSocketClient 12 | { 13 | private readonly ClientWebSocket socket; 14 | 15 | /// 16 | /// Creates a new instance of the websocket. 17 | /// 18 | public DefaultWebSocketClient() 19 | { 20 | socket = new ClientWebSocket(); 21 | } 22 | 23 | /// 24 | public void Dispose() 25 | { 26 | socket.Dispose(); 27 | } 28 | 29 | /// 30 | public WebSocketCloseStatus? CloseStatus => socket.CloseStatus; 31 | 32 | /// 33 | public string CloseStatusDescription => socket.CloseStatusDescription; 34 | 35 | /// 36 | public async ValueTask CloseAsync( 37 | WebSocketCloseStatus closeStatus, 38 | string closeStatusDescription, 39 | CancellationToken token) 40 | { 41 | await socket.CloseOutputAsync(closeStatus, closeStatusDescription, token); 42 | } 43 | 44 | /// 45 | public async ValueTask ConnectAsync(Uri endpoint, CancellationToken token) 46 | { 47 | await socket.ConnectAsync(endpoint, token); 48 | } 49 | 50 | /// 51 | public async ValueTask ReceiveAsync( 52 | Memory payload, CancellationToken token) 53 | { 54 | return await socket.ReceiveAsync(payload, token); 55 | } 56 | 57 | /// 58 | public async ValueTask SendAsync( 59 | ArraySegment payload, 60 | WebSocketMessageType type, 61 | bool endOfMessage, 62 | CancellationToken token) 63 | { 64 | await socket.SendAsync(payload, type, endOfMessage, token); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Miki.Discord.Gateway/WebSocket/IWebsocketClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.WebSockets; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace Miki.Discord.Gateway.WebSocket 7 | { 8 | /// 9 | /// Abstraction for websockets. 10 | /// 11 | public interface IWebSocketClient : IDisposable 12 | { 13 | /// 14 | /// Current reason if websocket is closed. 15 | /// 16 | WebSocketCloseStatus? CloseStatus { get; } 17 | 18 | /// 19 | /// Additional server-fed data sent with a close. 20 | /// 21 | string CloseStatusDescription { get; } 22 | 23 | /// 24 | /// Closes the WebSocket connection. 25 | /// 26 | ValueTask CloseAsync( 27 | WebSocketCloseStatus closeStatus, string closeStatusDescription, CancellationToken token); 28 | 29 | /// 30 | /// Connects a WebSocket. 31 | /// 32 | ValueTask ConnectAsync(Uri endpoint, CancellationToken token); 33 | 34 | /// 35 | /// Receive a buffer from the WebSocket stream. 36 | /// 37 | ValueTask ReceiveAsync( 38 | Memory payload, 39 | CancellationToken token); 40 | 41 | /// 42 | /// Send a message to the server. 43 | /// 44 | ValueTask SendAsync( 45 | ArraySegment payload, 46 | WebSocketMessageType type, 47 | bool endOfMessage, 48 | CancellationToken token); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Miki.Discord.Rest/Converters/StringToEnumConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Text.Json; 4 | using System.Text.Json.Serialization; 5 | 6 | namespace Miki.Discord.Rest.Converters 7 | { 8 | public class StringToEnumConverter : JsonConverter where T : Enum 9 | { 10 | public StringToEnumConverter() 11 | { 12 | if(!typeof(T).IsEnum) 13 | { 14 | throw new InvalidOperationException( 15 | "Cannot use non-enum types in StringToEnumConverter"); 16 | } 17 | } 18 | 19 | /// 20 | public override T Read( 21 | ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 22 | { 23 | var value = reader.GetString(); 24 | if(Enum.TryParse(typeof(T), value, out var enumValue)) 25 | { 26 | return (T)enumValue; 27 | } 28 | 29 | throw new InvalidOperationException( 30 | $"Value '{value}' was not a valid short."); 31 | } 32 | 33 | /// 34 | public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) 35 | { 36 | var numericValue = (long)(object)value; 37 | writer.WriteStringValue(numericValue.ToString()); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Miki.Discord.Rest/Converters/StringToShortConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Text.Json; 4 | using System.Text.Json.Serialization; 5 | 6 | namespace Miki.Discord.Rest.Converters 7 | { 8 | public sealed class StringToShortConverter : JsonConverter 9 | { 10 | /// 11 | public override short Read( 12 | ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 13 | { 14 | var value = reader.GetString(); 15 | if(short.TryParse(value, out var shortValue)) 16 | { 17 | return shortValue; 18 | } 19 | 20 | throw new InvalidOperationException( 21 | $"Value '{value}' was not a valid short."); 22 | } 23 | 24 | /// 25 | public override void Write(Utf8JsonWriter writer, short value, JsonSerializerOptions options) 26 | { 27 | writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture)); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /Miki.Discord.Rest/Converters/StringToUlongConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Text.Json; 4 | using System.Text.Json.Serialization; 5 | 6 | namespace Miki.Discord.Rest.Converters 7 | { 8 | public sealed class StringToUlongConverter : JsonConverter 9 | { 10 | /// 11 | public override ulong Read( 12 | ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 13 | { 14 | var value = reader.GetString(); 15 | if(ulong.TryParse(value, out var longValue)) 16 | { 17 | return longValue; 18 | } 19 | 20 | throw new InvalidOperationException( 21 | $"Value '{value}' was not a valid integer."); 22 | } 23 | 24 | /// 25 | public override void Write(Utf8JsonWriter writer, ulong value, JsonSerializerOptions options) 26 | { 27 | writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture)); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /Miki.Discord.Rest/Converters/UserAvatarConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | using Miki.Discord.Common.Packets; 5 | 6 | namespace Miki.Discord.Rest.Converters 7 | { 8 | public class UserAvatarConverter : JsonConverter 9 | { 10 | public override UserAvatar Read( 11 | ref Utf8JsonReader reader, Type objectType, JsonSerializerOptions serializer) 12 | { 13 | // Never need to be read. 14 | throw new NotSupportedException(); 15 | } 16 | 17 | public override void Write( 18 | Utf8JsonWriter writer, UserAvatar value, JsonSerializerOptions serializer) 19 | { 20 | if(value == null) 21 | { 22 | writer.WriteNullValue(); 23 | return; 24 | } 25 | 26 | string imageData = Convert.ToBase64String(value.Stream.GetBuffer()); 27 | writer.WriteStringValue($"data:image/{value.Type.ToString().ToLower()};base64,{imageData}"); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Miki.Discord.Rest/Exceptions/DiscordRestException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Miki.Discord.Rest.Exceptions 4 | { 5 | public class DiscordRestException : Exception 6 | { 7 | readonly DiscordRestError _error; 8 | 9 | public DiscordRestException(DiscordRestError error) 10 | { 11 | _error = error; 12 | } 13 | 14 | public override string ToString() 15 | { 16 | return $"{nameof(DiscordRestException)}: {_error.Code} - {_error.Message}\n{StackTrace}"; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Miki.Discord.Rest/Http/DiscordRateLimiter.cs: -------------------------------------------------------------------------------- 1 | using Miki.Cache; 2 | using Miki.Net.Http; 3 | using System; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace Miki.Discord.Rest.Http 8 | { 9 | public class DiscordRateLimiter : IRateLimiter 10 | { 11 | private readonly ICacheClient cache; 12 | 13 | private const string LimitHeader = "X-RateLimit-Limit"; 14 | private const string RemainingHeader = "X-RateLimit-Remaining"; 15 | private const string ResetHeader = "X-RateLimit-Reset"; 16 | private const string GlobalHeader = "X-RateLimit-Global"; 17 | 18 | private string GetCacheKey(string route, string id) 19 | => $"discord:ratelimit:{route}:{id}"; 20 | 21 | public DiscordRateLimiter(ICacheClient cache) 22 | { 23 | this.cache = cache; 24 | } 25 | 26 | public async Task CanStartRequestAsync(RequestMethod method, string requestUri) 27 | { 28 | string key = GetCacheKey(requestUri.Split('/')[0], requestUri.Split('/')[1]); 29 | 30 | Ratelimit rateLimit = await cache.GetAsync(key); 31 | rateLimit.Remaining--; 32 | 33 | await cache.UpsertAsync(key, rateLimit); 34 | 35 | return !rateLimit.IsRatelimited(); 36 | } 37 | 38 | public async Task OnRequestSuccessAsync(HttpResponse response) 39 | { 40 | var httpMessage = response.HttpResponseMessage; 41 | 42 | Uri requestUri = httpMessage.RequestMessage.RequestUri; 43 | string[] paths = requestUri.AbsolutePath.Split('/'); 44 | string key = GetCacheKey(paths[2], paths[3]); 45 | 46 | if(httpMessage.Headers.Contains(LimitHeader)) 47 | { 48 | var ratelimit = new Ratelimit(); 49 | if(httpMessage.Headers.TryGetValues(RemainingHeader, out var values)) 50 | { 51 | ratelimit.Remaining = int.Parse(values.FirstOrDefault()); 52 | } 53 | 54 | if(httpMessage.Headers.TryGetValues(LimitHeader, out var limitValues)) 55 | { 56 | ratelimit.Limit = int.Parse(limitValues.FirstOrDefault()); 57 | } 58 | 59 | if(httpMessage.Headers.TryGetValues(ResetHeader, out var resetValues)) 60 | { 61 | ratelimit.Reset = int.Parse(resetValues.FirstOrDefault()); 62 | } 63 | 64 | if(httpMessage.Headers.TryGetValues(GlobalHeader, out var globalValues)) 65 | { 66 | ratelimit.Global = int.Parse(globalValues.FirstOrDefault()); 67 | } 68 | 69 | await cache.UpsertAsync(key, ratelimit); 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Miki.Discord.Rest/Miki.Discord.Rest.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1 5 | true 6 | Discord rest API 7 | Velddev 8 | Miki.Discord.Rest 9 | Velddev 10 | 4.0.0-rc.5 11 | https://github.com/mikibot/miki.discord 12 | 13 | LICENSE 14 | true 15 | icon.png 16 | 17 | 18 | 19 | D:\Projects\Miki.Discord\Miki.Discord.Rest\Miki.Discord.Rest.xml 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | True 32 | 33 | 34 | 35 | True 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /Miki.Discord.Rest/Miki.Discord.Rest.csproj.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True -------------------------------------------------------------------------------- /Miki.Discord.Rest/Models/DiscordPruneObject.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | 3 | namespace Miki.Discord.Rest 4 | { 5 | [DataContract] 6 | public class DiscordPruneObject 7 | { 8 | [DataMember(Name = "pruned")] 9 | public int Pruned { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Miki.Discord.Rest/Models/DiscordRestError.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace Miki.Discord.Rest 4 | { 5 | public class DiscordRestError 6 | { 7 | [JsonProperty("code")] 8 | public int Code { get; set; } 9 | 10 | [JsonProperty("message")] 11 | public string Message { get; set; } 12 | 13 | public override string ToString() 14 | { 15 | return $"{Code}: {Message}\n"; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Miki.Discord.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True 3 | True -------------------------------------------------------------------------------- /Miki.Discord/Cache/DefaultCacheHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Miki.Cache; 5 | using Miki.Discord.Common; 6 | using Miki.Discord.Common.Packets; 7 | using Miki.Discord.Internal.Repositories; 8 | using Miki.Patterns.Repositories; 9 | 10 | namespace Miki.Discord.Cache 11 | { 12 | /// 13 | /// Handles entity caching 14 | /// 15 | public class DefaultCacheHandler : ICacheHandler 16 | { 17 | private readonly IExtendedCacheClient cache; 18 | 19 | /// 20 | public IAsyncRepository Channels { get; } 21 | 22 | /// 23 | public IAsyncRepository Guilds { get; } 24 | 25 | /// 26 | public IAsyncRepository Members { get; } 27 | 28 | /// 29 | public IAsyncRepository Roles { get; } 30 | 31 | /// 32 | public IAsyncRepository Users { get; } 33 | 34 | /// 35 | /// Default caching strategy for Miki.Discord 36 | /// 37 | /// Cache provider 38 | public DefaultCacheHandler(IExtendedCacheClient cache, IApiClient apiClient) 39 | { 40 | this.cache = cache; 41 | 42 | Channels = new DiscordChannelCacheRepository(cache, apiClient); 43 | Guilds = new DiscordGuildCacheRepository(cache, apiClient); 44 | Members = new DiscordMemberCacheRepository(cache, apiClient); 45 | Roles = new DiscordRoleCacheRepository(cache, apiClient); 46 | Users = new DiscordUserCacheRepository(cache, apiClient); 47 | } 48 | 49 | /// 50 | public async ValueTask GetCurrentUserAsync() 51 | { 52 | return await cache.HashGetAsync(CacheHelpers.UsersCacheKey, "me"); 53 | } 54 | 55 | public async ValueTask> GetChannelsFromGuildAsync(ulong guildId) 56 | { 57 | return (await cache.HashValuesAsync(CacheHelpers.ChannelsKey(guildId))).ToList(); 58 | } 59 | 60 | public async ValueTask> GetMembersFromGuildAsync(ulong guildId) 61 | { 62 | return (await cache.HashValuesAsync(CacheHelpers.GuildMembersKey(guildId))).ToList(); 63 | } 64 | 65 | public async ValueTask> GetRolesFromGuildAsync(ulong guildId) 66 | { 67 | return (await cache.HashValuesAsync(CacheHelpers.GuildRolesKey(guildId))).ToList(); 68 | } 69 | 70 | /// 71 | public async ValueTask SetCurrentUserAsync(DiscordUserPacket packet) 72 | { 73 | await cache.HashUpsertAsync(CacheHelpers.UsersCacheKey, "me", packet); 74 | } 75 | public async ValueTask HasGuildAsync(ulong guildId) 76 | { 77 | var guild = await Guilds.GetAsync(guildId); 78 | return guild != null; 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /Miki.Discord/Cache/ICacheHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Miki.Discord.Common; 4 | using Miki.Discord.Common.Packets; 5 | using Miki.Patterns.Repositories; 6 | 7 | namespace Miki.Discord.Cache 8 | { 9 | /// 10 | /// Cache service for Miki.Discord. 11 | /// 12 | public interface ICacheHandler 13 | { 14 | /// 15 | /// Channel repository 16 | /// 17 | IAsyncRepository Channels { get; } 18 | 19 | /// 20 | /// Guild repository 21 | /// 22 | IAsyncRepository Guilds { get; } 23 | 24 | /// 25 | /// Guild Member repository 26 | /// 27 | IAsyncRepository Members { get; } 28 | 29 | /// 30 | /// Guild Role repository 31 | /// 32 | IAsyncRepository Roles { get; } 33 | 34 | /// 35 | /// User repository 36 | /// 37 | IAsyncRepository Users { get; } 38 | 39 | /// 40 | /// Gets the current bot connected to the gateway. 41 | /// 42 | ValueTask GetCurrentUserAsync(); 43 | ValueTask> GetChannelsFromGuildAsync(ulong guildId); 44 | ValueTask> GetMembersFromGuildAsync(ulong guildId); 45 | ValueTask> GetRolesFromGuildAsync(ulong guildId); 46 | 47 | /// 48 | /// Sets the current user connected to the gateway. 49 | /// 50 | ValueTask SetCurrentUserAsync(DiscordUserPacket user); 51 | 52 | ValueTask HasGuildAsync(ulong guildId); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Miki.Discord/DiscordClientConfiguration.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | 3 | using Miki.Cache; 4 | using Miki.Discord.Common; 5 | 6 | namespace Miki.Discord 7 | { 8 | /// 9 | /// Configuration properties for DiscordClient. 10 | /// 11 | public class DiscordClientConfiguration 12 | { 13 | /// 14 | /// Creates a new Configuration setup. 15 | /// 16 | public DiscordClientConfiguration( 17 | IApiClient apiClient, IGateway gateway, IExtendedCacheClient cache) 18 | { 19 | ApiClient = apiClient; 20 | Gateway = gateway; 21 | CacheClient = cache; 22 | } 23 | 24 | public IApiClient ApiClient { get; } 25 | 26 | public IGateway Gateway { get; } 27 | 28 | public IExtendedCacheClient CacheClient { get; } 29 | } 30 | } -------------------------------------------------------------------------------- /Miki.Discord/Exceptions/Discord/DiscordPermissionException.cs: -------------------------------------------------------------------------------- 1 | using Miki.Discord.Common; 2 | using System; 3 | 4 | namespace Miki.Discord.Exceptions 5 | { 6 | public class DiscordPermissionException : Exception 7 | { 8 | public DiscordPermissionException(GuildPermission permissions) 9 | : base($"Could not perform actions as permission(s) {permissions} is required.") 10 | { } 11 | 12 | public DiscordPermissionException(string message) 13 | : base(message) 14 | { } 15 | } 16 | } -------------------------------------------------------------------------------- /Miki.Discord/Helpers/AbstractionHelpers.cs: -------------------------------------------------------------------------------- 1 | using Miki.Discord.Common; 2 | using Miki.Discord.Common.Packets.API; 3 | using Miki.Discord.Internal.Data; 4 | 5 | namespace Miki.Discord.Helpers 6 | { 7 | internal static class AbstractionHelpers 8 | { 9 | internal static IDiscordMessage ResolveMessage(IDiscordClient client, DiscordMessagePacket packet) 10 | { 11 | if (packet == null) 12 | { 13 | return null; 14 | } 15 | 16 | if (packet.GuildId.HasValue) 17 | { 18 | return new DiscordGuildMessage(packet, client); 19 | } 20 | return new DiscordMessage(packet, client); 21 | } 22 | 23 | internal static IDiscordChannel ResolveChannel( 24 | IDiscordClient client, DiscordChannelPacket packet) 25 | { 26 | switch (packet.Type) 27 | { 28 | case ChannelType.GuildText: 29 | return new DiscordGuildTextChannel(packet, client); 30 | 31 | case ChannelType.DirectText: 32 | case ChannelType.GroupDirect: 33 | return new DiscordTextChannel(packet, client); 34 | 35 | default: 36 | return new DiscordGuildChannel(packet, client); 37 | } 38 | } 39 | 40 | internal static T ResolveChannelAs(IDiscordClient client, DiscordChannelPacket packet) 41 | where T : IDiscordChannel 42 | { 43 | var channel = ResolveChannel(client, packet); 44 | if(channel is T t) 45 | { 46 | return t; 47 | } 48 | 49 | return default; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Miki.Discord/Helpers/DiscordChannelHelper.cs: -------------------------------------------------------------------------------- 1 | using Miki.Discord.Internal.Data; 2 | using System.Linq; 3 | using Miki.Discord.Common; 4 | using Miki.Discord.Internal; 5 | using System.Threading.Tasks; 6 | 7 | namespace Miki.Discord.Helpers 8 | { 9 | public static class DiscordChannelHelper 10 | { 11 | public static async Task CreateMessageAsync( 12 | IDiscordClient client, DiscordChannelPacket channel, MessageArgs args) 13 | { 14 | var message = await client.ApiClient.SendMessageAsync(channel.Id, args); 15 | if(channel.Type == ChannelType.GuildText 16 | || channel.Type == ChannelType.GuildVoice 17 | || channel.Type == ChannelType.GuildCategory 18 | || channel.Type == ChannelType.GuildNews 19 | || channel.Type == ChannelType.GuildStore) 20 | { 21 | message.GuildId = channel.GuildId; 22 | } 23 | return new DiscordMessage(message, client); 24 | } 25 | 26 | public static GuildPermission GetOverwritePermissions( 27 | IDiscordGuildUser user, IDiscordGuildChannel channel, GuildPermission basePermissions) 28 | { 29 | var permissions = basePermissions; 30 | if(permissions.HasFlag(GuildPermission.Administrator)) 31 | { 32 | return GuildPermission.All; 33 | } 34 | 35 | if(channel.PermissionOverwrites != null) 36 | { 37 | PermissionOverwrite overwriteEveryone = channel.PermissionOverwrites 38 | .FirstOrDefault(x => x.Id == channel.GuildId); 39 | 40 | if(overwriteEveryone != null) 41 | { 42 | permissions &= ~overwriteEveryone.DeniedPermissions; 43 | permissions |= overwriteEveryone.AllowedPermissions; 44 | } 45 | 46 | PermissionOverwrite overwrites = new PermissionOverwrite(); 47 | 48 | if(user.RoleIds != null) 49 | { 50 | foreach(ulong roleId in user.RoleIds) 51 | { 52 | PermissionOverwrite roleOverwrites = channel.PermissionOverwrites 53 | .FirstOrDefault(x => x.Id == roleId); 54 | 55 | if(roleOverwrites != null) 56 | { 57 | overwrites.AllowedPermissions |= roleOverwrites.AllowedPermissions; 58 | overwrites.DeniedPermissions &= roleOverwrites.DeniedPermissions; 59 | } 60 | } 61 | } 62 | 63 | permissions &= ~overwrites.DeniedPermissions; 64 | permissions |= overwrites.AllowedPermissions; 65 | 66 | PermissionOverwrite userOverwrite = channel.PermissionOverwrites 67 | .FirstOrDefault(x => x.Id == user.Id); 68 | 69 | if(userOverwrite != null) 70 | { 71 | permissions &= ~userOverwrite.DeniedPermissions; 72 | permissions |= userOverwrite.AllowedPermissions; 73 | } 74 | } 75 | 76 | return permissions; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Miki.Discord/Internal/Data/DiscordAttachment.cs: -------------------------------------------------------------------------------- 1 | using Miki.Discord.Common; 2 | 3 | namespace Miki.Discord.Internal.Data 4 | { 5 | internal class DiscordAttachment : IDiscordAttachment 6 | { 7 | readonly DiscordAttachmentPacket _packet; 8 | 9 | internal DiscordAttachment(DiscordAttachmentPacket packet) 10 | { 11 | _packet = packet; 12 | } 13 | 14 | /// 15 | public string FileName => _packet.FileName; 16 | 17 | /// 18 | public int? Height => _packet.Height; 19 | 20 | /// 21 | public ulong Id => _packet.Id; 22 | 23 | /// 24 | public string ProxyUrl => _packet.ProxyUrl; 25 | 26 | /// 27 | public int Size => _packet.Size; 28 | 29 | /// 30 | public string Url => _packet.Url; 31 | 32 | /// 33 | public int? Width => _packet.Width; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Miki.Discord/Internal/Data/DiscordChannel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Miki.Discord.Common; 4 | 5 | namespace Miki.Discord.Internal.Data 6 | { 7 | public class DiscordChannel : IDiscordChannel 8 | { 9 | protected DiscordChannelPacket packet; 10 | protected IDiscordClient client; 11 | 12 | public DiscordChannel() 13 | { 14 | } 15 | 16 | public DiscordChannel(DiscordChannelPacket packet, IDiscordClient client) 17 | { 18 | this.packet = packet; 19 | this.client = client; 20 | } 21 | 22 | public string Name 23 | => packet.Name; 24 | 25 | public ulong Id 26 | => packet.Id; 27 | 28 | public bool IsNsfw 29 | => packet?.IsNsfw.GetValueOrDefault(false) ?? false; 30 | 31 | public async Task DeleteAsync() 32 | { 33 | await client.ApiClient.DeleteChannelAsync(Id); 34 | } 35 | 36 | public Task ModifyAsync(object todo) 37 | { 38 | throw new NotImplementedException(); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /Miki.Discord/Internal/Data/DiscordGuildChannel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Miki.Discord.Common; 5 | using Miki.Discord.Helpers; 6 | 7 | namespace Miki.Discord.Internal.Data 8 | { 9 | public class DiscordGuildChannel : DiscordChannel, IDiscordGuildChannel 10 | { 11 | public IEnumerable PermissionOverwrites => packet.PermissionOverwrites; 12 | 13 | public DiscordGuildChannel(DiscordChannelPacket packet, IDiscordClient client) 14 | : base(packet, client) 15 | { 16 | } 17 | 18 | public ulong GuildId 19 | => packet.GuildId ?? throw new InvalidOperationException("Guild ID was invalid"); 20 | 21 | public ChannelType Type 22 | => packet.Type; 23 | 24 | public async Task GetGuildAsync() 25 | => await client.GetGuildAsync(GuildId); 26 | 27 | public async Task GetPermissionsAsync(IDiscordGuildUser user = null) 28 | { 29 | IDiscordGuild guild = await GetGuildAsync(); 30 | if(user == null) 31 | { 32 | user = await guild.GetSelfAsync(); 33 | } 34 | GuildPermission permissions = await guild.GetPermissionsAsync(user); 35 | return DiscordChannelHelper.GetOverwritePermissions(user, this, permissions); 36 | } 37 | 38 | public async Task GetSelfAsync() 39 | { 40 | var selfUser = await client.GetSelfAsync(); 41 | return await GetUserAsync(selfUser.Id); 42 | } 43 | 44 | public async Task GetUserAsync(ulong id) 45 | => await client.GetGuildUserAsync(id, GuildId); 46 | } 47 | } -------------------------------------------------------------------------------- /Miki.Discord/Internal/Data/DiscordGuildMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Miki.Discord.Common; 4 | using Miki.Discord.Common.Models; 5 | using Miki.Discord.Common.Packets.API; 6 | 7 | namespace Miki.Discord.Internal.Data 8 | { 9 | internal class DiscordGuildMessage : DiscordMessage, IDiscordGuildMessage 10 | { 11 | /// 12 | public DiscordGuildMessage(DiscordMessagePacket packet, IDiscordClient client) 13 | : base(packet, client) 14 | { 15 | } 16 | 17 | /// 18 | public ulong GuildId 19 | => packet.GuildId ?? throw new InvalidOperationException("Guild Message does not have guild ID set."); 20 | 21 | /// 22 | public Task GetGuildAsync() 23 | { 24 | return client.GetGuildAsync(GuildId); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Miki.Discord/Internal/Data/DiscordGuildTextChannel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Miki.Discord.Common; 7 | using Miki.Discord.Helpers; 8 | 9 | namespace Miki.Discord.Internal.Data 10 | { 11 | public class DiscordGuildTextChannel : DiscordGuildChannel, IDiscordTextChannel 12 | { 13 | public DiscordGuildTextChannel(DiscordChannelPacket packet, IDiscordClient client) 14 | : base(packet, client) 15 | { 16 | } 17 | 18 | public async Task DeleteMessagesAsync(params ulong[] id) 19 | { 20 | if(id.Length == 0) 21 | { 22 | throw new ArgumentNullException(nameof(id)); 23 | } 24 | 25 | if(id.Length < 2) 26 | { 27 | await client.ApiClient.DeleteMessageAsync(Id, id[0]); 28 | } 29 | 30 | if(id.Length > 100) 31 | { 32 | // TODO: Remove the messages in batches. 33 | // Note: Before we can implement this we have to implement the ratelimit queue. 34 | id = id.Take(100).ToArray(); 35 | } 36 | 37 | await client.ApiClient.DeleteMessagesAsync(Id, id); 38 | } 39 | 40 | public async Task DeleteMessagesAsync(params IDiscordMessage[] messages) 41 | { 42 | await DeleteMessagesAsync(messages.Select(x => x.Id).ToArray()); 43 | } 44 | 45 | public async Task GetMessageAsync(ulong id) 46 | { 47 | return new DiscordMessage(await client.ApiClient.GetMessageAsync(Id, id), client); 48 | } 49 | 50 | public async Task> GetMessagesAsync(int amount = 100) 51 | { 52 | return (await client.ApiClient.GetMessagesAsync(Id, amount)) 53 | .Select(x => new DiscordMessage(x, client)); 54 | } 55 | 56 | public async Task SendFileAsync(Stream file, string fileName, string content, bool isTTS = false, DiscordEmbed embed = null) 57 | => await client.SendFileAsync( 58 | Id, 59 | file, 60 | fileName, 61 | new MessageArgs(content, embed, isTTS)); 62 | 63 | public async Task SendMessageAsync( 64 | string content, 65 | bool isTTS = false, 66 | DiscordEmbed embed = null) 67 | { 68 | var permissions = await GetPermissionsAsync(); 69 | if(!permissions.HasFlag(GuildPermission.SendMessages)) 70 | { 71 | throw new UnauthorizedAccessException(); 72 | } 73 | 74 | return await DiscordChannelHelper.CreateMessageAsync( 75 | client, 76 | packet, 77 | new MessageArgs(content, embed, isTTS)); 78 | 79 | } 80 | 81 | public async Task TriggerTypingAsync() 82 | { 83 | await client.ApiClient.TriggerTypingAsync(Id); 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /Miki.Discord/Internal/Data/DiscordGuildUser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Miki.Discord.Common; 6 | 7 | namespace Miki.Discord.Internal.Data 8 | { 9 | public class DiscordGuildUser : DiscordUser, IDiscordGuildUser 10 | { 11 | private readonly DiscordGuildMemberPacket packet; 12 | 13 | public DiscordGuildUser(DiscordGuildMemberPacket packet, IDiscordClient client) 14 | : base(packet.User, client) 15 | { 16 | this.packet = packet; 17 | } 18 | 19 | public string Nickname 20 | => packet.Nickname; 21 | 22 | public IReadOnlyCollection RoleIds 23 | => packet.Roles; 24 | 25 | public ulong GuildId 26 | => packet.GuildId; 27 | 28 | public DateTimeOffset JoinedAt 29 | => new DateTimeOffset(packet.JoinedAt); 30 | 31 | public DateTimeOffset? PremiumSince 32 | => packet.PremiumSince.HasValue 33 | ? new DateTimeOffset(packet.PremiumSince.Value) 34 | : (DateTimeOffset?) null; 35 | 36 | public async Task AddRoleAsync(IDiscordRole role) 37 | { 38 | await client.ApiClient.AddGuildMemberRoleAsync(GuildId, Id, role.Id); 39 | } 40 | 41 | public async Task GetGuildAsync() 42 | => await client.GetGuildAsync(GuildId); 43 | 44 | public async Task KickAsync(string reason = null) 45 | { 46 | await client.ApiClient.RemoveGuildMemberAsync(GuildId, Id, reason); 47 | } 48 | 49 | public async Task RemoveRoleAsync(IDiscordRole role) 50 | { 51 | if(role == null) 52 | { 53 | throw new ArgumentNullException(nameof(role)); 54 | } 55 | 56 | await client.ApiClient.RemoveGuildMemberRoleAsync(GuildId, Id, role.Id); 57 | } 58 | 59 | public async Task> GetRolesAsync() 60 | { 61 | var guild = await GetGuildAsync(); 62 | var roles = await guild.GetRolesAsync(); 63 | return roles.Where(x => RoleIds.Contains(x.Id)); 64 | } 65 | 66 | public async Task HasPermissionsAsync(GuildPermission permissions) 67 | { 68 | var guild = await GetGuildAsync(); 69 | GuildPermission p = await guild.GetPermissionsAsync(this); 70 | return p.HasFlag(permissions); 71 | } 72 | 73 | public async Task GetHierarchyAsync() 74 | { 75 | var guild = await GetGuildAsync(); 76 | return (await guild.GetRolesAsync()) 77 | .Where(x => RoleIds.Contains(x.Id)) 78 | .Max(x => x.Position); 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /Miki.Discord/Internal/Data/DiscordMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Miki.Discord.Common; 6 | using Miki.Discord.Common.Packets.API; 7 | 8 | namespace Miki.Discord.Internal.Data 9 | { 10 | public class DiscordMessage : IDiscordMessage 11 | { 12 | protected readonly DiscordMessagePacket packet; 13 | protected readonly IDiscordClient client; 14 | 15 | public DiscordMessage(DiscordMessagePacket packet, IDiscordClient client) 16 | { 17 | this.packet = packet; 18 | if(this.packet.GuildId != null 19 | && this.packet.Member != null) 20 | { 21 | this.packet.Member.User = this.packet.Author; 22 | this.packet.Member.GuildId = this.packet.GuildId.Value; 23 | } 24 | this.client = client; 25 | } 26 | 27 | /// 28 | public IReadOnlyList Attachments 29 | => packet.Attachments 30 | .Select(x => new DiscordAttachment(x)) 31 | .ToList(); 32 | 33 | /// 34 | public IDiscordUser Author 35 | => packet.Member == null 36 | ? new DiscordUser(packet.Author, client) 37 | : new DiscordGuildUser(packet.Member, client); 38 | 39 | /// 40 | public string Content 41 | => packet.Content; 42 | 43 | /// 44 | public ulong ChannelId 45 | => packet.ChannelId; 46 | 47 | 48 | /// 49 | public IReadOnlyList MentionedUserIds 50 | => packet.Mentions.Select(x => x.Id) 51 | .ToList(); 52 | 53 | /// 54 | public DateTimeOffset Timestamp 55 | => packet.Timestamp; 56 | 57 | /// 58 | public ulong Id 59 | => packet.Id; 60 | 61 | /// 62 | public DiscordMessageType Type 63 | => packet.Type; 64 | 65 | /// 66 | public async Task EditAsync(EditMessageArgs args) 67 | => await client.EditMessageAsync(ChannelId, Id, args.Content, args.Embed); 68 | 69 | /// 70 | public async Task DeleteAsync() 71 | => await client.ApiClient.DeleteMessageAsync(packet.ChannelId, packet.Id); 72 | 73 | /// 74 | public async Task GetChannelAsync() 75 | { 76 | var channel = await client.GetChannelAsync(packet.ChannelId, packet.GuildId); 77 | return channel as IDiscordTextChannel; 78 | } 79 | 80 | /// 81 | public async Task> GetReactionsAsync(DiscordEmoji emoji) 82 | => await client.GetReactionsAsync(packet.ChannelId, Id, emoji); 83 | 84 | /// 85 | public async Task CreateReactionAsync(DiscordEmoji emoji) 86 | => await client.ApiClient.CreateReactionAsync(ChannelId, Id, emoji); 87 | 88 | /// 89 | public async Task DeleteReactionAsync(DiscordEmoji emoji) 90 | => await client.ApiClient.DeleteReactionAsync(ChannelId, Id, emoji); 91 | 92 | /// 93 | public async Task DeleteReactionAsync(DiscordEmoji emoji, IDiscordUser user) 94 | => await DeleteReactionAsync(emoji, user.Id); 95 | 96 | /// 97 | public async Task DeleteReactionAsync(DiscordEmoji emoji, ulong userId) 98 | => await client.ApiClient.DeleteReactionAsync(ChannelId, Id, emoji, userId); 99 | 100 | /// 101 | public async Task DeleteAllReactionsAsync() 102 | => await client.ApiClient.DeleteReactionsAsync(ChannelId, Id); 103 | } 104 | } -------------------------------------------------------------------------------- /Miki.Discord/Internal/Data/DiscordPresence.cs: -------------------------------------------------------------------------------- 1 | using Miki.Discord.Common; 2 | 3 | namespace Miki.Discord.Internal.Data 4 | { 5 | internal class DiscordPresence : IDiscordPresence 6 | { 7 | /// 8 | public DiscordActivity Activity => packet.Game; 9 | 10 | /// 11 | public UserStatus Status => packet.Status; 12 | 13 | private readonly DiscordPresencePacket packet; 14 | private readonly IDiscordClient client; 15 | 16 | public DiscordPresence(DiscordPresencePacket packet, IDiscordClient client) 17 | { 18 | this.packet = packet; 19 | this.client = client; 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /Miki.Discord/Internal/Data/DiscordReaction.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Miki.Discord.Common; 3 | 4 | namespace Miki.Discord.Internal.Data 5 | { 6 | internal class DiscordReaction : IDiscordReaction 7 | { 8 | private readonly DiscordReactionPacket packet; 9 | private readonly IDiscordClient client; 10 | 11 | public ulong ChannelId => packet.ChannelId; 12 | 13 | public DiscordEmoji Emoji => packet.Emoji; 14 | 15 | public ulong MessageId => packet.MessageId; 16 | 17 | public DiscordReaction(DiscordReactionPacket packet, IDiscordClient client) 18 | { 19 | this.packet = packet; 20 | this.client = client; 21 | } 22 | 23 | /// 24 | public async ValueTask GetChannelAsync() 25 | { 26 | return await client.GetChannelAsync(packet.ChannelId, packet.GuildId) 27 | as IDiscordTextChannel; 28 | } 29 | 30 | /// 31 | public async ValueTask GetUserAsync() 32 | { 33 | return await client.GetUserAsync(packet.UserId); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /Miki.Discord/Internal/Data/DiscordRole.cs: -------------------------------------------------------------------------------- 1 | using Miki.Discord.Common; 2 | 3 | namespace Miki.Discord.Internal.Data 4 | { 5 | public class DiscordRole : IDiscordRole 6 | { 7 | protected readonly DiscordRolePacket packet; 8 | protected readonly IDiscordClient client; 9 | 10 | public DiscordRole(DiscordRolePacket packet, IDiscordClient client) 11 | { 12 | this.packet = packet; 13 | this.client = client; 14 | } 15 | 16 | public string Name 17 | => packet.Name; 18 | 19 | public Color Color 20 | => new Color((uint)packet.Color); 21 | 22 | public int Position 23 | => packet.Position; 24 | 25 | public ulong Id 26 | => packet.Id; 27 | 28 | public GuildPermission Permissions 29 | => packet.Permissions; 30 | 31 | public bool IsManaged 32 | => packet.Managed; 33 | 34 | public bool IsHoisted 35 | => packet.IsHoisted; 36 | 37 | public bool IsMentionable 38 | => packet.Mentionable; 39 | } 40 | } -------------------------------------------------------------------------------- /Miki.Discord/Internal/Data/DiscordSelfUser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Miki.Discord.Common; 4 | using Miki.Discord.Common.Packets; 5 | 6 | namespace Miki.Discord.Internal.Data 7 | { 8 | public class DiscordSelfUser : DiscordUser, IDiscordSelfUser 9 | { 10 | /// 11 | public DiscordSelfUser(DiscordUserPacket user, IDiscordClient client) 12 | : base(user, client) 13 | { } 14 | 15 | public Task GetDMChannelsAsync() 16 | { 17 | throw new NotImplementedException(); 18 | } 19 | 20 | public async Task ModifyAsync(Action modifyArgs) 21 | { 22 | var args = new UserModifyArgs(); 23 | modifyArgs(args); 24 | await client.ApiClient.ModifySelfAsync(args); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Miki.Discord/Internal/Data/DiscordTextChannel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Miki.Discord.Common; 7 | using Miki.Discord.Helpers; 8 | 9 | namespace Miki.Discord.Internal.Data 10 | { 11 | internal class DiscordTextChannel : DiscordChannel, IDiscordTextChannel 12 | { 13 | public DiscordTextChannel(DiscordChannelPacket packet, IDiscordClient client) 14 | : base(packet, client) 15 | { 16 | } 17 | public async Task DeleteMessagesAsync(params ulong[] id) 18 | { 19 | if(id.Length == 0) 20 | { 21 | throw new ArgumentNullException(); 22 | } 23 | 24 | if(id.Length < 2) 25 | { 26 | await client.ApiClient.DeleteMessageAsync(Id, id[0]); 27 | } 28 | 29 | if(id.Length > 100) 30 | { 31 | id = id.Take(100).ToArray(); 32 | } 33 | 34 | await client.ApiClient.DeleteMessagesAsync(Id, id); 35 | } 36 | 37 | public async Task DeleteMessagesAsync(params IDiscordMessage[] messages) 38 | { 39 | await DeleteMessagesAsync(messages.Select(x => x.Id).ToArray()); 40 | } 41 | 42 | public async Task GetMessageAsync(ulong id) 43 | { 44 | return new DiscordMessage(await client.ApiClient.GetMessageAsync(Id, id), client); 45 | } 46 | 47 | public async Task> GetMessagesAsync(int amount = 100) 48 | { 49 | return (await client.ApiClient.GetMessagesAsync(Id, amount)) 50 | .Select(x => new DiscordMessage(x, client)); 51 | } 52 | 53 | public async Task SendFileAsync( 54 | Stream file, 55 | string fileName, 56 | string content, 57 | bool isTTS = false, 58 | DiscordEmbed embed = null) 59 | { 60 | return await client.SendFileAsync( 61 | Id, file, fileName, new MessageArgs(content, embed, isTTS)); 62 | } 63 | 64 | public async Task SendMessageAsync( 65 | string content, bool isTTS = false, DiscordEmbed embed = null) 66 | => await DiscordChannelHelper.CreateMessageAsync( 67 | client, packet, new MessageArgs(content, embed, isTTS)); 68 | 69 | public async Task TriggerTypingAsync() 70 | { 71 | await client.ApiClient.TriggerTypingAsync(Id); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Miki.Discord/Internal/Data/DiscordUser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Miki.Discord.Common; 4 | using Miki.Discord.Common.Packets; 5 | 6 | namespace Miki.Discord.Internal.Data 7 | { 8 | public class DiscordUser : IDiscordUser 9 | { 10 | private readonly DiscordUserPacket user; 11 | 12 | protected readonly IDiscordClient client; 13 | 14 | public DiscordUser(DiscordUserPacket packet, IDiscordClient client) 15 | { 16 | this.client = client; 17 | user = packet; 18 | } 19 | 20 | public string Username 21 | => user.Username; 22 | 23 | public short Discriminator 24 | => user.Discriminator; 25 | 26 | public bool IsBot 27 | => user.IsBot; 28 | 29 | public ulong Id 30 | => user.Id; 31 | 32 | public string AvatarId 33 | => user.Avatar; 34 | 35 | public string GetAvatarUrl(ImageType type = ImageType.AUTO, ImageSize size = ImageSize.x256) 36 | => DiscordHelpers.GetAvatarUrl(user, type, size); 37 | 38 | public string Mention 39 | => $"<@{Id}>"; 40 | 41 | public async Task GetPresenceAsync() 42 | => await client.GetUserPresence(Id); 43 | 44 | public DateTimeOffset CreatedAt 45 | => this.GetCreationTime(); 46 | 47 | public async Task GetDMChannelAsync() 48 | { 49 | var currentUser = await client.GetSelfAsync(); 50 | if(Id == currentUser.Id) 51 | { 52 | throw new InvalidOperationException("Can't create a DM channel with self."); 53 | } 54 | return await client.CreateDMAsync(Id); 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /Miki.Discord/Internal/Repositories/CacheRepositoryHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Miki.Cache; 6 | 7 | namespace Miki.Discord.Internal.Repositories 8 | { 9 | internal static class CacheRepositoryHelpers 10 | { 11 | } 12 | } -------------------------------------------------------------------------------- /Miki.Discord/Internal/Repositories/DiscordChannelCacheRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Miki.Cache; 6 | using Miki.Discord.Common; 7 | using Miki.Patterns.Repositories; 8 | 9 | namespace Miki.Discord.Internal.Repositories 10 | { 11 | internal class DiscordChannelCacheRepository : BaseCacheRepository 12 | { 13 | private readonly IExtendedCacheClient cacheClient; 14 | private readonly IApiClient apiClient; 15 | 16 | public DiscordChannelCacheRepository(IExtendedCacheClient cacheClient, IApiClient apiClient) 17 | : base(cacheClient) 18 | { 19 | this.cacheClient = cacheClient; 20 | this.apiClient = apiClient; 21 | } 22 | 23 | protected override string GetCacheKey(DiscordChannelPacket value) 24 | { 25 | return CacheHelpers.ChannelsKey(value.GuildId); 26 | } 27 | 28 | protected override string GetMemberKey(DiscordChannelPacket value) 29 | { 30 | return value.Id.ToString(); 31 | } 32 | 33 | protected override async ValueTask GetFromCacheAsync(params object[] id) 34 | { 35 | if (id.Length == 1) 36 | { 37 | return await cacheClient.HashGetAsync( 38 | CacheHelpers.ChannelsKey(), id[0].ToString()); 39 | } 40 | 41 | return await cacheClient.HashGetAsync( 42 | CacheHelpers.ChannelsKey((ulong) id[1]), id[0].ToString()); 43 | } 44 | 45 | protected override async ValueTask GetFromApiAsync(params object[] id) 46 | { 47 | return await apiClient.GetChannelAsync((ulong) id[0]); 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /Miki.Discord/Internal/Repositories/DiscordGuildCacheRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Miki.Cache; 3 | using Miki.Discord.Common; 4 | 5 | namespace Miki.Discord.Internal.Repositories 6 | { 7 | public class DiscordGuildCacheRepository : BaseCacheRepository 8 | { 9 | private readonly IExtendedCacheClient cacheClient; 10 | private readonly IApiClient apiClient; 11 | 12 | public DiscordGuildCacheRepository(IExtendedCacheClient cacheClient, IApiClient apiClient) 13 | : base(cacheClient) 14 | { 15 | this.cacheClient = cacheClient; 16 | this.apiClient = apiClient; 17 | } 18 | 19 | protected override string GetCacheKey(DiscordGuildPacket value) 20 | { 21 | return CacheHelpers.GuildsCacheKey; 22 | } 23 | 24 | protected override string GetMemberKey(DiscordGuildPacket value) 25 | { 26 | return value.Id.ToString(); 27 | } 28 | 29 | protected override async ValueTask GetFromCacheAsync(params object[] id) 30 | { 31 | return await cacheClient.HashGetAsync(CacheHelpers.GuildsCacheKey, id[0].ToString()); 32 | } 33 | 34 | protected override async ValueTask GetFromApiAsync(params object[] id) 35 | { 36 | return await apiClient.GetGuildAsync((ulong) id[0]); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /Miki.Discord/Internal/Repositories/DiscordMemberCacheRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Miki.Cache; 3 | using Miki.Discord.Common; 4 | 5 | namespace Miki.Discord.Internal.Repositories 6 | { 7 | internal class DiscordMemberCacheRepository : BaseCacheRepository 8 | { 9 | private readonly IExtendedCacheClient cacheClient; 10 | private readonly IApiClient apiClient; 11 | 12 | public DiscordMemberCacheRepository(IExtendedCacheClient cacheClient, IApiClient apiClient) 13 | : base(cacheClient) 14 | { 15 | this.cacheClient = cacheClient; 16 | this.apiClient = apiClient; 17 | } 18 | 19 | protected override string GetCacheKey(DiscordGuildMemberPacket value) 20 | => CacheHelpers.GuildMembersKey(value.GuildId); 21 | 22 | protected override string GetMemberKey(DiscordGuildMemberPacket value) 23 | => value.User.Id.ToString(); 24 | 25 | protected override async ValueTask GetFromCacheAsync(params object[] id) 26 | { 27 | return await cacheClient.HashGetAsync( 28 | CacheHelpers.GuildMembersKey((ulong)id[1]), id[0].ToString()); 29 | } 30 | 31 | protected override async ValueTask GetFromApiAsync(params object[] id) 32 | { 33 | return await apiClient.GetGuildUserAsync((ulong) id[0], (ulong) id[1]); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /Miki.Discord/Internal/Repositories/DiscordRoleCacheRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Miki.Cache; 6 | using Miki.Discord.Common; 7 | using Miki.Patterns.Repositories; 8 | 9 | namespace Miki.Discord.Internal.Repositories 10 | { 11 | internal class DiscordRoleCacheRepository : BaseCacheRepository 12 | { 13 | private readonly IExtendedCacheClient cacheClient; 14 | private readonly IApiClient apiClient; 15 | 16 | public DiscordRoleCacheRepository(IExtendedCacheClient cacheClient, IApiClient apiClient) 17 | : base(cacheClient) 18 | { 19 | this.cacheClient = cacheClient; 20 | this.apiClient = apiClient; 21 | } 22 | 23 | protected override string GetCacheKey(DiscordRolePacket value) 24 | { 25 | return CacheHelpers.GuildRolesKey(value.GuildId); 26 | } 27 | 28 | protected override string GetMemberKey(DiscordRolePacket value) 29 | { 30 | return value.Id.ToString(); 31 | } 32 | 33 | protected override async ValueTask GetFromCacheAsync(params object[] id) 34 | { 35 | var rolePacket = await cacheClient.HashGetAsync( 36 | CacheHelpers.GuildRolesKey((ulong) id[1]), id[0].ToString()); 37 | 38 | if (rolePacket != null) 39 | { 40 | rolePacket.GuildId = (ulong) id[1]; 41 | } 42 | 43 | return rolePacket; 44 | } 45 | 46 | protected override async ValueTask GetFromApiAsync(params object[] id) 47 | { 48 | var roles = await apiClient.GetRolesAsync((ulong) id[1]); 49 | 50 | var role = roles?.FirstOrDefault(x => x.Id == (ulong) id[0]); 51 | 52 | if (role != null) 53 | { 54 | role.GuildId = (ulong) id[1]; 55 | } 56 | 57 | return role; 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /Miki.Discord/Internal/Repositories/DiscordUserCacheRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Miki.Cache; 3 | using Miki.Discord.Common; 4 | using Miki.Discord.Common.Packets; 5 | 6 | namespace Miki.Discord.Internal.Repositories 7 | { 8 | public class DiscordUserCacheRepository : BaseCacheRepository 9 | { 10 | private readonly IExtendedCacheClient cacheClient; 11 | private readonly IApiClient apiClient; 12 | 13 | public DiscordUserCacheRepository(IExtendedCacheClient cacheClient, IApiClient apiClient) 14 | : base(cacheClient) 15 | { 16 | this.cacheClient = cacheClient; 17 | this.apiClient = apiClient; 18 | } 19 | 20 | protected override string GetCacheKey(DiscordUserPacket value) 21 | { 22 | return CacheHelpers.UsersCacheKey; 23 | } 24 | 25 | protected override string GetMemberKey(DiscordUserPacket value) 26 | { 27 | return value.Id.ToString(); 28 | } 29 | 30 | protected override async ValueTask GetFromCacheAsync(params object[] id) 31 | { 32 | return await cacheClient.HashGetAsync(CacheHelpers.UsersCacheKey, id[0].ToString()); 33 | } 34 | 35 | protected override async ValueTask GetFromApiAsync(params object[] id) 36 | { 37 | return await apiClient.GetUserAsync((ulong) id[0]); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /Miki.Discord/Miki.Discord.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1 5 | Miki.Discord 6 | Miki.Discord 7 | false 8 | 4.0.0-rc.11 9 | Velddev 10 | Velddev 11 | Abstractified wrapper over Miki.Discord components. Used as a high level client relatable to other Discord libraries. 12 | discord, api 13 | 14 | https://github.com/mikibot/miki.discord 15 | LICENSE 16 | icon.png 17 | 18 | 19 | 20 | D:\Projects\Miki.Discord\Miki.Discord\Miki.Discord.xml 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | True 29 | 30 | 31 | 32 | True 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | 4 |

5 |

6 | A discord client with a focus on accessibility and customizability. 7 |

8 | 9 |

10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |

21 | -------------------------------------------------------------------------------- /Tests/Miki.Discord.Tests/Cache/CacheRepositoryTests.cs: -------------------------------------------------------------------------------- 1 | using Miki.Cache; 2 | using Miki.Cache.InMemory; 3 | using Miki.Discord.Internal.Repositories; 4 | using Miki.Serialization.Protobuf; 5 | using Moq; 6 | using System.Threading.Tasks; 7 | using Xunit; 8 | 9 | namespace Miki.Discord.Tests.Cache 10 | { 11 | class TestCache : BaseCacheRepository 12 | { 13 | public TestCache(IExtendedCacheClient cacheClient) 14 | : base(cacheClient) 15 | { 16 | } 17 | 18 | protected override string GetCacheKey(string value) 19 | => value; 20 | 21 | protected override ValueTask GetFromApiAsync(params object[] id) 22 | => new ValueTask(id[0].ToString()); 23 | 24 | protected override ValueTask GetFromCacheAsync(params object[] id) 25 | => new ValueTask(id[0].ToString()); 26 | 27 | protected override string GetMemberKey(string value) 28 | => value; 29 | } 30 | 31 | public class CacheRepositoryTests 32 | { 33 | [Fact] 34 | public async Task GetAsync_FetchesFromCache() 35 | { 36 | var cacheMock = new Mock(); 37 | cacheMock.Setup(x => x.GetAsync(It.IsAny())) 38 | .Returns(x => Task.FromResult(x)); 39 | 40 | var cache = new TestCache(cacheMock.Object); 41 | 42 | var response = await cache.GetAsync("12"); 43 | 44 | Assert.Equal("12", response); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Tests/Miki.Discord.Tests/Gateway/Converters/StringToEnumConverterTests.cs: -------------------------------------------------------------------------------- 1 | using Miki.Discord.Gateway.Converters; 2 | using System.IO; 3 | using System.Text.Json; 4 | using Xunit; 5 | 6 | namespace Miki.Discord.Tests.Gateway.Converters 7 | { 8 | internal enum TestEnum : long 9 | { 10 | A = 1, 11 | B = 2 12 | } 13 | 14 | internal struct TestObject 15 | { 16 | public TestEnum Enum { get; set; } 17 | } 18 | 19 | public class StringToEnumConverterTests 20 | { 21 | private JsonSerializerOptions options; 22 | 23 | public StringToEnumConverterTests() 24 | { 25 | options = new JsonSerializerOptions 26 | { 27 | Converters = { new StringToEnumConverter() } 28 | }; 29 | } 30 | 31 | [Fact] 32 | public void ValueToJsonString() 33 | { 34 | var value = new TestObject 35 | { 36 | Enum = TestEnum.A 37 | }; 38 | 39 | var jsonString = JsonSerializer.Serialize(value, options); 40 | Assert.Equal("{\"Enum\":\"1\"}", jsonString); 41 | } 42 | 43 | [Fact] 44 | public void JsonStringToValue() 45 | { 46 | var json = "{\"Enum\":\"1\"}"; 47 | var value = JsonSerializer.Deserialize(json, options); 48 | Assert.Equal(TestEnum.A, value.Enum); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Tests/Miki.Discord.Tests/Gateway/GatewayConnectionTests.cs: -------------------------------------------------------------------------------- 1 | using Miki.Discord.Common.Gateway; 2 | using Miki.Discord.Common.Packets; 3 | using Miki.Discord.Gateway; 4 | using Miki.Discord.Gateway.Connection; 5 | using Miki.Discord.Gateway.WebSocket; 6 | using Miki.Discord.Tests.Utils; 7 | using Moq; 8 | using System.Text.Json; 9 | using System.Threading.Tasks; 10 | using Xunit; 11 | 12 | namespace Miki.Discord.Tests.Gateway 13 | { 14 | public class GatewayConnectionTests 15 | { 16 | [Fact] 17 | public async Task TestStateModificationAsync() 18 | { 19 | var mockWebsocketClient = new MockWebsocketClient(); 20 | 21 | var gateway = new GatewayConnection( 22 | new GatewayProperties 23 | { 24 | Token = "cannot be null", 25 | WebSocketFactory = () => mockWebsocketClient, 26 | }); 27 | 28 | await gateway.StartAsync(default); 29 | await gateway.StopAsync(default); 30 | 31 | Assert.False(gateway.IsRunning); 32 | Assert.Equal(ConnectionStatus.Disconnected, gateway.ConnectionStatus); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Tests/Miki.Discord.Tests/Helpers.cs: -------------------------------------------------------------------------------- 1 | using Miki.Discord.Common; 2 | using Miki.Discord.Common.Packets; 3 | using Xunit; 4 | 5 | namespace Miki.Discord.Tests 6 | { 7 | public class Helpers 8 | { 9 | public class User 10 | { 11 | private readonly DiscordUserPacket user; 12 | 13 | public User() 14 | { 15 | user = new DiscordUserPacket() 16 | { 17 | Id = 111, 18 | Discriminator = 1234 19 | }; 20 | } 21 | 22 | [Fact] 23 | public void AvatarStatic() 24 | { 25 | user.Avatar = "2345243f3oim4foi34mf3k4f"; 26 | 27 | Assert.Equal( 28 | "https://cdn.discordapp.com/avatars/111/2345243f3oim4foi34mf3k4f.png?size=256", 29 | DiscordHelpers.GetAvatarUrl(user)); 30 | Assert.Equal( 31 | "https://cdn.discordapp.com/avatars/111/2345243f3oim4foi34mf3k4f.webp?size=2048", 32 | DiscordHelpers.GetAvatarUrl(user, ImageType.WEBP, ImageSize.x2048)); 33 | Assert.Equal("https://cdn.discordapp.com/avatars/111/2345243f3oim4foi34mf3k4f.jpeg?size=16", 34 | DiscordHelpers.GetAvatarUrl(user, ImageType.JPEG, ImageSize.x16)); 35 | } 36 | 37 | [Fact] 38 | public void AvatarAnimated() 39 | { 40 | user.Avatar = "a_owiejfowiejf432ijf3o"; 41 | 42 | Assert.Equal( 43 | "https://cdn.discordapp.com/avatars/111/a_owiejfowiejf432ijf3o.gif?size=256", 44 | DiscordHelpers.GetAvatarUrl(user)); 45 | Assert.Equal( 46 | "https://cdn.discordapp.com/avatars/111/a_owiejfowiejf432ijf3o.webp?size=2048", 47 | DiscordHelpers.GetAvatarUrl(user, ImageType.WEBP, ImageSize.x2048)); 48 | Assert.Equal( 49 | "https://cdn.discordapp.com/avatars/111/a_owiejfowiejf432ijf3o.jpeg?size=16", 50 | DiscordHelpers.GetAvatarUrl(user, ImageType.JPEG, ImageSize.x16)); 51 | } 52 | 53 | [Fact] 54 | public void AvatarNull() 55 | { 56 | user.Avatar = null; 57 | 58 | Assert.Equal 59 | ($"https://cdn.discordapp.com/embed/avatars/{user.Discriminator % 5}.png", 60 | DiscordHelpers.GetAvatarUrl(user)); 61 | Assert.Equal( 62 | $"https://cdn.discordapp.com/embed/avatars/{user.Discriminator % 5}.png", 63 | DiscordHelpers.GetAvatarUrl(user, ImageType.PNG, ImageSize.x512)); 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Tests/Miki.Discord.Tests/Miki.Discord.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Tests/Miki.Discord.Tests/Ratelimits.cs: -------------------------------------------------------------------------------- 1 | using Miki.Discord.Rest; 2 | using System; 3 | using Xunit; 4 | 5 | namespace Miki.Discord.Tests 6 | { 7 | public class Ratelimits 8 | { 9 | [Fact] 10 | public void IsRatelimited() 11 | { 12 | var rateLimit = new Ratelimit 13 | { 14 | Remaining = 5, 15 | Reset = (DateTimeOffset.Now + TimeSpan.FromSeconds(1)).ToUnixTimeSeconds() 16 | }; 17 | 18 | Assert.False(Ratelimit.IsRatelimited(rateLimit)); 19 | 20 | rateLimit.Remaining = 0; 21 | 22 | Assert.True(Ratelimit.IsRatelimited(rateLimit)); 23 | 24 | rateLimit.Global = 0; 25 | rateLimit.Remaining = 3; 26 | 27 | Assert.True(Ratelimit.IsRatelimited(rateLimit)); 28 | 29 | rateLimit.Global = null; 30 | rateLimit.Remaining = 0; 31 | rateLimit.Reset = 0; 32 | 33 | Assert.False(Ratelimit.IsRatelimited(rateLimit)); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Tests/Miki.Discord.Tests/Utils/MentionParserTests.cs: -------------------------------------------------------------------------------- 1 | using Miki.Discord.Common; 2 | using Xunit; 3 | 4 | namespace Miki.Discord.Tests.Utils 5 | { 6 | public class MentionParserTests 7 | { 8 | [Theory] 9 | [InlineData("<@0>", MentionType.USER, 0, null)] 10 | [InlineData("<@10000000>", MentionType.USER, 10000000, null)] 11 | [InlineData("<@!0>", MentionType.USER_NICKNAME, 0, null)] 12 | [InlineData("<@!10000000>", MentionType.USER_NICKNAME, 10000000, null)] 13 | [InlineData("<#0>", MentionType.CHANNEL, 0, null)] 14 | [InlineData("<#10000000>", MentionType.CHANNEL, 10000000, null)] 15 | [InlineData("<@&0>", MentionType.ROLE, 0, null)] 16 | [InlineData("<@&10000000>", MentionType.ROLE, 10000000, null)] 17 | [InlineData("<:anim:0>", MentionType.EMOJI, 0, "anim")] 18 | [InlineData("<:anim:10000000>", MentionType.EMOJI, 10000000, "anim")] 19 | [InlineData("", MentionType.ANIMATED_EMOJI, 0, "anim")] 20 | [InlineData("", MentionType.ANIMATED_EMOJI, 10000000, "anim")] 21 | [InlineData("@everyone", MentionType.USER_ALL, 0, "everyone")] 22 | [InlineData("@here", MentionType.USER_ALL_ONLINE, 0, "here")] 23 | public void ParseValidAsync( 24 | string userData, MentionType expectedType, ulong expectedId, string expectedData) 25 | { 26 | bool result = Mention.TryParse(userData, out var mention); 27 | 28 | Assert.True(result); 29 | Assert.Equal(expectedType, mention.Type); 30 | Assert.Equal(expectedId, mention.Id); 31 | Assert.Equal(expectedData, mention.Data); 32 | } 33 | 34 | [Theory] 35 | [InlineData("<")] 36 | public void ParseInvalidAsync(string userData) 37 | { 38 | Assert.False(Mention.TryParse(userData, out _)); 39 | } 40 | 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Tests/Miki.Discord.Tests/Utils/MockWebsocketClient.cs: -------------------------------------------------------------------------------- 1 | using Miki.Discord.Common; 2 | using Miki.Discord.Common.Gateway; 3 | using Miki.Discord.Common.Packets; 4 | using Miki.Discord.Gateway.WebSocket; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Net.WebSockets; 8 | using System.Text; 9 | using System.Text.Json; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | 13 | namespace Miki.Discord.Tests.Utils 14 | { 15 | public class MockWebsocketClient : IWebSocketClient 16 | { 17 | private bool isClosed; 18 | 19 | private List queuedMessages; 20 | private int currentIndex; 21 | 22 | public WebSocketCloseStatus? CloseStatus 23 | => isClosed ? WebSocketCloseStatus.NormalClosure : (WebSocketCloseStatus?)null; 24 | 25 | public string CloseStatusDescription => "none"; 26 | 27 | public MockWebsocketClient() 28 | { 29 | queuedMessages = new List(); 30 | } 31 | 32 | public ValueTask CloseAsync(WebSocketCloseStatus closeStatus, string closeStatusDescription, CancellationToken token) 33 | { 34 | isClosed = true; 35 | return default; 36 | } 37 | 38 | public void PushMessage(GatewayMessage message) 39 | { 40 | var json = JsonSerializer.Serialize(message); 41 | queuedMessages.Add(Encoding.UTF8.GetBytes(json)); 42 | } 43 | 44 | public ValueTask ConnectAsync(Uri endpoint, CancellationToken token) 45 | { 46 | isClosed = false; 47 | 48 | queuedMessages.Clear(); 49 | 50 | PushMessage( 51 | new GatewayMessage 52 | { 53 | Data = new GatewayHelloPacket 54 | { 55 | HeartbeatInterval = 2000000, 56 | TraceServers = new string[0], 57 | }, 58 | }); 59 | 60 | PushMessage( 61 | new GatewayMessage 62 | { 63 | Data = new GatewayReadyPacket 64 | { 65 | CurrentUser = new DiscordUserPacket(), 66 | Guilds = new DiscordGuildPacket[0], 67 | PrivateChannels = new DiscordChannelPacket[0], 68 | ProtocolVersion = 6, 69 | SessionId = "lol", 70 | Shard = new[] { 1, 0 }, 71 | TraceGuilds = new string[0] 72 | } 73 | }); 74 | 75 | return default; 76 | } 77 | 78 | public void Dispose() 79 | { 80 | 81 | } 82 | 83 | public async ValueTask ReceiveAsync( 84 | Memory payload, CancellationToken token) 85 | { 86 | while(queuedMessages.Count == 0) 87 | { 88 | token.ThrowIfCancellationRequested(); 89 | await Task.Delay(100, token); 90 | } 91 | 92 | var msg = queuedMessages[0]; 93 | msg.CopyTo(payload.Slice(currentIndex)); 94 | 95 | var count = Math.Min(payload.Length, msg.Length - currentIndex); 96 | currentIndex += count; 97 | 98 | var endOfMessage = currentIndex == msg.Length; 99 | 100 | if(endOfMessage) 101 | { 102 | queuedMessages.RemoveAt(0); 103 | currentIndex = 0; 104 | } 105 | 106 | return new ValueWebSocketReceiveResult( 107 | count, WebSocketMessageType.Text, endOfMessage); 108 | } 109 | 110 | public ValueTask SendAsync( 111 | ArraySegment payload, 112 | WebSocketMessageType type, 113 | bool endOfMessage, 114 | CancellationToken token) 115 | { 116 | return default; 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - master 3 | 4 | pool: 5 | vmImage: 'windows-latest' 6 | 7 | variables: 8 | buildConfiguration: 'Release' 9 | 10 | steps: 11 | - task: DotNetCoreCLI@2 12 | inputs: 13 | command: restore 14 | projects: '**/*.csproj' 15 | 16 | - task: DotNetCoreCLI@2 17 | displayName: Build 18 | inputs: 19 | command: build 20 | projects: '**/*.csproj' 21 | arguments: '--configuration $(buildConfiguration)' 22 | 23 | - task: DotNetCoreCLI@2 24 | inputs: 25 | command: test 26 | projects: '**/*Tests/*.csproj' 27 | arguments: '--configuration $(buildConfiguration) --collect "Code coverage"' --------------------------------------------------------------------------------