several enhancements:

- fixed UpdateAffectedMessages on wrong mbox
- MarkdownToEntities allow reserved characters in code block
- collector system more flexible & open
- improved UpdateManager resilience
- some MTPG improvements
This commit is contained in:
Wizou 2024-04-03 21:05:07 +02:00
parent 210a3365e5
commit abeed476e7
11 changed files with 290 additions and 244 deletions

8
FAQ.md
View file

@ -349,13 +349,11 @@ var manager = client.WithUpdateManager(OnUpdate);
var manager = client.WithUpdateManager(OnUpdate, "Updates.state");
// to save the state later, preferably after disposing the client:
manager.SaveState("Updates.state")
// (WithUpdateManager has other parameters for advanced use)
```
Your `OnUpdate` method will directly take a single `Update` as parameter, instead of a container of updates.
The `manager.Users` and `manager.Chats` dictionaries will collect the users/chats data from updates.
You can also feed them manually from result of your API calls by calling `result.CollectUsersChats(manager.Users, manager.Chats);` and resolve Peer fields via `manager.UserOrChat(peer)`
Your `OnUpdate` method will directly take a single `Update` as parameter, instead of a container of updates.
The `manager.Users` and `manager.Chats` dictionaries will collect the users/chats data from updates.
You can also feed them manually from result of your API calls by calling `result.CollectUsersChats(manager.Users, manager.Chats);` and resolve Peer fields via `manager.UserOrChat(peer)`
See [Examples/Program_ListenUpdates.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ListenUpdates.cs?ts=4#L21) for an example of implementation.
Notes:

View file

@ -164,14 +164,14 @@ See [FAQ #4](https://wiz0u.github.io/WTelegramClient/FAQ#access-hash) to learn m
# Other things to know
The Client class offers `OnUpdates` and `OnOther` events that are triggered when Telegram servers sends Updates (like new messages or status) or other notifications, independently of your API requests.
You can also use the [UpdateManager class](https://wiz0u.github.io/WTelegramClient/FAQ#manager) to simplify the handling of such updates.
The Client class offers `OnUpdates` and `OnOther` events that are triggered when Telegram servers sends Updates (like new messages or status) or other notifications, independently of your API requests.
You can also use the [UpdateManager class](https://wiz0u.github.io/WTelegramClient/FAQ#manager) to simplify the handling of such updates.
See [Examples/Program_ListenUpdates.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ListenUpdates.cs?ts=4#L21) and [Examples/Program_ReactorError.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ReactorError.cs?ts=4#L30)
An invalid API request can result in a `RpcException` being raised, reflecting the [error code and status text](https://revgram.github.io/errors.html) of the problem.
To [prevent getting banned](https://wiz0u.github.io/WTelegramClient/FAQ#prevent-ban) during dev, you can connect to [test servers](https://docs.pyrogram.org/topics/test-servers), by adding this line in your Config callback:
`case "server_address": return "149.154.167.40:443"; // test DC`
`case "server_address": return "2>149.154.167.40:443"; // test DC`
The other configuration items that you can provide include: **session_pathname, email, email_verification_code, session_key, device_model, system_version, app_version, system_lang_code, lang_pack, lang_code, firebase, user_id, bot_token**

View file

@ -14,34 +14,44 @@ namespace TL.Generator;
[Generator]
public class MTProtoGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var classDeclarations = context.SyntaxProvider.ForAttributeWithMetadataName("TL.TLDefAttribute",
(_, _) => true, (context, _) => (ClassDeclarationSyntax)context.TargetNode);
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var classDeclarations = context.SyntaxProvider.ForAttributeWithMetadataName("TL.TLDefAttribute",
(_, _) => true, (context, _) => (ClassDeclarationSyntax)context.TargetNode);
var source = context.CompilationProvider.Combine(classDeclarations.Collect());
context.RegisterSourceOutput(source, Execute);
}
static void Execute(SourceProductionContext context, (Compilation compilation, ImmutableArray<ClassDeclarationSyntax> classes) unit)
{
var object_ = unit.compilation.GetSpecialType(SpecialType.System_Object);
if (unit.compilation.GetTypeByMetadataName("TL.TLDefAttribute") is not { } tlDefAttribute) return;
if (unit.compilation.GetTypeByMetadataName("TL.IfFlagAttribute") is not { } ifFlagAttribute) return;
{
var object_ = unit.compilation.GetSpecialType(SpecialType.System_Object);
if (unit.compilation.GetTypeByMetadataName("TL.TLDefAttribute") is not { } tlDefAttribute) return;
if (unit.compilation.GetTypeByMetadataName("TL.IfFlagAttribute") is not { } ifFlagAttribute) return;
if (unit.compilation.GetTypeByMetadataName("TL.Layer") is not { } layer) return;
if (unit.compilation.GetTypeByMetadataName("TL.IObject") is not { } iobject) return;
var nullables = LoadNullables(layer);
var namespaces = new Dictionary<string, Dictionary<string, string>>(); // namespace,class,methods
var readTL = new StringBuilder();
readTL
.AppendLine("\t\tpublic static IObject ReadTL(this BinaryReader reader, uint ctorId = 0) => (ctorId != 0 ? ctorId : reader.ReadUInt32()) switch")
.AppendLine("\t\t{");
var nullables = LoadNullables(layer);
var namespaces = new Dictionary<string, Dictionary<string, string>>(); // namespace,class,methods
var makeTL = new StringBuilder();
var source = new StringBuilder();
source
.AppendLine("using System;")
.AppendLine("using System.ComponentModel;")
.AppendLine("using System.IO;")
.AppendLine("using System.Linq;")
.AppendLine("using TL;")
.AppendLine()
.AppendLine("#pragma warning disable CS0109")
.AppendLine();
makeTL
.AppendLine("\t\tpublic static IObject ReadTL(this BinaryReader reader, uint ctorId = 0) => (ctorId != 0 ? ctorId : reader.ReadUInt32()) switch")
.AppendLine("\t\t{");
foreach (var classDecl in unit.classes)
{
var semanticModel = unit.compilation.GetSemanticModel(classDecl.SyntaxTree);
if (semanticModel.GetDeclaredSymbol(classDecl) is not { } symbol) continue;
var tldef = symbol.GetAttributes().FirstOrDefault(a => a.AttributeClass == tlDefAttribute);
if (tldef == null) continue;
{
var semanticModel = unit.compilation.GetSemanticModel(classDecl.SyntaxTree);
if (semanticModel.GetDeclaredSymbol(classDecl) is not { } symbol) continue;
var tldef = symbol.GetAttributes().FirstOrDefault(a => a.AttributeClass == tlDefAttribute);
if (tldef == null) continue;
var id = (uint)tldef.ConstructorArguments[0].Value;
var inheritBefore = (bool?)tldef.NamedArguments.FirstOrDefault(k => k.Key == "inheritBefore").Value.Value ?? false;
StringBuilder writeTl = new(), ctorTL = new();
@ -55,7 +65,10 @@ public class MTProtoGenerator : IIncrementalGenerator
{
if (parentMethods == null)
{
writeTl.AppendLine("\t\tpublic abstract void WriteTL(BinaryWriter writer);");
if (name is "Peer")
writeTl.AppendLine("\t\tpublic virtual void WriteTL(BinaryWriter writer) => throw new NotSupportedException();");
else
writeTl.AppendLine("\t\tpublic abstract void WriteTL(BinaryWriter writer);");
parentClasses[name] = writeTl.ToString();
writeTl.Clear();
}
@ -72,40 +85,43 @@ public class MTProtoGenerator : IIncrementalGenerator
continue;
}
if (id == 0x3072CFA1) // GzipPacked
readTL.AppendLine($"\t\t\t0x{id:X8} => reader.ReadTLGzipped(),");
makeTL.AppendLine($"\t\t\t0x{id:X8} => reader.ReadTLGzipped(),");
else if (name != "Null" && (ns != "TL.Methods" || name == "Ping"))
readTL.AppendLine($"\t\t\t0x{id:X8} => new {(ns == "TL" ? "" : ns + '.')}{name}(reader),");
var override_ = symbol.BaseType == object_ ? "" : "override ";
makeTL.AppendLine($"\t\t\t0x{id:X8} => new {(ns == "TL" ? "" : ns + '.')}{name}().ReadTL(reader),");
var override_ = symbol.BaseType == object_ ? "" : "override ";
if (name == "Messages_AffectedMessages") override_ = "virtual ";
if (symbol.Constructors[0].IsImplicitlyDeclared)
ctorTL.AppendLine($"\t\tpublic {name}() {{ }}");
//if (symbol.Constructors[0].IsImplicitlyDeclared)
// ctorTL.AppendLine($"\t\tpublic {name}() {{ }}");
if (symbol.IsGenericType) name += "<X>";
ctorTL
.AppendLine($"\t\tpublic {name}(BinaryReader reader)")
.AppendLine("\t\t{");
.AppendLine("\t\t[EditorBrowsable(EditorBrowsableState.Never)]")
.AppendLine($"\t\tpublic new {name} ReadTL(BinaryReader reader)")
.AppendLine("\t\t{");
writeTl
.AppendLine($"\t\tpublic {override_}void WriteTL(BinaryWriter writer)")
.AppendLine("\t\t{")
.AppendLine($"\t\t\twriter.Write(0x{id:X8});");
var members = symbol.GetMembers().ToList();
for (var parent = symbol.BaseType; parent != object_; parent = parent.BaseType)
if (inheritBefore) members.InsertRange(0, parent.GetMembers());
else members.AddRange(parent.GetMembers());
.AppendLine("\t\t[EditorBrowsable(EditorBrowsableState.Never)]")
.AppendLine($"\t\tpublic {override_}void WriteTL(BinaryWriter writer)")
.AppendLine("\t\t{")
.AppendLine($"\t\t\twriter.Write(0x{id:X8});");
var members = symbol.GetMembers().ToList();
for (var parent = symbol.BaseType; parent != object_; parent = parent.BaseType)
if (inheritBefore) members.InsertRange(0, parent.GetMembers());
else members.AddRange(parent.GetMembers());
foreach (var member in members.OfType<IFieldSymbol>())
{
if (member.DeclaredAccessibility != Accessibility.Public || member.IsStatic) continue;
ctorTL.Append("\t\t\t");
{
if (member.DeclaredAccessibility != Accessibility.Public || member.IsStatic) continue;
ctorTL.Append("\t\t\t");
writeTl.Append("\t\t\t");
var ifFlag = (int?)member.GetAttributes().FirstOrDefault(a => a.AttributeClass == ifFlagAttribute)?.ConstructorArguments[0].Value;
if (ifFlag != null)
if (ifFlag != null)
{
var condition = ifFlag < 32 ? $"if (((uint)flags & 0x{1 << ifFlag:X}) != 0) "
: $"if (((uint)flags2 & 0x{1 << (ifFlag - 32):X}) != 0) ";
ctorTL.Append(condition);
writeTl.Append(condition);
var condition = ifFlag < 32 ? $"if (((uint)flags & 0x{1 << ifFlag:X}) != 0) "
: $"if (((uint)flags2 & 0x{1 << (ifFlag - 32):X}) != 0) ";
ctorTL.Append(condition);
writeTl.Append(condition);
}
string memberType = member.Type.ToString();
switch (memberType)
{
{
case "int":
ctorTL.AppendLine($"{member.Name} = reader.ReadInt32();");
writeTl.AppendLine($"writer.Write({member.Name});");
@ -127,34 +143,34 @@ public class MTProtoGenerator : IIncrementalGenerator
writeTl.AppendLine($"writer.WriteTLStamp({member.Name});");
break;
case "string":
ctorTL.AppendLine($"{member.Name} = reader.ReadTLString();");
writeTl.AppendLine($"writer.WriteTLString({member.Name});");
ctorTL.AppendLine($"{member.Name} = reader.ReadTLString();");
writeTl.AppendLine($"writer.WriteTLString({member.Name});");
break;
case "byte[]":
ctorTL.AppendLine($"{member.Name} = reader.ReadTLBytes();");
writeTl.AppendLine($"writer.WriteTLBytes({member.Name});");
break;
case "byte[]":
ctorTL.AppendLine($"{member.Name} = reader.ReadTLBytes();");
writeTl.AppendLine($"writer.WriteTLBytes({member.Name});");
break;
case "TL.Int128":
ctorTL.AppendLine($"{member.Name} = new Int128(reader);");
writeTl.AppendLine($"writer.Write({member.Name});");
break;
case "TL.Int256":
ctorTL.AppendLine($"{member.Name} = new Int256(reader);");
writeTl.AppendLine($"writer.Write({member.Name});");
writeTl.AppendLine($"writer.Write({member.Name});");
break;
case "TL._Message[]":
ctorTL.AppendLine($"throw new NotSupportedException();");
writeTl.AppendLine($"writer.WriteTLMessages({member.Name});");
break;
case "TL.IObject": case "TL.IMethod<X>":
case "TL._Message[]":
ctorTL.AppendLine($"{member.Name} = reader.ReadTLRawVector<_Message>(0x5BB8E511);");
writeTl.AppendLine($"writer.WriteTLMessages({member.Name});");
break;
case "TL.IObject": case "TL.IMethod<X>":
ctorTL.AppendLine($"{member.Name} = {(memberType == "TL.IObject" ? "" : $"({memberType})")}reader.ReadTL();");
writeTl.AppendLine($"{member.Name}.WriteTL(writer);");
break;
case "System.Collections.Generic.Dictionary<long, TL.User>":
break;
case "System.Collections.Generic.Dictionary<long, TL.User>":
ctorTL.AppendLine($"{member.Name} = reader.ReadTLDictionary<User>();");
writeTl.AppendLine($"writer.WriteTLVector({member.Name}.Values.ToArray());");
break;
case "System.Collections.Generic.Dictionary<long, TL.ChatBase>":
case "System.Collections.Generic.Dictionary<long, TL.ChatBase>":
ctorTL.AppendLine($"{member.Name} = reader.ReadTLDictionary<ChatBase>();");
writeTl.AppendLine($"writer.WriteTLVector({member.Name}.Values.ToArray());");
break;
@ -180,56 +196,49 @@ public class MTProtoGenerator : IIncrementalGenerator
}
else
writeTl.AppendLine($"Cannot serialize {memberType}");
break;
break;
}
}
ctorTL.AppendLine("\t\t\treturn this;");
ctorTL.AppendLine("\t\t}");
writeTl.AppendLine("\t\t}");
ctorTL.Append(writeTl.ToString());
if (symbol.IsGenericType) name += "<X>";
classes[name] = ctorTL.ToString();
}
var source = new StringBuilder();
source
.AppendLine("using System;")
.AppendLine("using System.IO;")
.AppendLine("using System.Linq;")
.AppendLine("using TL;")
.AppendLine();
foreach (var nullable in nullables)
readTL.AppendLine($"\t\t\t0x{nullable.Value:X8} => null,");
readTL.AppendLine("\t\t\tvar ctorNb => throw new Exception($\"Cannot find type for ctor #{ctorNb:x}\")");
readTL.AppendLine("\t\t};");
namespaces["TL"]["Layer"] = readTL.ToString();
foreach (var namesp in namespaces)
{
source.Append("namespace ").AppendLine(namesp.Key).Append('{');
foreach (var method in namesp.Value)
source.AppendLine().Append("\tpartial class ").AppendLine(method.Key).AppendLine("\t{").Append(method.Value).AppendLine("\t}");
makeTL.AppendLine($"\t\t\t0x{nullable.Value:X8} => null,");
makeTL.AppendLine("\t\t\tvar ctorNb => throw new Exception($\"Cannot find type for ctor #{ctorNb:x}\")");
makeTL.AppendLine("\t\t};");
namespaces["TL"]["Layer"] = makeTL.ToString();
foreach (var namesp in namespaces)
{
source.Append("namespace ").AppendLine(namesp.Key).Append('{');
foreach (var method in namesp.Value)
source.AppendLine().Append("\tpartial class ").AppendLine(method.Key).AppendLine("\t{").Append(method.Value).AppendLine("\t}");
source.AppendLine("}").AppendLine();
}
string text = source.ToString();
Debug.Write(text);
Debug.Write(text);
context.AddSource("TL.Generated.cs", text);
}
private static Dictionary<string, uint> LoadNullables(INamedTypeSymbol layer)
{
var nullables = layer.GetMembers("Nullables").Single() as IFieldSymbol;
var initializer = nullables.DeclaringSyntaxReferences[0].GetSyntax().ToString();
var nullables = layer.GetMembers("Nullables").Single() as IFieldSymbol;
var initializer = nullables.DeclaringSyntaxReferences[0].GetSyntax().ToString();
var table = new Dictionary<string, uint>();
foreach (var line in initializer.Split('\n'))
{
int index = line.IndexOf("[typeof(");
if (index == -1) continue;
int index2 = line.IndexOf(')', index += 8);
string className = "TL." + line.Substring(index, index2 - index);
index = line.IndexOf("= 0x", index2);
if (index == -1) continue;
index2 = line.IndexOf(',', index += 4);
table[className] = uint.Parse(line.Substring(index, index2 - index), System.Globalization.NumberStyles.HexNumber);
}
return table;
{
int index = line.IndexOf("[typeof(");
if (index == -1) continue;
int index2 = line.IndexOf(')', index += 8);
string className = "TL." + line.Substring(index, index2 - index);
index = line.IndexOf("= 0x", index2);
if (index == -1) continue;
index2 = line.IndexOf(',', index += 4);
table[className] = uint.Parse(line.Substring(index, index2 - index), System.Globalization.NumberStyles.HexNumber);
}
return table;
}
}

View file

@ -586,8 +586,6 @@ namespace WTelegram
if (OnOwnUpdates != null)
if (result is UpdatesBase updates)
RaiseOwnUpdates(updates);
else if (result is Messages_AffectedMessages affected)
RaiseOwnUpdates(new UpdateShort { update = new UpdateAffectedMessages { affected = affected }, date = MsgIdToStamp(_lastRecvMsgId) });
}
rpc.tcs.SetResult(result);
@ -1454,5 +1452,16 @@ namespace WTelegram
throw new WTException($"{query.GetType().Name} call got a result of type {result.GetType().Name} instead of {typeof(T).Name}");
}
}
public async Task<T> InvokeAffected<T>(IMethod<T> query, long peerId) where T : Messages_AffectedMessages
{
var result = await Invoke(query);
RaiseOwnUpdates(new UpdateShort
{
update = new UpdateAffectedMessages { mbox_id = peerId, pts = result.pts, pts_count = result.pts_count},
date = MsgIdToStamp(_lastRecvMsgId)
});
return result;
}
}
}

View file

@ -267,7 +267,9 @@ namespace WTelegram
public class WTException : ApplicationException
{
public readonly int ErrorCode;
public WTException(string message) : base(message) { }
public WTException(string message, int code) : base(message) => ErrorCode = code;
public WTException(string message, Exception innerException) : base(message, innerException) { }
}
}

View file

@ -5,98 +5,104 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using WTelegram; // for GetValueOrDefault
using WTelegram;
namespace TL
{
public static class Extensions
{
internal sealed partial class CollectorPeer : Peer
public sealed partial class CollectorPeer(IDictionary<long, User> _users, IDictionary<long, ChatBase> _chats) : Peer, IPeerCollector
{
public override long ID => 0;
internal IDictionary<long, User> _users;
internal IDictionary<long, ChatBase> _chats;
protected internal override IPeerInfo UserOrChat(Dictionary<long, User> users, Dictionary<long, ChatBase> chats)
{
if (_users != null)
lock (_users)
foreach (var user in users.Values)
if (user != null)
if (!user.flags.HasFlag(User.Flags.min) || !_users.TryGetValue(user.id, out var prevUser) || prevUser.flags.HasFlag(User.Flags.min))
_users[user.id] = user;
else
{ // update previously full user from min user:
const User.Flags updated_flags = (User.Flags)0x5DAFE000;
const User.Flags2 updated_flags2 = (User.Flags2)0x711;
// tdlib updated flags: deleted | bot | bot_chat_history | bot_nochats | verified | bot_inline_geo
// | support | scam | fake | bot_attach_menu | premium
// tdesktop non-updated flags: bot | bot_chat_history | bot_nochats | bot_attach_menu
// updated flags2: stories_unavailable (tdlib) | contact_require_premium (tdesktop)
prevUser.flags = (prevUser.flags & ~updated_flags) | (user.flags & updated_flags);
prevUser.flags2 = (prevUser.flags2 & ~updated_flags2) | (user.flags2 & updated_flags2);
prevUser.first_name ??= user.first_name; // tdlib: not updated ; tdesktop: updated only if unknown
prevUser.last_name ??= user.last_name; // tdlib: not updated ; tdesktop: updated only if unknown
//prevUser.username ??= user.username; // tdlib/tdesktop: not updated
prevUser.phone ??= user.phone; // tdlib: updated only if unknown ; tdesktop: not updated
if (prevUser.flags.HasFlag(User.Flags.apply_min_photo) && user.photo != null)
{
prevUser.photo = user.photo; // tdlib/tdesktop: updated on apply_min_photo
prevUser.flags |= User.Flags.has_photo;
}
prevUser.bot_info_version = user.bot_info_version; // tdlib: updated ; tdesktop: not updated
prevUser.restriction_reason = user.restriction_reason; // tdlib: updated ; tdesktop: not updated
prevUser.bot_inline_placeholder = user.bot_inline_placeholder;// tdlib: updated ; tdesktop: ignored
if (user.lang_code != null)
prevUser.lang_code = user.lang_code; // tdlib: updated if present ; tdesktop: ignored
prevUser.emoji_status = user.emoji_status; // tdlib/tdesktop: updated
prevUser.usernames = user.usernames; // tdlib: not updated ; tdesktop: updated
if (user.stories_max_id > 0)
prevUser.stories_max_id = user.stories_max_id; // tdlib: updated if > 0 ; tdesktop: not updated
prevUser.color = user.color; // tdlib/tdesktop: updated
prevUser.profile_color = user.profile_color; // tdlib/tdesktop: unimplemented yet
_users[user.id] = prevUser;
}
if (_chats != null)
lock (_chats)
foreach (var kvp in chats)
if (kvp.Value is not Channel channel)
_chats[kvp.Key] = kvp.Value;
else if (!channel.flags.HasFlag(Channel.Flags.min) || !_chats.TryGetValue(kvp.Key, out var prevChat) || prevChat is not Channel prevChannel || prevChannel.flags.HasFlag(Channel.Flags.min))
_chats[kvp.Key] = channel;
else
{ // update previously full channel from min channel:
const Channel.Flags updated_flags = (Channel.Flags)0x7FDC0BE0;
const Channel.Flags2 updated_flags2 = (Channel.Flags2)0x781;
// tdesktop updated flags: broadcast | verified | megagroup | signatures | scam | has_link | slowmode_enabled
// | call_active | call_not_empty | fake | gigagroup | noforwards | join_to_send | join_request | forum
// tdlib nonupdated flags: broadcast | signatures | call_active | call_not_empty | noforwards
prevChannel.flags = (prevChannel.flags & ~updated_flags) | (channel.flags & updated_flags);
prevChannel.flags2 = (prevChannel.flags2 & ~updated_flags2) | (channel.flags2 & updated_flags2);
prevChannel.title = channel.title; // tdlib/tdesktop: updated
prevChannel.username = channel.username; // tdlib/tdesktop: updated
prevChannel.photo = channel.photo; // tdlib: updated if not banned ; tdesktop: updated
prevChannel.restriction_reason = channel.restriction_reason; // tdlib: updated ; tdesktop: not updated
prevChannel.default_banned_rights = channel.default_banned_rights; // tdlib/tdesktop: updated
if (channel.participants_count > 0)
prevChannel.participants_count = channel.participants_count; // tdlib/tdesktop: updated if present
prevChannel.usernames = channel.usernames; // tdlib/tdesktop: updated
prevChannel.color = channel.color; // tdlib: not updated ; tdesktop: updated
prevChannel.profile_color = channel.profile_color; // tdlib/tdesktop: ignored
prevChannel.emoji_status = channel.emoji_status; // tdlib: not updated ; tdesktop: updated
prevChannel.level = channel.level; // tdlib: not updated ; tdesktop: updated
_chats[kvp.Key] = prevChannel;
}
if (users != null) Collect(users.Values);
if (chats != null) Collect(chats.Values);
return null;
}
#if MTPG
public override void WriteTL(System.IO.BinaryWriter writer) => throw new NotImplementedException();
#endif
public void Collect(IEnumerable<TL.User> users)
{
lock (_users)
foreach (var user in users)
if (user != null)
if (!user.flags.HasFlag(User.Flags.min) || !_users.TryGetValue(user.id, out var prevUser) || prevUser.flags.HasFlag(User.Flags.min))
_users[user.id] = user;
else
{ // update previously full user from min user:
const User.Flags updated_flags = (User.Flags)0x5DAFE000;
const User.Flags2 updated_flags2 = (User.Flags2)0x711;
// tdlib updated flags: deleted | bot | bot_chat_history | bot_nochats | verified | bot_inline_geo
// | support | scam | fake | bot_attach_menu | premium
// tdesktop non-updated flags: bot | bot_chat_history | bot_nochats | bot_attach_menu
// updated flags2: stories_unavailable (tdlib) | contact_require_premium (tdesktop)
prevUser.flags = (prevUser.flags & ~updated_flags) | (user.flags & updated_flags);
prevUser.flags2 = (prevUser.flags2 & ~updated_flags2) | (user.flags2 & updated_flags2);
prevUser.first_name ??= user.first_name; // tdlib: not updated ; tdesktop: updated only if unknown
prevUser.last_name ??= user.last_name; // tdlib: not updated ; tdesktop: updated only if unknown
//prevUser.username ??= user.username; // tdlib/tdesktop: not updated
prevUser.phone ??= user.phone; // tdlib: updated only if unknown ; tdesktop: not updated
if (prevUser.flags.HasFlag(User.Flags.apply_min_photo) && user.photo != null)
{
prevUser.photo = user.photo; // tdlib/tdesktop: updated on apply_min_photo
prevUser.flags |= User.Flags.has_photo;
}
prevUser.bot_info_version = user.bot_info_version; // tdlib: updated ; tdesktop: not updated
prevUser.restriction_reason = user.restriction_reason; // tdlib: updated ; tdesktop: not updated
prevUser.bot_inline_placeholder = user.bot_inline_placeholder;// tdlib: updated ; tdesktop: ignored
if (user.lang_code != null)
prevUser.lang_code = user.lang_code; // tdlib: updated if present ; tdesktop: ignored
prevUser.emoji_status = user.emoji_status; // tdlib/tdesktop: updated
prevUser.usernames = user.usernames; // tdlib: not updated ; tdesktop: updated
if (user.stories_max_id > 0)
prevUser.stories_max_id = user.stories_max_id; // tdlib: updated if > 0 ; tdesktop: not updated
prevUser.color = user.color; // tdlib/tdesktop: updated
prevUser.profile_color = user.profile_color; // tdlib/tdesktop: unimplemented yet
_users[user.id] = prevUser;
}
}
public void Collect(IEnumerable<ChatBase> chats)
{
lock (_chats)
foreach (var chat in chats)
if (chat is not Channel channel)
_chats[chat.ID] = chat;
else if (!channel.flags.HasFlag(Channel.Flags.min) || !_chats.TryGetValue(channel.id, out var prevChat) || prevChat is not Channel prevChannel || prevChannel.flags.HasFlag(Channel.Flags.min))
_chats[channel.id] = channel;
else
{ // update previously full channel from min channel:
const Channel.Flags updated_flags = (Channel.Flags)0x7FDC0BE0;
const Channel.Flags2 updated_flags2 = (Channel.Flags2)0x781;
// tdesktop updated flags: broadcast | verified | megagroup | signatures | scam | has_link | slowmode_enabled
// | call_active | call_not_empty | fake | gigagroup | noforwards | join_to_send | join_request | forum
// tdlib nonupdated flags: broadcast | signatures | call_active | call_not_empty | noforwards
prevChannel.flags = (prevChannel.flags & ~updated_flags) | (channel.flags & updated_flags);
prevChannel.flags2 = (prevChannel.flags2 & ~updated_flags2) | (channel.flags2 & updated_flags2);
prevChannel.title = channel.title; // tdlib/tdesktop: updated
prevChannel.username = channel.username; // tdlib/tdesktop: updated
prevChannel.photo = channel.photo; // tdlib: updated if not banned ; tdesktop: updated
prevChannel.restriction_reason = channel.restriction_reason; // tdlib: updated ; tdesktop: not updated
prevChannel.default_banned_rights = channel.default_banned_rights; // tdlib/tdesktop: updated
if (channel.participants_count > 0)
prevChannel.participants_count = channel.participants_count; // tdlib/tdesktop: updated if present
prevChannel.usernames = channel.usernames; // tdlib/tdesktop: updated
prevChannel.color = channel.color; // tdlib: not updated ; tdesktop: updated
prevChannel.profile_color = channel.profile_color; // tdlib/tdesktop: ignored
prevChannel.emoji_status = channel.emoji_status; // tdlib: not updated ; tdesktop: updated
prevChannel.level = channel.level; // tdlib: not updated ; tdesktop: updated
_chats[channel.id] = prevChannel;
}
}
public bool HasUser(long id) { lock (_users) return _users.ContainsKey(id); }
public bool HasChat(long id) { lock (_chats) return _chats.ContainsKey(id); }
}
/// <summary>Accumulate users/chats found in this structure in your dictionaries, ignoring <see href="https://core.telegram.org/api/min">Min constructors</see> when the full object is already stored</summary>
/// <param name="structure">The structure having a <c>users</c></param>
public static void CollectUsersChats(this IPeerResolver structure, IDictionary<long, User> users, IDictionary<long, ChatBase> chats)
=> structure.UserOrChat(new CollectorPeer { _users = users, _chats = chats });
=> structure.UserOrChat(new CollectorPeer(users, chats));
[EditorBrowsable(EditorBrowsableState.Never)]
public static Task<Messages_Chats> Messages_GetChats(this Client _) => throw new WTException("The method you're looking for is Messages_GetAllChats");
@ -120,6 +126,7 @@ namespace TL
{
var entities = new List<MessageEntity>();
MessageEntityBlockquote lastBlockQuote = null;
int inCode = 0;
var sb = new StringBuilder(text);
for (int offset = 0; offset < sb.Length;)
{
@ -127,9 +134,9 @@ namespace TL
{
case '\r': sb.Remove(offset, 1); break;
case '\\': sb.Remove(offset++, 1); break;
case '*': ProcessEntity<MessageEntityBold>(); break;
case '~': ProcessEntity<MessageEntityStrike>(); break;
case '_':
case '*' when inCode == 0: ProcessEntity<MessageEntityBold>(); break;
case '~' when inCode == 0: ProcessEntity<MessageEntityStrike>(); break;
case '_' when inCode == 0:
if (offset + 1 < sb.Length && sb[offset + 1] == '_')
{
sb.Remove(offset, 1);
@ -139,7 +146,7 @@ namespace TL
ProcessEntity<MessageEntityItalic>();
break;
case '|':
if (offset + 1 < sb.Length && sb[offset + 1] == '|')
if (inCode == 0 && offset + 1 < sb.Length && sb[offset + 1] == '|')
{
sb.Remove(offset, 1);
ProcessEntity<MessageEntitySpoiler>();
@ -148,6 +155,7 @@ namespace TL
offset++;
break;
case '`':
int count = entities.Count;
if (offset + 2 < sb.Length && sb[offset + 1] == '`' && sb[offset + 2] == '`')
{
int len = 3;
@ -164,8 +172,9 @@ namespace TL
}
else
ProcessEntity<MessageEntityCode>();
if (entities.Count > count) inCode++; else inCode--;
break;
case '>' when offset == 0 || sb[offset - 1] == '\n':
case '>' when inCode == 0 && offset == 0 || sb[offset - 1] == '\n':
sb.Remove(offset, 1);
if (lastBlockQuote is null || lastBlockQuote.length < offset - lastBlockQuote.offset)
entities.Add(lastBlockQuote = new MessageEntityBlockquote { offset = offset, length = -1 });
@ -175,15 +184,15 @@ namespace TL
case '\n' when lastBlockQuote is { length: -1 }:
lastBlockQuote.length = ++offset - lastBlockQuote.offset;
break;
case '!' when offset + 1 < sb.Length && sb[offset + 1] == '[':
case '!' when inCode == 0 && offset + 1 < sb.Length && sb[offset + 1] == '[':
sb.Remove(offset, 1);
goto case '[';
case '[':
break;
case '[' when inCode == 0:
entities.Add(new MessageEntityTextUrl { offset = offset, length = -1 });
sb.Remove(offset, 1);
break;
case ']':
if (offset + 2 < sb.Length && sb[offset + 1] == '(')
if (inCode == 0 && offset + 2 < sb.Length && sb[offset + 1] == '(')
{
var lastIndex = entities.FindLastIndex(e => e.length == -1);
if (lastIndex >= 0 && entities[lastIndex] is MessageEntityTextUrl textUrl)
@ -213,11 +222,14 @@ namespace TL
void ProcessEntity<T>() where T : MessageEntity, new()
{
sb.Remove(offset, 1);
if (entities.LastOrDefault(e => e.length == -1) is T prevEntity)
prevEntity.length = offset - prevEntity.offset;
if (offset == prevEntity.offset)
entities.Remove(prevEntity);
else
prevEntity.length = offset - prevEntity.offset;
else
entities.Add(new T { offset = offset, length = -1 });
sb.Remove(offset, 1);
}
}
if (lastBlockQuote is { length: -1 })

View file

@ -105,7 +105,7 @@ namespace TL
{
file = inputFile;
mime_type = mimeType;
if (inputFile.Name is string filename) attributes = new[] { new DocumentAttributeFilename { file_name = filename } };
if (inputFile.Name is string filename) attributes = [new DocumentAttributeFilename { file_name = filename }];
}
public InputMediaUploadedDocument(InputFileBase inputFile, string mimeType, params DocumentAttribute[] attribs)
{

View file

@ -1604,11 +1604,11 @@ namespace TL
/// <param name="peer">Target user or group</param>
/// <param name="max_id">If a positive value is passed, only messages with identifiers less or equal than the given one will be read</param>
public static Task<Messages_AffectedMessages> Messages_ReadHistory(this Client client, InputPeer peer, int max_id = default)
=> client.Invoke(new Messages_ReadHistory
=> client.InvokeAffected(new Messages_ReadHistory
{
peer = peer,
max_id = max_id,
});
}, peer is InputPeerChannel ipc ? ipc.channel_id : 0);
/// <summary>Deletes communication history. <para>See <a href="https://corefork.telegram.org/method/messages.deleteHistory"/></para> <para>Possible <see cref="RpcException"/> codes: 400 (<a href="https://corefork.telegram.org/method/messages.deleteHistory#possible-errors">details</a>)</para></summary>
/// <param name="just_clear">Just clear history for the current user, without actually removing messages for every chat user</param>
@ -1618,24 +1618,24 @@ namespace TL
/// <param name="min_date">Delete all messages newer than this UNIX timestamp</param>
/// <param name="max_date">Delete all messages older than this UNIX timestamp</param>
public static Task<Messages_AffectedHistory> Messages_DeleteHistory(this Client client, InputPeer peer, int max_id = default, DateTime? min_date = null, DateTime? max_date = null, bool just_clear = false, bool revoke = false)
=> client.Invoke(new Messages_DeleteHistory
=> client.InvokeAffected(new Messages_DeleteHistory
{
flags = (Messages_DeleteHistory.Flags)((min_date != null ? 0x4 : 0) | (max_date != null ? 0x8 : 0) | (just_clear ? 0x1 : 0) | (revoke ? 0x2 : 0)),
peer = peer,
max_id = max_id,
min_date = min_date.GetValueOrDefault(),
max_date = max_date.GetValueOrDefault(),
});
}, peer is InputPeerChannel ipc ? ipc.channel_id : 0);
/// <summary><para>⚠ <b>This method is only for basic Chat</b>. See <see href="https://wiz0u.github.io/WTelegramClient/#terminology">Terminology</see> in the README to understand what this means<br/>Search for a similar method name starting with <c>Channels_</c> if you're dealing with a <see cref="Channel"/></para> Deletes messages by their identifiers. <para>See <a href="https://corefork.telegram.org/method/messages.deleteMessages"/> [bots: ✓]</para> <para>Possible <see cref="RpcException"/> codes: 400,403 (<a href="https://corefork.telegram.org/method/messages.deleteMessages#possible-errors">details</a>)</para></summary>
/// <param name="revoke">Whether to delete messages for all participants of the chat</param>
/// <param name="id">Message ID list</param>
public static Task<Messages_AffectedMessages> Messages_DeleteMessages(this Client client, int[] id, bool revoke = false)
=> client.Invoke(new Messages_DeleteMessages
=> client.InvokeAffected(new Messages_DeleteMessages
{
flags = (Messages_DeleteMessages.Flags)(revoke ? 0x1 : 0),
id = id,
});
}, 0);
/// <summary><para>⚠ <b>This method is only for basic Chat</b>. See <see href="https://wiz0u.github.io/WTelegramClient/#terminology">Terminology</see> in the README to understand what this means<br/>Search for a similar method name starting with <c>Channels_</c> if you're dealing with a <see cref="Channel"/></para> Confirms receipt of messages by a client, cancels PUSH-notification sending. <para>See <a href="https://corefork.telegram.org/method/messages.receivedMessages"/></para></summary>
/// <param name="max_id">Maximum message ID available in a client.</param>
@ -1977,10 +1977,10 @@ namespace TL
/// <summary><para>⚠ <b>This method is only for basic Chat</b>. See <see href="https://wiz0u.github.io/WTelegramClient/#terminology">Terminology</see> in the README to understand what this means<br/>Search for a similar method name starting with <c>Channels_</c> if you're dealing with a <see cref="Channel"/></para> Notifies the sender about the recipient having listened a voice message or watched a video. <para>See <a href="https://corefork.telegram.org/method/messages.readMessageContents"/></para></summary>
/// <param name="id">Message ID list</param>
public static Task<Messages_AffectedMessages> Messages_ReadMessageContents(this Client client, params int[] id)
=> client.Invoke(new Messages_ReadMessageContents
=> client.InvokeAffected(new Messages_ReadMessageContents
{
id = id,
});
}, 0);
/// <summary>Get stickers by emoji <para>See <a href="https://corefork.telegram.org/method/messages.getStickers"/></para> <para>Possible <see cref="RpcException"/> codes: 400 (<a href="https://corefork.telegram.org/method/messages.getStickers#possible-errors">details</a>)</para></summary>
/// <param name="emoticon">The emoji</param>
@ -2646,12 +2646,12 @@ namespace TL
/// <param name="peer">Dialog</param>
/// <param name="top_msg_id">Mark as read only mentions within the specified <a href="https://corefork.telegram.org/api/forum#forum-topics">forum topic</a></param>
public static Task<Messages_AffectedHistory> Messages_ReadMentions(this Client client, InputPeer peer, int? top_msg_id = null)
=> client.Invoke(new Messages_ReadMentions
=> client.InvokeAffected(new Messages_ReadMentions
{
flags = (Messages_ReadMentions.Flags)(top_msg_id != null ? 0x1 : 0),
peer = peer,
top_msg_id = top_msg_id.GetValueOrDefault(),
});
}, peer is InputPeerChannel ipc ? ipc.channel_id : 0);
/// <summary>Get live location history of a certain user <para>See <a href="https://corefork.telegram.org/method/messages.getRecentLocations"/></para></summary>
/// <param name="peer">User</param>
@ -3057,12 +3057,12 @@ namespace TL
/// <param name="peer">Chat where to unpin</param>
/// <param name="top_msg_id"><a href="https://corefork.telegram.org/api/forum#forum-topics">Forum topic</a> where to unpin</param>
public static Task<Messages_AffectedHistory> Messages_UnpinAllMessages(this Client client, InputPeer peer, int? top_msg_id = null)
=> client.Invoke(new Messages_UnpinAllMessages
=> client.InvokeAffected(new Messages_UnpinAllMessages
{
flags = (Messages_UnpinAllMessages.Flags)(top_msg_id != null ? 0x1 : 0),
peer = peer,
top_msg_id = top_msg_id.GetValueOrDefault(),
});
}, peer is InputPeerChannel ipc ? ipc.channel_id : 0);
/// <summary><para>⚠ <b>This method is only for basic Chat</b>. See <see href="https://wiz0u.github.io/WTelegramClient/#terminology">Terminology</see> in the README to understand what this means<br/>Search for a similar method name starting with <c>Channels_</c> if you're dealing with a <see cref="Channel"/></para> Delete a <a href="https://corefork.telegram.org/api/channel">chat</a> <para>See <a href="https://corefork.telegram.org/method/messages.deleteChat"/></para> <para>Possible <see cref="RpcException"/> codes: 400 (<a href="https://corefork.telegram.org/method/messages.deleteChat#possible-errors">details</a>)</para></summary>
/// <param name="chat_id">Chat ID</param>
@ -3075,10 +3075,10 @@ namespace TL
/// <summary>Delete the entire phone call history. <para>See <a href="https://corefork.telegram.org/method/messages.deletePhoneCallHistory"/></para></summary>
/// <param name="revoke">Whether to remove phone call history for participants as well</param>
public static Task<Messages_AffectedFoundMessages> Messages_DeletePhoneCallHistory(this Client client, bool revoke = false)
=> client.Invoke(new Messages_DeletePhoneCallHistory
=> client.InvokeAffected(new Messages_DeletePhoneCallHistory
{
flags = (Messages_DeletePhoneCallHistory.Flags)(revoke ? 0x1 : 0),
});
}, 0);
/// <summary>Obtains information about a chat export file, generated by a foreign chat app, <a href="https://corefork.telegram.org/api/import">click here for more info about imported chats »</a>. <para>See <a href="https://corefork.telegram.org/method/messages.checkHistoryImport"/></para> <para>Possible <see cref="RpcException"/> codes: 400 (<a href="https://corefork.telegram.org/method/messages.checkHistoryImport#possible-errors">details</a>)</para></summary>
/// <param name="import_head">Beginning of the message file; up to 100 lines.</param>
@ -3446,12 +3446,12 @@ namespace TL
/// <param name="peer">Peer</param>
/// <param name="top_msg_id">Mark as read only reactions to messages within the specified <a href="https://corefork.telegram.org/api/forum#forum-topics">forum topic</a></param>
public static Task<Messages_AffectedHistory> Messages_ReadReactions(this Client client, InputPeer peer, int? top_msg_id = null)
=> client.Invoke(new Messages_ReadReactions
=> client.InvokeAffected(new Messages_ReadReactions
{
flags = (Messages_ReadReactions.Flags)(top_msg_id != null ? 0x1 : 0),
peer = peer,
top_msg_id = top_msg_id.GetValueOrDefault(),
});
}, peer is InputPeerChannel ipc ? ipc.channel_id : 0);
/// <summary>View and search recently sent media.<br/>This method does not support pagination. <para>See <a href="https://corefork.telegram.org/method/messages.searchSentMedia"/></para> <para>Possible <see cref="RpcException"/> codes: 400 (<a href="https://corefork.telegram.org/method/messages.searchSentMedia#possible-errors">details</a>)</para></summary>
/// <param name="q">Optional search query</param>
@ -3859,14 +3859,14 @@ namespace TL
/// <param name="min_date">Delete all messages newer than this UNIX timestamp</param>
/// <param name="max_date">Delete all messages older than this UNIX timestamp</param>
public static Task<Messages_AffectedHistory> Messages_DeleteSavedHistory(this Client client, InputPeer peer, int max_id = default, DateTime? min_date = null, DateTime? max_date = null)
=> client.Invoke(new Messages_DeleteSavedHistory
=> client.InvokeAffected(new Messages_DeleteSavedHistory
{
flags = (Messages_DeleteSavedHistory.Flags)((min_date != null ? 0x4 : 0) | (max_date != null ? 0x8 : 0)),
peer = peer,
max_id = max_id,
min_date = min_date.GetValueOrDefault(),
max_date = max_date.GetValueOrDefault(),
});
}, peer is InputPeerChannel ipc ? ipc.channel_id : 0);
/// <summary>Get pinned <a href="https://corefork.telegram.org/api/saved-messages">saved dialogs, see here »</a> for more info. <para>See <a href="https://corefork.telegram.org/method/messages.getPinnedSavedDialogs"/></para></summary>
public static Task<Messages_SavedDialogsBase> Messages_GetPinnedSavedDialogs(this Client client)
@ -4421,11 +4421,11 @@ namespace TL
/// <param name="channel"><a href="https://corefork.telegram.org/api/channel">Channel/supergroup</a></param>
/// <param name="id">IDs of messages to delete</param>
public static Task<Messages_AffectedMessages> Channels_DeleteMessages(this Client client, InputChannelBase channel, params int[] id)
=> client.Invoke(new Channels_DeleteMessages
=> client.InvokeAffected(new Channels_DeleteMessages
{
channel = channel,
id = id,
});
}, channel.ChannelId);
/// <summary>Reports some messages from a user in a supergroup as spam; requires administrator rights in the supergroup <para>See <a href="https://corefork.telegram.org/method/channels.reportSpam"/></para> <para>Possible <see cref="RpcException"/> codes: 400 (<a href="https://corefork.telegram.org/method/channels.reportSpam#possible-errors">details</a>)</para></summary>
/// <param name="channel">Supergroup</param>
@ -4811,11 +4811,11 @@ namespace TL
/// <param name="channel">Supergroup</param>
/// <param name="participant">The participant whose messages should be deleted</param>
public static Task<Messages_AffectedHistory> Channels_DeleteParticipantHistory(this Client client, InputChannelBase channel, InputPeer participant)
=> client.Invoke(new Channels_DeleteParticipantHistory
=> client.InvokeAffected(new Channels_DeleteParticipantHistory
{
channel = channel,
participant = participant,
});
}, channel.ChannelId);
/// <summary>Set whether all users <a href="https://corefork.telegram.org/api/discussion#requiring-users-to-join-the-group">should join a discussion group in order to comment on a post »</a> <para>See <a href="https://corefork.telegram.org/method/channels.toggleJoinToSend"/></para> <para>Possible <see cref="RpcException"/> codes: 400 (<a href="https://corefork.telegram.org/method/channels.toggleJoinToSend#possible-errors">details</a>)</para></summary>
/// <param name="channel">Discussion group</param>
@ -4960,11 +4960,11 @@ namespace TL
/// <param name="channel">Forum</param>
/// <param name="top_msg_id">Topic ID</param>
public static Task<Messages_AffectedHistory> Channels_DeleteTopicHistory(this Client client, InputChannelBase channel, int top_msg_id)
=> client.Invoke(new Channels_DeleteTopicHistory
=> client.InvokeAffected(new Channels_DeleteTopicHistory
{
channel = channel,
top_msg_id = top_msg_id,
});
}, channel.ChannelId);
/// <summary>Reorder pinned forum topics <para>See <a href="https://corefork.telegram.org/method/channels.reorderPinnedForumTopics"/> [bots: ✓]</para></summary>
/// <param name="force">If not set, the order of only the topics present both server-side and in <c>order</c> will be changed (i.e. mentioning topics not pinned server-side in <c>order</c> will not pin them, and not mentioning topics pinned server-side will not unpin them). <br/>If set, the entire server-side pinned topic list will be replaced with <c>order</c> (i.e. mentioning topics not pinned server-side in <c>order</c> will pin them, and not mentioning topics pinned server-side will unpin them)</param>

View file

@ -32,9 +32,9 @@ namespace TL
public readonly int Bit = bit;
}
public sealed class RpcException(int code, string message, int x = -1) : WTelegram.WTException(message)
public sealed class RpcException(int code, string message, int x = -1) : WTelegram.WTException(message, code)
{
public readonly int Code = code;
public int Code => ErrorCode;
/// <summary>The value of X in the message, -1 if no variable X was found</summary>
public readonly int X = x;
public override string ToString() { var str = base.ToString(); return str.Insert(str.IndexOf(':') + 1, " " + Code); }
@ -398,8 +398,10 @@ namespace TL
public sealed partial class UpdateAffectedMessages : Update // auto-generated for OnOwnUpdates in case of such API call result
{
public Messages_AffectedMessages affected;
public override (long, int, int) GetMBox() => (0, affected.pts, affected.pts_count);
public long mbox_id;
public int pts;
public int pts_count;
public override (long, int, int) GetMBox() => (mbox_id, pts, pts_count);
#if MTPG
public override void WriteTL(BinaryWriter writer) => throw new NotSupportedException();
#endif

View file

@ -9,8 +9,6 @@ using TL;
namespace WTelegram
{
public delegate IPeerInfo UserChatCollector(Dictionary<long, User> users, Dictionary<long, ChatBase> chats);
public class UpdateManager : IPeerResolver
{
/// <summary>Collected info about Users <i>(only if using the default collector)</i></summary>
@ -37,10 +35,9 @@ namespace WTelegram
private readonly Client _client;
private readonly Func<Update, Task> _onUpdate;
private readonly UserChatCollector _onCollect;
private readonly IPeerCollector _collector;
private readonly bool _reentrant;
private readonly SemaphoreSlim _sem = new(1);
private readonly Extensions.CollectorPeer _collector;
private readonly List<(Update update, UpdatesBase updates, bool own, DateTime stamp)> _pending = [];
private readonly Dictionary<long, MBoxState> _local; // -2 for seq/date, -1 for qts, 0 for common pts, >0 for channel pts
private const int L_SEQ = -2, L_QTS = -1, L_PTS = 0;
@ -55,17 +52,11 @@ namespace WTelegram
/// <param name="state">(optional) Resume session by recovering all updates that occured since this state</param>
/// <param name="collector">Custom users/chats collector. By default, those are collected in properties Users/Chats</param>
/// <param name="reentrant"><see langword="true"/> if your <paramref name="onUpdate"/> method can be called again even when last async call didn't return yet</param>
public UpdateManager(Client client, Func<Update, Task> onUpdate, IDictionary<long, MBoxState> state = null, UserChatCollector collector = null, bool reentrant = false)
public UpdateManager(Client client, Func<Update, Task> onUpdate, IDictionary<long, MBoxState> state = null, IPeerCollector collector = null, bool reentrant = false)
{
_client = client;
_onUpdate = onUpdate;
if (collector != null)
_onCollect = collector;
else
{
_collector = new() { _users = Users = [], _chats = Chats = [] };
_onCollect = _collector.UserOrChat;
}
_collector = collector ?? new Extensions.CollectorPeer(Users = [], Chats = []);
if (state == null)
_local = new() { [L_SEQ] = new() { access_hash = UndefinedSeqDate }, [L_QTS] = new(), [L_PTS] = new() };
@ -85,7 +76,7 @@ namespace WTelegram
if (_local[L_PTS].pts != 0) await ResyncState();
break;
case User user when user.flags.HasFlag(User.Flags.self):
if (Users != null) Users[user.id] = user;
_collector.Collect([user]);
goto newSession;
case NewSessionCreated when _client.User != null:
newSession:
@ -130,15 +121,14 @@ namespace WTelegram
var now = _lastUpdateStamp = DateTime.UtcNow;
var updateList = updates.UpdateList;
if (updates is UpdateShortSentMessage sent)
updateList = new[] { new UpdateNewMessage { pts = sent.pts, pts_count = sent.pts_count, message = new Message {
flags = (Message.Flags)sent.flags,
id = sent.id, date = sent.date, entities = sent.entities, media = sent.media, ttl_period = sent.ttl_period,
} } };
else if (Users != null)
if (updates is UpdateShortMessage usm && !Users.ContainsKey(usm.user_id))
(await _client.Updates_GetDifference(usm.pts - usm.pts_count, usm.date, 0)).UserOrChat(_collector);
else if (updates is UpdateShortChatMessage uscm && (!Users.ContainsKey(uscm.from_id) || !Chats.ContainsKey(uscm.chat_id)))
(await _client.Updates_GetDifference(uscm.pts - uscm.pts_count, uscm.date, 0)).UserOrChat(_collector);
updateList = [new UpdateNewMessage { pts = sent.pts, pts_count = sent.pts_count, message = new Message {
flags = (Message.Flags)sent.flags,
id = sent.id, date = sent.date, entities = sent.entities, media = sent.media, ttl_period = sent.ttl_period,
} }];
else if (updates is UpdateShortMessage usm && !_collector.HasUser(usm.user_id))
RaiseCollect(await _client.Updates_GetDifference(usm.pts - usm.pts_count, usm.date, 0));
else if (updates is UpdateShortChatMessage uscm && (!_collector.HasUser(uscm.from_id) || !_collector.HasChat(uscm.chat_id)))
RaiseCollect(await _client.Updates_GetDifference(uscm.pts - uscm.pts_count, uscm.date, 0));
bool ptsChanged = false, gotUPts = false;
int seq = 0;
@ -261,10 +251,12 @@ namespace WTelegram
Log?.Invoke(2, $"({mbox_id,10}, new +{pts_count}->{pts,-6}) {update,-30} First appearance of MBox {ExtendedLog(update)}");
else if (local.access_hash == -1) // no valid access_hash for this channel, so just raise this update
Log?.Invoke(3, $"({mbox_id,10}, {local.pts,6}+{pts_count}->{pts,-6}) {update,-30} No access_hash to recover {ExtendedLog(update)}");
else if (local.pts + pts_count - pts >= 0)
getDiffSuccess = true;
else
{
Log?.Invoke(1, $"({mbox_id,10}, {local.pts,6}+{pts_count}->{pts,-6}) {update,-30} Calling GetDifference {ExtendedLog(update)}");
getDiffSuccess = await GetDifference(mbox_id, pts - pts_count, local);
getDiffSuccess = await GetDifference(mbox_id, pts, local);
}
if (!getDiffSuccess) // no getDiff => just raise received pending updates in order
{
@ -382,6 +374,11 @@ namespace WTelegram
catch (Exception ex)
{
Log?.Invoke(4, $"GetDifference({mbox_id}, {local.pts}->{expected_pts}) raised {ex}");
if (ex.Message == "PERSISTENT_TIMESTAMP_INVALID") // oh boy, we're lost!
if (mbox_id <= 0)
await HandleDifference(null, null, await _client.Updates_GetState(), null);
else if ((await _client.Channels_GetFullChannel(await GetInputChannel(mbox_id, local))).full_chat is ChannelFull full)
local.pts = full.pts;
}
finally
{
@ -441,6 +438,14 @@ namespace WTelegram
}
}
private void RaiseCollect(Updates_DifferenceBase diff)
{
if (diff is Updates_DifferenceSlice uds)
RaiseCollect(uds.users, uds.chats);
else if (diff is Updates_Difference ud)
RaiseCollect(ud.users, ud.chats);
}
private void RaiseCollect(Dictionary<long, User> users, Dictionary<long, ChatBase> chats)
{
try
@ -449,11 +454,12 @@ namespace WTelegram
if (chat is Channel channel && !channel.flags.HasFlag(Channel.Flags.min))
if (_local.TryGetValue(channel.id, out var local))
local.access_hash = channel.access_hash;
_onCollect(users, chats);
_collector.Collect(users.Values);
_collector.Collect(chats.Values);
}
catch (Exception ex)
{
Log?.Invoke(4, $"onCollect({users?.Count},{chats?.Count}) raised {ex}");
Log?.Invoke(4, $"Collect({users?.Count},{chats?.Count}) raised {ex}");
}
}
@ -516,6 +522,14 @@ namespace WTelegram
/// <summary>returns a <see cref="User"/> or <see cref="ChatBase"/> for the given Peer</summary>
public IPeerInfo UserOrChat(Peer peer) => peer?.UserOrChat(Users, Chats);
}
public interface IPeerCollector
{
void Collect(IEnumerable<User> users);
void Collect(IEnumerable<ChatBase> chats);
bool HasUser(long id);
bool HasChat(long id);
}
}
namespace TL
@ -530,7 +544,7 @@ namespace TL
/// <param name="statePath">Resume session by recovering all updates that occured since the state saved in this file</param>
/// <param name="collector">Custom users/chats collector. By default, those are collected in properties Users/Chats</param>
/// <param name="reentrant"><see langword="true"/> if your <paramref name="onUpdate"/> method can be called again even when last async call didn't return yet</param>
public static UpdateManager WithUpdateManager(this Client client, Func<TL.Update, Task> onUpdate, string statePath, UserChatCollector collector = null, bool reentrant = false)
public static UpdateManager WithUpdateManager(this Client client, Func<TL.Update, Task> onUpdate, string statePath, IPeerCollector collector = null, bool reentrant = false)
=> new(client, onUpdate, UpdateManager.LoadState(statePath), collector, reentrant);
/// <summary>Manager ensuring that you receive Telegram updates in correct order, without missing any</summary>
@ -538,7 +552,7 @@ namespace TL
/// <param name="state">(optional) Resume session by recovering all updates that occured since this state</param>
/// <param name="collector">Custom users/chats collector. By default, those are collected in properties Users/Chats</param>
/// <param name="reentrant"><see langword="true"/> if your <paramref name="onUpdate"/> method can be called again even when last async call didn't return yet</param>
public static UpdateManager WithUpdateManager(this Client client, Func<TL.Update, Task> onUpdate, IDictionary<long, UpdateManager.MBoxState> state = null, UserChatCollector collector = null, bool reentrant = false)
public static UpdateManager WithUpdateManager(this Client client, Func<TL.Update, Task> onUpdate, IDictionary<long, UpdateManager.MBoxState> state = null, IPeerCollector collector = null, bool reentrant = false)
=> new(client, onUpdate, state, collector, reentrant);
}
}

View file

@ -44,7 +44,7 @@
</ItemGroup>-->
<ItemGroup Condition="$(DefineConstants.Contains('MTPG'))" >
<ProjectReference Include="..\generator\MTProtoGenerator.csproj" OutputItemType="Analyzer" />
<ProjectReference Include="..\generator\MTProtoGenerator.csproj" OutputItemType="Analyzer" PrivateAssets="All" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">