feat(api): add news to /api/meta response
This commit is contained in:
parent
31b6ac2cac
commit
5c57b75335
6 changed files with 147 additions and 5 deletions
|
|
@ -14,11 +14,43 @@
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
using Catalogger.Backend.Database.Redis;
|
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;
|
namespace Catalogger.Backend.Api;
|
||||||
|
|
||||||
public class ApiCache(RedisService redisService)
|
public class ApiCache(RedisService redisService, IDiscordRestChannelAPI channelApi, Config config)
|
||||||
{
|
{
|
||||||
|
private List<IMessage>? _news;
|
||||||
|
private readonly SemaphoreSlim _newsSemaphore = new(1);
|
||||||
|
|
||||||
|
public async Task<List<IMessage>> 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 UserKey(string id) => $"api-user:{id}";
|
||||||
|
|
||||||
private static string GuildsKey(string userId) => $"api-user-guilds:{userId}";
|
private static string GuildsKey(string userId) => $"api-user-guilds:{userId}";
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ namespace Catalogger.Backend.Api;
|
||||||
|
|
||||||
[Route("/api/guilds/{id}")]
|
[Route("/api/guilds/{id}")]
|
||||||
public partial class GuildsController(
|
public partial class GuildsController(
|
||||||
Config config,
|
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
DatabaseContext db,
|
DatabaseContext db,
|
||||||
ChannelCache channelCache,
|
ChannelCache channelCache,
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,10 @@
|
||||||
|
|
||||||
using Catalogger.Backend.Api.Middleware;
|
using Catalogger.Backend.Api.Middleware;
|
||||||
using Catalogger.Backend.Cache.InMemoryCache;
|
using Catalogger.Backend.Cache.InMemoryCache;
|
||||||
|
using Catalogger.Backend.Extensions;
|
||||||
|
using Catalogger.Backend.Services;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Remora.Discord.API.Abstractions.Objects;
|
||||||
|
|
||||||
namespace Catalogger.Backend.Api;
|
namespace Catalogger.Backend.Api;
|
||||||
|
|
||||||
|
|
@ -23,20 +26,24 @@ namespace Catalogger.Backend.Api;
|
||||||
public class MetaController(
|
public class MetaController(
|
||||||
Config config,
|
Config config,
|
||||||
GuildCache guildCache,
|
GuildCache guildCache,
|
||||||
|
NewsService newsService,
|
||||||
DiscordRequestService discordRequestService
|
DiscordRequestService discordRequestService
|
||||||
) : ApiControllerBase
|
) : ApiControllerBase
|
||||||
{
|
{
|
||||||
[HttpGet("meta")]
|
[HttpGet("meta")]
|
||||||
public IActionResult GetMeta()
|
public async Task<IActionResult> GetMetaAsync()
|
||||||
{
|
{
|
||||||
var inviteUrl =
|
var inviteUrl =
|
||||||
$"https://discord.com/oauth2/authorize?client_id={config.Discord.ApplicationId}"
|
$"https://discord.com/oauth2/authorize?client_id={config.Discord.ApplicationId}"
|
||||||
+ "&permissions=537250993&scope=bot+applications.commands";
|
+ "&permissions=537250993&scope=bot+applications.commands";
|
||||||
|
|
||||||
|
var news = await newsService.GetNewsAsync();
|
||||||
|
|
||||||
return Ok(
|
return Ok(
|
||||||
new MetaResponse(
|
new MetaResponse(
|
||||||
Guilds: (int)CataloggerMetrics.GuildsCached.Value,
|
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<NewsService.NewsMessage> News
|
||||||
|
);
|
||||||
|
|
||||||
private record CurrentUserResponse(ApiUser User, IEnumerable<ApiGuild> Guilds);
|
private record CurrentUserResponse(ApiUser User, IEnumerable<ApiGuild> Guilds);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,5 +62,7 @@ public class Config
|
||||||
public int Port { get; init; } = 5000;
|
public int Port { get; init; } = 5000;
|
||||||
public string BaseUrl { get; init; } = null!;
|
public string BaseUrl { get; init; } = null!;
|
||||||
public string Address => $"http://{Host}:{Port}";
|
public string Address => $"http://{Host}:{Port}";
|
||||||
|
|
||||||
|
public ulong? NewsChannel { get; init; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,7 @@ public static class StartupExtensions
|
||||||
.AddSingleton<AuditLogCache>()
|
.AddSingleton<AuditLogCache>()
|
||||||
.AddSingleton<EmojiCache>()
|
.AddSingleton<EmojiCache>()
|
||||||
.AddSingleton<PluralkitApiService>()
|
.AddSingleton<PluralkitApiService>()
|
||||||
|
.AddSingleton<NewsService>()
|
||||||
.AddScoped<IEncryptionService, EncryptionService>()
|
.AddScoped<IEncryptionService, EncryptionService>()
|
||||||
.AddSingleton<MetricsCollectionService>()
|
.AddSingleton<MetricsCollectionService>()
|
||||||
.AddScoped<MessageRepository>()
|
.AddScoped<MessageRepository>()
|
||||||
|
|
|
||||||
97
Catalogger.Backend/Services/NewsService.cs
Normal file
97
Catalogger.Backend/Services/NewsService.cs
Normal file
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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<NewsService>();
|
||||||
|
private List<IMessage>? _messages;
|
||||||
|
private readonly SemaphoreSlim _lock = new(1);
|
||||||
|
private bool _isExpired => clock.GetCurrentInstant() > clock.GetCurrentInstant() + ExpiresAfter;
|
||||||
|
|
||||||
|
public async Task<IEnumerable<NewsMessage>> 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<List<IMessage>> 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<string> 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
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue