mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2025-10-03 09:49:20 +02:00
Add channel collaborators feature
This commit is contained in:
parent
94e55dfc6c
commit
b30ded66f6
192 changed files with 5534 additions and 2642 deletions
|
@ -23,11 +23,11 @@ import { ConfigManager } from '../../../shared/config-manager.js'
|
||||||
import { logger } from '../../../shared/index.js'
|
import { logger } from '../../../shared/index.js'
|
||||||
import { buildFFmpegLive, ProcessOptions } from './common.js'
|
import { buildFFmpegLive, ProcessOptions } from './common.js'
|
||||||
|
|
||||||
type CustomLiveRTMPHLSTranscodingUpdatePayload =
|
type CustomLiveRTMPHLSTranscodingUpdatePayload = Omit<LiveRTMPHLSTranscodingUpdatePayload, 'resolutionPlaylistFile'> & {
|
||||||
Omit<LiveRTMPHLSTranscodingUpdatePayload, 'resolutionPlaylistFile'> & { resolutionPlaylistFile?: [ Buffer, string ] | Blob | string }
|
resolutionPlaylistFile?: [Buffer, string] | Blob | string
|
||||||
|
}
|
||||||
|
|
||||||
export class ProcessLiveRTMPHLSTranscoding {
|
export class ProcessLiveRTMPHLSTranscoding {
|
||||||
|
|
||||||
private readonly outputPath: string
|
private readonly outputPath: string
|
||||||
private readonly fsWatchers: FSWatcher[] = []
|
private readonly fsWatchers: FSWatcher[] = []
|
||||||
|
|
||||||
|
@ -326,7 +326,7 @@ export class ProcessLiveRTMPHLSTranscoding {
|
||||||
|
|
||||||
const p = payloadBuilder().then(p => this.updateWithRetry(p))
|
const p = payloadBuilder().then(p => this.updateWithRetry(p))
|
||||||
|
|
||||||
if (!sequentialPromises) sequentialPromises = p
|
if (sequentialPromises === undefined) sequentialPromises = p
|
||||||
else sequentialPromises = sequentialPromises.then(() => p)
|
else sequentialPromises = sequentialPromises.then(() => p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -140,7 +140,8 @@ export class MyVideoChannelsComponent {
|
||||||
})),
|
})),
|
||||||
switchMap(options => this.videoChannelService.listAccountVideoChannels(options))
|
switchMap(options => this.videoChannelService.listAccountVideoChannels(options))
|
||||||
)
|
)
|
||||||
.subscribe(res => {
|
.subscribe({
|
||||||
|
next: res => {
|
||||||
this.videoChannels = this.videoChannels.concat(res.data)
|
this.videoChannels = this.videoChannels.concat(res.data)
|
||||||
this.pagination.totalItems = res.total
|
this.pagination.totalItems = res.total
|
||||||
|
|
||||||
|
@ -167,6 +168,9 @@ export class MyVideoChannelsComponent {
|
||||||
this.buildChartOptions()
|
this.buildChartOptions()
|
||||||
|
|
||||||
this.onChannelDataSubject.next(res.data)
|
this.onChannelDataSubject.next(res.data)
|
||||||
|
},
|
||||||
|
|
||||||
|
error: err => this.notifier.error(err.message)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
ActorInfo,
|
ActorInfo,
|
||||||
FollowState,
|
FollowState,
|
||||||
PluginType_Type,
|
PluginType_Type,
|
||||||
|
UserNotificationData,
|
||||||
UserNotification as UserNotificationServer,
|
UserNotification as UserNotificationServer,
|
||||||
UserNotificationType,
|
UserNotificationType,
|
||||||
UserNotificationType_Type,
|
UserNotificationType_Type,
|
||||||
|
@ -21,6 +22,7 @@ export class UserNotification implements UserNotificationServer {
|
||||||
id: number
|
id: number
|
||||||
type: UserNotificationType_Type
|
type: UserNotificationType_Type
|
||||||
read: boolean
|
read: boolean
|
||||||
|
data: UserNotificationData
|
||||||
|
|
||||||
video?: VideoInfo & {
|
video?: VideoInfo & {
|
||||||
channel: ActorInfo & { avatarUrl?: string }
|
channel: ActorInfo & { avatarUrl?: string }
|
||||||
|
|
|
@ -69,7 +69,7 @@ export class PeerTubeEmbed {
|
||||||
this.peertubePlugin = new PeerTubePlugin(this.http)
|
this.peertubePlugin = new PeerTubePlugin(this.http)
|
||||||
this.peertubeTheme = new PeerTubeTheme(this.peertubePlugin)
|
this.peertubeTheme = new PeerTubeTheme(this.peertubePlugin)
|
||||||
this.playerHTML = new PlayerHTML(videoWrapperId)
|
this.playerHTML = new PlayerHTML(videoWrapperId)
|
||||||
this.playerOptionsBuilder = new PlayerOptionsBuilder(this.playerHTML, this.videoFetcher, this.peertubePlugin, this.config)
|
this.playerOptionsBuilder = new PlayerOptionsBuilder(this.playerHTML, this.videoFetcher, this.peertubePlugin)
|
||||||
this.liveManager = new LiveManager(this.playerHTML)
|
this.liveManager = new LiveManager(this.playerHTML)
|
||||||
this.requiresPassword = false
|
this.requiresPassword = false
|
||||||
|
|
||||||
|
|
|
@ -65,8 +65,7 @@ export class PlayerOptionsBuilder {
|
||||||
constructor (
|
constructor (
|
||||||
private readonly playerHTML: PlayerHTML,
|
private readonly playerHTML: PlayerHTML,
|
||||||
private readonly videoFetcher: VideoFetcher,
|
private readonly videoFetcher: VideoFetcher,
|
||||||
private readonly peertubePlugin: PeerTubePlugin,
|
private readonly peertubePlugin: PeerTubePlugin
|
||||||
private readonly serverConfig: HTMLServerConfig
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
hasAPIEnabled () {
|
hasAPIEnabled () {
|
||||||
|
|
|
@ -85,7 +85,11 @@ export default defineConfig([
|
||||||
'@typescript-eslint/no-unnecessary-boolean-literal-compare': 'off',
|
'@typescript-eslint/no-unnecessary-boolean-literal-compare': 'off',
|
||||||
'@typescript-eslint/strict-boolean-expressions': 'off',
|
'@typescript-eslint/strict-boolean-expressions': 'off',
|
||||||
'@typescript-eslint/consistent-type-definitions': 'off',
|
'@typescript-eslint/consistent-type-definitions': 'off',
|
||||||
'@typescript-eslint/no-misused-promises': 'off',
|
'@typescript-eslint/no-misused-promises': [ 'error', {
|
||||||
|
checksConditionals: true,
|
||||||
|
checksSpreads: true,
|
||||||
|
checksVoidReturn: false
|
||||||
|
} ],
|
||||||
'@typescript-eslint/no-namespace': 'off',
|
'@typescript-eslint/no-namespace': 'off',
|
||||||
'@typescript-eslint/no-empty-interface': 'off',
|
'@typescript-eslint/no-empty-interface': 'off',
|
||||||
'@typescript-eslint/no-extraneous-class': 'off',
|
'@typescript-eslint/no-extraneous-class': 'off',
|
||||||
|
|
|
@ -4,6 +4,7 @@ export * from './user-create-result.model.js'
|
||||||
export * from './user-create.model.js'
|
export * from './user-create.model.js'
|
||||||
export * from './user-flag.model.js'
|
export * from './user-flag.model.js'
|
||||||
export * from './user-login.model.js'
|
export * from './user-login.model.js'
|
||||||
|
export * from './user-notification-data.model.js'
|
||||||
export * from './user-notification-list-query.model.js'
|
export * from './user-notification-list-query.model.js'
|
||||||
export * from './user-notification-setting.model.js'
|
export * from './user-notification-setting.model.js'
|
||||||
export * from './user-notification.model.js'
|
export * from './user-notification.model.js'
|
||||||
|
|
12
packages/models/src/users/user-notification-data.model.ts
Normal file
12
packages/models/src/users/user-notification-data.model.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
export type UserNotificationData = UserNotificationDataCollaborationRejected
|
||||||
|
|
||||||
|
export interface UserNotificationDataCollaborationRejected {
|
||||||
|
channelDisplayName: string
|
||||||
|
channelHandle: string
|
||||||
|
|
||||||
|
channelOwnerDisplayName: string
|
||||||
|
channelOwnerHandle: string
|
||||||
|
|
||||||
|
collaboratorDisplayName: string
|
||||||
|
collaboratorHandle: string
|
||||||
|
}
|
|
@ -1,8 +1,10 @@
|
||||||
import { FollowState } from '../actors/index.js'
|
import { FollowState } from '../actors/index.js'
|
||||||
import { AbuseStateType } from '../moderation/index.js'
|
import { AbuseStateType } from '../moderation/index.js'
|
||||||
import { PluginType_Type } from '../plugins/index.js'
|
import { PluginType_Type } from '../plugins/index.js'
|
||||||
|
import { VideoChannelCollaboratorStateType } from '../videos/index.js'
|
||||||
import { VideoConstant } from '../videos/video-constant.model.js'
|
import { VideoConstant } from '../videos/video-constant.model.js'
|
||||||
import { VideoStateType } from '../videos/video-state.enum.js'
|
import { VideoStateType } from '../videos/video-state.enum.js'
|
||||||
|
import { UserNotificationData } from './user-notification-data.model.js'
|
||||||
|
|
||||||
export const UserNotificationType = {
|
export const UserNotificationType = {
|
||||||
NEW_VIDEO_FROM_SUBSCRIPTION: 1,
|
NEW_VIDEO_FROM_SUBSCRIPTION: 1,
|
||||||
|
@ -40,7 +42,11 @@ export const UserNotificationType = {
|
||||||
|
|
||||||
NEW_LIVE_FROM_SUBSCRIPTION: 21,
|
NEW_LIVE_FROM_SUBSCRIPTION: 21,
|
||||||
|
|
||||||
MY_VIDEO_TRANSCRIPTION_GENERATED: 22
|
MY_VIDEO_TRANSCRIPTION_GENERATED: 22,
|
||||||
|
|
||||||
|
INVITED_TO_COLLABORATE_TO_CHANNEL: 23,
|
||||||
|
ACCEPTED_TO_COLLABORATE_TO_CHANNEL: 24,
|
||||||
|
REFUSED_TO_COLLABORATE_TO_CHANNEL: 25
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type UserNotificationType_Type = typeof UserNotificationType[keyof typeof UserNotificationType]
|
export type UserNotificationType_Type = typeof UserNotificationType[keyof typeof UserNotificationType]
|
||||||
|
@ -79,6 +85,7 @@ export interface UserNotification {
|
||||||
id: number
|
id: number
|
||||||
type: UserNotificationType_Type
|
type: UserNotificationType_Type
|
||||||
read: boolean
|
read: boolean
|
||||||
|
data: UserNotificationData
|
||||||
|
|
||||||
video?: VideoInfo & {
|
video?: VideoInfo & {
|
||||||
channel: ActorInfo
|
channel: ActorInfo
|
||||||
|
@ -156,6 +163,15 @@ export interface UserNotification {
|
||||||
video: VideoInfo
|
video: VideoInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
videoChannelCollaborator?: {
|
||||||
|
id: number
|
||||||
|
|
||||||
|
state: VideoConstant<VideoChannelCollaboratorStateType>
|
||||||
|
|
||||||
|
channel: ActorInfo
|
||||||
|
account: ActorInfo
|
||||||
|
}
|
||||||
|
|
||||||
createdAt: string
|
createdAt: string
|
||||||
updatedAt: string
|
updatedAt: string
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
export * from './video-channel-collaborator.model.js'
|
||||||
export * from './video-channel-create-result.model.js'
|
export * from './video-channel-create-result.model.js'
|
||||||
export * from './video-channel-create.model.js'
|
export * from './video-channel-create.model.js'
|
||||||
export * from './video-channel-update.model.js'
|
export * from './video-channel-update.model.js'
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { AccountSummary } from '../../actors/index.js'
|
||||||
|
|
||||||
|
export const VideoChannelCollaboratorState = {
|
||||||
|
PENDING: 'PENDING',
|
||||||
|
ACCEPTED: 'ACCEPTED'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type VideoChannelCollaboratorStateType = typeof VideoChannelCollaboratorState[keyof typeof VideoChannelCollaboratorState]
|
||||||
|
|
||||||
|
export interface VideoChannelCollaborator {
|
||||||
|
id: number
|
||||||
|
account: AccountSummary
|
||||||
|
|
||||||
|
state: {
|
||||||
|
id: VideoChannelCollaboratorStateType
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
createdAt: string
|
||||||
|
updatedAt: string
|
||||||
|
}
|
|
@ -31,6 +31,7 @@ import {
|
||||||
BlacklistCommand,
|
BlacklistCommand,
|
||||||
CaptionsCommand,
|
CaptionsCommand,
|
||||||
ChangeOwnershipCommand,
|
ChangeOwnershipCommand,
|
||||||
|
ChannelCollaboratorsCommand,
|
||||||
ChannelSyncsCommand,
|
ChannelSyncsCommand,
|
||||||
ChannelsCommand,
|
ChannelsCommand,
|
||||||
ChaptersCommand,
|
ChaptersCommand,
|
||||||
|
@ -82,6 +83,7 @@ export class PeerTubeServer {
|
||||||
|
|
||||||
parallel?: boolean
|
parallel?: boolean
|
||||||
internalServerNumber: number
|
internalServerNumber: number
|
||||||
|
adminEmail: string
|
||||||
|
|
||||||
serverNumber?: number
|
serverNumber?: number
|
||||||
customConfigFile?: string
|
customConfigFile?: string
|
||||||
|
@ -170,6 +172,8 @@ export class PeerTubeServer {
|
||||||
watchedWordsLists?: WatchedWordsCommand
|
watchedWordsLists?: WatchedWordsCommand
|
||||||
autoTags?: AutomaticTagsCommand
|
autoTags?: AutomaticTagsCommand
|
||||||
|
|
||||||
|
channelCollaborators?: ChannelCollaboratorsCommand
|
||||||
|
|
||||||
constructor (options: { serverNumber: number } | { url: string }) {
|
constructor (options: { serverNumber: number } | { url: string }) {
|
||||||
if ((options as any).url) {
|
if ((options as any).url) {
|
||||||
this.setUrl((options as any).url)
|
this.setUrl((options as any).url)
|
||||||
|
@ -188,6 +192,7 @@ export class PeerTubeServer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.adminEmail = this.buildEmail()
|
||||||
this.assignCommands()
|
this.assignCommands()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -406,7 +411,7 @@ export class PeerTubeServer {
|
||||||
well_known: this.getDirectoryPath('well-known') + '/'
|
well_known: this.getDirectoryPath('well-known') + '/'
|
||||||
},
|
},
|
||||||
admin: {
|
admin: {
|
||||||
email: `admin${this.internalServerNumber}@example.com`
|
email: this.buildEmail()
|
||||||
},
|
},
|
||||||
live: {
|
live: {
|
||||||
rtmp: {
|
rtmp: {
|
||||||
|
@ -477,5 +482,11 @@ export class PeerTubeServer {
|
||||||
|
|
||||||
this.watchedWordsLists = new WatchedWordsCommand(this)
|
this.watchedWordsLists = new WatchedWordsCommand(this)
|
||||||
this.autoTags = new AutomaticTagsCommand(this)
|
this.autoTags = new AutomaticTagsCommand(this)
|
||||||
|
|
||||||
|
this.channelCollaborators = new ChannelCollaboratorsCommand(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildEmail () {
|
||||||
|
return `admin${this.internalServerNumber}@example.com`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,8 @@ import {
|
||||||
UserUpdate,
|
UserUpdate,
|
||||||
UserUpdateMe,
|
UserUpdateMe,
|
||||||
UserVideoQuota,
|
UserVideoQuota,
|
||||||
UserVideoRate
|
UserVideoRate,
|
||||||
|
VideoChannel
|
||||||
} from '@peertube/peertube-models'
|
} from '@peertube/peertube-models'
|
||||||
import { unwrapBody } from '../requests/index.js'
|
import { unwrapBody } from '../requests/index.js'
|
||||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||||
|
@ -282,6 +283,20 @@ export class UsersCommand extends AbstractCommand {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
listMyChannels (options: OverrideCommandOptions = {}) {
|
||||||
|
const path = '/api/v1/users/me/video-channels'
|
||||||
|
|
||||||
|
return this.getRequestBody<ResultList<VideoChannel>>({
|
||||||
|
...options,
|
||||||
|
|
||||||
|
path,
|
||||||
|
implicitToken: true,
|
||||||
|
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
deleteMe (options: OverrideCommandOptions = {}) {
|
deleteMe (options: OverrideCommandOptions = {}) {
|
||||||
const path = '/api/v1/users/me'
|
const path = '/api/v1/users/me'
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
import { HttpStatusCode, ResultList, VideoChannelCollaborator } from '@peertube/peertube-models'
|
||||||
|
import { unwrapBody } from '../requests/index.js'
|
||||||
|
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||||
|
|
||||||
|
export class ChannelCollaboratorsCommand extends AbstractCommand {
|
||||||
|
list (
|
||||||
|
options: OverrideCommandOptions & {
|
||||||
|
channel: string
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { channel } = options
|
||||||
|
const path = '/api/v1/video-channels/' + channel + '/collaborators'
|
||||||
|
|
||||||
|
return this.getRequestBody<ResultList<VideoChannelCollaborator>>({
|
||||||
|
...options,
|
||||||
|
|
||||||
|
path,
|
||||||
|
implicitToken: true,
|
||||||
|
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async invite (
|
||||||
|
options: OverrideCommandOptions & {
|
||||||
|
channel: string
|
||||||
|
target: string
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { channel, target } = options
|
||||||
|
const path = '/api/v1/video-channels/' + channel + '/collaborators/invite'
|
||||||
|
|
||||||
|
const body = await unwrapBody<{ collaborator: VideoChannelCollaborator }>(this.postBodyRequest({
|
||||||
|
...options,
|
||||||
|
|
||||||
|
path,
|
||||||
|
fields: {
|
||||||
|
accountHandle: target
|
||||||
|
},
|
||||||
|
implicitToken: true,
|
||||||
|
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||||
|
}))
|
||||||
|
|
||||||
|
return body.collaborator
|
||||||
|
}
|
||||||
|
|
||||||
|
accept (
|
||||||
|
options: OverrideCommandOptions & {
|
||||||
|
channel: string
|
||||||
|
id: number
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { id, channel } = options
|
||||||
|
const path = '/api/v1/video-channels/' + channel + '/collaborators/' + id + '/accept'
|
||||||
|
|
||||||
|
return this.postBodyRequest({
|
||||||
|
...options,
|
||||||
|
|
||||||
|
path,
|
||||||
|
implicitToken: true,
|
||||||
|
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
reject (
|
||||||
|
options: OverrideCommandOptions & {
|
||||||
|
channel: string
|
||||||
|
id: number
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { id, channel } = options
|
||||||
|
const path = '/api/v1/video-channels/' + channel + '/collaborators/' + id + '/reject'
|
||||||
|
|
||||||
|
return this.postBodyRequest({
|
||||||
|
...options,
|
||||||
|
|
||||||
|
path,
|
||||||
|
implicitToken: true,
|
||||||
|
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
remove (
|
||||||
|
options: OverrideCommandOptions & {
|
||||||
|
channel: string
|
||||||
|
id: number
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { id, channel } = options
|
||||||
|
const path = '/api/v1/video-channels/' + channel + '/collaborators/' + id
|
||||||
|
|
||||||
|
return this.deleteRequest({
|
||||||
|
...options,
|
||||||
|
|
||||||
|
path,
|
||||||
|
implicitToken: true,
|
||||||
|
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -39,6 +39,7 @@ export class ChannelsCommand extends AbstractCommand {
|
||||||
sort?: string
|
sort?: string
|
||||||
withStats?: boolean
|
withStats?: boolean
|
||||||
search?: string
|
search?: string
|
||||||
|
includeCollaborations?: boolean
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const { accountName, sort = 'createdAt' } = options
|
const { accountName, sort = 'createdAt' } = options
|
||||||
|
@ -48,7 +49,7 @@ export class ChannelsCommand extends AbstractCommand {
|
||||||
...options,
|
...options,
|
||||||
|
|
||||||
path,
|
path,
|
||||||
query: { sort, ...pick(options, [ 'start', 'count', 'withStats', 'search' ]) },
|
query: { sort, ...pick(options, [ 'start', 'count', 'withStats', 'search', 'includeCollaborations' ]) },
|
||||||
implicitToken: false,
|
implicitToken: false,
|
||||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||||
})
|
})
|
||||||
|
|
|
@ -23,7 +23,6 @@ type ListForAdminOrAccountCommonOptions = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CommentsCommand extends AbstractCommand {
|
export class CommentsCommand extends AbstractCommand {
|
||||||
|
|
||||||
private lastVideoId: number | string
|
private lastVideoId: number | string
|
||||||
private lastThreadId: number
|
private lastThreadId: number
|
||||||
private lastReplyId: number
|
private lastReplyId: number
|
||||||
|
@ -51,6 +50,7 @@ export class CommentsCommand extends AbstractCommand {
|
||||||
|
|
||||||
listCommentsOnMyVideos (options: OverrideCommandOptions & ListForAdminOrAccountCommonOptions & {
|
listCommentsOnMyVideos (options: OverrideCommandOptions & ListForAdminOrAccountCommonOptions & {
|
||||||
isHeldForReview?: boolean
|
isHeldForReview?: boolean
|
||||||
|
includeCollaborations?: boolean
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const path = '/api/v1/users/me/videos/comments'
|
const path = '/api/v1/users/me/videos/comments'
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ export class CommentsCommand extends AbstractCommand {
|
||||||
query: {
|
query: {
|
||||||
...this.buildListForAdminOrAccountQuery(options),
|
...this.buildListForAdminOrAccountQuery(options),
|
||||||
|
|
||||||
isHeldForReview: options.isHeldForReview
|
...pick(options, [ 'isHeldForReview', 'includeCollaborations' ])
|
||||||
},
|
},
|
||||||
implicitToken: true,
|
implicitToken: true,
|
||||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||||
|
@ -78,13 +78,15 @@ export class CommentsCommand extends AbstractCommand {
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
listThreads (options: OverrideCommandOptions & {
|
listThreads (
|
||||||
|
options: OverrideCommandOptions & {
|
||||||
videoId: number | string
|
videoId: number | string
|
||||||
videoPassword?: string
|
videoPassword?: string
|
||||||
start?: number
|
start?: number
|
||||||
count?: number
|
count?: number
|
||||||
sort?: string
|
sort?: string
|
||||||
}) {
|
}
|
||||||
|
) {
|
||||||
const { start, count, sort, videoId, videoPassword } = options
|
const { start, count, sort, videoId, videoPassword } = options
|
||||||
const path = '/api/v1/videos/' + videoId + '/comment-threads'
|
const path = '/api/v1/videos/' + videoId + '/comment-threads'
|
||||||
|
|
||||||
|
@ -99,10 +101,12 @@ export class CommentsCommand extends AbstractCommand {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
getThread (options: OverrideCommandOptions & {
|
getThread (
|
||||||
|
options: OverrideCommandOptions & {
|
||||||
videoId: number | string
|
videoId: number | string
|
||||||
threadId: number
|
threadId: number
|
||||||
}) {
|
}
|
||||||
|
) {
|
||||||
const { videoId, threadId } = options
|
const { videoId, threadId } = options
|
||||||
const path = '/api/v1/videos/' + videoId + '/comment-threads/' + threadId
|
const path = '/api/v1/videos/' + videoId + '/comment-threads/' + threadId
|
||||||
|
|
||||||
|
@ -115,21 +119,25 @@ export class CommentsCommand extends AbstractCommand {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async getThreadOf (options: OverrideCommandOptions & {
|
async getThreadOf (
|
||||||
|
options: OverrideCommandOptions & {
|
||||||
videoId: number | string
|
videoId: number | string
|
||||||
text: string
|
text: string
|
||||||
}) {
|
}
|
||||||
|
) {
|
||||||
const { videoId, text } = options
|
const { videoId, text } = options
|
||||||
const threadId = await this.findCommentId({ videoId, text })
|
const threadId = await this.findCommentId({ videoId, text })
|
||||||
|
|
||||||
return this.getThread({ ...options, videoId, threadId })
|
return this.getThread({ ...options, videoId, threadId })
|
||||||
}
|
}
|
||||||
|
|
||||||
async createThread (options: OverrideCommandOptions & {
|
async createThread (
|
||||||
|
options: OverrideCommandOptions & {
|
||||||
videoId: number | string
|
videoId: number | string
|
||||||
text: string
|
text: string
|
||||||
videoPassword?: string
|
videoPassword?: string
|
||||||
}) {
|
}
|
||||||
|
) {
|
||||||
const { videoId, text, videoPassword } = options
|
const { videoId, text, videoPassword } = options
|
||||||
const path = '/api/v1/videos/' + videoId + '/comment-threads'
|
const path = '/api/v1/videos/' + videoId + '/comment-threads'
|
||||||
|
|
||||||
|
@ -149,12 +157,14 @@ export class CommentsCommand extends AbstractCommand {
|
||||||
return body.comment
|
return body.comment
|
||||||
}
|
}
|
||||||
|
|
||||||
async addReply (options: OverrideCommandOptions & {
|
async addReply (
|
||||||
|
options: OverrideCommandOptions & {
|
||||||
videoId: number | string
|
videoId: number | string
|
||||||
toCommentId: number
|
toCommentId: number
|
||||||
text: string
|
text: string
|
||||||
videoPassword?: string
|
videoPassword?: string
|
||||||
}) {
|
}
|
||||||
|
) {
|
||||||
const { videoId, toCommentId, text, videoPassword } = options
|
const { videoId, toCommentId, text, videoPassword } = options
|
||||||
const path = '/api/v1/videos/' + videoId + '/comments/' + toCommentId
|
const path = '/api/v1/videos/' + videoId + '/comments/' + toCommentId
|
||||||
|
|
||||||
|
@ -173,22 +183,28 @@ export class CommentsCommand extends AbstractCommand {
|
||||||
return body.comment
|
return body.comment
|
||||||
}
|
}
|
||||||
|
|
||||||
async addReplyToLastReply (options: OverrideCommandOptions & {
|
async addReplyToLastReply (
|
||||||
|
options: OverrideCommandOptions & {
|
||||||
text: string
|
text: string
|
||||||
}) {
|
}
|
||||||
|
) {
|
||||||
return this.addReply({ ...options, videoId: this.lastVideoId, toCommentId: this.lastReplyId })
|
return this.addReply({ ...options, videoId: this.lastVideoId, toCommentId: this.lastReplyId })
|
||||||
}
|
}
|
||||||
|
|
||||||
async addReplyToLastThread (options: OverrideCommandOptions & {
|
async addReplyToLastThread (
|
||||||
|
options: OverrideCommandOptions & {
|
||||||
text: string
|
text: string
|
||||||
}) {
|
}
|
||||||
|
) {
|
||||||
return this.addReply({ ...options, videoId: this.lastVideoId, toCommentId: this.lastThreadId })
|
return this.addReply({ ...options, videoId: this.lastVideoId, toCommentId: this.lastThreadId })
|
||||||
}
|
}
|
||||||
|
|
||||||
async findCommentId (options: OverrideCommandOptions & {
|
async findCommentId (
|
||||||
|
options: OverrideCommandOptions & {
|
||||||
videoId: number | string
|
videoId: number | string
|
||||||
text: string
|
text: string
|
||||||
}) {
|
}
|
||||||
|
) {
|
||||||
const { videoId, text } = options
|
const { videoId, text } = options
|
||||||
const { data } = await this.listForAdmin({ videoId, count: 25, sort: '-createdAt' })
|
const { data } = await this.listForAdmin({ videoId, count: 25, sort: '-createdAt' })
|
||||||
|
|
||||||
|
@ -197,10 +213,12 @@ export class CommentsCommand extends AbstractCommand {
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
delete (options: OverrideCommandOptions & {
|
delete (
|
||||||
|
options: OverrideCommandOptions & {
|
||||||
videoId: number | string
|
videoId: number | string
|
||||||
commentId: number
|
commentId: number
|
||||||
}) {
|
}
|
||||||
|
) {
|
||||||
const { videoId, commentId } = options
|
const { videoId, commentId } = options
|
||||||
const path = '/api/v1/videos/' + videoId + '/comments/' + commentId
|
const path = '/api/v1/videos/' + videoId + '/comments/' + commentId
|
||||||
|
|
||||||
|
@ -213,9 +231,11 @@ export class CommentsCommand extends AbstractCommand {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteAllComments (options: OverrideCommandOptions & {
|
async deleteAllComments (
|
||||||
|
options: OverrideCommandOptions & {
|
||||||
videoUUID: string
|
videoUUID: string
|
||||||
}) {
|
}
|
||||||
|
) {
|
||||||
const { data } = await this.listForAdmin({ ...options, start: 0, count: 20 })
|
const { data } = await this.listForAdmin({ ...options, start: 0, count: 20 })
|
||||||
|
|
||||||
for (const comment of data) {
|
for (const comment of data) {
|
||||||
|
@ -227,10 +247,12 @@ export class CommentsCommand extends AbstractCommand {
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
approve (options: OverrideCommandOptions & {
|
approve (
|
||||||
|
options: OverrideCommandOptions & {
|
||||||
videoId: number | string
|
videoId: number | string
|
||||||
commentId: number
|
commentId: number
|
||||||
}) {
|
}
|
||||||
|
) {
|
||||||
const { videoId, commentId } = options
|
const { videoId, commentId } = options
|
||||||
const path = '/api/v1/videos/' + videoId + '/comments/' + commentId + '/approve'
|
const path = '/api/v1/videos/' + videoId + '/comments/' + commentId + '/approve'
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
export * from './blacklist-command.js'
|
export * from './blacklist-command.js'
|
||||||
export * from './captions-command.js'
|
export * from './captions-command.js'
|
||||||
|
export * from './channel-collaborators-command.js'
|
||||||
export * from './change-ownership-command.js'
|
export * from './change-ownership-command.js'
|
||||||
export * from './channels.js'
|
export * from './channels.js'
|
||||||
export * from './channels-command.js'
|
export * from './channels-command.js'
|
||||||
|
|
|
@ -71,10 +71,11 @@ export class PlaylistsCommand extends AbstractCommand {
|
||||||
sort?: string
|
sort?: string
|
||||||
search?: string
|
search?: string
|
||||||
playlistType?: VideoPlaylistType_Type
|
playlistType?: VideoPlaylistType_Type
|
||||||
|
includeCollaborations?: boolean
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const path = '/api/v1/accounts/' + options.handle + '/video-playlists'
|
const path = '/api/v1/accounts/' + options.handle + '/video-playlists'
|
||||||
const query = pick(options, [ 'start', 'count', 'sort', 'search', 'playlistType' ])
|
const query = pick(options, [ 'start', 'count', 'sort', 'search', 'playlistType', 'includeCollaborations' ])
|
||||||
|
|
||||||
return this.getRequestBody<ResultList<VideoPlaylist>>({
|
return this.getRequestBody<ResultList<VideoPlaylist>>({
|
||||||
...options,
|
...options,
|
||||||
|
|
|
@ -229,14 +229,18 @@ export class VideosCommand extends AbstractCommand {
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
listMyVideos (options: OverrideCommandOptions & VideosCommonQuery & { channelId?: number, channelNameOneOf?: string[] } = {}) {
|
listMyVideos (options: OverrideCommandOptions & VideosCommonQuery & {
|
||||||
|
channelId?: number
|
||||||
|
channelNameOneOf?: string[]
|
||||||
|
includeCollaborations?: boolean
|
||||||
|
} = {}) {
|
||||||
const path = '/api/v1/users/me/videos'
|
const path = '/api/v1/users/me/videos'
|
||||||
|
|
||||||
return this.getRequestBody<ResultList<Video>>({
|
return this.getRequestBody<ResultList<Video>>({
|
||||||
...options,
|
...options,
|
||||||
|
|
||||||
path,
|
path,
|
||||||
query: { ...this.buildListQuery(options), ...pick(options, [ 'channelId', 'channelNameOneOf' ]) },
|
query: { ...this.buildListQuery(options), ...pick(options, [ 'channelId', 'channelNameOneOf', 'includeCollaborations' ]) },
|
||||||
implicitToken: true,
|
implicitToken: true,
|
||||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||||
})
|
})
|
||||||
|
|
445
packages/tests/src/api/check-params/channel-collaborators.ts
Normal file
445
packages/tests/src/api/check-params/channel-collaborators.ts
Normal file
|
@ -0,0 +1,445 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
|
|
||||||
|
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||||
|
import {
|
||||||
|
cleanupTests,
|
||||||
|
createMultipleServers,
|
||||||
|
doubleFollow,
|
||||||
|
PeerTubeServer,
|
||||||
|
setAccessTokensToServers,
|
||||||
|
waitJobs
|
||||||
|
} from '@peertube/peertube-server-commands'
|
||||||
|
|
||||||
|
describe('Test video channel collaborators API validators', function () {
|
||||||
|
let server: PeerTubeServer
|
||||||
|
let remoteServer: PeerTubeServer
|
||||||
|
|
||||||
|
let collaboratorToken: string
|
||||||
|
let collaboratorId: number
|
||||||
|
|
||||||
|
let collaboratorId2: number
|
||||||
|
|
||||||
|
let unrelatedCollaboratorId: number
|
||||||
|
|
||||||
|
let userToken: string
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
this.timeout(60000)
|
||||||
|
|
||||||
|
const servers = await createMultipleServers(2)
|
||||||
|
|
||||||
|
server = servers[0]
|
||||||
|
remoteServer = servers[1]
|
||||||
|
await setAccessTokensToServers(servers)
|
||||||
|
await doubleFollow(servers[0], servers[1])
|
||||||
|
|
||||||
|
await servers[1].videos.quickUpload({ name: 'remote video' })
|
||||||
|
|
||||||
|
collaboratorToken = await server.users.generateUserAndToken('collaborator')
|
||||||
|
await server.users.generateUserAndToken('collaborator2')
|
||||||
|
userToken = await server.users.generateUserAndToken('user1')
|
||||||
|
|
||||||
|
await server.users.generateUserAndToken('user2')
|
||||||
|
|
||||||
|
const { id } = await server.channelCollaborators.invite({ channel: 'user1_channel', target: 'user2' })
|
||||||
|
unrelatedCollaboratorId = id
|
||||||
|
|
||||||
|
await waitJobs(servers)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Invite', function () {
|
||||||
|
it('Should fail when not authenticated', async function () {
|
||||||
|
await server.channelCollaborators.invite({
|
||||||
|
token: null,
|
||||||
|
channel: 'root_channel',
|
||||||
|
target: 'collaborator',
|
||||||
|
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with a bad channel handle', async function () {
|
||||||
|
await server.channelCollaborators.invite({
|
||||||
|
channel: 'bad handle',
|
||||||
|
target: 'collaborator',
|
||||||
|
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||||
|
})
|
||||||
|
|
||||||
|
await server.channelCollaborators.invite({
|
||||||
|
channel: 'root_channel@' + remoteServer.host,
|
||||||
|
target: 'collaborator',
|
||||||
|
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with a non owned channel', async function () {
|
||||||
|
await server.channelCollaborators.invite({
|
||||||
|
token: userToken,
|
||||||
|
channel: 'root_channel',
|
||||||
|
target: 'collaborator',
|
||||||
|
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with a bad account handle', async function () {
|
||||||
|
await server.channelCollaborators.invite({
|
||||||
|
channel: 'root_channel',
|
||||||
|
target: 'bad handle',
|
||||||
|
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||||
|
})
|
||||||
|
|
||||||
|
await server.channelCollaborators.invite({
|
||||||
|
channel: 'root_channel',
|
||||||
|
target: 'root@' + remoteServer.host,
|
||||||
|
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with myself', async function () {
|
||||||
|
await server.channelCollaborators.invite({
|
||||||
|
channel: 'root_channel',
|
||||||
|
target: 'root',
|
||||||
|
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should succeed with the correct parameters', async function () {
|
||||||
|
{
|
||||||
|
const { id } = await server.channelCollaborators.invite({ channel: 'root_channel', target: 'collaborator' })
|
||||||
|
collaboratorId = id
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const { id } = await server.channelCollaborators.invite({ channel: 'root_channel', target: 'collaborator2' })
|
||||||
|
collaboratorId2 = id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail to re-invite the user', async function () {
|
||||||
|
await server.channelCollaborators.invite({
|
||||||
|
channel: 'root_channel',
|
||||||
|
target: 'collaborator',
|
||||||
|
expectedStatus: HttpStatusCode.CONFLICT_409
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Common Accept/Reject', function () {
|
||||||
|
it('Should fail when not authenticated', async function () {
|
||||||
|
const options = {
|
||||||
|
token: null,
|
||||||
|
channel: 'root_channel',
|
||||||
|
id: collaboratorId,
|
||||||
|
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||||
|
}
|
||||||
|
|
||||||
|
await server.channelCollaborators.accept(options)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail to accept the collaborator with another user', async function () {
|
||||||
|
const options = {
|
||||||
|
token: userToken,
|
||||||
|
channel: 'root_channel',
|
||||||
|
id: collaboratorId,
|
||||||
|
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||||
|
}
|
||||||
|
|
||||||
|
await server.channelCollaborators.accept(options)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with an invalid collaborator id', async function () {
|
||||||
|
{
|
||||||
|
const options = {
|
||||||
|
token: collaboratorToken,
|
||||||
|
channel: 'root_channel',
|
||||||
|
id: 'toto' as any,
|
||||||
|
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||||
|
}
|
||||||
|
|
||||||
|
await server.channelCollaborators.accept(options)
|
||||||
|
|
||||||
|
{
|
||||||
|
const options = {
|
||||||
|
token: collaboratorToken,
|
||||||
|
channel: 'root_channel',
|
||||||
|
id: 42,
|
||||||
|
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||||
|
}
|
||||||
|
|
||||||
|
await server.channelCollaborators.accept(options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with a bad channel handle', async function () {
|
||||||
|
{
|
||||||
|
const options = {
|
||||||
|
token: collaboratorToken,
|
||||||
|
channel: 'bad handle',
|
||||||
|
id: collaboratorId,
|
||||||
|
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||||
|
}
|
||||||
|
|
||||||
|
await server.channelCollaborators.accept(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const options = {
|
||||||
|
token: collaboratorToken,
|
||||||
|
channel: 'root_channel@' + remoteServer.host,
|
||||||
|
id: collaboratorId,
|
||||||
|
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||||
|
}
|
||||||
|
|
||||||
|
await server.channelCollaborators.accept(options)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with another channel than the collaborator id', async function () {
|
||||||
|
const options = {
|
||||||
|
token: collaboratorToken,
|
||||||
|
channel: 'user1_channel',
|
||||||
|
id: collaboratorId,
|
||||||
|
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||||
|
}
|
||||||
|
|
||||||
|
await server.channelCollaborators.accept(options)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail to accept another collaborator invitation', async function () {
|
||||||
|
const options = {
|
||||||
|
token: collaboratorToken,
|
||||||
|
channel: 'root_channel',
|
||||||
|
id: collaboratorId2,
|
||||||
|
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||||
|
}
|
||||||
|
|
||||||
|
await server.channelCollaborators.accept(options)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with a channel not related to the collaborator id', async function () {
|
||||||
|
{
|
||||||
|
const options = {
|
||||||
|
token: collaboratorToken,
|
||||||
|
channel: 'root_channel',
|
||||||
|
id: unrelatedCollaboratorId,
|
||||||
|
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||||
|
}
|
||||||
|
|
||||||
|
await server.channelCollaborators.accept(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const options = {
|
||||||
|
token: collaboratorToken,
|
||||||
|
channel: 'user1_channel',
|
||||||
|
id: collaboratorId,
|
||||||
|
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||||
|
}
|
||||||
|
|
||||||
|
await server.channelCollaborators.accept(options)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Accept', function () {
|
||||||
|
it('Should succeed with the correct params', async function () {
|
||||||
|
await server.channelCollaborators.accept({
|
||||||
|
token: collaboratorToken,
|
||||||
|
channel: 'root_channel',
|
||||||
|
id: collaboratorId
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail to re-accept the same collaborator', async function () {
|
||||||
|
await server.channelCollaborators.accept({
|
||||||
|
token: collaboratorToken,
|
||||||
|
channel: 'root_channel',
|
||||||
|
id: collaboratorId,
|
||||||
|
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail to invite an accepted collaborator', async function () {
|
||||||
|
await server.channelCollaborators.invite({
|
||||||
|
channel: 'root_channel',
|
||||||
|
target: 'collaborator',
|
||||||
|
expectedStatus: HttpStatusCode.CONFLICT_409
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail to invite another collaborator with an existing collaborator token', async function () {
|
||||||
|
await server.channelCollaborators.invite({
|
||||||
|
token: collaboratorToken,
|
||||||
|
channel: 'root_channel',
|
||||||
|
target: 'collaborator2',
|
||||||
|
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Remove', function () {
|
||||||
|
it('Should fail when not authenticated', async function () {
|
||||||
|
await server.channelCollaborators.remove({
|
||||||
|
token: null,
|
||||||
|
channel: 'root_channel',
|
||||||
|
id: collaboratorId,
|
||||||
|
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail to remove the collaborator with another user', async function () {
|
||||||
|
await server.channelCollaborators.remove({
|
||||||
|
token: userToken,
|
||||||
|
channel: 'root_channel',
|
||||||
|
id: collaboratorId,
|
||||||
|
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with an invalid collaborator id', async function () {
|
||||||
|
await server.channelCollaborators.remove({
|
||||||
|
token: collaboratorToken,
|
||||||
|
channel: 'root_channel',
|
||||||
|
id: 'toto' as any,
|
||||||
|
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||||
|
})
|
||||||
|
|
||||||
|
await server.channelCollaborators.remove({
|
||||||
|
token: collaboratorToken,
|
||||||
|
channel: 'root_channel',
|
||||||
|
id: 42,
|
||||||
|
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with a bad channel handle', async function () {
|
||||||
|
await server.channelCollaborators.remove({
|
||||||
|
token: collaboratorToken,
|
||||||
|
channel: 'bad handle',
|
||||||
|
id: collaboratorId,
|
||||||
|
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||||
|
})
|
||||||
|
|
||||||
|
await server.channelCollaborators.remove({
|
||||||
|
token: collaboratorToken,
|
||||||
|
channel: 'root@' + remoteServer.host,
|
||||||
|
id: collaboratorId,
|
||||||
|
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with a channel not related to the collaborator id', async function () {
|
||||||
|
await server.channelCollaborators.remove({
|
||||||
|
token: collaboratorToken,
|
||||||
|
channel: 'root_channel',
|
||||||
|
id: unrelatedCollaboratorId,
|
||||||
|
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||||
|
})
|
||||||
|
|
||||||
|
await server.channelCollaborators.remove({
|
||||||
|
token: collaboratorToken,
|
||||||
|
channel: 'user1_channel',
|
||||||
|
id: collaboratorId,
|
||||||
|
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should succeed with the correct params', async function () {
|
||||||
|
await server.channelCollaborators.remove({
|
||||||
|
token: collaboratorToken,
|
||||||
|
channel: 'root_channel',
|
||||||
|
id: collaboratorId
|
||||||
|
})
|
||||||
|
|
||||||
|
await server.channelCollaborators.remove({
|
||||||
|
token: server.accessToken,
|
||||||
|
channel: 'root_channel',
|
||||||
|
id: collaboratorId2
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Reject', function () {
|
||||||
|
before(async function () {
|
||||||
|
const { id } = await server.channelCollaborators.invite({ channel: 'root_channel', target: 'collaborator' })
|
||||||
|
collaboratorId = id
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should succeed with the correct params', async function () {
|
||||||
|
await server.channelCollaborators.reject({
|
||||||
|
token: collaboratorToken,
|
||||||
|
channel: 'root_channel',
|
||||||
|
id: collaboratorId
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail to reject the same collaborator', async function () {
|
||||||
|
await server.channelCollaborators.reject({
|
||||||
|
token: collaboratorToken,
|
||||||
|
channel: 'root_channel',
|
||||||
|
id: collaboratorId,
|
||||||
|
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('List', function () {
|
||||||
|
before(async function () {
|
||||||
|
const { id } = await server.channelCollaborators.invite({ channel: 'root_channel', target: 'collaborator' })
|
||||||
|
collaboratorId = id
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail when not authenticated', async function () {
|
||||||
|
await server.channelCollaborators.list({
|
||||||
|
token: null,
|
||||||
|
channel: 'root_channel',
|
||||||
|
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with a bad channel handle', async function () {
|
||||||
|
await server.channelCollaborators.list({
|
||||||
|
token: userToken,
|
||||||
|
channel: 'bad handle',
|
||||||
|
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||||
|
})
|
||||||
|
|
||||||
|
await server.channelCollaborators.list({
|
||||||
|
token: userToken,
|
||||||
|
channel: 'root@' + remoteServer.host,
|
||||||
|
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with a non owned channel', async function () {
|
||||||
|
await server.channelCollaborators.list({
|
||||||
|
token: userToken,
|
||||||
|
channel: 'root_channel',
|
||||||
|
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail to list if the collaborator is not accepted yet', async function () {
|
||||||
|
await server.channelCollaborators.list({
|
||||||
|
token: collaboratorToken,
|
||||||
|
channel: 'root_channel',
|
||||||
|
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||||
|
})
|
||||||
|
|
||||||
|
await server.channelCollaborators.accept({ token: collaboratorToken, channel: 'root_channel', id: collaboratorId })
|
||||||
|
|
||||||
|
await server.channelCollaborators.list({
|
||||||
|
token: collaboratorToken,
|
||||||
|
channel: 'root_channel'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should succeed with the correct parameters', async function () {
|
||||||
|
await server.channelCollaborators.list({ channel: 'root_channel' })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async function () {
|
||||||
|
await cleanupTests([ server, remoteServer ])
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,8 +1,7 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
|
|
||||||
import { FIXTURE_URLS } from '@tests/shared/fixture-urls.js'
|
|
||||||
import { areHttpImportTestsDisabled } from '@peertube/peertube-node-utils'
|
|
||||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||||
|
import { areHttpImportTestsDisabled } from '@peertube/peertube-node-utils'
|
||||||
import {
|
import {
|
||||||
ChannelsCommand,
|
ChannelsCommand,
|
||||||
cleanupTests,
|
cleanupTests,
|
||||||
|
@ -11,6 +10,7 @@ import {
|
||||||
setAccessTokensToServers,
|
setAccessTokensToServers,
|
||||||
setDefaultVideoChannel
|
setDefaultVideoChannel
|
||||||
} from '@peertube/peertube-server-commands'
|
} from '@peertube/peertube-server-commands'
|
||||||
|
import { FIXTURE_URLS } from '@tests/shared/fixture-urls.js'
|
||||||
|
|
||||||
describe('Test videos import in a channel API validator', function () {
|
describe('Test videos import in a channel API validator', function () {
|
||||||
let server: PeerTubeServer
|
let server: PeerTubeServer
|
||||||
|
|
|
@ -186,7 +186,7 @@ describe('Test video lives API validator', function () {
|
||||||
it('Should fail with a bad channel', async function () {
|
it('Should fail with a bad channel', async function () {
|
||||||
const fields = { ...baseCorrectParams, channelId: 545454 }
|
const fields = { ...baseCorrectParams, channelId: 545454 }
|
||||||
|
|
||||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should fail with a bad privacy for replay settings', async function () {
|
it('Should fail with a bad privacy for replay settings', async function () {
|
||||||
|
@ -208,7 +208,7 @@ describe('Test video lives API validator', function () {
|
||||||
|
|
||||||
const fields = { ...baseCorrectParams, channelId: customChannelId }
|
const fields = { ...baseCorrectParams, channelId: customChannelId }
|
||||||
|
|
||||||
await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields })
|
await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should fail with too many tags', async function () {
|
it('Should fail with too many tags', async function () {
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
|
|
||||||
import { expect } from 'chai'
|
|
||||||
import { omit } from '@peertube/peertube-core-utils'
|
import { omit } from '@peertube/peertube-core-utils'
|
||||||
import { HttpStatusCode, VideoChannelUpdate } from '@peertube/peertube-models'
|
import { HttpStatusCode, VideoChannelUpdate } from '@peertube/peertube-models'
|
||||||
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
|
|
||||||
import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
|
import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
|
||||||
import {
|
import {
|
||||||
ChannelsCommand,
|
ChannelsCommand,
|
||||||
|
@ -16,6 +14,8 @@ import {
|
||||||
PeerTubeServer,
|
PeerTubeServer,
|
||||||
setAccessTokensToServers
|
setAccessTokensToServers
|
||||||
} from '@peertube/peertube-server-commands'
|
} from '@peertube/peertube-server-commands'
|
||||||
|
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
|
||||||
describe('Test video channels API validator', function () {
|
describe('Test video channels API validator', function () {
|
||||||
const videoChannelPath = '/api/v1/video-channels'
|
const videoChannelPath = '/api/v1/video-channels'
|
||||||
|
|
|
@ -229,7 +229,7 @@ describe('Test video imports API validator', function () {
|
||||||
it('Should fail with a bad channel', async function () {
|
it('Should fail with a bad channel', async function () {
|
||||||
const fields = { ...baseCorrectParams, channelId: 545454 }
|
const fields = { ...baseCorrectParams, channelId: 545454 }
|
||||||
|
|
||||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should fail with another user channel', async function () {
|
it('Should fail with another user channel', async function () {
|
||||||
|
@ -245,7 +245,7 @@ describe('Test video imports API validator', function () {
|
||||||
|
|
||||||
const fields = { ...baseCorrectParams, channelId: customChannelId }
|
const fields = { ...baseCorrectParams, channelId: customChannelId }
|
||||||
|
|
||||||
await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields })
|
await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should fail with too many tags', async function () {
|
it('Should fail with too many tags', async function () {
|
||||||
|
|
|
@ -260,7 +260,7 @@ describe('Test video playlists API validator', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should fail with an unknown video channel id', async function () {
|
it('Should fail with an unknown video channel id', async function () {
|
||||||
const params = getBase({ videoChannelId: 42 }, { expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
const params = getBase({ videoChannelId: 42 }, { expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||||
|
|
||||||
await command.create(params)
|
await command.create(params)
|
||||||
await command.update(getUpdate(params, playlist.shortUUID))
|
await command.update(getUpdate(params, playlist.shortUUID))
|
||||||
|
@ -307,7 +307,7 @@ describe('Test video playlists API validator', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should fail to set a playlist to a channel owned by another user', async function () {
|
it('Should fail to set a playlist to a channel owned by another user', async function () {
|
||||||
const params = getBase({}, { token: userAccessToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
const params = getBase({}, { token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||||
|
|
||||||
await command.create(params)
|
await command.create(params)
|
||||||
await command.update(getUpdate(params, userPlaylist.shortUUID))
|
await command.update(getUpdate(params, userPlaylist.shortUUID))
|
||||||
|
|
|
@ -358,7 +358,11 @@ describe('Test videos API validator', function () {
|
||||||
const fields = { ...baseCorrectParams, channelId: 545454 }
|
const fields = { ...baseCorrectParams, channelId: 545454 }
|
||||||
const attaches = baseCorrectAttaches
|
const attaches = baseCorrectAttaches
|
||||||
|
|
||||||
await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } })
|
await checkUploadVideoParam({
|
||||||
|
...baseOptions(),
|
||||||
|
attributes: { ...fields, ...attaches },
|
||||||
|
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should fail with another user channel', async function () {
|
it('Should fail with another user channel', async function () {
|
||||||
|
@ -378,7 +382,8 @@ describe('Test videos API validator', function () {
|
||||||
await checkUploadVideoParam({
|
await checkUploadVideoParam({
|
||||||
...baseOptions(),
|
...baseOptions(),
|
||||||
token: userAccessToken,
|
token: userAccessToken,
|
||||||
attributes: { ...fields, ...attaches }
|
attributes: { ...fields, ...attaches },
|
||||||
|
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -680,7 +685,13 @@ describe('Test videos API validator', function () {
|
||||||
it('Should fail with a bad channel', async function () {
|
it('Should fail with a bad channel', async function () {
|
||||||
const fields = { ...baseCorrectParams, channelId: 545454 }
|
const fields = { ...baseCorrectParams, channelId: 545454 }
|
||||||
|
|
||||||
await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields })
|
await makePutBodyRequest({
|
||||||
|
url: server.url,
|
||||||
|
path: path + video.shortUUID,
|
||||||
|
token: server.accessToken,
|
||||||
|
fields,
|
||||||
|
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should fail with too many tags', async function () {
|
it('Should fail with too many tags', async function () {
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
|
|
||||||
import { expect } from 'chai'
|
|
||||||
import { wait } from '@peertube/peertube-core-utils'
|
import { wait } from '@peertube/peertube-core-utils'
|
||||||
import { PluginType, UserNotification, UserNotificationType } from '@peertube/peertube-models'
|
import { PluginType, UserNotification, UserNotificationType } from '@peertube/peertube-models'
|
||||||
import { cleanupTests, PeerTubeServer } from '@peertube/peertube-server-commands'
|
import { cleanupTests, PeerTubeServer } from '@peertube/peertube-server-commands'
|
||||||
import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
|
import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
|
||||||
import { MockJoinPeerTubeVersions } from '@tests/shared/mock-servers/mock-joinpeertube-versions.js'
|
import { MockJoinPeerTubeVersions } from '@tests/shared/mock-servers/mock-joinpeertube-versions.js'
|
||||||
import { CheckerBaseParams, prepareNotificationsTest, checkNewPeerTubeVersion, checkNewPluginVersion } from '@tests/shared/notifications.js'
|
import { checkNewPeerTubeVersion, checkNewPluginVersion } from '@tests/shared/notifications/check-admin-notifications.js'
|
||||||
|
import { prepareNotificationsTest } from '@tests/shared/notifications/notifications-common.js'
|
||||||
|
import { CheckerBaseParams } from '@tests/shared/notifications/shared/notification-checker.js'
|
||||||
import { SQLCommand } from '@tests/shared/sql-command.js'
|
import { SQLCommand } from '@tests/shared/sql-command.js'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
|
||||||
describe('Test admin notifications', function () {
|
describe('Test admin notifications', function () {
|
||||||
let server: PeerTubeServer
|
let server: PeerTubeServer
|
||||||
|
|
|
@ -3,7 +3,10 @@
|
||||||
import { UserNotification } from '@peertube/peertube-models'
|
import { UserNotification } from '@peertube/peertube-models'
|
||||||
import { PeerTubeServer, cleanupTests, waitJobs } from '@peertube/peertube-server-commands'
|
import { PeerTubeServer, cleanupTests, waitJobs } from '@peertube/peertube-server-commands'
|
||||||
import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
|
import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
|
||||||
import { CheckerBaseParams, checkMyVideoTranscriptionGenerated, prepareNotificationsTest } from '@tests/shared/notifications.js'
|
import { checkMyVideoTranscriptionGenerated } from '@tests/shared/notifications/check-video-notifications.js'
|
||||||
|
import { prepareNotificationsTest } from '@tests/shared/notifications/notifications-common.js'
|
||||||
|
import { CheckerBaseParams } from '@tests/shared/notifications/shared/notification-checker.js'
|
||||||
|
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
|
|
||||||
describe('Test caption notifications', function () {
|
describe('Test caption notifications', function () {
|
||||||
|
|
|
@ -0,0 +1,101 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
|
|
||||||
|
import { UserNotification } from '@peertube/peertube-models'
|
||||||
|
import { cleanupTests, PeerTubeServer, waitJobs } from '@peertube/peertube-server-commands'
|
||||||
|
import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
|
||||||
|
import {
|
||||||
|
checkAcceptedToCollaborateToChannel,
|
||||||
|
CheckChannelCollaboratorOptions,
|
||||||
|
checkInvitedToCollaborateToChannel,
|
||||||
|
checkRefusedToCollaborateToChannel
|
||||||
|
} from '@tests/shared/notifications/check-channel-notifications.js'
|
||||||
|
import { prepareNotificationsTest } from '@tests/shared/notifications/notifications-common.js'
|
||||||
|
|
||||||
|
type BaseParam = Omit<CheckChannelCollaboratorOptions, 'checkType'>
|
||||||
|
|
||||||
|
describe('Test channel collaborators notifications', function () {
|
||||||
|
let server: PeerTubeServer
|
||||||
|
|
||||||
|
let userNotifications: UserNotification[] = []
|
||||||
|
let adminNotifications: UserNotification[] = []
|
||||||
|
let emails: object[] = []
|
||||||
|
|
||||||
|
let baseAdminParams: BaseParam
|
||||||
|
let baseUserParams: BaseParam
|
||||||
|
|
||||||
|
let userAccessToken: string
|
||||||
|
let collaboratorId: number
|
||||||
|
|
||||||
|
const userEmail = 'user_1@example.com'
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
this.timeout(120000)
|
||||||
|
|
||||||
|
const res = await prepareNotificationsTest(1)
|
||||||
|
emails = res.emails
|
||||||
|
userAccessToken = res.userAccessToken
|
||||||
|
server = res.servers[0]
|
||||||
|
userNotifications = res.userNotifications
|
||||||
|
adminNotifications = res.adminNotifications
|
||||||
|
|
||||||
|
baseAdminParams = {
|
||||||
|
server,
|
||||||
|
emails,
|
||||||
|
to: server.adminEmail,
|
||||||
|
token: server.accessToken,
|
||||||
|
socketNotifications: adminNotifications,
|
||||||
|
channelDisplayName: 'Main root channel',
|
||||||
|
targetDisplayName: 'User 1',
|
||||||
|
sourceDisplayName: 'root'
|
||||||
|
}
|
||||||
|
baseUserParams = {
|
||||||
|
server,
|
||||||
|
emails,
|
||||||
|
to: userEmail,
|
||||||
|
token: userAccessToken,
|
||||||
|
socketNotifications: userNotifications,
|
||||||
|
channelDisplayName: 'Main root channel',
|
||||||
|
targetDisplayName: 'User 1',
|
||||||
|
sourceDisplayName: 'root'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should send a notification when a user is invited to collaborate to a channel', async function () {
|
||||||
|
const res = await server.channelCollaborators.invite({ channel: 'root_channel', target: 'user_1' })
|
||||||
|
collaboratorId = res.id
|
||||||
|
|
||||||
|
await waitJobs([ server ])
|
||||||
|
|
||||||
|
await checkInvitedToCollaborateToChannel({ ...baseUserParams, checkType: 'presence' })
|
||||||
|
await checkInvitedToCollaborateToChannel({ ...baseAdminParams, checkType: 'absence' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should send a notification when a user accepts to collaborate to a channel', async function () {
|
||||||
|
await server.channelCollaborators.accept({ id: collaboratorId, token: userAccessToken, channel: 'root_channel' })
|
||||||
|
await waitJobs([ server ])
|
||||||
|
|
||||||
|
await checkAcceptedToCollaborateToChannel({ ...baseUserParams, checkType: 'absence' })
|
||||||
|
await checkAcceptedToCollaborateToChannel({ ...baseAdminParams, checkType: 'presence' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should send a notification when a user refuses to collaborate to a channel', async function () {
|
||||||
|
// Re-invite the user
|
||||||
|
{
|
||||||
|
await server.channelCollaborators.remove({ channel: 'root_channel', id: collaboratorId })
|
||||||
|
const res = await server.channelCollaborators.invite({ channel: 'root_channel', target: 'user_1' })
|
||||||
|
collaboratorId = res.id
|
||||||
|
}
|
||||||
|
|
||||||
|
await server.channelCollaborators.reject({ id: collaboratorId, channel: 'root_channel', token: userAccessToken })
|
||||||
|
await waitJobs([ server ])
|
||||||
|
|
||||||
|
await checkRefusedToCollaborateToChannel({ ...baseUserParams, checkType: 'absence' })
|
||||||
|
await checkRefusedToCollaborateToChannel({ ...baseAdminParams, checkType: 'presence' })
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async function () {
|
||||||
|
await MockSmtpServer.Instance.kill()
|
||||||
|
|
||||||
|
await cleanupTests([ server ])
|
||||||
|
})
|
||||||
|
})
|
|
@ -3,7 +3,9 @@
|
||||||
import { UserNotification, UserNotificationType, VideoCommentPolicy } from '@peertube/peertube-models'
|
import { UserNotification, UserNotificationType, VideoCommentPolicy } from '@peertube/peertube-models'
|
||||||
import { PeerTubeServer, cleanupTests, setDefaultAccountAvatar, waitJobs } from '@peertube/peertube-server-commands'
|
import { PeerTubeServer, cleanupTests, setDefaultAccountAvatar, waitJobs } from '@peertube/peertube-server-commands'
|
||||||
import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
|
import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
|
||||||
import { CheckerBaseParams, checkCommentMention, checkNewCommentOnMyVideo, prepareNotificationsTest } from '@tests/shared/notifications.js'
|
import { checkCommentMention, checkNewCommentOnMyVideo } from '@tests/shared/notifications/check-comment-notifications.js'
|
||||||
|
import { prepareNotificationsTest } from '@tests/shared/notifications/notifications-common.js'
|
||||||
|
import { CheckerBaseParams } from '@tests/shared/notifications/shared/notification-checker.js'
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
|
|
||||||
describe('Test comments notifications', function () {
|
describe('Test comments notifications', function () {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import './admin-notifications.js'
|
import './admin-notifications.js'
|
||||||
import './caption-notifications.js'
|
import './caption-notifications.js'
|
||||||
|
import './channel-collaborators-notification.js'
|
||||||
import './comments-notifications.js'
|
import './comments-notifications.js'
|
||||||
import './moderation-notifications.js'
|
import './moderation-notifications.js'
|
||||||
import './notifications-api.js'
|
import './notifications-api.js'
|
||||||
|
|
|
@ -6,21 +6,19 @@ import { buildUUID } from '@peertube/peertube-node-utils'
|
||||||
import { cleanupTests, PeerTubeServer, waitJobs } from '@peertube/peertube-server-commands'
|
import { cleanupTests, PeerTubeServer, waitJobs } from '@peertube/peertube-server-commands'
|
||||||
import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
|
import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
|
||||||
import { MockInstancesIndex } from '@tests/shared/mock-servers/mock-instances-index.js'
|
import { MockInstancesIndex } from '@tests/shared/mock-servers/mock-instances-index.js'
|
||||||
|
import { checkAutoInstanceFollowing, checkNewInstanceFollower } from '@tests/shared/notifications/check-follow-notifications.js'
|
||||||
import {
|
import {
|
||||||
prepareNotificationsTest,
|
|
||||||
CheckerBaseParams,
|
|
||||||
checkNewVideoAbuseForModerators,
|
|
||||||
checkNewCommentAbuseForModerators,
|
|
||||||
checkNewAccountAbuseForModerators,
|
|
||||||
checkAbuseStateChange,
|
checkAbuseStateChange,
|
||||||
checkNewAbuseMessage,
|
checkNewAbuseMessage,
|
||||||
|
checkNewAccountAbuseForModerators,
|
||||||
checkNewBlacklistOnMyVideo,
|
checkNewBlacklistOnMyVideo,
|
||||||
checkNewInstanceFollower,
|
checkNewCommentAbuseForModerators,
|
||||||
checkAutoInstanceFollowing,
|
checkNewVideoAbuseForModerators,
|
||||||
checkVideoAutoBlacklistForModerators,
|
checkVideoAutoBlacklistForModerators
|
||||||
checkMyVideoIsPublished,
|
} from '@tests/shared/notifications/check-moderation-notifications.js'
|
||||||
checkNewVideoFromSubscription
|
import { checkMyVideoIsPublished, checkNewVideoFromSubscription } from '@tests/shared/notifications/check-video-notifications.js'
|
||||||
} from '@tests/shared/notifications.js'
|
import { prepareNotificationsTest } from '@tests/shared/notifications/notifications-common.js'
|
||||||
|
import { CheckerBaseParams } from '@tests/shared/notifications/shared/notification-checker.js'
|
||||||
|
|
||||||
describe('Test moderation notifications', function () {
|
describe('Test moderation notifications', function () {
|
||||||
let servers: PeerTubeServer[] = []
|
let servers: PeerTubeServer[] = []
|
||||||
|
|
|
@ -1,15 +1,12 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
|
|
||||||
import { expect } from 'chai'
|
|
||||||
import { UserNotification, UserNotificationSettingValue, UserNotificationType } from '@peertube/peertube-models'
|
import { UserNotification, UserNotificationSettingValue, UserNotificationType } from '@peertube/peertube-models'
|
||||||
import { cleanupTests, PeerTubeServer, waitJobs } from '@peertube/peertube-server-commands'
|
import { cleanupTests, PeerTubeServer, waitJobs } from '@peertube/peertube-server-commands'
|
||||||
import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
|
import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
|
||||||
import {
|
import { checkNewVideoFromSubscription } from '@tests/shared/notifications/check-video-notifications.js'
|
||||||
prepareNotificationsTest,
|
import { getAllNotificationsSettings, prepareNotificationsTest } from '@tests/shared/notifications/notifications-common.js'
|
||||||
CheckerBaseParams,
|
import { CheckerBaseParams } from '@tests/shared/notifications/shared/notification-checker.js'
|
||||||
getAllNotificationsSettings,
|
import { expect } from 'chai'
|
||||||
checkNewVideoFromSubscription
|
|
||||||
} from '@tests/shared/notifications.js'
|
|
||||||
|
|
||||||
describe('Test notifications API', function () {
|
describe('Test notifications API', function () {
|
||||||
let server: PeerTubeServer
|
let server: PeerTubeServer
|
||||||
|
|
|
@ -3,7 +3,9 @@
|
||||||
import { UserNotification } from '@peertube/peertube-models'
|
import { UserNotification } from '@peertube/peertube-models'
|
||||||
import { cleanupTests, PeerTubeServer, waitJobs } from '@peertube/peertube-server-commands'
|
import { cleanupTests, PeerTubeServer, waitJobs } from '@peertube/peertube-server-commands'
|
||||||
import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
|
import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
|
||||||
import { CheckerBaseParams, prepareNotificationsTest, checkUserRegistered, checkRegistrationRequest } from '@tests/shared/notifications.js'
|
import { checkRegistrationRequest, checkUserRegistered } from '@tests/shared/notifications/check-moderation-notifications.js'
|
||||||
|
import { prepareNotificationsTest } from '@tests/shared/notifications/notifications-common.js'
|
||||||
|
import { CheckerBaseParams } from '@tests/shared/notifications/shared/notification-checker.js'
|
||||||
|
|
||||||
describe('Test registrations notifications', function () {
|
describe('Test registrations notifications', function () {
|
||||||
let server: PeerTubeServer
|
let server: PeerTubeServer
|
||||||
|
|
|
@ -6,17 +6,16 @@ import { buildUUID } from '@peertube/peertube-node-utils'
|
||||||
import { cleanupTests, findExternalSavedVideo, PeerTubeServer, stopFfmpeg, waitJobs } from '@peertube/peertube-server-commands'
|
import { cleanupTests, findExternalSavedVideo, PeerTubeServer, stopFfmpeg, waitJobs } from '@peertube/peertube-server-commands'
|
||||||
import { FIXTURE_URLS } from '@tests/shared/fixture-urls.js'
|
import { FIXTURE_URLS } from '@tests/shared/fixture-urls.js'
|
||||||
import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
|
import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
|
||||||
|
import { checkNewActorFollow } from '@tests/shared/notifications/check-follow-notifications.js'
|
||||||
import {
|
import {
|
||||||
CheckerBaseParams,
|
|
||||||
checkMyVideoImportIsFinished,
|
checkMyVideoImportIsFinished,
|
||||||
checkMyVideoIsPublished,
|
checkMyVideoIsPublished,
|
||||||
checkNewActorFollow,
|
|
||||||
checkNewLiveFromSubscription,
|
checkNewLiveFromSubscription,
|
||||||
checkNewVideoFromSubscription,
|
checkNewVideoFromSubscription,
|
||||||
checkVideoStudioEditionIsFinished,
|
checkVideoStudioEditionIsFinished
|
||||||
prepareNotificationsTest,
|
} from '@tests/shared/notifications/check-video-notifications.js'
|
||||||
waitUntilNotification
|
import { prepareNotificationsTest, waitUntilNotification } from '@tests/shared/notifications/notifications-common.js'
|
||||||
} from '@tests/shared/notifications.js'
|
import { CheckerBaseParams } from '@tests/shared/notifications/shared/notification-checker.js'
|
||||||
import { uploadRandomVideoOnServers } from '@tests/shared/videos.js'
|
import { uploadRandomVideoOnServers } from '@tests/shared/videos.js'
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
|
|
||||||
|
|
304
packages/tests/src/api/videos/channel-collaborators.ts
Normal file
304
packages/tests/src/api/videos/channel-collaborators.ts
Normal file
|
@ -0,0 +1,304 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
|
|
||||||
|
import { HttpStatusCode, VideoChannelCollaboratorState, VideoPrivacy } from '@peertube/peertube-models'
|
||||||
|
import {
|
||||||
|
cleanupTests,
|
||||||
|
createMultipleServers,
|
||||||
|
doubleFollow,
|
||||||
|
PeerTubeServer,
|
||||||
|
setAccessTokensToServers,
|
||||||
|
waitJobs
|
||||||
|
} from '@peertube/peertube-server-commands'
|
||||||
|
import { checkActorImage } from '@tests/shared/actors.js'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
|
||||||
|
describe('Test channel collaborators', function () {
|
||||||
|
let servers: PeerTubeServer[]
|
||||||
|
|
||||||
|
let collaborator1: string
|
||||||
|
let collaborator2: string
|
||||||
|
|
||||||
|
let collaboratorId1: number
|
||||||
|
let collaboratorId2: number
|
||||||
|
|
||||||
|
let channelCollab: number
|
||||||
|
|
||||||
|
async function expectMyChannels (token: string, names: string[]) {
|
||||||
|
const me = await servers[0].users.getMyInfo({ token })
|
||||||
|
|
||||||
|
const { total, data } = await servers[0].channels.listByAccount({ accountName: me.username, token, includeCollaborations: true })
|
||||||
|
expect(total).to.equal(names.length)
|
||||||
|
expect(data).to.have.lengthOf(names.length)
|
||||||
|
|
||||||
|
expect(data.map(c => c.name)).to.deep.equal(names)
|
||||||
|
}
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
this.timeout(60000)
|
||||||
|
|
||||||
|
servers = await createMultipleServers(2)
|
||||||
|
|
||||||
|
await setAccessTokensToServers(servers)
|
||||||
|
await doubleFollow(servers[0], servers[1])
|
||||||
|
|
||||||
|
await waitJobs(servers)
|
||||||
|
|
||||||
|
collaborator1 = await servers[0].users.generateUserAndToken('collaborator1')
|
||||||
|
collaborator2 = await servers[0].users.generateUserAndToken('collaborator2')
|
||||||
|
|
||||||
|
await servers[0].users.updateMyAvatar({ fixture: 'avatar.png', token: collaborator1 })
|
||||||
|
|
||||||
|
await waitJobs(servers)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Manage collaborators', function () {
|
||||||
|
it('Should not have collaborators by default', async function () {
|
||||||
|
const collaborators = await servers[0].channelCollaborators.list({ channel: 'root_channel' })
|
||||||
|
expect(collaborators.total).to.equal(0)
|
||||||
|
expect(collaborators.data).to.have.lengthOf(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should create a channel and invite a collaborator', async function () {
|
||||||
|
const channel = await servers[0].channels.create({ attributes: { name: 'channel_collaboration1' } })
|
||||||
|
channelCollab = channel.id
|
||||||
|
|
||||||
|
await servers[0].channels.updateImage({ channelName: 'channel_collaboration1', fixture: 'avatar.png', type: 'avatar' })
|
||||||
|
|
||||||
|
const { id } = await servers[0].channelCollaborators.invite({ channel: 'channel_collaboration1', target: 'collaborator1' })
|
||||||
|
collaboratorId1 = id
|
||||||
|
|
||||||
|
const { total, data } = await servers[0].channelCollaborators.list({ channel: 'channel_collaboration1' })
|
||||||
|
expect(total).to.equal(1)
|
||||||
|
expect(data).to.have.lengthOf(1)
|
||||||
|
|
||||||
|
const collab = data[0]
|
||||||
|
expect(collab.id).to.exist
|
||||||
|
expect(collab.createdAt).to.exist
|
||||||
|
expect(collab.updatedAt).to.exist
|
||||||
|
expect(collab.state.id).to.equal(VideoChannelCollaboratorState.PENDING)
|
||||||
|
expect(collab.state.label).to.equal('Pending')
|
||||||
|
|
||||||
|
expect(collab.account.displayName).to.equal('collaborator1')
|
||||||
|
expect(collab.account.host).to.equal(servers[0].host)
|
||||||
|
expect(collab.account.id).to.exist
|
||||||
|
expect(collab.account.name).to.equal('collaborator1')
|
||||||
|
|
||||||
|
await checkActorImage(collab.account)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should invite another collaborator', async function () {
|
||||||
|
const { id } = await servers[0].channelCollaborators.invite({ channel: 'channel_collaboration1', target: 'collaborator2' })
|
||||||
|
collaboratorId2 = id
|
||||||
|
|
||||||
|
const { total, data } = await servers[0].channelCollaborators.list({ channel: 'channel_collaboration1' })
|
||||||
|
expect(total).to.equal(2)
|
||||||
|
expect(data).to.have.lengthOf(2)
|
||||||
|
|
||||||
|
expect(data[0].account.name).to.equal('collaborator2')
|
||||||
|
expect(data[1].account.name).to.equal('collaborator1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should not list channels when collaboration is not yet accepted', async function () {
|
||||||
|
await expectMyChannels(servers[0].accessToken, [ 'root_channel', 'channel_collaboration1' ])
|
||||||
|
await expectMyChannels(collaborator1, [ 'collaborator1_channel' ])
|
||||||
|
await expectMyChannels(collaborator2, [ 'collaborator2_channel' ])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should accept an invitation', async function () {
|
||||||
|
await servers[0].channelCollaborators.accept({ channel: 'channel_collaboration1', id: collaboratorId1, token: collaborator1 })
|
||||||
|
|
||||||
|
const { total, data } = await servers[0].channelCollaborators.list({ channel: 'channel_collaboration1', token: collaborator1 })
|
||||||
|
expect(total).to.equal(2)
|
||||||
|
expect(data).to.have.lengthOf(2)
|
||||||
|
|
||||||
|
expect(data[0].account.name).to.equal('collaborator2')
|
||||||
|
expect(data[0].state.id).to.equal(VideoChannelCollaboratorState.PENDING)
|
||||||
|
expect(data[1].account.name).to.equal('collaborator1')
|
||||||
|
expect(data[1].state.id).to.equal(VideoChannelCollaboratorState.ACCEPTED)
|
||||||
|
expect(data[1].state.label).to.equal('Accepted')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should list channel collaborations after having accepted an invitation', async function () {
|
||||||
|
await expectMyChannels(servers[0].accessToken, [ 'root_channel', 'channel_collaboration1' ])
|
||||||
|
await expectMyChannels(collaborator1, [ 'collaborator1_channel', 'channel_collaboration1' ])
|
||||||
|
await expectMyChannels(collaborator2, [ 'collaborator2_channel' ])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should reject an invitation', async function () {
|
||||||
|
await servers[0].channelCollaborators.reject({ channel: 'channel_collaboration1', id: collaboratorId2, token: collaborator2 })
|
||||||
|
|
||||||
|
const { total, data } = await servers[0].channelCollaborators.list({ channel: 'channel_collaboration1' })
|
||||||
|
expect(total).to.equal(1)
|
||||||
|
expect(data).to.have.lengthOf(1)
|
||||||
|
|
||||||
|
expect(data[0].account.name).to.equal('collaborator1')
|
||||||
|
expect(data[0].state.id).to.equal(VideoChannelCollaboratorState.ACCEPTED)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should list channel collaborations after having rejected an invitation', async function () {
|
||||||
|
await expectMyChannels(servers[0].accessToken, [ 'root_channel', 'channel_collaboration1' ])
|
||||||
|
await expectMyChannels(collaborator1, [ 'collaborator1_channel', 'channel_collaboration1' ])
|
||||||
|
await expectMyChannels(collaborator2, [ 'collaborator2_channel' ])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should delete a pending invitation', async function () {
|
||||||
|
const { id } = await servers[0].channelCollaborators.invite({ channel: 'channel_collaboration1', target: 'collaborator2' })
|
||||||
|
collaboratorId2 = id
|
||||||
|
|
||||||
|
{
|
||||||
|
const { total, data } = await servers[0].channelCollaborators.list({ channel: 'channel_collaboration1' })
|
||||||
|
expect(total).to.equal(2)
|
||||||
|
expect(data).to.have.lengthOf(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
await servers[0].channelCollaborators.remove({ channel: 'channel_collaboration1', id: collaboratorId2 })
|
||||||
|
|
||||||
|
{
|
||||||
|
const { total, data } = await servers[0].channelCollaborators.list({ channel: 'channel_collaboration1' })
|
||||||
|
expect(total).to.equal(1)
|
||||||
|
expect(data).to.have.lengthOf(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
await servers[0].channelCollaborators.accept({
|
||||||
|
channel: 'channel_collaboration1',
|
||||||
|
id: collaboratorId2,
|
||||||
|
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should delete an accepted invitation', async function () {
|
||||||
|
await servers[0].channelCollaborators.remove({ channel: 'channel_collaboration1', id: collaboratorId1 })
|
||||||
|
|
||||||
|
const { total, data } = await servers[0].channelCollaborators.list({ channel: 'channel_collaboration1' })
|
||||||
|
expect(total).to.equal(0)
|
||||||
|
expect(data).to.have.lengthOf(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should not list collab channels anymore', async function () {
|
||||||
|
await expectMyChannels(servers[0].accessToken, [ 'root_channel', 'channel_collaboration1' ])
|
||||||
|
await expectMyChannels(collaborator1, [ 'collaborator1_channel' ])
|
||||||
|
await expectMyChannels(collaborator2, [ 'collaborator2_channel' ])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('With a collaborator', function () {
|
||||||
|
let user1: string
|
||||||
|
let external: string
|
||||||
|
let videoId: string
|
||||||
|
let playlistId: string
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
user1 = await servers[0].users.generateUserAndToken('user1')
|
||||||
|
external = await servers[0].users.generateUserAndToken('external')
|
||||||
|
|
||||||
|
{
|
||||||
|
const { id } = await servers[0].channelCollaborators.invite({ channel: 'channel_collaboration1', target: 'collaborator1' })
|
||||||
|
await servers[0].channelCollaborators.accept({ channel: 'channel_collaboration1', id, token: collaborator1 })
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const { id } = await servers[0].channelCollaborators.invite({ channel: 'channel_collaboration1', target: 'collaborator2' })
|
||||||
|
await servers[0].channelCollaborators.accept({ channel: 'channel_collaboration1', id, token: collaborator2 })
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const { id } = await servers[0].channelCollaborators.invite({ channel: 'user1_channel', target: 'collaborator1', token: user1 })
|
||||||
|
await servers[0].channelCollaborators.accept({ channel: 'user1_channel', id, token: collaborator1 })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should list videos from collab channels', async function () {
|
||||||
|
const { uuid } = await servers[0].videos.upload({
|
||||||
|
attributes: { name: 'video collab 1', channelId: channelCollab },
|
||||||
|
token: collaborator1
|
||||||
|
})
|
||||||
|
videoId = uuid
|
||||||
|
|
||||||
|
await servers[0].videos.quickUpload({ name: 'video collab 2', channelId: channelCollab })
|
||||||
|
|
||||||
|
for (const token of [ collaborator1, collaborator2 ]) {
|
||||||
|
const videos = await servers[0].videos.listMyVideos({ token, includeCollaborations: true })
|
||||||
|
|
||||||
|
expect(videos.total).to.equal(2)
|
||||||
|
expect(videos.data).to.have.lengthOf(2)
|
||||||
|
expect(videos.data[0].name).to.equal('video collab 2')
|
||||||
|
expect(videos.data[1].name).to.equal('video collab 1')
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const token of [ external, user1 ]) {
|
||||||
|
const videos = await servers[0].videos.listMyVideos({ token, includeCollaborations: true })
|
||||||
|
expect(videos.total).to.equal(0)
|
||||||
|
expect(videos.data).to.have.lengthOf(0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should list comments from collab channels', async function () {
|
||||||
|
await servers[0].comments.createThread({ token: external, videoId, text: 'A thread from collab channel' })
|
||||||
|
await servers[0].comments.addReplyToLastThread({ token: external, text: 'A reply from collab channel' })
|
||||||
|
|
||||||
|
for (const token of [ collaborator1, collaborator2 ]) {
|
||||||
|
const comments = await servers[0].comments.listCommentsOnMyVideos({ token, includeCollaborations: true })
|
||||||
|
expect(comments.total).to.equal(2)
|
||||||
|
expect(comments.data).to.have.lengthOf(2)
|
||||||
|
expect(comments.data[0].text).to.equal('A reply from collab channel')
|
||||||
|
expect(comments.data[1].text).to.equal('A thread from collab channel')
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const token of [ external, user1 ]) {
|
||||||
|
const comments = await servers[0].comments.listCommentsOnMyVideos({ token, includeCollaborations: true })
|
||||||
|
expect(comments.total).to.equal(0)
|
||||||
|
expect(comments.data).to.have.lengthOf(0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should list playlists from collab channels', async function () {
|
||||||
|
for (const displayName of [ 'playlist1', 'playlist2' ]) {
|
||||||
|
const playlist = await servers[0].playlists.create({
|
||||||
|
token: collaborator1,
|
||||||
|
attributes: { displayName, privacy: VideoPrivacy.PUBLIC, videoChannelId: channelCollab }
|
||||||
|
})
|
||||||
|
playlistId = playlist.uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
await servers[0].playlists.addElement({ playlistId, attributes: { videoId }, token: collaborator2 })
|
||||||
|
|
||||||
|
for (const token of [ collaborator1, collaborator2 ]) {
|
||||||
|
const me = await servers[0].users.getMyInfo({ token })
|
||||||
|
|
||||||
|
const playlists = await servers[0].playlists.listByAccount({ token, handle: me.username, includeCollaborations: true })
|
||||||
|
expect(playlists.total).to.equal(3)
|
||||||
|
expect(playlists.data).to.have.lengthOf(3)
|
||||||
|
expect(playlists.data[0].displayName).to.equal('playlist2')
|
||||||
|
expect(playlists.data[1].displayName).to.equal('playlist1')
|
||||||
|
expect(playlists.data[2].displayName).to.equal('Watch later')
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const token of [ external, user1 ]) {
|
||||||
|
const me = await servers[0].users.getMyInfo({ token })
|
||||||
|
|
||||||
|
const playlists = await servers[0].playlists.listByAccount({ token, handle: me.username, includeCollaborations: true })
|
||||||
|
expect(playlists.total).to.equal(1)
|
||||||
|
expect(playlists.data).to.have.lengthOf(1)
|
||||||
|
expect(playlists.data[0].displayName).to.equal('Watch later')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should list imports from collab channels', async function () {
|
||||||
|
// TODO
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should have federated objects created by collaborators', async function () {
|
||||||
|
await waitJobs(servers)
|
||||||
|
|
||||||
|
const video = await servers[1].videos.get({ id: videoId })
|
||||||
|
expect(video.name).to.equal('video collab 1')
|
||||||
|
|
||||||
|
const playlist = await servers[1].playlists.get({ playlistId })
|
||||||
|
expect(playlist.displayName).to.equal('playlist2')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async function () {
|
||||||
|
await cleanupTests(servers)
|
||||||
|
})
|
||||||
|
})
|
|
@ -21,8 +21,8 @@ import './video-schedule-update.js'
|
||||||
import './video-source.js'
|
import './video-source.js'
|
||||||
import './video-static-file-privacy.js'
|
import './video-static-file-privacy.js'
|
||||||
import './video-storyboard.js'
|
import './video-storyboard.js'
|
||||||
import './video-storyboard-remote-runner.js'
|
|
||||||
import './video-transcription.js'
|
import './video-transcription.js'
|
||||||
import './videos-common-filters.js'
|
import './videos-common-filters.js'
|
||||||
import './videos-history.js'
|
import './videos-history.js'
|
||||||
import './videos-overview.js'
|
import './videos-overview.js'
|
||||||
|
import './channel-collaborators.js'
|
||||||
|
|
|
@ -8,9 +8,9 @@ describe('Comment model', function () {
|
||||||
const text = '@florian @jean@localhost:9000 @flo @another@localhost:9000 @flo2@jean.com hello ' +
|
const text = '@florian @jean@localhost:9000 @flo @another@localhost:9000 @flo2@jean.com hello ' +
|
||||||
'email@localhost:9000 coucou.com no? @chocobozzz @chocobozzz @end'
|
'email@localhost:9000 coucou.com no? @chocobozzz @chocobozzz @end'
|
||||||
|
|
||||||
const isOwned = true
|
const isLocal = true
|
||||||
|
|
||||||
const result = extractMentions(text, isOwned).sort((a, b) => a.localeCompare(b))
|
const result = extractMentions(text, isLocal).sort((a, b) => a.localeCompare(b))
|
||||||
|
|
||||||
expect(result).to.deep.equal([ 'another', 'chocobozzz', 'end', 'flo', 'florian', 'jean' ])
|
expect(result).to.deep.equal([ 'another', 'chocobozzz', 'end', 'flo', 'florian', 'jean' ])
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
|
|
||||||
|
import { Account, AccountSummary, HttpStatusCode, VideoChannel, VideoChannelSummary } from '@peertube/peertube-models'
|
||||||
|
import { makeRawRequest, PeerTubeServer } from '@peertube/peertube-server-commands'
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { pathExists } from 'fs-extra/esm'
|
import { pathExists } from 'fs-extra/esm'
|
||||||
import { readdir } from 'fs/promises'
|
import { readdir } from 'fs/promises'
|
||||||
import { Account, VideoChannel } from '@peertube/peertube-models'
|
|
||||||
import { PeerTubeServer } from '@peertube/peertube-server-commands'
|
|
||||||
|
|
||||||
async function expectChannelsFollows (options: {
|
export async function expectChannelsFollows (options: {
|
||||||
server: PeerTubeServer
|
server: PeerTubeServer
|
||||||
handle: string
|
handle: string
|
||||||
followers: number
|
followers: number
|
||||||
|
@ -18,7 +18,7 @@ async function expectChannelsFollows (options: {
|
||||||
return expectActorFollow({ ...options, data })
|
return expectActorFollow({ ...options, data })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function expectAccountFollows (options: {
|
export async function expectAccountFollows (options: {
|
||||||
server: PeerTubeServer
|
server: PeerTubeServer
|
||||||
handle: string
|
handle: string
|
||||||
followers: number
|
followers: number
|
||||||
|
@ -30,7 +30,7 @@ async function expectAccountFollows (options: {
|
||||||
return expectActorFollow({ ...options, data })
|
return expectActorFollow({ ...options, data })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkActorFilesWereRemoved (filename: string, server: PeerTubeServer) {
|
export async function checkActorFilesWereRemoved (filename: string, server: PeerTubeServer) {
|
||||||
for (const directory of [ 'avatars' ]) {
|
for (const directory of [ 'avatars' ]) {
|
||||||
const directoryPath = server.getDirectoryPath(directory)
|
const directoryPath = server.getDirectoryPath(directory)
|
||||||
|
|
||||||
|
@ -44,12 +44,22 @@ async function checkActorFilesWereRemoved (filename: string, server: PeerTubeSer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export async function checkActorImage (actor: AccountSummary | VideoChannelSummary) {
|
||||||
expectAccountFollows,
|
expect(actor.avatars).to.have.lengthOf(4)
|
||||||
expectChannelsFollows,
|
|
||||||
checkActorFilesWereRemoved
|
for (const avatar of actor.avatars) {
|
||||||
|
expect(avatar.createdAt).to.exist
|
||||||
|
expect(avatar.fileUrl).to.exist
|
||||||
|
expect(avatar.height).to.be.greaterThan(0)
|
||||||
|
expect(avatar.width).to.be.greaterThan(0)
|
||||||
|
expect(avatar.updatedAt).to.exist
|
||||||
|
|
||||||
|
await makeRawRequest({ url: avatar.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Private
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function expectActorFollow (options: {
|
function expectActorFollow (options: {
|
||||||
|
|
|
@ -33,7 +33,7 @@ import { tmpdir } from 'os'
|
||||||
import { basename, join, resolve } from 'path'
|
import { basename, join, resolve } from 'path'
|
||||||
import { testFileExistsOnFSOrNot } from './checks.js'
|
import { testFileExistsOnFSOrNot } from './checks.js'
|
||||||
import { MockSmtpServer } from './mock-servers/mock-email.js'
|
import { MockSmtpServer } from './mock-servers/mock-email.js'
|
||||||
import { getAllNotificationsSettings } from './notifications.js'
|
import { getAllNotificationsSettings } from './notifications/notifications-common.js'
|
||||||
|
|
||||||
type ExportOutbox = ActivityPubOrderedCollection<ActivityCreate<VideoObject | VideoCommentObject>>
|
type ExportOutbox = ActivityPubOrderedCollection<ActivityCreate<VideoObject | VideoCommentObject>>
|
||||||
|
|
||||||
|
|
|
@ -66,7 +66,7 @@ class MockSmtpServer {
|
||||||
async kill () {
|
async kill () {
|
||||||
if (!this.maildev) return
|
if (!this.maildev) return
|
||||||
|
|
||||||
if (this.relayingEmail) {
|
if (this.relayingEmail !== undefined) {
|
||||||
await this.relayingEmail
|
await this.relayingEmail
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,70 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
|
|
||||||
|
import { PluginType_Type, UserNotification, UserNotificationType } from '@peertube/peertube-models'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
import { CheckerBaseParams, CheckerType, checkNotification } from './shared/notification-checker.js'
|
||||||
|
|
||||||
|
export async function checkNewPeerTubeVersion (
|
||||||
|
options: CheckerBaseParams & {
|
||||||
|
latestVersion: string
|
||||||
|
checkType: CheckerType
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { latestVersion } = options
|
||||||
|
const notificationType = UserNotificationType.NEW_PEERTUBE_VERSION
|
||||||
|
|
||||||
|
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||||
|
if (checkType === 'presence') {
|
||||||
|
expect(notification).to.not.be.undefined
|
||||||
|
expect(notification.type).to.equal(notificationType)
|
||||||
|
|
||||||
|
expect(notification.peertube).to.exist
|
||||||
|
expect(notification.peertube.latestVersion).to.equal(latestVersion)
|
||||||
|
} else {
|
||||||
|
expect(notification).to.satisfy((n: UserNotification) => {
|
||||||
|
return n?.peertube === undefined || n.peertube.latestVersion !== latestVersion
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emailNotificationFinder (email: object) {
|
||||||
|
const text = email['text']
|
||||||
|
|
||||||
|
return text.includes(latestVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkNewPluginVersion (
|
||||||
|
options: CheckerBaseParams & {
|
||||||
|
pluginType: PluginType_Type
|
||||||
|
pluginName: string
|
||||||
|
checkType: CheckerType
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { pluginName, pluginType } = options
|
||||||
|
const notificationType = UserNotificationType.NEW_PLUGIN_VERSION
|
||||||
|
|
||||||
|
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||||
|
if (checkType === 'presence') {
|
||||||
|
expect(notification).to.not.be.undefined
|
||||||
|
expect(notification.type).to.equal(notificationType)
|
||||||
|
|
||||||
|
expect(notification.plugin.name).to.equal(pluginName)
|
||||||
|
expect(notification.plugin.type).to.equal(pluginType)
|
||||||
|
} else {
|
||||||
|
expect(notification).to.satisfy((n: UserNotification) => {
|
||||||
|
return n?.plugin === undefined || n.plugin.name !== pluginName
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emailNotificationFinder (email: object) {
|
||||||
|
const text = email['text']
|
||||||
|
|
||||||
|
return text.includes(pluginName)
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||||
|
}
|
|
@ -0,0 +1,162 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
|
|
||||||
|
import {
|
||||||
|
UserNotification,
|
||||||
|
UserNotificationType,
|
||||||
|
UserNotificationType_Type,
|
||||||
|
VideoChannelCollaboratorState,
|
||||||
|
VideoChannelCollaboratorStateType
|
||||||
|
} from '@peertube/peertube-models'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
import { checkActor, CheckerBaseParams, CheckerType, checkNotification } from './shared/notification-checker.js'
|
||||||
|
|
||||||
|
export type CheckChannelCollaboratorOptions = CheckerBaseParams & {
|
||||||
|
channelDisplayName: string
|
||||||
|
targetDisplayName: string
|
||||||
|
sourceDisplayName: string
|
||||||
|
checkType: CheckerType
|
||||||
|
to: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkInvitedToCollaborateToChannel (options: CheckChannelCollaboratorOptions) {
|
||||||
|
const { channelDisplayName, targetDisplayName, sourceDisplayName, to } = options
|
||||||
|
const notificationType = UserNotificationType.INVITED_TO_COLLABORATE_TO_CHANNEL
|
||||||
|
|
||||||
|
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||||
|
if (checkType === 'presence') {
|
||||||
|
checkCollaboratorNotification({
|
||||||
|
notification,
|
||||||
|
notificationType,
|
||||||
|
channelDisplayName,
|
||||||
|
targetDisplayName,
|
||||||
|
sourceDisplayName,
|
||||||
|
state: VideoChannelCollaboratorState.PENDING
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
expect(notification).to.satisfy(c => isNotificationAbsent(c))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emailNotificationFinder (email: object) {
|
||||||
|
if (email['to'][0]['address'] !== to) return false
|
||||||
|
|
||||||
|
const text: string = email['text']
|
||||||
|
return text.includes(`${sourceDisplayName} invited you`) &&
|
||||||
|
text.includes(`of channel ${channelDisplayName}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkAcceptedToCollaborateToChannel (options: CheckChannelCollaboratorOptions) {
|
||||||
|
const { channelDisplayName, targetDisplayName, sourceDisplayName, to } = options
|
||||||
|
const notificationType = UserNotificationType.ACCEPTED_TO_COLLABORATE_TO_CHANNEL
|
||||||
|
|
||||||
|
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||||
|
if (checkType === 'presence') {
|
||||||
|
checkCollaboratorNotification({
|
||||||
|
notification,
|
||||||
|
notificationType,
|
||||||
|
channelDisplayName,
|
||||||
|
targetDisplayName,
|
||||||
|
sourceDisplayName,
|
||||||
|
state: VideoChannelCollaboratorState.ACCEPTED
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
expect(notification).to.satisfy(c => isNotificationAbsent(c))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emailNotificationFinder (email: object) {
|
||||||
|
if (email['to'][0]['address'] !== to) return false
|
||||||
|
|
||||||
|
const text: string = email['text']
|
||||||
|
return text.includes(`${targetDisplayName} accepted`) &&
|
||||||
|
text.includes(`of ${channelDisplayName}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkRefusedToCollaborateToChannel (options: CheckChannelCollaboratorOptions) {
|
||||||
|
const { channelDisplayName, targetDisplayName, sourceDisplayName, to } = options
|
||||||
|
const notificationType = UserNotificationType.REFUSED_TO_COLLABORATE_TO_CHANNEL
|
||||||
|
|
||||||
|
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||||
|
if (checkType === 'presence') {
|
||||||
|
checkCollaboratorNotification({ notification, notificationType, channelDisplayName, targetDisplayName, sourceDisplayName })
|
||||||
|
} else {
|
||||||
|
expect(notification).to.satisfy(c => isNotificationAbsent(c))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emailNotificationFinder (email: object) {
|
||||||
|
if (email['to'][0]['address'] !== to) return false
|
||||||
|
|
||||||
|
const text: string = email['text']
|
||||||
|
return text.includes(`${targetDisplayName} refused`) &&
|
||||||
|
text.includes(`of ${channelDisplayName}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Private
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function checkCollaboratorNotification (options: {
|
||||||
|
notification: UserNotification
|
||||||
|
notificationType: UserNotificationType_Type
|
||||||
|
channelDisplayName: string
|
||||||
|
targetDisplayName: string
|
||||||
|
sourceDisplayName: string
|
||||||
|
state?: VideoChannelCollaboratorStateType
|
||||||
|
}) {
|
||||||
|
const { channelDisplayName, targetDisplayName, notificationType, notification, state, sourceDisplayName } = options
|
||||||
|
|
||||||
|
expect(notification).to.exist
|
||||||
|
expect(notification.type).to.equal(notificationType)
|
||||||
|
|
||||||
|
const collaborator = notification.videoChannelCollaborator
|
||||||
|
|
||||||
|
if (collaborator) {
|
||||||
|
expect(collaborator.channel.avatars).to.have.lengthOf(4)
|
||||||
|
expect(collaborator.account.avatars).to.have.lengthOf(4)
|
||||||
|
expect(collaborator.id).to.exist
|
||||||
|
expect(collaborator.state.id).to.equal(state)
|
||||||
|
expect(collaborator.account.displayName).to.equal(targetDisplayName)
|
||||||
|
expect(collaborator.channel.displayName).to.equal(channelDisplayName)
|
||||||
|
|
||||||
|
checkActor(collaborator.account)
|
||||||
|
checkActor(collaborator.channel)
|
||||||
|
} else {
|
||||||
|
expect(notification.data.channelDisplayName).to.equal(channelDisplayName)
|
||||||
|
expect(notification.data.collaboratorDisplayName).to.equal(targetDisplayName)
|
||||||
|
expect(notification.data.channelOwnerDisplayName).to.equal(sourceDisplayName)
|
||||||
|
|
||||||
|
expect(notification.data.channelHandle).to.exist
|
||||||
|
expect(notification.data.collaboratorHandle).to.exist
|
||||||
|
expect(notification.data.channelOwnerHandle).to.exist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNotificationAbsent (options: {
|
||||||
|
notification: UserNotification
|
||||||
|
notificationType: UserNotificationType_Type
|
||||||
|
channelDisplayName: string
|
||||||
|
targetDisplayName: string
|
||||||
|
}) {
|
||||||
|
const { notification: n, notificationType, channelDisplayName, targetDisplayName } = options
|
||||||
|
|
||||||
|
if (!n) return true
|
||||||
|
if (!n.videoChannelCollaborator) return true
|
||||||
|
if (n.type !== notificationType) return true
|
||||||
|
|
||||||
|
if (
|
||||||
|
n.videoChannelCollaborator.account.displayName !== targetDisplayName &&
|
||||||
|
n.videoChannelCollaborator.channel.displayName !== channelDisplayName
|
||||||
|
) return true
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
|
|
||||||
|
import { UserNotification, UserNotificationType } from '@peertube/peertube-models'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
import { checkActor, checkComment, CheckerBaseParams, CheckerType, checkNotification, checkVideo } from './shared/notification-checker.js'
|
||||||
|
|
||||||
|
export async function checkCommentMention (
|
||||||
|
options: CheckerBaseParams & {
|
||||||
|
shortUUID: string
|
||||||
|
commentId: number
|
||||||
|
threadId: number
|
||||||
|
byAccountDisplayName: string
|
||||||
|
checkType: CheckerType
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { shortUUID, commentId, threadId, byAccountDisplayName } = options
|
||||||
|
const notificationType = UserNotificationType.COMMENT_MENTION
|
||||||
|
|
||||||
|
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||||
|
if (checkType === 'presence') {
|
||||||
|
expect(notification).to.not.be.undefined
|
||||||
|
expect(notification.type).to.equal(notificationType)
|
||||||
|
|
||||||
|
checkComment(notification.comment, commentId, threadId)
|
||||||
|
checkActor(notification.comment.account)
|
||||||
|
expect(notification.comment.account.displayName).to.equal(byAccountDisplayName)
|
||||||
|
|
||||||
|
checkVideo(notification.comment.video, undefined, shortUUID)
|
||||||
|
} else {
|
||||||
|
expect(notification).to.satisfy(n => n.type !== notificationType || n.comment.id !== commentId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emailNotificationFinder (email: object) {
|
||||||
|
const text: string = email['text']
|
||||||
|
|
||||||
|
return text.match(/\bmentioned\b/) && text.includes(shortUUID) && text.includes(byAccountDisplayName)
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastEmailCount = 0
|
||||||
|
|
||||||
|
export async function checkNewCommentOnMyVideo (
|
||||||
|
options: CheckerBaseParams & {
|
||||||
|
shortUUID: string
|
||||||
|
commentId: number
|
||||||
|
threadId: number
|
||||||
|
checkType: CheckerType
|
||||||
|
approval?: boolean // default false
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { server, shortUUID, commentId, threadId, checkType, emails, approval = false } = options
|
||||||
|
const notificationType = UserNotificationType.NEW_COMMENT_ON_MY_VIDEO
|
||||||
|
|
||||||
|
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||||
|
if (checkType === 'presence') {
|
||||||
|
expect(notification).to.not.be.undefined
|
||||||
|
expect(notification.type).to.equal(notificationType)
|
||||||
|
|
||||||
|
checkComment(notification.comment, commentId, threadId)
|
||||||
|
checkActor(notification.comment.account)
|
||||||
|
checkVideo(notification.comment.video, undefined, shortUUID)
|
||||||
|
|
||||||
|
expect(notification.comment.heldForReview).to.equal(approval)
|
||||||
|
} else {
|
||||||
|
expect(notification).to.satisfy((n: UserNotification) => {
|
||||||
|
return n?.comment === undefined || n.comment.id !== commentId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const commentUrl = approval
|
||||||
|
? `${server.url}/my-account/videos/comments?search=heldForReview:true`
|
||||||
|
: `${server.url}/w/${shortUUID};threadId=${threadId}`
|
||||||
|
|
||||||
|
function emailNotificationFinder (email: object) {
|
||||||
|
const text = email['text']
|
||||||
|
|
||||||
|
return text.includes(commentUrl) &&
|
||||||
|
(approval && text.includes('requires approval')) ||
|
||||||
|
(!approval && !text.includes('requires approval'))
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||||
|
|
||||||
|
if (checkType === 'presence') {
|
||||||
|
// We cannot detect email duplicates, so check we received another email
|
||||||
|
expect(emails).to.have.length.above(lastEmailCount)
|
||||||
|
lastEmailCount = emails.length
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
|
|
||||||
|
import { UserNotification, UserNotificationType } from '@peertube/peertube-models'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
import { checkActor, CheckerBaseParams, CheckerType, checkNotification } from './shared/notification-checker.js'
|
||||||
|
|
||||||
|
export async function checkNewActorFollow (
|
||||||
|
options: CheckerBaseParams & {
|
||||||
|
followType: 'channel' | 'account'
|
||||||
|
followerName: string
|
||||||
|
followerDisplayName: string
|
||||||
|
followingDisplayName: string
|
||||||
|
checkType: CheckerType
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { followType, followerName, followerDisplayName, followingDisplayName } = options
|
||||||
|
const notificationType = UserNotificationType.NEW_FOLLOW
|
||||||
|
|
||||||
|
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||||
|
if (checkType === 'presence') {
|
||||||
|
expect(notification).to.not.be.undefined
|
||||||
|
expect(notification.type).to.equal(notificationType)
|
||||||
|
|
||||||
|
checkActor(notification.actorFollow.follower)
|
||||||
|
expect(notification.actorFollow.follower.displayName).to.equal(followerDisplayName)
|
||||||
|
expect(notification.actorFollow.follower.name).to.equal(followerName)
|
||||||
|
expect(notification.actorFollow.follower.host).to.not.be.undefined
|
||||||
|
|
||||||
|
const following = notification.actorFollow.following
|
||||||
|
expect(following.displayName).to.equal(followingDisplayName)
|
||||||
|
expect(following.type).to.equal(followType)
|
||||||
|
} else {
|
||||||
|
expect(notification).to.satisfy(n => {
|
||||||
|
return n.type !== notificationType ||
|
||||||
|
(n.actorFollow.follower.name !== followerName && n.actorFollow.following !== followingDisplayName)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emailNotificationFinder (email: object) {
|
||||||
|
const text: string = email['text']
|
||||||
|
|
||||||
|
return text.includes(followType) && text.includes(followingDisplayName) && text.includes(followerDisplayName)
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkNewInstanceFollower (
|
||||||
|
options: CheckerBaseParams & {
|
||||||
|
followerHost: string
|
||||||
|
checkType: CheckerType
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { followerHost } = options
|
||||||
|
const notificationType = UserNotificationType.NEW_INSTANCE_FOLLOWER
|
||||||
|
|
||||||
|
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||||
|
if (checkType === 'presence') {
|
||||||
|
expect(notification).to.not.be.undefined
|
||||||
|
expect(notification.type).to.equal(notificationType)
|
||||||
|
|
||||||
|
checkActor(notification.actorFollow.follower, { withAvatar: false })
|
||||||
|
expect(notification.actorFollow.follower.name).to.equal('peertube')
|
||||||
|
expect(notification.actorFollow.follower.host).to.equal(followerHost)
|
||||||
|
|
||||||
|
expect(notification.actorFollow.following.name).to.equal('peertube')
|
||||||
|
} else {
|
||||||
|
expect(notification).to.satisfy(n => {
|
||||||
|
return n.type !== notificationType || n.actorFollow.follower.host !== followerHost
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emailNotificationFinder (email: object) {
|
||||||
|
const text: string = email['text']
|
||||||
|
|
||||||
|
return text.includes('PeerTube has a new follower') && text.includes(followerHost)
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkAutoInstanceFollowing (
|
||||||
|
options: CheckerBaseParams & {
|
||||||
|
followerHost: string
|
||||||
|
followingHost: string
|
||||||
|
checkType: CheckerType
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { followerHost, followingHost } = options
|
||||||
|
const notificationType = UserNotificationType.AUTO_INSTANCE_FOLLOWING
|
||||||
|
|
||||||
|
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||||
|
if (checkType === 'presence') {
|
||||||
|
expect(notification).to.not.be.undefined
|
||||||
|
expect(notification.type).to.equal(notificationType)
|
||||||
|
|
||||||
|
const following = notification.actorFollow.following
|
||||||
|
|
||||||
|
checkActor(following, { withAvatar: false })
|
||||||
|
expect(following.name).to.equal('peertube')
|
||||||
|
expect(following.host).to.equal(followingHost)
|
||||||
|
|
||||||
|
expect(notification.actorFollow.follower.name).to.equal('peertube')
|
||||||
|
expect(notification.actorFollow.follower.host).to.equal(followerHost)
|
||||||
|
} else {
|
||||||
|
expect(notification).to.satisfy(n => {
|
||||||
|
return n.type !== notificationType || n.actorFollow.following.host !== followingHost
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emailNotificationFinder (email: object) {
|
||||||
|
const text: string = email['text']
|
||||||
|
|
||||||
|
return text.match(/\bautomatically followed\b/) && text.includes(followingHost)
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||||
|
}
|
|
@ -0,0 +1,297 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
|
|
||||||
|
import { AbuseState, AbuseStateType, UserNotification, UserNotificationType } from '@peertube/peertube-models'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
import { checkActor, CheckerBaseParams, CheckerType, checkNotification, checkVideo } from './shared/notification-checker.js'
|
||||||
|
|
||||||
|
export async function checkNewVideoAbuseForModerators (
|
||||||
|
options: CheckerBaseParams & {
|
||||||
|
shortUUID: string
|
||||||
|
videoName: string
|
||||||
|
checkType: CheckerType
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { shortUUID, videoName } = options
|
||||||
|
const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS
|
||||||
|
|
||||||
|
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||||
|
if (checkType === 'presence') {
|
||||||
|
expect(notification).to.not.be.undefined
|
||||||
|
expect(notification.type).to.equal(notificationType)
|
||||||
|
|
||||||
|
expect(notification.abuse.id).to.be.a('number')
|
||||||
|
checkVideo(notification.abuse.video, videoName, shortUUID)
|
||||||
|
} else {
|
||||||
|
expect(notification).to.satisfy((n: UserNotification) => {
|
||||||
|
return n?.abuse === undefined || n.abuse.video.shortUUID !== shortUUID
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emailNotificationFinder (email: object) {
|
||||||
|
const text = email['text']
|
||||||
|
return text.indexOf(shortUUID) !== -1 && text.indexOf('abuse') !== -1
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkNewAbuseMessage (
|
||||||
|
options: CheckerBaseParams & {
|
||||||
|
abuseId: number
|
||||||
|
message: string
|
||||||
|
toEmail: string
|
||||||
|
checkType: CheckerType
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { abuseId, message, toEmail } = options
|
||||||
|
const notificationType = UserNotificationType.ABUSE_NEW_MESSAGE
|
||||||
|
|
||||||
|
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||||
|
if (checkType === 'presence') {
|
||||||
|
expect(notification).to.not.be.undefined
|
||||||
|
expect(notification.type).to.equal(notificationType)
|
||||||
|
|
||||||
|
expect(notification.abuse.id).to.equal(abuseId)
|
||||||
|
} else {
|
||||||
|
expect(notification).to.satisfy((n: UserNotification) => {
|
||||||
|
return n === undefined || n.type !== notificationType || n.abuse === undefined || n.abuse.id !== abuseId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emailNotificationFinder (email: object) {
|
||||||
|
const text = email['text']
|
||||||
|
const to = email['to'].filter(t => t.address === toEmail)
|
||||||
|
|
||||||
|
return text.indexOf(message) !== -1 && to.length !== 0
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkAbuseStateChange (
|
||||||
|
options: CheckerBaseParams & {
|
||||||
|
abuseId: number
|
||||||
|
state: AbuseStateType
|
||||||
|
checkType: CheckerType
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { abuseId, state } = options
|
||||||
|
const notificationType = UserNotificationType.ABUSE_STATE_CHANGE
|
||||||
|
|
||||||
|
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||||
|
if (checkType === 'presence') {
|
||||||
|
expect(notification).to.not.be.undefined
|
||||||
|
expect(notification.type).to.equal(notificationType)
|
||||||
|
|
||||||
|
expect(notification.abuse.id).to.equal(abuseId)
|
||||||
|
expect(notification.abuse.state).to.equal(state)
|
||||||
|
} else {
|
||||||
|
expect(notification).to.satisfy((n: UserNotification) => {
|
||||||
|
return n?.abuse === undefined || n.abuse.id !== abuseId
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emailNotificationFinder (email: object) {
|
||||||
|
const text = email['text']
|
||||||
|
|
||||||
|
const contains = state === AbuseState.ACCEPTED
|
||||||
|
? ' accepted'
|
||||||
|
: ' rejected'
|
||||||
|
|
||||||
|
return text.indexOf(contains) !== -1
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkNewCommentAbuseForModerators (
|
||||||
|
options: CheckerBaseParams & {
|
||||||
|
shortUUID: string
|
||||||
|
videoName: string
|
||||||
|
checkType: CheckerType
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { shortUUID, videoName } = options
|
||||||
|
const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS
|
||||||
|
|
||||||
|
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||||
|
if (checkType === 'presence') {
|
||||||
|
expect(notification).to.not.be.undefined
|
||||||
|
expect(notification.type).to.equal(notificationType)
|
||||||
|
|
||||||
|
expect(notification.abuse.id).to.be.a('number')
|
||||||
|
checkVideo(notification.abuse.comment.video, videoName, shortUUID)
|
||||||
|
} else {
|
||||||
|
expect(notification).to.satisfy((n: UserNotification) => {
|
||||||
|
return n?.abuse === undefined || n.abuse.comment.video.shortUUID !== shortUUID
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emailNotificationFinder (email: object) {
|
||||||
|
const text = email['text']
|
||||||
|
return text.indexOf(shortUUID) !== -1 && text.indexOf('abuse') !== -1
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkNewAccountAbuseForModerators (
|
||||||
|
options: CheckerBaseParams & {
|
||||||
|
displayName: string
|
||||||
|
checkType: CheckerType
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { displayName } = options
|
||||||
|
const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS
|
||||||
|
|
||||||
|
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||||
|
if (checkType === 'presence') {
|
||||||
|
expect(notification).to.not.be.undefined
|
||||||
|
expect(notification.type).to.equal(notificationType)
|
||||||
|
|
||||||
|
expect(notification.abuse.id).to.be.a('number')
|
||||||
|
expect(notification.abuse.account.displayName).to.equal(displayName)
|
||||||
|
} else {
|
||||||
|
expect(notification).to.satisfy((n: UserNotification) => {
|
||||||
|
return n?.abuse === undefined || n.abuse.account.displayName !== displayName
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emailNotificationFinder (email: object) {
|
||||||
|
const text = email['text']
|
||||||
|
return text.indexOf(displayName) !== -1 && text.indexOf('abuse') !== -1
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkVideoAutoBlacklistForModerators (
|
||||||
|
options: CheckerBaseParams & {
|
||||||
|
shortUUID: string
|
||||||
|
videoName: string
|
||||||
|
checkType: CheckerType
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { shortUUID, videoName } = options
|
||||||
|
const notificationType = UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS
|
||||||
|
|
||||||
|
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||||
|
if (checkType === 'presence') {
|
||||||
|
expect(notification).to.not.be.undefined
|
||||||
|
expect(notification.type).to.equal(notificationType)
|
||||||
|
|
||||||
|
expect(notification.videoBlacklist.video.id).to.be.a('number')
|
||||||
|
checkVideo(notification.videoBlacklist.video, videoName, shortUUID)
|
||||||
|
} else {
|
||||||
|
expect(notification).to.satisfy((n: UserNotification) => {
|
||||||
|
return n?.video === undefined || n.video.shortUUID !== shortUUID
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emailNotificationFinder (email: object) {
|
||||||
|
const text = email['text']
|
||||||
|
return text.indexOf(shortUUID) !== -1 && email['text'].indexOf('moderation/video-blocks/list') !== -1
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkNewBlacklistOnMyVideo (
|
||||||
|
options: CheckerBaseParams & {
|
||||||
|
shortUUID: string
|
||||||
|
videoName: string
|
||||||
|
blacklistType: 'blacklist' | 'unblacklist'
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { videoName, shortUUID, blacklistType } = options
|
||||||
|
const notificationType = blacklistType === 'blacklist'
|
||||||
|
? UserNotificationType.BLACKLIST_ON_MY_VIDEO
|
||||||
|
: UserNotificationType.UNBLACKLIST_ON_MY_VIDEO
|
||||||
|
|
||||||
|
function notificationChecker (notification: UserNotification) {
|
||||||
|
expect(notification).to.not.be.undefined
|
||||||
|
expect(notification.type).to.equal(notificationType)
|
||||||
|
|
||||||
|
const video = blacklistType === 'blacklist' ? notification.videoBlacklist.video : notification.video
|
||||||
|
|
||||||
|
checkVideo(video, videoName, shortUUID)
|
||||||
|
}
|
||||||
|
|
||||||
|
function emailNotificationFinder (email: object) {
|
||||||
|
const text: string = email['text']
|
||||||
|
|
||||||
|
const blacklistReg = blacklistType === 'blacklist'
|
||||||
|
? /\bblocked\b/
|
||||||
|
: /\bunblocked\b/
|
||||||
|
|
||||||
|
return text.includes(shortUUID) && !!text.match(blacklistReg)
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkNotification({ ...options, notificationChecker, emailNotificationFinder, checkType: 'presence' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkUserRegistered (
|
||||||
|
options: CheckerBaseParams & {
|
||||||
|
username: string
|
||||||
|
checkType: CheckerType
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { username } = options
|
||||||
|
const notificationType = UserNotificationType.NEW_USER_REGISTRATION
|
||||||
|
|
||||||
|
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||||
|
if (checkType === 'presence') {
|
||||||
|
expect(notification).to.not.be.undefined
|
||||||
|
expect(notification.type).to.equal(notificationType)
|
||||||
|
|
||||||
|
checkActor(notification.account, { withAvatar: false })
|
||||||
|
expect(notification.account.name).to.equal(username)
|
||||||
|
} else {
|
||||||
|
expect(notification).to.satisfy(n => n.type !== notificationType || n.account.name !== username)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emailNotificationFinder (email: object) {
|
||||||
|
const text: string = email['text']
|
||||||
|
|
||||||
|
return text.includes(' registered.') && text.includes(username)
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkRegistrationRequest (
|
||||||
|
options: CheckerBaseParams & {
|
||||||
|
username: string
|
||||||
|
registrationReason: string
|
||||||
|
checkType: CheckerType
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { username, registrationReason } = options
|
||||||
|
const notificationType = UserNotificationType.NEW_USER_REGISTRATION_REQUEST
|
||||||
|
|
||||||
|
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||||
|
if (checkType === 'presence') {
|
||||||
|
expect(notification).to.not.be.undefined
|
||||||
|
expect(notification.type).to.equal(notificationType)
|
||||||
|
|
||||||
|
expect(notification.registration.username).to.equal(username)
|
||||||
|
} else {
|
||||||
|
expect(notification).to.satisfy(n => n.type !== notificationType || n.registration.username !== username)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emailNotificationFinder (email: object) {
|
||||||
|
const text: string = email['text']
|
||||||
|
|
||||||
|
return text.includes(' wants to register ') && text.includes(username) && text.includes(registrationReason)
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||||
|
}
|
|
@ -0,0 +1,203 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
|
|
||||||
|
import { UserNotification, UserNotificationType } from '@peertube/peertube-models'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
import { checkActor, CheckerBaseParams, CheckerType, checkNotification, checkVideo } from './shared/notification-checker.js'
|
||||||
|
|
||||||
|
export async function checkNewVideoFromSubscription (
|
||||||
|
options: CheckerBaseParams & {
|
||||||
|
videoName: string
|
||||||
|
shortUUID: string
|
||||||
|
checkType: CheckerType
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { videoName, shortUUID } = options
|
||||||
|
const notificationType = UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION
|
||||||
|
|
||||||
|
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||||
|
if (checkType === 'presence') {
|
||||||
|
expect(notification).to.not.be.undefined
|
||||||
|
expect(notification.type).to.equal(notificationType)
|
||||||
|
|
||||||
|
checkVideo(notification.video, videoName, shortUUID)
|
||||||
|
checkActor(notification.video.channel)
|
||||||
|
} else {
|
||||||
|
expect(notification).to.satisfy((n: UserNotification) => {
|
||||||
|
return n === undefined || n.type !== UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION || n.video.name !== videoName
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emailNotificationFinder (email: object) {
|
||||||
|
const text = email['text']
|
||||||
|
return text.indexOf(shortUUID) !== -1 && text.indexOf('Your subscription') !== -1
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkNewLiveFromSubscription (
|
||||||
|
options: CheckerBaseParams & {
|
||||||
|
videoName: string
|
||||||
|
shortUUID: string
|
||||||
|
checkType: CheckerType
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { videoName, shortUUID } = options
|
||||||
|
const notificationType = UserNotificationType.NEW_LIVE_FROM_SUBSCRIPTION
|
||||||
|
|
||||||
|
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||||
|
if (checkType === 'presence') {
|
||||||
|
expect(notification).to.not.be.undefined
|
||||||
|
expect(notification.type).to.equal(notificationType)
|
||||||
|
|
||||||
|
checkVideo(notification.video, videoName, shortUUID)
|
||||||
|
checkActor(notification.video.channel)
|
||||||
|
} else {
|
||||||
|
expect(notification).to.satisfy((n: UserNotification) => {
|
||||||
|
return n === undefined || n.type !== UserNotificationType.NEW_LIVE_FROM_SUBSCRIPTION || n.video.name !== videoName
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emailNotificationFinder (email: object) {
|
||||||
|
const text = email['text']
|
||||||
|
return text.indexOf(shortUUID) !== -1 && text.indexOf('Your subscription') !== -1
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkMyVideoIsPublished (
|
||||||
|
options: CheckerBaseParams & {
|
||||||
|
videoName: string
|
||||||
|
shortUUID: string
|
||||||
|
checkType: CheckerType
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { videoName, shortUUID } = options
|
||||||
|
const notificationType = UserNotificationType.MY_VIDEO_PUBLISHED
|
||||||
|
|
||||||
|
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||||
|
if (checkType === 'presence') {
|
||||||
|
expect(notification).to.not.be.undefined
|
||||||
|
expect(notification.type).to.equal(notificationType)
|
||||||
|
|
||||||
|
checkVideo(notification.video, videoName, shortUUID)
|
||||||
|
checkActor(notification.video.channel)
|
||||||
|
} else {
|
||||||
|
expect(notification.video).to.satisfy(v => v === undefined || v.name !== videoName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emailNotificationFinder (email: object) {
|
||||||
|
const text: string = email['text']
|
||||||
|
return text.includes(shortUUID) && text.includes('Your video')
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkVideoStudioEditionIsFinished (
|
||||||
|
options: CheckerBaseParams & {
|
||||||
|
videoName: string
|
||||||
|
shortUUID: string
|
||||||
|
checkType: CheckerType
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { videoName, shortUUID } = options
|
||||||
|
const notificationType = UserNotificationType.MY_VIDEO_STUDIO_EDITION_FINISHED
|
||||||
|
|
||||||
|
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||||
|
if (checkType === 'presence') {
|
||||||
|
expect(notification).to.not.be.undefined
|
||||||
|
expect(notification.type).to.equal(notificationType)
|
||||||
|
|
||||||
|
checkVideo(notification.video, videoName, shortUUID)
|
||||||
|
checkActor(notification.video.channel)
|
||||||
|
} else {
|
||||||
|
expect(notification.video).to.satisfy(v => v === undefined || v.name !== videoName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emailNotificationFinder (email: object) {
|
||||||
|
const text: string = email['text']
|
||||||
|
return text.includes(shortUUID) && text.includes('Edition of your video')
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkMyVideoImportIsFinished (
|
||||||
|
options: CheckerBaseParams & {
|
||||||
|
videoName: string
|
||||||
|
shortUUID: string
|
||||||
|
url: string
|
||||||
|
success: boolean
|
||||||
|
checkType: CheckerType
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { videoName, shortUUID, url, success } = options
|
||||||
|
|
||||||
|
const notificationType = success ? UserNotificationType.MY_VIDEO_IMPORT_SUCCESS : UserNotificationType.MY_VIDEO_IMPORT_ERROR
|
||||||
|
|
||||||
|
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||||
|
if (checkType === 'presence') {
|
||||||
|
expect(notification).to.not.be.undefined
|
||||||
|
expect(notification.type).to.equal(notificationType)
|
||||||
|
|
||||||
|
expect(notification.videoImport.targetUrl).to.equal(url)
|
||||||
|
|
||||||
|
if (success) checkVideo(notification.videoImport.video, videoName, shortUUID)
|
||||||
|
} else {
|
||||||
|
expect(notification.videoImport).to.satisfy(i => i === undefined || i.targetUrl !== url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emailNotificationFinder (email: object) {
|
||||||
|
const text: string = email['text']
|
||||||
|
const toFind = success
|
||||||
|
? /\bfinished\b/
|
||||||
|
: /\berror\b/
|
||||||
|
|
||||||
|
return text.includes(url) && !!text.match(toFind)
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkMyVideoTranscriptionGenerated (
|
||||||
|
options: CheckerBaseParams & {
|
||||||
|
videoName: string
|
||||||
|
shortUUID: string
|
||||||
|
language: {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
checkType: CheckerType
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { videoName, shortUUID, language } = options
|
||||||
|
const notificationType = UserNotificationType.MY_VIDEO_TRANSCRIPTION_GENERATED
|
||||||
|
|
||||||
|
function notificationChecker (notification: UserNotification, checkType: CheckerType) {
|
||||||
|
if (checkType === 'presence') {
|
||||||
|
expect(notification).to.not.be.undefined
|
||||||
|
expect(notification.type).to.equal(notificationType)
|
||||||
|
|
||||||
|
expect(notification.videoCaption).to.exist
|
||||||
|
expect(notification.videoCaption.language.id).to.equal(language.id)
|
||||||
|
expect(notification.videoCaption.language.label).to.equal(language.label)
|
||||||
|
checkVideo(notification.videoCaption.video, videoName, shortUUID)
|
||||||
|
} else {
|
||||||
|
expect(notification.videoCaption).to.satisfy(c => c === undefined || c.Video.shortUUID !== shortUUID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emailNotificationFinder (email: object) {
|
||||||
|
const text: string = email['text']
|
||||||
|
return text.includes(shortUUID) && text.includes('Transcription in ' + language.label)
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
|
||||||
|
}
|
137
packages/tests/src/shared/notifications/notifications-common.ts
Normal file
137
packages/tests/src/shared/notifications/notifications-common.ts
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
|
|
||||||
|
import { wait } from '@peertube/peertube-core-utils'
|
||||||
|
import {
|
||||||
|
UserNotification,
|
||||||
|
UserNotificationSetting,
|
||||||
|
UserNotificationSettingValue,
|
||||||
|
UserNotificationType_Type
|
||||||
|
} from '@peertube/peertube-models'
|
||||||
|
import {
|
||||||
|
ConfigCommand,
|
||||||
|
createMultipleServers,
|
||||||
|
doubleFollow,
|
||||||
|
PeerTubeServer,
|
||||||
|
setAccessTokensToServers,
|
||||||
|
setDefaultAccountAvatar,
|
||||||
|
setDefaultChannelAvatar,
|
||||||
|
setDefaultVideoChannel,
|
||||||
|
waitJobs
|
||||||
|
} from '@peertube/peertube-server-commands'
|
||||||
|
import { MockSmtpServer } from '../mock-servers/mock-email.js'
|
||||||
|
|
||||||
|
export function getAllNotificationsSettings (): UserNotificationSetting {
|
||||||
|
return {
|
||||||
|
newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||||
|
newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||||
|
abuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||||
|
videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||||
|
blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||||
|
myVideoImportFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||||
|
myVideoPublished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||||
|
commentMention: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||||
|
newFollow: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||||
|
newUserRegistration: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||||
|
newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||||
|
abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||||
|
abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||||
|
autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||||
|
newPeerTubeVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||||
|
myVideoStudioEditionFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||||
|
myVideoTranscriptionGenerated: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
|
||||||
|
newPluginVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function waitUntilNotification (options: {
|
||||||
|
server: PeerTubeServer
|
||||||
|
notificationType: UserNotificationType_Type
|
||||||
|
token: string
|
||||||
|
fromDate: Date
|
||||||
|
}) {
|
||||||
|
const { server, fromDate, notificationType, token } = options
|
||||||
|
|
||||||
|
do {
|
||||||
|
const { data } = await server.notifications.list({ start: 0, count: 5, token })
|
||||||
|
if (data.some(n => n.type === notificationType && new Date(n.createdAt) >= fromDate)) break
|
||||||
|
|
||||||
|
await wait(500)
|
||||||
|
} while (true)
|
||||||
|
|
||||||
|
await waitJobs([ server ])
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function prepareNotificationsTest (serversCount = 3, overrideConfigArg: any = {}) {
|
||||||
|
const userNotifications: UserNotification[] = []
|
||||||
|
const adminNotifications: UserNotification[] = []
|
||||||
|
const adminNotificationsServer2: UserNotification[] = []
|
||||||
|
const emails: object[] = []
|
||||||
|
|
||||||
|
const port = await MockSmtpServer.Instance.collectEmails(emails)
|
||||||
|
|
||||||
|
const overrideConfig = {
|
||||||
|
...ConfigCommand.getEmailOverrideConfig(port),
|
||||||
|
|
||||||
|
signup: {
|
||||||
|
limit: 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const servers = await createMultipleServers(serversCount, Object.assign(overrideConfig, overrideConfigArg))
|
||||||
|
|
||||||
|
await setAccessTokensToServers(servers)
|
||||||
|
await setDefaultVideoChannel(servers)
|
||||||
|
await setDefaultChannelAvatar(servers)
|
||||||
|
await setDefaultAccountAvatar(servers)
|
||||||
|
|
||||||
|
if (servers[1]) {
|
||||||
|
await servers[1].config.enableStudio()
|
||||||
|
await servers[1].config.enableLive({ allowReplay: true, transcoding: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serversCount > 1) {
|
||||||
|
await doubleFollow(servers[0], servers[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = { username: 'user_1', password: 'super password' }
|
||||||
|
await servers[0].users.create({ ...user, videoQuota: 10 * 1000 * 1000 })
|
||||||
|
const userAccessToken = await servers[0].login.getAccessToken(user)
|
||||||
|
await servers[0].users.updateMe({ token: userAccessToken, displayName: 'User 1' })
|
||||||
|
|
||||||
|
await servers[0].notifications.updateMySettings({ token: userAccessToken, settings: getAllNotificationsSettings() })
|
||||||
|
await servers[0].users.updateMyAvatar({ token: userAccessToken, fixture: 'avatar.png' })
|
||||||
|
await servers[0].channels.updateImage({ channelName: 'user_1_channel', token: userAccessToken, fixture: 'avatar.png', type: 'avatar' })
|
||||||
|
|
||||||
|
await servers[0].notifications.updateMySettings({ settings: getAllNotificationsSettings() })
|
||||||
|
|
||||||
|
if (serversCount > 1) {
|
||||||
|
await servers[1].notifications.updateMySettings({ settings: getAllNotificationsSettings() })
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const socket = servers[0].socketIO.getUserNotificationSocket({ token: userAccessToken })
|
||||||
|
socket.on('new-notification', n => userNotifications.push(n))
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const socket = servers[0].socketIO.getUserNotificationSocket()
|
||||||
|
socket.on('new-notification', n => adminNotifications.push(n))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serversCount > 1) {
|
||||||
|
const socket = servers[1].socketIO.getUserNotificationSocket()
|
||||||
|
socket.on('new-notification', n => adminNotificationsServer2.push(n))
|
||||||
|
}
|
||||||
|
|
||||||
|
const { videoChannels } = await servers[0].users.getMyInfo()
|
||||||
|
const channelId = videoChannels[0].id
|
||||||
|
|
||||||
|
return {
|
||||||
|
userNotifications,
|
||||||
|
adminNotifications,
|
||||||
|
adminNotificationsServer2,
|
||||||
|
userAccessToken,
|
||||||
|
emails,
|
||||||
|
servers,
|
||||||
|
channelId,
|
||||||
|
baseOverrideConfig: overrideConfig
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
|
|
||||||
|
import { UserNotification } from '@peertube/peertube-models'
|
||||||
|
import { PeerTubeServer } from '@peertube/peertube-server-commands'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
import { inspect } from 'util'
|
||||||
|
|
||||||
|
export type CheckerBaseParams = {
|
||||||
|
server: PeerTubeServer
|
||||||
|
emails: any[]
|
||||||
|
socketNotifications: UserNotification[]
|
||||||
|
token: string
|
||||||
|
check?: { web: boolean, mail: boolean }
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CheckerType = 'presence' | 'absence'
|
||||||
|
|
||||||
|
export async function checkNotification (
|
||||||
|
options: CheckerBaseParams & {
|
||||||
|
notificationChecker: (notification: UserNotification, checkType: CheckerType) => void
|
||||||
|
emailNotificationFinder: (email: object) => boolean
|
||||||
|
checkType: CheckerType
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { server, token, checkType, notificationChecker, emailNotificationFinder, socketNotifications, emails } = options
|
||||||
|
|
||||||
|
const check = options.check || { web: true, mail: true }
|
||||||
|
|
||||||
|
if (check.web) {
|
||||||
|
const notification = await server.notifications.getLatest({ token })
|
||||||
|
|
||||||
|
if (notification || checkType !== 'absence') {
|
||||||
|
notificationChecker(notification, checkType)
|
||||||
|
}
|
||||||
|
|
||||||
|
const socketNotification = socketNotifications.find(n => {
|
||||||
|
try {
|
||||||
|
notificationChecker(n, 'presence')
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (checkType === 'presence') {
|
||||||
|
const obj = inspect(socketNotifications, { depth: 5 })
|
||||||
|
expect(socketNotification, 'The socket notification is absent when it should be present. ' + obj).to.not.be.undefined
|
||||||
|
} else {
|
||||||
|
const obj = inspect(socketNotification, { depth: 5 })
|
||||||
|
expect(socketNotification, 'The socket notification is present when it should not be present. ' + obj).to.be.undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (check.mail) {
|
||||||
|
// Last email
|
||||||
|
const email = emails.slice()
|
||||||
|
.reverse()
|
||||||
|
.find(e => emailNotificationFinder(e))
|
||||||
|
|
||||||
|
if (checkType === 'presence') {
|
||||||
|
const texts = emails.map(e => e.text)
|
||||||
|
expect(email, 'The email is absent when is should be present. ' + inspect(texts)).to.not.be.undefined
|
||||||
|
} else {
|
||||||
|
expect(email, 'The email is present when is should not be present. ' + inspect(email)).to.be.undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkVideo (video: any, videoName?: string, shortUUID?: string) {
|
||||||
|
if (videoName) {
|
||||||
|
expect(video.name).to.be.a('string')
|
||||||
|
expect(video.name).to.not.be.empty
|
||||||
|
expect(video.name).to.equal(videoName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shortUUID) {
|
||||||
|
expect(video.shortUUID).to.be.a('string')
|
||||||
|
expect(video.shortUUID).to.not.be.empty
|
||||||
|
expect(video.shortUUID).to.equal(shortUUID)
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(video.state).to.exist
|
||||||
|
expect(video.id).to.be.a('number')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkActor (actor: any, options: { withAvatar?: boolean } = {}) {
|
||||||
|
const { withAvatar = true } = options
|
||||||
|
|
||||||
|
expect(actor.displayName).to.be.a('string')
|
||||||
|
expect(actor.displayName).to.not.be.empty
|
||||||
|
expect(actor.host).to.not.be.undefined
|
||||||
|
|
||||||
|
if (withAvatar) {
|
||||||
|
expect(actor.avatars).to.be.an('array')
|
||||||
|
expect(actor.avatars).to.have.lengthOf(4)
|
||||||
|
expect(actor.avatars[0].path).to.exist.and.not.empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkComment (comment: any, commentId: number, threadId: number) {
|
||||||
|
expect(comment.id).to.equal(commentId)
|
||||||
|
expect(comment.threadId).to.equal(threadId)
|
||||||
|
}
|
|
@ -61,28 +61,28 @@ activityPubClientRouter.get(
|
||||||
[ '/accounts?/:handle', '/accounts?/:handle/video-channels', '/a/:handle', '/a/:handle/video-channels' ],
|
[ '/accounts?/:handle', '/accounts?/:handle/video-channels', '/a/:handle', '/a/:handle/video-channels' ],
|
||||||
executeIfActivityPub,
|
executeIfActivityPub,
|
||||||
activityPubRateLimiter,
|
activityPubRateLimiter,
|
||||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkManage: false })),
|
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkCanManage: false })),
|
||||||
asyncMiddleware(accountController)
|
asyncMiddleware(accountController)
|
||||||
)
|
)
|
||||||
activityPubClientRouter.get(
|
activityPubClientRouter.get(
|
||||||
'/accounts?/:handle/followers',
|
'/accounts?/:handle/followers',
|
||||||
executeIfActivityPub,
|
executeIfActivityPub,
|
||||||
activityPubRateLimiter,
|
activityPubRateLimiter,
|
||||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkManage: false })),
|
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkCanManage: false })),
|
||||||
asyncMiddleware(accountFollowersController)
|
asyncMiddleware(accountFollowersController)
|
||||||
)
|
)
|
||||||
activityPubClientRouter.get(
|
activityPubClientRouter.get(
|
||||||
'/accounts?/:handle/following',
|
'/accounts?/:handle/following',
|
||||||
executeIfActivityPub,
|
executeIfActivityPub,
|
||||||
activityPubRateLimiter,
|
activityPubRateLimiter,
|
||||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkManage: false })),
|
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkCanManage: false })),
|
||||||
asyncMiddleware(accountFollowingController)
|
asyncMiddleware(accountFollowingController)
|
||||||
)
|
)
|
||||||
activityPubClientRouter.get(
|
activityPubClientRouter.get(
|
||||||
'/accounts?/:handle/playlists',
|
'/accounts?/:handle/playlists',
|
||||||
executeIfActivityPub,
|
executeIfActivityPub,
|
||||||
activityPubRateLimiter,
|
activityPubRateLimiter,
|
||||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkManage: false })),
|
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkCanManage: false })),
|
||||||
asyncMiddleware(accountPlaylistsController)
|
asyncMiddleware(accountPlaylistsController)
|
||||||
)
|
)
|
||||||
activityPubClientRouter.get(
|
activityPubClientRouter.get(
|
||||||
|
@ -212,35 +212,35 @@ activityPubClientRouter.get(
|
||||||
[ '/video-channels/:handle', '/video-channels/:handle/videos', '/c/:handle', '/c/:handle/videos' ],
|
[ '/video-channels/:handle', '/video-channels/:handle/videos', '/c/:handle', '/c/:handle/videos' ],
|
||||||
executeIfActivityPub,
|
executeIfActivityPub,
|
||||||
activityPubRateLimiter,
|
activityPubRateLimiter,
|
||||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: false })),
|
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: false, checkIsOwner: false })),
|
||||||
asyncMiddleware(videoChannelController)
|
asyncMiddleware(videoChannelController)
|
||||||
)
|
)
|
||||||
activityPubClientRouter.get(
|
activityPubClientRouter.get(
|
||||||
'/video-channels/:handle/followers',
|
'/video-channels/:handle/followers',
|
||||||
executeIfActivityPub,
|
executeIfActivityPub,
|
||||||
activityPubRateLimiter,
|
activityPubRateLimiter,
|
||||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: false })),
|
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: false, checkIsOwner: false })),
|
||||||
asyncMiddleware(videoChannelFollowersController)
|
asyncMiddleware(videoChannelFollowersController)
|
||||||
)
|
)
|
||||||
activityPubClientRouter.get(
|
activityPubClientRouter.get(
|
||||||
'/video-channels/:handle/following',
|
'/video-channels/:handle/following',
|
||||||
executeIfActivityPub,
|
executeIfActivityPub,
|
||||||
activityPubRateLimiter,
|
activityPubRateLimiter,
|
||||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: false })),
|
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: false, checkIsOwner: false })),
|
||||||
asyncMiddleware(videoChannelFollowingController)
|
asyncMiddleware(videoChannelFollowingController)
|
||||||
)
|
)
|
||||||
activityPubClientRouter.get(
|
activityPubClientRouter.get(
|
||||||
'/video-channels/:handle/playlists',
|
'/video-channels/:handle/playlists',
|
||||||
executeIfActivityPub,
|
executeIfActivityPub,
|
||||||
activityPubRateLimiter,
|
activityPubRateLimiter,
|
||||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: false })),
|
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: false, checkIsOwner: false })),
|
||||||
asyncMiddleware(videoChannelPlaylistsController)
|
asyncMiddleware(videoChannelPlaylistsController)
|
||||||
)
|
)
|
||||||
activityPubClientRouter.get(
|
activityPubClientRouter.get(
|
||||||
'/video-channels/:handle/player-settings',
|
'/video-channels/:handle/player-settings',
|
||||||
executeIfActivityPub,
|
executeIfActivityPub,
|
||||||
activityPubRateLimiter,
|
activityPubRateLimiter,
|
||||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: false })),
|
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: false, checkIsOwner: false })),
|
||||||
asyncMiddleware(channelPlayerSettingsController)
|
asyncMiddleware(channelPlayerSettingsController)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -462,7 +462,7 @@ async function videoCommentController (req: express.Request, res: express.Respon
|
||||||
const videoComment = res.locals.videoCommentFull
|
const videoComment = res.locals.videoCommentFull
|
||||||
|
|
||||||
if (redirectIfNotOwned(videoComment.url, res)) return
|
if (redirectIfNotOwned(videoComment.url, res)) return
|
||||||
if (videoComment.Video.isOwned() && videoComment.heldForReview === true) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
|
if (videoComment.Video.isLocal() && videoComment.heldForReview === true) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
|
||||||
|
|
||||||
const threadParentComments = await VideoCommentModel.listThreadParentComments({ comment: videoComment })
|
const threadParentComments = await VideoCommentModel.listThreadParentComments({ comment: videoComment })
|
||||||
|
|
||||||
|
@ -484,7 +484,7 @@ async function videoCommentController (req: express.Request, res: express.Respon
|
||||||
async function videoCommentApprovedController (req: express.Request, res: express.Response) {
|
async function videoCommentApprovedController (req: express.Request, res: express.Response) {
|
||||||
const comment = res.locals.videoCommentFull
|
const comment = res.locals.videoCommentFull
|
||||||
|
|
||||||
if (!comment.Video.isOwned() || comment.heldForReview === true) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
|
if (!comment.Video.isLocal() || comment.heldForReview === true) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
|
||||||
|
|
||||||
const activity = buildApprovalActivity({ comment, type: 'ApproveReply' })
|
const activity = buildApprovalActivity({ comment, type: 'ApproveReply' })
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,7 @@ inboxRouter.post(
|
||||||
activityPubRateLimiter,
|
activityPubRateLimiter,
|
||||||
signatureValidator,
|
signatureValidator,
|
||||||
asyncMiddleware(checkSignature),
|
asyncMiddleware(checkSignature),
|
||||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkManage: false })),
|
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkCanManage: false })),
|
||||||
asyncMiddleware(activityPubValidator),
|
asyncMiddleware(activityPubValidator),
|
||||||
inboxController
|
inboxController
|
||||||
)
|
)
|
||||||
|
@ -39,7 +39,7 @@ inboxRouter.post(
|
||||||
activityPubRateLimiter,
|
activityPubRateLimiter,
|
||||||
signatureValidator,
|
signatureValidator,
|
||||||
asyncMiddleware(checkSignature),
|
asyncMiddleware(checkSignature),
|
||||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: false })),
|
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: false, checkIsOwner: false })),
|
||||||
asyncMiddleware(activityPubValidator),
|
asyncMiddleware(activityPubValidator),
|
||||||
inboxController
|
inboxController
|
||||||
)
|
)
|
||||||
|
|
|
@ -23,7 +23,7 @@ outboxRouter.get(
|
||||||
'/accounts/:handle/outbox',
|
'/accounts/:handle/outbox',
|
||||||
activityPubRateLimiter,
|
activityPubRateLimiter,
|
||||||
apPaginationValidator,
|
apPaginationValidator,
|
||||||
accountHandleGetValidatorFactory({ checkIsLocal: true, checkManage: false }),
|
accountHandleGetValidatorFactory({ checkIsLocal: true, checkCanManage: false }),
|
||||||
asyncMiddleware(outboxController)
|
asyncMiddleware(outboxController)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ outboxRouter.get(
|
||||||
'/video-channels/:handle/outbox',
|
'/video-channels/:handle/outbox',
|
||||||
activityPubRateLimiter,
|
activityPubRateLimiter,
|
||||||
apPaginationValidator,
|
apPaginationValidator,
|
||||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: false })),
|
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: false, checkIsOwner: false })),
|
||||||
asyncMiddleware(outboxController)
|
asyncMiddleware(outboxController)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -26,12 +26,16 @@ import {
|
||||||
accountHandleGetValidatorFactory,
|
accountHandleGetValidatorFactory,
|
||||||
accountsFollowersSortValidator,
|
accountsFollowersSortValidator,
|
||||||
accountsSortValidator,
|
accountsSortValidator,
|
||||||
|
listAccountChannelsValidator,
|
||||||
videoChannelsSortValidator,
|
videoChannelsSortValidator,
|
||||||
videoChannelStatsValidator,
|
|
||||||
videoChannelSyncsSortValidator,
|
videoChannelSyncsSortValidator,
|
||||||
videosSortValidator
|
videosSortValidator
|
||||||
} from '../../middlewares/validators/index.js'
|
} from '../../middlewares/validators/index.js'
|
||||||
import { commonVideoPlaylistFiltersValidator, videoPlaylistsSearchValidator } from '../../middlewares/validators/videos/video-playlists.js'
|
import {
|
||||||
|
commonVideoPlaylistFiltersValidator,
|
||||||
|
videoPlaylistsAccountValidator,
|
||||||
|
videoPlaylistsSearchValidator
|
||||||
|
} from '../../middlewares/validators/videos/video-playlists.js'
|
||||||
import { AccountVideoRateModel } from '../../models/account/account-video-rate.js'
|
import { AccountVideoRateModel } from '../../models/account/account-video-rate.js'
|
||||||
import { AccountModel } from '../../models/account/account.js'
|
import { AccountModel } from '../../models/account/account.js'
|
||||||
import { guessAdditionalAttributesFromQuery } from '../../models/video/formatter/index.js'
|
import { guessAdditionalAttributesFromQuery } from '../../models/video/formatter/index.js'
|
||||||
|
@ -54,13 +58,13 @@ accountsRouter.get(
|
||||||
|
|
||||||
accountsRouter.get(
|
accountsRouter.get(
|
||||||
'/:handle',
|
'/:handle',
|
||||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: false, checkManage: false })),
|
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: false, checkCanManage: false })),
|
||||||
getAccount
|
getAccount
|
||||||
)
|
)
|
||||||
|
|
||||||
accountsRouter.get(
|
accountsRouter.get(
|
||||||
'/:handle/videos',
|
'/:handle/videos',
|
||||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: false, checkManage: false })),
|
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: false, checkCanManage: false })),
|
||||||
paginationValidator,
|
paginationValidator,
|
||||||
videosSortValidator,
|
videosSortValidator,
|
||||||
setDefaultVideosSort,
|
setDefaultVideosSort,
|
||||||
|
@ -72,8 +76,8 @@ accountsRouter.get(
|
||||||
|
|
||||||
accountsRouter.get(
|
accountsRouter.get(
|
||||||
'/:handle/video-channels',
|
'/:handle/video-channels',
|
||||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: false, checkManage: false })),
|
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: false, checkCanManage: false })),
|
||||||
videoChannelStatsValidator,
|
listAccountChannelsValidator,
|
||||||
paginationValidator,
|
paginationValidator,
|
||||||
videoChannelsSortValidator,
|
videoChannelsSortValidator,
|
||||||
setDefaultSort,
|
setDefaultSort,
|
||||||
|
@ -84,20 +88,21 @@ accountsRouter.get(
|
||||||
accountsRouter.get(
|
accountsRouter.get(
|
||||||
'/:handle/video-playlists',
|
'/:handle/video-playlists',
|
||||||
optionalAuthenticate,
|
optionalAuthenticate,
|
||||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: false, checkManage: false })),
|
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: false, checkCanManage: false })),
|
||||||
paginationValidator,
|
paginationValidator,
|
||||||
videoPlaylistsSortValidator,
|
videoPlaylistsSortValidator,
|
||||||
setDefaultSort,
|
setDefaultSort,
|
||||||
setDefaultPagination,
|
setDefaultPagination,
|
||||||
commonVideoPlaylistFiltersValidator,
|
commonVideoPlaylistFiltersValidator,
|
||||||
videoPlaylistsSearchValidator,
|
videoPlaylistsSearchValidator,
|
||||||
|
videoPlaylistsAccountValidator,
|
||||||
asyncMiddleware(listAccountPlaylists)
|
asyncMiddleware(listAccountPlaylists)
|
||||||
)
|
)
|
||||||
|
|
||||||
accountsRouter.get(
|
accountsRouter.get(
|
||||||
'/:handle/video-channel-syncs',
|
'/:handle/video-channel-syncs',
|
||||||
authenticate,
|
authenticate,
|
||||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkManage: true })),
|
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkCanManage: true })),
|
||||||
paginationValidator,
|
paginationValidator,
|
||||||
videoChannelSyncsSortValidator,
|
videoChannelSyncsSortValidator,
|
||||||
setDefaultSort,
|
setDefaultSort,
|
||||||
|
@ -108,7 +113,7 @@ accountsRouter.get(
|
||||||
accountsRouter.get(
|
accountsRouter.get(
|
||||||
'/:handle/ratings',
|
'/:handle/ratings',
|
||||||
authenticate,
|
authenticate,
|
||||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkManage: true })),
|
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkCanManage: true })),
|
||||||
paginationValidator,
|
paginationValidator,
|
||||||
videoRatesSortValidator,
|
videoRatesSortValidator,
|
||||||
setDefaultSort,
|
setDefaultSort,
|
||||||
|
@ -120,7 +125,7 @@ accountsRouter.get(
|
||||||
accountsRouter.get(
|
accountsRouter.get(
|
||||||
'/:handle/followers',
|
'/:handle/followers',
|
||||||
authenticate,
|
authenticate,
|
||||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkManage: true })),
|
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkCanManage: true })),
|
||||||
paginationValidator,
|
paginationValidator,
|
||||||
accountsFollowersSortValidator,
|
accountsFollowersSortValidator,
|
||||||
setDefaultSort,
|
setDefaultSort,
|
||||||
|
@ -153,16 +158,15 @@ async function listAccounts (req: express.Request, res: express.Response) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listAccountChannels (req: express.Request, res: express.Response) {
|
async function listAccountChannels (req: express.Request, res: express.Response) {
|
||||||
const options = {
|
const resultList = await VideoChannelModel.listByAccountForAPI({
|
||||||
accountId: res.locals.account.id,
|
accountId: res.locals.account.id,
|
||||||
start: req.query.start,
|
start: req.query.start,
|
||||||
count: req.query.count,
|
count: req.query.count,
|
||||||
sort: req.query.sort,
|
sort: req.query.sort,
|
||||||
withStats: req.query.withStats,
|
withStats: req.query.withStats,
|
||||||
|
includeCollaborations: req.query.includeCollaborations,
|
||||||
search: req.query.search
|
search: req.query.search
|
||||||
}
|
})
|
||||||
|
|
||||||
const resultList = await VideoChannelModel.listByAccountForAPI(options)
|
|
||||||
|
|
||||||
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
||||||
}
|
}
|
||||||
|
@ -183,7 +187,7 @@ async function listAccountChannelsSync (req: express.Request, res: express.Respo
|
||||||
|
|
||||||
async function listAccountPlaylists (req: express.Request, res: express.Response) {
|
async function listAccountPlaylists (req: express.Request, res: express.Response) {
|
||||||
const serverActor = await getServerActor()
|
const serverActor = await getServerActor()
|
||||||
const query = req.query as VideoPlaylistsListQuery
|
const query = req.query as VideoPlaylistsListQuery & { includeCollaborations?: boolean }
|
||||||
|
|
||||||
// Allow users to see their private/unlisted video playlists
|
// Allow users to see their private/unlisted video playlists
|
||||||
let listMyPlaylists = false
|
let listMyPlaylists = false
|
||||||
|
@ -204,7 +208,9 @@ async function listAccountPlaylists (req: express.Request, res: express.Response
|
||||||
sort: query.sort,
|
sort: query.sort,
|
||||||
search: query.search,
|
search: query.search,
|
||||||
|
|
||||||
type: query.playlistType
|
type: query.playlistType,
|
||||||
|
|
||||||
|
includeCollaborations: query.includeCollaborations
|
||||||
})
|
})
|
||||||
|
|
||||||
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
||||||
|
@ -260,7 +266,7 @@ async function listAccountRatings (req: express.Request, res: express.Response)
|
||||||
async function listAccountFollowers (req: express.Request, res: express.Response) {
|
async function listAccountFollowers (req: express.Request, res: express.Response) {
|
||||||
const account = res.locals.account
|
const account = res.locals.account
|
||||||
|
|
||||||
const channels = await VideoChannelModel.listAllByAccount(account.id)
|
const channels = await VideoChannelModel.listAllOwnedByAccount(account.id)
|
||||||
const actorIds = [ account.actorId ].concat(channels.map(c => c.actorId))
|
const actorIds = [ account.actorId ].concat(channels.map(c => c.actorId))
|
||||||
|
|
||||||
const resultList = await ActorFollowModel.listFollowersForApi({
|
const resultList = await ActorFollowModel.listFollowersForApi({
|
||||||
|
|
|
@ -21,7 +21,7 @@ import { searchRouter } from './search/index.js'
|
||||||
import { serverRouter } from './server/index.js'
|
import { serverRouter } from './server/index.js'
|
||||||
import { usersRouter } from './users/index.js'
|
import { usersRouter } from './users/index.js'
|
||||||
import { videoChannelSyncRouter } from './video-channel-sync.js'
|
import { videoChannelSyncRouter } from './video-channel-sync.js'
|
||||||
import { videoChannelRouter } from './video-channel.js'
|
import { videoChannelRouter } from './video-channels/index.js'
|
||||||
import { videoPlaylistRouter } from './video-playlist.js'
|
import { videoPlaylistRouter } from './video-playlist.js'
|
||||||
import { videosRouter } from './videos/index.js'
|
import { videosRouter } from './videos/index.js'
|
||||||
import { watchedWordsRouter } from './watched-words.js'
|
import { watchedWordsRouter } from './watched-words.js'
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { PlayerChannelSettingsUpdate, PlayerVideoSettingsUpdate } from '@peertube/peertube-models'
|
import { PlayerChannelSettingsUpdate, PlayerVideoSettingsUpdate } from '@peertube/peertube-models'
|
||||||
|
import { sendUpdateChannelPlayerSettings, sendUpdateVideoPlayerSettings } from '@server/lib/activitypub/send/send-update.js'
|
||||||
import { upsertPlayerSettings } from '@server/lib/player-settings.js'
|
import { upsertPlayerSettings } from '@server/lib/player-settings.js'
|
||||||
import {
|
import {
|
||||||
getChannelPlayerSettingsValidator,
|
getChannelPlayerSettingsValidator,
|
||||||
|
@ -15,7 +16,6 @@ import {
|
||||||
optionalAuthenticate,
|
optionalAuthenticate,
|
||||||
videoChannelsHandleValidatorFactory
|
videoChannelsHandleValidatorFactory
|
||||||
} from '../../middlewares/index.js'
|
} from '../../middlewares/index.js'
|
||||||
import { sendUpdateChannelPlayerSettings, sendUpdateVideoPlayerSettings } from '@server/lib/activitypub/send/send-update.js'
|
|
||||||
|
|
||||||
const playerSettingsRouter = express.Router()
|
const playerSettingsRouter = express.Router()
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ playerSettingsRouter.put(
|
||||||
playerSettingsRouter.get(
|
playerSettingsRouter.get(
|
||||||
'/video-channels/:handle',
|
'/video-channels/:handle',
|
||||||
optionalAuthenticate,
|
optionalAuthenticate,
|
||||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: false, checkManage: false })),
|
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: false, checkCanManage: false, checkIsOwner: false })),
|
||||||
getChannelPlayerSettingsValidator,
|
getChannelPlayerSettingsValidator,
|
||||||
asyncMiddleware(getChannelPlayerSettings)
|
asyncMiddleware(getChannelPlayerSettings)
|
||||||
)
|
)
|
||||||
|
@ -47,7 +47,7 @@ playerSettingsRouter.get(
|
||||||
playerSettingsRouter.put(
|
playerSettingsRouter.put(
|
||||||
'/video-channels/:handle',
|
'/video-channels/:handle',
|
||||||
authenticate,
|
authenticate,
|
||||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: true })),
|
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: true, checkIsOwner: false })),
|
||||||
updatePlayerSettingsValidatorFactory('channel'),
|
updatePlayerSettingsValidatorFactory('channel'),
|
||||||
asyncMiddleware(updateChannelPlayerSettings)
|
asyncMiddleware(updateChannelPlayerSettings)
|
||||||
)
|
)
|
||||||
|
|
|
@ -29,7 +29,8 @@ import { searchLocalUrl } from './shared/index.js'
|
||||||
|
|
||||||
const searchChannelsRouter = express.Router()
|
const searchChannelsRouter = express.Router()
|
||||||
|
|
||||||
searchChannelsRouter.get('/video-channels',
|
searchChannelsRouter.get(
|
||||||
|
'/video-channels',
|
||||||
openapiOperationDoc({ operationId: 'searchChannels' }),
|
openapiOperationDoc({ operationId: 'searchChannels' }),
|
||||||
paginationValidator,
|
paginationValidator,
|
||||||
setDefaultPagination,
|
setDefaultPagination,
|
||||||
|
@ -102,7 +103,7 @@ async function searchVideoChannelsDB (query: VideoChannelsSearchQueryAfterSaniti
|
||||||
}, 'filter:api.search.video-channels.local.list.params')
|
}, 'filter:api.search.video-channels.local.list.params')
|
||||||
|
|
||||||
const resultList = await Hooks.wrapPromiseFun(
|
const resultList = await Hooks.wrapPromiseFun(
|
||||||
VideoChannelModel.searchForApi.bind(VideoChannelModel),
|
VideoChannelModel.listForApi.bind(VideoChannelModel),
|
||||||
apiOptions,
|
apiOptions,
|
||||||
'filter:api.search.video-channels.local.list.result'
|
'filter:api.search.video-channels.local.list.result'
|
||||||
)
|
)
|
||||||
|
|
|
@ -56,10 +56,10 @@ const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_
|
||||||
|
|
||||||
const meRouter = express.Router()
|
const meRouter = express.Router()
|
||||||
|
|
||||||
meRouter.get('/me', authenticate, asyncMiddleware(getUserInformation))
|
meRouter.get('/me', authenticate, asyncMiddleware(getMyInformation))
|
||||||
meRouter.delete('/me', authenticate, deleteMeValidator, asyncMiddleware(deleteMe))
|
meRouter.delete('/me', authenticate, deleteMeValidator, asyncMiddleware(deleteMe))
|
||||||
|
|
||||||
meRouter.get('/me/video-quota-used', authenticate, asyncMiddleware(getUserVideoQuotaUsed))
|
meRouter.get('/me/video-quota-used', authenticate, asyncMiddleware(getMyVideoQuotaUsed))
|
||||||
|
|
||||||
meRouter.get(
|
meRouter.get(
|
||||||
'/me/videos/imports',
|
'/me/videos/imports',
|
||||||
|
@ -69,7 +69,7 @@ meRouter.get(
|
||||||
setDefaultSort,
|
setDefaultSort,
|
||||||
setDefaultPagination,
|
setDefaultPagination,
|
||||||
getMyVideoImportsValidator,
|
getMyVideoImportsValidator,
|
||||||
asyncMiddleware(getUserVideoImports)
|
asyncMiddleware(listMyVideoImports)
|
||||||
)
|
)
|
||||||
|
|
||||||
meRouter.get(
|
meRouter.get(
|
||||||
|
@ -92,14 +92,14 @@ meRouter.get(
|
||||||
setDefaultPagination,
|
setDefaultPagination,
|
||||||
commonVideosFiltersValidator,
|
commonVideosFiltersValidator,
|
||||||
asyncMiddleware(usersVideosValidator),
|
asyncMiddleware(usersVideosValidator),
|
||||||
asyncMiddleware(listUserVideos)
|
asyncMiddleware(listMyVideos)
|
||||||
)
|
)
|
||||||
|
|
||||||
meRouter.get(
|
meRouter.get(
|
||||||
'/me/videos/:videoId/rating',
|
'/me/videos/:videoId/rating',
|
||||||
authenticate,
|
authenticate,
|
||||||
asyncMiddleware(usersVideoRatingValidator),
|
asyncMiddleware(usersVideoRatingValidator),
|
||||||
asyncMiddleware(getUserVideoRating)
|
asyncMiddleware(getMyVideoRating)
|
||||||
)
|
)
|
||||||
|
|
||||||
meRouter.put(
|
meRouter.put(
|
||||||
|
@ -131,14 +131,15 @@ export {
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async function listUserVideos (req: express.Request, res: express.Response) {
|
async function listMyVideos (req: express.Request, res: express.Response) {
|
||||||
const user = res.locals.oauth.token.User
|
const user = res.locals.oauth.token.User
|
||||||
const countVideos = getCountVideos(req)
|
const countVideos = getCountVideos(req)
|
||||||
const query = pickCommonVideoQuery(req.query)
|
const query = pickCommonVideoQuery(req.query)
|
||||||
|
|
||||||
const include = (query.include || VideoInclude.NONE) | VideoInclude.BLACKLISTED | VideoInclude.NOT_PUBLISHED_STATE
|
const include = (query.include || VideoInclude.NONE) | VideoInclude.BLACKLISTED | VideoInclude.NOT_PUBLISHED_STATE
|
||||||
|
|
||||||
const apiOptions = await Hooks.wrapObject({
|
const apiOptions = await Hooks.wrapObject(
|
||||||
|
{
|
||||||
privacyOneOf: getAllPrivacies(),
|
privacyOneOf: getAllPrivacies(),
|
||||||
|
|
||||||
...query,
|
...query,
|
||||||
|
@ -152,11 +153,14 @@ async function listUserVideos (req: express.Request, res: express.Response) {
|
||||||
|
|
||||||
videoChannelId: res.locals.videoChannel?.id,
|
videoChannelId: res.locals.videoChannel?.id,
|
||||||
channelNameOneOf: req.query.channelNameOneOf,
|
channelNameOneOf: req.query.channelNameOneOf,
|
||||||
|
includeCollaborations: req.query.includeCollaborations || false,
|
||||||
|
|
||||||
countVideos,
|
countVideos,
|
||||||
|
|
||||||
include
|
include
|
||||||
}, 'filter:api.user.me.videos.list.params')
|
} satisfies Parameters<typeof VideoModel.listForApi>[0],
|
||||||
|
'filter:api.user.me.videos.list.params'
|
||||||
|
)
|
||||||
|
|
||||||
const resultList = await Hooks.wrapPromiseFun(
|
const resultList = await Hooks.wrapPromiseFun(
|
||||||
VideoModel.listForApi.bind(VideoModel),
|
VideoModel.listForApi.bind(VideoModel),
|
||||||
|
@ -170,7 +174,7 @@ async function listUserVideos (req: express.Request, res: express.Response) {
|
||||||
async function listCommentsOnUserVideos (req: express.Request, res: express.Response) {
|
async function listCommentsOnUserVideos (req: express.Request, res: express.Response) {
|
||||||
const userAccount = res.locals.oauth.token.User.Account
|
const userAccount = res.locals.oauth.token.User.Account
|
||||||
|
|
||||||
const options = {
|
const resultList = await VideoCommentModel.listCommentsForApi({
|
||||||
...pick(req.query, [
|
...pick(req.query, [
|
||||||
'start',
|
'start',
|
||||||
'count',
|
'count',
|
||||||
|
@ -182,14 +186,15 @@ async function listCommentsOnUserVideos (req: express.Request, res: express.Resp
|
||||||
]),
|
]),
|
||||||
|
|
||||||
autoTagOfAccountId: userAccount.id,
|
autoTagOfAccountId: userAccount.id,
|
||||||
|
|
||||||
videoAccountOwnerId: userAccount.id,
|
videoAccountOwnerId: userAccount.id,
|
||||||
|
videoAccountOwnerIncludeCollaborations: req.query.includeCollaborations || false,
|
||||||
|
|
||||||
heldForReview: req.query.isHeldForReview,
|
heldForReview: req.query.isHeldForReview,
|
||||||
|
|
||||||
videoChannelOwnerId: res.locals.videoChannel?.id,
|
videoChannelOwnerId: res.locals.videoChannel?.id,
|
||||||
videoId: res.locals.videoAll?.id
|
videoId: res.locals.videoAll?.id
|
||||||
}
|
})
|
||||||
|
|
||||||
const resultList = await VideoCommentModel.listCommentsForApi(options)
|
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
total: resultList.total,
|
total: resultList.total,
|
||||||
|
@ -197,7 +202,7 @@ async function listCommentsOnUserVideos (req: express.Request, res: express.Resp
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getUserVideoImports (req: express.Request, res: express.Response) {
|
async function listMyVideoImports (req: express.Request, res: express.Response) {
|
||||||
const user = res.locals.oauth.token.User
|
const user = res.locals.oauth.token.User
|
||||||
const resultList = await VideoImportModel.listUserVideoImportsForApi({
|
const resultList = await VideoImportModel.listUserVideoImportsForApi({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
@ -208,7 +213,7 @@ async function getUserVideoImports (req: express.Request, res: express.Response)
|
||||||
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getUserInformation (req: express.Request, res: express.Response) {
|
async function getMyInformation (req: express.Request, res: express.Response) {
|
||||||
// We did not load channels in res.locals.user
|
// We did not load channels in res.locals.user
|
||||||
const user = await UserModel.loadForMeAPI(res.locals.oauth.token.user.id)
|
const user = await UserModel.loadForMeAPI(res.locals.oauth.token.user.id)
|
||||||
|
|
||||||
|
@ -221,7 +226,7 @@ async function getUserInformation (req: express.Request, res: express.Response)
|
||||||
return res.json(result)
|
return res.json(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getUserVideoQuotaUsed (req: express.Request, res: express.Response) {
|
async function getMyVideoQuotaUsed (req: express.Request, res: express.Response) {
|
||||||
const user = res.locals.oauth.token.user
|
const user = res.locals.oauth.token.user
|
||||||
const videoQuotaUsed = await getOriginalVideoFileTotalFromUser(user)
|
const videoQuotaUsed = await getOriginalVideoFileTotalFromUser(user)
|
||||||
const videoQuotaUsedDaily = await getOriginalVideoFileTotalDailyFromUser(user)
|
const videoQuotaUsedDaily = await getOriginalVideoFileTotalDailyFromUser(user)
|
||||||
|
@ -233,7 +238,7 @@ async function getUserVideoQuotaUsed (req: express.Request, res: express.Respons
|
||||||
return res.json(data)
|
return res.json(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getUserVideoRating (req: express.Request, res: express.Response) {
|
async function getMyVideoRating (req: express.Request, res: express.Response) {
|
||||||
const videoId = res.locals.videoId.id
|
const videoId = res.locals.videoId.id
|
||||||
const accountId = +res.locals.oauth.token.User.Account.id
|
const accountId = +res.locals.oauth.token.User.Account.id
|
||||||
|
|
||||||
|
|
|
@ -14,17 +14,17 @@ import { ActorFollowModel } from '@server/models/actor/actor-follow.js'
|
||||||
import { getServerActor } from '@server/models/application/application.js'
|
import { getServerActor } from '@server/models/application/application.js'
|
||||||
import { MChannelBannerAccountDefault } from '@server/types/models/index.js'
|
import { MChannelBannerAccountDefault } from '@server/types/models/index.js'
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger.js'
|
import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../../helpers/audit-logger.js'
|
||||||
import { resetSequelizeInstance } from '../../helpers/database-utils.js'
|
import { resetSequelizeInstance } from '../../../helpers/database-utils.js'
|
||||||
import { buildNSFWFilters, createReqFiles, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils.js'
|
import { buildNSFWFilters, createReqFiles, getCountVideos, isUserAbleToSearchRemoteURI } from '../../../helpers/express-utils.js'
|
||||||
import { logger } from '../../helpers/logger.js'
|
import { logger } from '../../../helpers/logger.js'
|
||||||
import { getFormattedObjects } from '../../helpers/utils.js'
|
import { getFormattedObjects } from '../../../helpers/utils.js'
|
||||||
import { MIMETYPES } from '../../initializers/constants.js'
|
import { MIMETYPES } from '../../../initializers/constants.js'
|
||||||
import { sequelizeTypescript } from '../../initializers/database.js'
|
import { sequelizeTypescript } from '../../../initializers/database.js'
|
||||||
import { sendUpdateActor } from '../../lib/activitypub/send/index.js'
|
import { sendUpdateActor } from '../../../lib/activitypub/send/index.js'
|
||||||
import { JobQueue } from '../../lib/job-queue/index.js'
|
import { JobQueue } from '../../../lib/job-queue/index.js'
|
||||||
import { deleteLocalActorImageFile, updateLocalActorImageFiles } from '../../lib/local-actor.js'
|
import { deleteLocalActorImageFile, updateLocalActorImageFiles } from '../../../lib/local-actor.js'
|
||||||
import { createLocalVideoChannelWithoutKeys, federateAllVideosOfChannel } from '../../lib/video-channel.js'
|
import { createLocalVideoChannelWithoutKeys, federateAllVideosOfChannel } from '../../../lib/video-channel.js'
|
||||||
import {
|
import {
|
||||||
apiRateLimiter,
|
apiRateLimiter,
|
||||||
asyncMiddleware,
|
asyncMiddleware,
|
||||||
|
@ -41,8 +41,8 @@ import {
|
||||||
videoChannelsSortValidator,
|
videoChannelsSortValidator,
|
||||||
videoChannelsUpdateValidator,
|
videoChannelsUpdateValidator,
|
||||||
videoPlaylistsSortValidator
|
videoPlaylistsSortValidator
|
||||||
} from '../../middlewares/index.js'
|
} from '../../../middlewares/index.js'
|
||||||
import { updateAvatarValidator, updateBannerValidator } from '../../middlewares/validators/actor-image.js'
|
import { updateAvatarValidator, updateBannerValidator } from '../../../middlewares/validators/actor-image.js'
|
||||||
import {
|
import {
|
||||||
ensureChannelOwnerCanUpload,
|
ensureChannelOwnerCanUpload,
|
||||||
videoChannelImportVideosValidator,
|
videoChannelImportVideosValidator,
|
||||||
|
@ -50,16 +50,17 @@ import {
|
||||||
videoChannelsHandleValidatorFactory,
|
videoChannelsHandleValidatorFactory,
|
||||||
videoChannelsListValidator,
|
videoChannelsListValidator,
|
||||||
videosSortValidator
|
videosSortValidator
|
||||||
} from '../../middlewares/validators/index.js'
|
} from '../../../middlewares/validators/index.js'
|
||||||
import {
|
import {
|
||||||
commonVideoPlaylistFiltersValidator,
|
commonVideoPlaylistFiltersValidator,
|
||||||
videoPlaylistsReorderInChannelValidator
|
videoPlaylistsReorderInChannelValidator
|
||||||
} from '../../middlewares/validators/videos/video-playlists.js'
|
} from '../../../middlewares/validators/videos/video-playlists.js'
|
||||||
import { AccountModel } from '../../models/account/account.js'
|
import { AccountModel } from '../../../models/account/account.js'
|
||||||
import { guessAdditionalAttributesFromQuery } from '../../models/video/formatter/index.js'
|
import { guessAdditionalAttributesFromQuery } from '../../../models/video/formatter/index.js'
|
||||||
import { VideoChannelModel } from '../../models/video/video-channel.js'
|
import { VideoChannelModel } from '../../../models/video/video-channel.js'
|
||||||
import { VideoPlaylistModel } from '../../models/video/video-playlist.js'
|
import { VideoPlaylistModel } from '../../../models/video/video-playlist.js'
|
||||||
import { VideoModel } from '../../models/video/video.js'
|
import { VideoModel } from '../../../models/video/video.js'
|
||||||
|
import { channelCollaborators } from './video-channel-collaborators.js'
|
||||||
|
|
||||||
const auditLogger = auditLoggerFactory('channels')
|
const auditLogger = auditLoggerFactory('channels')
|
||||||
const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT)
|
const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT)
|
||||||
|
@ -68,6 +69,7 @@ const reqBannerFile = createReqFiles([ 'bannerfile' ], MIMETYPES.IMAGE.MIMETYPE_
|
||||||
const videoChannelRouter = express.Router()
|
const videoChannelRouter = express.Router()
|
||||||
|
|
||||||
videoChannelRouter.use(apiRateLimiter)
|
videoChannelRouter.use(apiRateLimiter)
|
||||||
|
videoChannelRouter.use(channelCollaborators)
|
||||||
|
|
||||||
videoChannelRouter.get(
|
videoChannelRouter.get(
|
||||||
'/',
|
'/',
|
||||||
|
@ -85,7 +87,7 @@ videoChannelRouter.post(
|
||||||
'/:handle/avatar/pick',
|
'/:handle/avatar/pick',
|
||||||
authenticate,
|
authenticate,
|
||||||
reqAvatarFile,
|
reqAvatarFile,
|
||||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: true })),
|
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: true, checkIsOwner: false })),
|
||||||
updateAvatarValidator,
|
updateAvatarValidator,
|
||||||
asyncMiddleware(updateVideoChannelAvatar)
|
asyncMiddleware(updateVideoChannelAvatar)
|
||||||
)
|
)
|
||||||
|
@ -94,7 +96,7 @@ videoChannelRouter.post(
|
||||||
'/:handle/banner/pick',
|
'/:handle/banner/pick',
|
||||||
authenticate,
|
authenticate,
|
||||||
reqBannerFile,
|
reqBannerFile,
|
||||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: true })),
|
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: true, checkIsOwner: false })),
|
||||||
updateBannerValidator,
|
updateBannerValidator,
|
||||||
asyncMiddleware(updateVideoChannelBanner)
|
asyncMiddleware(updateVideoChannelBanner)
|
||||||
)
|
)
|
||||||
|
@ -102,21 +104,21 @@ videoChannelRouter.post(
|
||||||
videoChannelRouter.delete(
|
videoChannelRouter.delete(
|
||||||
'/:handle/avatar',
|
'/:handle/avatar',
|
||||||
authenticate,
|
authenticate,
|
||||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: true })),
|
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: true, checkIsOwner: false })),
|
||||||
asyncMiddleware(deleteVideoChannelAvatar)
|
asyncMiddleware(deleteVideoChannelAvatar)
|
||||||
)
|
)
|
||||||
|
|
||||||
videoChannelRouter.delete(
|
videoChannelRouter.delete(
|
||||||
'/:handle/banner',
|
'/:handle/banner',
|
||||||
authenticate,
|
authenticate,
|
||||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: true })),
|
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: true, checkIsOwner: false })),
|
||||||
asyncMiddleware(deleteVideoChannelBanner)
|
asyncMiddleware(deleteVideoChannelBanner)
|
||||||
)
|
)
|
||||||
|
|
||||||
videoChannelRouter.put(
|
videoChannelRouter.put(
|
||||||
'/:handle',
|
'/:handle',
|
||||||
authenticate,
|
authenticate,
|
||||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: true })),
|
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: true, checkIsOwner: false })),
|
||||||
videoChannelsUpdateValidator,
|
videoChannelsUpdateValidator,
|
||||||
asyncRetryTransactionMiddleware(updateVideoChannel)
|
asyncRetryTransactionMiddleware(updateVideoChannel)
|
||||||
)
|
)
|
||||||
|
@ -124,14 +126,14 @@ videoChannelRouter.put(
|
||||||
videoChannelRouter.delete(
|
videoChannelRouter.delete(
|
||||||
'/:handle',
|
'/:handle',
|
||||||
authenticate,
|
authenticate,
|
||||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: true })),
|
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: true, checkIsOwner: true })),
|
||||||
asyncMiddleware(videoChannelsRemoveValidator),
|
asyncMiddleware(videoChannelsRemoveValidator),
|
||||||
asyncRetryTransactionMiddleware(removeVideoChannel)
|
asyncRetryTransactionMiddleware(removeVideoChannel)
|
||||||
)
|
)
|
||||||
|
|
||||||
videoChannelRouter.get(
|
videoChannelRouter.get(
|
||||||
'/:handle',
|
'/:handle',
|
||||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: false, checkManage: false })),
|
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: false, checkCanManage: false, checkIsOwner: false })),
|
||||||
asyncMiddleware(getVideoChannel)
|
asyncMiddleware(getVideoChannel)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -140,7 +142,7 @@ videoChannelRouter.get(
|
||||||
videoChannelRouter.get(
|
videoChannelRouter.get(
|
||||||
'/:handle/video-playlists',
|
'/:handle/video-playlists',
|
||||||
optionalAuthenticate,
|
optionalAuthenticate,
|
||||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: false, checkManage: false })),
|
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: false, checkCanManage: false, checkIsOwner: false })),
|
||||||
paginationValidator,
|
paginationValidator,
|
||||||
videoPlaylistsSortValidator,
|
videoPlaylistsSortValidator,
|
||||||
setDefaultSort,
|
setDefaultSort,
|
||||||
|
@ -152,7 +154,7 @@ videoChannelRouter.get(
|
||||||
videoChannelRouter.post(
|
videoChannelRouter.post(
|
||||||
'/:handle/video-playlists/reorder',
|
'/:handle/video-playlists/reorder',
|
||||||
authenticate,
|
authenticate,
|
||||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: true })),
|
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: true, checkIsOwner: false })),
|
||||||
asyncMiddleware(videoPlaylistsReorderInChannelValidator),
|
asyncMiddleware(videoPlaylistsReorderInChannelValidator),
|
||||||
asyncRetryTransactionMiddleware(reorderPlaylistsInChannel)
|
asyncRetryTransactionMiddleware(reorderPlaylistsInChannel)
|
||||||
)
|
)
|
||||||
|
@ -161,7 +163,7 @@ videoChannelRouter.post(
|
||||||
|
|
||||||
videoChannelRouter.get(
|
videoChannelRouter.get(
|
||||||
'/:handle/videos',
|
'/:handle/videos',
|
||||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: false, checkManage: false })),
|
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: false, checkCanManage: false, checkIsOwner: false })),
|
||||||
paginationValidator,
|
paginationValidator,
|
||||||
videosSortValidator,
|
videosSortValidator,
|
||||||
setDefaultVideosSort,
|
setDefaultVideosSort,
|
||||||
|
@ -174,7 +176,7 @@ videoChannelRouter.get(
|
||||||
videoChannelRouter.get(
|
videoChannelRouter.get(
|
||||||
'/:handle/followers',
|
'/:handle/followers',
|
||||||
authenticate,
|
authenticate,
|
||||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: false, checkManage: true })),
|
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: false, checkCanManage: true, checkIsOwner: false })),
|
||||||
paginationValidator,
|
paginationValidator,
|
||||||
videoChannelsFollowersSortValidator,
|
videoChannelsFollowersSortValidator,
|
||||||
setDefaultSort,
|
setDefaultSort,
|
||||||
|
@ -185,7 +187,7 @@ videoChannelRouter.get(
|
||||||
videoChannelRouter.post(
|
videoChannelRouter.post(
|
||||||
'/:handle/import-videos',
|
'/:handle/import-videos',
|
||||||
authenticate,
|
authenticate,
|
||||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: true })),
|
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: true, checkIsOwner: false })),
|
||||||
asyncMiddleware(videoChannelImportVideosValidator),
|
asyncMiddleware(videoChannelImportVideosValidator),
|
||||||
asyncMiddleware(ensureChannelOwnerCanUpload),
|
asyncMiddleware(ensureChannelOwnerCanUpload),
|
||||||
asyncMiddleware(importVideosInChannel)
|
asyncMiddleware(importVideosInChannel)
|
|
@ -0,0 +1,113 @@
|
||||||
|
import { HttpStatusCode, VideoChannelCollaboratorState } from '@peertube/peertube-models'
|
||||||
|
import { deleteInTransactionWithRetries, saveInTransactionWithRetries } from '@server/helpers/database-utils.js'
|
||||||
|
import { getFormattedObjects } from '@server/helpers/utils.js'
|
||||||
|
import { Notifier } from '@server/lib/notifier/notifier.js'
|
||||||
|
import {
|
||||||
|
channelAcceptOrRejectInviteCollaboratorsValidator,
|
||||||
|
channelDeleteCollaboratorsValidator,
|
||||||
|
channelInviteCollaboratorsValidator,
|
||||||
|
channelListCollaboratorsValidator
|
||||||
|
} from '@server/middlewares/validators/videos/video-channel-collaborators.js'
|
||||||
|
import { VideoChannelCollaboratorModel } from '@server/models/video/video-channel-collaborator.js'
|
||||||
|
import { MChannelCollaboratorAccount } from '@server/types/models/index.js'
|
||||||
|
import express from 'express'
|
||||||
|
import { asyncMiddleware, authenticate } from '../../../middlewares/index.js'
|
||||||
|
|
||||||
|
const channelCollaborators = express.Router()
|
||||||
|
|
||||||
|
channelCollaborators.get(
|
||||||
|
'/:handle/collaborators',
|
||||||
|
authenticate,
|
||||||
|
asyncMiddleware(channelListCollaboratorsValidator),
|
||||||
|
asyncMiddleware(listCollaborators)
|
||||||
|
)
|
||||||
|
|
||||||
|
channelCollaborators.post(
|
||||||
|
'/:handle/collaborators/invite',
|
||||||
|
authenticate,
|
||||||
|
asyncMiddleware(channelInviteCollaboratorsValidator),
|
||||||
|
asyncMiddleware(inviteCollaborator)
|
||||||
|
)
|
||||||
|
|
||||||
|
channelCollaborators.post(
|
||||||
|
'/:handle/collaborators/:collaboratorId/accept',
|
||||||
|
authenticate,
|
||||||
|
asyncMiddleware(channelAcceptOrRejectInviteCollaboratorsValidator),
|
||||||
|
asyncMiddleware(acceptCollaboratorInvite)
|
||||||
|
)
|
||||||
|
|
||||||
|
channelCollaborators.post(
|
||||||
|
'/:handle/collaborators/:collaboratorId/reject',
|
||||||
|
authenticate,
|
||||||
|
asyncMiddleware(channelAcceptOrRejectInviteCollaboratorsValidator),
|
||||||
|
asyncMiddleware(rejectCollaboratorInvite)
|
||||||
|
)
|
||||||
|
|
||||||
|
channelCollaborators.delete(
|
||||||
|
'/:handle/collaborators/:collaboratorId',
|
||||||
|
authenticate,
|
||||||
|
asyncMiddleware(channelDeleteCollaboratorsValidator),
|
||||||
|
asyncMiddleware(removeCollaborator)
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export {
|
||||||
|
channelCollaborators
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async function listCollaborators (req: express.Request, res: express.Response) {
|
||||||
|
const resultList = await VideoChannelCollaboratorModel.listForApi({
|
||||||
|
channelId: res.locals.videoChannel.id,
|
||||||
|
start: 0,
|
||||||
|
count: 100,
|
||||||
|
sort: '-createdAt'
|
||||||
|
})
|
||||||
|
|
||||||
|
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function inviteCollaborator (req: express.Request, res: express.Response) {
|
||||||
|
const collaborator = new VideoChannelCollaboratorModel({
|
||||||
|
state: VideoChannelCollaboratorState.PENDING,
|
||||||
|
accountId: res.locals.account.id,
|
||||||
|
channelId: res.locals.videoChannel.id
|
||||||
|
}) as MChannelCollaboratorAccount
|
||||||
|
|
||||||
|
await saveInTransactionWithRetries(collaborator)
|
||||||
|
collaborator.Account = res.locals.account
|
||||||
|
|
||||||
|
Notifier.Instance.notifyOfChannelCollaboratorInvitation(collaborator, res.locals.videoChannel)
|
||||||
|
|
||||||
|
return res.json({ collaborator: collaborator.toFormattedJSON() })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function acceptCollaboratorInvite (req: express.Request, res: express.Response) {
|
||||||
|
const collaborator = res.locals.channelCollaborator
|
||||||
|
collaborator.state = VideoChannelCollaboratorState.ACCEPTED
|
||||||
|
|
||||||
|
await saveInTransactionWithRetries(collaborator)
|
||||||
|
|
||||||
|
Notifier.Instance.notifyOfAcceptedChannelCollaborator(collaborator, res.locals.videoChannel)
|
||||||
|
|
||||||
|
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rejectCollaboratorInvite (req: express.Request, res: express.Response) {
|
||||||
|
const collaborator = res.locals.channelCollaborator
|
||||||
|
await deleteInTransactionWithRetries(collaborator)
|
||||||
|
|
||||||
|
Notifier.Instance.notifyOfRefusedChannelCollaborator(collaborator, res.locals.videoChannel)
|
||||||
|
|
||||||
|
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeCollaborator (req: express.Request, res: express.Response) {
|
||||||
|
const collaborator = res.locals.channelCollaborator
|
||||||
|
|
||||||
|
await deleteInTransactionWithRetries(collaborator)
|
||||||
|
|
||||||
|
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||||
|
}
|
|
@ -109,7 +109,7 @@ videoPlaylistRouter.get(
|
||||||
paginationValidator,
|
paginationValidator,
|
||||||
setDefaultPagination,
|
setDefaultPagination,
|
||||||
optionalAuthenticate,
|
optionalAuthenticate,
|
||||||
asyncMiddleware(getVideoPlaylistVideos)
|
asyncMiddleware(listVideosOfPlaylist)
|
||||||
)
|
)
|
||||||
|
|
||||||
videoPlaylistRouter.post(
|
videoPlaylistRouter.post(
|
||||||
|
@ -517,7 +517,7 @@ async function reorderVideosOfPlaylist (req: express.Request, res: express.Respo
|
||||||
return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end()
|
return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getVideoPlaylistVideos (req: express.Request, res: express.Response) {
|
async function listVideosOfPlaylist (req: express.Request, res: express.Response) {
|
||||||
const videoPlaylistInstance = res.locals.videoPlaylistSummary
|
const videoPlaylistInstance = res.locals.videoPlaylistSummary
|
||||||
const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
|
const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
|
||||||
const server = await getServerActor()
|
const server = await getServerActor()
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { sequelizeTypescript } from '@server/initializers/database.js'
|
||||||
import { federateVideoIfNeeded } from '@server/lib/activitypub/videos/index.js'
|
import { federateVideoIfNeeded } from '@server/lib/activitypub/videos/index.js'
|
||||||
import { LocalVideoCreator } from '@server/lib/local-video-creator.js'
|
import { LocalVideoCreator } from '@server/lib/local-video-creator.js'
|
||||||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||||
|
import { checkCanManageVideo } from '@server/middlewares/validators/shared/videos.js'
|
||||||
import {
|
import {
|
||||||
videoLiveAddValidator,
|
videoLiveAddValidator,
|
||||||
videoLiveFindReplaySessionValidator,
|
videoLiveFindReplaySessionValidator,
|
||||||
|
@ -44,21 +45,30 @@ liveRouter.get(
|
||||||
'/live/:videoId/sessions',
|
'/live/:videoId/sessions',
|
||||||
authenticate,
|
authenticate,
|
||||||
asyncMiddleware(videoLiveGetValidator),
|
asyncMiddleware(videoLiveGetValidator),
|
||||||
videoLiveListSessionsValidator,
|
asyncMiddleware(videoLiveListSessionsValidator),
|
||||||
asyncMiddleware(getLiveVideoSessions)
|
asyncMiddleware(getLiveVideoSessions)
|
||||||
)
|
)
|
||||||
|
|
||||||
liveRouter.get('/live/:videoId', optionalAuthenticate, asyncMiddleware(videoLiveGetValidator), getLiveVideo)
|
liveRouter.get(
|
||||||
|
'/live/:videoId',
|
||||||
|
optionalAuthenticate,
|
||||||
|
asyncMiddleware(videoLiveGetValidator),
|
||||||
|
asyncMiddleware(getLiveVideo)
|
||||||
|
)
|
||||||
|
|
||||||
liveRouter.put(
|
liveRouter.put(
|
||||||
'/live/:videoId',
|
'/live/:videoId',
|
||||||
authenticate,
|
authenticate,
|
||||||
asyncMiddleware(videoLiveGetValidator),
|
asyncMiddleware(videoLiveGetValidator),
|
||||||
videoLiveUpdateValidator,
|
asyncMiddleware(videoLiveUpdateValidator),
|
||||||
asyncRetryTransactionMiddleware(updateLiveVideo)
|
asyncRetryTransactionMiddleware(updateLiveVideo)
|
||||||
)
|
)
|
||||||
|
|
||||||
liveRouter.get('/:videoId/live-session', asyncMiddleware(videoLiveFindReplaySessionValidator), getLiveReplaySession)
|
liveRouter.get(
|
||||||
|
'/:videoId/live-session',
|
||||||
|
asyncMiddleware(videoLiveFindReplaySessionValidator),
|
||||||
|
getLiveReplaySession
|
||||||
|
)
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@ -68,10 +78,10 @@ export {
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function getLiveVideo (req: express.Request, res: express.Response) {
|
async function getLiveVideo (req: express.Request, res: express.Response) {
|
||||||
const videoLive = res.locals.videoLive
|
const videoLive = res.locals.videoLive
|
||||||
|
|
||||||
return res.json(videoLive.toFormattedJSON(canSeePrivateLiveInformation(res)))
|
return res.json(videoLive.toFormattedJSON(await canSeePrivateLiveInformation(req, res)))
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLiveReplaySession (req: express.Request, res: express.Response) {
|
function getLiveReplaySession (req: express.Request, res: express.Response) {
|
||||||
|
@ -88,14 +98,16 @@ async function getLiveVideoSessions (req: express.Request, res: express.Response
|
||||||
return res.json(getFormattedObjects(data, data.length))
|
return res.json(getFormattedObjects(data, data.length))
|
||||||
}
|
}
|
||||||
|
|
||||||
function canSeePrivateLiveInformation (res: express.Response) {
|
function canSeePrivateLiveInformation (req: express.Request, res: express.Response) {
|
||||||
const user = res.locals.oauth?.token.User
|
return checkCanManageVideo({
|
||||||
if (!user) return false
|
user: res.locals.oauth?.token.User,
|
||||||
|
video: res.locals.videoAll,
|
||||||
if (user.hasRight(UserRight.GET_ANY_LIVE)) return true
|
right: UserRight.GET_ANY_LIVE,
|
||||||
|
req,
|
||||||
const video = res.locals.videoAll
|
res: null,
|
||||||
return video.VideoChannel.Account.userId === user.id
|
checkIsLocal: true,
|
||||||
|
checkIsOwner: false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateLiveVideo (req: express.Request, res: express.Response) {
|
async function updateLiveVideo (req: express.Request, res: express.Response) {
|
||||||
|
|
|
@ -12,7 +12,7 @@ servicesRouter.use('/oembed', cors(), apiRateLimiter, asyncMiddleware(oembedVali
|
||||||
servicesRouter.use(
|
servicesRouter.use(
|
||||||
'/redirect/accounts/:handle',
|
'/redirect/accounts/:handle',
|
||||||
apiRateLimiter,
|
apiRateLimiter,
|
||||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: false, checkManage: false })),
|
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: false, checkCanManage: false })),
|
||||||
redirectToAccountUrl
|
redirectToAccountUrl
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,25 @@
|
||||||
import { Response } from 'express'
|
import { HttpStatusCode, UserRight } from '@peertube/peertube-models'
|
||||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
import { checkCanManageAccount } from '@server/middlewares/validators/shared/users.js'
|
||||||
import { MUserId } from '@server/types/models/index.js'
|
import { MUserAccountId } from '@server/types/models/index.js'
|
||||||
import { MVideoChangeOwnershipFull } from '@server/types/models/video/video-change-ownership.js'
|
import { MVideoChangeOwnershipFull } from '@server/types/models/video/video-change-ownership.js'
|
||||||
|
import { Request, Response } from 'express'
|
||||||
|
|
||||||
function checkUserCanTerminateOwnershipChange (user: MUserId, videoChangeOwnership: MVideoChangeOwnershipFull, res: Response) {
|
export function checkCanTerminateOwnershipChange (options: {
|
||||||
if (videoChangeOwnership.NextOwner.userId === user.id) {
|
user: MUserAccountId
|
||||||
return true
|
videoChangeOwnership: MVideoChangeOwnershipFull
|
||||||
}
|
req: Request
|
||||||
|
res: Response
|
||||||
|
}) {
|
||||||
|
const { user, videoChangeOwnership, req, res } = options
|
||||||
|
|
||||||
|
if (!checkCanManageAccount({ user, account: videoChangeOwnership.NextOwner, req, res: null, specialRight: UserRight.MANAGE_USERS })) {
|
||||||
res.fail({
|
res.fail({
|
||||||
status: HttpStatusCode.FORBIDDEN_403,
|
status: HttpStatusCode.FORBIDDEN_403,
|
||||||
message: 'Cannot terminate an ownership change of another user'
|
message: req.t('Cannot terminate an ownership change of another user')
|
||||||
})
|
})
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
return true
|
||||||
checkUserCanTerminateOwnershipChange
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { REMOTE_SCHEME, WEBSERVER } from '../../initializers/constants.js'
|
||||||
import { sanitizeHost } from '../core-utils.js'
|
import { sanitizeHost } from '../core-utils.js'
|
||||||
import { exists } from './misc.js'
|
import { exists } from './misc.js'
|
||||||
|
|
||||||
function isWebfingerLocalResourceValid (value: string) {
|
export function isWebfingerLocalResourceValid (value: string) {
|
||||||
if (!exists(value)) return false
|
if (!exists(value)) return false
|
||||||
if (value.startsWith('acct:') === false) return false
|
if (value.startsWith('acct:') === false) return false
|
||||||
|
|
||||||
|
@ -13,9 +13,3 @@ function isWebfingerLocalResourceValid (value: string) {
|
||||||
const host = actorParts[1]
|
const host = actorParts[1]
|
||||||
return sanitizeHost(host, REMOTE_SCHEME.HTTP) === WEBSERVER.HOST
|
return sanitizeHost(host, REMOTE_SCHEME.HTTP) === WEBSERVER.HOST
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
export {
|
|
||||||
isWebfingerLocalResourceValid
|
|
||||||
}
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { Model } from 'sequelize-typescript'
|
||||||
import { sequelizeTypescript } from '@server/initializers/database.js'
|
import { sequelizeTypescript } from '@server/initializers/database.js'
|
||||||
import { logger } from './logger.js'
|
import { logger } from './logger.js'
|
||||||
|
|
||||||
function retryTransactionWrapper<T, A, B, C, D> (
|
export function retryTransactionWrapper<T, A, B, C, D> (
|
||||||
functionToRetry: (arg1: A, arg2: B, arg3: C, arg4: D) => Promise<T>,
|
functionToRetry: (arg1: A, arg2: B, arg3: C, arg4: D) => Promise<T>,
|
||||||
arg1: A,
|
arg1: A,
|
||||||
arg2: B,
|
arg2: B,
|
||||||
|
@ -13,29 +13,29 @@ function retryTransactionWrapper<T, A, B, C, D> (
|
||||||
arg4: D
|
arg4: D
|
||||||
): Promise<T>
|
): Promise<T>
|
||||||
|
|
||||||
function retryTransactionWrapper<T, A, B, C> (
|
export function retryTransactionWrapper<T, A, B, C> (
|
||||||
functionToRetry: (arg1: A, arg2: B, arg3: C) => Promise<T>,
|
functionToRetry: (arg1: A, arg2: B, arg3: C) => Promise<T>,
|
||||||
arg1: A,
|
arg1: A,
|
||||||
arg2: B,
|
arg2: B,
|
||||||
arg3: C
|
arg3: C
|
||||||
): Promise<T>
|
): Promise<T>
|
||||||
|
|
||||||
function retryTransactionWrapper<T, A, B> (
|
export function retryTransactionWrapper<T, A, B> (
|
||||||
functionToRetry: (arg1: A, arg2: B) => Promise<T>,
|
functionToRetry: (arg1: A, arg2: B) => Promise<T>,
|
||||||
arg1: A,
|
arg1: A,
|
||||||
arg2: B
|
arg2: B
|
||||||
): Promise<T>
|
): Promise<T>
|
||||||
|
|
||||||
function retryTransactionWrapper<T, A> (
|
export function retryTransactionWrapper<T, A> (
|
||||||
functionToRetry: (arg1: A) => Promise<T>,
|
functionToRetry: (arg1: A) => Promise<T>,
|
||||||
arg1: A
|
arg1: A
|
||||||
): Promise<T>
|
): Promise<T>
|
||||||
|
|
||||||
function retryTransactionWrapper<T> (
|
export function retryTransactionWrapper<T> (
|
||||||
functionToRetry: () => Promise<T> | Bluebird<T>
|
functionToRetry: () => Promise<T> | Bluebird<T>
|
||||||
): Promise<T>
|
): Promise<T>
|
||||||
|
|
||||||
function retryTransactionWrapper<T> (
|
export function retryTransactionWrapper<T> (
|
||||||
functionToRetry: (...args: any[]) => Promise<T>,
|
functionToRetry: (...args: any[]) => Promise<T>,
|
||||||
...args: any[]
|
...args: any[]
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
|
@ -50,7 +50,7 @@ function retryTransactionWrapper<T> (
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function transactionRetryer<T> (func: (err: any, data: T) => any) {
|
export function transactionRetryer<T> (func: (err: any, data: T) => any) {
|
||||||
return new Promise<T>((res, rej) => {
|
return new Promise<T>((res, rej) => {
|
||||||
retry(
|
retry(
|
||||||
{
|
{
|
||||||
|
@ -68,7 +68,7 @@ function transactionRetryer<T> (func: (err: any, data: T) => any) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveInTransactionWithRetries<T extends Pick<Model, 'save' | 'changed'>> (model: T) {
|
export function saveInTransactionWithRetries<T extends Pick<Model, 'save' | 'changed'>> (model: T) {
|
||||||
const changedKeys = model.changed() || []
|
const changedKeys = model.changed() || []
|
||||||
|
|
||||||
return retryTransactionWrapper(() => {
|
return retryTransactionWrapper(() => {
|
||||||
|
@ -87,46 +87,41 @@ function saveInTransactionWithRetries<T extends Pick<Model, 'save' | 'changed'>>
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function deleteInTransactionWithRetries<T extends Pick<Model, 'destroy'>> (model: T) {
|
||||||
|
return retryTransactionWrapper(() => {
|
||||||
|
return sequelizeTypescript.transaction(async transaction => {
|
||||||
|
await model.destroy({ transaction })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function resetSequelizeInstance<T> (instance: Model<T>) {
|
export function resetSequelizeInstance<T> (instance: Model<T>) {
|
||||||
return instance.reload()
|
return instance.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterNonExistingModels<T extends { hasSameUniqueKeysThan(other: T): boolean }> (
|
export function filterNonExistingModels<T extends { hasSameUniqueKeysThan(other: T): boolean }> (
|
||||||
fromDatabase: T[],
|
fromDatabase: T[],
|
||||||
newModels: T[]
|
newModels: T[]
|
||||||
) {
|
) {
|
||||||
return fromDatabase.filter(f => !newModels.find(newModel => newModel.hasSameUniqueKeysThan(f)))
|
return fromDatabase.filter(f => !newModels.find(newModel => newModel.hasSameUniqueKeysThan(f)))
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteAllModels<T extends Pick<Model, 'destroy'>> (models: T[], transaction: Transaction) {
|
export function deleteAllModels<T extends Pick<Model, 'destroy'>> (models: T[], transaction: Transaction) {
|
||||||
return Promise.all(models.map(f => f.destroy({ transaction })))
|
return Promise.all(models.map(f => f.destroy({ transaction })))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function runInReadCommittedTransaction<T> (fn: (t: Transaction) => Promise<T>) {
|
export function runInReadCommittedTransaction<T> (fn: (t: Transaction) => Promise<T>) {
|
||||||
const options = { isolationLevel: Transaction.ISOLATION_LEVELS.READ_COMMITTED }
|
const options = { isolationLevel: Transaction.ISOLATION_LEVELS.READ_COMMITTED }
|
||||||
|
|
||||||
return sequelizeTypescript.transaction(options, t => fn(t))
|
return sequelizeTypescript.transaction(options, t => fn(t))
|
||||||
}
|
}
|
||||||
|
|
||||||
function afterCommitIfTransaction (t: Transaction, fn: Function) {
|
export function afterCommitIfTransaction (t: Transaction, fn: Function) {
|
||||||
if (t) return t.afterCommit(() => fn())
|
if (t) return t.afterCommit(() => fn())
|
||||||
|
|
||||||
return fn()
|
return fn()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
export {
|
|
||||||
resetSequelizeInstance,
|
|
||||||
retryTransactionWrapper,
|
|
||||||
transactionRetryer,
|
|
||||||
saveInTransactionWithRetries,
|
|
||||||
afterCommitIfTransaction,
|
|
||||||
filterNonExistingModels,
|
|
||||||
deleteAllModels,
|
|
||||||
runInReadCommittedTransaction
|
|
||||||
}
|
|
||||||
|
|
|
@ -30,7 +30,10 @@ export class GeoIP {
|
||||||
if (CONFIG.GEO_IP.ENABLED === false) return emptyResult
|
if (CONFIG.GEO_IP.ENABLED === false) return emptyResult
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!this.initReadersPromise) this.initReadersPromise = this.initReadersIfNeeded()
|
if (this.initReadersPromise === undefined) {
|
||||||
|
this.initReadersPromise = this.initReadersIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
await this.initReadersPromise
|
await this.initReadersPromise
|
||||||
this.initReadersPromise = undefined
|
this.initReadersPromise = undefined
|
||||||
|
|
||||||
|
|
|
@ -3,13 +3,13 @@ import { WEBSERVER } from '@server/initializers/constants.js'
|
||||||
import { actorNameAlphabet } from './custom-validators/activitypub/actor.js'
|
import { actorNameAlphabet } from './custom-validators/activitypub/actor.js'
|
||||||
import { regexpCapture } from './regexp.js'
|
import { regexpCapture } from './regexp.js'
|
||||||
|
|
||||||
export function extractMentions (text: string, isOwned: boolean) {
|
export function extractMentions (text: string, isLocal: boolean) {
|
||||||
let result: string[] = []
|
let result: string[] = []
|
||||||
|
|
||||||
const localMention = `@(${actorNameAlphabet}+)`
|
const localMention = `@(${actorNameAlphabet}+)`
|
||||||
const remoteMention = `${localMention}@${WEBSERVER.HOST}`
|
const remoteMention = `${localMention}@${WEBSERVER.HOST}`
|
||||||
|
|
||||||
const mentionRegex = isOwned
|
const mentionRegex = isLocal
|
||||||
? '(?:(?:' + remoteMention + ')|(?:' + localMention + '))' // Include local mentions?
|
? '(?:(?:' + remoteMention + ')|(?:' + localMention + '))' // Include local mentions?
|
||||||
: '(?:' + remoteMention + ')'
|
: '(?:' + remoteMention + ')'
|
||||||
|
|
||||||
|
@ -20,16 +20,14 @@ export function extractMentions (text: string, isOwned: boolean) {
|
||||||
result = result.concat(
|
result = result.concat(
|
||||||
regexpCapture(text, firstMentionRegex)
|
regexpCapture(text, firstMentionRegex)
|
||||||
.map(([ , username1, username2 ]) => username1 || username2),
|
.map(([ , username1, username2 ]) => username1 || username2),
|
||||||
|
|
||||||
regexpCapture(text, endMentionRegex)
|
regexpCapture(text, endMentionRegex)
|
||||||
.map(([ , username1, username2 ]) => username1 || username2),
|
.map(([ , username1, username2 ]) => username1 || username2),
|
||||||
|
|
||||||
regexpCapture(text, remoteMentionsRegex)
|
regexpCapture(text, remoteMentionsRegex)
|
||||||
.map(([ , username ]) => username)
|
.map(([ , username ]) => username)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Include local mentions
|
// Include local mentions
|
||||||
if (isOwned) {
|
if (isLocal) {
|
||||||
const localMentionsRegex = new RegExp(' ' + localMention + ' ', 'g')
|
const localMentionsRegex = new RegExp(' ' + localMention + ' ', 'g')
|
||||||
|
|
||||||
result = result.concat(
|
result = result.concat(
|
||||||
|
|
|
@ -20,6 +20,8 @@ import {
|
||||||
UserImportStateType,
|
UserImportStateType,
|
||||||
UserRegistrationState,
|
UserRegistrationState,
|
||||||
UserRegistrationStateType,
|
UserRegistrationStateType,
|
||||||
|
VideoChannelCollaboratorState,
|
||||||
|
VideoChannelCollaboratorStateType,
|
||||||
VideoChannelSyncState,
|
VideoChannelSyncState,
|
||||||
VideoChannelSyncStateType,
|
VideoChannelSyncStateType,
|
||||||
VideoCommentPolicy,
|
VideoCommentPolicy,
|
||||||
|
@ -52,7 +54,7 @@ import { CONFIG, registerConfigChangedHandler } from './config.js'
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export const LAST_MIGRATION_VERSION = 930
|
export const LAST_MIGRATION_VERSION = 935
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@ -690,6 +692,11 @@ export const VIDEO_COMMENTS_POLICY: { [id in VideoCommentPolicyType]: string } =
|
||||||
[VideoCommentPolicy.REQUIRES_APPROVAL]: 'Requires approval'
|
[VideoCommentPolicy.REQUIRES_APPROVAL]: 'Requires approval'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const CHANNEL_COLLABORATOR_STATE: { [id in VideoChannelCollaboratorStateType]: string } = {
|
||||||
|
[VideoChannelCollaboratorState.ACCEPTED]: 'Accepted',
|
||||||
|
[VideoChannelCollaboratorState.PENDING]: 'Pending'
|
||||||
|
}
|
||||||
|
|
||||||
export const MIMETYPES = {
|
export const MIMETYPES = {
|
||||||
AUDIO: {
|
AUDIO: {
|
||||||
MIMETYPE_EXT: {
|
MIMETYPE_EXT: {
|
||||||
|
|
|
@ -16,7 +16,9 @@ import { UserNotificationModel } from '@server/models/user/user-notification.js'
|
||||||
import { UserRegistrationModel } from '@server/models/user/user-registration.js'
|
import { UserRegistrationModel } from '@server/models/user/user-registration.js'
|
||||||
import { UserVideoHistoryModel } from '@server/models/user/user-video-history.js'
|
import { UserVideoHistoryModel } from '@server/models/user/user-video-history.js'
|
||||||
import { UserModel } from '@server/models/user/user.js'
|
import { UserModel } from '@server/models/user/user.js'
|
||||||
|
import { PlayerSettingModel } from '@server/models/video/player-setting.js'
|
||||||
import { StoryboardModel } from '@server/models/video/storyboard.js'
|
import { StoryboardModel } from '@server/models/video/storyboard.js'
|
||||||
|
import { VideoChannelCollaboratorModel } from '@server/models/video/video-channel-collaborator.js'
|
||||||
import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync.js'
|
import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync.js'
|
||||||
import { VideoChapterModel } from '@server/models/video/video-chapter.js'
|
import { VideoChapterModel } from '@server/models/video/video-chapter.js'
|
||||||
import { VideoJobInfoModel } from '@server/models/video/video-job-info.js'
|
import { VideoJobInfoModel } from '@server/models/video/video-job-info.js'
|
||||||
|
@ -69,7 +71,6 @@ import { VideoTagModel } from '../models/video/video-tag.js'
|
||||||
import { VideoModel } from '../models/video/video.js'
|
import { VideoModel } from '../models/video/video.js'
|
||||||
import { VideoViewModel } from '../models/view/video-view.js'
|
import { VideoViewModel } from '../models/view/video-view.js'
|
||||||
import { CONFIG } from './config.js'
|
import { CONFIG } from './config.js'
|
||||||
import { PlayerSettingModel } from '@server/models/video/player-setting.js'
|
|
||||||
|
|
||||||
pg.defaults.parseInt8 = true // Avoid BIGINT to be converted to string
|
pg.defaults.parseInt8 = true // Avoid BIGINT to be converted to string
|
||||||
|
|
||||||
|
@ -191,7 +192,8 @@ export async function initDatabaseModels (silent: boolean) {
|
||||||
AccountAutomaticTagPolicyModel,
|
AccountAutomaticTagPolicyModel,
|
||||||
UploadImageModel,
|
UploadImageModel,
|
||||||
VideoLiveScheduleModel,
|
VideoLiveScheduleModel,
|
||||||
PlayerSettingModel
|
PlayerSettingModel,
|
||||||
|
VideoChannelCollaboratorModel
|
||||||
])
|
])
|
||||||
|
|
||||||
// Check extensions exist in the database
|
// Check extensions exist in the database
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
import * as Sequelize from 'sequelize'
|
||||||
|
|
||||||
|
async function up (utils: {
|
||||||
|
transaction: Sequelize.Transaction
|
||||||
|
queryInterface: Sequelize.QueryInterface
|
||||||
|
sequelize: Sequelize.Sequelize
|
||||||
|
}): Promise<void> {
|
||||||
|
{
|
||||||
|
const query = `CREATE TABLE IF NOT EXISTS "videoChannelCollaborator" (
|
||||||
|
"id" SERIAL,
|
||||||
|
"state" VARCHAR(255) NOT NULL,
|
||||||
|
"accountId" INTEGER REFERENCES "account" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
"channelId" INTEGER NOT NULL REFERENCES "videoChannel" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
)`
|
||||||
|
|
||||||
|
await utils.sequelize.query(query, { transaction: utils.transaction })
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const metadata = {
|
||||||
|
type: Sequelize.JSONB,
|
||||||
|
allowNull: true
|
||||||
|
}
|
||||||
|
await utils.queryInterface.addColumn('userNotification', 'data', metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
await utils.sequelize.query(`
|
||||||
|
ALTER TABLE "userNotification"
|
||||||
|
ADD COLUMN "channelCollaboratorId" INTEGER REFERENCES "videoChannelCollaborator" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function down (options) {
|
||||||
|
throw new Error('Not implemented.')
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
up,
|
||||||
|
down
|
||||||
|
}
|
|
@ -9,13 +9,7 @@ import { updateRemotePlaylistMiniatureFromUrl } from '@server/lib/thumbnail.js'
|
||||||
import { VideoPlaylistElementModel } from '@server/models/video/video-playlist-element.js'
|
import { VideoPlaylistElementModel } from '@server/models/video/video-playlist-element.js'
|
||||||
import { VideoPlaylistModel } from '@server/models/video/video-playlist.js'
|
import { VideoPlaylistModel } from '@server/models/video/video-playlist.js'
|
||||||
import { FilteredModelAttributes } from '@server/types/index.js'
|
import { FilteredModelAttributes } from '@server/types/index.js'
|
||||||
import {
|
import { MAccountHost, MThumbnail, MVideoPlaylist, MVideoPlaylistFull, MVideoPlaylistVideosLength } from '@server/types/models/index.js'
|
||||||
MAccountHost,
|
|
||||||
MThumbnail,
|
|
||||||
MVideoPlaylist,
|
|
||||||
MVideoPlaylistFull,
|
|
||||||
MVideoPlaylistVideosLength
|
|
||||||
} from '@server/types/models/index.js'
|
|
||||||
import Bluebird from 'bluebird'
|
import Bluebird from 'bluebird'
|
||||||
import { getAPId } from '../activity.js'
|
import { getAPId } from '../activity.js'
|
||||||
import { getOrCreateAPActor } from '../actors/index.js'
|
import { getOrCreateAPActor } from '../actors/index.js'
|
||||||
|
@ -33,6 +27,11 @@ import { isActivityPubUrlValid } from '@server/helpers/custom-validators/activit
|
||||||
const lTags = loggerTagsFactory('ap', 'video-playlist')
|
const lTags = loggerTagsFactory('ap', 'video-playlist')
|
||||||
|
|
||||||
export async function createAccountPlaylists (playlistUrls: string[], account: MAccountHost) {
|
export async function createAccountPlaylists (playlistUrls: string[], account: MAccountHost) {
|
||||||
|
logger.info(
|
||||||
|
`Creating or updating ${playlistUrls.length} playlists for account ${account.Actor.preferredUsername}`,
|
||||||
|
lTags()
|
||||||
|
)
|
||||||
|
|
||||||
await Bluebird.map(playlistUrls, async playlistUrl => {
|
await Bluebird.map(playlistUrls, async playlistUrl => {
|
||||||
if (!checkUrlsSameHost(playlistUrl, account.Actor.url)) {
|
if (!checkUrlsSameHost(playlistUrl, account.Actor.url)) {
|
||||||
logger.warn(`Playlist ${playlistUrl} is not on the same host as owner account ${account.Actor.url}`, lTags(playlistUrl))
|
logger.warn(`Playlist ${playlistUrl} is not on the same host as owner account ${account.Actor.url}`, lTags(playlistUrl))
|
||||||
|
@ -69,6 +68,8 @@ export async function createOrUpdateVideoPlaylist (options: {
|
||||||
throw new Error(`Playlist ${playlistObject.id} is not on the same host as context URL ${contextUrl}`)
|
throw new Error(`Playlist ${playlistObject.id} is not on the same host as context URL ${contextUrl}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.debug(`Creating or updating playlist ${playlistObject.id}`, lTags(playlistObject.id))
|
||||||
|
|
||||||
const playlistAttributes = playlistObjectToDBAttributes(playlistObject, to || playlistObject.to)
|
const playlistAttributes = playlistObjectToDBAttributes(playlistObject, to || playlistObject.to)
|
||||||
|
|
||||||
const channel = await getRemotePlaylistChannel(playlistObject)
|
const channel = await getRemotePlaylistChannel(playlistObject)
|
||||||
|
|
|
@ -51,7 +51,7 @@ async function processVideoShare (actorAnnouncer: MActorSignature, activity: Act
|
||||||
transaction: t
|
transaction: t
|
||||||
})
|
})
|
||||||
|
|
||||||
if (video.isOwned() && created === true) {
|
if (video.isLocal() && created === true) {
|
||||||
// Don't resend the activity to the sender
|
// Don't resend the activity to the sender
|
||||||
const exceptions = [ actorAnnouncer ]
|
const exceptions = [ actorAnnouncer ]
|
||||||
|
|
||||||
|
|
|
@ -91,7 +91,7 @@ async function processCreateCacheFile (
|
||||||
|
|
||||||
const { video } = await getOrCreateAPVideo({ videoObject: cacheFile.object })
|
const { video } = await getOrCreateAPVideo({ videoObject: cacheFile.object })
|
||||||
|
|
||||||
if (video.isOwned() && !canVideoBeFederated(video)) {
|
if (video.isLocal() && !canVideoBeFederated(video)) {
|
||||||
logger.warn(`Do not process create cache file ${cacheFile.object} on a video that cannot be federated`)
|
logger.warn(`Do not process create cache file ${cacheFile.object} on a video that cannot be federated`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -100,7 +100,7 @@ async function processCreateCacheFile (
|
||||||
return createOrUpdateCacheFile(cacheFile, video, byActor, t)
|
return createOrUpdateCacheFile(cacheFile, video, byActor, t)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (video.isOwned()) {
|
if (video.isLocal()) {
|
||||||
// Don't resend the activity to the sender
|
// Don't resend the activity to the sender
|
||||||
const exceptions = [ byActor ]
|
const exceptions = [ byActor ]
|
||||||
await forwardVideoRelatedActivity(activity, undefined, exceptions, video)
|
await forwardVideoRelatedActivity(activity, undefined, exceptions, video)
|
||||||
|
@ -152,7 +152,7 @@ async function processCreateVideoComment (
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to not forward unwanted comments on our videos
|
// Try to not forward unwanted comments on our videos
|
||||||
if (video.isOwned()) {
|
if (video.isLocal()) {
|
||||||
if (!canVideoBeFederated(video)) {
|
if (!canVideoBeFederated(video)) {
|
||||||
logger.info('Skip comment forward on non federated video' + video.url)
|
logger.info('Skip comment forward on non federated video' + video.url)
|
||||||
return
|
return
|
||||||
|
|
|
@ -55,7 +55,7 @@ async function processDeleteActivity (options: APProcessorOptions<ActivityDelete
|
||||||
{
|
{
|
||||||
const videoInstance = await VideoModel.loadByUrlAndPopulateAccountAndFiles(objectUrl)
|
const videoInstance = await VideoModel.loadByUrlAndPopulateAccountAndFiles(objectUrl)
|
||||||
if (videoInstance) {
|
if (videoInstance) {
|
||||||
if (videoInstance.isOwned()) throw new Error(`Remote instance cannot delete owned video ${videoInstance.url}.`)
|
if (videoInstance.isLocal()) throw new Error(`Remote instance cannot delete owned video ${videoInstance.url}.`)
|
||||||
|
|
||||||
return retryTransactionWrapper(processDeleteVideo, byActor, videoInstance)
|
return retryTransactionWrapper(processDeleteVideo, byActor, videoInstance)
|
||||||
}
|
}
|
||||||
|
@ -64,7 +64,7 @@ async function processDeleteActivity (options: APProcessorOptions<ActivityDelete
|
||||||
{
|
{
|
||||||
const videoPlaylist = await VideoPlaylistModel.loadByUrlAndPopulateAccount(objectUrl)
|
const videoPlaylist = await VideoPlaylistModel.loadByUrlAndPopulateAccount(objectUrl)
|
||||||
if (videoPlaylist) {
|
if (videoPlaylist) {
|
||||||
if (videoPlaylist.isOwned()) throw new Error(`Remote instance cannot delete owned playlist ${videoPlaylist.url}.`)
|
if (videoPlaylist.isLocal()) throw new Error(`Remote instance cannot delete owned playlist ${videoPlaylist.url}.`)
|
||||||
|
|
||||||
return retryTransactionWrapper(processDeleteVideoPlaylist, byActor, videoPlaylist)
|
return retryTransactionWrapper(processDeleteVideoPlaylist, byActor, videoPlaylist)
|
||||||
}
|
}
|
||||||
|
@ -144,7 +144,7 @@ function processDeleteVideoComment (byActor: MActorSignature, videoComment: MCom
|
||||||
|
|
||||||
await videoComment.save({ transaction: t })
|
await videoComment.save({ transaction: t })
|
||||||
|
|
||||||
if (videoComment.Video.isOwned()) {
|
if (videoComment.Video.isLocal()) {
|
||||||
// Don't resend the activity to the sender
|
// Don't resend the activity to the sender
|
||||||
const exceptions = [ byActor ]
|
const exceptions = [ byActor ]
|
||||||
await forwardVideoRelatedActivity(activity, t, exceptions, videoComment.Video)
|
await forwardVideoRelatedActivity(activity, t, exceptions, videoComment.Video)
|
||||||
|
|
|
@ -28,7 +28,7 @@ async function processDislike (activity: ActivityDislike, byActor: MActorSignatu
|
||||||
if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url)
|
if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url)
|
||||||
|
|
||||||
const { video: onlyVideo } = await maybeGetOrCreateAPVideo({ videoObject: videoUrl, fetchType: 'only-video-and-blacklist' })
|
const { video: onlyVideo } = await maybeGetOrCreateAPVideo({ videoObject: videoUrl, fetchType: 'only-video-and-blacklist' })
|
||||||
if (!onlyVideo?.isOwned()) return
|
if (!onlyVideo?.isLocal()) return
|
||||||
|
|
||||||
if (!canVideoBeFederated(onlyVideo)) {
|
if (!canVideoBeFederated(onlyVideo)) {
|
||||||
logger.warn(`Do not process dislike on video ${videoUrl} that cannot be federated`)
|
logger.warn(`Do not process dislike on video ${videoUrl} that cannot be federated`)
|
||||||
|
|
|
@ -38,7 +38,7 @@ async function processFollow (byActor: MActorSignature, activityId: string, targ
|
||||||
const targetActor = await ActorModel.loadByUrlAndPopulateAccountAndChannel(targetActorURL, t)
|
const targetActor = await ActorModel.loadByUrlAndPopulateAccountAndChannel(targetActorURL, t)
|
||||||
|
|
||||||
if (!targetActor) throw new Error('Unknown actor')
|
if (!targetActor) throw new Error('Unknown actor')
|
||||||
if (targetActor.isOwned() === false) throw new Error('This is not a local actor.')
|
if (targetActor.isLocal() === false) throw new Error('This is not a local actor.')
|
||||||
|
|
||||||
if (await rejectIfInstanceFollowDisabled(byActor, activityId, targetActor)) return { actorFollow: undefined }
|
if (await rejectIfInstanceFollowDisabled(byActor, activityId, targetActor)) return { actorFollow: undefined }
|
||||||
if (await rejectIfMuted(byActor, activityId, targetActor)) return { actorFollow: undefined }
|
if (await rejectIfMuted(byActor, activityId, targetActor)) return { actorFollow: undefined }
|
||||||
|
|
|
@ -30,7 +30,7 @@ async function processLikeVideo (byActor: MActorSignature, activity: ActivityLik
|
||||||
if (!byAccount) throw new Error('Cannot create like with the non account actor ' + byActor.url)
|
if (!byAccount) throw new Error('Cannot create like with the non account actor ' + byActor.url)
|
||||||
|
|
||||||
const { video: onlyVideo } = await maybeGetOrCreateAPVideo({ videoObject: videoUrl, fetchType: 'only-video-and-blacklist' })
|
const { video: onlyVideo } = await maybeGetOrCreateAPVideo({ videoObject: videoUrl, fetchType: 'only-video-and-blacklist' })
|
||||||
if (!onlyVideo?.isOwned()) return
|
if (!onlyVideo?.isLocal()) return
|
||||||
|
|
||||||
if (!canVideoBeFederated(onlyVideo)) {
|
if (!canVideoBeFederated(onlyVideo)) {
|
||||||
logger.warn(`Do not process like on video ${videoUrl} that cannot be federated`)
|
logger.warn(`Do not process like on video ${videoUrl} that cannot be federated`)
|
||||||
|
|
|
@ -16,7 +16,7 @@ export function processReplyApprovalFactory (type: Extract<ActivityType, 'Approv
|
||||||
throw new Error(`Cannot process reply approval on comment ${comment.url} that doesn't exist`)
|
throw new Error(`Cannot process reply approval on comment ${comment.url} that doesn't exist`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (comment.isOwned() !== true) {
|
if (comment.isLocal() !== true) {
|
||||||
throw new Error(`Cannot process reply approval on non-owned comment ${comment.url}`)
|
throw new Error(`Cannot process reply approval on non-owned comment ${comment.url}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -68,7 +68,7 @@ async function processUndoLike (byActor: MActorSignature, activity: ActivityUndo
|
||||||
const likeActivity = activity.object
|
const likeActivity = activity.object
|
||||||
|
|
||||||
const { video: onlyVideo } = await maybeGetOrCreateAPVideo({ videoObject: likeActivity.object })
|
const { video: onlyVideo } = await maybeGetOrCreateAPVideo({ videoObject: likeActivity.object })
|
||||||
if (!onlyVideo?.isOwned()) return
|
if (!onlyVideo?.isLocal()) return
|
||||||
|
|
||||||
return sequelizeTypescript.transaction(async t => {
|
return sequelizeTypescript.transaction(async t => {
|
||||||
if (!byActor.Account) throw new Error('Unknown account ' + byActor.url)
|
if (!byActor.Account) throw new Error('Unknown account ' + byActor.url)
|
||||||
|
@ -92,7 +92,7 @@ async function processUndoDislike (byActor: MActorSignature, activity: ActivityU
|
||||||
const dislikeActivity = activity.object
|
const dislikeActivity = activity.object
|
||||||
|
|
||||||
const { video: onlyVideo } = await maybeGetOrCreateAPVideo({ videoObject: dislikeActivity.object })
|
const { video: onlyVideo } = await maybeGetOrCreateAPVideo({ videoObject: dislikeActivity.object })
|
||||||
if (!onlyVideo?.isOwned()) return
|
if (!onlyVideo?.isLocal()) return
|
||||||
|
|
||||||
return sequelizeTypescript.transaction(async t => {
|
return sequelizeTypescript.transaction(async t => {
|
||||||
if (!byActor.Account) throw new Error('Unknown account ' + byActor.url)
|
if (!byActor.Account) throw new Error('Unknown account ' + byActor.url)
|
||||||
|
@ -132,7 +132,7 @@ async function processUndoCacheFile (
|
||||||
|
|
||||||
await cacheFile.destroy({ transaction: t })
|
await cacheFile.destroy({ transaction: t })
|
||||||
|
|
||||||
if (video.isOwned()) {
|
if (video.isLocal()) {
|
||||||
// Don't resend the activity to the sender
|
// Don't resend the activity to the sender
|
||||||
const exceptions = [ byActor ]
|
const exceptions = [ byActor ]
|
||||||
|
|
||||||
|
@ -153,7 +153,7 @@ function processUndoAnnounce (byActor: MActorSignature, announceActivity: Activi
|
||||||
|
|
||||||
await share.destroy({ transaction: t })
|
await share.destroy({ transaction: t })
|
||||||
|
|
||||||
if (share.Video.isOwned()) {
|
if (share.Video.isLocal()) {
|
||||||
// Don't resend the activity to the sender
|
// Don't resend the activity to the sender
|
||||||
const exceptions = [ byActor ]
|
const exceptions = [ byActor ]
|
||||||
|
|
||||||
|
|
|
@ -103,7 +103,7 @@ async function processUpdateCacheFile (
|
||||||
|
|
||||||
const { video } = await getOrCreateAPVideo({ videoObject: cacheFileObject.object })
|
const { video } = await getOrCreateAPVideo({ videoObject: cacheFileObject.object })
|
||||||
|
|
||||||
if (video.isOwned() && !canVideoBeFederated(video)) {
|
if (video.isLocal() && !canVideoBeFederated(video)) {
|
||||||
logger.warn(`Do not process update cache file on video ${activity.object} that cannot be federated`)
|
logger.warn(`Do not process update cache file on video ${activity.object} that cannot be federated`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -112,7 +112,7 @@ async function processUpdateCacheFile (
|
||||||
await createOrUpdateCacheFile(cacheFileObject, video, byActor, t)
|
await createOrUpdateCacheFile(cacheFileObject, video, byActor, t)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (video.isOwned()) {
|
if (video.isLocal()) {
|
||||||
// Don't resend the activity to the sender
|
// Don't resend the activity to the sender
|
||||||
const exceptions = [ byActor ]
|
const exceptions = [ byActor ]
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ async function processCreateView (activity: ActivityView, byActor: MActorSignatu
|
||||||
viewerResultCounter: getViewerResultCounter(activity)
|
viewerResultCounter: getViewerResultCounter(activity)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (video.isOwned()) {
|
if (video.isLocal()) {
|
||||||
// Forward the view but don't resend the activity to the sender
|
// Forward the view but don't resend the activity to the sender
|
||||||
const exceptions = [ byActor ]
|
const exceptions = [ byActor ]
|
||||||
await forwardVideoRelatedActivity(activity, undefined, exceptions, video)
|
await forwardVideoRelatedActivity(activity, undefined, exceptions, video)
|
||||||
|
|
|
@ -110,7 +110,7 @@ export async function sendCreateVideoPlaylist (playlist: MVideoPlaylistFull, tra
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendCreateVideoCommentIfNeeded (comment: MCommentOwnerVideoReply, transaction: Transaction) {
|
export async function sendCreateVideoCommentIfNeeded (comment: MCommentOwnerVideoReply, transaction: Transaction) {
|
||||||
const isOrigin = comment.Video.isOwned()
|
const isOrigin = comment.Video.isLocal()
|
||||||
|
|
||||||
if (isOrigin) {
|
if (isOrigin) {
|
||||||
const videoWithBlacklist = await VideoModel.loadWithBlacklist(comment.Video.id)
|
const videoWithBlacklist = await VideoModel.loadWithBlacklist(comment.Video.id)
|
||||||
|
|
|
@ -53,13 +53,13 @@ async function sendDeleteActor (byActor: ActorModel, transaction: Transaction) {
|
||||||
async function sendDeleteVideoComment (videoComment: MCommentOwnerVideo, transaction: Transaction) {
|
async function sendDeleteVideoComment (videoComment: MCommentOwnerVideo, transaction: Transaction) {
|
||||||
logger.info('Creating job to send delete of comment %s.', videoComment.url)
|
logger.info('Creating job to send delete of comment %s.', videoComment.url)
|
||||||
|
|
||||||
const isVideoOrigin = videoComment.Video.isOwned()
|
const isVideoOrigin = videoComment.Video.isLocal()
|
||||||
|
|
||||||
const url = getDeleteActivityPubUrl(videoComment.url)
|
const url = getDeleteActivityPubUrl(videoComment.url)
|
||||||
|
|
||||||
const videoAccount = await AccountModel.load(videoComment.Video.VideoChannel.Account.id, transaction)
|
const videoAccount = await AccountModel.load(videoComment.Video.VideoChannel.Account.id, transaction)
|
||||||
|
|
||||||
const byActor = videoComment.isOwned()
|
const byActor = videoComment.isLocal()
|
||||||
? videoComment.Account.Actor
|
? videoComment.Account.Actor
|
||||||
: videoAccount.Actor
|
: videoAccount.Actor
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,7 @@ async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAud
|
||||||
const { byActor, video, transaction, contextType, parallelizable } = options
|
const { byActor, video, transaction, contextType, parallelizable } = options
|
||||||
|
|
||||||
// Send to origin
|
// Send to origin
|
||||||
if (video.isOwned() === false) {
|
if (video.isLocal() === false) {
|
||||||
return sendVideoActivityToOrigin(activityBuilder, options)
|
return sendVideoActivityToOrigin(activityBuilder, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,7 +62,7 @@ async function sendVideoActivityToOrigin (activityBuilder: (audience: ActivityAu
|
||||||
}) {
|
}) {
|
||||||
const { byActor, video, actorsInvolvedInVideo, transaction, contextType } = options
|
const { byActor, video, actorsInvolvedInVideo, transaction, contextType } = options
|
||||||
|
|
||||||
if (video.isOwned()) throw new Error('Cannot send activity to owned video origin ' + video.url)
|
if (video.isLocal()) throw new Error('Cannot send activity to owned video origin ' + video.url)
|
||||||
|
|
||||||
let accountActor: MActorLight = (video as MVideoAccountLight).VideoChannel?.Account?.Actor
|
let accountActor: MActorLight = (video as MVideoAccountLight).VideoChannel?.Account?.Actor
|
||||||
if (!accountActor) accountActor = await ActorModel.loadAccountActorByVideoId(video.id, transaction)
|
if (!accountActor) accountActor = await ActorModel.loadAccountActorByVideoId(video.id, transaction)
|
||||||
|
|
|
@ -109,7 +109,7 @@ async function tryToResolveThreadFromVideo (params: ResolveThreadParams) {
|
||||||
const syncParam = { rates: true, shares: true, comments: false, refreshVideo: false }
|
const syncParam = { rates: true, shares: true, comments: false, refreshVideo: false }
|
||||||
const { video } = await getOrCreateAPVideo({ videoObject: url, syncParam })
|
const { video } = await getOrCreateAPVideo({ videoObject: url, syncParam })
|
||||||
|
|
||||||
if (video.isOwned() && !canVideoBeFederated(video)) {
|
if (video.isLocal() && !canVideoBeFederated(video)) {
|
||||||
throw new Error('Cannot resolve thread of video that is not compatible with federation')
|
throw new Error('Cannot resolve thread of video that is not compatible with federation')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -169,7 +169,7 @@ async function getAutomaticTagsAndAssignReview (comment: MComment, video: MVideo
|
||||||
const automaticTags = await new AutomaticTagger().buildCommentsAutomaticTags({ ownerAccount, text: comment.text })
|
const automaticTags = await new AutomaticTagger().buildCommentsAutomaticTags({ ownerAccount, text: comment.text })
|
||||||
|
|
||||||
// Third parties rely on origin, so if origin has the comment it's not held for review
|
// Third parties rely on origin, so if origin has the comment it's not held for review
|
||||||
if (video.isOwned() || comment.isOwned()) {
|
if (video.isLocal() || comment.isLocal()) {
|
||||||
comment.heldForReview = await shouldCommentBeHeldForReview({ user: null, video, automaticTags })
|
comment.heldForReview = await shouldCommentBeHeldForReview({ user: null, video, automaticTags })
|
||||||
} else {
|
} else {
|
||||||
comment.heldForReview = false
|
comment.heldForReview = false
|
||||||
|
|
|
@ -13,7 +13,7 @@ async function sendVideoRateChange (
|
||||||
dislikes: number,
|
dislikes: number,
|
||||||
t: Transaction
|
t: Transaction
|
||||||
) {
|
) {
|
||||||
if (video.isOwned()) return federateVideoIfNeeded(video, false, t)
|
if (video.isLocal()) return federateVideoIfNeeded(video, false, t)
|
||||||
|
|
||||||
return sendVideoRateChangeToOrigin(account, video, likes, dislikes, t)
|
return sendVideoRateChangeToOrigin(account, video, likes, dislikes, t)
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,7 @@ async function sendVideoRateChangeToOrigin (
|
||||||
t: Transaction
|
t: Transaction
|
||||||
) {
|
) {
|
||||||
// Local video, we don't need to send like
|
// Local video, we don't need to send like
|
||||||
if (video.isOwned()) return
|
if (video.isLocal()) return
|
||||||
|
|
||||||
const actor = account.Actor
|
const actor = account.Actor
|
||||||
|
|
||||||
|
|
|
@ -490,7 +490,7 @@ export class Emailer {
|
||||||
}
|
}
|
||||||
|
|
||||||
private initHandlebarsIfNeeded () {
|
private initHandlebarsIfNeeded () {
|
||||||
if (this.registeringHandlebars) return this.registeringHandlebars
|
if (this.registeringHandlebars !== undefined) return this.registeringHandlebars
|
||||||
|
|
||||||
this.registeringHandlebars = this._initHandlebarsIfNeeded()
|
this.registeringHandlebars = this._initHandlebarsIfNeeded()
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ type ImageModel = {
|
||||||
filename: string
|
filename: string
|
||||||
onDisk: boolean
|
onDisk: boolean
|
||||||
|
|
||||||
isOwned (): boolean
|
isLocal(): boolean
|
||||||
getPath(): string
|
getPath(): string
|
||||||
|
|
||||||
save(): Promise<Model>
|
save(): Promise<Model>
|
||||||
|
@ -28,7 +28,6 @@ export abstract class AbstractPermanentFileCache <M extends ImageModel> {
|
||||||
protected abstract loadModel (filename: string): Promise<M>
|
protected abstract loadModel (filename: string): Promise<M>
|
||||||
|
|
||||||
constructor (private readonly directory: string) {
|
constructor (private readonly directory: string) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async lazyServe (options: {
|
async lazyServe (options: {
|
||||||
|
@ -102,7 +101,7 @@ export abstract class AbstractPermanentFileCache <M extends ImageModel> {
|
||||||
const { err, image, filename, next } = options
|
const { err, image, filename, next } = options
|
||||||
|
|
||||||
// It seems this actor image is not on the disk anymore
|
// It seems this actor image is not on the disk anymore
|
||||||
if (err.status === HttpStatusCode.NOT_FOUND_404 && !image.isOwned()) {
|
if (err.status === HttpStatusCode.NOT_FOUND_404 && !image.isLocal()) {
|
||||||
logger.error('Cannot lazy serve image %s.', filename, { err })
|
logger.error('Cannot lazy serve image %s.', filename, { err })
|
||||||
|
|
||||||
this.filenameToPathUnsafeCache.delete(filename)
|
this.filenameToPathUnsafeCache.delete(filename)
|
||||||
|
|
|
@ -2,10 +2,9 @@ import { remove } from 'fs-extra/esm'
|
||||||
import { logger } from '../../../helpers/logger.js'
|
import { logger } from '../../../helpers/logger.js'
|
||||||
import memoizee from 'memoizee'
|
import memoizee from 'memoizee'
|
||||||
|
|
||||||
type GetFilePathResult = { isOwned: boolean, path: string, downloadName?: string } | undefined
|
type GetFilePathResult = { isLocal: boolean, path: string, downloadName?: string } | undefined
|
||||||
|
|
||||||
export abstract class AbstractSimpleFileCache<T> {
|
export abstract class AbstractSimpleFileCache<T> {
|
||||||
|
|
||||||
getFilePath: (params: T) => Promise<GetFilePathResult>
|
getFilePath: (params: T) => Promise<GetFilePathResult>
|
||||||
|
|
||||||
abstract getFilePathImpl (params: T): Promise<GetFilePathResult>
|
abstract getFilePathImpl (params: T): Promise<GetFilePathResult>
|
||||||
|
@ -19,7 +18,7 @@ export abstract class AbstractSimpleFileCache <T> {
|
||||||
max,
|
max,
|
||||||
promise: true,
|
promise: true,
|
||||||
dispose: (result?: GetFilePathResult) => {
|
dispose: (result?: GetFilePathResult) => {
|
||||||
if (result && result.isOwned !== true) {
|
if (result && result.isLocal !== true) {
|
||||||
remove(result.path)
|
remove(result.path)
|
||||||
.then(() => logger.debug('%s removed from %s', result.path, this.constructor.name))
|
.then(() => logger.debug('%s removed from %s', result.path, this.constructor.name))
|
||||||
.catch(err => logger.error('Cannot remove %s from cache %s.', result.path, this.constructor.name, { err }))
|
.catch(err => logger.error('Cannot remove %s from cache %s.', result.path, this.constructor.name, { err }))
|
||||||
|
|
|
@ -21,8 +21,8 @@ class VideoCaptionsSimpleFileCache extends AbstractSimpleFileCache<string> {
|
||||||
const videoCaption = await VideoCaptionModel.loadWithVideoByFilename(filename)
|
const videoCaption = await VideoCaptionModel.loadWithVideoByFilename(filename)
|
||||||
if (!videoCaption) return undefined
|
if (!videoCaption) return undefined
|
||||||
|
|
||||||
if (videoCaption.isOwned()) {
|
if (videoCaption.isLocal()) {
|
||||||
return { isOwned: true, path: videoCaption.getFSFilePath() }
|
return { isLocal: true, path: videoCaption.getFSFilePath() }
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.loadRemoteFile(filename)
|
return this.loadRemoteFile(filename)
|
||||||
|
@ -33,7 +33,7 @@ class VideoCaptionsSimpleFileCache extends AbstractSimpleFileCache<string> {
|
||||||
const videoCaption = await VideoCaptionModel.loadWithVideoByFilename(key)
|
const videoCaption = await VideoCaptionModel.loadWithVideoByFilename(key)
|
||||||
if (!videoCaption) return undefined
|
if (!videoCaption) return undefined
|
||||||
|
|
||||||
if (videoCaption.isOwned()) throw new Error('Cannot load remote caption of owned video.')
|
if (videoCaption.isLocal()) throw new Error('Cannot load remote caption of owned video.')
|
||||||
|
|
||||||
// Used to fetch the path
|
// Used to fetch the path
|
||||||
const video = await VideoModel.loadFull(videoCaption.videoId)
|
const video = await VideoModel.loadFull(videoCaption.videoId)
|
||||||
|
@ -45,7 +45,7 @@ class VideoCaptionsSimpleFileCache extends AbstractSimpleFileCache<string> {
|
||||||
try {
|
try {
|
||||||
await doRequestAndSaveToFile(remoteUrl, destPath)
|
await doRequestAndSaveToFile(remoteUrl, destPath)
|
||||||
|
|
||||||
return { isOwned: false, path: destPath }
|
return { isLocal: false, path: destPath }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.info('Cannot fetch remote caption file %s.', remoteUrl, { err })
|
logger.info('Cannot fetch remote caption file %s.', remoteUrl, { err })
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
|
import { ThumbnailType } from '@peertube/peertube-models'
|
||||||
|
import { logger } from '@server/helpers/logger.js'
|
||||||
|
import { doRequestAndSaveToFile } from '@server/helpers/requests.js'
|
||||||
|
import { ThumbnailModel } from '@server/models/video/thumbnail.js'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { FILES_CACHE } from '../../initializers/constants.js'
|
import { FILES_CACHE } from '../../initializers/constants.js'
|
||||||
import { VideoModel } from '../../models/video/video.js'
|
import { VideoModel } from '../../models/video/video.js'
|
||||||
import { AbstractSimpleFileCache } from './shared/abstract-simple-file-cache.js'
|
import { AbstractSimpleFileCache } from './shared/abstract-simple-file-cache.js'
|
||||||
import { doRequestAndSaveToFile } from '@server/helpers/requests.js'
|
|
||||||
import { ThumbnailModel } from '@server/models/video/thumbnail.js'
|
|
||||||
import { ThumbnailType } from '@peertube/peertube-models'
|
|
||||||
import { logger } from '@server/helpers/logger.js'
|
|
||||||
|
|
||||||
class VideoPreviewsSimpleFileCache extends AbstractSimpleFileCache<string> {
|
class VideoPreviewsSimpleFileCache extends AbstractSimpleFileCache<string> {
|
||||||
|
|
||||||
private static instance: VideoPreviewsSimpleFileCache
|
private static instance: VideoPreviewsSimpleFileCache
|
||||||
|
|
||||||
private constructor () {
|
private constructor () {
|
||||||
|
@ -23,7 +22,7 @@ class VideoPreviewsSimpleFileCache extends AbstractSimpleFileCache <string> {
|
||||||
const thumbnail = await ThumbnailModel.loadWithVideoByFilename(filename, ThumbnailType.PREVIEW)
|
const thumbnail = await ThumbnailModel.loadWithVideoByFilename(filename, ThumbnailType.PREVIEW)
|
||||||
if (!thumbnail) return undefined
|
if (!thumbnail) return undefined
|
||||||
|
|
||||||
if (thumbnail.Video.isOwned()) return { isOwned: true, path: thumbnail.getPath() }
|
if (thumbnail.Video.isLocal()) return { isLocal: true, path: thumbnail.getPath() }
|
||||||
|
|
||||||
return this.loadRemoteFile(thumbnail.Video.uuid)
|
return this.loadRemoteFile(thumbnail.Video.uuid)
|
||||||
}
|
}
|
||||||
|
@ -33,7 +32,7 @@ class VideoPreviewsSimpleFileCache extends AbstractSimpleFileCache <string> {
|
||||||
const video = await VideoModel.loadFull(key)
|
const video = await VideoModel.loadFull(key)
|
||||||
if (!video) return undefined
|
if (!video) return undefined
|
||||||
|
|
||||||
if (video.isOwned()) throw new Error('Cannot load remote preview of owned video.')
|
if (video.isLocal()) throw new Error('Cannot load remote preview of owned video.')
|
||||||
|
|
||||||
const preview = video.getPreview()
|
const preview = video.getPreview()
|
||||||
const destPath = join(FILES_CACHE.PREVIEWS.DIRECTORY, preview.filename)
|
const destPath = join(FILES_CACHE.PREVIEWS.DIRECTORY, preview.filename)
|
||||||
|
@ -44,7 +43,7 @@ class VideoPreviewsSimpleFileCache extends AbstractSimpleFileCache <string> {
|
||||||
|
|
||||||
logger.debug('Fetched remote preview %s to %s.', remoteUrl, destPath)
|
logger.debug('Fetched remote preview %s to %s.', remoteUrl, destPath)
|
||||||
|
|
||||||
return { isOwned: false, path: destPath }
|
return { isLocal: false, path: destPath }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.info('Cannot fetch remote preview file %s.', remoteUrl, { err })
|
logger.info('Cannot fetch remote preview file %s.', remoteUrl, { err })
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import { join } from 'path'
|
|
||||||
import { logger } from '@server/helpers/logger.js'
|
import { logger } from '@server/helpers/logger.js'
|
||||||
import { doRequestAndSaveToFile } from '@server/helpers/requests.js'
|
import { doRequestAndSaveToFile } from '@server/helpers/requests.js'
|
||||||
import { StoryboardModel } from '@server/models/video/storyboard.js'
|
import { StoryboardModel } from '@server/models/video/storyboard.js'
|
||||||
|
import { join } from 'path'
|
||||||
import { FILES_CACHE } from '../../initializers/constants.js'
|
import { FILES_CACHE } from '../../initializers/constants.js'
|
||||||
import { AbstractSimpleFileCache } from './shared/abstract-simple-file-cache.js'
|
import { AbstractSimpleFileCache } from './shared/abstract-simple-file-cache.js'
|
||||||
|
|
||||||
class VideoStoryboardsSimpleFileCache extends AbstractSimpleFileCache<string> {
|
class VideoStoryboardsSimpleFileCache extends AbstractSimpleFileCache<string> {
|
||||||
|
|
||||||
private static instance: VideoStoryboardsSimpleFileCache
|
private static instance: VideoStoryboardsSimpleFileCache
|
||||||
|
|
||||||
private constructor () {
|
private constructor () {
|
||||||
|
@ -21,7 +20,7 @@ class VideoStoryboardsSimpleFileCache extends AbstractSimpleFileCache <string> {
|
||||||
const storyboard = await StoryboardModel.loadWithVideoByFilename(filename)
|
const storyboard = await StoryboardModel.loadWithVideoByFilename(filename)
|
||||||
if (!storyboard) return undefined
|
if (!storyboard) return undefined
|
||||||
|
|
||||||
if (storyboard.Video.isOwned()) return { isOwned: true, path: storyboard.getPath() }
|
if (storyboard.Video.isLocal()) return { isLocal: true, path: storyboard.getPath() }
|
||||||
|
|
||||||
return this.loadRemoteFile(storyboard.filename)
|
return this.loadRemoteFile(storyboard.filename)
|
||||||
}
|
}
|
||||||
|
@ -39,7 +38,7 @@ class VideoStoryboardsSimpleFileCache extends AbstractSimpleFileCache <string> {
|
||||||
|
|
||||||
logger.debug('Fetched remote storyboard %s to %s.', remoteUrl, destPath)
|
logger.debug('Fetched remote storyboard %s to %s.', remoteUrl, destPath)
|
||||||
|
|
||||||
return { isOwned: false, path: destPath }
|
return { isLocal: false, path: destPath }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.info('Cannot fetch remote storyboard file %s.', remoteUrl, { err })
|
logger.info('Cannot fetch remote storyboard file %s.', remoteUrl, { err })
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,6 @@ import { VideoModel } from '../../models/video/video.js'
|
||||||
import { AbstractSimpleFileCache } from './shared/abstract-simple-file-cache.js'
|
import { AbstractSimpleFileCache } from './shared/abstract-simple-file-cache.js'
|
||||||
|
|
||||||
class VideoTorrentsSimpleFileCache extends AbstractSimpleFileCache<string> {
|
class VideoTorrentsSimpleFileCache extends AbstractSimpleFileCache<string> {
|
||||||
|
|
||||||
private static instance: VideoTorrentsSimpleFileCache
|
private static instance: VideoTorrentsSimpleFileCache
|
||||||
|
|
||||||
private constructor () {
|
private constructor () {
|
||||||
|
@ -24,10 +23,10 @@ class VideoTorrentsSimpleFileCache extends AbstractSimpleFileCache <string> {
|
||||||
const file = await VideoFileModel.loadWithVideoOrPlaylistByTorrentFilename(filename)
|
const file = await VideoFileModel.loadWithVideoOrPlaylistByTorrentFilename(filename)
|
||||||
if (!file) return undefined
|
if (!file) return undefined
|
||||||
|
|
||||||
if (file.getVideo().isOwned()) {
|
if (file.getVideo().isLocal()) {
|
||||||
const downloadName = this.buildDownloadName(file.getVideo(), file)
|
const downloadName = this.buildDownloadName(file.getVideo(), file)
|
||||||
|
|
||||||
return { isOwned: true, path: join(CONFIG.STORAGE.TORRENTS_DIR, file.torrentFilename), downloadName }
|
return { isLocal: true, path: join(CONFIG.STORAGE.TORRENTS_DIR, file.torrentFilename), downloadName }
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.loadRemoteFile(filename)
|
return this.loadRemoteFile(filename)
|
||||||
|
@ -38,7 +37,7 @@ class VideoTorrentsSimpleFileCache extends AbstractSimpleFileCache <string> {
|
||||||
const file = await VideoFileModel.loadWithVideoOrPlaylistByTorrentFilename(key)
|
const file = await VideoFileModel.loadWithVideoOrPlaylistByTorrentFilename(key)
|
||||||
if (!file) return undefined
|
if (!file) return undefined
|
||||||
|
|
||||||
if (file.getVideo().isOwned()) throw new Error('Cannot load remote file of owned video.')
|
if (file.getVideo().isLocal()) throw new Error('Cannot load remote file of owned video.')
|
||||||
|
|
||||||
// Used to fetch the path
|
// Used to fetch the path
|
||||||
const video = await VideoModel.loadFull(file.getVideo().id)
|
const video = await VideoModel.loadFull(file.getVideo().id)
|
||||||
|
@ -52,7 +51,7 @@ class VideoTorrentsSimpleFileCache extends AbstractSimpleFileCache <string> {
|
||||||
|
|
||||||
const downloadName = this.buildDownloadName(video, file)
|
const downloadName = this.buildDownloadName(video, file)
|
||||||
|
|
||||||
return { isOwned: false, path: destPath, downloadName }
|
return { isLocal: false, path: destPath, downloadName }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.info('Cannot fetch remote torrent file %s.', remoteUrl, { err })
|
logger.info('Cannot fetch remote torrent file %s.', remoteUrl, { err })
|
||||||
|
|
||||||
|
|
|
@ -108,7 +108,7 @@ export class ActorHtml {
|
||||||
updatedAt: entity.updatedAt
|
updatedAt: entity.updatedAt
|
||||||
},
|
},
|
||||||
|
|
||||||
forbidIndexation: !entity.Actor.isOwned(),
|
forbidIndexation: !entity.Actor.isLocal(),
|
||||||
embedIndexation: false,
|
embedIndexation: false,
|
||||||
|
|
||||||
rssFeeds: getRSSFeeds(entity)
|
rssFeeds: getRSSFeeds(entity)
|
||||||
|
|
|
@ -113,7 +113,7 @@ export class PlaylistHtml {
|
||||||
|
|
||||||
forbidIndexation: isEmbed
|
forbidIndexation: isEmbed
|
||||||
? playlist.privacy !== VideoPlaylistPrivacy.PUBLIC && playlist.privacy !== VideoPlaylistPrivacy.UNLISTED
|
? playlist.privacy !== VideoPlaylistPrivacy.PUBLIC && playlist.privacy !== VideoPlaylistPrivacy.UNLISTED
|
||||||
: !playlist.isOwned() || playlist.privacy !== VideoPlaylistPrivacy.PUBLIC,
|
: !playlist.isLocal() || playlist.privacy !== VideoPlaylistPrivacy.PUBLIC,
|
||||||
|
|
||||||
embedIndexation: isEmbed,
|
embedIndexation: isEmbed,
|
||||||
|
|
||||||
|
|
|
@ -438,7 +438,10 @@ class MuxingSession extends EventEmitter implements MuxingSession {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Wait latest segments generation, and close watchers
|
// Wait latest segments generation, and close watchers
|
||||||
|
|
||||||
const promise = this.filesWatcher?.close() || Promise.resolve()
|
const promise = this.filesWatcher
|
||||||
|
? this.filesWatcher.close()
|
||||||
|
: Promise.resolve()
|
||||||
|
|
||||||
promise
|
promise
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Process remaining segments hash
|
// Process remaining segments hash
|
||||||
|
|
|
@ -129,7 +129,7 @@ async function createVideoAbuse (options: {
|
||||||
videoAbuseInstance.Video = videoInstance
|
videoAbuseInstance.Video = videoInstance
|
||||||
abuseInstance.VideoAbuse = videoAbuseInstance
|
abuseInstance.VideoAbuse = videoAbuseInstance
|
||||||
|
|
||||||
return { isOwned: videoInstance.isOwned() }
|
return { isLocal: videoInstance.isLocal() }
|
||||||
}
|
}
|
||||||
|
|
||||||
return createAbuse({
|
return createAbuse({
|
||||||
|
@ -160,7 +160,7 @@ function createVideoCommentAbuse (options: {
|
||||||
commentAbuseInstance.VideoComment = commentInstance
|
commentAbuseInstance.VideoComment = commentInstance
|
||||||
abuseInstance.VideoCommentAbuse = commentAbuseInstance
|
abuseInstance.VideoCommentAbuse = commentAbuseInstance
|
||||||
|
|
||||||
return { isOwned: commentInstance.isOwned() }
|
return { isLocal: commentInstance.isLocal() }
|
||||||
}
|
}
|
||||||
|
|
||||||
return createAbuse({
|
return createAbuse({
|
||||||
|
@ -183,7 +183,7 @@ function createAccountAbuse (options: {
|
||||||
const { baseAbuse, accountInstance, transaction, reporterAccount, skipNotification } = options
|
const { baseAbuse, accountInstance, transaction, reporterAccount, skipNotification } = options
|
||||||
|
|
||||||
const associateFun = () => {
|
const associateFun = () => {
|
||||||
return Promise.resolve({ isOwned: accountInstance.isOwned() })
|
return Promise.resolve({ isLocal: accountInstance.isLocal() })
|
||||||
}
|
}
|
||||||
|
|
||||||
return createAbuse({
|
return createAbuse({
|
||||||
|
@ -217,7 +217,7 @@ async function createAbuse (options: {
|
||||||
base: FilteredModelAttributes<AbuseModel>
|
base: FilteredModelAttributes<AbuseModel>
|
||||||
reporterAccount: MAccountDefault
|
reporterAccount: MAccountDefault
|
||||||
flaggedAccount: MAccountLight
|
flaggedAccount: MAccountLight
|
||||||
associateFun: (abuseInstance: MAbuseFull) => Promise<{ isOwned: boolean }>
|
associateFun: (abuseInstance: MAbuseFull) => Promise<{ isLocal: boolean }>
|
||||||
skipNotification: boolean
|
skipNotification: boolean
|
||||||
transaction: Transaction
|
transaction: Transaction
|
||||||
}) {
|
}) {
|
||||||
|
@ -230,9 +230,9 @@ async function createAbuse (options: {
|
||||||
abuseInstance.ReporterAccount = reporterAccount
|
abuseInstance.ReporterAccount = reporterAccount
|
||||||
abuseInstance.FlaggedAccount = flaggedAccount
|
abuseInstance.FlaggedAccount = flaggedAccount
|
||||||
|
|
||||||
const { isOwned } = await associateFun(abuseInstance)
|
const { isLocal } = await associateFun(abuseInstance)
|
||||||
|
|
||||||
if (isOwned === false) {
|
if (isLocal === false) {
|
||||||
sendAbuse(reporterAccount.Actor, abuseInstance, abuseInstance.FlaggedAccount, transaction)
|
sendAbuse(reporterAccount.Actor, abuseInstance, abuseInstance.FlaggedAccount, transaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,9 @@ import {
|
||||||
MAbuseMessage,
|
MAbuseMessage,
|
||||||
MActorFollowFull,
|
MActorFollowFull,
|
||||||
MApplication,
|
MApplication,
|
||||||
|
MChannelAccountDefault,
|
||||||
|
MChannelCollaboratorAccount,
|
||||||
|
MChannelDefault,
|
||||||
MCommentOwnerVideo,
|
MCommentOwnerVideo,
|
||||||
MPlugin,
|
MPlugin,
|
||||||
MVideoAccountLight,
|
MVideoAccountLight,
|
||||||
|
@ -17,6 +20,9 @@ import {
|
||||||
import { JobQueue } from '../job-queue/index.js'
|
import { JobQueue } from '../job-queue/index.js'
|
||||||
import { PeerTubeSocket } from '../peertube-socket.js'
|
import { PeerTubeSocket } from '../peertube-socket.js'
|
||||||
import { Hooks } from '../plugins/hooks.js'
|
import { Hooks } from '../plugins/hooks.js'
|
||||||
|
import { AcceptedToCollaborateToChannel } from './shared/channel/accepted-to-collaborate-to-channel.js'
|
||||||
|
import { InvitedToCollaborateToChannel } from './shared/channel/invited-to-collaborate-to-channel.js'
|
||||||
|
import { RefusedToCollaborateToChannel } from './shared/channel/refused-to-collaborate-to-channel.js'
|
||||||
import {
|
import {
|
||||||
AbstractNotification,
|
AbstractNotification,
|
||||||
AbuseStateChangeForReporter,
|
AbuseStateChangeForReporter,
|
||||||
|
@ -71,7 +77,10 @@ class Notifier {
|
||||||
newPeertubeVersion: [ NewPeerTubeVersionForAdmins ],
|
newPeertubeVersion: [ NewPeerTubeVersionForAdmins ],
|
||||||
newPluginVersion: [ NewPluginVersionForAdmins ],
|
newPluginVersion: [ NewPluginVersionForAdmins ],
|
||||||
videoStudioEditionFinished: [ StudioEditionFinishedForOwner ],
|
videoStudioEditionFinished: [ StudioEditionFinishedForOwner ],
|
||||||
videoTranscriptionGenerated: [ VideoTranscriptionGeneratedForOwner ]
|
videoTranscriptionGenerated: [ VideoTranscriptionGeneratedForOwner ],
|
||||||
|
channelCollaboratorInvitation: [ InvitedToCollaborateToChannel ],
|
||||||
|
channelCollaborationAccepted: [ AcceptedToCollaborateToChannel ],
|
||||||
|
channelCollaborationRefused: [ RefusedToCollaborateToChannel ]
|
||||||
}
|
}
|
||||||
|
|
||||||
private static instance: Notifier
|
private static instance: Notifier
|
||||||
|
@ -287,6 +296,44 @@ class Notifier {
|
||||||
.catch(err => logger.error('Cannot notify on generated video transcription %s of video %s.', caption.language, video.url, { err }))
|
.catch(err => logger.error('Cannot notify on generated video transcription %s of video %s.', caption.language, video.url, { err }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notifyOfChannelCollaboratorInvitation (collaborator: MChannelCollaboratorAccount, channel: MChannelAccountDefault) {
|
||||||
|
const models = this.notificationModels.channelCollaboratorInvitation
|
||||||
|
|
||||||
|
const channelName = channel.Actor.preferredUsername
|
||||||
|
const collaboratorName = collaborator.Account.Actor.preferredUsername
|
||||||
|
|
||||||
|
logger.debug('Notify on channel collaborator invitation', { channelName, collaboratorName, ...lTags() })
|
||||||
|
|
||||||
|
this.sendNotifications(models, { channel, collaborator })
|
||||||
|
.catch(err => logger.error(`Cannot notify ${collaboratorName} of invitation to collaborate to channel ${channelName}`, { err }))
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyOfAcceptedChannelCollaborator (collaborator: MChannelCollaboratorAccount, channel: MChannelDefault) {
|
||||||
|
const models = this.notificationModels.channelCollaborationAccepted
|
||||||
|
|
||||||
|
const channelName = channel.Actor.preferredUsername
|
||||||
|
const channelOwner = collaborator.Account.Actor.preferredUsername
|
||||||
|
|
||||||
|
logger.debug('Notify of accepted channel collaboration invitation', { channelName, channelOwner, ...lTags() })
|
||||||
|
|
||||||
|
this.sendNotifications(models, { channel, collaborator })
|
||||||
|
.catch(err => logger.error(`Cannot notify ${channelOwner} of accepted invitation to collaborate to channel ${channelName}`, { err }))
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyOfRefusedChannelCollaborator (collaborator: MChannelCollaboratorAccount, channel: MChannelDefault) {
|
||||||
|
const models = this.notificationModels.channelCollaborationRefused
|
||||||
|
|
||||||
|
const channelName = channel.Actor.preferredUsername
|
||||||
|
const channelOwner = collaborator.Account.Actor.preferredUsername
|
||||||
|
|
||||||
|
logger.debug('Notify of refused channel collaboration invitation', { channelName, channelOwner, ...lTags() })
|
||||||
|
|
||||||
|
this.sendNotifications(models, { channel, collaborator })
|
||||||
|
.catch(err => logger.error(`Cannot notify ${channelOwner} of refused invitation to collaborate to channel ${channelName}`, { err }))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
private async notify<T> (object: AbstractNotification<T>) {
|
private async notify<T> (object: AbstractNotification<T>) {
|
||||||
await object.prepare()
|
await object.prepare()
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ export class AbuseStateChangeForReporter extends AbstractNotification<MAbuseFull
|
||||||
|
|
||||||
async prepare () {
|
async prepare () {
|
||||||
const reporter = this.abuse.ReporterAccount
|
const reporter = this.abuse.ReporterAccount
|
||||||
if (reporter.isOwned() !== true) return
|
if (reporter.isLocal() !== true) return
|
||||||
|
|
||||||
this.user = await UserModel.loadByAccountActorId(this.abuse.ReporterAccount.actorId)
|
this.user = await UserModel.loadByAccountActorId(this.abuse.ReporterAccount.actorId)
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue