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