feat: add remote config over http/redis

This commit is contained in:
alyssa 2025-03-10 15:12:56 +00:00
parent c4db95796d
commit a72afb35a0
12 changed files with 326 additions and 4 deletions

View file

@ -24,6 +24,8 @@ public class BotConfig
public string? HttpCacheUrl { get; set; }
public bool HttpUseInnerCache { get; set; } = false;
public string? HttpListenerAddr { get; set; }
public string? DiscordBaseUrl { get; set; }
public string? AvatarServiceUrl { get; set; }

View file

@ -76,6 +76,13 @@ public class Init
// Init the bot instance itself, register handlers and such to the client before beginning to connect
bot.Init();
// load runtime config from redis
await services.Resolve<RuntimeConfigService>().LoadConfig();
// Start HTTP server
if (config.HttpListenerAddr != null)
services.Resolve<HttpListenerService>().Start(config.HttpListenerAddr);
// Start the Discord shards themselves (handlers already set up)
logger.Information("Connecting to Discord");
await StartCluster(services);

View file

@ -153,6 +153,8 @@ public class BotModule: Module
builder.RegisterType<CommandMessageService>().AsSelf().SingleInstance();
builder.RegisterType<InteractionDispatchService>().AsSelf().SingleInstance();
builder.RegisterType<AvatarHostingService>().AsSelf().SingleInstance();
builder.RegisterType<HttpListenerService>().AsSelf().SingleInstance();
builder.RegisterType<RuntimeConfigService>().AsSelf().SingleInstance();
// Sentry stuff
builder.Register(_ => new Scope(null)).AsSelf().InstancePerLifetimeScope();

View file

@ -24,5 +24,6 @@
<ItemGroup>
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
<PackageReference Include="Sentry" Version="4.13.0" />
<PackageReference Include="Watson.Lite" Version="6.3.5" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,56 @@
using Serilog;
using Newtonsoft.Json;
using WatsonWebserver.Lite;
using WatsonWebserver.Core;
namespace PluralKit.Bot;
public class HttpListenerService
{
private readonly ILogger _logger;
private readonly RuntimeConfigService _runtimeConfig;
public HttpListenerService(ILogger logger, RuntimeConfigService runtimeConfig)
{
_logger = logger.ForContext<HttpListenerService>();
_runtimeConfig = runtimeConfig;
}
public void Start(string host)
{
var server = new WebserverLite(new WebserverSettings(host, 5002), DefaultRoute);
server.Routes.PreAuthentication.Static.Add(WatsonWebserver.Core.HttpMethod.GET, "/runtime_config", RuntimeConfigGet);
server.Routes.PreAuthentication.Parameter.Add(WatsonWebserver.Core.HttpMethod.POST, "/runtime_config/{key}", RuntimeConfigSet);
server.Routes.PreAuthentication.Parameter.Add(WatsonWebserver.Core.HttpMethod.DELETE, "/runtime_config/{key}", RuntimeConfigDelete);
server.Start();
}
private async Task DefaultRoute(HttpContextBase ctx)
=> await ctx.Response.Send("hellorld");
private async Task RuntimeConfigGet(HttpContextBase ctx)
{
var config = _runtimeConfig.GetAll();
ctx.Response.Headers.Add("content-type", "application/json");
await ctx.Response.Send(JsonConvert.SerializeObject(config));
}
private async Task RuntimeConfigSet(HttpContextBase ctx)
{
var key = ctx.Request.Url.Parameters["key"];
var value = ctx.Request.DataAsString;
await _runtimeConfig.Set(key, value);
await RuntimeConfigGet(ctx);
}
private async Task RuntimeConfigDelete(HttpContextBase ctx)
{
var key = ctx.Request.Url.Parameters["key"];
await _runtimeConfig.Delete(key);
await RuntimeConfigGet(ctx);
}
}

View file

@ -0,0 +1,58 @@
using Newtonsoft.Json;
using Serilog;
using StackExchange.Redis;
using PluralKit.Core;
namespace PluralKit.Bot;
public class RuntimeConfigService
{
private readonly RedisService _redis;
private readonly ILogger _logger;
private Dictionary<string, string> settings = new();
private string RedisKey;
public RuntimeConfigService(ILogger logger, RedisService redis, BotConfig config)
{
_logger = logger.ForContext<RuntimeConfigService>();
_redis = redis;
var clusterId = config.Cluster?.NodeIndex ?? 0;
RedisKey = $"remote_config:dotnet_bot:{clusterId}";
}
public async Task LoadConfig()
{
var redisConfig = await _redis.Connection.GetDatabase().HashGetAllAsync(RedisKey);
foreach (var entry in redisConfig)
settings.Add(entry.Name, entry.Value);
var configStr = JsonConvert.SerializeObject(settings);
_logger.Information($"starting with runtime config: {configStr}");
}
public async Task Set(string key, string value)
{
await _redis.Connection.GetDatabase().HashSetAsync(RedisKey, new[] { new HashEntry(key, new RedisValue(value)) });
settings.Add(key, value);
_logger.Information($"updated runtime config: {key}={value}");
}
public async Task Delete(string key)
{
await _redis.Connection.GetDatabase().HashDeleteAsync(RedisKey, key);
settings.Remove(key);
_logger.Information($"updated runtime config: {key} removed");
}
public object? Get(string key) => settings.GetValueOrDefault(key);
public bool Exists(string key) => settings.ContainsKey(key);
public Dictionary<string, string> GetAll() => settings;
}

View file

@ -14,6 +14,16 @@
"resolved": "4.13.0",
"contentHash": "Wfw3M1WpFcrYaGzPm7QyUTfIOYkVXQ1ry6p4WYjhbLz9fPwV23SGQZTFDpdox67NHM0V0g1aoQ4YKLm4ANtEEg=="
},
"Watson.Lite": {
"type": "Direct",
"requested": "[6.3.5, )",
"resolved": "6.3.5",
"contentHash": "YF8+se3IVenn8YlyNeb4wSJK6QMnVD0QHIOEiZ22wS4K2wkwoSDzWS+ZAjk1MaPeB+XO5gRoENUN//pOc+wI2g==",
"dependencies": {
"CavemanTcp": "2.0.5",
"Watson.Core": "6.3.5"
}
},
"App.Metrics": {
"type": "Transitive",
"resolved": "4.3.0",
@ -107,6 +117,11 @@
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1"
}
},
"CavemanTcp": {
"type": "Transitive",
"resolved": "2.0.5",
"contentHash": "90wywmGpjrj26HMAkufYZwuZI8sVYB1mRwEdqugSR3kgDnPX+3l0jO86gwtFKsPvsEpsS4Dn/1EbhguzUxMU8Q=="
},
"Dapper": {
"type": "Transitive",
"resolved": "2.1.35",
@ -130,6 +145,11 @@
"System.Diagnostics.DiagnosticSource": "5.0.0"
}
},
"IpMatcher": {
"type": "Transitive",
"resolved": "1.0.5",
"contentHash": "WXNlWERj+0GN699AnMNsuJ7PfUAbU4xhOHP3nrNXLHqbOaBxybu25luSYywX1133NSlitA4YkSNmJuyPvea4sw=="
},
"IPNetwork2": {
"type": "Transitive",
"resolved": "3.0.667",
@ -391,6 +411,11 @@
"resolved": "8.5.0",
"contentHash": "VYYMZNitZ85UEhwOKkTQI63WEMvzUqwQc74I2mm8h/DBVAMcBBxqYPni4DmuRtbCwngmuONuK2yBJfWNRKzI+A=="
},
"RegexMatcher": {
"type": "Transitive",
"resolved": "1.0.9",
"contentHash": "RkQGXIrqHjD5h1mqefhgCbkaSdRYNRG5rrbzyw5zeLWiS0K1wq9xR3cNhQdzYR2MsKZ3GN523yRUsEQIMPxh3Q=="
},
"Serilog": {
"type": "Transitive",
"resolved": "4.2.0",
@ -714,6 +739,28 @@
"System.Runtime": "4.3.0"
}
},
"Timestamps": {
"type": "Transitive",
"resolved": "1.0.11",
"contentHash": "SnWhXm3FkEStQGgUTfWMh9mKItNW032o/v8eAtFrOGqG0/ejvPPA1LdLZx0N/qqoY0TH3x11+dO00jeVcM8xNQ=="
},
"UrlMatcher": {
"type": "Transitive",
"resolved": "3.0.1",
"contentHash": "hHBZVzFSfikrx4XsRsnCIwmGLgbNKtntnlqf4z+ygcNA6Y/L/J0x5GiZZWfXdTfpxhy5v7mlt2zrZs/L9SvbOA=="
},
"Watson.Core": {
"type": "Transitive",
"resolved": "6.3.5",
"contentHash": "Y5YxKOCSLe2KDmfwvI/J0qApgmmZR77LwyoufRVfKH7GLdHiE7fY0IfoNxWTG7nNv8knBfgwyOxdehRm+4HaCg==",
"dependencies": {
"IpMatcher": "1.0.5",
"RegexMatcher": "1.0.9",
"System.Text.Json": "8.0.5",
"Timestamps": "1.0.11",
"UrlMatcher": "3.0.1"
}
},
"myriad": {
"type": "Project",
"dependencies": {