/
telegram.py
   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