Overseer and HttpsMessageBuilder
This commit is contained in:
parent
327d06b3d0
commit
7243a2b0d3
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DiscordProjectSettings">
|
||||
<option name="show" value="ASK" />
|
||||
<option name="show" value="PROJECT_FILES" />
|
||||
<option name="description" value="" />
|
||||
</component>
|
||||
</project>
|
||||
@ -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;
|
||||
}
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -2,7 +2,7 @@ namespace InstaFollowersOverseer;
|
||||
|
||||
public class UsersData : DtsodFile
|
||||
{
|
||||
private Dictionary<string, List<InstagramObservableParams>> usersData=new();
|
||||
public Dictionary<string, List<InstagramObservableParams>> UsersDict=new();
|
||||
|
||||
public UsersData(string fileName) : base(fileName) {}
|
||||
|
||||
@ -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<InstagramObservableParams, DtsodV23>(iop =>
|
||||
iop.ToDtsod()
|
||||
@ -40,28 +41,30 @@ public class UsersData : DtsodFile
|
||||
return b;
|
||||
}
|
||||
|
||||
public List<InstagramObservableParams> Get(string telegramUserId)
|
||||
public List<InstagramObservableParams>? 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> instagramObservableParams)
|
||||
public void AddOrSet(long telegramUserId, IEnumerable<InstagramObservableParams> instagramObservableParams)
|
||||
{
|
||||
foreach (var p in instagramObservableParams)
|
||||
AddOrSet(telegramUserId, p);
|
||||
@ -19,6 +19,8 @@
|
||||
<!--third-party dependencies-->
|
||||
<ItemGroup>
|
||||
<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.Bots.Extensions.Polling" Version="5.9.0" /-->
|
||||
</ItemGroup>
|
||||
|
||||
4
InstaFollowersOverseer.csproj.DotSettings
Normal file
4
InstaFollowersOverseer.csproj.DotSettings
Normal 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>
|
||||
@ -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">
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/SweaWarningsMode/@EntryValue">ShowAndRun</s:String>
|
||||
<s:String x:Key="/Default/Environment/AssemblyExplorer/XmlDocument/@EntryValue"><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></s:String>
|
||||
</wpf:ResourceDictionary>
|
||||
45
Instagram/FollowersDiff.cs
Normal file
45
Instagram/FollowersDiff.cs
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<InstaUser?> GetUserAsync(string usernameOrUrl)
|
||||
public static async Task<InstaUser?> 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<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);
|
||||
}
|
||||
}
|
||||
20
Program.cs
20
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)
|
||||
|
||||
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)
|
||||
@ -51,19 +50,33 @@ public static class TelegramWrapper
|
||||
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)
|
||||
{
|
||||
try
|
||||
{
|
||||
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)
|
||||
{
|
||||
case "start":
|
||||
await Bot.SendTextMessageAsync(message.Chat, "hi");
|
||||
await SendInfo(message.Chat, rb.Text("bot started"));
|
||||
break;
|
||||
case "oversee":
|
||||
{
|
||||
string usernameOrUrl = args[0];
|
||||
await SendInfoReply($"searching for instagram user <{usernameOrUrl}>", message);
|
||||
var user = await InstagramWrapper.GetUserAsync(usernameOrUrl);
|
||||
await SendInfo(message.Chat, rb.Text("searching for instagram user"), message.MessageId);
|
||||
var user = await InstagramWrapper.TryGetUserAsync(usernameOrUrl);
|
||||
if (user is null)
|
||||
{
|
||||
await SendErrorReply($"user **{usernameOrUrl}** doesnt exist", message);
|
||||
await SendError(message.Chat, rb.Text("user ").Text(usernameOrUrl).Text(" not found"));
|
||||
return;
|
||||
}
|
||||
CurrentUsersData.AddOrSet(message.Chat.Id.ToString(), new InstagramObservableParams(usernameOrUrl));
|
||||
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;
|
||||
}
|
||||
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 SendErrorReply("ivalid command", message);
|
||||
await SendError(message.Chat, rb.Text("ivalid command"), message.MessageId);
|
||||
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
|
||||
}
|
||||
@ -2,3 +2,4 @@
|
||||
botToken:"19815858:aAjfawIAHAWw4_kAkg321";
|
||||
instagramLogin:"aboba";
|
||||
instagramPassword:"01234567";
|
||||
checksIntervalMinutes: 1.0;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user