/
irgramd
  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()