Skip to content

Commit 7f8a3a5

Browse files
Merge upstream develop
2 parents 51a99ba + 867391e commit 7f8a3a5

6 files changed

Lines changed: 155 additions & 35 deletions

File tree

services/notifications/entity/src/push_token_model.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pub struct Model {
1515
pub token: String,
1616

1717
pub created_at: TimeDateTime,
18+
pub updated_at: TimeDateTime,
1819
}
1920

2021
impl ActiveModelBehavior for ActiveModel {}
Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
pub use sea_orm_migration::prelude::*;
22

33
mod m20260504_000001_create_push_token_table;
4+
mod m20260616_000001_add_updated_at_to_push_token;
45

56
pub struct Migrator;
67

78
#[async_trait::async_trait]
89
impl MigratorTrait for Migrator {
910
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
10-
vec![Box::new(
11-
m20260504_000001_create_push_token_table::Migration,
12-
)]
11+
vec![
12+
Box::new(m20260504_000001_create_push_token_table::Migration),
13+
Box::new(m20260616_000001_add_updated_at_to_push_token::Migration),
14+
]
1315
}
1416
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
use notifications_entity::push_token_model as push_token;
2+
use sea_orm_migration::prelude::*;
3+
4+
#[derive(DeriveMigrationName)]
5+
pub struct Migration;
6+
7+
#[async_trait::async_trait]
8+
impl MigrationTrait for Migration {
9+
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
10+
if manager.has_column("push_token", "updated_at").await? {
11+
return Ok(());
12+
}
13+
14+
// Add the column nullable first so the existing rows are accepted.
15+
manager
16+
.alter_table(
17+
Table::alter()
18+
.table(push_token::Entity)
19+
.add_column(
20+
ColumnDef::new(push_token::Column::UpdatedAt)
21+
.date_time()
22+
.null(),
23+
)
24+
.to_owned(),
25+
)
26+
.await?;
27+
28+
// Backfill: a token's last-updated time starts as its creation time.
29+
let conn = manager.get_connection();
30+
let backfill = Query::update()
31+
.table(push_token::Entity)
32+
.value(
33+
push_token::Column::UpdatedAt,
34+
Expr::col(push_token::Column::CreatedAt),
35+
)
36+
.to_owned();
37+
conn.execute(&backfill).await?;
38+
39+
// Now enforce NOT NULL to match the entity definition.
40+
manager
41+
.alter_table(
42+
Table::alter()
43+
.table(push_token::Entity)
44+
.modify_column(
45+
ColumnDef::new(push_token::Column::UpdatedAt)
46+
.date_time()
47+
.not_null(),
48+
)
49+
.to_owned(),
50+
)
51+
.await?;
52+
53+
Ok(())
54+
}
55+
56+
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
57+
manager
58+
.alter_table(
59+
Table::alter()
60+
.table(push_token::Entity)
61+
.drop_column(push_token::Column::UpdatedAt)
62+
.to_owned(),
63+
)
64+
.await?;
65+
66+
Ok(())
67+
}
68+
}

services/notifications/src/manager.rs

Lines changed: 76 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::{error::Error, fmt};
22

3-
use log::warn;
3+
use log::{debug, warn};
44
use sea_orm::{DbConn, DbErr, EnumIter};
55

66
use super::repository as token_repository;
@@ -202,18 +202,27 @@ impl NotificationManager {
202202

203203
let rich_content = Self::avatar_rich_content(ctx, profile.avatar).await;
204204

205-
self.send_to_identity(
206-
ctx,
207-
&reply_recipient,
208-
NotificationData {
209-
collapse_id: collapse_id.to_owned(),
210-
title,
211-
body,
212-
data,
213-
rich_content,
214-
},
215-
)
216-
.await?;
205+
debug!("Firing reply notification: from={author} to={reply_recipient} key={key:?}");
206+
207+
let response = self
208+
.send_to_identity(
209+
ctx,
210+
&reply_recipient,
211+
NotificationData {
212+
collapse_id: collapse_id.to_owned(),
213+
title,
214+
body,
215+
data,
216+
rich_content,
217+
},
218+
)
219+
.await?;
220+
221+
if let Some(errors) = Self::ticket_errors(&response) {
222+
warn!(
223+
"Reply notification errors: from={author} to={reply_recipient} key={key:?} errors=[{errors}]"
224+
);
225+
}
217226

218227
Ok(())
219228
}
@@ -247,18 +256,31 @@ impl NotificationManager {
247256

248257
let rich_content = Self::avatar_rich_content(ctx, profile.avatar).await;
249258

250-
self.send_to_identity(
251-
ctx,
252-
&follow.identity,
253-
NotificationData {
254-
collapse_id: collapse_id.to_owned(),
255-
title,
256-
body: "Followed you".to_string(),
257-
data,
258-
rich_content,
259-
},
260-
)
261-
.await?;
259+
debug!(
260+
"Firing follow notification: from={author} to={} key={key:?}",
261+
follow.identity
262+
);
263+
264+
let response = self
265+
.send_to_identity(
266+
ctx,
267+
&follow.identity,
268+
NotificationData {
269+
collapse_id: collapse_id.to_owned(),
270+
title,
271+
body: "Followed you".to_string(),
272+
data,
273+
rich_content,
274+
},
275+
)
276+
.await?;
277+
278+
if let Some(errors) = Self::ticket_errors(&response) {
279+
warn!(
280+
"Follow notification errors: from={author} to={} key={key:?} errors=[{errors}]",
281+
follow.identity
282+
);
283+
}
262284

263285
Ok(())
264286
}
@@ -310,6 +332,29 @@ impl NotificationManager {
310332
format!("{cdn_url}/blob/{}_{}", digest.r#type, hex)
311333
}
312334

335+
/// The error details of any failed tickets in a push response,
336+
/// comma-separated, or `None` when every ticket succeeded. Lets callers
337+
/// log failures while staying silent on success.
338+
fn ticket_errors(response: &ExpoPushResponse) -> Option<String> {
339+
let errors: Vec<&str> = response
340+
.data
341+
.iter()
342+
.filter(|ticket| ticket.status == "error")
343+
.map(|ticket| {
344+
ticket
345+
.details
346+
.as_ref()
347+
.and_then(|details| details.error.as_deref())
348+
.unwrap_or("unknown error")
349+
})
350+
// DeviceNotRegistered is already handled by
351+
// clean_unregistered_push_tokens
352+
.filter(|error| *error != "DeviceNotRegistered")
353+
.collect();
354+
355+
(!errors.is_empty()).then(|| errors.join(","))
356+
}
357+
313358
/// Sends a push notification to every authorized key of an identity that
314359
/// has a registered token. Tokens reported as invalid by the push service
315360
/// are unregistered as part of the send.
@@ -318,7 +363,7 @@ impl NotificationManager {
318363
ctx: &Context,
319364
identity: &str,
320365
notification: NotificationData,
321-
) -> Result<(), NotificationError> {
366+
) -> Result<ExpoPushResponse, NotificationError> {
322367
let authorized_keys = ctx.polycentric.authorized_keys(identity).await;
323368

324369
let mut rows = vec![];
@@ -341,7 +386,7 @@ impl NotificationManager {
341386
}
342387

343388
if expo_tokens.is_empty() {
344-
return Ok(());
389+
return Ok(ExpoPushResponse { data: vec![] });
345390
}
346391

347392
let expo_tokens_raw: Vec<String> = expo_tokens.iter().map(|item| item.1.clone()).collect();
@@ -360,17 +405,17 @@ impl NotificationManager {
360405
.post_requests(vec![expo_push_request])
361406
.await?;
362407

363-
self.clean_unregistered_push_tokens(ctx, response, expo_tokens)
408+
self.clean_unregistered_push_tokens(ctx, &response, expo_tokens)
364409
.await?;
365410

366-
Ok(())
411+
Ok(response)
367412
}
368413

369414
/// Removes any tokens from a given batch which are no longer registered from the database
370415
async fn clean_unregistered_push_tokens(
371416
&self,
372417
ctx: &Context,
373-
response: ExpoPushResponse,
418+
response: &ExpoPushResponse,
374419
expo_tokens: Vec<(PublicKey, String)>,
375420
) -> Result<(), NotificationError> {
376421
if response.data.len() != expo_tokens.len() {
@@ -639,6 +684,7 @@ mod tests {
639684
service: PushService::Expo.as_ref().to_string(),
640685
token: token.to_string(),
641686
created_at: time::PrimitiveDateTime::MIN,
687+
updated_at: time::PrimitiveDateTime::MIN,
642688
}
643689
}
644690

services/notifications/src/repository.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,15 @@ impl Mutation {
2828
token: String,
2929
) -> Result<(), DbErr> {
3030
let now = time::OffsetDateTime::now_utc();
31-
let created_at = time::PrimitiveDateTime::new(now.date(), now.time());
31+
let timestamp = time::PrimitiveDateTime::new(now.date(), now.time());
3232

3333
let active = PushTokenModel::ActiveModel {
3434
public_key_type: Set(public_key.key_type as i16),
3535
public_key: Set(public_key.key.clone()),
3636
service: Set(service),
3737
token: Set(token),
38-
created_at: Set(created_at),
38+
created_at: Set(timestamp),
39+
updated_at: Set(timestamp),
3940
};
4041

4142
PushTokenModel::Entity::insert(active)
@@ -47,6 +48,7 @@ impl Mutation {
4748
.update_columns([
4849
PushTokenModel::Column::Service,
4950
PushTokenModel::Column::Token,
51+
PushTokenModel::Column::UpdatedAt,
5052
])
5153
.to_owned(),
5254
)

services/notifications/src/rpc.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ mod tests {
144144
service: PushService::Expo.as_ref().to_string(),
145145
token: "ExponentPushToken[abc123]".to_string(),
146146
created_at: synced_at,
147+
updated_at: synced_at,
147148
}]])
148149
.into_connection();
149150

0 commit comments

Comments
 (0)