patch 10725c4fff915a1c95e95b5f998f26c97b146d5e
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Wed Jan 22 00:35:42 CET 2025
  * telegram: Try to fix ChatAdminRequiredError when getting participants from a channel
  reported in https://github.com/prsai/irgramd/issues/1
hunk ./telegram.py 180
-        async for user in self.telegram_client.iter_participants(chat.id):
-            user_nick = self.set_ircuser_from_telegram(user)
-            if not user.is_self:
-                self.irc.irc_channels[chan].add(user_nick)
-            # Add admin users as ops in irc
-            if isinstance(user.participant, tgty.ChatParticipantAdmin) or \
-               isinstance(user.participant, tgty.ChannelParticipantAdmin):
-                self.irc.irc_channels_ops[chan].add(user_nick)
-            # Add creator users as founders in irc
-            elif isinstance(user.participant, tgty.ChatParticipantCreator) or \
-                 isinstance(user.participant, tgty.ChannelParticipantCreator):
-                self.irc.irc_channels_founder[chan].add(user_nick)
+        try:
+            async for user in self.telegram_client.iter_participants(chat.id):
+                user_nick = self.set_ircuser_from_telegram(user)
+                if not user.is_self:
+                    self.irc.irc_channels[chan].add(user_nick)
+                # Add admin users as ops in irc
+                if isinstance(user.participant, tgty.ChatParticipantAdmin) or \
+                   isinstance(user.participant, tgty.ChannelParticipantAdmin):
+                    self.irc.irc_channels_ops[chan].add(user_nick)
+                # Add creator users as founders in irc
+                elif isinstance(user.participant, tgty.ChatParticipantCreator) or \
+                     isinstance(user.participant, tgty.ChannelParticipantCreator):
+                    self.irc.irc_channels_founder[chan].add(user_nick)
+        except:
+            self.logger.warning('Not possible to get participants of channel %s', channel)
patch 0c96892ec68150c96ef7ba50ec4e2db3db2296e4
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Thu Jan 16 22:00:14 CET 2025
  * exclam: Fix send a reaction when there was already a previous reaction from other user on the same message,
  for some reason the 'message' attribute is not received
hunk ./exclam.py 208
-                        self.tmp_tg_msg = update.updates[0].message
-                        reply = True
+                        self.tmp_tg_msg = getattr(update.updates[0], 'message', None)
+                        reply = bool(self.tmp_tg_msg)
patch 89bc0957a4fb31bf8066e338bc4548cd2d52c821
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sat Nov  2 20:20:45 CET 2024
  * telegram, irc: Set topic in IRC with Telegram channel/chat description
hunk ./include.py 14
+MAX_LINE                     = 400
hunk ./irc.py 21
-from include import VERSION, CHAN_MAX_LENGTH, NICK_MAX_LENGTH
+from include import VERSION, CHAN_MAX_LENGTH, NICK_MAX_LENGTH, MAX_LINE
hunk ./irc.py 262
-                await self.reply_code(user, 'RPL_LIST', (real_chan, users_count, topic))
+                await self.reply_code(user, 'RPL_LIST', (real_chan, users_count, topic[:MAX_LINE]))
hunk ./irc.py 615
-        await self.reply_code(user, 'RPL_TOPIC', (channel, topic))
+        await self.reply_code(user, 'RPL_TOPIC', (channel, topic[:MAX_LINE]))
hunk ./telegram.py 18
-from telethon.tl.functions.messages import GetMessagesReactionsRequest
+from telethon.tl.functions.messages import GetMessagesReactionsRequest, GetFullChatRequest
+from telethon.tl.functions.channels import GetFullChannelRequest
hunk ./telegram.py 313
+        if isinstance(entity, tgty.Channel): [_$_]
+            full = await self.telegram_client(GetFullChannelRequest(channel=entity))
+        elif isinstance(entity, tgty.Chat):
+            full = await self.telegram_client(GetFullChatRequest(chat_id=tid))
+        else:
+            return ''
hunk ./telegram.py 320
-        return 'Telegram ' + entity_type + ' ' + str(tid) + ': ' + entity.title
+        topic = full.full_chat.about
+        sep = ': ' if topic else ''
+        return entity_type + sep + topic
hunk ./utils.py 23
+from include import MAX_LINE
+
hunk ./utils.py 66
-    MAX = 400
hunk ./utils.py 67
-    wr = textwrap.TextWrapper(width=MAX)
+    wr = textwrap.TextWrapper(width=MAX_LINE)
patch 86ef89f9f9220ecde8477db2aa673f94f32656ab
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sat Oct 26 20:43:28 CEST 2024
  * telegram: Fix op and founder detection in channels
hunk ./telegram.py 184
-            if isinstance(user.participant, tgty.ChatParticipantAdmin):
+            if isinstance(user.participant, tgty.ChatParticipantAdmin) or \
+               isinstance(user.participant, tgty.ChannelParticipantAdmin):
hunk ./telegram.py 188
-            elif isinstance(user.participant, tgty.ChatParticipantCreator):
+            elif isinstance(user.participant, tgty.ChatParticipantCreator) or \
+                 isinstance(user.participant, tgty.ChannelParticipantCreator):
patch 6d99fce9643fbdda1594f99331f4a4642ecc7f0f
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sat Oct 26 20:35:11 CEST 2024
  * telegram: Fix handler for next reactions when the event is empty
hunk ./telegram.py 577
-        react = max(reactions, key=lambda y: y.date)
-        [_$_]
-        if self.last_reaction != react.date:
+        react = max(reactions, key=lambda y: y.date) if reactions else None
+
+        if react and self.last_reaction != react.date:
patch efaf8df4b3fa314997615e2fc07a3decb3a7cfba
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sat Oct 26 13:01:02 CEST 2024
  * exclam: Reorder handler list so commands are listed ordered in help
hunk ./exclam.py 21
-            '!re':        (self.handle_command_re,                    2,  2,  2),
-            '!ed':        (self.handle_command_ed,                    2,  2,  2),
hunk ./exclam.py 22
+            '!ed':        (self.handle_command_ed,                    2,  2,  2),
hunk ./exclam.py 24
-            '!upl':       (self.handle_command_upl,                   1,  2,  2),
-            '!reupl':     (self.handle_command_reupl,                 2,  3,  3),
+            '!re':        (self.handle_command_re,                    2,  2,  2),
hunk ./exclam.py 26
+            '!reupl':     (self.handle_command_reupl,                 2,  3,  3),
+            '!upl':       (self.handle_command_upl,                   1,  2,  2),
patch 0cfc7e59b24fb1a1b279fc593f8d04d0648e3880
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Mon Oct 21 00:54:16 CEST 2024
  * README update
hunk ./README.md 53
-- Reactions (receive, send)
+- Reactions (receive, send, remove)
patch abf1d31ddcf3cddd55844900065a3c3dd6bf9c67
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sun Oct 20 02:32:44 CEST 2024
  * exclam: Add "-" parameter to "!react" to remove a reaction
hunk ./emoji2emoticon.py 92
-emo_inv = {}
+emo_inv = { '-': None }
hunk ./exclam.py 202
-                    reaction = [ tgty.ReactionEmoji(emoticon=utf8_emo) ]
+                    reaction = [ tgty.ReactionEmoji(emoticon=utf8_emo) ] if utf8_emo else None
hunk ./exclam.py 219
-              '   !react <compact_id> <emoticon reaction>',
+              '   !react <compact_id> <emoticon reaction>|-',
hunk ./exclam.py 222
+              'Use - to remove a previous reaction.',
patch 5c1687a16a1e49af7b5f70e9449c447bbf7c9df9
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sat Oct 19 22:56:31 CEST 2024
  * telegram: On emoticon->emoji conversions for reactions,
  when several emoticons can be mapped to an emoji,
  keep first elements that are more probable allowed for reactions
hunk ./emoji2emoticon.py 93
-for k in emo:
+for k in reversed(emo):
patch 7db9d9a07af099b00a32a6ee0197c400a52240e5
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sun Oct 13 23:00:24 CEST 2024
  * telegram: Limit text length for reactions to twice as replies
hunk ./telegram.py 508
-        if len(message_rendered) > self.quote_len and not msg.is_reply:
-            text_old = '{}...'.format(message_rendered[:self.quote_len])
+        react_quote_len = self.quote_len * 2
+        if len(message_rendered) > react_quote_len:
+            text_old = '{}...'.format(message_rendered[:react_quote_len])
patch 42fe2f72d41ed36616e6a19b9388eec356679e38
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sat Oct 12 23:17:45 CEST 2024
  * README update
hunk ./README.md 53
-- Reactions (receive)
+- Reactions (receive, send)
patch 873004141f83b60da113e8967a2148d9d33008be
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Thu Oct 10 23:11:40 CEST 2024
  * exclam: Add reaction (!react) command to send emoticon->emoji as Telegram reaction
hunk ./emoji2emoticon.py 89
-        '\U0001f970': '":)e>"'
+        '\U0001f970': '":)e>"',
hunk ./emoji2emoticon.py 92
+emo_inv = {}
+for k in emo:
+  emo_inv[emo[k][1:-1]] = k
+
hunk ./exclam.py 10
-from telethon.errors.rpcerrorlist import MessageNotModifiedError, MessageAuthorRequiredError
+from telethon.tl.functions.messages import SendReactionRequest
+from telethon import types as tgty
+from telethon.errors.rpcerrorlist import MessageNotModifiedError, MessageAuthorRequiredError, ReactionInvalidError
hunk ./exclam.py 15
+from emoji2emoticon import emo_inv
hunk ./exclam.py 27
+            '!react':     (self.handle_command_react,                 2,  2, -1),
hunk ./exclam.py 193
+            )
+        return reply
+
+    async def handle_command_react(self, cid=None, act=None, help=None):
+        if not help:
+            id, chk_msg = await self.check_msg(cid)
+            if chk_msg is not None:
+                if act in emo_inv:
+                    utf8_emo = emo_inv[act]
+                    reaction = [ tgty.ReactionEmoji(emoticon=utf8_emo) ]
+                    try:
+                        update = await self.tg.telegram_client(SendReactionRequest(self.tmp_telegram_id, id, reaction=reaction))
+                    except ReactionInvalidError:
+                        reply = ('!react: Reaction not allowed',)
+                    else:
+                        self.tmp_tg_msg = update.updates[0].message
+                        reply = True
+                else:
+                    reply = ('!react: Unknown reaction',)
+            else:
+                reply = ('!react: Unknown message to react',)
+        else: # HELP.brief or HELP.desc (first line)
+            reply = ('   !react      React to a message',)
+        if help == HELP.desc:  # rest of HELP.desc
+            reply += \
+            (
+              '   !react <compact_id> <emoticon reaction>',
+              'React with <emoticon reaction> to a message with <compact_id>,',
+              'irgramd will translate emoticon to closest emoji.',
patch 69a63d9f5f49ece35c5d552ba8b3081c8277490d
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Mon Oct  7 00:07:54 CEST 2024
  * telegram, service: Move initial help to service module
  add a line for "equivalent command"
hunk ./irc.py 535
-        for line in (
-                      'Welcome to irgramd service',
-                      'use /msg {} help'.format(self.service_user.irc_nick),
-                      'to get help',
-                    ):
+        for line in self.service.initial_help():
hunk ./service.py 28
+    def initial_help(self):
+        return (
+                  'Welcome to irgramd service',
+                  'use /msg {} help'.format(self.irc.service_user.irc_nick),
+                  'or equivalent in your IRC client',
+                  'to get help',
+               )
+
patch c1ffe716a42ea01ca345d7a756b685d7174f99c5
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sun Oct  6 23:59:23 CEST 2024
  * telegram: Fix regression in delete reaction event
hunk ./telegram.py 520
-            user = self.get_irc_user_from_telegram(event.sender_id)
+            user = self.get_irc_user_from_telegram(msg.sender_id)
hunk ./telegram.py 558
-            if self.last_reaction == reaction.date:
-                return
-            self.last_reaction = reaction.date
+            if reaction:
+                if self.last_reaction == reaction.date:
+                    return
+                self.last_reaction = reaction.date
patch beacde93a685dd954f9823dc0a6fea4594b2c1e4
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sat Sep 28 02:53:06 CEST 2024
  * telegram: Avoid duplicated reactions events in some cases
hunk ./telegram.py 74
+        self.last_reaction = None
hunk ./telegram.py 558
+            if self.last_reaction == reaction.date:
+                return
+            self.last_reaction = reaction.date
hunk ./telegram.py 576
-        id = event.msg_id
-        msg = await self.telegram_client.get_messages(entity=event.peer, ids=id)
-        mid = self.mid.num_to_id_offset(msg.peer_id, id)
-        message = self.filters(msg.message)
-        message_rendered = await self.render_text(msg, mid, upd_to_webpend=None)
-
-        text_old, edition_react, user = self.format_reaction(msg, message_rendered, edition_case='react-add', reaction=react)
-
-        text = '|React {}| {}'.format(text_old, edition_react)
-
-        chan = await self.relay_telegram_message(msg, user, text)
-
-        self.to_cache(id, mid, message, message_rendered, user, chan, msg.media)
-        self.to_volatile_cache(self.prev_id, id, text, user, chan, current_date())
+        [_$_]
+        if self.last_reaction != react.date:
+            self.last_reaction = react.date
+            id = event.msg_id
+            msg = await self.telegram_client.get_messages(entity=event.peer, ids=id)
+            mid = self.mid.num_to_id_offset(msg.peer_id, id)
+            message = self.filters(msg.message)
+            message_rendered = await self.render_text(msg, mid, upd_to_webpend=None)
+
+            text_old, edition_react, user = self.format_reaction(msg, message_rendered, edition_case='react-add', reaction=react)
+
+            text = '|React {}| {}'.format(text_old, edition_react)
+
+            chan = await self.relay_telegram_message(msg, user, text)
+
+            self.to_cache(id, mid, message, message_rendered, user, chan, msg.media)
+            self.to_volatile_cache(self.prev_id, id, text, user, chan, current_date())
patch b2f8fe9251a26c43e16ba1aadff8d71e64a5a7e9
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Fri Sep 27 11:05:53 CEST 2024
  * telegram: Add handler for next reactions (2nd, 3rd, etc.) that don't come from
  the same events as 1st (why?!)
hunk ./telegram.py 506
+    def format_reaction(self, msg, message_rendered, edition_case, reaction):
+        if len(message_rendered) > self.quote_len and not msg.is_reply:
+            text_old = '{}...'.format(message_rendered[:self.quote_len])
+            text_old = fix_braces(text_old)
+        else:
+            text_old = message_rendered
+
+        if edition_case == 'react-add':
+            user = self.get_irc_user_from_telegram(reaction.peer_id.user_id)
+            emoji = reaction.reaction.emoticon
+            react_action = '+'
+            react_icon = e.emo[emoji] if emoji in e.emo else emoji
+        elif edition_case == 'react-del':
+            user = self.get_irc_user_from_telegram(event.sender_id)
+            react_action = '-'
+            react_icon = ''
+        return text_old, '{}{}'.format(react_action, react_icon), user
+
hunk ./telegram.py 558
-            if len(message_rendered) > self.quote_len and not event.message.is_reply:
-                text_old = '{}...'.format(message_rendered[:self.quote_len])
-                text_old = fix_braces(text_old)
-            else:
-                text_old = message_rendered
-
-            if edition_case == 'react-add':
-                user = self.get_irc_user_from_telegram(reaction.peer_id.user_id)
-                emoji = reaction.reaction.emoticon
-                react_action = '+'
-                react_icon = e.emo[emoji] if emoji in e.emo else emoji
-            elif edition_case == 'react-del':
-                user = self.get_irc_user_from_telegram(event.sender_id)
-                react_action = '-'
-                react_icon = ''
-            edition_react = '{}{}'.format(react_action, react_icon)
+            text_old, edition_react, user = self.format_reaction(event.message, message_rendered, edition_case, reaction)
hunk ./telegram.py 567
+    async def handle_next_reaction(self, event):
+        self.logger.debug('Handling Telegram Next Reaction (2nd, 3rd, ...): %s', pretty(event))
+
+        reactions = event.reactions.recent_reactions
+        react = max(reactions, key=lambda y: y.date)
+        id = event.msg_id
+        msg = await self.telegram_client.get_messages(entity=event.peer, ids=id)
+        mid = self.mid.num_to_id_offset(msg.peer_id, id)
+        message = self.filters(msg.message)
+        message_rendered = await self.render_text(msg, mid, upd_to_webpend=None)
+
+        text_old, edition_react, user = self.format_reaction(msg, message_rendered, edition_case='react-add', reaction=react)
+
+        text = '|React {}| {}'.format(text_old, edition_react)
+
+        chan = await self.relay_telegram_message(msg, user, text)
+
+        self.to_cache(id, mid, message, message_rendered, user, chan, msg.media)
+        self.to_volatile_cache(self.prev_id, id, text, user, chan, current_date())
+
hunk ./telegram.py 610
+        elif isinstance(update, tgty.UpdateMessageReactions):
+            await self.handle_next_reaction(update)
+
patch 8770a66d55d4d1c34e009fe5d0078f77c3be4d34
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Wed Sep 25 01:36:06 CEST 2024
  * telegram: Fix in reaction handler
hunk ./telegram.py 404
-        elif react := next((x for x in reactions if x.date == msg.edit_date), None):
+        elif react := max(reactions, key=lambda y: y.date):
patch 7e550077a65e4737ca30a81e0decbcb4db0485a4
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Fri Sep 20 23:50:13 CEST 2024
  * telegram: Don't truncate text for reactions of replies
hunk ./telegram.py 540
-            if len(message_rendered) > self.quote_len:
+            if len(message_rendered) > self.quote_len and not event.message.is_reply:
patch f9ff84bf789b6bd5109c5953590ca41c438fe123
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sun Sep 15 23:50:10 CEST 2024
  * telegram: Minor improvement in debug of relay methods
hunk ./telegram.py 662
-        self.logger.debug('Handling Telegram Private Message: %s, %s', user, message)
+        self.logger.debug('Relaying Telegram Private Message: %s, %s', user, message)
hunk ./telegram.py 670
-        self.logger.debug('Handling Telegram Channel Message: %s', pretty(message) or text)
-
hunk ./telegram.py 676
+        self.logger.debug('Relaying Telegram Channel Message: %s, %s', chan, text)
+
patch 1d527812923bdf50653bb371185bec70c2abad40
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sun Sep 15 01:23:24 CEST 2024
  * telegram: Improve a bit reactions handler
hunk ./telegram.py 392
-            return react.updates[0].reactions.recent_reactions
+            updates = react.updates
+            r = next((x for x in updates if type(x) is tgty.UpdateMessageReactions), None)
+            return r.reactions.recent_reactions if r else None
patch b43b2bc6a4e9dcf0eaddb66ea3fd5abf7c95082b
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sat Sep  7 23:20:27 CEST 2024
  * Fix typo in a constant
hunk ./include.py 13
-CHAN_MAX_LENGHT              = 50
+CHAN_MAX_LENGTH              = 50
hunk ./irc.py 21
-from include import VERSION, CHAN_MAX_LENGHT, NICK_MAX_LENGTH
+from include import VERSION, CHAN_MAX_LENGTH, NICK_MAX_LENGTH
hunk ./irc.py 532
-        await self.reply_code(user, 'RPL_ISUPPORT', (CHAN_MAX_LENGHT, NICK_MAX_LENGTH))
+        await self.reply_code(user, 'RPL_ISUPPORT', (CHAN_MAX_LENGTH, NICK_MAX_LENGTH))
hunk ./telegram.py 22
-from include import CHAN_MAX_LENGHT, NICK_MAX_LENGTH
+from include import CHAN_MAX_LENGTH, NICK_MAX_LENGTH
patch 6aaf9a2af5898f8b6ec1027a3ccb7e85f6893f22
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sun Sep  1 01:01:19 CEST 2024
  * Increase virtual version to 0.2
  Remove alpha status
hunk ./README.md 16
-**irgramd is under active development in alpha state, though usable, several
+**irgramd is under active development, though usable, several
hunk ./include.py 11
-VERSION                      = '0.1'
+VERSION                      = '0.2'
patch a39c65dc932ee95b44b5a759cad3e413177fc5aa
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Fri Aug 30 21:53:13 CEST 2024
  * telegram: Add a cache of "volatile" events (delete, edit, react) to be shown in history
hunk ./telegram.py 71
+        self.volatile_cache = collections.OrderedDict()
+        self.prev_id = {}
hunk ./telegram.py 413
-        if len(self.cache) >= 10000:
-            self.cache.popitem(last=False)
+        self.limit_cache(self.cache)
hunk ./telegram.py 423
+    def to_volatile_cache(self, prev_id, id, ev, user, chan, date):
+        if chan in prev_id:
+            prid = prev_id[chan] if chan else prev_id[user]
+            self.limit_cache(self.volatile_cache)
+            elem = {
+                     'id': id,
+                     'rendered_event': ev,
+                     'user': user,
+                     'channel': chan,
+                     'date': date,
+                   }
+            if prid not in self.volatile_cache:
+                self.volatile_cache[prid] = [elem]
+            else:
+                self.volatile_cache[prid].append(elem)
+
+    def limit_cache(self, cache):
+        if len(cache) >= 10000:
+            cache.popitem(last=False)
+
hunk ./telegram.py 560
+        self.to_volatile_cache(self.prev_id, id, text, user, chan, current_date())
hunk ./telegram.py 572
+                self.to_volatile_cache(self.prev_id, deleted_id, text, user, chan, current_date())
hunk ./telegram.py 595
+        await self.history_search_volatile(history, msg.id)
hunk ./telegram.py 598
+        peer = chan if chan else user
+        self.prev_id[peer] = msg.id
hunk ./telegram.py 638
+    async def history_search_volatile(self, history, id):
+        if history:
+            if id in self.volatile_cache:
+                for item in self.volatile_cache[id]:
+                    user = item['user']
+                    text = item['rendered_event']
+                    chan = item['channel']
+                    date = item['date']
+                    text_send = self.set_history_timestamp(text, history=True, date=date, action=False)
+                    await self.relay_telegram_message(None, user, text_send, chan)
+
patch 95e72ac9b26835162b8ba997c5ff99edfd5d464e
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Fri Aug 30 19:00:53 CEST 2024
  * utils: Small optimization in pretty()
hunk ./utils.py 46
+class LOGL:
+    debug = False
+
hunk ./utils.py 230
+    if levelu == 'DEBUG':
+        LOGL.debug = True
hunk ./utils.py 241
-    return object.stringify() if object else object
+    return object.stringify() if LOGL.debug and object else object
patch 799dcf8a6a7c8346af93e7f17841baf08db70e7c
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Fri Aug 30 01:58:06 CEST 2024
  * utils: Add current_date() shortcut
hunk ./telegram.py 12
-import datetime
hunk ./telegram.py 24
-from utils import sanitize_filename, add_filename, is_url_equiv, extract_url, get_human_size, get_human_duration, get_highlighted, fix_braces, format_timestamp, pretty
+from utils import sanitize_filename, add_filename, is_url_equiv, extract_url, get_human_size, get_human_duration
+from utils import get_highlighted, fix_braces, format_timestamp, pretty, current_date
hunk ./telegram.py 289
-            current = datetime.datetime.now(datetime.timezone.utc)
+            current = current_date()
hunk ./utils.py 155
-    delta = datetime.datetime.now(datetime.timezone.utc) - date
+    delta = current_date() - date
hunk ./utils.py 167
+def current_date():
+    return datetime.datetime.now(datetime.timezone.utc)
+
patch 6c991a90a37dcc5a96906992b5c4df41e7f68991
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Thu Aug 29 23:06:59 CEST 2024
  * Add trailing commas (and some spacing)
hunk ./exclam.py 62
-              'channel/chat.'
+              'channel/chat.',
hunk ./exclam.py 88
-              '<new_message> replaces the current message.'
+              '<new_message> replaces the current message.',
hunk ./exclam.py 168
-              'irgramd local directory or be an external HTTP/HTTPS URL.'
+              'irgramd local directory or be an external HTTP/HTTPS URL.',
hunk ./exclam.py 188
-              'HTTP/HTTPS URL.'
+              'HTTP/HTTPS URL.',
hunk ./service.py 252
-              'reset the number of mentions to you on <peer>.'
+              'reset the number of mentions to you on <peer>.',
hunk ./telegram.py 32
-             3: '149.154.175.117'
+             3: '149.154.175.117',
hunk ./telegram.py 105
-            (self.handle_raw,                  telethon.events.Raw),
+            (self.handle_raw                 , telethon.events.Raw),
hunk ./telegram.py 108
-            (self.handle_telegram_edited,      telethon.events.MessageEdited),
+            (self.handle_telegram_edited     , telethon.events.MessageEdited),
hunk ./telegram.py 419
-                           'media': media
+                           'media': media,
patch 6057cbb6c30094c80cba9f6326a5b513a9ab540c
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sun Aug 25 01:21:01 CEST 2024
  * utils, telegram: Add pretty() function to print readable objects in debug
hunk ./telegram.py 25
-from utils import sanitize_filename, add_filename, is_url_equiv, extract_url, get_human_size, get_human_duration, get_highlighted, fix_braces, format_timestamp
+from utils import sanitize_filename, add_filename, is_url_equiv, extract_url, get_human_size, get_human_duration, get_highlighted, fix_braces, format_timestamp, pretty
hunk ./telegram.py 484
-        self.logger.debug('Handling Telegram Message Edited: %s', event)
+        self.logger.debug('Handling Telegram Message Edited: %s', pretty(event))
hunk ./telegram.py 541
-        self.logger.debug('Handling Telegram Message Deleted: %s', event)
+        self.logger.debug('Handling Telegram Message Deleted: %s', pretty(event))
hunk ./telegram.py 555
-        self.logger.debug('Handling Telegram Raw Event: %s', update)
+        self.logger.debug('Handling Telegram Raw Event: %s', pretty(update))
hunk ./telegram.py 563
-        self.logger.debug('Handling Telegram Message: %s', event or message)
+        self.logger.debug('Handling Telegram Message: %s', pretty(event or message))
hunk ./telegram.py 631
-        self.logger.debug('Handling Telegram Channel Message: %s', message or text)
+        self.logger.debug('Handling Telegram Channel Message: %s', pretty(message) or text)
hunk ./telegram.py 647
-        self.logger.debug('Handling Telegram Chat Action: %s', event)
+        self.logger.debug('Handling Telegram Chat Action: %s', pretty(event))
hunk ./utils.py 231
+
+def pretty(object):
+    return object.stringify() if object else object
patch 0fd4392905932f144e14844f164d801a22b68467
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sun Aug 18 14:01:26 CEST 2024
  * exclam: Add re-upload (!reupl) command to upload files/media as a reply to a message
hunk ./exclam.py 23
+            '!reupl':     (self.handle_command_reupl,                 2,  3,  3),
hunk ./exclam.py 148
-    async def handle_command_upl(self, file=None, caption=None, help=None):
+    async def handle_command_upl(self, file=None, caption=None, help=None, re_id=None):
hunk ./exclam.py 155
-                self.tmp_tg_msg = await self.tg.telegram_client.send_file(self.tmp_telegram_id, file_path, caption=caption)
+                self.tmp_tg_msg = await self.tg.telegram_client.send_file(self.tmp_telegram_id, file_path, caption=caption, reply_to=re_id)
hunk ./exclam.py 158
-                reply = ('!upl: Error uploading',)
+                cmd = '!reupl' if re_id else '!upl'
+                reply = ('{}: Error uploading'.format(cmd),)
hunk ./exclam.py 169
+            )
+        return reply
+
+    async def handle_command_reupl(self, cid=None, file=None, caption=None, help=None):
+        if not help:
+            id, chk_msg = await self.check_msg(cid)
+            if chk_msg is not None:
+                reply = await self.handle_command_upl(file, caption, re_id=id)
+            else:
+                reply = ('!reupl: Unknown message to reply',)
+        else: # HELP.brief or HELP.desc (first line)
+            reply = ('   !reupl      Reply to a message with an upload',)
+        if help == HELP.desc:  # rest of HELP.desc
+            reply += \
+            (
+              '   !reupl <compact_id> <file name/URL> [<optional caption>]',
+              'Reply with the upload of <file name/URL> to a message with',
+              '<compact_id> on current channel/chat. The file must be',
+              'present in "upload" irgramd local directory or be an external',
+              'HTTP/HTTPS URL.'
patch 477b15fc239d17cccf626d042db2f323e1fa1b4b
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sun Aug 18 13:58:56 CEST 2024
  * exclam: Check valid range for message IDs
hunk ./exclam.py 40
-        if id is None:
+        if id is None or id < -2147483648 or id > 2147483647:
patch 35206dbcb8c561df88e525160e5d6a50dad07481
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Thu Aug 15 01:30:54 CEST 2024
  * Handle replies to deleted messages (maybe this case is only given from history)
hunk ./telegram.py 696
-        replied_msg = replied.message
-        cid = self.mid.num_to_id_offset(replied.peer_id, replied.id)
+        if replied:
+            replied_msg = replied.message
+            cid = self.mid.num_to_id_offset(replied.peer_id, replied.id)
+            replied_user = self.get_irc_user_from_telegram(replied.sender_id)
+        else:
+            replied_id = message.reply_to.reply_to_msg_id
+            cid = self.mid.num_to_id_offset(message.peer_id, replied_id)
+            if replied_id in self.cache:
+                text = self.cache[replied_id]['text']
+                replied_user = self.cache[replied_id]['user']
+                sp = ' '
+            else:
+                text = ''
+                replied_user = ''
+                sp = ''
+            replied_msg = '|Deleted|{}{}'.format(sp, text)
hunk ./telegram.py 718
-        replied_user = self.get_irc_user_from_telegram(replied.sender_id)
hunk ./telegram.py 721
+        elif replied_user == '':
+            replied_nick = ''
patch ca113b48abe430eb11d500a5eb086edd15f23b0d
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sun Apr 28 19:45:06 CEST 2024
  * Update copyright year in LICENSE
hunk ./LICENSE 4
-Copyright (c) 2020-2023 E. Bosch <presidev@AT@gmail.com>
+Copyright (c) 2020-2024 E. Bosch <presidev@AT@gmail.com>
patch 3ad99bad13beb07c34a9a992b026ed923f39b047
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sun Apr 28 13:16:24 CEST 2024
  * README update
hunk ./README.md 119
+## Inspired by
+
+- [telegramircd]
+- [ibotg]
+- [bitlbee]
+
hunk ./README.md 146
+[telegramircd]: https://github.com/prsai/telegramircd
+[ibotg]: https://github.com/prsai/ibotg
+[bitlbee]: https://www.bitlbee.org
patch c5d6314d751bbaca2ba4d24f4af0465af751ac40
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sun Apr 28 00:28:22 CEST 2024
  * utils: Fix when a filename has no extension
hunk ./utils.py 5
-# Copyright (c) 2020-2023 E. Bosch <presidev@AT@gmail.com>
+# Copyright (c) 2020-2024 E. Bosch <presidev@AT@gmail.com>
hunk ./utils.py 96
-        ext = aux[1]
+        try:
+            ext = aux[1]
+        except:
+            ext = ''
patch 5d8ba95f7bbee459c4a9c7a0d524894ae8836c83
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sat Apr 27 20:32:49 CEST 2024
  * exclam: Add command indicator to error messages
hunk ./exclam.py 53
-                reply = ('Unknown message to reply',)
+                reply = ('!re: Unknown message to reply',)
hunk ./exclam.py 75
-                    reply = ('Not the author of the message to edit',)
+                    reply = ('!ed: Not the author of the message to edit',)
hunk ./exclam.py 97
-                    reply = ('Not possible to delete',)
+                    reply = ('!del: Not possible to delete',)
hunk ./exclam.py 134
-                    reply = ('Unknown chat to forward',)
+                    reply = ('!fwd: Unknown chat to forward',)
hunk ./exclam.py 157
-                reply = ('Error uploading',)
+                reply = ('!upl: Error uploading',)
patch b287d9843e3a4854c7ab0dc15558546ab7fd4c86
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sun Apr 21 21:19:29 CEST 2024
  * README update
hunk ./README.md 48
-- Media in messages (receive, download)
+- Media in messages (receive, download, upload)
patch d523591db91c8bf0ceb6ea65bac6358e5080f35a
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sun Apr 21 20:59:04 CEST 2024
  * exclam: !upl: Add support for HTTP/HTTPS URL for file upload
hunk ./exclam.py 150
-                file_path = os.path.join(self.tg.telegram_upload_dir, file)
+                if file[:8] == 'https://' or file[:7] == 'http://':
+                    file_path = file
+                else:
+                    file_path = os.path.join(self.tg.telegram_upload_dir, file)
hunk ./exclam.py 163
-              '   !upl <file name>  [<optional caption>]',
-              'Upload the file referenced by <file name>  to current ',
+              '   !upl <file name/URL> [<optional caption>]',
+              'Upload the file referenced by <file name/URL> to current',
hunk ./exclam.py 166
-              'local directory.'
+              'irgramd local directory or be an external HTTP/HTTPS URL.'
patch 18c87eca10fbc53fd73e8adf6b4da64dd4f24109
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Fri Apr 19 01:11:38 CEST 2024
  * service: Disable by now the help for subcommands "archive" and "delete" from
  command "dialog" as they are not really implemented yet
hunk ./service.py 94
-              '   archive <id>   Archive the dialog specified by id',
-              '   delete <id>    Delete the dialog specified by id',
+#              '   archive <id>   Archive the dialog specified by id',
+#              '   delete <id>    Delete the dialog specified by id',
patch ca68ae9cfb315331dd08f8033ea2270e8a93e626
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sun Apr 14 22:48:30 CEST 2024
  * exclam: Add upload (!upl) command to upload files/media to chats/channels
  Add "upload_dir" option to define the local directory to pick the files up,
  by default "~/.cache/irgramd/upload"
hunk ./exclam.py 9
+import os
hunk ./exclam.py 22
+            '!upl':       (self.handle_command_upl,                   1,  2,  2),
hunk ./exclam.py 144
+            )
+        return reply
+
+    async def handle_command_upl(self, file=None, caption=None, help=None):
+        if not help:
+            try:
+                file_path = os.path.join(self.tg.telegram_upload_dir, file)
+                self.tmp_tg_msg = await self.tg.telegram_client.send_file(self.tmp_telegram_id, file_path, caption=caption)
+                reply = True
+            except:
+                reply = ('Error uploading',)
+        else: # HELP.brief or HELP.desc (first line)
+            reply = ('   !upl        Upload a file to current channel/chat',)
+        if help == HELP.desc:  # rest of HELP.desc
+            reply += \
+            (
+              '   !upl <file name>  [<optional caption>]',
+              'Upload the file referenced by <file name>  to current ',
+              'channel/chat, the file must be present in "upload"',
+              'local directory.'
hunk ./irgramd 113
+    tornado.options.define('upload_dir', default=None, metavar='PATH', help='Directory where files to upload are picked up, default "upload" in `cache_dir`')
hunk ./telegram.py 46
+        self.upload_dir = settings['upload_dir']
hunk ./telegram.py 81
+        # Setup upload folder
+        self.telegram_upload_dir = os.path.expanduser(self.upload_dir or os.path.join(self.cache_dir, 'upload'))
+        if not os.path.exists(self.telegram_upload_dir):
+            os.makedirs(self.telegram_upload_dir)
+
patch fa015f3a5b1ea9fe2b6c068491481d57837ecdc5
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sun Apr 14 02:13:34 CEST 2024
  * telegram: Use directory ".cache/irgramd/media" instead of
  ".config/irgramd/media" by default (relative to home directory)
  Added "cache_dir" option to override the default "~/.cache/irgramd"
hunk ./irgramd 6
-# Copyright (c) 2020-2023 E. Bosch <presidev@AT@gmail.com>
+# Copyright (c) 2020-2024 E. Bosch <presidev@AT@gmail.com>
hunk ./irgramd 82
+    tornado.options.define('cache_dir', default='~/.cache/irgramd', metavar='PATH', help='Cache directory where telegram media is saved by default')
hunk ./irgramd 98
-    tornado.options.define('media_dir', default=None, metavar='PATH', help='Directory where Telegram media files are downloaded, default "media" in `config_dir`')
+    tornado.options.define('media_dir', default=None, metavar='PATH', help='Directory where Telegram media files are downloaded, default "media" in `cache_dir`')
hunk ./telegram.py 41
+        self.cache_dir  = settings['cache_dir']
hunk ./telegram.py 76
-        self.telegram_media_dir = self.media_dir or os.path.join(self.config_dir, 'media')
+        self.telegram_media_dir = os.path.expanduser(self.media_dir or os.path.join(self.cache_dir, 'media'))
patch 170328f9bde0160c50f56b98cbf51c8726ab4d69
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sun Apr  7 19:48:33 CEST 2024
  * telegram: Fix a corner case in forward handler when saved_from_peer is not present
hunk ./telegram.py 5
-# Copyright (c) 2020-2023 E. Bosch <presidev@AT@gmail.com>
+# Copyright (c) 2020-2024 E. Bosch <presidev@AT@gmail.com>
hunk ./telegram.py 715
-            if self.refwd_me:
-               secondary_name = self.get_irc_user_from_telegram(message.fwd_from.saved_from_peer.user_id).irc_nick
+            if self.refwd_me and (saved_from_peer := message.fwd_from.saved_from_peer) is not None:
+               secondary_name = self.get_irc_user_from_telegram(saved_from_peer.user_id).irc_nick
patch 734e8c9f78627a6536e3ff52bd2db4879737830d
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sun Apr  7 19:08:52 CEST 2024
  * README update
hunk ./README.md 50
-- Forwards (receive)
+- Forwards (receive, send)
hunk ./README.md 122
-Copyright (c) 2020-2023 E. Bosch <presidev@AT@gmail.com>
+Copyright (c) 2020-2024 E. Bosch <presidev@AT@gmail.com>
patch 4bb5866f7a3278949670e153444b9b1db74344ad
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sun Apr  7 19:07:04 CEST 2024
  * exclam: Add forward (!fwd) command to forward messages to other channels or chats
hunk ./exclam.py 4
-# Copyright (c) 2023 E. Bosch <presidev@AT@gmail.com>
+# Copyright (c) 2023, 2024 E. Bosch <presidev@AT@gmail.com>
hunk ./exclam.py 20
+            '!fwd':       (self.handle_command_fwd,                   2,  2, -1),
hunk ./exclam.py 108
+            )
+        return reply
+
+    async def handle_command_fwd(self, cid=None, chat=None, help=None):
+        if not help:
+            id, chk_msg = await self.check_msg(cid)
+            if chk_msg is not None:
+                async def send_fwd(tgt_ent, id):
+                    from_ent = await self.tg.telegram_client.get_entity(self.tmp_telegram_id)
+                    self.tmp_tg_msg = await self.tg.telegram_client.forward_messages(tgt_ent, id, from_ent)
+                    return self.tmp_tg_msg
+
+                tgt = chat.lower()
+                if tgt in self.irc.iid_to_tid:
+                    tgt_ent = await self.tg.telegram_client.get_entity(self.irc.iid_to_tid[tgt])
+                    msg = await send_fwd(tgt_ent, id)
+                    # echo fwded message
+                    await self.tg.handle_telegram_message(event=None, message=msg)
+                    reply = True
+                elif tgt in (u.irc_nick.lower() for u in self.irc.users.values() if u.stream):
+                    tgt_ent = await self.tg.telegram_client.get_me()
+                    await send_fwd(tgt_ent, id)
+                    reply = True
+                else:
+                    reply = ('Unknown chat to forward',)
+            else:
+                reply = ('Unknown message to forward',)
+        else: # HELP.brief or HELP.desc (first line)
+            reply = ('   !fwd        Forward a message',)
+        if help == HELP.desc:  # rest of HELP.desc
+            reply += \
+            (
+              '   !fwd <compact_id> <chat>',
+              'Forward a message with <compact_id> to <chat> channel/chat.'
patch aa90a8fae2fb0ed855e80f146dc88cbf7069a2dd
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sun Dec 31 01:26:30 CET 2023
  * README update
hunk ./README.md 108
-In background (without logs):
-
-    ./irgramd --logging=none &
+In background (with logs):
+
+    ./irgramd --log-file=irgramd.log &
patch f7068578a6b2806e38c5c5bfc1c03b8e59a14455
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Wed Dec 20 01:50:56 CET 2023
  * Fix logging system.                                                                                                                                
  Remove logging options from tornado.log that were not working correctly in this setup                                                              
  and use the new options from irgramd ("log_file" and "log_level").
  Defer first logs to be included in log file opened later.
  Improve option error handling.
hunk ./irgramd 23
+from utils import parse_loglevel
hunk ./irgramd 69
-    logger = logging.getLogger()
+    # Remove tornado.log options (ugly hacks but these must not be defined)
+    tornado.options.options.logging = None
+    tornado_log_options = tuple(x for x in tornado.options.options._options.keys() if x != 'help' and x != 'logging')
+    for opt in tornado_log_options:
+        del tornado.options.options._options[opt]
+    # and reuse "--logging" to document empty "--" ;)
+    tornado.options.options._options['logging'].help = 'Stop parsing options'
+    for att in ('name', 'metavar', 'group_name', 'default'):
+        setattr(tornado.options.options._options['logging'], att, '')
+    # Define irgramd options
hunk ./irgramd 95
+    tornado.options.define('log_file', default=None, metavar='PATH', help='File where logs are appended, if not set will be stderr')
+    tornado.options.define('log_level', default='INFO', metavar='DEBUG|INFO|WARNING|ERROR|CRITICAL|NONE', help='The log level (and any higher to it) that will be logged')
hunk ./irgramd 112
-    # parse cmd line first time to get --config and --config_dir
-    tornado.options.parse_command_line()
+    try:
+        # parse cmd line first time to get --config and --config_dir
+        tornado.options.parse_command_line()
+    except Exception as exc:
+        print(exc)
+        exit(1)
hunk ./irgramd 122
-    logger.info('Configuration Directory: %s', config_dir)
+    defered_logs = [(logging.INFO, 'Configuration Directory: %s', config_dir)]
hunk ./irgramd 127
-        logger.info('Using configuration file: %s', config_file)
-        tornado.options.parse_config_file(config_file)
+        defered_logs.append((logging.INFO, 'Using configuration file: %s', config_file))
+        try:
+            tornado.options.parse_config_file(config_file)
+        except Exception as exc:
+            print(exc)
+            exit(1)
hunk ./irgramd 134
-        logger.warning('Configuration file not present, using only command line options and defaults')
+        defered_logs.append((logging.WARNING, 'Configuration file not present, using only command line options and defaults'))
hunk ./irgramd 141
+    # configure logging
+    loglevel = parse_loglevel(options['log_level'])
+    if loglevel == False:
+        print("Option 'log_level' requires one of these values: {}".format(tornado.options.options._options['log-level'].metavar))
+        exit(1)
+    logger_formats = { 'datefmt':'%Y-%m-%d %H:%M:%S', 'format':'[%(levelname).1s %(asctime)s %(module)s:%(lineno)d] %(message)s' }
+    logger = logging.getLogger()
+    if options['log_file']:
+        logging.basicConfig(filename=options['log_file'], level=loglevel, **logger_formats)
+    else:
+        logging.basicConfig(level=loglevel, **logger_formats)
+
+    for log in defered_logs:
+        logger.log(*log)
+
+    # main loop
hunk ./utils.py 16
+import logging
hunk ./utils.py 218
+
+def parse_loglevel(level):
+    levelu = level.upper()
+    if levelu == 'NONE':
+        l = None
+    elif levelu in ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'):
+        l = getattr(logging, levelu)
+    else:
+        l = False
+    return l
patch 987c7436cd18763a950f5799ef5fac6e7cb127e4
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Mon Dec 18 21:18:42 CET 2023
  * telegram, utils: Replace invalid characters in filenames with number sequences instead of just removing.
  This will prevent some filename collisions in corner cases.
hunk ./utils.py 84
-    return FILENAME_INVALID_CHARS.sub('', fn).strip('-').replace(' ','_')
+    cn = str(sanitize_filename.cn)
+    new_fn, ns = FILENAME_INVALID_CHARS.subn(cn, fn)
+    if ns:
+        sanitize_filename.cn += 1
+    return new_fn.strip('-').replace(' ','_')
+sanitize_filename.cn = 0
patch 3ba3cb2a3d628290c5d421637e779869c00fba7d
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sun Dec 17 03:45:05 CET 2023
  * telegram: Remove characters '#', '%' and '?' from filenames,
  are not valid for static files in HTTP URLs.
hunk ./utils.py 19
-FILENAME_INVALID_CHARS = re.compile('[/{}<>()"\'\\|&]')
+FILENAME_INVALID_CHARS = re.compile('[/{}<>()"\'\\|&#%?]')
patch 59776df1810cf643a8b7d70ab0502afde6a5c47f
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sun Dec 17 02:49:18 CET 2023
  * telegram: Add compact message IDs to filenames of media,
  this will prevent most of the possible collisions of media with the same filename
hunk ./telegram.py 25
-from utils import sanitize_filename, is_url_equiv, extract_url, get_human_size, get_human_duration, get_highlighted, fix_braces, format_timestamp
+from utils import sanitize_filename, add_filename, is_url_equiv, extract_url, get_human_size, get_human_duration, get_highlighted, fix_braces, format_timestamp
hunk ./telegram.py 572
-            text = await self.handle_webpage(upd_to_webpend, message)
+            text = await self.handle_webpage(upd_to_webpend, message, mid)
hunk ./telegram.py 579
-            final_text = await self.handle_telegram_action(message)
+            final_text = await self.handle_telegram_action(message, mid)
hunk ./telegram.py 672
-    async def handle_telegram_action(self, message):
+    async def handle_telegram_action(self, message, mid):
hunk ./telegram.py 679
-            photo_url = await self.download_telegram_media(message)
+            photo_url = await self.download_telegram_media(message, mid)
hunk ./telegram.py 747
-                return await self.handle_webpage(message.media.webpage, message)
+                return await self.handle_webpage(message.media.webpage, message, mid)
hunk ./telegram.py 834
-            media_url_or_data = await self.download_telegram_media(message, filename, size, relay_attr)
+            media_url_or_data = await self.download_telegram_media(message, mid, filename, size, relay_attr)
hunk ./telegram.py 857
-    async def handle_webpage(self, webpage, message):
+    async def handle_webpage(self, webpage, message, mid):
hunk ./telegram.py 859
-        logo = await self.download_telegram_media(message)
+        logo = await self.download_telegram_media(message, mid)
hunk ./telegram.py 917
-    async def download_telegram_media(self, message, filename=None, size=0, relay_attr=None):
+    async def download_telegram_media(self, message, mid, filename=None, size=0, relay_attr=None):
hunk ./telegram.py 921
-            new_file = sanitize_filename(filename)
+            idd_file = add_filename(filename, mid)
+            new_file = sanitize_filename(idd_file)
hunk ./telegram.py 935
-            new_file = str(self.media_cn) + filetype
+            gen_file = str(self.media_cn) + filetype
+            idd_file = add_filename(gen_file, mid)
+            new_file = sanitize_filename(idd_file)
hunk ./utils.py 86
+def add_filename(filename, add):
+    if add:
+        aux = filename.rsplit('.', 1)
+        name = aux[0]
+        ext = aux[1]
+        return '{}-{}.{}'.format(name, add, ext)
+    else:
+        return filename
+
patch 50155e1ef1b508f47640d4f570da84407efac9cb
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Fri Dec 15 00:00:44 CET 2023
  * telegram: Improve metadata shown for audio and recording/voice media,
  and get filename from them
hunk ./telegram.py 731
-            attrib_file = attrib_video = filename = None
+            attrib_file = attrib_av = filename = None
hunk ./telegram.py 735
-                if isinstance(x, tgty.DocumentAttributeVideo):
-                    attrib_video = x
+                if isinstance(x, tgty.DocumentAttributeVideo) or isinstance(x, tgty.DocumentAttributeAudio):
+                    attrib_av = x
hunk ./telegram.py 741
-            return size, h_size, attrib_video, filename
+            return size, h_size, attrib_av, filename
hunk ./telegram.py 759
-        elif message.audio:        media_type = 'audio'
-        elif message.voice:        media_type = 'rec'
+        elif message.audio:
+            size, h_size, attrib_audio, filename = scan_doc_attributes(message.media.document)
+            dur = get_human_duration(attrib_audio.duration) if attrib_audio else ''
+            per = attrib_audio.performer or ''
+            tit = attrib_audio.title or ''
+            theme = ',{}/{}'.format(per, tit) if per or tit else ''
+            media_type = 'audio:{},{}{}'.format(h_size, dur, theme)
+        elif message.voice:
+            size, _, attrib_audio, filename = scan_doc_attributes(message.media.document)
+            dur = get_human_duration(attrib_audio.duration) if attrib_audio else ''
+            media_type = 'rec:{}'.format(dur)
hunk ./utils.py 133
-    if s > 0: res += str(s) + 's'
+    if s > 0 or duration < 60: res += str(s) + 's'
patch 17b3e7fb0ee1d0153c40740e2934a2030bd9b0ba
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Thu Dec  7 21:16:12 CET 2023
  * README update
hunk ./README.md 35
-several Telegram accounts you will need to run several irgramd instances.
+several Telegram accounts you will need to run several irgramd instances. If
+all IRC clients are disconnected, irgramd will remain connected to Telegram.
+
+irgramd can also be seen as a kind of bouncer ([BNC]), with the difference
+that instead of talking IRC protocol on the client side, it talks Telegram
+protocol (MTProto), and can hide the IP and location of the IRC client (if
+executed in a different host).
hunk ./README.md 139
+[BNC]: https://en.wikipedia.org/wiki/BNC_(software)
patch ec320fd2c408d1239ba7e980e42318e1ab50bb21
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sun Dec  3 00:12:34 CET 2023
  * Correct OpenStreetMap URL in irgramdrc.sample
hunk ./irgramdrc.sample 19
-#geo_url='https://osm.org/search?query={lat}%20{long}#map=15'
+#geo_url='https://osm.org/?mlat={lat}&mlon={long}&zoom=15'
patch 3976770a9424b2085ba1260328b3adb5ce6b1ebe
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sat Dec  2 20:41:44 CET 2023
  * telegram: Add target nick in private messages sent from the self user
  from another Telegram client
hunk ./service.py 80
-                        if id in self.tg.tid_to_iid.keys():
-                            name_in_irc = self.tg.tid_to_iid[id]
-                        else:
-                            name_in_irc = '<Unknown>'
+                        name_in_irc = self.tg.get_irc_name_from_telegram_id(id)
+
hunk ./telegram.py 209
+    def get_irc_name_from_telegram_id(self, tid):
+        if tid in self.tid_to_iid.keys():
+            name_in_irc = self.tid_to_iid[tid]
+        else:
+            name_in_irc = '<Unknown>'
+        return name_in_irc
+
hunk ./telegram.py 588
-        final_text = '[{}] {}{}'.format(mid, refwd_text, text)
+        target_mine = self.handle_target_mine(message.peer_id, user)
+
+        final_text = '[{}] {}{}{}'.format(mid, target_mine, refwd_text, text)
hunk ./telegram.py 835
+    def handle_target_mine(self, target, user):
+        # Add the target of messages sent by self user (me)
+        # received in other clients
+        target_id, target_type = self.get_peer_id_and_type(target)
+        if user is None and target_type == 'user' and target_id != self.id:
+           # self user^
+           # as sender
+            irc_id = self.get_irc_name_from_telegram_id(target_id)
+            target_mine = '[T: {}] '.format(irc_id)
+        else:
+            target_mine = ''
+        return target_mine
+
patch d9d472cb672a8172ee1d54241f20899754fb0640
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Tue Nov 28 23:53:52 CET 2023
  * README update
hunk ./README.md 49
+- Actions [pin message, channel photo] (receive)
patch 9c73d6fcff003a7d9b4f5f90df4f6dc71d84ae9d
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Tue Nov 28 22:38:01 CET 2023
  * service: Add absolute ID as argument to get command
hunk ./service.py 109
-            id = self.tg.mid.id_to_num_offset(peer_id, mid)
+            # If the ID starts with '=' is absolute ID, not compact ID
+            # character '=' is not used by compact IDs
+            if mid[0] == '=':
+                id = int(mid[1:])
+            else:
+                id = self.tg.mid.id_to_num_offset(peer_id, mid)
hunk ./service.py 128
-              '   get <peer> <compact_id>',
-              'Get one message from peer with the compact ID',
+              '   get <peer> <compact_id|=ID>',
+              'Get one message from peer with the compact or absolute ID',
patch c36c361baf23612e31780ba510278ceef611b7ec
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sun Nov 26 23:13:52 CET 2023
  * telegram: Add support for the change of channel photo action
  including download of the new photo
  mapped to CTCP action on IRC
hunk ./telegram.py 668
+        elif isinstance(message.action, tgty.MessageActionChatEditPhoto):
+            _, media_type = self.scan_photo_attributes(message.action.photo)
+            photo_url = await self.download_telegram_media(message)
+            action_text = 'has changed chat [{}] {}'.format(media_type, photo_url)
hunk ./telegram.py 749
-            ph_size = message.media.photo.sizes[-1]
-            if isinstance(ph_size, tgty.PhotoSizeProgressive):
-                size = ph_size.sizes[-1]
-            else:
-                size = ph_size.size
-            if hasattr(ph_size, 'w') and hasattr(ph_size, 'h'):
-                media_type = 'photo:{}x{}'.format(ph_size.w, ph_size.h)
-            else:
-                media_type = 'photo'
+            size, media_type = self.scan_photo_attributes(message.media.photo)
hunk ./telegram.py 867
+    def scan_photo_attributes(self, photo):
+        size = 0
+        sizes = photo.sizes
+        ph_size = sizes[-1]
+        if isinstance(ph_size, tgty.PhotoSizeProgressive):
+            size = ph_size.sizes[-1]
+        else:
+            for x in sizes:
+                if isinstance(x, tgty.PhotoSize):
+                    if x.size > size:
+                        size = x.size
+                        ph_size = x
+        if hasattr(ph_size, 'w') and hasattr(ph_size, 'h'):
+            media_type = 'photo:{}x{}'.format(ph_size.w, ph_size.h)
+        else:
+            media_type = 'photo'
+
+        return size, media_type
+
patch 9c00648eaf7c048826dc4fe6bdbd509eabe57f47
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sun Nov 26 20:07:44 CET 2023
  * telegram: Add support for pin message action
  irc: Add support for sending CTCP actions (me) to map actions from Telegram
hunk ./irc.py 473
+    async def send_action(self, source, target, message):
+        action_message = '\x01ACTION {}\x01'.format(message)
+        await self.send_msg(source, target, action_message)
+
hunk ./telegram.py 556
-        text_send = self.set_history_timestamp(text, history, msg.date)
+        text_send = self.set_history_timestamp(text, history, msg.date, msg.action)
hunk ./telegram.py 571
-        if message.is_reply:
+        if message.action:
+            final_text = await self.handle_telegram_action(message)
+            return final_text
+        elif message.is_reply:
hunk ./telegram.py 585
-    def set_history_timestamp(self, text, history, date):
+    def set_history_timestamp(self, text, history, date, action):
hunk ./telegram.py 588
-            res = '{} {}'.format(timestamp, text)
+            if action:
+                res = '{} {}'.format(text, timestamp)
+            else:
+                res = '{} {}'.format(timestamp, text)
hunk ./telegram.py 598
+        action = (message and message.action)
hunk ./telegram.py 600
-            await self.relay_telegram_private_message(user, text)
+            await self.relay_telegram_private_message(user, text, action)
hunk ./telegram.py 603
-            chan = await self.relay_telegram_channel_message(message, user, text, channel)
+            chan = await self.relay_telegram_channel_message(message, user, text, channel, action)
hunk ./telegram.py 606
-    async def relay_telegram_private_message(self, user, message):
+    async def relay_telegram_private_message(self, user, message, action=None):
hunk ./telegram.py 609
-        await self.irc.send_msg(user, None, message)
-
-    async def relay_telegram_channel_message(self, message, user, text, channel=None):
+        if action:
+            await self.irc.send_action(user, None, message)
+        else:
+            await self.irc.send_msg(user, None, message)
+
+    async def relay_telegram_channel_message(self, message, user, text, channel, action):
hunk ./telegram.py 622
-        await self.irc.send_msg(user, chan, text)
+
+        if action:
+            await self.irc.send_action(user, chan, text)
+        else:
+            await self.irc.send_msg(user, chan, text)
+
hunk ./telegram.py 663
+    async def handle_telegram_action(self, message):
+        if isinstance(message.action, tgty.MessageActionPinMessage):
+            replied = await message.get_reply_message()
+            cid = self.mid.num_to_id_offset(replied.peer_id, replied.id)
+            action_text = 'has pinned message [{}]'.format(cid)
+        else:
+            action_text = ''
+        return action_text
+
patch 583b2fd653aa6786aba38cb9a00c6e990f3a1d11
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sun Nov 19 00:54:47 CET 2023
  * telegram: Change add symbol in editions from "_" to "+"
hunk ./utils.py 180
-                res += '_{}_ '.format(i[2:])
+                res += '+{}+ '.format(i[2:])
patch 8167f7dfd2712a9a103504fe7763e2a497adf723
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sun Nov 19 00:43:04 CET 2023
  * telegram: Refactor download media code.
  If named media files already downloaded, don't try to download again
hunk ./telegram.py 691
+        filename = None
+
+        def scan_doc_attributes(document):
+            attrib_file = attrib_video = filename = None
+            size = document.size
+            h_size = get_human_size(size)
+            for x in document.attributes:
+                if isinstance(x, tgty.DocumentAttributeVideo):
+                    attrib_video = x
+                if isinstance(x, tgty.DocumentAttributeFilename):
+                    attrib_file = x
+            filename = attrib_file.file_name if attrib_file else None
+
+            return size, h_size, attrib_video, filename
hunk ./telegram.py 733
-            size = message.media.document.size
-            h_size = get_human_size(size)
-            attrib = next(x for x in message.media.document.attributes if isinstance(x, tgty.DocumentAttributeVideo))
-            dur = get_human_duration(attrib.duration)
+            size, h_size, attrib_video, filename = scan_doc_attributes(message.media.document)
+            dur = get_human_duration(attrib_video.duration) if attrib_video else ''
hunk ./telegram.py 740
-            size = message.media.document.size
-            h_size = get_human_size(size)
+            size, h_size, _, filename = scan_doc_attributes(message.media.document)
hunk ./telegram.py 795
-            if self.download and size > self.notice_size:
-                await self.relay_telegram_message(message, user, '[{}] [{}] [Downloading]'.format(mid, media_type))
-
-            media_url_or_data = await self.download_telegram_media(message)
+            relay_attr = (message, user, mid, media_type)
+            media_url_or_data = await self.download_telegram_media(message, filename, size, relay_attr)
hunk ./telegram.py 847
-    async def download_telegram_media(self, message):
-        local_path = None
-        if self.download:
+    async def download_telegram_media(self, message, filename=None, size=0, relay_attr=None):
+        if not self.download:
+            return ''
+        if filename:
+            new_file = sanitize_filename(filename)
+            new_path = os.path.join(self.telegram_media_dir, new_file)
+            if os.path.exists(new_path):
+                local_path = new_path
+            else:
+                await self.notice_downloading(size, relay_attr)
+                local_path = await message.download_media(new_path)
+                if not local_path: return ''
+        else:
+            await self.notice_downloading(size, relay_attr)
hunk ./telegram.py 862
-        if not local_path: return ''
-
-        if message.document:
-            new_file = sanitize_filename(os.path.basename(local_path))
-        else:
+            if not local_path: return ''
hunk ./telegram.py 866
-
-        new_path = os.path.join(self.telegram_media_dir, new_file)
+            new_path = os.path.join(self.telegram_media_dir, new_file)
+
hunk ./telegram.py 874
+    async def notice_downloading(self, size, relay_attr):
+        if relay_attr and size > self.notice_size:
+            message, user, mid, media_type = relay_attr
+            await self.relay_telegram_message(message, user, '[{}] [{}] [Downloading]'.format(mid, media_type))
+
patch 24a50b886f4cef2e1da003b74921982f5da0941c
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Mon Oct 16 00:24:53 CEST 2023
  * telegram: Add option "download_notice" to show the start of a big download
hunk ./irgramd 77
+    tornado.options.define('download_notice', default=10, metavar='SIZE (MiB)', help='Enable a notice when a download starts if its size is greater than SIZE, this is useful when a download takes some time to be completed')
hunk ./telegram.py 42
+        self.notice_size = settings['download_notice'] * 1048576
hunk ./telegram.py 555
-        text = await self.render_text(msg, mid, upd_to_webpend)
+        text = await self.render_text(msg, mid, upd_to_webpend, user)
hunk ./telegram.py 563
-    async def render_text(self, message, mid, upd_to_webpend, history=False):
+    async def render_text(self, message, mid, upd_to_webpend, user=None):
hunk ./telegram.py 567
-            text = await self.handle_telegram_media(message)
+            text = await self.handle_telegram_media(message, user, mid)
hunk ./telegram.py 686
-    async def handle_telegram_media(self, message):
+    async def handle_telegram_media(self, message, user, mid):
hunk ./telegram.py 690
+        size = 0
hunk ./telegram.py 707
-            size = message.media.photo.sizes[-1]
-            if hasattr(size, 'w') and hasattr(size, 'h'):
-                media_type = 'photo:{}x{}'.format(size.w, size.h)
+            ph_size = message.media.photo.sizes[-1]
+            if isinstance(ph_size, tgty.PhotoSizeProgressive):
+                size = ph_size.sizes[-1]
hunk ./telegram.py 711
+                size = ph_size.size
+            if hasattr(ph_size, 'w') and hasattr(ph_size, 'h'):
+                media_type = 'photo:{}x{}'.format(ph_size.w, ph_size.h)
+            else:
hunk ./telegram.py 719
-            size = get_human_size(message.media.document.size)
+            size = message.media.document.size
+            h_size = get_human_size(size)
hunk ./telegram.py 723
-            media_type = 'video:{},{}'.format(size, dur)
+            media_type = 'video:{},{}'.format(h_size, dur)
hunk ./telegram.py 728
-            size = get_human_size(message.media.document.size)
-            media_type = 'file:{}'.format(size)
+            size = message.media.document.size
+            h_size = get_human_size(size)
+            media_type = 'file:{}'.format(h_size)
hunk ./telegram.py 784
+            if self.download and size > self.notice_size:
+                await self.relay_telegram_message(message, user, '[{}] [{}] [Downloading]'.format(mid, media_type))
+
patch 1fb88a2962a05a4a46ccec54dc3ae3b65f73ab78
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sun Oct 15 22:06:49 CEST 2023
  * telegram: Add option "download_media" to control if media files must be downloaded
hunk ./irgramd 76
+    tornado.options.define('download_media', default=True, help='Enable download of any media (photos, documents, etc.), if not set only a message of media will be shown')
hunk ./telegram.py 41
+        self.download   = settings['download_media']
hunk ./telegram.py 828
-        local_path = await message.download_media(self.telegram_media_dir)
+        local_path = None
+        if self.download:
+            local_path = await message.download_media(self.telegram_media_dir)
patch a62e1a9973921a97198519dd5a74e1a4472b9364
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Wed Oct 11 00:35:28 CEST 2023
  * telegram: Support for showing question and options of polls
hunk ./README.md 48
+- Polls (receive, show)
hunk ./telegram.py 761
-            media_url_or_data = ''
+            media_url_or_data = self.handle_poll(message.media.poll)
hunk ./telegram.py 779
+    def handle_poll(self, poll):
+        text = poll.question
+        for ans in poll.answers:
+            text += '\n* ' + ans.text
+        return text
+
patch 400207de8aa3ca0b2dfc8ff1eeaafeddc8221070
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sun Sep 17 20:57:11 CEST 2023
  * README update
hunk ./README.md 49
+- History
patch 2ab08fbae8ec48e123e86a498c287e7afcd173e1
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sat Jul 22 20:32:19 CEST 2023
  * telegram: Add compact ID to all replies
hunk ./telegram.py 647
+        space = ' '
hunk ./telegram.py 651
+        cid = self.mid.num_to_id_offset(replied.peer_id, replied.id)
hunk ./telegram.py 653
-            replied_msg = '[{}]'.format(self.mid.num_to_id_offset(replied.peer_id, replied.id))
+            replied_msg = ''
+            space = ''
hunk ./telegram.py 665
-        return '|Re {}: {}{}| '.format(replied_nick, replied_msg, trunc)
+        return '|Re {}: [{}]{}{}{}| '.format(replied_nick, cid, space, replied_msg, trunc)
patch 505cd255e7a1cea83385a48467c6d1d099b3bc5d
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Fri Jul 21 01:12:18 CEST 2023
  * telegram: Add option for geo custom URL
hunk ./irgramd 77
+    tornado.options.define('geo_url', type=str, default=None, metavar='TEMPLATE_URL', help='Use custom URL for showing geo latitude/longitude location, eg. OpenStreetMap')
hunk ./irgramdrc.sample 18
+#geo url OpenStreetMap
+#geo_url='https://osm.org/search?query={lat}%20{long}#map=15'
+#geo url Google Maps
+#geo_url='https://maps.google.com/?q={lat},{long}'
hunk ./telegram.py 54
+        self.geo_url    = settings['geo_url']
hunk ./telegram.py 741
-            media_url_or_data = 'lat: {}, long: {}'.format(message.media.geo.lat, message.media.geo.long)
+            if self.geo_url:
+                geo_url = ' | ' + self.geo_url
+            else:
+                geo_url = ''
+            lat_long_template = 'lat: {lat}, long: {long}' + geo_url
+            media_url_or_data = lat_long_template.format(lat=message.media.geo.lat, long=message.media.geo.long)
patch f83be7c1c04d7dcb5ebd0119895eafbc6d784ecb
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Thu Jul 20 04:46:06 CEST 2023
  * README update
hunk ./README.md 45
-- Deletions (receive)
+- Deletions (receive, do)
patch ee2b02319b5759d67df9fcc08fdafbd57a8fcc80
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Mon Jul 17 02:37:21 CEST 2023
  * exclam: Add delete (!del) command to delete messages
hunk ./exclam.py 19
+            '!del':       (self.handle_command_del,                   1,  1, -1),
hunk ./exclam.py 37
-        chk_msg = await self.tg.telegram_client.get_messages(entity=self.tmp_telegram_id, ids=id)
+        if id is None:
+            chk_msg = None
+        else:
+            chk_msg = await self.tg.telegram_client.get_messages(entity=self.tmp_telegram_id, ids=id)
hunk ./exclam.py 85
+            )
+        return reply
+
+    async def handle_command_del(self, cid=None, help=None):
+        if not help:
+            id, del_msg = await self.check_msg(cid)
+            if del_msg is not None:
+                deleted = await self.tg.telegram_client.delete_messages(self.tmp_telegram_id, del_msg)
+                if deleted[0].pts_count == 0:
+                    reply = ('Not possible to delete',)
+                else:
+                    self.tmp_tg_msg = None
+                    reply = None
+            else:
+                reply = ('Unknown message to delete',)
+        else: # HELP.brief or HELP.desc (first line)
+            reply = ('   !del        Delete a message',)
+        if help == HELP.desc:  # rest of HELP.desc
+            reply += \
+            (
+              '   !del <compact_id>',
+              'Delete a message with <compact_id> on current channel/chat'
patch 1de628ed1063db8129e0c142d5fe08907b27930a
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sun Jul 16 20:09:53 CEST 2023
  * README update
hunk ./README.md 112
-Copyright (c) 2019 Peter Bui <pbui@bx612.space> [_$_]
+Copyright (c) 2019 Peter Bui <pbui@bx612.space>  [_$_]
patch cbe1c8b9a949b8eb5b466496eb7eab3b2f3874bd
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sun Jul 16 20:02:03 CEST 2023
  * README update
hunk ./README.md 46
-- Editions (receive)
+- Editions (receive, do)
hunk ./README.md 112
-Copyright (c) 2019 Peter Bui <pbui@bx612.space>
+Copyright (c) 2019 Peter Bui <pbui@bx612.space> [_$_]
patch fe0c125aa83d95b5472c0bc599e883c9b47213f6
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Tue Jul 11 23:54:41 CEST 2023
  * exclam: Fix indentation
hunk ./exclam.py 43
-              self.tmp_tg_msg = await self.tg.telegram_client.send_message(self.tmp_telegram_id, msg, reply_to=id)
-              reply = True
+                self.tmp_tg_msg = await self.tg.telegram_client.send_message(self.tmp_telegram_id, msg, reply_to=id)
+                reply = True
hunk ./exclam.py 46
-              reply = ('Unknown message to reply',)
+                reply = ('Unknown message to reply',)
hunk ./exclam.py 62
-              try:
-                  self.tmp_tg_msg = await self.tg.telegram_client.edit_message(ed_msg, new_msg)
-              except MessageNotModifiedError:
-                  self.tmp_tg_msg = ed_msg
-                  reply = True
-              except MessageAuthorRequiredError:
-                  reply = ('Not the author of the message to edit',)
-              else:
-                  reply = True
+                try:
+                    self.tmp_tg_msg = await self.tg.telegram_client.edit_message(ed_msg, new_msg)
+                except MessageNotModifiedError:
+                    self.tmp_tg_msg = ed_msg
+                    reply = True
+                except MessageAuthorRequiredError:
+                    reply = ('Not the author of the message to edit',)
+                else:
+                    reply = True
hunk ./exclam.py 72
-              reply = ('Unknown message to edit',)
+                reply = ('Unknown message to edit',)
patch 74e097435eeb72086c6ed3f43a98ce3f5463d1c6
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Tue Jul 11 01:48:40 CEST 2023
  * exclam: Add edit (!ed) command to modify already sent messages
hunk ./exclam.py 9
+from telethon.errors.rpcerrorlist import MessageNotModifiedError, MessageAuthorRequiredError
+
hunk ./exclam.py 18
+            '!ed':        (self.handle_command_ed,                    2,  2,  2),
hunk ./exclam.py 33
+
+    async def check_msg(self, cid):
+        id = self.tg.mid.id_to_num_offset(self.tmp_telegram_id, cid)
+        chk_msg = await self.tg.telegram_client.get_messages(entity=self.tmp_telegram_id, ids=id)
+        return id, chk_msg
hunk ./exclam.py 41
-            id = self.tg.mid.id_to_num_offset(self.tmp_telegram_id, cid)
-            chk_msg = await self.tg.telegram_client.get_messages(entity=self.tmp_telegram_id, ids=id)
+            id, chk_msg = await self.check_msg(cid)
hunk ./exclam.py 55
+            )
+        return reply
+
+    async def handle_command_ed(self, cid=None, new_msg=None, help=None):
+        if not help:
+            id, ed_msg = await self.check_msg(cid)
+            if ed_msg is not None:
+              try:
+                  self.tmp_tg_msg = await self.tg.telegram_client.edit_message(ed_msg, new_msg)
+              except MessageNotModifiedError:
+                  self.tmp_tg_msg = ed_msg
+                  reply = True
+              except MessageAuthorRequiredError:
+                  reply = ('Not the author of the message to edit',)
+              else:
+                  reply = True
+            else:
+              reply = ('Unknown message to edit',)
+        else: # HELP.brief or HELP.desc (first line)
+            reply = ('   !ed         Edit a message',)
+        if help == HELP.desc:  # rest of HELP.desc
+            reply += \
+            (
+              '   !ed <compact_id> <new_message>',
+              'Edit a message with <compact_id> on current channel/chat,',
+              '<new_message> replaces the current message.'
patch a8d4d79f7ef62a0938f42c2895c99e5d9341d50c
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Mon Jul 10 00:56:15 CEST 2023
  * README update
hunk ./README.md 60
+## Instalation
+
+### From darcs
+
+    darcs clone https://src.presi.org/repos/darcs/irgramd
+    chmod +x irgramd/irgramd
+
+### From git
+
+    git clone https://github.com/prsai/irgramd.git
+    chmod +x irgramd/irgramd
+
+## Configuration
+
+From irgramd directory `./irgramd --help` will show all configuration
+options available, these options can be used directy in the command line or
+in a file.
+
+When used in command line the separator is `-` (dash) with two leading
+dashes, example: `--api-hash`.
+
+When used in a file the separator is `_` (underscore) without two leading
+dashes nor underscores, example: `api_hash`. The syntax of this file is just
+Python so strings are surrounded by quotes (`'`) and lists by brackets (`[]`).
+
+A sample of the configuration file is provided, copy it to the default
+configuration location:
+
+    mkdir -p  ~/.config/irgramd
+    cp irgramd/irgramdrc.sample ~/.config/irgramd/irgramdrc
+
+And modified it with your API IDs and preferences.
+
+## Usage
+
+From irgramd directory, in foreground:
+
+    ./irgramd
+
+In background (without logs):
+
+    ./irgramd --logging=none &
+
hunk ./README.md 107
-(e.g.  in Linux be in the shadow group or equivalent). The dependency is
+(e.g. in Linux be in the shadow group or equivalent). The dependency is
hunk ./README.md 112
-Copyright (c) 2019 Peter Bui <pbui@bx612.space>  [_$_]
+Copyright (c) 2019 Peter Bui <pbui@bx612.space>
patch 93ee900b41a7ba1e912d3897d242f714e866de09
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Mon Jul 10 00:52:02 CEST 2023
  * Add configuration file sample (irgramdrc.sample)
addfile ./irgramdrc.sample
hunk ./irgramdrc.sample 1
+api_id=XXXXXX
+api_hash='XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
+phone='XXXXXXXXXXX'
+#ask_code=True
+#irc_address='0.0.0.0'
+#tls=True
+#tls_cert='/path/to/certificate'
+#tls_key='/path/to/key'
+irc_nicks=['XXXXX', 'XXXXXX']
+media_url='https://server/token/'
+#pam=True
+#pam_group='XXXXX'
+#char_in_encoding='iso-8859-1'
+#char_out_encoding='iso-8859-1'
+#emoji_ascii=True
+hist_timestamp_format='[%m-%d %H:%M]'
+#timezone='Europe/Madrid'
patch 274ec21ecc7d6c159237df2648668f54b65d5332
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sat Jul  8 01:58:26 CEST 2023
  * telegram, irc: Refactor and improve routine for conversion of mentions
hunk ./telegram.py 66
+        self.sorted_len_usernames = []
hunk ./telegram.py 136
+        self.add_sorted_len_usernames(self.tg_username)
hunk ./telegram.py 152
+                self.add_sorted_len_usernames(tg_ni)
hunk ./telegram.py 409
-        def repl_mentioned(text, me_nick, received, mark, index, rargs):
-            if text and text[index] == mark:
+        def repl_mentioned(text, me_nick, received, mark, repl_pref, repl_suff):
+            new_text = text
+
+            for user in self.sorted_len_usernames:
+                if user == self.tg_username:
+                    if me_nick:
+                        username = me_nick
+                    else:
+                        continue
+                else:
+                    username = self.irc.users[user].irc_nick
+
hunk ./telegram.py 422
-                    subtext = text[1:]
-                else:
-                    subtext = text[:-1]
-                part = subtext.lower()
-                if me_nick and part == self.tg_username:
-                    return replacement(me_nick, **rargs)
-                if part in self.irc.users:
-                    return replacement(self.irc.users[part].irc_nick, **rargs)
-            return text
-
-        def replacement(nick, repl_pref, repl_suff):
-            return '{}{}{}'.format(repl_pref, nick, repl_suff)
+                    mention = mark + user
+                    mention_case = mark + username
+                else: # sent
+                    mention = user + mark
+                    mention_case = username + mark
+                replcmnt = repl_pref + username + repl_suff
+
+                # Start of the text
+                for ment in (mention, mention_case):
+                    if new_text.startswith(ment):
+                        new_text = new_text.replace(ment, replcmnt, 1)
+
+                # Next words (with space as separator)
+                mention = ' ' + mention
+                mention_case = ' ' + mention_case
+                replcmnt = ' ' + replcmnt
+                new_text = new_text.replace(mention, replcmnt).replace(mention_case, replcmnt)
+
+            return new_text
hunk ./telegram.py 444
-            index = 0
hunk ./telegram.py 446
-        else:
+        else: # sent
hunk ./telegram.py 448
-            index = -1
hunk ./telegram.py 452
-            words = text.split(' ')
-            words_replaced = [repl_mentioned(elem, me_nick, received, mark, index, rargs) for elem in words]
-            text_replaced = ' '.join(words_replaced)
+            text_replaced = repl_mentioned(text, me_nick, received, mark, **rargs)
hunk ./telegram.py 462
+    def add_sorted_len_usernames(self, username):
+        self.sorted_len_usernames.append(username)
+        self.sorted_len_usernames.sort(key=lambda k: len(k), reverse=True)
+
patch 153aba3773e6ca3c7c505212ac1aeac19f15e68d
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Tue Jun 27 03:00:57 CEST 2023
  * Fix typos in README
hunk ./README.md 64
-(e.g. in Linux be in the shadow group or equivalent).
-The dependency is totally optional, if not use, the module pyPAM is no
-needed.
+(e.g.  in Linux be in the shadow group or equivalent). The dependency is
+totally optional, if not used, the module pyPAM is not needed.
patch 7225855530de2f42820f61b5a8f1083269aab749
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Mon Jun 26 22:55:35 CEST 2023
  * README update
hunk ./README.md 58
+- [pyPAM] (optional, tested with v0.4.2-13.4 from deb, [legacy web](https://web.archive.org/web/20110316070059/http://www.pangalactic.org/PyPAM/))
+
+## Notes
+
+PAM authentication: it allows to authenticate IRC users from the system in
+Unix/Linux. The user that executes irgramd must have permissions to use PAM
+(e.g. in Linux be in the shadow group or equivalent).
+The dependency is totally optional, if not use, the module pyPAM is no
+needed.
hunk ./README.md 87
+[pyPAM]: https://packages.debian.org/bullseye/python3-pam
patch 1b816235b1b9baeae5f394f01c379bbb2e0136ce
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Mon Jun 26 22:23:59 CEST 2023
  * README update
hunk ./README.md 43
-- Replies (receive)
+- Replies (receive, send)
patch ae837b8af1788904bfa4ed7430d331028475e8e3
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Mon Jun 26 00:29:50 CEST 2023
  * Remove trailing spaces
hunk ./service.py 115
-                reply = ('Message not found',)            [_$_]
+                reply = ('Message not found',)
patch 5b2c938f7967a3345435fe21b2127999d0975f50
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Mon Jun 26 00:17:22 CEST 2023
  * Add exclam module to handle channel/chat commands begining with exclamation mark (!)
  Implement reply (!re) as first exclam command
addfile ./exclam.py
hunk ./exclam.py 1
+# irgramd: IRC-Telegram gateway
+# exclam.py: IRC exclamation command handlers
+#
+# Copyright (c) 2023 E. Bosch <presidev@AT@gmail.com>
+#
+# Use of this source code is governed by a MIT style license that
+# can be found in the LICENSE file included in this project.
+
+from utils import command, HELP
+
+class exclam(command):
+    def __init__(self, telegram):
+        self.commands = \
+        { # Command         Handler                       Arguments  Min Max Maxsplit
+            '!re':        (self.handle_command_re,                    2,  2,  2),
+        }
+        self.tg = telegram
+        self.irc = telegram.irc
+        self.tmp_ircnick = None
+        self.tmp_telegram_id = None
+        self.tmp_tg_msg = None
+
+    async def command(self, message, telegram_id, user):
+        self.tmp_telegram_id = telegram_id
+        res = await self.parse_command(message, nick=None)
+        if isinstance(res, tuple):
+            await self.irc.send_msg(self.irc.service_user, None, res[0], user)
+            res = False
+        return res, self.tmp_tg_msg
+
+    async def handle_command_re(self, cid=None, msg=None, help=None):
+        if not help:
+            id = self.tg.mid.id_to_num_offset(self.tmp_telegram_id, cid)
+            chk_msg = await self.tg.telegram_client.get_messages(entity=self.tmp_telegram_id, ids=id)
+            if chk_msg is not None:
+              self.tmp_tg_msg = await self.tg.telegram_client.send_message(self.tmp_telegram_id, msg, reply_to=id)
+              reply = True
+            else:
+              reply = ('Unknown message to reply',)
+        else: # HELP.brief or HELP.desc (first line)
+            reply = ('   !re         Reply to a message',)
+        if help == HELP.desc:  # rest of HELP.desc
+            reply += \
+            (
+              '   !re <compact_id> <message>',
+              'Reply with <message> to a message with <compact_id> on current',
+              'channel/chat.'
+            )
+        return reply
hunk ./irc.py 25
+from exclam import exclam
hunk ./irc.py 113
+        self.exclam = exclam(self.tg)
hunk ./irc.py 413
-            tg_msg = await self.tg.telegram_client.send_message(telegram_id, message)
-
-            mid = self.tg.mid.num_to_id_offset(telegram_id, tg_msg.id)
-            text = '[{}] {}'.format(mid, message)
-            self.tg.to_cache(tg_msg.id, mid, text, message, user, chan, media=None)
-
-            if defered_send:
-                await defered_send(user, defered_target, text)
+            if message[0] == '!':
+                cont, tg_msg = await self.exclam.command(message, telegram_id, user)
+            else:
+                tg_msg = await self.tg.telegram_client.send_message(telegram_id, message)
+                cont = True
+            if cont:
+                mid = self.tg.mid.num_to_id_offset(telegram_id, tg_msg.id)
+                text = '[{}] {}'.format(mid, message)
+                self.tg.to_cache(tg_msg.id, mid, text, message, user, chan, media=None)
+
+                if defered_send:
+                    await defered_send(user, defered_target, text)
hunk ./service.py 9
-from utils import compact_date, command
+from utils import compact_date, command, HELP
hunk ./service.py 15
-        { # Command         Handler                       Arguments  Min Max
-            'code':        (self.handle_command_code,                 1,  1),
-            'dialog':      (self.handle_command_dialog,               1,  2),
-            'get':         (self.handle_command_get,                  2,  2),
-            'help':        (self.handle_command_help,                 0,  1),
-            'history':     (self.handle_command_history,              1,  3),
-            'mark_read':   (self.handle_command_mark_read,            1,  1),
+        { # Command         Handler                       Arguments  Min Max Maxsplit
+            'code':        (self.handle_command_code,                 1,  1, -1),
+            'dialog':      (self.handle_command_dialog,               1,  2, -1),
+            'get':         (self.handle_command_get,                  2,  2, -1),
+            'help':        (self.handle_command_help,                 0,  1, -1),
+            'history':     (self.handle_command_history,              1,  3, -1),
+            'mark_read':   (self.handle_command_mark_read,            1,  1, -1),
hunk ./service.py 147
+              'The commands begining with ! (exclamation) must be used directly',
+              'in channels or chats. The following ! commands are available:',
+            )
+            for command in self.irc.exclam.commands.values():
+                handler = command[0]
+                help_text += await handler(help=HELP.brief)
+            help_text += \
+            (
hunk ./service.py 159
-        elif help_command in self.commands.keys():
-            handler = self.commands[help_command][0]
+        elif help_command in (all_commands := dict(**self.commands, **self.irc.exclam.commands)).keys():
+            handler = all_commands[help_command][0]
hunk ./service.py 261
-
-class HELP:
-    desc = 1
-    brief = 2
hunk ./utils.py 26
-        words = line.split()
-        command = words.pop(0).lower()
+        command = line.partition(' ')[0].lower()
hunk ./utils.py 29
-            handler, min_args, max_args = self.commands[command]
+            handler, min_args, max_args, maxsplit = self.commands[command]
+            words = line.split(maxsplit=maxsplit)[1:]
hunk ./utils.py 41
+class HELP:
+    desc = 1
+    brief = 2
+
patch cbec6bc5a68bcadaaea6113355f12056d10cf577
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Fri Jun 23 23:49:58 CEST 2023
  * telegram: Fix: in forwards when the original user is unknown use the ID
hunk ./telegram.py 212
-                user = self.get_irc_user_from_telegram(peer_id)
-                if user is None:
-                    name = '{}'
-                    self.refwd_me = True
+                try:
+                    user = self.get_irc_user_from_telegram(peer_id)
+                except:
+                    name = str(peer_id)
hunk ./telegram.py 217
-                    name = user.irc_nick
+                    if user is None:
+                        name = '{}'
+                        self.refwd_me = True
+                    else:
+                        name = user.irc_nick
patch dd182f24b1eb76b357ab4ab84363ca55669f7b97
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Thu Jun 22 21:57:16 CEST 2023
  * Move parse_command() to a new class, the code will be reused for the future
  exclam module
hunk ./service.py 9
-from utils import compact_date
+from utils import compact_date, command
hunk ./service.py 12
-class service:
+class service(command):
hunk ./service.py 28
-    async def parse_command(self, line, nick):
-
-        words = line.split()
-        command = words.pop(0).lower()
-        self.tmp_ircnick = nick
-        if command in self.commands.keys():
-            handler, min_args, max_args = self.commands[command]
-            num_words = len(words)
-            if num_words < min_args or num_words > max_args:
-                reply = ('Wrong number of arguments',)
-            else:
-                reply = await handler(*words)
-        else:
-            reply = ('Unknown command',)
-
-        return reply
-
hunk ./utils.py 24
+class command:
+    async def parse_command(self, line, nick):
+        words = line.split()
+        command = words.pop(0).lower()
+        self.tmp_ircnick = nick
+        if command in self.commands.keys():
+            handler, min_args, max_args = self.commands[command]
+            num_words = len(words)
+            if num_words < min_args or num_words > max_args:
+                reply = ('Wrong number of arguments',)
+            else:
+                reply = await handler(*words)
+        else:
+            reply = ('Unknown command',)
+
+        return reply
+
patch 3ec451218b80bdd3c409b4aec91ae94ef4fa5c38
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Thu Jun 15 20:08:03 CEST 2023
  * telegram, irc: Add conversion of "mention:" (IRC style) to "@mention" in sent messages
hunk ./irc.py 409
+            message = self.tg.replace_mentions(message, me_nick='', received=False)
hunk ./telegram.py 398
-    def replace_mentions(self, text, me_nick=''):
-        def repl_mentioned(text, me_nick):
-            if text and text[0] == '@':
-                part = text[1:].lower()
+    def replace_mentions(self, text, me_nick='', received=True):
+        # For received replace @mention to ~mention~
+        # For sent replace mention: to @mention
+        rargs = {}
+        def repl_mentioned(text, me_nick, received, mark, index, rargs):
+            if text and text[index] == mark:
+                if received:
+                    subtext = text[1:]
+                else:
+                    subtext = text[:-1]
+                part = subtext.lower()
hunk ./telegram.py 410
-                    return replacement(me_nick)
+                    return replacement(me_nick, **rargs)
hunk ./telegram.py 412
-                    return replacement(self.irc.users[part].irc_nick)
+                    return replacement(self.irc.users[part].irc_nick, **rargs)
hunk ./telegram.py 415
-        def replacement(nick):
-            return '{}{}{}'.format('~',nick, '~')
-
-        if text.find('@') != -1:
+        def replacement(nick, repl_pref, repl_suff):
+            return '{}{}{}'.format(repl_pref, nick, repl_suff)
+
+        if received:
+            mark = '@'
+            index = 0
+            rargs['repl_pref'] = '~'
+            rargs['repl_suff'] = '~'
+        else:
+            mark = ':'
+            index = -1
+            rargs['repl_pref'] = '@'
+            rargs['repl_suff'] = ''
+
+        if text.find(mark) != -1:
hunk ./telegram.py 431
-            words_replaced = [repl_mentioned(elem, me_nick) for elem in words]
+            words_replaced = [repl_mentioned(elem, me_nick, received, mark, index, rargs) for elem in words]
patch eb5b15eba474680cab8764d0242229617d960e38
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Thu Jun 15 01:15:43 CEST 2023
  * irc: Add log for user registered (authorized in IRC)
hunk ./irc.py 143
-        self.service_user = IRCUser(None, ('Services',), self.conf['service_user'],
+        self.service_user = IRCUser(None, ('Services',''), self.conf['service_user'],
hunk ./irc.py 430
+        self.logger.info('Registered IRC user "%s" from %s:%s', user.irc_nick, user.address, user.port)
+
hunk ./irc.py 645
+        self.port = str(address[1])
hunk ./telegram.py 148
-                irc_user = IRCUser(None, ('Telegram',), tg_nick, user.id, self.get_telegram_display_name(user))
+                irc_user = IRCUser(None, ('Telegram',''), tg_nick, user.id, self.get_telegram_display_name(user))
patch 5da8c71bd90c689771ee2afddbb6f16a663c7c35
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Wed Jun 14 00:43:05 CEST 2023
  * emoji2emoticon: Add emoji "thinking face" to convert to ASCII
hunk ./emoji2emoticon.py 87
+        '\U0001f914': '":-L"',
patch 38a5f5ab57d1d819c40ed65f00e86c2cf6a9042f
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sun Jun 11 22:27:57 CEST 2023
  * emoji2emoticon: Add emoji "shushing face" to convert to ASCII
hunk ./emoji2emoticon.py 87
+        '\U0001f92b': '":-o-m"',
patch 746b8dec8a7b53ef2750501c320aaa627d5ae7fe
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sun Jun 11 00:44:55 CEST 2023
  * irc: Separate character encoding options as input and output
hunk ./irc.py 85
-            message = message.decode(self.conf['char_encoding'], errors='replace').replace('\r','\n')
+            message = message.decode(self.conf['char_in_encoding'], errors='replace').replace('\r','\n')
hunk ./irc.py 150
-        user.stream.write(command.encode(self.conf['char_encoding'], errors='replace'))
+        user.stream.write(command.encode(self.conf['char_out_encoding'], errors='replace'))
hunk ./irgramd 72
-    tornado.options.define('char_encoding', default='utf-8', metavar='ENCODING', help='Character encoding for IRC')
+    tornado.options.define('char_in_encoding', default='utf-8', metavar='ENCODING', help='Character input encoding for IRC')
+    tornado.options.define('char_out_encoding', default='utf-8', metavar='ENCODING', help='Character output encoding for IRC')
patch 7300fb2b6fde17386971ea497343afae8526fb4c
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sat Jun 10 22:31:22 CEST 2023
  * telegram, irc: Add conversion of mentions for self @username as well as
  other mentions in self messages [saved messages]
hunk ./irc.py 471
+        # replace self @username and other mentions for self messages sent by this instance of irgramd
+        msg = self.tg.replace_mentions(msg, user.irc_nick)
+
hunk ./telegram.py 398
-    def replace_mentions(self, text):
-        def repl_mentioned(text):
+    def replace_mentions(self, text, me_nick=''):
+        def repl_mentioned(text, me_nick):
hunk ./telegram.py 402
+                if me_nick and part == self.tg_username:
+                    return replacement(me_nick)
hunk ./telegram.py 405
-                    return '{}{}{}'.format('~', self.irc.users[part].irc_nick, '~')
+                    return replacement(self.irc.users[part].irc_nick)
hunk ./telegram.py 408
+        def replacement(nick):
+            return '{}{}{}'.format('~',nick, '~')
+
hunk ./telegram.py 413
-            words_replaced = [repl_mentioned(elem) for elem in words]
+            words_replaced = [repl_mentioned(elem, me_nick) for elem in words]
patch da6b06a9a974df34a72286bd8b93194e712490d5
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Thu Jun  8 00:26:21 CEST 2023
  * telegram: Refactor forward handle and related functions
  Fix the use of saved_from_peer attribute and other improvements
hunk ./telegram.py 203
-    def get_irc_nick_from_telegram_forward(self, fwd):
-        if fwd.from_id is None:
+    async def get_irc_name_from_telegram_forward(self, fwd, saved):
+        from_id = fwd.saved_from_peer if saved else fwd.from_id
+        if from_id is None:
hunk ./telegram.py 207
-            nick = fwd.from_name
+            # or was a broadcast from a channel (no user)
+            name = fwd.from_name
hunk ./telegram.py 210
-            user = self.get_irc_user_from_telegram(fwd.from_id.user_id)
-            if user is None:
-                nick = '{}'
-                self.refwd_me = True
+            peer_id, type = self.get_peer_id_and_type(from_id)
+            if type == 'user':
+                user = self.get_irc_user_from_telegram(peer_id)
+                if user is None:
+                    name = '{}'
+                    self.refwd_me = True
+                else:
+                    name = user.irc_nick
hunk ./telegram.py 219
-                nick = user.irc_nick
-        return nick
+                try:
+                    name = await self.get_irc_channel_from_telegram_id(peer_id)
+                except:
+                    name = ''
+        return name
hunk ./telegram.py 330
+    def get_peer_id_and_type(self, peer):
+        if isinstance(peer, tgty.PeerChannel):
+            id = peer.channel_id
+            type = 'chan'
+        elif isinstance(peer, tgty.PeerChat):
+            id = peer.chat_id
+            type = 'chan'
+        elif isinstance(peer, tgty.PeerUser):
+            id = peer.user_id
+            type = 'user'
+        else:
+            id = peer
+            type = ''
+        return id, type
+
hunk ./telegram.py 617
-        forwarded_nick = self.get_irc_nick_from_telegram_forward(message.fwd_from)
-        forwarded_peer = message.fwd_from.saved_from_peer
-        if isinstance(forwarded_peer, tgty.PeerChannel):
-            dest = ' ' + await self.get_irc_channel_from_telegram_id(forwarded_peer.channel_id)
-        elif isinstance(forwarded_peer, tgty.PeerChat):
-            dest = ' ' + await self.get_irc_channel_from_telegram_id(forwarded_peer.chat_id)
+        space = space2 = ' '
+        if not (forwarded_peer_name := await self.get_irc_name_from_telegram_forward(message.fwd_from, saved=False)):
+            space = ''
+        saved_peer_name = await self.get_irc_name_from_telegram_forward(message.fwd_from, saved=True)
+        if saved_peer_name and saved_peer_name != forwarded_peer_name:
+            secondary_name = saved_peer_name
hunk ./telegram.py 626
-               dest = ' ' + self.get_irc_user_from_telegram(forwarded_peer.user_id).irc_nick
+               secondary_name = self.get_irc_user_from_telegram(message.fwd_from.saved_from_peer.user_id).irc_nick
hunk ./telegram.py 628
-               dest = ''
-
-        return '|Fwd {}{}| '.format(forwarded_nick, dest)
+               secondary_name = ''
+               space2 = ''
+
+        return '|Fwd{}{}{}{}| '.format(space, forwarded_peer_name, space2, secondary_name)
patch d6b83b6ed9dfd2c1692e79da8b54046f96e059f5
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Tue Jun  6 00:09:40 CEST 2023
  * telegram: Add "media_dir" option to set the download media directory outside
  of "config_dir" that didn't make sense, but keep it for compatibility if
  "media_dir" is not set
hunk ./irgramd 81
+    tornado.options.define('media_dir', default=None, metavar='PATH', help='Directory where Telegram media files are downloaded, default "media" in `config_dir`')
hunk ./telegram.py 41
+        self.media_dir  = settings['media_dir']
hunk ./telegram.py 71
-        self.telegram_media_dir = os.path.join(self.config_dir, 'media')
+        self.telegram_media_dir = self.media_dir or os.path.join(self.config_dir, 'media')
patch 4c771fc4851d84b5ae451e1cd9280223c3c5b014
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Thu Jun  1 19:55:14 CEST 2023
  * telegram: Fix media contact attributes
hunk ./telegram.py 649
-            if message.media.contact.first_name:
-                media_url_or_data += message.media.contact.first_name + ' '
-            if message.media.contact.last_name:
-                media_url_or_data += message.media.contact.last_name + ' '
-            if message.media.contact.phone_number:
-                media_url_or_data += message.media.contact.phone_number
+            if message.media.first_name:
+                media_url_or_data += message.media.first_name + ' '
+            if message.media.last_name:
+                media_url_or_data += message.media.last_name + ' '
+            if message.media.phone_number:
+                media_url_or_data += message.media.phone_number
patch d147fb4ac2eb91490933ee7ba89ecffcde0255c1
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sat May 20 23:39:03 CEST 2023
  * telegram: Add filters for received messages, in these filters:
  Include existing emoji to ASCII function
  Add conversion of "@mention" to "~mention~" as "@user" is used to denote
  channel operator in most IRC clients
hunk ./telegram.py 373
+    def replace_mentions(self, text):
+        def repl_mentioned(text):
+            if text and text[0] == '@':
+                part = text[1:].lower()
+                if part in self.irc.users:
+                    return '{}{}{}'.format('~', self.irc.users[part].irc_nick, '~')
+            return text
+
+        if text.find('@') != -1:
+            words = text.split(' ')
+            words_replaced = [repl_mentioned(elem) for elem in words]
+            text_replaced = ' '.join(words_replaced)
+        else:
+            text_replaced = text
+        return text_replaced
+
+    def filters(self, text):
+        filtered = e.replace_mult(text, e.emo)
+        filtered = self.replace_mentions(filtered)
+        return filtered
+
hunk ./telegram.py 400
-        message = e.replace_mult(event.message.message, e.emo)
+        message = self.filters(event.message.message)
hunk ./telegram.py 408
-                t = e.replace_mult(self.cache[id]['text'], e.emo)
+                t = self.filters(self.cache[id]['text'])
hunk ./telegram.py 504
-        final_text = e.replace_mult(final_text, e.emo)
+        final_text = self.filters(final_text)
patch 00e75e93c744727f1f30cb8e409d53b495c500ed
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Tue May 16 23:13:51 CEST 2023
  * service: Refactor "get" command handle, make it robust when compact id has
  not been initialized yet
hunk ./service.py 121
+            msg = None
hunk ./service.py 124
+            else: reply = ()
hunk ./service.py 127
-            msg = await self.tg.telegram_client.get_messages(entity=peer_id, ids=id)
-            if msg is None:
-                reply = ('Message not found',)
-                return reply
-            await self.tg.handle_telegram_message(event=None, message=msg, history=True)
-            reply = ()
+            if id is not None:
+                msg = await self.tg.telegram_client.get_messages(entity=peer_id, ids=id)
+            if msg is not None:
+                await self.tg.handle_telegram_message(event=None, message=msg, history=True)
+            else:
+                reply = ('Message not found',)            [_$_]
patch 72bb23b64e2392ff01f7e060670e98093cfe0839
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Mon May 15 21:27:05 CEST 2023
  * telegram: As messages are referenced by peer and id (in "get" and future
  commands), make the compact IDs more compact with offsets relative to peers
  (users or channels)
hunk ./irc.py 412
-            mid = self.tg.mid.num_to_id_offset(tg_msg.id)
+            mid = self.tg.mid.num_to_id_offset(telegram_id, tg_msg.id)
hunk ./service.py 121
-            id = self.tg.mid.id_to_num_offset(mid)
hunk ./service.py 124
+            id = self.tg.mid.id_to_num_offset(peer_id, mid)
hunk ./telegram.py 377
-        mid = self.mid.num_to_id_offset(id)
+        mid = self.mid.num_to_id_offset(event.message.peer_id, id)
hunk ./telegram.py 441
-                mid = self.mid.num_to_id_offset(deleted_id)
-                text = 'Message id {} deleted not in cache'.format(mid)
+                text = 'Message id {} deleted not in cache'.format(deleted_id)
hunk ./telegram.py 458
-        mid = self.mid.num_to_id_offset(msg.id)
+        mid = self.mid.num_to_id_offset(msg.peer_id, msg.id)
hunk ./telegram.py 557
-            replied_msg = '[{}]'.format(self.mid.num_to_id_offset(replied.id))
+            replied_msg = '[{}]'.format(self.mid.num_to_id_offset(replied.peer_id, replied.id))
hunk ./telegram.py 740
-        self.mesg_base = None
+        self.mesg_base = {}
hunk ./telegram.py 751
-    def num_to_id_offset(self, num):
-        if self.mesg_base is None:
-            self.mesg_base = num
-        return self.num_to_id(num - self.mesg_base)
+    def num_to_id_offset(self, peer, num):
+        peer_id = self.get_peer_id(peer)
+        if peer_id not in self.mesg_base:
+            self.mesg_base[peer_id] = num
+        return self.num_to_id(num - self.mesg_base[peer_id])
hunk ./telegram.py 766
-    def id_to_num_offset(self, mid):
-        if self.mesg_base is not None:
+    def id_to_num_offset(self, peer, mid):
+        peer_id = self.get_peer_id(peer)
+        if peer_id in self.mesg_base:
hunk ./telegram.py 770
-            id = id_rel + self.mesg_base
+            id = id_rel + self.mesg_base[peer_id]
hunk ./telegram.py 773
+        return id
+
+    def get_peer_id(self, peer):
+        if isinstance(peer, tgty.PeerChannel):
+            id = peer.channel_id
+        elif isinstance(peer, tgty.PeerChat):
+            id = peer.chat_id
+        elif isinstance(peer, tgty.PeerUser):
+            id = peer.user_id
+        else:
+            id = peer
patch 3fe74b2337a9d66bddd47052220c413836cb05f9
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Sun May 14 00:39:42 CEST 2023
  * service: Add "get" command to retrieve a specific message
hunk ./service.py 18
+            'get':         (self.handle_command_get,                  2,  2),
hunk ./service.py 116
+            )
+        return reply
+
+    async def handle_command_get(self, peer=None, mid=None, help=None):
+        if not help:
+            id = self.tg.mid.id_to_num_offset(mid)
+            peer_id, reply = self.get_peer_id(peer.lower())
+            if reply: return reply
+
+            msg = await self.tg.telegram_client.get_messages(entity=peer_id, ids=id)
+            if msg is None:
+                reply = ('Message not found',)
+                return reply
+            await self.tg.handle_telegram_message(event=None, message=msg, history=True)
+            reply = ()
+            return reply
+
+        else: # HELP.brief or HELP.desc (first line)
+            reply = ('   get         Get a message by id and peer',)
+        if help == HELP.desc:  # rest of HELP.desc
+            reply += \
+            (
+              '   get <peer> <compact_id>',
+              'Get one message from peer with the compact ID',
hunk ./telegram.py 765
+
+    def id_to_num_offset(self, mid):
+        if self.mesg_base is not None:
+            id_rel = self.id_to_num(mid)
+            id = id_rel + self.mesg_base
+        else:
+            id = None
+        return id
patch 720468dce764675132030772fcb93f4bb5b00724
Author: E. Bosch <presidev@AT@gmail.com>
Date:   Mon May  8 01:12:09 CEST 2023
  * service: Add more indentation in brief help descriptions
hunk ./service.py 60
-            reply = ('   code      Enter authorization code',)
+            reply = ('   code        Enter authorization code',)
hunk ./service.py 105
-            reply = ('   dialog    Manage conversations (dialogs)',)
+            reply = ('   dialog      Manage conversations (dialogs)',)
hunk ./service.py 124
-            help_text = ('   help      This help',)
+            help_text = ('   help        This help',)
hunk ./service.py 203
-            reply = ('   history   Get messages from history',)
+            reply = ('   history     Get messages from history',)
hunk ./service.py 225
-            reply = ('   mark_read Mark messages as read',)
+            reply = ('   mark_read   Mark messages as read',)