From 0bf17d869c03ba1442d0b98c110285a8dad2d589 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 20 Feb 2025 11:48:04 +0100 Subject: [PATCH] Add typeOneOf filter to list notifications --- packages/models/src/users/index.ts | 1 + .../user-notification-list-query.model.ts | 11 ++ .../src/users/notifications-command.ts | 6 +- .../api/check-params/user-notifications.ts | 11 +- .../api/notifications/notifications-api.ts | 30 ++++- .../controllers/api/users/my-notifications.ts | 31 +++-- server/core/middlewares/validators/logs.ts | 2 +- .../validators/users/user-notifications.ts | 21 ++- .../user-notitication-list-query-builder.ts | 11 +- server/core/models/user/user-notification.ts | 48 ++++--- support/doc/api/openapi.yaml | 126 +++++++++++------- 11 files changed, 200 insertions(+), 98 deletions(-) create mode 100644 packages/models/src/users/user-notification-list-query.model.ts diff --git a/packages/models/src/users/index.ts b/packages/models/src/users/index.ts index 6f5218234..5c7be1121 100644 --- a/packages/models/src/users/index.ts +++ b/packages/models/src/users/index.ts @@ -4,6 +4,7 @@ export * from './user-create-result.model.js' export * from './user-create.model.js' export * from './user-flag.model.js' export * from './user-login.model.js' +export * from './user-notification-list-query.model.js' export * from './user-notification-setting.model.js' export * from './user-notification.model.js' export * from './user-refresh-token.model.js' diff --git a/packages/models/src/users/user-notification-list-query.model.ts b/packages/models/src/users/user-notification-list-query.model.ts new file mode 100644 index 000000000..e1b3e4ae0 --- /dev/null +++ b/packages/models/src/users/user-notification-list-query.model.ts @@ -0,0 +1,11 @@ +import { UserNotificationType_Type } from './user-notification.model.js' + +export type UserNotificationListQuery = { + start?: number + count?: number + sort?: string + + unread?: boolean + + typeOneOf?: UserNotificationType_Type[] +} diff --git a/packages/server-commands/src/users/notifications-command.ts b/packages/server-commands/src/users/notifications-command.ts index d90d56900..692a365f0 100644 --- a/packages/server-commands/src/users/notifications-command.ts +++ b/packages/server-commands/src/users/notifications-command.ts @@ -1,4 +1,4 @@ -import { HttpStatusCode, ResultList, UserNotification, UserNotificationSetting } from '@peertube/peertube-models' +import { HttpStatusCode, ResultList, UserNotification, UserNotificationSetting, UserNotificationType_Type } from '@peertube/peertube-models' import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js' export class NotificationsCommand extends AbstractCommand { @@ -23,8 +23,9 @@ export class NotificationsCommand extends AbstractCommand { count?: number unread?: boolean sort?: string + typeOneOf?: UserNotificationType_Type[] }) { - const { start, count, unread, sort = '-createdAt' } = options + const { start, count, unread, typeOneOf, sort = '-createdAt' } = options const path = '/api/v1/users/me/notifications' return this.getRequestBody>({ @@ -35,6 +36,7 @@ export class NotificationsCommand extends AbstractCommand { start, count, sort, + typeOneOf, unread }, implicitToken: true, diff --git a/packages/tests/src/api/check-params/user-notifications.ts b/packages/tests/src/api/check-params/user-notifications.ts index cdbf09b62..8001eeff5 100644 --- a/packages/tests/src/api/check-params/user-notifications.ts +++ b/packages/tests/src/api/check-params/user-notifications.ts @@ -3,7 +3,7 @@ import { io } from 'socket.io-client' import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' import { wait } from '@peertube/peertube-core-utils' -import { HttpStatusCode, UserNotificationSetting, UserNotificationSettingValue } from '@peertube/peertube-models' +import { HttpStatusCode, UserNotificationSetting, UserNotificationSettingValue, UserNotificationType } from '@peertube/peertube-models' import { cleanupTests, createSingleServer, @@ -42,15 +42,15 @@ describe('Test user notifications API validators', function () { await checkBadSortPagination(server.url, path, server.accessToken) }) - it('Should fail with an incorrect unread parameter', async function () { + it('Should fail with an incorrect typeOneOf parameter', async function () { await makeGetRequest({ url: server.url, path, query: { - unread: 'toto' + typeOneOf: 'toto' }, token: server.accessToken, - expectedStatus: HttpStatusCode.OK_200 + expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) }) @@ -66,6 +66,9 @@ describe('Test user notifications API validators', function () { await makeGetRequest({ url: server.url, path, + query: { + typeOneOf: [ UserNotificationType.ABUSE_NEW_MESSAGE ] + }, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 }) diff --git a/packages/tests/src/api/notifications/notifications-api.ts b/packages/tests/src/api/notifications/notifications-api.ts index f99b81e94..7c880bfed 100644 --- a/packages/tests/src/api/notifications/notifications-api.ts +++ b/packages/tests/src/api/notifications/notifications-api.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ import { expect } from 'chai' -import { UserNotification, UserNotificationSettingValue } from '@peertube/peertube-models' +import { UserNotification, UserNotificationSettingValue, UserNotificationType } from '@peertube/peertube-models' import { cleanupTests, PeerTubeServer, waitJobs } from '@peertube/peertube-server-commands' import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js' import { @@ -43,6 +43,34 @@ describe('Test notifications API', function () { expect(data).to.have.lengthOf(2) expect(total).to.equal(10) }) + + it('Should correctly filter notifications on its type', async function () { + { + const { data, total } = await server.notifications.list({ + token: userToken, + start: 0, + count: 2, + typeOneOf: [ UserNotificationType.ABUSE_NEW_MESSAGE ] + }) + + expect(data).to.have.lengthOf(0) + expect(total).to.equal(0) + } + + { + const { data, total } = await server.notifications.list({ + token: userToken, + start: 0, + count: 2, + typeOneOf: [ UserNotificationType.ABUSE_NEW_MESSAGE, UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION ] + }) + + console.log(data) + + expect(data).to.have.lengthOf(2) + expect(total).to.equal(10) + } + }) }) describe('Mark as read', function () { diff --git a/server/core/controllers/api/users/my-notifications.ts b/server/core/controllers/api/users/my-notifications.ts index cd37eeb00..63049d3ef 100644 --- a/server/core/controllers/api/users/my-notifications.ts +++ b/server/core/controllers/api/users/my-notifications.ts @@ -1,8 +1,8 @@ -import 'multer' -import express from 'express' -import { HttpStatusCode, UserNotificationSetting } from '@peertube/peertube-models' +import { HttpStatusCode, UserNotificationListQuery, UserNotificationSetting } from '@peertube/peertube-models' import { getFormattedObjects } from '@server/helpers/utils.js' import { UserNotificationModel } from '@server/models/user/user-notification.js' +import express from 'express' +import 'multer' import { asyncMiddleware, asyncRetryTransactionMiddleware, @@ -22,13 +22,15 @@ import { meRouter } from './me.js' const myNotificationsRouter = express.Router() -meRouter.put('/me/notification-settings', +meRouter.put( + '/me/notification-settings', authenticate, updateNotificationSettingsValidator, asyncRetryTransactionMiddleware(updateNotificationSettings) ) -myNotificationsRouter.get('/me/notifications', +myNotificationsRouter.get( + '/me/notifications', authenticate, paginationValidator, userNotificationsSortValidator, @@ -38,16 +40,14 @@ myNotificationsRouter.get('/me/notifications', asyncMiddleware(listUserNotifications) ) -myNotificationsRouter.post('/me/notifications/read', +myNotificationsRouter.post( + '/me/notifications/read', authenticate, markAsReadUserNotificationsValidator, asyncMiddleware(markAsReadUserNotifications) ) -myNotificationsRouter.post('/me/notifications/read-all', - authenticate, - asyncMiddleware(markAsReadAllUserNotifications) -) +myNotificationsRouter.post('/me/notifications/read-all', authenticate, asyncMiddleware(markAsReadAllUserNotifications)) export { myNotificationsRouter @@ -88,7 +88,16 @@ async function updateNotificationSettings (req: express.Request, res: express.Re async function listUserNotifications (req: express.Request, res: express.Response) { const user = res.locals.oauth.token.User - const resultList = await UserNotificationModel.listForApi(user.id, req.query.start, req.query.count, req.query.sort, req.query.unread) + const query = req.query as UserNotificationListQuery + + const resultList = await UserNotificationModel.listForApi({ + userId: user.id, + start: query.start, + count: query.count, + sort: query.sort, + unread: query.unread, + typeOneOf: query.typeOneOf + }) return res.json(getFormattedObjects(resultList.data, resultList.total)) } diff --git a/server/core/middlewares/validators/logs.ts b/server/core/middlewares/validators/logs.ts index e93d8a618..c49baec3f 100644 --- a/server/core/middlewares/validators/logs.ts +++ b/server/core/middlewares/validators/logs.ts @@ -58,7 +58,7 @@ const getLogsValidator = [ query('tagsOneOf') .optional() .customSanitizer(arrayify) - .custom(isStringArray).withMessage('Should have a valid one of tags array'), + .custom(isStringArray).withMessage('Should have a valid tags one of array'), query('endDate') .optional() .custom(isDateValid).withMessage('Should have an end date that conforms to ISO 8601'), diff --git a/server/core/middlewares/validators/users/user-notifications.ts b/server/core/middlewares/validators/users/user-notifications.ts index e254e7cfb..948d4606e 100644 --- a/server/core/middlewares/validators/users/user-notifications.ts +++ b/server/core/middlewares/validators/users/user-notifications.ts @@ -1,15 +1,22 @@ +import { arrayify } from '@peertube/peertube-core-utils' +import { isNumberArray } from '@server/helpers/custom-validators/search.js' import express from 'express' import { body, query } from 'express-validator' import { isNotEmptyIntArray, toBooleanOrNull } from '../../../helpers/custom-validators/misc.js' import { isUserNotificationSettingValid } from '../../../helpers/custom-validators/user-notifications.js' import { areValidationErrors } from '../shared/index.js' -const listUserNotificationsValidator = [ +export const listUserNotificationsValidator = [ query('unread') .optional() .customSanitizer(toBooleanOrNull) .isBoolean().withMessage('Should have a valid unread boolean'), + query('typeOneOf') + .optional() + .customSanitizer(arrayify) + .custom(isNumberArray).withMessage('Should have a valid typeOneOf array'), + (req: express.Request, res: express.Response, next: express.NextFunction) => { if (areValidationErrors(req, res)) return @@ -17,7 +24,7 @@ const listUserNotificationsValidator = [ } ] -const updateNotificationSettingsValidator = [ +export const updateNotificationSettingsValidator = [ body('newVideoFromSubscription') .custom(isUserNotificationSettingValid), body('newCommentOnMyVideo') @@ -50,7 +57,7 @@ const updateNotificationSettingsValidator = [ } ] -const markAsReadUserNotificationsValidator = [ +export const markAsReadUserNotificationsValidator = [ body('ids') .optional() .custom(isNotEmptyIntArray).withMessage('Should have a valid array of notification ids'), @@ -61,11 +68,3 @@ const markAsReadUserNotificationsValidator = [ return next() } ] - -// --------------------------------------------------------------------------- - -export { - listUserNotificationsValidator, - updateNotificationSettingsValidator, - markAsReadUserNotificationsValidator -} diff --git a/server/core/models/user/sql/user-notitication-list-query-builder.ts b/server/core/models/user/sql/user-notitication-list-query-builder.ts index 02728d9dc..5f67b1854 100644 --- a/server/core/models/user/sql/user-notitication-list-query-builder.ts +++ b/server/core/models/user/sql/user-notitication-list-query-builder.ts @@ -1,12 +1,14 @@ -import { Sequelize } from 'sequelize' +import { ActorImageType, UserNotificationType_Type } from '@peertube/peertube-models' import { AbstractRunQuery, ModelBuilder } from '@server/models/shared/index.js' import { UserNotificationModelForApi } from '@server/types/models/index.js' -import { ActorImageType } from '@peertube/peertube-models' +import { Sequelize } from 'sequelize' import { getSort } from '../../shared/index.js' export interface ListNotificationsOptions { userId: number unread?: boolean + typeOneOf?: UserNotificationType_Type[] + sort: string offset: number limit: number @@ -61,6 +63,11 @@ export class UserNotificationListQueryBuilder extends AbstractRunQuery { base += 'AND "UserNotificationModel"."read" IS TRUE ' } + if (this.options.typeOneOf) { + base += 'AND "UserNotificationModel"."type" IN (:typeOneOf) ' + this.replacements.typeOneOf = this.options.typeOneOf + } + return `WHERE ${base}` } diff --git a/server/core/models/user/user-notification.ts b/server/core/models/user/user-notification.ts index 52d72a5ba..74110d4fb 100644 --- a/server/core/models/user/user-notification.ts +++ b/server/core/models/user/user-notification.ts @@ -112,7 +112,6 @@ import { ActorImageModel } from '../actor/actor-image.js' ] as (ModelIndexesOptions & { where?: WhereOptions })[] }) export class UserNotificationModel extends SequelizeModel { - @AllowNull(false) @Default(null) @Is('UserNotificationType', value => throwIfNotValid(value, isUserNotificationTypeValid, 'type')) @@ -275,22 +274,33 @@ export class UserNotificationModel extends SequelizeModel }) VideoCaption: Awaited - static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) { - const where = { userId } + static listForApi (options: { + userId: number + start: number + count: number + sort: string + unread?: boolean + typeOneOf?: UserNotificationType_Type[] + }) { + const { userId, start, count, sort, unread, typeOneOf } = options + + const countWhere = { userId } const query = { - userId, - unread, offset: start, limit: count, sort, - where + + userId, + unread, + typeOneOf } - if (unread !== undefined) query.where['read'] = !unread + if (unread !== undefined) countWhere['read'] = !unread + if (typeOneOf !== undefined) countWhere['type'] = { [Op.in]: typeOneOf } return Promise.all([ - UserNotificationModel.count({ where }) + UserNotificationModel.count({ where: countWhere }) .then(count => count || 0), count === 0 @@ -339,31 +349,31 @@ export class UserNotificationModel extends SequelizeModel const queries = [ buildAccountWhereQuery( `SELECT "userNotification"."id" FROM "userNotification" ` + - `INNER JOIN "account" ON "userNotification"."accountId" = "account"."id" ` + - `INNER JOIN actor ON "actor"."id" = "account"."actorId" ` + `INNER JOIN "account" ON "userNotification"."accountId" = "account"."id" ` + + `INNER JOIN actor ON "actor"."id" = "account"."actorId" ` ), // Remove notifications from muted accounts that followed ours buildAccountWhereQuery( `SELECT "userNotification"."id" FROM "userNotification" ` + - `INNER JOIN "actorFollow" ON "actorFollow".id = "userNotification"."actorFollowId" ` + - `INNER JOIN actor ON actor.id = "actorFollow"."actorId" ` + - `INNER JOIN account ON account."actorId" = actor.id ` + `INNER JOIN "actorFollow" ON "actorFollow".id = "userNotification"."actorFollowId" ` + + `INNER JOIN actor ON actor.id = "actorFollow"."actorId" ` + + `INNER JOIN account ON account."actorId" = actor.id ` ), // Remove notifications from muted accounts that commented something buildAccountWhereQuery( `SELECT "userNotification"."id" FROM "userNotification" ` + - `INNER JOIN "actorFollow" ON "actorFollow".id = "userNotification"."actorFollowId" ` + - `INNER JOIN actor ON actor.id = "actorFollow"."actorId" ` + - `INNER JOIN account ON account."actorId" = actor.id ` + `INNER JOIN "actorFollow" ON "actorFollow".id = "userNotification"."actorFollowId" ` + + `INNER JOIN actor ON actor.id = "actorFollow"."actorId" ` + + `INNER JOIN account ON account."actorId" = actor.id ` ), buildAccountWhereQuery( `SELECT "userNotification"."id" FROM "userNotification" ` + - `INNER JOIN "videoComment" ON "videoComment".id = "userNotification"."commentId" ` + - `INNER JOIN account ON account.id = "videoComment"."accountId" ` + - `INNER JOIN actor ON "actor"."id" = "account"."actorId" ` + `INNER JOIN "videoComment" ON "videoComment".id = "userNotification"."commentId" ` + + `INNER JOIN account ON account.id = "videoComment"."accountId" ` + + `INNER JOIN actor ON "actor"."id" = "account"."actorId" ` ) ] diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml index 19a119686..65a365d97 100644 --- a/support/doc/api/openapi.yaml +++ b/support/doc/api/openapi.yaml @@ -2317,6 +2317,13 @@ paths: tags: - My Notifications parameters: + - name: typeOneOf + in: query + description: only list notifications of these types + schema: + type: array + items: + $ref: '#/components/schemas/NotificationType' - name: unread in: query description: only list unread notifications @@ -10718,58 +10725,83 @@ components: - `1` WEB - `2` EMAIL + NotificationType: + type: integer + enum: + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + - 7 + - 8 + - 9 + - 10 + - 11 + - 12 + - 13 + - 14 + - 15 + - 16 + - 17 + - 18 + - 19 + - 20 + - 21 + - 22 + description: > + Notification type. One of the following values: + + - `1` NEW_VIDEO_FROM_SUBSCRIPTION + + - `2` NEW_COMMENT_ON_MY_VIDEO + + - `3` NEW_ABUSE_FOR_MODERATORS + + - `4` BLACKLIST_ON_MY_VIDEO + + - `5` UNBLACKLIST_ON_MY_VIDEO + + - `6` MY_VIDEO_PUBLISHED + + - `7` MY_VIDEO_IMPORT_SUCCESS + + - `8` MY_VIDEO_IMPORT_ERROR + + - `9` NEW_USER_REGISTRATION + + - `10` NEW_FOLLOW + + - `11` COMMENT_MENTION + + - `12` VIDEO_AUTO_BLACKLIST_FOR_MODERATORS + + - `13` NEW_INSTANCE_FOLLOWER + + - `14` AUTO_INSTANCE_FOLLOWING + + - `15` ABUSE_STATE_CHANGE + + - `16` ABUSE_NEW_MESSAGE + + - `17` NEW_PLUGIN_VERSION + + - `18` NEW_PEERTUBE_VERSION + + - `19` MY_VIDEO_STUDIO_EDITION_FINISHED + + - `20` NEW_USER_REGISTRATION_REQUEST + + - `21` NEW_LIVE_FROM_SUBSCRIPTION + + - `22` MY_VIDEO_TRANSCRIPTION_GENERATED Notification: properties: id: $ref: '#/components/schemas/id' type: - type: integer - description: > - Notification type, following the `UserNotificationType` enum: - - - `1` NEW_VIDEO_FROM_SUBSCRIPTION - - - `2` NEW_COMMENT_ON_MY_VIDEO - - - `3` NEW_ABUSE_FOR_MODERATORS - - - `4` BLACKLIST_ON_MY_VIDEO - - - `5` UNBLACKLIST_ON_MY_VIDEO - - - `6` MY_VIDEO_PUBLISHED - - - `7` MY_VIDEO_IMPORT_SUCCESS - - - `8` MY_VIDEO_IMPORT_ERROR - - - `9` NEW_USER_REGISTRATION - - - `10` NEW_FOLLOW - - - `11` COMMENT_MENTION - - - `12` VIDEO_AUTO_BLACKLIST_FOR_MODERATORS - - - `13` NEW_INSTANCE_FOLLOWER - - - `14` AUTO_INSTANCE_FOLLOWING - - - `15` ABUSE_STATE_CHANGE - - - `16` ABUSE_NEW_MESSAGE - - - `17` NEW_PLUGIN_VERSION - - - `18` NEW_PEERTUBE_VERSION - - - `19` MY_VIDEO_STUDIO_EDITION_FINISHED - - - `20` NEW_USER_REGISTRATION_REQUEST - - - `21` NEW_LIVE_FROM_SUBSCRIPTION - - - `22` MY_VIDEO_TRANSCRIPTION_GENERATED + $ref: '#/components/schemas/NotificationType' read: type: boolean video: