using System; using System.Collections.Generic; using System.Linq; using System.Threading; using NeoSmart.Unicode; using static TdLib.TdApi; using static tgcli.tgcli; namespace tgcli; public static class Util { public static class Ansi { public const string ResetAll = "\x1B[0m"; public const string Red = "\x1b[31m"; public const string Green = "\x1b[32m"; public const string Yellow = "\x1b[33m"; public const string Blue = "\x1b[34m"; public const string Magenta = "\x1b[35m"; public const string Cyan = "\x1b[36m"; public const string Bold = "\x1b[1m"; public const string BoldOff = "\x1b[22m"; public const string Inverse = "\x1b[7m"; public const string InverseOff = "\x1b[27m"; } public static User GetUser(long uid) { try { var uinfo = client.ExecuteAsync(new GetUser { UserId = uid }).Result; return uinfo; } catch { var user = new User(); user.FirstName = "null"; user.LastName = "null"; return user; } } public static Chat GetChat(long chatId) { try { return client.ExecuteAsync(new GetChat { ChatId = chatId }).Result; } catch { return null; } } public static User GetMe() { return client.ExecuteAsync(new GetMe()).Result; } public static Message GetMessage(long chatId, long messageId) { return client.ExecuteAsync(new GetMessage { ChatId = chatId, MessageId = messageId }).Result; } public static int GetTotalMessages(long chatId) { try { var response = client.ExecuteAsync(new SearchChatMessages { ChatId = chatId, Query = "+", Limit = 1 }); return response.Result.TotalCount; } catch { return 9999; } } public static List GetHistory(long chatId, int limit = 5, long fromMessageId = 0, int offset = 0, bool isSecret = false, bool skipTotal = false) { var history = new List(); var total = GetTotalMessages(chatId); var chat = GetChat(chatId); if (chat.Type is ChatType.ChatTypeSupergroup || isSecret) skipTotal = true; if (limit > total && !skipTotal) limit = total; for (var i = 5; i > 0; i--) { if (limit <= 0) { if (total == 0) return history; lock (@lock) messageQueue.Add($"{Ansi.Red}[tgcli] " + "Limit cannot be less than one. Usage: /history "); return history; } var response = client.ExecuteAsync(new GetChatHistory { ChatId = chatId, FromMessageId = fromMessageId, Limit = limit, Offset = offset, OnlyLocal = false }) .Result; if (response.Messages_.Length < limit && i > 1 && !isSecret) { Thread.Sleep(100); continue; } history.AddRange(response.Messages_); history.Reverse(); return history; } return history; } public static bool IsMuted(Chat c) { if (c.NotificationSettings.MuteFor == 0 && !c.NotificationSettings.UseDefaultMuteFor) return false; NotificationSettingsScope scope = c.Type switch { ChatType.ChatTypeBasicGroup => new NotificationSettingsScope.NotificationSettingsScopeGroupChats(), ChatType.ChatTypeSupergroup t => t.IsChannel ? new NotificationSettingsScope.NotificationSettingsScopeChannelChats() : new NotificationSettingsScope.NotificationSettingsScopeGroupChats(), ChatType.ChatTypePrivate => new NotificationSettingsScope.NotificationSettingsScopePrivateChats(), ChatType.ChatTypeSecret => new NotificationSettingsScope.NotificationSettingsScopePrivateChats(), _ => throw new ArgumentOutOfRangeException() }; return client.GetScopeNotificationSettingsAsync(scope).Result.MuteFor != 0; } public static List GetUnreadChats(bool all = false) { var output = new List(); var response = client.ExecuteAsync(new GetChats { Limit = int.MaxValue }).Result; output.AddRange(all ? response.ChatIds.Select(GetChat).Where(c => c.UnreadCount > 0 || c.IsMarkedAsUnread).ToList() : response.ChatIds.Select(GetChat).Where(c => (c.UnreadCount > 0 || c.IsMarkedAsUnread) && !IsMuted(c)).ToList()); return output; } public static List GetChats() { var response = client.ExecuteAsync(new GetChats { Limit = int.MaxValue }).Result; return response.ChatIds.Select(GetChat).ToList(); } public static List SearchChatsGlobal(string query) { if (query.TrimStart('@').Length < 5) { return new List(); } var response = client.ExecuteAsync(new SearchPublicChats { Query = query }).Result; var chats = response.ChatIds.Select(GetChat).ToList(); chats.AddRange(client.ExecuteAsync(new SearchChats { Query = query, Limit = int.MaxValue }).Result.ChatIds.Select(GetChat)); return chats; } public static Chat GetChatByUsernameGlobal(string username) { try { var response = client.ExecuteAsync(new SearchPublicChat { Username = username }).Result; return response; } catch { return null; } } public static long GetUserIdByUsername(string username) { try { var response = client.ExecuteAsync(new SearchPublicChat { Username = username }).Result; if (response.Type is ChatType.ChatTypePrivate priv) return priv.UserId; return 0; } catch { return 0; } } public static void AddUserToContacts(int userId, string name) { //TODO implement when TDLib 1.6 is released } public static List GetSecretChats() { var response = client.ExecuteAsync(new GetChats { Limit = int.MaxValue }).Result; return response.ChatIds.Select(GetChat).Where(c => c.Type is ChatType.ChatTypeSecret).ToList(); } public static void CloseSecretChat(int secretChatId) { client.ExecuteAsync(new CloseSecretChat { SecretChatId = secretChatId }).Wait(); } public static Chat CreateSecretChat(long userId) { return client.ExecuteAsync(new CreateNewSecretChat { UserId = userId }).Result; } public static void DeleteChatHistory(long chatId) { client.ExecuteAsync(new DeleteChatHistory { ChatId = chatId, RemoveFromChatList = true, Revoke = true }).Wait(); } public static SecretChat GetSecretChat(int secretChatId) { var response = client.ExecuteAsync(new GetSecretChat { SecretChatId = secretChatId }).Result; return response; } public static void ClearCurrentConsoleLine() { Console.Write("\u001b[2K\r"); //Console.SetCursorPosition(0, Console.WindowHeight); //Console.Write(new string(' ', Console.WindowWidth)); //Console.SetCursorPosition(0, Console.WindowHeight); } public static string ReadConsolePassword() { var pass = ""; do { var key = Console.ReadKey(true); if (key.Key != ConsoleKey.Backspace && key.Key != ConsoleKey.Enter) { pass += key.KeyChar; Console.Write("*"); } else { if (key.Key == ConsoleKey.Backspace && pass.Length > 0) { pass = pass[..^1]; Console.Write("\b \b"); } else if (key.Key == ConsoleKey.Enter) { break; } } } while (true); Console.WriteLine(); return pass; } public static void SendMessage(string message, long chatId, long replyTo = 0) { if (string.IsNullOrWhiteSpace(message)) return; Emojis.ForEach(em => message = message.Replace(em.Item1, em.Item2)); client.ExecuteAsync(new SendMessage { ChatId = chatId, InputMessageContent = new InputMessageContent.InputMessageText { Text = new FormattedText { Text = message } }, ReplyToMessageId = replyTo, }); currentUserRead = false; } public static Message EditMessage(string newText, Message message) { Emojis.ForEach(em => newText = newText.Replace(em.Item1, em.Item2)); var msg = client.ExecuteAsync(new EditMessageText { ChatId = message.ChatId, MessageId = message.Id, InputMessageContent = new InputMessageContent.InputMessageText { Text = new FormattedText { Text = newText } } }) .Result; return msg; } public static void MarkRead(long chatId, long messageId) { client.ExecuteAsync(new ViewMessages { ChatId = chatId, MessageIds = new[] { messageId }, ForceRead = true }); } public static void MarkUnread(long chatId) { client.ExecuteAsync(new ToggleChatIsMarkedAsUnread { ChatId = chatId, IsMarkedAsUnread = true, }); } public static long SearchChatId(string query) { try { var results = client.ExecuteAsync(new SearchChats { Query = query, Limit = 5 }).Result; return query.StartsWith("@") ? results.ChatIds.First(p => GetChat(p).Type is ChatType.ChatTypePrivate type && GetUser(type.UserId).Usernames.ActiveUsernames.Contains(query[1..])) : results.ChatIds.First(p => !(GetChat(p).Type is ChatType.ChatTypeSecret)); } catch { lock (@lock) messageQueue.Add($"{Ansi.Red}[tgcli] No results found."); return 0; } } public static long SearchUserInChats(string query) { var results = client.ExecuteAsync(new SearchChatsOnServer { Query = query, Limit = 5 }).Result; if (results.ChatIds.Length == 0) return 0; var output = results.ChatIds.Select(GetChat).Where(p => p.Type is ChatType.ChatTypePrivate).Select(p => ((ChatType.ChatTypePrivate)p.Type).UserId); return output.Any() ? output.First() : 0; } public static long SearchContacts(string query) { //TODO implement when TDLib 1.6 is released try { var results = client.ExecuteAsync(new SearchContacts { Query = query, Limit = 5 }).Result; return query.StartsWith("@") ? results.UserIds.First(p => GetUser(p).Usernames.ActiveUsernames.Contains(query[1..])) : results.UserIds.First(); } catch { lock (@lock) messageQueue.Add($"{Ansi.Red}[tgcli] No results found."); return 0; } } public static void LogOut() { lock (@lock) messageQueue.Add($"{Ansi.Yellow}[tgcli] Logging out..."); client.ExecuteAsync(new LogOut()).Wait(); } public static string GetFormattedUsername(MessageSender sender) { return sender switch { MessageSender.MessageSenderUser user => GetFormattedUsername(GetUser(user.UserId)), MessageSender.MessageSenderChat chat => GetFormattedUsername(GetChat(chat.ChatId)), _ => throw new InvalidCastException() }; } private static string GetFormattedUsername(User sender) { var username = sender.Usernames?.ActiveUsernames?.FirstOrDefault(); if (string.IsNullOrWhiteSpace(username)) username = sender.FirstName + " " + sender.LastName; else username = "@" + username; return username; } private static string GetFormattedUsername(Chat sender) { return $"{sender.Title} [as chat]"; } public static string FormatTime(long unix) { var time = DateTimeOffset.FromUnixTimeSeconds(unix).DateTime.ToLocalTime(); var currentTime = DateTime.Now.ToLocalTime(); return time.ToString(time.Date.Ticks == currentTime.Date.Ticks ? "HH:mm" : "yyyy-MM-dd HH:mm"); } public static bool IsMessageRead(long chatId, long messageId) { var chat = GetChat(chatId); return chat.LastReadOutboxMessageId >= messageId; } public static int GetActualStringWidth(string input) { input = input.Replace(Ansi.Blue, ""); input = input.Replace(Ansi.Bold, ""); input = input.Replace(Ansi.Cyan, ""); input = input.Replace(Ansi.Green, ""); input = input.Replace(Ansi.Magenta, ""); input = input.Replace(Ansi.Red, ""); input = input.Replace(Ansi.Yellow, ""); input = input.Replace(Ansi.Bold, ""); input = input.Replace(Ansi.BoldOff, ""); input = input.Replace(Ansi.Inverse, ""); input = input.Replace(Ansi.InverseOff, ""); input = input.Replace(Ansi.ResetAll, ""); return input.Length; } public static string GetFormattedStatus(bool isRead) { var output = " "; output += (isRead ? Ansi.Green : Ansi.Red) + "r"; return output + $"{Ansi.ResetAll}]"; } public static string TruncateString(string input, int maxLen, string truncateMarker = "~") { if (maxLen < 2) maxLen = 2; return input.Length <= maxLen ? input : input[..(maxLen - 1)] + truncateMarker; } public static (string messageBuffer, int relCursorPos) GetPagedMessageInputLine(string message, int absCursorPos, int bufferWidth) { const int wrapdOffsetPre = 2; // number of "untouchable" characters moving the cursor onto will cause a wrap on the right screen edge const int wrapOffsetPost = 5; // number of "untouchable" characters moving the cursor onto will cause a wrap on the left screen edge const int wrapOffsetPreI = wrapdOffsetPre + 1; // offset + 1 (indicator on the edge), for easier calculations const int wrapOffsetPostI = wrapOffsetPost + 1; // offset + 1 (indicator on the edge), for easier calculations if (absCursorPos > message.Length) throw new ArgumentOutOfRangeException(nameof(absCursorPos), "Cursor position exceeds message length"); if (message.Length < bufferWidth) // entire message fits in buffer return (message, absCursorPos); // return input as-is if (absCursorPos < bufferWidth - wrapdOffsetPre - 1) // message is longer than buffer but we're on the first page return (TruncateString(message, bufferWidth, $"{Ansi.Inverse}>{Ansi.InverseOff}"), absCursorPos); // return input as-is but truncated and with a > indicator var wraps = (absCursorPos - wrapOffsetPostI) / (bufferWidth - wrapOffsetPreI - wrapOffsetPostI); // black magic var finalCursorPos = absCursorPos - bufferWidth + wrapOffsetPreI + wrapOffsetPostI * wraps; // respect the special case of the first page & add one post offset per wrap finalCursorPos %= bufferWidth - wrapOffsetPreI; // make sure the final cursor position is within the acceptable range (between zero and bufWidth - wrapOffsetPreI) var messageOffset = (bufferWidth - wrapOffsetPreI - wrapOffsetPostI) * wraps + 1; // +1 to account for the first wrap not having a < indicator var finalMessage = message[messageOffset..]; // we only care about the message starting from the current page finalMessage = TruncateString(finalMessage, bufferWidth - 1, $"{Ansi.Inverse}>{Ansi.InverseOff}"); // replace the last character with a > indicator if required return ($"{Ansi.Inverse}<{Ansi.InverseOff}" + finalMessage, finalCursorPos); } public static readonly List> Emojis = new() { new Tuple("⏎ ", "\n"), new Tuple(":xd:", Emoji.FaceWithTearsOfJoy.Sequence.AsString), new Tuple(":check:", Emoji.CheckMark.Sequence.AsString), new Tuple(":thinking:", Emoji.ThinkingFace.Sequence.AsString), new Tuple(":eyes:", Emoji.Eyes.Sequence.AsString), new Tuple(":heart:", Emoji.RedHeart.Sequence.AsString), new Tuple(":shrug:", Emoji.PersonShrugging.Sequence.AsString), new Tuple(":shrugf:", Emoji.WomanShrugging.Sequence.AsString), new Tuple(":shrugm:", Emoji.ManShrugging.Sequence.AsString) }; public static void InsertToInputLine(string strToInsert) { var part1 = currentInputLine[..currentInputPos]; var part2 = currentInputLine[currentInputPos..]; currentInputLine = part1 + strToInsert + part2; currentInputPos += strToInsert.Length; } public static void SetInputLine(string newInputLine) { currentInputLine = newInputLine; currentInputPos = newInputLine.Length; } public static void RemoveFromInputLine(bool word = false) { var part1 = currentInputLine[..currentInputPos]; var oldlen = part1.Length; var part2 = currentInputLine[currentInputPos..]; if (word) { var lastIndex = part1.TrimEnd().LastIndexOf(" ", StringComparison.Ordinal); if (lastIndex < 0) lastIndex = 0; part1 = part1[..lastIndex]; if (lastIndex != 0) part1 += " "; //if (part1.EndsWith("⏎")) // part1 = part1.Remove(part1.Length - 1); var newlen = part1.Length; currentInputLine = part1 + part2; currentInputPos -= oldlen - newlen; return; } currentInputLine = part1[..^1] + part2; currentInputPos--; } public static void RemoveFromInputLineForward(bool word = false) { var part1 = currentInputLine[..currentInputPos]; var part2 = currentInputLine[currentInputPos..].TrimStart(); if (word) { var index = part2.IndexOf(" ", StringComparison.Ordinal); if (index < 0) index = part2.Length - 1; part2 = part2[(index + 1)..]; if (index != 0) part2 = " " + part2; //if (part2.StartsWith("⏎")) // part2 = part2.Remove(part1.Length - 1); currentInputLine = part1 + part2; return; } currentInputLine = part1 + part2[1..]; } public static readonly List SpecialKeys = new() { ConsoleKey.Backspace, ConsoleKey.Tab, ConsoleKey.Clear, ConsoleKey.Enter, ConsoleKey.Pause, ConsoleKey.Escape, ConsoleKey.PageUp, ConsoleKey.PageDown, ConsoleKey.End, ConsoleKey.Home, ConsoleKey.LeftArrow, ConsoleKey.UpArrow, ConsoleKey.RightArrow, ConsoleKey.DownArrow, ConsoleKey.Select, ConsoleKey.Print, ConsoleKey.Execute, ConsoleKey.PrintScreen, ConsoleKey.Insert, ConsoleKey.Delete, ConsoleKey.Help, ConsoleKey.LeftWindows, ConsoleKey.RightWindows, ConsoleKey.Applications, ConsoleKey.Sleep, ConsoleKey.F1, ConsoleKey.F2, ConsoleKey.F3, ConsoleKey.F4, ConsoleKey.F5, ConsoleKey.F6, ConsoleKey.F7, ConsoleKey.F8, ConsoleKey.F9, ConsoleKey.F10, ConsoleKey.F11, ConsoleKey.F12, ConsoleKey.F13, ConsoleKey.F14, ConsoleKey.F15, ConsoleKey.F16, ConsoleKey.F17, ConsoleKey.F18, ConsoleKey.F19, ConsoleKey.F20, ConsoleKey.F21, ConsoleKey.F22, ConsoleKey.F23, ConsoleKey.F24, ConsoleKey.BrowserBack, ConsoleKey.BrowserForward, ConsoleKey.BrowserRefresh, ConsoleKey.BrowserStop, ConsoleKey.BrowserSearch, ConsoleKey.BrowserFavorites, ConsoleKey.BrowserHome, ConsoleKey.VolumeMute, ConsoleKey.VolumeDown, ConsoleKey.VolumeUp, ConsoleKey.MediaNext, ConsoleKey.MediaPrevious, ConsoleKey.MediaStop, ConsoleKey.MediaPlay, ConsoleKey.LaunchMail, ConsoleKey.LaunchMediaSelect, ConsoleKey.LaunchApp1, ConsoleKey.LaunchApp2, ConsoleKey.Oem1, ConsoleKey.Oem2, ConsoleKey.Oem3, ConsoleKey.Oem4, ConsoleKey.Oem5, ConsoleKey.Oem6, ConsoleKey.Oem7, ConsoleKey.Oem8, ConsoleKey.Oem102, ConsoleKey.Process, ConsoleKey.Packet, ConsoleKey.Attention, ConsoleKey.CrSel, ConsoleKey.ExSel, ConsoleKey.EraseEndOfFile, ConsoleKey.Play, ConsoleKey.Zoom, ConsoleKey.NoName, ConsoleKey.Pa1, ConsoleKey.OemClear }; }