From d3c73ccc4e913485f3fb1f17553a11d5857c2e84 Mon Sep 17 00:00:00 2001 From: j <13580441+gary02@users.noreply.github.com> Date: Fri, 26 Jul 2024 11:30:14 +0800 Subject: [PATCH 1/4] refactor(notice): refactor to support multi recipients --- lib/notification/index.ts | 239 +++++++++++++++++++----------------- lib/notification/types.d.ts | 18 ++- 2 files changed, 139 insertions(+), 118 deletions(-) diff --git a/lib/notification/index.ts b/lib/notification/index.ts index 4b449b5..e8121c5 100644 --- a/lib/notification/index.ts +++ b/lib/notification/index.ts @@ -9,10 +9,11 @@ import type { NotificationEntity, NotificationType, PutNoticeParams, + PutNoticesParams, NotificationParams, UserNotifySettingDB, } from './types' -import type { Language, User } from '../types' +import type { User } from '../types' import uniqBy from 'lodash.uniqby' import lodash from 'lodash' @@ -42,69 +43,64 @@ export class NotificationService { } public async trigger(params: NotificationParams) { - const recipient = await this.knexRO('user') - .where({ id: params.recipientId }) - .first() - - if (!recipient) { - console.warn(`recipient ${params.recipientId} not found, skipped`) - return - } - - const noticeParams = await this.getNoticeParams(params, recipient.language) + const noticeParams = await this.getNoticeParams(params) if (!noticeParams) { return } - // skip if actor === recipient - if ('actorId' in params && params.actorId === params.recipientId) { - console.warn( - `Actor ${params.actorId} is same as recipient ${params.recipientId}, skipped` - ) - return - } + for (const [index, recipientId] of noticeParams.recipientIds.entries()) { + // skip if actor === recipient + if ('actorId' in params && params.actorId === recipientId) { + console.warn( + `Actor ${params.actorId} is same as recipient ${recipientId}, skipped` + ) + continue + } - // skip if user disable notify - const notifySetting = await this.findNotifySetting(recipient.id) - const enable = await this.checkUserNotifySetting({ - event: params.event, - setting: notifySetting, - }) + // skip if user disable notify + const notifySetting = await this.findNotifySetting(recipientId) + const enable = await this.checkUserNotifySetting({ + event: params.event, + setting: notifySetting, + }) - if (!enable) { - console.info( - `Send ${noticeParams.type} to ${noticeParams.recipientId} skipped` - ) - return - } + if (!enable) { + console.info(`Send ${noticeParams.type} to ${recipientId} skipped`) + continue + } - // skip if sender is blocked by recipient - if ('actorId' in params && params.actorId) { - const blocked = await this.knexRO - .select() - .from('action_user') - .where({ - userId: recipient.id, - targetId: params.actorId, - action: USER_ACTION.block, - }) - .first() + // skip if sender is blocked by recipient + if ('actorId' in params && params.actorId) { + const blocked = await this.knexRO + .select() + .from('action_user') + .where({ + userId: recipientId, + targetId: params.actorId, + action: USER_ACTION.block, + }) + .first() - if (blocked) { - console.info( - `Actor ${params.actorId} is blocked by recipient ${params.recipientId}, skipped` - ) - return + if (blocked) { + console.info( + `Actor ${params.actorId} is blocked by recipient ${recipientId}, skipped` + ) + continue + } } - } - // Put Notice to DB - const { created, bundled } = await this.process(noticeParams) + // Put Notice to DB + const { created, bundled } = await this.process({ + ...noticeParams, + recipientId, + message: noticeParams.messages ? noticeParams.messages[index] : null, + }) - if (!created && !bundled) { - console.info(`Notice ${params.event} to ${params.recipientId} skipped`) - return + if (!created && !bundled) { + console.info(`Notice ${params.event} to ${params.recipientId} skipped`) + continue + } } } @@ -545,15 +541,22 @@ export class NotificationService { } private getNoticeParams = async ( - params: NotificationParams, - language: Language - ): Promise => { + params: NotificationParams + ): Promise => { + const recipient = params.recipientId + ? await this.knexRO('user').where({ id: params.recipientId }).first() + : null + + if (params.recipientId && !recipient) { + console.warn(`recipient ${params.recipientId} not found, skipped`) + return + } switch (params.event) { // entity-free case NOTICE_TYPE.user_new_follower: return { type: params.event, - recipientId: params.recipientId, + recipientIds: [recipient.id], actorId: params.actorId, } // system as the actor @@ -563,7 +566,7 @@ export class NotificationService { case NOTICE_TYPE.circle_new_article: // deprecated return { type: params.event, - recipientId: params.recipientId, + recipientIds: [params.recipientId], entities: params.entities, } // single actor with one or more entities @@ -582,7 +585,7 @@ export class NotificationService { case NOTICE_TYPE.moment_comment_liked: return { type: params.event, - recipientId: params.recipientId, + recipientIds: [params.recipientId], actorId: params.actorId, entities: params.entities, } @@ -594,7 +597,7 @@ export class NotificationService { case NOTICE_TYPE.moment_comment_mentioned_you: return { type: params.event, - recipientId: params.recipientId, + recipientIds: [params.recipientId], actorId: params.actorId, entities: params.entities, bundle: { disabled: true }, @@ -602,7 +605,7 @@ export class NotificationService { case NOTICE_TYPE.circle_invitation: return { type: params.event, - recipientId: params.recipientId, + recipientIds: [params.recipientId], actorId: params.actorId, entities: params.entities, resend: true, @@ -613,7 +616,7 @@ export class NotificationService { case BUNDLED_NOTICE_TYPE.in_circle_new_broadcast_reply: return { type: NOTICE_TYPE.circle_new_broadcast_comments, - recipientId: params.recipientId, + recipientIds: [params.recipientId], actorId: params.actorId, entities: params.entities, data: params.data, // update latest comment to DB `data` field @@ -627,7 +630,7 @@ export class NotificationService { case BUNDLED_NOTICE_TYPE.in_circle_new_discussion_reply: return { type: NOTICE_TYPE.circle_new_discussion_comments, - recipientId: params.recipientId, + recipientIds: [params.recipientId], actorId: params.actorId, entities: params.entities, data: params.data, // update latest comment to DB `data` field @@ -637,96 +640,106 @@ export class NotificationService { case NOTICE_TYPE.official_announcement: return { type: NOTICE_TYPE.official_announcement, - recipientId: params.recipientId, - message: params.message, + recipientIds: [params.recipientId], + messages: [params.message], data: params.data, } case OFFICIAL_NOTICE_EXTEND_TYPE.user_banned: return { type: NOTICE_TYPE.official_announcement, - recipientId: params.recipientId, - message: trans.user_banned(language, {}), + recipientIds: [params.recipientId], + messages: [trans.user_banned(recipient.language, {})], } case OFFICIAL_NOTICE_EXTEND_TYPE.user_banned_payment: return { type: NOTICE_TYPE.official_announcement, - recipientId: params.recipientId, - message: trans.user_banned_payment(language, {}), + recipientIds: [params.recipientId], + messages: [trans.user_banned_payment(recipient.language, {})], } case OFFICIAL_NOTICE_EXTEND_TYPE.user_frozen: return { type: NOTICE_TYPE.official_announcement, - recipientId: params.recipientId, - message: trans.user_frozen(language, {}), + recipientIds: [params.recipientId], + messages: [trans.user_frozen(recipient.language, {})], } case OFFICIAL_NOTICE_EXTEND_TYPE.user_unbanned: return { type: NOTICE_TYPE.official_announcement, - recipientId: params.recipientId, - message: trans.user_unbanned(language, {}), + recipientIds: [params.recipientId], + messages: [trans.user_unbanned(recipient.language, {})], } case OFFICIAL_NOTICE_EXTEND_TYPE.comment_banned: return { type: NOTICE_TYPE.official_announcement, - recipientId: params.recipientId, - message: trans.comment_banned(language, { - content: params.entities[0].entity.content, - }), + recipientIds: [params.recipientId], + messages: [ + trans.comment_banned(recipient.language, { + content: params.entities[0].entity.content, + }), + ], entities: params.entities, } case OFFICIAL_NOTICE_EXTEND_TYPE.article_banned: return { type: NOTICE_TYPE.official_announcement, - recipientId: params.recipientId, - message: trans.article_banned(language, { - title: ( - await loadLatestArticleVersion( - params.entities[0].entity.id, - this.knexRO - ) - ).title, - }), + recipientIds: [params.recipientId], + messages: [ + trans.article_banned(recipient.language, { + title: ( + await loadLatestArticleVersion( + params.entities[0].entity.id, + this.knexRO + ) + ).title, + }), + ], entities: params.entities, } case OFFICIAL_NOTICE_EXTEND_TYPE.comment_reported: return { type: NOTICE_TYPE.official_announcement, - recipientId: params.recipientId, - message: trans.comment_reported(language, { - content: params.entities[0].entity.content, - }), + recipientIds: [params.recipientId], + messages: [ + trans.comment_reported(recipient.language, { + content: params.entities[0].entity.content, + }), + ], entities: params.entities, } case OFFICIAL_NOTICE_EXTEND_TYPE.article_reported: return { type: NOTICE_TYPE.official_announcement, - recipientId: params.recipientId, - message: trans.article_reported(language, { - title: ( - await loadLatestArticleVersion( - params.entities[0].entity.id, - this.knexRO - ) - ).title, - }), + recipientIds: [params.recipientId], + messages: [ + trans.article_reported(recipient.language, { + title: ( + await loadLatestArticleVersion( + params.entities[0].entity.id, + this.knexRO + ) + ).title, + }), + ], entities: params.entities, } case OFFICIAL_NOTICE_EXTEND_TYPE.write_challenge_applied: return { type: NOTICE_TYPE.official_announcement, - recipientId: params.recipientId, - message: trans.write_challenge_applied(language, { - name: - (await findTranslation( - { - table: 'campaign', - field: 'name', - id: params.entities[0].entity.id, - language, - }, - this.knexRO - )) ?? params.entities[0].entity.name, - }), + recipientIds: [params.recipientId], + messages: [ + trans.write_challenge_applied(recipient.language, { + name: + (await findTranslation( + { + table: 'campaign', + field: 'name', + id: params.entities[0].entity.id, + language: recipient.language, + }, + this.knexRO + )) ?? params.entities[0].entity.name, + }), + ], data: params.data, } case OFFICIAL_NOTICE_EXTEND_TYPE.badge_grand_slam_awarded: { @@ -737,8 +750,8 @@ export class NotificationService { const domain = process.env.MATTERS_DOMAIN ?? 'matters.town' return { type: NOTICE_TYPE.official_announcement, - recipientId: params.recipientId, - message: trans.badge_grand_slam_awarded(language, {}), + recipientIds: [params.recipientId], + messages: [trans.badge_grand_slam_awarded(recipient.language, {})], data: { link: `https://${domain}/@${recipient.userName}?dialog=grand-badge&step=congrats`, }, diff --git a/lib/notification/types.d.ts b/lib/notification/types.d.ts index 051e68f..17fd5ab 100644 --- a/lib/notification/types.d.ts +++ b/lib/notification/types.d.ts @@ -172,7 +172,7 @@ interface NoticeMomentNewCommentParams extends NotificationRequiredParams { recipientId: string actorId: string entities: [ - NotificationEntity<'target', 'article'>, + NotificationEntity<'target', 'moment'>, NotificationEntity<'comment', 'comment'> ] } @@ -444,12 +444,10 @@ export type NoticeItem = NoticeDetail & { entities?: NoticeEntitiesMap } -export interface PutNoticeParams { +type BasePutParams = { type: BaseNoticeType - actorId?: NoticeUserId | null - recipientId: NoticeUserId entities?: NotificationEntity[] - message?: NoticeMessage | null + actorId?: NoticeUserId | null data?: NoticeData | null resend?: boolean // used by circle invitation notice @@ -460,6 +458,16 @@ export interface PutNoticeParams { } } +export type PutNoticesParams = BasePutParams & { + recipientIds: NoticeUserId[] + messages?: NoticeMessage[] | null +} + +export type PutNoticeParams = BasePutParams & { + recipientId: NoticeUserId + message?: NoticeMessage | null +} + // DB schema export interface NoticeDB { From 1d0f56cc8b22ce698ede8f061c895bb5b9d1a5da Mon Sep 17 00:00:00 2001 From: j <13580441+gary02@users.noreply.github.com> Date: Fri, 26 Jul 2024 11:38:33 +0800 Subject: [PATCH 2/4] feat(notice): update copy --- lib/notification/index.ts | 15 +-------------- lib/notification/translations.ts | 13 +++++-------- 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/lib/notification/index.ts b/lib/notification/index.ts index e8121c5..8422827 100644 --- a/lib/notification/index.ts +++ b/lib/notification/index.ts @@ -726,20 +726,7 @@ export class NotificationService { return { type: NOTICE_TYPE.official_announcement, recipientIds: [params.recipientId], - messages: [ - trans.write_challenge_applied(recipient.language, { - name: - (await findTranslation( - { - table: 'campaign', - field: 'name', - id: params.entities[0].entity.id, - language: recipient.language, - }, - this.knexRO - )) ?? params.entities[0].entity.name, - }), - ], + messages: [trans.write_challenge_applied(recipient.language, {})], data: params.data, } case OFFICIAL_NOTICE_EXTEND_TYPE.badge_grand_slam_awarded: { diff --git a/lib/notification/translations.ts b/lib/notification/translations.ts index 987a647..81b93e6 100644 --- a/lib/notification/translations.ts +++ b/lib/notification/translations.ts @@ -89,17 +89,14 @@ export default { en: ({ title }) => `Your article "${title}" has been reported by other users`, }), - write_challenge_applied: i18n<{ name: string }>({ - zh_hant: ({ name }) => - `你已成功報名${name},前往查看更多資訊、結交馬特市文友`, - zh_hans: ({ name }) => - `你已成功报名${name},前往查看更多资讯、结交马特市文友`, - en: ({ name }) => - `You have successfully applied for ${name}. Go to check out more information and make friends in Matters.`, + write_challenge_applied: i18n({ + zh_hant: `你已成功報名七日書,點此閱讀公告查看詳情,了解如何發文、完成七天寫作`, + zh_hans: `你已成功报名七日书,点此阅读公告查看详情,了解如何发文、完成七天写作`, + en: `Your Free Write in 7 days application has been submitted successfully. Click here to read the announcement in detail and learn how to publish an article and complete the activity`, }), badge_grand_slam_awarded: i18n({ zh_hant: '太棒了!恭喜獲得七日書大滿貫,快去看看你的新徽章', zh_hans: '太棒了!恭喜获得七日书大满贯,快去看看你的新徽章', - en: 'Marvelous! Congratulations on winning the Seven-Day Free Writing Grand Slam, go check out your new badge.', + en: 'Marvelous! Congratulations on winning the Seven-Day Free Writing Grand Slam, go check out your new badge', }), } From 04e6e60c6c6ecff941d2fbeb5e9c2350e77569d0 Mon Sep 17 00:00:00 2001 From: j <13580441+gary02@users.noreply.github.com> Date: Fri, 26 Jul 2024 13:50:54 +0800 Subject: [PATCH 3/4] feat(notice): add write_challenge_announcement notice --- lib/__test__/notificationService.test.ts | 35 ++++++++++++++++++++++-- lib/notification/enums.ts | 3 +- lib/notification/index.ts | 35 +++++++++++++++++++----- lib/notification/types.d.ts | 12 ++++++-- 4 files changed, 73 insertions(+), 12 deletions(-) diff --git a/lib/__test__/notificationService.test.ts b/lib/__test__/notificationService.test.ts index 00a75df..27033d8 100644 --- a/lib/__test__/notificationService.test.ts +++ b/lib/__test__/notificationService.test.ts @@ -95,6 +95,7 @@ describe('user notify setting', () => { article_reported: true, write_challenge_applied: true, badge_grand_slam_awarded: true, + write_challenge_announcement: true, } test('user receives notifications', async () => { @@ -207,14 +208,14 @@ describe('trigger notifications', () => { }) }) test('trigger `badge_grand_slam_awarded` notice', async () => { - // no error + // no errors await notificationService.trigger({ event: OFFICIAL_NOTICE_EXTEND_TYPE.badge_grand_slam_awarded, recipientId: '1', }) }) test('trigger `collection_liked` notice', async () => { - // no error + // no errors await notificationService.trigger({ event: NOTICE_TYPE.collection_liked, actorId: '1', @@ -228,4 +229,34 @@ describe('trigger notifications', () => { ], }) }) + test('trigger `write_challenge_announcement` notice', async () => { + const [{ id: campaignId }] = await knex('campaign') + .insert({ + type: 'writing_challenge', + creatorId: '1', + name: 'test', + description: 'test', + state: 'active', + shortHash: 'test-notice-hash', + }) + .returning('id') + await knex('campaign_user').insert({ + campaignId, + userId: '1', + state: 'succeeded', + }) + // no errors + await notificationService.trigger({ + event: OFFICIAL_NOTICE_EXTEND_TYPE.write_challenge_announcement, + data: { + link: 'https://example.com', + campaignId, + messages: { + zh_hant: 'zh-Hant message', + zh_hans: 'zh-Hans message', + en: 'en message', + }, + }, + }) + }) }) diff --git a/lib/notification/enums.ts b/lib/notification/enums.ts index e6b1b46..a73e90a 100644 --- a/lib/notification/enums.ts +++ b/lib/notification/enums.ts @@ -73,8 +73,9 @@ export enum OFFICIAL_NOTICE_EXTEND_TYPE { article_reported = 'article_reported', comment_reported = 'comment_reported', // write challenge related - write_challenge_applied = 'write_challenge_applied', badge_grand_slam_awarded = 'badge_grand_slam_awarded', + write_challenge_applied = 'write_challenge_applied', + write_challenge_announcement = 'write_challenge_announcement', } export enum USER_ACTION { diff --git a/lib/notification/index.ts b/lib/notification/index.ts index 8422827..b9fdbed 100644 --- a/lib/notification/index.ts +++ b/lib/notification/index.ts @@ -13,7 +13,7 @@ import type { NotificationParams, UserNotifySettingDB, } from './types' -import type { User } from '../types' +import type { User, Language } from '../types' import uniqBy from 'lodash.uniqby' import lodash from 'lodash' @@ -30,7 +30,7 @@ import { const { isEqual } = lodash -import trans, { findTranslation } from './translations.js' +import trans from './translations.js' import { loadLatestArticleVersion, mergeDataWith } from './utils.js' export class NotificationService { @@ -98,7 +98,7 @@ export class NotificationService { }) if (!created && !bundled) { - console.info(`Notice ${params.event} to ${params.recipientId} skipped`) + console.info(`Notice ${params.event} to ${recipientId} skipped`) continue } } @@ -535,6 +535,7 @@ export class NotificationService { article_reported: true, write_challenge_applied: true, badge_grand_slam_awarded: true, + write_challenge_announcement: true, } return noticeSettingMap[event] @@ -543,11 +544,12 @@ export class NotificationService { private getNoticeParams = async ( params: NotificationParams ): Promise => { - const recipient = params.recipientId - ? await this.knexRO('user').where({ id: params.recipientId }).first() - : null + const recipient = + 'recipientId' in params + ? await this.knexRO('user').where({ id: params.recipientId }).first() + : null - if (params.recipientId && !recipient) { + if ('recipientId' in params && !recipient) { console.warn(`recipient ${params.recipientId} not found, skipped`) return } @@ -744,6 +746,25 @@ export class NotificationService { }, } } + case OFFICIAL_NOTICE_EXTEND_TYPE.write_challenge_announcement: { + const recipients = await this.knexRO('user') + .select('user.*') + .join('campaign_user', 'user.id', 'campaign_user.user_id') + .where({ + 'campaign_user.campaign_id': params.data.campaignId, + 'campaign_user.state': 'succeeded', + }) + return { + type: NOTICE_TYPE.official_announcement, + recipientIds: recipients.map((r) => r.id), + messages: recipients.map( + ({ language }) => params.data.messages[language as Language] + ), + data: { + link: params.data.link, + }, + } + } default: // for exhaustively handle enum values, // see https://medium.com/typescript-tidbits/exhaustively-handle-enum-values-in-switch-case-at-compile-time-abf6cf1a42b7 diff --git a/lib/notification/types.d.ts b/lib/notification/types.d.ts index 17fd5ab..94df4a5 100644 --- a/lib/notification/types.d.ts +++ b/lib/notification/types.d.ts @@ -1,9 +1,11 @@ +import type { TableName, User, Language } from '../types' + import { NOTICE_TYPE, BUNDLED_NOTICE_TYPE, OFFICIAL_NOTICE_EXTEND_TYPE, + LANGUAGES, } from './enums' -import { TableName, User } from '../types' type BaseNoticeType = keyof typeof NOTICE_TYPE @@ -29,7 +31,6 @@ export type NotificationType = interface NotificationRequiredParams { event: NotificationType - recipientId: string } interface NotificationEntity< @@ -356,6 +357,12 @@ interface NoticeBadgeGrandSlamAwardedParams extends NotificationRequiredParams { recipientId: string } +interface NoticeWriteChallengeAnnouncementParams + extends NotificationRequiredParams { + event: OFFICIAL_NOTICE_EXTEND_TYPE.write_challenge_announcement + data: { link: string; campaignId: string; messages: Record } +} + export type NotificationParams = // User | NoticeUserNewFollowerParams @@ -404,6 +411,7 @@ export type NotificationParams = | NoticeCommentReportedParams | NoticeWriteChallengeAppliedParams | NoticeBadgeGrandSlamAwardedParams + | NoticeWriteChallengeAnnouncementParams export type NoticeUserId = string From d69e0123e6de628a227852ad760769edfd78eb07 Mon Sep 17 00:00:00 2001 From: j <13580441+gary02@users.noreply.github.com> Date: Fri, 26 Jul 2024 13:53:37 +0800 Subject: [PATCH 4/4] chore: bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c6d4616..0cd0048 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lambda-handlers-image", - "version": "0.10.2", + "version": "0.10.3", "private": true, "type": "module", "scripts": {