Skip to content

Commit f8fca39

Browse files
committed
Merge branch 'develop' into na/web/pub-trips-tab
2 parents 055be17 + b5b2771 commit f8fca39

129 files changed

Lines changed: 2329 additions & 1205 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CLAUDE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ make mypy
4444
- gRPC for API (defined in `/app/proto`)
4545
- Background jobs in `couchers/jobs/handlers.py`
4646
- Notifications system in `couchers/notifications/`
47-
- Always run `make format` and `make mypy` after modifying backend code
47+
- Always run `make format` and `make mypy` after modifying backend code. mypy MUST pass — a failing mypy is never acceptable, so fix it before moving on (don't dismiss errors as "pre-existing")
48+
- If mypy or tests fail with import errors or missing symbols from generated proto modules (`couchers.proto.*` — e.g. a message type that exists in a `.proto` source but not in the generated `*_pb2.py`), your locally generated protos are stale: run `make protos` to regenerate them, then re-check
4849
- NEVER try-catch an exception and silently throw it away or just log it. By and large you don't need to wrap code in try-catch blocks, we already handle exceptions
4950
- Use `enum.auto()` for all enums (except in the rare case that they are inherently ordinal and we use that order in business logic)
5051
- Put relationships and constraints at the end of models

app/.gitlab-ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1040,7 +1040,9 @@ build:mobile-native-devtool:
10401040
- npm ci
10411041
- npm install -g eas-cli
10421042
- bash scripts/devtool-build.sh ios
1043+
- bash scripts/devtool-build.sh ios-sim
10431044
- bash scripts/devtool-build.sh android
1045+
- bash scripts/devtool-build.sh index
10441046
rules:
10451047
- if: ($BUILD_MOBILE == "true") && ($CI_COMMIT_BRANCH == $RELEASE_BRANCH)
10461048

app/backend.dev.env

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,6 @@ LISTMONK_API_USERNAME=...
6363
LISTMONK_API_KEY=...
6464
LISTMONK_LIST_ID=3
6565

66-
RECAPTHCA_PROJECT_ID=...
67-
RECAPTHCA_API_KEY=...
68-
RECAPTHCA_SITE_KEY=...
69-
7066
SENTRY_ENABLED=0
7167
SENTRY_URL=...
7268

app/backend/feature-flags.dev.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"strong_verification_enabled": false,
55
"log_native_ota_requests": true,
66
"donations_enabled": false,
7-
"recaptcha_enabled": false,
7+
"antibot_enabled": false,
88
"postal_verification_enabled": false,
99
"listmonk_enabled": false,
1010
"notification_translations_enabled": true,

app/backend/proto/internal/jobs.proto

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,17 @@ message SendEmailPayload {
1515
// source data as to where this email came from
1616
string source_data = 8;
1717
reserved 9; // Previous "attachments" field, which had mime_type+filename subfields.
18-
repeated EmailAttachmentV2 attachments = 10;
18+
repeated EmailPart attachments = 10;
19+
repeated EmailPart html_related_parts = 11; // MIME parts which are multipart/related to the HTML body.
1920
}
2021

21-
message EmailAttachmentV2 {
22+
// A MIME part of an SMPT email, usable for attached files or inline images.
23+
message EmailPart {
2224
bytes data = 1;
2325
string content_disposition = 2; // The Content-Disposition header, including parameters
2426
string content_type = 3; // The Content-Type header, including parameters
27+
string content_id = 4; // The Content-ID header, including parameters
28+
string data_file_path = 5; // The server-side path to the file containing the data (mutually exclusive with "data").
2529
}
2630

2731
message HandleNotificationPayload {

app/backend/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ module = ["couchers.migrations.versions.*"]
139139
ignore_errors = true
140140

141141
[[tool.mypy.overrides]]
142-
module = ["http_ece", "py_vapid", "luhn", "sqlalchemy_utils.*", "growthbook", "user_agents", "pyroscope"]
142+
module = ["http_ece", "py_vapid", "luhn", "sqlalchemy_utils.*", "growthbook", "user_agents", "pyroscope", "ics"]
143143
# These libraries don't have type stubs or py.typed markers
144144
ignore_missing_imports = true
145145

app/backend/src/couchers/abuse.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from couchers.context import CouchersContext
2+
from couchers.db import session_scope
3+
from couchers.metrics import observe_nonvisible_user_access
4+
from couchers.models import (
5+
NonvisibleUserAccess,
6+
NonvisibleUserAccessType,
7+
NonvisibleUserState,
8+
User,
9+
)
10+
11+
12+
def nonvisible_user_state(user: User) -> NonvisibleUserState | None:
13+
if user.banned_at is not None:
14+
return NonvisibleUserState.banned
15+
if user.shadowed_at is not None:
16+
return NonvisibleUserState.shadowed
17+
if user.deleted_at is not None:
18+
return NonvisibleUserState.deleted
19+
return None
20+
21+
22+
def maybe_log_nonvisible_user_access(
23+
context: CouchersContext,
24+
user: User,
25+
*,
26+
access_type: NonvisibleUserAccessType,
27+
actor_user_id: int | None,
28+
) -> None:
29+
target_state = nonvisible_user_state(user)
30+
if target_state is None:
31+
return
32+
33+
if actor_user_id == user.id:
34+
ip_address = context.get_header("x-couchers-real-ip")
35+
user_agent = context.get_header("user-agent")
36+
sofa = context._sofa
37+
else:
38+
ip_address = None
39+
user_agent = None
40+
sofa = None
41+
42+
with session_scope() as session:
43+
session.add(
44+
NonvisibleUserAccess(
45+
access_type=access_type,
46+
target_user_id=user.id,
47+
target_state=target_state,
48+
actor_user_id=actor_user_id,
49+
ip_address=ip_address,
50+
user_agent=user_agent,
51+
sofa=sofa,
52+
)
53+
)
54+
55+
observe_nonvisible_user_access(access_type, target_state)

app/backend/src/couchers/config.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,6 @@ class Config:
108108
LISTMONK_API_USERNAME: str
109109
LISTMONK_API_KEY: str
110110
LISTMONK_LIST_ID: int
111-
# Google recaptcha antibot (gated at runtime by the `recaptcha_enabled` feature flag)
112-
RECAPTHCA_PROJECT_ID: str
113-
RECAPTHCA_API_KEY: str
114-
RECAPTHCA_SITE_KEY: str
115111
# Whether we're in test
116112
IN_TEST: bool = False
117113
# Dev-only override file; when set, flags are read from it instead of GrowthBook.
@@ -196,8 +192,8 @@ def check(self) -> None:
196192
raise Exception("Listmonk credentials must be configured in production")
197193

198194
# The following features are gated at runtime by feature flags (`strong_verification_enabled`,
199-
# `postal_verification_enabled`, `recaptcha_enabled`), which can be flipped on remotely at any
200-
# time, so prod must always have their credentials present.
195+
# `postal_verification_enabled`), which can be flipped on remotely at any time, so prod must
196+
# always have their credentials present.
201197
if not self.IRIS_ID_PUBKEY or not self.IRIS_ID_SECRET or not self.VERIFICATION_DATA_PUBLIC_KEY:
202198
raise Exception("Iris ID credentials must be configured in production")
203199
if (
@@ -208,8 +204,6 @@ def check(self) -> None:
208204
or not self.MYPOSTCARD_CAMPAIGN_ID
209205
):
210206
raise Exception("MyPostcard API credentials must be configured in production")
211-
if not self.RECAPTHCA_PROJECT_ID or not self.RECAPTHCA_API_KEY or not self.RECAPTHCA_SITE_KEY:
212-
raise Exception("reCAPTCHA credentials must be configured in production")
213207

214208
if self.FEATURE_FLAGS_FILE_OVERRIDE_PATH:
215209
raise Exception("FEATURE_FLAGS_FILE_OVERRIDE_PATH is dev-only and must not be set in production")

app/backend/src/couchers/context.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from couchers import experimentation
66
from couchers.i18n import LocalizationContext
7+
from couchers.i18n.locales import get_translation_component
78

89
if TYPE_CHECKING:
910
from growthbook import GrowthBook
@@ -122,17 +123,30 @@ def abort(self, status_code: grpc.StatusCode, error_message: str) -> NoReturn:
122123
context.abort(status_code, error_message)
123124

124125
def abort_with_error_code(
125-
self, status_code: grpc.StatusCode, error_message_id: str, *, substitutions: dict[str, str | int] | None = None
126+
self,
127+
status_code: grpc.StatusCode,
128+
error_message_id: str,
129+
*,
130+
substitutions: dict[str, str | int] | None = None,
126131
) -> NoReturn:
127132
"""
128133
Raises an error that's returned to the user, but error_message_id should be an entry from translateable errors
134+
135+
error_message_id may be namespaced with a translation component, like i18next, e.g. "admin:object_not_found"
136+
looks up "object_not_found" in the "admin" component (where admin/editor errors live). Without a prefix the
137+
"main" component is used.
129138
"""
130139
if not self.__is_interactive:
131140
raise NonInteractiveAbortException(status_code, error_message_id)
132141
else:
133142
context = cast(grpc.ServicerContext, self._grpc_context)
143+
component, _, error_name = error_message_id.rpartition(":")
134144
# Get the translated error message using the user's language preference
135-
error_message = self.localization.localize_string(f"errors.{error_message_id}", substitutions=substitutions)
145+
error_message = self.localization.localize_string(
146+
f"errors.{error_name}",
147+
i18next=get_translation_component(component or "main"),
148+
substitutions=substitutions,
149+
)
136150
context.abort(status_code, error_message)
137151

138152
def set_cookies(self, cookies: list[str]) -> None:

0 commit comments

Comments
 (0)