Overseer and HttpsMessageBuilder
This commit is contained in:
parent
327d06b3d0
commit
7243a2b0d3
@ -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>
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
@ -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)
|
||||||
@ -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)
|
||||||
@ -2,7 +2,7 @@ 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) {}
|
||||||
|
|
||||||
@ -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);
|
||||||
@ -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>
|
||||||
|
|||||||
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">
|
<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"><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>
|
</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);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
20
Program.cs
20
Program.cs
@ -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)
|
||||||
|
|||||||
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.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)
|
||||||
@ -51,19 +50,33 @@ public static class TelegramWrapper
|
|||||||
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
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";
|
botToken:"19815858:aAjfawIAHAWw4_kAkg321";
|
||||||
instagramLogin:"aboba";
|
instagramLogin:"aboba";
|
||||||
instagramPassword:"01234567";
|
instagramPassword:"01234567";
|
||||||
|
checksIntervalMinutes: 1.0;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user