1
0
Fork 0
mirror of https://github.com/Chocobozzz/PeerTube.git synced 2025-10-03 09:49:20 +02:00

Add typeOneOf filter to list notifications

This commit is contained in:
Chocobozzz 2025-02-20 11:48:04 +01:00
parent 8f35e76928
commit 0bf17d869c
No known key found for this signature in database
GPG key ID: 583A612D890159BE
11 changed files with 200 additions and 98 deletions

View file

@ -4,6 +4,7 @@ export * from './user-create-result.model.js'
export * from './user-create.model.js' export * from './user-create.model.js'
export * from './user-flag.model.js' export * from './user-flag.model.js'
export * from './user-login.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-setting.model.js'
export * from './user-notification.model.js' export * from './user-notification.model.js'
export * from './user-refresh-token.model.js' export * from './user-refresh-token.model.js'

View file

@ -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[]
}

View file

@ -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' import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
export class NotificationsCommand extends AbstractCommand { export class NotificationsCommand extends AbstractCommand {
@ -23,8 +23,9 @@ export class NotificationsCommand extends AbstractCommand {
count?: number count?: number
unread?: boolean unread?: boolean
sort?: string 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' const path = '/api/v1/users/me/notifications'
return this.getRequestBody<ResultList<UserNotification>>({ return this.getRequestBody<ResultList<UserNotification>>({
@ -35,6 +36,7 @@ export class NotificationsCommand extends AbstractCommand {
start, start,
count, count,
sort, sort,
typeOneOf,
unread unread
}, },
implicitToken: true, implicitToken: true,

View file

@ -3,7 +3,7 @@
import { io } from 'socket.io-client' import { io } from 'socket.io-client'
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js' import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
import { wait } from '@peertube/peertube-core-utils' 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 { import {
cleanupTests, cleanupTests,
createSingleServer, createSingleServer,
@ -42,15 +42,15 @@ describe('Test user notifications API validators', function () {
await checkBadSortPagination(server.url, path, server.accessToken) 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({ await makeGetRequest({
url: server.url, url: server.url,
path, path,
query: { query: {
unread: 'toto' typeOneOf: 'toto'
}, },
token: server.accessToken, 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({ await makeGetRequest({
url: server.url, url: server.url,
path, path,
query: {
typeOneOf: [ UserNotificationType.ABUSE_NEW_MESSAGE ]
},
token: server.accessToken, token: server.accessToken,
expectedStatus: HttpStatusCode.OK_200 expectedStatus: HttpStatusCode.OK_200
}) })

View file

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import { expect } from 'chai' 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 { cleanupTests, PeerTubeServer, waitJobs } from '@peertube/peertube-server-commands'
import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js' import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
import { import {
@ -43,6 +43,34 @@ describe('Test notifications API', function () {
expect(data).to.have.lengthOf(2) expect(data).to.have.lengthOf(2)
expect(total).to.equal(10) 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 () { describe('Mark as read', function () {

View file

@ -1,8 +1,8 @@
import 'multer' import { HttpStatusCode, UserNotificationListQuery, UserNotificationSetting } from '@peertube/peertube-models'
import express from 'express'
import { HttpStatusCode, UserNotificationSetting } from '@peertube/peertube-models'
import { getFormattedObjects } from '@server/helpers/utils.js' import { getFormattedObjects } from '@server/helpers/utils.js'
import { UserNotificationModel } from '@server/models/user/user-notification.js' import { UserNotificationModel } from '@server/models/user/user-notification.js'
import express from 'express'
import 'multer'
import { import {
asyncMiddleware, asyncMiddleware,
asyncRetryTransactionMiddleware, asyncRetryTransactionMiddleware,
@ -22,13 +22,15 @@ import { meRouter } from './me.js'
const myNotificationsRouter = express.Router() const myNotificationsRouter = express.Router()
meRouter.put('/me/notification-settings', meRouter.put(
'/me/notification-settings',
authenticate, authenticate,
updateNotificationSettingsValidator, updateNotificationSettingsValidator,
asyncRetryTransactionMiddleware(updateNotificationSettings) asyncRetryTransactionMiddleware(updateNotificationSettings)
) )
myNotificationsRouter.get('/me/notifications', myNotificationsRouter.get(
'/me/notifications',
authenticate, authenticate,
paginationValidator, paginationValidator,
userNotificationsSortValidator, userNotificationsSortValidator,
@ -38,16 +40,14 @@ myNotificationsRouter.get('/me/notifications',
asyncMiddleware(listUserNotifications) asyncMiddleware(listUserNotifications)
) )
myNotificationsRouter.post('/me/notifications/read', myNotificationsRouter.post(
'/me/notifications/read',
authenticate, authenticate,
markAsReadUserNotificationsValidator, markAsReadUserNotificationsValidator,
asyncMiddleware(markAsReadUserNotifications) asyncMiddleware(markAsReadUserNotifications)
) )
myNotificationsRouter.post('/me/notifications/read-all', myNotificationsRouter.post('/me/notifications/read-all', authenticate, asyncMiddleware(markAsReadAllUserNotifications))
authenticate,
asyncMiddleware(markAsReadAllUserNotifications)
)
export { export {
myNotificationsRouter myNotificationsRouter
@ -88,7 +88,16 @@ async function updateNotificationSettings (req: express.Request, res: express.Re
async function listUserNotifications (req: express.Request, res: express.Response) { async function listUserNotifications (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User 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)) return res.json(getFormattedObjects(resultList.data, resultList.total))
} }

View file

@ -58,7 +58,7 @@ const getLogsValidator = [
query('tagsOneOf') query('tagsOneOf')
.optional() .optional()
.customSanitizer(arrayify) .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') query('endDate')
.optional() .optional()
.custom(isDateValid).withMessage('Should have an end date that conforms to ISO 8601'), .custom(isDateValid).withMessage('Should have an end date that conforms to ISO 8601'),

View file

@ -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 express from 'express'
import { body, query } from 'express-validator' import { body, query } from 'express-validator'
import { isNotEmptyIntArray, toBooleanOrNull } from '../../../helpers/custom-validators/misc.js' import { isNotEmptyIntArray, toBooleanOrNull } from '../../../helpers/custom-validators/misc.js'
import { isUserNotificationSettingValid } from '../../../helpers/custom-validators/user-notifications.js' import { isUserNotificationSettingValid } from '../../../helpers/custom-validators/user-notifications.js'
import { areValidationErrors } from '../shared/index.js' import { areValidationErrors } from '../shared/index.js'
const listUserNotificationsValidator = [ export const listUserNotificationsValidator = [
query('unread') query('unread')
.optional() .optional()
.customSanitizer(toBooleanOrNull) .customSanitizer(toBooleanOrNull)
.isBoolean().withMessage('Should have a valid unread boolean'), .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) => { (req: express.Request, res: express.Response, next: express.NextFunction) => {
if (areValidationErrors(req, res)) return if (areValidationErrors(req, res)) return
@ -17,7 +24,7 @@ const listUserNotificationsValidator = [
} }
] ]
const updateNotificationSettingsValidator = [ export const updateNotificationSettingsValidator = [
body('newVideoFromSubscription') body('newVideoFromSubscription')
.custom(isUserNotificationSettingValid), .custom(isUserNotificationSettingValid),
body('newCommentOnMyVideo') body('newCommentOnMyVideo')
@ -50,7 +57,7 @@ const updateNotificationSettingsValidator = [
} }
] ]
const markAsReadUserNotificationsValidator = [ export const markAsReadUserNotificationsValidator = [
body('ids') body('ids')
.optional() .optional()
.custom(isNotEmptyIntArray).withMessage('Should have a valid array of notification ids'), .custom(isNotEmptyIntArray).withMessage('Should have a valid array of notification ids'),
@ -61,11 +68,3 @@ const markAsReadUserNotificationsValidator = [
return next() return next()
} }
] ]
// ---------------------------------------------------------------------------
export {
listUserNotificationsValidator,
updateNotificationSettingsValidator,
markAsReadUserNotificationsValidator
}

View file

@ -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 { AbstractRunQuery, ModelBuilder } from '@server/models/shared/index.js'
import { UserNotificationModelForApi } from '@server/types/models/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' import { getSort } from '../../shared/index.js'
export interface ListNotificationsOptions { export interface ListNotificationsOptions {
userId: number userId: number
unread?: boolean unread?: boolean
typeOneOf?: UserNotificationType_Type[]
sort: string sort: string
offset: number offset: number
limit: number limit: number
@ -61,6 +63,11 @@ export class UserNotificationListQueryBuilder extends AbstractRunQuery {
base += 'AND "UserNotificationModel"."read" IS TRUE ' 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}` return `WHERE ${base}`
} }

View file

@ -112,7 +112,6 @@ import { ActorImageModel } from '../actor/actor-image.js'
] as (ModelIndexesOptions & { where?: WhereOptions })[] ] as (ModelIndexesOptions & { where?: WhereOptions })[]
}) })
export class UserNotificationModel extends SequelizeModel<UserNotificationModel> { export class UserNotificationModel extends SequelizeModel<UserNotificationModel> {
@AllowNull(false) @AllowNull(false)
@Default(null) @Default(null)
@Is('UserNotificationType', value => throwIfNotValid(value, isUserNotificationTypeValid, 'type')) @Is('UserNotificationType', value => throwIfNotValid(value, isUserNotificationTypeValid, 'type'))
@ -275,22 +274,33 @@ export class UserNotificationModel extends SequelizeModel<UserNotificationModel>
}) })
VideoCaption: Awaited<VideoCaptionModel> VideoCaption: Awaited<VideoCaptionModel>
static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) { static listForApi (options: {
const where = { userId } 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 = { const query = {
userId,
unread,
offset: start, offset: start,
limit: count, limit: count,
sort, 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([ return Promise.all([
UserNotificationModel.count({ where }) UserNotificationModel.count({ where: countWhere })
.then(count => count || 0), .then(count => count || 0),
count === 0 count === 0
@ -339,31 +349,31 @@ export class UserNotificationModel extends SequelizeModel<UserNotificationModel>
const queries = [ const queries = [
buildAccountWhereQuery( buildAccountWhereQuery(
`SELECT "userNotification"."id" FROM "userNotification" ` + `SELECT "userNotification"."id" FROM "userNotification" ` +
`INNER JOIN "account" ON "userNotification"."accountId" = "account"."id" ` + `INNER JOIN "account" ON "userNotification"."accountId" = "account"."id" ` +
`INNER JOIN actor ON "actor"."id" = "account"."actorId" ` `INNER JOIN actor ON "actor"."id" = "account"."actorId" `
), ),
// Remove notifications from muted accounts that followed ours // Remove notifications from muted accounts that followed ours
buildAccountWhereQuery( buildAccountWhereQuery(
`SELECT "userNotification"."id" FROM "userNotification" ` + `SELECT "userNotification"."id" FROM "userNotification" ` +
`INNER JOIN "actorFollow" ON "actorFollow".id = "userNotification"."actorFollowId" ` + `INNER JOIN "actorFollow" ON "actorFollow".id = "userNotification"."actorFollowId" ` +
`INNER JOIN actor ON actor.id = "actorFollow"."actorId" ` + `INNER JOIN actor ON actor.id = "actorFollow"."actorId" ` +
`INNER JOIN account ON account."actorId" = actor.id ` `INNER JOIN account ON account."actorId" = actor.id `
), ),
// Remove notifications from muted accounts that commented something // Remove notifications from muted accounts that commented something
buildAccountWhereQuery( buildAccountWhereQuery(
`SELECT "userNotification"."id" FROM "userNotification" ` + `SELECT "userNotification"."id" FROM "userNotification" ` +
`INNER JOIN "actorFollow" ON "actorFollow".id = "userNotification"."actorFollowId" ` + `INNER JOIN "actorFollow" ON "actorFollow".id = "userNotification"."actorFollowId" ` +
`INNER JOIN actor ON actor.id = "actorFollow"."actorId" ` + `INNER JOIN actor ON actor.id = "actorFollow"."actorId" ` +
`INNER JOIN account ON account."actorId" = actor.id ` `INNER JOIN account ON account."actorId" = actor.id `
), ),
buildAccountWhereQuery( buildAccountWhereQuery(
`SELECT "userNotification"."id" FROM "userNotification" ` + `SELECT "userNotification"."id" FROM "userNotification" ` +
`INNER JOIN "videoComment" ON "videoComment".id = "userNotification"."commentId" ` + `INNER JOIN "videoComment" ON "videoComment".id = "userNotification"."commentId" ` +
`INNER JOIN account ON account.id = "videoComment"."accountId" ` + `INNER JOIN account ON account.id = "videoComment"."accountId" ` +
`INNER JOIN actor ON "actor"."id" = "account"."actorId" ` `INNER JOIN actor ON "actor"."id" = "account"."actorId" `
) )
] ]

View file

@ -2317,6 +2317,13 @@ paths:
tags: tags:
- My Notifications - My Notifications
parameters: parameters:
- name: typeOneOf
in: query
description: only list notifications of these types
schema:
type: array
items:
$ref: '#/components/schemas/NotificationType'
- name: unread - name: unread
in: query in: query
description: only list unread notifications description: only list unread notifications
@ -10718,58 +10725,83 @@ components:
- `1` WEB - `1` WEB
- `2` EMAIL - `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: Notification:
properties: properties:
id: id:
$ref: '#/components/schemas/id' $ref: '#/components/schemas/id'
type: type:
type: integer $ref: '#/components/schemas/NotificationType'
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
read: read:
type: boolean type: boolean
video: video: