はじまりの大地
このコミットが含まれているのは:
@@ -0,0 +1,284 @@
|
||||
import { Sequelize } from 'sequelize'
|
||||
import { AbstractRunQuery, ModelBuilder } from '@server/models/shared/index.js'
|
||||
import { UserNotificationModelForApi } from '@server/types/models/index.js'
|
||||
import { ActorImageType } from '@peertube/peertube-models'
|
||||
import { getSort } from '../../shared/index.js'
|
||||
|
||||
export interface ListNotificationsOptions {
|
||||
userId: number
|
||||
unread?: boolean
|
||||
sort: string
|
||||
offset: number
|
||||
limit: number
|
||||
}
|
||||
|
||||
export class UserNotificationListQueryBuilder extends AbstractRunQuery {
|
||||
private innerQuery: string
|
||||
|
||||
constructor (
|
||||
protected readonly sequelize: Sequelize,
|
||||
private readonly options: ListNotificationsOptions
|
||||
) {
|
||||
super(sequelize)
|
||||
}
|
||||
|
||||
async listNotifications () {
|
||||
this.buildQuery()
|
||||
|
||||
const results = await this.runQuery({ nest: true })
|
||||
const modelBuilder = new ModelBuilder<UserNotificationModelForApi>(this.sequelize)
|
||||
|
||||
return modelBuilder.createModels(results, 'UserNotification')
|
||||
}
|
||||
|
||||
private buildInnerQuery () {
|
||||
this.innerQuery = `SELECT * FROM "userNotification" AS "UserNotificationModel" ` +
|
||||
`${this.getWhere()} ` +
|
||||
`${this.getOrder()} ` +
|
||||
`LIMIT :limit OFFSET :offset `
|
||||
|
||||
this.replacements.limit = this.options.limit
|
||||
this.replacements.offset = this.options.offset
|
||||
}
|
||||
|
||||
private buildQuery () {
|
||||
this.buildInnerQuery()
|
||||
|
||||
this.query = `
|
||||
${this.getSelect()}
|
||||
FROM (${this.innerQuery}) "UserNotificationModel"
|
||||
${this.getJoins()}
|
||||
${this.getOrder()}`
|
||||
}
|
||||
|
||||
private getWhere () {
|
||||
let base = '"UserNotificationModel"."userId" = :userId '
|
||||
this.replacements.userId = this.options.userId
|
||||
|
||||
if (this.options.unread === true) {
|
||||
base += 'AND "UserNotificationModel"."read" IS FALSE '
|
||||
} else if (this.options.unread === false) {
|
||||
base += 'AND "UserNotificationModel"."read" IS TRUE '
|
||||
}
|
||||
|
||||
return `WHERE ${base}`
|
||||
}
|
||||
|
||||
private getOrder () {
|
||||
const orders = getSort(this.options.sort)
|
||||
|
||||
return 'ORDER BY ' + orders.map(o => `"UserNotificationModel"."${o[0]}" ${o[1]}`).join(', ')
|
||||
}
|
||||
|
||||
private getSelect () {
|
||||
return `SELECT
|
||||
"UserNotificationModel"."id",
|
||||
"UserNotificationModel"."type",
|
||||
"UserNotificationModel"."read",
|
||||
"UserNotificationModel"."createdAt",
|
||||
"UserNotificationModel"."updatedAt",
|
||||
"Video"."id" AS "Video.id",
|
||||
"Video"."uuid" AS "Video.uuid",
|
||||
"Video"."name" AS "Video.name",
|
||||
"Video->VideoChannel"."id" AS "Video.VideoChannel.id",
|
||||
"Video->VideoChannel"."name" AS "Video.VideoChannel.name",
|
||||
"Video->VideoChannel->Actor"."id" AS "Video.VideoChannel.Actor.id",
|
||||
"Video->VideoChannel->Actor"."preferredUsername" AS "Video.VideoChannel.Actor.preferredUsername",
|
||||
"Video->VideoChannel->Actor->Avatars"."id" AS "Video.VideoChannel.Actor.Avatars.id",
|
||||
"Video->VideoChannel->Actor->Avatars"."width" AS "Video.VideoChannel.Actor.Avatars.width",
|
||||
"Video->VideoChannel->Actor->Avatars"."type" AS "Video.VideoChannel.Actor.Avatars.type",
|
||||
"Video->VideoChannel->Actor->Avatars"."filename" AS "Video.VideoChannel.Actor.Avatars.filename",
|
||||
"Video->VideoChannel->Actor->Server"."id" AS "Video.VideoChannel.Actor.Server.id",
|
||||
"Video->VideoChannel->Actor->Server"."host" AS "Video.VideoChannel.Actor.Server.host",
|
||||
"VideoComment"."id" AS "VideoComment.id",
|
||||
"VideoComment"."originCommentId" AS "VideoComment.originCommentId",
|
||||
"VideoComment"."heldForReview" AS "VideoComment.heldForReview",
|
||||
"VideoComment->Account"."id" AS "VideoComment.Account.id",
|
||||
"VideoComment->Account"."name" AS "VideoComment.Account.name",
|
||||
"VideoComment->Account->Actor"."id" AS "VideoComment.Account.Actor.id",
|
||||
"VideoComment->Account->Actor"."preferredUsername" AS "VideoComment.Account.Actor.preferredUsername",
|
||||
"VideoComment->Account->Actor->Avatars"."id" AS "VideoComment.Account.Actor.Avatars.id",
|
||||
"VideoComment->Account->Actor->Avatars"."width" AS "VideoComment.Account.Actor.Avatars.width",
|
||||
"VideoComment->Account->Actor->Avatars"."type" AS "VideoComment.Account.Actor.Avatars.type",
|
||||
"VideoComment->Account->Actor->Avatars"."filename" AS "VideoComment.Account.Actor.Avatars.filename",
|
||||
"VideoComment->Account->Actor->Server"."id" AS "VideoComment.Account.Actor.Server.id",
|
||||
"VideoComment->Account->Actor->Server"."host" AS "VideoComment.Account.Actor.Server.host",
|
||||
"VideoComment->Video"."id" AS "VideoComment.Video.id",
|
||||
"VideoComment->Video"."uuid" AS "VideoComment.Video.uuid",
|
||||
"VideoComment->Video"."name" AS "VideoComment.Video.name",
|
||||
"Abuse"."id" AS "Abuse.id",
|
||||
"Abuse"."state" AS "Abuse.state",
|
||||
"Abuse->VideoAbuse"."id" AS "Abuse.VideoAbuse.id",
|
||||
"Abuse->VideoAbuse->Video"."id" AS "Abuse.VideoAbuse.Video.id",
|
||||
"Abuse->VideoAbuse->Video"."uuid" AS "Abuse.VideoAbuse.Video.uuid",
|
||||
"Abuse->VideoAbuse->Video"."name" AS "Abuse.VideoAbuse.Video.name",
|
||||
"Abuse->VideoCommentAbuse"."id" AS "Abuse.VideoCommentAbuse.id",
|
||||
"Abuse->VideoCommentAbuse->VideoComment"."id" AS "Abuse.VideoCommentAbuse.VideoComment.id",
|
||||
"Abuse->VideoCommentAbuse->VideoComment"."originCommentId" AS "Abuse.VideoCommentAbuse.VideoComment.originCommentId",
|
||||
"Abuse->VideoCommentAbuse->VideoComment->Video"."id" AS "Abuse.VideoCommentAbuse.VideoComment.Video.id",
|
||||
"Abuse->VideoCommentAbuse->VideoComment->Video"."name" AS "Abuse.VideoCommentAbuse.VideoComment.Video.name",
|
||||
"Abuse->VideoCommentAbuse->VideoComment->Video"."uuid" AS "Abuse.VideoCommentAbuse.VideoComment.Video.uuid",
|
||||
"Abuse->FlaggedAccount"."id" AS "Abuse.FlaggedAccount.id",
|
||||
"Abuse->FlaggedAccount"."name" AS "Abuse.FlaggedAccount.name",
|
||||
"Abuse->FlaggedAccount"."description" AS "Abuse.FlaggedAccount.description",
|
||||
"Abuse->FlaggedAccount"."actorId" AS "Abuse.FlaggedAccount.actorId",
|
||||
"Abuse->FlaggedAccount"."userId" AS "Abuse.FlaggedAccount.userId",
|
||||
"Abuse->FlaggedAccount"."applicationId" AS "Abuse.FlaggedAccount.applicationId",
|
||||
"Abuse->FlaggedAccount"."createdAt" AS "Abuse.FlaggedAccount.createdAt",
|
||||
"Abuse->FlaggedAccount"."updatedAt" AS "Abuse.FlaggedAccount.updatedAt",
|
||||
"Abuse->FlaggedAccount->Actor"."id" AS "Abuse.FlaggedAccount.Actor.id",
|
||||
"Abuse->FlaggedAccount->Actor"."preferredUsername" AS "Abuse.FlaggedAccount.Actor.preferredUsername",
|
||||
"Abuse->FlaggedAccount->Actor->Avatars"."id" AS "Abuse.FlaggedAccount.Actor.Avatars.id",
|
||||
"Abuse->FlaggedAccount->Actor->Avatars"."width" AS "Abuse.FlaggedAccount.Actor.Avatars.width",
|
||||
"Abuse->FlaggedAccount->Actor->Avatars"."type" AS "Abuse.FlaggedAccount.Actor.Avatars.type",
|
||||
"Abuse->FlaggedAccount->Actor->Avatars"."filename" AS "Abuse.FlaggedAccount.Actor.Avatars.filename",
|
||||
"Abuse->FlaggedAccount->Actor->Server"."id" AS "Abuse.FlaggedAccount.Actor.Server.id",
|
||||
"Abuse->FlaggedAccount->Actor->Server"."host" AS "Abuse.FlaggedAccount.Actor.Server.host",
|
||||
"VideoBlacklist"."id" AS "VideoBlacklist.id",
|
||||
"VideoBlacklist->Video"."id" AS "VideoBlacklist.Video.id",
|
||||
"VideoBlacklist->Video"."uuid" AS "VideoBlacklist.Video.uuid",
|
||||
"VideoBlacklist->Video"."name" AS "VideoBlacklist.Video.name",
|
||||
"VideoImport"."id" AS "VideoImport.id",
|
||||
"VideoImport"."magnetUri" AS "VideoImport.magnetUri",
|
||||
"VideoImport"."targetUrl" AS "VideoImport.targetUrl",
|
||||
"VideoImport"."torrentName" AS "VideoImport.torrentName",
|
||||
"VideoImport->Video"."id" AS "VideoImport.Video.id",
|
||||
"VideoImport->Video"."uuid" AS "VideoImport.Video.uuid",
|
||||
"VideoImport->Video"."name" AS "VideoImport.Video.name",
|
||||
"Plugin"."id" AS "Plugin.id",
|
||||
"Plugin"."name" AS "Plugin.name",
|
||||
"Plugin"."type" AS "Plugin.type",
|
||||
"Plugin"."latestVersion" AS "Plugin.latestVersion",
|
||||
"Application"."id" AS "Application.id",
|
||||
"Application"."latestPeerTubeVersion" AS "Application.latestPeerTubeVersion",
|
||||
"ActorFollow"."id" AS "ActorFollow.id",
|
||||
"ActorFollow"."state" AS "ActorFollow.state",
|
||||
"ActorFollow->ActorFollower"."id" AS "ActorFollow.ActorFollower.id",
|
||||
"ActorFollow->ActorFollower"."preferredUsername" AS "ActorFollow.ActorFollower.preferredUsername",
|
||||
"ActorFollow->ActorFollower->Account"."id" AS "ActorFollow.ActorFollower.Account.id",
|
||||
"ActorFollow->ActorFollower->Account"."name" AS "ActorFollow.ActorFollower.Account.name",
|
||||
"ActorFollow->ActorFollower->Avatars"."id" AS "ActorFollow.ActorFollower.Avatars.id",
|
||||
"ActorFollow->ActorFollower->Avatars"."width" AS "ActorFollow.ActorFollower.Avatars.width",
|
||||
"ActorFollow->ActorFollower->Avatars"."type" AS "ActorFollow.ActorFollower.Avatars.type",
|
||||
"ActorFollow->ActorFollower->Avatars"."filename" AS "ActorFollow.ActorFollower.Avatars.filename",
|
||||
"ActorFollow->ActorFollower->Server"."id" AS "ActorFollow.ActorFollower.Server.id",
|
||||
"ActorFollow->ActorFollower->Server"."host" AS "ActorFollow.ActorFollower.Server.host",
|
||||
"ActorFollow->ActorFollowing"."id" AS "ActorFollow.ActorFollowing.id",
|
||||
"ActorFollow->ActorFollowing"."preferredUsername" AS "ActorFollow.ActorFollowing.preferredUsername",
|
||||
"ActorFollow->ActorFollowing"."type" AS "ActorFollow.ActorFollowing.type",
|
||||
"ActorFollow->ActorFollowing->VideoChannel"."id" AS "ActorFollow.ActorFollowing.VideoChannel.id",
|
||||
"ActorFollow->ActorFollowing->VideoChannel"."name" AS "ActorFollow.ActorFollowing.VideoChannel.name",
|
||||
"ActorFollow->ActorFollowing->Account"."id" AS "ActorFollow.ActorFollowing.Account.id",
|
||||
"ActorFollow->ActorFollowing->Account"."name" AS "ActorFollow.ActorFollowing.Account.name",
|
||||
"ActorFollow->ActorFollowing->Server"."id" AS "ActorFollow.ActorFollowing.Server.id",
|
||||
"ActorFollow->ActorFollowing->Server"."host" AS "ActorFollow.ActorFollowing.Server.host",
|
||||
"Account"."id" AS "Account.id",
|
||||
"Account"."name" AS "Account.name",
|
||||
"Account->Actor"."id" AS "Account.Actor.id",
|
||||
"Account->Actor"."preferredUsername" AS "Account.Actor.preferredUsername",
|
||||
"Account->Actor->Avatars"."id" AS "Account.Actor.Avatars.id",
|
||||
"Account->Actor->Avatars"."width" AS "Account.Actor.Avatars.width",
|
||||
"Account->Actor->Avatars"."type" AS "Account.Actor.Avatars.type",
|
||||
"Account->Actor->Avatars"."filename" AS "Account.Actor.Avatars.filename",
|
||||
"Account->Actor->Server"."id" AS "Account.Actor.Server.id",
|
||||
"Account->Actor->Server"."host" AS "Account.Actor.Server.host",
|
||||
"UserRegistration"."id" AS "UserRegistration.id",
|
||||
"UserRegistration"."username" AS "UserRegistration.username",
|
||||
"VideoCaption"."id" AS "VideoCaption.id",
|
||||
"VideoCaption"."language" AS "VideoCaption.language",
|
||||
"VideoCaption->Video"."id" AS "VideoCaption.Video.id",
|
||||
"VideoCaption->Video"."uuid" AS "VideoCaption.Video.uuid",
|
||||
"VideoCaption->Video"."name" AS "VideoCaption.Video.name"`
|
||||
}
|
||||
|
||||
private getJoins () {
|
||||
return `
|
||||
LEFT JOIN (
|
||||
"video" AS "Video"
|
||||
INNER JOIN "videoChannel" AS "Video->VideoChannel" ON "Video"."channelId" = "Video->VideoChannel"."id"
|
||||
INNER JOIN "actor" AS "Video->VideoChannel->Actor" ON "Video->VideoChannel"."actorId" = "Video->VideoChannel->Actor"."id"
|
||||
LEFT JOIN "actorImage" AS "Video->VideoChannel->Actor->Avatars"
|
||||
ON "Video->VideoChannel->Actor"."id" = "Video->VideoChannel->Actor->Avatars"."actorId"
|
||||
AND "Video->VideoChannel->Actor->Avatars"."type" = ${ActorImageType.AVATAR}
|
||||
LEFT JOIN "server" AS "Video->VideoChannel->Actor->Server"
|
||||
ON "Video->VideoChannel->Actor"."serverId" = "Video->VideoChannel->Actor->Server"."id"
|
||||
) ON "UserNotificationModel"."videoId" = "Video"."id"
|
||||
|
||||
LEFT JOIN (
|
||||
"videoComment" AS "VideoComment"
|
||||
INNER JOIN "account" AS "VideoComment->Account" ON "VideoComment"."accountId" = "VideoComment->Account"."id"
|
||||
INNER JOIN "actor" AS "VideoComment->Account->Actor" ON "VideoComment->Account"."actorId" = "VideoComment->Account->Actor"."id"
|
||||
LEFT JOIN "actorImage" AS "VideoComment->Account->Actor->Avatars"
|
||||
ON "VideoComment->Account->Actor"."id" = "VideoComment->Account->Actor->Avatars"."actorId"
|
||||
AND "VideoComment->Account->Actor->Avatars"."type" = ${ActorImageType.AVATAR}
|
||||
LEFT JOIN "server" AS "VideoComment->Account->Actor->Server"
|
||||
ON "VideoComment->Account->Actor"."serverId" = "VideoComment->Account->Actor->Server"."id"
|
||||
INNER JOIN "video" AS "VideoComment->Video" ON "VideoComment"."videoId" = "VideoComment->Video"."id"
|
||||
) ON "UserNotificationModel"."commentId" = "VideoComment"."id"
|
||||
|
||||
LEFT JOIN "abuse" AS "Abuse" ON "UserNotificationModel"."abuseId" = "Abuse"."id"
|
||||
LEFT JOIN "videoAbuse" AS "Abuse->VideoAbuse" ON "Abuse"."id" = "Abuse->VideoAbuse"."abuseId"
|
||||
LEFT JOIN "video" AS "Abuse->VideoAbuse->Video" ON "Abuse->VideoAbuse"."videoId" = "Abuse->VideoAbuse->Video"."id"
|
||||
LEFT JOIN "commentAbuse" AS "Abuse->VideoCommentAbuse" ON "Abuse"."id" = "Abuse->VideoCommentAbuse"."abuseId"
|
||||
LEFT JOIN "videoComment" AS "Abuse->VideoCommentAbuse->VideoComment"
|
||||
ON "Abuse->VideoCommentAbuse"."videoCommentId" = "Abuse->VideoCommentAbuse->VideoComment"."id"
|
||||
LEFT JOIN "video" AS "Abuse->VideoCommentAbuse->VideoComment->Video"
|
||||
ON "Abuse->VideoCommentAbuse->VideoComment"."videoId" = "Abuse->VideoCommentAbuse->VideoComment->Video"."id"
|
||||
LEFT JOIN (
|
||||
"account" AS "Abuse->FlaggedAccount"
|
||||
INNER JOIN "actor" AS "Abuse->FlaggedAccount->Actor" ON "Abuse->FlaggedAccount"."actorId" = "Abuse->FlaggedAccount->Actor"."id"
|
||||
LEFT JOIN "actorImage" AS "Abuse->FlaggedAccount->Actor->Avatars"
|
||||
ON "Abuse->FlaggedAccount->Actor"."id" = "Abuse->FlaggedAccount->Actor->Avatars"."actorId"
|
||||
AND "Abuse->FlaggedAccount->Actor->Avatars"."type" = ${ActorImageType.AVATAR}
|
||||
LEFT JOIN "server" AS "Abuse->FlaggedAccount->Actor->Server"
|
||||
ON "Abuse->FlaggedAccount->Actor"."serverId" = "Abuse->FlaggedAccount->Actor->Server"."id"
|
||||
) ON "Abuse"."flaggedAccountId" = "Abuse->FlaggedAccount"."id"
|
||||
|
||||
LEFT JOIN (
|
||||
"videoBlacklist" AS "VideoBlacklist"
|
||||
INNER JOIN "video" AS "VideoBlacklist->Video" ON "VideoBlacklist"."videoId" = "VideoBlacklist->Video"."id"
|
||||
) ON "UserNotificationModel"."videoBlacklistId" = "VideoBlacklist"."id"
|
||||
|
||||
LEFT JOIN "videoImport" AS "VideoImport" ON "UserNotificationModel"."videoImportId" = "VideoImport"."id"
|
||||
LEFT JOIN "video" AS "VideoImport->Video" ON "VideoImport"."videoId" = "VideoImport->Video"."id"
|
||||
|
||||
LEFT JOIN "plugin" AS "Plugin" ON "UserNotificationModel"."pluginId" = "Plugin"."id"
|
||||
|
||||
LEFT JOIN "application" AS "Application" ON "UserNotificationModel"."applicationId" = "Application"."id"
|
||||
|
||||
LEFT JOIN (
|
||||
"actorFollow" AS "ActorFollow"
|
||||
INNER JOIN "actor" AS "ActorFollow->ActorFollower" ON "ActorFollow"."actorId" = "ActorFollow->ActorFollower"."id"
|
||||
INNER JOIN "account" AS "ActorFollow->ActorFollower->Account"
|
||||
ON "ActorFollow->ActorFollower"."id" = "ActorFollow->ActorFollower->Account"."actorId"
|
||||
LEFT JOIN "actorImage" AS "ActorFollow->ActorFollower->Avatars"
|
||||
ON "ActorFollow->ActorFollower"."id" = "ActorFollow->ActorFollower->Avatars"."actorId"
|
||||
AND "ActorFollow->ActorFollower->Avatars"."type" = ${ActorImageType.AVATAR}
|
||||
LEFT JOIN "server" AS "ActorFollow->ActorFollower->Server"
|
||||
ON "ActorFollow->ActorFollower"."serverId" = "ActorFollow->ActorFollower->Server"."id"
|
||||
INNER JOIN "actor" AS "ActorFollow->ActorFollowing" ON "ActorFollow"."targetActorId" = "ActorFollow->ActorFollowing"."id"
|
||||
LEFT JOIN "videoChannel" AS "ActorFollow->ActorFollowing->VideoChannel"
|
||||
ON "ActorFollow->ActorFollowing"."id" = "ActorFollow->ActorFollowing->VideoChannel"."actorId"
|
||||
LEFT JOIN "account" AS "ActorFollow->ActorFollowing->Account"
|
||||
ON "ActorFollow->ActorFollowing"."id" = "ActorFollow->ActorFollowing->Account"."actorId"
|
||||
LEFT JOIN "server" AS "ActorFollow->ActorFollowing->Server"
|
||||
ON "ActorFollow->ActorFollowing"."serverId" = "ActorFollow->ActorFollowing->Server"."id"
|
||||
) ON "UserNotificationModel"."actorFollowId" = "ActorFollow"."id"
|
||||
|
||||
LEFT JOIN (
|
||||
"account" AS "Account"
|
||||
INNER JOIN "actor" AS "Account->Actor" ON "Account"."actorId" = "Account->Actor"."id"
|
||||
LEFT JOIN "actorImage" AS "Account->Actor->Avatars"
|
||||
ON "Account->Actor"."id" = "Account->Actor->Avatars"."actorId"
|
||||
AND "Account->Actor->Avatars"."type" = ${ActorImageType.AVATAR}
|
||||
LEFT JOIN "server" AS "Account->Actor->Server" ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"
|
||||
) ON "UserNotificationModel"."accountId" = "Account"."id"
|
||||
|
||||
LEFT JOIN "userRegistration" as "UserRegistration" ON "UserNotificationModel"."userRegistrationId" = "UserRegistration"."id"
|
||||
|
||||
LEFT JOIN (
|
||||
"videoCaption" AS "VideoCaption"
|
||||
INNER JOIN "video" AS "VideoCaption->Video" ON "VideoCaption"."videoId" = "VideoCaption->Video"."id"
|
||||
) ON "UserNotificationModel"."videoCaptionId" = "VideoCaption"."id"`
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
import { FileStorage, UserExportState, type FileStorageType, type UserExport, type UserExportStateType } from '@peertube/peertube-models'
|
||||
import { logger } from '@server/helpers/logger.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import {
|
||||
JWT_TOKEN_USER_EXPORT_FILE_LIFETIME,
|
||||
STATIC_DOWNLOAD_PATHS,
|
||||
USER_EXPORT_FILE_PREFIX,
|
||||
USER_EXPORT_STATES,
|
||||
WEBSERVER
|
||||
} from '@server/initializers/constants.js'
|
||||
import { removeUserExportObjectStorage } from '@server/lib/object-storage/user-export.js'
|
||||
import { getFSUserExportFilePath } from '@server/lib/paths.js'
|
||||
import { MUserAccountId, MUserExport } from '@server/types/models/index.js'
|
||||
import { remove } from 'fs-extra/esm'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import { join } from 'path'
|
||||
import { FindOptions, Op } from 'sequelize'
|
||||
import { AllowNull, BeforeDestroy, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
import { doesExist } from '../shared/query.js'
|
||||
import { SequelizeModel } from '../shared/sequelize-type.js'
|
||||
import { getSort } from '../shared/sort.js'
|
||||
import { UserModel } from './user.js'
|
||||
|
||||
@Table({
|
||||
tableName: 'userExport',
|
||||
indexes: [
|
||||
{
|
||||
fields: [ 'userId' ]
|
||||
},
|
||||
{
|
||||
fields: [ 'filename' ],
|
||||
unique: true
|
||||
}
|
||||
]
|
||||
})
|
||||
export class UserExportModel extends SequelizeModel<UserExportModel> {
|
||||
@CreatedAt
|
||||
createdAt: Date
|
||||
|
||||
@UpdatedAt
|
||||
updatedAt: Date
|
||||
|
||||
@AllowNull(true)
|
||||
@Column
|
||||
filename: string
|
||||
|
||||
@AllowNull(false)
|
||||
@Column
|
||||
withVideoFiles: boolean
|
||||
|
||||
@AllowNull(false)
|
||||
@Column
|
||||
state: UserExportStateType
|
||||
|
||||
@AllowNull(true)
|
||||
@Column(DataType.TEXT)
|
||||
error: string
|
||||
|
||||
@AllowNull(true)
|
||||
@Column(DataType.BIGINT)
|
||||
size: number
|
||||
|
||||
@AllowNull(false)
|
||||
@Column
|
||||
storage: FileStorageType
|
||||
|
||||
@AllowNull(true)
|
||||
@Column
|
||||
fileUrl: string
|
||||
|
||||
@ForeignKey(() => UserModel)
|
||||
@Column
|
||||
userId: number
|
||||
|
||||
@BelongsTo(() => UserModel, {
|
||||
foreignKey: {
|
||||
allowNull: false
|
||||
},
|
||||
onDelete: 'CASCADE'
|
||||
})
|
||||
User: Awaited<UserModel>
|
||||
|
||||
@BeforeDestroy
|
||||
static removeFile (instance: UserExportModel) {
|
||||
logger.info('Removing user export file %s.', instance.filename)
|
||||
|
||||
if (instance.storage === FileStorage.FILE_SYSTEM) {
|
||||
remove(getFSUserExportFilePath(instance))
|
||||
.catch(err => logger.error('Cannot delete user export archive %s from filesystem.', instance.filename, { err }))
|
||||
} else {
|
||||
removeUserExportObjectStorage(instance)
|
||||
.catch(err => logger.error('Cannot delete user export archive %s from object storage.', instance.filename, { err }))
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
static listByUser (user: MUserAccountId) {
|
||||
const query: FindOptions = {
|
||||
where: {
|
||||
userId: user.id
|
||||
}
|
||||
}
|
||||
|
||||
return UserExportModel.findAll<MUserExport>(query)
|
||||
}
|
||||
|
||||
static listExpired (expirationTimeMS: number) {
|
||||
const query: FindOptions = {
|
||||
where: {
|
||||
createdAt: {
|
||||
[Op.lt]: new Date(new Date().getTime() + expirationTimeMS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return UserExportModel.findAll<MUserExport>(query)
|
||||
}
|
||||
|
||||
static listForApi (options: {
|
||||
user: MUserAccountId
|
||||
start: number
|
||||
count: number
|
||||
}) {
|
||||
const { count, start, user } = options
|
||||
|
||||
const query: FindOptions = {
|
||||
offset: start,
|
||||
limit: count,
|
||||
order: getSort('createdAt'),
|
||||
where: {
|
||||
userId: user.id
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
UserExportModel.count(query),
|
||||
UserExportModel.findAll<MUserExport>(query)
|
||||
]).then(([ total, data ]) => ({ total, data }))
|
||||
}
|
||||
|
||||
static load (id: number | string) {
|
||||
return UserExportModel.findByPk<MUserExport>(id)
|
||||
}
|
||||
|
||||
static loadByFilename (filename: string) {
|
||||
return UserExportModel.findOne<MUserExport>({ where: { filename } })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static async doesOwnedFileExist (filename: string, storage: FileStorageType) {
|
||||
const query = 'SELECT 1 FROM "userExport" ' +
|
||||
`WHERE "filename" = $filename AND "storage" = $storage LIMIT 1`
|
||||
|
||||
return doesExist({ sequelize: this.sequelize, query, bind: { filename, storage } })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
generateAndSetFilename () {
|
||||
if (!this.userId) throw new Error('Cannot generate filename without userId')
|
||||
if (!this.createdAt) throw new Error('Cannot generate filename without createdAt')
|
||||
|
||||
this.filename = `${USER_EXPORT_FILE_PREFIX}${this.userId}-${this.createdAt.toISOString()}.zip`
|
||||
}
|
||||
|
||||
canBeSafelyRemoved () {
|
||||
const supportedStates = new Set<UserExportStateType>([ UserExportState.COMPLETED, UserExportState.ERRORED, UserExportState.PENDING ])
|
||||
|
||||
return supportedStates.has(this.state)
|
||||
}
|
||||
|
||||
generateJWT () {
|
||||
return jwt.sign(
|
||||
{
|
||||
userExportId: this.id
|
||||
},
|
||||
CONFIG.SECRETS.PEERTUBE,
|
||||
{
|
||||
expiresIn: JWT_TOKEN_USER_EXPORT_FILE_LIFETIME,
|
||||
audience: this.filename,
|
||||
issuer: WEBSERVER.URL
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
isJWTValid (jwtToken: string) {
|
||||
try {
|
||||
const payload = jwt.verify(jwtToken, CONFIG.SECRETS.PEERTUBE, {
|
||||
audience: this.filename,
|
||||
issuer: WEBSERVER.URL
|
||||
})
|
||||
|
||||
if ((payload as any).userExportId !== this.id) return false
|
||||
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
getFileDownloadUrl () {
|
||||
if (this.state !== UserExportState.COMPLETED) return null
|
||||
|
||||
return WEBSERVER.URL + join(STATIC_DOWNLOAD_PATHS.USER_EXPORTS, this.filename) + '?jwt=' + this.generateJWT()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
toFormattedJSON (this: MUserExport): UserExport {
|
||||
return {
|
||||
id: this.id,
|
||||
|
||||
state: {
|
||||
id: this.state,
|
||||
label: USER_EXPORT_STATES[this.state]
|
||||
},
|
||||
|
||||
size: this.size,
|
||||
|
||||
fileUrl: this.fileUrl,
|
||||
privateDownloadUrl: this.getFileDownloadUrl(),
|
||||
createdAt: this.createdAt.toISOString(),
|
||||
expiresOn: new Date(this.createdAt.getTime() + CONFIG.EXPORT.USERS.EXPORT_EXPIRATION).toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
import { MUserImport } from '@server/types/models/index.js'
|
||||
import { SequelizeModel } from '../shared/index.js'
|
||||
import { UserModel } from './user.js'
|
||||
import type { UserImportResultSummary, UserImportStateType } from '@peertube/peertube-models'
|
||||
import { getSort } from '../shared/sort.js'
|
||||
import { USER_IMPORT_STATES } from '@server/initializers/constants.js'
|
||||
|
||||
@Table({
|
||||
tableName: 'userImport',
|
||||
indexes: [
|
||||
{
|
||||
fields: [ 'userId' ]
|
||||
},
|
||||
{
|
||||
fields: [ 'filename' ],
|
||||
unique: true
|
||||
}
|
||||
]
|
||||
})
|
||||
export class UserImportModel extends SequelizeModel<UserImportModel> {
|
||||
@CreatedAt
|
||||
createdAt: Date
|
||||
|
||||
@UpdatedAt
|
||||
updatedAt: Date
|
||||
|
||||
@AllowNull(true)
|
||||
@Column
|
||||
filename: string
|
||||
|
||||
@AllowNull(false)
|
||||
@Column
|
||||
state: UserImportStateType
|
||||
|
||||
@AllowNull(true)
|
||||
@Column(DataType.TEXT)
|
||||
error: string
|
||||
|
||||
@AllowNull(true)
|
||||
@Column(DataType.JSONB)
|
||||
resultSummary: UserImportResultSummary
|
||||
|
||||
@ForeignKey(() => UserModel)
|
||||
@Column
|
||||
userId: number
|
||||
|
||||
@BelongsTo(() => UserModel, {
|
||||
foreignKey: {
|
||||
allowNull: false
|
||||
},
|
||||
onDelete: 'CASCADE'
|
||||
})
|
||||
User: Awaited<UserModel>
|
||||
|
||||
static load (id: number | string) {
|
||||
return UserImportModel.findByPk<MUserImport>(id)
|
||||
}
|
||||
|
||||
static loadLatestByUserId (userId: number) {
|
||||
return UserImportModel.findOne<MUserImport>({
|
||||
where: {
|
||||
userId
|
||||
},
|
||||
order: getSort('-createdAt')
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
generateAndSetFilename () {
|
||||
if (!this.userId) throw new Error('Cannot generate filename without userId')
|
||||
if (!this.createdAt) throw new Error('Cannot generate filename without createdAt')
|
||||
|
||||
this.filename = `user-import-${this.userId}-${this.createdAt.toISOString()}.zip`
|
||||
}
|
||||
|
||||
toFormattedJSON () {
|
||||
return {
|
||||
id: this.id,
|
||||
state: {
|
||||
id: this.state,
|
||||
label: USER_IMPORT_STATES[this.state]
|
||||
},
|
||||
createdAt: this.createdAt.toISOString()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
import { type UserNotificationSetting, type UserNotificationSettingValueType } from '@peertube/peertube-models'
|
||||
import { TokensCache } from '@server/lib/auth/tokens-cache.js'
|
||||
import { MNotificationSettingFormattable } from '@server/types/models/index.js'
|
||||
import {
|
||||
AfterDestroy,
|
||||
AfterUpdate,
|
||||
AllowNull,
|
||||
BelongsTo,
|
||||
Column,
|
||||
CreatedAt,
|
||||
Default,
|
||||
ForeignKey,
|
||||
Is, Table,
|
||||
UpdatedAt
|
||||
} from 'sequelize-typescript'
|
||||
import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications.js'
|
||||
import { SequelizeModel, throwIfNotValid } from '../shared/index.js'
|
||||
import { UserModel } from './user.js'
|
||||
|
||||
@Table({
|
||||
tableName: 'userNotificationSetting',
|
||||
indexes: [
|
||||
{
|
||||
fields: [ 'userId' ],
|
||||
unique: true
|
||||
}
|
||||
]
|
||||
})
|
||||
export class UserNotificationSettingModel extends SequelizeModel<UserNotificationSettingModel> {
|
||||
|
||||
@AllowNull(false)
|
||||
@Default(null)
|
||||
@Is(
|
||||
'UserNotificationSettingNewVideoFromSubscription',
|
||||
value => throwIfNotValid(value, isUserNotificationSettingValid, 'newVideoFromSubscription')
|
||||
)
|
||||
@Column
|
||||
newVideoFromSubscription: UserNotificationSettingValueType
|
||||
|
||||
@AllowNull(false)
|
||||
@Default(null)
|
||||
@Is(
|
||||
'UserNotificationSettingNewCommentOnMyVideo',
|
||||
value => throwIfNotValid(value, isUserNotificationSettingValid, 'newCommentOnMyVideo')
|
||||
)
|
||||
@Column
|
||||
newCommentOnMyVideo: UserNotificationSettingValueType
|
||||
|
||||
@AllowNull(false)
|
||||
@Default(null)
|
||||
@Is(
|
||||
'UserNotificationSettingAbuseAsModerator',
|
||||
value => throwIfNotValid(value, isUserNotificationSettingValid, 'abuseAsModerator')
|
||||
)
|
||||
@Column
|
||||
abuseAsModerator: UserNotificationSettingValueType
|
||||
|
||||
@AllowNull(false)
|
||||
@Default(null)
|
||||
@Is(
|
||||
'UserNotificationSettingVideoAutoBlacklistAsModerator',
|
||||
value => throwIfNotValid(value, isUserNotificationSettingValid, 'videoAutoBlacklistAsModerator')
|
||||
)
|
||||
@Column
|
||||
videoAutoBlacklistAsModerator: UserNotificationSettingValueType
|
||||
|
||||
@AllowNull(false)
|
||||
@Default(null)
|
||||
@Is(
|
||||
'UserNotificationSettingBlacklistOnMyVideo',
|
||||
value => throwIfNotValid(value, isUserNotificationSettingValid, 'blacklistOnMyVideo')
|
||||
)
|
||||
@Column
|
||||
blacklistOnMyVideo: UserNotificationSettingValueType
|
||||
|
||||
@AllowNull(false)
|
||||
@Default(null)
|
||||
@Is(
|
||||
'UserNotificationSettingMyVideoPublished',
|
||||
value => throwIfNotValid(value, isUserNotificationSettingValid, 'myVideoPublished')
|
||||
)
|
||||
@Column
|
||||
myVideoPublished: UserNotificationSettingValueType
|
||||
|
||||
@AllowNull(false)
|
||||
@Default(null)
|
||||
@Is(
|
||||
'UserNotificationSettingMyVideoImportFinished',
|
||||
value => throwIfNotValid(value, isUserNotificationSettingValid, 'myVideoImportFinished')
|
||||
)
|
||||
@Column
|
||||
myVideoImportFinished: UserNotificationSettingValueType
|
||||
|
||||
@AllowNull(false)
|
||||
@Default(null)
|
||||
@Is(
|
||||
'UserNotificationSettingNewUserRegistration',
|
||||
value => throwIfNotValid(value, isUserNotificationSettingValid, 'newUserRegistration')
|
||||
)
|
||||
@Column
|
||||
newUserRegistration: UserNotificationSettingValueType
|
||||
|
||||
@AllowNull(false)
|
||||
@Default(null)
|
||||
@Is(
|
||||
'UserNotificationSettingNewInstanceFollower',
|
||||
value => throwIfNotValid(value, isUserNotificationSettingValid, 'newInstanceFollower')
|
||||
)
|
||||
@Column
|
||||
newInstanceFollower: UserNotificationSettingValueType
|
||||
|
||||
@AllowNull(false)
|
||||
@Default(null)
|
||||
@Is(
|
||||
'UserNotificationSettingNewInstanceFollower',
|
||||
value => throwIfNotValid(value, isUserNotificationSettingValid, 'autoInstanceFollowing')
|
||||
)
|
||||
@Column
|
||||
autoInstanceFollowing: UserNotificationSettingValueType
|
||||
|
||||
@AllowNull(false)
|
||||
@Default(null)
|
||||
@Is(
|
||||
'UserNotificationSettingNewFollow',
|
||||
value => throwIfNotValid(value, isUserNotificationSettingValid, 'newFollow')
|
||||
)
|
||||
@Column
|
||||
newFollow: UserNotificationSettingValueType
|
||||
|
||||
@AllowNull(false)
|
||||
@Default(null)
|
||||
@Is(
|
||||
'UserNotificationSettingCommentMention',
|
||||
value => throwIfNotValid(value, isUserNotificationSettingValid, 'commentMention')
|
||||
)
|
||||
@Column
|
||||
commentMention: UserNotificationSettingValueType
|
||||
|
||||
@AllowNull(false)
|
||||
@Default(null)
|
||||
@Is(
|
||||
'UserNotificationSettingAbuseStateChange',
|
||||
value => throwIfNotValid(value, isUserNotificationSettingValid, 'abuseStateChange')
|
||||
)
|
||||
@Column
|
||||
abuseStateChange: UserNotificationSettingValueType
|
||||
|
||||
@AllowNull(false)
|
||||
@Default(null)
|
||||
@Is(
|
||||
'UserNotificationSettingAbuseNewMessage',
|
||||
value => throwIfNotValid(value, isUserNotificationSettingValid, 'abuseNewMessage')
|
||||
)
|
||||
@Column
|
||||
abuseNewMessage: UserNotificationSettingValueType
|
||||
|
||||
@AllowNull(false)
|
||||
@Default(null)
|
||||
@Is(
|
||||
'UserNotificationSettingNewPeerTubeVersion',
|
||||
value => throwIfNotValid(value, isUserNotificationSettingValid, 'newPeerTubeVersion')
|
||||
)
|
||||
@Column
|
||||
newPeerTubeVersion: UserNotificationSettingValueType
|
||||
|
||||
@AllowNull(false)
|
||||
@Default(null)
|
||||
@Is(
|
||||
'UserNotificationSettingNewPeerPluginVersion',
|
||||
value => throwIfNotValid(value, isUserNotificationSettingValid, 'newPluginVersion')
|
||||
)
|
||||
@Column
|
||||
newPluginVersion: UserNotificationSettingValueType
|
||||
|
||||
@AllowNull(false)
|
||||
@Default(null)
|
||||
@Is(
|
||||
'UserNotificationSettingMyVideoStudioEditionFinished',
|
||||
value => throwIfNotValid(value, isUserNotificationSettingValid, 'myVideoStudioEditionFinished')
|
||||
)
|
||||
@Column
|
||||
myVideoStudioEditionFinished: UserNotificationSettingValueType
|
||||
|
||||
@AllowNull(false)
|
||||
@Default(null)
|
||||
@Is(
|
||||
'UserNotificationSettingTranscriptionGeneratedForOwner',
|
||||
value => throwIfNotValid(value, isUserNotificationSettingValid, 'myVideoTranscriptionGenerated')
|
||||
)
|
||||
@Column
|
||||
myVideoTranscriptionGenerated: UserNotificationSettingValueType
|
||||
|
||||
@ForeignKey(() => UserModel)
|
||||
@Column
|
||||
userId: number
|
||||
|
||||
@BelongsTo(() => UserModel, {
|
||||
foreignKey: {
|
||||
allowNull: false
|
||||
},
|
||||
onDelete: 'cascade'
|
||||
})
|
||||
User: Awaited<UserModel>
|
||||
|
||||
@CreatedAt
|
||||
createdAt: Date
|
||||
|
||||
@UpdatedAt
|
||||
updatedAt: Date
|
||||
|
||||
@AfterUpdate
|
||||
@AfterDestroy
|
||||
static removeTokenCache (instance: UserNotificationSettingModel) {
|
||||
return TokensCache.Instance.clearCacheByUserId(instance.userId)
|
||||
}
|
||||
|
||||
static updateUserSettings (settings: UserNotificationSetting, userId: number) {
|
||||
const query = {
|
||||
where: {
|
||||
userId
|
||||
}
|
||||
}
|
||||
|
||||
return UserNotificationSettingModel.update(settings, query)
|
||||
}
|
||||
|
||||
toFormattedJSON (this: MNotificationSettingFormattable): UserNotificationSetting {
|
||||
return {
|
||||
newCommentOnMyVideo: this.newCommentOnMyVideo,
|
||||
newVideoFromSubscription: this.newVideoFromSubscription,
|
||||
abuseAsModerator: this.abuseAsModerator,
|
||||
videoAutoBlacklistAsModerator: this.videoAutoBlacklistAsModerator,
|
||||
blacklistOnMyVideo: this.blacklistOnMyVideo,
|
||||
myVideoPublished: this.myVideoPublished,
|
||||
myVideoImportFinished: this.myVideoImportFinished,
|
||||
newUserRegistration: this.newUserRegistration,
|
||||
commentMention: this.commentMention,
|
||||
newFollow: this.newFollow,
|
||||
newInstanceFollower: this.newInstanceFollower,
|
||||
autoInstanceFollowing: this.autoInstanceFollowing,
|
||||
abuseNewMessage: this.abuseNewMessage,
|
||||
abuseStateChange: this.abuseStateChange,
|
||||
newPeerTubeVersion: this.newPeerTubeVersion,
|
||||
myVideoStudioEditionFinished: this.myVideoStudioEditionFinished,
|
||||
myVideoTranscriptionGenerated: this.myVideoTranscriptionGenerated,
|
||||
newPluginVersion: this.newPluginVersion
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,566 @@
|
||||
import { forceNumber, maxBy } from '@peertube/peertube-core-utils'
|
||||
import { UserNotification, type UserNotificationType_Type } from '@peertube/peertube-models'
|
||||
import { uuidToShort } from '@peertube/peertube-node-utils'
|
||||
import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user/index.js'
|
||||
import { ModelIndexesOptions, Op, WhereOptions } from 'sequelize'
|
||||
import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
import { isBooleanValid } from '../../helpers/custom-validators/misc.js'
|
||||
import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications.js'
|
||||
import { AbuseModel } from '../abuse/abuse.js'
|
||||
import { AccountModel } from '../account/account.js'
|
||||
import { ActorFollowModel } from '../actor/actor-follow.js'
|
||||
import { ApplicationModel } from '../application/application.js'
|
||||
import { PluginModel } from '../server/plugin.js'
|
||||
import { SequelizeModel, throwIfNotValid } from '../shared/index.js'
|
||||
import { VideoBlacklistModel } from '../video/video-blacklist.js'
|
||||
import { VideoCaptionModel } from '../video/video-caption.js'
|
||||
import { VideoCommentModel } from '../video/video-comment.js'
|
||||
import { VideoImportModel } from '../video/video-import.js'
|
||||
import { VideoModel } from '../video/video.js'
|
||||
import { UserNotificationListQueryBuilder } from './sql/user-notitication-list-query-builder.js'
|
||||
import { UserRegistrationModel } from './user-registration.js'
|
||||
import { UserModel } from './user.js'
|
||||
|
||||
@Table({
|
||||
tableName: 'userNotification',
|
||||
indexes: [
|
||||
{
|
||||
fields: [ 'userId' ]
|
||||
},
|
||||
{
|
||||
fields: [ 'videoId' ],
|
||||
where: {
|
||||
videoId: {
|
||||
[Op.ne]: null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
fields: [ 'commentId' ],
|
||||
where: {
|
||||
commentId: {
|
||||
[Op.ne]: null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
fields: [ 'abuseId' ],
|
||||
where: {
|
||||
abuseId: {
|
||||
[Op.ne]: null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
fields: [ 'videoBlacklistId' ],
|
||||
where: {
|
||||
videoBlacklistId: {
|
||||
[Op.ne]: null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
fields: [ 'videoImportId' ],
|
||||
where: {
|
||||
videoImportId: {
|
||||
[Op.ne]: null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
fields: [ 'accountId' ],
|
||||
where: {
|
||||
accountId: {
|
||||
[Op.ne]: null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
fields: [ 'actorFollowId' ],
|
||||
where: {
|
||||
actorFollowId: {
|
||||
[Op.ne]: null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
fields: [ 'pluginId' ],
|
||||
where: {
|
||||
pluginId: {
|
||||
[Op.ne]: null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
fields: [ 'applicationId' ],
|
||||
where: {
|
||||
applicationId: {
|
||||
[Op.ne]: null
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
fields: [ 'userRegistrationId' ],
|
||||
where: {
|
||||
userRegistrationId: {
|
||||
[Op.ne]: null
|
||||
}
|
||||
}
|
||||
}
|
||||
] as (ModelIndexesOptions & { where?: WhereOptions })[]
|
||||
})
|
||||
export class UserNotificationModel extends SequelizeModel<UserNotificationModel> {
|
||||
|
||||
@AllowNull(false)
|
||||
@Default(null)
|
||||
@Is('UserNotificationType', value => throwIfNotValid(value, isUserNotificationTypeValid, 'type'))
|
||||
@Column
|
||||
type: UserNotificationType_Type
|
||||
|
||||
@AllowNull(false)
|
||||
@Default(false)
|
||||
@Is('UserNotificationRead', value => throwIfNotValid(value, isBooleanValid, 'read'))
|
||||
@Column
|
||||
read: boolean
|
||||
|
||||
@CreatedAt
|
||||
createdAt: Date
|
||||
|
||||
@UpdatedAt
|
||||
updatedAt: Date
|
||||
|
||||
@ForeignKey(() => UserModel)
|
||||
@Column
|
||||
userId: number
|
||||
|
||||
@BelongsTo(() => UserModel, {
|
||||
foreignKey: {
|
||||
allowNull: false
|
||||
},
|
||||
onDelete: 'cascade'
|
||||
})
|
||||
User: Awaited<UserModel>
|
||||
|
||||
@ForeignKey(() => VideoModel)
|
||||
@Column
|
||||
videoId: number
|
||||
|
||||
@BelongsTo(() => VideoModel, {
|
||||
foreignKey: {
|
||||
allowNull: true
|
||||
},
|
||||
onDelete: 'cascade'
|
||||
})
|
||||
Video: Awaited<VideoModel>
|
||||
|
||||
@ForeignKey(() => VideoCommentModel)
|
||||
@Column
|
||||
commentId: number
|
||||
|
||||
@BelongsTo(() => VideoCommentModel, {
|
||||
foreignKey: {
|
||||
allowNull: true
|
||||
},
|
||||
onDelete: 'cascade'
|
||||
})
|
||||
VideoComment: Awaited<VideoCommentModel>
|
||||
|
||||
@ForeignKey(() => AbuseModel)
|
||||
@Column
|
||||
abuseId: number
|
||||
|
||||
@BelongsTo(() => AbuseModel, {
|
||||
foreignKey: {
|
||||
allowNull: true
|
||||
},
|
||||
onDelete: 'cascade'
|
||||
})
|
||||
Abuse: Awaited<AbuseModel>
|
||||
|
||||
@ForeignKey(() => VideoBlacklistModel)
|
||||
@Column
|
||||
videoBlacklistId: number
|
||||
|
||||
@BelongsTo(() => VideoBlacklistModel, {
|
||||
foreignKey: {
|
||||
allowNull: true
|
||||
},
|
||||
onDelete: 'cascade'
|
||||
})
|
||||
VideoBlacklist: Awaited<VideoBlacklistModel>
|
||||
|
||||
@ForeignKey(() => VideoImportModel)
|
||||
@Column
|
||||
videoImportId: number
|
||||
|
||||
@BelongsTo(() => VideoImportModel, {
|
||||
foreignKey: {
|
||||
allowNull: true
|
||||
},
|
||||
onDelete: 'cascade'
|
||||
})
|
||||
VideoImport: Awaited<VideoImportModel>
|
||||
|
||||
@ForeignKey(() => AccountModel)
|
||||
@Column
|
||||
accountId: number
|
||||
|
||||
@BelongsTo(() => AccountModel, {
|
||||
foreignKey: {
|
||||
allowNull: true
|
||||
},
|
||||
onDelete: 'cascade'
|
||||
})
|
||||
Account: Awaited<AccountModel>
|
||||
|
||||
@ForeignKey(() => ActorFollowModel)
|
||||
@Column
|
||||
actorFollowId: number
|
||||
|
||||
@BelongsTo(() => ActorFollowModel, {
|
||||
foreignKey: {
|
||||
allowNull: true
|
||||
},
|
||||
onDelete: 'cascade'
|
||||
})
|
||||
ActorFollow: Awaited<ActorFollowModel>
|
||||
|
||||
@ForeignKey(() => PluginModel)
|
||||
@Column
|
||||
pluginId: number
|
||||
|
||||
@BelongsTo(() => PluginModel, {
|
||||
foreignKey: {
|
||||
allowNull: true
|
||||
},
|
||||
onDelete: 'cascade'
|
||||
})
|
||||
Plugin: Awaited<PluginModel>
|
||||
|
||||
@ForeignKey(() => ApplicationModel)
|
||||
@Column
|
||||
applicationId: number
|
||||
|
||||
@BelongsTo(() => ApplicationModel, {
|
||||
foreignKey: {
|
||||
allowNull: true
|
||||
},
|
||||
onDelete: 'cascade'
|
||||
})
|
||||
Application: Awaited<ApplicationModel>
|
||||
|
||||
@ForeignKey(() => UserRegistrationModel)
|
||||
@Column
|
||||
userRegistrationId: number
|
||||
|
||||
@BelongsTo(() => UserRegistrationModel, {
|
||||
foreignKey: {
|
||||
allowNull: true
|
||||
},
|
||||
onDelete: 'cascade'
|
||||
})
|
||||
UserRegistration: Awaited<UserRegistrationModel>
|
||||
|
||||
@ForeignKey(() => VideoCaptionModel)
|
||||
@Column
|
||||
videoCaptionId: number
|
||||
|
||||
@BelongsTo(() => VideoCaptionModel, {
|
||||
foreignKey: {
|
||||
allowNull: true
|
||||
},
|
||||
onDelete: 'cascade'
|
||||
})
|
||||
VideoCaption: Awaited<VideoCaptionModel>
|
||||
|
||||
static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) {
|
||||
const where = { userId }
|
||||
|
||||
const query = {
|
||||
userId,
|
||||
unread,
|
||||
offset: start,
|
||||
limit: count,
|
||||
sort,
|
||||
where
|
||||
}
|
||||
|
||||
if (unread !== undefined) query.where['read'] = !unread
|
||||
|
||||
return Promise.all([
|
||||
UserNotificationModel.count({ where })
|
||||
.then(count => count || 0),
|
||||
|
||||
count === 0
|
||||
? [] as UserNotificationModelForApi[]
|
||||
: new UserNotificationListQueryBuilder(this.sequelize, query).listNotifications()
|
||||
]).then(([ total, data ]) => ({ total, data }))
|
||||
}
|
||||
|
||||
static markAsRead (userId: number, notificationIds: number[]) {
|
||||
const query = {
|
||||
where: {
|
||||
userId,
|
||||
id: {
|
||||
[Op.in]: notificationIds
|
||||
},
|
||||
read: false
|
||||
}
|
||||
}
|
||||
|
||||
return UserNotificationModel.update({ read: true }, query)
|
||||
}
|
||||
|
||||
static markAllAsRead (userId: number) {
|
||||
const query = { where: { userId, read: false } }
|
||||
|
||||
return UserNotificationModel.update({ read: true }, query)
|
||||
}
|
||||
|
||||
static removeNotificationsOf (options: { id: number, type: 'account' | 'server', forUserId?: number }) {
|
||||
const id = forceNumber(options.id)
|
||||
|
||||
function buildAccountWhereQuery (base: string) {
|
||||
const whereSuffix = options.forUserId
|
||||
? ` AND "userNotification"."userId" = ${options.forUserId}`
|
||||
: ''
|
||||
|
||||
if (options.type === 'account') {
|
||||
return base +
|
||||
` WHERE "account"."id" = ${id} ${whereSuffix}`
|
||||
}
|
||||
|
||||
return base +
|
||||
` WHERE "actor"."serverId" = ${id} ${whereSuffix}`
|
||||
}
|
||||
|
||||
const queries = [
|
||||
buildAccountWhereQuery(
|
||||
`SELECT "userNotification"."id" FROM "userNotification" ` +
|
||||
`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 `
|
||||
),
|
||||
|
||||
// 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 `
|
||||
),
|
||||
|
||||
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" `
|
||||
)
|
||||
]
|
||||
|
||||
const query = `DELETE FROM "userNotification" WHERE id IN (${queries.join(' UNION ')})`
|
||||
|
||||
return UserNotificationModel.sequelize.query(query)
|
||||
}
|
||||
|
||||
toFormattedJSON (this: UserNotificationModelForApi): UserNotification {
|
||||
const video = this.Video
|
||||
? {
|
||||
...this.formatVideo(this.Video),
|
||||
|
||||
channel: this.formatActor(this.Video.VideoChannel)
|
||||
}
|
||||
: undefined
|
||||
|
||||
const videoImport = this.VideoImport
|
||||
? {
|
||||
id: this.VideoImport.id,
|
||||
video: this.VideoImport.Video
|
||||
? this.formatVideo(this.VideoImport.Video)
|
||||
: undefined,
|
||||
torrentName: this.VideoImport.torrentName,
|
||||
magnetUri: this.VideoImport.magnetUri,
|
||||
targetUrl: this.VideoImport.targetUrl
|
||||
}
|
||||
: undefined
|
||||
|
||||
const comment = this.VideoComment
|
||||
? {
|
||||
id: this.VideoComment.id,
|
||||
threadId: this.VideoComment.getThreadId(),
|
||||
account: this.formatActor(this.VideoComment.Account),
|
||||
video: this.formatVideo(this.VideoComment.Video),
|
||||
heldForReview: this.VideoComment.heldForReview
|
||||
}
|
||||
: undefined
|
||||
|
||||
const abuse = this.Abuse ? this.formatAbuse(this.Abuse) : undefined
|
||||
|
||||
const videoBlacklist = this.VideoBlacklist
|
||||
? {
|
||||
id: this.VideoBlacklist.id,
|
||||
video: this.formatVideo(this.VideoBlacklist.Video)
|
||||
}
|
||||
: undefined
|
||||
|
||||
const account = this.Account ? this.formatActor(this.Account) : undefined
|
||||
|
||||
const actorFollowingType = {
|
||||
Application: 'instance' as 'instance',
|
||||
Group: 'channel' as 'channel',
|
||||
Person: 'account' as 'account'
|
||||
}
|
||||
const actorFollow = this.ActorFollow
|
||||
? {
|
||||
id: this.ActorFollow.id,
|
||||
state: this.ActorFollow.state,
|
||||
follower: {
|
||||
id: this.ActorFollow.ActorFollower.Account.id,
|
||||
displayName: this.ActorFollow.ActorFollower.Account.getDisplayName(),
|
||||
name: this.ActorFollow.ActorFollower.preferredUsername,
|
||||
host: this.ActorFollow.ActorFollower.getHost(),
|
||||
|
||||
...this.formatAvatars(this.ActorFollow.ActorFollower.Avatars)
|
||||
},
|
||||
following: {
|
||||
type: actorFollowingType[this.ActorFollow.ActorFollowing.type],
|
||||
displayName: (this.ActorFollow.ActorFollowing.VideoChannel || this.ActorFollow.ActorFollowing.Account).getDisplayName(),
|
||||
name: this.ActorFollow.ActorFollowing.preferredUsername,
|
||||
host: this.ActorFollow.ActorFollowing.getHost()
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
|
||||
const plugin = this.Plugin
|
||||
? {
|
||||
name: this.Plugin.name,
|
||||
type: this.Plugin.type,
|
||||
latestVersion: this.Plugin.latestVersion
|
||||
}
|
||||
: undefined
|
||||
|
||||
const peertube = this.Application
|
||||
? { latestVersion: this.Application.latestPeerTubeVersion }
|
||||
: undefined
|
||||
|
||||
const registration = this.UserRegistration
|
||||
? { id: this.UserRegistration.id, username: this.UserRegistration.username }
|
||||
: undefined
|
||||
|
||||
const videoCaption = this.VideoCaption
|
||||
? {
|
||||
id: this.VideoCaption.id,
|
||||
language: {
|
||||
id: this.VideoCaption.language,
|
||||
label: VideoCaptionModel.getLanguageLabel(this.VideoCaption.language)
|
||||
},
|
||||
video: this.formatVideo(this.VideoCaption.Video)
|
||||
}
|
||||
: undefined
|
||||
|
||||
return {
|
||||
id: this.id,
|
||||
type: this.type,
|
||||
read: this.read,
|
||||
video,
|
||||
videoImport,
|
||||
comment,
|
||||
abuse,
|
||||
videoBlacklist,
|
||||
account,
|
||||
actorFollow,
|
||||
plugin,
|
||||
peertube,
|
||||
registration,
|
||||
videoCaption,
|
||||
createdAt: this.createdAt.toISOString(),
|
||||
updatedAt: this.updatedAt.toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
formatVideo (video: UserNotificationIncludes.VideoInclude) {
|
||||
return {
|
||||
id: video.id,
|
||||
uuid: video.uuid,
|
||||
shortUUID: uuidToShort(video.uuid),
|
||||
name: video.name
|
||||
}
|
||||
}
|
||||
|
||||
formatAbuse (abuse: UserNotificationIncludes.AbuseInclude) {
|
||||
const commentAbuse = abuse.VideoCommentAbuse?.VideoComment
|
||||
? {
|
||||
threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(),
|
||||
|
||||
video: abuse.VideoCommentAbuse.VideoComment.Video
|
||||
? {
|
||||
id: abuse.VideoCommentAbuse.VideoComment.Video.id,
|
||||
name: abuse.VideoCommentAbuse.VideoComment.Video.name,
|
||||
shortUUID: uuidToShort(abuse.VideoCommentAbuse.VideoComment.Video.uuid),
|
||||
uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
: undefined
|
||||
|
||||
const videoAbuse = abuse.VideoAbuse?.Video
|
||||
? this.formatVideo(abuse.VideoAbuse.Video)
|
||||
: undefined
|
||||
|
||||
const accountAbuse = (!commentAbuse && !videoAbuse && abuse.FlaggedAccount)
|
||||
? this.formatActor(abuse.FlaggedAccount)
|
||||
: undefined
|
||||
|
||||
return {
|
||||
id: abuse.id,
|
||||
state: abuse.state,
|
||||
video: videoAbuse,
|
||||
comment: commentAbuse,
|
||||
account: accountAbuse
|
||||
}
|
||||
}
|
||||
|
||||
formatActor (
|
||||
accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor
|
||||
) {
|
||||
return {
|
||||
id: accountOrChannel.id,
|
||||
displayName: accountOrChannel.getDisplayName(),
|
||||
name: accountOrChannel.Actor.preferredUsername,
|
||||
host: accountOrChannel.Actor.getHost(),
|
||||
|
||||
...this.formatAvatars(accountOrChannel.Actor.Avatars)
|
||||
}
|
||||
}
|
||||
|
||||
formatAvatars (avatars: UserNotificationIncludes.ActorImageInclude[]) {
|
||||
if (!avatars || avatars.length === 0) return { avatar: undefined, avatars: [] }
|
||||
|
||||
return {
|
||||
avatar: this.formatAvatar(maxBy(avatars, 'width')),
|
||||
|
||||
avatars: avatars.map(a => this.formatAvatar(a))
|
||||
}
|
||||
}
|
||||
|
||||
formatAvatar (a: UserNotificationIncludes.ActorImageInclude) {
|
||||
return {
|
||||
path: a.getStaticPath(),
|
||||
width: a.width
|
||||
}
|
||||
}
|
||||
|
||||
formatVideoCaption (a: UserNotificationIncludes.ActorImageInclude) {
|
||||
return {
|
||||
path: a.getStaticPath(),
|
||||
width: a.width
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
import { UserRegistration, UserRegistrationState, type UserRegistrationStateType } from '@peertube/peertube-models'
|
||||
import {
|
||||
isRegistrationModerationResponseValid,
|
||||
isRegistrationReasonValid,
|
||||
isRegistrationStateValid
|
||||
} from '@server/helpers/custom-validators/user-registration.js'
|
||||
import { isVideoChannelDisplayNameValid } from '@server/helpers/custom-validators/video-channels.js'
|
||||
import { cryptPassword } from '@server/helpers/peertube-crypto.js'
|
||||
import { USER_REGISTRATION_STATES } from '@server/initializers/constants.js'
|
||||
import { MRegistration, MRegistrationFormattable } from '@server/types/models/index.js'
|
||||
import { FindOptions, Op, QueryTypes, WhereOptions } from 'sequelize'
|
||||
import {
|
||||
AllowNull,
|
||||
BeforeCreate,
|
||||
BelongsTo,
|
||||
Column,
|
||||
CreatedAt,
|
||||
DataType,
|
||||
ForeignKey,
|
||||
Is,
|
||||
IsEmail, Table,
|
||||
UpdatedAt
|
||||
} from 'sequelize-typescript'
|
||||
import { isUserDisplayNameValid, isUserEmailVerifiedValid, isUserPasswordValid } from '../../helpers/custom-validators/users.js'
|
||||
import { SequelizeModel, getSort, parseAggregateResult, throwIfNotValid } from '../shared/index.js'
|
||||
import { UserModel } from './user.js'
|
||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
|
||||
@Table({
|
||||
tableName: 'userRegistration',
|
||||
indexes: [
|
||||
{
|
||||
fields: [ 'username' ],
|
||||
unique: true
|
||||
},
|
||||
{
|
||||
fields: [ 'email' ],
|
||||
unique: true
|
||||
},
|
||||
{
|
||||
fields: [ 'channelHandle' ],
|
||||
unique: true
|
||||
},
|
||||
{
|
||||
fields: [ 'userId' ],
|
||||
unique: true
|
||||
}
|
||||
]
|
||||
})
|
||||
export class UserRegistrationModel extends SequelizeModel<UserRegistrationModel> {
|
||||
|
||||
@AllowNull(false)
|
||||
@Is('RegistrationState', value => throwIfNotValid(value, isRegistrationStateValid, 'state'))
|
||||
@Column
|
||||
state: UserRegistrationStateType
|
||||
|
||||
@AllowNull(false)
|
||||
@Is('RegistrationReason', value => throwIfNotValid(value, isRegistrationReasonValid, 'registration reason'))
|
||||
@Column(DataType.TEXT)
|
||||
registrationReason: string
|
||||
|
||||
@AllowNull(true)
|
||||
@Is('RegistrationModerationResponse', value => throwIfNotValid(value, isRegistrationModerationResponseValid, 'moderation response', true))
|
||||
@Column(DataType.TEXT)
|
||||
moderationResponse: string
|
||||
|
||||
@AllowNull(true)
|
||||
@Is('RegistrationPassword', value => throwIfNotValid(value, isUserPasswordValid, 'registration password', true))
|
||||
@Column
|
||||
password: string
|
||||
|
||||
@AllowNull(false)
|
||||
@Column
|
||||
username: string
|
||||
|
||||
@AllowNull(false)
|
||||
@IsEmail
|
||||
@Column(DataType.STRING(400))
|
||||
email: string
|
||||
|
||||
@AllowNull(true)
|
||||
@Is('RegistrationEmailVerified', value => throwIfNotValid(value, isUserEmailVerifiedValid, 'email verified boolean', true))
|
||||
@Column
|
||||
emailVerified: boolean
|
||||
|
||||
@AllowNull(true)
|
||||
@Is('RegistrationAccountDisplayName', value => throwIfNotValid(value, isUserDisplayNameValid, 'account display name', true))
|
||||
@Column
|
||||
accountDisplayName: string
|
||||
|
||||
@AllowNull(true)
|
||||
@Is('ChannelHandle', value => throwIfNotValid(value, isVideoChannelDisplayNameValid, 'channel handle', true))
|
||||
@Column
|
||||
channelHandle: string
|
||||
|
||||
@AllowNull(true)
|
||||
@Is('ChannelDisplayName', value => throwIfNotValid(value, isVideoChannelDisplayNameValid, 'channel display name', true))
|
||||
@Column
|
||||
channelDisplayName: string
|
||||
|
||||
@AllowNull(true)
|
||||
@Column
|
||||
processedAt: Date
|
||||
|
||||
@CreatedAt
|
||||
createdAt: Date
|
||||
|
||||
@UpdatedAt
|
||||
updatedAt: Date
|
||||
|
||||
@ForeignKey(() => UserModel)
|
||||
@Column
|
||||
userId: number
|
||||
|
||||
@BelongsTo(() => UserModel, {
|
||||
foreignKey: {
|
||||
allowNull: true
|
||||
},
|
||||
onDelete: 'SET NULL'
|
||||
})
|
||||
User: Awaited<UserModel>
|
||||
|
||||
@BeforeCreate
|
||||
static async cryptPasswordIfNeeded (instance: UserRegistrationModel) {
|
||||
instance.password = await cryptPassword(instance.password)
|
||||
}
|
||||
|
||||
static load (id: number): Promise<MRegistration> {
|
||||
return UserRegistrationModel.findByPk(id)
|
||||
}
|
||||
|
||||
static loadByEmail (email: string): Promise<MRegistration> {
|
||||
const query = {
|
||||
where: { email }
|
||||
}
|
||||
|
||||
return UserRegistrationModel.findOne(query)
|
||||
}
|
||||
|
||||
static loadByEmailOrUsername (emailOrUsername: string): Promise<MRegistration> {
|
||||
const query = {
|
||||
where: {
|
||||
[Op.or]: [
|
||||
{ email: emailOrUsername },
|
||||
{ username: emailOrUsername }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
return UserRegistrationModel.findOne(query)
|
||||
}
|
||||
|
||||
static loadByEmailOrHandle (options: {
|
||||
email: string
|
||||
username: string
|
||||
channelHandle?: string
|
||||
}): Promise<MRegistration> {
|
||||
const { email, username, channelHandle } = options
|
||||
|
||||
let or: WhereOptions = [
|
||||
{ email },
|
||||
{ channelHandle: username },
|
||||
{ username }
|
||||
]
|
||||
|
||||
if (channelHandle) {
|
||||
or = or.concat([
|
||||
{ username: channelHandle },
|
||||
{ channelHandle }
|
||||
])
|
||||
}
|
||||
|
||||
const query = {
|
||||
where: {
|
||||
[Op.or]: or
|
||||
}
|
||||
}
|
||||
|
||||
return UserRegistrationModel.findOne(query)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static listForApi (options: {
|
||||
start: number
|
||||
count: number
|
||||
sort: string
|
||||
search?: string
|
||||
}) {
|
||||
const { start, count, sort, search } = options
|
||||
|
||||
const where: WhereOptions = {}
|
||||
|
||||
if (search) {
|
||||
Object.assign(where, {
|
||||
[Op.or]: [
|
||||
{
|
||||
email: {
|
||||
[Op.iLike]: '%' + search + '%'
|
||||
}
|
||||
},
|
||||
{
|
||||
username: {
|
||||
[Op.iLike]: '%' + search + '%'
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
const query: FindOptions = {
|
||||
offset: start,
|
||||
limit: count,
|
||||
order: getSort(sort),
|
||||
where,
|
||||
include: [
|
||||
{
|
||||
model: UserModel.unscoped(),
|
||||
required: false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
UserRegistrationModel.count(query),
|
||||
UserRegistrationModel.findAll<MRegistrationFormattable>(query)
|
||||
]).then(([ total, data ]) => ({ total, data }))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static getStats () {
|
||||
const query = `SELECT ` +
|
||||
`AVG(EXTRACT(EPOCH FROM ("processedAt" - "createdAt") * 1000)) ` +
|
||||
`FILTER (WHERE "processedAt" IS NOT NULL AND "createdAt" > CURRENT_DATE - INTERVAL '3 months')` +
|
||||
`AS "avgResponseTime", ` +
|
||||
// "processedAt" has been introduced in PeerTube 6.1 so also check the abuse state to check processed abuses
|
||||
`COUNT(*) FILTER (WHERE "processedAt" IS NOT NULL OR "state" != ${UserRegistrationState.PENDING}) AS "processedRequests", ` +
|
||||
`COUNT(*) AS "totalRequests" ` +
|
||||
`FROM "userRegistration"`
|
||||
|
||||
return UserRegistrationModel.sequelize.query<any>(query, {
|
||||
type: QueryTypes.SELECT,
|
||||
raw: true
|
||||
}).then(([ row ]) => {
|
||||
return {
|
||||
totalRegistrationRequests: parseAggregateResult(row.totalRequests),
|
||||
|
||||
totalRegistrationRequestsProcessed: parseAggregateResult(row.processedRequests),
|
||||
|
||||
averageRegistrationRequestResponseTimeMs: row?.avgResponseTime
|
||||
? forceNumber(row.avgResponseTime)
|
||||
: null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
toFormattedJSON (this: MRegistrationFormattable): UserRegistration {
|
||||
return {
|
||||
id: this.id,
|
||||
|
||||
state: {
|
||||
id: this.state,
|
||||
label: USER_REGISTRATION_STATES[this.state]
|
||||
},
|
||||
|
||||
registrationReason: this.registrationReason,
|
||||
moderationResponse: this.moderationResponse,
|
||||
|
||||
username: this.username,
|
||||
email: this.email,
|
||||
emailVerified: this.emailVerified,
|
||||
|
||||
accountDisplayName: this.accountDisplayName,
|
||||
|
||||
channelHandle: this.channelHandle,
|
||||
channelDisplayName: this.channelDisplayName,
|
||||
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: this.updatedAt,
|
||||
|
||||
user: this.User
|
||||
? { id: this.User.id }
|
||||
: null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import { DestroyOptions, Op, Transaction } from 'sequelize'
|
||||
import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, IsInt, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
import { ResultList } from '@peertube/peertube-models'
|
||||
import { MUserAccountId, MUserId } from '@server/types/models/index.js'
|
||||
import { VideoModel } from '../video/video.js'
|
||||
import { UserModel } from './user.js'
|
||||
import { SequelizeModel } from '../shared/sequelize-type.js'
|
||||
import { USER_EXPORT_MAX_ITEMS } from '@server/initializers/constants.js'
|
||||
import { getSort } from '../shared/sort.js'
|
||||
|
||||
@Table({
|
||||
tableName: 'userVideoHistory',
|
||||
indexes: [
|
||||
{
|
||||
fields: [ 'userId', 'videoId' ],
|
||||
unique: true
|
||||
},
|
||||
{
|
||||
fields: [ 'userId' ]
|
||||
},
|
||||
{
|
||||
fields: [ 'videoId' ]
|
||||
}
|
||||
]
|
||||
})
|
||||
export class UserVideoHistoryModel extends SequelizeModel<UserVideoHistoryModel> {
|
||||
@CreatedAt
|
||||
createdAt: Date
|
||||
|
||||
@UpdatedAt
|
||||
updatedAt: Date
|
||||
|
||||
@AllowNull(false)
|
||||
@IsInt
|
||||
@Column
|
||||
currentTime: number
|
||||
|
||||
@ForeignKey(() => VideoModel)
|
||||
@Column
|
||||
videoId: number
|
||||
|
||||
@BelongsTo(() => VideoModel, {
|
||||
foreignKey: {
|
||||
allowNull: false
|
||||
},
|
||||
onDelete: 'CASCADE'
|
||||
})
|
||||
Video: Awaited<VideoModel>
|
||||
|
||||
@ForeignKey(() => UserModel)
|
||||
@Column
|
||||
userId: number
|
||||
|
||||
@BelongsTo(() => UserModel, {
|
||||
foreignKey: {
|
||||
allowNull: false
|
||||
},
|
||||
onDelete: 'CASCADE'
|
||||
})
|
||||
User: Awaited<UserModel>
|
||||
|
||||
// FIXME: have to specify the result type to not break peertube typings generation
|
||||
static listForApi (user: MUserAccountId, start: number, count: number, search?: string): Promise<ResultList<VideoModel>> {
|
||||
return VideoModel.listForApi({
|
||||
start,
|
||||
count,
|
||||
search,
|
||||
sort: '-"userVideoHistory"."updatedAt"',
|
||||
nsfw: null, // All
|
||||
displayOnlyForFollower: null,
|
||||
user,
|
||||
historyOfUser: user
|
||||
})
|
||||
}
|
||||
|
||||
static async listForExport (user: MUserId) {
|
||||
const rows = await UserVideoHistoryModel.findAll({
|
||||
attributes: [ 'createdAt', 'updatedAt', 'currentTime' ],
|
||||
where: {
|
||||
userId: user.id
|
||||
},
|
||||
limit: USER_EXPORT_MAX_ITEMS,
|
||||
include: [
|
||||
{
|
||||
attributes: [ 'url' ],
|
||||
model: VideoModel.unscoped(),
|
||||
required: true
|
||||
}
|
||||
],
|
||||
order: getSort('updatedAt')
|
||||
})
|
||||
|
||||
return rows.map(r => ({ createdAt: r.createdAt, updatedAt: r.updatedAt, currentTime: r.currentTime, videoUrl: r.Video.url }))
|
||||
}
|
||||
|
||||
static removeUserHistoryElement (user: MUserId, videoId: number) {
|
||||
const query: DestroyOptions = {
|
||||
where: {
|
||||
userId: user.id,
|
||||
videoId
|
||||
}
|
||||
}
|
||||
|
||||
return UserVideoHistoryModel.destroy(query)
|
||||
}
|
||||
|
||||
static removeUserHistoryBefore (user: MUserId, beforeDate: string, t: Transaction) {
|
||||
const query: DestroyOptions = {
|
||||
where: {
|
||||
userId: user.id
|
||||
},
|
||||
transaction: t
|
||||
}
|
||||
|
||||
if (beforeDate) {
|
||||
query.where['updatedAt'] = {
|
||||
[Op.lt]: beforeDate
|
||||
}
|
||||
}
|
||||
|
||||
return UserVideoHistoryModel.destroy(query)
|
||||
}
|
||||
|
||||
static removeOldHistory (beforeDate: string) {
|
||||
const query: DestroyOptions = {
|
||||
where: {
|
||||
updatedAt: {
|
||||
[Op.lt]: beforeDate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return UserVideoHistoryModel.destroy(query)
|
||||
}
|
||||
}
|
||||
ファイル差分が大きすぎるため省略します
差分を読込み
新しい課題から参照
ユーザをブロックする