diff --git a/JetHerald/ChatCommandRouter.cs b/JetHerald/ChatCommandRouter.cs index 7c5dca7..b1a2bb2 100644 --- a/JetHerald/ChatCommandRouter.cs +++ b/JetHerald/ChatCommandRouter.cs @@ -1,66 +1,60 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Telegram.Bot.Args; +using Telegram.Bot.Types; -namespace JetHerald +namespace JetHerald; +public interface IChatCommand { - public interface IChatCommand + Task Execute(CommandString cmd, Update update); +} + +public class ChatCommandRouter +{ + string Username { get; } + ILogger Log { get; } + + Dictionary Commands { get; } + + public ChatCommandRouter(string username, ILogger log) { - Task Execute(CommandString cmd, MessageEventArgs messageEventArgs); + Log = log; + Username = username; + Commands = new Dictionary(); } - public class ChatCommandRouter + public async Task Execute(object sender, Update update) { - string Username { get; } - ILogger Log { get; } - - Dictionary Commands { get; } - - public ChatCommandRouter(string username, ILogger log) + var text = update.Message.Text; + if (CommandString.TryParse(text, out var cmd)) { - Log = log; - Username = username; - Commands = new Dictionary(); - } - - public async Task Execute(object sender, MessageEventArgs args) - { - var text = args.Message.Text; - if (CommandString.TryParse(text, out var cmd)) + if (cmd.Username != null && cmd.Username != Username) { - if (cmd.Username != null && cmd.Username != Username) - { - Log.LogDebug("Message not directed at us"); - return null; - } - if (Commands.ContainsKey(cmd.Command)) - { - try - { - Log.LogDebug($"Handling message via {Commands[cmd.Command].GetType().Name}"); - return await Commands[cmd.Command].Execute(cmd, args); - } - catch (Exception e) - { - Log.LogError(e, $"Error while executing command {cmd.Command}!"); - } - } - else - Log.LogDebug($"Command {cmd.Command} not found"); + Log.LogDebug("Message not directed at us"); + return null; } - return null; - } - - public void Add(IChatCommand c, params string[] cmds) - { - foreach (var cmd in cmds) + if (Commands.ContainsKey(cmd.Command)) { - if (Commands.ContainsKey(cmd)) - throw new ArgumentException($"collision for {cmd}, commands {Commands[cmd].GetType()} and {c.GetType()}"); - Commands[cmd] = c; + try + { + Log.LogDebug($"Handling message via {Commands[cmd.Command].GetType().Name}"); + return await Commands[cmd.Command].Execute(cmd, update); + } + catch (Exception e) + { + Log.LogError(e, $"Error while executing command {cmd.Command}!"); + } } + else + Log.LogDebug($"Command {cmd.Command} not found"); + } + return null; + } + + public void Add(IChatCommand c, params string[] cmds) + { + foreach (var cmd in cmds) + { + if (Commands.ContainsKey(cmd)) + throw new ArgumentException($"collision for {cmd}, commands {Commands[cmd].GetType()} and {c.GetType()}"); + Commands[cmd] = c; } } } diff --git a/JetHerald/CommandString.cs b/JetHerald/CommandString.cs index df79f6f..ed281da 100644 --- a/JetHerald/CommandString.cs +++ b/JetHerald/CommandString.cs @@ -1,43 +1,39 @@ -using System; -using System.Linq; -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; -namespace JetHerald +namespace JetHerald; +public class CommandString { - public class CommandString + public CommandString(string command, string username, params string[] parameters) { - public CommandString(string command, string username, params string[] parameters) - { - Command = command; - Username = username; - Parameters = parameters; - } + Command = command; + Username = username; + Parameters = parameters; + } - public string Command { get; } - public string Username { get; } - public string[] Parameters { get; } + public string Command { get; } + public string Username { get; } + public string[] Parameters { get; } - static readonly char[] WS_CHARS = new[] { ' ', '\r', '\n', '\n' }; + static readonly char[] WS_CHARS = new[] { ' ', '\r', '\n', '\n' }; - public static bool TryParse(string s, out CommandString result) - { - result = null; - if (string.IsNullOrWhiteSpace(s) || s[0] != '/') - return false; + public static bool TryParse(string s, out CommandString result) + { + result = null; + if (string.IsNullOrWhiteSpace(s) || s[0] != '/') + return false; - string[] words = s.Split(WS_CHARS, StringSplitOptions.RemoveEmptyEntries); + string[] words = s.Split(WS_CHARS, StringSplitOptions.RemoveEmptyEntries); - var cmdRegex = new Regex(@"/(?\w+)(@(?\w+))?"); - var match = cmdRegex.Match(words.First()); - if (!match.Success) - return false; + var cmdRegex = new Regex(@"/(?\w+)(@(?\w+))?"); + var match = cmdRegex.Match(words.First()); + if (!match.Success) + return false; - string cmd = match.Groups["cmd"].Captures[0].Value; - string username = match.Groups["name"].Captures.Count > 0 ? match.Groups["name"].Captures[0].Value : null; - string[] parameters = words.Skip(1).ToArray(); + string cmd = match.Groups["cmd"].Captures[0].Value; + string username = match.Groups["name"].Captures.Count > 0 ? match.Groups["name"].Captures[0].Value : null; + string[] parameters = words.Skip(1).ToArray(); - result = new CommandString(cmd, username, parameters); - return true; - } + result = new CommandString(cmd, username, parameters); + return true; } } diff --git a/JetHerald/Commands/CommandHelper.cs b/JetHerald/Commands/CommandHelper.cs index df8445a..0c49d91 100644 --- a/JetHerald/Commands/CommandHelper.cs +++ b/JetHerald/Commands/CommandHelper.cs @@ -1,20 +1,17 @@ -using System.Threading.Tasks; using Telegram.Bot; using Telegram.Bot.Types; using Telegram.Bot.Types.Enums; -namespace JetHerald.Commands +namespace JetHerald.Commands; +public static class CommandHelper { - public static class CommandHelper + public static async Task CheckAdministrator(TelegramBotClient bot, Message msg) { - public static async Task CheckAdministrator(TelegramBotClient bot, Message msg) + if (msg.Chat.Type != ChatType.Private) { - if (msg.Chat.Type != ChatType.Private) - { - var chatMember = await bot.GetChatMemberAsync(msg.Chat.Id, msg.From.Id); - return chatMember.Status is ChatMemberStatus.Administrator or ChatMemberStatus.Creator; - } - return true; + var chatMember = await bot.GetChatMemberAsync(msg.Chat.Id, msg.From.Id); + return chatMember.Status is ChatMemberStatus.Administrator or ChatMemberStatus.Creator; } + return true; } -} \ No newline at end of file +} diff --git a/JetHerald/Commands/CreateTopicCommand.cs b/JetHerald/Commands/CreateTopicCommand.cs index 4753366..351f43a 100644 --- a/JetHerald/Commands/CreateTopicCommand.cs +++ b/JetHerald/Commands/CreateTopicCommand.cs @@ -1,46 +1,44 @@ -using System.Linq; -using System.Threading.Tasks; -using MySql.Data.MySqlClient; -using Telegram.Bot.Args; +using MySql.Data.MySqlClient; +using Telegram.Bot.Types; using Telegram.Bot.Types.Enums; +using JetHerald.Services; -namespace JetHerald.Commands +namespace JetHerald.Commands; +public class CreateTopicCommand : IChatCommand { - public class CreateTopicCommand : IChatCommand + Db Db { get; } + + public CreateTopicCommand(Db db) { - readonly Db db; + Db = db; + } - public CreateTopicCommand(Db db) + public async Task Execute(CommandString cmd, Update update) + { + if (cmd.Parameters.Length < 1) + return null; + var msg = update.Message; + + if (msg.Chat.Type != ChatType.Private) + return null; + + string name = cmd.Parameters[0]; + string descr = name; + if (cmd.Parameters.Length > 1) + descr = string.Join(' ', cmd.Parameters.Skip(1)); + + try { - this.db = db; + var topic = await Db.CreateTopic(NamespacedId.Telegram(msg.From.Id), name, descr); + return $"created {topic.Name}\n" + + $"readToken\n{topic.ReadToken}\n" + + $"writeToken\n{topic.WriteToken}\n" + + $"adminToken\n{topic.AdminToken}\n"; } - - public async Task Execute(CommandString cmd, MessageEventArgs messageEventArgs) + catch (MySqlException myDuplicate) when (myDuplicate.Number == 1062) { - if (cmd.Parameters.Length < 1) - return null; - var msg = messageEventArgs.Message; - - if (msg.Chat.Type != ChatType.Private) - return null; - - string name = cmd.Parameters[0]; - string descr = name; - if (cmd.Parameters.Length > 1) - descr = string.Join(' ', cmd.Parameters.Skip(1)); - - try - { - var topic = await db.CreateTopic(NamespacedId.Telegram(msg.From.Id), name, descr); - return $"created {topic.Name}\n" + - $"readToken\n{topic.ReadToken}\n" + - $"writeToken\n{topic.WriteToken}\n" + - $"adminToken\n{topic.AdminToken}\n"; - } - catch (MySqlException myDuplicate) when (myDuplicate.Number == 1062) - { - return $"topic {name} already exists"; - } + return $"topic {name} already exists"; } } } + diff --git a/JetHerald/Commands/DeleteTopicCommand.cs b/JetHerald/Commands/DeleteTopicCommand.cs index 366112d..60e8693 100644 --- a/JetHerald/Commands/DeleteTopicCommand.cs +++ b/JetHerald/Commands/DeleteTopicCommand.cs @@ -1,35 +1,34 @@ -using System.Threading.Tasks; -using Telegram.Bot.Args; +using Telegram.Bot.Types; using Telegram.Bot.Types.Enums; +using JetHerald.Services; -namespace JetHerald.Commands +namespace JetHerald.Commands; +public class DeleteTopicCommand : IChatCommand { - public class DeleteTopicCommand : IChatCommand + Db Db { get; } + + public DeleteTopicCommand(Db db) { - readonly Db db; + Db = db; + } - public DeleteTopicCommand(Db db) - { - this.db = db; - } + public async Task Execute(CommandString cmd, Update update) + { + if (cmd.Parameters.Length < 2) + return null; + var msg = update.Message; - public async Task Execute(CommandString cmd, MessageEventArgs messageEventArgs) - { - if (cmd.Parameters.Length < 2) - return null; - var msg = messageEventArgs.Message; + if (msg.Chat.Type != ChatType.Private) + return null; - if (msg.Chat.Type != ChatType.Private) - return null; + string name = cmd.Parameters[0]; + string adminToken = cmd.Parameters[1]; - string name = cmd.Parameters[0]; - string adminToken = cmd.Parameters[1]; - - var changed = await db.DeleteTopic(name, adminToken); - if (changed > 0) - return ($"deleted {name} and all its subscriptions"); - else - return ($"invalid topic name or admin token"); - } + var changed = await Db.DeleteTopic(name, adminToken); + if (changed > 0) + return ($"deleted {name} and all its subscriptions"); + else + return ($"invalid topic name or admin token"); } } + diff --git a/JetHerald/Commands/DiscordCommands.cs b/JetHerald/Commands/DiscordCommands.cs deleted file mode 100644 index 42951f4..0000000 --- a/JetHerald/Commands/DiscordCommands.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System.Linq; -using System.Threading.Tasks; -using DSharpPlus.CommandsNext; -using DSharpPlus.CommandsNext.Attributes; -using MySql.Data.MySqlClient; - -namespace JetHerald.Commands -{ - [ModuleLifespan(ModuleLifespan.Transient)] - public class DiscordCommands : BaseCommandModule - { - public Db Db { get; set; } - - [Command("createtopic")] - [Description("Creates a topic.")] - [RequireDirectMessage] - public async Task CreateTopic( - CommandContext ctx, - [Description("The unique name of the new topic.")] - string name, - [RemainingText, Description("The name displayed in service messages. Defaults to `name`")] - string description = null) - { - if (description == null) - description = name; - - _ = ctx.TriggerTypingAsync(); - - try - { - var topic = await Db.CreateTopic(NamespacedId.Discord(ctx.User.Id), name, description); - await ctx.RespondAsync($"created {topic.Name}\n" + - $"readToken\n{topic.ReadToken}\n" + - $"writeToken\n{topic.WriteToken}\n" + - $"adminToken\n{topic.AdminToken}\n"); - } - catch (MySqlException myDuplicate) when (myDuplicate.Number == 1062) - { - await ctx.RespondAsync($"topic {name} already exists"); - } - } - - [Command("deletetopic")] - [Description("Deletes a topic.")] - [RequireDirectMessage] - public async Task DeleteTopic( - CommandContext ctx, - [Description("The name of the topic to be deleted.")] - string name, - [Description("The admin token of the topic to be deleted.")] - string adminToken) - { - _ = ctx.TriggerTypingAsync(); - var changed = await Db.DeleteTopic(name, adminToken); - if (changed > 0) - await ctx.RespondAsync($"deleted {name} and all its subscriptions"); - else - await ctx.RespondAsync($"invalid topic name or admin token"); - } - - [Command("list")] - [Description("List all subscriptions in this channel.")] - [RequireUserPermissions(DSharpPlus.Permissions.ManageGuild)] - public async Task ListSubscriptions(CommandContext ctx) - { - _ = ctx.TriggerTypingAsync(); - - var topics = await Db.GetTopicsForChat(NamespacedId.Discord(ctx.Channel.Id)); - - await ctx.RespondAsync(topics.Any() - ? "Topics:\n" + string.Join("\n", topics) - : "No subscriptions active."); - } - - [Command("subscribe")] - [Description("Subscribes to a topic.")] - [RequireUserPermissions(DSharpPlus.Permissions.ManageGuild)] - public async Task Subscribe( - CommandContext ctx, - [Description("The read token of the token to subscribe to.")] - string token - ) - { - _ = ctx.TriggerTypingAsync(); - - var chat = NamespacedId.Discord(ctx.Channel.Id); - var topic = await Db.GetTopicForSub(token, chat); - - if (topic == null) - await ctx.RespondAsync("topic not found"); - else if (topic.Chat.HasValue && topic.Chat.Value == chat) - await ctx.RespondAsync($"already subscribed to {topic.Name}"); - else if (topic.ReadToken != token) - await ctx.RespondAsync("token mismatch"); - else - { - await Db.CreateSubscription(topic.TopicId, chat); - await ctx.RespondAsync($"subscribed to {topic.Name}"); - } - } - - [Command("unsubscribe")] - [Description("Unsubscribes from a topic.")] - [RequireUserPermissions(DSharpPlus.Permissions.ManageGuild)] - public async Task Unsubscribe( - CommandContext ctx, - [Description("The name of the topic to unsubscribe from.")] - string name - ) - { - _ = ctx.TriggerTypingAsync(); - - int affected = await Db.RemoveSubscription(name, NamespacedId.Discord(ctx.Channel.Id)); - if (affected >= 1) - await ctx.RespondAsync($"unsubscribed from {name}"); - else - await ctx.RespondAsync($"could not find subscription for {name}"); - } - } -} \ No newline at end of file diff --git a/JetHerald/Commands/ListCommand.cs b/JetHerald/Commands/ListCommand.cs index 810abb6..5bcfdf1 100644 --- a/JetHerald/Commands/ListCommand.cs +++ b/JetHerald/Commands/ListCommand.cs @@ -1,33 +1,30 @@ -using System.Linq; -using System.Threading.Tasks; +using JetHerald.Services; using Telegram.Bot; -using Telegram.Bot.Args; +using Telegram.Bot.Types; -namespace JetHerald.Commands +namespace JetHerald.Commands; +public class ListCommand : IChatCommand { - public class ListCommand : IChatCommand + Db Db { get; } + TelegramBotClient Bot { get; } + + public ListCommand(Db db, TelegramBotClient bot) { - readonly Db db; - readonly TelegramBotClient bot; - - public ListCommand(Db db, TelegramBotClient bot) - { - this.db = db; - this.bot = bot; - } - - public async Task Execute(CommandString cmd, MessageEventArgs messageEventArgs) - { - if (!await CommandHelper.CheckAdministrator(bot, messageEventArgs.Message)) - return null; - - var msg = messageEventArgs.Message; - var chatid = msg.Chat.Id; - var topics = await db.GetTopicsForChat(NamespacedId.Telegram(chatid)); - - return topics.Any() - ? "Topics:\n" + string.Join("\n", topics) - : "No subscriptions active."; - } + Db = db; + Bot = bot; } -} \ No newline at end of file + + public async Task Execute(CommandString cmd, Update update) + { + if (!await CommandHelper.CheckAdministrator(Bot, update.Message)) + return null; + + var msg = update.Message; + var chatid = msg.Chat.Id; + var topics = await Db.GetTopicsForSub(NamespacedId.Telegram(chatid)); + + return topics.Any() + ? "Topics:\n" + string.Join("\n", topics) + : "No subscriptions active."; + } +} diff --git a/JetHerald/Commands/SubscribeCommand.cs b/JetHerald/Commands/SubscribeCommand.cs index 09525ed..fc21366 100644 --- a/JetHerald/Commands/SubscribeCommand.cs +++ b/JetHerald/Commands/SubscribeCommand.cs @@ -1,44 +1,42 @@ -using System.Threading.Tasks; -using Telegram.Bot; -using Telegram.Bot.Args; +using Telegram.Bot; +using Telegram.Bot.Types; +using JetHerald.Services; -namespace JetHerald.Commands +namespace JetHerald.Commands; +public class SubscribeCommand : IChatCommand { - public class SubscribeCommand : IChatCommand + Db Db { get; } + TelegramBotClient Bot { get; } + + public SubscribeCommand(Db db, TelegramBotClient bot) { - readonly Db db; - readonly TelegramBotClient bot; + Db = db; + Bot = bot; + } - public SubscribeCommand(Db db, TelegramBotClient bot) + public async Task Execute(CommandString cmd, Update args) + { + if (cmd.Parameters.Length < 1) + return null; + + if (!await CommandHelper.CheckAdministrator(Bot, args.Message)) + return null; + + var chat = NamespacedId.Telegram(args.Message.Chat.Id); + var token = cmd.Parameters[0]; + + var topic = await Db.GetTopicForSub(token, chat); + + if (topic == null) + return "topic not found"; + else if (topic.Sub == chat) + return $"already subscribed to {topic.Name}"; + else if (topic.ReadToken != token) + return "token mismatch"; + else { - this.db = db; - this.bot = bot; - } - - public async Task Execute(CommandString cmd, MessageEventArgs args) - { - if (cmd.Parameters.Length < 1) - return null; - - if (!await CommandHelper.CheckAdministrator(bot, args.Message)) - return null; - - var chat = NamespacedId.Telegram(args.Message.Chat.Id); - var token = cmd.Parameters[0]; - - var topic = await db.GetTopicForSub(token, chat); - - if (topic == null) - return "topic not found"; - else if (topic.Chat == chat) - return $"already subscribed to {topic.Name}"; - else if (topic.ReadToken != token) - return "token mismatch"; - else - { - await db.CreateSubscription(topic.TopicId, chat); - return $"subscribed to {topic.Name}"; - } + await Db.CreateSubscription(topic.TopicId, chat); + return $"subscribed to {topic.Name}"; } } -} \ No newline at end of file +} diff --git a/JetHerald/Commands/UnsubscribeCommand.cs b/JetHerald/Commands/UnsubscribeCommand.cs index 360d8df..99f4970 100644 --- a/JetHerald/Commands/UnsubscribeCommand.cs +++ b/JetHerald/Commands/UnsubscribeCommand.cs @@ -1,37 +1,35 @@ -using System.Threading.Tasks; -using Telegram.Bot; -using Telegram.Bot.Args; +using Telegram.Bot; +using Telegram.Bot.Types; +using JetHerald.Services; -namespace JetHerald.Commands +namespace JetHerald.Commands; +public class UnsubscribeCommand : IChatCommand { - public class UnsubscribeCommand : IChatCommand + Db Db { get; } + TelegramBotClient Bot { get; } + + public UnsubscribeCommand(Db db, TelegramBotClient bot) { - readonly Db db; - readonly TelegramBotClient bot; - - public UnsubscribeCommand(Db db, TelegramBotClient bot) - { - this.db = db; - this.bot = bot; - } - - public async Task Execute(CommandString cmd, MessageEventArgs messageEventArgs) - { - if (cmd.Parameters.Length < 1) - return null; - - if (!await CommandHelper.CheckAdministrator(bot, messageEventArgs.Message)) - return null; - - var msg = messageEventArgs.Message; - var chat = NamespacedId.Telegram(msg.Chat.Id); - - var topicName = cmd.Parameters[0]; - int affected = await db.RemoveSubscription(topicName, chat); - if (affected >= 1) - return $"unsubscribed from {topicName}"; - else - return $"could not find subscription for {topicName}"; - } + Db = db; + Bot = bot; } -} \ No newline at end of file + + public async Task Execute(CommandString cmd, Update update) + { + if (cmd.Parameters.Length < 1) + return null; + + if (!await CommandHelper.CheckAdministrator(Bot, update.Message)) + return null; + + var msg = update.Message; + var chat = NamespacedId.Telegram(msg.Chat.Id); + + var topicName = cmd.Parameters[0]; + int affected = await Db.RemoveSubscription(topicName, chat); + if (affected >= 1) + return $"unsubscribed from {topicName}"; + else + return $"could not find subscription for {topicName}"; + } +} diff --git a/JetHerald/Configs.cs b/JetHerald/Configs.cs deleted file mode 100644 index 33690ca..0000000 --- a/JetHerald/Configs.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace JetHerald.Options -{ - public class ConnectionStrings - { - public string DefaultConnection { get; set; } - } - - public class Telegram - { - public string ApiKey { get; set; } - - public bool UseProxy { get; set; } - public string ProxyUrl { get; set; } - public string ProxyPassword { get; set; } - public string ProxyLogin { get; set; } - } - - public class Discord - { - public string Token { get; set; } - } - - public class Timeout - { - public int DebtLimitSeconds { get; set; } - public int HeartbeatCost { get; set; } - public int ReportCost { get; set; } - } -} diff --git a/JetHerald/Contracts/IDb.cs b/JetHerald/Contracts/IDb.cs new file mode 100644 index 0000000..91b931d --- /dev/null +++ b/JetHerald/Contracts/IDb.cs @@ -0,0 +1,27 @@ +namespace JetHerald.Contracts; + +public class Topic +{ + public uint TopicId { get; set; } + public NamespacedId Creator { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string ReadToken { get; set; } + public string WriteToken { get; set; } + public string AdminToken { get; set; } + + public NamespacedId? Sub { get; set; } + + public override string ToString() + => Name == Description ? Name : $"{Name}: {Description}"; +} + +public class HeartEvent +{ + public ulong HeartEventId { get; set; } + public uint TopicId { get; set; } + public string Heart { get; set; } + public DateTime CreateTs { get; set; } + + public string Description { get; set; } +} diff --git a/JetHerald/Controllers/HeartbeatController.cs b/JetHerald/Controllers/HeartbeatController.cs index c6ae3c2..61dcd5c 100644 --- a/JetHerald/Controllers/HeartbeatController.cs +++ b/JetHerald/Controllers/HeartbeatController.cs @@ -1,97 +1,94 @@ -using System; -using System.Text.Json; +using System.Text.Json; using System.Text.Json.Serialization; -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; +using JetHerald.Services; -namespace JetHerald.Controllers +namespace JetHerald.Controllers; + +[ApiController] +public class HeartbeatController : ControllerBase { - [ApiController] - public class HeartbeatController : ControllerBase + Db Db { get; } + JetHeraldBot Herald { get; } + LeakyBucket Timeouts { get; } + Options.TimeoutConfig Config { get; } + + public HeartbeatController(Db db, JetHeraldBot herald, LeakyBucket timeouts, IOptions cfgOptions) { - Db Db { get; } - JetHeraldBot Herald { get; } - LeakyBucket Timeouts { get; } - Options.Timeout Config { get; } + Herald = herald; + Timeouts = timeouts; + Db = db; + Config = cfgOptions.Value; + } - public HeartbeatController(Db db, JetHeraldBot herald, LeakyBucket timeouts, IOptions cfgOptions) + // tfw when you need to manually parse body and query + [Route("api/heartbeat")] + [HttpPost] + public async Task Heartbeat() + { + var q = Request.Query; + if (q.ContainsKey("Topic") + && q.ContainsKey("ExpiryTimeout") + && q.ContainsKey("WriteToken")) { - Herald = herald; - Timeouts = timeouts; - Db = db; - Config = cfgOptions.Value; - } - - // tfw when you need to manually parse body and query - [Route("api/heartbeat")] - [HttpPost] - public async Task Heartbeat() - { - var q = Request.Query; - if (q.ContainsKey("Topic") - && q.ContainsKey("ExpiryTimeout") - && q.ContainsKey("WriteToken")) - { - HeartbeatArgs args = new(); - args.Topic = q["Topic"]; - args.WriteToken = q["WriteToken"]; - if (!int.TryParse(q["ExpiryTimeout"], out var expTimeout)) - { - return BadRequest(); - } - args.ExpiryTimeout = expTimeout; - return await DoHeartbeat(args); - } - - try - { - var args = await JsonSerializer.DeserializeAsync(HttpContext.Request.Body, new JsonSerializerOptions() - { - IncludeFields = true - }); - return await DoHeartbeat(args); - } - catch (JsonException) + HeartbeatArgs args = new(); + args.Topic = q["Topic"]; + args.WriteToken = q["WriteToken"]; + if (!int.TryParse(q["ExpiryTimeout"], out var expTimeout)) { return BadRequest(); } + args.ExpiryTimeout = expTimeout; + return await DoHeartbeat(args); } - [Route("api/heartbeat")] - [HttpGet] - public Task HeartbeatGet([FromQuery] HeartbeatArgs args) => DoHeartbeat(args); - - private async Task DoHeartbeat(HeartbeatArgs args) + try { - var heart = args.Heart ?? "General"; - - var t = await Db.GetTopic(args.Topic); - if (t == null) - return new NotFoundResult(); - else if (!t.WriteToken.Equals(args.WriteToken, StringComparison.Ordinal)) - return StatusCode(403); - - if (Timeouts.IsTimedOut(t.TopicId)) - return StatusCode(StatusCodes.Status429TooManyRequests); - - var affected = await Db.ReportHeartbeat(t.TopicId, heart, args.ExpiryTimeout); - - if (affected == 1) - await Herald.HeartbeatSent(t, heart); - - Timeouts.ApplyCost(t.TopicId, Config.HeartbeatCost); - - return new OkResult(); + var args = await JsonSerializer.DeserializeAsync(HttpContext.Request.Body, new JsonSerializerOptions() + { + IncludeFields = true + }); + return await DoHeartbeat(args); } - - public class HeartbeatArgs + catch (JsonException) { - [JsonPropertyName("Topic")] public string Topic { get; set; } - [JsonPropertyName("Heart")] public string Heart { get; set; } - [JsonPropertyName("ExpiryTimeout")] public int ExpiryTimeout { get; set; } - [JsonPropertyName("WriteToken")] public string WriteToken { get; set; } + return BadRequest(); } } -} \ No newline at end of file + + [Route("api/heartbeat")] + [HttpGet] + public Task HeartbeatGet([FromQuery] HeartbeatArgs args) => DoHeartbeat(args); + + private async Task DoHeartbeat(HeartbeatArgs args) + { + var heart = args.Heart ?? "General"; + + var t = await Db.GetTopic(args.Topic); + if (t == null) + return new NotFoundResult(); + else if (!t.WriteToken.Equals(args.WriteToken, StringComparison.Ordinal)) + return StatusCode(403); + + if (Timeouts.IsTimedOut(t.TopicId)) + return StatusCode(StatusCodes.Status429TooManyRequests); + + var affected = await Db.ReportHeartbeat(t.TopicId, heart, args.ExpiryTimeout); + + if (affected == 1) + await Herald.HeartbeatReceived(t, heart); + + Timeouts.ApplyCost(t.TopicId, Config.HeartbeatCost); + + return new OkResult(); + } + + public class HeartbeatArgs + { + [JsonPropertyName("Topic")] public string Topic { get; set; } + [JsonPropertyName("Heart")] public string Heart { get; set; } + [JsonPropertyName("ExpiryTimeout")] public int ExpiryTimeout { get; set; } + [JsonPropertyName("WriteToken")] public string WriteToken { get; set; } + } +} diff --git a/JetHerald/Controllers/HelloController.cs b/JetHerald/Controllers/HelloController.cs index 672a46b..40e9ecf 100644 --- a/JetHerald/Controllers/HelloController.cs +++ b/JetHerald/Controllers/HelloController.cs @@ -1,21 +1,20 @@ using System.Reflection; using Microsoft.AspNetCore.Mvc; -namespace JetHerald.Controllers +namespace JetHerald.Controllers; + +[ApiController] +public class HelloController : ControllerBase { - [ApiController] - public class HelloController : ControllerBase + [Route("api/hello")] + [HttpGet] + public object Hello() { - [Route("api/hello")] - [HttpGet] - public object Hello() + return new { - return new - { - status = "OK", - server_name = "JetHerald", - server_version = Assembly.GetExecutingAssembly().GetCustomAttribute().InformationalVersion - }; - } + status = "OK", + server_name = "JetHerald", + server_version = Assembly.GetExecutingAssembly().GetCustomAttribute().InformationalVersion + }; } -} \ No newline at end of file +} diff --git a/JetHerald/Controllers/ReportController.cs b/JetHerald/Controllers/ReportController.cs index dca583c..a9ae844 100644 --- a/JetHerald/Controllers/ReportController.cs +++ b/JetHerald/Controllers/ReportController.cs @@ -1,81 +1,77 @@ -using System; -using System.Text.Json; -using System.Threading.Tasks; +using System.Text.Json; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; +using JetHerald.Services; -namespace JetHerald.Controllers +namespace JetHerald.Controllers; +[ApiController] +public class ReportController : ControllerBase { - [ApiController] - public class ReportController : ControllerBase + Db Db { get; } + JetHeraldBot Herald { get; } + LeakyBucket Timeouts { get; } + Options.TimeoutConfig Config { get; } + + public ReportController(Db db, JetHeraldBot herald, LeakyBucket timeouts, IOptions cfgOptions) { - Db Db { get; } - JetHeraldBot Herald { get; } - LeakyBucket Timeouts { get; } - Options.Timeout Config { get; } + Herald = herald; + Timeouts = timeouts; + Db = db; + Config = cfgOptions.Value; + } - public ReportController(Db db, JetHeraldBot herald, LeakyBucket timeouts, IOptions cfgOptions) + [Route("api/report")] + [HttpPost] + public async Task Report() + { + var q = Request.Query; + if (q.ContainsKey("Topic") + && q.ContainsKey("Message") + && q.ContainsKey("WriteToken")) { - Herald = herald; - Timeouts = timeouts; - Db = db; - Config = cfgOptions.Value; + ReportArgs args = new(); + args.Topic = q["Topic"]; + args.Message = q["Message"]; + args.WriteToken = q["WriteToken"]; + return await DoReport(args); } - [Route("api/report")] - [HttpPost] - public async Task Report() + try { - var q = Request.Query; - if (q.ContainsKey("Topic") - && q.ContainsKey("Message") - && q.ContainsKey("WriteToken")) + var args = await JsonSerializer.DeserializeAsync(HttpContext.Request.Body, new JsonSerializerOptions() { - ReportArgs args = new(); - args.Topic = q["Topic"]; - args.Message = q["Message"]; - args.WriteToken = q["WriteToken"]; - return await DoReport(args); - } - - try - { - var args = await JsonSerializer.DeserializeAsync(HttpContext.Request.Body, new() - { - IncludeFields = true - }); - return await DoReport(args); - } - catch (JsonException) - { - return BadRequest(); - } + IncludeFields = true + }); + return await DoReport(args); } - - private async Task DoReport(ReportArgs args) + catch (JsonException) { - var t = await Db.GetTopic(args.Topic); - if (t == null) - return new NotFoundResult(); - else if (!t.WriteToken.Equals(args.WriteToken, StringComparison.OrdinalIgnoreCase)) - return StatusCode(403); - - if (Timeouts.IsTimedOut(t.TopicId)) - return StatusCode(StatusCodes.Status429TooManyRequests); - - await Herald.PublishMessage(t, args.Message); - - Timeouts.ApplyCost(t.TopicId, Config.ReportCost); - - return new OkResult(); - } - - public class ReportArgs - { - public string Topic { get; set; } - public string Message { get; set; } - public string WriteToken { get; set; } + return BadRequest(); } } + + private async Task DoReport(ReportArgs args) + { + var t = await Db.GetTopic(args.Topic); + if (t == null) + return new NotFoundResult(); + else if (!t.WriteToken.Equals(args.WriteToken, StringComparison.OrdinalIgnoreCase)) + return StatusCode(403); + + if (Timeouts.IsTimedOut(t.TopicId)) + return StatusCode(StatusCodes.Status429TooManyRequests); + + await Herald.PublishMessage(t, args.Message); + + Timeouts.ApplyCost(t.TopicId, Config.ReportCost); + + return new OkResult(); + } + + public class ReportArgs + { + public string Topic { get; set; } + public string Message { get; set; } + public string WriteToken { get; set; } + } } diff --git a/JetHerald/DapperExtensions.cs b/JetHerald/DapperExtensions.cs new file mode 100644 index 0000000..3e49d8d --- /dev/null +++ b/JetHerald/DapperExtensions.cs @@ -0,0 +1,22 @@ +using System.Data; +using Dapper; + +namespace JetHerald; +public static class DapperConverters +{ + static bool registered = false; + public static void Register() + { + if (registered) + return; + registered = true; + + SqlMapper.AddTypeHandler(new NamespacedIdHandler()); + } + + class NamespacedIdHandler : SqlMapper.TypeHandler + { + public override void SetValue(IDbDataParameter parameter, NamespacedId value) => parameter.Value = value.ToString(); + public override NamespacedId Parse(object value) => new((string)value); + } +} diff --git a/JetHerald/DapperMappers.cs b/JetHerald/DapperMappers.cs deleted file mode 100644 index 775c873..0000000 --- a/JetHerald/DapperMappers.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Data; - -using Dapper; - -namespace JetHerald -{ - public static class DapperConverters - { - static bool registered = false; - public static void Register() - { - if (registered) - return; - registered = true; - - SqlMapper.AddTypeHandler(new NamespacedIdHandler()); - } - - class NamespacedIdHandler : SqlMapper.TypeHandler - { - public override void SetValue(IDbDataParameter parameter, NamespacedId value) => parameter.Value = value.ToString(); - public override NamespacedId Parse(object value) => new((string)value); - } - } -} \ No newline at end of file diff --git a/JetHerald/Db.cs b/JetHerald/Db.cs deleted file mode 100644 index d87174d..0000000 --- a/JetHerald/Db.cs +++ /dev/null @@ -1,174 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.Extensions.Options; -using MySql.Data.MySqlClient; -using Dapper; -using System.Threading.Tasks; -using System.Threading; - -namespace JetHerald -{ - public class Db - { - public class Topic - { - public uint TopicId { get; set; } - public NamespacedId Creator { get; set; } - public string Name { get; set; } - public string Description { get; set; } - public string ReadToken { get; set; } - public string WriteToken { get; set; } - public string AdminToken { get; set; } - public DateTime? ExpiryTime { get; set; } - public bool ExpiryMessageSent { get; set; } - - public NamespacedId? Chat { get; set; } - - public override string ToString() - => Name == Description ? Name : $"{Name}: {Description}"; - } - - public class HeartAttack - { - public ulong HeartattackId { get; set; } - public uint TopicId { get; set; } - public string Heart { get; set; } - public DateTime ExpiryTime { get; set; } - - public string Description { get; set; } - } - - public async Task DeleteTopic(string name, string adminToken) - { - using var c = GetConnection(); - return await c.ExecuteAsync( - " DELETE" + - " FROM topic" + - " WHERE Name = @name AND AdminToken = @adminToken", - new { name, adminToken }); - } - - public async Task GetTopic(string name) - { - using var c = GetConnection(); - return await c.QuerySingleOrDefaultAsync( - "SELECT *" + - " FROM topic" + - " WHERE Name = @name", - new { name }); - } - - public async Task GetTopicForSub(string token, NamespacedId chat) - { - using var c = GetConnection(); - return await c.QuerySingleOrDefaultAsync( - " SELECT t.*, tc.Chat " + - " FROM topic t " + - " LEFT JOIN topic_chat tc ON t.TopicId = tc.TopicId AND tc.Chat = @chat " + - " WHERE ReadToken = @token", - new { token, chat }); - } - - public async Task CreateTopic(NamespacedId user, string name, string descr) - { - var t = new Topic - { - Creator = user, - Name = name, - Description = descr, - ReadToken = TokenHelper.GetToken(), - WriteToken = TokenHelper.GetToken(), - AdminToken = TokenHelper.GetToken() - }; - using var c = GetConnection(); - return await c.QuerySingleOrDefaultAsync( - " INSERT INTO herald.topic " + - " ( Creator, Name, Description, ReadToken, WriteToken, AdminToken) " + - " VALUES " + - " (@Creator, @Name, @Description, @ReadToken, @WriteToken, @AdminToken); " + - " SELECT * FROM topic WHERE TopicId = LAST_INSERT_ID(); ", - t); - } - public async Task> GetChatsForTopic(uint topicid) - { - using var c = GetConnection(); - return await c.QueryAsync( - " SELECT Chat " + - " FROM topic_chat " + - " WHERE TopicId = @topicid", - new { topicid }); - } - - public async Task> GetTopicsForChat(NamespacedId chat) - { - using var c = GetConnection(); - return await c.QueryAsync( - " SELECT t.*" + - " FROM topic_chat ct" + - " JOIN topic t on t.TopicId = ct.TopicId" + - " WHERE ct.Chat = @chat", - new { chat }); - } - - public async Task CreateSubscription(uint topicId, NamespacedId chat) - { - using var c = GetConnection(); - await c.ExecuteAsync( - " INSERT INTO topic_chat" + - " (Chat, TopicId)" + - " VALUES" + - " (@chat, @topicId)", - new { topicId, chat }); - } - - public async Task RemoveSubscription(string topicName, NamespacedId chat) - { - using var c = GetConnection(); - return await c.ExecuteAsync( - " DELETE tc " + - " FROM topic_chat tc" + - " JOIN topic t ON tc.TopicId = t.TopicId " + - " WHERE t.Name = @topicName AND tc.Chat = @chat;", - new { topicName, chat }); - } - - - public async Task ReportHeartbeat(uint topicId, string heart, int timeoutSeconds) - { - using var c = GetConnection(); - return await c.ExecuteAsync( - @" - INSERT INTO heartbeat - (TopicId, Heart, ExpiryTime) - VALUES - (@topicId, @heart, CURRENT_TIMESTAMP() + INTERVAL @timeoutSeconds SECOND) - ON DUPLICATE KEY UPDATE - ExpiryTime = CURRENT_TIMESTAMP() + INTERVAL @timeoutSeconds SECOND; - ", - new { topicId, heart, @timeoutSeconds }); - } - - public async Task> ProcessHeartAttacks() - { - using var c = GetConnection(); - return await c.QueryAsync("CALL process_heartattacks();"); - } - - public async Task MarkHeartAttackReported(ulong id, byte status = 1) - { - using var c = GetConnection(); - await c.ExecuteAsync("UPDATE heartattack SET Status = @status WHERE HeartattackId = @id", new { id, status }); - } - - - - - public Db(IOptions cfg) - { - Config = cfg.Value; - } - - Options.ConnectionStrings Config { get; } - MySqlConnection GetConnection() => new(Config.DefaultConnection); - } -} diff --git a/JetHerald/GlobalUsings.cs b/JetHerald/GlobalUsings.cs new file mode 100644 index 0000000..910adf3 --- /dev/null +++ b/JetHerald/GlobalUsings.cs @@ -0,0 +1,8 @@ +global using System; +global using System.Collections.Generic; +global using System.IO; +global using System.Linq; +global using System.Threading.Tasks; + +global using Microsoft.Extensions.Logging; +global using Microsoft.Extensions.Options; \ No newline at end of file diff --git a/JetHerald/JetHerald.csproj b/JetHerald/JetHerald.csproj index b3619fc..d81bc75 100644 --- a/JetHerald/JetHerald.csproj +++ b/JetHerald/JetHerald.csproj @@ -1,7 +1,7 @@ - net5.0 + net6.0 @@ -9,13 +9,13 @@ - - - - - - - + + + + + + + diff --git a/JetHerald/JetHeraldBot.Discord.cs b/JetHerald/JetHeraldBot.Discord.cs deleted file mode 100644 index 5feeb22..0000000 --- a/JetHerald/JetHeraldBot.Discord.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Threading.Tasks; -using DSharpPlus; -using DSharpPlus.CommandsNext; -using JetHerald.Commands; - -namespace JetHerald -{ - public partial class JetHeraldBot - { - DiscordClient DiscordBot { get; set; } - - async Task InitDiscord() - { - DiscordBot = new DiscordClient(new() - { - Token = DiscordConfig.Token, - TokenType = TokenType.Bot, - Intents = DiscordIntents.AllUnprivileged, - LoggerFactory = LoggerFactory - }); - - var commands = DiscordBot.UseCommandsNext(new CommandsNextConfiguration() - { - StringPrefixes = new[] { "!" }, - Services = ServiceProvider - }); - - commands.RegisterCommands(); - - await DiscordBot.ConnectAsync(); - } - - - async Task SendMessageToDiscordChannel(NamespacedId chat, string formatted) - { - var id = ulong.Parse(chat.Id); - await DiscordBot.SendMessageAsync(await DiscordBot.GetChannelAsync(id), formatted); - } - } -} \ No newline at end of file diff --git a/JetHerald/JetHeraldBot.Telegram.cs b/JetHerald/JetHeraldBot.Telegram.cs deleted file mode 100644 index 6eeb7da..0000000 --- a/JetHerald/JetHeraldBot.Telegram.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Net; -using System.Threading.Tasks; -using JetHerald.Commands; -using Microsoft.Extensions.Logging; -using Telegram.Bot; -using Telegram.Bot.Args; -using Telegram.Bot.Types.Enums; - -namespace JetHerald -{ - public partial class JetHeraldBot - { - TelegramBotClient TelegramBot { get; set; } - Telegram.Bot.Types.User Me { get; set; } - ChatCommandRouter Commands; - - async Task InitTelegram() - { - if (TelegramConfig.UseProxy) - { - var httpProxy = new WebProxy(TelegramConfig.ProxyUrl) - { Credentials = new NetworkCredential(TelegramConfig.ProxyLogin, TelegramConfig.ProxyPassword) }; - TelegramBot = new TelegramBotClient(TelegramConfig.ApiKey, httpProxy); - } - else - { - TelegramBot = new TelegramBotClient(TelegramConfig.ApiKey); - } - Me = await TelegramBot.GetMeAsync(); - - Commands = new ChatCommandRouter(Me.Username, Log); - Commands.Add(new CreateTopicCommand(Db), "createtopic"); - Commands.Add(new DeleteTopicCommand(Db), "deletetopic"); - Commands.Add(new SubscribeCommand(Db, TelegramBot), "subscribe", "sub"); - Commands.Add(new UnsubscribeCommand(Db, TelegramBot), "unsubscribe", "unsub"); - Commands.Add(new ListCommand(Db, TelegramBot), "list"); - - HeartbeatCancellation = new(); - HeartbeatTask = CheckHeartbeats(HeartbeatCancellation.Token); - - TelegramBot.OnMessage += TelegramMessageReceived; - TelegramBot.StartReceiving(); - } - - Task SendMessageToTelegramChannel(NamespacedId chat, string formatted) - { - var id = long.Parse(chat.Id); - return TelegramBot.SendTextMessageAsync(id, formatted); - } - - async void TelegramMessageReceived(object sender, MessageEventArgs messageEventArgs) - { - var msg = messageEventArgs.Message; - if (msg == null || msg.Type != MessageType.Text) - return; - - try - { - var reply = await Commands.Execute(sender, messageEventArgs); - if (reply != null) - await TelegramBot.SendTextMessageAsync( - chatId: msg.Chat.Id, - text: reply, - replyToMessageId: msg.MessageId); - } - catch (Exception e) - { - Log.LogError(e, "Exception occured during handling of command: " + msg.Text); - } - } - } -} \ No newline at end of file diff --git a/JetHerald/JetHeraldBot.cs b/JetHerald/JetHeraldBot.cs deleted file mode 100644 index c607b5b..0000000 --- a/JetHerald/JetHeraldBot.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using System.Threading; -using Microsoft.Extensions.Hosting; - -namespace JetHerald -{ - public partial class JetHeraldBot : IHostedService - { - Db Db { get; set; } - Options.Telegram TelegramConfig { get; } - Options.Discord DiscordConfig { get; } - ILogger Log { get; } - ILoggerFactory LoggerFactory { get; } - IServiceProvider ServiceProvider { get; } - - public JetHeraldBot(Db db, IOptions telegramCfg, IOptions discordCfg, ILogger log, ILoggerFactory loggerFactory, IServiceProvider serviceProvider) - { - Db = db; - TelegramConfig = telegramCfg.Value; - DiscordConfig = discordCfg.Value; - - Log = log; - LoggerFactory = loggerFactory; - ServiceProvider = serviceProvider; - } - - CancellationTokenSource HeartbeatCancellation; - Task HeartbeatTask; - - public async Task StartAsync(CancellationToken token) - { - await InitTelegram(); - await InitDiscord(); - } - - public async Task StopAsync(CancellationToken token) - { - await DiscordBot.DisconnectAsync(); - TelegramBot.StopReceiving(); - HeartbeatCancellation.Cancel(); - try - { - await HeartbeatTask; - } - catch (TaskCanceledException) - { - - } - } - - public async Task CheckHeartbeats(CancellationToken token) - { - while (!token.IsCancellationRequested) - { - await Task.Delay(1000 * 10, token); - try - { - var attacks = await Db.ProcessHeartAttacks(); - foreach (var attack in attacks) - { - var chats = await Db.GetChatsForTopic(attack.TopicId); - foreach (var chat in chats) - await SendMessageImpl(chat, $"!{attack.Description}!:\nHeart \"{attack.Heart}\" stopped beating at {attack.ExpiryTime}"); - await Db.MarkHeartAttackReported(attack.HeartattackId); - if (token.IsCancellationRequested) - return; - } - } - catch (Exception e) - { - Log.LogError(e, "Exception while checking heartbeats"); - } - } - } - - public Task HeartbeatSent(Db.Topic topic, string heart) - => BroadcastMessageImpl(topic, $"!{topic.Description}!:\nHeart \"{heart}\" has started beating."); - - public Task PublishMessage(Db.Topic topic, string message) - => BroadcastMessageImpl(topic, $"|{topic.Description}|:\n{message}"); - - async Task BroadcastMessageImpl(Db.Topic topic, string formatted) - { - var chatIds = await Db.GetChatsForTopic(topic.TopicId); - foreach (var c in chatIds) - await SendMessageImpl(c, formatted); - } - - async Task SendMessageImpl(NamespacedId chat, string formatted) - { - try - { - if (chat.Namespace == "telegram") - { - await SendMessageToTelegramChannel(chat, formatted); - } - else if (chat.Namespace == "discord") - { - await SendMessageToDiscordChannel(chat, formatted); - } - } - catch (Exception e) { Log.LogError(e, $"Error while sending message \"{formatted}\" to {chat}"); } - } - - } -} diff --git a/JetHerald/LeakyBucket.cs b/JetHerald/LeakyBucket.cs deleted file mode 100644 index 060658c..0000000 --- a/JetHerald/LeakyBucket.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace JetHerald -{ - public class LeakyBucket - { - private readonly ConcurrentDictionary expiryDates = new(); - private readonly Options.Timeout config; - private readonly ILogger log; - - public LeakyBucket(IOptions cfgOptions, ILogger log) - { - config = cfgOptions.Value; - this.log = log; - } - - public bool IsTimedOut(uint key) - { - var now = DateTime.UtcNow; - var debtLimit = now.AddSeconds(config.DebtLimitSeconds); - var time = expiryDates.GetValueOrDefault(key, now); - log.LogTrace("{key} had current timedebt of {time}", key, time); - return time > debtLimit; - } - - public void ApplyCost(uint key, int cost) - { - expiryDates.AddOrUpdate(key, - key => DateTime.UtcNow.AddSeconds(cost), - (key, oldDebt) => - { - var now = DateTime.UtcNow; - if (oldDebt < now) - return now.AddSeconds(cost); - - return oldDebt.AddSeconds(cost); - }); - } - } -} \ No newline at end of file diff --git a/JetHerald/NamespacedId.cs b/JetHerald/NamespacedId.cs index af63c3b..3297871 100644 --- a/JetHerald/NamespacedId.cs +++ b/JetHerald/NamespacedId.cs @@ -1,47 +1,40 @@ -using System; - -namespace JetHerald +namespace JetHerald; +public struct NamespacedId { - public struct NamespacedId + public string Namespace { get; init; } + public string Id { get; init; } + + public NamespacedId(string str) { - public string Namespace { get; init; } - public string Id { get; init; } - - public NamespacedId(string str) - { - var ind = str.IndexOf("://"); - if (ind < 0) throw new ArgumentException("Could not parse namespaced id"); - Namespace = str[..ind].ToLowerInvariant(); - Id = str[(ind + 3)..]; - } - - public NamespacedId(string ns, string id) - { - Namespace = ns; - Id = id; - } - - public static NamespacedId Telegram(long id) - => new("telegram", $"{id}"); - - public static NamespacedId Discord(ulong id) - => new("discord", $"{id}"); - - public override string ToString() => $"{Namespace}://{Id}"; - - public override int GetHashCode() => HashCode.Combine(Namespace, Id); - - public override bool Equals(object obj) - => obj is NamespacedId nsid && this == nsid; - - public static bool operator ==(NamespacedId a, NamespacedId b) - => a.Namespace == b.Namespace && a.Id == b.Id; - - public static bool operator !=(NamespacedId a, NamespacedId b) - => !(a == b); - + var ind = str.IndexOf("://"); + if (ind < 0) throw new ArgumentException("Could not parse namespaced id"); + Namespace = str[..ind].ToLowerInvariant(); + Id = str[(ind + 3)..]; } + public NamespacedId(string ns, string id) + { + Namespace = ns; + Id = id; + } + public static NamespacedId Telegram(long id) + => new("telegram", $"{id}"); + + public static NamespacedId Discord(ulong id) + => new("discord", $"{id}"); + + public override string ToString() => $"{Namespace}://{Id}"; + + public override int GetHashCode() => HashCode.Combine(Namespace, Id); + + public override bool Equals(object obj) + => obj is NamespacedId nsid && this == nsid; + + public static bool operator ==(NamespacedId a, NamespacedId b) + => a.Namespace == b.Namespace && a.Id == b.Id; + + public static bool operator !=(NamespacedId a, NamespacedId b) + => !(a == b); } diff --git a/JetHerald/Options.cs b/JetHerald/Options.cs new file mode 100644 index 0000000..5774b9d --- /dev/null +++ b/JetHerald/Options.cs @@ -0,0 +1,23 @@ +namespace JetHerald.Options; + +public class ConnectionStrings +{ + public string DefaultConnection { get; set; } +} + +public class TelegramConfig +{ + public string ApiKey { get; set; } +} + +public class DiscordConfig +{ + public string Token { get; set; } +} + +public class TimeoutConfig +{ + public int DebtLimitSeconds { get; set; } + public int HeartbeatCost { get; set; } + public int ReportCost { get; set; } +} diff --git a/JetHerald/Program.cs b/JetHerald/Program.cs index b6674e4..c869f64 100644 --- a/JetHerald/Program.cs +++ b/JetHerald/Program.cs @@ -1,51 +1,73 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; +using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using NLog.Web; +using JetHerald; +using JetHerald.Options; +using JetHerald.Services; -namespace JetHerald +var log = +#if DEBUG +NLogBuilder.ConfigureNLog("nlog.debug.config").GetCurrentClassLogger(); +#else +NLogBuilder.ConfigureNLog("nlog.config").GetCurrentClassLogger(); +#endif + +try { - public class Program + log.Info("init main"); + + var builder = WebApplication.CreateBuilder(args); + + builder.WebHost.ConfigureAppConfiguration((hostingContext, config) => { - public static void Main(string[] args) - { - DapperConverters.Register(); + config.SetBasePath(Directory.GetCurrentDirectory()); + config.AddIniFile("secrets.ini", + optional: true, reloadOnChange: true); + config.AddIniFile($"secrets.{hostingContext.HostingEnvironment.EnvironmentName}.ini", + optional: true, reloadOnChange: true); + config.AddJsonFile("secrets.json", optional: true, reloadOnChange: true); + config.AddJsonFile($"secrets.{hostingContext.HostingEnvironment.EnvironmentName}.json", optional: true, reloadOnChange: true); + }); - var logger = NLog.Web.NLogBuilder.ConfigureNLog("NLog.config").GetCurrentClassLogger(); - try - { - logger.Debug("init main"); - CreateWebHostBuilder(args).Build().Run(); - } - catch (Exception ex) - { - logger.Error(ex, "Stopped program because of exception"); - throw; - } - finally - { - NLog.LogManager.Shutdown(); - } - } + builder.Logging.ClearProviders(); + builder.Logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace); + builder.Host.UseNLog(); - public static IWebHostBuilder CreateWebHostBuilder(string[] args) => - WebHost.CreateDefaultBuilder(args) - .UseStartup() - .ConfigureAppConfiguration(config => - { - config.AddIniFile("secrets.ini"); - }) - .ConfigureLogging(logging => - { - logging.ClearProviders(); - logging.SetMinimumLevel(LogLevel.Trace); - }) - .UseNLog(); // NLog: setup NLog for Dependency injection - } + var cfg = builder.Configuration; + var services = builder.Services; + + services.Configure(cfg.GetSection("ConnectionStrings")); + services.Configure(cfg.GetSection("Telegram")); + services.Configure(cfg.GetSection("Discord")); + services.Configure(cfg.GetSection("Timeout")); + services.AddSingleton(); + services.AddSingleton().AddHostedService(s => s.GetService()); + services.AddSingleton(); + services.AddMvc(); + + + var app = builder.Build(); + app.UsePathBase(cfg.GetValue("PathBase")); + app.UseDeveloperExceptionPage(); + app.UseHsts(); + app.UseHttpsRedirection(); + app.UseStaticFiles(); + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + app.Run(); +} +catch (Exception exception) +{ + log.Error(exception, "Error while starting up"); + throw; +} +finally +{ + // Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux) + NLog.LogManager.Shutdown(); } diff --git a/JetHerald/Services/Db.cs b/JetHerald/Services/Db.cs new file mode 100644 index 0000000..a50d568 --- /dev/null +++ b/JetHerald/Services/Db.cs @@ -0,0 +1,138 @@ +using MySql.Data.MySqlClient; +using Dapper; +using JetHerald.Options; +using JetHerald.Contracts; + +namespace JetHerald.Services; +public class Db +{ + public async Task DeleteTopic(string name, string adminToken) + { + using var c = GetConnection(); + return await c.ExecuteAsync( + " DELETE" + + " FROM topic" + + " WHERE Name = @name AND AdminToken = @adminToken", + new { name, adminToken }); + } + + public async Task GetTopic(string name) + { + using var c = GetConnection(); + return await c.QuerySingleOrDefaultAsync( + " SELECT *" + + " FROM topic" + + " WHERE Name = @name", + new { name }); + } + + public async Task GetTopicForSub(string token, NamespacedId chat) + { + using var c = GetConnection(); + return await c.QuerySingleOrDefaultAsync( + " SELECT t.*, ts.Sub " + + " FROM topic t " + + " LEFT JOIN topic_sub ts ON t.TopicId = ts.TopicId AND ts.Chat = @chat " + + " WHERE ReadToken = @token", + new { token, chat }); + } + + public async Task CreateTopic(NamespacedId user, string name, string descr) + { + using var c = GetConnection(); + return await c.QuerySingleOrDefaultAsync( + " INSERT INTO topic " + + " ( Creator, Name, Description, ReadToken, WriteToken, AdminToken) " + + " VALUES " + + " (@Creator, @Name, @Description, @ReadToken, @WriteToken, @AdminToken); " + + " SELECT * FROM topic WHERE TopicId = LAST_INSERT_ID(); ", + new Topic + { + Creator = user, + Name = name, + Description = descr, + ReadToken = TokenHelper.GetToken(), + WriteToken = TokenHelper.GetToken(), + AdminToken = TokenHelper.GetToken() + }); + } + public async Task> GetSubsForTopic(uint topicId) + { + using var c = GetConnection(); + return await c.QueryAsync( + " SELECT Sub " + + " FROM topic_sub " + + " WHERE TopicId = @topicid", + new { topicId }); + } + + public async Task> GetTopicsForSub(NamespacedId sub) + { + using var c = GetConnection(); + return await c.QueryAsync( + " SELECT t.*" + + " FROM topic_sub ts" + + " JOIN topic t USING (TopicId)" + + " WHERE ts.Sub = @sub", + new { sub }); + } + + public async Task CreateSubscription(uint topicId, NamespacedId sub) + { + using var c = GetConnection(); + await c.ExecuteAsync( + " INSERT INTO topic_sub" + + " (TopicId, Sub)" + + " VALUES" + + " (@topicId, @sub)", + new { topicId, sub }); + } + + public async Task RemoveSubscription(string topicName, NamespacedId sub) + { + using var c = GetConnection(); + return await c.ExecuteAsync( + " DELETE ts " + + " FROM topic_sub ts" + + " JOIN topic t USING (TopicId) " + + " WHERE t.Name = @topicName AND tc.Sub = @sub;", + new { topicName, sub }); + } + + + public async Task ReportHeartbeat(uint topicId, string heart, int timeoutSeconds) + { + using var c = GetConnection(); + return await c.ExecuteAsync( + @" + INSERT INTO heart + (TopicId, Heart, Status, ExpiryTs) + VALUES + (@topicId, @heart, 'beating', CURRENT_TIMESTAMP() + INTERVAL @timeoutSeconds SECOND) + ON DUPLICATE KEY UPDATE + Status = 'beating', + ExpiryTs = CURRENT_TIMESTAMP() + INTERVAL @timeoutSeconds SECOND; + ", + new { topicId, heart, timeoutSeconds }); + } + + public async Task> ProcessHearts() + { + using var c = GetConnection(); + return await c.QueryAsync("CALL process_hearts();"); + } + + public async Task MarkHeartAttackReported(ulong id) + { + using var c = GetConnection(); + await c.ExecuteAsync("UPDATE heartevent SET Status = 'reported' WHERE HeartattackId = @id", new { id }); + } + + public Db(IOptionsMonitor cfg) + { + Config = cfg; + } + IOptionsMonitor Config { get; } + MySqlConnection GetConnection() => new(Config.CurrentValue.DefaultConnection); +} + diff --git a/JetHerald/Services/DiscordCommands.cs b/JetHerald/Services/DiscordCommands.cs new file mode 100644 index 0000000..90090ce --- /dev/null +++ b/JetHerald/Services/DiscordCommands.cs @@ -0,0 +1,117 @@ +using MySql.Data.MySqlClient; +using DSharpPlus.CommandsNext; +using DSharpPlus.CommandsNext.Attributes; +using JetHerald.Services; + +namespace JetHerald.Commands; +[ModuleLifespan(ModuleLifespan.Transient)] +public class DiscordCommands : BaseCommandModule +{ + public Db Db { get; set; } + + [Command("createtopic")] + [Description("Creates a topic.")] + [RequireDirectMessage] + public async Task CreateTopic( + CommandContext ctx, + [Description("The unique name of the new topic.")] + string name, + [RemainingText, Description("The name displayed in service messages. Defaults to `name`")] + string description = null) + { + if (description == null) + description = name; + + _ = ctx.TriggerTypingAsync(); + + try + { + var topic = await Db.CreateTopic(NamespacedId.Discord(ctx.User.Id), name, description); + await ctx.RespondAsync($"created {topic.Name}\n" + + $"readToken\n{topic.ReadToken}\n" + + $"writeToken\n{topic.WriteToken}\n" + + $"adminToken\n{topic.AdminToken}\n"); + } + catch (MySqlException myDuplicate) when (myDuplicate.Number == 1062) + { + await ctx.RespondAsync($"topic {name} already exists"); + } + } + + [Command("deletetopic")] + [Description("Deletes a topic.")] + [RequireDirectMessage] + public async Task DeleteTopic( + CommandContext ctx, + [Description("The name of the topic to be deleted.")] + string name, + [Description("The admin token of the topic to be deleted.")] + string adminToken) + { + _ = ctx.TriggerTypingAsync(); + var changed = await Db.DeleteTopic(name, adminToken); + if (changed > 0) + await ctx.RespondAsync($"deleted {name} and all its subscriptions"); + else + await ctx.RespondAsync($"invalid topic name or admin token"); + } + + [Command("list")] + [Description("List all subscriptions in this channel.")] + [RequireUserPermissions(DSharpPlus.Permissions.ManageGuild)] + public async Task ListSubscriptions(CommandContext ctx) + { + _ = ctx.TriggerTypingAsync(); + + var topics = await Db.GetTopicsForSub(NamespacedId.Discord(ctx.Channel.Id)); + + await ctx.RespondAsync(topics.Any() + ? "Topics:\n" + string.Join("\n", topics) + : "No subscriptions active."); + } + + [Command("subscribe")] + [Description("Subscribes to a topic.")] + [RequireUserPermissions(DSharpPlus.Permissions.ManageGuild)] + public async Task Subscribe( + CommandContext ctx, + [Description("The read token of the token to subscribe to.")] + string token + ) + { + _ = ctx.TriggerTypingAsync(); + + var chat = NamespacedId.Discord(ctx.Channel.Id); + var topic = await Db.GetTopicForSub(token, chat); + + if (topic == null) + await ctx.RespondAsync("topic not found"); + else if (topic.Sub.HasValue && topic.Sub.Value == chat) + await ctx.RespondAsync($"already subscribed to {topic.Name}"); + else if (topic.ReadToken != token) + await ctx.RespondAsync("token mismatch"); + else + { + await Db.CreateSubscription(topic.TopicId, chat); + await ctx.RespondAsync($"subscribed to {topic.Name}"); + } + } + + [Command("unsubscribe")] + [Description("Unsubscribes from a topic.")] + [RequireUserPermissions(DSharpPlus.Permissions.ManageGuild)] + public async Task Unsubscribe( + CommandContext ctx, + [Description("The name of the topic to unsubscribe from.")] + string name + ) + { + _ = ctx.TriggerTypingAsync(); + + int affected = await Db.RemoveSubscription(name, NamespacedId.Discord(ctx.Channel.Id)); + if (affected >= 1) + await ctx.RespondAsync($"unsubscribed from {name}"); + else + await ctx.RespondAsync($"could not find subscription for {name}"); + } +} diff --git a/JetHerald/Services/HeartMonitor.cs b/JetHerald/Services/HeartMonitor.cs new file mode 100644 index 0000000..9d617ae --- /dev/null +++ b/JetHerald/Services/HeartMonitor.cs @@ -0,0 +1,46 @@ +using System.Threading; +using Microsoft.Extensions.Hosting; + +namespace JetHerald.Services; +public class HeartMonitor : BackgroundService +{ + public HeartMonitor( + Db db, + JetHeraldBot herald, + ILogger log) + { + Db = db; + Herald = herald; + Log = log; + } + + Db Db { get; } + JetHeraldBot Herald { get; } + ILogger Log { get; } + protected override async Task ExecuteAsync(CancellationToken token) + { + while (!token.IsCancellationRequested) + { + await Task.Delay(1000 * 10, token); + try + { + var attacks = await Db.ProcessHearts(); + foreach (var a in attacks) + { + await Herald.BroadcastMessageRaw( + a.TopicId, + $"!{a.Description}!:\nHeart \"{a.Heart}\" stopped beating at {a.CreateTs}"); + + await Db.MarkHeartAttackReported(a.HeartEventId); + + if (token.IsCancellationRequested) + return; + } + } + catch (Exception e) + { + Log.LogError(e, "Exception while checking heartbeats"); + } + } + } +} diff --git a/JetHerald/Services/JetHeraldBot.Discord.cs b/JetHerald/Services/JetHeraldBot.Discord.cs new file mode 100644 index 0000000..a707997 --- /dev/null +++ b/JetHerald/Services/JetHeraldBot.Discord.cs @@ -0,0 +1,36 @@ +using DSharpPlus; +using DSharpPlus.CommandsNext; +using JetHerald.Commands; +namespace JetHerald.Services; +public partial class JetHeraldBot +{ + DiscordClient DiscordBot { get; set; } + + async Task StartDiscord() + { + DiscordBot = new DiscordClient(new() + { + Token = DiscordConfig.Token, + TokenType = TokenType.Bot, + Intents = DiscordIntents.AllUnprivileged, + LoggerFactory = LoggerFactory + }); + + var commands = DiscordBot.UseCommandsNext(new CommandsNextConfiguration() + { + StringPrefixes = new[] { "!" }, + Services = ServiceProvider + }); + + commands.RegisterCommands(); + + await DiscordBot.ConnectAsync(); + } + Task StopDiscord() => DiscordBot.DisconnectAsync(); + + async Task SendMessageToDiscordChannel(NamespacedId chat, string formatted) + { + var id = ulong.Parse(chat.Id); + await DiscordBot.SendMessageAsync(await DiscordBot.GetChannelAsync(id), formatted); + } +} diff --git a/JetHerald/Services/JetHeraldBot.Telegram.cs b/JetHerald/Services/JetHeraldBot.Telegram.cs new file mode 100644 index 0000000..5133f4d --- /dev/null +++ b/JetHerald/Services/JetHeraldBot.Telegram.cs @@ -0,0 +1,79 @@ +using System.Threading; +using Telegram.Bot; +using Telegram.Bot.Types.Enums; +using Telegram.Bot.Extensions.Polling; +using Telegram.Bot.Exceptions; +using Telegram.Bot.Types; +using JetHerald.Commands; +using Telegram.Bot.Types.ReplyMarkups; + +namespace JetHerald.Services; +public partial class JetHeraldBot +{ + TelegramBotClient TelegramBot { get; set; } + Telegram.Bot.Types.User Me { get; set; } + ChatCommandRouter Commands; + CancellationTokenSource TelegramBotShutdownToken { get; } = new(); + async Task StartTelegram() + { + TelegramBot = new TelegramBotClient(TelegramConfig.ApiKey); + Me = await TelegramBot.GetMeAsync(); + + Commands = new ChatCommandRouter(Me.Username, Log); + Commands.Add(new CreateTopicCommand(Db), "createtopic"); + Commands.Add(new DeleteTopicCommand(Db), "deletetopic"); + Commands.Add(new SubscribeCommand(Db, TelegramBot), "subscribe", "sub"); + Commands.Add(new UnsubscribeCommand(Db, TelegramBot), "unsubscribe", "unsub"); + Commands.Add(new ListCommand(Db, TelegramBot), "list"); + + var receiverOptions = new ReceiverOptions { AllowedUpdates = new[] { UpdateType.Message } }; + TelegramBot.StartReceiving( + HandleUpdateAsync, + HandleErrorAsync, + receiverOptions, + TelegramBotShutdownToken.Token); + } + + Task StopTelegram() + { + TelegramBotShutdownToken.Cancel(); + return Task.CompletedTask; + } + + Task SendMessageToTelegramChannel(NamespacedId chat, string formatted) + { + var id = long.Parse(chat.Id); + return TelegramBot.SendTextMessageAsync(id, formatted); + } + + Task HandleErrorAsync(ITelegramBotClient botClient, Exception exception, CancellationToken cancellationToken) + { + var ErrorMessage = exception switch + { + ApiRequestException apiRequestException => $"Telegram API Error:\n[{apiRequestException.ErrorCode}]\n{apiRequestException.Message}", + _ => exception.ToString() + }; + Log.LogError(ErrorMessage); + return Task.CompletedTask; + } + + async Task HandleUpdateAsync(ITelegramBotClient sender, Update update, CancellationToken cancellationToken) + { + if (update.Type != UpdateType.Message || update?.Message?.Type != MessageType.Text) + return; + var msg = update.Message!; + try + { + var reply = await Commands.Execute(sender, update); + if (reply != null) + await TelegramBot.SendTextMessageAsync( + chatId: msg.Chat.Id, + text: reply, + replyToMessageId: msg.MessageId); + } + catch (Exception e) + { + Log.LogError(e, "Exception occured during handling of command: " + msg.Text); + } + } +} diff --git a/JetHerald/Services/JetHeraldBot.cs b/JetHerald/Services/JetHeraldBot.cs new file mode 100644 index 0000000..ff34497 --- /dev/null +++ b/JetHerald/Services/JetHeraldBot.cs @@ -0,0 +1,75 @@ +using System.Threading; +using JetHerald.Contracts; +using JetHerald.Options; +using Microsoft.Extensions.Hosting; + +namespace JetHerald.Services; +public partial class JetHeraldBot : IHostedService +{ + Db Db { get; set; } + TelegramConfig TelegramConfig { get; } + DiscordConfig DiscordConfig { get; } + ILogger Log { get; } + ILoggerFactory LoggerFactory { get; } + IServiceProvider ServiceProvider { get; } + + public JetHeraldBot( + Db db, + IOptions telegramCfg, + IOptions discordCfg, + ILogger log, + ILoggerFactory loggerFactory, + IServiceProvider serviceProvider) + { + Db = db; + TelegramConfig = telegramCfg.Value; + DiscordConfig = discordCfg.Value; + + Log = log; + LoggerFactory = loggerFactory; + ServiceProvider = serviceProvider; + } + + public async Task StartAsync(CancellationToken token) + { + await StartTelegram(); + await StartDiscord(); + } + + public async Task StopAsync(CancellationToken token) + { + await StopDiscord(); + await StopTelegram(); + } + + public Task HeartbeatReceived(Topic topic, string heart) + => BroadcastMessageRaw(topic.TopicId, $"!{topic.Description}!:\nHeart \"{heart}\" has started beating."); + + public Task PublishMessage(Topic topic, string message) + => BroadcastMessageRaw(topic.TopicId, $"|{topic.Description}|:\n{message}"); + + public async Task BroadcastMessageRaw(uint topicId, string formatted) + { + var chatIds = await Db.GetSubsForTopic(topicId); + foreach (var c in chatIds) + await SendMessageRaw(c, formatted); + } + + async Task SendMessageRaw(NamespacedId chat, string formatted) + { + try + { + if (chat.Namespace == "telegram") + { + await SendMessageToTelegramChannel(chat, formatted); + } + else if (chat.Namespace == "discord") + { + await SendMessageToDiscordChannel(chat, formatted); + } + } + catch (Exception e) { Log.LogError(e, $"Error while sending message \"{formatted}\" to {chat}"); } + } + +} + diff --git a/JetHerald/Services/LeakyBucket.cs b/JetHerald/Services/LeakyBucket.cs new file mode 100644 index 0000000..ffa6b4b --- /dev/null +++ b/JetHerald/Services/LeakyBucket.cs @@ -0,0 +1,37 @@ +using System.Collections.Concurrent; +namespace JetHerald.Services; +public class LeakyBucket +{ + private readonly ConcurrentDictionary expiryDates = new(); + private readonly Options.TimeoutConfig config; + private readonly ILogger log; + + public LeakyBucket(IOptions cfgOptions, ILogger log) + { + config = cfgOptions.Value; + this.log = log; + } + + public bool IsTimedOut(uint key) + { + var now = DateTime.UtcNow; + var debtLimit = now.AddSeconds(config.DebtLimitSeconds); + var time = expiryDates.GetValueOrDefault(key, now); + log.LogTrace("{key} had current timedebt of {time}", key, time); + return time > debtLimit; + } + + public void ApplyCost(uint key, int cost) + { + expiryDates.AddOrUpdate(key, + key => DateTime.UtcNow.AddSeconds(cost), + (key, oldDebt) => + { + var now = DateTime.UtcNow; + if (oldDebt < now) + return now.AddSeconds(cost); + + return oldDebt.AddSeconds(cost); + }); + } +} diff --git a/JetHerald/Startup.cs b/JetHerald/Startup.cs deleted file mode 100644 index 8c3121a..0000000 --- a/JetHerald/Startup.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace JetHerald -{ - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.Configure(Configuration.GetSection("ConnectionStrings")); - services.Configure(Configuration.GetSection("Telegram")); - services.Configure(Configuration.GetSection("Discord")); - services.Configure(Configuration.GetSection("Timeout")); - services.AddSingleton(); - services.AddSingleton(); - services.AddHostedService(s => s.GetService()); - services.AddSingleton(); - services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_3_0); - - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - app.UsePathBase(Configuration.GetValue("PathBase")); - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseRouting(); - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } - } -} diff --git a/JetHerald/TokenHelper.cs b/JetHerald/TokenHelper.cs deleted file mode 100644 index f2d4cc6..0000000 --- a/JetHerald/TokenHelper.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Security.Cryptography; - -namespace JetHerald -{ - public static class TokenHelper - { - static readonly RNGCryptoServiceProvider rng = new(); - static readonly byte[] buf = new byte[24]; - static readonly object SyncLock = new(); - - public static string GetToken() - { - lock (SyncLock) - { - rng.GetBytes(buf); - return Convert.ToBase64String(buf).Replace('+', '_').Replace('/', '_'); - } - } - } -} diff --git a/JetHerald/Utils/TokenHelper.cs b/JetHerald/Utils/TokenHelper.cs new file mode 100644 index 0000000..39d52b2 --- /dev/null +++ b/JetHerald/Utils/TokenHelper.cs @@ -0,0 +1,17 @@ +using System.Security.Cryptography; + +namespace JetHerald; +public static class TokenHelper +{ + static readonly byte[] buf = new byte[24]; + static readonly object SyncLock = new(); + + public static string GetToken() + { + lock (SyncLock) + { + RandomNumberGenerator.Fill(buf); + return Convert.ToBase64String(buf).Replace('+', '_').Replace('/', '_'); + } + } +}