diff --git a/.idea/.idea.InstaFollowersOverseer/.idea/discord.xml b/.idea/.idea.InstaFollowersOverseer/.idea/discord.xml index 30bab2a..d8e9561 100644 --- a/.idea/.idea.InstaFollowersOverseer/.idea/discord.xml +++ b/.idea/.idea.InstaFollowersOverseer/.idea/discord.xml @@ -1,7 +1,7 @@ - \ No newline at end of file diff --git a/Config.cs b/DataModels/Config.cs similarity index 75% rename from Config.cs rename to DataModels/Config.cs index 7adf586..2260d05 100644 --- a/Config.cs +++ b/DataModels/Config.cs @@ -6,6 +6,7 @@ public class Config : DtsodFile public string botToken; public string instagramLogin; public string instagramPassword; + public double checksIntervalMinutes; #nullable enable public Config(string fileNameWithoutExt) : base(fileNameWithoutExt) { } @@ -18,9 +19,11 @@ public class Config : DtsodFile botToken = dtsod[nameof(botToken)]; instagramLogin = dtsod[nameof(instagramLogin)]; instagramPassword = dtsod[nameof(instagramPassword)]; + checksIntervalMinutes = dtsod[nameof(checksIntervalMinutes)]; } catch (Exception ex) { + LoadedSuccessfully = false; throw new Exception($"your {FileName} format is invalid\n" + $"See {FileExampleName}", innerException: ex); } @@ -32,7 +35,8 @@ public class Config : DtsodFile { { nameof(botToken), botToken }, { nameof(instagramLogin), instagramLogin }, - { nameof(instagramPassword), instagramPassword } + { nameof(instagramPassword), instagramPassword }, + { nameof(checksIntervalMinutes), checksIntervalMinutes } }; return d; } diff --git a/DtsodFile.cs b/DataModels/DtsodFile.cs similarity index 88% rename from DtsodFile.cs rename to DataModels/DtsodFile.cs index 24cd9f8..16bf467 100644 --- a/DtsodFile.cs +++ b/DataModels/DtsodFile.cs @@ -4,6 +4,8 @@ namespace InstaFollowersOverseer; public abstract class DtsodFile { + public bool LoadedSuccessfully = false; + public readonly string FileNameWithoutExt; public readonly string FileName; public readonly string FileExampleName; @@ -42,6 +44,8 @@ public abstract class DtsodFile string fileText = File.ReadAllText(FileName); Program.MainLogger.LogDebug(fileText); + // should be set to false on LoadFromFile() errors + LoadedSuccessfully = true; return new DtsodV23(fileText); } @@ -51,11 +55,14 @@ public abstract class DtsodFile public void SaveToFile() { + if(!LoadedSuccessfully) + return; + + if(File.Exists(FileName)) + CreateBackup(); Program.MainLogger.LogInfo($"saving file {FileName}"); string dtsodStr = ToDtsod().ToString(); Program.MainLogger.LogDebug(dtsodStr); - if(File.Exists(FileName)) - CreateBackup(); File.OpenWrite(FileName) .FluentWriteString("#DtsodV23\n") .FluentWriteString(dtsodStr) diff --git a/InstagramObservableParams.cs b/DataModels/InstagramObservableParams.cs similarity index 74% rename from InstagramObservableParams.cs rename to DataModels/InstagramObservableParams.cs index 6edcd8f..f49452b 100644 --- a/InstagramObservableParams.cs +++ b/DataModels/InstagramObservableParams.cs @@ -2,18 +2,18 @@ namespace InstaFollowersOverseer; public class InstagramObservableParams { - public string instagramUserId; + public string instagramUsername; public bool notifyOnFollowing=true; public bool notifyOnUnfollowing=true; - public InstagramObservableParams(string instaUserId) + public InstagramObservableParams(string instaUsername) { - instagramUserId = instaUserId; + instagramUsername = instaUsername; } public InstagramObservableParams(DtsodV23 _overseeParams) { - instagramUserId = _overseeParams["instagramUserId"]; + instagramUsername = _overseeParams["instagramUsername"]; if (_overseeParams.TryGetValue("notifyOnFollowing", out var _notifyOnFollowing)) notifyOnFollowing = _notifyOnFollowing; if (_overseeParams.TryGetValue("notifyOnUnfollowing", out var _notifyOnUnfollowing)) @@ -23,7 +23,7 @@ public class InstagramObservableParams public DtsodV23 ToDtsod() { var d = new DtsodV23(); - d.Add(nameof(instagramUserId), instagramUserId); + d.Add(nameof(instagramUsername), instagramUsername); if(!notifyOnFollowing) d.Add(nameof(notifyOnFollowing), false); if(!notifyOnUnfollowing) diff --git a/UsersData.cs b/DataModels/UsersData.cs similarity index 60% rename from UsersData.cs rename to DataModels/UsersData.cs index ca7d553..398d7b9 100644 --- a/UsersData.cs +++ b/DataModels/UsersData.cs @@ -2,8 +2,8 @@ namespace InstaFollowersOverseer; public class UsersData : DtsodFile { - private Dictionary> usersData=new(); - + public Dictionary> UsersDict=new(); + public UsersData(string fileName) : base(fileName) {} public override void LoadFromFile() @@ -19,11 +19,12 @@ public class UsersData : DtsodFile foreach (DtsodV23 _overseeParams in uset.Value) oparams.Add(new InstagramObservableParams(_overseeParams)); - usersData.Add(telegramUserId, oparams); + UsersDict.Add(telegramUserId, oparams); } } catch (Exception ex) { + LoadedSuccessfully = false; throw new Exception($"your {FileName} format is invalid\n" + $"See {FileExampleName}", innerException: ex); } @@ -32,7 +33,7 @@ public class UsersData : DtsodFile public override DtsodV23 ToDtsod() { var b = new DtsodV23(); - foreach (var userS in usersData) + foreach (var userS in UsersDict) b.Add(userS.Key, userS.Value.Select(iop => iop.ToDtsod() @@ -40,28 +41,30 @@ public class UsersData : DtsodFile return b; } - public List Get(string telegramUserId) + public List? Get(long telegramUserId) { - if (!usersData.TryGetValue(telegramUserId, out var overseeParams)) - throw new Exception($"there is no settings for user {telegramUserId}"); + string userIdStr = telegramUserId.ToString(); + if (!UsersDict.TryGetValue(userIdStr, out var overseeParams)) + return null; return overseeParams; } - public void AddOrSet(string telegramUserId, InstagramObservableParams instagramObservableParams) + public void AddOrSet(long telegramUserId, InstagramObservableParams instagramObservableParams) { // Add // doesnt contain settings for telegramUserId - if (!usersData.TryGetValue(telegramUserId, out var thisUsersData)) + string userIdStr = telegramUserId.ToString(); + if (!UsersDict.TryGetValue(userIdStr, out var thisUsersData)) { - usersData.Add(telegramUserId, new (){ instagramObservableParams }); + UsersDict.Add(userIdStr, new (){ instagramObservableParams }); return; } // Set - // settings for telegramUserId contain InstagramObservableParams with instagramObservableParams.instagramUserId + // settings for telegramUserId contain InstagramObservableParams with instagramObservableParams.instagramUsername for (var i = 0; i < thisUsersData.Count; i++) { - if (thisUsersData[i].instagramUserId == instagramObservableParams.instagramUserId) + if (thisUsersData[i].instagramUsername == instagramObservableParams.instagramUsername) { thisUsersData[i] = instagramObservableParams; return; @@ -69,11 +72,11 @@ public class UsersData : DtsodFile } // Add - // doesnt contain InstagramObservableParams with instagramObservableParams.instagramUserId + // doesnt contain InstagramObservableParams with instagramObservableParams.instagramUsername thisUsersData.Add(instagramObservableParams); } - public void AddOrSet(string telegramUserId, IEnumerable instagramObservableParams) + public void AddOrSet(long telegramUserId, IEnumerable instagramObservableParams) { foreach (var p in instagramObservableParams) AddOrSet(telegramUserId, p); diff --git a/InstaFollowersOverseer.csproj b/InstaFollowersOverseer.csproj index 98614b7..51fe669 100644 --- a/InstaFollowersOverseer.csproj +++ b/InstaFollowersOverseer.csproj @@ -19,6 +19,8 @@ + + diff --git a/InstaFollowersOverseer.csproj.DotSettings b/InstaFollowersOverseer.csproj.DotSettings new file mode 100644 index 0000000..5148b1c --- /dev/null +++ b/InstaFollowersOverseer.csproj.DotSettings @@ -0,0 +1,4 @@ + + True + True + True \ No newline at end of file diff --git a/InstaFollowersOverseer.sln.DotSettings.user b/InstaFollowersOverseer.sln.DotSettings.user index 0c717f8..088758d 100644 --- a/InstaFollowersOverseer.sln.DotSettings.user +++ b/InstaFollowersOverseer.sln.DotSettings.user @@ -1,3 +1,8 @@  ShowAndRun + <AssemblyExplorer> + <Assembly Path="C:\Users\User\.nuget\packages\telegram.bot\18.0.0\lib\netcoreapp3.1\Telegram.Bot.dll" /> + <Assembly Path="C:\Users\User\.nuget\packages\markdowndeep.net.core\1.5.0.4\lib\netcoreapp2.0\MarkdownDeep.Core.dll" /> + <Assembly Path="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\7.0.2\ref\net7.0\System.Collections.NonGeneric.dll" /> +</AssemblyExplorer> \ No newline at end of file diff --git a/Instagram/FollowersDiff.cs b/Instagram/FollowersDiff.cs new file mode 100644 index 0000000..e4f1e45 --- /dev/null +++ b/Instagram/FollowersDiff.cs @@ -0,0 +1,45 @@ +using DTLib.Ben.Demystifier.Enumerable; + +namespace InstaFollowersOverseer.Instagram; + +public readonly record struct FollowersDiff(IList Unfollowed, IList Followed) +{ + public static readonly FollowersDiff Empty = + new FollowersDiff(EnumerableIList.Empty, EnumerableIList.Empty); + + public bool IsEmpty() => Followed.Count + Unfollowed.Count == 0; + + /// + /// generates message aouut followed and unfollowed users + /// + /// string builder to append the message to + /// diff computation happens in this method because it enumerates yield returned enumerables + public void AppendDiffMessageTo(HtmlMessageBuilder b, CancellationToken ct) + { + if (Followed.Count != 0) + { + b.BeginStyle(TextStyle.Italic).Text(Followed.Count).Text(" users followed:\n").EndStyle(); + foreach (var u in Followed) + { + if (ct.IsCancellationRequested) + return; + // username with clickable link + b.SetUrl("https://www.instagram.com/" + u).BeginStyle(TextStyle.Link).Text(u).EndStyle() + .Text('\n'); + } + } + + if (Unfollowed.Count != 0) + { + b.BeginStyle(TextStyle.Italic).Text(Unfollowed.Count).Text(" users unfollowed:\n").EndStyle(); + foreach (var u in Followed) + { + if (ct.IsCancellationRequested) + return; + // username with clickable link + b.SetUrl("https://www.instagram.com/" + u).BeginStyle(TextStyle.Link).Text(u).EndStyle() + .Text('\n'); + } + } + } +} \ No newline at end of file diff --git a/Instagram/InstagramWrapper.cs b/Instagram/InstagramWrapper.cs index 3e1ca01..464bea3 100644 --- a/Instagram/InstagramWrapper.cs +++ b/Instagram/InstagramWrapper.cs @@ -10,7 +10,7 @@ public static class InstagramWrapper public static ContextLogger InstagramLogger = new("instagram",ParentLogger); private static IInstaApi Api=null!; - public static async void Init() + public static async Task InitAsync() { try { @@ -27,13 +27,13 @@ public static class InstagramWrapper UserName = CurrentConfig.instagramLogin, Password = CurrentConfig.instagramPassword }) - .SetRequestDelay(RequestDelay.FromSeconds(0, 1)) + .SetRequestDelay(RequestDelay.FromSeconds(5, 10)) .Build(); InstagramLogger.LogInfo("instagram login starting"); var rezult= await Api.LoginAsync(); if (!rezult.Succeeded) throw new Exception("login exception:\n" + rezult.Info + '\n' + rezult.Value); - InstagramLogger.LogInfo("instagram wrapper have initialized and connected successfully"); + InstagramLogger.LogInfo("instagram wrapper initialized and connected successfully"); } catch (OperationCanceledException) {} catch (Exception ex) @@ -43,7 +43,7 @@ public static class InstagramWrapper } } - public static async Task GetUserAsync(string usernameOrUrl) + public static async Task TryGetUserAsync(string usernameOrUrl) { // url if (usernameOrUrl.Contains('/')) @@ -55,4 +55,26 @@ public static class InstagramWrapper var u=await Api.GetUserAsync(usernameOrUrl); return u.Succeeded ? u.Value : null; } + + private static Dictionary> FollowersDict=new(); + + /// may took long time if user have many followers + public static async Task GetFollowersDiffAsync(string instaUser) + { + if (await TryGetUserAsync(instaUser) is null) + throw new Exception($"instagram user {instaUser} doesnt exist"); + var maybeFollowers = await Api.GetUserFollowersAsync(instaUser, PaginationParameters.Empty); + if (!maybeFollowers.Succeeded) + throw new Exception($"can't get followers of user {instaUser}"); + var currentFollowers = maybeFollowers.Value.Select(f=>f.UserName).ToList(); + if(!FollowersDict.TryGetValue(instaUser, out var _prevFollowers)) + { + FollowersDict.Add(instaUser, currentFollowers); + return FollowersDiff.Empty; + } + var prevFollowers = _prevFollowers.ToList(); + var unfollowed = prevFollowers.Except(currentFollowers).ToList(); + var followed = currentFollowers.Except(prevFollowers).ToList(); + return new FollowersDiff(unfollowed, followed); + } } \ No newline at end of file diff --git a/Program.cs b/Program.cs index f8b7700..94a10e2 100644 --- a/Program.cs +++ b/Program.cs @@ -1,7 +1,9 @@ global using System; -global using System.Threading.Tasks; -global using System.Linq; global using System.Collections.Generic; +global using System.Linq; +global using System.Text; +global using System.Threading; +global using System.Threading.Tasks; global using DTLib; global using DTLib.Filesystem; global using DTLib.Extensions; @@ -11,8 +13,6 @@ global using File = DTLib.Filesystem.File; global using Directory = DTLib.Filesystem.Directory; global using Path = DTLib.Filesystem.Path; global using static InstaFollowersOverseer.SharedData; -using System.Text; -using System.Threading; namespace InstaFollowersOverseer; @@ -40,15 +40,21 @@ static class Program { Stop(); Thread.Sleep(1000); - MainLogger.LogInfo("all have cancelled"); + Overseer.Stop(); e.Cancel = false; }; - Instagram.InstagramWrapper.Init(); - Telegram.TelegramWrapper.Init(); + Task[] tasks={ + Instagram.InstagramWrapper.InitAsync(), + TelegramWrapper.InitAsync() + }; + Task.WaitAll(tasks); + + Overseer.Start(); Task.Delay(-1, MainCancel.Token).GetAwaiter().GetResult(); Thread.Sleep(1000); + MainLogger.LogInfo("all have cancelled"); } catch (OperationCanceledException) {} catch (Exception ex) diff --git a/Telegram/HtmlMessageBuilder.cs b/Telegram/HtmlMessageBuilder.cs new file mode 100644 index 0000000..1e0e3fb --- /dev/null +++ b/Telegram/HtmlMessageBuilder.cs @@ -0,0 +1,182 @@ + +using System.Net.Mime; +using Microsoft.Extensions.Primitives; + +namespace InstaFollowersOverseer; + +/// +/// 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 +/// +// supported tags: +// bold +// italic +// crossed +// underline +// spoiler +// inline URL +// inline mention of a user +// inlne code +//
code block
+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 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(""); + if (0!=(_state.Style & TextStyle.Italic)) _html.Append(""); + if (0!=(_state.Style & TextStyle.Crossed)) _html.Append(""); + if (0!=(_state.Style & TextStyle.Underline)) _html.Append(""); + if (0!=(_state.Style & TextStyle.Spoiler)) _html.Append(""); + if (0!=(_state.Style & TextStyle.Link)) + { + _html.Append(""); + } + if (0!=(_state.Style & TextStyle.CodeLine)) _html.Append(""); + if (0!=(_state.Style & TextStyle.CodeBlock)) + { + _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(""); + if (0!=(_state.Style & TextStyle.CodeLine)) _html.Append(""); + if (0!=(_state.Style & TextStyle.Link)) _html.Append(""); + if (0!=(_state.Style & TextStyle.Spoiler)) _html.Append(""); + if (0!=(_state.Style & TextStyle.Underline)) _html.Append(""); + if (0!=(_state.Style & TextStyle.Crossed)) _html.Append(""); + if (0!=(_state.Style & TextStyle.Italic)) _html.Append(""); + if (0!=(_state.Style & TextStyle.Bold)) _html.Append(""); + _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(); + } +} \ No newline at end of file diff --git a/Telegram/Overseer.cs b/Telegram/Overseer.cs new file mode 100644 index 0000000..bdabd52 --- /dev/null +++ b/Telegram/Overseer.cs @@ -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(); + } +} \ No newline at end of file diff --git a/Telegram/TelegramWrapper.cs b/Telegram/TelegramWrapper.cs index 37c4415..6796b52 100644 --- a/Telegram/TelegramWrapper.cs +++ b/Telegram/TelegramWrapper.cs @@ -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); } } } \ No newline at end of file diff --git a/Telegram/TextStyle.cs b/Telegram/TextStyle.cs new file mode 100644 index 0000000..7ea7fc6 --- /dev/null +++ b/Telegram/TextStyle.cs @@ -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 +} \ No newline at end of file diff --git a/resources/config-example.dtsod b/resources/config-example.dtsod index d1c4378..f372a03 100644 --- a/resources/config-example.dtsod +++ b/resources/config-example.dtsod @@ -2,3 +2,4 @@ botToken:"19815858:aAjfawIAHAWw4_kAkg321"; instagramLogin:"aboba"; instagramPassword:"01234567"; +checksIntervalMinutes: 1.0;