Overseer and HttpsMessageBuilder

This commit is contained in:
timerix 2023-02-24 20:29:20 +06:00
parent 327d06b3d0
commit 7243a2b0d3
16 changed files with 488 additions and 66 deletions

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="DiscordProjectSettings"> <component name="DiscordProjectSettings">
<option name="show" value="ASK" /> <option name="show" value="PROJECT_FILES" />
<option name="description" value="" /> <option name="description" value="" />
</component> </component>
</project> </project>

View File

@ -6,6 +6,7 @@ public class Config : DtsodFile
public string botToken; public string botToken;
public string instagramLogin; public string instagramLogin;
public string instagramPassword; public string instagramPassword;
public double checksIntervalMinutes;
#nullable enable #nullable enable
public Config(string fileNameWithoutExt) : base(fileNameWithoutExt) { } public Config(string fileNameWithoutExt) : base(fileNameWithoutExt) { }
@ -18,9 +19,11 @@ public class Config : DtsodFile
botToken = dtsod[nameof(botToken)]; botToken = dtsod[nameof(botToken)];
instagramLogin = dtsod[nameof(instagramLogin)]; instagramLogin = dtsod[nameof(instagramLogin)];
instagramPassword = dtsod[nameof(instagramPassword)]; instagramPassword = dtsod[nameof(instagramPassword)];
checksIntervalMinutes = dtsod[nameof(checksIntervalMinutes)];
} }
catch (Exception ex) catch (Exception ex)
{ {
LoadedSuccessfully = false;
throw new Exception($"your {FileName} format is invalid\n" throw new Exception($"your {FileName} format is invalid\n"
+ $"See {FileExampleName}", innerException: ex); + $"See {FileExampleName}", innerException: ex);
} }
@ -32,7 +35,8 @@ public class Config : DtsodFile
{ {
{ nameof(botToken), botToken }, { nameof(botToken), botToken },
{ nameof(instagramLogin), instagramLogin }, { nameof(instagramLogin), instagramLogin },
{ nameof(instagramPassword), instagramPassword } { nameof(instagramPassword), instagramPassword },
{ nameof(checksIntervalMinutes), checksIntervalMinutes }
}; };
return d; return d;
} }

View File

@ -4,6 +4,8 @@ namespace InstaFollowersOverseer;
public abstract class DtsodFile public abstract class DtsodFile
{ {
public bool LoadedSuccessfully = false;
public readonly string FileNameWithoutExt; public readonly string FileNameWithoutExt;
public readonly string FileName; public readonly string FileName;
public readonly string FileExampleName; public readonly string FileExampleName;
@ -42,6 +44,8 @@ public abstract class DtsodFile
string fileText = File.ReadAllText(FileName); string fileText = File.ReadAllText(FileName);
Program.MainLogger.LogDebug(fileText); Program.MainLogger.LogDebug(fileText);
// should be set to false on LoadFromFile() errors
LoadedSuccessfully = true;
return new DtsodV23(fileText); return new DtsodV23(fileText);
} }
@ -51,11 +55,14 @@ public abstract class DtsodFile
public void SaveToFile() public void SaveToFile()
{ {
if(!LoadedSuccessfully)
return;
if(File.Exists(FileName))
CreateBackup();
Program.MainLogger.LogInfo($"saving file {FileName}"); Program.MainLogger.LogInfo($"saving file {FileName}");
string dtsodStr = ToDtsod().ToString(); string dtsodStr = ToDtsod().ToString();
Program.MainLogger.LogDebug(dtsodStr); Program.MainLogger.LogDebug(dtsodStr);
if(File.Exists(FileName))
CreateBackup();
File.OpenWrite(FileName) File.OpenWrite(FileName)
.FluentWriteString("#DtsodV23\n") .FluentWriteString("#DtsodV23\n")
.FluentWriteString(dtsodStr) .FluentWriteString(dtsodStr)

View File

@ -2,18 +2,18 @@ namespace InstaFollowersOverseer;
public class InstagramObservableParams public class InstagramObservableParams
{ {
public string instagramUserId; public string instagramUsername;
public bool notifyOnFollowing=true; public bool notifyOnFollowing=true;
public bool notifyOnUnfollowing=true; public bool notifyOnUnfollowing=true;
public InstagramObservableParams(string instaUserId) public InstagramObservableParams(string instaUsername)
{ {
instagramUserId = instaUserId; instagramUsername = instaUsername;
} }
public InstagramObservableParams(DtsodV23 _overseeParams) public InstagramObservableParams(DtsodV23 _overseeParams)
{ {
instagramUserId = _overseeParams["instagramUserId"]; instagramUsername = _overseeParams["instagramUsername"];
if (_overseeParams.TryGetValue("notifyOnFollowing", out var _notifyOnFollowing)) if (_overseeParams.TryGetValue("notifyOnFollowing", out var _notifyOnFollowing))
notifyOnFollowing = _notifyOnFollowing; notifyOnFollowing = _notifyOnFollowing;
if (_overseeParams.TryGetValue("notifyOnUnfollowing", out var _notifyOnUnfollowing)) if (_overseeParams.TryGetValue("notifyOnUnfollowing", out var _notifyOnUnfollowing))
@ -23,7 +23,7 @@ public class InstagramObservableParams
public DtsodV23 ToDtsod() public DtsodV23 ToDtsod()
{ {
var d = new DtsodV23(); var d = new DtsodV23();
d.Add(nameof(instagramUserId), instagramUserId); d.Add(nameof(instagramUsername), instagramUsername);
if(!notifyOnFollowing) if(!notifyOnFollowing)
d.Add(nameof(notifyOnFollowing), false); d.Add(nameof(notifyOnFollowing), false);
if(!notifyOnUnfollowing) if(!notifyOnUnfollowing)

View File

@ -2,8 +2,8 @@ namespace InstaFollowersOverseer;
public class UsersData : DtsodFile public class UsersData : DtsodFile
{ {
private Dictionary<string, List<InstagramObservableParams>> usersData=new(); public Dictionary<string, List<InstagramObservableParams>> UsersDict=new();
public UsersData(string fileName) : base(fileName) {} public UsersData(string fileName) : base(fileName) {}
public override void LoadFromFile() public override void LoadFromFile()
@ -19,11 +19,12 @@ public class UsersData : DtsodFile
foreach (DtsodV23 _overseeParams in uset.Value) foreach (DtsodV23 _overseeParams in uset.Value)
oparams.Add(new InstagramObservableParams(_overseeParams)); oparams.Add(new InstagramObservableParams(_overseeParams));
usersData.Add(telegramUserId, oparams); UsersDict.Add(telegramUserId, oparams);
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
LoadedSuccessfully = false;
throw new Exception($"your {FileName} format is invalid\n" throw new Exception($"your {FileName} format is invalid\n"
+ $"See {FileExampleName}", innerException: ex); + $"See {FileExampleName}", innerException: ex);
} }
@ -32,7 +33,7 @@ public class UsersData : DtsodFile
public override DtsodV23 ToDtsod() public override DtsodV23 ToDtsod()
{ {
var b = new DtsodV23(); var b = new DtsodV23();
foreach (var userS in usersData) foreach (var userS in UsersDict)
b.Add(userS.Key, b.Add(userS.Key,
userS.Value.Select<InstagramObservableParams, DtsodV23>(iop => userS.Value.Select<InstagramObservableParams, DtsodV23>(iop =>
iop.ToDtsod() iop.ToDtsod()
@ -40,28 +41,30 @@ public class UsersData : DtsodFile
return b; return b;
} }
public List<InstagramObservableParams> Get(string telegramUserId) public List<InstagramObservableParams>? Get(long telegramUserId)
{ {
if (!usersData.TryGetValue(telegramUserId, out var overseeParams)) string userIdStr = telegramUserId.ToString();
throw new Exception($"there is no settings for user {telegramUserId}"); if (!UsersDict.TryGetValue(userIdStr, out var overseeParams))
return null;
return overseeParams; return overseeParams;
} }
public void AddOrSet(string telegramUserId, InstagramObservableParams instagramObservableParams) public void AddOrSet(long telegramUserId, InstagramObservableParams instagramObservableParams)
{ {
// Add // Add
// doesnt contain settings for telegramUserId // 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; return;
} }
// Set // 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++) for (var i = 0; i < thisUsersData.Count; i++)
{ {
if (thisUsersData[i].instagramUserId == instagramObservableParams.instagramUserId) if (thisUsersData[i].instagramUsername == instagramObservableParams.instagramUsername)
{ {
thisUsersData[i] = instagramObservableParams; thisUsersData[i] = instagramObservableParams;
return; return;
@ -69,11 +72,11 @@ public class UsersData : DtsodFile
} }
// Add // Add
// doesnt contain InstagramObservableParams with instagramObservableParams.instagramUserId // doesnt contain InstagramObservableParams with instagramObservableParams.instagramUsername
thisUsersData.Add(instagramObservableParams); thisUsersData.Add(instagramObservableParams);
} }
public void AddOrSet(string telegramUserId, IEnumerable<InstagramObservableParams> instagramObservableParams) public void AddOrSet(long telegramUserId, IEnumerable<InstagramObservableParams> instagramObservableParams)
{ {
foreach (var p in instagramObservableParams) foreach (var p in instagramObservableParams)
AddOrSet(telegramUserId, p); AddOrSet(telegramUserId, p);

View File

@ -19,6 +19,8 @@
<!--third-party dependencies--> <!--third-party dependencies-->
<ItemGroup> <ItemGroup>
<PackageReference Include="InstaSharper" Version="1.4.82" /> <PackageReference Include="InstaSharper" Version="1.4.82" />
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
<PackageReference Include="MarkedNet" Version="2.1.4" />
<PackageReference Include="Telegram.Bot" Version="18.0.0" /> <PackageReference Include="Telegram.Bot" Version="18.0.0" />
<!--PackageReference Include="Telegram.Bots.Extensions.Polling" Version="5.9.0" /--> <!--PackageReference Include="Telegram.Bots.Extensions.Polling" Version="5.9.0" /-->
</ItemGroup> </ItemGroup>

View File

@ -0,0 +1,4 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=datamodels/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=instagram/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=telegram/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@ -1,3 +1,8 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/Highlighting/SweaWarningsMode/@EntryValue">ShowAndRun</s:String> <s:String x:Key="/Default/CodeInspection/Highlighting/SweaWarningsMode/@EntryValue">ShowAndRun</s:String>
<s:String x:Key="/Default/Environment/AssemblyExplorer/XmlDocument/@EntryValue">&lt;AssemblyExplorer&gt;&#xD;
&lt;Assembly Path="C:\Users\User\.nuget\packages\telegram.bot\18.0.0\lib\netcoreapp3.1\Telegram.Bot.dll" /&gt;&#xD;
&lt;Assembly Path="C:\Users\User\.nuget\packages\markdowndeep.net.core\1.5.0.4\lib\netcoreapp2.0\MarkdownDeep.Core.dll" /&gt;&#xD;
&lt;Assembly Path="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\7.0.2\ref\net7.0\System.Collections.NonGeneric.dll" /&gt;&#xD;
&lt;/AssemblyExplorer&gt;</s:String>
</wpf:ResourceDictionary> </wpf:ResourceDictionary>

View File

@ -0,0 +1,45 @@
using DTLib.Ben.Demystifier.Enumerable;
namespace InstaFollowersOverseer.Instagram;
public readonly record struct FollowersDiff(IList<string> Unfollowed, IList<string> Followed)
{
public static readonly FollowersDiff Empty =
new FollowersDiff(EnumerableIList<string>.Empty, EnumerableIList<string>.Empty);
public bool IsEmpty() => Followed.Count + Unfollowed.Count == 0;
/// <summary>
/// generates message aouut followed and unfollowed users
/// </summary>
/// <param name="b">string builder to append the message to</param>
/// <param name="ct">diff computation happens in this method because it enumerates yield returned enumerables</param>
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');
}
}
}
}

View File

@ -10,7 +10,7 @@ public static class InstagramWrapper
public static ContextLogger InstagramLogger = new("instagram",ParentLogger); public static ContextLogger InstagramLogger = new("instagram",ParentLogger);
private static IInstaApi Api=null!; private static IInstaApi Api=null!;
public static async void Init() public static async Task InitAsync()
{ {
try try
{ {
@ -27,13 +27,13 @@ public static class InstagramWrapper
UserName = CurrentConfig.instagramLogin, UserName = CurrentConfig.instagramLogin,
Password = CurrentConfig.instagramPassword Password = CurrentConfig.instagramPassword
}) })
.SetRequestDelay(RequestDelay.FromSeconds(0, 1)) .SetRequestDelay(RequestDelay.FromSeconds(5, 10))
.Build(); .Build();
InstagramLogger.LogInfo("instagram login starting"); InstagramLogger.LogInfo("instagram login starting");
var rezult= await Api.LoginAsync(); var rezult= await Api.LoginAsync();
if (!rezult.Succeeded) if (!rezult.Succeeded)
throw new Exception("login exception:\n" + rezult.Info + '\n' + rezult.Value); 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 (OperationCanceledException) {}
catch (Exception ex) catch (Exception ex)
@ -43,7 +43,7 @@ public static class InstagramWrapper
} }
} }
public static async Task<InstaUser?> GetUserAsync(string usernameOrUrl) public static async Task<InstaUser?> TryGetUserAsync(string usernameOrUrl)
{ {
// url // url
if (usernameOrUrl.Contains('/')) if (usernameOrUrl.Contains('/'))
@ -55,4 +55,26 @@ public static class InstagramWrapper
var u=await Api.GetUserAsync(usernameOrUrl); var u=await Api.GetUserAsync(usernameOrUrl);
return u.Succeeded ? u.Value : null; return u.Succeeded ? u.Value : null;
} }
private static Dictionary<string, IEnumerable<string>> FollowersDict=new();
/// may took long time if user have many followers
public static async Task<FollowersDiff> 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);
}
} }

View File

@ -1,7 +1,9 @@
global using System; global using System;
global using System.Threading.Tasks;
global using System.Linq;
global using System.Collections.Generic; 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;
global using DTLib.Filesystem; global using DTLib.Filesystem;
global using DTLib.Extensions; global using DTLib.Extensions;
@ -11,8 +13,6 @@ global using File = DTLib.Filesystem.File;
global using Directory = DTLib.Filesystem.Directory; global using Directory = DTLib.Filesystem.Directory;
global using Path = DTLib.Filesystem.Path; global using Path = DTLib.Filesystem.Path;
global using static InstaFollowersOverseer.SharedData; global using static InstaFollowersOverseer.SharedData;
using System.Text;
using System.Threading;
namespace InstaFollowersOverseer; namespace InstaFollowersOverseer;
@ -40,15 +40,21 @@ static class Program
{ {
Stop(); Stop();
Thread.Sleep(1000); Thread.Sleep(1000);
MainLogger.LogInfo("all have cancelled"); Overseer.Stop();
e.Cancel = false; e.Cancel = false;
}; };
Instagram.InstagramWrapper.Init(); Task[] tasks={
Telegram.TelegramWrapper.Init(); Instagram.InstagramWrapper.InitAsync(),
TelegramWrapper.InitAsync()
};
Task.WaitAll(tasks);
Overseer.Start();
Task.Delay(-1, MainCancel.Token).GetAwaiter().GetResult(); Task.Delay(-1, MainCancel.Token).GetAwaiter().GetResult();
Thread.Sleep(1000); Thread.Sleep(1000);
MainLogger.LogInfo("all have cancelled");
} }
catch (OperationCanceledException) {} catch (OperationCanceledException) {}
catch (Exception ex) catch (Exception ex)

View 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
{
'<'=>"&lt",
'>'=>"&gt",
'&'=>"&apm",
'"'=>"&quot",
'\''=>"&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
View 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();
}
}

View File

@ -1,19 +1,18 @@
using System.Net.Http; using System.Net.Http;
using System.Threading;
using Telegram.Bot; using Telegram.Bot;
using Telegram.Bot.Polling; using Telegram.Bot.Polling;
using Telegram.Bot.Types; using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums; using Telegram.Bot.Types.Enums;
using InstaFollowersOverseer.Instagram; using InstaFollowersOverseer.Instagram;
namespace InstaFollowersOverseer.Telegram; namespace InstaFollowersOverseer;
public static class TelegramWrapper public static class TelegramWrapper
{ {
private static ContextLogger TelegramLogger = new("telegram", ParentLogger); private static ContextLogger TelegramLogger = new("telegram", ParentLogger);
private static TelegramBotClient Bot=null!; private static TelegramBotClient Bot=null!;
public static async void Init() public static async Task InitAsync()
{ {
try try
{ {
@ -35,7 +34,7 @@ public static class TelegramWrapper
}; };
TelegramLogger.LogInfo("bot starting recieving long polls"); TelegramLogger.LogInfo("bot starting recieving long polls");
Bot.StartReceiving(BotApiUpdateHandler, BotApiExceptionHandler, receiverOptions, Program.MainCancelToken); Bot.StartReceiving(BotApiUpdateHandler, BotApiExceptionHandler, receiverOptions, Program.MainCancelToken);
TelegramLogger.LogInfo("telegram wrapper have initialized successfully"); TelegramLogger.LogInfo("telegram wrapper initialized successfully");
} }
catch (OperationCanceledException) {} catch (OperationCanceledException) {}
catch (Exception ex) catch (Exception ex)
@ -50,20 +49,34 @@ public static class TelegramWrapper
TelegramLogger.LogError(ex); TelegramLogger.LogError(ex);
return Task.CompletedTask; 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); string html = message.ToHtml();
await Bot.SendTextMessageAsync(replyToMessage.Chat, text, await Bot.SendTextMessageAsync(chatId, html,
replyToMessageId: replyToMessage.MessageId, replyToMessageId: replyToMesId,
parseMode:ParseMode.MarkdownV2); 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); TelegramLogger.LogInfo(message);
await Bot.SendTextMessageAsync(replyToMessage.Chat, "error: "+text, await SendMessage(chatId, message, replyToMesId);
replyToMessageId: replyToMessage.MessageId, }
parseMode:ParseMode.MarkdownV2);
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) 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) private static async Task ExecCommandAsync(string command, string[] args, Message message)
{ {
switch (command) try
{ {
case "start": HtmlMessageBuilder rb = new();
await Bot.SendTextMessageAsync(message.Chat, "hi"); long senderId = message.From?.Id ?? message.Chat.Id;
break; string senderName = message.From?.FirstName ?? message.Chat.FirstName ??
case "oversee": message.Chat.Username ?? "UnknownUser";
switch (command)
{ {
string usernameOrUrl = args[0]; case "start":
await SendInfoReply($"searching for instagram user <{usernameOrUrl}>", message); await SendInfo(message.Chat, rb.Text("bot started"));
var user = await InstagramWrapper.GetUserAsync(usernameOrUrl); break;
if (user is null) case "oversee":
{ {
await SendErrorReply($"user **{usernameOrUrl}** doesnt exist", message); string usernameOrUrl = args[0];
return; 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)); case "list":
CurrentUsersData.SaveToFile(); {
break; 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); catch(OperationCanceledException){}
break; catch (Exception ex)
{
await SendError(message.Chat, ex);
} }
} }
} }

9
Telegram/TextStyle.cs Normal file
View 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
}

View File

@ -2,3 +2,4 @@
botToken:"19815858:aAjfawIAHAWw4_kAkg321"; botToken:"19815858:aAjfawIAHAWw4_kAkg321";
instagramLogin:"aboba"; instagramLogin:"aboba";
instagramPassword:"01234567"; instagramPassword:"01234567";
checksIntervalMinutes: 1.0;