From b4e726c79ecd4a6568d4616f44d57af42c4d37bc Mon Sep 17 00:00:00 2001 From: "David G. Moore, Jr." Date: Tue, 26 Mar 2024 13:36:45 -0400 Subject: [PATCH] Telegram userbot --- src/UserBot/.vscode/launch.json | 21 +++ src/UserBot/.vscode/tasks.json | 24 +++ src/UserBot/Config/IUSerBotConfig.cs | 33 ++-- .../Config/Telegram.UserBot.Config.csproj | 10 +- src/UserBot/Config/UserBotConfig.cs | 104 ++++++---- ...ild.targets => Driectory.Build.targets.no} | 0 ...m.UserBot.Store.EntityFrameworkCore.csproj | 1 + .../Store.FileSystem/FileUserBotStore.cs | 24 ++- .../Telegram.UserBot.Store.FileSystem.csproj | 5 + .../Store.Memory/MemoryUserBotStore.cs | 39 ++-- .../Telegram.UserBot.Store.Memory.csproj | 1 + .../Telegram.UserBot.Store.Redis.csproj | 1 + src/UserBot/Telegram.UserBot.Cli/LICENSE.md | 35 ++++ src/UserBot/Telegram.UserBot.Cli/Program.cs | 2 + .../Telegram.UserBot.Cli.csproj | 13 ++ src/UserBot/Telegram.UserBot.Cli/icon.png | Bin 0 -> 26997 bytes src/UserBot/UserBot.Cli/LoggingExtensions.cs | 23 +++ src/UserBot/UserBot.Cli/Program.cs | 96 +++++++++- src/UserBot/UserBot.Cli/ProgramRunner.cs | 77 ++++++++ src/UserBot/UserBot.Cli/UserBot.Cli.csproj | 6 + src/UserBot/UserBot/DI.cs | 18 +- src/UserBot/UserBot/Extensions.cs | 12 ++ src/UserBot/UserBot/IUserBot.cs | 66 +++---- src/UserBot/UserBot/IWTelegramClient.cs | 72 +++++++ src/UserBot/UserBot/PeerConstructors.cs | 12 ++ src/UserBot/UserBot/Telegram.UserBot.csproj | 2 +- src/UserBot/UserBot/Telegram.UserBot.props | 3 +- src/UserBot/UserBot/UserBot.cs | 178 ++++++++++++++---- src/UserBot/UserBot/UserBotLogger.cs | 143 +++++++++++--- 29 files changed, 839 insertions(+), 182 deletions(-) create mode 100644 src/UserBot/.vscode/launch.json create mode 100644 src/UserBot/.vscode/tasks.json rename src/UserBot/{Driectory.Build.targets => Driectory.Build.targets.no} (100%) create mode 100644 src/UserBot/Telegram.UserBot.Cli/LICENSE.md create mode 100644 src/UserBot/Telegram.UserBot.Cli/Program.cs create mode 100644 src/UserBot/Telegram.UserBot.Cli/Telegram.UserBot.Cli.csproj create mode 100644 src/UserBot/Telegram.UserBot.Cli/icon.png create mode 100644 src/UserBot/UserBot.Cli/LoggingExtensions.cs create mode 100644 src/UserBot/UserBot.Cli/ProgramRunner.cs create mode 100644 src/UserBot/UserBot/Extensions.cs create mode 100644 src/UserBot/UserBot/IWTelegramClient.cs create mode 100644 src/UserBot/UserBot/PeerConstructors.cs diff --git a/src/UserBot/.vscode/launch.json b/src/UserBot/.vscode/launch.json new file mode 100644 index 0000000..1e8f51e --- /dev/null +++ b/src/UserBot/.vscode/launch.json @@ -0,0 +1,21 @@ +{ + "configurations": [ + + { + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "Build UserBot.Cli", + "program": "${workspaceFolder}/UserBot.Cli/bin/Local/net8.0/UserBot.Cli.dll", + "args": [], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + "console": "internalConsole", + "env": { + "api_id": "24851338", + "api_hash": "82a9e257eb717d3a46f8479cdf307434", + "persist_to": "Memory" + } + } + ] +} diff --git a/src/UserBot/.vscode/tasks.json b/src/UserBot/.vscode/tasks.json new file mode 100644 index 0000000..f52ec67 --- /dev/null +++ b/src/UserBot/.vscode/tasks.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://raw.githubusercontent.com/microsoft/azure-pipelines-task-lib/master/tasks.schema.json", + "version": { + "Major": 2, + "Minor": 0, + "Patch": 0 + }, + "tasks": [ + { + "label": "Build UserBot.Cli", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "-c:Local", + "-p:BuildFromSource=false", + "${workspaceFolder}/UserBot.Cli/UserBot.Cli.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + } + ] +} diff --git a/src/UserBot/Config/IUSerBotConfig.cs b/src/UserBot/Config/IUSerBotConfig.cs index 91f0d2e..486c7db 100644 --- a/src/UserBot/Config/IUSerBotConfig.cs +++ b/src/UserBot/Config/IUSerBotConfig.cs @@ -1,15 +1,18 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Telegram.UserBot.Store.Abstractions; - -namespace Telegram.UserBot.Config -{ - public partial interface IUserBotConfig - { - IUserBotStore? GetSessionStore(); - string? GetConfigVariable(string variable); - string Prompt(string variable); - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Telegram.UserBot.Store.Abstractions; + +namespace Telegram.UserBot.Config +{ + public partial interface IUserBotConfig + { + // GetSessionStore GetSessionStore { get; set; } + // IUserBotStore? GetSessionStore(); + string? GetConfigVariable(string variable); + string? Prompt(string variable); + } + + public delegate IUserBotStore GetSessionStore(IServiceProvider services); +} diff --git a/src/UserBot/Config/Telegram.UserBot.Config.csproj b/src/UserBot/Config/Telegram.UserBot.Config.csproj index ab5985a..c26709c 100644 --- a/src/UserBot/Config/Telegram.UserBot.Config.csproj +++ b/src/UserBot/Config/Telegram.UserBot.Config.csproj @@ -1,17 +1,17 @@ - - - + + + - + diff --git a/src/UserBot/Config/UserBotConfig.cs b/src/UserBot/Config/UserBotConfig.cs index 448f98f..b9d40aa 100644 --- a/src/UserBot/Config/UserBotConfig.cs +++ b/src/UserBot/Config/UserBotConfig.cs @@ -4,13 +4,12 @@ namespace Telegram.UserBot.Config; using Telegram.UserBot.Store.Abstractions; using Telegram.UserBot.Store; -using Telegram.UserBot.Store.EntityFrameworkCore; -using Telegram.UserBot.Store.Redis; using static Environment; using Microsoft.EntityFrameworkCore; -public class UserBotConfig +public class UserBotConfig : IUserBotConfig { +#pragma warning disable IDE1006 public const string api_hash = nameof(api_hash); public const string api_id = nameof(api_id); public const string verification_code = nameof(verification_code); @@ -23,18 +22,25 @@ public class UserBotConfig public const string session_pathname = nameof(session_pathname); public const string session_key = nameof(session_key); public const string server_address = nameof(server_address); - - public virtual string Prompt(string variable) + public const string system_lang_code = nameof(system_lang_code); + public const string lang_code = nameof(lang_code); + public const string app_version = nameof(app_version); + public const string system_version = nameof(system_version); + public const string device_model = nameof(device_model); +#pragma warning restore IDE1006 + + public virtual string? Prompt(string variable) { Console.Write($"Enter {variable}: "); - return Console.ReadLine(); + var value = Console.ReadLine(); + return value.IsPresent() ? value : null; } public string? GetConfigVariable(string variable) => variable switch { #pragma warning disable S1121 - api_id => (ApiId = long.Parse(ApiId.ToString() ?? Prompt(api_id))).ToString(), + api_id => ApiId ??= Prompt(api_id), api_hash => ApiHash ??= Prompt(api_hash), verification_code => VerificationCode ??= Prompt(verification_code), first_name => FirstName ??= Prompt(first_name), @@ -47,46 +53,47 @@ public virtual string Prompt(string variable) _ => Prompt(variable) }; - public IUserBotStore? GetSessionStore() - { - return PersistTo switch - { - PersistTo.Memory => new MemoryUserBotStore(), - PersistTo.File => new FileUserBotStore(SessionPathname), - PersistTo.Database - => new DbUserBotStore( - new UserBotDbContext( - new DbContextOptionsBuilder() - .UseSqlServer(DbConnectionString) - .Options - ), - SessionPathname - ), - PersistTo.Redis => new RedisUserBotStore( - new RedisUserBotOptions { Key = SessionKey, ConnectionString = RedisConnectionString } - ), - _ => null, - }; - } - - [JProp((persist_to))] - public PersistTo PersistTo { get; set; } = + // public IUserBotStore? GetSessionStore() + // { + // return PersistTo switch + // { + // PersistTo.Memory => new MemoryUserBotStore(), + // PersistTo.File => new FileUserBotStore(SessionPathname), + // PersistTo.Database + // => new DbUserBotStore( + // new UserBotDbContext( + // new DbContextOptionsBuilder() + // .UseSqlServer(DbConnectionString) + // .Options + // ), + // SessionPathname + // ), + // PersistTo.Redis => new RedisUserBotStore( + // new RedisUserBotOptions { Key = SessionKey, ConnectionString = RedisConnectionString } + // ), + // _ => null, + // }; + // } + + // public GetSessionStore GetSessionStore { get; set; } + + [JProp(persist_to)] + public virtual PersistTo PersistTo { get; set; } = Enum.TryParse(GetEnvironmentVariable(persist_to), out var @enum) ? @enum : default; [JProp(db_connection_string)] - public string DbConnectionString { get; set; } = GetEnvironmentVariable(db_connection_string); + public virtual string DbConnectionString { get; set; } = GetEnvironmentVariable(db_connection_string); [JProp(api_id)] - public long ApiId { get; set; } = - long.TryParse(GetEnvironmentVariable(api_id), out var @long) ? @long : default; + public virtual string ApiId { get; set; } = GetEnvironmentVariable(api_id); [JProp(api_hash)] - public string ApiHash { get; set; } = GetEnvironmentVariable(api_hash); + public virtual string ApiHash { get; set; } = GetEnvironmentVariable(api_hash); [JProp(verification_code)] - public string VerificationCode { get; set; } = GetEnvironmentVariable(verification_code); + public virtual string VerificationCode { get; set; } = GetEnvironmentVariable(verification_code); [JProp(first_name)] public string FirstName { get; set; } = GetEnvironmentVariable(first_name); @@ -95,17 +102,32 @@ public virtual string Prompt(string variable) public string LastName { get; set; } = GetEnvironmentVariable(last_name); [JProp(password)] - public string Password { get; set; } = GetEnvironmentVariable(password); + public virtual string Password { get; set; } = GetEnvironmentVariable(password); [JProp(session_pathname)] - public string SessionPathname { get; set; } = GetEnvironmentVariable(session_pathname); + public virtual string SessionPathname { get; set; } = GetEnvironmentVariable(session_pathname); [JProp(redis_connection_string)] - public string RedisConnectionString { get; set; } = GetEnvironmentVariable(redis_connection_string); + public virtual string RedisConnectionString { get; set; } = GetEnvironmentVariable(redis_connection_string); [JProp(session_key)] - public string SessionKey { get; set; } = GetEnvironmentVariable(session_key); + public virtual string SessionKey { get; set; } = GetEnvironmentVariable(session_key); [JProp(server_address)] - public string ServerAddress { get; set; } = GetEnvironmentVariable(server_address); + public virtual string ServerAddress { get; set; } = GetEnvironmentVariable(server_address); + + [JProp(system_lang_code)] + public virtual string SystemLanguageCode { get; set; } = GetEnvironmentVariable(system_lang_code); + + [JProp(lang_code)] + public virtual string LanguageCode { get; set; } = GetEnvironmentVariable(lang_code); + + [JProp(app_version)] + public virtual string AppVersion { get; set; } = GetEnvironmentVariable(app_version); + + [JProp(system_version)] + public virtual string SystemVersion { get; set; } = GetEnvironmentVariable(system_version); + + [JProp(device_model)] + public virtual string DeviceModel { get; set; } = GetEnvironmentVariable(device_model); } diff --git a/src/UserBot/Driectory.Build.targets b/src/UserBot/Driectory.Build.targets.no similarity index 100% rename from src/UserBot/Driectory.Build.targets rename to src/UserBot/Driectory.Build.targets.no diff --git a/src/UserBot/Store.EntityFrameworkCore/Telegram.UserBot.Store.EntityFrameworkCore.csproj b/src/UserBot/Store.EntityFrameworkCore/Telegram.UserBot.Store.EntityFrameworkCore.csproj index a56a111..d248b64 100644 --- a/src/UserBot/Store.EntityFrameworkCore/Telegram.UserBot.Store.EntityFrameworkCore.csproj +++ b/src/UserBot/Store.EntityFrameworkCore/Telegram.UserBot.Store.EntityFrameworkCore.csproj @@ -10,6 +10,7 @@ + diff --git a/src/UserBot/Store.FileSystem/FileUserBotStore.cs b/src/UserBot/Store.FileSystem/FileUserBotStore.cs index 152f7c6..01459df 100644 --- a/src/UserBot/Store.FileSystem/FileUserBotStore.cs +++ b/src/UserBot/Store.FileSystem/FileUserBotStore.cs @@ -1,21 +1,30 @@ namespace Telegram.UserBot.Store.FileSystem; using Telegram.UserBot.Store.Abstractions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; -internal class FileUserBotStore : IUserBotStore +public class FileUserBotStore : IUserBotStore, ILog { private readonly string _filePath; + public ILogger? Logger { get; } + private readonly FilePersistedUserBotConfig _options; - public FileUserBotStore(string? filePath = null) + public FileUserBotStore(IOptions options, ILogger? logger = null) { + _options = options.Value; + _filePath = - filePath - ?? Path.Combine(Path.GetDirectoryName(GetType().Assembly.Location), "session.dat"); + _options.FilePath + ?? Combine(GetDirectoryName(GetType().Assembly.Location), "session.dat"); if (!File.Exists(_filePath)) { - File.Create(_filePath).Close(); + Create(_filePath).Close(); } + + Logger = logger; } public Stream GetStream() @@ -23,3 +32,8 @@ public Stream GetStream() return new FileStream(_filePath, FileMode.OpenOrCreate, FileAccess.ReadWrite); } } + +public class FilePersistedUserBotConfig : Telegram.UserBot.Config.UserBotConfig +{ + public string? FilePath { get; set; } +} diff --git a/src/UserBot/Store.FileSystem/Telegram.UserBot.Store.FileSystem.csproj b/src/UserBot/Store.FileSystem/Telegram.UserBot.Store.FileSystem.csproj index 9417d52..c11f201 100644 --- a/src/UserBot/Store.FileSystem/Telegram.UserBot.Store.FileSystem.csproj +++ b/src/UserBot/Store.FileSystem/Telegram.UserBot.Store.FileSystem.csproj @@ -8,8 +8,13 @@ + + + + + diff --git a/src/UserBot/Store.Memory/MemoryUserBotStore.cs b/src/UserBot/Store.Memory/MemoryUserBotStore.cs index 0fd30cb..e5fe9bc 100644 --- a/src/UserBot/Store.Memory/MemoryUserBotStore.cs +++ b/src/UserBot/Store.Memory/MemoryUserBotStore.cs @@ -1,18 +1,21 @@ -namespace Telegram.UserBot.Store; - -using Telegram.UserBot.Store.Abstractions; - -internal class MemoryUserBotStore : IUserBotStore -{ - private readonly MemoryStream _stream; - - public MemoryUserBotStore() - { - _stream = new MemoryStream(); - } - - public Stream GetStream() - { - return _stream; - } -} +namespace Telegram.UserBot.Store; +using Microsoft.Extensions.Logging; +using Telegram.UserBot.Store.Abstractions; + +internal class MemoryUserBotStore : IUserBotStore, ILog +{ + private readonly MemoryStream _stream; + public ILogger? Logger { get; } + + public MemoryUserBotStore(ILogger? logger = null) + { + _stream = new MemoryStream(); + Logger = logger; + } + + public Stream GetStream() + { + Logger?.LogInformation("Getting memory stream for userbot session storage."); + return _stream; + } +} diff --git a/src/UserBot/Store.Memory/Telegram.UserBot.Store.Memory.csproj b/src/UserBot/Store.Memory/Telegram.UserBot.Store.Memory.csproj index 9417d52..dd78918 100644 --- a/src/UserBot/Store.Memory/Telegram.UserBot.Store.Memory.csproj +++ b/src/UserBot/Store.Memory/Telegram.UserBot.Store.Memory.csproj @@ -8,6 +8,7 @@ + diff --git a/src/UserBot/Store.Redis/Telegram.UserBot.Store.Redis.csproj b/src/UserBot/Store.Redis/Telegram.UserBot.Store.Redis.csproj index 961ae92..5b92cd7 100644 --- a/src/UserBot/Store.Redis/Telegram.UserBot.Store.Redis.csproj +++ b/src/UserBot/Store.Redis/Telegram.UserBot.Store.Redis.csproj @@ -5,6 +5,7 @@ + diff --git a/src/UserBot/Telegram.UserBot.Cli/LICENSE.md b/src/UserBot/Telegram.UserBot.Cli/LICENSE.md new file mode 100644 index 0000000..4f592f8 --- /dev/null +++ b/src/UserBot/Telegram.UserBot.Cli/LICENSE.md @@ -0,0 +1,35 @@ +--- +date: 2023-07-13T05:44:46:00-05:00Z +description: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files, yadda, yadda, yadda... +keywords: +- IP +- copyright +- license +- mit +permissions: +- commercial-use +- modifications +- distribution +- private-use +conditions: +- include-copyright +limitations: +- liability +- warranty +lastmod: 2024-01-0T00:39:00.0000+05:00Z +license: MIT +slug: mit-license +title: MIT License +type: license +--- + +# MIT License + +## Copyright © 2022-2024 [David G. Moore, Jr.](mailto:david@dgmjr.io "Send Dr. Moore") ([@dgmjr](https://github.com/dgmjr "Contact Dr. Moore on GitHub")), All Rights Reserved + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/src/UserBot/Telegram.UserBot.Cli/Program.cs b/src/UserBot/Telegram.UserBot.Cli/Program.cs new file mode 100644 index 0000000..83fa4f4 --- /dev/null +++ b/src/UserBot/Telegram.UserBot.Cli/Program.cs @@ -0,0 +1,2 @@ +// See https://aka.ms/new-console-template for more information +Console.WriteLine("Hello, World!"); diff --git a/src/UserBot/Telegram.UserBot.Cli/Telegram.UserBot.Cli.csproj b/src/UserBot/Telegram.UserBot.Cli/Telegram.UserBot.Cli.csproj new file mode 100644 index 0000000..0a32649 --- /dev/null +++ b/src/UserBot/Telegram.UserBot.Cli/Telegram.UserBot.Cli.csproj @@ -0,0 +1,13 @@ + + + + Exe + net8.0 + enable + enable + false + false + + + + diff --git a/src/UserBot/Telegram.UserBot.Cli/icon.png b/src/UserBot/Telegram.UserBot.Cli/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..db07a039193d57c5147e651a7ab732121bfd20ba GIT binary patch literal 26997 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 CAey28 literal 0 HcmV?d00001 diff --git a/src/UserBot/UserBot.Cli/LoggingExtensions.cs b/src/UserBot/UserBot.Cli/LoggingExtensions.cs new file mode 100644 index 0000000..24ca834 --- /dev/null +++ b/src/UserBot/UserBot.Cli/LoggingExtensions.cs @@ -0,0 +1,23 @@ +namespace Telegram.UserBot; +using Microsoft.Extensions.Logging; + +public static partial class LoggingExtensions +{ + [LoggerMessage(0, LogLevel.Information, "Chat {ChatId} has {ParticipantsCount} participants")] + public static partial void ChatParticipants2(this ILogger logger, long chatId, int participantsCount); + + [LoggerMessage(1, LogLevel.Information, "Channel {ChannelId} {ChannelTitle} has {ParticipantsCount} participants")] + public static partial void ChannelParticipants(this ILogger logger, long channelId, string channelTitle, int participantsCount); + + [LoggerMessage(3, LogLevel.Information, "User {UserId} is in the chat")] + public static partial void UserInChat(this ILogger logger, long userId); + + [LoggerMessage(4, LogLevel.Information, "User {UserId} is the owner '{Rank}'")] + public static partial void CreatorInChat(this ILogger logger, long userId, string rank); + + [LoggerMessage(5, LogLevel.Information, "User {UserId} is admin '{Rank}'")] + public static partial void AdminInChat(this ILogger logger, long userId, string rank); + + [LoggerMessage(6, LogLevel.Information, "User {UserId} is bot")] + public static partial void BotInChat(this ILogger logger, long userId); +} diff --git a/src/UserBot/UserBot.Cli/Program.cs b/src/UserBot/UserBot.Cli/Program.cs index fd68f40..794cd7f 100644 --- a/src/UserBot/UserBot.Cli/Program.cs +++ b/src/UserBot/UserBot.Cli/Program.cs @@ -1,12 +1,102 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Configuration; +using Telegram.UserBot; +using TL; -var builder = Host.CreateApplicationBuilder(args); +var builder = Host.CreateApplicationBuilder( + new HostApplicationBuilderSettings + { + ApplicationName = "Telegram UserBot CLI", + EnvironmentName = Environments.Development, + ContentRootPath = Path.GetDirectoryName(typeof(Program).Assembly.Location), + Args = args + } +); +builder.Configuration.AddJsonFile("appsettings.json"); builder.Services.AddLogging(builder => builder.AddConsole()); -builder.Services.AddUserBot(_ => {}); +builder.AddUserBot(_ => { }); +builder.Services.AddSingleton(); var app = builder.Build(); await app.StartAsync(); -var bot = app. +await app.Services.GetRequiredService().RunAsync(); +// var bot = app.Services.GetRequiredService(); +// await bot.LoginUserIfNeeded(); + +// Console.WriteLine("Chats:"); +// var chats = await bot.Messages_GetAllChats(); + +// chats.chats.ForEach(chat => Console.WriteLine(chat.Value.ID + " " + chat.Value.Title)); + +// var backroomChatIds = app.Services +// .GetRequiredService() +// .GetSection("BackroomChatIds") +// .Get>(); + +// var backroomChats = chats.chats.Where(chat => backroomChatIds.Contains(chat.Value.ID)); +// Console.WriteLine("Backroom Chats:"); +// var distinctUsers = new HashSet(); +// backroomChats.ForEach(async kvp => +// { +// try +// { +// var chat = kvp.Value; +// if(chat.IsGroup || chat.IsChannel) +// { +// if(chat is Chat chat1) +// { +// var participants = await ((WTelegram.Client)bot).Messages_GetFullChat(chat.ID); +// foreach(var participant in participants.users) +// { +// var user = participants.users[participant.Value.ID]; +// if(!user.IsBot) +// { +// distinctUsers.Add(participant.Value.ID); +// } +// Console.WriteLine($"{participant.Value} is in the chat"); +// } +// } +// else if(chat is Channel channel) +// { +// var participants = await bot.Channels_GetAllParticipants(channel); +// Logger?.($"{channel.ID} {channel.Title} {participants.participants.Length}"); +// foreach(var participant in participants.participants) +// { +// var user = participants.users[participant.UserId]; +// if(!user.IsBot) +// { +// distinctUsers.Add(participant.UserId); +// } +// if (participant is ChannelParticipantCreator cpc) Console.WriteLine($"{user} is the owner '{cpc.rank}'"); +// else if (participant is ChannelParticipantAdmin cpa) Console.WriteLine($"{user} is admin '{cpa.rank}'"); +// else if(user.IsBot) Console.WriteLine($"{user} is bot"); +// else Console.WriteLine(user); +// } +// } +// } +// } +// catch(Exception ex) +// { +// Console.WriteLine($"Error: {ex.Message}\n{ex.StackTrace}"); +// } +// }); +// Console.WriteLine($"Distinct Users ({distinctUsers.Count}):"); +// distinctUsers.ForEach(userId => +// { +// var user = bot.Users[userId]; +// Console.WriteLine(user); +// }); + + +// Console.WriteLine("Dialogs:"); +// var dialogs = await bot.Messages_GetAllDialogs(); +// dialogs.dialogs.ForEach(dialog => Console.WriteLine(dialog.Peer.ID + " " + dialog.TopMessage)); + +// Console.WriteLine("Chats:"); +// bot.Chats.ForEach(chat => Console.WriteLine(chat.Value.ID + " " + chat.Value.Title)); + +// Console.WriteLine("Users:"); +// bot.Users.ForEach(user => Console.WriteLine(user.Value.ID + " " + user.Value.first_name + " " + user.Value.last_name)); diff --git a/src/UserBot/UserBot.Cli/ProgramRunner.cs b/src/UserBot/UserBot.Cli/ProgramRunner.cs new file mode 100644 index 0000000..7f052a9 --- /dev/null +++ b/src/UserBot/UserBot.Cli/ProgramRunner.cs @@ -0,0 +1,77 @@ +namespace Telegram.UserBot; +using Microsoft.Extensions.Configuration; +using WTelegram; +using Telegram.UserBot; +using Microsoft.Extensions.Logging; +using Dgmjr.Abstractions; + +public class ProgramRunner(IUserBot bot, IConfiguration config, Logger logger) : ILog +{ + public ILogger Logger => logger; + public IConfiguration Configuration => config; + public IUserBot Bot => bot; + + public async Task RunAsync() + { + await Bot.LoginUserIfNeeded(); + Console.WriteLine("Chats:"); + var chats = await ((WT.Client)Bot).Messages_GetAllChats(); + chats.CollectChats(Bot.Chats); + + var backroomChatIds = Configuration + .GetSection("BackroomChatIds") + .Get>(); + var backroomChats = chats.chats.Where(chat => backroomChatIds.Contains(chat.Value.ID)); + + var distinctUsers = new HashSet(); + backroomChats.ForEach(async kvp => + { + try + { + var chat = kvp.Value; + if(chat.IsGroup || chat.IsChannel) + { + if(chat is Chat) + { + var participants = await ((WTelegram.Client)Bot).Messages_GetFullChat(chat.ID); + participants.CollectUsersChats(Bot.Users, Bot.Chats); + Logger?.ChatParticipants2(chat.ID, participants.users.Count); + foreach(var participantId in participants.users.Select(p => p.Value.ID)) + { + var user = participants.users[participantId]; + if(!user.IsBot) + { + distinctUsers.Add(participantId); + } + Logger?.UserInChat(participantId); + } + } + else if(chat is Channel channel) + { + Logger?.LogInformation("Processing channel \"{Title}\" {ChatId}, access_hash: {AccessHash}", channel.Title, channel.id, channel.access_hash); + var participants = await (Bot as WT.Client).Channels_GetAllParticipants(channel); + Logger?.ChannelParticipants(channel.ID, channel.Title, participants.participants.Length); + foreach(var participant in participants.participants) + { + var user = participants.users[participant.UserId]; + if(!user.IsBot) + { + distinctUsers.Add(participant.UserId); + } + if (participant is ChannelParticipantCreator cpc) Logger?.CreatorInChat(participant.UserId, cpc.rank); + else if (participant is ChannelParticipantAdmin cpa) Logger?.AdminInChat(participant.UserId, cpa.rank); + else if(user.IsBot) Logger?.BotInChat(participant.UserId); + else Logger?.UserInChat(participant.UserId); + } + } + } + } + catch(Exception ex) + { + Logger?.LogError(ex, "There was an error processing chat {ChatId}", kvp.Value.ID); + } + }); + Console.WriteLine("Distinct Users:"); + distinctUsers.ForEach(id => Console.WriteLine(id)); + } +} diff --git a/src/UserBot/UserBot.Cli/UserBot.Cli.csproj b/src/UserBot/UserBot.Cli/UserBot.Cli.csproj index 55fe58c..6367c55 100644 --- a/src/UserBot/UserBot.Cli/UserBot.Cli.csproj +++ b/src/UserBot/UserBot.Cli/UserBot.Cli.csproj @@ -3,6 +3,7 @@ Exe net8.0 + net8.0 enable enable true @@ -11,10 +12,15 @@ + + + + + diff --git a/src/UserBot/UserBot/DI.cs b/src/UserBot/UserBot/DI.cs index 0ac23e8..af8b156 100644 --- a/src/UserBot/UserBot/DI.cs +++ b/src/UserBot/UserBot/DI.cs @@ -1,13 +1,23 @@ namespace Microsoft.Extensions.DependencyInjection; using Telegram.UserBot.Config; using Telegram.UserBot; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Configuration; +using Telegram.UserBot.Store.Abstractions; +using Telegram.UserBot.Store.FileSystem; public static class DI { - public static IServiceCollection AddUserBot(this IServiceCollection services, Action config) + public static IHostApplicationBuilder AddUserBot(this IHostApplicationBuilder builder, Action config) { - services.AddSingleton(); - services.ConfigureAll(config); - return services; + builder.Services.AddSingleton(); + Action config2 = cfg => + { + builder.Configuration.GetSection("UserBot").Bind(cfg); + config(cfg); + }; + builder.Services.AddSingleton(); + builder.Services.ConfigureAll(config2); + return builder; } } diff --git a/src/UserBot/UserBot/Extensions.cs b/src/UserBot/UserBot/Extensions.cs new file mode 100644 index 0000000..09a8e31 --- /dev/null +++ b/src/UserBot/UserBot/Extensions.cs @@ -0,0 +1,12 @@ +namespace Telegram.UserBot; + +public static class Extensions +{ + public static void CollectChats(this Messages_Chats chats, IDictionary chatsDict) + { + foreach(var chat in chats.chats.Select(chat => chat.Value)) + { + chatsDict[chat.ID] = chat; + } + } +} diff --git a/src/UserBot/UserBot/IUserBot.cs b/src/UserBot/UserBot/IUserBot.cs index 9a8c9bb..47a31c9 100644 --- a/src/UserBot/UserBot/IUserBot.cs +++ b/src/UserBot/UserBot/IUserBot.cs @@ -12,50 +12,52 @@ namespace Telegram.UserBot; -public partial interface IUserBot : ILog +public partial interface IUserBot : ILog, WT.IClient { -#if NET6_0_OR_GREATER - public static IDictionary Users { get; } - public static IDictionary Chats { get; } -#endif + IDictionary Users { get; } + IDictionary Chats { get; } - /// This event will be called when unsolicited updates/messages are sent by Telegram servers - /// Make your handler , or return or
See Examples/Program_ListenUpdate.cs for how to use this
- event Func OnUpdate; + // /// This event will be called when unsolicited updates/messages are sent by Telegram servers + // /// Make your handler , or return or
See Examples/Program_ListenUpdate.cs for how to use this
+ // event Func OnUpdate; - /// This event is called for other types of notifications (login states, reactor errors, ...) - event Func OnOther; + // /// This event is called for other types of notifications (login states, reactor errors, ...) + // event Func OnOther; - /// Url for using a MTProxy. https://t.me/proxy?server=... - string MTProxyUrl { get; set; } + // /// Url for using a MTProxy. https://t.me/proxy?server=... + // string MTProxyUrl { get; set; } - /// Telegram configuration, obtained at connection time - TL.Config TLConfig { get; } + // /// Telegram configuration, obtained at connection time + // TL.Config TLConfig { get; } - /// Number of automatic reconnections on connection/reactor failure - int MaxAutoReconnects { get; set; } + // /// Number of automatic reconnections on connection/reactor failure + // int MaxAutoReconnects { get; set; } - /// Number of attempts in case of wrong verification_code or password - int MaxCodePwdAttempts { get; set; } + // /// Number of attempts in case of wrong verification_code or password + // int MaxCodePwdAttempts { get; set; } - /// Number of seconds under which an error 420 FLOOD_WAIT_X will not be raised and your request will instead be auto-retried after the delay - int FloodRetryThreshold { get; set; } + // /// Number of seconds under which an error 420 FLOOD_WAIT_X will not be raised and your request will instead be auto-retried after the delay + // int FloodRetryThreshold { get; set; } - /// Number of seconds between each keep-alive ping. Increase this if you have a slow connection or you're debugging your code - int PingInterval { get; set; } + // /// Number of seconds between each keep-alive ping. Increase this if you have a slow connection or you're debugging your code + // int PingInterval { get; set; } - /// Size of chunks when uploading/downloading files. Reduce this if you don't have much memory - int FilePartSize { get; set; } + // /// Size of chunks when uploading/downloading files. Reduce this if you don't have much memory + // int FilePartSize { get; set; } - /// Is this Client instance the main or a secondary DC session - bool IsMainDC { get; } + // /// Is this Client instance the main or a secondary DC session + // bool IsMainDC { get; } - /// Has this Client established connection been disconnected? - bool Disconnected { get; } + // /// Has this Client established connection been disconnected? + // bool Disconnected { get; } - /// ID of the current logged-in user or 0 - long UserId { get; } + // /// ID of the current logged-in user or 0 + // long UserId { get; } - /// Info about the current logged-in user. This is filled after a successful (re)login - User User { get; } + // /// Info about the current logged-in user. This is filled after a successful (re)login + // User User { get; } } + + +// public delegate Task HandleUpdate(UpdatesBase update); +// public delegate Task HandleOther(IObject obj); diff --git a/src/UserBot/UserBot/IWTelegramClient.cs b/src/UserBot/UserBot/IWTelegramClient.cs new file mode 100644 index 0000000..f867d56 --- /dev/null +++ b/src/UserBot/UserBot/IWTelegramClient.cs @@ -0,0 +1,72 @@ +namespace WTelegram; +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Reflection; +using System.Security.Cryptography; +using System.Threading; +using System.Threading.Tasks; +using System.Web; +using TL; + +public partial interface IClient +{ + // TcpFactory TcpHandler { get; } + String MTProxyUrl { get; } + Config TLConfig { get; } + Int32 MaxAutoReconnects { get; } + Int32 MaxCodePwdAttempts { get; } + Int32 FloodRetryThreshold { get; } + Int32 PingInterval { get; } + Int32 FilePartSize { get; } + Boolean IsMainDC { get; } + Boolean Disconnected { get; } + Int64 UserId { get; } + User User { get; } + void Dispose(); + void DisableUpdates(Boolean disable = true); + void Reset(Boolean resetUser = true, Boolean resetSessions = true); + Task GetClientForDC(Int32 dcId, Boolean media_only = true, Boolean connect = true); + Task ConnectAsync(); + Task Login(String loginInfo); + Task LoginBotIfNeeded(String bot_token = null); + Task LoginUserIfNeeded(CodeSettings settings = null, Boolean reloginOnFailedResume = true); + User LoginAlreadyDone(Auth_AuthorizationBase authorization); + Task Invoke(IMethod query); + // Task UploadFileAsync(String pathname, ProgressCallback progress = null); + // Task UploadFileAsync(Stream stream, String filename, ProgressCallback progress = null); + // Task Messages_Search(InputPeer peer, String text = null, Int32 offset_id = 0, Int32 limit = 2147483647); + // Task Messages_SearchGlobal(String text = null, Int32 offset_id = 0, Int32 limit = 2147483647); + Task SendMediaAsync(InputPeer peer, String caption, InputFileBase mediaFile, String mimeType = null, Int32 reply_to_msg_id = 0, MessageEntity[] entities = null, DateTime schedule_date = default(DateTime)); + Task SendMessageAsync(InputPeer peer, String text, InputMedia media = null, Int32 reply_to_msg_id = 0, MessageEntity[] entities = null, DateTime schedule_date = default(DateTime), Boolean disable_preview = false); + Task SendAlbumAsync(InputPeer peer, ICollection medias, String caption = null, Int32 reply_to_msg_id = 0, MessageEntity[] entities = null, DateTime schedule_date = default(DateTime)); + // Task DownloadFileAsync(Photo photo, Stream outputStream, PhotoSizeBase photoSize = null, ProgressCallback progress = null); + // Task DownloadFileAsync(Document document, Stream outputStream, PhotoSizeBase thumbSize = null, ProgressCallback progress = null); + // Task DownloadFileAsync(InputFileLocationBase fileLocation, Stream outputStream, Int32 dc_id = 0, Int64 fileSize = 0, ProgressCallback progress = null); + Task DownloadProfilePhotoAsync(IPeerInfo peer, Stream outputStream, Boolean big = false, Boolean miniThumb = false); + Task Messages_GetAllChats(); + Task Messages_GetAllDialogs(Int32? folder_id = null); + Task Channels_GetAllParticipants(InputChannelBase channel, Boolean includeKickBan = false, String alphabet1 = "АБCДЕЄЖФГHИІJКЛМНОПQРСТУВWХЦЧШЩЫЮЯЗ", String alphabet2 = "АCЕHИJЛМНОРСТУВWЫ", CancellationToken cancellationToken = default(CancellationToken)); + // Task Channels_GetAdminLog(InputChannelBase channel, Flags events_filter = (Flags)0, String q = null, InputUserBase admin = null); + Task AddChatUser(InputPeer peer, InputUserBase user); + Task DeleteChatUser(InputPeer peer, InputUser user); + Task LeaveChat(InputPeer peer); + Task EditChatAdmin(InputPeer peer, InputUserBase user, Boolean is_admin); + Task EditChatPhoto(InputPeer peer, InputChatPhotoBase photo); + Task EditChatTitle(InputPeer peer, String title); + Task GetFullChat(InputPeer peer); + Task DeleteChat(InputPeer peer); + Task GetMessages(InputPeer peer); + Task GetMessages(InputPeer peer, params InputMessage[] id); + Task DeleteMessages(InputPeer peer, params Int32[] id); + Task ReadHistory(InputPeer peer, Int32 max_id = 0); + Task AnalyzeInviteLink(String url, Boolean join = false, IDictionary chats = null); + Task GetMessageByLink(String url, IDictionary chats = null); +} diff --git a/src/UserBot/UserBot/PeerConstructors.cs b/src/UserBot/UserBot/PeerConstructors.cs new file mode 100644 index 0000000..cd1724f --- /dev/null +++ b/src/UserBot/UserBot/PeerConstructors.cs @@ -0,0 +1,12 @@ +namespace Telegram.UserBot; + +public static class PeerConstructors +{ + public static User? GetUser(this IUserBot bot, long id) => bot.Users.TryGetValue(id, out var user) ? user : default; + public static ChatBase? GetChat(this IUserBot bot, long id) => bot.Chats.TryGetValue(id, out var chat) ? chat : default; + public static InputChannelBase? GetChannel(this IUserBot bot, long id) => bot.Chats.TryGetValue(id, out var chat) && chat is Channel channel ? channel : default; + public static string? GetUserName(this IUserBot bot, long id) => bot.Users.TryGetValue(id, out var user) ? user.ToString() : $"User {id}"; + public static string? GetChatName(this IUserBot bot, long id) => bot.Chats.TryGetValue(id, out var chat) ? chat.ToString() : $"Chat {id}"; + public static string? GetPeerName(this IUserBot bot, Peer peer) => peer is null ? null : peer is PeerUser user ? bot.GetUserName(user.user_id) + : peer is PeerChat or PeerChannel ? bot.GetChatName(peer.ID) : $"Peer {peer.ID}"; +} diff --git a/src/UserBot/UserBot/Telegram.UserBot.csproj b/src/UserBot/UserBot/Telegram.UserBot.csproj index 73459a3..be97c95 100644 --- a/src/UserBot/UserBot/Telegram.UserBot.csproj +++ b/src/UserBot/UserBot/Telegram.UserBot.csproj @@ -23,7 +23,7 @@ - +
diff --git a/src/UserBot/UserBot/Telegram.UserBot.props b/src/UserBot/UserBot/Telegram.UserBot.props index c33f0ac..d6b9dd7 100644 --- a/src/UserBot/UserBot/Telegram.UserBot.props +++ b/src/UserBot/UserBot/Telegram.UserBot.props @@ -15,7 +15,8 @@ - + + diff --git a/src/UserBot/UserBot/UserBot.cs b/src/UserBot/UserBot/UserBot.cs index 51bf4f7..0d2d422 100644 --- a/src/UserBot/UserBot/UserBot.cs +++ b/src/UserBot/UserBot/UserBot.cs @@ -17,29 +17,60 @@ namespace Telegram.UserBot; using Dgmjr.Abstractions; +using Microsoft.AspNetCore.Session; + using Telegram.UserBot.Config; using Telegram.UserBot.Store.Abstractions; using WTelegram; +using TL; -public class UserBot : WTelegram.Client, IUserBot +public class UserBot : Client, IUserBot { public virtual ILogger? Logger { get; } public virtual User Me { get; protected set; } - public static IDictionary Users { get; } = new Dictionary(); - public static IDictionary Chats { get; } = new Dictionary(); + public virtual IDictionary Users { get; } = new Dictionary(); + public virtual IDictionary Chats { get; } = new Dictionary(); + public event MessageHandler OnMessageReceived; - public UserBot(IOptions cfg, ILogger logger) - : this(cfg.Value, logger) { } + public UserBot(IOptions cfg, ILogger logger, IUserBotStore sessionStore) + : this(cfg.Value, logger, sessionStore) { } - public UserBot(IUserBotConfig cfg, ILogger logger) - : this(cfg.GetConfigVariable, cfg.GetSessionStore().GetStream(), logger) { } + public UserBot(IUserBotConfig cfg, ILogger logger, IUserBotStore sessionStore) + : this(cfg.GetConfigVariable, sessionStore.GetStream(), logger) { } - public UserBot(Func config, Stream? store = null, ILogger? logger = null) + public UserBot( + Func config, + Stream? store = null, + ILogger? logger = null + ) : base(config, store) { Logger = logger; OnUpdate += Client_OnUpdate; OnOther += Client_OnOther; + OnMessageReceived += Client_OnMessageReceived; + OnUpdateDeleteChannelMessages += async udcm => + Logger?.ChatMessagesDeleted(Chats[udcm.channel_id], udcm.messages.Length); + OnUpdateDeleteMessages += async udm => + Logger?.MessagesDeleted(udm.messages.Length); + OnUpdateUserTyping += async uut => + Logger?.UserAction(Users[uut.user_id], uut.action.GetType().Name); + OnUpdateChatUserTyping += async ucut => + Logger?.ChatUserTyping(Users[ucut.from_id.ID], Chats[ucut.chat_id]); + OnUpdateChannelUserTyping += async ucut2 => + Logger?.ChatUserTyping(Users[ucut2.from_id.ID], Chats[ucut2.channel_id]); + OnUpdateChatParticipants += async ucp => + Logger?.ChatParticipants(ucp.participants.ChatId, ucp.participants.Participants.Length); + OnUpdateUserStatus += async uus => + Logger?.UserStatus(Users[uus.user_id], uus.status); + OnUpdateUserName += async uun => + Logger?.UserChangedProfileName(Users[uun.user_id], uun.first_name, uun.last_name); + OnUpdateUser += async uu => + Logger?.UserChangedInfos(Users[uu.user_id]); + OnOtherUpdate += async u => + Logger?.UnhandledUpdate(u); + OnUpdateGroupCallParticipants += async ugcp => + Logger?.GroupCallParticipants(ugcp.call.id, ugcp.participants.Length); } protected virtual Task Client_OnOther(IObject obj) => Task.CompletedTask; @@ -47,22 +78,67 @@ public UserBot(Func config, Stream? store = null, ILogger OnUpdateNewMessage; + public event UpdateHandler OnUpdateEditMessage; + public event UpdateHandler OnUpdateDeleteChannelMessages; + public event UpdateHandler OnUpdateDeleteMessages; + public event UpdateHandler OnUpdateUserTyping; + public event UpdateHandler OnUpdateChatUserTyping; + public event UpdateHandler OnUpdateChannelUserTyping; + public event UpdateHandler OnUpdateChatParticipants; + public event UpdateHandler OnUpdateUserStatus; + public event UpdateHandler OnUpdateUserName; + public event UpdateHandler OnUpdateUser; + public event UpdateHandler OnUpdateGroupCallParticipants; + public event UpdateHandler OnOtherUpdate; } + +public delegate Task MessageHandler(MessageBase messageBase, bool edit = false); +public delegate Task UpdateHandler(T update) + where T : Update; diff --git a/src/UserBot/UserBot/UserBotLogger.cs b/src/UserBot/UserBot/UserBotLogger.cs index dfbb65e..dd0403e 100644 --- a/src/UserBot/UserBot/UserBotLogger.cs +++ b/src/UserBot/UserBot/UserBotLogger.cs @@ -1,25 +1,118 @@ -using Microsoft.Extensions.Logging; -using TL; - -namespace Telegram.UserBot; - -public static partial class UserBotLoggerExtensions -{ - [LoggerMessage( - EventId = 0, - Level = LogLevel.Information, - Message = "New message from {User}: {Message}" - )] - public static partial void NewMessageFromUser( - this ILogger logger, - User user, - TL.Message message - ); - - [LoggerMessage( - EventId = 1, - Level = LogLevel.Information, - Message = "{Me} logged in with phone number {PhoneNumber}" - )] - public static partial void LoggedIn(this ILogger logger, string phoneNumber, User me); -} +using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.Extensions.Logging; +using TL; + +namespace Telegram.UserBot; + +public static partial class UserBotLoggerExtensions +{ + [LoggerMessage( + EventId = 0, + Level = LogLevel.Information, + Message = "New message from {User}: {Message}" + )] + public static partial void NewMessageFromUser( + this ILogger logger, + User user, + TL.Message message + ); + + [LoggerMessage( + EventId = 1, + Level = LogLevel.Information, + Message = "{Me} logged in with phone number {PhoneNumber}" + )] + public static partial void LoggedIn(this ILogger logger, string phoneNumber, User me); + + [LoggerMessage( + EventId = 2, + Level = LogLevel.Information, + Message = "Chat {ChatId} has {Count} participants" + )] + public static partial void ChatParticipants(this ILogger logger, long chatId, int count); + + [LoggerMessage( + EventId = 3, + Level = LogLevel.Information, + Message = "Chat {Chat} has {Count} messages deleted" + )] + public static partial void ChatMessagesDeleted(this ILogger logger, ChatBase chat, int count); + + [LoggerMessage( + EventId = 4, + Level = LogLevel.Information, + Message = "{User} is {Action}" + )] + public static partial void UserAction(this ILogger logger, User user, string action); + + [LoggerMessage( + EventId = 5, + Level = LogLevel.Information, + Message = "{User} is typing in {Chat}" + )] + public static partial void ChatUserTyping(this ILogger logger, User user, ChatBase chat); + + [LoggerMessage( + EventId = 6, + Level = LogLevel.Information, + Message = "{Number} message(s) deleted" + )] + public static partial void MessagesDeleted(this ILogger logger, int number); + + [LoggerMessage( + EventId = 7, + Level = LogLevel.Information, + Message = "{User} has changed profile name: {FirstName} {LastName}" + )] + public static partial void UserChangedProfileName( + this ILogger logger, + User user, + string firstName, + string lastName + ); + + [LoggerMessage( + EventId = 8, + Level = LogLevel.Information, + Message = "{User} is now {Status}" + )] + public static partial void UserStatus(this ILogger logger, User user, UserStatus status); + + [LoggerMessage( + EventId = 9, + Level = LogLevel.Information, + Message = "{User} has changed infos/photo" + )] + public static partial void UserChangedInfos(this ILogger logger, User user); + + [LoggerMessage( + EventId = 10, + Level = LogLevel.Information, + Message = "Unhandled update: {Update}" + )] + public static partial void UnhandledUpdate(this ILogger logger, IObject update); + + [LoggerMessage(EventId = 11, Level = LogLevel.Information, Message = "{Length} participants in {Chat}")] + public static partial void ChatParticipants(this ILogger logger, int length, Chat chat); + + [LoggerMessage( + EventId = 12, + Level = LogLevel.Information, + Message = "{From} in {Where}> {Message}" + )] + public static partial void MessageReceived(this ILogger logger, Message message, string from, string where); + + [LoggerMessage( + EventId = 13, + Level = LogLevel.Information, + Message = "{From} in {Where} [{Action}]" + )] + public static partial void UserAction(this ILogger logger, MessageBase message, string from, string where, string action); + + [LoggerMessage( + EventId = 14, + Level = LogLevel.Information, + Message = "Group call {CallId} has {ParticipantsCount} participants" + )] + public static partial void GroupCallParticipants(this ILogger logger, long callId, int participantsCount); +}