From b9c43550aa0f29014142b221c4561287a1280571 Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Sun, 8 Jan 2023 22:57:00 +0100 Subject: [PATCH] Initial commit --- .gitignore | 34 ++++++ Program.cs | 172 ++++++++++++++++++++++++++++++ README.md | 26 +++++ Telegram.Bot.DecisionMaker.csproj | 14 +++ Telegram.Bot.DecisionMaker.sln | 16 +++ 5 files changed, 262 insertions(+) create mode 100644 .gitignore create mode 100644 Program.cs create mode 100644 README.md create mode 100644 Telegram.Bot.DecisionMaker.csproj create mode 100644 Telegram.Bot.DecisionMaker.sln diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..58a7791 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +bin/ +obj/ +/packages/ +riderModule.iml +.idea/ +/_ReSharper.Caches/ + +# Created by https://www.toptal.com/developers/gitignore/api/macos +# Edit at https://www.toptal.com/developers/gitignore?templates=macos + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +.apdisk + +# End of https://www.toptal.com/developers/gitignore/api/macos diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..9544d44 --- /dev/null +++ b/Program.cs @@ -0,0 +1,172 @@ +using Telegram.Bot; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; + +var token = Environment.GetEnvironmentVariable("TELEGRAM_BOT_TOKEN")!; + +var bot = new TelegramBotClient(token); +var me = await bot.GetMeAsync(); +using var cts = new CancellationTokenSource(); + +await bot.SetMyCommandsAsync(new[] { + new BotCommand { Command = "help", Description = "Lists all commands" }, + new BotCommand { Command = "coinflip", Description = "Flips a coin" }, + new BotCommand { Command = "shouldi", Description = "Tells you if you should or shouldn't do something" }, + new BotCommand { Command = "yesno", Description = "Answes binary questions" }, + new BotCommand { Command = "makedecision", Description = "Makes a decision. Expects a list of comma separated options." }, + new BotCommand { Command = "choose", Description = "Chooses an item. Expects a list of comma separated options." }, +}); + +var iBotCommands = new List { + "coinflip", + "shouldi", + "yesno", + "makedecision", + "choose", + "help", + "start" +}; + +bot.StartReceiving(HandleUpdateAsync, PollingErrorHandler, null, cts.Token); +Console.WriteLine($"Start listening for @{me.Username}"); +Console.ReadLine(); +cts.Cancel(); + +Task PollingErrorHandler(ITelegramBotClient ibot, Exception ex, CancellationToken ct) { + Console.WriteLine($"Exception while polling for updates: {ex}"); + return Task.CompletedTask; +} + +async Task HandleUpdateAsync(ITelegramBotClient ibot, Update update, CancellationToken ct) { + try { + await (update.Type switch { + UpdateType.Message => BotOnMessageReceived(ibot, update.Message!), + _ => Task.CompletedTask + }); + } + catch (Exception ex) { + Console.WriteLine($"Exception while handling {update.Type}: {ex}"); + } +} + +async Task BotOnMessageReceived(ITelegramBotClient botClient, Message message) { + var command = ""; + var query = ""; + try { + // if we didn't receive any text, return + if (string.IsNullOrWhiteSpace(message.Text)) + return; + + // we need to figure out which way of sending commands is being used + // let's get the first word of the message + var firstword = message.Text!.Split(" ")[0]; + + // is it a slash command? + if (firstword.StartsWith("/")) { + // trim explicit username mention + command = firstword.Replace($"@{me.Username}", "").TrimStart('/'); + // remove command (and optional username) from message text + query = message.Text?[firstword.Length..]; + } + else { + // are we sure the bot is being asked? + if (message.Chat.Type == ChatType.Private || message.Text!.Contains($"@{me.Username}")) { + var iquery = message.Text.Replace($"@{me.Username}", "").Trim(); + if (string.IsNullOrWhiteSpace(iquery)) + return; + + // is the first word a command? + firstword = iquery.Split(" ")[0]; + if (iBotCommands.Contains(firstword)) { + command = firstword; + query = iquery[firstword.Length..]; + } + else { + // try to guess what was meant + if (iquery.ToLowerInvariant().Contains(" or ") && !iquery.Contains(',')) { + command = "makedecision"; + query = iquery.Replace(" or ", ",").TrimEnd('?'); + } + else if (iquery.EndsWith("?")) { + command = "yesno"; + } + else if (iquery.ToLowerInvariant().Contains("flip") && iquery.ToLowerInvariant().Contains("coin")) { + command = "coinflip"; + } + } + } + } + + switch (command) { + case "coinflip": + await botClient.SendTextMessageAsync(message.Chat.Id, RandomOption(new[] { "Heads", "Tails" }), replyToMessageId: message.MessageId); + break; + case "shouldi": + await botClient.SendTextMessageAsync(message.Chat.Id, RandomOption(new[] { "Go for it!", "Can't hurt to try!", "Probably not.", "I wouldn't recommend it." }), + replyToMessageId: message.MessageId); + break; + case "yesno": + await botClient.SendTextMessageAsync(message.Chat.Id, RandomOption(new[] { "Yeah!", "Nah..." }), replyToMessageId: message.MessageId); + break; + case "makedecision": + case "choose": + var options = query?.Split(",").Where(p => !string.IsNullOrWhiteSpace(p)).ToList(); + if (options == null || !options.Any()) + options = new List { "Syntax error. (Can't select from zero options)" }; + await botClient.SendTextMessageAsync(message.Chat.Id, RandomOption(options).Trim(), replyToMessageId: message.MessageId); + break; + case "help": + case "start": + await botClient.SendTextMessageAsync(message.Chat.Id, """ + Heya, I am a bot that can help you make decisions! Here's a list of things I can do: + + /coinflip - flips a coin + /shouldi - when you are unsure if you should do something + /yesno - answers more generic yes/no questions + /makedecision - helps you make a decision (chooses from a comma separated list of options; aliased to /choose) + + In direct messages, these also work without the / at the start. + + If your message doesn't start with a recognized command, I'll do my best to figure out what you are asking. + + This currently works with the following queries: + - `thing1 or thing2 or thing3` gets translated into `/makedecision thing1,thing2,thing3` + - `flip a coin` gets translated into `/coinflip` + - `this is a question?` gets translated to `/yesno this is a question` + + If there's an error, please forward the stacktrace message to @zotan (my creator). You can also DM them for feature requests. + + Enjoy! + + - Powered by [~zotan](https://zotan.pw)'s [Telegram.Bot.DecisionMaker](https://git.ztn.sh/zotan/Telegram.Bot.DecisionMaker) v1.0 + """, replyToMessageId: message.MessageId, parseMode: ParseMode.Markdown, disableWebPagePreview: true); + break; + default: + await botClient.SendTextMessageAsync(message.Chat.Id, $""" + Apologies, I can't figure out what you want me to do >w< + Message received: `{message.Text}` + Parsed command: `{command}` + Parsed query: `{query}` + """, replyToMessageId: message.MessageId, parseMode: ParseMode.Markdown); + break; + } + } + catch (Exception e) { + await botClient.SendTextMessageAsync(message.Chat.Id, $""" + Error processing message. + Message content: `{message.Text}` + Parsed command: `{command}` + Parsed query: `{query}` + + Exception: `{e.Message.Replace("`", "'")}` + Stacktrace: `{e.StackTrace?.Replace("`", "'")}` + """, replyToMessageId: message.MessageId, parseMode: ParseMode.Markdown); + throw; + } +} + +static string RandomOption(IReadOnlyList options) { + var random = new Random(Guid.NewGuid().GetHashCode()); + var index = random.Next(options.Count); + return options[index]; +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..5afdc2a --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# Telegram.Bot.DecisionMaker +Telegram bot that helps you make decisions. Production instance running on [@decisionparalysisbot](https://t.me/decisionparalysisbot). + +In case the official instance ever goes down, here's how to set this up yourself: +- Obtain your own bot token from [@BotFather](https://t.me/BotFather) +- Start this bot using `TELEGRAM_BOT_TOKEN=yourtoken dotnet run` + +systemd service example: +``` +# /etc/systemd/system/decisionbot.service +[Unit] +Description=Telegram.Bot.DecisionMaker +Wants=network-online.target nss-lookup.target +After=network-online.target nss-lookup.target + +[Service] +Type=simple +User=botuser +WorkingDirectory=/opt/Telegram.Bot.DecisionMaker +Environment=TELEGRAM_BOT_TOKEN='yourtoken' +ExecStart=/usr/bin/dotnet run +Restart=on-failure + +[Install] +WantedBy=multi-user.target +``` diff --git a/Telegram.Bot.DecisionMaker.csproj b/Telegram.Bot.DecisionMaker.csproj new file mode 100644 index 0000000..4b8f47b --- /dev/null +++ b/Telegram.Bot.DecisionMaker.csproj @@ -0,0 +1,14 @@ + + + + Exe + net7.0 + enable + enable + + + + + + + diff --git a/Telegram.Bot.DecisionMaker.sln b/Telegram.Bot.DecisionMaker.sln new file mode 100644 index 0000000..149b372 --- /dev/null +++ b/Telegram.Bot.DecisionMaker.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Telegram.Bot.DecisionMaker", "Telegram.Bot.DecisionMaker.csproj", "{DF92F931-77E4-4DA3-BC79-2E56CC4C016B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {DF92F931-77E4-4DA3-BC79-2E56CC4C016B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DF92F931-77E4-4DA3-BC79-2E56CC4C016B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DF92F931-77E4-4DA3-BC79-2E56CC4C016B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DF92F931-77E4-4DA3-BC79-2E56CC4C016B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal