はじまりの大地

このコミットが含まれているのは:
2024-07-15 09:14:04 +09:00
コミット 6632905f32
3501個のファイルの変更1439465行の追加0行の削除
+284
ファイルの表示
@@ -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"`
}
}
+229
ファイルの表示
@@ -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()
}
}
}
+88
ファイルの表示
@@ -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()
}
}
}
+249
ファイルの表示
@@ -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
}
}
}
+566
ファイルの表示
@@ -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
}
}
}
+289
ファイルの表示
@@ -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
}
}
}
+135
ファイルの表示
@@ -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)
}
}
ファイル差分が大きすぎるため省略します 差分を読込み