// Copyright (C) 2021-present sam (starshines.gay) // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published // by the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . using System.Net.Http.Headers; using System.Text.Json; using System.Text.Json.Serialization; using Catalogger.Backend.Bot; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Gateway.Commands; using Remora.Discord.API.Objects; namespace Catalogger.Backend.Services; public class StatusUpdateService(ILogger logger, ShardedGatewayClient shardedClient, Config config) : BackgroundService { private readonly ILogger _logger = logger.ForContext(); private readonly HttpClient _client = new(); protected override async Task ExecuteAsync(CancellationToken ct) { using var timer = new PeriodicTimer(TimeSpan.FromMinutes(3)); while (await timer.WaitForNextTickAsync(ct)) await UpdateShardStatuses(ct); } private async Task UpdateShardStatuses(CancellationToken ct = default) { _logger.Information( "Updating status for {TotalShards} shards. Guild count is {GuildCount}", shardedClient.TotalShards, CataloggerMetrics.GuildsCached.Value ); if (config.Discord.TestMode) { _logger.Debug("Not updating shard statuses because test mode is enabled."); return; } foreach (var (shardId, client) in shardedClient.Shards) { if (!ShardedGatewayClient.IsConnected(client)) { _logger.Warning( "Cannot update status for shard {ShardId} as it is not connected", shardId ); continue; } client.SubmitCommand(PresenceFor(shardId)); } await ReportStatsAsync(); } private UpdatePresence PresenceFor(int shardId) { var status = $"/catalogger help | in {CataloggerMetrics.GuildsCached.Value} servers"; if (shardedClient.TotalShards != 1) status += $" | shard {shardId + 1}/{shardedClient.TotalShards}"; return new UpdatePresence( Status: UserStatus.Online, IsAFK: false, Since: null, Activities: [new Activity(Name: "Beep", Type: ActivityType.Custom, State: status)] ); } private async Task ReportStatsAsync() { if (config.Discord.BotsGgToken == null) return; _logger.Debug("Posting stats to discord.bots.gg"); var req = new HttpRequestMessage( HttpMethod.Post, $"https://discord.bots.gg/api/v1/bots/{config.Discord.ApplicationId}/stats" ); req.Headers.Add("Authorization", config.Discord.BotsGgToken); req.Content = new StringContent( JsonSerializer.Serialize( new BotsGgStats( (int)CataloggerMetrics.GuildsCached.Value, shardedClient.TotalShards ) ), new MediaTypeHeaderValue("application/json", "utf-8") ); var resp = await _client.SendAsync(req); if (!resp.IsSuccessStatusCode) { var content = await resp.Content.ReadAsStringAsync(); _logger.Error( "Error updating stats for discord.bots.gg: {StatusCode}, {Content}", (int)resp.StatusCode, content ); } } private record BotsGgStats( [property: JsonPropertyName("guildCount")] int GuildCount, [property: JsonPropertyName("shardCount")] int ShardCount ); }