Coverage for app/backend/src/couchers/phone/sms.py: 100%
22 statements
« prev ^ index » next coverage.py v7.14.2, created at 2026-06-21 09:29 +0000
« prev ^ index » next coverage.py v7.14.2, created at 2026-06-21 09:29 +0000
1from typing import cast
3import boto3
4import luhn
6from couchers import crypto
7from couchers.config import config
8from couchers.db import session_scope
9from couchers.models import SMS
12def generate_random_code() -> str:
13 """Return a random 6-digit string with correct Luhn checksum"""
14 return cast(str, luhn.append(crypto.generate_random_5digit_string()))
17def looks_like_a_code(string: str) -> bool:
18 return len(string) == 6 and string.isdigit() and luhn.verify(string)
21def format_message(token: str) -> str:
22 return f"{token} is your Couchers.org verification code. If you did not request this, please ignore this message. Best, the Couchers.org team."
25def send_sms(number: str, message: str) -> str:
26 """Send SMS to a E.164 formatted phone number with leading +. Return "success" on
27 success, "unsupported operator" on unsupported operator, and any other
28 string for any other error."""
30 assert len(message) <= 140, "Message too long"
32 sns = boto3.client("sns")
33 sender_id = config.SMS_SENDER_ID
35 response = sns.publish(
36 PhoneNumber=number,
37 Message=message,
38 MessageAttributes={
39 "AWS.SNS.SMS.SMSType": {"DataType": "String", "StringValue": "Transactional"},
40 "AWS.SNS.SMS.SenderID": {"DataType": "String", "StringValue": sender_id},
41 },
42 )
44 message_id = response["MessageId"]
46 with session_scope() as session:
47 session.add(
48 SMS(
49 message_id=message_id,
50 number=number,
51 message=message,
52 sms_sender_id=sender_id,
53 )
54 )
56 return "success"