Coverage for app/backend/src/couchers/servicers/auth_unsubscribe.py: 68%
45 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-01 03:25 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-01 03:25 +0000
1"""
2Implements the "quick links" that are included in emails, for unsubscribing without logging in.
3"""
5import logging
7import grpc
8from sqlalchemy import select
9from sqlalchemy.orm import Session
11from couchers.constants import DATETIME_INFINITY
12from couchers.context import CouchersContext, make_one_off_interactive_user_context
13from couchers.models import (
14 GroupChat,
15 GroupChatSubscription,
16 HostingStatus,
17 MeetupStatus,
18 NotificationDeliveryType,
19 User,
20)
21from couchers.notifications import settings
22from couchers.notifications.utils import enum_from_topic_action
23from couchers.proto import conversations_pb2, requests_pb2
24from couchers.proto.internal import unsubscribe_pb2
25from couchers.servicers.requests import Requests
26from couchers.sql import where_moderated_content_visible
28logger = logging.getLogger(__name__)
31def handle_unsubscribe(payload: unsubscribe_pb2.UnsubscribePayload, context: CouchersContext, session: Session) -> str:
32 """
33 Returns a response string or uses context.abort upon error
34 """
35 user = session.execute(select(User).where(User.id == payload.user_id)).scalar_one()
37 if payload.HasField("do_not_email"):
38 logger.info(f"User {user.name} turning of emails")
39 user.do_not_email = True
40 user.hosting_status = HostingStatus.cant_host
41 user.meetup_status = MeetupStatus.does_not_want_to_meetup
42 return context.localization.localize_string("quick_links.do_not_email")
44 if payload.HasField("topic_action"):
45 logger.info(f"User {user.name} unsubscribing from topic_action")
46 topic = payload.topic_action.topic
47 action = payload.topic_action.action
48 topic_action = enum_from_topic_action[topic, action]
49 # disable emails for this type
50 settings.set_preference(session, user.id, topic_action, NotificationDeliveryType.email, False)
51 return context.localization.localize_string("quick_links.topic_action")
53 if payload.HasField("topic_key"): 53 ↛ 54line 53 didn't jump to line 54 because the condition on line 53 was never true
54 logger.info(f"User {user.name} unsubscribing from topic_key")
55 topic = payload.topic_key.topic
56 key = payload.topic_key.key
57 # a bunch of manual stuff
58 if topic == "chat":
59 group_chat_id = int(key)
60 subscription = session.execute(
61 where_moderated_content_visible(
62 select(GroupChatSubscription).join(
63 GroupChat, GroupChat.conversation_id == GroupChatSubscription.group_chat_id
64 ),
65 context,
66 GroupChat,
67 is_list_operation=False,
68 )
69 .where(GroupChatSubscription.group_chat_id == group_chat_id)
70 .where(GroupChatSubscription.user_id == user.id)
71 .where(GroupChatSubscription.left == None)
72 ).scalar_one_or_none()
74 if subscription is None:
75 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "chat_not_found")
77 subscription.muted_until = DATETIME_INFINITY
78 return context.localization.localize_string("quick_links.chat_unsub")
79 else:
80 context.abort_with_error_code(grpc.StatusCode.UNIMPLEMENTED, "cant_unsub_topic")
82 if payload.HasField("host_request_quick_decline"): 82 ↛ 93line 82 didn't jump to line 93 because the condition on line 82 was always true
83 Requests().RespondHostRequest(
84 request=requests_pb2.RespondHostRequestReq(
85 host_request_id=payload.host_request_quick_decline.host_request_id,
86 status=conversations_pb2.HOST_REQUEST_STATUS_REJECTED,
87 ),
88 context=make_one_off_interactive_user_context(couchers_context=context, user_id=payload.user_id),
89 session=session,
90 )
91 return context.localization.localize_string("quick_links.host_request_quick_decline")
93 raise Exception("Unhandled quick link type")