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