Overseer and HttpsMessageBuilder
This commit is contained in:
182
Telegram/HtmlMessageBuilder.cs
Normal file
182
Telegram/HtmlMessageBuilder.cs
Normal file
@@ -0,0 +1,182 @@
|
||||
|
||||
using System.Net.Mime;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace InstaFollowersOverseer;
|
||||
|
||||
/// <summary>
|
||||
/// Class that builds selegram message with html tags.
|
||||
/// It is a state machine.
|
||||
/// Exapmple:
|
||||
/// SetUrl("https://x.com").BeginStyle(TextStyle.Url).Text("X").EndStyle()
|
||||
/// opens html a tag with href=https://x.com and content X, then closes tag
|
||||
/// </summary>
|
||||
// supported tags:
|
||||
// <b>bold</b>
|
||||
// <i>italic</i>
|
||||
// <s>crossed</s>
|
||||
// <u>underline</u>
|
||||
// <tg-spoiler>spoiler</tg-spoiler>
|
||||
// <a href="http://www.example.com/">inline URL</a>
|
||||
// <a href="tg://user?id=123456789">inline mention of a user</a>
|
||||
// <code>inlne code</code>
|
||||
// <pre language="c++">code block</pre>
|
||||
public class HtmlMessageBuilder
|
||||
{
|
||||
record struct BuilderState(TextStyle Style, string? Url = null, long? UserId = null, string? CodeLang = null)
|
||||
{
|
||||
public void Reset()
|
||||
{
|
||||
Style = TextStyle.PlainText;
|
||||
Url = null;
|
||||
UserId = null;
|
||||
CodeLang = null;
|
||||
}
|
||||
}
|
||||
|
||||
private BuilderState _state=new(TextStyle.PlainText);
|
||||
StringBuilder _plainText=new();
|
||||
StringBuilder _html=new();
|
||||
|
||||
protected void ReplaceHtmlReservedChar(char c) =>
|
||||
_html.Append(c switch
|
||||
{
|
||||
'<'=>"<",
|
||||
'>'=>">",
|
||||
'&'=>"&apm",
|
||||
'"'=>""",
|
||||
'\''=>"&apos",
|
||||
_ => c
|
||||
});
|
||||
|
||||
protected void ReplaceHtmlReservedChars(ReadOnlySpan<char> text)
|
||||
{
|
||||
for (int i = 0; i < text.Length; i++)
|
||||
ReplaceHtmlReservedChar(text[i]);
|
||||
}
|
||||
|
||||
/// opens html tags enabled in state fields
|
||||
protected void OpenTags()
|
||||
{
|
||||
if(_state.Style==TextStyle.PlainText)
|
||||
return;
|
||||
|
||||
// the order of fields is very importang, it must be in reversed in CloseTags()
|
||||
if (0!=(_state.Style & TextStyle.Bold)) _html.Append("<b>");
|
||||
if (0!=(_state.Style & TextStyle.Italic)) _html.Append("<i>");
|
||||
if (0!=(_state.Style & TextStyle.Crossed)) _html.Append("<s>");
|
||||
if (0!=(_state.Style & TextStyle.Underline)) _html.Append("<u>");
|
||||
if (0!=(_state.Style & TextStyle.Spoiler)) _html.Append("<tg-spoiler>");
|
||||
if (0!=(_state.Style & TextStyle.Link))
|
||||
{
|
||||
_html.Append("<a href='");
|
||||
if (_state.UserId is not null)
|
||||
_html.Append("tg://user?id=").Append(_state.UserId);
|
||||
else if (!_state.Url.IsNullOrEmpty())
|
||||
_html.Append(_state.Url);
|
||||
else throw new Exception("empty url");
|
||||
_html.Append("'>");
|
||||
}
|
||||
if (0!=(_state.Style & TextStyle.CodeLine)) _html.Append("<code>");
|
||||
if (0!=(_state.Style & TextStyle.CodeBlock))
|
||||
{
|
||||
_html.Append("<pre");
|
||||
if (!_state.CodeLang.IsNullOrEmpty())
|
||||
_html.Append(" language='").Append(_state.CodeLang).Append('\'');
|
||||
_html.Append('>');
|
||||
}
|
||||
}
|
||||
|
||||
/// closes opened html tags
|
||||
protected void CloseTags()
|
||||
{
|
||||
if(_state.Style==TextStyle.PlainText)
|
||||
return;
|
||||
|
||||
// the order of fields is very importang, it must be in reversed in CloseTags()
|
||||
if (0!=(_state.Style & TextStyle.CodeBlock)) _html.Append("</pre>");
|
||||
if (0!=(_state.Style & TextStyle.CodeLine)) _html.Append("</code>");
|
||||
if (0!=(_state.Style & TextStyle.Link)) _html.Append("</a>");
|
||||
if (0!=(_state.Style & TextStyle.Spoiler)) _html.Append("</tg-spoiler>");
|
||||
if (0!=(_state.Style & TextStyle.Underline)) _html.Append("</u>");
|
||||
if (0!=(_state.Style & TextStyle.Crossed)) _html.Append("</s>");
|
||||
if (0!=(_state.Style & TextStyle.Italic)) _html.Append("</i>");
|
||||
if (0!=(_state.Style & TextStyle.Bold)) _html.Append("</b>");
|
||||
_state.Reset();
|
||||
}
|
||||
|
||||
/// appends text to builder
|
||||
public HtmlMessageBuilder Text(string text)
|
||||
{
|
||||
_plainText.Append(text);
|
||||
ReplaceHtmlReservedChars(text);
|
||||
return this;
|
||||
}
|
||||
public HtmlMessageBuilder Text(char ch)
|
||||
{
|
||||
_plainText.Append(ch);
|
||||
ReplaceHtmlReservedChar(ch);
|
||||
return this;
|
||||
}
|
||||
public HtmlMessageBuilder Text(int o)
|
||||
{
|
||||
string text = o.ToString();
|
||||
_plainText.Append(text);
|
||||
ReplaceHtmlReservedChars(text);
|
||||
return this;
|
||||
}
|
||||
public HtmlMessageBuilder Text(long o)
|
||||
{
|
||||
string text = o.ToString();
|
||||
_plainText.Append(text);
|
||||
ReplaceHtmlReservedChars(text);
|
||||
return this;
|
||||
}
|
||||
public HtmlMessageBuilder Text(object o)
|
||||
{
|
||||
if (o is null)
|
||||
throw new NullReferenceException("object is null");
|
||||
string text = o.ToString()!;
|
||||
_plainText.Append(text);
|
||||
ReplaceHtmlReservedChars(text);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// enables specified styles
|
||||
public HtmlMessageBuilder BeginStyle(TextStyle style)
|
||||
{
|
||||
if (_state.Style != TextStyle.PlainText)
|
||||
throw new Exception("can't begin new style before ending previous");
|
||||
_state.Style = style;
|
||||
OpenTags();
|
||||
return this;
|
||||
}
|
||||
|
||||
/// removes all styles
|
||||
public HtmlMessageBuilder EndStyle()
|
||||
{
|
||||
CloseTags();
|
||||
return this;
|
||||
}
|
||||
|
||||
// use before BeginStyle
|
||||
public HtmlMessageBuilder SetUrl(string url) { _state.Url = url; return this; }
|
||||
public HtmlMessageBuilder SetUserMention(long id) { _state.UserId=id ; return this; }
|
||||
public HtmlMessageBuilder SetCodeLanguage(string codeLang="") { _state.CodeLang=codeLang ; return this; }
|
||||
|
||||
public string ToPlainText() => _plainText.ToString();
|
||||
public string ToHtml() => _html.ToString();
|
||||
|
||||
#if DEBUG
|
||||
public override string ToString() => ToHtml();
|
||||
#else
|
||||
public override string ToString() => ToPlainText();
|
||||
#endif
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_plainText.Clear();
|
||||
_html.Clear();
|
||||
_state.Reset();
|
||||
}
|
||||
}
|
||||
81
Telegram/Overseer.cs
Normal file
81
Telegram/Overseer.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using InstaFollowersOverseer.Instagram;
|
||||
|
||||
namespace InstaFollowersOverseer;
|
||||
|
||||
public static class Overseer
|
||||
{
|
||||
private static ContextLogger ObserverLogger = new("observer",ParentLogger);
|
||||
|
||||
private static CancellationTokenSource OverseeCancel = new();
|
||||
|
||||
public static async void Start()
|
||||
{
|
||||
try
|
||||
{
|
||||
ObserverLogger.LogInfo("observer is starting");
|
||||
while (!OverseeCancel.Token.IsCancellationRequested)
|
||||
{
|
||||
ObserverLogger.LogDebug("loop begins");
|
||||
// parallel diff computation per telegram user
|
||||
Parallel.ForEach(CurrentUsersData.UsersDict,
|
||||
tgUserData =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var instaUsers = tgUserData.Value;
|
||||
long chatId = tgUserData.Key.ToLong();
|
||||
|
||||
// parallel diff computation per instagram user
|
||||
Parallel.For(0, instaUsers.Count, DiffInstaUser);
|
||||
|
||||
async void DiffInstaUser(int i)
|
||||
{
|
||||
try
|
||||
{
|
||||
HtmlMessageBuilder b = new();
|
||||
ObserverLogger.LogInfo($"comparing followers lists of user {instaUsers[i]}");
|
||||
// slow operation
|
||||
FollowersDiff diff =
|
||||
await InstagramWrapper.GetFollowersDiffAsync(instaUsers[i].instagramUsername);
|
||||
|
||||
b.BeginStyle(TextStyle.Bold | TextStyle.Underline)
|
||||
.Text(instaUsers[i].instagramUsername)
|
||||
.EndStyle()
|
||||
.Text('\n');
|
||||
diff.AppendDiffMessageTo(b, OverseeCancel.Token);
|
||||
ObserverLogger.LogInfo($"sending notification to {tgUserData.Key}");
|
||||
await TelegramWrapper.SendInfo(chatId, b);
|
||||
}
|
||||
catch(OperationCanceledException){}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ObserverLogger.LogWarn("ObserveLoop", ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
catch (OperationCanceledException) {}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ObserverLogger.LogWarn("ObserveLoop", ex);
|
||||
}
|
||||
});
|
||||
|
||||
ObserverLogger.LogDebug("loop ends");
|
||||
await Task.Delay(TimeSpan.FromMinutes(CurrentConfig.checksIntervalMinutes));
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) {}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ObserverLogger.LogError("ObserveLoop", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static void Stop()
|
||||
{
|
||||
ObserverLogger.LogInfo("observer is stopping");
|
||||
OverseeCancel.Cancel();
|
||||
OverseeCancel = new CancellationTokenSource();
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,18 @@
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using Telegram.Bot;
|
||||
using Telegram.Bot.Polling;
|
||||
using Telegram.Bot.Types;
|
||||
using Telegram.Bot.Types.Enums;
|
||||
using InstaFollowersOverseer.Instagram;
|
||||
|
||||
namespace InstaFollowersOverseer.Telegram;
|
||||
namespace InstaFollowersOverseer;
|
||||
|
||||
public static class TelegramWrapper
|
||||
{
|
||||
private static ContextLogger TelegramLogger = new("telegram", ParentLogger);
|
||||
private static TelegramBotClient Bot=null!;
|
||||
|
||||
public static async void Init()
|
||||
public static async Task InitAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -35,7 +34,7 @@ public static class TelegramWrapper
|
||||
};
|
||||
TelegramLogger.LogInfo("bot starting recieving long polls");
|
||||
Bot.StartReceiving(BotApiUpdateHandler, BotApiExceptionHandler, receiverOptions, Program.MainCancelToken);
|
||||
TelegramLogger.LogInfo("telegram wrapper have initialized successfully");
|
||||
TelegramLogger.LogInfo("telegram wrapper initialized successfully");
|
||||
}
|
||||
catch (OperationCanceledException) {}
|
||||
catch (Exception ex)
|
||||
@@ -50,20 +49,34 @@ public static class TelegramWrapper
|
||||
TelegramLogger.LogError(ex);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
static async Task SendInfoReply(string text, Message replyToMessage)
|
||||
|
||||
/// parses text from markdown to html and sends to telegram chat
|
||||
public static async Task SendMessage(ChatId chatId, HtmlMessageBuilder message, int? replyToMesId=null)
|
||||
{
|
||||
TelegramLogger.LogInfo(text);
|
||||
await Bot.SendTextMessageAsync(replyToMessage.Chat, text,
|
||||
replyToMessageId: replyToMessage.MessageId,
|
||||
parseMode:ParseMode.MarkdownV2);
|
||||
string html = message.ToHtml();
|
||||
await Bot.SendTextMessageAsync(chatId, html,
|
||||
replyToMessageId: replyToMesId,
|
||||
parseMode: ParseMode.Html);
|
||||
message.Clear();
|
||||
}
|
||||
static async Task SendErrorReply(string text, Message replyToMessage)
|
||||
|
||||
public static async Task SendInfo(ChatId chatId, HtmlMessageBuilder message, int? replyToMesId=null)
|
||||
{
|
||||
TelegramLogger.LogWarn(text);
|
||||
await Bot.SendTextMessageAsync(replyToMessage.Chat, "error: "+text,
|
||||
replyToMessageId: replyToMessage.MessageId,
|
||||
parseMode:ParseMode.MarkdownV2);
|
||||
TelegramLogger.LogInfo(message);
|
||||
await SendMessage(chatId, message, replyToMesId);
|
||||
}
|
||||
|
||||
public static async Task SendError(ChatId chatId, HtmlMessageBuilder message, int? replyToMesId=null)
|
||||
{
|
||||
TelegramLogger.LogWarn(message);
|
||||
await SendMessage(chatId, new HtmlMessageBuilder().BeginStyle(TextStyle.Bold | TextStyle.Italic)
|
||||
.Text("error: ").EndStyle().Text(message), replyToMesId);
|
||||
}
|
||||
public static async Task SendError(ChatId chatId, Exception ex, int? replyToMesId=null)
|
||||
{
|
||||
TelegramLogger.LogWarn(ex);
|
||||
await SendMessage(chatId, new HtmlMessageBuilder().BeginStyle(TextStyle.Bold | TextStyle.Italic)
|
||||
.Text("error: ").EndStyle().Text(ex.Message), replyToMesId);
|
||||
}
|
||||
|
||||
private static async Task BotApiUpdateHandler(ITelegramBotClient bot, Update update, CancellationToken cls)
|
||||
@@ -107,28 +120,66 @@ public static class TelegramWrapper
|
||||
|
||||
private static async Task ExecCommandAsync(string command, string[] args, Message message)
|
||||
{
|
||||
switch (command)
|
||||
try
|
||||
{
|
||||
case "start":
|
||||
await Bot.SendTextMessageAsync(message.Chat, "hi");
|
||||
break;
|
||||
case "oversee":
|
||||
HtmlMessageBuilder rb = new();
|
||||
long senderId = message.From?.Id ?? message.Chat.Id;
|
||||
string senderName = message.From?.FirstName ?? message.Chat.FirstName ??
|
||||
message.Chat.Username ?? "UnknownUser";
|
||||
switch (command)
|
||||
{
|
||||
string usernameOrUrl = args[0];
|
||||
await SendInfoReply($"searching for instagram user <{usernameOrUrl}>", message);
|
||||
var user = await InstagramWrapper.GetUserAsync(usernameOrUrl);
|
||||
if (user is null)
|
||||
case "start":
|
||||
await SendInfo(message.Chat, rb.Text("bot started"));
|
||||
break;
|
||||
case "oversee":
|
||||
{
|
||||
await SendErrorReply($"user **{usernameOrUrl}** doesnt exist", message);
|
||||
return;
|
||||
string usernameOrUrl = args[0];
|
||||
await SendInfo(message.Chat, rb.Text("searching for instagram user"), message.MessageId);
|
||||
var user = await InstagramWrapper.TryGetUserAsync(usernameOrUrl);
|
||||
if (user is null)
|
||||
{
|
||||
await SendError(message.Chat, rb.Text("user ").Text(usernameOrUrl).Text(" not found"));
|
||||
return;
|
||||
}
|
||||
await SendInfo(message.Chat, rb.Text("user ").Text(usernameOrUrl).Text(" found"));
|
||||
// user id or chat id
|
||||
CurrentUsersData.AddOrSet(senderId, new InstagramObservableParams(usernameOrUrl));
|
||||
CurrentUsersData.SaveToFile();
|
||||
break;
|
||||
}
|
||||
CurrentUsersData.AddOrSet(message.Chat.Id.ToString(), new InstagramObservableParams(usernameOrUrl));
|
||||
CurrentUsersData.SaveToFile();
|
||||
break;
|
||||
case "list":
|
||||
{
|
||||
var userData = CurrentUsersData.Get(senderId);
|
||||
if (userData is null)
|
||||
{
|
||||
await SendError(message.Chat, rb.Text("no data for user"), message.MessageId);
|
||||
return;
|
||||
}
|
||||
|
||||
rb.Text(userData.Count).Text("instagram users:\n");
|
||||
foreach (var iuParams in userData)
|
||||
{
|
||||
rb.BeginStyle(TextStyle.Bold).Text(iuParams.instagramUsername).EndStyle().Text(" - ");
|
||||
var iu = await InstagramWrapper.TryGetUserAsync(iuParams.instagramUsername);
|
||||
rb.Text(iu is null ? "user no longer exists" : iu.FullName);
|
||||
rb.SetUrl("https://www.instagram.com/"+iuParams.instagramUsername)
|
||||
.BeginStyle(TextStyle.Link)
|
||||
.Text(iuParams.instagramUsername)
|
||||
.EndStyle().Text('\n');
|
||||
}
|
||||
|
||||
await SendInfo(message.Chat, rb, message.MessageId);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
await SendError(message.Chat, rb.Text("ivalid command"), message.MessageId);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
await SendErrorReply("ivalid command", message);
|
||||
break;
|
||||
}
|
||||
catch(OperationCanceledException){}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await SendError(message.Chat, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
9
Telegram/TextStyle.cs
Normal file
9
Telegram/TextStyle.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace InstaFollowersOverseer;
|
||||
|
||||
[Flags]
|
||||
public enum TextStyle
|
||||
{
|
||||
PlainText=0,
|
||||
Bold=1, Italic=2, Crossed=4, Underline=8,
|
||||
Spoiler=16, Link=32, CodeLine=64, CodeBlock=128
|
||||
}
|
||||
Reference in New Issue
Block a user