/
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-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