diff --git a/DiscordBotsList.Api.Adapter.Discord.Net/Adapter.cs b/DiscordBotsList.Api.Adapter.Discord.Net/Adapter.cs new file mode 100644 index 0000000..dbbf5ac --- /dev/null +++ b/DiscordBotsList.Api.Adapter.Discord.Net/Adapter.cs @@ -0,0 +1,89 @@ +#nullable enable + +using DiscordBotsList.Api.Objects; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace DiscordBotsList.Api.Adapter.Discord.Net +{ + public class Adapter : IAdapter + { + public event Action Posted = _ => { }; + private readonly TimeSpan updateTime; + + private CancellationTokenSource? cancellationTokenSource; + + public Adapter(TimeSpan updateTime) + { + if (updateTime < TimeSpan.FromMinutes(15)) + { + updateTime = TimeSpan.FromMinutes(15); + } + + this.updateTime = updateTime; + cancellationTokenSource = null; + } + + public virtual Task RunAsync() + { + throw new NotImplementedException(); + } + + public bool IsRunning() + { + return cancellationTokenSource != null; + } + + public void Start() + { + if (IsRunning()) + { + return; + } + + cancellationTokenSource = new CancellationTokenSource(); + + Task.Run(async () => + { + while (!cancellationTokenSource.Token.IsCancellationRequested) + { + try + { + await RunAsync(); + } + catch (Exception err) + { + cancellationTokenSource.Cancel(); + cancellationTokenSource = null; + + Posted?.Invoke(err); + break; + } + + Posted?.Invoke(null); + + await Task.Delay(updateTime, cancellationTokenSource.Token); + } + }, cancellationTokenSource.Token); + } + + public void Stop() + { + if (IsRunning()) + { + cancellationTokenSource!.Cancel(); + cancellationTokenSource = null; + } + } + + public async Task StopAsync() + { + if (IsRunning()) + { + await cancellationTokenSource!.CancelAsync(); + cancellationTokenSource = null; + } + } + } +} \ No newline at end of file diff --git a/DiscordBotsList.Api.Adapter.Discord.Net/DiscordBotsList.Api.Adapter.Discord.Net.csproj b/DiscordBotsList.Api.Adapter.Discord.Net/DiscordBotsList.Api.Adapter.Discord.Net.csproj index d1c8d48..e4af33a 100644 --- a/DiscordBotsList.Api.Adapter.Discord.Net/DiscordBotsList.Api.Adapter.Discord.Net.csproj +++ b/DiscordBotsList.Api.Adapter.Discord.Net/DiscordBotsList.Api.Adapter.Discord.Net.csproj @@ -2,23 +2,23 @@ net8.0 - Velddev, Faith Viola + Velddev, Faith Top.gg DiscordBotsList.Api.Adapter.Discord.Net - Adapter for Discord.Net + Top.gg API adapter for Discord.Net Initial release true Mike Veldsink - discord bots list org wrapper api discord.net + discord bots topgg api discord.net git - https://github.com/DiscordBotList/DBL-dotnet-Library - https://github.com/DiscordBotList/DBL-dotnet-Library - 1.5.0 + https://github.com/Top-gg-Community/dotnet-sdk + https://github.com/Top-gg-Community/dotnet-sdk + 2.0.0 LICENSE - + diff --git a/DiscordBotsList.Api.Adapter.Discord.Net/DiscordNetDiscordBotsListApi.cs b/DiscordBotsList.Api.Adapter.Discord.Net/DiscordNetDiscordBotsListApi.cs index 4882985..ddbcc03 100644 --- a/DiscordBotsList.Api.Adapter.Discord.Net/DiscordNetDiscordBotsListApi.cs +++ b/DiscordBotsList.Api.Adapter.Discord.Net/DiscordNetDiscordBotsListApi.cs @@ -1,8 +1,6 @@ using Discord; using Discord.WebSocket; -using DiscordBotsList.Api.Objects; using System; -using System.Threading.Tasks; namespace DiscordBotsList.Api.Adapter.Discord.Net { @@ -12,14 +10,9 @@ public static DiscordNetDblApi CreateDblApi(this DiscordSocketClient client, str { return new DiscordNetDblApi(client, dblToken); } - - public static ShardedDiscordNetDblApi CreateDblApi(this DiscordShardedClient client, string dblToken) - { - return new ShardedDiscordNetDblApi(client, dblToken); - } } - public class DiscordNetDblApi : AuthDiscordBotListApi + public class DiscordNetDblApi : DiscordBotListApi { protected IDiscordClient client; @@ -28,51 +21,17 @@ public DiscordNetDblApi(IDiscordClient client, string dblToken) : base(client.Cu this.client = client; } - public async Task GetBotAsync(IUser user) - { - return await GetBotAsync(user.Id); - } - - public async Task GetUserAsync(IUser user) - { - return await GetUserAsync(user.Id); - } - - /// - /// Creates an IAdapter that updates your servercount on RunAsync(). - /// - /// Your already connected client - /// - /// Timespan for when you want to submit guildcount, leave null if you want it every JoinedGuild - /// event - /// - /// an IAdapter that updates your servercount on RunAsync(), does not automatically do it yet. - /// - public virtual IAdapter CreateListener(TimeSpan? updateTime = null) - { - return new SubmissionAdapter(this, client, updateTime ?? TimeSpan.Zero); - } - } - - public class ShardedDiscordNetDblApi : DiscordNetDblApi - { - public ShardedDiscordNetDblApi(DiscordShardedClient client, string dblToken) : base(client, dblToken) - { - } - /// - /// Creates an IAdapter that updates your servercount on RunAsync(). + /// Creates a SubmissionAdapter that updates your servercount on RunAsync(). /// - /// Your already connected client /// - /// Timespan for when you want to submit guildcount, leave null if you want it every JoinedGuild - /// event + /// Timespan for when you want to submit guildcount, must be at least 15 minutes /// - /// an IAdapter that updates your servercount on RunAsync(), does not automatically do it yet. + /// A SubmissionAdapter that updates your servercount on RunAsync(). /// - public override IAdapter CreateListener(TimeSpan? updateTime = null) + public SubmissionAdapter CreateListener(TimeSpan? updateTime = null) { - return new ShardedSubmissionAdapter(this, client as DiscordShardedClient, updateTime ?? TimeSpan.Zero); + return new SubmissionAdapter(this, client, updateTime ?? TimeSpan.FromMinutes(15)); } } } \ No newline at end of file diff --git a/DiscordBotsList.Api.Adapter.Discord.Net/SubmissionAdapter.cs b/DiscordBotsList.Api.Adapter.Discord.Net/SubmissionAdapter.cs index e9951e4..5c5bd14 100644 --- a/DiscordBotsList.Api.Adapter.Discord.Net/SubmissionAdapter.cs +++ b/DiscordBotsList.Api.Adapter.Discord.Net/SubmissionAdapter.cs @@ -1,76 +1,25 @@ using Discord; -using Discord.WebSocket; using DiscordBotsList.Api.Objects; using System; -using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace DiscordBotsList.Api.Adapter.Discord.Net { - internal class SubmissionAdapter : IAdapter + public class SubmissionAdapter : Adapter { - protected AuthDiscordBotListApi api; - protected IDiscordClient client; + private readonly DiscordBotListApi api; + private readonly IDiscordClient client; - protected DateTime lastTimeUpdated; - protected TimeSpan updateTime; - - public SubmissionAdapter(AuthDiscordBotListApi api, IDiscordClient client, TimeSpan updateTime) + public SubmissionAdapter(DiscordBotListApi api, IDiscordClient client, TimeSpan updateTime) : base(updateTime) { + this.api = api; this.client = client; - this.updateTime = updateTime; - } - - public event Action Log; - - public virtual async Task RunAsync() - { - if (DateTime.Now > lastTimeUpdated + updateTime) - { - await api.UpdateStats( - (await client.GetGuildsAsync()).Count - ); - - lastTimeUpdated = DateTime.Now; - SendLog("Submitted stats to Top.gg!"); - } - } - - public virtual void Start() - { - } - - public virtual void Stop() - { - throw new NotImplementedException(); - } - - protected void SendLog(string msg) - { - Log?.Invoke(msg); - } - } - - internal class ShardedSubmissionAdapter : SubmissionAdapter, IAdapter - { - public ShardedSubmissionAdapter(AuthDiscordBotListApi api, DiscordShardedClient client, TimeSpan updateTime) - : base(api, client, updateTime) - { } public override async Task RunAsync() { - if (DateTime.Now > lastTimeUpdated + updateTime) - { - await api.UpdateStats( - 0, - (client as DiscordShardedClient).Shards.Count, - (client as DiscordShardedClient).Shards.Select(x => x.Guilds.Count).ToArray() - ); - - lastTimeUpdated = DateTime.Now; - SendLog("Sent stats to Top.gg!"); - } + await api.UpdateBotServerCountAsync((await client.GetGuildsAsync()).Count); } } } \ No newline at end of file diff --git a/DiscordBotsList.Api.Adapter.Discord.Net/Utils/DiscordNetDblUtils.cs b/DiscordBotsList.Api.Adapter.Discord.Net/Utils/DiscordNetDblUtils.cs index 2231dec..f694c98 100644 --- a/DiscordBotsList.Api.Adapter.Discord.Net/Utils/DiscordNetDblUtils.cs +++ b/DiscordBotsList.Api.Adapter.Discord.Net/Utils/DiscordNetDblUtils.cs @@ -14,16 +14,5 @@ public static DiscordNetDblApi CreateDblApi(this DiscordSocketClient client, str { return new DiscordNetDblApi(client, dblToken); } - - /// - /// Creates a DiscordBotsList Api - /// - /// your client - /// Your DiscordBotsList token - /// A new instance of a DblApi - public static ShardedDiscordNetDblApi CreateDblApi(this DiscordShardedClient client, string dblToken) - { - return new ShardedDiscordNetDblApi(client, dblToken); - } } } \ No newline at end of file diff --git a/DiscordBotsList.Api.Tests/DiscordBotsList.Api.Tests.csproj b/DiscordBotsList.Api.Tests/DiscordBotsList.Api.Tests.csproj index 462f4fc..8c2d6c6 100644 --- a/DiscordBotsList.Api.Tests/DiscordBotsList.Api.Tests.csproj +++ b/DiscordBotsList.Api.Tests/DiscordBotsList.Api.Tests.csproj @@ -8,22 +8,22 @@ - + - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + diff --git a/DiscordBotsList.Api.Tests/UnitTests.cs b/DiscordBotsList.Api.Tests/UnitTests.cs index 364a0b3..2df812e 100644 --- a/DiscordBotsList.Api.Tests/UnitTests.cs +++ b/DiscordBotsList.Api.Tests/UnitTests.cs @@ -12,29 +12,23 @@ public class Credentials public static Credentials LoadFromEnv() { - var cred = new Credentials(); - cred.BotId = ulong.Parse(Environment.GetEnvironmentVariable("BOT_ID")); - cred.Token = Environment.GetEnvironmentVariable("API_KEY"); - return cred; + return new Credentials() + { + BotId = ulong.Parse(Environment.GetEnvironmentVariable("BOT_ID")), + Token = Environment.GetEnvironmentVariable("API_KEY"), + }; } } public class UnitTests { - private readonly AuthDiscordBotListApi _api; + private readonly DiscordBotListApi _api; private readonly Credentials _cred; public UnitTests() { _cred = Credentials.LoadFromEnv(); - _api = new AuthDiscordBotListApi(_cred.BotId, _cred.Token); - } - - [Fact] - public void GetUserTest() - { - Assert.NotNull(_api.GetMeAsync()); - Assert.NotNull(_api.GetUserAsync(_cred.BotId)); + _api = new DiscordBotListApi(_cred.BotId, _cred.Token); } [Fact] @@ -55,29 +49,17 @@ public async Task TaskGetVotersTestAsync() Assert.NotNull(await _api.GetVotersAsync()); } - [Fact] - public async Task GetUserTestAsync() - { - Assert.NotNull(await _api.GetUserAsync(181514288278536193)); - } - [Fact] public async Task GetBotTestAsync() { - var botId = 423593006436712458U; + var botId = 264811613708746752U; var bot = await _api.GetBotAsync(botId); Assert.NotNull(bot); Assert.Equal(botId, bot.Id); } - + [Fact] - public async Task GetMeTestAsync() - { - Assert.NotNull(await _api.GetMeAsync()); - } - - [Fact] - public async Task GetUsersGetStatsTest() + public async Task GetBotsTestAsync() { var bots = await _api.GetBotsAsync(); @@ -86,9 +68,19 @@ public async Task GetUsersGetStatsTest() var firstBot = bots.Items.First(); - var stats = await firstBot.GetStatsAsync(); + Assert.NotNull(firstBot); + } - Assert.NotNull(stats); + [Fact] + public async Task GetBotServerCountTestAsync() + { + await _api.GetBotServerCountAsync(); + } + + [Fact] + public async Task UpdateBotServerCountTestAsync() + { + await _api.UpdateBotServerCountAsync(2); } } } \ No newline at end of file diff --git a/DiscordBotsList.Api.Webhooks/DiscordBotsList.Api.Webhooks.csproj b/DiscordBotsList.Api.Webhooks/DiscordBotsList.Api.Webhooks.csproj new file mode 100644 index 0000000..7ed6ed7 --- /dev/null +++ b/DiscordBotsList.Api.Webhooks/DiscordBotsList.Api.Webhooks.csproj @@ -0,0 +1,28 @@ + + + net8.0 + Velddev, Faith, null8626 + Top.gg + Top.gg API webhooks wrapper for ASP.NET + Mike Veldsink + https://github.com/Top-gg-Community/dotnet-sdk + https://github.com/Top-gg-Community/dotnet-sdk + git + discord bots topgg api webhooks asp.net + 2.0.0 + LICENSE + + + + + + + + + + True + + + + + \ No newline at end of file diff --git a/DiscordBotsList.Api.Webhooks/DiscordBotsList.Api.Webhooks.sln b/DiscordBotsList.Api.Webhooks/DiscordBotsList.Api.Webhooks.sln new file mode 100644 index 0000000..f37b2b1 --- /dev/null +++ b/DiscordBotsList.Api.Webhooks/DiscordBotsList.Api.Webhooks.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordBotsList.Api.Webhooks", "DiscordBotsList.Api.Webhooks.csproj", "{1D925476-7407-E766-5044-48D0FCF35938}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1D925476-7407-E766-5044-48D0FCF35938}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1D925476-7407-E766-5044-48D0FCF35938}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1D925476-7407-E766-5044-48D0FCF35938}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1D925476-7407-E766-5044-48D0FCF35938}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {2888D49C-0344-4012-8648-7B2959A50321} + EndGlobalSection +EndGlobal diff --git a/DiscordBotsList.Api.Webhooks/IReceiver.cs b/DiscordBotsList.Api.Webhooks/IReceiver.cs new file mode 100644 index 0000000..8ce84b8 --- /dev/null +++ b/DiscordBotsList.Api.Webhooks/IReceiver.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; + +namespace DiscordBotsList.Api.Webhooks +{ + public interface IReceiver + { + /// + /// Receives webhook data. + /// + /// The webhook data. + /// Return type can be anything as it's ignored. + Task Callback(T data); + } +} \ No newline at end of file diff --git a/DiscordBotsList.Api.Webhooks/Middleware.cs b/DiscordBotsList.Api.Webhooks/Middleware.cs new file mode 100644 index 0000000..a481886 --- /dev/null +++ b/DiscordBotsList.Api.Webhooks/Middleware.cs @@ -0,0 +1,68 @@ +using Microsoft.AspNetCore.Http; +using System.Text.Json; +using System.Threading.Tasks; + +namespace DiscordBotsList.Api.Webhooks +{ + public class Middleware where Receiver : IReceiver + { + private readonly JsonSerializerOptions _serializerOptions; + private readonly RequestDelegate _next; + private readonly string _path; + private readonly string _auth; + private readonly Receiver _receiver; + + public Middleware(RequestDelegate next, string path, string auth, Receiver receiver) + { + _next = next; + _path = path; + _auth = auth; + _receiver = receiver; + + _serializerOptions = new JsonSerializerOptions(); + _serializerOptions.Converters.Add(new ULongToStringConverter()); + } + + public async Task InvokeAsync(HttpContext context) + { + if (context.Request.Path.StartsWithSegments(_path) && context.Request.Method == "POST") + { + if (!context.Request.Headers.TryGetValue("Authorization", out var authorizationInput) || !authorizationInput.Equals(_auth)) + { + if (!context.Response.HasStarted) + { + context.Response.StatusCode = 401; + await context.Response.WriteAsync("Unauthorized"); + } + + return; + } + + var data = await JsonSerializer.DeserializeAsync(context.Request.Body, _serializerOptions); + + if (data != null) + { + await _receiver.Callback(data); + + if (!context.Response.HasStarted) + { + context.Response.StatusCode = 204; + } + } + else if (!context.Response.HasStarted) + { + context.Response.StatusCode = 400; + await context.Response.WriteAsync("Bad request"); + } + } + else + { + await _next(context); + } + } + } + + public class VoteMiddleware(RequestDelegate next, string path, string auth, Receiver receiver) : Middleware(next, path, auth, receiver) where Receiver : IReceiver + { + } +} \ No newline at end of file diff --git a/DiscordBotsList.Api.Webhooks/ULongToStringConverter.cs b/DiscordBotsList.Api.Webhooks/ULongToStringConverter.cs new file mode 100644 index 0000000..58b92d4 --- /dev/null +++ b/DiscordBotsList.Api.Webhooks/ULongToStringConverter.cs @@ -0,0 +1,27 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace DiscordBotsList.Api.Webhooks +{ + /// + /// Converts API responses from strings to longs and vice versa. + /// + internal class ULongToStringConverter : JsonConverter + { + public override ulong Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String && ulong.TryParse(reader.GetString(), out var value)) + { + return value; + } + + throw new InvalidOperationException(); + } + + public override void Write(Utf8JsonWriter writer, ulong value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString()); + } + } +} \ No newline at end of file diff --git a/DiscordBotsList.Api.Webhooks/Vote.cs b/DiscordBotsList.Api.Webhooks/Vote.cs new file mode 100644 index 0000000..4057931 --- /dev/null +++ b/DiscordBotsList.Api.Webhooks/Vote.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; +using System.Web; + +namespace DiscordBotsList.Api.Webhooks +{ + public class Vote + { + [JsonPropertyName("bot")] + [JsonConverter(typeof(ULongToStringConverter))] + public ulong botId { get; init; } + + [JsonPropertyName("guild")] + [JsonConverter(typeof(ULongToStringConverter))] + public ulong serverId { get; init; } + + [JsonPropertyName("user")] + [JsonConverter(typeof(ULongToStringConverter))] + public ulong voterId { get; init; } + + [JsonPropertyName("isWeekend")] + public bool isWeekend { get; init; } + + [JsonPropertyName("type")] + public string type { get; init; } + + [JsonPropertyName("query")] + public string query { get; init; } + + public ulong ReceiverId => botId == 0 ? serverId : botId; + public ulong VoterId => voterId; + public bool IsWeekend => isWeekend; + public bool IsTest => type == "test"; + public Dictionary Query + { + get + { + if (query == null) + { + return null; + } + + var parsedQuery = HttpUtility.ParseQueryString(query); + return parsedQuery.AllKeys.ToDictionary(key => key, key => parsedQuery[key]); + } + } + } +} \ No newline at end of file diff --git a/DiscordBotsList.Api/AuthenticatedBotListApi.cs b/DiscordBotsList.Api/AuthenticatedBotListApi.cs deleted file mode 100644 index 764ab5d..0000000 --- a/DiscordBotsList.Api/AuthenticatedBotListApi.cs +++ /dev/null @@ -1,120 +0,0 @@ -using DiscordBotsList.Api.Internal; -using DiscordBotsList.Api.Objects; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; -using System.Text.Json; -using System.Threading.Tasks; - -namespace DiscordBotsList.Api -{ - public class AuthDiscordBotListApi : DiscordBotListApi - { - private readonly ulong _selfId; - private readonly string _token; - - public AuthDiscordBotListApi(ulong selfId, string token) - { - _selfId = selfId; - _token = token; - _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - } - - /// - /// Gets your own bot with as an ISelfBot - /// - /// your own bot with as an ISelfBot - public async Task GetMeAsync() - { - var bot = await GetBotAsync(_selfId); - bot.api = this; - return bot; - } - - /// - /// Gets all voters that have voted on your bot - /// Max 1000, If you have more, you MUST use WEBHOOKS instead. - /// - /// A list of voters - public async Task> GetVotersAsync() - { - return (await GetVotersAsync()).Cast().ToList(); - } - - /// - /// Update your stats unsharded - /// - /// count of guilds - public async Task UpdateStats(int guildCount) - { - await UpdateStatsAsync(new GuildCountObject(guildCount)); - } - - /// - /// Update your stats sharded - /// - /// Begin shard id - /// Total shards - /// Guild count per shards - public async Task UpdateStats(int shardId, int shardCount, params int[] shards) - { - await UpdateStatsAsync(new ShardedGuildCountObject - { - ShardId = shardId, - ShardCount = shardCount, - Shards = shards - }); - } - - /// - /// Update your stats sharded - /// - /// count of guilds - /// Total shards - public async Task UpdateStats(int guildCount, int shardCount) - { - await UpdateStatsAsync(new ShardedGuildCountObject - { - ShardCount = shardCount, - GuildCount = guildCount - }); - } - - /// - /// returns true if user have voted for the past 12 hours - /// - /// Amount of days to filter - /// True or False - public async Task HasVoted(ulong userId) - { - return await HasVotedAsync(userId); - } - - protected async Task> GetVotersAsync() - { - var query = $"bots/{_selfId}/votes"; - return await GetAuthorizedAsync>(Utils.CreateQuery(query)); - } - - protected async Task UpdateStatsAsync(object statsObject) - { - var json = JsonSerializer.Serialize(statsObject); - var httpContent = new StringContent(json, Encoding.UTF8, "application/json"); - await _httpClient - .PostAsync($"{baseEndpoint}/bots/{_selfId}/stats", httpContent); - } - - protected async Task GetAuthorizedAsync(string url) - { - return await GetAsync(url); - } - - protected async Task HasVotedAsync(ulong userId) - { - var url = $"bots/{_selfId}/check?userId={userId}"; - return (await GetAsync(url)).HasVoted.GetValueOrDefault(0) == 1; - } - } -} \ No newline at end of file diff --git a/DiscordBotsList.Api/DiscordBotListApi.cs b/DiscordBotsList.Api/DiscordBotListApi.cs index 107bc9b..9a5fb60 100644 --- a/DiscordBotsList.Api/DiscordBotListApi.cs +++ b/DiscordBotsList.Api/DiscordBotListApi.cs @@ -1,41 +1,72 @@ using DiscordBotsList.Api.Internal; using DiscordBotsList.Api.Internal.Queries; using DiscordBotsList.Api.Objects; +using System; +using System.Collections.Generic; +using System.Linq; using System.Net.Http; +using System.Net.Http.Headers; using System.Net.Http.Json; +using System.Text; using System.Text.Json; using System.Threading.Tasks; namespace DiscordBotsList.Api { + public enum SortBotsBy + { + MonthlyPoints, + Id, + Date, + } + public class DiscordBotListApi { - protected const string baseEndpoint = "https://top.gg/api/"; + internal const string baseEndpoint = "https://top.gg/api"; private readonly JsonSerializerOptions _serializerOptions; - protected HttpClient _httpClient; + private readonly ulong _selfId; + private readonly HttpClient _httpClient; - public DiscordBotListApi() + public DiscordBotListApi(ulong selfId, string token) { + _selfId = selfId; + _httpClient = new HttpClient(); + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + _serializerOptions = new JsonSerializerOptions(); _serializerOptions.Converters.Add(new ULongToStringConverter()); } /// - /// Gets bots from botlist + /// Fetches bots from Top.gg /// - /// amount of bots to appear per page (max: 500) - /// current page to query + /// sorts results based on their monthly vote count, id, or their submission date + /// amount of bots to retrieve (max: 500) + /// amount of bots to skip /// List of Bot Objects - public async Task> GetBotsAsync(int count = 50, int page = 0) + public async Task> GetBotsAsync(SortBotsBy sortBy = SortBotsBy.MonthlyPoints, int count = 50, int offset = 0) { - var result = await GetAsync("bots"); + if (count < 0 || count > 500) + { + count = 50; + } + + if (offset < 0) + { + offset = 0; + } + + var sortByString = sortBy.ToString(); + var result = await GetAsync($"/bots?sort={char.ToLowerInvariant(sortByString[0]) + sortByString.Substring(1)}&limit=${count}&offset=${offset}"); + foreach (var bot in result.Items) (bot as Bot).api = this; + return result; } /// - /// Get specific bot by Discord id + /// Fetches specific bot by Discord ID /// /// Discord id /// Bot Object @@ -45,23 +76,14 @@ public async Task GetBotAsync(ulong id) } /// - /// Get bot stats + /// Fetches your bot's server count /// - /// Discord id - /// IBotStats object related to the bot - public async Task GetBotStatsAsync(ulong id) + /// Your bot's server count if available + public async Task GetBotServerCountAsync() { - return await GetAsync($"bots/{id}/stats"); - } + var result = await GetAsync("/bots/stats"); - /// - /// Get specific user by Discord id - /// - /// Discord id - /// User Object - public async Task GetUserAsync(ulong id) - { - return await GetAsync($"users/{id}"); + return result.ServerCount; } /// @@ -73,26 +95,24 @@ public async Task GetUserAsync(ulong id) /// Bot object of type T internal async Task GetBotAsync(ulong id) where T : Bot { - var t = await GetAsync($"bots/{id}"); + var t = await GetAsync($"/bots/{id}"); if (t == null) return null; t.api = this; return t; } /// - /// Gets and parses objects + /// Fetches and parses objects /// /// Type to parse to /// Url to get from /// Object of type T - protected async Task GetAsync(string url) + private async Task GetAsync(string url) { var t = await _httpClient.GetAsync(baseEndpoint + url); - var payload = await t.Content.ReadAsStringAsync(); - var o = JsonSerializer.Deserialize(payload, _serializerOptions); var result = t.IsSuccessStatusCode - ? ApiResult.FromSuccess(await t.Content.ReadFromJsonAsync(_serializerOptions)) - : ApiResult.FromHttpError(t.StatusCode); + ? ApiResult.FromSuccess(await t.Content.ReadFromJsonAsync(_serializerOptions)) + : ApiResult.FromHttpError(t.StatusCode); return result.Value; } @@ -102,7 +122,44 @@ protected async Task GetAsync(string url) /// True or False public async Task IsWeekendAsync() { - return (await GetAsync("weekend")).Weekend; + return (await GetAsync("/weekend")).Weekend; + } + + /// + /// Fetches unique voters that have voted for your project + /// + /// The page number, defaults to 1 + /// A list of voters + public async Task> GetVotersAsync(int page = 1) + { + return (await GetAsync>($"/bots/{_selfId}/votes?page={Math.Max(page, 1)}")).Cast().ToList(); + } + + /// + /// Updates your bot's server count + /// + /// Your bot's server count + public async Task UpdateBotServerCountAsync(int serverCount) + { + if (serverCount <= 0) + { + throw new ArgumentOutOfRangeException(nameof(serverCount), "serverCount cannot be less than 1."); + } + + var json = JsonSerializer.Serialize(new ServerCountObject(serverCount)); + var httpContent = new StringContent(json, Encoding.UTF8, "application/json"); + + await _httpClient.PostAsync($"{baseEndpoint}/bots/stats", httpContent); + } + + /// + /// returns true if user have voted for your project in the past 12 hours + /// + /// The specified user's ID + /// True or False + public async Task HasVoted(ulong userId) + { + return (await GetAsync($"/bots/check?userId={userId}")).HasVoted.GetValueOrDefault(0) == 1; } } } \ No newline at end of file diff --git a/DiscordBotsList.Api/DiscordBotsList.Api.csproj b/DiscordBotsList.Api/DiscordBotsList.Api.csproj index e4a13d4..208c31c 100644 --- a/DiscordBotsList.Api/DiscordBotsList.Api.csproj +++ b/DiscordBotsList.Api/DiscordBotsList.Api.csproj @@ -2,26 +2,26 @@ net8.0 - Velddev, Faith Viola + Velddev, Faith Top.gg - top.gg api wrapper + The community-maintained .NET library for Top.gg Mike Veldsink - https://github.com/DiscordBotList/DBL-dotnet-Library - https://github.com/DiscordBotList/DBL-dotnet-Library + https://github.com/Top-gg-Community/dotnet-sdk + https://github.com/Top-gg-Community/dotnet-sdk git - discord bots list org wrapper api + discord bots topgg api update to net 8 false true DiscordBotsList.Api DiscordBotsList.Api DiscordBotsList.Api - 1.5.0 + 2.0.0 LICENSE - + diff --git a/DiscordBotsList.Api/Internal/Bot.cs b/DiscordBotsList.Api/Internal/Bot.cs index 2025737..7141bf3 100644 --- a/DiscordBotsList.Api/Internal/Bot.cs +++ b/DiscordBotsList.Api/Internal/Bot.cs @@ -1,53 +1,75 @@ -using DiscordBotsList.Api.Objects; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; -using System.Threading.Tasks; +using DiscordBotsList.Api.Objects; namespace DiscordBotsList.Api.Internal { - public class Bot : Entity, IDblBot + public class Bot : Project, IDblBot { internal DiscordBotListApi api; - [JsonPropertyName("prefix")] public string prefix { get; set; } + [JsonPropertyName("clientid")] + [JsonConverter(typeof(ULongToStringConverter))] + public ulong clientId { get; set; } - [JsonPropertyName("shortdesc")] public string shortDescription { get; set; } + [JsonPropertyName("prefix")] + public string prefix { get; set; } - [JsonPropertyName("longdesc")] public string longDescription { get; set; } + [JsonPropertyName("shortdesc")] + public string shortDescription { get; set; } - [JsonPropertyName("tags")] public List tags { get; set; } + [JsonPropertyName("longdesc")] + public string longDescription { get; set; } - [JsonPropertyName("website")] public string websiteUrl { get; set; } + [JsonPropertyName("tags")] + public List tags { get; set; } - [JsonPropertyName("support")] public string SupportInviteCode { get; set; } + [JsonPropertyName("website")] + public string websiteUrl { get; set; } - [JsonPropertyName("github")] public string githubUrl { get; set; } + [JsonPropertyName("support")] + public string supportUrl { get; set; } - [JsonPropertyName("owners")] public List owners { get; set; } + [JsonPropertyName("github")] + public string githubUrl { get; set; } - [JsonPropertyName("invite")] public string customInvite { get; set; } + [JsonPropertyName("owners")] + public List owners { get; set; } - [JsonPropertyName("date")] public DateTime approvedAt { get; set; } + [JsonPropertyName("invite")] + public string inviteUrl { get; set; } - [JsonPropertyName("certifiedBot")] public bool certified { get; set; } + [JsonPropertyName("date")] + public DateTime submittedAt { get; set; } - [JsonPropertyName("vanity")] public string vanity { get; set; } + [JsonPropertyName("server_count")] + public int? serverCount { get; set; } - [JsonPropertyName("points")] public int points { get; set; } - - [JsonPropertyName("monthlyPoints")] public int monthlyPoints { get; set; } + [JsonPropertyName("vanity")] + public string vanity { get; set; } + + [JsonPropertyName("points")] + public int points { get; set; } + + [JsonPropertyName("monthlyPoints")] + public int monthlyPoints { get; set; } + + [JsonPropertyName("reviews")] + public BotReviews reviews { get; set; } public string VanityTag => vanity; - public DateTime ApprovedAt => approvedAt; + public ulong ClientId => clientId; - public string GithubUrl => githubUrl; + public DateTime SubmittedAt => submittedAt; - public string InviteUrl => customInvite ?? $"https://discord.com/oauth2/authorize?&client_id={Id}&scope=bot"; + public int? ServerCount => serverCount; - public bool IsCertified => certified; + public string GithubUrl => githubUrl; + + public string InviteUrl => inviteUrl ?? $"https://discord.com/oauth2/authorize?&client_id={Id}&scope=bot"; public string LongDescription => longDescription; @@ -56,22 +78,19 @@ public class Bot : Entity, IDblBot public List OwnerIds => owners.ToList(); public int Points => points; - + public int MonthlyPoints => monthlyPoints; public string ShortDescription => shortDescription; public List Tags => tags.ToList(); - public string SupportUrl => "https://discord.gg/" + SupportInviteCode; - + public string SupportUrl => supportUrl; + public string VanityUrl => "https://top.gg/bot/" + vanity; public string WebsiteUrl => websiteUrl; - public async Task GetStatsAsync() - { - return await api.GetBotStatsAsync(Id); - } + public BotReviews Reviews => reviews; } } \ No newline at end of file diff --git a/DiscordBotsList.Api/Internal/Entity.cs b/DiscordBotsList.Api/Internal/Entity.cs deleted file mode 100644 index 3dc0066..0000000 --- a/DiscordBotsList.Api/Internal/Entity.cs +++ /dev/null @@ -1,27 +0,0 @@ -using DiscordBotsList.Api.Objects; -using System.Text.Json.Serialization; - -namespace DiscordBotsList.Api.Internal -{ - public class Entity : IDblEntity - { - [JsonPropertyName("avatar")] public string Avatar { get; set; } - - [JsonPropertyName("defAvatar")] public string DefaultAvatar { get; set; } - - public string AvatarUrl => !string.IsNullOrEmpty(Avatar) - ? $"https://cdn.discordapp.com/{Id}/{Avatar}.png" - : $"https://cdn.discordapp.com/{Id}/{DefaultAvatar}.png"; - - [JsonPropertyName("id")] public ulong Id { get; set; } - - [JsonPropertyName("username")] public string Username { get; set; } - - [JsonPropertyName("discriminator")] public string Discriminator { get; set; } - - public override string ToString() - { - return $"{Username}#{Discriminator}"; - } - } -} \ No newline at end of file diff --git a/DiscordBotsList.Api/Internal/GuildCountObject.cs b/DiscordBotsList.Api/Internal/GuildCountObject.cs index 83055ac..e2eddb4 100644 --- a/DiscordBotsList.Api/Internal/GuildCountObject.cs +++ b/DiscordBotsList.Api/Internal/GuildCountObject.cs @@ -1,45 +1,23 @@ using DiscordBotsList.Api.Objects; -using System.Collections.Generic; -using System.Linq; using System.Text.Json.Serialization; namespace DiscordBotsList.Api.Internal { - internal class GuildCountObject + internal class ServerCountObject { - [JsonPropertyName("server_count")] internal int guildCount; + [JsonPropertyName("server_count")] + internal int serverCount; - public GuildCountObject(int count) + public ServerCountObject(int count) { - guildCount = count; + serverCount = count; } } - internal class BotStatsObject : ShardedObject, IDblBotStats + internal class BotStatsObject : IDblBotStats { - [JsonPropertyName("server_count")] internal int guildCount { get; set; } - public int GuildCount => guildCount; - - public IReadOnlyList Shards => shards.ToList(); - - public int ShardCount => shardCount; - } - - internal class ShardedObject - { - [JsonPropertyName("shards")] internal int[] shards { get; set; } - - [JsonPropertyName("shard_count")] internal int shardCount { get; set; } - } - - internal class ShardedGuildCountObject - { - [JsonPropertyName("shards")] public int[] Shards { get; set; } - - [JsonPropertyName("shard_id")] public int ShardId { get; set; } - - [JsonPropertyName("shard_count")] public int ShardCount { get; set; } - - [JsonPropertyName("server_count")] public int GuildCount { get; set; } + [JsonPropertyName("server_count")] + internal int serverCount { get; set; } + public int ServerCount => serverCount; } } \ No newline at end of file diff --git a/DiscordBotsList.Api/Internal/HasVotedObject.cs b/DiscordBotsList.Api/Internal/HasVotedObject.cs index 668c965..5185fe8 100644 --- a/DiscordBotsList.Api/Internal/HasVotedObject.cs +++ b/DiscordBotsList.Api/Internal/HasVotedObject.cs @@ -4,6 +4,7 @@ namespace DiscordBotsList.Api.Internal { internal class HasVotedObject { - [JsonPropertyName("voted")] public int? HasVoted { get; set; } + [JsonPropertyName("voted")] + public int? HasVoted { get; init; } } } \ No newline at end of file diff --git a/DiscordBotsList.Api/Internal/Project.cs b/DiscordBotsList.Api/Internal/Project.cs new file mode 100644 index 0000000..6d939f2 --- /dev/null +++ b/DiscordBotsList.Api/Internal/Project.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; +using DiscordBotsList.Api.Objects; + +namespace DiscordBotsList.Api.Internal +{ + public class Project: IDblEntity + { + [JsonPropertyName("avatar")] + public string AvatarUrl { get; set; } + + [JsonPropertyName("id")] + [JsonConverter(typeof(ULongToStringConverter))] + public ulong Id { get; set; } + + [JsonPropertyName("username")] + public string Username { get; set; } + } +} \ No newline at end of file diff --git a/DiscordBotsList.Api/Internal/Queries/BotListQuery.cs b/DiscordBotsList.Api/Internal/Queries/BotListQuery.cs index 17c43ae..c548a95 100644 --- a/DiscordBotsList.Api/Internal/Queries/BotListQuery.cs +++ b/DiscordBotsList.Api/Internal/Queries/BotListQuery.cs @@ -8,26 +8,31 @@ namespace DiscordBotsList.Api.Internal.Queries { internal class BotListQuery : ISearchResult { - [JsonPropertyName("results")] public List results { get; set; } + [JsonPropertyName("results")] + public List results { get; set; } - [JsonPropertyName("limit")] public int limit { get; set; } + [JsonPropertyName("limit")] + public int limit { get; set; } - [JsonPropertyName("offset")] public int offset { get; set; } + [JsonPropertyName("offset")] + public int? offset { get; set; } - [JsonPropertyName("count")] public int count { get; set; } + [JsonPropertyName("count")] + public int count { get; set; } - [JsonPropertyName("total")] public int total { get; set; } + [JsonPropertyName("total")] + public int total { get; set; } public List Items => results .Cast() .ToList(); - public int CurrentPage => (int)Math.Ceiling((double)offset / limit); + public int CurrentPage => (int)Math.Ceiling((double)(offset ?? 0) / limit); public int ItemsPerPage => limit; public int TotalItems => total; - public int TotalPages => (int)Math.Ceiling((double)limit / count); + public int TotalPages => (int)Math.Ceiling((double)(offset ?? 0) / count); } } \ No newline at end of file diff --git a/DiscordBotsList.Api/Internal/SelfBot.cs b/DiscordBotsList.Api/Internal/SelfBot.cs index b3cc608..bbf6d37 100644 --- a/DiscordBotsList.Api/Internal/SelfBot.cs +++ b/DiscordBotsList.Api/Internal/SelfBot.cs @@ -6,34 +6,24 @@ namespace DiscordBotsList.Api.Internal { internal class SelfBot : Bot, IDblSelfBot { - public async Task> GetVotersAsync() + public async Task> GetVotersAsync(int page = 1) { - return await ((AuthDiscordBotListApi)api).GetVotersAsync(); + return await api.GetVotersAsync(page); } public async Task HasVotedAsync(ulong userId) { - return await ((AuthDiscordBotListApi)api).HasVoted(userId); + return await api.HasVoted(userId); } public async Task IsWeekendAsync() { - return await ((AuthDiscordBotListApi)api).IsWeekendAsync(); + return await api.IsWeekendAsync(); } - public async Task UpdateStatsAsync(int guildCount) + public async Task UpdateBotServerCountAsync(int serverCount) { - await ((AuthDiscordBotListApi)api).UpdateStats(guildCount); - } - - public async Task UpdateStatsAsync(int[] shards) - { - await ((AuthDiscordBotListApi)api).UpdateStats(0, shards.Length, shards); - } - - public async Task UpdateStatsAsync(int shardCount, int totalShards, params int[] shards) - { - await ((AuthDiscordBotListApi)api).UpdateStats(shardCount, totalShards, shards); + await api.UpdateBotServerCountAsync(serverCount); } } } \ No newline at end of file diff --git a/DiscordBotsList.Api/Internal/User.cs b/DiscordBotsList.Api/Internal/User.cs deleted file mode 100644 index 66ca92e..0000000 --- a/DiscordBotsList.Api/Internal/User.cs +++ /dev/null @@ -1,28 +0,0 @@ -using DiscordBotsList.Api.Objects; -using System.Text.Json.Serialization; - -namespace DiscordBotsList.Api.Internal -{ - public class User : Entity, IDblUser - { - [JsonPropertyName("social")] public SocialConnections Social { get; set; } - - [JsonPropertyName("bio")] public string Biography { get; set; } - - [JsonPropertyName("banner")] public string BannerUrl { get; set; } - - [JsonPropertyName("color")] public string Color { get; set; } - - [JsonPropertyName("supporter")] public bool IsSupporter { get; set; } - - [JsonPropertyName("certifiedDev")] public bool IsCertified { get; set; } - - [JsonPropertyName("mod")] public bool IsModerator { get; set; } - - [JsonPropertyName("webMod")] public bool IsWebModerator { get; set; } - - [JsonPropertyName("admin")] public bool IsAdmin { get; set; } - - public SocialConnections Connections => Social; - } -} \ No newline at end of file diff --git a/DiscordBotsList.Api/Objects/BotReviews.cs b/DiscordBotsList.Api/Objects/BotReviews.cs new file mode 100644 index 0000000..5e060ee --- /dev/null +++ b/DiscordBotsList.Api/Objects/BotReviews.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace DiscordBotsList.Api.Objects +{ + public class BotReviews + { + [JsonPropertyName("averageScore")] + public double AverageScore { get; internal set; } + + [JsonPropertyName("count")] + public int Count { get; internal set; } + } +} \ No newline at end of file diff --git a/DiscordBotsList.Api/Objects/IAdapter.cs b/DiscordBotsList.Api/Objects/IAdapter.cs index e58695e..e9fb685 100644 --- a/DiscordBotsList.Api/Objects/IAdapter.cs +++ b/DiscordBotsList.Api/Objects/IAdapter.cs @@ -1,16 +1,22 @@ -using System; +#nullable enable + +using System; using System.Threading.Tasks; namespace DiscordBotsList.Api.Objects { public interface IAdapter { - event Action Log; + event Action Posted; Task RunAsync(); + bool IsRunning(); + void Start(); void Stop(); + + Task StopAsync(); } } \ No newline at end of file diff --git a/DiscordBotsList.Api/Objects/IBotStats.cs b/DiscordBotsList.Api/Objects/IBotStats.cs index 116efb0..fb2ab1b 100644 --- a/DiscordBotsList.Api/Objects/IBotStats.cs +++ b/DiscordBotsList.Api/Objects/IBotStats.cs @@ -4,10 +4,6 @@ namespace DiscordBotsList.Api.Objects { public interface IDblBotStats { - int GuildCount { get; } - - IReadOnlyList Shards { get; } - - int ShardCount { get; } + int ServerCount { get; } } } \ No newline at end of file diff --git a/DiscordBotsList.Api/Objects/IDblBot.cs b/DiscordBotsList.Api/Objects/IDblBot.cs index 3a32bde..eb4ceb6 100644 --- a/DiscordBotsList.Api/Objects/IDblBot.cs +++ b/DiscordBotsList.Api/Objects/IDblBot.cs @@ -24,31 +24,25 @@ public interface IDblBot : IDblEntity string InviteUrl { get; } - DateTime ApprovedAt { get; } - - bool IsCertified { get; } + DateTime SubmittedAt { get; } string VanityUrl { get; } int Points { get; } - + int MonthlyPoints { get; } - Task GetStatsAsync(); + BotReviews Reviews { get; } } public interface IDblSelfBot : IDblBot { - Task> GetVotersAsync(); + Task> GetVotersAsync(int page); Task HasVotedAsync(ulong userId); Task IsWeekendAsync(); - Task UpdateStatsAsync(int guildCount); - - Task UpdateStatsAsync(int[] shards); - - Task UpdateStatsAsync(int shardCount, int totalShards, params int[] shards); + Task UpdateBotServerCountAsync(int serverCount); } } \ No newline at end of file diff --git a/DiscordBotsList.Api/Objects/IDblEntity.cs b/DiscordBotsList.Api/Objects/IDblEntity.cs index 5a63b63..c6ca1b0 100644 --- a/DiscordBotsList.Api/Objects/IDblEntity.cs +++ b/DiscordBotsList.Api/Objects/IDblEntity.cs @@ -3,22 +3,17 @@ public interface IDblEntity { /// - /// Discord Id + /// ID /// ulong Id { get; } /// - /// Username of the entity + /// Username /// string Username { get; } /// - /// Discriminator, the XXXX#1234 part - /// - string Discriminator { get; } - - /// - /// Discord avatar url, or default avatar if none found. + /// Discord avatar URL, or default avatar if none found. /// string AvatarUrl { get; } } diff --git a/DiscordBotsList.Api/Objects/IDblUser.cs b/DiscordBotsList.Api/Objects/IDblUser.cs deleted file mode 100644 index 8d8073a..0000000 --- a/DiscordBotsList.Api/Objects/IDblUser.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace DiscordBotsList.Api.Objects -{ - public interface IDblUser : IDblEntity - { - string Biography { get; } - - string BannerUrl { get; } - - SocialConnections Connections { get; } - - string Color { get; } - - bool IsSupporter { get; } - - bool IsCertified { get; } - - bool IsModerator { get; } - - bool IsWebModerator { get; } - - bool IsAdmin { get; } - } -} \ No newline at end of file diff --git a/DiscordBotsList.Api/Objects/SmallWidgetOptions.cs b/DiscordBotsList.Api/Objects/SmallWidgetOptions.cs deleted file mode 100644 index a04effe..0000000 --- a/DiscordBotsList.Api/Objects/SmallWidgetOptions.cs +++ /dev/null @@ -1,202 +0,0 @@ -using System.Collections.Generic; - -namespace DiscordBotsList.Api.Objects -{ - public enum WidgetType - { - STATUS, - SERVERS, - LIB, - UPVOTES, - OWNER - } - - public class SmallWidgetOptions - { - private int? AvatarBackgroundColor; - private int? LeftColor; - private int? LeftTextColor; - private int? RightColor; - private int? RightTextColor; - private WidgetType Type; - - public SmallWidgetOptions SetType(WidgetType t) - { - Type = t; - return this; - } - - public SmallWidgetOptions SetAvatarBackgroundColor(int r, int g, int b) - { - AvatarBackgroundColor = Utils.FromColor(r, g, b); - return this; - } - - public SmallWidgetOptions SetLeftColor(int r, int g, int b) - { - LeftColor = Utils.FromColor(r, g, b); - return this; - } - - public SmallWidgetOptions SetRightColor(int r, int g, int b) - { - RightColor = Utils.FromColor(r, g, b); - return this; - } - - public SmallWidgetOptions SetLeftTextColor(int r, int g, int b) - { - LeftTextColor = Utils.FromColor(r, g, b); - return this; - } - - public SmallWidgetOptions SetRightTextColor(int r, int g, int b) - { - RightTextColor = Utils.FromColor(r, g, b); - return this; - } - - /// - /// Builds and returns a value. - /// - /// Id of the bot - /// Widget url - public string Build(ulong botId) - { - var query = $"https://top.gg/api/widget/{Type.ToString().ToLower()}/{botId}.svg"; - - var args = new List(); - - if (AvatarBackgroundColor != null) - args.Add($"avatarbg={AvatarBackgroundColor.Value.ToString("X")}"); - - if (LeftColor != null) - args.Add($"leftcolor={LeftColor.Value.ToString("X")}"); - - if (RightColor != null) - args.Add($"rightcolor={RightColor.Value.ToString("X")}"); - - if (LeftTextColor != null) - args.Add($"lefttextcolor={LeftTextColor.Value.ToString("X")}"); - - if (RightTextColor != null) - args.Add($"righttextcolor={RightTextColor.Value.ToString("X")}"); - - return Utils.CreateQuery(query, args.ToArray()); - } - } - - public class LargeWidgetOptions - { - private int? CertifiedColor; - private int? DataColor; - private int? HighlightColor; - private int? LabelColor; - private int? MiddleColor; - private int? TopColor; - private int? UsernameColor; - - public LargeWidgetOptions SetTopColor(int r, int g, int b) - { - TopColor = Utils.FromColor(r, g, b); - return this; - } - - public LargeWidgetOptions SetMiddleColor(int r, int g, int b) - { - MiddleColor = Utils.FromColor(r, g, b); - return this; - } - - public LargeWidgetOptions SetUsernameColor(int r, int g, int b) - { - UsernameColor = Utils.FromColor(r, g, b); - return this; - } - - public LargeWidgetOptions SetCertifiedColor(int r, int g, int b) - { - CertifiedColor = Utils.FromColor(r, g, b); - return this; - } - - public LargeWidgetOptions SetDataColor(int r, int g, int b) - { - DataColor = Utils.FromColor(r, g, b); - return this; - } - - public LargeWidgetOptions SetLabelColor(int r, int g, int b) - { - LabelColor = Utils.FromColor(r, g, b); - return this; - } - - public LargeWidgetOptions SetHighlightColor(int r, int g, int b) - { - HighlightColor = Utils.FromColor(r, g, b); - return this; - } - - /// - /// Builds and returns a value. - /// - /// Id of the bot - /// Widget url - public string Build(ulong botId) - { - var query = $"https://top.gg/api/widget/{botId}.svg"; - - var args = new List(); - - if (TopColor != null) - args.Add($"topcolor={TopColor.Value.ToString("X")}"); - - if (MiddleColor != null) - args.Add($"middlecolor={MiddleColor.Value.ToString("X")}"); - - if (UsernameColor != null) - args.Add($"usernamecolor={UsernameColor.Value.ToString("X")}"); - - if (CertifiedColor != null) - args.Add($"certifiedcolor={CertifiedColor.Value.ToString("X")}"); - - if (DataColor != null) - args.Add($"datacolor={DataColor.Value.ToString("X")}"); - - if (LabelColor != null) - args.Add($"labelcolor={LabelColor.Value.ToString("X")}"); - - if (HighlightColor != null) - args.Add($"highlightcolor={HighlightColor.Value.ToString("X")}"); - - return Utils.CreateQuery(query, args.ToArray()); - } - } - - internal static class Utils - { - public static int FromColor(float r, float g, float b) - { - return FromColor((int)(r * 255), (int)(g * 255), (int)(b * 255)); - } - - public static int FromColor(int r, int g, int b) - { - return (255 << 24) | ((byte)r << 16) | ((byte)g << 8) | ((byte)b << 0); - } - - /// - /// Creates rest parameters - /// - /// url - /// arguments - /// baseUrl?argument[0]&argument[1]&... - public static string CreateQuery(string baseUrl, params string[] args) - { - if (args.Length > 0) return $"{baseUrl}?{string.Join("&", args)}"; - - return baseUrl; - } - } -} \ No newline at end of file diff --git a/DiscordBotsList.Api/Objects/SocialConnections.cs b/DiscordBotsList.Api/Objects/SocialConnections.cs deleted file mode 100644 index ab8a62b..0000000 --- a/DiscordBotsList.Api/Objects/SocialConnections.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Text.Json.Serialization; - -namespace DiscordBotsList.Api.Objects -{ - public class SocialConnections - { - [JsonPropertyName("youtube")] public string YouTubeChannelId { get; internal set; } - - [JsonPropertyName("reddit")] public string RedditName { get; internal set; } - - [JsonPropertyName("twitter")] public string TwitterName { get; internal set; } - - [JsonPropertyName("instagram")] public string InstagramName { get; internal set; } - - [JsonPropertyName("github")] public string GitHubName { get; internal set; } - } -} \ No newline at end of file diff --git a/DiscordBotsList.Api/Objects/WeekendObject.cs b/DiscordBotsList.Api/Objects/WeekendObject.cs index bfcabf0..e24b116 100644 --- a/DiscordBotsList.Api/Objects/WeekendObject.cs +++ b/DiscordBotsList.Api/Objects/WeekendObject.cs @@ -1,7 +1,10 @@ -namespace DiscordBotsList.Api.Objects +using System.Text.Json.Serialization; + +namespace DiscordBotsList.Api.Objects { - public struct WeekendObject + public class WeekendObject { - public bool Weekend; + [JsonPropertyName("is_weekend")] + public bool Weekend { get; set; } } } \ No newline at end of file diff --git a/DiscordBotsList.Api/Objects/Widget.cs b/DiscordBotsList.Api/Objects/Widget.cs new file mode 100644 index 0000000..c349d29 --- /dev/null +++ b/DiscordBotsList.Api/Objects/Widget.cs @@ -0,0 +1,48 @@ +using System.Text.RegularExpressions; + +namespace DiscordBotsList.Api.Objects +{ + public enum WidgetType + { + DiscordBot, + DiscordServer, + } + + public static partial class Widget + { + [GeneratedRegex("(? + /// Generates a large widget URL. + /// + /// The widget type. + /// The project ID. + /// The widget URL. + public static string Large(WidgetType type, ulong projectId) => $"{DiscordBotListApi.baseEndpoint}/v1/widgets/large/{typeConversionRegex().Replace(type.ToString(), "/$1").ToLower()}/{projectId}"; + + /// + /// Generates a small widget URL for displaying votes. + /// + /// The widget type. + /// The project ID. + /// The widget URL. + public static string Votes(WidgetType type, ulong projectId) => $"{DiscordBotListApi.baseEndpoint}/v1/widgets/small/votes/{typeConversionRegex().Replace(type.ToString(), "/$1").ToLower()}/{projectId}"; + + /// + /// Generates a small widget URL for displaying a project's owner. + /// + /// The widget type. + /// The project ID. + /// The widget URL. + public static string Owner(WidgetType type, ulong projectId) => $"{DiscordBotListApi.baseEndpoint}/v1/widgets/small/owner/{typeConversionRegex().Replace(type.ToString(), "/$1").ToLower()}/{projectId}"; + + /// + /// Generates a small widget URL for displaying social stats. + /// + /// The widget type. + /// The project ID. + /// The widget URL. + public static string Social(WidgetType type, ulong projectId) => $"{DiscordBotListApi.baseEndpoint}/v1/widgets/small/social/{typeConversionRegex().Replace(type.ToString(), "/$1").ToLower()}/{projectId}"; + } +} \ No newline at end of file diff --git a/DiscordBotsList.Api/Serialization/LongToStringConverter.cs b/DiscordBotsList.Api/Serialization/ULongToStringConverter.cs similarity index 52% rename from DiscordBotsList.Api/Serialization/LongToStringConverter.cs rename to DiscordBotsList.Api/Serialization/ULongToStringConverter.cs index 5cbc7c4..1cd39f8 100644 --- a/DiscordBotsList.Api/Serialization/LongToStringConverter.cs +++ b/DiscordBotsList.Api/Serialization/ULongToStringConverter.cs @@ -9,21 +9,17 @@ namespace DiscordBotsList.Api.Internal /// internal class ULongToStringConverter : JsonConverter { - public override ulong Read( - ref Utf8JsonReader reader, - Type typeToConvert, - JsonSerializerOptions options) + public override ulong Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - var stringValue = reader.GetString(); - if (ulong.TryParse(stringValue, out var value)) return value; + if (reader.TokenType == JsonTokenType.String && ulong.TryParse(reader.GetString(), out var value)) + { + return value; + } throw new InvalidOperationException(); } - public override void Write( - Utf8JsonWriter writer, - ulong value, - JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ulong value, JsonSerializerOptions options) { writer.WriteStringValue(value.ToString()); } diff --git a/LICENSE b/LICENSE index 9e3b10f..fe6a0b5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 Discord Bots +Copyright (c) 2019-2025 Discord Bots Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 9c3d32e..8784148 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,184 @@ -# DBL-dotnet-Library -top.gg botlist wrapper +# Top.gg .NET SDK + +The community-maintained .NET library for Top.gg. + +## Installation + +### Main API wrapper + +#### Library agnostic + +```powershell +> Install-Package DiscordBotsList.Api +``` + +#### Discord.NET-based + +```powershell +> Install-Package DiscordBotsList.Api.Adapter.Discord.Net +``` + +### Webhooks only + +```powershell +> Install-Package DiscordBotsList.Api.Webhooks +``` + +## Setting up + +### Library agnostic + +```cs +var client = new DiscordBotListApi(DISCORD_ID, "TOPGG_TOKEN"); +``` + +### Discord.NET-based + +```cs +var discordNetClient = ...; +var client = new DiscordNetDblApi(discordNetClient, "TOPGG_TOKEN"); +``` ## Usage -### Unauthorized api usage -#### Setting up + +### Getting a bot + ```cs -DiscordBotListApi DblApi = new DiscordBotListApi(); +var bot = await client.GetBotAsync(264811613708746752U); ``` -#### Getting bots +### Getting several bots + +#### With defaults + ```cs -// discord id -IBot bot = DblApi.GetBotAsync(160105994217586689); +var bots = await client.GetBotsAsync(); +var firstBot = bots.Items.First(); ``` -#### Getting users +#### With explicit arguments + ```cs -// discord id -IUser bot = DblApi.GetUserAsync(121919449996460033); +// Sort by Limit Offset +var bots = await client.GetBotsAsync(SortBotsBy.MonthlyPoints, 100, 1); +var firstBot = bots.Items.First(); ``` -### Authorized api usage -#### Setting up +### Getting your bot's voters + +#### First page + ```cs -AuthDiscordBotListApi DblApi = new AuthDiscordBotListApi(BOT_DISCORD_ID, YOUR_TOKEN); +var voters = await client.GetVotersAsync(); ``` -#### Updating stats +#### Subsequent pages + ```cs -IDblSelfBot me = await DblApi.GetMeAsync(); -// Update stats sharded indexShard shardCount shards -await me.UpdateStatsAsync(24, 50, new[] { 12, 421, 62, 241, 524, 534 }); +var voters = await client.GetVotersAsync(2); +``` + +### Check if a user has voted for your bot -// Update stats guildCount -await me.UpdateStatsAsync(2133); +```cs +var voted = await client.HasVoted(661200758510977084U); ``` -#### Widgets +### Getting your bot's server count + ```cs -string widgetUrl = new SmallWidgetOptions() - .SetType(WidgetType.OWNER) - .SetLeftColor(255, 255, 255); - .Build(160105994217586689); +var serverCount = await client.GetServerCountAsync(); ``` -Generates ![](https://top.gg/api/widget/status/160105994217586689.svg?leftcolor=FFFFFF) +### Posting your bot's server count -### Download -#### Nuget -If you're using Nuget you can use find it with the ID `DiscordBotsList.Api` or use -> Install-Package DiscordBotsList.Api +```cs +await client.UpdateServerCountAsync(bot.GetServerCount()); +``` + +### Automatically posting your bot's server count every few minutes + +With Discord.NET: + +```cs +var submissionAdapter = client.CreateListener(); + +submissionAdapter.Start(); + +// ... + +submissionAdapter.Stop(); // Optional +``` + +### Checking if the weekend vote multiplier is active + +```cs +var isWeekend = await client.IsWeekendAsync(); +``` + +### Generating widget URLs + +#### Large + +```cs +var widgetUrl = Widget.Large(WidgetType.DISCORD_BOT, 1026525568344264724U); +``` + +#### Votes + +```cs +var widgetUrl = Widget.Votes(WidgetType.DISCORD_BOT, 1026525568344264724U); +``` + +#### Owner + +```cs +var widgetUrl = Widget.Owner(WidgetType.DISCORD_BOT, 1026525568344264724U); +``` + +#### Social + +```cs +var widgetUrl = Widget.Social(WidgetType.DISCORD_BOT, 1026525568344264724U); +``` + +### Webhooks + +#### Being notified whenever someone voted for your bot + +With ASP.NET Core or Blazor: + +```cs +using DiscordBotsList.Api.Webhooks; + +namespace MyServer +{ + internal class MyVoteListener : IReceiver + { + public Task Callback(Vote vote) + { + Console.WriteLine($"A user with the ID of {vote.VoterId} has voted us on Top.gg!"); + + return Task.CompletedTask; + } + } + + public class Program + { + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + var app = builder.Build(); + + app.UseMiddleware>("/votes", Environment.GetEnvironmentVariable("MY_TOPGG_WEBHOOK_SECRET"), new MyVoteListener()); + + app.Map("/", async context => + { + await context.Response.WriteAsync("Hello, World!"); + }); + + app.Run("http://localhost:8080"); + } + } +} +``` \ No newline at end of file