56 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
f9d1f9cc27 Initial plan 2025-07-17 17:12:38 +00:00
renovate[bot]
ecf4483937 chore(deps): lock file maintenance (#58)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-14 06:25:48 +00:00
renovate[bot]
d9e52d13a5 chore(deps): update dependency ruff to v0.12.3 (#57)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-11 22:58:52 +00:00
renovate[bot]
3f3e232fdb chore(deps): lock file maintenance (#56)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 06:41:27 +00:00
renovate[bot]
771bbfec91 chore(deps): update dependency ruff to v0.12.2 (#55)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-03 23:52:26 +00:00
renovate[bot]
afc5194cfb chore(deps): lock file maintenance (#54)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 08:09:00 +00:00
renovate[bot]
6696976012 chore(deps): update dependency ruff to v0.12.1 (#53)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-27 02:52:23 +00:00
renovate[bot]
716d4355fc chore(deps): lock file maintenance (#52)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-23 05:31:19 +00:00
renovate[bot]
ff2424de8c chore(deps): update dependency ruff to ^0.12.0 (#51)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-17 22:32:48 +00:00
renovate[bot]
e8dc6b6bbc chore(deps): lock file maintenance (#50)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-16 04:45:28 +00:00
renovate[bot]
ec77ebbdb5 chore(deps): update dependency ruff to v0.11.13 (#46)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-11 12:50:54 +00:00
e02a3169a4 Add common renovate config 2025-06-11 06:43:37 +01:00
ade5e32387 Merge pull request #45 from nikdoof/renovate/ruff-0.x-lockfile
chore(deps): update dependency ruff to v0.11.8
2025-05-04 13:52:24 +01:00
renovate[bot]
3f0753b1b7 chore(deps): update dependency ruff to v0.11.8 2025-05-04 12:51:30 +00:00
eb8bce763d Merge pull request #43 from nikdoof/renovate/ruff-0.x
chore(deps): update dependency ruff to ^0.11.0
2025-05-04 13:50:34 +01:00
fc2aa86b89 fix(build): Install the environment as part of linting 2025-05-04 13:49:35 +01:00
51eff4a4fe fix(build): Fix linting on GitHub Actions 2025-05-04 13:37:55 +01:00
f49037ab09 chore(docs): Add note about updating loglevel 2025-05-04 12:59:39 +01:00
5984bdd76f feat(build): Switch to Taskfile 2025-05-04 12:59:25 +01:00
414e50e559 fix(docker): Fix default expose port 2025-05-04 12:59:09 +01:00
6d4d86447c chore(lock): Update lock file 2025-05-04 12:47:57 +01:00
5cacd8e23a chore(lint): Linting cleanup 2025-05-04 12:47:57 +01:00
renovate[bot]
81232914cd Update dependency ruff to ^0.11.0 2025-03-14 15:57:34 +00:00
5980bc67dd Merge pull request #42 from nikdoof/renovate/pytest-8.x-lockfile
Update dependency pytest to v8.3.5
2025-03-13 14:32:44 +00:00
bf0c3e51a7 Merge pull request #41 from nikdoof/renovate/ruff-0.x-lockfile
Update dependency ruff to v0.9.10
2025-03-13 14:32:34 +00:00
5dae3d007d Merge pull request #39 from nikdoof/renovate/helm-chart-releaser-action-1.x
Update helm/chart-releaser-action action to v1.7.0
2025-03-13 14:32:15 +00:00
renovate[bot]
efe96bf152 Update dependency ruff to v0.9.10 2025-03-07 19:07:06 +00:00
renovate[bot]
d575bfe8d2 Update dependency pytest to v8.3.5 2025-03-02 13:45:42 +00:00
4a2dd76401 Fix release workflow 2025-02-27 22:11:48 +00:00
899b88e907 Version 0.0.6 2025-02-27 22:08:17 +00:00
000f0932d8 Fix docker container 2025-02-27 22:07:32 +00:00
3d28716817 Bump chart app version 2025-02-27 17:35:14 +00:00
74dac7603e Fix lint and release 2025-02-27 17:32:33 +00:00
076e2464a0 Fix docker build 2025-02-27 17:27:03 +00:00
85c72462c4 Migrate to Poetry 2025-02-27 15:31:58 +00:00
renovate[bot]
6651827018 Update helm/chart-releaser-action action to v1.7.0 2025-01-20 14:14:04 +00:00
41971aabdf Merge pull request #29 from nikdoof/renovate/python-3.x
Update python Docker tag to v3.12
2024-07-26 10:54:24 +01:00
befa3df48d Merge pull request #35 from nikdoof/renovate/softprops-action-gh-release-2.x
Update softprops/action-gh-release action to v2
2024-07-26 10:54:09 +01:00
d504dfb95d Merge pull request #34 from nikdoof/renovate/docker-build-push-action-6.x
Update docker/build-push-action action to v6
2024-07-26 10:53:57 +01:00
e51541c79a Merge pull request #30 from nikdoof/renovate/helm-chart-releaser-action-1.x
Update helm/chart-releaser-action action to v1.6.0
2024-07-26 10:53:36 +01:00
cd3699f185 Merge pull request #32 from nikdoof/renovate/azure-setup-helm-4.x
Update azure/setup-helm action to v4
2024-07-26 10:53:25 +01:00
7d7aa97c24 Merge pull request #31 from nikdoof/renovate/actions-setup-python-5.x
Update actions/setup-python action to v5
2024-07-26 10:53:13 +01:00
renovate[bot]
d51c6fbe73 Update softprops/action-gh-release action to v2 2024-07-26 09:53:03 +00:00
21276e1c24 Merge pull request #26 from nikdoof/renovate/docker-login-action-3.x
Update docker/login-action action to v3
2024-07-26 10:53:01 +01:00
renovate[bot]
4e15d4a8ed Update docker/build-push-action action to v6 2024-07-26 09:52:58 +00:00
b1bdcb456a Merge pull request #28 from nikdoof/renovate/docker-setup-qemu-action-3.x
Update docker/setup-qemu-action action to v3
2024-07-26 10:52:51 +01:00
f7a151235a Merge pull request #27 from nikdoof/renovate/docker-setup-buildx-action-3.x
Update docker/setup-buildx-action action to v3
2024-07-26 10:52:41 +01:00
1d8b177cb4 Merge pull request #24 from nikdoof/renovate/actions-checkout-4.x
Update actions/checkout action to v4
2024-07-26 10:52:30 +01:00
renovate[bot]
3389144484 Update azure/setup-helm action to v4 2024-03-06 23:02:35 +00:00
renovate[bot]
aa3ca58b58 Update actions/setup-python action to v5 2023-12-06 12:16:22 +00:00
renovate[bot]
2854626c27 Update helm/chart-releaser-action action to v1.6.0 2023-11-02 19:10:52 +00:00
renovate[bot]
dc0745ce76 Update actions/checkout action to v4 2023-10-19 11:25:47 +00:00
renovate[bot]
19f8183ddc Update python Docker tag to v3.12 2023-10-03 04:28:27 +00:00
renovate[bot]
f51928cb4b Update docker/setup-qemu-action action to v3 2023-09-12 14:01:27 +00:00
renovate[bot]
80be469a90 Update docker/setup-buildx-action action to v3 2023-09-12 14:01:22 +00:00
renovate[bot]
076216e089 Update docker/login-action action to v3 2023-09-12 10:13:06 +00:00
20 changed files with 1778 additions and 152 deletions

View File

@@ -1,5 +1,6 @@
{ {
"extends": [ "extends": [
"config:base" "config:base",
"github>nikdoof/renovate-config:python"
] ]
} }

View File

@@ -12,11 +12,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- name: Login to GHCR - name: Login to GHCR
uses: docker/login-action@v2 uses: docker/login-action@v3
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
with: with:
registry: ghcr.io registry: ghcr.io
@@ -24,7 +24,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push - name: Build and push
id: docker_build id: docker_build
uses: docker/build-push-action@v4 uses: docker/build-push-action@v6
with: with:
push: true push: true
tags: | tags: |

View File

@@ -1,6 +1,6 @@
name: Lint name: Lint
'on': "on":
push: push:
branches: branches:
- main - main
@@ -9,13 +9,26 @@ name: Lint
- main - main
jobs: jobs:
run-linters: lint:
name: Run linters
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9"]
steps: steps:
- name: Check out Git repository - uses: actions/checkout@v4
uses: actions/checkout@v3 - name: Install Task
uses: arduino/setup-task@v2
- name: wemake-python-styleguide with:
uses: wemake-services/wemake-python-styleguide@0.17.0 version: 3.x
- 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: |
task python:lint

View File

@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -21,12 +21,12 @@ jobs:
git config user.name "$GITHUB_ACTOR" git config user.name "$GITHUB_ACTOR"
git config user.email "$GITHUB_ACTOR@users.noreply.github.com" git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
- name: Install Helm - name: Install Helm
uses: azure/setup-helm@v3 uses: azure/setup-helm@v4
with: with:
version: v3.6.3 version: v3.6.3
- name: Run chart-releaser - name: Run chart-releaser
uses: helm/chart-releaser-action@v1.5.0 uses: helm/chart-releaser-action@v1.7.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

@@ -1,4 +1,3 @@
---
name: Release name: Release
on: on:
push: push:
@@ -10,20 +9,25 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- uses: actions/setup-python@v4 - name: Setup Python
uses: actions/setup-python@v5
with: with:
python-version: '3.10' python-version: "3.9"
- run: pip install -r requirements-dev.txt
- name: Build Assets - name: Install Poetry
run: python setup.py sdist bdist_wheel uses: snok/install-poetry@v1
with:
virtualenvs-create: true
virtualenvs-in-project: true
- name: Build Release
run: poetry build
- name: Release - name: Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v2
with: with:
name: "Version ${{ github.ref_name }}" name: "Version ${{ github.ref_name }}"
generate_release_notes: true
files: | files: |
dist/* dist/*

View File

@@ -1,5 +1,21 @@
FROM python:3.11-alpine FROM python:3.9-alpine AS base
WORKDIR /app
COPY . /app # Builder
RUN pip install . FROM base AS builder
ENTRYPOINT smsbot
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 80/tcp
CMD ["smsbot"]

17
Taskfile.yaml Normal file
View File

@@ -0,0 +1,17 @@
version: 3
tasks:
python:env:
cmds:
- poetry install --with=dev,github
python:lint:
desc: Lint Python files
deps:
- python:env
cmds:
- poetry run ruff check --output-format=github --select=E9,F63,F7,F82 --target-version=py39 .
- poetry run ruff check --output-format=github --target-version=py39 .
docker:build:
desc: Build the container using Docker
cmds:
- docker build . -t smsbot:latest

View File

@@ -1,8 +1,8 @@
apiVersion: v2 apiVersion: v2
appVersion: 0.0.4 appVersion: 0.0.5
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.5 version: 0.0.6
kubeVersion: ">=1.19.0-0" kubeVersion: ">=1.19.0-0"
keywords: keywords:
- smsbot - smsbot

View File

@@ -11,7 +11,7 @@ image:
# -- image pull policy # -- image pull policy
pullPolicy: IfNotPresent pullPolicy: IfNotPresent
# -- image tag # -- image tag
tag: 0.0.4 tag: 0.0.5
# -- environment variables. # -- environment variables.
# @default -- See below # @default -- See below

1544
poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

38
pyproject.toml Normal file
View 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.12.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"

View File

@@ -1 +0,0 @@
wheel

View File

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

View File

@@ -1,36 +0,0 @@
[metadata]
name = smsbot
version = 0.0.4
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.11
[options]
zip_safe = False
include_package_data = True
packages = smsbot
install_requires =
flask
waitress
twilio
python-telegram-bot<=14
prometheus_client
[options.entry_points]
console_scripts =
smsbot = smsbot.cli:main
[flake8]
format = wemake
ignore = D,E501,WPS432,WPS323,WPS2,S104,WPS412,WPS100
max-line-length = 120
exclude = setup.py,env,build
[darglint]
docstring_style=sphinx

View File

@@ -1,3 +0,0 @@
from setuptools import setup
setup()

View File

@@ -1,4 +1,4 @@
from smsbot.cli import main from smsbot.cli import main
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@@ -2,27 +2,33 @@ import argparse
import logging import logging
import os import os
import pkg_resources
from smsbot.telegram import TelegramSmsBot from smsbot.telegram import TelegramSmsBot
from smsbot.utils import get_smsbot_version
from smsbot.webhook_handler import TwilioWebhookHandler from smsbot.webhook_handler import TwilioWebhookHandler
pkg_version = pkg_resources.require('smsbot')[0].version
def main(): def main():
parser = argparse.ArgumentParser('smsbot') parser = argparse.ArgumentParser("smsbot")
parser.add_argument('--listen-host', default=os.environ.get('SMSBOT_LISTEN_HOST') or '0.0.0.0') parser.add_argument(
parser.add_argument('--listen-port', default=os.environ.get('SMSBOT_LISTEN_PORT') or '80') "--listen-host", default=os.environ.get("SMSBOT_LISTEN_HOST") or "0.0.0.0"
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(
parser.add_argument('--default-subscribers', default=os.environ.get('SMSBOT_DEFAULT_SUBSCRIBERS')) "--listen-port", default=os.environ.get("SMSBOT_LISTEN_PORT") or "80"
parser.add_argument('--log-level', default='INFO') )
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() args = parser.parse_args()
# TODO: Replace for Py >=3.11
logging.basicConfig(level=logging.getLevelName(args.log_level)) logging.basicConfig(level=logging.getLevelName(args.log_level))
logging.info('smsbot v%s', pkg_version) logging.info("smsbot v%s", get_smsbot_version())
logging.debug('Arguments: %s', args) logging.debug("Arguments: %s", args)
# Start bot # Start bot
telegram_bot = TelegramSmsBot(args.telegram_bot_token) telegram_bot = TelegramSmsBot(args.telegram_bot_token)
@@ -31,11 +37,11 @@ def main():
if args.owner_id: if args.owner_id:
telegram_bot.set_owner(args.owner_id) telegram_bot.set_owner(args.owner_id)
else: else:
logging.warning('No Owner ID is set, which is not a good idea...') logging.warning("No Owner ID is set, which is not a good idea...")
# Add default subscribers # Add default subscribers
if args.default_subscribers: if args.default_subscribers:
for chat_id in args.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()

View File

@@ -5,33 +5,40 @@ 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') REQUEST_TIME = Summary(
COMMAND_COUNT = Counter('telegram_command_count', 'Total number of commands processed') "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__(
def __init__(self, telegram_token, allow_subscribing=False, owner=None, subscribers=None): 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 = telegram_token self.bot_token = telegram_token
self.subscriber_ids = subscribers or [] self.subscriber_ids = subscribers or []
self.set_owner(owner) self.set_owner(owner)
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))
if allow_subscribing: if allow_subscribing:
self.updater.dispatcher.add_handler(CommandHandler('subscribe', self.subscribe_handler)) self.updater.dispatcher.add_handler(
self.updater.dispatcher.add_handler(CommandHandler('unsubscribe', self.unsubscribe_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): def start(self):
self.logger.info('Starting bot...') 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")
def stop(self): def stop(self):
self.updater.stop() self.updater.stop()
@@ -39,43 +46,57 @@ class TelegramSmsBot(object):
@REQUEST_TIME.time() @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)
commands = [] commands = []
for command in self.updater.dispatcher.handlers[0]: for command in self.updater.dispatcher.handlers[0]:
commands.extend(['/{0}'.format(cmd) for cmd in command.command]) 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))) update.message.reply_markdown(
"Smsbot v{0}\n\n{1}".format(get_smsbot_version(), "\n".join(commands))
)
COMMAND_COUNT.inc() COMMAND_COUNT.inc()
@REQUEST_TIME.time() @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:
self.logger.info('{0} subscribed'.format(update.message.chat['username'])) self.logger.info("{0} subscribed".format(update.message.chat["username"]))
self.subscriber_ids.append(update.message.chat['id']) self.subscriber_ids.append(update.message.chat["id"])
self.send_owner('{0} has subscribed'.format(update.message.chat['username'])) self.send_owner(
update.message.reply_markdown('You have been subscribed to SMS notifications') "{0} has subscribed".format(update.message.chat["username"])
)
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() COMMAND_COUNT.inc()
@REQUEST_TIME.time() @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:
self.logger.info('{0} unsubscribed'.format(update.message.chat['username'])) self.logger.info("{0} unsubscribed".format(update.message.chat["username"]))
self.subscriber_ids.remove(update.message.chat['id']) self.subscriber_ids.remove(update.message.chat["id"])
self.send_owner('{0} has unsubscribed'.format(update.message.chat['username'])) self.send_owner(
update.message.reply_markdown('You have been unsubscribed to SMS notifications') "{0} has unsubscribed".format(update.message.chat["username"])
)
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() 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."""
self.logger.warning('Update "%s" caused error "%s"', update, context.error) self.logger.warning('Update "%s" caused error "%s"', update, context.error)
self.send_owner('Update "%{0}" caused error "{1}"'.format(update, context.error)) self.send_owner(
'Update "%{0}" caused error "{1}"'.format(update, context.error)
)
def send_message(self, message, chat_id): def send_message(self, message, chat_id):
self.bot.sendMessage(text=message, chat_id=chat_id) self.bot.sendMessage(text=message, chat_id=chat_id)

View File

@@ -1,5 +1,5 @@
import pkg_resources from importlib.metadata import version
def get_smsbot_version(): def get_smsbot_version():
return pkg_resources.require('smsbot')[0].version return version("smsbot")

View File

@@ -1,4 +1,3 @@
import os import os
from functools import wraps from functools import wraps
@@ -10,20 +9,23 @@ 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') REQUEST_TIME = Summary("webhook_request_processing_seconds", "Time spent processing request")
MESSAGE_COUNT = Counter('webhook_message_count', 'Total number of messages processed') MESSAGE_COUNT = Counter("webhook_message_count", "Total number of messages processed")
CALL_COUNT = Counter('webhook_call_count', 'Total number of calls 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"""
@wraps(func) @wraps(func)
def decorated_function(*args, **kwargs): # noqa: WPS430 def decorated_function(*args, **kwargs): # noqa: WPS430
# Create an instance of the RequestValidator class # Create an instance of the RequestValidator class
twilio_token = os.environ.get('SMSBOT_TWILIO_AUTH_TOKEN') twilio_token = os.environ.get("SMSBOT_TWILIO_AUTH_TOKEN")
if not twilio_token: if not twilio_token:
current_app.logger.warning('Twilio request validation skipped due to SMSBOT_TWILIO_AUTH_TOKEN missing') current_app.logger.warning(
"Twilio request validation skipped due to SMSBOT_TWILIO_AUTH_TOKEN missing"
)
return func(*args, **kwargs) return func(*args, **kwargs)
validator = RequestValidator(twilio_token) validator = RequestValidator(twilio_token)
@@ -33,7 +35,7 @@ def validate_twilio_request(func):
request_valid = validator.validate( request_valid = validator.validate(
request.url, request.url,
request.form, request.form,
request.headers.get('X-TWILIO-SIGNATURE', ''), request.headers.get("X-TWILIO-SIGNATURE", ""),
) )
# Continue processing the request if it's valid, return a 403 error if # Continue processing the request if it's valid, return a 403 error if
@@ -41,58 +43,67 @@ def validate_twilio_request(func):
if request_valid or current_app.debug: if request_valid or current_app.debug:
return func(*args, **kwargs) return func(*args, **kwargs)
return abort(403) return abort(403)
return decorated_function return decorated_function
class TwilioWebhookHandler(object): class TwilioWebhookHandler(object):
def __init__(self): def __init__(self):
self.app = Flask(self.__class__.__name__) self.app = Flask(self.__class__.__name__)
self.app.add_url_rule('/', 'index', self.index, methods=['GET']) 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("/health", "health", self.health, methods=["GET"])
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 # Add prometheus wsgi middleware to route /metrics requests
self.app.wsgi_app = DispatcherMiddleware(self.app.wsgi_app, { self.app.wsgi_app = DispatcherMiddleware(
'/metrics': make_wsgi_app(), 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() @REQUEST_TIME.time()
def health(self): def health(self):
return { return {
'version': get_smsbot_version(), "version": get_smsbot_version(),
'owner': self.bot.owner_id, "owner": self.bot.owner_id,
'subscribers': self.bot.subscriber_ids, "subscribers": self.bot.subscriber_ids,
} }
@REQUEST_TIME.time() @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())
)
message = 'From: {From}\n\n{Body}'.format(**request.values.to_dict()) message = "From: {From}\n\n{Body}".format(**request.values.to_dict())
self.bot.send_subscribers(message) self.bot.send_subscribers(message)
# Return a blank response # Return a blank response
MESSAGE_COUNT.inc() MESSAGE_COUNT.inc()
return '<response></response>' return "<response></response>"
@REQUEST_TIME.time() @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(
self.bot.send_subscribers('Received Call from {From}, rejecting.'.format(**request.values.to_dict())) "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 # Always reject calls
CALL_COUNT.inc() 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):
serve(self.app, host=host, port=port) serve(self.app, host=host, port=port)