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