Forge
csharp1a59f514
1using System;
2using System.Collections.Generic;
3using System.Linq;
4using System.Text;
5using System.Threading.Tasks;
6
7using TL;
8
9using WTelegram;
10
11namespace TelegramNotifier
12{
13 public class TelegramManager
14 {
15 private const string USER_PRIVACY_RESTRICTED_MESSAGE = "USER_PRIVACY_RESTRICTED";
16 private const string USER_NOT_MUTUAL_CONTACT = "USER_NOT_MUTUAL_CONTACT";
17 private const long SUPERGROUP_CHAT_ID_THRESHOLD = -1000000000000L;
18
19 readonly Client _telegramClient;
20 private Dictionary<long, InputPeer>? _peerCache;
21 private Dictionary<long, InputChannel>? _channelCache;
22
23 public TelegramManager(Client client)
24 {
25 _telegramClient = client;
26 }
27
28 /// <summary>
29 /// Resolves InputPeer for the given chat_id. Basic groups use InputPeerChat;
30 /// supergroups (forum-capable) use InputPeerChannel from dialogs.
31 /// </summary>
32 private async Task<InputPeer> GetInputPeer(long chatId)
33 {
34 // Basic group: small positive id (e.g. 840666093). Supergroups from list-chats: 1e9..1e12
35 if (chatId > 0 && chatId < 1000000000L)
36 return new InputPeerChat(chatId);
37
38 if (_peerCache != null && _peerCache.TryGetValue(chatId, out var cached))
39 return cached;
40
41 var dialogs = await _telegramClient.Messages_GetAllDialogs();
42 // Supergroup: from list-chats we use positive 1000000000000 - ch.id; standard is negative -1000000000000 - ch.id
43 long channelId = chatId > 0 ? 1000000000000L - chatId : -SUPERGROUP_CHAT_ID_THRESHOLD - chatId;
44 if (dialogs is Messages_DialogsSlice slice)
45 {
46 foreach (var (id, chat) in slice.chats)
47 {
48 if (chat is Channel ch && ch.id == channelId)
49 {
50 var peer = new InputPeerChannel(channelId, ch.access_hash);
51 _peerCache ??= new Dictionary<long, InputPeer>();
52 _peerCache[chatId] = peer;
53 return peer;
54 }
55 }
56 }
57 throw new InvalidOperationException($"Channel for chat_id {chatId} not found in dialogs. Is the account in the group?");
58 }
59
60 /// <summary>
61 /// Resolves InputChannel for a supergroup (required for forum topic API). Throws for basic groups.
62 /// </summary>
63 private async Task<InputChannel> GetInputChannel(long chatId)
64 {
65 if (chatId > 0 && chatId < 1000000000L)
66 throw new InvalidOperationException("create-topic works only for supergroups (forum). Use a forum chat_id from list-chats.");
67 if (_channelCache != null && _channelCache.TryGetValue(chatId, out var cached))
68 return cached;
69 var dialogs = await _telegramClient.Messages_GetAllDialogs();
70 long channelId = chatId > 0 ? 1000000000000L - chatId : -SUPERGROUP_CHAT_ID_THRESHOLD - chatId;
71 if (dialogs is Messages_DialogsSlice slice)
72 {
73 foreach (var (_, chat) in slice.chats)
74 {
75 if (chat is Channel ch && ch.id == channelId)
76 {
77 var inputChannel = new InputChannel(channelId, ch.access_hash);
78 _channelCache ??= new Dictionary<long, InputChannel>();
79 _channelCache[chatId] = inputChannel;
80 return inputChannel;
81 }
82 }
83 }
84 throw new InvalidOperationException($"Channel for chat_id {chatId} not found in dialogs. Is the account in the group?");
85 }
86
87 /// <summary>
88 /// List all chats (groups and supergroups/channels) the user is in. Returns (chat_id, title) for each.
89 /// </summary>
90 public async Task<IReadOnlyList<(long ChatId, string Title)>> ListChatsAsync()
91 {
92 var dialogs = await _telegramClient.Messages_GetAllDialogs();
93 var list = new List<(long, string)>();
94 IEnumerable<KeyValuePair<long, ChatBase>>? chats = null;
95 if (dialogs is Messages_DialogsSlice slice)
96 chats = slice.chats;
97 else if (dialogs is Messages_Dialogs full)
98 chats = full.chats;
99 if (chats == null)
100 return list;
101 foreach (var (_, chat) in chats)
102 {
103 if (chat is Channel ch)
104 {
105 // This codebase uses positive chat_id for supergroups: 1000000000000 - ch.id
106 long chatId = -SUPERGROUP_CHAT_ID_THRESHOLD - ch.id;
107 list.Add((chatId, ch.Title ?? ""));
108 }
109 else if (chat is Chat c)
110 {
111 list.Add((c.id, c.Title ?? ""));
112 }
113 }
114 return list;
115 }
116
117 /// <summary>
118 /// Create a forum topic in a supergroup (requires forum enabled and manage_topics rights).
119 /// Returns the created topic's message id (top_msg_id).
120 /// </summary>
121 public async Task<int> CreateForumTopic(long chatId, string title)
122 {
123 var channel = await GetInputChannel(chatId);
124 var randomId = (long)(DateTime.UtcNow.Ticks % long.MaxValue);
125 var updates = await _telegramClient.Channels_CreateForumTopic(channel, title, randomId);
126 if (updates is Updates updatesObj && updatesObj.updates?.Length > 0)
127 {
128 foreach (var u in updatesObj.updates)
129 {
130 if (u is UpdateMessageID mid)
131 return mid.id;
132 }
133 }
134 return 0;
135 }
136
137 public async Task DoLogin(string loginInfo) // (add this method to your code)
138 {
139 while (_telegramClient.User == null)
140 {
141 switch (await _telegramClient.Login(loginInfo)) // returns which config is needed to continue login
142 {
143 case "verification_code": Console.Write("Code: "); loginInfo = Console.ReadLine(); break;
144 case "name": loginInfo = "John Doe"; break; // if sign-up is required (first/last_name)
145 case "password": loginInfo = Console.ReadLine(); break;// if user has enabled 2FA
146 default: loginInfo = null; break;
147 }
148 }
149
150 Console.WriteLine($"We are logged-in as {_telegramClient.User} (id {_telegramClient.User.id})");
151 }
152 public async Task CreateChat(string groupName, string invitedUserName)
153 {
154 var result = await _telegramClient.Contacts_ResolveUsername(invitedUserName);
155 var data = await _telegramClient.Messages_CreateChat(new InputUser[] { result.User }, groupName);
156 Console.WriteLine($"Chat created with id={data.Chats.Keys.ElementAt(0)}");
157 }
158 public async Task DeleteChat(long chat_id)
159 {
160 await _telegramClient.DeleteChat(new InputPeerChat(chat_id));
161 Console.WriteLine($"chat with id = {chat_id} is deleted");
162 }
163 public async Task AddUserToChat(long chat_id, string invitedUserName)
164 {
165 var result = await _telegramClient.Contacts_ResolveUsername(invitedUserName);
166
167 try
168 {
169 await _telegramClient.AddChatUser(new InputPeerChat(chat_id), result.User);
170 }
171 catch (RpcException e)
172 {
173 if (e.Message== USER_PRIVACY_RESTRICTED_MESSAGE)
174 {
175 Console.WriteLine($"Addition for user @{invitedUserName} to chat {chat_id} is restricted by privacy settings of invited user");
176 }
177 if (e.Message==USER_NOT_MUTUAL_CONTACT)
178 {
179 Console.WriteLine($"User @{invitedUserName} already left chat {chat_id} and I can't add it");
180 }
181 }
182 }
183 public async Task DeleteUserFromChat(long chat_id, string deletedUserName)
184 {
185 var result = await _telegramClient.Contacts_ResolveUsername(deletedUserName);
186 await _telegramClient.DeleteChatUser(new InputPeerChat(chat_id), result.User);
187 }
188 public async Task SendMessageToChat(long chat_id, string message, int? topicId = null)
189 {
190 var peer = await GetInputPeer(chat_id);
191 string html = TelegramFormatHelper.MdToTelegramHtml(message);
192 MessageEntity[]? entities = _telegramClient.HtmlToEntities(ref html);
193 if (topicId is int topId)
194 await _telegramClient.SendMessageAsync(peer, html, reply_to_msg_id: topId, entities: entities);
195 else
196 await _telegramClient.SendMessageAsync(peer, html, entities: entities);
197 }
198
199 /// <summary>
200 /// Edit an existing message in a chat. Only messages sent by this account can be edited.
201 /// </summary>
202 public async Task EditMessage(long chatId, int messageId, string newText)
203 {
204 var peer = await GetInputPeer(chatId);
205 string html = TelegramFormatHelper.MdToTelegramHtml(newText);
206 MessageEntity[]? entities = _telegramClient.HtmlToEntities(ref html);
207 const int flagMessage = 1 << 11;
208 const int flagEntities = 1 << 15; // optional entities
209 var req = new TL.Methods.Messages_EditMessage
210 {
211 flags = (TL.Methods.Messages_EditMessage.Flags)(flagMessage | flagEntities),
212 peer = peer,
213 id = messageId,
214 message = html,
215 entities = entities
216 };
217 await _telegramClient.Invoke(req);
218 }
219
220 /// <summary>
221 /// Delete a message in a chat. Only messages sent by this account (or with admin rights in channels).
222 /// </summary>
223 public async Task DeleteMessage(long chatId, int messageId)
224 {
225 var peer = await GetInputPeer(chatId);
226 await _telegramClient.DeleteMessages(peer, [messageId]);
227 }
228
229 public async Task SendMessageToUser(string targetUserName, string message)
230 {
231 var result = await _telegramClient.Contacts_ResolveUsername(targetUserName);
232 string html = TelegramFormatHelper.MdToTelegramHtml(message);
233 MessageEntity[]? entities = _telegramClient.HtmlToEntities(ref html);
234 try
235 {
236 await _telegramClient.SendMessageAsync(result.User, html, entities: entities);
237 }
238 catch (RpcException e)
239 {
240 Console.WriteLine($"I have a trouble {e.Message} when send message to @{targetUserName}");
241 }
242 }
243 /// <summary>
244 /// Re-sends each of our messages in the chat through MD→HTML so they get formatting (bold, code, etc.).
245 /// Only messages sent by the current account are edited. Uses the last page of history.
246 /// </summary>
247 public async Task<int> UpdateOldMessagesInChat(long chatId, int? topicId = null)
248 {
249 long? ourUserId = _telegramClient.User?.id;
250 if (ourUserId == null)
251 return 0;
252 var messages = await GetMessagesFromChat(chatId, topicId);
253 var ours = messages.Where(m => m.from_id is PeerUser pu && pu.user_id == ourUserId && !string.IsNullOrEmpty(m.message)).ToList();
254 int edited = 0;
255 foreach (var msg in ours)
256 {
257 try
258 {
259 await EditMessage(chatId, msg.id, msg.message ?? "").ConfigureAwait(false);
260 edited++;
261 }
262 catch (RpcException)
263 {
264 // skip failed (e.g. message too old, no change, etc.)
265 }
266 }
267 return edited;
268 }
269
270 public async Task<IEnumerable<Message>> GetMessagesFromChat(long chat_id, int? topicId = null)
271 {
272 var peer = await GetInputPeer(chat_id);
273 var history = await _telegramClient.Messages_GetHistory(peer);
274 var messages = history.Messages.OfType<Message>();
275 if (topicId is int topId)
276 messages = messages.Where(m => m.reply_to?.reply_to_top_id == topId || m.reply_to?.reply_to_msg_id == topId || (m.reply_to == null && topId == 0));
277 return messages;
278 }
279
280 /// <summary>
281 /// Get messages as DTOs for JSON output (MCP / scripting).
282 /// </summary>
283 public async Task<IReadOnlyList<ChatMessageDto>> GetMessagesFromChatAsDtos(long chatId, int? topicId = null)
284 {
285 var messages = await GetMessagesFromChat(chatId, topicId);
286 var dtos = new List<ChatMessageDto>();
287 foreach (var msg in messages)
288 {
289 long? fromId = msg.from_id is PeerUser pu ? pu.user_id : null;
290 int? replyTo = msg.reply_to?.reply_to_msg_id;
291 int? topId = msg.reply_to?.reply_to_top_id ?? replyTo;
292 var dateUnix = (int)((DateTimeOffset)DateTime.SpecifyKind(msg.date, DateTimeKind.Utc)).ToUnixTimeSeconds();
293 dtos.Add(new ChatMessageDto
294 {
295 Id = msg.id,
296 Date = dateUnix,
297 FromUserId = fromId,
298 Text = msg.message ?? "",
299 ReplyToMsgId = replyTo,
300 TopicId = topId
301 });
302 }
303 return dtos;
304 }
305 public async Task DownloadMediaFromMessage(Message message, string mediaFolder)
306 {
307 var d = (MessageMediaDocument) message.media;
308
309 string directory = Path.Combine(Directory.GetCurrentDirectory(), mediaFolder);
310 if (!Directory.Exists(directory))
311 {
312 Directory.CreateDirectory(directory);
313 }
314
315 using var outStr = new FileStream(Path.Combine(directory, (d.document as Document).Filename), FileMode.OpenOrCreate);
316 _ = await _telegramClient.DownloadFileAsync(d.document as Document, outStr);
317 }
318
319 }
320}
321
View only · write via MCP/CIDE