diff --git a/.gitignore b/.gitignore index 7a1146b..a31f4be 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ *.user -*.json */.vs .vs/ */obj 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 deleted file mode 100644 index f9c182c..0000000 --- a/DiscordBotsList.Api.Adapter.Discord.Net/DiscordBotsList.Api.Adapter.Discord.Net.csproj +++ /dev/null @@ -1,35 +0,0 @@ - - - - net8.0 - Velddev, Faith - Top.gg - DiscordBotsList.Api.Adapter.Discord.Net - Top.gg API adapter for Discord.Net - Initial release - true - Mike Veldsink - discord bots topgg api discord.net - git - https://github.com/Top-gg-Community/dotnet-sdk - https://github.com/Top-gg-Community/dotnet-sdk - 1.6.0 - LICENSE - - - - - - - - - - - - - True - - - - - diff --git a/DiscordBotsList.Api.Adapter.Discord.Net/DiscordNetDiscordBotsListApi.cs b/DiscordBotsList.Api.Adapter.Discord.Net/DiscordNetDiscordBotsListApi.cs deleted file mode 100644 index 4882985..0000000 --- a/DiscordBotsList.Api.Adapter.Discord.Net/DiscordNetDiscordBotsListApi.cs +++ /dev/null @@ -1,78 +0,0 @@ -using Discord; -using Discord.WebSocket; -using DiscordBotsList.Api.Objects; -using System; -using System.Threading.Tasks; - -namespace DiscordBotsList.Api.Adapter.Discord.Net -{ - public static class DiscordNetDblUtils - { - public static DiscordNetDblApi CreateDblApi(this DiscordSocketClient client, string dblToken) - { - return new DiscordNetDblApi(client, dblToken); - } - - public static ShardedDiscordNetDblApi CreateDblApi(this DiscordShardedClient client, string dblToken) - { - return new ShardedDiscordNetDblApi(client, dblToken); - } - } - - public class DiscordNetDblApi : AuthDiscordBotListApi - { - protected IDiscordClient client; - - public DiscordNetDblApi(IDiscordClient client, string dblToken) : base(client.CurrentUser.Id, dblToken) - { - 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(). - /// - /// 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 override IAdapter CreateListener(TimeSpan? updateTime = null) - { - return new ShardedSubmissionAdapter(this, client as DiscordShardedClient, updateTime ?? TimeSpan.Zero); - } - } -} \ No newline at end of file diff --git a/DiscordBotsList.Api.Adapter.Discord.Net/SubmissionAdapter.cs b/DiscordBotsList.Api.Adapter.Discord.Net/SubmissionAdapter.cs deleted file mode 100644 index e9951e4..0000000 --- a/DiscordBotsList.Api.Adapter.Discord.Net/SubmissionAdapter.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Discord; -using Discord.WebSocket; -using DiscordBotsList.Api.Objects; -using System; -using System.Linq; -using System.Threading.Tasks; - -namespace DiscordBotsList.Api.Adapter.Discord.Net -{ - internal class SubmissionAdapter : IAdapter - { - protected AuthDiscordBotListApi api; - protected IDiscordClient client; - - protected DateTime lastTimeUpdated; - protected TimeSpan updateTime; - - public SubmissionAdapter(AuthDiscordBotListApi api, IDiscordClient client, TimeSpan updateTime) - { - 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!"); - } - } - } -} \ 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 deleted file mode 100644 index 2231dec..0000000 --- a/DiscordBotsList.Api.Adapter.Discord.Net/Utils/DiscordNetDblUtils.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Discord.WebSocket; - -namespace DiscordBotsList.Api.Adapter.Discord.Net.Utils -{ - public static class DiscordNetDblUtils - { - /// - /// Creates a DiscordBotsList Api - /// - /// your client - /// Your DiscordBotsList token - /// A new instance of a DblApi - public static DiscordNetDblApi CreateDblApi(this DiscordSocketClient client, string dblToken) - { - 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/.runsettings.template b/DiscordBotsList.Api.Tests/.runsettings.template deleted file mode 100644 index 7657d98..0000000 --- a/DiscordBotsList.Api.Tests/.runsettings.template +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - MY_API_KEY - 123456 - - - \ No newline at end of file diff --git a/DiscordBotsList.Api.Tests/DiscordBotsList.Api.Tests.csproj b/DiscordBotsList.Api.Tests/DiscordBotsList.Api.Tests.csproj deleted file mode 100644 index 8c2d6c6..0000000 --- a/DiscordBotsList.Api.Tests/DiscordBotsList.Api.Tests.csproj +++ /dev/null @@ -1,29 +0,0 @@ - - - - net8.0 - 8.0.0 - - false - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - diff --git a/DiscordBotsList.Api.Tests/UnitTests.cs b/DiscordBotsList.Api.Tests/UnitTests.cs deleted file mode 100644 index b22cbd2..0000000 --- a/DiscordBotsList.Api.Tests/UnitTests.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Threading.Tasks; -using Xunit; - -namespace DiscordBotsList.Api.Tests -{ - public class Credentials - { - public ulong BotId { get; set; } - public string Token { get; set; } - - public static Credentials LoadFromEnv() - { - return new Credentials() - { - BotId = ulong.Parse(Environment.GetEnvironmentVariable("BOT_ID")), - Token = Environment.GetEnvironmentVariable("API_KEY") - }; - } - } - - public class UnitTests - { - private readonly AuthDiscordBotListApi _api; - private readonly Credentials _cred; - - public UnitTests() - { - _cred = Credentials.LoadFromEnv(); - _api = new AuthDiscordBotListApi(_cred.BotId, _cred.Token); - } - - [Fact] - public async Task HasVotedTestAsync() - { - Assert.False(await _api.HasVoted(0)); - } - - [Fact] - public async Task TaskIsWeekendTestAsync() - { - await _api.IsWeekendAsync(); - } - - [Fact] - public async Task TaskGetVotersTestAsync() - { - Assert.NotNull(await _api.GetVotersAsync()); - } - - [Fact] - public async Task GetBotTestAsync() - { - var botId = 1026525568344264724U; - 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() - { - var bots = await _api.GetBotsAsync(); - - Assert.NotNull(bots); - Assert.NotEmpty(bots.Items); - } - } -} \ No newline at end of file diff --git a/DiscordBotsList.Api/AuthenticatedBotListApi.cs b/DiscordBotsList.Api/AuthenticatedBotListApi.cs deleted file mode 100644 index 80828ca..0000000 --- a/DiscordBotsList.Api/AuthenticatedBotListApi.cs +++ /dev/null @@ -1,190 +0,0 @@ -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.Text; -using System.Text.Json; -using System.Threading.Tasks; - -namespace DiscordBotsList.Api -{ - public enum SortBotsBy - { - MonthlyPoints, - Id, - Date, - } - - public class AuthDiscordBotListApi : DiscordBotListApi - { - private readonly ulong _selfId; - - public AuthDiscordBotListApi(ulong selfId, string token) - { - _selfId = selfId; - _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - } - - /// - /// Fetches bots from Top.gg - /// - /// amount of bots to retrieve (max: 500) - /// amount of bots to skip - /// sorts results based on their monthly vote count, id, or their submission date - /// List of Bot Objects - public async Task> GetBotsAsync(int count = 50, int offset = 0, SortBotsBy sortBy = SortBotsBy.MonthlyPoints) - { - var sortByString = sortBy.ToString(); - sortByString = char.ToLowerInvariant(sortByString[0]) + sortByString[1..]; - - var result = await GetAsync($"bots?limit={count}&offset={offset}&sort={sortByString}"); - - foreach (var bot in result.Items) (bot as Bot).api = this; - return result; - } - - /// - /// Template - /// of GetBotAsync for internal usage. - /// - /// Type of Bot - /// Discord id - /// Bot object of type T - internal async Task GetBotAsync(ulong id) where T : Bot - { - var t = await GetAsync($"bots/{id}"); - if (t == null) return null; - t.api = this; - return t; - } - - /// - /// Get specific bot by Discord id - /// - /// Discord id - /// Bot Object - public new async Task GetBotAsync(ulong id) - { - return await GetBotAsync(id); - } - - /// - /// Get bot stats - /// - /// Discord id, no longer needed - /// IBotStats object related to the bot - public new async Task GetBotStatsAsync(ulong id = 0) - { - return await GetAsync($"bots/{_selfId}/stats"); - } - - /// - /// Get specific user by Discord id - /// - /// Discord id - /// User Object - public new async Task GetUserAsync(ulong id) - { - return await GetAsync($"users/{id}"); - } - - /// - /// 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; - } - - /// - /// 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(); - } - - /// - /// 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 the user has voted for your project in the past 12 hours - /// - /// the user ID - /// 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 Task GetAuthorizedAsync(string url) - { - return 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 deleted file mode 100644 index 1989805..0000000 --- a/DiscordBotsList.Api/DiscordBotListApi.cs +++ /dev/null @@ -1,95 +0,0 @@ -using DiscordBotsList.Api.Internal; -using DiscordBotsList.Api.Objects; -using System; -using System.Net.Http; -using System.Net.Http.Json; -using System.Text.Json; -using System.Threading.Tasks; - -namespace DiscordBotsList.Api -{ - public class DiscordBotListApi - { - protected const string baseEndpoint = "https://top.gg/api/"; - private readonly JsonSerializerOptions _serializerOptions; - protected readonly HttpClient _httpClient; - - public DiscordBotListApi() - { - _httpClient = new HttpClient(); - _serializerOptions = new JsonSerializerOptions(); - _serializerOptions.Converters.Add(new ULongToStringConverter()); - } - - /// - /// Gets bots from botlist - /// - /// amount of bots to appear per page (max: 500) - /// current page to query - /// List of Bot Objects - [Obsolete("This method requires a token to work. Please use the AuthenticatedBotListApi class instead.", true)] - public Task> GetBotsAsync(int count = 50, int page = 0) - { - return null; - } - - /// - /// Get specific bot by Discord id - /// - /// Discord id - /// Bot Object - [Obsolete("This method requires a token to work. Please use the AuthenticatedBotListApi class instead.", true)] - public Task GetBotAsync(ulong id) - { - return null; - } - - /// - /// Get bot stats - /// - /// Discord id - /// IBotStats object related to the bot - [Obsolete("This method requires a token to work. Please use the AuthenticatedBotListApi class instead.", true)] - public Task GetBotStatsAsync(ulong id) - { - return null; - } - - /// - /// Get specific user by Discord id - /// - /// Discord id - /// User Object - [Obsolete("This method requires a token to work. Please use the AuthenticatedBotListApi class instead.", true)] - public Task GetUserAsync(ulong id) - { - return null; - } - - /// - /// Gets and parses objects - /// - /// Type to parse to - /// Url to get from - /// Object of type T - protected 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); - return result.Value; - } - - /// - /// returns true if voting multiplier = x2 - /// - /// True or False - public async Task IsWeekendAsync() - { - return (await GetAsync("weekend")).Weekend; - } - } -} \ No newline at end of file diff --git a/DiscordBotsList.Api/DiscordBotsList.Api.csproj b/DiscordBotsList.Api/DiscordBotsList.Api.csproj deleted file mode 100644 index 9bc63ff..0000000 --- a/DiscordBotsList.Api/DiscordBotsList.Api.csproj +++ /dev/null @@ -1,34 +0,0 @@ - - - - net8.0 - Velddev, Faith - Top.gg - top.gg api wrapper - Mike Veldsink - https://github.com/DiscordBotList/DBL-dotnet-Library - https://github.com/DiscordBotList/DBL-dotnet-Library - git - discord bots list org wrapper api - update to net 8 - false - true - DiscordBotsList.Api - DiscordBotsList.Api - DiscordBotsList.Api - 1.6.0 - LICENSE - - - - - - - - - True - - - - - diff --git a/DiscordBotsList.Api/Internal/Bot.cs b/DiscordBotsList.Api/Internal/Bot.cs deleted file mode 100644 index 204bbe4..0000000 --- a/DiscordBotsList.Api/Internal/Bot.cs +++ /dev/null @@ -1,97 +0,0 @@ -using DiscordBotsList.Api.Objects; -using System; -using System.Collections.Generic; -using System.Text.Json.Serialization; -using System.Threading.Tasks; - -namespace DiscordBotsList.Api.Internal -{ - public class Bot : Entity, IDblBot - { - internal DiscordBotListApi api; - - [JsonPropertyName("clientid")] - [JsonConverter(typeof(ULongToStringConverter))] - public ulong clientId { get; set; } - - [JsonPropertyName("prefix")] public string prefix { get; set; } - - [JsonPropertyName("shortdesc")] public string shortDescription { get; set; } - - [JsonPropertyName("longdesc")] public string longDescription { get; set; } - - [JsonPropertyName("tags")] public List tags { get; set; } - - [JsonPropertyName("website")] public string websiteUrl { get; set; } - - [JsonPropertyName("support")] - public string supportUrl { get; set; } - - [Obsolete("Actually refers to the entire support invite URL, not just its invite code. Use SupportUrl instead.")] - public string SupportInviteCode => supportUrl; - - [JsonPropertyName("github")] public string githubUrl { get; set; } - - [JsonPropertyName("owners")] public List owners { get; set; } - - [JsonPropertyName("invite")] public string customInvite { get; set; } - - [Obsolete("Actually refers to when the bot was submitted. Use submittedAt instead.")] - public DateTime approvedAt => submittedAt; - - [JsonPropertyName("date")] public DateTime submittedAt { get; set; } - - [JsonPropertyName("certifiedBot")] public bool certified { 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 ulong ClientId => clientId; - - public string VanityTag => vanity; - - [Obsolete("Actually refers to when the bot was submitted. Use SubmittedAt instead.")] - public DateTime ApprovedAt => submittedAt; - - public DateTime SubmittedAt => submittedAt; - - public string GithubUrl => githubUrl; - - public string InviteUrl => customInvite ?? $"https://discord.com/oauth2/authorize?&client_id={Id}&scope=bot"; - - public bool IsCertified => certified; - - public string LongDescription => longDescription; - - public string PrefixUsed => prefix; - - public List OwnerIds => owners; - - public int Points => points; - - public int MonthlyPoints => monthlyPoints; - - public string ShortDescription => shortDescription; - - public List Tags => tags; - - public string SupportUrl => supportUrl; - - public string VanityUrl => "https://top.gg/bot/" + vanity; - - public string WebsiteUrl => websiteUrl; - - public BotReviews Reviews => reviews; - - public async Task GetStatsAsync() - { - return await ((AuthDiscordBotListApi)api).GetBotStatsAsync(Id); - } - } -} \ No newline at end of file diff --git a/DiscordBotsList.Api/Internal/BotReviews.cs b/DiscordBotsList.Api/Internal/BotReviews.cs deleted file mode 100644 index 61fd342..0000000 --- a/DiscordBotsList.Api/Internal/BotReviews.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Text.Json.Serialization; - -namespace DiscordBotsList.Api.Internal -{ - 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/Internal/Entity.cs b/DiscordBotsList.Api/Internal/Entity.cs deleted file mode 100644 index 739e836..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 => Avatar; - - [JsonPropertyName("id")] - [JsonConverter(typeof(ULongToStringConverter))] - 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 deleted file mode 100644 index 83055ac..0000000 --- a/DiscordBotsList.Api/Internal/GuildCountObject.cs +++ /dev/null @@ -1,45 +0,0 @@ -using DiscordBotsList.Api.Objects; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json.Serialization; - -namespace DiscordBotsList.Api.Internal -{ - internal class GuildCountObject - { - [JsonPropertyName("server_count")] internal int guildCount; - - public GuildCountObject(int count) - { - guildCount = count; - } - } - - internal class BotStatsObject : ShardedObject, 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; } - } -} \ No newline at end of file diff --git a/DiscordBotsList.Api/Internal/HasVotedObject.cs b/DiscordBotsList.Api/Internal/HasVotedObject.cs deleted file mode 100644 index 668c965..0000000 --- a/DiscordBotsList.Api/Internal/HasVotedObject.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Text.Json.Serialization; - -namespace DiscordBotsList.Api.Internal -{ - internal class HasVotedObject - { - [JsonPropertyName("voted")] public int? HasVoted { get; set; } - } -} \ No newline at end of file diff --git a/DiscordBotsList.Api/Internal/Queries/BotListQuery.cs b/DiscordBotsList.Api/Internal/Queries/BotListQuery.cs deleted file mode 100644 index 656fb80..0000000 --- a/DiscordBotsList.Api/Internal/Queries/BotListQuery.cs +++ /dev/null @@ -1,33 +0,0 @@ -using DiscordBotsList.Api.Objects; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json.Serialization; - -namespace DiscordBotsList.Api.Internal.Queries -{ - internal class BotListQuery : ISearchResult - { - [JsonPropertyName("results")] public List results { get; set; } - - [JsonPropertyName("limit")] public int limit { get; set; } - - [JsonPropertyName("offset")] public int? offset { get; set; } - - [JsonPropertyName("count")] public int count { get; set; } - - [JsonPropertyName("total")] public int total { get; set; } - - public List Items => results - .Cast() - .ToList(); - - 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); - } -} \ No newline at end of file diff --git a/DiscordBotsList.Api/Internal/SelfBot.cs b/DiscordBotsList.Api/Internal/SelfBot.cs deleted file mode 100644 index 946ba01..0000000 --- a/DiscordBotsList.Api/Internal/SelfBot.cs +++ /dev/null @@ -1,39 +0,0 @@ -using DiscordBotsList.Api.Objects; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace DiscordBotsList.Api.Internal -{ - internal class SelfBot : Bot, IDblSelfBot - { - public async Task> GetVotersAsync(int page = 1) - { - return await ((AuthDiscordBotListApi)api).GetVotersAsync(page); - } - - public async Task HasVotedAsync(ulong userId) - { - return await ((AuthDiscordBotListApi)api).HasVoted(userId); - } - - public async Task IsWeekendAsync() - { - return await ((AuthDiscordBotListApi)api).IsWeekendAsync(); - } - - public async Task UpdateStatsAsync(int guildCount) - { - 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); - } - } -} \ 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/ApiRequest.cs b/DiscordBotsList.Api/Objects/ApiRequest.cs deleted file mode 100644 index 9aaecb1..0000000 --- a/DiscordBotsList.Api/Objects/ApiRequest.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Net; - -namespace DiscordBotsList.Api.Objects -{ - public class ApiResult - { - private ApiResult() - { - } - - public T Value { get; private set; } - - /// - /// The error reason for this API request, if any. - /// - public string ErrorReason { get; private set; } - - public bool IsSuccess { get; private set; } - - internal static ApiResult FromSuccess(T value) - { - return new ApiResult { Value = value, IsSuccess = true }; - } - - internal static ApiResult FromError(Exception ex) - { - return new ApiResult { Value = default, ErrorReason = ex.Message, IsSuccess = false }; - } - - internal static ApiResult - FromHttpError( - HttpStatusCode statusCode) // This could be altered to collect an object that provides more information - { - return new ApiResult { Value = default, ErrorReason = statusCode.ToString(), IsSuccess = false }; - } - } -} \ No newline at end of file diff --git a/DiscordBotsList.Api/Objects/IAdapter.cs b/DiscordBotsList.Api/Objects/IAdapter.cs deleted file mode 100644 index e58695e..0000000 --- a/DiscordBotsList.Api/Objects/IAdapter.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace DiscordBotsList.Api.Objects -{ - public interface IAdapter - { - event Action Log; - - Task RunAsync(); - - void Start(); - - void Stop(); - } -} \ No newline at end of file diff --git a/DiscordBotsList.Api/Objects/IBotStats.cs b/DiscordBotsList.Api/Objects/IBotStats.cs deleted file mode 100644 index 116efb0..0000000 --- a/DiscordBotsList.Api/Objects/IBotStats.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Collections.Generic; - -namespace DiscordBotsList.Api.Objects -{ - public interface IDblBotStats - { - int GuildCount { get; } - - IReadOnlyList Shards { get; } - - int ShardCount { get; } - } -} \ No newline at end of file diff --git a/DiscordBotsList.Api/Objects/IDblBot.cs b/DiscordBotsList.Api/Objects/IDblBot.cs deleted file mode 100644 index e69ce9e..0000000 --- a/DiscordBotsList.Api/Objects/IDblBot.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using DiscordBotsList.Api.Internal; - -namespace DiscordBotsList.Api.Objects -{ - public interface IDblBot : IDblEntity - { - ulong ClientId { get; } - - string VanityTag { get; } - - string PrefixUsed { get; } - - string ShortDescription { get; } - - string LongDescription { get; } - - List Tags { get; } - - string WebsiteUrl { get; } - - string SupportUrl { get; } - - string GithubUrl { get; } - - List OwnerIds { get; } - - string InviteUrl { get; } - - DateTime SubmittedAt { get; } - - bool IsCertified { get; } - - string VanityUrl { get; } - - int Points { get; } - - int MonthlyPoints { get; } - - BotReviews Reviews { get; } - - Task GetStatsAsync(); - } - - public interface IDblSelfBot : IDblBot - { - Task> GetVotersAsync(int page = 1); - - Task HasVotedAsync(ulong userId); - - Task IsWeekendAsync(); - - Task UpdateStatsAsync(int guildCount); - - Task UpdateStatsAsync(int[] shards); - - Task UpdateStatsAsync(int shardCount, int totalShards, params int[] shards); - } -} \ No newline at end of file diff --git a/DiscordBotsList.Api/Objects/IDblEntity.cs b/DiscordBotsList.Api/Objects/IDblEntity.cs deleted file mode 100644 index d952521..0000000 --- a/DiscordBotsList.Api/Objects/IDblEntity.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace DiscordBotsList.Api.Objects -{ - public interface IDblEntity - { - /// - /// Id - /// - ulong Id { get; } - - /// - /// Username - /// - string Username { get; } - - /// - /// Discriminator, the XXXX#1234 part - /// - string Discriminator { get; } - - /// - /// Avatar url, or default avatar if none found. - /// - string AvatarUrl { get; } - } -} \ No newline at end of file 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/ISearchResult.cs b/DiscordBotsList.Api/Objects/ISearchResult.cs deleted file mode 100644 index 4eb5c45..0000000 --- a/DiscordBotsList.Api/Objects/ISearchResult.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Collections.Generic; - -namespace DiscordBotsList.Api.Objects -{ - public interface ISearchResult - { - /// - /// Items returned from search - /// - List Items { get; } - - /// - /// The current page you've navigated - /// - int CurrentPage { get; } - - /// - /// Set items per page - /// - int ItemsPerPage { get; } - - /// - /// Total amount of items found in the search - /// - int TotalItems { get; } - - /// - /// Total amount of pages found in the search - /// - int TotalPages { 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 deleted file mode 100644 index e24b116..0000000 --- a/DiscordBotsList.Api/Objects/WeekendObject.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Text.Json.Serialization; - -namespace DiscordBotsList.Api.Objects -{ - public class WeekendObject - { - [JsonPropertyName("is_weekend")] - public bool Weekend { get; set; } - } -} \ No newline at end of file diff --git a/DiscordBotsList.Api/Serialization/ULongToStringConverter.cs b/DiscordBotsList.Api/Serialization/ULongToStringConverter.cs deleted file mode 100644 index 1cd39f8..0000000 --- a/DiscordBotsList.Api/Serialization/ULongToStringConverter.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace DiscordBotsList.Api.Internal -{ - /// - /// 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/LICENSE b/LICENSE index fe6a0b5..c4c27be 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019-2025 Discord Bots +Copyright (c) 2019-2026 Top.gg 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..a972e35 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,201 @@ -# DBL-dotnet-Library -top.gg botlist wrapper +# Top.gg .NET SDK + +The community-maintained .NET SDK for Top.gg. + +## Chapters + +- [Installation](#installation) +- [Setting up](#setting-up) +- [Usage](#usage) + - [Getting your project's information](#getting-your-projects-information) + - [Getting your project's vote information of a user](#getting-your-projects-vote-information-of-a-user) + - [Getting a cursor-based paginated list of votes for your project](#getting-a-cursor-based-paginated-list-of-votes-for-your-project) + - [Posting your bot's application commands list](#posting-your-bots-application-commands-list) + - [Generating widget URLs](#generating-widget-urls) + - [Webhooks](#webhooks) + +## Installation + +### Main API wrapper + +```console +$ dotnet add package Topgg.Sdk.Api --version 1.0.0 +``` + +### Webhooks only + +```console +$ dotnet add package Topgg.Sdk.Webhooks --version 1.0.0 +``` + +## Setting up + +```cs +using Topgg.Sdk.Api; + +var client = new TopggApi(Environment.GetEnvironmentVariable("TOPGG_TOKEN")); +``` ## Usage -### Unauthorized api usage -#### Setting up + +### Getting your project's information + +```cs +var project = await client.GetSelfAsync(); +``` + +### Getting your project's vote information of a user + +#### Discord ID + +```cs +using Topgg.Sdk.Api.Data; + +var vote = await client.GetVoteAsync(661200758510977084); +``` + +#### Top.gg ID + +```cs +using Topgg.Sdk.Api.Data; + +var vote = await client.GetVoteAsync(8226924471638491136, UserSource.Topgg); +``` + +### Getting a cursor-based paginated list of votes for your project + +```cs +var since = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc); + +var firstPage = await client.GetVotesAsync(since); + +foreach (var vote in firstPage.Votes) +{ + // ... +} + +var secondPage = await firstPage.Next(); + +foreach (var vote in secondPage.Votes) +{ + // ... +} +``` + +### Posting your bot's application commands list + +#### Discord.Net + ```cs -DiscordBotListApi DblApi = new DiscordBotListApi(); +var commands = $"[{string.Join(",", (await bot.GetGlobalApplicationCommandsAsync()).Select(command => command.ToJson()))}]"; + +await client.PostCommandsAsync(commands); ``` -#### Getting bots +#### Raw + ```cs -// discord id -IBot bot = DblApi.GetBotAsync(160105994217586689); +// Array of application commands that +// can be serialized to Discord API's raw JSON format. +var commands = @"[ + { + ""options"": [], + ""name"": ""test"", + ""name_localizations"": null, + ""description"": ""command description"", + ""description_localizations"": null, + ""contexts"": [], + ""default_permission"": null, + ""default_member_permissions"": null, + ""dm_permission"": false, + ""integration_types"": [], + ""nsfw"": false + } +]"; + +await client.PostCommandsAsync(commands); ``` -#### Getting users +### Generating widget URLs + +#### Large + ```cs -// discord id -IUser bot = DblApi.GetUserAsync(121919449996460033); +using Topgg.Sdk.Api.Data; +using Topgg.Sdk.Api; + +var widgetUrl = Widget.Large(Platform.Discord, ProjectType.Bot, 1026525568344264724); ``` -### Authorized api usage -#### Setting up +#### Votes + ```cs -AuthDiscordBotListApi DblApi = new AuthDiscordBotListApi(BOT_DISCORD_ID, YOUR_TOKEN); +using Topgg.Sdk.Api.Data; +using Topgg.Sdk.Api; + +var widgetUrl = Widget.Votes(Platform.Discord, ProjectType.Bot, 1026525568344264724); ``` -#### Updating stats +#### Owner + ```cs -IDblSelfBot me = await DblApi.GetMeAsync(); -// Update stats sharded indexShard shardCount shards -await me.UpdateStatsAsync(24, 50, new[] { 12, 421, 62, 241, 524, 534 }); +using Topgg.Sdk.Api.Data; +using Topgg.Sdk.Api; -// Update stats guildCount -await me.UpdateStatsAsync(2133); +var widgetUrl = Widget.Owner(Platform.Discord, ProjectType.Bot, 1026525568344264724); ``` -#### Widgets +#### Social + +```cs +using Topgg.Sdk.Api.Data; +using Topgg.Sdk.Api; + +var widgetUrl = Widget.Social(Platform.Discord, ProjectType.Bot, 1026525568344264724); +``` + +### Webhooks + +With ASP.NET Core: + ```cs -string widgetUrl = new SmallWidgetOptions() - .SetType(WidgetType.OWNER) - .SetLeftColor(255, 255, 255); - .Build(160105994217586689); +using Topgg.Sdk.Webhooks.Payloads; +using Topgg.Sdk.Webhooks; + +public class Webhooks() : WebhookEventListener(Environment.GetEnvironmentVariable("TOPGG_WEBHOOK_SECRET")) +{ + // Optional + public override Task OnIntegrationCreate(HttpContext context, IntegrationCreatePayload payload, string trace) => DefaultResponse(context); + + // Optional + public override Task OnIntegrationDelete(HttpContext context, IntegrationDeletePayload payload, string trace) => DefaultResponse(context); + + // Optional + public override Task OnTest(HttpContext context, TestPayload payload, string trace) => DefaultResponse(context); + + // Optional + public override Task OnVoteCreate(HttpContext context, VoteCreatePayload payload, string trace) => DefaultResponse(context); + + private static async Task DefaultResponse(HttpContext context) + { + if (!context.Response.HasStarted) + { + context.Response.StatusCode = 204; + } + } +} ``` -Generates ![](https://top.gg/api/widget/status/160105994217586689.svg?leftcolor=FFFFFF) +Later, in your server's setup: + +```cs +var builder = WebApplication.CreateBuilder(args); +var app = builder.Build(); + +var webhooks = new CustomWebhooks(); + +// POST /webhook +app.MapPost("/webhook", webhooks.Handler); -### Download -#### Nuget -If you're using Nuget you can use find it with the ID `DiscordBotsList.Api` or use -> Install-Package DiscordBotsList.Api +app.Run(); +``` \ No newline at end of file diff --git a/Topgg.Sdk.Api/Data/Project.cs b/Topgg.Sdk.Api/Data/Project.cs new file mode 100644 index 0000000..4304b17 --- /dev/null +++ b/Topgg.Sdk.Api/Data/Project.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Topgg.Sdk.Api.Serialization; + +namespace Topgg.Sdk.Api.Data; + +/// A project's platform. +public enum Platform +{ + Discord +} + +/// A project's type. +public enum ProjectType +{ + Bot, + Server +} + +/// A project listed on Top.gg. +public class Project +{ + /// The project's ID. + [JsonConverter(typeof(ULongToStringConverter))] + public ulong Id { get; internal init; } + + /// The project's name sourced from the external platform. + public string Name { get; internal init; } + + /// The project's platform. + [JsonConverter(typeof(JsonStringEnumConverter))] + public Platform Platform { get; internal init; } + + /// The project's type. + [JsonConverter(typeof(JsonStringEnumConverter))] + public ProjectType Type { get; internal init; } + + /// The project's short description. + public string Headline { get; internal init; } + + /// The project's tag IDs. + public List Tags { get; internal init; } + + /// The project's current vote count that affects the project's ranking. + public int Votes { get; internal init; } + + /// The project's total vote count. + [JsonPropertyName("votes_total")] + public int TotalVotes { get; internal init; } + + /// The project's review score out of 5. + public float ReviewScore { get; internal init; } + + /// The project's total review count. + public int ReviewCount { get; internal init; } +} + +/// A brief information on a project listed on Top.gg. +public class PartialProject +{ + /// The project's ID. + [JsonConverter(typeof(ULongToStringConverter))] + public ulong Id { get; internal init; } + + /// The project's type. + [JsonConverter(typeof(JsonStringEnumConverter))] + public ProjectType Type { get; internal init; } + + /// The project's platform. + [JsonConverter(typeof(JsonStringEnumConverter))] + public Platform Platform { get; internal init; } + + /// The project's platform ID. + [JsonConverter(typeof(ULongToStringConverter))] + public ulong PlatformId { get; internal init; } + +} diff --git a/Topgg.Sdk.Api/Data/User.cs b/Topgg.Sdk.Api/Data/User.cs new file mode 100644 index 0000000..b44d269 --- /dev/null +++ b/Topgg.Sdk.Api/Data/User.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Topgg.Sdk.Api.Serialization; + +namespace Topgg.Sdk.Api.Data; + +/// A user account from an external platform that is linked to a Top.gg user account. +public enum UserSource +{ + Discord, + Topgg, +} + +/// A brief information of a project's vote. +public class PartialVote +{ + /// When the vote was cast. + [JsonPropertyName("created_at")] + public DateTime VotedAt { get; internal init; } + + /// When the vote expires and the user is required to vote again. + public DateTime ExpiresAt { get; internal init; } + + /// The number of votes this vote counted for. This is a rounded integer value which determines how many points this individual vote was worth. + public int Weight { get; internal init; } +} + +/// A project's vote information. +public class Vote : PartialVote +{ + /// The voter's ID. + [JsonPropertyName("user_id")] + [JsonConverter(typeof(ULongToStringConverter))] + public ulong VoterId { get; internal init; } + + /// The voter's ID on the project's platform. + [JsonConverter(typeof(ULongToStringConverter))] + public ulong PlatformId { get; internal init; } +} + +/// A paginated list of a project's vote information. +public class PaginatedVotes +{ + /// The votes in this page. + [JsonPropertyName("data")] + public List Votes { get; internal init; } + + [JsonInclude] + internal string Cursor { get; init; } + + internal TopggApi Client; + + /// Tries to advance to the next page. + /// The next page of votes. + public async Task Next() => await Client.GetVotesAsync(Cursor); +} \ No newline at end of file diff --git a/Topgg.Sdk.Api/Serialization/ULongToStringConverter.cs b/Topgg.Sdk.Api/Serialization/ULongToStringConverter.cs new file mode 100644 index 0000000..c53f7a6 --- /dev/null +++ b/Topgg.Sdk.Api/Serialization/ULongToStringConverter.cs @@ -0,0 +1,21 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Topgg.Sdk.Api.Serialization; + +/// 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/Topgg.Sdk.Api/Tests/Mock.cs b/Topgg.Sdk.Api/Tests/Mock.cs new file mode 100644 index 0000000..d40c138 --- /dev/null +++ b/Topgg.Sdk.Api/Tests/Mock.cs @@ -0,0 +1,64 @@ +#nullable enable + +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; + +namespace Topgg.Sdk.Api.Tests; + +internal class Mock : HttpMessageHandler +{ +#pragma warning disable SYSLIB1045 + private static readonly (HttpMethod Method, Regex Endpoint, string? Name)[] Routes = { + (HttpMethod.Get, new Regex(@"^\/projects\/@me$", RegexOptions.Compiled), "GetSelf"), + (HttpMethod.Get, new Regex(@"^\/projects\/@me\/votes\/\d+$", RegexOptions.Compiled), "GetVote"), + (HttpMethod.Get, new Regex(@"^\/projects\/@me\/votes$", RegexOptions.Compiled), "GetVotes"), + (HttpMethod.Post, new Regex(@"^\/projects\/@me\/commands$", RegexOptions.Compiled), null) + }; +#pragma warning restore SYSLIB1045 + + private static Stream StreamJson(string name) => typeof(Mock).Assembly.GetManifestResourceStream($"Topgg.Sdk.Api.Tests.Mocks.{name}.json")!; + + internal static string ReadJson(string name) + { + using var stream = StreamJson(name); + using var reader = new StreamReader(stream); + + return reader.ReadToEnd(); + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (request.RequestUri!.Host == "top.gg" && request.RequestUri.AbsolutePath.StartsWith("/api/v1/")) + { + var endpoint = request.RequestUri.AbsolutePath[7..]; + + foreach (var (Method, Endpoint, Name) in Routes) + { + if (request.Method == Method && Endpoint.IsMatch(endpoint)) + { + if (Name == null) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NoContent)); + } + else + { + var content = new StreamContent(StreamJson(Name)); + content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = content, + }); + } + } + } + } + + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)); + } +} \ No newline at end of file diff --git a/Topgg.Sdk.Api/Tests/Mocks/GetSelf.json b/Topgg.Sdk.Api/Tests/Mocks/GetSelf.json new file mode 100644 index 0000000..e74997b --- /dev/null +++ b/Topgg.Sdk.Api/Tests/Mocks/GetSelf.json @@ -0,0 +1,16 @@ +{ + "id": "364806029876555776", + "name": "Top.gg Lib Dev API Access", + "type": "bot", + "platform": "discord", + "headline": "API access for Top.gg Library Developers", + "tags": [ + "api", + "library", + "topgg" + ], + "votes": 4, + "votes_total": 34, + "review_score": 5, + "review_count": 2 +} \ No newline at end of file diff --git a/Topgg.Sdk.Api/Tests/Mocks/GetVote.json b/Topgg.Sdk.Api/Tests/Mocks/GetVote.json new file mode 100644 index 0000000..c6a0227 --- /dev/null +++ b/Topgg.Sdk.Api/Tests/Mocks/GetVote.json @@ -0,0 +1,5 @@ +{ + "created_at": "2026-02-25T22:35:36.978392+00:00", + "expires_at": "2026-02-26T10:35:36.978392+00:00", + "weight": 1 +} \ No newline at end of file diff --git a/Topgg.Sdk.Api/Tests/Mocks/GetVotes.json b/Topgg.Sdk.Api/Tests/Mocks/GetVotes.json new file mode 100644 index 0000000..5eab378 --- /dev/null +++ b/Topgg.Sdk.Api/Tests/Mocks/GetVotes.json @@ -0,0 +1,33 @@ +{ + "cursor": "", + "data": [ + { + "user_id": "800506814562787328", + "platform_id": "1461830808796139662", + "weight": 2, + "created_at": "2026-01-17T23:36:06.34732Z", + "expires_at": "2026-01-18T11:36:06.34732Z" + }, + { + "user_id": "316026718115037184", + "platform_id": "481068576363773972", + "weight": 2, + "created_at": "2026-02-20T05:43:58.392411Z", + "expires_at": "2026-02-20T17:43:58.392411Z" + }, + { + "user_id": "794153497215045632", + "platform_id": "1425259851600101457", + "weight": 2, + "created_at": "2026-02-21T18:59:20.660734Z", + "expires_at": "2026-02-22T06:59:20.660734Z" + }, + { + "user_id": "8226924471638491136", + "platform_id": "661200758510977084", + "weight": 1, + "created_at": "2026-02-25T22:35:36.978392Z", + "expires_at": "2026-02-26T10:35:36.978392Z" + } + ] +} \ No newline at end of file diff --git a/Topgg.Sdk.Api/Tests/Mocks/PostCommands.json b/Topgg.Sdk.Api/Tests/Mocks/PostCommands.json new file mode 100644 index 0000000..7284384 --- /dev/null +++ b/Topgg.Sdk.Api/Tests/Mocks/PostCommands.json @@ -0,0 +1,15 @@ +[ + { + "options": [], + "name": "test", + "name_localizations": null, + "description": "command description", + "description_localizations": null, + "contexts": [], + "default_permission": null, + "default_member_permissions": null, + "dm_permission": false, + "integration_types": [], + "nsfw": false + } +] \ No newline at end of file diff --git a/Topgg.Sdk.Api/Tests/Tests.cs b/Topgg.Sdk.Api/Tests/Tests.cs new file mode 100644 index 0000000..f14f9bd --- /dev/null +++ b/Topgg.Sdk.Api/Tests/Tests.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Topgg.Sdk.Api.Data; +using Xunit; + +namespace Topgg.Sdk.Api.Tests; + +public class Tests +{ + public static IEnumerable> UserSources => Enum.GetValues().Select(source => new TheoryDataRow(source)); + public static IEnumerable> PlatformsAndProjectTypes => Enum.GetValues().SelectMany(platform => Enum.GetValues(), (platform, projectType) => new TheoryDataRow(platform, projectType)); + + private readonly TopggApi Client = new(new HttpClient(new Mock())); + + [Fact] + public async Task GetSelfAsync() => await Client.GetSelfAsync(); + + [Fact] + public async Task PostCommandsAsync() => await Client.PostCommandsAsync(Mock.ReadJson("PostCommands")); + + [Theory] + [MemberData(nameof(UserSources))] + public async Task GetVoteAsync(UserSource source) + { + await Client.GetVoteAsync(661200758510977084, source); + } + + [Fact] + public async Task GetVotesAsync() + { + var since = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + var firstPage = await Client.GetVotesAsync(since); + await firstPage.Next(); + } + + [Theory] + [MemberData(nameof(PlatformsAndProjectTypes))] + public void Widgets(Platform platform, ProjectType projectType) + { + Widget.Large(platform, projectType, 123456); + Widget.Votes(platform, projectType, 123456); + Widget.Owner(platform, projectType, 123456); + Widget.Social(platform, projectType, 123456); + } +} \ No newline at end of file diff --git a/Topgg.Sdk.Api/Topgg.Sdk.Api.csproj b/Topgg.Sdk.Api/Topgg.Sdk.Api.csproj new file mode 100644 index 0000000..eccf4ec --- /dev/null +++ b/Topgg.Sdk.Api/Topgg.Sdk.Api.csproj @@ -0,0 +1,41 @@ + + + net8.0 + Velddev, Faith, null8626 + Top.gg + The community-maintained .NET SDK for the Top.gg API. + Mike Veldsink + https://github.com/Top-gg-Community/dotnet-sdk + https://github.com/Top-gg-Community/dotnet-sdk + git + discord bots topgg api + Adapt to Top.gg API v1 + false + true + Topgg.Sdk.Api + Topgg.Sdk.Api + The .NET SDK for the Top.gg API + 1.0.0 + LICENSE + + + + + True + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/Topgg.Sdk.Api/TopggApi.cs b/Topgg.Sdk.Api/TopggApi.cs new file mode 100644 index 0000000..03d2cec --- /dev/null +++ b/Topgg.Sdk.Api/TopggApi.cs @@ -0,0 +1,120 @@ +#nullable enable + +using Topgg.Sdk.Api.Data; +using Topgg.Sdk.Api.Serialization; +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using System.Text.Json.Serialization; + +namespace Topgg.Sdk.Api; + +/// Interact with Top.gg API v1's endpoints. +public class TopggApi(HttpClient httpClient) +{ + internal static readonly string BaseURL = "https://top.gg/api/v1"; + private readonly JsonSerializerOptions SerializerOptions = new() + { + Converters = { new ULongToStringConverter(), new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }, + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower + }; + private readonly HttpClient Http = httpClient; + + /// Creates a new client instance. + /// The API token to use. + public TopggApi(string token) : this(new HttpClient() + { + DefaultRequestHeaders = { { "Authorization", $"Bearer {token}" } } + }) + { } + + /// Tries to get your project's information. + /// Your project's information. + public async Task GetSelfAsync() => (await GetAsync("/projects/@me"))!; + + /// Tries to update the application commands list in your Discord bot's Top.gg page. + /// Serializable list of Discord application commands. + /// A list of your Discord bot's application commands in the form of Discord API's raw JSON format. + public async Task PostCommandsAsync(T commands) => await PostAsync("/projects/@me/commands", commands); + + /// Tries to get the latest vote information of a user on your project. Returns null if the user has not voted. + /// The user's ID. + /// The user's source. + /// The latest vote information of a user on your project or null if the user has not voted. + public async Task GetVoteAsync(ulong id, UserSource source = UserSource.Discord) + { + try + { + return await GetAsync($"/projects/@me/votes/{id}?source={source.ToString().ToLower()}"); + } + catch (HttpRequestException error) + { + if (error.StatusCode == HttpStatusCode.NotFound) + { + return null; + } + + throw; + } + } + + + /// Tries to get a cursor-based paginated list of votes for your project, ordered by creation date. + /// The earliest possible date for all votes. + /// A cursor-based paginated list of votes for your project, ordered by creation date. + public async Task GetVotesAsync(DateTime since) + { + var votes = (await GetAsync($"/projects/@me/votes?startDate={Uri.EscapeDataString(since.ToString("yyyy-MM-ddTHH:mm:ss.fffK"))}"))!; + + votes.Client = this; + + return votes; + } + + internal async Task GetVotesAsync(string cursor) + { + var votes = (await GetAsync($"/projects/@me/votes?cursor={cursor}"))!; + + votes.Client = this; + + return votes; + } + + private async Task ProcessResponse(HttpResponseMessage response) + { + response.EnsureSuccessStatusCode(); + + if (typeof(T) == typeof(string)) + { + return (T)(object)await response.Content.ReadAsStringAsync(); + } + else + { + return await response.Content.ReadFromJsonAsync(SerializerOptions); + } + } + + private async Task GetAsync(string url) => await ProcessResponse(await Http.GetAsync(BaseURL + url)); + + private async Task PostAsync(string url, B body) + { + StringContent httpContent; + + if (typeof(B) == typeof(string)) + { + httpContent = new StringContent((string)(object)body!, Encoding.UTF8, "application/json"); + } + else + { + var json = JsonSerializer.Serialize(body); + + httpContent = new StringContent(json, Encoding.UTF8, "application/json"); + } + + return await ProcessResponse(await Http.PostAsync(BaseURL + url, httpContent)); + } +} \ No newline at end of file diff --git a/Topgg.Sdk.Api/Widget.cs b/Topgg.Sdk.Api/Widget.cs new file mode 100644 index 0000000..fb4fc00 --- /dev/null +++ b/Topgg.Sdk.Api/Widget.cs @@ -0,0 +1,34 @@ +using Topgg.Sdk.Api.Data; + +namespace Topgg.Sdk.Api; + +public static class Widget +{ + /// Generates a large widget URL. + /// The project's platform. + /// The project's type. + /// The project ID. + /// The widget URL. + public static string Large(Platform platform, ProjectType projectType, ulong id) => $"{TopggApi.BaseURL}/widgets/large/{platform.ToString().ToLower()}/{projectType.ToString().ToLower()}/{id}"; + + /// Generates a small widget URL for displaying votes. + /// The project's platform. + /// The project's type. + /// The project ID. + /// The widget URL. + public static string Votes(Platform platform, ProjectType projectType, ulong id) => $"{TopggApi.BaseURL}/widgets/small/votes/{platform.ToString().ToLower()}/{projectType.ToString().ToLower()}/{id}"; + + /// Generates a small widget URL for displaying a project's owner. + /// The project's platform. + /// The project's type. + /// The project ID. + /// The widget URL. + public static string Owner(Platform platform, ProjectType projectType, ulong id) => $"{TopggApi.BaseURL}/widgets/small/owner/{platform.ToString().ToLower()}/{projectType.ToString().ToLower()}/{id}"; + + /// Generates a small widget URL for displaying social stats. + /// The project's platform. + /// The project's type. + /// The project ID. + /// The widget URL. + public static string Social(Platform platform, ProjectType projectType, ulong id) => $"{TopggApi.BaseURL}/widgets/small/social/{platform.ToString().ToLower()}/{projectType.ToString().ToLower()}/{id}"; +} \ No newline at end of file diff --git a/DiscordBotsList.Api.sln b/Topgg.Sdk.sln similarity index 58% rename from DiscordBotsList.Api.sln rename to Topgg.Sdk.sln index 7209a51..1f9a667 100644 --- a/DiscordBotsList.Api.sln +++ b/Topgg.Sdk.sln @@ -1,37 +1,29 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27130.2020 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscordBotsList.Api", "DiscordBotsList.Api\DiscordBotsList.Api.csproj", "{B20E0634-86B7-4392-A40D-93B368A68158}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscordBotsList.Api.Tests", "DiscordBotsList.Api.Tests\DiscordBotsList.Api.Tests.csproj", "{4A37AC0E-B9C5-49DB-B8C2-D701A37CA771}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscordBotsList.Api.Adapter.Discord.Net", "DiscordBotsList.Api.Adapter.Discord.Net\DiscordBotsList.Api.Adapter.Discord.Net.csproj", "{0E4A5566-863E-402A-9359-49FC7900D4E8}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {B20E0634-86B7-4392-A40D-93B368A68158}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B20E0634-86B7-4392-A40D-93B368A68158}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B20E0634-86B7-4392-A40D-93B368A68158}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B20E0634-86B7-4392-A40D-93B368A68158}.Release|Any CPU.Build.0 = Release|Any CPU - {4A37AC0E-B9C5-49DB-B8C2-D701A37CA771}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4A37AC0E-B9C5-49DB-B8C2-D701A37CA771}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4A37AC0E-B9C5-49DB-B8C2-D701A37CA771}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4A37AC0E-B9C5-49DB-B8C2-D701A37CA771}.Release|Any CPU.Build.0 = Release|Any CPU - {0E4A5566-863E-402A-9359-49FC7900D4E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0E4A5566-863E-402A-9359-49FC7900D4E8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0E4A5566-863E-402A-9359-49FC7900D4E8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0E4A5566-863E-402A-9359-49FC7900D4E8}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {B3317C2A-4530-4F3A-9A45-8D8DA228E8B5} - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27130.2020 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Topgg.Sdk.Api", "Topgg.Sdk.Api\Topgg.Sdk.Api.csproj", "{B20E0634-86B7-4392-A40D-93B368A68158}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B20E0634-86B7-4392-A40D-93B368A68158}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B20E0634-86B7-4392-A40D-93B368A68158}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B20E0634-86B7-4392-A40D-93B368A68158}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B20E0634-86B7-4392-A40D-93B368A68158}.Release|Any CPU.Build.0 = Release|Any CPU + {4A37AC0E-B9C5-49DB-B8C2-D701A37CA771}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4A37AC0E-B9C5-49DB-B8C2-D701A37CA771}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4A37AC0E-B9C5-49DB-B8C2-D701A37CA771}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4A37AC0E-B9C5-49DB-B8C2-D701A37CA771}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B3317C2A-4530-4F3A-9A45-8D8DA228E8B5} + EndGlobalSection +EndGlobal