1 #!/usr/bin/env python3 2 # 3 # irgramd: IRC-Telegram gateway - Main file 4 # 5 # Copyright (c) 2019 Peter Bui <pbui@bx612.space> 6 # Copyright (c) 2020-2024 E. Bosch <presidev@AT@gmail.com> 7 # 8 # Use of this source code is governed by a MIT style license that 9 # can be found in the LICENSE file included in this project. 10 11 import logging 12 import os 13 import asyncio 14 15 import tornado.options 16 import tornado.tcpserver 17 import ssl 18 19 # Local modules 20 21 from irc import IRCHandler 22 from telegram import TelegramHandler 23 from utils import parse_loglevel 24 25 # IRC Telegram Daemon 26 27 class IRCTelegramd(tornado.tcpserver.TCPServer): 28 def __init__(self, logger, settings): 29 self.logger = logger 30 effective_port = settings['irc_port'] 31 32 if settings['tls']: 33 if not settings['tls_cert']: # error 34 self.logger.error('TLS configured but certificate not present') 35 exit(1) 36 tls_context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH) 37 tls_context.load_cert_chain(os.path.expanduser(settings['tls_cert']), os.path.expanduser(settings['tls_key'])) 38 if not effective_port: 39 effective_port = 6697 40 self.logger.info('TLS configured') 41 else: 42 tls_context = None 43 if not effective_port: 44 effective_port = 6667 45 46 tornado.tcpserver.TCPServer.__init__(self, ssl_options=tls_context) 47 48 self.address = settings['irc_address'] 49 self.port = effective_port 50 self.irc_handler = None 51 self.tg_handler = None 52 53 54 async def handle_stream(self, stream, address): 55 await self.irc_handler.run(stream, address) 56 57 async def run(self, settings): 58 self.listen(self.port, self.address) 59 self.logger.info('irgramd listening on %s:%s', self.address, self.port) 60 self.irc_handler = IRCHandler(settings) 61 self.tg_handler = TelegramHandler(self.irc_handler, settings) 62 self.irc_handler.set_telegram(self.tg_handler) 63 await self.tg_handler.initialize_telegram() 64 65 66 # Main Execution 67 68 if __name__ == '__main__': 69 # Remove tornado.log options (ugly hacks but these must not be defined) 70 tornado.options.options.logging = None 71 tornado_log_options = tuple(x for x in tornado.options.options._options.keys() if x != 'help' and x != 'logging') 72 for opt in tornado_log_options: 73 del tornado.options.options._options[opt] 74 # and reuse "--logging" to document empty "--" ;) 75 tornado.options.options._options['logging'].help = 'Stop parsing options' 76 for att in ('name', 'metavar', 'group_name', 'default'): 77 setattr(tornado.options.options._options['logging'], att, '') 78 # Define irgramd options 79 tornado.options.define('api_hash', default=None, metavar='HASH', help='Telegram API Hash for your account (obtained from https://my.telegram.org/apps)') 80 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)') 81 tornado.options.define('ask_code', default=False, help='Ask authentication code (sent by Telegram) in console instead of "code" service command in IRC') 82 tornado.options.define('cache_dir', default='~/.cache/irgramd', metavar='PATH', help='Cache directory where telegram media is saved by default') 83 tornado.options.define('char_in_encoding', default='utf-8', metavar='ENCODING', help='Character input encoding for IRC') 84 tornado.options.define('char_out_encoding', default='utf-8', metavar='ENCODING', help='Character output encoding for IRC') 85 tornado.options.define('config', default='irgramdrc', metavar='CONFIGFILE', help='Config file absolute or relative to `config_dir` (command line options override it)') 86 tornado.options.define('config_dir', default='~/.config/irgramd', metavar='PATH', help='Configuration directory where telegram session info is saved') 87 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') 88 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') 89 tornado.options.define('emoji_ascii', default=False, help='Replace emoji with ASCII emoticons') 90 tornado.options.define('geo_url', type=str, default=None, metavar='TEMPLATE_URL', help='Use custom URL for showing geo latitude/longitude location, eg. OpenStreetMap') 91 tornado.options.define('hist_timestamp_format', metavar='DATETIME_FORMAT', help='Format string for timestamps in history, see https://www.strfti.me') 92 tornado.options.define('irc_address', default='127.0.0.1', metavar='ADDRESS', help='Address to listen on for IRC') 93 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') 94 tornado.options.define('irc_password', default='', metavar='PASSWORD', help='Password for IRC authentication, if `pam` is set, PAM authentication will be used instead') 95 tornado.options.define('irc_port', type=int, default=None, metavar='PORT', help='Port to listen on for IRC. (default 6667, default with TLS 6697)') 96 tornado.options.define('log_file', default=None, metavar='PATH', help='File where logs are appended, if not set will be stderr') 97 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') 98 tornado.options.define('media_dir', default=None, metavar='PATH', help='Directory where Telegram media files are downloaded, default "media" in `cache_dir`') 99 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') 100 tornado.options.define('pam', default=False, help='Use PAM for IRC authentication, if not set you should set `irc_password`') 101 tornado.options.define('pam_group', default=None, metavar='GROUP', help='Unix group allowed if `pam` enabled, if empty any user is allowed') 102 tornado.options.define('phone', default=None, metavar='PHONE_NUMBER', help='Phone number associated with the Telegram account to receive the authorization codes if necessary') 103 tornado.options.define('quote_length', default=50, metavar='LENGTH', help='Max length of the text quoted in replies and reactions, if longer is truncated') 104 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') 105 tornado.options.define('test', default=False, help='Connect to Telegram test environment') 106 tornado.options.define('test_datacenter', default=2, metavar='DATACENTER_NUMBER', help='Datacenter to connect to Telegram test environment') 107 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)') 108 tornado.options.define('test_port', default=443, metavar='PORT', help='Port to connect to Telegram test environment') 109 tornado.options.define('timezone', default='UTC', metavar='TIMEZONE', help='Timezone to use for dates (timestamps in history, last in dialogs, etc.)') 110 tornado.options.define('tls', default=False, help='Use TLS/SSL encrypted connection for IRC server') 111 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`') 112 tornado.options.define('tls_key', default=None, metavar='KEYFILE', help='IRC server private key for TLS/SSL') 113 tornado.options.define('upload_dir', default=None, metavar='PATH', help='Directory where files to upload are picked up, default "upload" in `cache_dir`') 114 try: 115 # parse cmd line first time to get --config and --config_dir 116 tornado.options.parse_command_line() 117 except Exception as exc: 118 print(exc) 119 exit(1) 120 config_file = os.path.expanduser(tornado.options.options.config) 121 config_dir = os.path.expanduser(tornado.options.options.config_dir) 122 if not os.path.exists(config_dir): 123 os.makedirs(config_dir) 124 defered_logs = [(logging.INFO, 'Configuration Directory: %s', config_dir)] 125 126 if not os.path.isabs(config_file): 127 config_file = os.path.join(config_dir, config_file) 128 if os.path.isfile(config_file): 129 defered_logs.append((logging.INFO, 'Using configuration file: %s', config_file)) 130 try: 131 tornado.options.parse_config_file(config_file) 132 except Exception as exc: 133 print(exc) 134 exit(1) 135 else: 136 defered_logs.append((logging.WARNING, 'Configuration file not present, using only command line options and defaults')) 137 # parse cmd line second time to override file options 138 tornado.options.parse_command_line() 139 140 options = tornado.options.options.as_dict() 141 options['config_dir'] = config_dir 142 143 # configure logging 144 loglevel = parse_loglevel(options['log_level']) 145 if loglevel == False: 146 print("Option 'log_level' requires one of these values: {}".format(tornado.options.options._options['log-level'].metavar)) 147 exit(1) 148 logger_formats = { 'datefmt':'%Y-%m-%d %H:%M:%S', 'format':'[%(levelname).1s %(asctime)s %(module)s:%(lineno)d] %(message)s' } 149 logger = logging.getLogger() 150 if options['log_file']: 151 logging.basicConfig(filename=options['log_file'], level=loglevel, **logger_formats) 152 else: 153 logging.basicConfig(level=loglevel, **logger_formats) 154 155 for log in defered_logs: 156 logger.log(*log) 157 158 # main loop 159 irc_server = IRCTelegramd(logger, options) 160 loop = asyncio.new_event_loop() 161 loop.run_until_complete(irc_server.run(options)) 162 loop.run_forever()