mirror of
https://github.com/nikdoof/smsbot.git
synced 2025-12-13 10:02:15 +00:00
Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
899b88e907
|
|||
|
000f0932d8
|
|||
|
3d28716817
|
|||
|
74dac7603e
|
|||
|
076e2464a0
|
|||
|
85c72462c4
|
|||
| 41971aabdf | |||
| befa3df48d | |||
| d504dfb95d | |||
| e51541c79a | |||
| cd3699f185 | |||
| 7d7aa97c24 | |||
|
|
d51c6fbe73 | ||
| 21276e1c24 | |||
|
|
4e15d4a8ed | ||
| b1bdcb456a | |||
| f7a151235a | |||
| 1d8b177cb4 | |||
|
|
3389144484 | ||
|
|
aa3ca58b58 | ||
|
|
2854626c27 | ||
|
|
dc0745ce76 | ||
|
|
19f8183ddc | ||
|
|
f51928cb4b | ||
|
|
80be469a90 | ||
|
|
076216e089 | ||
|
90fc864be4
|
|||
|
3bf3823b77
|
|||
|
8c91dacc7e
|
|||
|
4bdf3e0f63
|
|||
| 24f72ade72 | |||
| 852a684d4e | |||
| 41bc52d046 | |||
|
|
90b5fa0621 | ||
|
|
f2fec6b5b6 | ||
| c7f7ef3fda | |||
| cdff8dab7b | |||
|
|
98aacb8093 | ||
| b30fa38e41 | |||
| c51ba0e0ae | |||
|
|
692a7eab37 | ||
|
|
5d7db991e6 | ||
|
|
30835e4a6f | ||
|
|
3b55f71db6 | ||
| d918d95c7f | |||
| 70c71ff731 | |||
| b9dcd8a880 | |||
| 6e569e16c7 | |||
|
|
d1b1f94dc7 | ||
|
|
e762f99168 | ||
|
2c35051b54
|
|||
|
dea3ae84bd
|
|||
|
5d4f83bdc3
|
|||
|
7f481c47ed
|
|||
|
db04de242a
|
|||
|
5a61444535
|
|||
|
a89c560145
|
|||
|
9adaf6c663
|
|||
|
ddba5cb011
|
|||
|
c70b2485f5
|
|||
|
e8e41bbfef
|
|||
|
6fd8a2e8bd
|
|||
|
5c89056a9e
|
|||
|
0c260c9a08
|
|||
|
a2692fecc9
|
|||
|
8f5218c2bb
|
|||
|
237db7fa7d
|
|||
|
f87f2f68a4
|
|||
|
c6a6ffbc9f
|
|||
|
509c6f7c43
|
|||
|
d54c860b16
|
8
.github/workflows/build-container.yaml
vendored
8
.github/workflows/build-container.yaml
vendored
@@ -12,11 +12,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
registry: ghcr.io
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
tags: |
|
||||
|
||||
27
.github/workflows/lint.yaml
vendored
27
.github/workflows/lint.yaml
vendored
@@ -1,6 +1,6 @@
|
||||
name: Lint
|
||||
|
||||
'on':
|
||||
"on":
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
@@ -9,13 +9,22 @@ name: Lint
|
||||
- main
|
||||
|
||||
jobs:
|
||||
run-linters:
|
||||
name: Run linters
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9"]
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: wemake-python-styleguide
|
||||
uses: wemake-services/wemake-python-styleguide@0.16.1
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install Poetry
|
||||
uses: snok/install-poetry@v1
|
||||
with:
|
||||
virtualenvs-create: true
|
||||
virtualenvs-in-project: true
|
||||
- name: Lint with ruff
|
||||
run: |
|
||||
make lint
|
||||
|
||||
32
.github/workflows/release-chart.yaml
vendored
Normal file
32
.github/workflows/release-chart.yaml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: Release Helm Chart
|
||||
|
||||
"on":
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "charts/**/Chart.yaml"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config user.name "$GITHUB_ACTOR"
|
||||
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@v4
|
||||
with:
|
||||
version: v3.6.3
|
||||
|
||||
- name: Run chart-releaser
|
||||
uses: helm/chart-releaser-action@v1.6.0
|
||||
env:
|
||||
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
CR_RELEASE_NAME_TEMPLATE: "smsbot-helm-chart-{{ .Version }}"
|
||||
20
.github/workflows/release.yaml
vendored
20
.github/workflows/release.yaml
vendored
@@ -1,4 +1,3 @@
|
||||
---
|
||||
name: Release
|
||||
on:
|
||||
push:
|
||||
@@ -10,18 +9,23 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
- run: pip install -r requirements-dev.txt
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
|
||||
- name: Build Assets
|
||||
run: python setup.py sdist bdist_wheel
|
||||
- name: Install Poetry
|
||||
uses: snok/install-poetry@v1
|
||||
with:
|
||||
virtualenvs-create: true
|
||||
virtualenvs-in-project: true
|
||||
|
||||
- name: Build Release
|
||||
run: poetry build
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
name: "Version ${{ github.ref_name }}"
|
||||
generate_release_notes: true
|
||||
files: |
|
||||
dist/*
|
||||
26
Dockerfile
26
Dockerfile
@@ -1,5 +1,21 @@
|
||||
FROM python:3.10-alpine
|
||||
WORKDIR /app
|
||||
COPY . /app
|
||||
RUN pip install .
|
||||
ENTRYPOINT smsbot
|
||||
FROM python:3.9-alpine AS base
|
||||
|
||||
# Builder
|
||||
FROM base AS builder
|
||||
|
||||
ENV POETRY_VERSION=2.1.1
|
||||
|
||||
RUN apk add build-base unzip wget python3-dev libffi-dev rust cargo openssl-dev && pip install "poetry==$POETRY_VERSION" && poetry self add poetry-plugin-bundle
|
||||
WORKDIR /src
|
||||
COPY poetry.lock pyproject.toml README.md /src/
|
||||
COPY smsbot /src/smsbot
|
||||
RUN poetry bundle venv /runtime
|
||||
|
||||
|
||||
# Final container
|
||||
FROM base AS runtime
|
||||
|
||||
COPY --from=builder /runtime /runtime
|
||||
ENV PATH=/runtime/bin:$PATH
|
||||
EXPOSE 8000/tcp
|
||||
CMD ["smsbot"]
|
||||
7
Makefile
Normal file
7
Makefile
Normal file
@@ -0,0 +1,7 @@
|
||||
.venv:
|
||||
python3 -m pip install poetry
|
||||
python3 -m poetry install --with github
|
||||
|
||||
lint: .venv
|
||||
python3 -m poetry run ruff check --output-format=github --select=E9,F63,F7,F82 --target-version=py37 .
|
||||
python3 -m poetry run ruff check --output-format=github --target-version=py37 .
|
||||
26
README.md
26
README.md
@@ -9,3 +9,29 @@ Forked from [FiveBoroughs/Twilio2Telegram](https://github.com/FiveBoroughs/Twili
|
||||
This simple tool acts as a webhook receiver for the Twilio API, taking messages sent over and posting them on Telegram to a target chat or channel. This is useful for forwarding on 2FA messages, notification text messages for services that don't support international numbers (*cough* Disney, of all people).
|
||||
|
||||
The bot is designed to run within a Kubernetes environment, but can be operated as a individual container, or as a hand ran service.
|
||||
|
||||
## Configuration
|
||||
|
||||
All configuration is provided via environment variables
|
||||
|
||||
| Variable | Required? | Description |
|
||||
| -------------------------- | --------- | --------------------------------------------------------------------------- |
|
||||
| SMSBOT_DEFAULT_SUBSCRIBERS | No | A list of IDs, seperated by commas, to add to the subscribers list on start |
|
||||
| SMSBOT_LISTEN_HOST | No | The host for the webhooks to listen on, defaults to `0.0.0.0` |
|
||||
| SMSBOT_LISTEN_PORT | No | The port to listen to, defaults to `80` |
|
||||
| SMSBOT_OWNER_ID | No | ID of the owner of this bot |
|
||||
| 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 |
|
||||
|
||||
## 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.
|
||||
|
||||
18
charts/smsbot/Chart.yaml
Normal file
18
charts/smsbot/Chart.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
apiVersion: v2
|
||||
appVersion: 0.0.5
|
||||
description: A simple Telegram bot to receive SMS messages.
|
||||
name: smsbot
|
||||
version: 0.0.6
|
||||
kubeVersion: ">=1.19.0-0"
|
||||
keywords:
|
||||
- smsbot
|
||||
home: https://github.com/nikdoof/smsbot/tree/main/charts/smsbot
|
||||
sources:
|
||||
- https://github.com/nikdoof/smsbot
|
||||
maintainers:
|
||||
- name: nikdoof
|
||||
email: nikdoof@users.noreply.github.com
|
||||
dependencies:
|
||||
- name: common
|
||||
repository: https://library-charts.k8s-at-home.com
|
||||
version: 4.5.2
|
||||
10
charts/smsbot/ci/ct-values.yaml
Normal file
10
charts/smsbot/ci/ct-values.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
env:
|
||||
SMSBOT_TELEGRAM_BOT_TOKEN: xxxx
|
||||
ingress:
|
||||
main:
|
||||
enabled: true
|
||||
hosts:
|
||||
- host: smsbot.domain.tld
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
1
charts/smsbot/templates/NOTES.txt
Normal file
1
charts/smsbot/templates/NOTES.txt
Normal file
@@ -0,0 +1 @@
|
||||
{{ include "common.notes.defaultNotes" . }}
|
||||
1
charts/smsbot/templates/common.yaml
Normal file
1
charts/smsbot/templates/common.yaml
Normal file
@@ -0,0 +1 @@
|
||||
{{ include "common.all" . }}
|
||||
35
charts/smsbot/values.yaml
Normal file
35
charts/smsbot/values.yaml
Normal file
@@ -0,0 +1,35 @@
|
||||
#
|
||||
# IMPORTANT NOTE
|
||||
#
|
||||
# This chart inherits from the k8s@home library chart. You can check the default values/options here:
|
||||
# https://github.com/k8s-at-home/library-charts/tree/main/charts/stable/common/values.yaml
|
||||
#
|
||||
|
||||
image:
|
||||
# -- image repository
|
||||
repository: ghcr.io/nikdoof/smsbot
|
||||
# -- image pull policy
|
||||
pullPolicy: IfNotPresent
|
||||
# -- image tag
|
||||
tag: 0.0.5
|
||||
|
||||
# -- environment variables.
|
||||
# @default -- See below
|
||||
env:
|
||||
# -- Set the container timezone
|
||||
TZ: UTC
|
||||
# SMSBOT_TELEGRAM_BOT_TOKEN:
|
||||
|
||||
# -- Configures service settings for the chart.
|
||||
# @default -- See values.yaml
|
||||
service:
|
||||
main:
|
||||
ports:
|
||||
http:
|
||||
port: 80
|
||||
|
||||
ingress:
|
||||
# -- Enable and configure ingress settings for the chart under this key.
|
||||
# @default -- See values.yaml
|
||||
main:
|
||||
enabled: false
|
||||
1466
poetry.lock
generated
Normal file
1466
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
38
pyproject.toml
Normal file
38
pyproject.toml
Normal file
@@ -0,0 +1,38 @@
|
||||
[project]
|
||||
name = "smsbot"
|
||||
version = "0.0.6"
|
||||
description = "A simple Telegram bot to receive SMS messages."
|
||||
authors = [{ name = "Andrew Williams", email = "andy@tensixtyone.com" }]
|
||||
license = { text = "MIT" }
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.9,<3.10"
|
||||
dependencies = [
|
||||
"flask (>=3.1.0,<4.0.0)",
|
||||
"waitress (>=3.0.2,<4.0.0)",
|
||||
"twilio (>=9.4.6,<10.0.0)",
|
||||
"python-telegram-bot (<20)",
|
||||
"prometheus-client (>=0.21.1,<0.22.0)",
|
||||
]
|
||||
|
||||
[tool.poetry]
|
||||
packages = [{ include = "smsbot" }]
|
||||
|
||||
[tool.poetry.scripts]
|
||||
smsbot = "smsbot.cli:main"
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "^8.0.0"
|
||||
ruff = "^0.9.0"
|
||||
pytest-mock = "^3.12.0"
|
||||
pytest-flask = "^1.3.0"
|
||||
|
||||
[tool.poetry.group.github]
|
||||
optional = true
|
||||
|
||||
[tool.poetry.group.github.dependencies]
|
||||
pytest-github-actions-annotate-failures = "^0.3.0"
|
||||
@@ -1,4 +0,0 @@
|
||||
flask
|
||||
waitress
|
||||
twilio
|
||||
python-telegram-bot
|
||||
35
setup.cfg
35
setup.cfg
@@ -1,35 +0,0 @@
|
||||
[metadata]
|
||||
name = smsbot
|
||||
version = 0.0.1
|
||||
description = A simple Telegram bot to receive SMS messages.
|
||||
long_description = file: README.md, LICENSE
|
||||
license = MIT
|
||||
license_file = LICENSE
|
||||
classifiers =
|
||||
Framework :: Flask
|
||||
License :: OSI Approved :: MIT License
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3.9
|
||||
|
||||
[options]
|
||||
zip_safe = False
|
||||
include_package_data = True
|
||||
packages = smsbot
|
||||
install_requires =
|
||||
flask
|
||||
waitress
|
||||
twilio
|
||||
python-telegram-bot
|
||||
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
smsbot = smsbot:main
|
||||
|
||||
[flake8]
|
||||
format = wemake
|
||||
ignore = E501,D,WPS226,WPS110, WPS210,WPS231,WPS202
|
||||
max-line-length = 120
|
||||
exclude = setup.py
|
||||
|
||||
[darglint]
|
||||
docstring_style=sphinx
|
||||
@@ -1,168 +0,0 @@
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from functools import wraps
|
||||
|
||||
from flask import Flask, abort, current_app, request
|
||||
from telegram.ext import CommandHandler, Updater
|
||||
from twilio.request_validator import RequestValidator
|
||||
from waitress import serve
|
||||
import pkg_resources
|
||||
|
||||
pkg_version = pkg_resources.require("smsbot")[0].version
|
||||
|
||||
|
||||
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(pkg_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)
|
||||
|
||||
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):
|
||||
# Create an instance of the RequestValidator class
|
||||
twilio_token = os.environ.get('SMSBOT_TWILIO_AUTH_TOKEN')
|
||||
|
||||
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', ''))
|
||||
|
||||
# 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:
|
||||
return abort(403)
|
||||
return decorated_function
|
||||
|
||||
|
||||
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 set_bot(self, bot):
|
||||
self.bot = bot
|
||||
|
||||
def index(self):
|
||||
return 'Smsbot v{0} - {1}'.format(pkg_version, request.values.to_dict())
|
||||
|
||||
@validate_twilio_request
|
||||
def message(self):
|
||||
|
||||
message = "From: {From}\n\n{Body}".format(**request.values.to_dict())
|
||||
|
||||
current_app.logger.info('Received SMS from {From}: {Body}'.format(**request.values.to_dict()))
|
||||
self.bot.send_subscribers(message)
|
||||
|
||||
return '<response></response>'
|
||||
|
||||
@validate_twilio_request
|
||||
def call(self):
|
||||
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()))
|
||||
# Always reject calls
|
||||
return '<Response><Reject/></Response>'
|
||||
|
||||
def serve(self, host='0.0.0.0', port=80, debug=False):
|
||||
serve(self.app, host=host, port=port)
|
||||
|
||||
|
||||
def main():
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logging.info('smsbot v%s', pkg_version)
|
||||
|
||||
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
|
||||
telegram_bot = TelegramSmsBot(token)
|
||||
telegram_bot.start()
|
||||
|
||||
# Start webhooks
|
||||
webhooks = TwilioWebhookHandler()
|
||||
webhooks.set_bot(telegram_bot)
|
||||
webhooks.serve(host=listen_host, port=listen_port)
|
||||
@@ -1,4 +1,4 @@
|
||||
from . import main
|
||||
from smsbot.cli import main
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
43
smsbot/cli.py
Normal file
43
smsbot/cli.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
|
||||
from smsbot.telegram import TelegramSmsBot
|
||||
from smsbot.webhook_handler import TwilioWebhookHandler
|
||||
from smsbot.utils import get_smsbot_version
|
||||
|
||||
|
||||
def main():
|
||||
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', get_smsbot_version())
|
||||
logging.debug('Arguments: %s', args)
|
||||
|
||||
# Start bot
|
||||
telegram_bot = TelegramSmsBot(args.telegram_bot_token)
|
||||
|
||||
# Set the owner ID if configured
|
||||
if args.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
|
||||
if args.default_subscribers:
|
||||
for chat_id in args.default_subscribers.split(','):
|
||||
telegram_bot.add_subscriber(chat_id)
|
||||
|
||||
telegram_bot.start()
|
||||
|
||||
# Start webhooks
|
||||
webhooks = TwilioWebhookHandler()
|
||||
webhooks.set_bot(telegram_bot)
|
||||
webhooks.serve(host=args.listen_host, port=args.listen_port)
|
||||
97
smsbot/telegram.py
Normal file
97
smsbot/telegram.py
Normal file
@@ -0,0 +1,97 @@
|
||||
import logging
|
||||
|
||||
from prometheus_client import Counter, Summary
|
||||
from telegram.ext import CommandHandler, Updater
|
||||
|
||||
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):
|
||||
|
||||
def __init__(self, telegram_token, allow_subscribing=False, owner=None, subscribers=None):
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
self.bot_token = telegram_token
|
||||
self.subscriber_ids = subscribers or []
|
||||
self.set_owner(owner)
|
||||
|
||||
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))
|
||||
|
||||
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)
|
||||
|
||||
def start(self):
|
||||
self.logger.info('Starting bot...')
|
||||
self.updater.start_polling()
|
||||
self.bot = self.updater.bot
|
||||
self.logger.info('Bot Ready')
|
||||
|
||||
def stop(self):
|
||||
self.updater.stop()
|
||||
|
||||
@REQUEST_TIME.time()
|
||||
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)
|
||||
|
||||
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):
|
||||
self.logger.info('/subscribe command received')
|
||||
if update.message.chat['id'] not in self.subscriber_ids:
|
||||
self.logger.info('{0} subscribed'.format(update.message.chat['username']))
|
||||
self.subscriber_ids.append(update.message.chat['id'])
|
||||
self.send_owner('{0} has subscribed'.format(update.message.chat['username']))
|
||||
update.message.reply_markdown('You have been subscribed to SMS notifications')
|
||||
else:
|
||||
update.message.reply_markdown('You are already subscribed to SMS notifications')
|
||||
COMMAND_COUNT.inc()
|
||||
|
||||
@REQUEST_TIME.time()
|
||||
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'])
|
||||
self.send_owner('{0} has unsubscribed'.format(update.message.chat['username']))
|
||||
update.message.reply_markdown('You have been unsubscribed to SMS notifications')
|
||||
else:
|
||||
update.message.reply_markdown('You are not subscribed to SMS notifications')
|
||||
COMMAND_COUNT.inc()
|
||||
|
||||
def error_handler(self, update, context):
|
||||
"""Log Errors caused by Updates."""
|
||||
self.logger.warning('Update "%s" caused error "%s"', update, context.error)
|
||||
self.send_owner('Update "%{0}" caused error "{1}"'.format(update, context.error))
|
||||
|
||||
def send_message(self, message, chat_id):
|
||||
self.bot.sendMessage(text=message, chat_id=chat_id)
|
||||
|
||||
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
|
||||
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):
|
||||
self.subscriber_ids.append(chat_id)
|
||||
5
smsbot/utils.py
Normal file
5
smsbot/utils.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from importlib.metadata import version
|
||||
|
||||
|
||||
def get_smsbot_version():
|
||||
return version("smsbot")
|
||||
98
smsbot/webhook_handler.py
Normal file
98
smsbot/webhook_handler.py
Normal file
@@ -0,0 +1,98 @@
|
||||
|
||||
import os
|
||||
from functools import wraps
|
||||
|
||||
from flask import Flask, abort, current_app, request
|
||||
from prometheus_client import Counter, Summary, make_wsgi_app
|
||||
from twilio.request_validator import RequestValidator
|
||||
from waitress import serve
|
||||
from werkzeug.middleware.dispatcher import DispatcherMiddleware
|
||||
|
||||
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):
|
||||
"""Validates that incoming requests genuinely originated from Twilio"""
|
||||
@wraps(func)
|
||||
def decorated_function(*args, **kwargs): # noqa: WPS430
|
||||
# Create an instance of the RequestValidator class
|
||||
twilio_token = os.environ.get('SMSBOT_TWILIO_AUTH_TOKEN')
|
||||
|
||||
if not twilio_token:
|
||||
current_app.logger.warning('Twilio request validation skipped due to SMSBOT_TWILIO_AUTH_TOKEN missing')
|
||||
return func(*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', ''),
|
||||
)
|
||||
|
||||
# Continue processing the request if it's valid, return a 403 error if
|
||||
# it's not
|
||||
if request_valid or current_app.debug:
|
||||
return func(*args, **kwargs)
|
||||
return abort(403)
|
||||
return decorated_function
|
||||
|
||||
|
||||
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('/health', 'health', self.health, methods=['GET'])
|
||||
self.app.add_url_rule('/message', 'message', self.message, 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
|
||||
self.bot = bot
|
||||
|
||||
def index(self):
|
||||
return ''
|
||||
|
||||
@REQUEST_TIME.time()
|
||||
def health(self):
|
||||
return {
|
||||
'version': get_smsbot_version(),
|
||||
'owner': self.bot.owner_id,
|
||||
'subscribers': self.bot.subscriber_ids,
|
||||
}
|
||||
|
||||
@REQUEST_TIME.time()
|
||||
@validate_twilio_request
|
||||
def message(self):
|
||||
current_app.logger.info('Received SMS from {From}: {Body}'.format(**request.values.to_dict()))
|
||||
|
||||
message = 'From: {From}\n\n{Body}'.format(**request.values.to_dict())
|
||||
self.bot.send_subscribers(message)
|
||||
|
||||
# Return a blank response
|
||||
MESSAGE_COUNT.inc()
|
||||
return '<response></response>'
|
||||
|
||||
@REQUEST_TIME.time()
|
||||
@validate_twilio_request
|
||||
def call(self):
|
||||
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()))
|
||||
|
||||
# Always reject calls
|
||||
CALL_COUNT.inc()
|
||||
return '<Response><Reject/></Response>'
|
||||
|
||||
def serve(self, host='0.0.0.0', port=80, debug=False):
|
||||
serve(self.app, host=host, port=port)
|
||||
Reference in New Issue
Block a user