diff --git a/Config.cs b/Config.cs index 762dd7f..7adf586 100644 --- a/Config.cs +++ b/Config.cs @@ -1,57 +1,39 @@ namespace InstaFollowersOverseer; -public class Config +public class Config : DtsodFile { - - private const string config_file="config.dtsod"; - private const string config_example_file="config-example.dtsod"; - + #nullable disable public string botToken; public string instagramLogin; public string instagramPassword; + #nullable enable - public Config(DtsodV23 configDtsod) + public Config(string fileNameWithoutExt) : base(fileNameWithoutExt) { } + + public override void LoadFromFile() { - botToken = configDtsod[nameof(botToken)]; - instagramLogin = configDtsod[nameof(instagramLogin)]; - instagramPassword = configDtsod[nameof(instagramPassword)]; - } - - public static Config ReadFromFile() - { - if (!File.Exists(config_file)) + var dtsod = ReadDtsodFromFile(true); + try { - EmbeddedResources.CopyToFile( - $"{EmbeddedResourcesPrefix}.{config_example_file}", - config_example_file); - throw new Exception($"File {config_file} doesnt exist. You have create config. See {config_example_file}"); + botToken = dtsod[nameof(botToken)]; + instagramLogin = dtsod[nameof(instagramLogin)]; + instagramPassword = dtsod[nameof(instagramPassword)]; + } + catch (Exception ex) + { + throw new Exception($"your {FileName} format is invalid\n" + + $"See {FileExampleName}", innerException: ex); } - - return new Config(new DtsodV23(File.ReadAllText(config_file))); } - public DtsodV23 ToDtsod() + public override DtsodV23 ToDtsod() { var d = new DtsodV23 { { nameof(botToken), botToken }, { nameof(instagramLogin), instagramLogin }, - { nameof(instagramLogin), instagramLogin } + { nameof(instagramPassword), instagramPassword } }; return d; } - - public override string ToString() => ToDtsod().ToString(); - - public void SaveToFile() - { - File.Copy(config_file, - $"backups/{config_file}.old-"+ - "{DateTime.Now.ToString(MyTimeFormat.ForFileNames)}", - true); - - File.OpenWrite(config_file) - .FluentWriteString("#DtsodV23\n") - .WriteString(ToDtsod().ToString()); - } } \ No newline at end of file diff --git a/DtsodFile.cs b/DtsodFile.cs new file mode 100644 index 0000000..24cd9f8 --- /dev/null +++ b/DtsodFile.cs @@ -0,0 +1,64 @@ +using System.IO; + +namespace InstaFollowersOverseer; + +public abstract class DtsodFile +{ + public readonly string FileNameWithoutExt; + public readonly string FileName; + public readonly string FileExampleName; + + public DtsodFile(string fileNameWithoutExt) + { + FileNameWithoutExt = fileNameWithoutExt; + FileName = fileNameWithoutExt + ".dtsod"; + FileExampleName = fileNameWithoutExt + "-example.dtsod"; + } + + public void CreateBackup() + { + string backupPath=$"backups/{FileNameWithoutExt}.d/{FileNameWithoutExt}" + +DateTime.Now.ToString(MyTimeFormat.ForFileNames)+".dtsod"; + Program.MainLogger.LogInfo($"creating backup if file {FileName} at path {backupPath}"); + File.Copy(FileName,backupPath,false); + } + + public DtsodV23 ReadDtsodFromFile(bool trhowIfFileNotFound) + { + Program.MainLogger.LogInfo($"reading file {FileName}"); + EmbeddedResources.CopyToFile( + $"{EmbeddedResourcesPrefix}.{FileExampleName}", + FileExampleName); + + if (!File.Exists(FileName)) + { + File.WriteAllText(FileName, "#DtsodV23\n"); + string message = $"file {FileName} doesnt exist, created new blank"; + if (trhowIfFileNotFound) + throw new FileNotFoundException(message); + Program.MainLogger.LogWarn(message); + return new DtsodV23(); + } + + string fileText = File.ReadAllText(FileName); + Program.MainLogger.LogDebug(fileText); + return new DtsodV23(fileText); + } + + public abstract void LoadFromFile(); + + public abstract DtsodV23 ToDtsod(); + + public void SaveToFile() + { + 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) + .Close(); + } +} \ No newline at end of file diff --git a/InstaFollowersOverseer.csproj b/InstaFollowersOverseer.csproj index 06a38b0..98614b7 100644 --- a/InstaFollowersOverseer.csproj +++ b/InstaFollowersOverseer.csproj @@ -1,17 +1,29 @@ - + + Timerix + Telegram bot that notifies users when somebody follows/unfollows theit instagram accounts + GIT + https://github.com/Timerix22/InstaFollowersOverseer + MIT + Exe - net6.0 + net6.0;net7.0 + embedded + + preview disable enable - preview + + - + + + @@ -25,6 +37,6 @@ + - diff --git a/InstaFollowersOverseer.sln.DotSettings.user b/InstaFollowersOverseer.sln.DotSettings.user new file mode 100644 index 0000000..0c717f8 --- /dev/null +++ b/InstaFollowersOverseer.sln.DotSettings.user @@ -0,0 +1,3 @@ + + ShowAndRun + \ No newline at end of file diff --git a/Instagram/InstagramApiLogger.cs b/Instagram/InstagramApiLogger.cs new file mode 100644 index 0000000..30e7ece --- /dev/null +++ b/Instagram/InstagramApiLogger.cs @@ -0,0 +1,37 @@ +using System.Net.Http; +using InstaSharper.Logger; + +namespace InstaFollowersOverseer.Instagram; + +public class InstagramApiLogger : IInstaLogger +{ + public ContextLogger _logger = new("api", InstagramWrapper.InstagramLogger); + + public void LogRequest(HttpRequestMessage r) + { + _logger.LogDebug("http",$"request {r.Method.Method.ToUpper()} from {r.RequestUri}:\n" + + r.Content?.ReadAsStringAsync().GetAwaiter().GetResult()); + } + + public void LogRequest(Uri uri) + { + + } + + public void LogResponse(HttpResponseMessage r) + { + _logger.LogDebug("http",$"responce from " + + (r.RequestMessage!=null && r.RequestMessage.RequestUri!=null ? r.RequestMessage.RequestUri.ToString() : "unknown") + + $" :\n "+ r.Content.ReadAsStringAsync().GetAwaiter().GetResult()); + } + + public void LogException(Exception ex) + { + _logger.LogError(ex); + } + + public void LogInfo(string info) + { + _logger.LogInfo(info); + } +} \ No newline at end of file diff --git a/Instagram/InstagramWrapper.cs b/Instagram/InstagramWrapper.cs new file mode 100644 index 0000000..3e1ca01 --- /dev/null +++ b/Instagram/InstagramWrapper.cs @@ -0,0 +1,58 @@ +using InstaSharper.API; +using InstaSharper.API.Builder; +using InstaSharper.Classes; +using InstaSharper.Classes.Models; + +namespace InstaFollowersOverseer.Instagram; + +public static class InstagramWrapper +{ + public static ContextLogger InstagramLogger = new("instagram",ParentLogger); + private static IInstaApi Api=null!; + + public static async void Init() + { + try + { + InstagramLogger.LogInfo("initializing instagram wrapper"); + if (CurrentConfig is null) + throw new NullReferenceException("config is null"); + var apiLogger = new InstagramApiLogger(); + // disabling http request/responce logging + apiLogger._logger.DebugLogEnabled = false; + Api = InstaApiBuilder.CreateBuilder() + .UseLogger(apiLogger) + .SetUser(new UserSessionData + { + UserName = CurrentConfig.instagramLogin, + Password = CurrentConfig.instagramPassword + }) + .SetRequestDelay(RequestDelay.FromSeconds(0, 1)) + .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"); + } + catch (OperationCanceledException) {} + catch (Exception ex) + { + InstagramLogger.LogError("init", ex); + Program.Stop(); + } + } + + public static async Task GetUserAsync(string usernameOrUrl) + { + // url + if (usernameOrUrl.Contains('/')) + { + throw new NotImplementedException("get user by url"); + } + + // username + var u=await Api.GetUserAsync(usernameOrUrl); + return u.Succeeded ? u.Value : null; + } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs index 4dabd70..f8b7700 100644 --- a/Program.cs +++ b/Program.cs @@ -2,6 +2,7 @@ global using System.Threading.Tasks; global using System.Linq; global using System.Collections.Generic; +global using DTLib; global using DTLib.Filesystem; global using DTLib.Extensions; global using DTLib.Dtsod; @@ -10,18 +11,20 @@ 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.Net.Http; using System.Text; using System.Threading; -using Telegram.Bot; -using Telegram.Bot.Polling; -using Telegram.Bot.Types; -using Telegram.Bot.Types.Enums; namespace InstaFollowersOverseer; static class Program { + public static readonly ContextLogger MainLogger = new("main", ParentLogger); + + private static CancellationTokenSource MainCancel=new(); + public static CancellationToken MainCancelToken = MainCancel.Token; + public static void Stop() => MainCancel.Cancel(); + + static void Main() { Console.InputEncoding=Encoding.UTF8; @@ -29,90 +32,31 @@ static class Program DTLibInternalLogging.SetLogger(MainLogger.ParentLogger); try { - config = Config.ReadFromFile(); - userSettings = UserSettings.ReadFromFile(); + MainLogger.LogInfo("reading config"); + CurrentConfig.LoadFromFile(); + CurrentUsersData.LoadFromFile(); - CancellationTokenSource mainCancel = new CancellationTokenSource(); Console.CancelKeyPress += (_, e) => { - mainCancel.Cancel(); + Stop(); Thread.Sleep(1000); MainLogger.LogInfo("all have cancelled"); e.Cancel = false; }; - var bot = new TelegramBotClient(config.botToken, new HttpClient()); - var receiverOptions = new ReceiverOptions - { - AllowedUpdates = { }, // receive all update types + Instagram.InstagramWrapper.Init(); + Telegram.TelegramWrapper.Init(); - }; - bot.StartReceiving(BotApiUpdateHandler, BotApiExceptionHandler, receiverOptions, mainCancel.Token); - - Task.Delay(-1, mainCancel.Token).GetAwaiter().GetResult(); + Task.Delay(-1, MainCancel.Token).GetAwaiter().GetResult(); Thread.Sleep(1000); } + catch (OperationCanceledException) {} catch (Exception ex) { MainLogger.LogError(ex); } + CurrentConfig.SaveToFile(); + CurrentUsersData.SaveToFile(); Console.ResetColor(); } - - private static ContextLogger botLogger = new ContextLogger("bot", MainLogger.ParentLogger); - - static async Task BotApiUpdateHandler(ITelegramBotClient bot, Update update, CancellationToken cls) - { - try - { - switch (update.Type) - { - case UpdateType.Message: - { - var message = update.Message!; - if (message.Text!.StartsWith('/')) - { - botLogger.LogInfo($"user {message.Chat.Id} sent command {message.Text}"); - var spl = message.Text.SplitToList(' '); - string command = spl[0].Substring(1); - spl.RemoveAt(0); - string[] args = spl.ToArray(); - switch (command) - { - case "start": - await bot.SendTextMessageAsync(message.Chat, "hi"); - break; - case "oversee": - break; - // default: - // throw new BotCommandException(command, args); - } - } - else botLogger.LogDebug($"message recieved: {message.Text}"); - - break; - } /* - case UpdateType.EditedMessage: - break; - case UpdateType.InlineQuery: - break; - case UpdateType.ChosenInlineResult: - break; - case UpdateType.CallbackQuery: - break;*/ - default: - botLogger.LogWarn($"unknown update type: {update.Type}"); - break; - } - } - catch (Exception ex) - { - botLogger.LogWarn("UpdateHandler", ex); - } - } - static Task BotApiExceptionHandler(ITelegramBotClient bot, Exception ex, CancellationToken cls) - { - botLogger.LogError(ex); - return Task.CompletedTask; - } } \ No newline at end of file diff --git a/SharedData.cs b/SharedData.cs index f45d423..41cc837 100644 --- a/SharedData.cs +++ b/SharedData.cs @@ -4,13 +4,10 @@ public static class SharedData { internal const string EmbeddedResourcesPrefix = "InstaFollowersOverseer.resources"; -#nullable disable - internal static Config config; - internal static UserSettings userSettings; -#nullable enable - - public static readonly ContextLogger MainLogger = new ContextLogger("main",new CompositeLogger( - new ConsoleLogger(), - new FileLogger("logs","InstaFollowersOverseer")) - ); + internal static Config CurrentConfig = new("config"); + internal static UsersData CurrentUsersData = new("users-data"); + + public static readonly CompositeLogger ParentLogger = new( + new ConsoleLogger(), + new FileLogger("logs", "InstaFollowersOverseer")); } \ No newline at end of file diff --git a/Telegram/TelegramWrapper.cs b/Telegram/TelegramWrapper.cs new file mode 100644 index 0000000..37c4415 --- /dev/null +++ b/Telegram/TelegramWrapper.cs @@ -0,0 +1,134 @@ +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; + +public static class TelegramWrapper +{ + private static ContextLogger TelegramLogger = new("telegram", ParentLogger); + private static TelegramBotClient Bot=null!; + + public static async void Init() + { + try + { + TelegramLogger.LogInfo("initializing telegram wrapper"); + if (CurrentConfig is null) + throw new NullReferenceException("config is null"); + Bot = new TelegramBotClient(CurrentConfig.botToken, new HttpClient()); + await Bot.SetMyCommandsAsync(new BotCommand[] + { + new() { Command = "start", Description = "starts the bot"}, + // new() { Command = "help", Description = "shows commands list" }, + new() { Command = "oversee", Description = "[instagram username] - " + + "enables notifications about instagram user's followers" }, + new() { Command = "list", Description = "shows list of overseeing instagram users" } + }); + var receiverOptions = new ReceiverOptions + { + // AllowedUpdates = { }, // receive all update types + }; + TelegramLogger.LogInfo("bot starting recieving long polls"); + Bot.StartReceiving(BotApiUpdateHandler, BotApiExceptionHandler, receiverOptions, Program.MainCancelToken); + TelegramLogger.LogInfo("telegram wrapper have initialized successfully"); + } + catch (OperationCanceledException) {} + catch (Exception ex) + { + TelegramLogger.LogError("init", ex); + Program.Stop(); + } + } + + private static Task BotApiExceptionHandler(ITelegramBotClient bot, Exception ex, CancellationToken cls) + { + TelegramLogger.LogError(ex); + return Task.CompletedTask; + } + + static async Task SendInfoReply(string text, Message replyToMessage) + { + TelegramLogger.LogInfo(text); + await Bot.SendTextMessageAsync(replyToMessage.Chat, text, + replyToMessageId: replyToMessage.MessageId, + parseMode:ParseMode.MarkdownV2); + } + static async Task SendErrorReply(string text, Message replyToMessage) + { + TelegramLogger.LogWarn(text); + await Bot.SendTextMessageAsync(replyToMessage.Chat, "error: "+text, + replyToMessageId: replyToMessage.MessageId, + parseMode:ParseMode.MarkdownV2); + } + + private static async Task BotApiUpdateHandler(ITelegramBotClient bot, Update update, CancellationToken cls) + { + try + { + switch (update.Type) + { + case UpdateType.Message: + { + var message = update.Message!; + if (message.Text!.StartsWith('/')) + { + TelegramLogger.LogInfo($"user {message.Chat.Id} sent command {message.Text}"); + var spl = message.Text.SplitToList(' '); + string command = spl[0].Substring(1); + spl.RemoveAt(0); + string[] args = spl.ToArray(); + await ExecCommandAsync(command, args, message); + } + else TelegramLogger.LogDebug($"message recieved: {message.Text}"); + break; + } + /*case UpdateType.InlineQuery: + break; + case UpdateType.ChosenInlineResult: + break; + case UpdateType.CallbackQuery: + break;*/ + default: + TelegramLogger.LogWarn($"unknown update type: {update.Type}"); + break; + } + } + catch (OperationCanceledException) {} + catch (Exception ex) + { + TelegramLogger.LogWarn("UpdateHandler", ex); + } + } + + private static async Task ExecCommandAsync(string command, string[] args, Message message) + { + switch (command) + { + case "start": + await Bot.SendTextMessageAsync(message.Chat, "hi"); + break; + case "oversee": + { + string usernameOrUrl = args[0]; + await SendInfoReply($"searching for instagram user <{usernameOrUrl}>", message); + var user = await InstagramWrapper.GetUserAsync(usernameOrUrl); + if (user is null) + { + await SendErrorReply($"user **{usernameOrUrl}** doesnt exist", message); + return; + } + CurrentUsersData.AddOrSet(message.Chat.Id.ToString(), new InstagramObservableParams(usernameOrUrl)); + CurrentUsersData.SaveToFile(); + break; + } + default: + await SendErrorReply("ivalid command", message); + break; + } + } +} \ No newline at end of file diff --git a/UserSettings.cs b/UserSettings.cs deleted file mode 100644 index 914e87a..0000000 --- a/UserSettings.cs +++ /dev/null @@ -1,114 +0,0 @@ -namespace InstaFollowersOverseer; - -public class UserSettings -{ - private const string user_settings_file="user-settings.dtsod"; - private const string user_settings_example_file="user-settings-example.dtsod"; - - private Dictionary> userSettings=new(); - - private UserSettings() - { - - } - - public UserSettings(DtsodV23 _userSettings) - { - try - { - foreach (var uset in _userSettings) - { - string telegramUserId = uset.Key; - - List oparams = new List(); - foreach (DtsodV23 _overseeParams in uset.Value) - oparams.Add(new InstagramObservableParams(_overseeParams)); - - userSettings.Add(telegramUserId, oparams); - } - } - catch (Exception ex) - { - throw new Exception($"your {user_settings_file} format is invalid\n" - + $"See {user_settings_example_file}", innerException:ex); - } - } - - public static UserSettings ReadFromFile() - { - EmbeddedResources.CopyToFile( - $"{EmbeddedResourcesPrefix}.{user_settings_example_file}", - user_settings_example_file); - - if (File.Exists(user_settings_file)) - return new UserSettings(new DtsodV23(File.ReadAllText(user_settings_file))); - - MainLogger.LogWarn($"file {user_settings_file} doesnt exist, creating new"); - File.WriteAllText(user_settings_file,"#DtsodV23\n"); - return new UserSettings(); - } - - public DtsodV23 ToDtsod() - { - var b = new DtsodV23(); - foreach (var userS in userSettings) - b.Add(userS.Key, - userS.Value.Select(iop => - iop.ToDtsod() - ).ToList()); - return b; - } - - public override string ToString() => ToDtsod().ToString(); - - public void SaveToFile() - { - File.Copy(user_settings_file, - $"backups/{user_settings_file}.old-"+ - "{DateTime.Now.ToString(MyTimeFormat.ForFileNames)}", - true); - - File.OpenWrite(user_settings_file) - .FluentWriteString("#DtsodV23\n") - .WriteString(ToDtsod().ToString()); - } - - public List Get(string telegramUserId) - { - if (!userSettings.TryGetValue(telegramUserId, out var overseeParams)) - throw new Exception($"there is no settings for user {telegramUserId}"); - return overseeParams; - } - - public void AddOrSet(string telegramUserId, InstagramObservableParams instagramObservableParams) - { - // Add - // doesnt contain settings for telegramUserId - if (!userSettings.TryGetValue(telegramUserId, out var thisUserSettings)) - { - userSettings.Add(telegramUserId, new (){ instagramObservableParams }); - return; - } - - // Set - // settings for telegramUserId contain InstagramObservableParams with instagramObservableParams.instagramUserId - for (var i = 0; i < thisUserSettings.Count; i++) - { - if (thisUserSettings[i].instagramUserId == instagramObservableParams.instagramUserId) - { - thisUserSettings[i] = instagramObservableParams; - return; - } - } - - // Add - // doesnt contain InstagramObservableParams with instagramObservableParams.instagramUserId - thisUserSettings.Add(instagramObservableParams); - } - - public void AddOrSet(string telegramUserId, IEnumerable instagramObservableParams) - { - foreach (var p in instagramObservableParams) - AddOrSet(telegramUserId, p); - } -} \ No newline at end of file diff --git a/UsersData.cs b/UsersData.cs new file mode 100644 index 0000000..ca7d553 --- /dev/null +++ b/UsersData.cs @@ -0,0 +1,81 @@ +namespace InstaFollowersOverseer; + +public class UsersData : DtsodFile +{ + private Dictionary> usersData=new(); + + public UsersData(string fileName) : base(fileName) {} + + public override void LoadFromFile() + { + var dtsod=ReadDtsodFromFile(false); + try + { + foreach (var uset in dtsod) + { + string telegramUserId = uset.Key; + + List oparams = new(); + foreach (DtsodV23 _overseeParams in uset.Value) + oparams.Add(new InstagramObservableParams(_overseeParams)); + + usersData.Add(telegramUserId, oparams); + } + } + catch (Exception ex) + { + throw new Exception($"your {FileName} format is invalid\n" + + $"See {FileExampleName}", innerException: ex); + } + } + + public override DtsodV23 ToDtsod() + { + var b = new DtsodV23(); + foreach (var userS in usersData) + b.Add(userS.Key, + userS.Value.Select(iop => + iop.ToDtsod() + ).ToList()); + return b; + } + + public List Get(string telegramUserId) + { + if (!usersData.TryGetValue(telegramUserId, out var overseeParams)) + throw new Exception($"there is no settings for user {telegramUserId}"); + return overseeParams; + } + + public void AddOrSet(string telegramUserId, InstagramObservableParams instagramObservableParams) + { + // Add + // doesnt contain settings for telegramUserId + if (!usersData.TryGetValue(telegramUserId, out var thisUsersData)) + { + usersData.Add(telegramUserId, new (){ instagramObservableParams }); + return; + } + + // Set + // settings for telegramUserId contain InstagramObservableParams with instagramObservableParams.instagramUserId + for (var i = 0; i < thisUsersData.Count; i++) + { + if (thisUsersData[i].instagramUserId == instagramObservableParams.instagramUserId) + { + thisUsersData[i] = instagramObservableParams; + return; + } + } + + // Add + // doesnt contain InstagramObservableParams with instagramObservableParams.instagramUserId + thisUsersData.Add(instagramObservableParams); + } + + public void AddOrSet(string telegramUserId, IEnumerable instagramObservableParams) + { + foreach (var p in instagramObservableParams) + AddOrSet(telegramUserId, p); + } +} \ No newline at end of file diff --git a/resources/user-settings-example.dtsod b/resources/users-data-example.dtsod similarity index 100% rename from resources/user-settings-example.dtsod rename to resources/users-data-example.dtsod