Support sending SMS

This commit is contained in:
2025-08-17 11:59:11 +01:00
parent b4e833f440
commit 594f4ba8ef
5 changed files with 71 additions and 18 deletions

View File

@@ -5,6 +5,7 @@ import os
import sys import sys
from configparser import ConfigParser from configparser import ConfigParser
from signal import SIGINT, SIGTERM from signal import SIGINT, SIGTERM
from twilio.rest import Client
import uvicorn import uvicorn
from asgiref.wsgi import WsgiToAsgi from asgiref.wsgi import WsgiToAsgi
@@ -50,15 +51,37 @@ def main():
# Validate configuration # Validate configuration
if not config.has_section("telegram") or not config.get("telegram", "bot_token"): if not config.has_section("telegram") or not config.get("telegram", "bot_token"):
logging.error("Telegram bot token is required, define a token either in the config file or as an environment variable.") logging.error(
"Telegram bot token is required, define a token either in the config file or as an environment variable."
)
return
if config.has_section("twilio") and not (config.get("twilio", "account_sid") and config.get("twilio", "auth_token") and config.get("twilio", "from_number")):
logging.error(
"Twilio account SID, auth token, and from number are required for outbound SMS functionality, define them in the config file or as environment variables."
)
return return
# Now the config is loaded, set the logger level # Now the config is loaded, set the logger level
level = getattr(logging, config.get("logging", "level", fallback="INFO").upper(), logging.INFO) level = getattr(logging, config.get("logging", "level", fallback="INFO").upper(), logging.INFO)
logging.getLogger().setLevel(level) logging.getLogger().setLevel(level)
# Configure Twilio client if we have credentials
if config.has_section("twilio") and config.get("twilio", "account_sid") and config.get("twilio", "auth_token"):
twilio_client = Client(
config.get("twilio", "account_sid"),
config.get("twilio", "auth_token"),
)
else:
twilio_client = None
logging.warning("No Twilio credentials found, outbound SMS functionality will be disabled.")
# Start bot # Start bot
telegram_bot = TelegramSmsBot(token=config.get("telegram", "bot_token")) telegram_bot = TelegramSmsBot(
token=config.get("telegram", "bot_token"),
twilio_client=twilio_client,
twilio_from_number=config.get("twilio", "from_number", fallback=None),
)
# Set the owner ID if configured # Set the owner ID if configured
if config.has_option("telegram", "owner_id"): if config.has_option("telegram", "owner_id"):
@@ -76,7 +99,7 @@ def main():
account_sid=config.get("twilio", "account_sid", fallback=None), account_sid=config.get("twilio", "account_sid", fallback=None),
auth_token=config.get("twilio", "auth_token", fallback=None), auth_token=config.get("twilio", "auth_token", fallback=None),
) )
webhooks.set_bot(telegram_bot) webhooks.set_telegram_application(telegram_bot)
# Build a uvicorn ASGI server # Build a uvicorn ASGI server
flask_app = uvicorn.Server( flask_app = uvicorn.Server(

View File

@@ -9,6 +9,7 @@ from telegram.ext import (
ContextTypes, ContextTypes,
TypeHandler, TypeHandler,
) )
from twilio.rest import Client
from smsbot.utils import get_smsbot_version from smsbot.utils import get_smsbot_version
@@ -17,11 +18,20 @@ COMMAND_COUNT = Counter("telegram_command_count", "Total number of commands proc
class TelegramSmsBot: class TelegramSmsBot:
def __init__(self, token: str, owners: list[int] = [], subscribers: list[int] = []): def __init__(
self,
token: str,
twilio_client: Client | None = None,
twilio_from_number: str | None = None,
owners: list[int] = [],
subscribers: list[int] = [],
):
self.logger = logging.getLogger(self.__class__.__name__) self.logger = logging.getLogger(self.__class__.__name__)
self.app = Application.builder().token(token).build() self.app = Application.builder().token(token).build()
self.owners = owners self.owners = owners
self.subscribers = subscribers self.subscribers = subscribers
self.twilio_client = twilio_client
self.twilio_from_number = twilio_from_number
self.init_handlers() self.init_handlers()
@@ -30,6 +40,7 @@ class TelegramSmsBot:
self.app.add_handler(CommandHandler(["help", "start"], 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))
self.app.add_handler(CommandHandler("sms", self.handler_sms))
async def callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE): async def callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Handle the update""" """Handle the update"""
@@ -97,3 +108,22 @@ class TelegramSmsBot:
await update.message.reply_markdown("You have successfully unsubscribed from updates.") await update.message.reply_markdown("You have successfully unsubscribed from updates.")
else: else:
self.logger.info(f"User {user_id} is not subscribed.") self.logger.info(f"User {user_id} is not subscribed.")
@REQUEST_TIME.time()
async def handler_sms(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Handle sending SMS requests"""
if update.effective_user and update.message:
user_id = update.effective_user.id
if self.twilio_client and self.twilio_from_number:
to = context.args[0] if context.args else "No recipient provided"
message = context.args[1] if context.args else "No message provided"
self.logger.info(f"Sending SMS from user {user_id} -> {to}: {message}")
try:
self.twilio_client.messages.create(body=message, to=to, from_=self.twilio_from_number)
except Exception as e:
self.logger.exception("Failed to send SMS due to exception")
await update.message.reply_markdown("Failed to send SMS")
pass
else:
await update.message.reply_markdown("Twilio client is not configured, cannot send SMS.")

5
smsbot/utils/__init__.py Normal file
View File

@@ -0,0 +1,5 @@
from importlib.metadata import version
def get_smsbot_version() -> str:
return version("smsbot")

View File

@@ -1,10 +1,3 @@
from importlib.metadata import version
def get_smsbot_version() -> str:
return version("smsbot")
class TwilioWebhookPayload: class TwilioWebhookPayload:
@staticmethod @staticmethod
def parse(data: dict[str, str]) -> "TwilioCall | TwilioMessage | None": def parse(data: dict[str, str]) -> "TwilioCall | TwilioMessage | None":

View File

@@ -6,7 +6,8 @@ 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 TwilioWebhookPayload, get_smsbot_version from smsbot.utils import get_smsbot_version
from smsbot.utils.twilio import TwilioWebhookPayload
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")
@@ -68,8 +69,9 @@ class TwilioWebhookHandler(object):
return decorated_function return decorated_function
def set_bot(self, bot): def set_telegram_application(self, app):
self.bot = bot """Set the Telegram application instance to use for any webhook calls"""
self.telegram_app = app
async def index(self) -> str: async def index(self) -> str:
return f'smsbot v{get_smsbot_version()} - <a href="https://github.com/nikdoof/smsbot">GitHub</a>' return f'smsbot v{get_smsbot_version()} - <a href="https://github.com/nikdoof/smsbot">GitHub</a>'
@@ -78,8 +80,8 @@ class TwilioWebhookHandler(object):
"""Return basic health information""" """Return basic health information"""
return { return {
"version": get_smsbot_version(), "version": get_smsbot_version(),
"owners": self.bot.owners, "owners": self.telegram_app.owners,
"subscribers": len(self.bot.subscribers), "subscribers": len(self.telegram_app.subscribers),
} }
@time(REQUEST_TIME) @time(REQUEST_TIME)
@@ -88,7 +90,7 @@ class TwilioWebhookHandler(object):
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()))
hook_data = TwilioWebhookPayload.parse(request.values.to_dict()) hook_data = TwilioWebhookPayload.parse(request.values.to_dict())
if hook_data: if hook_data:
await self.bot.send_subscribers(hook_data.to_markdownv2()) await self.telegram_app.send_subscribers(hook_data.to_markdownv2())
# Return a blank response # Return a blank response
MESSAGE_COUNT.inc() MESSAGE_COUNT.inc()
@@ -100,7 +102,7 @@ class TwilioWebhookHandler(object):
current_app.logger.info("Received Call from {From}".format(**request.values.to_dict())) current_app.logger.info("Received Call from {From}".format(**request.values.to_dict()))
hook_data = TwilioWebhookPayload.parse(request.values.to_dict()) hook_data = TwilioWebhookPayload.parse(request.values.to_dict())
if hook_data: if hook_data:
await self.bot.send_subscribers(hook_data.to_markdownv2()) await self.telegram_app.send_subscribers(hook_data.to_markdownv2())
# Always reject calls # Always reject calls
CALL_COUNT.inc() CALL_COUNT.inc()