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