patch 9c530196db77dfde94d09e8dfbee848665084731 Author: Lucas de Sena Date: Sat Jul 25 00:27:23 CEST 2026 * Merge pull request #16 on github from phillbush/show-reactions add "show_reactions" option Set it to false if you do not want to be annoyed with reactions and emojis. patch b785776c7df3016fb9c83aa7f7b9e51815435bab Author: E. Bosch Date: Thu Jul 23 18:06:45 CEST 2026 * Merge pull request #18 on github from phillbush/message-deleted-not-in-cache along with an option to configure the new behaviour move "Message id ... deleted not in cache" into logger TelegramServ can get filled with warnings like Message id 012345 deleted not in cache if you are in a channel/group that automatically deletes messages after a certain time, and the deleted message is not in cache. Warn that in the logger. With "info" severity, because that's not actually an issue, just something that can happen. Add option "log_deleted" to enable this, by default false (use TelegramServ) patch fae93b3a3f74e5858d8bf4f090f8ed245894e4c9 Author: E. Bosch Date: Sun Jul 19 22:35:25 CEST 2026 * telegram: Add support for 2nd factor authentication (2FA) password for Telegram accounts that have it enabled. This will work consistenly with "ask_code" option whether True or False. Refactored authentication code to not produce too many login code requests to avoid bans. Improve error logging. Remove the use of aioconsole, use to_thread with standard functions instead. patch 2922f89ba9e3b2a0983b0e564ce29105e670c3f2 Author: E. Bosch Date: Tue Jul 14 11:30:59 CEST 2026 * service: In auth help add separator if there is init help only patch 7f3352bbc9fe55c1c399495c13e36549390d9b07 Author: E. Bosch Date: Tue Jul 14 01:15:16 CEST 2026 * Move auth help from irc.py to service.py patch abf2b7ea17e019e9d6933da27c16e614eddbb8f7 Author: E. Bosch Date: Sun Jul 12 00:09:26 CEST 2026 * Update Telethon URL in README patch 26fc7b94f1d10c6528a0de866eed28a552c31a36 Author: E. Bosch Date: Fri Jul 10 23:47:21 CEST 2026 * Fix line break in README patch 96f3cc24071c7b58c1647b1996dac3fee46eb367 Author: E. Bosch Date: Fri Jul 10 22:56:40 CEST 2026 * Update year in copyright notices patch 664aa2ec859e448f7dfdfc722f364f066f559708 Author: E. Bosch Date: Thu Jul 9 22:15:58 CEST 2026 * Add "initial_help" option (by default True) to enable or disable initial help message from TelegramServ This closes issue #4 on github patch 1ba8d679bed7d5d2294d7094664cbe15b1e4d0db Author: E. Bosch Date: Wed Jul 8 22:35:04 CEST 2026 * Partial merge pull request #12 on github from Mirochill/fix-7-10-irc-debug-join-prefix Fix IRC PASS debug log patch 679eb7246c0f16324226b8f8213c9db37d652f31 Author: E. Bosch Date: Wed Jul 8 01:56:41 CEST 2026 * Update copyright notices patch 3f2611ef2ec2a73f0327df45855e40f04ebb5955 Author: E. Bosch Date: Tue Jul 7 23:15:14 CEST 2026 * Merge pull request #11 on github from Mirochill/fix-9-invalid-escape-warnings Fix invalid escape warnings patch 7827ddfc286acd03ba80f6dfef6efed92fc0457a Author: E. Bosch Date: Sun Jun 21 23:41:03 CEST 2026 * Update year in copyright notices patch 6493c04f9d8bdb7b1d2c037fdd66c337a7a91af5 Author: E. Bosch 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 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 diff -rN -u old-irgramd/LICENSE new-irgramd/LICENSE --- old-irgramd/LICENSE 2026-07-25 11:29:22.134816372 +0200 +++ new-irgramd/LICENSE 2026-07-25 11:29:22.138816373 +0200 @@ -1,7 +1,8 @@ MIT License Copyright (c) 2019 Peter Bui -Copyright (c) 2020-2024 E. Bosch +Copyright (c) 2020-2026 E. Bosch +Copyright (c) 2026 Miro (Mirochill on github.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff -rN -u old-irgramd/README.md new-irgramd/README.md --- old-irgramd/README.md 2026-07-25 11:29:22.134816372 +0200 +++ new-irgramd/README.md 2026-07-25 11:29:22.138816373 +0200 @@ -55,6 +55,7 @@ - Actions [pin message, channel photo] (receive) - Dialogs management - History +- Login code and 2FA password support for Telegram - Authentication and TLS for IRC - Multiple connections from IRC @@ -63,7 +64,6 @@ - [python] (>= v3.9) - [telethon] (tested with v1.28.5) - [tornado] (tested with v6.1.0) -- [aioconsole] (tested with v0.6.1) - [pyPAM] (optional, tested with v0.4.2-13.4 from deb, [legacy web](https://web.archive.org/web/20110316070059/http://www.pangalactic.org/PyPAM/)) ## Instalation @@ -125,7 +125,8 @@ ## License Copyright (c) 2019 Peter Bui -Copyright (c) 2020-2024 E. Bosch +Copyright (c) 2020-2026 E. Bosch +Copyright (c) 2026 Miro (Mirochill on github.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. @@ -133,7 +134,7 @@ [IRC]: https://en.wikipedia.org/wiki/Internet_Relay_Chat [Telegram]: https://telegram.org/ [python]: https://www.python.org/ -[Telethon]: https://github.com/LonamiWebs/Telethon +[Telethon]: https://codeberg.org/Lonami/Telethon [irgramd primary repository]: https://src.presi.org/darcs/irgramd [darcs]: http://darcs.net [project management and secondary repository]: https://github.com/prsai/irgramd diff -rN -u old-irgramd/emoji2emoticon.py new-irgramd/emoji2emoticon.py --- old-irgramd/emoji2emoticon.py 2026-07-25 11:29:22.134816372 +0200 +++ new-irgramd/emoji2emoticon.py 2026-07-25 11:29:22.138816373 +0200 @@ -1,7 +1,8 @@ # irgramd: IRC-Telegram gateway # emoji2emoticon.py: UTF-8 Emoji to ASCII emoticon replacement # -# (C) Copyright 2019,2023 E. Bosch +# (C) Copyright 2019,2023,2025 E. Bosch +# (C) Copyright 2026 Miro (Mirochill on github.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. @@ -14,7 +15,7 @@ '\U0001f44d': '"+1"', '\U0001f44e': '"-1"', '\U0001f44f': '"m_m"', - '\U0001f525': '"\^^^/"', + '\U0001f525': r'"\^^^/"', '\U0001f600': '":D"', '\U0001f601': '"xD"', '\U0001f602': '"x_D"', diff -rN -u old-irgramd/exclam.py new-irgramd/exclam.py --- old-irgramd/exclam.py 2026-07-25 11:29:22.134816372 +0200 +++ new-irgramd/exclam.py 2026-07-25 11:29:22.138816373 +0200 @@ -1,7 +1,7 @@ # irgramd: IRC-Telegram gateway # exclam.py: IRC exclamation command handlers # -# Copyright (c) 2023, 2024 E. Bosch +# Copyright (c) 2023-2025 E. Bosch # # Use of this source code is governed by a MIT style license that # can be found in the LICENSE file included in this project. diff -rN -u old-irgramd/include.py new-irgramd/include.py --- old-irgramd/include.py 2026-07-25 11:29:22.134816372 +0200 +++ new-irgramd/include.py 2026-07-25 11:29:22.138816373 +0200 @@ -1,7 +1,7 @@ # irgramd: IRC-Telegram gateway # include.py: Constants and other definitions to be included in other files # -# Copyright (c) 2020-2022 E. Bosch +# Copyright (c) 2020-2024 E. Bosch # # Use of this source code is governed by a MIT style license that # can be found in the LICENSE file included in this project. diff -rN -u old-irgramd/irc.py new-irgramd/irc.py --- old-irgramd/irc.py 2026-07-25 11:29:22.134816372 +0200 +++ new-irgramd/irc.py 2026-07-25 11:29:22.138816373 +0200 @@ -2,7 +2,8 @@ # irc.py: IRC server side implementation # # Copyright (c) 2019 Peter Bui -# Copyright (c) 2020-2023 E. Bosch +# Copyright (c) 2020-2024,2026 E. Bosch +# Copyright (c) 2026 Miro (Mirochill on github.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. @@ -28,7 +29,7 @@ SRV = None ALL_PARAMS = 16 -VALID_IRC_NICK_FIRST_CHARS = string.ascii_letters + '[]\`_^{|}' +VALID_IRC_NICK_FIRST_CHARS = string.ascii_letters + r'[]\`_^{|}' VALID_IRC_NICK_CHARS = VALID_IRC_NICK_FIRST_CHARS + string.digits + '-' # IRC Regular Expressions @@ -154,7 +155,7 @@ # IRC handlers async def handle_irc_pass(self, user, password): - self.logger.debug('Handling PASS: %s %s', password) + self.logger.debug('Handling PASS: %s', password) if user.registered: await self.reply_code(user, 'ERR_ALREADYREGISTRED') @@ -438,7 +439,8 @@ user.registered = True await self.send_greeting(user) - await self.send_help(user) + if self.conf['initial_help']: + await self.send_help(user) await self.check_telegram_auth(user) async def send_msg(self, source, target, message, selfuser=None): @@ -538,14 +540,7 @@ async def check_telegram_auth(self, user): await self.tg.auth_checked.wait() if not self.tg.authorized and not self.tg.ask_code: - for line in ( - '----', - 'Your Telegram account is not authorized yet,', - 'you must supply the code that Telegram sent to your phone', - 'or another client that is currently connected', - 'use /msg {} code '.format(self.service_user.irc_nick), - 'e.g. /msg {} code 12345'.format(self.service_user.irc_nick), - ): + for line in self.service.auth_help(): await self.send_msg(self.service_user, user.irc_nick, line) async def send_users_irc(self, prfx, command, params): diff -rN -u old-irgramd/irgramd new-irgramd/irgramd --- old-irgramd/irgramd 2026-07-25 11:29:22.134816372 +0200 +++ new-irgramd/irgramd 2026-07-25 11:29:22.138816373 +0200 @@ -3,7 +3,7 @@ # irgramd: IRC-Telegram gateway - Main file # # Copyright (c) 2019 Peter Bui -# Copyright (c) 2020-2024 E. Bosch +# Copyright (c) 2020-2024,2026 E. Bosch # # Use of this source code is governed by a MIT style license that # can be found in the LICENSE file included in this project. @@ -78,7 +78,7 @@ # Define irgramd options tornado.options.define('api_hash', default=None, metavar='HASH', help='Telegram API Hash for your account (obtained from https://my.telegram.org/apps)') tornado.options.define('api_id', type=int, default=None, metavar='ID', help='Telegram API ID for your account (obtained from https://my.telegram.org/apps)') - tornado.options.define('ask_code', default=False, help='Ask authentication code (sent by Telegram) in console instead of "code" service command in IRC') + tornado.options.define('ask_code', default=False, help='Ask authentication code (sent by Telegram) and 2FA password (if enabled) in console instead of "code" service command in IRC') tornado.options.define('cache_dir', default='~/.cache/irgramd', metavar='PATH', help='Cache directory where telegram media is saved by default') 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') @@ -89,10 +89,12 @@ tornado.options.define('emoji_ascii', default=False, help='Replace emoji with ASCII emoticons') tornado.options.define('geo_url', type=str, default=None, metavar='TEMPLATE_URL', help='Use custom URL for showing geo latitude/longitude location, eg. OpenStreetMap') tornado.options.define('hist_timestamp_format', metavar='DATETIME_FORMAT', help='Format string for timestamps in history, see https://www.strfti.me') + tornado.options.define('initial_help', default=True, help='Enable/disable initial help message from service user [TelegramServ]') tornado.options.define('irc_address', default='127.0.0.1', metavar='ADDRESS', help='Address to listen on for IRC') tornado.options.define('irc_nicks', type=str, multiple=True, metavar='nick,..', help='List of nicks allowed for IRC, if `pam` and optionally `pam_group` are set, PAM authentication will be used instead') tornado.options.define('irc_password', default='', metavar='PASSWORD', help='Password for IRC authentication, if `pam` is set, PAM authentication will be used instead') tornado.options.define('irc_port', type=int, default=None, metavar='PORT', help='Port to listen on for IRC. (default 6667, default with TLS 6697)') + tornado.options.define('log_deleted', default=False, help='Deleted messages not in cache are logged, if disabled service user [TelegramServ] will notify them') 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') tornado.options.define('media_dir', default=None, metavar='PATH', help='Directory where Telegram media files are downloaded, default "media" in `cache_dir`') @@ -102,6 +104,7 @@ tornado.options.define('phone', default=None, metavar='PHONE_NUMBER', help='Phone number associated with the Telegram account to receive the authorization codes if necessary') tornado.options.define('quote_length', default=50, metavar='LENGTH', help='Max length of the text quoted in replies and reactions, if longer is truncated') tornado.options.define('service_user', default='TelegramServ', metavar='SERVICE_NICK', help='Nick of the service/control user, must be a nick not used by a real Telegram user') + tornado.options.define('show_reactions', default=True, help='Show reactions to messages') tornado.options.define('test', default=False, help='Connect to Telegram test environment') tornado.options.define('test_datacenter', default=2, metavar='DATACENTER_NUMBER', help='Datacenter to connect to Telegram test environment') tornado.options.define('test_host', default=None, metavar='HOST_IP', help='Host to connect to Telegram test environment (default: use a internal table depending on datacenter)') diff -rN -u old-irgramd/service.py new-irgramd/service.py --- old-irgramd/service.py 2026-07-25 11:29:22.138816373 +0200 +++ new-irgramd/service.py 2026-07-25 11:29:22.138816373 +0200 @@ -1,19 +1,20 @@ # irgramd: IRC-Telegram gateway # service.py: IRC service/control command handlers # -# Copyright (c) 2022,2023 E. Bosch +# Copyright (c) 2022-2024 E. Bosch # # 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 compact_date, command, HELP from telethon import utils as tgutils +from telethon.errors.rpcerrorlist import SessionPasswordNeededError class service(command): def __init__(self, settings, telegram): self.commands = \ { # Command Handler Arguments Min Max Maxsplit - 'code': (self.handle_command_code, 1, 1, -1), + 'code': (self.handle_command_code, 1, 2, -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), @@ -21,6 +22,7 @@ 'mark_read': (self.handle_command_mark_read, 1, 1, -1), } self.ask_code = settings['ask_code'] + self.init_help = settings['initial_help'] self.tg = telegram self.irc = telegram.irc self.tmp_ircnick = None @@ -33,30 +35,53 @@ 'to get help', ) - async def handle_command_code(self, code=None, help=None): + def auth_help(self): + sep = '----' if self.init_help else '' + return ( + sep, + 'Your Telegram account is not authorized yet,', + 'you must supply the code that Telegram sent to your phone', + 'or another client that is currently connected', + 'use /msg or equivalent in your IRC client', + 'e.g. /msg {} code 12345'.format(self.irc.service_user.irc_nick), + 'If 2nd authentication factor (2FA) password is enabled in', + 'your account, you must provide it as well', + 'e.g. /msg {} code 12345 password'.format(self.irc.service_user.irc_nick), + ) + + async def handle_command_code(self, code=None, passw=None, help=None): if not help: if self.ask_code: reply = ('Code will be asked on console',) elif code.isdigit(): + valid_auth = True try: await self.tg.telegram_client.sign_in(code=code) + except SessionPasswordNeededError: + try: + await self.tg.telegram_client.sign_in(password=passw) + except: + reply = ('Invalid 2FA password',) + valid_auth = False except: reply = ('Invalid code',) - else: - reply = ('Valid code', 'Telegram account authorized') + valid_auth = False + if valid_auth: + reply = ('Valid authentication', 'Telegram account authorized') await self.tg.continue_auth() else: # not isdigit reply = ('Code must be numeric',) else: # HELP.brief or HELP.desc (first line) - reply = (' code Enter authorization code',) + reply = (' code Enter authorization code and 2FA password',) if help == HELP.desc: # rest of HELP.desc reply += \ ( - ' code ', + ' code []', 'Enter authorization code sent by Telegram to the phone or to', - 'another client connected.', - 'This authorization code usually is only needed the first time', + 'another client connected. If 2nd factor authentication (2FA) password', + 'is enabled in your account, must be provided too.', + 'This authentication usually is only needed the first time', 'that irgramd connects to Telegram with a given account.', ) return reply diff -rN -u old-irgramd/telegram.py new-irgramd/telegram.py --- old-irgramd/telegram.py 2026-07-25 11:29:22.138816373 +0200 +++ new-irgramd/telegram.py 2026-07-25 11:29:22.142816373 +0200 @@ -2,7 +2,7 @@ # telegram.py: Interface to Telethon Telegram library # # Copyright (c) 2019 Peter Bui -# Copyright (c) 2020-2024 E. Bosch +# Copyright (c) 2020-2026 E. Bosch # # Use of this source code is governed by a MIT style license that # can be found in the LICENSE file included in this project. @@ -10,13 +10,14 @@ import logging import os import re -import aioconsole import asyncio import collections import telethon +from getpass import getpass from telethon import types as tgty, utils as tgutils from telethon.tl.functions.messages import GetMessagesReactionsRequest, GetFullChatRequest from telethon.tl.functions.channels import GetFullChannelRequest +from telethon.errors.rpcerrorlist import SessionPasswordNeededError # Local modules @@ -56,9 +57,11 @@ self.test_port = settings['test_port'] self.ask_code = settings['ask_code'] self.quote_len = settings['quote_length'] + self.show_react = settings['show_reactions'] self.hist_fmt = settings['hist_timestamp_format'] self.timezone = settings['timezone'] self.geo_url = settings['geo_url'] + self.log_del = settings['log_deleted'] if not settings['emoji_ascii']: e.emo = {} self.token = token('+0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz') @@ -123,19 +126,41 @@ else: await self.telegram_client.connect() - while not await self.telegram_client.is_user_authorized(): + if not await self.telegram_client.is_user_authorized(): self.logger.info('Telegram account not authorized') await self.telegram_client.send_code_request(self.phone) self.auth_checked.set() if not self.ask_code: return - self.logger.info('You must provide the Login code that Telegram will ' - 'sent you via SMS or another connected client') - code = await aioconsole.ainput('Login code: ') + attempts_msg = ('After 3 failed authentication attempts, aborted. ' + 'Please restart irgramd to try again') + count = 1 + while not await self.telegram_client.is_user_authorized(): + if count > 3: + self.logger.error(attempts_msg) + return + await asyncio.to_thread(print, 'You must provide the Login code that Telegram will ' + 'sent you via SMS or another connected client') + code = await asyncio.to_thread(input, 'Login code: ') try: await self.telegram_client.sign_in(code=code) - except: - pass + except SessionPasswordNeededError: + count = 1 + while not await self.telegram_client.is_user_authorized(): + if count > 3: + self.logger.error(attempts_msg) + return + await asyncio.to_thread(print, '2nd factor authentication (2FA) password is enabled ' + 'in your account, you must provide it') + passw = await asyncio.to_thread(getpass, 'Password (not shown): ') + try: + await self.telegram_client.sign_in(password=passw) + except BaseException as err: + self.logger.error(repr(err)) + count += 1 + except BaseException as err: + self.logger.error(repr(err)) + count += 1 await self.continue_auth() @@ -573,6 +598,8 @@ # Reactions else: + if not self.show_react: + return if reaction: if self.last_reaction == reaction.date: return @@ -588,6 +615,8 @@ self.to_volatile_cache(self.prev_id, id, text, user, chan, current_date()) async def handle_next_reaction(self, event): + if not self.show_react: + return self.logger.debug('Handling Telegram Next Reaction (2nd, 3rd, ...): %s', pretty(event)) reactions = event.reactions.recent_reactions @@ -622,8 +651,11 @@ await self.relay_telegram_message(message=None, user=user, text=text, channel=chan) self.to_volatile_cache(self.prev_id, deleted_id, text, user, chan, current_date()) else: - text = 'Message id {} deleted not in cache'.format(deleted_id) - await self.relay_telegram_private_message(self.irc.service_user, text) + if self.log_del: + self.logger.info('Message id {} deleted not in cache'.format(deleted_id)) + else: + text = 'Message id {} deleted not in cache'.format(deleted_id) + await self.relay_telegram_private_message(self.irc.service_user, text) async def handle_raw(self, update): self.logger.debug('Handling Telegram Raw Event: %s', pretty(update)) diff -rN -u old-irgramd/utils.py new-irgramd/utils.py --- old-irgramd/utils.py 2026-07-25 11:29:22.138816373 +0200 +++ new-irgramd/utils.py 2026-07-25 11:29:22.142816373 +0200 @@ -2,7 +2,7 @@ # utils.py: Helper functions # # Copyright (c) 2019 Peter Bui -# Copyright (c) 2020-2024 E. Bosch +# Copyright (c) 2020-2026 E. Bosch # # Use of this source code is governed by a MIT style license that # can be found in the LICENSE file included in this project. @@ -18,7 +18,7 @@ # 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 @@ -87,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