mirror of
https://github.com/Jetsparrow/jetherald.git
synced 2026-01-20 23:56:08 +03:00
.
This commit is contained in:
parent
0c74bc4cea
commit
05c491ff0d
@ -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<string> Execute(CommandString cmd, Update update);
|
||||
}
|
||||
|
||||
public class ChatCommandRouter
|
||||
{
|
||||
string Username { get; }
|
||||
ILogger Log { get; }
|
||||
|
||||
Dictionary<string, IChatCommand> Commands { get; }
|
||||
|
||||
public ChatCommandRouter(string username, ILogger log)
|
||||
{
|
||||
Task<string> Execute(CommandString cmd, MessageEventArgs messageEventArgs);
|
||||
Log = log;
|
||||
Username = username;
|
||||
Commands = new Dictionary<string, IChatCommand>();
|
||||
}
|
||||
|
||||
public class ChatCommandRouter
|
||||
public async Task<string> Execute(object sender, Update update)
|
||||
{
|
||||
string Username { get; }
|
||||
ILogger Log { get; }
|
||||
|
||||
Dictionary<string, IChatCommand> 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<string, IChatCommand>();
|
||||
}
|
||||
|
||||
public async Task<string> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(@"/(?<cmd>\w+)(@(?<name>\w+))?");
|
||||
var match = cmdRegex.Match(words.First());
|
||||
if (!match.Success)
|
||||
return false;
|
||||
var cmdRegex = new Regex(@"/(?<cmd>\w+)(@(?<name>\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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<bool> CheckAdministrator(TelegramBotClient bot, Message msg)
|
||||
{
|
||||
public static async Task<bool> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<string> 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<string> 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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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<string> Execute(CommandString cmd, Update update)
|
||||
{
|
||||
if (cmd.Parameters.Length < 2)
|
||||
return null;
|
||||
var msg = update.Message;
|
||||
|
||||
public async Task<string> 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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<string> 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;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> 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.";
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<string> 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<string> 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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<string> 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;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> 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}";
|
||||
}
|
||||
}
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
27
JetHerald/Contracts/IDb.cs
Normal file
27
JetHerald/Contracts/IDb.cs
Normal file
@ -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; }
|
||||
}
|
||||
@ -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<Options.TimeoutConfig> 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<Options.Timeout> cfgOptions)
|
||||
// tfw when you need to manually parse body and query
|
||||
[Route("api/heartbeat")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> 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<IActionResult> 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<HeartbeatArgs>(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<IActionResult> HeartbeatGet([FromQuery] HeartbeatArgs args) => DoHeartbeat(args);
|
||||
|
||||
private async Task<IActionResult> 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<HeartbeatArgs>(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Route("api/heartbeat")]
|
||||
[HttpGet]
|
||||
public Task<IActionResult> HeartbeatGet([FromQuery] HeartbeatArgs args) => DoHeartbeat(args);
|
||||
|
||||
private async Task<IActionResult> 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; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<AssemblyInformationalVersionAttribute>().InformationalVersion
|
||||
};
|
||||
}
|
||||
status = "OK",
|
||||
server_name = "JetHerald",
|
||||
server_version = Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<Options.TimeoutConfig> 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<Options.Timeout> cfgOptions)
|
||||
[Route("api/report")]
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> 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<IActionResult> Report()
|
||||
try
|
||||
{
|
||||
var q = Request.Query;
|
||||
if (q.ContainsKey("Topic")
|
||||
&& q.ContainsKey("Message")
|
||||
&& q.ContainsKey("WriteToken"))
|
||||
var args = await JsonSerializer.DeserializeAsync<ReportArgs>(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<ReportArgs>(HttpContext.Request.Body, new()
|
||||
{
|
||||
IncludeFields = true
|
||||
});
|
||||
return await DoReport(args);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
IncludeFields = true
|
||||
});
|
||||
return await DoReport(args);
|
||||
}
|
||||
|
||||
private async Task<IActionResult> 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<IActionResult> 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; }
|
||||
}
|
||||
}
|
||||
|
||||
22
JetHerald/DapperExtensions.cs
Normal file
22
JetHerald/DapperExtensions.cs
Normal file
@ -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<NamespacedId>
|
||||
{
|
||||
public override void SetValue(IDbDataParameter parameter, NamespacedId value) => parameter.Value = value.ToString();
|
||||
public override NamespacedId Parse(object value) => new((string)value);
|
||||
}
|
||||
}
|
||||
@ -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<NamespacedId>
|
||||
{
|
||||
public override void SetValue(IDbDataParameter parameter, NamespacedId value) => parameter.Value = value.ToString();
|
||||
public override NamespacedId Parse(object value) => new((string)value);
|
||||
}
|
||||
}
|
||||
}
|
||||
174
JetHerald/Db.cs
174
JetHerald/Db.cs
@ -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<int> 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<Topic> GetTopic(string name)
|
||||
{
|
||||
using var c = GetConnection();
|
||||
return await c.QuerySingleOrDefaultAsync<Topic>(
|
||||
"SELECT *" +
|
||||
" FROM topic" +
|
||||
" WHERE Name = @name",
|
||||
new { name });
|
||||
}
|
||||
|
||||
public async Task<Topic> GetTopicForSub(string token, NamespacedId chat)
|
||||
{
|
||||
using var c = GetConnection();
|
||||
return await c.QuerySingleOrDefaultAsync<Topic>(
|
||||
" 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<Topic> 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<Topic>(
|
||||
" 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<IEnumerable<NamespacedId>> GetChatsForTopic(uint topicid)
|
||||
{
|
||||
using var c = GetConnection();
|
||||
return await c.QueryAsync<NamespacedId>(
|
||||
" SELECT Chat " +
|
||||
" FROM topic_chat " +
|
||||
" WHERE TopicId = @topicid",
|
||||
new { topicid });
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Topic>> GetTopicsForChat(NamespacedId chat)
|
||||
{
|
||||
using var c = GetConnection();
|
||||
return await c.QueryAsync<Topic>(
|
||||
" 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<int> 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<int> 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<IEnumerable<HeartAttack>> ProcessHeartAttacks()
|
||||
{
|
||||
using var c = GetConnection();
|
||||
return await c.QueryAsync<HeartAttack>("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<Options.ConnectionStrings> cfg)
|
||||
{
|
||||
Config = cfg.Value;
|
||||
}
|
||||
|
||||
Options.ConnectionStrings Config { get; }
|
||||
MySqlConnection GetConnection() => new(Config.DefaultConnection);
|
||||
}
|
||||
}
|
||||
8
JetHerald/GlobalUsings.cs
Normal file
8
JetHerald/GlobalUsings.cs
Normal file
@ -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;
|
||||
@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -9,13 +9,13 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapper" Version="1.60.6" />
|
||||
<PackageReference Include="DSharpPlus" Version="4.0.0" />
|
||||
<PackageReference Include="DSharpPlus.CommandsNext" Version="4.0.0" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.1.1" />
|
||||
<PackageReference Include="MySql.Data" Version="8.0.17" />
|
||||
<PackageReference Include="NLog.Web.AspNetCore" Version="4.8.4" />
|
||||
<PackageReference Include="Telegram.Bot" Version="14.12.0" />
|
||||
<PackageReference Include="Dapper" Version="2.0.123" />
|
||||
<PackageReference Include="DSharpPlus" Version="4.1.0" />
|
||||
<PackageReference Include="DSharpPlus.CommandsNext" Version="4.1.0" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="6.0.1" />
|
||||
<PackageReference Include="MySql.Data" Version="8.0.28" />
|
||||
<PackageReference Include="NLog.Web.AspNetCore" Version="5.0.0-rc2" />
|
||||
<PackageReference Include="Telegram.Bot.Extensions.Polling" Version="1.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@ -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<DiscordCommands>();
|
||||
|
||||
await DiscordBot.ConnectAsync();
|
||||
}
|
||||
|
||||
|
||||
async Task SendMessageToDiscordChannel(NamespacedId chat, string formatted)
|
||||
{
|
||||
var id = ulong.Parse(chat.Id);
|
||||
await DiscordBot.SendMessageAsync(await DiscordBot.GetChannelAsync(id), formatted);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<JetHeraldBot> Log { get; }
|
||||
ILoggerFactory LoggerFactory { get; }
|
||||
IServiceProvider ServiceProvider { get; }
|
||||
|
||||
public JetHeraldBot(Db db, IOptions<Options.Telegram> telegramCfg, IOptions<Options.Discord> discordCfg, ILogger<JetHeraldBot> 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}"); }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -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<uint, DateTime> expiryDates = new();
|
||||
private readonly Options.Timeout config;
|
||||
private readonly ILogger log;
|
||||
|
||||
public LeakyBucket(IOptions<Options.Timeout> cfgOptions, ILogger<LeakyBucket> 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
}
|
||||
|
||||
23
JetHerald/Options.cs
Normal file
23
JetHerald/Options.cs
Normal file
@ -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; }
|
||||
}
|
||||
@ -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<Startup>()
|
||||
.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<ConnectionStrings>(cfg.GetSection("ConnectionStrings"));
|
||||
services.Configure<JetHerald.Options.TelegramConfig>(cfg.GetSection("Telegram"));
|
||||
services.Configure<DiscordConfig>(cfg.GetSection("Discord"));
|
||||
services.Configure<TimeoutConfig>(cfg.GetSection("Timeout"));
|
||||
services.AddSingleton<Db>();
|
||||
services.AddSingleton<JetHeraldBot>().AddHostedService(s => s.GetService<JetHeraldBot>());
|
||||
services.AddSingleton<LeakyBucket>();
|
||||
services.AddMvc();
|
||||
|
||||
|
||||
var app = builder.Build();
|
||||
app.UsePathBase(cfg.GetValue<string>("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();
|
||||
}
|
||||
|
||||
138
JetHerald/Services/Db.cs
Normal file
138
JetHerald/Services/Db.cs
Normal file
@ -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<int> 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<Topic> GetTopic(string name)
|
||||
{
|
||||
using var c = GetConnection();
|
||||
return await c.QuerySingleOrDefaultAsync<Topic>(
|
||||
" SELECT *" +
|
||||
" FROM topic" +
|
||||
" WHERE Name = @name",
|
||||
new { name });
|
||||
}
|
||||
|
||||
public async Task<Topic> GetTopicForSub(string token, NamespacedId chat)
|
||||
{
|
||||
using var c = GetConnection();
|
||||
return await c.QuerySingleOrDefaultAsync<Topic>(
|
||||
" 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<Topic> CreateTopic(NamespacedId user, string name, string descr)
|
||||
{
|
||||
using var c = GetConnection();
|
||||
return await c.QuerySingleOrDefaultAsync<Topic>(
|
||||
" 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<IEnumerable<NamespacedId>> GetSubsForTopic(uint topicId)
|
||||
{
|
||||
using var c = GetConnection();
|
||||
return await c.QueryAsync<NamespacedId>(
|
||||
" SELECT Sub " +
|
||||
" FROM topic_sub " +
|
||||
" WHERE TopicId = @topicid",
|
||||
new { topicId });
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Topic>> GetTopicsForSub(NamespacedId sub)
|
||||
{
|
||||
using var c = GetConnection();
|
||||
return await c.QueryAsync<Topic>(
|
||||
" 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<int> 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<int> 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<IEnumerable<HeartEvent>> ProcessHearts()
|
||||
{
|
||||
using var c = GetConnection();
|
||||
return await c.QueryAsync<HeartEvent>("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<ConnectionStrings> cfg)
|
||||
{
|
||||
Config = cfg;
|
||||
}
|
||||
IOptionsMonitor<ConnectionStrings> Config { get; }
|
||||
MySqlConnection GetConnection() => new(Config.CurrentValue.DefaultConnection);
|
||||
}
|
||||
|
||||
117
JetHerald/Services/DiscordCommands.cs
Normal file
117
JetHerald/Services/DiscordCommands.cs
Normal file
@ -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}");
|
||||
}
|
||||
}
|
||||
46
JetHerald/Services/HeartMonitor.cs
Normal file
46
JetHerald/Services/HeartMonitor.cs
Normal file
@ -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<HeartMonitor> log)
|
||||
{
|
||||
Db = db;
|
||||
Herald = herald;
|
||||
Log = log;
|
||||
}
|
||||
|
||||
Db Db { get; }
|
||||
JetHeraldBot Herald { get; }
|
||||
ILogger<HeartMonitor> 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
36
JetHerald/Services/JetHeraldBot.Discord.cs
Normal file
36
JetHerald/Services/JetHeraldBot.Discord.cs
Normal file
@ -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<DiscordCommands>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
79
JetHerald/Services/JetHeraldBot.Telegram.cs
Normal file
79
JetHerald/Services/JetHeraldBot.Telegram.cs
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
75
JetHerald/Services/JetHeraldBot.cs
Normal file
75
JetHerald/Services/JetHeraldBot.cs
Normal file
@ -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<JetHeraldBot> Log { get; }
|
||||
ILoggerFactory LoggerFactory { get; }
|
||||
IServiceProvider ServiceProvider { get; }
|
||||
|
||||
public JetHeraldBot(
|
||||
Db db,
|
||||
IOptions<TelegramConfig> telegramCfg,
|
||||
IOptions<DiscordConfig> discordCfg,
|
||||
ILogger<JetHeraldBot> 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}"); }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
37
JetHerald/Services/LeakyBucket.cs
Normal file
37
JetHerald/Services/LeakyBucket.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using System.Collections.Concurrent;
|
||||
namespace JetHerald.Services;
|
||||
public class LeakyBucket
|
||||
{
|
||||
private readonly ConcurrentDictionary<uint, DateTime> expiryDates = new();
|
||||
private readonly Options.TimeoutConfig config;
|
||||
private readonly ILogger log;
|
||||
|
||||
public LeakyBucket(IOptions<Options.TimeoutConfig> cfgOptions, ILogger<LeakyBucket> 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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<Options.ConnectionStrings>(Configuration.GetSection("ConnectionStrings"));
|
||||
services.Configure<Options.Telegram>(Configuration.GetSection("Telegram"));
|
||||
services.Configure<Options.Discord>(Configuration.GetSection("Discord"));
|
||||
services.Configure<Options.Timeout>(Configuration.GetSection("Timeout"));
|
||||
services.AddSingleton<Db>();
|
||||
services.AddSingleton<JetHeraldBot>();
|
||||
services.AddHostedService(s => s.GetService<JetHeraldBot>());
|
||||
services.AddSingleton<LeakyBucket>();
|
||||
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<string>("PathBase"));
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
}
|
||||
|
||||
app.UseRouting();
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapControllers();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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('/', '_');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
JetHerald/Utils/TokenHelper.cs
Normal file
17
JetHerald/Utils/TokenHelper.cs
Normal file
@ -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('/', '_');
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user