mirror of
https://github.com/nikdoof/smsbot.git
synced 2025-12-28 13:09:03 +00:00
Compare commits
10 Commits
1cea8f3182
...
2ae7704b59
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ae7704b59 | ||
|
facb9c4991
|
|||
|
fb5e1bffee
|
|||
|
34ba83ffb8
|
|||
|
23984da65a
|
|||
|
4206bb63f4
|
|||
|
f52d8ca81e
|
|||
|
1872a97088
|
|||
|
51d37a3e61
|
|||
|
8ff16ba9d3
|
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@@ -2,8 +2,6 @@ name: Release
|
|||||||
|
|
||||||
"on":
|
"on":
|
||||||
push:
|
push:
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
tags:
|
tags:
|
||||||
- "[0-9]+.[0-9]+.[0-9]+"
|
- "[0-9]+.[0-9]+.[0-9]+"
|
||||||
|
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -212,3 +212,6 @@ __marimo__/
|
|||||||
|
|
||||||
# Built Visual Studio Code Extensions
|
# Built Visual Studio Code Extensions
|
||||||
*.vsix
|
*.vsix
|
||||||
|
|
||||||
|
# smsbot config file
|
||||||
|
config.ini
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM ghcr.io/astral-sh/uv:python3.9-bookworm-slim AS builder
|
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim AS builder
|
||||||
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy
|
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy
|
||||||
ENV UV_PYTHON_DOWNLOADS=0
|
ENV UV_PYTHON_DOWNLOADS=0
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|||||||
14
config-example.ini
Normal file
14
config-example.ini
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[logging]
|
||||||
|
level = INFO
|
||||||
|
|
||||||
|
[webhook]
|
||||||
|
host = 127.0.0.1
|
||||||
|
port = 80
|
||||||
|
|
||||||
|
[telegram]
|
||||||
|
owner_id = OWNER_USER_ID
|
||||||
|
bot_token = BOT_TOKEN
|
||||||
|
|
||||||
|
[twilio]
|
||||||
|
account_sid = TWILIO_ACCOUNT_SID
|
||||||
|
auth_token = TWILIO_AUTH_TOKEN
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "smsbot"
|
name = "smsbot"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
description = "A simple Telegram bot to receive SMS messages."
|
description = "A simple Telegram bot to receive SMS messages."
|
||||||
authors = [{ name = "Andrew Williams", email = "andy@tensixtyone.com" }]
|
authors = [{ name = "Andrew Williams", email = "andy@tensixtyone.com" }]
|
||||||
license = { text = "MIT" }
|
license = { text = "MIT" }
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.13,<3.14"
|
requires-python = ">=3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"flask[async]>=3.1.1",
|
"flask[async]>=3.1.1",
|
||||||
"prometheus-async>=25.1.0",
|
"prometheus-async>=25.1.0",
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import argparse
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
from configparser import ConfigParser
|
||||||
from signal import SIGINT, SIGTERM
|
from signal import SIGINT, SIGTERM
|
||||||
|
|
||||||
import uvicorn
|
import uvicorn
|
||||||
@@ -9,59 +11,79 @@ from asgiref.wsgi import WsgiToAsgi
|
|||||||
|
|
||||||
from smsbot.telegram import TelegramSmsBot
|
from smsbot.telegram import TelegramSmsBot
|
||||||
from smsbot.utils import get_smsbot_version
|
from smsbot.utils import get_smsbot_version
|
||||||
from smsbot.webhook_handler import TwilioWebhookHandler
|
from smsbot.webhook import TwilioWebhookHandler
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser("smsbot")
|
parser = argparse.ArgumentParser("smsbot")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--listen-host", default=os.environ.get("SMSBOT_LISTEN_HOST") or "0.0.0.0"
|
"-c",
|
||||||
|
"--config",
|
||||||
|
default=os.environ.get("SMSBOT_CONFIG_FILE", "config.ini"),
|
||||||
|
type=argparse.FileType("r"),
|
||||||
|
help="Path to the config file",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument("--debug", action="store_true", help="Enable debug mode")
|
||||||
"--listen-port", default=os.environ.get("SMSBOT_LISTEN_PORT") or 80, type=int
|
|
||||||
)
|
|
||||||
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("--debug", action="store_true")
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if args.debug:
|
if args.debug:
|
||||||
level = logging.DEBUG
|
level = logging.DEBUG
|
||||||
else:
|
else:
|
||||||
level = logging.INFO
|
level = logging.INFO
|
||||||
logging.basicConfig(level=level)
|
logging.basicConfig(level=level, stream=sys.stdout)
|
||||||
logging.info("smsbot v%s", get_smsbot_version())
|
logging.info("smsbot v%s", get_smsbot_version())
|
||||||
logging.debug("Arguments: %s", args)
|
logging.debug("Arguments: %s", args)
|
||||||
|
|
||||||
|
# Load configuration ini file if provided
|
||||||
|
config = ConfigParser()
|
||||||
|
if args.config:
|
||||||
|
logging.info("Loading configuration from %s", args.config.name)
|
||||||
|
config.read_file(args.config)
|
||||||
|
|
||||||
|
# Override with environment variables, named SMSBOT_<SECTION>_<VALUE>
|
||||||
|
for key, value in os.environ.items():
|
||||||
|
if key.startswith("SMSBOT_"):
|
||||||
|
logging.debug("Overriding config %s with value %s", key, value)
|
||||||
|
section, option = key[7:].split("_", 1)
|
||||||
|
config[section][option] = value
|
||||||
|
|
||||||
|
# Validate configuration
|
||||||
|
if not config.has_section("telegram") or not config.get("telegram", "bot_token"):
|
||||||
|
logging.error("Telegram bot token is required")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Now the config is loaded, set the logger level
|
||||||
|
level = getattr(logging, config.get("logging", "level", fallback="INFO").upper(), logging.INFO)
|
||||||
|
logging.getLogger().setLevel(level)
|
||||||
|
|
||||||
# Start bot
|
# Start bot
|
||||||
telegram_bot = TelegramSmsBot(token=args.telegram_bot_token)
|
telegram_bot = TelegramSmsBot(token=config.get("telegram", "bot_token"))
|
||||||
|
|
||||||
# Set the owner ID if configured
|
# Set the owner ID if configured
|
||||||
if args.owner_id:
|
if config.has_option("telegram", "owner_id"):
|
||||||
telegram_bot.owners = [int(args.owner_id)]
|
telegram_bot.owners = [config.getint("telegram", "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 config.has_option("telegram", "default_subscribers"):
|
||||||
for chat_id in args.default_subscribers.split(","):
|
for chat_id in config.get("telegram", "default_subscribers").split(","):
|
||||||
telegram_bot.subscribers.append(int(chat_id.strip()))
|
telegram_bot.subscribers.append(int(chat_id.strip()))
|
||||||
|
|
||||||
webhooks = TwilioWebhookHandler()
|
# Init the webhook handler
|
||||||
|
webhooks = TwilioWebhookHandler(
|
||||||
|
account_sid=config.get("twilio", "account_sid", fallback=None),
|
||||||
|
auth_token=config.get("twilio", "auth_token", fallback=None),
|
||||||
|
)
|
||||||
webhooks.set_bot(telegram_bot)
|
webhooks.set_bot(telegram_bot)
|
||||||
|
|
||||||
# Build a uvicorn ASGI server
|
# Build a uvicorn ASGI server
|
||||||
flask_app = uvicorn.Server(
|
flask_app = uvicorn.Server(
|
||||||
config=uvicorn.Config(
|
config=uvicorn.Config(
|
||||||
app=WsgiToAsgi(webhooks.app),
|
app=WsgiToAsgi(webhooks.app),
|
||||||
port=args.listen_port,
|
port=config.getint("webhook", "port", fallback=5000),
|
||||||
use_colors=False,
|
use_colors=False,
|
||||||
host=args.listen_host,
|
host=config.get("webhook", "host", fallback="127.0.0.1"),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from smsbot.utils import get_smsbot_version
|
|
||||||
from prometheus_client import Counter, Summary
|
from prometheus_client import Counter, Summary
|
||||||
from telegram import Update
|
from telegram import Update
|
||||||
from telegram.ext import (
|
from telegram.ext import (
|
||||||
Application,
|
Application,
|
||||||
ApplicationHandlerStop,
|
ApplicationHandlerStop,
|
||||||
|
CommandHandler,
|
||||||
ContextTypes,
|
ContextTypes,
|
||||||
TypeHandler,
|
TypeHandler,
|
||||||
CommandHandler,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from smsbot.utils import get_smsbot_version
|
||||||
|
|
||||||
REQUEST_TIME = Summary(
|
REQUEST_TIME = Summary(
|
||||||
"telegram_request_processing_seconds", "Time spent processing request"
|
"telegram_request_processing_seconds", "Time spent processing request"
|
||||||
)
|
)
|
||||||
@@ -28,7 +29,7 @@ class TelegramSmsBot:
|
|||||||
|
|
||||||
def init_handlers(self):
|
def init_handlers(self):
|
||||||
self.app.add_handler(TypeHandler(Update, self.callback), -1)
|
self.app.add_handler(TypeHandler(Update, self.callback), -1)
|
||||||
self.app.add_handler(CommandHandler("help", self.handler_help))
|
self.app.add_handler(CommandHandler(["help", "start"], self.handler_help))
|
||||||
self.app.add_handler(CommandHandler("subscribe", self.handler_subscribe))
|
self.app.add_handler(CommandHandler("subscribe", self.handler_subscribe))
|
||||||
self.app.add_handler(CommandHandler("unsubscribe", self.handler_unsubscribe))
|
self.app.add_handler(CommandHandler("unsubscribe", self.handler_unsubscribe))
|
||||||
|
|
||||||
|
|||||||
@@ -5,19 +5,14 @@ def get_smsbot_version():
|
|||||||
return version("smsbot")
|
return version("smsbot")
|
||||||
|
|
||||||
|
|
||||||
class TwilioMessage:
|
class TwilioWebhookPayload:
|
||||||
"""
|
@staticmethod
|
||||||
Parses a Twilio webhook message.
|
def parse(data: dict):
|
||||||
"""
|
"""Return the correct class for the incoming Twilio webhook payload"""
|
||||||
|
if "SmsMessageSid" in data:
|
||||||
def __init__(self, data: dict) -> None:
|
return TwilioMessage(data)
|
||||||
self.from_number: str = data.get("From", "Unknown")
|
if "CallSid" in data:
|
||||||
self.to_number: str = data.get("To", "Unknown")
|
return TwilioCall(data)
|
||||||
self.body: str = data.get("Body", "")
|
|
||||||
|
|
||||||
self.media = []
|
|
||||||
for i in range(0, int(data.get("NumMedia", "0"))):
|
|
||||||
self.media.append(data.get(f"MediaUrl{i}"))
|
|
||||||
|
|
||||||
def _escape(self, text: str) -> str:
|
def _escape(self, text: str) -> str:
|
||||||
"""Escape text for MarkdownV2"""
|
"""Escape text for MarkdownV2"""
|
||||||
@@ -45,6 +40,19 @@ class TwilioMessage:
|
|||||||
text = text.replace(char, rf"\{char}")
|
text = text.replace(char, rf"\{char}")
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
class TwilioMessage(TwilioWebhookPayload):
|
||||||
|
"""Represents a Twilio SMS message"""
|
||||||
|
|
||||||
|
def __init__(self, data: dict) -> None:
|
||||||
|
self.from_number: str = data.get("From", "Unknown")
|
||||||
|
self.to_number: str = data.get("To", "Unknown")
|
||||||
|
self.body: str = data.get("Body", "")
|
||||||
|
|
||||||
|
self.media = []
|
||||||
|
for i in range(0, int(data.get("NumMedia", "0"))):
|
||||||
|
self.media.append(data.get(f"MediaUrl{i}"))
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"TwilioWebhookMessage(from={self.from_number}, to={self.to_number})"
|
return f"TwilioWebhookMessage(from={self.from_number}, to={self.to_number})"
|
||||||
|
|
||||||
@@ -54,6 +62,29 @@ class TwilioMessage:
|
|||||||
return msg
|
return msg
|
||||||
|
|
||||||
def to_markdownv2(self):
|
def to_markdownv2(self):
|
||||||
media_str = "\n".join([f"{self._escape(url)}" for url in self.media]) if self.media else ""
|
media_str = (
|
||||||
|
"\n".join([f"{self._escape(url)}" for url in self.media])
|
||||||
|
if self.media
|
||||||
|
else ""
|
||||||
|
)
|
||||||
msg = f"**From**: {self._escape(self.from_number)}\n**To**: {self._escape(self.to_number)}\n\n{self._escape(self.body)}\n\n{media_str}"
|
msg = f"**From**: {self._escape(self.from_number)}\n**To**: {self._escape(self.to_number)}\n\n{self._escape(self.body)}\n\n{media_str}"
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
class TwilioCall(TwilioWebhookPayload):
|
||||||
|
"""Represents a Twilio voice call"""
|
||||||
|
|
||||||
|
def __init__(self, data: dict) -> None:
|
||||||
|
self.from_number: str = data.get("From", "Unknown")
|
||||||
|
self.to_number: str = data.get("To", "Unknown")
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"TwilioCall(from={self.from_number}, to={self.to_number})"
|
||||||
|
|
||||||
|
def to_str(self) -> str:
|
||||||
|
msg = f"Call from {self.from_number}, rejected."
|
||||||
|
return msg
|
||||||
|
|
||||||
|
def to_markdownv2(self):
|
||||||
|
msg = f"Call from {self._escape(self.from_number)}, rejected\\."
|
||||||
|
return msg
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
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
|
||||||
@@ -7,7 +6,7 @@ from prometheus_client import Counter, Summary, make_wsgi_app
|
|||||||
from twilio.request_validator import RequestValidator
|
from twilio.request_validator import RequestValidator
|
||||||
from werkzeug.middleware.dispatcher import DispatcherMiddleware
|
from werkzeug.middleware.dispatcher import DispatcherMiddleware
|
||||||
|
|
||||||
from smsbot.utils import TwilioMessage, get_smsbot_version
|
from smsbot.utils import TwilioWebhookPayload, get_smsbot_version
|
||||||
|
|
||||||
REQUEST_TIME = Summary(
|
REQUEST_TIME = Summary(
|
||||||
"webhook_request_processing_seconds", "Time spent processing request"
|
"webhook_request_processing_seconds", "Time spent processing request"
|
||||||
@@ -16,47 +15,22 @@ MESSAGE_COUNT = Counter("webhook_message_count", "Total number of messages proce
|
|||||||
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):
|
|
||||||
"""Validates that incoming requests genuinely originated from Twilio"""
|
|
||||||
|
|
||||||
@wraps(func)
|
|
||||||
async 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 await 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):
|
class TwilioWebhookHandler(object):
|
||||||
def __init__(self):
|
def __init__(self, account_sid: str | None = None, auth_token: str | None = None):
|
||||||
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"])
|
||||||
|
|
||||||
|
self.account_sid = account_sid
|
||||||
|
self.auth_token = auth_token
|
||||||
|
|
||||||
|
# Wrap validation around hook endpoints
|
||||||
|
self.message = self.validate_twilio_request(self.message)
|
||||||
|
self.call = self.validate_twilio_request(self.call)
|
||||||
|
|
||||||
# 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 = DispatcherMiddleware(
|
||||||
self.app.wsgi_app,
|
self.app.wsgi_app,
|
||||||
@@ -65,6 +39,35 @@ class TwilioWebhookHandler(object):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def validate_twilio_request(self, func):
|
||||||
|
"""Validates that incoming requests genuinely originated from Twilio"""
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
async def decorated_function(*args, **kwargs):
|
||||||
|
# Create an instance of the RequestValidator class
|
||||||
|
if not self.auth_token:
|
||||||
|
current_app.logger.warning(
|
||||||
|
"Twilio request validation skipped due to Twilio Auth Token missing"
|
||||||
|
)
|
||||||
|
return await func(*args, **kwargs)
|
||||||
|
validator = RequestValidator(self.auth_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 await func(*args, **kwargs)
|
||||||
|
return abort(403)
|
||||||
|
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
def set_bot(self, bot):
|
def set_bot(self, bot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
||||||
@@ -80,7 +83,6 @@ class TwilioWebhookHandler(object):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@time(REQUEST_TIME)
|
@time(REQUEST_TIME)
|
||||||
@validate_twilio_request
|
|
||||||
async def message(self):
|
async def message(self):
|
||||||
"""Handle incoming SMS messages from Twilio"""
|
"""Handle incoming SMS messages from Twilio"""
|
||||||
current_app.logger.info(
|
current_app.logger.info(
|
||||||
@@ -88,7 +90,7 @@ class TwilioWebhookHandler(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
await self.bot.send_subscribers(
|
await self.bot.send_subscribers(
|
||||||
TwilioMessage(request.values.to_dict()).to_markdownv2()
|
TwilioWebhookPayload.parse(request.values.to_dict()).to_markdownv2()
|
||||||
)
|
)
|
||||||
|
|
||||||
# Return a blank response
|
# Return a blank response
|
||||||
@@ -96,14 +98,13 @@ class TwilioWebhookHandler(object):
|
|||||||
return '<?xml version="1.0" encoding="UTF-8"?><Response></Response>'
|
return '<?xml version="1.0" encoding="UTF-8"?><Response></Response>'
|
||||||
|
|
||||||
@time(REQUEST_TIME)
|
@time(REQUEST_TIME)
|
||||||
@validate_twilio_request
|
|
||||||
async def call(self):
|
async def call(self):
|
||||||
"""Handle incoming calls from Twilio"""
|
"""Handle incoming calls from Twilio"""
|
||||||
current_app.logger.info(
|
current_app.logger.info(
|
||||||
"Received Call from {From}".format(**request.values.to_dict())
|
"Received Call from {From}".format(**request.values.to_dict())
|
||||||
)
|
)
|
||||||
await self.bot.send_subscribers(
|
await self.bot.send_subscribers(
|
||||||
"Received Call from {From}, rejecting.".format(**request.values.to_dict())
|
TwilioWebhookPayload.parse(request.values.to_dict()).to_markdownv2()
|
||||||
)
|
)
|
||||||
|
|
||||||
# Always reject calls
|
# Always reject calls
|
||||||
37
uv.lock
generated
37
uv.lock
generated
@@ -1,6 +1,6 @@
|
|||||||
version = 1
|
version = 1
|
||||||
revision = 3
|
revision = 2
|
||||||
requires-python = "==3.13.*"
|
requires-python = ">=3.13"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aiohappyeyeballs"
|
name = "aiohappyeyeballs"
|
||||||
@@ -135,6 +135,17 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" },
|
{ url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" },
|
{ url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" },
|
{ url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" },
|
{ url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -581,7 +592,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smsbot"
|
name = "smsbot"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "flask", extra = ["async"] },
|
{ name = "flask", extra = ["async"] },
|
||||||
@@ -696,6 +707,26 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" },
|
{ url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" },
|
{ url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" },
|
{ url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" },
|
{ url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user