was wiped out as it's unused here anyway
- if (renderer.EnableHtmlForBlock)
- {
- renderer.Write("
");
- }
-
- renderer.WriteLeafRawLines(obj, true, true);
-
- if (renderer.EnableHtmlForBlock)
- {
- renderer.WriteLine("
");
- }
-
- renderer.EnsureLine();
-
- }
- }
-}
diff --git a/WebToTelegramCore/Helpers/TelegramMarkdownFormatter.cs b/WebToTelegramCore/Helpers/TelegramMarkdownFormatter.cs
new file mode 100644
index 0000000..b4213ea
--- /dev/null
+++ b/WebToTelegramCore/Helpers/TelegramMarkdownFormatter.cs
@@ -0,0 +1,31 @@
+namespace WebToTelegramCore.Helpers
+{
+ ///
+ /// Static helper to make not hardcoded texts suitable for sending.
+ ///
+ public static class TelegramMarkdownFormatter
+ {
+ ///
+ /// List of characters to escape in order to satisfy the API.
+ ///
+ private readonly static string[] _toEscape = new[]
+ {
+ "_", "*", "[", "]", "(", ")", "~", "`", ">",
+ "#", "+", "-", "=", "|", "{", "}", ".", "!"
+ };
+
+ ///
+ /// Escapes the dangerous symbols in the string.
+ ///
+ ///
String to process.
+ ///
Telegram Markdown v2 friendly string.
+ public static string Escape(string s)
+ {
+ foreach (var c in _toEscape)
+ {
+ s = s.Replace(c, @"\" + c);
+ }
+ return s;
+ }
+ }
+}
diff --git a/WebToTelegramCore/BotCommands/IBotCommand.cs b/WebToTelegramCore/Interfaces/IBotCommand.cs
similarity index 82%
rename from WebToTelegramCore/BotCommands/IBotCommand.cs
rename to WebToTelegramCore/Interfaces/IBotCommand.cs
index ea1d100..a9ed703 100644
--- a/WebToTelegramCore/BotCommands/IBotCommand.cs
+++ b/WebToTelegramCore/Interfaces/IBotCommand.cs
@@ -1,6 +1,6 @@
using WebToTelegramCore.Models;
-namespace WebToTelegramCore.BotCommands
+namespace WebToTelegramCore.Interfaces
{
///
/// Interface to implement various bot commands without arguments.
@@ -17,8 +17,8 @@ public interface IBotCommand
///
/// Method to process received message.
///
- /// Record associated with user who sent teh command,
- /// or null if user has no Record (have not received the token).
+ /// Record associated with user who sent the command,
+ /// or a mock Record with everything but account id set to default values.
/// Message to send back to user. Markdown is supported.
string Process(Record record);
}
diff --git a/WebToTelegramCore/Services/IOwnApiService.cs b/WebToTelegramCore/Interfaces/IOwnApiService.cs
similarity index 60%
rename from WebToTelegramCore/Services/IOwnApiService.cs
rename to WebToTelegramCore/Interfaces/IOwnApiService.cs
index d08acd7..f8d5150 100644
--- a/WebToTelegramCore/Services/IOwnApiService.cs
+++ b/WebToTelegramCore/Interfaces/IOwnApiService.cs
@@ -1,6 +1,7 @@
-using WebToTelegramCore.Models;
+using System.Threading.Tasks;
+using WebToTelegramCore.Models;
-namespace WebToTelegramCore.Services
+namespace WebToTelegramCore.Interfaces
{
///
/// Interface to implement non-Telegram web API handling.
@@ -11,7 +12,6 @@ public interface IOwnApiService
/// Method to handle incoming request from the web API.
///
/// Request to handle.
- /// Response to the request, ready to be returned to client.
- Response HandleRequest(Request request);
+ Task HandleRequest(Request request);
}
}
diff --git a/WebToTelegramCore/Interfaces/IRecordService.cs b/WebToTelegramCore/Interfaces/IRecordService.cs
new file mode 100644
index 0000000..a2275d5
--- /dev/null
+++ b/WebToTelegramCore/Interfaces/IRecordService.cs
@@ -0,0 +1,28 @@
+using WebToTelegramCore.Models;
+
+namespace WebToTelegramCore.Interfaces
+{
+ ///
+ /// Interface that makes managing records look easy for its users.
+ ///
+ public interface IRecordService
+ {
+ ///
+ /// Creates a new Record, setting common default values
+ /// for all instances.
+ ///
+ /// Token to create Record with.
+ /// Account id to create token with.
+ /// Record with all properties populated.
+ Record Create(string token, long accountId);
+
+ ///
+ /// Checks if Record holds enough charges to be able to send a message
+ /// immediately ( > 0). If so, returns true,
+ /// and updates Record's state to indicate the message was sent just now.
+ ///
+ /// Record to check and possibly update.
+ /// True if message can and should be sent, false otherwise.
+ bool CheckIfCanSend(Record record);
+ }
+}
diff --git a/WebToTelegramCore/Services/ITelegramApiService.cs b/WebToTelegramCore/Interfaces/ITelegramApiService.cs
similarity index 81%
rename from WebToTelegramCore/Services/ITelegramApiService.cs
rename to WebToTelegramCore/Interfaces/ITelegramApiService.cs
index 6467854..a7a0aea 100644
--- a/WebToTelegramCore/Services/ITelegramApiService.cs
+++ b/WebToTelegramCore/Interfaces/ITelegramApiService.cs
@@ -1,6 +1,7 @@
-using Update = Telegram.Bot.Types.Update;
+using System.Threading.Tasks;
+using Update = Telegram.Bot.Types.Update;
-namespace WebToTelegramCore.Services
+namespace WebToTelegramCore.Interfaces
{
///
/// Interface to implement Telegram webhook handling.
@@ -19,6 +20,6 @@ public interface ITelegramApiService
/// Method to handle incoming updates from the webhook.
///
/// Received update.
- void HandleUpdate(Update update);
+ Task HandleUpdate(Update update);
}
}
diff --git a/WebToTelegramCore/Interfaces/ITelegramBotService.cs b/WebToTelegramCore/Interfaces/ITelegramBotService.cs
new file mode 100644
index 0000000..8fb0c02
--- /dev/null
+++ b/WebToTelegramCore/Interfaces/ITelegramBotService.cs
@@ -0,0 +1,44 @@
+using System.Threading.Tasks;
+using WebToTelegramCore.Data;
+
+namespace WebToTelegramCore.Interfaces
+{
+ ///
+ /// Interface that exposes greatly simplified version of Telegram bot API.
+ /// The only method allows sending of text messages.
+ ///
+ public interface ITelegramBotService
+ {
+ ///
+ /// Sends text message. Markdown formatting should be supported.
+ ///
+ /// ID of account to send message to.
+ /// Text of the message.
+ /// Flag to set whether to suppress the notification.
+ /// Like the API defaults to false for bot responses, which are sent immediately after user's message.
+ /// Formatting type used in the message.
+ /// Unlike the API, defaults for Markdown for bot responses, which do use some formatting.
+ Task Send(long accountId, string message, bool silent = false, MessageParsingType parsingType = MessageParsingType.Markdown);
+
+ ///
+ /// Sends a predefined sticker. Used as an easter egg with a 5% chance
+ /// of message on unknown user input to be the sticker instead of text.
+ ///
+ /// ID of account to send sticker to.
+ Task SendTheSticker(long accountId);
+
+ ///
+ /// Method to manage external webhook for the Telegram API.
+ /// Part one: called to set the webhook on application start.
+ ///
+ Task SetExternalWebhook();
+
+ ///
+ /// Method to manage external webhook for the Telegram API.
+ /// Part two: called to remove the webhook gracefully on application exit.
+ /// Not that it makes much sense in an app designed to be run 24/7, but is
+ /// nice to have nonetheless.
+ ///
+ Task ClearExternalWebhook();
+ }
+}
diff --git a/WebToTelegramCore/Services/ITokenGeneratorService.cs b/WebToTelegramCore/Interfaces/ITokenGeneratorService.cs
similarity index 92%
rename from WebToTelegramCore/Services/ITokenGeneratorService.cs
rename to WebToTelegramCore/Interfaces/ITokenGeneratorService.cs
index 25294dd..3fabe6e 100644
--- a/WebToTelegramCore/Services/ITokenGeneratorService.cs
+++ b/WebToTelegramCore/Interfaces/ITokenGeneratorService.cs
@@ -1,4 +1,4 @@
-namespace WebToTelegramCore.Services
+namespace WebToTelegramCore.Interfaces
{
///
/// Interface to services that generate auth tokens.
diff --git a/WebToTelegramCore/Models/Record.cs b/WebToTelegramCore/Models/Record.cs
index b850fb3..f1d6cee 100644
--- a/WebToTelegramCore/Models/Record.cs
+++ b/WebToTelegramCore/Models/Record.cs
@@ -9,16 +9,6 @@ namespace WebToTelegramCore.Models
///
public class Record
{
- ///
- /// Maximum possible amount of messages available immidiately.
- ///
- private static int _counterMax;
-
- ///
- /// Boolean indicating whether max value from config was set.
- ///
- private static bool _maxSet = false;
-
///
/// Auth token associated with this record. Primary key in the DB.
///
@@ -30,32 +20,9 @@ public class Record
public long AccountNumber { get; set; }
///
- /// Backing field of UsageCounter property.
+ /// Holds amount of messages available immidiately.
///
- private int _counter;
-
- ///
- /// Holds amount of messages available immidiately, prevents over- and underflows.
- ///
- public int UsageCounter
- {
- get => _counter;
- set
- {
- if (value <= 0)
- {
- _counter = 0;
- }
- else if (value >= _counterMax)
- {
- _counter = _counterMax;
- }
- else
- {
- _counter = value;
- }
- }
- }
+ public int UsageCounter { get; set; }
///
/// Timestamp of last successful request. Used to calculate how much to add
@@ -68,37 +35,5 @@ public int UsageCounter
/// on destructive command.
///
public RecordState State { get; set; }
-
- ///
- /// Constructor that sets up default values for properties not stored in the DB.
- ///
- public Record()
- {
- if (!_maxSet)
- {
- throw new ApplicationException("No default maximum count value was set.");
- }
- _counter = _counterMax;
- LastSuccessTimestamp = DateTime.Now;
- State = RecordState.Normal;
- }
-
- ///
- /// Sets maximum value once. Throws exception when value is already set.
- ///
- /// Value to set.
- public static void SetMaxValue(int value)
- {
- if (!_maxSet)
- {
- _counterMax = value;
- _maxSet = true;
- }
- else
- {
- throw new ApplicationException("Record's maximum count value " +
- "was already set.");
- }
- }
}
}
diff --git a/WebToTelegramCore/Models/Request.cs b/WebToTelegramCore/Models/Request.cs
index 95c9e1a..922702e 100644
--- a/WebToTelegramCore/Models/Request.cs
+++ b/WebToTelegramCore/Models/Request.cs
@@ -1,4 +1,7 @@
-using System.ComponentModel.DataAnnotations;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+using System.ComponentModel.DataAnnotations;
+using WebToTelegramCore.Data;
namespace WebToTelegramCore.Models
{
@@ -19,5 +22,20 @@ public class Request
///
[Required, StringLength(4096)]
public string Message { get; set; }
+
+ ///
+ /// Optional parameter to suppress the notification for the
+ /// message from the bot. If the to true, message will be silent.
+ /// Note that it's possible for the end used to mute the bot so
+ /// this setting will have no effect.
+ ///
+ public bool Silent { get; set; } = false;
+
+ ///
+ /// Optional parameter to set parsing mode of the message.
+ /// Defaults to plaintext if not specified.
+ ///
+ [JsonConverter(typeof(StringEnumConverter))]
+ public MessageParsingType Type { get; set; } = MessageParsingType.Plaintext;
}
}
diff --git a/WebToTelegramCore/Models/Response.cs b/WebToTelegramCore/Models/Response.cs
deleted file mode 100644
index 88249a6..0000000
--- a/WebToTelegramCore/Models/Response.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-using ResponseState = WebToTelegramCore.Data.ResponseState;
-
-namespace WebToTelegramCore.Models
-{
- ///
- /// Represents web API response to user.
- /// Also in JSON, just like the initial request.
- ///
- public class Response
- {
- ///
- /// Quick indicator of whether request was accepted.
- ///
- public bool Ok { get; private set; }
-
- ///
- /// Machine-readable error code.
- ///
- public int Code { get; private set; }
-
- ///
- /// Human-readable description of error.
- ///
- public string Details { get; private set; }
-
- ///
- /// Class constructor.
- ///
- ///
ResponseState field values are based on.
- ///
Human-readable message corresponding to state.
- public Response(ResponseState state, string details)
- {
- Ok = state == ResponseState.OkSent;
- Code = (int)state;
- Details = details;
- }
- }
-}
diff --git a/WebToTelegramCore/Options/LocalizationOptions.cs b/WebToTelegramCore/Options/LocalizationOptions.cs
deleted file mode 100644
index 8001d1d..0000000
--- a/WebToTelegramCore/Options/LocalizationOptions.cs
+++ /dev/null
@@ -1,141 +0,0 @@
-namespace WebToTelegramCore.Options
-{
- ///
- /// Class that holds all customizable string the bot may reply with.
- /// Also, descriptions of error codes.
- ///
- public class LocalizationOptions
- {
- ///
- /// Message to show when token deletion was cancelled.
- ///
- public string CancelDeletion { get; set; }
-
- ///
- /// Message to show when token regeneration was cancelled.
- ///
- public string CancelRegeneration { get; set; }
-
- ///
- /// Message to show when token deletion was completed.
- ///
- public string ConfirmDeletion { get; set; }
-
- ///
- /// Template for message to show when token regeneration was completed.
- /// {0} is new token.
- ///
- public string ConfirmRegeneration { get; set; }
-
- ///
- /// Message to reply to /create with when registration is disabled.
- ///
- public string CreateGoAway { get; set; }
-
- ///
- /// Template for message to show when token was created successfully.
- /// {0} is new token.
- ///
- public string CreateSuccess { get; set; }
-
- ///
- /// Message to show when user initiated token deletion and registration is off.
- ///
- public string DeletionNoTurningBack { get; set; }
-
- ///
- /// Message to show when user initiated token deletion.
- ///
- public string DeletionPending { get; set; }
-
- ///
- /// Message to show when confirmation is pending and user tries to use
- /// non-confirming command.
- ///
- public string ErrorConfirmationPending { get; set; }
-
- ///
- /// Message to show to unknown commands starting with slash.
- ///
- public string ErrorDave { get; set; }
-
- ///
- /// Message to show when usage of command requires no token set,
- /// but user has one.
- ///
- public string ErrorMustBeGuest { get; set; }
-
- ///
- /// Message to show when usage of command requires a token, but user has none.
- ///
- public string ErrorMustBeUser { get; set; }
-
- ///
- /// Message to show when user is trying to use confirming command, but no
- /// confirmation is pending.
- ///
- public string ErrorNoConfirmationPending { get; set; }
-
- ///
- /// Message to show on unknown input.
- ///
- public string ErrorWhat { get; set; }
-
- ///
- /// Output of /help command.
- ///
- public string Help { get; set; }
-
- ///
- /// Message to show when user initiated token regeneration.
- ///
- public string RegenerationPending { get; set; }
-
- ///
- /// Human-readable representation of ResponseState.BandwidthExceeded enum member.
- ///
- public string RequestBandwidthExceeded { get; set; }
-
- ///
- /// Human-readable representation of ResponseState.NoSuchToken enum member.
- ///
- public string RequestNoToken { get; set; }
-
- ///
- /// Human-readable representation of ResponseState.OkSent enum member.
- ///
- public string RequestOk { get; set; }
-
- ///
- /// Human-readable representation of ResponseState.SomethingBadHappened
- /// enum member.
- ///
- public string RequestWhat { get; set; }
-
- ///
- /// Message to warn new users that registration is closed.
- ///
- public string StartGoAway { get; set; }
-
- ///
- /// Message to show when user first engages the bot.
- ///
- public string StartMessage { get; set; }
-
- ///
- /// Message to encourage new users to register.
- ///
- public string StartRegistrationHint { get; set; }
-
- ///
- /// Helper text that explains web API output.
- ///
- public string TokenErrorsDescription { get; set; }
-
- ///
- /// Template for message to reply to /token command with.
- /// {0} is token, {1} is API endpoint URL, {2} is vanity quote.
- ///
- public string TokenTemplate { get; set; }
- }
-}
diff --git a/WebToTelegramCore/Program.cs b/WebToTelegramCore/Program.cs
index aef82ae..01b4068 100644
--- a/WebToTelegramCore/Program.cs
+++ b/WebToTelegramCore/Program.cs
@@ -1,31 +1,57 @@
-using Microsoft.AspNetCore;
+using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
+using System;
+using WebToTelegramCore.Interfaces;
namespace WebToTelegramCore
{
public class Program
{
-
public static void Main(string[] args)
{
- CreateWebHostBuilder(args).Build().Run();
- }
-
- public static IWebHostBuilder CreateWebHostBuilder(string[] args)
- {
- // hardcoded default
+ // hardcoded default, because 8080 and 8081 were already taken on my machine
int port = 8082;
- if (args.Length == 2 && args[0].Equals("--port")
- && System.Int32.TryParse(args[1], out int nonDefPort))
+ if (args.Length == 2 && args[0].Equals("--port") && int.TryParse(args[1], out int nonDefPort))
{
port = nonDefPort;
}
- return WebHost.CreateDefaultBuilder(args)
- .UseKestrel()
- .UseUrls($"http://localhost:{port}")
- .UseStartup
();
+ var builder = WebApplication.CreateBuilder(new WebApplicationOptions
+ {
+ // quick fix to prevent using solution folder for configuration files
+ // instead of built binary folder, where these files are overridden with juicy secrets
+ ContentRootPath = AppContext.BaseDirectory
+ });
+
+ // this is used to force the code to use database from the current (e.g output for debug) directory
+ // instead of using the file in the project root every launch
+
+ // please note that this value ends with backslash, so in the connection string,
+ // file name goes straight after |DataDirectory|, no slashes of any kind
+ AppDomain.CurrentDomain.SetData("DataDirectory", AppContext.BaseDirectory);
+
+ builder.WebHost.UseKestrel(kestrelOptions =>
+ {
+ // this won't work without a SSL proxy over it anyway,
+ // so listening to localhost only
+ kestrelOptions.ListenLocalhost(port);
+ });
+
+ // carrying over legacy startup-based configuration (for now?)
+ var startup = new Startup(builder.Configuration);
+ startup.ConfigureServices(builder.Services);
+
+ var app = builder.Build();
+ app.MapControllers();
+
+ app.Lifetime.ApplicationStarted.Register(async () =>
+ await (app.Services.GetService(typeof(ITelegramBotService)) as ITelegramBotService).SetExternalWebhook());
+
+ app.Lifetime.ApplicationStopped.Register(async () =>
+ await (app.Services.GetService(typeof(ITelegramBotService)) as ITelegramBotService).ClearExternalWebhook());
+
+ app.Run();
}
}
}
diff --git a/WebToTelegramCore/Properties/PublishProfiles/FolderProfile.pubxml b/WebToTelegramCore/Properties/PublishProfiles/FolderProfile.pubxml
deleted file mode 100644
index df979ca..0000000
--- a/WebToTelegramCore/Properties/PublishProfiles/FolderProfile.pubxml
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
- FileSystem
- FileSystem
- Release
- Any CPU
-
- True
- False
- netcoreapp2.1
- 2.1.3
- linux-x64
- 09c905a4-e83d-42f1-9f7c-299082194b74
- false
- <_IsPortable>true
- bin\Debug\netcoreapp2.1\publish\
- True
-
-
\ No newline at end of file
diff --git a/WebToTelegramCore/Properties/launchSettings.json b/WebToTelegramCore/Properties/launchSettings.json
index dadd404..1a8f352 100644
--- a/WebToTelegramCore/Properties/launchSettings.json
+++ b/WebToTelegramCore/Properties/launchSettings.json
@@ -1,8 +1,4 @@
{
- "iisSettings": {
- "windowsAuthentication": false,
- "anonymousAuthentication": true
- },
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"WebToTelegramCore": {
diff --git a/WebToTelegramCore/RecordContext.cs b/WebToTelegramCore/RecordContext.cs
index aaa37e1..0c0e154 100644
--- a/WebToTelegramCore/RecordContext.cs
+++ b/WebToTelegramCore/RecordContext.cs
@@ -1,7 +1,5 @@
using Microsoft.EntityFrameworkCore;
-using System;
-using System.Linq;
-
+using System.Threading.Tasks;
using WebToTelegramCore.Models;
namespace WebToTelegramCore
@@ -44,18 +42,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
///
/// Token to search for in the DB.
/// Associated Record or null if none found.
- public Record GetRecordByToken(string token)
+ public async Task GetRecordByToken(string token)
{
- Record ret;
- try
- {
- ret = Records.Single(r => r.Token.Equals(token));
- }
- catch (InvalidOperationException)
- {
- return null;
- }
- return ret;
+ return await Records.SingleOrDefaultAsync(r => r.Token.Equals(token));
}
///
@@ -63,18 +52,9 @@ public Record GetRecordByToken(string token)
///
/// ID to search for.
/// Associated Record or null if none present.
- public Record GetRecordByAccountId(long accountId)
+ public async Task GetRecordByAccountId(long accountId)
{
- Record ret;
- try
- {
- ret = Records.Single(r => r.AccountNumber == accountId);
- }
- catch (InvalidOperationException)
- {
- return null;
- }
- return ret;
+ return await Records.SingleOrDefaultAsync(r => r.AccountNumber == accountId);
}
}
}
diff --git a/WebToTelegramCore/Resources/Locale.cs b/WebToTelegramCore/Resources/Locale.cs
new file mode 100644
index 0000000..05fc594
--- /dev/null
+++ b/WebToTelegramCore/Resources/Locale.cs
@@ -0,0 +1,212 @@
+namespace WebToTelegramCore.Resources
+{
+ ///
+ /// Class that holds all customizable string the bot may reply with.
+ /// Also, descriptions of error codes.
+ /// Not ideal, but slightly better (is it?) than holding these in appsettings.json.
+ ///
+
+ // Please note that these should be formatted as Telegram-flavoured Markdown
+ // see https://core.telegram.org/bots/api#markdownv2-style
+ // Templating parameters (like {0}) are filled in before sending to the API,
+ // so { and } in these should NOT be escaped. But the actual text in these should be.
+
+ public static class Locale
+ {
+ ///
+ /// Template for reply for /about command. {0} is assembly version.
+ ///
+ public const string About = """
+ **Dotnet Telegram forwarder** v {0}\.
+
+ [Open\-source\!](https://github.com/bnfour/dotnet-telegram-forwarder)
+ by bnfour, 2018, 2020\-2023\.
+ """;
+
+ ///
+ /// Message to show when token deletion was cancelled.
+ ///
+ public const string CancelDeletion = """
+ Token deletion cancelled\.
+ """;
+
+ ///
+ /// Message to show when token regeneration was cancelled.
+ ///
+ public const string CancelRegeneration = """
+ Token regeneration cancelled\.
+ """;
+
+ ///
+ /// Message to show when token deletion was completed.
+ ///
+ public const string ConfirmDeletion = """
+ Token deleted\.
+ Thank you for giving us an oppurtunity to serve you\.
+ """;
+
+ ///
+ /// Template for message to show when token regeneration was completed.
+ /// {0} is new token.
+ ///
+ public const string ConfirmRegeneration = """
+ Your new token is
+
+ `{0}`
+
+ Don't forget to update your clients' settings\.
+ """;
+
+ ///
+ /// Message to reply to /create with when registration is disabled.
+ ///
+ public const string CreateGoAway = """
+ This instance of the bot is not accepting new users for now\.
+ """;
+
+ ///
+ /// Template for message to show when token was created successfully.
+ /// {0} is new token.
+ ///
+ public const string CreateSuccess = """
+ Success\! Your token is:
+
+ `{0}`
+
+ Please consult /token for usage\.
+ """;
+
+ ///
+ /// Message to show when user initiated token deletion and registration is off.
+ ///
+ public const string DeletionNoTurningBack = """
+ This bot has registration turned *off*\. You won't be able to create new token\. Please be certain\.
+ """;
+
+ ///
+ /// Message to show when user initiated token deletion.
+ ///
+ public const string DeletionPending = """
+ *Token deletion pending\!*
+
+ Please either /confirm or /cancel it\. This cannot be undone\.
+ If you need to change your token, consider to /regenerate it instead of deleting and re\-creating\.
+ """;
+
+ ///
+ /// Message to show when confirmation is pending and user tries to use
+ /// non-confirming command.
+ ///
+ public const string ErrorConfirmationPending = """
+ You have an operation pending confirmation\. Please /confirm or /cancel it before using other commands\.
+ """;
+
+ ///
+ /// Message to show to unknown commands starting with slash.
+ ///
+ public const string ErrorDave = """
+ I'm afraid I can't let you do that\.
+ """;
+
+ ///
+ /// Message to show when usage of command requires no token set,
+ /// but user has one.
+ ///
+ public const string ErrorMustBeGuest = """
+ In order to use this command, you must have no token associated with your account\. You can /delete your existing one, but why?
+ """;
+
+ ///
+ /// Message to show when usage of command requires a token, but user has none.
+ ///
+ public const string ErrorMustBeUser = """
+ In order to use this command, you must have a token associated with your account\. /create one\.
+ """;
+
+ ///
+ /// Message to show when user is trying to use confirming command, but no
+ /// confirmation is pending.
+ ///
+ public const string ErrorNoConfirmationPending = """
+ This command is only useful when you're trying to /delete or /regenerate your token\.
+ """;
+
+ ///
+ /// Message to show on unknown input.
+ ///
+ public const string ErrorWhat = """
+ Unfortunately, I'm not sure how to interpret this\.
+ """;
+
+ ///
+ /// Output of /help command.
+ ///
+ public const string Help = """
+ This app provides a web API to route messages from API's endpoint to you in Telegram via this bot\. It can be used to notify you in Telegram from any Internet\-enabled device you want \(provided you know how to make POST requests from it\)\.
+ To start, /create your token\. You can /delete it anytime\. To change your token for whatever reason, please /regenerate it and not delete and re\-create\.
+ Once you have a token, see /token for additional usage help\.
+
+ Other commands supported by bot include:
+ \- /confirm and /cancel are used to prevent accidental deletions and regenerations;
+ \- /help displays this message;
+ \- /about displays general info about this bot\.
+
+ \-\-bnfour
+ """;
+
+ ///
+ /// Message to show when user initiated token regeneration.
+ ///
+ public const string RegenerationPending = """
+ *Token regenration pending\!*
+
+ Please either /confirm or /cancel it\. It cannot be undone\. Please be certain\.
+ """;
+
+ ///
+ /// Message to warn new users that registration is closed.
+ ///
+ public const string StartGoAway = """
+ Sorry, this instance of bot is not accepting new users for now\.
+ """;
+
+ ///
+ /// Message to show when user first engages the bot.
+ ///
+ public const string StartMessage = """
+ Hello\!
+
+ This bot provides a standalone web API to relay messages from anything that can send web requests to Telegram as messages from the bot\.
+
+ *Please note*: this requires some external tools and knowledge\. If you consider "Send a POST request" a magic gibberish you don't understand, this bot probably isn't much of use to you\.
+ """;
+
+ ///
+ /// Message to encourage new users to register.
+ ///
+ public const string StartRegistrationHint = """
+ If that does not stop you, feel free to /create your very own token\.
+ """;
+
+ ///
+ /// Template for message to reply to /token command with.
+ /// {0} is token, {1} is API endpoint URL, {2} is vanity quote.
+ ///
+ // double braces are escaping for formatting
+ public const string TokenTemplate = """
+ Your token is
+
+ `{0}`
+
+ *Usage:* To deliver a message, send a POST request to {1} with JSON body\. The payload must contain two parameters: your token and your message\. There are also optional parameters, please consult [the documentation](https://github.com/bnfour/dotnet-telegram-forwarder#web-api) for details\. Example of a payload:
+ ```
+ {{
+ "token": "{0}",
+ "message": "{2}"
+ }}
+ ```
+
+ If everything is okay, the API will return a blank 200 OK response\. If something is not okay, a different status code will be returned\. Consult [the documentation](https://github.com/bnfour/dotnet-telegram-forwarder#web-api) to see error code list\.
+ """;
+ }
+}
diff --git a/WebToTelegramCore/Services/FormatterService.cs b/WebToTelegramCore/Services/FormatterService.cs
deleted file mode 100644
index b159676..0000000
--- a/WebToTelegramCore/Services/FormatterService.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-using System.IO;
-using System.Linq;
-using WebToTelegramCore.FormatterHelpers;
-
-namespace WebToTelegramCore.Services
-{
- ///
- /// Class that converts Markdown to HTML.
- ///
- public class FormatterService : IFormatterService
- {
- ///
- /// Instance of MarkdownTransformer to use.
- ///
- private readonly MarkdownTransformer _transformer = new MarkdownTransformer();
-
- ///
- /// Transforms Markdown to HTML. Expects Markdown like Telegram clients do:
- /// **bold** and __italic__ are main differences from the CommonMark.
- ///
- /// Markdown-formatted text.
- /// Text with same format but in HTML supported by Telegram's API.
- public string TransformToHtml(string TgFlavoredMarkdown)
- {
- // TODO think how to reuse these
- TextWriter writer = new StringWriter();
- var renderer = new Markdig.Renderers.HtmlRenderer(writer);
-
- renderer = new Markdig.Renderers.HtmlRenderer(writer)
- {
- ImplicitParagraph = true
- };
-
- // replacing default renderes with our "fixed" versions
- var toRemove = renderer.ObjectRenderers.Where(x =>
- x.GetType() == typeof(Markdig.Renderers.Html.CodeBlockRenderer) ||
- x.GetType() == typeof(Markdig.Renderers.Html.Inlines.LinkInlineRenderer))
- .ToList();
- foreach (var r in toRemove)
- {
- renderer.ObjectRenderers.Remove(r);
- }
- renderer.ObjectRenderers.Add(new NonNestedHtmlLinkInlineRenderer());
- renderer.ObjectRenderers.Add(new TweakedCodeBlockRenderer());
-
- // setting up rendering pipeline
- var pipeline = new Markdig.MarkdownPipelineBuilder().Build();
- pipeline.Setup(renderer);
-
- // actually rendering the HTML
- var cm = _transformer.ToCommonMark(TgFlavoredMarkdown);
- var doc = Markdig.Parsers.MarkdownParser.Parse(cm);
- renderer.Render(doc);
- writer.Flush();
- var formatted = writer.ToString();
- formatted = formatted
- .Replace("
", "
");
- return formatted;
- }
- }
-}
diff --git a/WebToTelegramCore/Services/IFormatterService.cs b/WebToTelegramCore/Services/IFormatterService.cs
deleted file mode 100644
index cc2e5da..0000000
--- a/WebToTelegramCore/Services/IFormatterService.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-namespace WebToTelegramCore.Services
-{
- ///
- /// Interface that exposes method to convert Markdown flavor that used in
- /// Telegram clients to HTML.
- ///
- public interface IFormatterService
- {
- ///
- /// Transforms Markdown to HTML. Expects Markdown like Telegram clients do:
- /// **bold** and __italic__ are main differences from the CommonMark.
- ///
- /// Markdown-formatted text.
- /// Text with same format but in HTML supported by Telegram's API.
- string TransformToHtml(string TgFlavoredMarkdown);
- }
-}
diff --git a/WebToTelegramCore/Services/ITelegramBotService.cs b/WebToTelegramCore/Services/ITelegramBotService.cs
deleted file mode 100644
index 279327b..0000000
--- a/WebToTelegramCore/Services/ITelegramBotService.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-namespace WebToTelegramCore.Services
-{
- ///
- /// Interface that exposes greatly simplified version of Telegram bot API.
- /// The only method allows sending of text messages.
- ///
- public interface ITelegramBotService
- {
- ///
- /// Sends text message. Markdown formatting should be supported.
- ///
- /// ID of account to send message to.
- /// Text of the message.
- void Send(long accountId, string message);
-
- ///
- /// Sends a predefined sticker. Used as an easter egg with a 5% chance
- /// of message on unknown user input to be the sticker instead of text.
- ///
- /// ID of account to send sticker to.
- void SendTheSticker(long accountId);
-
- ///
- /// Sends message in CommonMark as Markdown. Used only internally as a crutch
- /// to display properly formatteded pre-defined messages. HTML breaks them :(
- ///
- /// ID of account to send message to.
- /// Text of the message.
- void SendPureMarkdown(long accountId, string message);
- }
-}
diff --git a/WebToTelegramCore/Services/OwnApiService.cs b/WebToTelegramCore/Services/OwnApiService.cs
index 3398e9d..f8c128a 100644
--- a/WebToTelegramCore/Services/OwnApiService.cs
+++ b/WebToTelegramCore/Services/OwnApiService.cs
@@ -1,10 +1,7 @@
-using Microsoft.Extensions.Options;
-using System;
-using System.Collections.Generic;
-
-using WebToTelegramCore.Data;
+using System.Threading.Tasks;
+using WebToTelegramCore.Exceptions;
+using WebToTelegramCore.Interfaces;
using WebToTelegramCore.Models;
-using WebToTelegramCore.Options;
namespace WebToTelegramCore.Services
{
@@ -13,11 +10,6 @@ namespace WebToTelegramCore.Services
///
public class OwnApiService : IOwnApiService
{
- ///
- /// Amount of seconds lince last successful API call to regenerate counter.
- ///
- private readonly int _secondsPerRegen;
-
///
/// Field to store app's database context.
///
@@ -29,9 +21,9 @@ public class OwnApiService : IOwnApiService
private readonly ITelegramBotService _bot;
///
- /// Holds string representations of various response results.
+ /// Field to store Record management service.
///
- private readonly Dictionary _details;
+ private readonly IRecordService _recordService;
///
/// Constuctor that injects dependencies.
@@ -39,79 +31,28 @@ public class OwnApiService : IOwnApiService
/// Database context to use.
/// Bot service to use.
/// Bandwidth options.
- /// Localization options.
- public OwnApiService(RecordContext context, ITelegramBotService bot,
- IOptions options, IOptions locale)
+ public OwnApiService(RecordContext context, ITelegramBotService bot, IRecordService recordService)
{
_context = context;
_bot = bot;
-
- _secondsPerRegen = options.Value.SecondsPerRegeneration;
-
-
- LocalizationOptions locOptions = locale.Value;
- _details = new Dictionary()
- {
- [ResponseState.OkSent] = locOptions.RequestOk,
- [ResponseState.BandwidthExceeded] = locOptions.RequestBandwidthExceeded,
- [ResponseState.NoSuchToken] = locOptions.RequestNoToken,
- [ResponseState.SomethingBadHappened] = locOptions.RequestWhat
- };
+ _recordService = recordService;
}
///
- /// Public method to handle incoming requests. Call underlying internal method
- /// and wraps its output into a Response.
+ /// Public method to handle incoming requests. Call underlying internal method.
///
/// Request to handle.
- /// Response to the request, ready to be returned to client.
- public Response HandleRequest(Request request)
+ public async Task HandleRequest(Request request)
{
- ResponseState result = HandleRequestInternally(request);
- return new Response(result, _details[result]);
- }
-
- ///
- /// Internal method to handle requests.
- ///
- /// Request to handle.
- /// ResponseState indicatig result of the request processing.
- private ResponseState HandleRequestInternally(Request request)
- {
- // wrapping all these into a big try clause
- // returning ResponseState.SomethingBadHappened in catch is silly
- // but how else API can be made "robust"?
- // TODO: think about role of ResponseState.SomethingBadHappened
- var record = _context.GetRecordByToken(request.Token);
- if (record == null)
- {
- return ResponseState.NoSuchToken;
- }
- UpdateRecordCounter(record);
- if (record.UsageCounter > 0)
+ var record = await _context.GetRecordByToken(request.Token) ?? throw new TokenNotFoundException();
+ if (_recordService.CheckIfCanSend(record))
{
- record.LastSuccessTimestamp = DateTime.Now;
- record.UsageCounter--;
- _bot.Send(record.AccountNumber, request.Message);
- return ResponseState.OkSent;
+ await _bot.Send(record.AccountNumber, request.Message, request.Silent, request.Type);
}
else
{
- return ResponseState.BandwidthExceeded;
+ throw new BandwidthExceededException();
}
}
-
- ///
- /// Updates Record's usage counter based on time passed since last successful
- /// message delivery.
- ///
- /// Record to update.
- private void UpdateRecordCounter(Record record)
- {
- TimeSpan sinceLastSuccess = DateTime.Now - record.LastSuccessTimestamp;
- int toAdd = (int)(sinceLastSuccess.TotalSeconds / _secondsPerRegen);
- // overflows are handled in the property's setter
- record.UsageCounter += toAdd;
- }
}
}
diff --git a/WebToTelegramCore/Services/RecordService.cs b/WebToTelegramCore/Services/RecordService.cs
new file mode 100644
index 0000000..75ce038
--- /dev/null
+++ b/WebToTelegramCore/Services/RecordService.cs
@@ -0,0 +1,78 @@
+using Microsoft.Extensions.Options;
+using System;
+using WebToTelegramCore.Data;
+using WebToTelegramCore.Interfaces;
+using WebToTelegramCore.Models;
+using WebToTelegramCore.Options;
+
+namespace WebToTelegramCore.Services
+{
+ ///
+ /// Class that makes managing Records look easy for its users.
+ /// hide_the_pain_harold.jpg
+ ///
+ public class RecordService : IRecordService
+ {
+ ///
+ /// Maximum possible amount of messages available immidiately.
+ ///
+ private readonly int _counterMax;
+
+ ///
+ /// Amount of seconds lince last successful API call to regenerate counter.
+ ///
+ private readonly TimeSpan _timeToRegen;
+
+ ///
+ /// Constructor that gets message bandwiths settings for later use.
+ ///
+ /// Options to use.
+ public RecordService(IOptions options)
+ {
+ _counterMax = options.Value.InitialCount;
+ _timeToRegen = TimeSpan.FromSeconds(options.Value.SecondsPerRegeneration);
+ }
+
+ ///
+ /// Checks if Record holds enough charges to be able to send a message
+ /// immediately ( > 0). If so, returns true,
+ /// and updates Record's state to indicate the message was sent just now.
+ ///
+ /// Record to check and possibly update.
+ /// True if message can and should be sent, false otherwise.
+ public bool CheckIfCanSend(Record record)
+ {
+ var sinceLastSuccess = DateTime.UtcNow - record.LastSuccessTimestamp;
+ var toAdd = (int)(sinceLastSuccess / _timeToRegen);
+
+ record.UsageCounter = Math.Min(_counterMax, record.UsageCounter + toAdd);
+
+ if (record.UsageCounter > 0)
+ {
+ record.LastSuccessTimestamp = DateTime.UtcNow;
+ record.UsageCounter--;
+ return true;
+ }
+ return false;
+ }
+
+ ///
+ /// Creates a new Record, setting common default values
+ /// for all instances.
+ ///
+ /// Token to create Record with.
+ /// Account id to create token with.
+ /// Record with all properties populated.
+ public Record Create(string token, long accountId)
+ {
+ return new Record
+ {
+ AccountNumber = accountId,
+ LastSuccessTimestamp = DateTime.UtcNow,
+ State = RecordState.Normal,
+ Token = token,
+ UsageCounter = _counterMax
+ };
+ }
+ }
+}
diff --git a/WebToTelegramCore/Services/TelegramApiService.cs b/WebToTelegramCore/Services/TelegramApiService.cs
index cb36f12..200ab74 100644
--- a/WebToTelegramCore/Services/TelegramApiService.cs
+++ b/WebToTelegramCore/Services/TelegramApiService.cs
@@ -2,12 +2,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Threading.Tasks;
using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums;
using WebToTelegramCore.BotCommands;
+using WebToTelegramCore.Interfaces;
using WebToTelegramCore.Models;
using WebToTelegramCore.Options;
+using WebToTelegramCore.Resources;
namespace WebToTelegramCore.Services
{
@@ -37,79 +40,58 @@ public class TelegramApiService : ITelegramApiService
private readonly ITokenGeneratorService _generator;
///
- /// List of commands available to the bot.
- ///
- private readonly List _commands;
-
- ///
- /// /create handler, as it requires special treating since i'm bad at programming.
- ///
- private readonly CreateCommand _thatOneCommand;
-
- ///
- /// Indicates whether usage of /create command is enabled.
- ///
- private readonly bool _isRegistrationOpen;
-
- ///
- /// Message to reply with when input is starting with slash, but none of the
- /// commands fired in response.
+ /// Record manipulation service helper reference.
///
- private readonly string _invalidCommandReply;
+ private readonly IRecordService _recordService;
///
- /// Message to reply with when input isn't even resembles a command.
+ /// List of commands available to the bot.
///
- private readonly string _invalidReply;
+ private readonly List _commands;
///
/// Constructor that injects dependencies and configures list of commands.
///
/// Options that include token.
- /// Localization options.
/// Database context to use.
/// Bot service instance to use.
/// Token generator service to use.
- public TelegramApiService(IOptions options,
- IOptions locale, RecordContext context,
- ITelegramBotService bot, ITokenGeneratorService generator)
+ /// Record helper service to use.
+ public TelegramApiService(IOptions options,
+ RecordContext context, ITelegramBotService bot,
+ ITokenGeneratorService generator, IRecordService recordService)
{
_token = options.Value.Token;
_context = context;
_bot = bot;
_generator = generator;
+ _recordService = recordService;
- _isRegistrationOpen = options.Value.RegistrationEnabled;
-
- LocalizationOptions locOptions = locale.Value;
-
- _invalidCommandReply = locOptions.ErrorDave;
- _invalidReply = locOptions.ErrorWhat;
+ var isRegistrationOpen = options.Value.RegistrationEnabled;
_commands = new List()
{
- new StartCommand(locOptions, _isRegistrationOpen),
- new TokenCommand(locOptions, options.Value.ApiEndpointUrl),
- new RegenerateCommand(locOptions),
- new DeleteCommand(locOptions, _isRegistrationOpen),
- new ConfirmCommand(locOptions, _context, _generator),
- new CancelCommand(locOptions),
- new HelpCommand(locOptions),
- new DirectiveCommand(locOptions),
- new AboutCommand(locOptions)
-
+ new StartCommand(isRegistrationOpen),
+ new TokenCommand(options.Value.ApiEndpointUrl),
+ new RegenerateCommand(),
+ new DeleteCommand(isRegistrationOpen),
+ new ConfirmCommand(_context, _generator, _recordService),
+ new CancelCommand(),
+ new HelpCommand(),
+ new DirectiveCommand(),
+ new AboutCommand(),
+ new CreateCommand(_context, _generator, _recordService, isRegistrationOpen)
};
- _thatOneCommand = new CreateCommand(locOptions, _context, _generator,
- _isRegistrationOpen);
}
///
/// Method to handle incoming updates from the webhook.
///
/// Received update.
- public void HandleUpdate(Update update)
+ public async Task HandleUpdate(Update update)
{
- // a few sanity checks
+ // a few sanity checks:
+ // only handles text messages, hopefully commands
if (update.Message.Type != MessageType.Text)
{
return;
@@ -117,32 +99,27 @@ public void HandleUpdate(Update update)
long? userId = update?.Message?.From?.Id;
string text = update?.Message?.Text;
-
- if (userId == null || String.IsNullOrEmpty(text))
+ // and the update contains everything we need to process it
+ if (userId == null || string.IsNullOrEmpty(text))
{
return;
}
- // null check was done above, it's safe to use userId.Value directly
- Record record = _context.GetRecordByAccountId(userId.Value);
+ // if user has no record associated, make him a mock one with just an account number,
+ // so we know who they are in case we're going to create them a proper one
+ Record record = await _context.GetRecordByAccountId(userId.Value)
+ ?? _recordService.Create(null, userId.Value);
IBotCommand handler = null;
- if (text.StartsWith(_thatOneCommand.Command))
- {
- handler = _thatOneCommand;
- _thatOneCommand.Crutch = userId.Value;
- }
- else
- {
- _thatOneCommand.Crutch = null;
- handler = _commands.SingleOrDefault(c => text.StartsWith(c.Command));
- }
+ string commandText = text.Split(' ').FirstOrDefault();
+ // will crash if multiple command classes share same text, who cares
+ handler = _commands.SingleOrDefault(c => c.Command.Equals(commandText));
if (handler != null)
{
- _bot.SendPureMarkdown(userId.Value, handler.Process(record));
+ await _bot.Send(userId.Value, handler.Process(record));
}
else
{
- HandleUnknownText(userId.Value, text);
+ await HandleUnknownText(userId.Value, commandText);
}
}
@@ -164,18 +141,19 @@ public bool IsToken(string calledToken)
/// User to reply to.
/// Received message that was not processed
/// by actual commands.
- private void HandleUnknownText(long accountId, string text)
+ private async Task HandleUnknownText(long accountId, string text)
{
// suddenly, cat!
if (new Random().Next(0, 19) == 0)
{
- _bot.SendTheSticker(accountId);
+ await _bot.SendTheSticker(accountId);
}
else
{
- string reply = text.StartsWith("/") ?
- _invalidCommandReply : _invalidReply;
- _bot.Send(accountId, reply);
+ string reply = text.StartsWith("/")
+ ? Locale.ErrorDave
+ : Locale.ErrorWhat;
+ await _bot.Send(accountId, reply);
}
}
}
diff --git a/WebToTelegramCore/Services/TelegramBotService.cs b/WebToTelegramCore/Services/TelegramBotService.cs
index 557a582..893362a 100644
--- a/WebToTelegramCore/Services/TelegramBotService.cs
+++ b/WebToTelegramCore/Services/TelegramBotService.cs
@@ -1,9 +1,12 @@
using Microsoft.Extensions.Options;
+using System;
+using System.Threading.Tasks;
using Telegram.Bot;
using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums;
using Telegram.Bot.Types.InputFiles;
-
+using WebToTelegramCore.Data;
+using WebToTelegramCore.Interfaces;
using WebToTelegramCore.Options;
namespace WebToTelegramCore.Services
@@ -31,34 +34,37 @@ public class TelegramBotService : ITelegramBotService
private readonly InputOnlineFile _sticker = new InputOnlineFile(_theStickerID);
///
- /// Field to store used instance of formatter.
+ /// Field to store our webhook URL to be advertised to Telegram's API.
///
- private readonly IFormatterService _formatter;
+ private readonly string _webhookUrl;
///
- /// Constructor that also sets up the webhook.
+ /// Constructor that get the options required for this service to operate.
///
/// Options to use.
- /// Formatter to use.
- public TelegramBotService(IOptions options,
- IFormatterService formatter)
+ public TelegramBotService(IOptions options)
{
_client = new TelegramBotClient(options.Value.Token);
+ // made unclear that "api" part is needed as well, shot myself in the leg 3 years after
+ _webhookUrl = options.Value.ApiEndpointUrl + "/api/" + options.Value.Token;
+ }
- _formatter = formatter;
-
- var webhookUrl = options.Value.ApiEndpointUrl + "/" + options.Value.Token;
- // this code is dumb and single-threaded. _Maybe_ later
- _client.SetWebhookAsync(webhookUrl,
- allowedUpdates: new[] { UpdateType.Message });
+ ///
+ /// Method to manage external webhook for the Telegram API.
+ /// Part one: called to set the webhook on application start.
+ ///
+ public async Task SetExternalWebhook()
+ {
+ await _client.SetWebhookAsync(_webhookUrl, allowedUpdates: new[] { UpdateType.Message });
}
///
- /// Destructor that removes the webhook.
+ /// Method to manage external webhook for the Telegram API.
+ /// Part two: called to remove the webhook gracefully on application exit.
///
- ~TelegramBotService()
+ public async Task ClearExternalWebhook()
{
- _client.DeleteWebhookAsync();
+ await _client.DeleteWebhookAsync();
}
///
@@ -66,34 +72,38 @@ public TelegramBotService(IOptions options,
///
/// ID of the account to send to.
/// Markdown-formatted message.
- public void Send(long accountId, string message)
+ /// Flag to set whether to suppress the notification.
+ /// Formatting type used in the message.
+ public async Task Send(long accountId, string message, bool silent = false, MessageParsingType parsingType = MessageParsingType.Markdown)
{
// I think we have to promote account ID back to ID of chat with this bot
var chatId = new ChatId(accountId);
- _client.SendTextMessageAsync(chatId, _formatter.TransformToHtml(message),
- ParseMode.Html, true);
+
+ await _client.SendTextMessageAsync(chatId, message,
+ ResolveRequestParseMode(parsingType), disableWebPagePreview: true,
+ disableNotification: silent);
}
///
/// Method to send predefined sticker on behalf of the bot.
///
/// ID of the account to send to.
- public void SendTheSticker(long accountId)
+ public async Task SendTheSticker(long accountId)
{
var chatId = new ChatId(accountId);
- _client.SendStickerAsync(chatId, _sticker);
+ await _client.SendStickerAsync(chatId, _sticker);
}
- ///
- /// Sends message in CommonMark as Markdown. Used only internally as a crutch
- /// to display properly formatteded pre-defined messages. HTML breaks them :(
- ///
- /// ID of account to send message to.
- /// Text of the message.
- public void SendPureMarkdown(long accountId, string message)
+ private ParseMode? ResolveRequestParseMode(MessageParsingType fromRequest)
{
- var chatId = new ChatId(accountId);
- _client.SendTextMessageAsync(chatId, message, ParseMode.Markdown, true);
+ return fromRequest switch
+ {
+ MessageParsingType.Plaintext => null,
+ MessageParsingType.Markdown => ParseMode.MarkdownV2,
+ // should never happen, but for sake of completeness,
+ // fall back to plaintext
+ _ => null
+ };
}
}
}
diff --git a/WebToTelegramCore/Services/TokenGeneratorService.cs b/WebToTelegramCore/Services/TokenGeneratorService.cs
index b56c388..92ddbf0 100644
--- a/WebToTelegramCore/Services/TokenGeneratorService.cs
+++ b/WebToTelegramCore/Services/TokenGeneratorService.cs
@@ -1,6 +1,8 @@
using System;
using System.Security.Cryptography;
+using System.Linq;
using System.Text;
+using WebToTelegramCore.Interfaces;
namespace WebToTelegramCore.Services
{
@@ -22,18 +24,19 @@ internal class TokenGeneratorService : ITokenGeneratorService
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz").ToCharArray();
///
- /// Strong random instance used to generate random tokens.
+ /// DB context. Used to check for collisions with existing tokens.
///
- private readonly RandomNumberGenerator _random;
+ private readonly RecordContext _context;
///
/// Class constructor.
///
- public TokenGeneratorService()
+ public TokenGeneratorService(RecordContext context)
{
- // let's pretend we're serious business for a moment
- _random = RandomNumberGenerator.Create();
+ _context = context;
+
// sanity check for random evenness
+ // TODO consider it to be a warning instead of an exception
if (256 % _alphabet.Length != 0)
{
throw new ApplicationException("Selected alphabet does not map evenly " +
@@ -42,13 +45,33 @@ public TokenGeneratorService()
}
///
- /// Token generation method.
+ /// Generates a token and ensures it is not yet assigned to other accounts.
///
- /// Token.
+ /// An unique token.
public string Generate()
+ {
+ string token = null;
+ var done = false;
+ while (!done)
+ {
+ token = GenerateRandom();
+ done = !_context.Records.Any(r => r.Token == token);
+ }
+ return token;
+ }
+
+ ///
+ /// Actual token generation method.
+ ///
+ /// A completely random token.
+ private string GenerateRandom()
{
var randomBytes = new byte[_tokenLength];
- _random.GetBytes(randomBytes);
+ // let's pretend we're serious business for a moment
+ using (var random = RandomNumberGenerator.Create())
+ {
+ random.GetBytes(randomBytes);
+ }
var sb = new StringBuilder();
foreach (var b in randomBytes)
diff --git a/WebToTelegramCore/Startup.cs b/WebToTelegramCore/Startup.cs
index 845cea0..b2ba015 100644
--- a/WebToTelegramCore/Startup.cs
+++ b/WebToTelegramCore/Startup.cs
@@ -1,15 +1,10 @@
-using Microsoft.AspNetCore.Builder;
-using Microsoft.AspNetCore.Hosting;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
-
+using WebToTelegramCore.Interfaces;
using WebToTelegramCore.Options;
using WebToTelegramCore.Services;
-using Record = WebToTelegramCore.Models.Record;
-
namespace WebToTelegramCore
{
public class Startup
@@ -24,40 +19,25 @@ public Startup(IConfiguration configuration)
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
- services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
-
// singleton makes changes to non-db properties persistent
services.AddDbContext(options =>
options.UseSqlite(Configuration.GetConnectionString("DefaultConnection")),
ServiceLifetime.Singleton, ServiceLifetime.Singleton);
- services.AddSingleton();
services.AddSingleton();
- services.AddSingleton();
services.AddScoped();
services.AddScoped();
+ services.AddScoped();
- // Options pattern to the rescue?
- services.Configure(Configuration.GetSection("General"));
- services.Configure(Configuration.GetSection("Bandwidth"));
- services.Configure(Configuration.GetSection("Strings"));
- // loading this explicitly as there's no straightforward way to pass options
- // to models; I can be wrong though
- // TODO: see if there's a better way
- var preload = Configuration.GetSection("Bandwidth").GetValue("InitialCount");
- Record.SetMaxValue(preload);
- }
+ services.AddTransient();
- // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
- public void Configure(IApplicationBuilder app, IHostingEnvironment env)
- {
- if (env.IsDevelopment())
- {
- app.UseDeveloperExceptionPage();
- }
+ // Telegram.Bot's Update class relies on some Newtonsoft attributes,
+ // so to deserialize it correctly, we need to use this library as well
+ services.AddControllers().AddNewtonsoftJson();
- app.UseMvc();
+ services.Configure(Configuration.GetSection("General"));
+ services.Configure(Configuration.GetSection("Bandwidth"));
}
}
}
diff --git a/WebToTelegramCore/WebToTelegramCore.csproj b/WebToTelegramCore/WebToTelegramCore.csproj
index 64c0dac..c07ebd4 100644
--- a/WebToTelegramCore/WebToTelegramCore.csproj
+++ b/WebToTelegramCore/WebToTelegramCore.csproj
@@ -1,24 +1,26 @@
- netcoreapp2.1
- 1.2.0.0
+ net7.0
+ 2.0.0.0
bnfour
Dotnet Telegram forwarder
- © 2018 bnfour
+ © 2018, 2020–2023, bnfour
-
-
-
-
+
+
+
PreserveNewest
+
+ PreserveNewest
+
diff --git a/WebToTelegramCore/appsettings.json b/WebToTelegramCore/appsettings.json
index 30a4cd6..1fc1bf1 100644
--- a/WebToTelegramCore/appsettings.json
+++ b/WebToTelegramCore/appsettings.json
@@ -1,47 +1,20 @@
{
- "Logging": {
- "LogLevel": {
- "Default": "Warning"
- }
- },
- "AllowedHosts": "*",
- "ConnectionStrings": {
- "DefaultConnection": "Data Source=database.sqlite"
- },
- "General": {
- "Token": "it's a secret",
- "ApiEndpointUrl": "also a secret for now",
- "RegistrationEnabled": true
- },
- "Bandwidth": {
- "InitialCount": 20,
- "SecondsPerRegeneration": 60
- },
- "Strings": {
- "CancelDeletion": "Token deletion cancelled.",
- "CancelRegeneration": "Token regeneration cancelled.",
- "ConfirmDeletion": "Token deleted. Thank you for giving us an oppurtunity to serve you.",
- "ConfirmRegeneration": "Your new token is\n\n`{0}`\n\nDon't forget to update your clients' settings.",
- "CreateGoAway": "This instance of bot is not accepting new users for now.",
- "CreateSuccess": "Success! Your token is:\n\n`{0}`\n\nPlease consult /token for usage.",
- "DeletionNoTurningBack": "This bot has registration turned *off*. You won't be able to create new token. Please be certain.",
- "DeletionPending": "*Token deletion pending!*\n\nPlease either /confirm or /cancel it. It cannot be undone.\nIf you need to change your token, please consider to /regenerate it instead of deleting and re-creating it.",
- "ErrorConfirmationPending": "You have an operation pending cancellation. Please /confirm or /cancel it before using other commands.",
- "ErrorDave": "I'm afraid I can't let you do that.",
- "ErrorMustBeGuest": "In order to use this command, you must have no token associated with your account. You can /delete your existing one, but why?",
- "ErrorMustBeUser": "In order to use this command, you must have a token associated with your account. Try running /create first.",
- "ErrorNoConfirmationPending": "This command is only useful when you're trying to /delete or /regenerate your token.",
- "ErrorWhat": "Unfortunately, I'm not sure how to interpret this.",
- "Help": "This bot provides web API to route messages from API's endpoint to you in Telegram via bot. It can be used to notify you in Telegram from any Internet-enabled device you want (provided you know how to make POST requests from it).\nTo start, /create your token (if this particular instance of bot has registration of new users open). You can /delete it anytime. To change your token for whatever reason, please /regenerate it and not delete and re-create.\nOnce you have a token, see /token for additional usage help.\nOther commands supported by bot include:\n- /confirm and /cancel are used to prevent accidental deletions and regenerations;\n- /help displays this message;\n- /about displays general info about this bot.\n\nThere's also an easter egg command and a rare response to unknown commands: one way to satisfy your curiosity is to check out bot's source code.\n\n--bnfour",
- "RegenerationPending": "*Token regenration pending!*\n\nPlease either /confirm or /cancel it. It cannot be undone. Please be certain.",
- "RequestBandwidthExceeded": "Too many messages. Try again later.",
- "RequestNoToken": "Invalid token.",
- "RequestOk": "Request accepted.",
- "RequestWhat": "[UNUSED] Something bad happened",
- "StartGoAway": "Sorry, this instance of bot is not accepting new users for now.",
- "StartMessage": "Hello!\n\nThis bot provides a standalone web API to relay messages from whatever you'll use it from to Telegram as messages from the bot. It might come in handy to unify your notifications in one place.\n\n*Please note*: this requires some external tools. If you consider phrases like \"Send a POST request to the endpoint with JSON body with two string fields\" a magic gibberish you don't understand, this bot probably isn't much of use to you.",
- "StartRegistrationHint": "If that does not stop you, feel free to /create your very own token.",
- "TokenErrorsDescription": "If you send a malformed request, the API will return `400 Bad Request`. If request is properly formed, `200 OK` is returned, along with a response in JSON. The response contains three fields:\n- *Ok: boolean.* If `true`, your message is received and will be sent via bot shortly. If `false`, something went wrong, see the next field.\n- *Code: int.* Represents various error codes:\n-- 0: No error. Sent when *Ok* is true;\n-- 1: No such token found in database. Your client should not retry same request but should ask user to double-check their settings;\n-- 2: Bandwidth exceeded. Bot's throughput is limited. You shouldn't see this unless you send *a lot* of messages, but when you do, please wait a few minutes before retrying your request.\n- *Details: string.* Human-readable description of *Code* field, similar to the text above.\n\n There's also always a possibility of `500 Internal Server Error`.",
- "TokenTemplate": "Your token is\n\n`{0}`\n\n*Usage*:\nSend a POST request to {1} with JSON body. Body must contain two string fields: \"token\" with your token, and \"message\" with text to send via bot.\n\n*Example*:\n```\n{{\n \"token\": \"{0}\",\n \"message\": \"{2}\"\n}}\n```"
+ "Logging": {
+ "LogLevel": {
+ "Default": "Warning"
}
+ },
+ "AllowedHosts": "*",
+ "ConnectionStrings": {
+ "DefaultConnection": "Data Source=|DataDirectory|database.sqlite"
+ },
+ "General": {
+ "Token": "it's a secret",
+ "ApiEndpointUrl": "also a secret for now",
+ "RegistrationEnabled": true
+ },
+ "Bandwidth": {
+ "InitialCount": 20,
+ "SecondsPerRegeneration": 60
+ }
}
diff --git a/readme.md b/readme.md
index a7dd1ec..af3c620 100644
--- a/readme.md
+++ b/readme.md
@@ -1,73 +1,80 @@
# Dotnet Telegram forwarder
-(temporary name that stuck since I can't think of a better one)
-An ASP.NET Core app providing HTTP API for notifications via Telegram bot.
-
-## What's this?
-My attempt to grasp the basics of both ASP.NET and EF by writing moderately useful app.
-Telegram bot is used to provide auth tokens to users and to actually notify them when something makes a request to the provided web API.
-This can be used to deliver all kinds of notifications via Telegram.
+("temporary" generic name from the very start of development)
+An app providing HTTP API to deliver arbitrary text notifications via associated Telegram bot from anywhere where making HTTP POST requests is available.
## Status
-Pretty much works on my server (sorry, registration's closed). Your mileage may vary.
-I still have to write some actual clients though.
+Operational. First version has been serving me flawlessly (as I can tell) since mid 2018.
## Description
-Let's try [readme driven development](http://tom.preston-werner.com/2010/08/23/readme-driven-development.html) this time. So this app consists of two parts: Telegram bot and a web API.
+Let's try [readme driven development](http://tom.preston-werner.com/2010/08/23/readme-driven-development.html) this time. So this app consists of two equally important parts: Telegram bot and a web API.
### Web API
-Has only one method to send the notification. Its endpoint listens for requests with JSON body containing auth token and message to deliver. (Messages should support markdown.)
-Request should look like that:
-```
-{
- "token": "basically `[0-9a-zA-Z+=]{16}`>",
- "message": "text to be delivered via bot"
-}
-```
-Messages use the same format Telegram does: major differences are `**bold**` for **bold**, `__italic__` for *italic*. Also, `<` and `>` should be escaped as `<` and `>` for notifications to work. Complex formatting may break down the makeshift "parser", test notification to get through.
+Has only one method to send the notification. Its endpoint listens for POST requests with JSON body. Actual endpoint URL will be provided via the bot itself when a new token is created.
-Tokens are tied to Telegram IDs internally, also there is limitations on how often messages can be sent: every token has up to 20 instant deliveries,
-with one regenerating every minute after last successful message delivery.
-If request was correctly formed, API will respond with another JSON object:
+#### Request
+Request's body has this structure:
```
{
- "ok": boolean, (if true message is accepted. If false, some kind of error happened),
- "code": int, (represents error codes in machine-friendly format),
- "details": string (human-friendly error description)
+ "token": string,
+ "type": (optional) string,
+ "message": string,
+ "silent": (optional) boolean
}
```
-Possible error codes:
-* 0 -- No error, message sent. Used when "ok" is true;
-* 1 -- No such token. Token is in valid format but is not found in the database;
-* 2 -- Bandwidth exceeded. Too many messages in a given amount of time. Client should wait at least a minute (by default) before retrying.
+* Token is this service's user identifier, randomly generated per Telegram user, with abilities to withdraw it or replace with a new one anytime. It's a 16 characters long string that may contain alphanumerics, and plus and equals signs (So `[0-9a-zA-Z+=]{16}`).
+* Type is used to select between two supported parse modes: `"plaintext"` for plain text, and `"markdown"` for MarkdownV2 as described in Telegram docs [here](https://core.telegram.org/bots/api#markdownv2-style). If value is not supplied, defaults to `"plaintext"`. These two are separated, because Telegram flavoured Markdown requires escaping for a fairly common plaintext punctuation marks, and will fail if not formed correctly.
+* Message is the text of the message to be sent via the bot. Maximum length is 4096 (also happens to be a maximum length of one Telegram message).
+* Silent is boolean to indicate whether them message from the bot in Telegram will come with a notification with sound. Behaves what you'd expect. If not supplied, defaults to `false`. Please note that the end user is able to mute the bot, effectively rendering this option useless.
-If request isn't in correct format, blank 400 Bad Request is thrown instead.
+#### Response
+API returns an empty HTTP response with any of the following status codes:
+* `200 OK` if everything is indeed OK and message should be expeted to be delivered via the bot
+No further actions from the client required.
+* `400 Bad Request` if the user request is malformed and cannot be processed
+Client should check that the request is well-formed and meet the specifications, and retry with the fixed request.
+* `404 Not Found` if supplied token is not present in the database
+Client should check that the token they provided is valid and has not been removed or changed, and retry with the correct one.
+* `429 Too Many Requests` if current limit of sent messages is exhausted
+Client should retry later, after waiting at least one minute (on default throughput config).
+* `500 Internal Server Error` in case anything goes wrong
+Client can try to retry later, but ¯\\\_(ツ)\_/¯
+#### Rate limitation
+The API has a rate limitation, preventing large(ish) amount of notifications in a short amount of time. By default (can be adjusted via config files), every user has 20 ...message points, I guess? Every notification sent removes 1 message point, and requests will be refused with `429 Too Many Requests` status code when all points are depleted. A single point is regenerated every minute after last message was sent.
+For instance, if API is used to send 40 notifications in quick succession, only 20 first messages will be sent to the user. If client waits 5 minutes after API starts responding with 429's, they will be able to send 5 more messages instantenously before hitting the limit again. After 20 minutes of idle time since the last successfully sent message, the API will behave as usual.
### Telegram bot
The bot is used both to deliver messages and to obtain token for requests for a given account.
It has some commands:
* `/start` -- obligatory one, contains short description;
-* `/help` -- also obligatory, displays output similar to this text;
+* `/help` -- also obligatory, displays output similar to this section;
* `/about` -- displays basic info about the bot, like link back to this repo;
-* `/token` -- reminds user's token if there is one, also API usage hints;
+* `/token` -- reminds user's token if there is one, also API usage hints and endpoint location;
* `/create` -- generates new token for user if none present;
* `/regenerate` -- once confirmed (see `/confirm` and `/cancel`) replaces user's token with a new one;
* `/delete` -- once confirmed removes user's token;
* `/confirm` -- confirms regeneration or deletion;
* `/cancel` -- cancels regeneration or deletion.
-When there is destructive (either regeneration or deletion) operation pending after initial command, only `/cancel` or `/confirm` commands are
-accepted.
+The ability for anyone to create a token for themselves is toggleable via config entry. You can always run direct queries against bot's DB for quick editing. Note that messages will not be delivered unless user actually engaged in a conversation with the bot.
+
+When there is a destructive (either regeneration or deletion) operation pending, only `/cancel` or `/confirm` commands are accepted.
## Configuration and deployment
+You'll need .NET 7 runtime. Hosting on GNU/Linux in a proper data center is highly encouraged.
By default, listens on port 8082. This can be changed with `--port ` command-line argument.
Rest of the settings are inside `appsettings.json`:
-* "Strings" section contains all the customizable strings just in case localization is ever needed.
-* "General" section contains Telegram's bot token, API endpoint URL as seen from outside world (certainly not localhost:8082 as Kestrel would told you it listens to) and a boolean that controls whether new users can create tokens.
+* "General" section contains Telegram's bot token, API endpoint URL as seen from outside world (certainly not localhost:8082 as Kestrel would told you it listens to) and a boolean that controls whether new users can create tokens. Please note that `/api` will be appended to this address. So, for example, if you set `https://foo.example.com/bar` here, actual endpoint to be used with the app (both for Telegram API webhooks and for user messages) will be `https://foo.example.com/bar/api`
* "Bandwidth" section controls bot's throughput: maximum amount of messages to be delivered at once and amount of seconds to regenerate one message delivery is set here.
-To deploy this bot, you'll need something that will append SSL as required by Telegram. As always, I recommend `nginx` as a reverse proxy. Another quirk is that launching via `./WebToTelegramCore`
-didn't worked with my setup: server had ASP.NET Core 2.1.3 runtime, but app was expecting 2.1.2. `dotnet WebToTelegramCore.dll` worked though.
+To deploy this bot, you'll need something that will append SSL as required by Telegram. As always with Telegram bots, I recommend `nginx` as a reverse proxy. You'll need to set up HTTPS as well.
-## Possible TODO
-Make a companion website that also allows token manipulation just like the bot?
\ No newline at end of file
+## Version history
+* **v 1.0**, 2018-08-29
+Initial release. More an excercise in ASP.NET Core than an app I needed at the moment. Actually helped me to score a software engineering job and turned out to be moderately useful tool.
+* **v 1.2**, 2018-10-05
+Greatly increased the reliability of Markdown parsing in one of the most **not** straightforward ways you can imagine -- by converting the Markdown to HTML with a few custom convertion quirks.
+* **no version number**, 2020-05-14
+Shelved attempt to improve the codebase. Consists of one architecture change and is fully ~~included~~ rewritten in the next release.
+* **v 2.0**, 2023-05-06
+Really proper markdown support this time (Telegram's version with questionable selection of characters to be escaped), option to send a silent notification, async everthing, .NET 7, HTTP status codes instead of custom errors, and probably something else I forgot about.