14 Commits

Author SHA1 Message Date
d918d95c7f Bump helm chart version 2022-06-14 10:12:28 +01:00
70c71ff731 Merge pull request #9 from nikdoof/renovate/common-4.x
Update Helm release common to v4.4.2
2022-06-14 10:11:25 +01:00
b9dcd8a880 Merge pull request #10 from nikdoof/renovate/helm-chart-releaser-action-1.x
Update helm/chart-releaser-action action to v1.4.0
2022-06-14 10:11:15 +01:00
6e569e16c7 Update README.md 2022-06-14 10:10:53 +01:00
Renovate Bot
d1b1f94dc7 Update helm/chart-releaser-action action to v1.4.0 2022-06-09 14:07:47 +00:00
Renovate Bot
e762f99168 Update Helm release common to v4.4.2 2022-06-09 14:07:44 +00:00
2c35051b54 Clean up lint issues 2022-06-09 12:46:05 +01:00
dea3ae84bd Switch to use argparse 2022-06-09 12:43:45 +01:00
5d4f83bdc3 Sort imports 2022-06-09 12:12:32 +01:00
7f481c47ed Add Prometheus metrics 2022-06-09 12:11:48 +01:00
db04de242a Cleanup init and owner setup 2022-06-09 11:45:46 +01:00
5a61444535 Build command list for help from handlers 2022-06-09 11:36:40 +01:00
a89c560145 Fix release workflow 2022-06-09 10:21:16 +01:00
9adaf6c663 Add wheel requirements for dev 2022-06-09 10:17:32 +01:00
10 changed files with 91 additions and 29 deletions

View File

@@ -26,7 +26,7 @@ jobs:
version: v3.6.3 version: v3.6.3
- name: Run chart-releaser - name: Run chart-releaser
uses: helm/chart-releaser-action@v1.2.1 uses: helm/chart-releaser-action@v1.4.0
env: env:
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
CR_RELEASE_NAME_TEMPLATE: "smsbot-helm-chart-{{ .Version }}" CR_RELEASE_NAME_TEMPLATE: "smsbot-helm-chart-{{ .Version }}"

View File

@@ -13,6 +13,8 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
with:
python-version: '3.10'
- run: pip install -r requirements-dev.txt - run: pip install -r requirements-dev.txt
- name: Build Assets - name: Build Assets

View File

@@ -22,3 +22,16 @@ All configuration is provided via environment variables
| SMSBOT_OWNER_ID | No | ID of the owner of this bot | | SMSBOT_OWNER_ID | No | ID of the owner of this bot |
| SMSBOT_TELEGRAM_BOT_TOKEN | Yes | Your Bot Token for Telegram | | SMSBOT_TELEGRAM_BOT_TOKEN | Yes | Your Bot Token for Telegram |
| SMSBOT_TWILIO_AUTH_TOKEN | No | Twilio auth token, used to validate any incoming webhook calls | | SMSBOT_TWILIO_AUTH_TOKEN | No | Twilio auth token, used to validate any incoming webhook calls |
## Setup
To configure SMSBot, you'll need a Twilio account, either paid or trial is fine.
* Setup a number in the location you want.
* Under Phone Numbers -> Manage -> Active Numbers, click the number you want to setup.
* In the "Voice & Fax" section, update the "A Call Comes In" to the URL of your SMSBot instance, with the endpoint being `/call`, e.g. `http://mymachine.test.com/call`
* In the "Messaging" section, update the "A Message Comes In" to the URL of your SMSBot instance, with the endpoint being `/message`, e.g. `http://mymachine.test.com/message`
Your bot should now receive messages, on Telegram you need to start a chat or invite it into any channels you want, then update the `SMSBOT_DEFAULT_SUBSCRIBERS` values with their IDs.
**Note**: You cannot send test messages from your Twilio account to your Twilio numbers, they'll be silently dropped or fail with an "Invalid Number" error.

View File

@@ -2,7 +2,7 @@ apiVersion: v2
appVersion: 0.0.3 appVersion: 0.0.3
description: A simple Telegram bot to receive SMS messages. description: A simple Telegram bot to receive SMS messages.
name: smsbot name: smsbot
version: 0.0.3 version: 0.0.4
kubeVersion: ">=1.19.0-0" kubeVersion: ">=1.19.0-0"
keywords: keywords:
- smsbot - smsbot
@@ -15,4 +15,4 @@ maintainers:
dependencies: dependencies:
- name: common - name: common
repository: https://library-charts.k8s-at-home.com repository: https://library-charts.k8s-at-home.com
version: 4.3.0 version: 4.4.2

1
requirements-dev.txt Normal file
View File

@@ -0,0 +1 @@
wheel

View File

@@ -1,4 +1,5 @@
flask flask
waitress waitress
twilio twilio
python-telegram-bot python-telegram-bot
prometheus_client

View File

@@ -20,6 +20,7 @@ install_requires =
waitress waitress
twilio twilio
python-telegram-bot python-telegram-bot
prometheus_client
[options.entry_points] [options.entry_points]
console_scripts = console_scripts =

View File

@@ -1,6 +1,6 @@
import argparse
import logging import logging
import os import os
import sys
import pkg_resources import pkg_resources
@@ -11,27 +11,31 @@ pkg_version = pkg_resources.require('smsbot')[0].version
def main(): def main():
logging.basicConfig(level=logging.INFO) parser = argparse.ArgumentParser('smsbot')
parser.add_argument('--listen-host', default=os.environ.get('SMSBOT_LISTEN_HOST') or '0.0.0.0')
parser.add_argument('--listen-port', default=os.environ.get('SMSBOT_LISTEN_PORT') or '80')
parser.add_argument('--telegram-bot-token', default=os.environ.get('SMSBOT_TELEGRAM_BOT_TOKEN'))
parser.add_argument('--owner-id', default=os.environ.get('SMSBOT_OWNER_ID'))
parser.add_argument('--default-subscribers', default=os.environ.get('SMSBOT_DEFAULT_SUBSCRIBERS'))
parser.add_argument('--log-level', default='INFO')
args = parser.parse_args()
logging.basicConfig(level=logging.getLevelName(args.log_level))
logging.info('smsbot v%s', pkg_version) logging.info('smsbot v%s', pkg_version)
logging.debug('Arguments: %s', args)
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.error('Telegram Bot token missing')
sys.exit(1)
# Start bot # Start bot
telegram_bot = TelegramSmsBot(token) telegram_bot = TelegramSmsBot(args.telegram_bot_token)
# Set the owner ID if configured # Set the owner ID if configured
if 'SMSBOT_OWNER_ID' in os.environ: if args.owner_id:
telegram_bot.set_owner(os.environ.get('SMSBOT_OWNER_ID')) telegram_bot.set_owner(args.owner_id)
else:
logging.warning('No Owner ID is set, which is not a good idea...')
# Add default subscribers # Add default subscribers
if 'SMSBOT_DEFAULT_SUBSCRIBERS' in os.environ: if args.default_subscribers:
for chat_id in os.environ.get('SMSBOT_DEFAULT_SUBSCRIBERS').split(','): for chat_id in args.default_subscribers.split(','):
telegram_bot.add_subscriber(chat_id) telegram_bot.add_subscriber(chat_id)
telegram_bot.start() telegram_bot.start()
@@ -39,4 +43,4 @@ def main():
# Start webhooks # Start webhooks
webhooks = TwilioWebhookHandler() webhooks = TwilioWebhookHandler()
webhooks.set_bot(telegram_bot) webhooks.set_bot(telegram_bot)
webhooks.serve(host=listen_host, port=listen_port) webhooks.serve(host=args.listen_host, port=args.listen_port)

View File

@@ -1,27 +1,34 @@
import logging import logging
from prometheus_client import Counter, Summary
from telegram.ext import CommandHandler, Updater from telegram.ext import CommandHandler, Updater
from smsbot.utils import get_smsbot_version from smsbot.utils import get_smsbot_version
REQUEST_TIME = Summary('telegram_request_processing_seconds', 'Time spent processing request')
COMMAND_COUNT = Counter('telegram_command_count', 'Total number of commands processed')
class TelegramSmsBot(object): class TelegramSmsBot(object):
def __init__(self, token, owner=None, subscribers=None): def __init__(self, telegram_token, allow_subscribing=False, owner=None, subscribers=None):
self.logger = logging.getLogger(self.__class__.__name__) self.logger = logging.getLogger(self.__class__.__name__)
self.bot_token = token self.bot_token = telegram_token
self.owner_id = owner
self.subscriber_ids = subscribers or [] self.subscriber_ids = subscribers or []
self.set_owner(owner)
def start(self):
self.logger.info('Starting bot...')
self.updater = Updater(self.bot_token, use_context=True) 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('help', self.help_handler))
self.updater.dispatcher.add_handler(CommandHandler('start', 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)) if allow_subscribing:
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.dispatcher.add_error_handler(self.error_handler)
def start(self):
self.logger.info('Starting bot...')
self.updater.start_polling() self.updater.start_polling()
self.bot = self.updater.bot self.bot = self.updater.bot
self.logger.info('Bot Ready') self.logger.info('Bot Ready')
@@ -29,11 +36,19 @@ class TelegramSmsBot(object):
def stop(self): def stop(self):
self.updater.stop() self.updater.stop()
@REQUEST_TIME.time()
def help_handler(self, update, context): def help_handler(self, update, context):
"""Send a message when the command /help is issued.""" """Send a message when the command /help is issued."""
self.logger.info('/help command received in chat: %s', update.message.chat) 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(get_smsbot_version()))
commands = []
for command in self.updater.dispatcher.handlers[0]:
commands.extend(['/{0}'.format(cmd) for cmd in command.command])
update.message.reply_markdown('Smsbot v{0}\n\n{1}'.format(get_smsbot_version(), '\n'.join(commands)))
COMMAND_COUNT.inc()
@REQUEST_TIME.time()
def subscribe_handler(self, update, context): def subscribe_handler(self, update, context):
self.logger.info('/subscribe command received') self.logger.info('/subscribe command received')
if update.message.chat['id'] not in self.subscriber_ids: if update.message.chat['id'] not in self.subscriber_ids:
@@ -43,7 +58,9 @@ class TelegramSmsBot(object):
update.message.reply_markdown('You have been subscribed to SMS notifications') update.message.reply_markdown('You have been subscribed to SMS notifications')
else: else:
update.message.reply_markdown('You are already subscribed to SMS notifications') update.message.reply_markdown('You are already subscribed to SMS notifications')
COMMAND_COUNT.inc()
@REQUEST_TIME.time()
def unsubscribe_handler(self, update, context): def unsubscribe_handler(self, update, context):
self.logger.info('/unsubscribe command received') self.logger.info('/unsubscribe command received')
if update.message.chat['id'] in self.subscriber_ids: if update.message.chat['id'] in self.subscriber_ids:
@@ -53,6 +70,7 @@ class TelegramSmsBot(object):
update.message.reply_markdown('You have been unsubscribed to SMS notifications') update.message.reply_markdown('You have been unsubscribed to SMS notifications')
else: else:
update.message.reply_markdown('You are not subscribed to SMS notifications') update.message.reply_markdown('You are not subscribed to SMS notifications')
COMMAND_COUNT.inc()
def error_handler(self, update, context): def error_handler(self, update, context):
"""Log Errors caused by Updates.""" """Log Errors caused by Updates."""
@@ -72,6 +90,8 @@ class TelegramSmsBot(object):
def set_owner(self, chat_id): def set_owner(self, chat_id):
self.owner_id = chat_id self.owner_id = chat_id
if self.owner_id and self.owner_id not in self.subscriber_ids:
self.subscriber_ids.append(self.owner_id)
def add_subscriber(self, chat_id): def add_subscriber(self, chat_id):
self.subscriber_ids.append(chat_id) self.subscriber_ids.append(chat_id)

View File

@@ -3,11 +3,17 @@ import os
from functools import wraps from functools import wraps
from flask import Flask, abort, current_app, request from flask import Flask, abort, current_app, request
from prometheus_client import Counter, Summary, make_wsgi_app
from twilio.request_validator import RequestValidator from twilio.request_validator import RequestValidator
from waitress import serve from waitress import serve
from werkzeug.middleware.dispatcher import DispatcherMiddleware
from smsbot.utils import get_smsbot_version from smsbot.utils import get_smsbot_version
REQUEST_TIME = Summary('webhook_request_processing_seconds', 'Time spent processing request')
MESSAGE_COUNT = Counter('webhook_message_count', 'Total number of messages processed')
CALL_COUNT = Counter('webhook_call_count', 'Total number of calls processed')
def validate_twilio_request(func): def validate_twilio_request(func):
"""Validates that incoming requests genuinely originated from Twilio""" """Validates that incoming requests genuinely originated from Twilio"""
@@ -47,15 +53,26 @@ class TwilioWebhookHandler(object):
self.app.add_url_rule('/message', 'message', self.message, methods=['POST']) self.app.add_url_rule('/message', 'message', self.message, methods=['POST'])
self.app.add_url_rule('/call', 'call', self.call, methods=['POST']) self.app.add_url_rule('/call', 'call', self.call, methods=['POST'])
# Add prometheus wsgi middleware to route /metrics requests
self.app.wsgi_app = DispatcherMiddleware(self.app.wsgi_app, {
'/metrics': make_wsgi_app(),
})
def set_bot(self, bot): # noqa: WPS615 def set_bot(self, bot): # noqa: WPS615
self.bot = bot self.bot = bot
def index(self): def index(self):
return '' return ''
@REQUEST_TIME.time()
def health(self): def health(self):
return '<h1>Smsbot v{0}</h1><p><b>Owner</b>: {1}</p><p><b>Subscribers</b>: {2}</p>'.format(get_smsbot_version(), self.bot.owner_id, self.bot.subscriber_ids) return {
'version': get_smsbot_version(),
'owner': self.bot.owner_id,
'subscribers': self.bot.subscriber_ids,
}
@REQUEST_TIME.time()
@validate_twilio_request @validate_twilio_request
def message(self): def message(self):
current_app.logger.info('Received SMS from {From}: {Body}'.format(**request.values.to_dict())) current_app.logger.info('Received SMS from {From}: {Body}'.format(**request.values.to_dict()))
@@ -64,14 +81,17 @@ class TwilioWebhookHandler(object):
self.bot.send_subscribers(message) self.bot.send_subscribers(message)
# Return a blank response # Return a blank response
MESSAGE_COUNT.inc()
return '<response></response>' return '<response></response>'
@REQUEST_TIME.time()
@validate_twilio_request @validate_twilio_request
def call(self): def call(self):
current_app.logger.info('Received Call from {From}'.format(**request.values.to_dict())) current_app.logger.info('Received Call from {From}'.format(**request.values.to_dict()))
self.bot.send_subscribers('Received Call from {From}, rejecting.'.format(**request.values.to_dict())) self.bot.send_subscribers('Received Call from {From}, rejecting.'.format(**request.values.to_dict()))
# Always reject calls # Always reject calls
CALL_COUNT.inc()
return '<Response><Reject/></Response>' return '<Response><Reject/></Response>'
def serve(self, host='0.0.0.0', port=80, debug=False): def serve(self, host='0.0.0.0', port=80, debug=False):