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

1import logging 

2from typing import cast 

3 

4import boto3 

5import luhn 

6 

7from couchers import crypto 

8from couchers.config import config 

9from couchers.db import session_scope 

10from couchers.models import SMS 

11 

12logger = logging.getLogger(__name__) 

13 

14 

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())) 

18 

19 

20def looks_like_a_code(string: str) -> bool: 

21 return len(string) == 6 and string.isdigit() and luhn.verify(string) 

22 

23 

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." 

26 

27 

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.""" 

32 

33 assert len(message) <= 140, "Message too long" 

34 

35 if not config["ENABLE_SMS"]: 

36 logger.info(f"SMS not enabled, need to send to {number}: {message}") 

37 return "SMS not enabled." 

38 

39 sns = boto3.client("sns") 

40 sender_id = config["SMS_SENDER_ID"] 

41 

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 ) 

50 

51 message_id = response["MessageId"] 

52 

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 ) 

62 

63 return "success"