diff --git a/Catalogger.Backend/Api/ApiCache.cs b/Catalogger.Backend/Api/ApiCache.cs
index c4161ad..b790643 100644
--- a/Catalogger.Backend/Api/ApiCache.cs
+++ b/Catalogger.Backend/Api/ApiCache.cs
@@ -14,11 +14,43 @@
// along with this program. If not, see .
using Catalogger.Backend.Database.Redis;
+using Remora.Discord.API;
+using Remora.Discord.API.Abstractions.Objects;
+using Remora.Discord.API.Abstractions.Rest;
namespace Catalogger.Backend.Api;
-public class ApiCache(RedisService redisService)
+public class ApiCache(RedisService redisService, IDiscordRestChannelAPI channelApi, Config config)
{
+ private List? _news;
+ private readonly SemaphoreSlim _newsSemaphore = new(1);
+
+ public async Task> GetNewsAsync()
+ {
+ await _newsSemaphore.WaitAsync();
+ try
+ {
+ if (_news != null)
+ return _news;
+
+ if (config.Web.NewsChannel == null)
+ return [];
+
+ var res = await channelApi.GetChannelMessagesAsync(
+ DiscordSnowflake.New(config.Web.NewsChannel.Value),
+ limit: 5
+ );
+ if (res.IsSuccess)
+ return _news = res.Entity.ToList();
+
+ return [];
+ }
+ finally
+ {
+ _newsSemaphore.Release();
+ }
+ }
+
private static string UserKey(string id) => $"api-user:{id}";
private static string GuildsKey(string userId) => $"api-user-guilds:{userId}";
diff --git a/Catalogger.Backend/Api/GuildsController.cs b/Catalogger.Backend/Api/GuildsController.cs
index 11acc42..f7210a7 100644
--- a/Catalogger.Backend/Api/GuildsController.cs
+++ b/Catalogger.Backend/Api/GuildsController.cs
@@ -31,7 +31,6 @@ namespace Catalogger.Backend.Api;
[Route("/api/guilds/{id}")]
public partial class GuildsController(
- Config config,
ILogger logger,
DatabaseContext db,
ChannelCache channelCache,
diff --git a/Catalogger.Backend/Api/MetaController.cs b/Catalogger.Backend/Api/MetaController.cs
index f5d9fe6..533bc6d 100644
--- a/Catalogger.Backend/Api/MetaController.cs
+++ b/Catalogger.Backend/Api/MetaController.cs
@@ -15,7 +15,10 @@
using Catalogger.Backend.Api.Middleware;
using Catalogger.Backend.Cache.InMemoryCache;
+using Catalogger.Backend.Extensions;
+using Catalogger.Backend.Services;
using Microsoft.AspNetCore.Mvc;
+using Remora.Discord.API.Abstractions.Objects;
namespace Catalogger.Backend.Api;
@@ -23,20 +26,24 @@ namespace Catalogger.Backend.Api;
public class MetaController(
Config config,
GuildCache guildCache,
+ NewsService newsService,
DiscordRequestService discordRequestService
) : ApiControllerBase
{
[HttpGet("meta")]
- public IActionResult GetMeta()
+ public async Task GetMetaAsync()
{
var inviteUrl =
$"https://discord.com/oauth2/authorize?client_id={config.Discord.ApplicationId}"
+ "&permissions=537250993&scope=bot+applications.commands";
+ var news = await newsService.GetNewsAsync();
+
return Ok(
new MetaResponse(
Guilds: (int)CataloggerMetrics.GuildsCached.Value,
- InviteUrl: inviteUrl
+ InviteUrl: inviteUrl,
+ News: news
)
);
}
@@ -60,7 +67,11 @@ public class MetaController(
);
}
- private record MetaResponse(int Guilds, string InviteUrl);
+ private record MetaResponse(
+ int Guilds,
+ string InviteUrl,
+ IEnumerable News
+ );
private record CurrentUserResponse(ApiUser User, IEnumerable Guilds);
}
diff --git a/Catalogger.Backend/Config.cs b/Catalogger.Backend/Config.cs
index d3489bf..e8badc9 100644
--- a/Catalogger.Backend/Config.cs
+++ b/Catalogger.Backend/Config.cs
@@ -62,5 +62,7 @@ public class Config
public int Port { get; init; } = 5000;
public string BaseUrl { get; init; } = null!;
public string Address => $"http://{Host}:{Port}";
+
+ public ulong? NewsChannel { get; init; }
}
}
diff --git a/Catalogger.Backend/Extensions/StartupExtensions.cs b/Catalogger.Backend/Extensions/StartupExtensions.cs
index 46cf25b..95c7626 100644
--- a/Catalogger.Backend/Extensions/StartupExtensions.cs
+++ b/Catalogger.Backend/Extensions/StartupExtensions.cs
@@ -110,6 +110,7 @@ public static class StartupExtensions
.AddSingleton()
.AddSingleton()
.AddSingleton()
+ .AddSingleton()
.AddScoped()
.AddSingleton()
.AddScoped()
diff --git a/Catalogger.Backend/Services/NewsService.cs b/Catalogger.Backend/Services/NewsService.cs
new file mode 100644
index 0000000..e52c783
--- /dev/null
+++ b/Catalogger.Backend/Services/NewsService.cs
@@ -0,0 +1,97 @@
+// 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 Catalogger.Backend.Extensions;
+using NodaTime;
+using NodaTime.Extensions;
+using Remora.Discord.API;
+using Remora.Discord.API.Abstractions.Objects;
+using Remora.Discord.API.Abstractions.Rest;
+
+namespace Catalogger.Backend.Services;
+
+public class NewsService(
+ Config config,
+ ILogger logger,
+ IDiscordRestChannelAPI channelApi,
+ IClock clock
+)
+{
+ private static readonly Duration MaxNewsAge = Duration.FromDays(90); // 3 months
+ private static readonly Duration ExpiresAfter = Duration.FromHours(1);
+
+ private readonly ILogger _logger = logger.ForContext();
+ private List? _messages;
+ private readonly SemaphoreSlim _lock = new(1);
+ private bool _isExpired => clock.GetCurrentInstant() > clock.GetCurrentInstant() + ExpiresAfter;
+
+ public async Task> GetNewsAsync()
+ {
+ if (_messages != null && !_isExpired)
+ return _messages.Select(FormatIMessage);
+
+ var messages = await GetRawNewsAsync();
+ return messages.Select(FormatIMessage);
+ }
+
+ public void AddMessage(IMessage message) =>
+ _messages = _messages?.Take(4).Prepend(message).ToList();
+
+ private async Task> GetRawNewsAsync()
+ {
+ await _lock.WaitAsync();
+ try
+ {
+ if (config.Web.NewsChannel == null)
+ return [];
+
+ _logger.Information("Fetching news from channel {ChannelId}", config.Web.NewsChannel);
+
+ var res = await channelApi.GetChannelMessagesAsync(
+ DiscordSnowflake.New(config.Web.NewsChannel.Value),
+ limit: 5
+ );
+ if (res.IsSuccess)
+ return _messages = res
+ .Entity.Where(m =>
+ m.ID.Timestamp.ToInstant() > clock.GetCurrentInstant() - MaxNewsAge
+ )
+ .ToList();
+
+ return [];
+ }
+ finally
+ {
+ _lock.Release();
+ }
+ }
+
+ public record NewsMessage(
+ string Author,
+ string Content,
+ IEnumerable AttachmentUrls,
+ DateTimeOffset PostedAt,
+ DateTimeOffset? EditedAt
+ );
+
+ private NewsMessage FormatIMessage(IMessage msg) =>
+ new(
+ msg.Author.Tag(),
+ msg.Content,
+ msg.Attachments.Select(a => a.Url),
+ msg.ID.Timestamp,
+ msg.EditedTimestamp
+ );
+}