exclam: Add "-" parameter to "!react" to remove a reaction --> to head
patch 6493c04f9d8bdb7b1d2c037fdd66c337a7a91af5
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sun May 31 00:26:34 CEST 2026
* utils: Modify sanitize_filename(): convert invalid chars to hex representation,
this makes the filenames deterministic for the same session,
and helps to not download a file more than once
patch dd18dbebc3877faa7fe1c7f1257fb3a4048aab32
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sat May 30 22:25:32 CEST 2026
* utils: In add_filename() don't put a final dot if there is no extension in the filename
patch feebc9eac596c484be6e5f67934196e64d52beeb
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sat May 30 21:33:57 CEST 2026
* telegram: Refactor download media routine
optimized to download files only once
utils: Add session tokens for unnamed files
patch 4009bc07c6261c0ea2cb1a4112f8aaf266cbd65d
Author: E. Bosch <presidev@AT@gmail.com>
Date: Sat Jul 26 01:56:12 CEST 2025
* emoji2emoticon: Add emoji "rolling on the floor laughing" to convert to ASCII
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
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
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
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
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
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
patch 0cfc7e59b24fb1a1b279fc593f8d04d0648e3880
Author: E. Bosch <presidev@AT@gmail.com>
Date: Mon Oct 21 00:54:16 CEST 2024
* README update
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
diff -rN -u old-irgramd/README.md new-irgramd/README.md
--- old-irgramd/README.md 2026-06-09 14:27:44.720186431 +0200
+++ new-irgramd/README.md 2026-06-09 14:27:44.724186427 +0200
@@ -50,7 +50,7 @@
- Forwards (receive, send)
- Deletions (receive, do)
- Editions (receive, do)
-- Reactions (receive, send)
+- Reactions (receive, send, remove)
- Polls (receive, show)
- Actions [pin message, channel photo] (receive)
- Dialogs management
diff -rN -u old-irgramd/emoji2emoticon.py new-irgramd/emoji2emoticon.py
--- old-irgramd/emoji2emoticon.py 2026-06-09 14:27:44.720186431 +0200
+++ new-irgramd/emoji2emoticon.py 2026-06-09 14:27:44.724186427 +0200
@@ -85,11 +85,12 @@
'\U0001f643': '"(:"',
'\U0001f644': '"o o,"',
'\U0001f914': '":-L"',
+ '\U0001f923': '"X__D"',
'\U0001f92b': '":-o-m"',
'\U0001f970': '":)e>"',
}
-emo_inv = {}
+emo_inv = { '-': None }
for k in reversed(emo):
emo_inv[emo[k][1:-1]] = k
diff -rN -u old-irgramd/exclam.py new-irgramd/exclam.py
--- old-irgramd/exclam.py 2026-06-09 14:27:44.720186431 +0200
+++ new-irgramd/exclam.py 2026-06-09 14:27:44.724186427 +0200
@@ -18,13 +18,13 @@
def __init__(self, telegram):
self.commands = \
{ # Command Handler Arguments Min Max Maxsplit
- '!re': (self.handle_command_re, 2, 2, 2),
- '!ed': (self.handle_command_ed, 2, 2, 2),
'!del': (self.handle_command_del, 1, 1, -1),
+ '!ed': (self.handle_command_ed, 2, 2, 2),
'!fwd': (self.handle_command_fwd, 2, 2, -1),
- '!upl': (self.handle_command_upl, 1, 2, 2),
- '!reupl': (self.handle_command_reupl, 2, 3, 3),
+ '!re': (self.handle_command_re, 2, 2, 2),
'!react': (self.handle_command_react, 2, 2, -1),
+ '!reupl': (self.handle_command_reupl, 2, 3, 3),
+ '!upl': (self.handle_command_upl, 1, 2, 2),
}
self.tg = telegram
self.irc = telegram.irc
@@ -199,14 +199,14 @@
if chk_msg is not None:
if act in emo_inv:
utf8_emo = emo_inv[act]
- reaction = [ tgty.ReactionEmoji(emoticon=utf8_emo) ]
+ reaction = [ tgty.ReactionEmoji(emoticon=utf8_emo) ] if utf8_emo else None
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
+ self.tmp_tg_msg = getattr(update.updates[0], 'message', None)
+ reply = bool(self.tmp_tg_msg)
else:
reply = ('!react: Unknown reaction',)
else:
@@ -216,8 +216,9 @@
if help == HELP.desc: # rest of HELP.desc
reply += \
(
- ' !react <compact_id> <emoticon reaction>',
+ ' !react <compact_id> <emoticon reaction>|-',
'React with <emoticon reaction> to a message with <compact_id>,',
'irgramd will translate emoticon to closest emoji.',
+ 'Use - to remove a previous reaction.',
)
return reply
diff -rN -u old-irgramd/include.py new-irgramd/include.py
--- old-irgramd/include.py 2026-06-09 14:27:44.720186431 +0200
+++ new-irgramd/include.py 2026-06-09 14:27:44.724186427 +0200
@@ -11,3 +11,4 @@
VERSION = '0.2'
NICK_MAX_LENGTH = 20
CHAN_MAX_LENGTH = 50
+MAX_LINE = 400
diff -rN -u old-irgramd/irc.py new-irgramd/irc.py
--- old-irgramd/irc.py 2026-06-09 14:27:44.724186427 +0200
+++ new-irgramd/irc.py 2026-06-09 14:27:44.724186427 +0200
@@ -18,7 +18,7 @@
# Local modules
-from include import VERSION, CHAN_MAX_LENGTH, NICK_MAX_LENGTH
+from include import VERSION, CHAN_MAX_LENGTH, NICK_MAX_LENGTH, MAX_LINE
from irc_replies import irc_codes
from utils import chunks, set_replace, split_lines
from service import service
@@ -259,7 +259,7 @@
real_chan = self.get_realcaps_name(chan)
users_count = len(self.irc_channels[chan])
topic = await self.tg.get_channel_topic(chan, [None])
- 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]))
await self.reply_code(user, 'RPL_LISTEND')
async def handle_irc_names(self, user, channels):
@@ -612,7 +612,7 @@
founder = list(self.irc_channels_founder[chan])[0]
else:
founder = self.service_user.irc_nick
- await self.reply_code(user, 'RPL_TOPIC', (channel, topic))
+ await self.reply_code(user, 'RPL_TOPIC', (channel, topic[:MAX_LINE]))
await self.reply_code(user, 'RPL_TOPICWHOTIME', (channel, founder, timestamp))
async def irc_namelist(self, user, channel):
diff -rN -u old-irgramd/telegram.py new-irgramd/telegram.py
--- old-irgramd/telegram.py 2026-06-09 14:27:44.724186427 +0200
+++ new-irgramd/telegram.py 2026-06-09 14:27:44.728186423 +0200
@@ -15,14 +15,15 @@
import collections
import telethon
from telethon import types as tgty, utils as tgutils
-from telethon.tl.functions.messages import GetMessagesReactionsRequest
+from telethon.tl.functions.messages import GetMessagesReactionsRequest, GetFullChatRequest
+from telethon.tl.functions.channels import GetFullChannelRequest
# Local modules
from include import CHAN_MAX_LENGTH, NICK_MAX_LENGTH
from irc import IRCUser
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
+from utils import get_highlighted, fix_braces, format_timestamp, pretty, current_date, token
import emoji2emoticon as e
# Test IP table
@@ -43,6 +44,8 @@
self.notice_size = settings['download_notice'] * 1048576
self.media_dir = settings['media_dir']
self.media_url = settings['media_url']
+ if self.media_url[-1:] != '/':
+ self.media_url += '/'
self.upload_dir = settings['upload_dir']
self.api_id = settings['api_id']
self.api_hash = settings['api_hash']
@@ -58,7 +61,8 @@
self.geo_url = settings['geo_url']
if not settings['emoji_ascii']:
e.emo = {}
- self.media_cn = 0
+ self.token = token('+0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz')
+ self.filename_token = self.token.gen_token(5)
self.irc = irc
self.authorized = False
self.id = None
@@ -176,16 +180,21 @@
self.irc.iid_to_tid[chan] = chat.id
self.irc.irc_channels[chan] = set()
# Add users from the channel
- 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):
- self.irc.irc_channels_ops[chan].add(user_nick)
- # Add creator users as founders in irc
- elif isinstance(user.participant, tgty.ChatParticipantCreator):
- 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)
def get_telegram_nick(self, user):
nick = (user.username
@@ -307,8 +316,16 @@
else:
entity = await self.telegram_client.get_entity(tid)
entity_cache[0] = entity
+ 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 ''
entity_type = self.get_entity_type(entity, format='long')
- return 'Telegram ' + entity_type + ' ' + str(tid) + ': ' + entity.title
+ topic = full.full_chat.about
+ sep = ': ' if topic else ''
+ return entity_type + sep + topic
async def get_channel_creation(self, channel, entity_cache):
tid = self.get_tid(channel)
@@ -574,9 +591,9 @@
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)
-
- 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:
self.last_reaction = react.date
id = event.msg_id
msg = await self.telegram_client.get_messages(entity=event.peer, ids=id)
@@ -1014,30 +1031,25 @@
if not self.download:
return ''
if filename:
- idd_file = add_filename(filename, mid)
- new_file = sanitize_filename(idd_file)
- new_path = os.path.join(self.telegram_media_dir, new_file)
- if os.path.exists(new_path):
- local_path = new_path
+ aux_file = filename
+ else:
+ if hasattr(message, 'file') and message.file is not None:
+ aux_file = self.filename_token + message.file.ext
else:
- await self.notice_downloading(size, relay_attr)
- local_path = await message.download_media(new_path)
- if not local_path: return ''
+ aux_file = self.filename_token
+
+ idd_file = add_filename(aux_file, mid)
+ new_file = sanitize_filename(idd_file)
+ new_path = os.path.join(self.telegram_media_dir, new_file)
+ if os.path.exists(new_path) and (size == 0 or size == os.path.getsize(new_path)):
+ local_path = new_path
else:
await self.notice_downloading(size, relay_attr)
- local_path = await message.download_media(self.telegram_media_dir)
+ local_path = await message.download_media(new_path)
if not local_path: return ''
- filetype = os.path.splitext(local_path)[1]
- gen_file = str(self.media_cn) + filetype
- idd_file = add_filename(gen_file, mid)
- new_file = sanitize_filename(idd_file)
- self.media_cn += 1
- new_path = os.path.join(self.telegram_media_dir, new_file)
if local_path != new_path:
os.replace(local_path, new_path)
- if self.media_url[-1:] != '/':
- self.media_url += '/'
return self.media_url + new_file
async def notice_downloading(self, size, relay_attr):
diff -rN -u old-irgramd/utils.py new-irgramd/utils.py
--- old-irgramd/utils.py 2026-06-09 14:27:44.724186427 +0200
+++ new-irgramd/utils.py 2026-06-09 14:27:44.728186423 +0200
@@ -14,12 +14,15 @@
import zoneinfo
import difflib
import logging
+import random
# Constants
-FILENAME_INVALID_CHARS = re.compile('[/{}<>()"\'\\|&#%?]')
+FILENAME_INVALID_CHARS = re.compile('[\0-\x1F/{}<>"\'\\|*&#%?\x7F]')
SIMPLE_URL = re.compile('http(|s)://[^ ]+')
+from include import MAX_LINE
+
# Utilities
class command:
@@ -61,9 +64,8 @@
return (x + mark if n != length else x for n, x in enumerate(items, start=1))
def split_lines(message):
- MAX = 400
messages_limited = []
- wr = textwrap.TextWrapper(width=MAX)
+ wr = textwrap.TextWrapper(width=MAX_LINE)
# Split when Telegram original message has breaks
messages = message.splitlines()
@@ -85,22 +87,23 @@
return messages_limited
def sanitize_filename(fn):
- 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
+ def hexize(m):
+ return '-{:x}-'.format(ord(m.group(0)))
+
+ new_fn = FILENAME_INVALID_CHARS.sub(hexize, fn)
+ return new_fn.lstrip('-').replace(' ','_')
def add_filename(filename, add):
if add:
aux = filename.rsplit('.', 1)
name = aux[0]
+ last_dot = '.'
try:
ext = aux[1]
except:
ext = ''
- return '{}-{}.{}'.format(name, add, ext)
+ last_dot = ''
+ return '{}-{}{}{}'.format(name, add, last_dot, ext)
else:
return filename
@@ -239,3 +242,15 @@
def pretty(object):
return object.stringify() if LOGL.debug and object else object
+
+class token:
+ def __init__(self, alpha):
+ self.alpha = alpha
+ self.long_alpha = len(alpha)
+
+ def gen_token(self, long_token):
+ if long_token == 1:
+ return self.alpha[random.randrange(self.long_alpha)]
+ else:
+ aux = self.gen_token(long_token - 1)
+ return aux + self.alpha[random.randrange(self.long_alpha)]