diff --git a/EXAMPLES.md b/EXAMPLES.md index 8bee3d7..3b91feb 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -15,11 +15,12 @@ Remember that these are just simple example codes that you should adjust to your ```csharp using var client = new WTelegram.Client(Environment.GetEnvironmentVariable); await client.LoginUserIfNeeded(); -var resolved = await client.Contacts_ResolveUsername("username"); +var resolved = await client.Contacts_ResolveUsername("username"); // without the @ await client.SendMessageAsync(resolved, "Hello!"); ``` *Note: This also works if the @username points to a chat, but you must join the chat before posting there. You can check `resolved` properties to ensure it's a user or a chat. If the username is invalid/unused, the API call raises an exception.* + ### Send a message to someone by phone number ```csharp using var client = new WTelegram.Client(Environment.GetEnvironmentVariable); @@ -30,6 +31,16 @@ if (contacts.imported.Length > 0) ``` *Note: To prevent spam, Telegram may restrict your ability to add new phone numbers.* +### Send a Markdown message to ourself (Saved Messages) +```csharp +using var client = new WTelegram.Client(Environment.GetEnvironmentVariable); +await client.LoginUserIfNeeded(); +var text = "Hello __dear *friend*__!\nEnjoy this `userbot` written with [WTelegramClient](https://github.com/wiz0u/WTelegramClient)"; +var entities = client.MarkdownToEntities(ref text); +await client.SendMessageAsync(InputPeer.Self, text, entities: entities); +``` +See [MarkdownV2 formatting style](https://core.telegram.org/bots/api/#markdownv2-style) for details. + ### List all chats (groups/channels) the user is in and send a message to one ```csharp using var client = new WTelegram.Client(Environment.GetEnvironmentVariable); diff --git a/src/Encryption.cs b/src/Encryption.cs index dc0ee0a..863fff8 100644 --- a/src/Encryption.cs +++ b/src/Encryption.cs @@ -436,7 +436,7 @@ j4WcDuXc2CTHgH8gFTNhp/Y8/SpDOhvn9QIDAQAB #if !NETCOREAPP2_0_OR_GREATER // adapted from https://github.com/dotnet/aspnetcore/blob/main/src/DataProtection/Cryptography.KeyDerivation/src/PBKDF2/ManagedPbkdf2Provider.cs - public static byte[] PBKDF2_SHA512(byte[] password, byte[] salt, int iterationCount, int numBytesRequested) + private static byte[] PBKDF2_SHA512(byte[] password, byte[] salt, int iterationCount, int numBytesRequested) { // PBKDF2 is defined in NIST SP800-132, Sec. 5.3: http://csrc.nist.gov/publications/nistpubs/800-132/nist-sp800-132.pdf byte[] retVal = new byte[numBytesRequested]; diff --git a/src/Helpers.TL.cs b/src/Helpers.TL.cs index f4f0daa..feeaeaf 100644 --- a/src/Helpers.TL.cs +++ b/src/Helpers.TL.cs @@ -353,4 +353,93 @@ namespace TL return sb.Append('}').ToString(); } } + + public static class Helpers + { + public static MessageEntity[] MarkdownToEntities(this WTelegram.Client client, ref string text) + { + var entities = new List(); + var sb = new StringBuilder(text); + for (int offset = 0; offset < sb.Length;) + { + switch (sb[offset]) + { + case '\\': sb.Remove(offset++, 1); break; + case '*': + ProcessEntity(); + break; + case '_': + if (offset + 1 < sb.Length && sb[offset + 1] == '_') + { + sb.Remove(offset, 1); + ProcessEntity(); + } + else + ProcessEntity(); + break; + case '~': + ProcessEntity(); + break; + case '`': + if (offset + 2 < sb.Length && sb[offset + 1] == '`' && sb[offset + 2] == '`') + { + int header = 3; + if (entities.FindLast(e => e.length == 0) is MessageEntityPre pre) + pre.length = offset - pre.offset; + else + { + while (offset + header < sb.Length && !char.IsWhiteSpace(sb[offset + header])) + header++; + entities.Add(new MessageEntityPre { offset = offset, language = sb.ToString(offset + 3, header - 3) }); + } + sb.Remove(offset, header); + } + else + ProcessEntity(); + break; + case '[': + entities.Add(new MessageEntityTextUrl { offset = offset }); + sb.Remove(offset, 1); + break; + case ']': + if (offset + 1 < sb.Length && sb[offset + 1] == '(' && entities.FindLast(e => e.length == 0) is MessageEntityTextUrl textUrl) + { + textUrl.length = offset - textUrl.offset; + sb.Remove(offset, 2); + } + else + offset++; + break; + case ')': + var lastIndex = entities.FindLastIndex(e => e.length == 0 || e is MessageEntityTextUrl { url: null }); + MessageEntity entity; + if (lastIndex >= 0 && (entity = entities[lastIndex]).length != 0) + { + var urlStart = entity.offset + entity.length; + var urlLength = offset - urlStart; + var url = sb.ToString(urlStart, urlLength); + sb.Remove(urlStart, urlLength + 1); + offset = urlStart; + if (url.StartsWith("tg://user?id=") && long.TryParse(url[13..], out var user_id) && client.GetAccessHashFor(user_id) is long hash && hash != 0) + entities[lastIndex] = new InputMessageEntityMentionName { offset = entity.offset, length = entity.length, user_id = new InputUser { user_id = user_id, access_hash = hash } }; + else + ((MessageEntityTextUrl)entity).url = url; + } + break; + default: offset++; break; + } + + void ProcessEntity() where T : MessageEntity, new() + { + if (entities.LastOrDefault(e => e.length == 0) is T prevEntity) + prevEntity.length = offset - prevEntity.offset; + else + entities.Add(new T { offset = offset }); + sb.Remove(offset, 1); + } + } + text = sb.ToString(); + return entities.Count == 0 ? null : entities.ToArray(); + } + } }