";
+ else if (nextEntity is MessageEntityPre mep && !string.IsNullOrEmpty(mep.language))
+ {
+ closing.Item2 = "";
+ tag = $"";
+ }
+ else if (nextEntity is MessageEntityBlockquote { flags: MessageEntityBlockquote.Flags.collapsed })
+ tag = "";
+ else if (nextEntity is MessageEntityFormattedDate mefd)
+ tag = $"";
+ else
+ tag = $"<{tag}>";
+ int index = ~closings.BinarySearch(closing, Comparer<(int, string)>.Create((x, y) => x.Item1.CompareTo(y.Item1) | 1));
+ closings.Insert(index, closing);
+ sb.Insert(i, tag); i += tag.Length;
+ }
+ }
+ switch (sb[i])
+ {
+ case '&': sb.Insert(i + 1, "amp;"); i += 4; break;
+ case '<': sb.Insert(i, "<"); sb[i += 3] = ';'; break;
+ case '>': sb.Insert(i, ">"); sb[i += 3] = ';'; break;
+ }
+ }
+ return sb.ToString();
+ }
+
+ static readonly Dictionary EntityToTag = new()
+ {
+ [typeof(MessageEntityBold)] = "b",
+ [typeof(MessageEntityItalic)] = "i",
+ [typeof(MessageEntityCode)] = "code",
+ [typeof(MessageEntityPre)] = "pre",
+ [typeof(MessageEntityTextUrl)] = "a",
+ [typeof(MessageEntityMentionName)] = "a",
+ [typeof(InputMessageEntityMentionName)] = "a",
+ [typeof(MessageEntityUnderline)] = "u",
+ [typeof(MessageEntityStrike)] = "s",
+ [typeof(MessageEntitySpoiler)] = "tg-spoiler",
+ [typeof(MessageEntityCustomEmoji)] = "tg-emoji",
+ [typeof(MessageEntityBlockquote)] = "blockquote",
+ [typeof(MessageEntityFormattedDate)] = "tg-time",
+ };
+
+ /// Replace special HTML characters with their &xx; equivalent
+ /// The text to make HTML-safe
+ /// The HTML-safe text, ready to be used in HtmlToEntities without problems
+ public static string Escape(string text)
+ => text?.Replace("&", "&").Replace("<", "<").Replace(">", ">");
+
+ internal static string DateFormat(MessageEntityFormattedDate.Flags flags) => flags.HasFlag(MessageEntityFormattedDate.Flags.relative) ? "r" :
+ ((flags & MessageEntityFormattedDate.Flags.day_of_week) != 0 ? "w" : "") +
+ ((flags & MessageEntityFormattedDate.Flags.short_date) != 0 ? "d" : "") +
+ ((flags & MessageEntityFormattedDate.Flags.long_date) != 0 ? "D" : "") +
+ ((flags & MessageEntityFormattedDate.Flags.short_time) != 0 ? "t" : "") +
+ ((flags & MessageEntityFormattedDate.Flags.long_time) != 0 ? "T" : "");
+
+ internal static MessageEntityFormattedDate.Flags DateFlags(string format)
+ => (MessageEntityFormattedDate.Flags)format.Sum(c => 1 << "rtTdDw".IndexOf(c));
+ }
+}
diff --git a/src/Session.cs b/src/Session.cs
index e5a8f12..321c319 100644
--- a/src/Session.cs
+++ b/src/Session.cs
@@ -7,68 +7,73 @@ using System.Net;
using System.Security.Cryptography;
using System.Text.Json;
+// Don't change this code to lower the security. It's following Telegram security recommendations https://corefork.telegram.org/mtproto/description
+
namespace WTelegram
{
- internal class Session : IDisposable
+ internal sealed partial class Session : IDisposable
{
public int ApiId;
public long UserId;
public int MainDC;
- public Dictionary DCSessions = new();
+ public Dictionary DCSessions = [];
public TL.DcOption[] DcOptions;
- public class DCSession
+ public sealed class DCSession
{
- public long Id;
- public long AuthKeyID;
public byte[] AuthKey; // 2048-bit = 256 bytes
public long UserId;
+ public long OldSalt; // still accepted for a further 1800 seconds
public long Salt;
- public int Seqno;
- public long ServerTicksOffset;
- public long LastSentMsgId;
+ public SortedList Salts;
public TL.DcOption DataCenter;
- public bool WithoutUpdates;
+ public int Layer;
+ internal long id = Helpers.RandomLong();
+ internal long authKeyID;
+ internal int seqno;
+ internal long serverTicksOffset;
+ internal long lastSentMsgId;
+ internal bool withoutUpdates;
internal Client Client;
- internal int DcID => DataCenter?.id ?? 0;
+ internal int DcID => DataCenter == null ? 0 : DataCenter.flags.HasFlag(TL.DcOption.Flags.media_only) ? -DataCenter.id : DataCenter.id;
internal IPEndPoint EndPoint => DataCenter == null ? null : new(IPAddress.Parse(DataCenter.ip_address), DataCenter.port);
- internal void Renew() { Helpers.Log(3, $"Renewing session on DC {DcID}..."); Id = Helpers.RandomLong(); Seqno = 0; LastSentMsgId = 0; }
- public void DisableUpdates(bool disable = true) { if (WithoutUpdates != disable) { WithoutUpdates = disable; Renew(); } }
+ internal void Renew() { Helpers.Log(3, $"Renewing session on DC {DcID}..."); id = Helpers.RandomLong(); seqno = 0; lastSentMsgId = 0; }
+ public void DisableUpdates(bool disable = true) { if (withoutUpdates != disable) { withoutUpdates = disable; Renew(); } }
- const int msgIdsN = 512;
- private long[] msgIds;
- private int msgIdsHead;
+ const int MsgIdsN = 512;
+ private long[] _msgIds;
+ private int _msgIdsHead;
internal bool CheckNewMsgId(long msg_id)
{
- if (msgIds == null)
+ if (_msgIds == null)
{
- msgIds = new long[msgIdsN];
- msgIds[0] = msg_id;
+ _msgIds = new long[MsgIdsN];
+ _msgIds[0] = msg_id;
msg_id -= 300L << 32; // until the array is filled with real values, allow ids up to 300 seconds in the past
- for (int i = 1; i < msgIdsN; i++) msgIds[i] = msg_id;
+ for (int i = 1; i < MsgIdsN; i++) _msgIds[i] = msg_id;
return true;
}
- int newHead = (msgIdsHead + 1) % msgIdsN;
- if (msg_id > msgIds[msgIdsHead])
- msgIds[msgIdsHead = newHead] = msg_id;
- else if (msg_id <= msgIds[newHead])
+ int newHead = (_msgIdsHead + 1) % MsgIdsN;
+ if (msg_id > _msgIds[_msgIdsHead])
+ _msgIds[_msgIdsHead = newHead] = msg_id;
+ else if (msg_id <= _msgIds[newHead])
return false;
else
{
- int min = 0, max = msgIdsN - 1;
+ int min = 0, max = MsgIdsN - 1;
while (min <= max) // binary search (rotated at newHead)
{
int mid = (min + max) / 2;
- int sign = msg_id.CompareTo(msgIds[(mid + newHead) % msgIdsN]);
+ int sign = msg_id.CompareTo(_msgIds[(mid + newHead) % MsgIdsN]);
if (sign == 0) return false;
else if (sign < 0) max = mid - 1;
else min = mid + 1;
}
- msgIdsHead = newHead;
- for (min = (min + newHead) % msgIdsN; newHead != min;)
- msgIds[newHead] = msgIds[newHead = newHead == 0 ? msgIdsN - 1 : newHead - 1];
- msgIds[min] = msg_id;
+ _msgIdsHead = newHead;
+ for (min = (min + newHead) % MsgIdsN; newHead != min;)
+ _msgIds[newHead] = _msgIds[newHead = newHead == 0 ? MsgIdsN - 1 : newHead - 1];
+ _msgIds[min] = msg_id;
}
return true;
}
@@ -104,28 +109,31 @@ namespace WTelegram
{
var input = new byte[length];
if (store.Read(input, 0, length) != length)
- throw new ApplicationException($"Can't read session block ({store.Position}, {length})");
+ throw new WTException($"Can't read session block ({store.Position}, {length})");
using var sha256 = SHA256.Create();
using var decryptor = aes.CreateDecryptor(rgbKey, input[0..16]);
var utf8Json = decryptor.TransformFinalBlock(input, 16, input.Length - 16);
if (!sha256.ComputeHash(utf8Json, 32, utf8Json.Length - 32).SequenceEqual(utf8Json[0..32]))
- throw new ApplicationException("Integrity check failed in session loading");
+ throw new WTException("Integrity check failed in session loading");
session = JsonSerializer.Deserialize(utf8Json.AsSpan(32), Helpers.JsonOptions);
Helpers.Log(2, "Loaded previous session");
+ using var sha1 = SHA1.Create();
+ foreach (var dcs in session.DCSessions.Values)
+ dcs.authKeyID = BinaryPrimitives.ReadInt64LittleEndian(sha1.ComputeHash(dcs.AuthKey).AsSpan(12));
}
+ session ??= new Session();
+ session._store = store;
+ Encryption.RNG.GetBytes(session._encrypted, 0, 16);
+ session._encryptor = aes.CreateEncryptor(rgbKey, session._encrypted);
+ if (!session._encryptor.CanReuseTransform) session._reuseKey = rgbKey;
+ session._jsonWriter = new Utf8JsonWriter(session._jsonStream, default);
+ return session;
}
catch (Exception ex)
{
store.Dispose();
- throw new ApplicationException($"Exception while reading session file: {ex.Message}\nUse the correct api_hash/id/key, or delete the file to start a new session", ex);
+ throw new WTException($"Exception while reading session file: {ex.Message}\nUse the correct api_hash/id/key, or delete the file to start a new session", ex);
}
- session ??= new Session();
- session._store = store;
- Encryption.RNG.GetBytes(session._encrypted, 0, 16);
- session._encryptor = aes.CreateEncryptor(rgbKey, session._encrypted);
- if (!session._encryptor.CanReuseTransform) session._reuseKey = rgbKey;
- session._jsonWriter = new Utf8JsonWriter(session._jsonStream, default);
- return session;
}
internal void Save() // must be called with lock(session)
@@ -144,16 +152,23 @@ namespace WTelegram
if (!_encryptor.CanReuseTransform) // under Mono, AES encryptor is not reusable
using (var aes = Aes.Create())
_encryptor = aes.CreateEncryptor(_reuseKey, _encrypted[0..16]);
- _store.Position = 0;
- _store.Write(_encrypted, 0, encryptedLen);
- _store.SetLength(encryptedLen);
+ try
+ {
+ _store.Position = 0;
+ _store.Write(_encrypted, 0, encryptedLen);
+ _store.SetLength(encryptedLen);
+ }
+ catch (Exception ex)
+ {
+ Helpers.Log(4, $"{_store} raised {ex}");
+ }
}
_jsonStream.Position = 0;
_jsonWriter.Reset();
}
}
- internal class SessionStore : FileStream
+ internal sealed class SessionStore : FileStream // This class is designed to be high-performance and failure-resilient with Writes (but when you're Andrei, you can't understand that)
{
public override long Length { get; }
public override long Position { get => base.Position; set { } }
@@ -186,4 +201,10 @@ namespace WTelegram
base.Write(_header, 0, 8);
}
}
+
+ internal sealed class ActionStore(byte[] initial, Action save) : MemoryStream(initial ?? [])
+ {
+ public override void Write(byte[] buffer, int offset, int count) => save(buffer[offset..(offset + count)]);
+ public override void SetLength(long value) { }
+ }
}
\ No newline at end of file
diff --git a/src/TL.Extensions.cs b/src/TL.Extensions.cs
deleted file mode 100644
index be79d66..0000000
--- a/src/TL.Extensions.cs
+++ /dev/null
@@ -1,418 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using System.Web;
-
-namespace TL
-{
- public static class Extensions
- {
- private class CollectorPeer : Peer
- {
- public override long ID => 0;
- internal Dictionary _users;
- internal Dictionary _chats;
- internal override IPeerInfo UserOrChat(Dictionary users, Dictionary chats)
- {
- 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;
- 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(channel.id, out var prevChat) || prevChat is not Channel prevChannel || prevChannel.flags.HasFlag(Channel.Flags.min))
- _chats[kvp.Key] = channel;
- return null;
- }
- }
-
- /// Accumulate users/chats found in this structure in your dictionaries, ignoring Min constructors when the full object is already stored
- /// The structure having a users
- public static void CollectUsersChats(this IPeerResolver structure, Dictionary users, Dictionary chats)
- => structure.UserOrChat(new CollectorPeer { _users = users, _chats = chats });
-
- public static Task Messages_GetChats(this WTelegram.Client _) => throw new ApplicationException("The method you're looking for is Messages_GetAllChats");
- public static Task Channels_GetChannels(this WTelegram.Client _) => throw new ApplicationException("The method you're looking for is Messages_GetAllChats");
- public static Task