Skip to content

Commit 9385b3e

Browse files
authored
Merge branch 'develop' into web/feature/profile-message-form
2 parents d9c7283 + d383262 commit 9385b3e

17 files changed

Lines changed: 410 additions & 191 deletions

File tree

app/backend/src/couchers/email/dev.py

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,12 @@
22

33
from couchers.crypto import random_hex
44
from couchers.models import Email
5+
from couchers.proto.internal import jobs_pb2
56

67
logger = logging.getLogger(__name__)
78

89

9-
def print_dev_email(
10-
sender_name: str,
11-
sender_email: str,
12-
recipient: str,
13-
subject: str,
14-
plain: str,
15-
html: str,
16-
list_unsubscribe_header: str | None,
17-
source_data: str | None,
18-
) -> Email:
10+
def print_dev_email(payload: jobs_pb2.SendEmailPayload) -> Email:
1911
"""
2012
Generates a dummy Email object and prints the plain email contents to the logger
2113
@@ -26,16 +18,16 @@ def print_dev_email(
2618
message_id = random_hex()
2719

2820
logger.info("Dev email:")
29-
logger.info(plain)
21+
logger.info(payload.plain)
3022

3123
return Email(
3224
id=message_id,
33-
sender_name=sender_name,
34-
sender_email=sender_email,
35-
recipient=recipient,
36-
subject=subject,
37-
plain=plain,
38-
html=html,
39-
list_unsubscribe_header=list_unsubscribe_header,
40-
source_data=source_data,
25+
sender_name=payload.sender_name,
26+
sender_email=payload.sender_email,
27+
recipient=payload.recipient,
28+
subject=payload.subject,
29+
plain=payload.plain,
30+
html=payload.html,
31+
list_unsubscribe_header=payload.list_unsubscribe_header,
32+
source_data=payload.source_data,
4133
)

app/backend/src/couchers/email/queuing.py

Lines changed: 20 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -13,34 +13,14 @@
1313
from couchers.utils import now
1414

1515

16-
def _queue_email(
17-
session: Session,
18-
sender_name: str,
19-
sender_email: str,
20-
recipient: str,
21-
subject: str,
22-
plain: str,
23-
html: str | None,
24-
list_unsubscribe_header: str | None,
25-
source_data: str | None,
26-
) -> None:
16+
def _queue_email(session: Session, payload: jobs_pb2.SendEmailPayload) -> None:
2717
"""
2818
This indirection is so that this can be easily mocked. Not sure how to do it better :(
2919
"""
3020

3121
# Import here to avoid circular dependency
3222
from couchers.jobs.handlers import send_email # noqa: PLC0415
3323

34-
payload = jobs_pb2.SendEmailPayload(
35-
sender_name=sender_name,
36-
sender_email=sender_email,
37-
recipient=recipient,
38-
subject=subject,
39-
plain=plain,
40-
html=html,
41-
list_unsubscribe_header=list_unsubscribe_header,
42-
source_data=source_data,
43-
)
4424
queue_job(
4525
session,
4626
job=send_email,
@@ -51,28 +31,8 @@ def _queue_email(
5131
emails_counter.inc()
5232

5333

54-
def queue_email(
55-
session: Session,
56-
sender_name: str,
57-
sender_email: str,
58-
recipient: str,
59-
subject: str,
60-
plain: str,
61-
html: str | None,
62-
list_unsubscribe_header: str | None = None,
63-
source_data: str | None = None,
64-
) -> None:
65-
_queue_email(
66-
session=session,
67-
sender_name=sender_name,
68-
sender_email=sender_email,
69-
recipient=recipient,
70-
subject=subject,
71-
plain=plain,
72-
html=html,
73-
list_unsubscribe_header=list_unsubscribe_header,
74-
source_data=source_data,
75-
)
34+
def queue_email(session: Session, payload: jobs_pb2.SendEmailPayload) -> None:
35+
_queue_email(session, payload)
7636

7737

7838
def queue_userless_email(
@@ -109,13 +69,15 @@ def queue_userless_email(
10969

11070
queue_email(
11171
session,
112-
sender_name=config["NOTIFICATION_EMAIL_SENDER"],
113-
sender_email=config["NOTIFICATION_EMAIL_ADDRESS"],
114-
recipient=recipient,
115-
subject=config["NOTIFICATION_PREFIX"] + subject,
116-
plain=plain,
117-
html=html,
118-
source_data=config["VERSION"] + f"/{template_name}",
72+
jobs_pb2.SendEmailPayload(
73+
sender_name=config["NOTIFICATION_EMAIL_SENDER"],
74+
sender_email=config["NOTIFICATION_EMAIL_ADDRESS"],
75+
recipient=recipient,
76+
subject=config["NOTIFICATION_PREFIX"] + subject,
77+
plain=plain,
78+
html=html,
79+
source_data=config["VERSION"] + f"/{template_name}",
80+
),
11981
)
12082

12183

@@ -134,11 +96,12 @@ def queue_system_email(session: Session, recipient: str, template_name: str, tem
13496

13597
queue_email(
13698
session,
137-
sender_name=config["NOTIFICATION_EMAIL_SENDER"],
138-
sender_email=config["NOTIFICATION_EMAIL_ADDRESS"],
139-
recipient=recipient,
140-
subject=config["NOTIFICATION_PREFIX"] + frontmatter["subject"],
141-
plain=plain,
142-
html=None,
143-
source_data=template_name,
99+
jobs_pb2.SendEmailPayload(
100+
sender_name=config["NOTIFICATION_EMAIL_SENDER"],
101+
sender_email=config["NOTIFICATION_EMAIL_ADDRESS"],
102+
recipient=recipient,
103+
subject=config["NOTIFICATION_PREFIX"] + frontmatter["subject"],
104+
plain=plain,
105+
source_data=template_name,
106+
),
144107
)

app/backend/src/couchers/email/smtp.py

Lines changed: 26 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from couchers.config import config
99
from couchers.crypto import EMAIL_SOURCE_DATA_KEY_NAME, random_hex, simple_hash_signature
1010
from couchers.models import Email
11+
from couchers.proto.internal import jobs_pb2
1112

1213
template_base = Path(Path(__file__).parent / ".." / ".." / ".." / "templates" / "v2")
1314

@@ -18,51 +19,43 @@ def make_cid(sender_email: str) -> tuple[str, str]:
1819
return cid, without_tag
1920

2021

21-
def send_smtp_email(
22-
sender_name: str,
23-
sender_email: str,
24-
recipient: str,
25-
subject: str,
26-
plain: str,
27-
html: str,
28-
list_unsubscribe_header: str | None,
29-
source_data: str | None,
30-
) -> Email:
22+
def send_smtp_email(payload: jobs_pb2.SendEmailPayload) -> Email:
3123
"""
3224
Sends out the email through SMTP, settings from config.
3325
3426
Returns a models.Email object that can be straight away added to the database.
3527
"""
3628
message_id = random_hex()
3729
msg = EmailMessage()
38-
msg["Subject"] = subject
39-
msg["From"] = Address(sender_name, addr_spec=sender_email)
40-
msg["To"] = Address(addr_spec=recipient)
30+
msg["Subject"] = payload.subject
31+
msg["From"] = Address(payload.sender_name, addr_spec=payload.sender_email)
32+
msg["To"] = Address(addr_spec=payload.recipient)
4133
msg["X-Couchers-ID"] = message_id
4234

43-
if list_unsubscribe_header:
44-
msg["List-Unsubscribe"] = list_unsubscribe_header
35+
if payload.list_unsubscribe_header:
36+
msg["List-Unsubscribe"] = payload.list_unsubscribe_header
4537

46-
if source_data:
47-
msg["X-Couchers-Source-Data"] = source_data
48-
msg["X-Couchers-Source-Sig"] = simple_hash_signature(source_data, EMAIL_SOURCE_DATA_KEY_NAME)
38+
if payload.source_data:
39+
msg["X-Couchers-Source-Data"] = payload.source_data
40+
msg["X-Couchers-Source-Sig"] = simple_hash_signature(payload.source_data, EMAIL_SOURCE_DATA_KEY_NAME)
4941

50-
msg.set_content(plain)
42+
msg.set_content(payload.plain)
5143

52-
if html:
44+
updated_html = payload.html
45+
if updated_html:
5346
# for any png files in attachment_imgs/, goes through and replaces instances of the filename with attachment
5447
used_attachments = []
5548
for attachment in (template_base / "attachment_imgs").glob("*.png"):
5649
attachment_html_path = str(attachment.relative_to(template_base))
57-
if attachment_html_path not in html:
50+
if attachment_html_path not in updated_html:
5851
continue
5952
# it's used in this template, so attach and replace it
6053
data = attachment.read_bytes()
61-
cid, wcid = make_cid(sender_email)
62-
html = html.replace(attachment_html_path, f"cid:{wcid}")
54+
cid, wcid = make_cid(payload.sender_email)
55+
updated_html = updated_html.replace(attachment_html_path, f"cid:{wcid}")
6356
used_attachments.append((cid, "image", "png", data))
6457

65-
msg.add_alternative(html, subtype="html")
58+
msg.add_alternative(updated_html, subtype="html")
6659

6760
for cid, mime_type, mime_subtype, data in used_attachments:
6861
payloads = cast(list[MIMEPart], msg.get_payload())
@@ -75,16 +68,16 @@ def send_smtp_email(
7568
# stmplib docs recommend calling ehlo() before and after starttls()
7669
server.ehlo()
7770
server.login(config["SMTP_USERNAME"], config["SMTP_PASSWORD"])
78-
server.sendmail(sender_email, recipient, msg.as_string())
71+
server.sendmail(payload.sender_email, payload.recipient, msg.as_string())
7972

8073
return Email(
8174
id=message_id,
82-
sender_name=sender_name,
83-
sender_email=sender_email,
84-
recipient=recipient,
85-
subject=subject,
86-
plain=plain,
87-
html=html,
88-
list_unsubscribe_header=list_unsubscribe_header,
89-
source_data=source_data,
75+
sender_name=payload.sender_name,
76+
sender_email=payload.sender_email,
77+
recipient=payload.recipient,
78+
subject=payload.subject,
79+
plain=payload.plain,
80+
html=updated_html,
81+
list_unsubscribe_header=payload.list_unsubscribe_header,
82+
source_data=payload.source_data,
9083
)

app/backend/src/couchers/jobs/handlers.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -139,16 +139,7 @@ def send_email(payload: jobs_pb2.SendEmailPayload) -> None:
139139
# selects a "sender", which either prints the email to the logger or sends it out with SMTP
140140
sender = send_smtp_email if config["ENABLE_EMAIL"] else print_dev_email
141141
# the sender must return a models.Email object that can be added to the database
142-
email = sender(
143-
sender_name=payload.sender_name,
144-
sender_email=payload.sender_email,
145-
recipient=payload.recipient,
146-
subject=payload.subject,
147-
plain=payload.plain,
148-
html=payload.html,
149-
list_unsubscribe_header=payload.list_unsubscribe_header,
150-
source_data=payload.source_data,
151-
)
142+
email = sender(payload)
152143
with session_scope() as session:
153144
session.add(email)
154145

app/backend/src/couchers/notifications/background.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,16 @@ def _send_email_notification(session: Session, user: User, notification: Notific
4949

5050
queue_email(
5151
session,
52-
sender_name=config["NOTIFICATION_EMAIL_SENDER"],
53-
sender_email=config["NOTIFICATION_EMAIL_ADDRESS"],
54-
recipient=user.email,
55-
subject=config["NOTIFICATION_PREFIX"] + rendered.subject,
56-
plain=rendered.body_plaintext,
57-
html=rendered.body_html,
58-
source_data=rendered.source_data,
59-
list_unsubscribe_header=rendered.list_unsubscribe_header,
52+
jobs_pb2.SendEmailPayload(
53+
sender_name=config["NOTIFICATION_EMAIL_SENDER"],
54+
sender_email=config["NOTIFICATION_EMAIL_ADDRESS"],
55+
recipient=user.email,
56+
subject=config["NOTIFICATION_PREFIX"] + rendered.subject,
57+
plain=rendered.body_plaintext,
58+
html=rendered.body_html,
59+
source_data=rendered.source_data,
60+
list_unsubscribe_header=rendered.list_unsubscribe_header,
61+
),
6062
)
6163

6264

app/backend/src/couchers/servicers/admin.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import grpc
55
from google.protobuf import empty_pb2
66
from sqlalchemy import select
7-
from sqlalchemy.orm import Session, selectinload
7+
from sqlalchemy.orm import Session, aliased, selectinload
88
from sqlalchemy.sql import and_, func, or_
99
from user_agents import parse as user_agents_parse
1010

@@ -114,6 +114,8 @@ def _user_to_details(session: Session, user: User) -> admin_pb2.UserDetails:
114114
level=adminactionlevel2api[action.level],
115115
note=action.note or "",
116116
tag=action.tag or "",
117+
target_user_id=action.target_user_id,
118+
target_username=user.username,
117119
)
118120
)
119121

@@ -1085,3 +1087,49 @@ def SetModScore(
10851087
user.mod_score = request.mod_score
10861088
log_admin_action(session, context, user, "set_mod_score", note=f"mod_score={request.mod_score}")
10871089
return _user_to_details(session, user)
1090+
1091+
def ListAdminActions(
1092+
self, request: admin_pb2.ListAdminActionsReq, context: CouchersContext, session: Session
1093+
) -> admin_pb2.ListAdminActionsRes:
1094+
page_size = min(MAX_PAGINATION_LENGTH, request.page_size or MAX_PAGINATION_LENGTH)
1095+
1096+
admin_user = aliased(User)
1097+
target_user = aliased(User)
1098+
1099+
statement = (
1100+
select(AdminAction, admin_user.username, target_user.username)
1101+
.join(admin_user, AdminAction.admin_user_id == admin_user.id)
1102+
.join(target_user, AdminAction.target_user_id == target_user.id)
1103+
)
1104+
1105+
if request.admin_user_id:
1106+
statement = statement.where(AdminAction.admin_user_id == request.admin_user_id)
1107+
if request.target_user_id:
1108+
statement = statement.where(AdminAction.target_user_id == request.target_user_id)
1109+
if request.page_token:
1110+
statement = statement.where(AdminAction.id < int(request.page_token))
1111+
1112+
statement = statement.order_by(AdminAction.id.desc()).limit(page_size + 1)
1113+
1114+
rows = session.execute(statement).all()
1115+
1116+
action_pbs = [
1117+
admin_pb2.AdminActionLog(
1118+
admin_action_id=action.id,
1119+
created=Timestamp_from_datetime(action.created),
1120+
admin_user_id=action.admin_user_id,
1121+
admin_username=admin_username,
1122+
action_type=action.action_type,
1123+
level=adminactionlevel2api[action.level],
1124+
note=action.note or "",
1125+
tag=action.tag or "",
1126+
target_user_id=action.target_user_id,
1127+
target_username=target_username,
1128+
)
1129+
for action, admin_username, target_username in rows[:page_size]
1130+
]
1131+
1132+
return admin_pb2.ListAdminActionsRes(
1133+
admin_actions=action_pbs,
1134+
next_page_token=str(rows[page_size - 1][0].id) if len(rows) > page_size else None,
1135+
)

0 commit comments

Comments
 (0)