Coverage for src/couchers/phone/sms.py: 100%
27 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-12-20 11:53 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-12-20 11:53 +0000
1import logging
2from typing import cast
4import boto3
5import luhn
7from couchers import crypto
8from couchers.config import config
9from couchers.db import session_scope
10from couchers.models import SMS
12logger = logging.getLogger(__name__)
15def generate_random_code() -> str:
16 """Return a random 6-digit string with correct Luhn checksum"""
17 return cast(str, luhn.append(crypto.generate_random_5digit_string()))
20def looks_like_a_code(string: str) -> bool:
21 return len(string) == 6 and string.isdigit() and luhn.verify(string)
24def format_message(token: str) -> str:
25 return f"{token} is your Couchers.org verification code. If you did not request this, please ignore this message. Best, the Couchers.org team."
28def send_sms(number: str, message: str) -> str:
29 """Send SMS to a E.164 formatted phone number with leading +. Return "success" on
30 success, "unsupported operator" on unsupported operator, and any other
31 string for any other error."""
33 assert len(message) <= 140, "Message too long"
35 if not config["ENABLE_SMS"]:
36 logger.info(f"SMS not enabled, need to send to {number}: {message}")
37 return "SMS not enabled."
39 sns = boto3.client("sns")
40 sender_id = config["SMS_SENDER_ID"]
42 response = sns.publish(
43 PhoneNumber=number,
44 Message=message,
45 MessageAttributes={
46 "AWS.SNS.SMS.SMSType": {"DataType": "String", "StringValue": "Transactional"},
47 "AWS.SNS.SMS.SenderID": {"DataType": "String", "StringValue": sender_id},
48 },
49 )
51 message_id = response["MessageId"]
53 with session_scope() as session:
54 session.add(
55 SMS(
56 message_id=message_id,
57 number=number,
58 message=message,
59 sms_sender_id=sender_id,
60 )
61 )
63 return "success"