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