From 04a04f418cf498ba3b0b6b4f4f650461238d48cb Mon Sep 17 00:00:00 2001 From: Basique Evangelist Date: Sun, 18 Apr 2021 23:25:40 +0300 Subject: [PATCH] Discord support part 1 --- JetHerald/Commands/CreateTopicCommand.cs | 2 +- JetHerald/Commands/DiscordCommands.cs | 116 +++++++++++++++++++++++ JetHerald/Commands/ListCommand.cs | 4 +- JetHerald/Commands/SubscribeCommand.cs | 4 +- JetHerald/Commands/UnsubscribeCommand.cs | 2 +- JetHerald/Configs.cs | 5 + JetHerald/Db.cs | 52 ++++++---- JetHerald/JetHerald.csproj | 2 + JetHerald/JetHeraldBot.Discord.cs | 38 ++++++++ JetHerald/JetHeraldBot.cs | 56 +++++++---- JetHerald/Startup.cs | 3 +- 11 files changed, 238 insertions(+), 46 deletions(-) create mode 100644 JetHerald/Commands/DiscordCommands.cs create mode 100644 JetHerald/JetHeraldBot.Discord.cs diff --git a/JetHerald/Commands/CreateTopicCommand.cs b/JetHerald/Commands/CreateTopicCommand.cs index cb8d7e3..bd373ac 100644 --- a/JetHerald/Commands/CreateTopicCommand.cs +++ b/JetHerald/Commands/CreateTopicCommand.cs @@ -32,7 +32,7 @@ namespace JetHerald.Commands try { - var topic = await db.CreateTopic(msg.From.Id, name, descr); + var topic = await db.CreateTopic(msg.From.Id, "Telegram", name, descr); return $"created {topic.Name}\n" + $"readToken\n{topic.ReadToken}\n" + $"writeToken\n{topic.WriteToken}\n" + diff --git a/JetHerald/Commands/DiscordCommands.cs b/JetHerald/Commands/DiscordCommands.cs new file mode 100644 index 0000000..a39af9b --- /dev/null +++ b/JetHerald/Commands/DiscordCommands.cs @@ -0,0 +1,116 @@ +using System.Linq; +using System.Threading.Tasks; +using DSharpPlus.CommandsNext; +using DSharpPlus.CommandsNext.Attributes; +using MySql.Data.MySqlClient; + +namespace JetHerald +{ + [ModuleLifespan(ModuleLifespan.Transient)] + public class DiscordCommands : BaseCommandModule + { + public Db db { private 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; + + await ctx.TriggerTypingAsync(); + + try + { + var topic = await db.CreateTopic((long)ctx.User.Id, "Discord", 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) + { + await 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.")] + public async Task ListSubscriptions(CommandContext ctx) + { + await ctx.TriggerTypingAsync(); + + var topics = await db.GetTopicsForChat((long)ctx.Channel.Id, "Discord"); + + await ctx.RespondAsync(topics.Any() + ? "Topics:\n" + string.Join("\n", topics.Select(ListCommand.GetTopicListing)) + : "No subscriptions active."); + } + + [Command("subscribe")] + [Description("Subscribes to a topic.")] + public async Task Subscribe( + CommandContext ctx, + [Description("The read token of the token to subscribe to.")] + string token + ) + { + await ctx.TriggerTypingAsync(); + + var topic = await db.GetTopic(token, (long)ctx.Channel.Id, "Discord"); + + if (topic == null) + await ctx.RespondAsync("topic not found"); + else if (topic.ChatId == (long)ctx.Channel.Id) + await ctx.RespondAsync($"already subscribed to {topic.Name}"); + else if (topic.ReadToken != token) + await ctx.RespondAsync("token mismatch"); + else + { + await db.CreateSubscription(topic.TopicId, (long)ctx.Channel.Id, "Discord"); + await ctx.RespondAsync($"subscribed to {topic.Name}"); + } + } + + [Command("unsubscribe")] + [Description("Unsubscribes from a topic.")] + public async Task Unsubscribe( + CommandContext ctx, + [Description("The name of the topic to unsubscribe from.")] + string name + ) + { + await ctx.TriggerTypingAsync(); + + int affected = await db.RemoveSubscription(name, (long)ctx.Channel.Id, "Discord"); + 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 b2f89dc..f0a328c 100644 --- a/JetHerald/Commands/ListCommand.cs +++ b/JetHerald/Commands/ListCommand.cs @@ -17,14 +17,14 @@ namespace JetHerald { var msg = messageEventArgs.Message; var chatid = msg.Chat.Id; - var topics = await db.GetTopicsForChat(chatid); + var topics = await db.GetTopicsForChat(chatid, "Telegram"); return topics.Any() ? "Topics:\n" + string.Join("\n", topics.Select(GetTopicListing)) : "No subscriptions active."; } - static string GetTopicListing(Db.Topic t) + internal static string GetTopicListing(Db.Topic t) => t.Name == t.Description ? t.Name : $"{t.Name}: {t.Description}"; } diff --git a/JetHerald/Commands/SubscribeCommand.cs b/JetHerald/Commands/SubscribeCommand.cs index 46b3dcd..87b4b57 100644 --- a/JetHerald/Commands/SubscribeCommand.cs +++ b/JetHerald/Commands/SubscribeCommand.cs @@ -20,7 +20,7 @@ namespace JetHerald var chatid = args.Message.Chat.Id; var token = cmd.Parameters[0]; - var topic = await db.GetTopic(token, chatid); + var topic = await db.GetTopic(token, chatid, "Telegram"); if (topic == null) return "topic not found"; @@ -30,7 +30,7 @@ namespace JetHerald return "token mismatch"; else { - await db.CreateSubscription(topic.TopicId, chatid); + await db.CreateSubscription(topic.TopicId, chatid, "Telegram"); return $"subscribed to {topic.Name}"; } } diff --git a/JetHerald/Commands/UnsubscribeCommand.cs b/JetHerald/Commands/UnsubscribeCommand.cs index 6ada499..9126f4d 100644 --- a/JetHerald/Commands/UnsubscribeCommand.cs +++ b/JetHerald/Commands/UnsubscribeCommand.cs @@ -20,7 +20,7 @@ namespace JetHerald var msg = messageEventArgs.Message; var chatid = msg.Chat.Id; var topicName = cmd.Parameters[0]; - int affected = await db.RemoveSubscription(topicName, chatid); + int affected = await db.RemoveSubscription(topicName, chatid, "Telegram"); if (affected >= 1) return $"unsubscribed from {topicName}"; else diff --git a/JetHerald/Configs.cs b/JetHerald/Configs.cs index 758be17..9f53084 100644 --- a/JetHerald/Configs.cs +++ b/JetHerald/Configs.cs @@ -14,4 +14,9 @@ public string ProxyPassword { get; set; } public string ProxyLogin { get; set; } } + + public class Discord + { + public string Token { get; set; } + } } diff --git a/JetHerald/Db.cs b/JetHerald/Db.cs index 57693e0..14db2f3 100644 --- a/JetHerald/Db.cs +++ b/JetHerald/Db.cs @@ -14,6 +14,7 @@ namespace JetHerald { public uint TopicId { get; set; } public long CreatorId { get; set; } + public string CreatorService { get; set; } public string Name { get; set; } public string Description { get; set; } public string ReadToken { get; set; } @@ -23,15 +24,23 @@ namespace JetHerald public bool ExpiryMessageSent { get; set; } public long? ChatId { get; set; } + public string Service { get; set; } } public class ExpiredTopicChat { public long ChatId; + public string Service; public string Description; public DateTime ExpiryTime { get; set; } } + public class ChatData + { + public long ChatId; + public string Service; + } + public async Task DeleteTopic(string name, string adminToken) { using (var c = GetConnection()) @@ -54,18 +63,18 @@ namespace JetHerald new { name }); } - public async Task GetTopic(string token, long chatId) + public async Task GetTopic(string token, long chatId, string service) { using (var c = GetConnection()) return await c.QuerySingleOrDefaultAsync( " SELECT t.*, tc.ChatId " + " FROM topic t " + - " LEFT JOIN topic_chat tc ON t.TopicId = tc.TopicId AND tc.ChatId = @chatId " + + " LEFT JOIN topic_chat tc ON t.TopicId = tc.TopicId AND tc.ChatId = @chatId AND tc.Service = @service " + " WHERE ReadToken = @token", - new { token, chatId }); + new { token, chatId, service }); } - public async Task CreateTopic(long userId, string name, string descr) + public async Task CreateTopic(long userId, string service, string name, string descr) { var t = new Topic { @@ -74,60 +83,61 @@ namespace JetHerald Description = descr, ReadToken = TokenHelper.GetToken(), WriteToken = TokenHelper.GetToken(), - AdminToken = TokenHelper.GetToken() + AdminToken = TokenHelper.GetToken(), + Service = service }; using (var c = GetConnection()) { return await c.QuerySingleOrDefaultAsync( " INSERT INTO herald.topic " + - " ( CreatorId, Name, Description, ReadToken, WriteToken, AdminToken) " + + " ( CreatorId, Name, Description, ReadToken, WriteToken, AdminToken, Service) " + " VALUES " + - " (@CreatorId, @Name, @Description, @ReadToken, @WriteToken, @AdminToken); " + + " (@CreatorId, @Name, @Description, @ReadToken, @WriteToken, @AdminToken, @Service); " + " SELECT * FROM topic WHERE TopicId = LAST_INSERT_ID(); ", t); } } - public async Task> GetChatIdsForTopic(uint topicid) + public async Task> GetChatIdsForTopic(uint topicid) { using (var c = GetConnection()) - return await c.QueryAsync( - " SELECT ChatId" + + return await c.QueryAsync( + " SELECT ChatId, Service" + " FROM topic_chat" + " WHERE TopicId = @topicid", new { topicid }); } - public async Task> GetTopicsForChat(long chatid) + public async Task> GetTopicsForChat(long chatid, string service) { using (var c = GetConnection()) return await c.QueryAsync( " SELECT t.*" + " FROM topic_chat ct" + " JOIN topic t on t.TopicId = ct.TopicId" + - " WHERE ct.ChatId = @chatid", - new { chatid }); + " WHERE ct.ChatId = @chatid AND ct.Service = @service", + new { chatid, service }); } - public async Task CreateSubscription(uint topicId, long chatId) + public async Task CreateSubscription(uint topicId, long chatId, string service) { using (var c = GetConnection()) await c.ExecuteAsync( " INSERT INTO topic_chat" + - " (ChatId, TopicId )" + + " (ChatId, TopicId, Service)" + " VALUES" + - " (@chatId, @topicId)", - new { topicId, chatId }); + " (@chatId, @topicId, @service)", + new { topicId, chatId, service }); } - public async Task RemoveSubscription(string topicName, long chatId) + public async Task RemoveSubscription(string topicName, long chatId, string service) { 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.ChatId = @chatId;", - new { topicName, chatId }); + " WHERE t.Name = @topicName AND tc.ChatId = @chatId AND tc.Service = @service;", + new { topicName, chatId, service }); } public Task AddExpiry(string topicName, int addedTime) @@ -156,7 +166,7 @@ namespace JetHerald { using var c = GetConnection(); return c.QueryAsync( - " SELECT tc.ChatId, t.Description, t.ExpiryTime" + + " SELECT tc.ChatId, tc.Service, t.Description, t.ExpiryTime" + " FROM topic_chat tc" + " INNER JOIN topic t ON t.TopicId = tc.TopicId" + " WHERE t.ExpiryTime < CURRENT_TIMESTAMP() AND NOT t.ExpiryMessageSent", diff --git a/JetHerald/JetHerald.csproj b/JetHerald/JetHerald.csproj index 0ac17b9..b3619fc 100644 --- a/JetHerald/JetHerald.csproj +++ b/JetHerald/JetHerald.csproj @@ -10,6 +10,8 @@ + + diff --git a/JetHerald/JetHeraldBot.Discord.cs b/JetHerald/JetHeraldBot.Discord.cs new file mode 100644 index 0000000..0d06974 --- /dev/null +++ b/JetHerald/JetHeraldBot.Discord.cs @@ -0,0 +1,38 @@ +using System.Threading.Tasks; +using DSharpPlus; +using DSharpPlus.CommandsNext; + +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(); + } + + + private async Task SendMessageToDiscordChannel(long chatId, string formatted) + { + await DiscordBot.SendMessageAsync(await DiscordBot.GetChannelAsync((ulong)chatId), formatted); + } + } +} \ No newline at end of file diff --git a/JetHerald/JetHeraldBot.cs b/JetHerald/JetHeraldBot.cs index dc0eea0..4ad9e4c 100644 --- a/JetHerald/JetHeraldBot.cs +++ b/JetHerald/JetHeraldBot.cs @@ -12,20 +12,27 @@ using System.Threading; namespace JetHerald { - public class JetHeraldBot + public partial class JetHeraldBot { Db Db { get; set; } - Options.Telegram Config { get; } + Options.Telegram TelegramConfig { get; } + Options.Discord DiscordConfig { get; } ILogger Log { get; } + ILoggerFactory LoggerFactory { get; } + IServiceProvider ServiceProvider { get; } - public JetHeraldBot(Db db, IOptions cfg, ILogger log) + public JetHeraldBot(Db db, IOptions telegramCfg, IOptions discordCfg, ILogger log, ILoggerFactory loggerFactory, IServiceProvider serviceProvider) { Db = db; - Config = cfg.Value; + TelegramConfig = telegramCfg.Value; + DiscordConfig = discordCfg.Value; + Log = log; + LoggerFactory = loggerFactory; + ServiceProvider = serviceProvider; } - TelegramBotClient Client { get; set; } + TelegramBotClient TelegramBot { get; set; } ChatCommandRouter Commands; CancellationTokenSource HeartbeatCancellation; Task HeartbeatTask; @@ -33,17 +40,17 @@ namespace JetHerald public async Task Init() { - if (Config.UseProxy) + if (TelegramConfig.UseProxy) { - var httpProxy = new WebProxy(Config.ProxyUrl) - { Credentials = new NetworkCredential(Config.ProxyLogin, Config.ProxyPassword) }; - Client = new TelegramBotClient(Config.ApiKey, httpProxy); + var httpProxy = new WebProxy(TelegramConfig.ProxyUrl) + { Credentials = new NetworkCredential(TelegramConfig.ProxyLogin, TelegramConfig.ProxyPassword) }; + TelegramBot = new TelegramBotClient(TelegramConfig.ApiKey, httpProxy); } else { - Client = new TelegramBotClient(Config.ApiKey); + TelegramBot = new TelegramBotClient(TelegramConfig.ApiKey); } - Me = await Client.GetMeAsync(); + Me = await TelegramBot.GetMeAsync(); Commands = new ChatCommandRouter(Me.Username, Log); Commands.Add(new CreateTopicCommand(Db), "createtopic"); @@ -55,13 +62,16 @@ namespace JetHerald HeartbeatCancellation = new(); HeartbeatTask = CheckHeartbeats(HeartbeatCancellation.Token); - Client.OnMessage += BotOnMessageReceived; - Client.StartReceiving(); + TelegramBot.OnMessage += BotOnMessageReceived; + TelegramBot.StartReceiving(); + + await InitDiscord(); } public async Task Stop() { - Client.StopReceiving(); + await DiscordBot.DisconnectAsync(); + TelegramBot.StopReceiving(); HeartbeatCancellation.Cancel(); try { @@ -82,7 +92,7 @@ namespace JetHerald foreach (var chatSent in await Db.GetExpiredTopics(token)) { var formatted = $"!{chatSent.Description}!:\nTimeout expired at {chatSent.ExpiryTime}"; - await Client.SendTextMessageAsync(chatSent.ChatId, formatted, cancellationToken: token); + await TelegramBot.SendTextMessageAsync(chatSent.ChatId, formatted, cancellationToken: token); } await Db.MarkExpiredTopics(token); @@ -94,7 +104,12 @@ namespace JetHerald var chatIds = await Db.GetChatIdsForTopic(topic.TopicId); var formatted = $"!{topic.Description}!:\nA heartbeat has been sent."; foreach (var c in chatIds) - await Client.SendTextMessageAsync(c, formatted); + { + if (c.Service == "Telegram") + await TelegramBot.SendTextMessageAsync(c.ChatId, formatted); + else if (c.Service == "Discord") + await SendMessageToDiscordChannel(c.ChatId, formatted); + } } public async Task PublishMessage(Db.Topic topic, string message) @@ -102,7 +117,12 @@ namespace JetHerald var chatIds = await Db.GetChatIdsForTopic(topic.TopicId); var formatted = $"|{topic.Description}|:\n{message}"; foreach (var c in chatIds) - await Client.SendTextMessageAsync(c, formatted); + { + if (c.Service == "Telegram") + await TelegramBot.SendTextMessageAsync(c.ChatId, formatted); + else if (c.Service == "Discord") + await SendMessageToDiscordChannel(c.ChatId, formatted); + } } async void BotOnMessageReceived(object sender, MessageEventArgs messageEventArgs) @@ -115,7 +135,7 @@ namespace JetHerald { var reply = await Commands.Execute(sender, messageEventArgs); if (reply != null) - await Client.SendTextMessageAsync( + await TelegramBot.SendTextMessageAsync( chatId: msg.Chat.Id, text: reply, replyToMessageId: msg.MessageId); diff --git a/JetHerald/Startup.cs b/JetHerald/Startup.cs index 0fe5121..793a7d9 100644 --- a/JetHerald/Startup.cs +++ b/JetHerald/Startup.cs @@ -21,6 +21,7 @@ namespace JetHerald { services.Configure(Configuration.GetSection("ConnectionStrings")); services.Configure(Configuration.GetSection("Telegram")); + services.Configure(Configuration.GetSection("Discord")); services.AddSingleton(); services.AddSingleton(); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_3_0); @@ -31,7 +32,7 @@ namespace JetHerald public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { var bot = app.ApplicationServices.GetService(); - bot.Init().Wait(); + bot.Init().GetAwaiter().GetResult(); app.UsePathBase(Configuration.GetValue("PathBase")); if (env.IsDevelopment()) {