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