Add typeOneOf filter to list notifications
This commit is contained in:
parent
8f35e76928
commit
0bf17d869c
11 changed files with 200 additions and 98 deletions
|
@ -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'
|
||||
|
|
|
@ -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[]
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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}`
|
||||
}
|
||||
|
||||
|
|
|
@ -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" `
|
||||
)
|
||||
]
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue