1
0
Fork 0

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-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'

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'
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<ResultList<UserNotification>>({
@ -35,6 +36,7 @@ export class NotificationsCommand extends AbstractCommand {
start,
count,
sort,
typeOneOf,
unread
},
implicitToken: true,

View file

@ -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
})

View file

@ -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 () {

View file

@ -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))
}

View file

@ -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'),

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 { 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
}

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 { 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}`
}

View file

@ -112,7 +112,6 @@ import { ActorImageModel } from '../actor/actor-image.js'
] as (ModelIndexesOptions & { where?: WhereOptions })[]
})
export class UserNotificationModel extends SequelizeModel<UserNotificationModel> {
@AllowNull(false)
@Default(null)
@Is('UserNotificationType', value => throwIfNotValid(value, isUserNotificationTypeValid, 'type'))
@ -275,22 +274,33 @@ export class UserNotificationModel extends SequelizeModel<UserNotificationModel>
})
VideoCaption: Awaited<VideoCaptionModel>
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<UserNotificationModel>
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" `
)
]

View file

@ -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: