diff --git a/app/Resources/Tracker/scripts/state.js b/app/Resources/Tracker/scripts/state.js index cfeeac3..53ad375 100644 --- a/app/Resources/Tracker/scripts/state.js +++ b/app/Resources/Tracker/scripts/state.js @@ -142,11 +142,19 @@ const STATE = (function() { if (DISCORD.CHANNEL_TYPE.isPrivate(channelInfo.type)) { server.id = channelInfo.id; server.name = channel.name = getPrivateChannelName(channelInfo); + + if (channelInfo.icon) { + server.icon = channelInfo.icon; + } } else if (serverInfo) { server.id = serverInfo.id; server.name = serverInfo.name; channel.name = channelInfo.name; + + if (serverInfo.icon) { + server.icon = serverInfo.icon; + } } else { return; diff --git a/app/Resources/Tracker/scripts/types.js b/app/Resources/Tracker/scripts/types.js index 9dce7f6..7957eb5 100644 --- a/app/Resources/Tracker/scripts/types.js +++ b/app/Resources/Tracker/scripts/types.js @@ -2,6 +2,7 @@ * @name DiscordGuild * @property {String} id * @property {String} name + * @property {String|null|undefined} [icon] */ /** @@ -14,6 +15,7 @@ * @property {Number} [position] * @property {String} [topic] * @property {Boolean} [nsfw] + * @property {String|null|undefined} [icon] * @property {DiscordUser[]} [rawRecipients] */ diff --git a/app/Server/Data/Server.cs b/app/Server/Data/Server.cs index 824e475..7a55742 100644 --- a/app/Server/Data/Server.cs +++ b/app/Server/Data/Server.cs @@ -1,7 +1,12 @@ +using DHT.Server.Download; + namespace DHT.Server.Data; public readonly struct Server { public ulong Id { get; init; } public string Name { get; init; } public ServerType? Type { get; init; } + public string? IconHash { get; init; } + + internal FileUrl? IconUrl => Type == null || IconHash == null ? null : DownloadLinkExtractor.ServerIcon(Type.Value, Id, IconHash); } diff --git a/app/Server/Database/Export/ViewerJson.cs b/app/Server/Database/Export/ViewerJson.cs index 2604491..16dcf57 100644 --- a/app/Server/Database/Export/ViewerJson.cs +++ b/app/Server/Database/Export/ViewerJson.cs @@ -23,6 +23,9 @@ static class ViewerJson { public sealed class JsonServer { public required string Name { get; init; } public required string Type { get; init; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? IconUrl { get; init; } } public sealed class JsonChannel { diff --git a/app/Server/Database/Export/ViewerJsonExport.cs b/app/Server/Database/Export/ViewerJsonExport.cs index a98e2a4..38d91d0 100644 --- a/app/Server/Database/Export/ViewerJsonExport.cs +++ b/app/Server/Database/Export/ViewerJsonExport.cs @@ -108,6 +108,7 @@ static class ViewerJsonExport { servers[server.Id] = new ViewerJson.JsonServer { Name = server.Name, Type = ServerTypes.ToJsonViewerString(server.Type), + IconUrl = server.IconUrl?.DownloadUrl, }; } diff --git a/app/Server/Database/Sqlite/Repositories/SqliteDownloadRepository.cs b/app/Server/Database/Sqlite/Repositories/SqliteDownloadRepository.cs index d6428fb..f79577a 100644 --- a/app/Server/Database/Sqlite/Repositories/SqliteDownloadRepository.cs +++ b/app/Server/Database/Sqlite/Repositories/SqliteDownloadRepository.cs @@ -394,16 +394,6 @@ sealed class SqliteDownloadRepository(SqliteConnectionPool pool) : BaseSqliteRep } } - await using (var cmd = conn.Command("SELECT id, avatar_url FROM users WHERE avatar_url IS NOT NULL")) { - await using var reader = await cmd.ExecuteReaderAsync(cancellationToken); - - while (await reader.ReadAsync(cancellationToken)) { - ulong id = reader.GetUint64(0); - string avatarHash = reader.GetString(1); - yield return DownloadLinkExtractor.UserAvatar(id, avatarHash); - } - } - await using (var cmd = conn.Command("SELECT DISTINCT emoji_id, emoji_flags FROM message_reactions WHERE emoji_id IS NOT NULL")) { await using var reader = await cmd.ExecuteReaderAsync(cancellationToken); @@ -413,5 +403,29 @@ sealed class SqliteDownloadRepository(SqliteConnectionPool pool) : BaseSqliteRep yield return DownloadLinkExtractor.Emoji(emojiId, emojiFlags); } } + + await using (var cmd = conn.Command("SELECT id, type, icon_hash FROM servers WHERE icon_hash IS NOT NULL")) { + await using var reader = await cmd.ExecuteReaderAsync(cancellationToken); + + while (await reader.ReadAsync(cancellationToken)) { + ulong id = reader.GetUint64(0); + ServerType? type = ServerTypes.FromString(reader.GetString(1)); + string iconHash = reader.GetString(2); + + if (DownloadLinkExtractor.ServerIcon(type, id, iconHash) is {} result) { + yield return result; + } + } + } + + await using (var cmd = conn.Command("SELECT id, avatar_url FROM users WHERE avatar_url IS NOT NULL")) { + await using var reader = await cmd.ExecuteReaderAsync(cancellationToken); + + while (await reader.ReadAsync(cancellationToken)) { + ulong id = reader.GetUint64(0); + string avatarHash = reader.GetString(1); + yield return DownloadLinkExtractor.UserAvatar(id, avatarHash); + } + } } } diff --git a/app/Server/Database/Sqlite/Repositories/SqliteServerRepository.cs b/app/Server/Database/Sqlite/Repositories/SqliteServerRepository.cs index a2673ce..2a7990c 100644 --- a/app/Server/Database/Sqlite/Repositories/SqliteServerRepository.cs +++ b/app/Server/Database/Sqlite/Repositories/SqliteServerRepository.cs @@ -10,15 +10,9 @@ using Microsoft.Data.Sqlite; namespace DHT.Server.Database.Sqlite.Repositories; -sealed class SqliteServerRepository : BaseSqliteRepository, IServerRepository { +sealed class SqliteServerRepository(SqliteConnectionPool pool, SqliteDownloadRepository downloads) : BaseSqliteRepository(Log), IServerRepository { private static readonly Log Log = Log.ForType(); - private readonly SqliteConnectionPool pool; - - public SqliteServerRepository(SqliteConnectionPool pool) : base(Log) { - this.pool = pool; - } - public async Task Add(IReadOnlyList servers) { await using (var conn = await pool.Take()) { await conn.BeginTransactionAsync(); @@ -27,13 +21,18 @@ sealed class SqliteServerRepository : BaseSqliteRepository, IServerRepository { ("id", SqliteType.Integer), ("name", SqliteType.Text), ("type", SqliteType.Text), + ("icon_hash", SqliteType.Text), ]); + await using var downloadCollector = new SqliteDownloadRepository.NewDownloadCollector(downloads, conn); + foreach (Data.Server server in servers) { cmd.Set(":id", server.Id); cmd.Set(":name", server.Name); cmd.Set(":type", ServerTypes.ToString(server.Type)); + cmd.Set(":icon_hash", server.IconHash); await cmd.ExecuteNonQueryAsync(); + await downloadCollector.AddIfNotNull(server.IconUrl?.ToPendingDownload()); } await conn.CommitTransactionAsync(); @@ -50,7 +49,7 @@ sealed class SqliteServerRepository : BaseSqliteRepository, IServerRepository { public async IAsyncEnumerable Get([EnumeratorCancellation] CancellationToken cancellationToken) { await using var conn = await pool.Take(); - await using var cmd = conn.Command("SELECT id, name, type FROM servers"); + await using var cmd = conn.Command("SELECT id, name, type, icon_hash FROM servers"); await using var reader = await cmd.ExecuteReaderAsync(cancellationToken); while (await reader.ReadAsync(cancellationToken)) { @@ -58,6 +57,7 @@ sealed class SqliteServerRepository : BaseSqliteRepository, IServerRepository { Id = reader.GetUint64(0), Name = reader.GetString(1), Type = ServerTypes.FromString(reader.GetString(2)), + IconHash = reader.IsDBNull(3) ? null : reader.GetString(3), }; } } diff --git a/app/Server/Database/Sqlite/Schema/SqliteSchemaUpgradeTo11.cs b/app/Server/Database/Sqlite/Schema/SqliteSchemaUpgradeTo11.cs new file mode 100644 index 0000000..0bd62d9 --- /dev/null +++ b/app/Server/Database/Sqlite/Schema/SqliteSchemaUpgradeTo11.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using DHT.Server.Database.Sqlite.Utils; + +namespace DHT.Server.Database.Sqlite.Schema; + +sealed class SqliteSchemaUpgradeTo11 : ISchemaUpgrade { + async Task ISchemaUpgrade.Run(ISqliteConnection conn, ISchemaUpgradeCallbacks.IProgressReporter reporter) { + await reporter.MainWork("Applying schema changes...", finishedItems: 0, totalItems: 1); + await conn.ExecuteAsync("ALTER TABLE servers ADD icon_hash TEXT"); + } +} diff --git a/app/Server/Database/Sqlite/SqliteDatabaseFile.cs b/app/Server/Database/Sqlite/SqliteDatabaseFile.cs index 0667e49..9fdece7 100644 --- a/app/Server/Database/Sqlite/SqliteDatabaseFile.cs +++ b/app/Server/Database/Sqlite/SqliteDatabaseFile.cs @@ -66,16 +66,16 @@ public sealed class SqliteDatabaseFile : IDatabaseFile { downloads = new SqliteDownloadRepository(pool); settings = new SqliteSettingsRepository(pool); users = new SqliteUserRepository(pool, downloads); - servers = new SqliteServerRepository(pool); + servers = new SqliteServerRepository(pool, downloads); channels = new SqliteChannelRepository(pool); messages = new SqliteMessageRepository(pool, downloads); } public async ValueTask DisposeAsync() { - users.Dispose(); - servers.Dispose(); - channels.Dispose(); messages.Dispose(); + channels.Dispose(); + servers.Dispose(); + users.Dispose(); downloads.Dispose(); await pool.DisposeAsync(); } diff --git a/app/Server/Database/Sqlite/SqliteSchema.cs b/app/Server/Database/Sqlite/SqliteSchema.cs index 559d315..1de7613 100644 --- a/app/Server/Database/Sqlite/SqliteSchema.cs +++ b/app/Server/Database/Sqlite/SqliteSchema.cs @@ -13,7 +13,7 @@ using Microsoft.Data.Sqlite; namespace DHT.Server.Database.Sqlite; sealed class SqliteSchema(CustomSqliteConnection conn) { - internal const int Version = 10; + internal const int Version = 11; private static readonly Log Log = Log.ForType(); @@ -92,9 +92,10 @@ sealed class SqliteSchema(CustomSqliteConnection conn) { await conn.ExecuteAsync(""" CREATE TABLE servers ( - id INTEGER PRIMARY KEY NOT NULL, - name TEXT NOT NULL, - type TEXT NOT NULL + id INTEGER PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + type TEXT NOT NULL, + icon_hash TEXT ) """); @@ -222,6 +223,7 @@ sealed class SqliteSchema(CustomSqliteConnection conn) { { 7, new SqliteSchemaUpgradeTo8() }, { 8, new SqliteSchemaUpgradeTo9() }, { 9, new SqliteSchemaUpgradeTo10() }, + { 10, new SqliteSchemaUpgradeTo11() }, }; Perf perf = Log.Start("from version " + dbVersion); diff --git a/app/Server/Download/DownloadLinkExtractor.cs b/app/Server/Download/DownloadLinkExtractor.cs index a44066c..d341f8e 100644 --- a/app/Server/Download/DownloadLinkExtractor.cs +++ b/app/Server/Download/DownloadLinkExtractor.cs @@ -12,6 +12,14 @@ namespace DHT.Server.Download; static class DownloadLinkExtractor { private static readonly Log Log = Log.ForType(typeof(DownloadLinkExtractor)); + public static FileUrl? ServerIcon(ServerType? type, ulong id, string iconHash) { + return type switch { + ServerType.Server => new FileUrl($"https://cdn.discordapp.com/icons/{id}/{iconHash}.webp", MediaTypeNames.Image.Webp), + ServerType.Group => new FileUrl($"https://cdn.discordapp.com/channel-icons/{id}/{iconHash}.webp", MediaTypeNames.Image.Webp), + _ => null, + }; + } + public static FileUrl UserAvatar(ulong id, string avatarHash) { return new FileUrl($"https://cdn.discordapp.com/avatars/{id}/{avatarHash}.webp", MediaTypeNames.Image.Webp); } diff --git a/app/Server/Endpoints/TrackChannelEndpoint.cs b/app/Server/Endpoints/TrackChannelEndpoint.cs index 176fb46..ab50bde 100644 --- a/app/Server/Endpoints/TrackChannelEndpoint.cs +++ b/app/Server/Endpoints/TrackChannelEndpoint.cs @@ -24,6 +24,7 @@ sealed class TrackChannelEndpoint(IDatabaseFile db) : BaseEndpoint { Id = json.RequireSnowflake("id", path), Name = json.RequireString("name", path), Type = ServerTypes.FromString(json.RequireString("type", path)) ?? throw new HttpException(HttpStatusCode.BadRequest, "Server type must be either 'SERVER', 'GROUP', or 'DM'."), + IconHash = json.HasKey("icon") ? json.RequireString("icon", path) : null, }; } diff --git a/app/empty.dht b/app/empty.dht index b95243a..5bce7af 100644 Binary files a/app/empty.dht and b/app/empty.dht differ