1 # irgramd: IRC-Telegram gateway 2 # telegram.py: Interface to Telethon Telegram library 3 # 4 # Copyright (c) 2019 Peter Bui <pbui@bx612.space> 5 # Copyright (c) 2020-2024 E. Bosch <presidev@AT@gmail.com> 6 # 7 # Use of this source code is governed by a MIT style license that 8 # can be found in the LICENSE file included in this project. 9 10 import logging 11 import os 12 import re 13 import aioconsole 14 import asyncio 15 import collections 16 import telethon 17 from telethon import types as tgty, utils as tgutils 18 from telethon.tl.functions.messages import GetMessagesReactionsRequest 19 20 # Local modules 21 22 from include import CHAN_MAX_LENGTH, NICK_MAX_LENGTH 23 from irc import IRCUser 24 from utils import sanitize_filename, add_filename, is_url_equiv, extract_url, get_human_size, get_human_duration 25 from utils import get_highlighted, fix_braces, format_timestamp, pretty, current_date 26 import emoji2emoticon as e 27 28 # Test IP table 29 30 TEST_IPS = { 1: '149.154.175.10', 31 2: '149.154.167.40', 32 3: '149.154.175.117', 33 } 34 35 # Telegram 36 37 class TelegramHandler(object): 38 def __init__(self, irc, settings): 39 self.logger = logging.getLogger() 40 self.config_dir = settings['config_dir'] 41 self.cache_dir = settings['cache_dir'] 42 self.download = settings['download_media'] 43 self.notice_size = settings['download_notice'] * 1048576 44 self.media_dir = settings['media_dir'] 45 self.media_url = settings['media_url'] 46 self.upload_dir = settings['upload_dir'] 47 self.api_id = settings['api_id'] 48 self.api_hash = settings['api_hash'] 49 self.phone = settings['phone'] 50 self.test = settings['test'] 51 self.test_dc = settings['test_datacenter'] 52 self.test_ip = settings['test_host'] if settings['test_host'] else TEST_IPS[self.test_dc] 53 self.test_port = settings['test_port'] 54 self.ask_code = settings['ask_code'] 55 self.quote_len = settings['quote_length'] 56 self.hist_fmt = settings['hist_timestamp_format'] 57 self.timezone = settings['timezone'] 58 self.geo_url = settings['geo_url'] 59 if not settings['emoji_ascii']: 60 e.emo = {} 61 self.media_cn = 0 62 self.irc = irc 63 self.authorized = False 64 self.id = None 65 self.tg_username = None 66 self.channels_date = {} 67 self.mid = mesg_id('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#$%+./_~') 68 self.webpending = {} 69 self.refwd_me = False 70 self.cache = collections.OrderedDict() 71 self.volatile_cache = collections.OrderedDict() 72 self.prev_id = {} 73 self.sorted_len_usernames = [] 74 self.last_reaction = None 75 # Set event to be waited by irc.check_telegram_auth() 76 self.auth_checked = asyncio.Event() 77 78 async def initialize_telegram(self): 79 # Setup media folder 80 self.telegram_media_dir = os.path.expanduser(self.media_dir or os.path.join(self.cache_dir, 'media')) 81 if not os.path.exists(self.telegram_media_dir): 82 os.makedirs(self.telegram_media_dir) 83 84 # Setup upload folder 85 self.telegram_upload_dir = os.path.expanduser(self.upload_dir or os.path.join(self.cache_dir, 'upload')) 86 if not os.path.exists(self.telegram_upload_dir): 87 os.makedirs(self.telegram_upload_dir) 88 89 # Setup session folder 90 self.telegram_session_dir = os.path.join(self.config_dir, 'session') 91 if not os.path.exists(self.telegram_session_dir): 92 os.makedirs(self.telegram_session_dir) 93 94 # Construct Telegram client 95 if self.test: 96 self.telegram_client = telethon.TelegramClient(None, self.api_id, self.api_hash) 97 self.telegram_client.session.set_dc(self.test_dc, self.test_ip, self.test_port) 98 else: 99 telegram_session = os.path.join(self.telegram_session_dir, 'telegram') 100 self.telegram_client = telethon.TelegramClient(telegram_session, self.api_id, self.api_hash) 101 102 # Initialize Telegram ID to IRC nick mapping 103 self.tid_to_iid = {} 104 105 # Register Telegram callbacks 106 callbacks = ( 107 (self.handle_telegram_message , telethon.events.NewMessage), 108 (self.handle_raw , telethon.events.Raw), 109 (self.handle_telegram_chat_action, telethon.events.ChatAction), 110 (self.handle_telegram_deleted , telethon.events.MessageDeleted), 111 (self.handle_telegram_edited , telethon.events.MessageEdited), 112 ) 113 for handler, event in callbacks: 114 self.telegram_client.add_event_handler(handler, event) 115 116 # Start Telegram client 117 if self.test: 118 await self.telegram_client.start(self.phone, code_callback=lambda: str(self.test_dc) * 5) 119 else: 120 await self.telegram_client.connect() 121 122 while not await self.telegram_client.is_user_authorized(): 123 self.logger.info('Telegram account not authorized') 124 await self.telegram_client.send_code_request(self.phone) 125 self.auth_checked.set() 126 if not self.ask_code: 127 return 128 self.logger.info('You must provide the Login code that Telegram will ' 129 'sent you via SMS or another connected client') 130 code = await aioconsole.ainput('Login code: ') 131 try: 132 await self.telegram_client.sign_in(code=code) 133 except: 134 pass 135 136 await self.continue_auth() 137 138 async def continue_auth(self): 139 self.logger.info('Telegram account authorized') 140 self.authorized = True 141 self.auth_checked.set() 142 await self.init_mapping() 143 144 async def init_mapping(self): 145 # Update IRC <-> Telegram mapping 146 tg_user = await self.telegram_client.get_me() 147 self.id = tg_user.id 148 self.tg_username = self.get_telegram_nick(tg_user) 149 self.add_sorted_len_usernames(self.tg_username) 150 self.set_ircuser_from_telegram(tg_user) 151 async for dialog in self.telegram_client.iter_dialogs(): 152 chat = dialog.entity 153 if isinstance(chat, tgty.User): 154 self.set_ircuser_from_telegram(chat) 155 else: 156 await self.set_irc_channel_from_telegram(chat) 157 158 def set_ircuser_from_telegram(self, user): 159 if user.id not in self.tid_to_iid: 160 tg_nick = self.get_telegram_nick(user) 161 tg_ni = tg_nick.lower() 162 if not user.is_self: 163 irc_user = IRCUser(None, ('Telegram',''), tg_nick, user.id, self.get_telegram_display_name(user)) 164 self.irc.users[tg_ni] = irc_user 165 self.add_sorted_len_usernames(tg_ni) 166 self.tid_to_iid[user.id] = tg_nick 167 self.irc.iid_to_tid[tg_ni] = user.id 168 else: 169 tg_nick = self.tid_to_iid[user.id] 170 return tg_nick 171 172 async def set_irc_channel_from_telegram(self, chat): 173 channel = self.get_telegram_channel(chat) 174 self.tid_to_iid[chat.id] = channel 175 chan = channel.lower() 176 self.irc.iid_to_tid[chan] = chat.id 177 self.irc.irc_channels[chan] = set() 178 # Add users from the channel 179 async for user in self.telegram_client.iter_participants(chat.id): 180 user_nick = self.set_ircuser_from_telegram(user) 181 if not user.is_self: 182 self.irc.irc_channels[chan].add(user_nick) 183 # Add admin users as ops in irc 184 if isinstance(user.participant, tgty.ChatParticipantAdmin): 185 self.irc.irc_channels_ops[chan].add(user_nick) 186 # Add creator users as founders in irc 187 elif isinstance(user.participant, tgty.ChatParticipantCreator): 188 self.irc.irc_channels_founder[chan].add(user_nick) 189 190 def get_telegram_nick(self, user): 191 nick = (user.username 192 or self.get_telegram_display_name(user) 193 or str(user.id)) 194 nick = nick[:NICK_MAX_LENGTH] 195 while nick in self.irc.iid_to_tid: 196 nick += '_' 197 return nick 198 199 def get_telegram_display_name(self, user): 200 name = telethon.utils.get_display_name(user) 201 name = name.replace(' ', '_') 202 return name 203 204 async def get_telegram_display_name_me(self): 205 tg_user = await self.telegram_client.get_me() 206 return self.get_telegram_display_name(tg_user) 207 208 def get_telegram_channel(self, chat): 209 chan = '#' + chat.title.replace(' ', '-').replace(',', '-') 210 while chan.lower() in self.irc.iid_to_tid: 211 chan += '_' 212 return chan 213 214 def get_irc_user_from_telegram(self, tid): 215 nick = self.tid_to_iid[tid] 216 if nick == self.tg_username: return None 217 return self.irc.users[nick.lower()] 218 219 def get_irc_name_from_telegram_id(self, tid): 220 if tid in self.tid_to_iid.keys(): 221 name_in_irc = self.tid_to_iid[tid] 222 else: 223 name_in_irc = '<Unknown>' 224 return name_in_irc 225 226 async def get_irc_name_from_telegram_forward(self, fwd, saved): 227 from_id = fwd.saved_from_peer if saved else fwd.from_id 228 if from_id is None: 229 # telegram user has privacy options to show only the name 230 # or was a broadcast from a channel (no user) 231 name = fwd.from_name 232 else: 233 peer_id, type = self.get_peer_id_and_type(from_id) 234 if type == 'user': 235 try: 236 user = self.get_irc_user_from_telegram(peer_id) 237 except: 238 name = str(peer_id) 239 else: 240 if user is None: 241 name = '{}' 242 self.refwd_me = True 243 else: 244 name = user.irc_nick 245 else: 246 try: 247 name = await self.get_irc_channel_from_telegram_id(peer_id) 248 except: 249 name = '' 250 return name 251 252 async def get_irc_nick_from_telegram_id(self, tid, entity=None): 253 if tid not in self.tid_to_iid: 254 user = entity or await self.telegram_client.get_entity(tid) 255 nick = self.get_telegram_nick(user) 256 self.tid_to_iid[tid] = nick 257 self.irc.iid_to_tid[nick] = tid 258 259 return self.tid_to_iid[tid] 260 261 async def get_irc_channel_from_telegram_id(self, tid, entity=None): 262 rtid, type = tgutils.resolve_id(tid) 263 if rtid not in self.tid_to_iid: 264 chat = entity or await self.telegram_client.get_entity(tid) 265 channel = self.get_telegram_channel(chat) 266 self.tid_to_iid[rtid] = channel 267 self.irc.iid_to_tid[channel] = rtid 268 269 return self.tid_to_iid[rtid] 270 271 async def get_telegram_channel_participants(self, tid): 272 channel = self.tid_to_iid[tid] 273 nicks = [] 274 async for user in self.telegram_client.iter_participants(tid): 275 user_nick = await self.get_irc_nick_from_telegram_id(user.id, user) 276 277 nicks.append(user_nick) 278 self.irc.irc_channels[channel].add(user_nick) 279 280 return nicks 281 282 async def get_telegram_idle(self, irc_nick, tid=None): 283 if self.irc.users[irc_nick].is_service: 284 return None 285 tid = self.get_tid(irc_nick, tid) 286 user = await self.telegram_client.get_entity(tid) 287 if isinstance(user.status,tgty.UserStatusRecently) or \ 288 isinstance(user.status,tgty.UserStatusOnline): 289 idle = 0 290 elif isinstance(user.status,tgty.UserStatusOffline): 291 last = user.status.was_online 292 current = current_date() 293 idle = int((current - last).total_seconds()) 294 elif isinstance(user.status,tgty.UserStatusLastWeek): 295 idle = 604800 296 elif isinstance(user.status,tgty.UserStatusLastMonth): 297 idle = 2678400 298 else: 299 idle = None 300 return idle 301 302 async def get_channel_topic(self, channel, entity_cache): 303 tid = self.get_tid(channel) 304 # entity_cache should be a list to be a persistent and by reference value 305 if entity_cache[0]: 306 entity = entity_cache[0] 307 else: 308 entity = await self.telegram_client.get_entity(tid) 309 entity_cache[0] = entity 310 entity_type = self.get_entity_type(entity, format='long') 311 return 'Telegram ' + entity_type + ' ' + str(tid) + ': ' + entity.title 312 313 async def get_channel_creation(self, channel, entity_cache): 314 tid = self.get_tid(channel) 315 if tid in self.channels_date.keys(): 316 timestamp = self.channels_date[tid] 317 else: 318 # entity_cache should be a list to be a persistent and by reference value 319 if entity_cache[0]: 320 entity = entity_cache[0] 321 else: 322 entity = await self.telegram_client.get_entity(tid) 323 entity_cache[0] = entity 324 timestamp = entity.date.timestamp() 325 self.channels_date[tid] = timestamp 326 return int(timestamp) 327 328 def get_tid(self, irc_item, tid=None): 329 it = irc_item.lower() 330 if tid: 331 pass 332 elif it in self.irc.iid_to_tid: 333 tid = self.irc.iid_to_tid[it] 334 else: 335 tid = self.id 336 return tid 337 338 def get_entity_type(self, entity, format): 339 if isinstance(entity, tgty.User): 340 short = long = 'User' 341 elif isinstance(entity, tgty.Chat): 342 short = 'Chat' 343 long = 'Chat/Basic Group' 344 elif isinstance(entity, tgty.Channel): 345 if entity.broadcast: 346 short = 'Broad' 347 long = 'Broadcast Channel' 348 elif entity.megagroup: 349 short = 'Mega' 350 long = 'Super/Megagroup Channel' 351 elif entity.gigagroup: 352 short = 'Giga' 353 long = 'Broadcast Gigagroup Channel' 354 355 return short if format == 'short' else long 356 357 def get_peer_id_and_type(self, peer): 358 if isinstance(peer, tgty.PeerChannel): 359 id = peer.channel_id 360 type = 'chan' 361 elif isinstance(peer, tgty.PeerChat): 362 id = peer.chat_id 363 type = 'chan' 364 elif isinstance(peer, tgty.PeerUser): 365 id = peer.user_id 366 type = 'user' 367 else: 368 id = peer 369 type = '' 370 return id, type 371 372 async def is_bot(self, irc_nick, tid=None): 373 user = self.irc.users[irc_nick] 374 if user.stream or user.is_service: 375 bot = False 376 else: 377 bot = user.bot 378 if bot == None: 379 tid = self.get_tid(irc_nick, tid) 380 tg_user = await self.telegram_client.get_entity(tid) 381 bot = tg_user.bot 382 user.bot = bot 383 return bot 384 385 async def edition_case(self, msg): 386 def msg_edited(m): 387 return m.id in self.cache and \ 388 ( m.message != self.cache[m.id]['text'] 389 or m.media != self.cache[m.id]['media'] 390 ) 391 async def get_reactions(m): 392 react = await self.telegram_client(GetMessagesReactionsRequest(m.peer_id, id=[m.id])) 393 updates = react.updates 394 r = next((x for x in updates if type(x) is tgty.UpdateMessageReactions), None) 395 return r.reactions.recent_reactions if r else None 396 397 react = None 398 if msg.reactions is None: 399 case = 'edition' 400 elif (reactions := await get_reactions(msg)) is None: 401 if msg_edited(msg): 402 case = 'edition' 403 else: 404 case = 'react-del' 405 elif react := max(reactions, key=lambda y: y.date): 406 case = 'react-add' 407 else: 408 if msg_edited(msg): 409 case = 'edition' 410 else: 411 case = 'react-del' 412 react = None 413 return case, react 414 415 def to_cache(self, id, mid, message, proc_message, user, chan, media): 416 self.limit_cache(self.cache) 417 self.cache[id] = { 418 'mid': mid, 419 'text': message, 420 'rendered_text': proc_message, 421 'user': user, 422 'channel': chan, 423 'media': media, 424 } 425 426 def to_volatile_cache(self, prev_id, id, ev, user, chan, date): 427 if chan in prev_id: 428 prid = prev_id[chan] if chan else prev_id[user] 429 self.limit_cache(self.volatile_cache) 430 elem = { 431 'id': id, 432 'rendered_event': ev, 433 'user': user, 434 'channel': chan, 435 'date': date, 436 } 437 if prid not in self.volatile_cache: 438 self.volatile_cache[prid] = [elem] 439 else: 440 self.volatile_cache[prid].append(elem) 441 442 def limit_cache(self, cache): 443 if len(cache) >= 10000: 444 cache.popitem(last=False) 445 446 def replace_mentions(self, text, me_nick='', received=True): 447 # For received replace @mention to ~mention~ 448 # For sent replace mention: to @mention 449 rargs = {} 450 def repl_mentioned(text, me_nick, received, mark, repl_pref, repl_suff): 451 new_text = text 452 453 for user in self.sorted_len_usernames: 454 if user == self.tg_username: 455 if me_nick: 456 username = me_nick 457 else: 458 continue 459 else: 460 username = self.irc.users[user].irc_nick 461 462 if received: 463 mention = mark + user 464 mention_case = mark + username 465 else: # sent 466 mention = user + mark 467 mention_case = username + mark 468 replcmnt = repl_pref + username + repl_suff 469 470 # Start of the text 471 for ment in (mention, mention_case): 472 if new_text.startswith(ment): 473 new_text = new_text.replace(ment, replcmnt, 1) 474 475 # Next words (with space as separator) 476 mention = ' ' + mention 477 mention_case = ' ' + mention_case 478 replcmnt = ' ' + replcmnt 479 new_text = new_text.replace(mention, replcmnt).replace(mention_case, replcmnt) 480 481 return new_text 482 483 if received: 484 mark = '@' 485 rargs['repl_pref'] = '~' 486 rargs['repl_suff'] = '~' 487 else: # sent 488 mark = ':' 489 rargs['repl_pref'] = '@' 490 rargs['repl_suff'] = '' 491 492 if text.find(mark) != -1: 493 text_replaced = repl_mentioned(text, me_nick, received, mark, **rargs) 494 else: 495 text_replaced = text 496 return text_replaced 497 498 def filters(self, text): 499 filtered = e.replace_mult(text, e.emo) 500 filtered = self.replace_mentions(filtered) 501 return filtered 502 503 def add_sorted_len_usernames(self, username): 504 self.sorted_len_usernames.append(username) 505 self.sorted_len_usernames.sort(key=lambda k: len(k), reverse=True) 506 507 def format_reaction(self, msg, message_rendered, edition_case, reaction): 508 react_quote_len = self.quote_len * 2 509 if len(message_rendered) > react_quote_len: 510 text_old = '{}...'.format(message_rendered[:react_quote_len]) 511 text_old = fix_braces(text_old) 512 else: 513 text_old = message_rendered 514 515 if edition_case == 'react-add': 516 user = self.get_irc_user_from_telegram(reaction.peer_id.user_id) 517 emoji = reaction.reaction.emoticon 518 react_action = '+' 519 react_icon = e.emo[emoji] if emoji in e.emo else emoji 520 elif edition_case == 'react-del': 521 user = self.get_irc_user_from_telegram(msg.sender_id) 522 react_action = '-' 523 react_icon = '' 524 return text_old, '{}{}'.format(react_action, react_icon), user 525 526 async def handle_telegram_edited(self, event): 527 self.logger.debug('Handling Telegram Message Edited: %s', pretty(event)) 528 529 id = event.message.id 530 mid = self.mid.num_to_id_offset(event.message.peer_id, id) 531 fmid = '[{}]'.format(mid) 532 message = self.filters(event.message.message) 533 message_rendered = await self.render_text(event.message, mid, upd_to_webpend=None) 534 535 edition_case, reaction = await self.edition_case(event.message) 536 if edition_case == 'edition': 537 action = 'Edited' 538 user = self.get_irc_user_from_telegram(event.sender_id) 539 if id in self.cache: 540 t = self.filters(self.cache[id]['text']) 541 rt = self.cache[id]['rendered_text'] 542 543 ht, is_ht = get_highlighted(t, message) 544 else: 545 rt = fmid 546 is_ht = False 547 548 if is_ht: 549 edition_react = ht 550 text_old = fmid 551 else: 552 edition_react = message 553 text_old = rt 554 if user is None: 555 self.refwd_me = True 556 557 # Reactions 558 else: 559 if reaction: 560 if self.last_reaction == reaction.date: 561 return 562 self.last_reaction = reaction.date 563 action = 'React' 564 text_old, edition_react, user = self.format_reaction(event.message, message_rendered, edition_case, reaction) 565 566 text = '|{} {}| {}'.format(action, text_old, edition_react) 567 568 chan = await self.relay_telegram_message(event, user, text) 569 570 self.to_cache(id, mid, message, message_rendered, user, chan, event.message.media) 571 self.to_volatile_cache(self.prev_id, id, text, user, chan, current_date()) 572 573 async def handle_next_reaction(self, event): 574 self.logger.debug('Handling Telegram Next Reaction (2nd, 3rd, ...): %s', pretty(event)) 575 576 reactions = event.reactions.recent_reactions 577 react = max(reactions, key=lambda y: y.date) 578 579 if self.last_reaction != react.date: 580 self.last_reaction = react.date 581 id = event.msg_id 582 msg = await self.telegram_client.get_messages(entity=event.peer, ids=id) 583 mid = self.mid.num_to_id_offset(msg.peer_id, id) 584 message = self.filters(msg.message) 585 message_rendered = await self.render_text(msg, mid, upd_to_webpend=None) 586 587 text_old, edition_react, user = self.format_reaction(msg, message_rendered, edition_case='react-add', reaction=react) 588 589 text = '|React {}| {}'.format(text_old, edition_react) 590 591 chan = await self.relay_telegram_message(msg, user, text) 592 593 self.to_cache(id, mid, message, message_rendered, user, chan, msg.media) 594 self.to_volatile_cache(self.prev_id, id, text, user, chan, current_date()) 595 596 async def handle_telegram_deleted(self, event): 597 self.logger.debug('Handling Telegram Message Deleted: %s', pretty(event)) 598 599 for deleted_id in event.original_update.messages: 600 if deleted_id in self.cache: 601 recovered_text = self.cache[deleted_id]['rendered_text'] 602 text = '|Deleted| {}'.format(recovered_text) 603 user = self.cache[deleted_id]['user'] 604 chan = self.cache[deleted_id]['channel'] 605 await self.relay_telegram_message(message=None, user=user, text=text, channel=chan) 606 self.to_volatile_cache(self.prev_id, deleted_id, text, user, chan, current_date()) 607 else: 608 text = 'Message id {} deleted not in cache'.format(deleted_id) 609 await self.relay_telegram_private_message(self.irc.service_user, text) 610 611 async def handle_raw(self, update): 612 self.logger.debug('Handling Telegram Raw Event: %s', pretty(update)) 613 614 if isinstance(update, tgty.UpdateWebPage) and isinstance(update.webpage, tgty.WebPage): 615 message = self.webpending.pop(update.webpage.id, None) 616 if message: 617 await self.handle_telegram_message(event=None, message=message, upd_to_webpend=update.webpage) 618 619 elif isinstance(update, tgty.UpdateMessageReactions): 620 await self.handle_next_reaction(update) 621 622 async def handle_telegram_message(self, event, message=None, upd_to_webpend=None, history=False): 623 self.logger.debug('Handling Telegram Message: %s', pretty(event or message)) 624 625 msg = event.message if event else message 626 627 user = self.get_irc_user_from_telegram(msg.sender_id) 628 mid = self.mid.num_to_id_offset(msg.peer_id, msg.id) 629 text = await self.render_text(msg, mid, upd_to_webpend, user) 630 text_send = self.set_history_timestamp(text, history, msg.date, msg.action) 631 chan = await self.relay_telegram_message(msg, user, text_send) 632 await self.history_search_volatile(history, msg.id) 633 634 self.to_cache(msg.id, mid, msg.message, text, user, chan, msg.media) 635 peer = chan if chan else user 636 self.prev_id[peer] = msg.id 637 638 self.refwd_me = False 639 640 async def render_text(self, message, mid, upd_to_webpend, user=None): 641 if upd_to_webpend: 642 text = await self.handle_webpage(upd_to_webpend, message, mid) 643 elif message.media: 644 text = await self.handle_telegram_media(message, user, mid) 645 else: 646 text = message.message 647 648 if message.action: 649 final_text = await self.handle_telegram_action(message, mid) 650 return final_text 651 elif message.is_reply: 652 refwd_text = await self.handle_telegram_reply(message) 653 elif message.forward: 654 refwd_text = await self.handle_telegram_forward(message) 655 else: 656 refwd_text = '' 657 658 target_mine = self.handle_target_mine(message.peer_id, user) 659 660 final_text = '[{}] {}{}{}'.format(mid, target_mine, refwd_text, text) 661 final_text = self.filters(final_text) 662 return final_text 663 664 def set_history_timestamp(self, text, history, date, action): 665 if history and self.hist_fmt: 666 timestamp = format_timestamp(self.hist_fmt, self.timezone, date) 667 if action: 668 res = '{} {}'.format(text, timestamp) 669 else: 670 res = '{} {}'.format(timestamp, text) 671 else: 672 res = text 673 return res 674 675 async def history_search_volatile(self, history, id): 676 if history: 677 if id in self.volatile_cache: 678 for item in self.volatile_cache[id]: 679 user = item['user'] 680 text = item['rendered_event'] 681 chan = item['channel'] 682 date = item['date'] 683 text_send = self.set_history_timestamp(text, history=True, date=date, action=False) 684 await self.relay_telegram_message(None, user, text_send, chan) 685 686 async def relay_telegram_message(self, message, user, text, channel=None): 687 private = (message and message.is_private) or (not message and not channel) 688 action = (message and message.action) 689 if private: 690 await self.relay_telegram_private_message(user, text, action) 691 chan = None 692 else: 693 chan = await self.relay_telegram_channel_message(message, user, text, channel, action) 694 return chan 695 696 async def relay_telegram_private_message(self, user, message, action=None): 697 self.logger.debug('Relaying Telegram Private Message: %s, %s', user, message) 698 699 if action: 700 await self.irc.send_action(user, None, message) 701 else: 702 await self.irc.send_msg(user, None, message) 703 704 async def relay_telegram_channel_message(self, message, user, text, channel, action): 705 if message: 706 entity = await message.get_chat() 707 chan = await self.get_irc_channel_from_telegram_id(message.chat_id, entity) 708 else: 709 chan = channel 710 711 self.logger.debug('Relaying Telegram Channel Message: %s, %s', chan, text) 712 713 if action: 714 await self.irc.send_action(user, chan, text) 715 else: 716 await self.irc.send_msg(user, chan, text) 717 718 return chan 719 720 async def handle_telegram_chat_action(self, event): 721 self.logger.debug('Handling Telegram Chat Action: %s', pretty(event)) 722 723 try: 724 tid = event.action_message.to_id.channel_id 725 except AttributeError: 726 tid = event.action_message.to_id.chat_id 727 finally: 728 irc_channel = await self.get_irc_channel_from_telegram_id(tid) 729 await self.get_telegram_channel_participants(tid) 730 731 try: # Join Chats 732 irc_nick = await self.get_irc_nick_from_telegram_id(event.action_message.action.users[0]) 733 except (IndexError, AttributeError): 734 try: # Kick 735 irc_nick = await self.get_irc_nick_from_telegram_id(event.action_message.action.user_id) 736 except (IndexError, AttributeError): # Join Channels 737 irc_nick = await self.get_irc_nick_from_telegram_id(event.action_message.sender_id) 738 739 if event.user_added or event.user_joined: 740 await self.irc.join_irc_channel(irc_nick, irc_channel, full_join=False) 741 elif event.user_kicked or event.user_left: 742 await self.irc.part_irc_channel(irc_nick, irc_channel) 743 744 async def join_all_telegram_channels(self): 745 async for dialog in self.telegram_client.iter_dialogs(): 746 chat = dialog.entity 747 if not isinstance(chat, tgty.User): 748 channel = self.get_telegram_channel(chat) 749 self.tid_to_iid[chat.id] = channel 750 self.irc.iid_to_tid[channel] = chat.id 751 await self.irc.join_irc_channel(self.irc.irc_nick, channel, full_join=True) 752 753 async def handle_telegram_action(self, message, mid): 754 if isinstance(message.action, tgty.MessageActionPinMessage): 755 replied = await message.get_reply_message() 756 cid = self.mid.num_to_id_offset(replied.peer_id, replied.id) 757 action_text = 'has pinned message [{}]'.format(cid) 758 elif isinstance(message.action, tgty.MessageActionChatEditPhoto): 759 _, media_type = self.scan_photo_attributes(message.action.photo) 760 photo_url = await self.download_telegram_media(message, mid) 761 action_text = 'has changed chat [{}] {}'.format(media_type, photo_url) 762 else: 763 action_text = '' 764 return action_text 765 766 async def handle_telegram_reply(self, message): 767 space = ' ' 768 trunc = '' 769 replied = await message.get_reply_message() 770 if replied: 771 replied_msg = replied.message 772 cid = self.mid.num_to_id_offset(replied.peer_id, replied.id) 773 replied_user = self.get_irc_user_from_telegram(replied.sender_id) 774 else: 775 replied_id = message.reply_to.reply_to_msg_id 776 cid = self.mid.num_to_id_offset(message.peer_id, replied_id) 777 if replied_id in self.cache: 778 text = self.cache[replied_id]['text'] 779 replied_user = self.cache[replied_id]['user'] 780 sp = ' ' 781 else: 782 text = '' 783 replied_user = '' 784 sp = '' 785 replied_msg = '|Deleted|{}{}'.format(sp, text) 786 if not replied_msg: 787 replied_msg = '' 788 space = '' 789 elif len(replied_msg) > self.quote_len: 790 replied_msg = replied_msg[:self.quote_len] 791 trunc = '...' 792 if replied_user is None: 793 replied_nick = '{}' 794 self.refwd_me = True 795 elif replied_user == '': 796 replied_nick = '' 797 else: 798 replied_nick = replied_user.irc_nick 799 800 return '|Re {}: [{}]{}{}{}| '.format(replied_nick, cid, space, replied_msg, trunc) 801 802 async def handle_telegram_forward(self, message): 803 space = space2 = ' ' 804 if not (forwarded_peer_name := await self.get_irc_name_from_telegram_forward(message.fwd_from, saved=False)): 805 space = '' 806 saved_peer_name = await self.get_irc_name_from_telegram_forward(message.fwd_from, saved=True) 807 if saved_peer_name and saved_peer_name != forwarded_peer_name: 808 secondary_name = saved_peer_name 809 else: 810 # if it's from me I want to know who was the destination of a message (user) 811 if self.refwd_me and (saved_from_peer := message.fwd_from.saved_from_peer) is not None: 812 secondary_name = self.get_irc_user_from_telegram(saved_from_peer.user_id).irc_nick 813 else: 814 secondary_name = '' 815 space2 = '' 816 817 return '|Fwd{}{}{}{}| '.format(space, forwarded_peer_name, space2, secondary_name) 818 819 async def handle_telegram_media(self, message, user, mid): 820 caption = ' | {}'.format(message.message) if message.message else '' 821 to_download = True 822 media_url_or_data = '' 823 size = 0 824 filename = None 825 826 def scan_doc_attributes(document): 827 attrib_file = attrib_av = filename = None 828 size = document.size 829 h_size = get_human_size(size) 830 for x in document.attributes: 831 if isinstance(x, tgty.DocumentAttributeVideo) or isinstance(x, tgty.DocumentAttributeAudio): 832 attrib_av = x 833 if isinstance(x, tgty.DocumentAttributeFilename): 834 attrib_file = x 835 filename = attrib_file.file_name if attrib_file else None 836 837 return size, h_size, attrib_av, filename 838 839 if isinstance(message.media, tgty.MessageMediaWebPage): 840 to_download = False 841 if isinstance(message.media.webpage, tgty.WebPage): 842 # web 843 return await self.handle_webpage(message.media.webpage, message, mid) 844 elif isinstance(message.media.webpage, tgty.WebPagePending): 845 media_type = 'webpending' 846 media_url_or_data = message.message 847 caption = '' 848 self.webpending[message.media.webpage.id] = message 849 else: 850 media_type = 'webunknown' 851 media_url_or_data = message.message 852 caption = '' 853 elif message.photo: 854 size, media_type = self.scan_photo_attributes(message.media.photo) 855 elif message.audio: 856 size, h_size, attrib_audio, filename = scan_doc_attributes(message.media.document) 857 dur = get_human_duration(attrib_audio.duration) if attrib_audio else '' 858 per = attrib_audio.performer or '' 859 tit = attrib_audio.title or '' 860 theme = ',{}/{}'.format(per, tit) if per or tit else '' 861 media_type = 'audio:{},{}{}'.format(h_size, dur, theme) 862 elif message.voice: 863 size, _, attrib_audio, filename = scan_doc_attributes(message.media.document) 864 dur = get_human_duration(attrib_audio.duration) if attrib_audio else '' 865 media_type = 'rec:{}'.format(dur) 866 elif message.video: 867 size, h_size, attrib_video, filename = scan_doc_attributes(message.media.document) 868 dur = get_human_duration(attrib_video.duration) if attrib_video else '' 869 media_type = 'video:{},{}'.format(h_size, dur) 870 elif message.video_note: media_type = 'videorec' 871 elif message.gif: media_type = 'anim' 872 elif message.sticker: media_type = 'sticker' 873 elif message.document: 874 size, h_size, _, filename = scan_doc_attributes(message.media.document) 875 media_type = 'file:{}'.format(h_size) 876 elif message.contact: 877 media_type = 'contact' 878 caption = '' 879 to_download = False 880 if message.media.first_name: 881 media_url_or_data += message.media.first_name + ' ' 882 if message.media.last_name: 883 media_url_or_data += message.media.last_name + ' ' 884 if message.media.phone_number: 885 media_url_or_data += message.media.phone_number 886 887 elif message.game: 888 media_type = 'game' 889 caption = '' 890 to_download = False 891 if message.media.game.title: 892 media_url_or_data = message.media.game.title 893 894 elif message.geo: 895 media_type = 'geo' 896 caption = '' 897 to_download = False 898 if self.geo_url: 899 geo_url = ' | ' + self.geo_url 900 else: 901 geo_url = '' 902 lat_long_template = 'lat: {lat}, long: {long}' + geo_url 903 media_url_or_data = lat_long_template.format(lat=message.media.geo.lat, long=message.media.geo.long) 904 905 elif message.invoice: 906 media_type = 'invoice' 907 caption = '' 908 to_download = False 909 media_url_or_data = '' 910 911 elif message.poll: 912 media_type = 'poll' 913 caption = '' 914 to_download = False 915 media_url_or_data = self.handle_poll(message.media.poll) 916 917 elif message.venue: 918 media_type = 'venue' 919 caption = '' 920 to_download = False 921 media_url_or_data = '' 922 else: 923 media_type = 'unknown' 924 caption = '' 925 to_download = False 926 media_url_or_data = message.message 927 928 if to_download: 929 relay_attr = (message, user, mid, media_type) 930 media_url_or_data = await self.download_telegram_media(message, mid, filename, size, relay_attr) 931 932 return self.format_media(media_type, media_url_or_data, caption) 933 934 def handle_poll(self, poll): 935 text = poll.question 936 for ans in poll.answers: 937 text += '\n* ' + ans.text 938 return text 939 940 def handle_target_mine(self, target, user): 941 # Add the target of messages sent by self user (me) 942 # received in other clients 943 target_id, target_type = self.get_peer_id_and_type(target) 944 if user is None and target_type == 'user' and target_id != self.id: 945 # self user^ 946 # as sender 947 irc_id = self.get_irc_name_from_telegram_id(target_id) 948 target_mine = '[T: {}] '.format(irc_id) 949 else: 950 target_mine = '' 951 return target_mine 952 953 async def handle_webpage(self, webpage, message, mid): 954 media_type = 'web' 955 logo = await self.download_telegram_media(message, mid) 956 if is_url_equiv(webpage.url, webpage.display_url): 957 url_data = webpage.url 958 else: 959 url_data = '{} | {}'.format(webpage.url, webpage.display_url) 960 if message: 961 # sometimes the 1st line of message contains the title, don't repeat it 962 message_line = message.message.splitlines()[0] 963 if message_line != webpage.title: 964 title = webpage.title 965 else: 966 title = '' 967 # extract the URL in the message, don't repeat it 968 message_url = extract_url(message.message) 969 if is_url_equiv(message_url, webpage.url): 970 if is_url_equiv(message_url, webpage.display_url): 971 media_url_or_data = message.message 972 else: 973 media_url_or_data = '{} | {}'.format(message.message, webpage.display_url) 974 else: 975 media_url_or_data = '{} | {}'.format(message.message, url_data) 976 else: 977 title = webpage.title 978 media_url_or_data = url_data 979 980 if title and logo: 981 caption = ' | {} | {}'.format(title, logo) 982 elif title: 983 caption = ' | {}'.format(title) 984 elif logo: 985 caption = ' | {}'.format(logo) 986 else: 987 caption = '' 988 989 return self.format_media(media_type, media_url_or_data, caption) 990 991 def format_media(self, media_type, media_url_or_data, caption): 992 return '[{}] {}{}'.format(media_type, media_url_or_data, caption) 993 994 def scan_photo_attributes(self, photo): 995 size = 0 996 sizes = photo.sizes 997 ph_size = sizes[-1] 998 if isinstance(ph_size, tgty.PhotoSizeProgressive): 999 size = ph_size.sizes[-1] 1000 else: 1001 for x in sizes: 1002 if isinstance(x, tgty.PhotoSize): 1003 if x.size > size: 1004 size = x.size 1005 ph_size = x 1006 if hasattr(ph_size, 'w') and hasattr(ph_size, 'h'): 1007 media_type = 'photo:{}x{}'.format(ph_size.w, ph_size.h) 1008 else: 1009 media_type = 'photo' 1010 1011 return size, media_type 1012 1013 async def download_telegram_media(self, message, mid, filename=None, size=0, relay_attr=None): 1014 if not self.download: 1015 return '' 1016 if filename: 1017 idd_file = add_filename(filename, mid) 1018 new_file = sanitize_filename(idd_file) 1019 new_path = os.path.join(self.telegram_media_dir, new_file) 1020 if os.path.exists(new_path): 1021 local_path = new_path 1022 else: 1023 await self.notice_downloading(size, relay_attr) 1024 local_path = await message.download_media(new_path) 1025 if not local_path: return '' 1026 else: 1027 await self.notice_downloading(size, relay_attr) 1028 local_path = await message.download_media(self.telegram_media_dir) 1029 if not local_path: return '' 1030 filetype = os.path.splitext(local_path)[1] 1031 gen_file = str(self.media_cn) + filetype 1032 idd_file = add_filename(gen_file, mid) 1033 new_file = sanitize_filename(idd_file) 1034 self.media_cn += 1 1035 new_path = os.path.join(self.telegram_media_dir, new_file) 1036 1037 if local_path != new_path: 1038 os.replace(local_path, new_path) 1039 if self.media_url[-1:] != '/': 1040 self.media_url += '/' 1041 return self.media_url + new_file 1042 1043 async def notice_downloading(self, size, relay_attr): 1044 if relay_attr and size > self.notice_size: 1045 message, user, mid, media_type = relay_attr 1046 await self.relay_telegram_message(message, user, '[{}] [{}] [Downloading]'.format(mid, media_type)) 1047 1048 class mesg_id: 1049 def __init__(self, alpha): 1050 self.alpha = alpha 1051 self.base = len(alpha) 1052 self.alphaval = { i:v for v, i in enumerate(alpha) } 1053 self.mesg_base = {} 1054 1055 def num_to_id(self, num, neg=''): 1056 if num < 0: return self.num_to_id(-num, '-') 1057 (high, low) = divmod(num, self.base) 1058 if high >= self.base: 1059 aux = self.num_to_id(high) 1060 return neg + aux + self.alpha[low] 1061 else: 1062 return neg + self.alpha[high] + self.alpha[low] 1063 1064 def num_to_id_offset(self, peer, num): 1065 peer_id = self.get_peer_id(peer) 1066 if peer_id not in self.mesg_base: 1067 self.mesg_base[peer_id] = num 1068 return self.num_to_id(num - self.mesg_base[peer_id]) 1069 1070 def id_to_num(self, id, n=1): 1071 if id: 1072 if id[0] == '-': return self.id_to_num(id[1:], -1) 1073 aux = self.alphaval[id[-1:]] * n 1074 sum = self.id_to_num(id[:-1], n * self.base) 1075 return sum + aux 1076 else: 1077 return 0 1078 1079 def id_to_num_offset(self, peer, mid): 1080 peer_id = self.get_peer_id(peer) 1081 if peer_id in self.mesg_base: 1082 id_rel = self.id_to_num(mid) 1083 id = id_rel + self.mesg_base[peer_id] 1084 else: 1085 id = None 1086 return id 1087 1088 def get_peer_id(self, peer): 1089 if isinstance(peer, tgty.PeerChannel): 1090 id = peer.channel_id 1091 elif isinstance(peer, tgty.PeerChat): 1092 id = peer.chat_id 1093 elif isinstance(peer, tgty.PeerUser): 1094 id = peer.user_id 1095 else: 1096 id = peer 1097 return id