diff --git a/smsbot.py b/smsbot.py
old mode 100644
new mode 100755
index 20a812d..bb8ebbe
--- a/smsbot.py
+++ b/smsbot.py
@@ -1,147 +1,160 @@
-try:
- import os
- import logging
- from functools import wraps
- from flask import Flask, request, abort
- from waitress import serve
- from twilio.request_validator import RequestValidator
- from telegram import ParseMode
- from telegram.ext import Updater, CommandHandler
-except ImportError as err:
- print(f"Failed to import required modules: {err}")
+#!/usr/bin/env python
+import logging
+import os
+import sys
+from functools import wraps
-# FB - Enable logging
-logging.basicConfig(
- format='%(asctime)s - %(levelname)s - %(message)s', level=logging.INFO)
-logger = logging.getLogger(__name__)
+from flask import Flask, abort, request, current_app
+from telegram import ParseMode
+from telegram.ext import CommandHandler, Updater
+from twilio.request_validator import RequestValidator
+from waitress import serve
-webhook_listener = Flask(__name__)
+__version__ = '0.0.1'
+
+
+class TelegramSmsBot(object):
+
+ owner_id = None
+ subscriber_ids = []
+
+ def __init__(self, token):
+ self.logger = logging.getLogger(self.__class__.__name__)
+ self.bot_token = token
+
+ def start(self):
+ self.logger.info('Starting bot...')
+ self.updater = Updater(self.bot_token, use_context=True)
+ self.updater.dispatcher.add_handler(CommandHandler('help', self.help_handler))
+ self.updater.dispatcher.add_handler(CommandHandler('start', self.help_handler))
+ self.updater.dispatcher.add_handler(CommandHandler('subscribe', self.subscribe_handler))
+ self.updater.dispatcher.add_handler(CommandHandler('unsubscribe', self.unsubscribe_handler))
+ self.updater.dispatcher.add_error_handler(self.error_handler)
+
+ self.updater.start_polling()
+ self.bot = self.updater.bot
+ self.logger.info('Bot Ready')
+
+ def stop(self):
+ self.updater.stop()
+
+ def help_handler(self, update, context):
+ """Send a message when the command /help is issued."""
+ self.logger.info('/help command received in chat: %s', update.message.chat)
+ update.message.reply_markdown('Smsbot v{0}\n\n/help\n/subscribe\n/unsubscribe'.format(__version__))
+
+ def subscribe_handler(self, update, context):
+ self.logger.info('/subscribe command received')
+ if not update.message.chat['id'] in self.subscriber_ids:
+ self.logger.info('{0} subscribed'.format(update.message.chat['username']))
+ self.subscriber_ids.append(update.message.chat['id'])
+ update.message.reply_markdown('You have been subscribed to SMS notifications')
+ else:
+ update.message.reply_markdown('You are already subscribed to SMS notifications')
+
+ def unsubscribe_handler(self, update, context):
+ self.logger.info('/unsubscribe command received')
+ if update.message.chat['id'] in self.subscriber_ids:
+ self.logger.info('{0} unsubscribed'.format(update.message.chat['username']))
+ self.subscriber_ids.remove(update.message.chat['id'])
+ update.message.reply_markdown('You have been unsubscribed to SMS notifications')
+ else:
+ update.message.reply_markdown('You are not subscribed to SMS notifications')
+
+ def error_handler(self, update, context):
+ """Log Errors caused by Updates."""
+ self.logger.warning('Update "%s" caused error "%s"', update, context.error)
+
+ def send_message(self, message, chat_id):
+ self.bot.sendMessage(text=message, chat_id=chat_id, parse_mode=ParseMode.MARKDOWN_V2)
+
+ def send_owner(self, message):
+ if self.owner_id:
+ self.send_message(message, self.owner_id)
+
+ def send_subscribers(self, message):
+ for chat_id in self.subscriber_ids:
+ self.send_message(message, chat_id)
+
+ def set_owner(self, chat_id):
+ self.owner_id = chat_id
+
+ def add_subscriber(self, chat_id):
+ self.subscriber_ids.append(chat_id)
def validate_twilio_request(f):
"""Validates that incoming requests genuinely originated from Twilio"""
@wraps(f)
def decorated_function(*args, **kwargs):
- # FB - Create an instance of the RequestValidator class
- validator = RequestValidator(os.environ.get('TWILIO_AUTH_TOKEN'))
+ # Create an instance of the RequestValidator class
+ twilio_token = os.environ.get('SMSBOT_TWILIO_AUTH_TOKEN')
- # FB - Validate the request using its URL, POST data, and X-TWILIO-SIGNATURE header
+ if not twilio_token:
+ current_app.logger.warning('Twilio request validation skipped due to SMSBOT_TWILIO_AUTH_TOKEN missing')
+ return f(*args, **kwargs)
+
+ validator = RequestValidator(twilio_token)
+
+ # Validate the request using its URL, POST data,
+ # and X-TWILIO-SIGNATURE header
request_valid = validator.validate(
request.url,
request.form,
request.headers.get('X-TWILIO-SIGNATURE', ''))
- # FB - Continue processing the request if it's valid, return a 403 error if it's not
- if request_valid:
+ # Continue processing the request if it's valid, return a 403 error if
+ # it's not
+ if request_valid or current_app.debug:
return f(*args, **kwargs)
else:
- logger.error('Invalid twilio request, aborting')
return abort(403)
return decorated_function
-def tg_help_handler(update, context):
- """Send a message when the command /help is issued."""
- logger.info('/help command received in chat: %s', update.message.chat)
- update.message.reply_markdown(
- 'Find out more on [Github](https://github.com/FiveBoroughs/Twilio2Telegram)')
+class TwilioWebhookHandler(object):
+ def __init__(self):
+ self.app = Flask(self.__class__.__name__)
+ self.app.add_url_rule('/', 'index', self.index, methods=['GET'])
+ self.app.add_url_rule('/message', 'message', self.message, methods=['POST'])
+ self.app.add_url_rule('/call', 'call', self.call, methods=['POST'])
-def tg_error_handler(update, context):
- """Log Errors caused by Updates."""
- logger.warning('Update "%s" caused error "%s"', update, context.error)
+ def set_bot(self, bot):
+ self.bot = bot
+ def index(self):
+ return 'Smsbot v{0}'.format(__version__)
-def tg_bot_start():
- """Start the telegram bot."""
- # FB - Create the Updater
- updater = Updater(os.environ.get('TELEGRAM_BOT_TOKEN'), use_context=True)
+ @validate_twilio_request
+ def message(self):
+ return ''
- # FB - Get the dispatcher to register handlers
- dispatcher = updater.dispatcher
+ @validate_twilio_request
+ def call(self):
+ # Always reject calls
+ return ''
- # FB - on /help command
- dispatcher.add_handler(CommandHandler("help", tg_help_handler))
-
- # FB - log all errors
- dispatcher.add_error_handler(tg_error_handler)
-
- # FB - Start the Bot
- updater.start_polling()
-
- return updater.bot
-
-
-def tg_send_owner_message(message):
- """Send telegram message to owner."""
- telegram_bot.sendMessage(text=message, chat_id=os.environ.get('TELEGRAM_OWNER'), parse_mode=ParseMode.MARKDOWN)
-
-
-def tg_send_subscribers_message(message):
- """Send telegram messages to subscribers."""
- for telegram_destination in os.environ.get('TELEGRAM_SUBSCRIBERS').split(','):
- telegram_bot.sendMessage(
- text=message, chat_id=telegram_destination, parse_mode=ParseMode.MARKDOWN)
-
-
-@webhook_listener.route('/', methods=['GET'])
-def index():
- """Upon call of homepage."""
- logger.info('"/" reached, IP: %s', request.remote_addr)
-
- return webhook_listener.send_static_file('Index.html')
-
-
-@webhook_listener.route('/message', methods=['POST'])
-@validate_twilio_request
-def recv_message():
- """Upon reception of a SMS."""
- logger.info(' "/message" reached, IP: %s', request.remote_addr)
- # FB - Format telegram Message
- telegram_message = 'Text from `{From}` ({Country}, {State}) :``` {Body}```'.format(
- From=request.values.get('From', 'unknown'),
- Country=request.values.get('FromCountry', 'unknown'),
- State=request.values.get('FromState', 'unknown'),
- Body=request.values.get('Body', 'unknown')
- )
-
- logger.info(telegram_message)
- # FB - Send telegram alerts
- tg_send_owner_message('Twilio ID : `{Id}`\n'.format(
- Id=request.values.get('MessageSid', 'unknown')) + telegram_message)
- tg_send_subscribers_message(telegram_message)
-
- # FB - return empty response to avoid further Twilio fees
- return ''
-
-
-
-@webhook_listener.route('/call', methods=['POST'])
-@validate_twilio_request
-def recv_call():
- """Upon reception of a call."""
- logger.info(' "/call" reached, IP: %s', request.remote_addr)
- # FB - Format telegram Message
- telegram_message = 'Call from `{From}` ({Country}, {State}) :``` {Status}```'.format(
- From=request.values.get('From', 'unknown'),
- Country=request.values.get('FromCountry', 'unknown'),
- State=request.values.get('FromState', 'unknown'),
- Status=request.values.get('CallStatus', 'unknown')
- )
-
- logger.info(telegram_message)
- # FB - Send telegram alerts
- tg_send_owner_message('Twilio ID : `{Id}`\n'.format(
- Id=request.values.get('CallSid', 'unknown')) + telegram_message)
- tg_send_subscribers_message(telegram_message)
-
- # FB - reject the call without being billed
- return ''
+ def serve(self, host='0.0.0.0', port=80, debug=False):
+ serve(self.app, host=host, port=port)
if __name__ == "__main__":
- logger.info('Starting bot')
- # FB - Start the telegram bot
- telegram_bot = tg_bot_start()
- # FB - Start the website
- serve(webhook_listener, host='0.0.0.0', port=80)
+ logging.basicConfig(level=logging.INFO)
+
+ listen_host = os.environ.get('SMSBOT_LISTEN_HOST') or '0.0.0.0'
+ listen_port = int(os.environ.get('SMSBOT_LISTEN_PORT') or '80')
+
+ token = os.environ.get('SMSBOT_TELEGRAM_BOT_TOKEN')
+ if not token:
+ logging.exception('Telegram Bot token missing')
+ sys.exit(1)
+
+ # Start bot
+ telegram_bot = TelegramSmsBot(token)
+ telegram_bot.start()
+
+ # Start webhooks
+ webhooks = TwilioWebhookHandler()
+ webhooks.set_bot(telegram_bot)
+ webhooks.serve()