Implement user import/export in server

This commit is contained in:
Chocobozzz 2024-02-12 10:47:52 +01:00 committed by Chocobozzz
parent 4d63e6f577
commit 8573e5a80a
196 changed files with 5661 additions and 722 deletions

View File

@ -1,4 +1,4 @@
# /!\ YOU SHOULD NOT UPDATE THIS FILE, USE production.yaml instead /!\ #
# /!\ DO NOT UPDATE THIS FILE, USE production.yaml instead /!\ #
listen:
hostname: '127.0.0.1'
@ -222,12 +222,16 @@ object_storage:
# Useful when you want to use a CDN/external proxy
base_url: '' # Example: 'https://mirror.example.com'
# Same settings but for web videos
web_videos:
bucket_name: 'web-videos'
prefix: ''
base_url: ''
user_exports:
bucket_name: 'user-exports'
prefix: ''
base_url: ''
log:
level: 'info' # 'debug' | 'info' | 'warn' | 'error'
@ -482,11 +486,14 @@ user:
videos:
# Enable or disable video history by default for new users.
enabled: true
# Default value of maximum video bytes the user can upload (does not take into account transcoded files)
# Default value of maximum video bytes the user can upload
# Does not take into account transcoded files or account export archives (that can include user uploaded files)
# Byte format is supported ("1GB" etc)
# -1 == unlimited
video_quota: -1
video_quota_daily: -1
default_channel_name: 'Main $1 channel' # The placeholder $1 is used to represent the user's username
video_channels:
@ -707,6 +714,24 @@ import:
# Max number of videos to import when the user asks for full sync
full_sync_videos_limit: 1000
users:
# Video quota is checked on import so the user doesn't upload a too big archive file
# Video quota (daily quota is not taken into account) is also checked for each video when PeerTube is processing the import
enabled: true
export:
users:
# Allow users to export their PeerTube data in a .zip for backup or re-import
# Only one export at a time is allowed per user
enabled: true
# Max size of the current user quota to accept or not the export
# Goal of this setting is to not store too big archive file on your server disk
max_user_video_quota: 10GB
# How long PeerTube should keep the user export
export_expiration: '2 days'
auto_blacklist:
# New videos automatically blacklisted so moderators can review before publishing
videos:
@ -867,6 +892,7 @@ client:
# By default PeerTube client displays author username
prefer_author_display_name: false
display_author_avatar: false
resumable_upload:
# Max size of upload chunks, e.g. '90MB'
# If null, it will be calculated based on network speed

View File

@ -220,12 +220,16 @@ object_storage:
# Useful when you want to use a CDN/external proxy
base_url: '' # Example: 'https://mirror.example.com'
# Same settings but for web videos
web_videos:
bucket_name: 'web-videos'
prefix: ''
base_url: ''
user_exports:
bucket_name: 'user-exports'
prefix: ''
base_url: ''
log:
level: 'info' # 'debug' | 'info' | 'warn' | 'error'
@ -492,11 +496,14 @@ user:
videos:
# Enable or disable video history by default for new users.
enabled: true
# Default value of maximum video bytes the user can upload (does not take into account transcoded files)
# Default value of maximum video bytes the user can upload
# Does not take into account transcoded files or account export archives (that can include user uploaded files)
# Byte format is supported ("1GB" etc)
# -1 == unlimited
video_quota: -1
video_quota_daily: -1
default_channel_name: 'Main $1 channel' # The placeholder $1 is used to represent the user's username
video_channels:
@ -717,6 +724,24 @@ import:
# Max number of videos to import when the user asks for full sync
full_sync_videos_limit: 1000
users:
# Video quota is checked on import so the user doesn't upload a too big archive file
# Video quota (daily quota is not taken into account) is also checked for each video when PeerTube is processing the import
enabled: true
export:
users:
# Allow users to export their PeerTube data in a .zip for backup or re-import
# Only one export at a time is allowed per user
enabled: true
# Max size of the current user quota to accept or not the export
# Goal of this setting is to not store too big archive file on your server disk
max_user_video_quota: 10GB
# How long PeerTube should keep the user export
export_expiration: '2 days'
auto_blacklist:
# New videos automatically blacklisted so moderators can review before publishing
videos:
@ -877,6 +902,7 @@ client:
# By default PeerTube client displays author username
prefer_author_display_name: false
display_author_avatar: false
resumable_upload:
# Max size of upload chunks, e.g. '90MB'
# If null, it will be calculated based on network speed

View File

@ -109,6 +109,7 @@
"@peertube/http-signature": "^1.7.0",
"@smithy/node-http-handler": "^2.1.7",
"@uploadx/core": "^6.0.0",
"archiver": "^6.0.1",
"async-mutex": "^0.4.0",
"bcrypt": "5.1.1",
"bencode": "^4.0.0",
@ -142,6 +143,7 @@
"jimp": "^0.22.4",
"js-yaml": "^4.0.0",
"jsonld": "~8.3.1",
"jsonwebtoken": "^9.0.2",
"lodash-es": "^4.17.21",
"lru-cache": "^10.0.1",
"magnet-uri": "^7.0.5",
@ -178,11 +180,13 @@
"webfinger.js": "^2.6.6",
"webtorrent": "^2.1.27",
"winston": "3.11.0",
"ws": "^8.0.0"
"ws": "^8.0.0",
"yauzl": "^2.10.0"
},
"devDependencies": {
"@peertube/maildev": "^1.2.0",
"@peertube/resolve-tspaths": "^0.8.14",
"@types/archiver": "^6.0.2",
"@types/bcrypt": "^5.0.0",
"@types/bencode": "^2.0.0",
"@types/bluebird": "^3.5.33",
@ -197,6 +201,7 @@
"@types/fluent-ffmpeg": "^2.1.16",
"@types/fs-extra": "^11.0.1",
"@types/jsonld": "^1.5.9",
"@types/jsonwebtoken": "^9.0.5",
"@types/lodash-es": "^4.17.8",
"@types/magnet-uri": "^5.1.1",
"@types/maildev": "^0.0.4",
@ -212,6 +217,7 @@
"@types/validator": "^13.9.0",
"@types/webtorrent": "^0.109.0",
"@types/ws": "^8.2.0",
"@types/yauzl": "^2.10.3",
"@typescript-eslint/eslint-plugin": "^6.7.5",
"autocannon": "^7.0.4",
"chai": "^4.1.1",
@ -228,6 +234,7 @@
"eslint-plugin-promise": "^6.0.0",
"fast-xml-parser": "^4.0.0-beta.8",
"jpeg-js": "^0.4.4",
"jszip": "^3.10.1",
"mocha": "^10.0.0",
"pixelmatch": "^5.3.0",
"pngjs": "^7.0.0",

View File

@ -19,6 +19,18 @@ function removeQueryParams (url: string) {
return objUrl.toString()
}
function queryParamsToObject (entries: any) {
const result: { [ id: string ]: string | number | boolean } = {}
for (const [ key, value ] of entries) {
result[key] = value
}
return result
}
// ---------------------------------------------------------------------------
function buildPlaylistLink (playlist: Pick<VideoPlaylist, 'shortUUID'>, base?: string) {
return (base ?? window.location.origin) + buildPlaylistWatchPath(playlist)
}
@ -123,6 +135,7 @@ function decoratePlaylistLink (options: {
export {
addQueryParams,
removeQueryParams,
queryParamsToObject,
buildPlaylistLink,
buildVideoLink,

View File

@ -18,7 +18,7 @@ export interface ActivityPubActor {
sharedInbox: string
}
summary: string
attributedTo: ActivityPubAttributedTo[]
attributedTo?: ActivityPubAttributedTo[]
support?: string
publicKey: {
@ -31,4 +31,8 @@ export interface ActivityPubActor {
icon?: ActivityIconObject | ActivityIconObject[]
published?: string
// For export
likes?: string
dislikes?: string
}

View File

@ -1,7 +1,7 @@
import { Activity } from './activity.js'
export interface ActivityPubCollection {
'@context': string[]
'@context': any[]
type: 'Collection' | 'CollectionPage'
totalItems: number
partOf?: string

View File

@ -1,5 +1,7 @@
export interface ActivityPubOrderedCollection<T> {
'@context': string[]
id: string
'@context': any[]
type: 'OrderedCollection' | 'OrderedCollectionPage'
totalItems: number
orderedItems: T[]

View File

@ -59,6 +59,16 @@ export interface VideoObject {
to?: string[]
cc?: string[]
// For export
attachment?: {
type: 'Video'
url: string
mediaType: string
height: number
size: number
fps: number
}[]
}
export interface ActivityPubStoryboard {

View File

@ -0,0 +1,6 @@
export const FileStorage = {
FILE_SYSTEM: 0,
OBJECT_STORAGE: 1
} as const
export type FileStorageType = typeof FileStorage[keyof typeof FileStorage]

View File

@ -1 +1,2 @@
export * from './file-storage.enum.js'
export * from './result-list.model.js'

View File

@ -0,0 +1,9 @@
export * from './peertube-export-format/index.js'
export * from './user-export-request-result.model.js'
export * from './user-export-request.model.js'
export * from './user-export-state.enum.js'
export * from './user-export.model.js'
export * from './user-import.model.js'
export * from './user-import-state.enum.js'
export * from './user-import-result.model.js'
export * from './user-import-upload-result.model.js'

View File

@ -0,0 +1,18 @@
import { UserActorImageJSON } from './actor-export.model.js'
export interface AccountExportJSON {
url: string
name: string
displayName: string
description: string
updatedAt: string
createdAt: string
avatars: UserActorImageJSON[]
archiveFiles: {
avatar: string | null
}
}

View File

@ -0,0 +1,6 @@
export interface UserActorImageJSON {
width: number
url: string
createdAt: string
updatedAt: string
}

View File

@ -0,0 +1,9 @@
export interface BlocklistExportJSON {
instances: {
host: string
}[]
actors: {
handle: string
}[]
}

View File

@ -0,0 +1,23 @@
import { UserActorImageJSON } from './actor-export.model.js'
export interface ChannelExportJSON {
channels: {
url: string
name: string
displayName: string
description: string
support: string
updatedAt: string
createdAt: string
avatars: UserActorImageJSON[]
banners: UserActorImageJSON[]
archiveFiles: {
avatar: string | null
banner: string | null
}
}[]
}

View File

@ -0,0 +1,12 @@
export interface CommentsExportJSON {
comments: {
url: string
text: string
createdAt: string
videoUrl: string
inReplyToCommentUrl?: string
archiveFiles?: never
}[]
}

View File

@ -0,0 +1,8 @@
export interface DislikesExportJSON {
dislikes: {
videoUrl: string
createdAt: string
archiveFiles?: never
}[]
}

View File

@ -0,0 +1,9 @@
export interface FollowersExportJSON {
followers: {
handle: string
createdAt: string
targetHandle: string
archiveFiles?: never
}[]
}

View File

@ -0,0 +1,9 @@
export interface FollowingExportJSON {
following: {
handle: string
targetHandle: string
createdAt: string
archiveFiles?: never
}[]
}

View File

@ -0,0 +1,12 @@
export * from './account-export.model.js'
export * from './actor-export.model.js'
export * from './blocklist-export.model.js'
export * from './channel-export.model.js'
export * from './comments-export.model.js'
export * from './dislikes-export.model.js'
export * from './followers-export.model.js'
export * from './following-export.model.js'
export * from './likes-export.model.js'
export * from './user-settings-export.model.js'
export * from './video-export.model.js'
export * from './video-playlists-export.model.js'

View File

@ -0,0 +1,8 @@
export interface LikesExportJSON {
likes: {
videoUrl: string
createdAt: string
archiveFiles?: never
}[]
}

View File

@ -0,0 +1,26 @@
import { UserNotificationSetting } from '../../users/user-notification-setting.model.js'
import { NSFWPolicyType } from '../../videos/nsfw-policy.type.js'
export interface UserSettingsExportJSON {
email: string
emailPublic: boolean
nsfwPolicy: NSFWPolicyType
autoPlayVideo: boolean
autoPlayNextVideo: boolean
autoPlayNextVideoPlaylist: boolean
p2pEnabled: boolean
videosHistoryEnabled: boolean
videoLanguages: string[]
theme: string
createdAt: Date
notificationSettings: UserNotificationSetting
archiveFiles?: never
}

View File

@ -0,0 +1,103 @@
import {
LiveVideoLatencyModeType,
VideoPrivacyType,
VideoStateType,
VideoStreamingPlaylistType_Type
} from '../../videos/index.js'
export interface VideoExportJSON {
videos: {
uuid: string
createdAt: string
updatedAt: string
publishedAt: string
originallyPublishedAt: string
name: string
category: number
licence: number
language: string
tags: string[]
privacy: VideoPrivacyType
passwords: string[]
duration: number
description: string
support: string
isLive: boolean
live?: {
saveReplay: boolean
permanentLive: boolean
latencyMode: LiveVideoLatencyModeType
streamKey: string
replaySettings?: {
privacy: VideoPrivacyType
}
}
url: string
thumbnailUrl: string
previewUrl: string
views: number
likes: number
dislikes: number
nsfw: boolean
commentsEnabled: boolean
downloadEnabled: boolean
channel: {
name: string
}
waitTranscoding: boolean
state: VideoStateType
captions: {
createdAt: string
updatedAt: string
language: string
filename: string
fileUrl: string
}[]
files: VideoFileExportJSON[]
streamingPlaylists: {
type: VideoStreamingPlaylistType_Type
playlistUrl: string
segmentsSha256Url: string
files: VideoFileExportJSON[]
}[]
source?: {
filename: string
}
archiveFiles: {
videoFile: string | null
thumbnail: string | null
captions: Record<string, string> // The key is the language code
}
}[]
}
// ---------------------------------------------------------------------------
export interface VideoFileExportJSON {
resolution: number
size: number // Bytes
fps: number
torrentUrl: string
fileUrl: string
}

View File

@ -0,0 +1,34 @@
import { VideoPlaylistPrivacyType } from '../../videos/playlist/video-playlist-privacy.model.js'
import { VideoPlaylistType_Type } from '../../videos/playlist/video-playlist-type.model.js'
export interface VideoPlaylistsExportJSON {
videoPlaylists: {
displayName: string
description: string
privacy: VideoPlaylistPrivacyType
url: string
uuid: string
type: VideoPlaylistType_Type
channel: {
name: string
}
createdAt: string
updatedAt: string
thumbnailUrl: string
elements: {
videoUrl: string
startTimestamp?: number
stopTimestamp?: number
}[]
archiveFiles: {
thumbnail: string | null
}
}[]
}

View File

@ -0,0 +1,5 @@
export interface UserExportRequestResult {
export: {
id: number
}
}

View File

@ -0,0 +1,3 @@
export interface UserExportRequest {
withVideoFiles: boolean
}

View File

@ -0,0 +1,8 @@
export const UserExportState = {
PENDING: 1,
PROCESSING: 2,
COMPLETED: 3,
ERRORED: 4
} as const
export type UserExportStateType = typeof UserExportState[keyof typeof UserExportState]

View File

@ -0,0 +1,18 @@
import { UserExportStateType } from './user-export-state.enum.js'
export interface UserExport {
id: number
state: {
id: UserExportStateType
label: string
}
// In bytes
size: number
privateDownloadUrl: string
createdAt: string | Date
expiresOn: string | Date
}

View File

@ -0,0 +1,20 @@
type Summary = {
success: number
duplicates: number
errors: number
}
export interface UserImportResultSummary {
stats: {
blocklist: Summary
channels: Summary
likes: Summary
dislikes: Summary
following: Summary
videoPlaylists: Summary
videos: Summary
account: Summary
userSettings: Summary
}
}

View File

@ -0,0 +1,8 @@
export const UserImportState = {
PENDING: 1,
PROCESSING: 2,
COMPLETED: 3,
ERRORED: 4
} as const
export type UserImportStateType = typeof UserImportState[keyof typeof UserImportState]

View File

@ -0,0 +1,5 @@
export interface UserImportUploadResult {
userImport: {
id: number
}
}

View File

@ -0,0 +1,10 @@
import { UserImportStateType } from './user-import-state.enum.js'
export interface UserImport {
id: number
state: {
id: UserImportStateType
label: string
}
createdAt: string
}

View File

@ -3,6 +3,7 @@ export * from './actors/index.js'
export * from './bulk/index.js'
export * from './common/index.js'
export * from './custom-markup/index.js'
export * from './import-export/index.js'
export * from './feeds/index.js'
export * from './http/index.js'
export * from './joinpeertube/index.js'

View File

@ -65,6 +65,8 @@ export const serverFilterHookObject = {
'filter:api.video.post-import-url.accept.result': true,
'filter:api.video.post-import-torrent.accept.result': true,
'filter:api.video.update-file.accept.result': true,
// PeerTube >= 6.1
'filter:api.video.user-import.accept.result': true,
// Filter the result of the accept comment (thread or reply) functions
// If the functions return false then the user cannot post its comment
'filter:api.video-thread.create.accept.result': true,
@ -75,6 +77,8 @@ export const serverFilterHookObject = {
'filter:api.video.import-url.video-attribute.result': true,
'filter:api.video.import-torrent.video-attribute.result': true,
'filter:api.video.live.video-attribute.result': true,
// PeerTube >= 6.1
'filter:api.video.user-import.video-attribute.result': true,
// Filter params/result used to list threads of a specific video
// (used by the video watch page)

View File

@ -193,10 +193,23 @@ export interface CustomConfig {
enabled: boolean
}
}
videoChannelSynchronization: {
enabled: boolean
maxPerUser: number
}
users: {
enabled: boolean
}
}
export: {
users: {
enabled: boolean
maxUserVideoQuota: number
exportExpiration: number
}
}
trending: {
@ -260,5 +273,4 @@ export interface CustomConfig {
storyboards: {
enabled: boolean
}
}

View File

@ -9,4 +9,5 @@ export interface SendDebugCommand {
| 'process-video-viewers'
| 'process-video-channel-sync-latest'
| 'process-update-videos-scheduler'
| 'remove-expired-user-exports'
}

View File

@ -31,6 +31,8 @@ export type JobType =
| 'video-transcoding'
| 'videos-views-stats'
| 'generate-video-storyboard'
| 'create-user-export'
| 'import-user-archive'
export interface Job {
id: number | string
@ -302,3 +304,15 @@ export interface GenerateStoryboardPayload {
videoUUID: string
federate: boolean
}
// ---------------------------------------------------------------------------
export interface CreateUserExportPayload {
userExportId: number
}
// ---------------------------------------------------------------------------
export interface ImportUserArchivePayload {
userImportId: number
}

View File

@ -207,9 +207,22 @@ export interface ServerConfig {
enabled: boolean
}
}
videoChannelSynchronization: {
enabled: boolean
}
users: {
enabled:boolean
}
}
export: {
users: {
enabled: boolean
exportExpiration: number
maxUserVideoQuota: number
}
}
autoBlacklist: {

View File

@ -54,7 +54,9 @@ export const ServerErrorCode = {
VIDEO_REQUIRES_PASSWORD:'video_requires_password',
INCORRECT_VIDEO_PASSWORD:'incorrect_video_password',
VIDEO_ALREADY_BEING_TRANSCODED:'video_already_being_transcoded'
VIDEO_ALREADY_BEING_TRANSCODED:'video_already_being_transcoded',
MAX_USER_VIDEO_QUOTA_EXCEEDED_FOR_USER_EXPORT: 'max_user_video_quota_exceeded_for_user_export'
} as const
/**

View File

@ -47,7 +47,10 @@ export const UserRight = {
MANAGE_REGISTRATIONS: 28,
MANAGE_RUNNERS: 29
MANAGE_RUNNERS: 29,
MANAGE_USER_EXPORTS: 30,
MANAGE_USER_IMPORTS: 31
} as const
export type UserRightType = typeof UserRight[keyof typeof UserRight]

View File

@ -29,7 +29,6 @@ export * from './video-rate.type.js'
export * from './video-schedule-update.model.js'
export * from './video-sort-field.type.js'
export * from './video-state.enum.js'
export * from './video-storage.enum.js'
export * from './video-source.model.js'
export * from './video-streaming-playlist.model.js'

View File

@ -1,6 +0,0 @@
export const VideoStorage = {
FILE_SYSTEM: 0,
OBJECT_STORAGE: 1
} as const
export type VideoStorageType = typeof VideoStorage[keyof typeof VideoStorage]

View File

@ -1,4 +1,4 @@
import { basename, extname, isAbsolute, join, resolve } from 'path'
import { basename, extname, isAbsolute, join, parse, resolve } from 'path'
import { fileURLToPath } from 'url'
let rootPath: string
@ -48,3 +48,15 @@ export function buildAbsoluteFixturePath (path: string, customCIPath = false) {
return join(root(), 'packages', 'tests', 'fixtures', path)
}
export function getFilenameFromUrl (url: string) {
return getFilename(new URL(url).pathname)
}
export function getFilename (path: string) {
return parse(path).base
}
export function getFilenameWithoutExt (path: string) {
return parse(path).name
}

View File

@ -45,3 +45,5 @@ export type DeepOmitArray<T extends any[], K> = {
}
export type Unpacked<T> = T extends (infer U)[] ? U : T
export type Awaitable<T> = T | PromiseLike<T>

View File

@ -6,6 +6,7 @@ import {
ABUSE_STATES,
buildLanguages,
RUNNER_JOB_STATES,
USER_EXPORT_STATES,
USER_REGISTRATION_STATES,
VIDEO_CATEGORIES,
VIDEO_CHANNEL_SYNC_STATE,
@ -14,6 +15,7 @@ import {
VIDEO_PLAYLIST_PRIVACIES,
VIDEO_PLAYLIST_TYPES,
VIDEO_PRIVACIES,
USER_IMPORT_STATES,
VIDEO_STATES
} from '@peertube/peertube-server/core/initializers/constants.js'
@ -96,6 +98,8 @@ Object.values(VIDEO_CATEGORIES)
.concat(Object.values(ABUSE_STATES))
.concat(Object.values(USER_REGISTRATION_STATES))
.concat(Object.values(RUNNER_JOB_STATES))
.concat(Object.values(USER_EXPORT_STATES))
.concat(Object.values(USER_IMPORT_STATES))
.concat([
'This video does not exist.',
'We cannot fetch the video. Please try again later.',

View File

@ -355,6 +355,16 @@ function customConfig (): CustomConfig {
videoChannelSynchronization: {
enabled: CONFIG.IMPORT.VIDEO_CHANNEL_SYNCHRONIZATION.ENABLED,
maxPerUser: CONFIG.IMPORT.VIDEO_CHANNEL_SYNCHRONIZATION.MAX_PER_USER
},
users: {
enabled: CONFIG.IMPORT.USERS.ENABLED
}
},
export: {
users: {
enabled: CONFIG.EXPORT.USERS.ENABLED,
exportExpiration: CONFIG.EXPORT.USERS.EXPORT_EXPIRATION,
maxUserVideoQuota: CONFIG.EXPORT.USERS.MAX_USER_VIDEO_QUOTA
}
},
trending: {

View File

@ -50,7 +50,6 @@ apiRouter.use('/custom-pages', customPageRouter)
apiRouter.use('/blocklist', blocklistRouter)
apiRouter.use('/runners', runnersRouter)
// apiRouter.use(apiRateLimiter)
apiRouter.use('/ping', pong)
apiRouter.use('/*', badRequest)

View File

@ -9,7 +9,7 @@ import {
runnerJobGetVideoStudioTaskFileValidator,
runnerJobGetVideoTranscodingFileValidator
} from '@server/middlewares/validators/runners/job-files.js'
import { RunnerJobState, VideoStorage } from '@peertube/peertube-models'
import { RunnerJobState, FileStorage } from '@peertube/peertube-models'
const lTags = loggerTagsFactory('api', 'runner')
@ -57,7 +57,7 @@ async function getMaxQualityVideoFile (req: express.Request, res: express.Respon
const file = video.getMaxQualityFile()
if (file.storage === VideoStorage.OBJECT_STORAGE) {
if (file.storage === FileStorage.OBJECT_STORAGE) {
if (file.isHLS()) {
return proxifyHLS({
req,

View File

@ -151,7 +151,7 @@ async function searchVideoURI (url: string, res: express.Response) {
logger.info('Cannot search remote video %s.', url, { err })
}
} else {
video = await searchLocalUrl(sanitizeLocalUrl(url), url => VideoModel.loadByUrlAndPopulateAccount(url))
video = await searchLocalUrl(sanitizeLocalUrl(url), url => VideoModel.loadByUrlAndPopulateAccountAndFiles(url))
}
return res.json({

View File

@ -7,6 +7,7 @@ import { VideoChannelSyncLatestScheduler } from '@server/lib/schedulers/video-ch
import { VideoViewsBufferScheduler } from '@server/lib/schedulers/video-views-buffer-scheduler.js'
import { VideoViewsManager } from '@server/lib/views/video-views-manager.js'
import { authenticate, ensureUserHasRight } from '../../../middlewares/index.js'
import { RemoveExpiredUserExportsScheduler } from '@server/lib/schedulers/remove-expired-user-exports-scheduler.js'
const debugRouter = express.Router()
@ -42,6 +43,7 @@ async function runCommand (req: express.Request, res: express.Response) {
const processors: { [id in SendDebugCommand['command']]: () => Promise<any> } = {
'remove-dandling-resumable-uploads': () => RemoveDanglingResumableUploadsScheduler.Instance.execute(),
'remove-expired-user-exports': () => RemoveExpiredUserExportsScheduler.Instance.execute(),
'process-video-views-buffer': () => VideoViewsBufferScheduler.Instance.execute(),
'process-video-viewers': () => VideoViewsManager.Instance.processViewerStats(),
'process-update-videos-scheduler': () => UpdateVideosScheduler.Instance.execute(),

View File

@ -1,9 +1,7 @@
import 'multer'
import express from 'express'
import { HttpStatusCode, UserRight } from '@peertube/peertube-models'
import { logger } from '@server/helpers/logger.js'
import { getServerActor } from '@server/models/application/application.js'
import { UserNotificationModel } from '@server/models/user/user-notification.js'
import { getFormattedObjects } from '../../../helpers/utils.js'
import {
addAccountInBlocklist,
@ -105,15 +103,9 @@ async function blockAccount (req: express.Request, res: express.Response) {
const serverActor = await getServerActor()
const accountToBlock = res.locals.account
await addAccountInBlocklist(serverActor.Account.id, accountToBlock.id)
await addAccountInBlocklist({ byAccountId: serverActor.Account.id, targetAccountId: accountToBlock.id, removeNotificationOfUserId: null })
UserNotificationModel.removeNotificationsOf({
id: accountToBlock.id,
type: 'account',
forUserId: null // For all users
}).catch(err => logger.error('Cannot remove notifications after an account mute.', { err }))
return res.status(HttpStatusCode.NO_CONTENT_204).end()
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
async function unblockAccount (req: express.Request, res: express.Response) {
@ -121,7 +113,7 @@ async function unblockAccount (req: express.Request, res: express.Response) {
await removeAccountFromBlocklist(accountBlock)
return res.status(HttpStatusCode.NO_CONTENT_204).end()
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
async function listBlockedServers (req: express.Request, res: express.Response) {
@ -142,15 +134,13 @@ async function blockServer (req: express.Request, res: express.Response) {
const serverActor = await getServerActor()
const serverToBlock = res.locals.server
await addServerInBlocklist(serverActor.Account.id, serverToBlock.id)
await addServerInBlocklist({
byAccountId: serverActor.Account.id,
targetServerId: serverToBlock.id,
removeNotificationOfUserId: null
})
UserNotificationModel.removeNotificationsOf({
id: serverToBlock.id,
type: 'server',
forUserId: null // For all users
}).catch(err => logger.error('Cannot remove notifications after a server mute.', { err }))
return res.status(HttpStatusCode.NO_CONTENT_204).end()
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
async function unblockServer (req: express.Request, res: express.Response) {
@ -158,5 +148,5 @@ async function unblockServer (req: express.Request, res: express.Response) {
await removeServerFromBlocklist(serverBlock)
return res.status(HttpStatusCode.NO_CONTENT_204).end()
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}

View File

@ -47,6 +47,8 @@ import { mySubscriptionsRouter } from './my-subscriptions.js'
import { myVideoPlaylistsRouter } from './my-video-playlists.js'
import { registrationsRouter } from './registrations.js'
import { twoFactorRouter } from './two-factor.js'
import { userExportsRouter } from './user-exports.js'
import { userImportRouter } from './user-imports.js'
const auditLogger = auditLoggerFactory('users')
@ -55,6 +57,8 @@ const usersRouter = express.Router()
usersRouter.use(apiRateLimiter)
usersRouter.use('/', emailVerificationRouter)
usersRouter.use('/', userExportsRouter)
usersRouter.use('/', userImportRouter)
usersRouter.use('/', registrationsRouter)
usersRouter.use('/', twoFactorRouter)
usersRouter.use('/', tokensRouter)

View File

@ -262,11 +262,12 @@ async function updateMyAvatar (req: express.Request, res: express.Response) {
const userAccount = await AccountModel.load(user.Account.id)
const avatars = await updateLocalActorImageFiles(
userAccount,
avatarPhysicalFile,
ActorImageType.AVATAR
)
const avatars = await updateLocalActorImageFiles({
accountOrChannel: userAccount,
imagePhysicalFile: avatarPhysicalFile,
type: ActorImageType.AVATAR,
sendActorUpdate: true
})
return res.json({
avatars: avatars.map(avatar => avatar.toFormattedJSON())

View File

@ -1,8 +1,6 @@
import 'multer'
import express from 'express'
import { HttpStatusCode } from '@peertube/peertube-models'
import { logger } from '@server/helpers/logger.js'
import { UserNotificationModel } from '@server/models/user/user-notification.js'
import { getFormattedObjects } from '../../../helpers/utils.js'
import {
addAccountInBlocklist,
@ -97,15 +95,9 @@ async function blockAccount (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
const accountToBlock = res.locals.account
await addAccountInBlocklist(user.Account.id, accountToBlock.id)
await addAccountInBlocklist({ byAccountId: user.Account.id, targetAccountId: accountToBlock.id, removeNotificationOfUserId: user.id })
UserNotificationModel.removeNotificationsOf({
id: accountToBlock.id,
type: 'account',
forUserId: user.id
}).catch(err => logger.error('Cannot remove notifications after an account mute.', { err }))
return res.status(HttpStatusCode.NO_CONTENT_204).end()
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
async function unblockAccount (req: express.Request, res: express.Response) {
@ -134,15 +126,13 @@ async function blockServer (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
const serverToBlock = res.locals.server
await addServerInBlocklist(user.Account.id, serverToBlock.id)
await addServerInBlocklist({
byAccountId: user.Account.id,
targetServerId: serverToBlock.id,
removeNotificationOfUserId: user.id
})
UserNotificationModel.removeNotificationsOf({
id: serverToBlock.id,
type: 'server',
forUserId: user.id
}).catch(err => logger.error('Cannot remove notifications after a server mute.', { err }))
return res.status(HttpStatusCode.NO_CONTENT_204).end()
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
async function unblockServer (req: express.Request, res: express.Response) {
@ -150,5 +140,5 @@ async function unblockServer (req: express.Request, res: express.Response) {
await removeServerFromBlocklist(serverBlock)
return res.status(HttpStatusCode.NO_CONTENT_204).end()
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}

View File

@ -16,7 +16,7 @@ import {
listUserNotificationsValidator,
markAsReadUserNotificationsValidator,
updateNotificationSettingsValidator
} from '../../../middlewares/validators/user-notifications.js'
} from '../../../middlewares/validators/users/user-notifications.js'
import { UserNotificationSettingModel } from '../../../models/user/user-notification-setting.js'
import { meRouter } from './me.js'
@ -59,12 +59,6 @@ async function updateNotificationSettings (req: express.Request, res: express.Re
const user = res.locals.oauth.token.User
const body = req.body as UserNotificationSetting
const query = {
where: {
userId: user.id
}
}
const values: UserNotificationSetting = {
newVideoFromSubscription: body.newVideoFromSubscription,
newCommentOnMyVideo: body.newCommentOnMyVideo,
@ -85,9 +79,9 @@ async function updateNotificationSettings (req: express.Request, res: express.Re
myVideoStudioEditionFinished: body.myVideoStudioEditionFinished
}
await UserNotificationSettingModel.update(values, query)
await UserNotificationSettingModel.updateUserSettings(values, user.id)
return res.status(HttpStatusCode.NO_CONTENT_204).end()
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
async function listUserNotifications (req: express.Request, res: express.Response) {
@ -103,7 +97,7 @@ async function markAsReadUserNotifications (req: express.Request, res: express.R
await UserNotificationModel.markAsRead(user.id, req.body.ids)
return res.status(HttpStatusCode.NO_CONTENT_204).end()
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
async function markAsReadAllUserNotifications (req: express.Request, res: express.Response) {
@ -111,5 +105,5 @@ async function markAsReadAllUserNotifications (req: express.Request, res: expres
await UserNotificationModel.markAllAsRead(user.id)
return res.status(HttpStatusCode.NO_CONTENT_204).end()
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}

View File

@ -0,0 +1,100 @@
import express from 'express'
import { FileStorage, HttpStatusCode, UserExportRequest, UserExportRequestResult, UserExportState } from '@peertube/peertube-models'
import {
apiRateLimiter,
asyncMiddleware,
authenticate,
userExportDeleteValidator,
userExportRequestValidator,
userExportsListValidator
} from '../../../middlewares/index.js'
import { UserExportModel } from '@server/models/user/user-export.js'
import { getFormattedObjects } from '@server/helpers/utils.js'
import { sequelizeTypescript } from '@server/initializers/database.js'