#!/usr/bin/env python3 # # irgramd: IRC-Telegram gateway - Main file # # Copyright (c) 2019 Peter Bui # 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. import logging import os import asyncio import tornado.options import tornado.tcpserver import ssl # Local modules from irc import IRCHandler from telegram import TelegramHandler from utils import parse_loglevel # IRC Telegram Daemon class IRCTelegramd(tornado.tcpserver.TCPServer): def __init__(self, logger, settings): self.logger = logger effective_port = settings['irc_port'] if settings['tls']: if not settings['tls_cert']: # error self.logger.error('TLS configured but certificate not present') exit(1) tls_context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH) tls_context.load_cert_chain(os.path.expanduser(settings['tls_cert']), os.path.expanduser(settings['tls_key'])) if not effective_port: effective_port = 6697 self.logger.info('TLS configured') else: tls_context = None if not effective_port: effective_port = 6667 tornado.tcpserver.TCPServer.__init__(self, ssl_options=tls_context) self.address = settings['irc_address'] self.port = effective_port self.irc_handler = None self.tg_handler = None async def handle_stream(self, stream, address): await self.irc_handler.run(stream, address) async def run(self, settings): self.listen(self.port, self.address) self.logger.info('irgramd listening on %s:%s', self.address, self.port) self.irc_handler = IRCHandler(settings) self.tg_handler = TelegramHandler(self.irc_handler, settings) self.irc_handler.set_telegram(self.tg_handler) await self.tg_handler.initialize_telegram() # Main Execution if __name__ == '__main__': # 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 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('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') tornado.options.define('config', default='irgramdrc', metavar='CONFIGFILE', help='Config file absolute or relative to `config_dir` (command line options override it)') tornado.options.define('config_dir', default='~/.config/irgramd', metavar='PATH', help='Configuration directory where telegram session info is saved') 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') 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') 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('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_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`') tornado.options.define('media_url', default=None, metavar='BASE_URL', help='Base URL for media files, should be configured in the external (to irgramd) webserver') tornado.options.define('pam', default=False, help='Use PAM for IRC authentication, if not set you should set `irc_password`') tornado.options.define('pam_group', default=None, metavar='GROUP', help='Unix group allowed if `pam` enabled, if empty any user is allowed') 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('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)') tornado.options.define('test_port', default=443, metavar='PORT', help='Port to connect to Telegram test environment') tornado.options.define('timezone', default='UTC', metavar='TIMEZONE', help='Timezone to use for dates (timestamps in history, last in dialogs, etc.)') tornado.options.define('tls', default=False, help='Use TLS/SSL encrypted connection for IRC server') tornado.options.define('tls_cert', default=None, metavar='CERTFILE', help='IRC server certificate chain for TLS/SSL, also can contain private key if not defined with `tls_key`') tornado.options.define('tls_key', default=None, metavar='KEYFILE', help='IRC server private key for TLS/SSL') tornado.options.define('upload_dir', default=None, metavar='PATH', help='Directory where files to upload are picked up, default "upload" in `cache_dir`') 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) config_file = os.path.expanduser(tornado.options.options.config) config_dir = os.path.expanduser(tornado.options.options.config_dir) if not os.path.exists(config_dir): os.makedirs(config_dir) defered_logs = [(logging.INFO, 'Configuration Directory: %s', config_dir)] if not os.path.isabs(config_file): config_file = os.path.join(config_dir, config_file) if os.path.isfile(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) else: defered_logs.append((logging.WARNING, 'Configuration file not present, using only command line options and defaults')) # parse cmd line second time to override file options tornado.options.parse_command_line() options = tornado.options.options.as_dict() options['config_dir'] = config_dir # 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 irc_server = IRCTelegramd(logger, options) loop = asyncio.new_event_loop() loop.run_until_complete(irc_server.run(options)) loop.run_forever()