mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2025-10-03 01:39:37 +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 { buildFFmpegLive, ProcessOptions } from './common.js'
|
||||
|
||||
type CustomLiveRTMPHLSTranscodingUpdatePayload =
|
||||
Omit<LiveRTMPHLSTranscodingUpdatePayload, 'resolutionPlaylistFile'> & { resolutionPlaylistFile?: [ Buffer, string ] | Blob | string }
|
||||
type CustomLiveRTMPHLSTranscodingUpdatePayload = Omit<LiveRTMPHLSTranscodingUpdatePayload, 'resolutionPlaylistFile'> & {
|
||||
resolutionPlaylistFile?: [Buffer, string] | Blob | string
|
||||
}
|
||||
|
||||
export class ProcessLiveRTMPHLSTranscoding {
|
||||
|
||||
private readonly outputPath: string
|
||||
private readonly fsWatchers: FSWatcher[] = []
|
||||
|
||||
|
@ -326,7 +326,7 @@ export class ProcessLiveRTMPHLSTranscoding {
|
|||
|
||||
const p = payloadBuilder().then(p => this.updateWithRetry(p))
|
||||
|
||||
if (!sequentialPromises) sequentialPromises = p
|
||||
if (sequentialPromises === undefined) sequentialPromises = p
|
||||
else sequentialPromises = sequentialPromises.then(() => p)
|
||||
}
|
||||
|
||||
|
@ -388,7 +388,7 @@ export class ProcessLiveRTMPHLSTranscoding {
|
|||
return [
|
||||
Buffer.from(this.latestFilteredPlaylistContent[playlistName], 'utf-8'),
|
||||
join(this.outputPath, 'master.m3u8')
|
||||
] as [ Buffer, string ]
|
||||
] as [Buffer, string]
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
@ -140,33 +140,37 @@ export class MyVideoChannelsComponent {
|
|||
})),
|
||||
switchMap(options => this.videoChannelService.listAccountVideoChannels(options))
|
||||
)
|
||||
.subscribe(res => {
|
||||
this.videoChannels = this.videoChannels.concat(res.data)
|
||||
this.pagination.totalItems = res.total
|
||||
.subscribe({
|
||||
next: res => {
|
||||
this.videoChannels = this.videoChannels.concat(res.data)
|
||||
this.pagination.totalItems = res.total
|
||||
|
||||
// chart data
|
||||
this.videoChannelsChartData = this.videoChannels.map(v => ({
|
||||
labels: v.viewsPerDay.map(day => day.date.toLocaleDateString()),
|
||||
datasets: [
|
||||
{
|
||||
label: $localize`Views for the day`,
|
||||
data: v.viewsPerDay.map(day => day.views),
|
||||
fill: false,
|
||||
borderColor: '#c6c6c6'
|
||||
}
|
||||
],
|
||||
// chart data
|
||||
this.videoChannelsChartData = this.videoChannels.map(v => ({
|
||||
labels: v.viewsPerDay.map(day => day.date.toLocaleDateString()),
|
||||
datasets: [
|
||||
{
|
||||
label: $localize`Views for the day`,
|
||||
data: v.viewsPerDay.map(day => day.views),
|
||||
fill: false,
|
||||
borderColor: '#c6c6c6'
|
||||
}
|
||||
],
|
||||
|
||||
total: v.viewsPerDay.map(day => day.views)
|
||||
.reduce((p, c) => p + c, 0),
|
||||
total: v.viewsPerDay.map(day => day.views)
|
||||
.reduce((p, c) => p + c, 0),
|
||||
|
||||
startDate: v.viewsPerDay.length !== 0
|
||||
? v.viewsPerDay[0].date.toLocaleDateString()
|
||||
: ''
|
||||
}))
|
||||
startDate: v.viewsPerDay.length !== 0
|
||||
? v.viewsPerDay[0].date.toLocaleDateString()
|
||||
: ''
|
||||
}))
|
||||
|
||||
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,
|
||||
FollowState,
|
||||
PluginType_Type,
|
||||
UserNotificationData,
|
||||
UserNotification as UserNotificationServer,
|
||||
UserNotificationType,
|
||||
UserNotificationType_Type,
|
||||
|
@ -21,6 +22,7 @@ export class UserNotification implements UserNotificationServer {
|
|||
id: number
|
||||
type: UserNotificationType_Type
|
||||
read: boolean
|
||||
data: UserNotificationData
|
||||
|
||||
video?: VideoInfo & {
|
||||
channel: ActorInfo & { avatarUrl?: string }
|
||||
|
|
|
@ -69,7 +69,7 @@ export class PeerTubeEmbed {
|
|||
this.peertubePlugin = new PeerTubePlugin(this.http)
|
||||
this.peertubeTheme = new PeerTubeTheme(this.peertubePlugin)
|
||||
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.requiresPassword = false
|
||||
|
||||
|
|
|
@ -65,8 +65,7 @@ export class PlayerOptionsBuilder {
|
|||
constructor (
|
||||
private readonly playerHTML: PlayerHTML,
|
||||
private readonly videoFetcher: VideoFetcher,
|
||||
private readonly peertubePlugin: PeerTubePlugin,
|
||||
private readonly serverConfig: HTMLServerConfig
|
||||
private readonly peertubePlugin: PeerTubePlugin
|
||||
) {}
|
||||
|
||||
hasAPIEnabled () {
|
||||
|
|
|
@ -85,7 +85,11 @@ export default defineConfig([
|
|||
'@typescript-eslint/no-unnecessary-boolean-literal-compare': 'off',
|
||||
'@typescript-eslint/strict-boolean-expressions': '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-empty-interface': '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-flag.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-setting.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 { AbuseStateType } from '../moderation/index.js'
|
||||
import { PluginType_Type } from '../plugins/index.js'
|
||||
import { VideoChannelCollaboratorStateType } from '../videos/index.js'
|
||||
import { VideoConstant } from '../videos/video-constant.model.js'
|
||||
import { VideoStateType } from '../videos/video-state.enum.js'
|
||||
import { UserNotificationData } from './user-notification-data.model.js'
|
||||
|
||||
export const UserNotificationType = {
|
||||
NEW_VIDEO_FROM_SUBSCRIPTION: 1,
|
||||
|
@ -40,7 +42,11 @@ export const UserNotificationType = {
|
|||
|
||||
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
|
||||
|
||||
export type UserNotificationType_Type = typeof UserNotificationType[keyof typeof UserNotificationType]
|
||||
|
@ -79,6 +85,7 @@ export interface UserNotification {
|
|||
id: number
|
||||
type: UserNotificationType_Type
|
||||
read: boolean
|
||||
data: UserNotificationData
|
||||
|
||||
video?: VideoInfo & {
|
||||
channel: ActorInfo
|
||||
|
@ -156,6 +163,15 @@ export interface UserNotification {
|
|||
video: VideoInfo
|
||||
}
|
||||
|
||||
videoChannelCollaborator?: {
|
||||
id: number
|
||||
|
||||
state: VideoConstant<VideoChannelCollaboratorStateType>
|
||||
|
||||
channel: ActorInfo
|
||||
account: ActorInfo
|
||||
}
|
||||
|
||||
createdAt: 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.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,
|
||||
CaptionsCommand,
|
||||
ChangeOwnershipCommand,
|
||||
ChannelCollaboratorsCommand,
|
||||
ChannelSyncsCommand,
|
||||
ChannelsCommand,
|
||||
ChaptersCommand,
|
||||
|
@ -82,6 +83,7 @@ export class PeerTubeServer {
|
|||
|
||||
parallel?: boolean
|
||||
internalServerNumber: number
|
||||
adminEmail: string
|
||||
|
||||
serverNumber?: number
|
||||
customConfigFile?: string
|
||||
|
@ -170,6 +172,8 @@ export class PeerTubeServer {
|
|||
watchedWordsLists?: WatchedWordsCommand
|
||||
autoTags?: AutomaticTagsCommand
|
||||
|
||||
channelCollaborators?: ChannelCollaboratorsCommand
|
||||
|
||||
constructor (options: { serverNumber: number } | { url: string }) {
|
||||
if ((options as any).url) {
|
||||
this.setUrl((options as any).url)
|
||||
|
@ -188,6 +192,7 @@ export class PeerTubeServer {
|
|||
}
|
||||
}
|
||||
|
||||
this.adminEmail = this.buildEmail()
|
||||
this.assignCommands()
|
||||
}
|
||||
|
||||
|
@ -406,7 +411,7 @@ export class PeerTubeServer {
|
|||
well_known: this.getDirectoryPath('well-known') + '/'
|
||||
},
|
||||
admin: {
|
||||
email: `admin${this.internalServerNumber}@example.com`
|
||||
email: this.buildEmail()
|
||||
},
|
||||
live: {
|
||||
rtmp: {
|
||||
|
@ -477,5 +482,11 @@ export class PeerTubeServer {
|
|||
|
||||
this.watchedWordsLists = new WatchedWordsCommand(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,
|
||||
UserUpdateMe,
|
||||
UserVideoQuota,
|
||||
UserVideoRate
|
||||
UserVideoRate,
|
||||
VideoChannel
|
||||
} from '@peertube/peertube-models'
|
||||
import { unwrapBody } from '../requests/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 = {}) {
|
||||
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
|
||||
withStats?: boolean
|
||||
search?: string
|
||||
includeCollaborations?: boolean
|
||||
}
|
||||
) {
|
||||
const { accountName, sort = 'createdAt' } = options
|
||||
|
@ -48,7 +49,7 @@ export class ChannelsCommand extends AbstractCommand {
|
|||
...options,
|
||||
|
||||
path,
|
||||
query: { sort, ...pick(options, [ 'start', 'count', 'withStats', 'search' ]) },
|
||||
query: { sort, ...pick(options, [ 'start', 'count', 'withStats', 'search', 'includeCollaborations' ]) },
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
|
|
|
@ -23,7 +23,6 @@ type ListForAdminOrAccountCommonOptions = {
|
|||
}
|
||||
|
||||
export class CommentsCommand extends AbstractCommand {
|
||||
|
||||
private lastVideoId: number | string
|
||||
private lastThreadId: number
|
||||
private lastReplyId: number
|
||||
|
@ -51,6 +50,7 @@ export class CommentsCommand extends AbstractCommand {
|
|||
|
||||
listCommentsOnMyVideos (options: OverrideCommandOptions & ListForAdminOrAccountCommonOptions & {
|
||||
isHeldForReview?: boolean
|
||||
includeCollaborations?: boolean
|
||||
} = {}) {
|
||||
const path = '/api/v1/users/me/videos/comments'
|
||||
|
||||
|
@ -61,7 +61,7 @@ export class CommentsCommand extends AbstractCommand {
|
|||
query: {
|
||||
...this.buildListForAdminOrAccountQuery(options),
|
||||
|
||||
isHeldForReview: options.isHeldForReview
|
||||
...pick(options, [ 'isHeldForReview', 'includeCollaborations' ])
|
||||
},
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
|
@ -78,13 +78,15 @@ export class CommentsCommand extends AbstractCommand {
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
listThreads (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
videoPassword?: string
|
||||
start?: number
|
||||
count?: number
|
||||
sort?: string
|
||||
}) {
|
||||
listThreads (
|
||||
options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
videoPassword?: string
|
||||
start?: number
|
||||
count?: number
|
||||
sort?: string
|
||||
}
|
||||
) {
|
||||
const { start, count, sort, videoId, videoPassword } = options
|
||||
const path = '/api/v1/videos/' + videoId + '/comment-threads'
|
||||
|
||||
|
@ -99,10 +101,12 @@ export class CommentsCommand extends AbstractCommand {
|
|||
})
|
||||
}
|
||||
|
||||
getThread (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
threadId: number
|
||||
}) {
|
||||
getThread (
|
||||
options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
threadId: number
|
||||
}
|
||||
) {
|
||||
const { videoId, threadId } = options
|
||||
const path = '/api/v1/videos/' + videoId + '/comment-threads/' + threadId
|
||||
|
||||
|
@ -115,21 +119,25 @@ export class CommentsCommand extends AbstractCommand {
|
|||
})
|
||||
}
|
||||
|
||||
async getThreadOf (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
text: string
|
||||
}) {
|
||||
async getThreadOf (
|
||||
options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
text: string
|
||||
}
|
||||
) {
|
||||
const { videoId, text } = options
|
||||
const threadId = await this.findCommentId({ videoId, text })
|
||||
|
||||
return this.getThread({ ...options, videoId, threadId })
|
||||
}
|
||||
|
||||
async createThread (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
text: string
|
||||
videoPassword?: string
|
||||
}) {
|
||||
async createThread (
|
||||
options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
text: string
|
||||
videoPassword?: string
|
||||
}
|
||||
) {
|
||||
const { videoId, text, videoPassword } = options
|
||||
const path = '/api/v1/videos/' + videoId + '/comment-threads'
|
||||
|
||||
|
@ -149,12 +157,14 @@ export class CommentsCommand extends AbstractCommand {
|
|||
return body.comment
|
||||
}
|
||||
|
||||
async addReply (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
toCommentId: number
|
||||
text: string
|
||||
videoPassword?: string
|
||||
}) {
|
||||
async addReply (
|
||||
options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
toCommentId: number
|
||||
text: string
|
||||
videoPassword?: string
|
||||
}
|
||||
) {
|
||||
const { videoId, toCommentId, text, videoPassword } = options
|
||||
const path = '/api/v1/videos/' + videoId + '/comments/' + toCommentId
|
||||
|
||||
|
@ -173,22 +183,28 @@ export class CommentsCommand extends AbstractCommand {
|
|||
return body.comment
|
||||
}
|
||||
|
||||
async addReplyToLastReply (options: OverrideCommandOptions & {
|
||||
text: string
|
||||
}) {
|
||||
async addReplyToLastReply (
|
||||
options: OverrideCommandOptions & {
|
||||
text: string
|
||||
}
|
||||
) {
|
||||
return this.addReply({ ...options, videoId: this.lastVideoId, toCommentId: this.lastReplyId })
|
||||
}
|
||||
|
||||
async addReplyToLastThread (options: OverrideCommandOptions & {
|
||||
text: string
|
||||
}) {
|
||||
async addReplyToLastThread (
|
||||
options: OverrideCommandOptions & {
|
||||
text: string
|
||||
}
|
||||
) {
|
||||
return this.addReply({ ...options, videoId: this.lastVideoId, toCommentId: this.lastThreadId })
|
||||
}
|
||||
|
||||
async findCommentId (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
text: string
|
||||
}) {
|
||||
async findCommentId (
|
||||
options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
text: string
|
||||
}
|
||||
) {
|
||||
const { videoId, text } = options
|
||||
const { data } = await this.listForAdmin({ videoId, count: 25, sort: '-createdAt' })
|
||||
|
||||
|
@ -197,10 +213,12 @@ export class CommentsCommand extends AbstractCommand {
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
delete (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
commentId: number
|
||||
}) {
|
||||
delete (
|
||||
options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
commentId: number
|
||||
}
|
||||
) {
|
||||
const { videoId, commentId } = options
|
||||
const path = '/api/v1/videos/' + videoId + '/comments/' + commentId
|
||||
|
||||
|
@ -213,9 +231,11 @@ export class CommentsCommand extends AbstractCommand {
|
|||
})
|
||||
}
|
||||
|
||||
async deleteAllComments (options: OverrideCommandOptions & {
|
||||
videoUUID: string
|
||||
}) {
|
||||
async deleteAllComments (
|
||||
options: OverrideCommandOptions & {
|
||||
videoUUID: string
|
||||
}
|
||||
) {
|
||||
const { data } = await this.listForAdmin({ ...options, start: 0, count: 20 })
|
||||
|
||||
for (const comment of data) {
|
||||
|
@ -227,10 +247,12 @@ export class CommentsCommand extends AbstractCommand {
|
|||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
approve (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
commentId: number
|
||||
}) {
|
||||
approve (
|
||||
options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
commentId: number
|
||||
}
|
||||
) {
|
||||
const { videoId, commentId } = options
|
||||
const path = '/api/v1/videos/' + videoId + '/comments/' + commentId + '/approve'
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
export * from './blacklist-command.js'
|
||||
export * from './captions-command.js'
|
||||
export * from './channel-collaborators-command.js'
|
||||
export * from './change-ownership-command.js'
|
||||
export * from './channels.js'
|
||||
export * from './channels-command.js'
|
||||
|
|
|
@ -71,10 +71,11 @@ export class PlaylistsCommand extends AbstractCommand {
|
|||
sort?: string
|
||||
search?: string
|
||||
playlistType?: VideoPlaylistType_Type
|
||||
includeCollaborations?: boolean
|
||||
}
|
||||
) {
|
||||
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>>({
|
||||
...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'
|
||||
|
||||
return this.getRequestBody<ResultList<Video>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: { ...this.buildListQuery(options), ...pick(options, [ 'channelId', 'channelNameOneOf' ]) },
|
||||
query: { ...this.buildListQuery(options), ...pick(options, [ 'channelId', 'channelNameOneOf', 'includeCollaborations' ]) },
|
||||
implicitToken: true,
|
||||
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 */
|
||||
|
||||
import { FIXTURE_URLS } from '@tests/shared/fixture-urls.js'
|
||||
import { areHttpImportTestsDisabled } from '@peertube/peertube-node-utils'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { areHttpImportTestsDisabled } from '@peertube/peertube-node-utils'
|
||||
import {
|
||||
ChannelsCommand,
|
||||
cleanupTests,
|
||||
|
@ -11,6 +10,7 @@ import {
|
|||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { FIXTURE_URLS } from '@tests/shared/fixture-urls.js'
|
||||
|
||||
describe('Test videos import in a channel API validator', function () {
|
||||
let server: PeerTubeServer
|
||||
|
|
|
@ -186,7 +186,7 @@ describe('Test video lives API validator', function () {
|
|||
it('Should fail with a bad channel', async function () {
|
||||
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 () {
|
||||
|
@ -208,7 +208,7 @@ describe('Test video lives API validator', function () {
|
|||
|
||||
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 () {
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { omit } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, VideoChannelUpdate } from '@peertube/peertube-models'
|
||||
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
|
||||
import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
|
||||
import {
|
||||
ChannelsCommand,
|
||||
|
@ -16,6 +14,8 @@ import {
|
|||
PeerTubeServer,
|
||||
setAccessTokensToServers
|
||||
} 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 () {
|
||||
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 () {
|
||||
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 () {
|
||||
|
@ -245,7 +245,7 @@ describe('Test video imports API validator', function () {
|
|||
|
||||
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 () {
|
||||
|
|
|
@ -260,7 +260,7 @@ describe('Test video playlists API validator', 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.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 () {
|
||||
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.update(getUpdate(params, userPlaylist.shortUUID))
|
||||
|
|
|
@ -358,7 +358,11 @@ describe('Test videos API validator', function () {
|
|||
const fields = { ...baseCorrectParams, channelId: 545454 }
|
||||
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 () {
|
||||
|
@ -378,7 +382,8 @@ describe('Test videos API validator', function () {
|
|||
await checkUploadVideoParam({
|
||||
...baseOptions(),
|
||||
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 () {
|
||||
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 () {
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { wait } from '@peertube/peertube-core-utils'
|
||||
import { PluginType, UserNotification, UserNotificationType } from '@peertube/peertube-models'
|
||||
import { cleanupTests, PeerTubeServer } from '@peertube/peertube-server-commands'
|
||||
import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.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 { expect } from 'chai'
|
||||
|
||||
describe('Test admin notifications', function () {
|
||||
let server: PeerTubeServer
|
||||
|
|
|
@ -3,7 +3,10 @@
|
|||
import { UserNotification } from '@peertube/peertube-models'
|
||||
import { PeerTubeServer, cleanupTests, waitJobs } from '@peertube/peertube-server-commands'
|
||||
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'
|
||||
|
||||
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 { PeerTubeServer, cleanupTests, setDefaultAccountAvatar, waitJobs } from '@peertube/peertube-server-commands'
|
||||
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'
|
||||
|
||||
describe('Test comments notifications', function () {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import './admin-notifications.js'
|
||||
import './caption-notifications.js'
|
||||
import './channel-collaborators-notification.js'
|
||||
import './comments-notifications.js'
|
||||
import './moderation-notifications.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 { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
|
||||
import { MockInstancesIndex } from '@tests/shared/mock-servers/mock-instances-index.js'
|
||||
import { checkAutoInstanceFollowing, checkNewInstanceFollower } from '@tests/shared/notifications/check-follow-notifications.js'
|
||||
import {
|
||||
prepareNotificationsTest,
|
||||
CheckerBaseParams,
|
||||
checkNewVideoAbuseForModerators,
|
||||
checkNewCommentAbuseForModerators,
|
||||
checkNewAccountAbuseForModerators,
|
||||
checkAbuseStateChange,
|
||||
checkNewAbuseMessage,
|
||||
checkNewAccountAbuseForModerators,
|
||||
checkNewBlacklistOnMyVideo,
|
||||
checkNewInstanceFollower,
|
||||
checkAutoInstanceFollowing,
|
||||
checkVideoAutoBlacklistForModerators,
|
||||
checkMyVideoIsPublished,
|
||||
checkNewVideoFromSubscription
|
||||
} from '@tests/shared/notifications.js'
|
||||
checkNewCommentAbuseForModerators,
|
||||
checkNewVideoAbuseForModerators,
|
||||
checkVideoAutoBlacklistForModerators
|
||||
} from '@tests/shared/notifications/check-moderation-notifications.js'
|
||||
import { checkMyVideoIsPublished, checkNewVideoFromSubscription } 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'
|
||||
|
||||
describe('Test moderation notifications', function () {
|
||||
let servers: PeerTubeServer[] = []
|
||||
|
|
|
@ -1,15 +1,12 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { UserNotification, UserNotificationSettingValue, UserNotificationType } from '@peertube/peertube-models'
|
||||
import { cleanupTests, PeerTubeServer, waitJobs } from '@peertube/peertube-server-commands'
|
||||
import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
|
||||
import {
|
||||
prepareNotificationsTest,
|
||||
CheckerBaseParams,
|
||||
getAllNotificationsSettings,
|
||||
checkNewVideoFromSubscription
|
||||
} from '@tests/shared/notifications.js'
|
||||
import { checkNewVideoFromSubscription } from '@tests/shared/notifications/check-video-notifications.js'
|
||||
import { getAllNotificationsSettings, prepareNotificationsTest } from '@tests/shared/notifications/notifications-common.js'
|
||||
import { CheckerBaseParams } from '@tests/shared/notifications/shared/notification-checker.js'
|
||||
import { expect } from 'chai'
|
||||
|
||||
describe('Test notifications API', function () {
|
||||
let server: PeerTubeServer
|
||||
|
|
|
@ -3,7 +3,9 @@
|
|||
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 { 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 () {
|
||||
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 { FIXTURE_URLS } from '@tests/shared/fixture-urls.js'
|
||||
import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
|
||||
import { checkNewActorFollow } from '@tests/shared/notifications/check-follow-notifications.js'
|
||||
import {
|
||||
CheckerBaseParams,
|
||||
checkMyVideoImportIsFinished,
|
||||
checkMyVideoIsPublished,
|
||||
checkNewActorFollow,
|
||||
checkNewLiveFromSubscription,
|
||||
checkNewVideoFromSubscription,
|
||||
checkVideoStudioEditionIsFinished,
|
||||
prepareNotificationsTest,
|
||||
waitUntilNotification
|
||||
} from '@tests/shared/notifications.js'
|
||||
checkVideoStudioEditionIsFinished
|
||||
} from '@tests/shared/notifications/check-video-notifications.js'
|
||||
import { prepareNotificationsTest, waitUntilNotification } from '@tests/shared/notifications/notifications-common.js'
|
||||
import { CheckerBaseParams } from '@tests/shared/notifications/shared/notification-checker.js'
|
||||
import { uploadRandomVideoOnServers } from '@tests/shared/videos.js'
|
||||
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-static-file-privacy.js'
|
||||
import './video-storyboard.js'
|
||||
import './video-storyboard-remote-runner.js'
|
||||
import './video-transcription.js'
|
||||
import './videos-common-filters.js'
|
||||
import './videos-history.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 ' +
|
||||
'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' ])
|
||||
})
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
/* 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 { pathExists } from 'fs-extra/esm'
|
||||
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
|
||||
handle: string
|
||||
followers: number
|
||||
|
@ -18,7 +18,7 @@ async function expectChannelsFollows (options: {
|
|||
return expectActorFollow({ ...options, data })
|
||||
}
|
||||
|
||||
async function expectAccountFollows (options: {
|
||||
export async function expectAccountFollows (options: {
|
||||
server: PeerTubeServer
|
||||
handle: string
|
||||
followers: number
|
||||
|
@ -30,7 +30,7 @@ async function expectAccountFollows (options: {
|
|||
return expectActorFollow({ ...options, data })
|
||||
}
|
||||
|
||||
async function checkActorFilesWereRemoved (filename: string, server: PeerTubeServer) {
|
||||
export async function checkActorFilesWereRemoved (filename: string, server: PeerTubeServer) {
|
||||
for (const directory of [ 'avatars' ]) {
|
||||
const directoryPath = server.getDirectoryPath(directory)
|
||||
|
||||
|
@ -44,12 +44,22 @@ async function checkActorFilesWereRemoved (filename: string, server: PeerTubeSer
|
|||
}
|
||||
}
|
||||
|
||||
export {
|
||||
expectAccountFollows,
|
||||
expectChannelsFollows,
|
||||
checkActorFilesWereRemoved
|
||||
export async function checkActorImage (actor: AccountSummary | VideoChannelSummary) {
|
||||
expect(actor.avatars).to.have.lengthOf(4)
|
||||
|
||||
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: {
|
||||
|
|
|
@ -33,7 +33,7 @@ import { tmpdir } from 'os'
|
|||
import { basename, join, resolve } from 'path'
|
||||
import { testFileExistsOnFSOrNot } from './checks.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>>
|
||||
|
||||
|
|
|
@ -66,7 +66,7 @@ class MockSmtpServer {
|
|||
async kill () {
|
||||
if (!this.maildev) return
|
||||
|
||||
if (this.relayingEmail) {
|
||||
if (this.relayingEmail !== undefined) {
|
||||
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' ],
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkManage: false })),
|
||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkCanManage: false })),
|
||||
asyncMiddleware(accountController)
|
||||
)
|
||||
activityPubClientRouter.get(
|
||||
'/accounts?/:handle/followers',
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkManage: false })),
|
||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkCanManage: false })),
|
||||
asyncMiddleware(accountFollowersController)
|
||||
)
|
||||
activityPubClientRouter.get(
|
||||
'/accounts?/:handle/following',
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkManage: false })),
|
||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkCanManage: false })),
|
||||
asyncMiddleware(accountFollowingController)
|
||||
)
|
||||
activityPubClientRouter.get(
|
||||
'/accounts?/:handle/playlists',
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkManage: false })),
|
||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkCanManage: false })),
|
||||
asyncMiddleware(accountPlaylistsController)
|
||||
)
|
||||
activityPubClientRouter.get(
|
||||
|
@ -212,35 +212,35 @@ activityPubClientRouter.get(
|
|||
[ '/video-channels/:handle', '/video-channels/:handle/videos', '/c/:handle', '/c/:handle/videos' ],
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: false })),
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: false, checkIsOwner: false })),
|
||||
asyncMiddleware(videoChannelController)
|
||||
)
|
||||
activityPubClientRouter.get(
|
||||
'/video-channels/:handle/followers',
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: false })),
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: false, checkIsOwner: false })),
|
||||
asyncMiddleware(videoChannelFollowersController)
|
||||
)
|
||||
activityPubClientRouter.get(
|
||||
'/video-channels/:handle/following',
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: false })),
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: false, checkIsOwner: false })),
|
||||
asyncMiddleware(videoChannelFollowingController)
|
||||
)
|
||||
activityPubClientRouter.get(
|
||||
'/video-channels/:handle/playlists',
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: false })),
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: false, checkIsOwner: false })),
|
||||
asyncMiddleware(videoChannelPlaylistsController)
|
||||
)
|
||||
activityPubClientRouter.get(
|
||||
'/video-channels/:handle/player-settings',
|
||||
executeIfActivityPub,
|
||||
activityPubRateLimiter,
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: false })),
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: false, checkIsOwner: false })),
|
||||
asyncMiddleware(channelPlayerSettingsController)
|
||||
)
|
||||
|
||||
|
@ -462,7 +462,7 @@ async function videoCommentController (req: express.Request, res: express.Respon
|
|||
const videoComment = res.locals.videoCommentFull
|
||||
|
||||
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 })
|
||||
|
||||
|
@ -484,7 +484,7 @@ async function videoCommentController (req: express.Request, res: express.Respon
|
|||
async function videoCommentApprovedController (req: express.Request, res: express.Response) {
|
||||
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' })
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ inboxRouter.post(
|
|||
activityPubRateLimiter,
|
||||
signatureValidator,
|
||||
asyncMiddleware(checkSignature),
|
||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkManage: false })),
|
||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkCanManage: false })),
|
||||
asyncMiddleware(activityPubValidator),
|
||||
inboxController
|
||||
)
|
||||
|
@ -39,7 +39,7 @@ inboxRouter.post(
|
|||
activityPubRateLimiter,
|
||||
signatureValidator,
|
||||
asyncMiddleware(checkSignature),
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: false })),
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: false, checkIsOwner: false })),
|
||||
asyncMiddleware(activityPubValidator),
|
||||
inboxController
|
||||
)
|
||||
|
|
|
@ -23,7 +23,7 @@ outboxRouter.get(
|
|||
'/accounts/:handle/outbox',
|
||||
activityPubRateLimiter,
|
||||
apPaginationValidator,
|
||||
accountHandleGetValidatorFactory({ checkIsLocal: true, checkManage: false }),
|
||||
accountHandleGetValidatorFactory({ checkIsLocal: true, checkCanManage: false }),
|
||||
asyncMiddleware(outboxController)
|
||||
)
|
||||
|
||||
|
@ -31,7 +31,7 @@ outboxRouter.get(
|
|||
'/video-channels/:handle/outbox',
|
||||
activityPubRateLimiter,
|
||||
apPaginationValidator,
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: false })),
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: false, checkIsOwner: false })),
|
||||
asyncMiddleware(outboxController)
|
||||
)
|
||||
|
||||
|
|
|
@ -26,12 +26,16 @@ import {
|
|||
accountHandleGetValidatorFactory,
|
||||
accountsFollowersSortValidator,
|
||||
accountsSortValidator,
|
||||
listAccountChannelsValidator,
|
||||
videoChannelsSortValidator,
|
||||
videoChannelStatsValidator,
|
||||
videoChannelSyncsSortValidator,
|
||||
videosSortValidator
|
||||
} 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 { AccountModel } from '../../models/account/account.js'
|
||||
import { guessAdditionalAttributesFromQuery } from '../../models/video/formatter/index.js'
|
||||
|
@ -54,13 +58,13 @@ accountsRouter.get(
|
|||
|
||||
accountsRouter.get(
|
||||
'/:handle',
|
||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: false, checkManage: false })),
|
||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: false, checkCanManage: false })),
|
||||
getAccount
|
||||
)
|
||||
|
||||
accountsRouter.get(
|
||||
'/:handle/videos',
|
||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: false, checkManage: false })),
|
||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: false, checkCanManage: false })),
|
||||
paginationValidator,
|
||||
videosSortValidator,
|
||||
setDefaultVideosSort,
|
||||
|
@ -72,8 +76,8 @@ accountsRouter.get(
|
|||
|
||||
accountsRouter.get(
|
||||
'/:handle/video-channels',
|
||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: false, checkManage: false })),
|
||||
videoChannelStatsValidator,
|
||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: false, checkCanManage: false })),
|
||||
listAccountChannelsValidator,
|
||||
paginationValidator,
|
||||
videoChannelsSortValidator,
|
||||
setDefaultSort,
|
||||
|
@ -84,20 +88,21 @@ accountsRouter.get(
|
|||
accountsRouter.get(
|
||||
'/:handle/video-playlists',
|
||||
optionalAuthenticate,
|
||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: false, checkManage: false })),
|
||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: false, checkCanManage: false })),
|
||||
paginationValidator,
|
||||
videoPlaylistsSortValidator,
|
||||
setDefaultSort,
|
||||
setDefaultPagination,
|
||||
commonVideoPlaylistFiltersValidator,
|
||||
videoPlaylistsSearchValidator,
|
||||
videoPlaylistsAccountValidator,
|
||||
asyncMiddleware(listAccountPlaylists)
|
||||
)
|
||||
|
||||
accountsRouter.get(
|
||||
'/:handle/video-channel-syncs',
|
||||
authenticate,
|
||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkManage: true })),
|
||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkCanManage: true })),
|
||||
paginationValidator,
|
||||
videoChannelSyncsSortValidator,
|
||||
setDefaultSort,
|
||||
|
@ -108,7 +113,7 @@ accountsRouter.get(
|
|||
accountsRouter.get(
|
||||
'/:handle/ratings',
|
||||
authenticate,
|
||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkManage: true })),
|
||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkCanManage: true })),
|
||||
paginationValidator,
|
||||
videoRatesSortValidator,
|
||||
setDefaultSort,
|
||||
|
@ -120,7 +125,7 @@ accountsRouter.get(
|
|||
accountsRouter.get(
|
||||
'/:handle/followers',
|
||||
authenticate,
|
||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkManage: true })),
|
||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkCanManage: true })),
|
||||
paginationValidator,
|
||||
accountsFollowersSortValidator,
|
||||
setDefaultSort,
|
||||
|
@ -153,16 +158,15 @@ async function listAccounts (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,
|
||||
start: req.query.start,
|
||||
count: req.query.count,
|
||||
sort: req.query.sort,
|
||||
withStats: req.query.withStats,
|
||||
includeCollaborations: req.query.includeCollaborations,
|
||||
search: req.query.search
|
||||
}
|
||||
|
||||
const resultList = await VideoChannelModel.listByAccountForAPI(options)
|
||||
})
|
||||
|
||||
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) {
|
||||
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
|
||||
let listMyPlaylists = false
|
||||
|
@ -204,7 +208,9 @@ async function listAccountPlaylists (req: express.Request, res: express.Response
|
|||
sort: query.sort,
|
||||
search: query.search,
|
||||
|
||||
type: query.playlistType
|
||||
type: query.playlistType,
|
||||
|
||||
includeCollaborations: query.includeCollaborations
|
||||
})
|
||||
|
||||
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) {
|
||||
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 resultList = await ActorFollowModel.listFollowersForApi({
|
||||
|
|
|
@ -21,7 +21,7 @@ import { searchRouter } from './search/index.js'
|
|||
import { serverRouter } from './server/index.js'
|
||||
import { usersRouter } from './users/index.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 { videosRouter } from './videos/index.js'
|
||||
import { watchedWordsRouter } from './watched-words.js'
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
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 {
|
||||
getChannelPlayerSettingsValidator,
|
||||
|
@ -15,7 +16,6 @@ import {
|
|||
optionalAuthenticate,
|
||||
videoChannelsHandleValidatorFactory
|
||||
} from '../../middlewares/index.js'
|
||||
import { sendUpdateChannelPlayerSettings, sendUpdateVideoPlayerSettings } from '@server/lib/activitypub/send/send-update.js'
|
||||
|
||||
const playerSettingsRouter = express.Router()
|
||||
|
||||
|
@ -39,7 +39,7 @@ playerSettingsRouter.put(
|
|||
playerSettingsRouter.get(
|
||||
'/video-channels/:handle',
|
||||
optionalAuthenticate,
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: false, checkManage: false })),
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: false, checkCanManage: false, checkIsOwner: false })),
|
||||
getChannelPlayerSettingsValidator,
|
||||
asyncMiddleware(getChannelPlayerSettings)
|
||||
)
|
||||
|
@ -47,7 +47,7 @@ playerSettingsRouter.get(
|
|||
playerSettingsRouter.put(
|
||||
'/video-channels/:handle',
|
||||
authenticate,
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: true })),
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: true, checkIsOwner: false })),
|
||||
updatePlayerSettingsValidatorFactory('channel'),
|
||||
asyncMiddleware(updateChannelPlayerSettings)
|
||||
)
|
||||
|
|
|
@ -29,7 +29,8 @@ import { searchLocalUrl } from './shared/index.js'
|
|||
|
||||
const searchChannelsRouter = express.Router()
|
||||
|
||||
searchChannelsRouter.get('/video-channels',
|
||||
searchChannelsRouter.get(
|
||||
'/video-channels',
|
||||
openapiOperationDoc({ operationId: 'searchChannels' }),
|
||||
paginationValidator,
|
||||
setDefaultPagination,
|
||||
|
@ -102,7 +103,7 @@ async function searchVideoChannelsDB (query: VideoChannelsSearchQueryAfterSaniti
|
|||
}, 'filter:api.search.video-channels.local.list.params')
|
||||
|
||||
const resultList = await Hooks.wrapPromiseFun(
|
||||
VideoChannelModel.searchForApi.bind(VideoChannelModel),
|
||||
VideoChannelModel.listForApi.bind(VideoChannelModel),
|
||||
apiOptions,
|
||||
'filter:api.search.video-channels.local.list.result'
|
||||
)
|
||||
|
|
|
@ -56,10 +56,10 @@ const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_
|
|||
|
||||
const meRouter = express.Router()
|
||||
|
||||
meRouter.get('/me', authenticate, asyncMiddleware(getUserInformation))
|
||||
meRouter.get('/me', authenticate, asyncMiddleware(getMyInformation))
|
||||
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(
|
||||
'/me/videos/imports',
|
||||
|
@ -69,7 +69,7 @@ meRouter.get(
|
|||
setDefaultSort,
|
||||
setDefaultPagination,
|
||||
getMyVideoImportsValidator,
|
||||
asyncMiddleware(getUserVideoImports)
|
||||
asyncMiddleware(listMyVideoImports)
|
||||
)
|
||||
|
||||
meRouter.get(
|
||||
|
@ -92,14 +92,14 @@ meRouter.get(
|
|||
setDefaultPagination,
|
||||
commonVideosFiltersValidator,
|
||||
asyncMiddleware(usersVideosValidator),
|
||||
asyncMiddleware(listUserVideos)
|
||||
asyncMiddleware(listMyVideos)
|
||||
)
|
||||
|
||||
meRouter.get(
|
||||
'/me/videos/:videoId/rating',
|
||||
authenticate,
|
||||
asyncMiddleware(usersVideoRatingValidator),
|
||||
asyncMiddleware(getUserVideoRating)
|
||||
asyncMiddleware(getMyVideoRating)
|
||||
)
|
||||
|
||||
meRouter.put(
|
||||
|
@ -131,32 +131,36 @@ 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 countVideos = getCountVideos(req)
|
||||
const query = pickCommonVideoQuery(req.query)
|
||||
|
||||
const include = (query.include || VideoInclude.NONE) | VideoInclude.BLACKLISTED | VideoInclude.NOT_PUBLISHED_STATE
|
||||
|
||||
const apiOptions = await Hooks.wrapObject({
|
||||
privacyOneOf: getAllPrivacies(),
|
||||
const apiOptions = await Hooks.wrapObject(
|
||||
{
|
||||
privacyOneOf: getAllPrivacies(),
|
||||
|
||||
...query,
|
||||
...query,
|
||||
|
||||
// Display all
|
||||
nsfw: null,
|
||||
// Display all
|
||||
nsfw: null,
|
||||
|
||||
user,
|
||||
accountId: user.Account.id,
|
||||
displayOnlyForFollower: null,
|
||||
user,
|
||||
accountId: user.Account.id,
|
||||
displayOnlyForFollower: null,
|
||||
|
||||
videoChannelId: res.locals.videoChannel?.id,
|
||||
channelNameOneOf: req.query.channelNameOneOf,
|
||||
videoChannelId: res.locals.videoChannel?.id,
|
||||
channelNameOneOf: req.query.channelNameOneOf,
|
||||
includeCollaborations: req.query.includeCollaborations || false,
|
||||
|
||||
countVideos,
|
||||
countVideos,
|
||||
|
||||
include
|
||||
}, 'filter:api.user.me.videos.list.params')
|
||||
include
|
||||
} satisfies Parameters<typeof VideoModel.listForApi>[0],
|
||||
'filter:api.user.me.videos.list.params'
|
||||
)
|
||||
|
||||
const resultList = await Hooks.wrapPromiseFun(
|
||||
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) {
|
||||
const userAccount = res.locals.oauth.token.User.Account
|
||||
|
||||
const options = {
|
||||
const resultList = await VideoCommentModel.listCommentsForApi({
|
||||
...pick(req.query, [
|
||||
'start',
|
||||
'count',
|
||||
|
@ -182,14 +186,15 @@ async function listCommentsOnUserVideos (req: express.Request, res: express.Resp
|
|||
]),
|
||||
|
||||
autoTagOfAccountId: userAccount.id,
|
||||
|
||||
videoAccountOwnerId: userAccount.id,
|
||||
videoAccountOwnerIncludeCollaborations: req.query.includeCollaborations || false,
|
||||
|
||||
heldForReview: req.query.isHeldForReview,
|
||||
|
||||
videoChannelOwnerId: res.locals.videoChannel?.id,
|
||||
videoId: res.locals.videoAll?.id
|
||||
}
|
||||
|
||||
const resultList = await VideoCommentModel.listCommentsForApi(options)
|
||||
})
|
||||
|
||||
return res.json({
|
||||
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 resultList = await VideoImportModel.listUserVideoImportsForApi({
|
||||
userId: user.id,
|
||||
|
@ -208,7 +213,7 @@ async function getUserVideoImports (req: express.Request, res: express.Response)
|
|||
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
|
||||
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)
|
||||
}
|
||||
|
||||
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 videoQuotaUsed = await getOriginalVideoFileTotalFromUser(user)
|
||||
const videoQuotaUsedDaily = await getOriginalVideoFileTotalDailyFromUser(user)
|
||||
|
@ -233,7 +238,7 @@ async function getUserVideoQuotaUsed (req: express.Request, res: express.Respons
|
|||
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 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 { MChannelBannerAccountDefault } from '@server/types/models/index.js'
|
||||
import express from 'express'
|
||||
import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger.js'
|
||||
import { resetSequelizeInstance } from '../../helpers/database-utils.js'
|
||||
import { buildNSFWFilters, createReqFiles, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils.js'
|
||||
import { logger } from '../../helpers/logger.js'
|
||||
import { getFormattedObjects } from '../../helpers/utils.js'
|
||||
import { MIMETYPES } from '../../initializers/constants.js'
|
||||
import { sequelizeTypescript } from '../../initializers/database.js'
|
||||
import { sendUpdateActor } from '../../lib/activitypub/send/index.js'
|
||||
import { JobQueue } from '../../lib/job-queue/index.js'
|
||||
import { deleteLocalActorImageFile, updateLocalActorImageFiles } from '../../lib/local-actor.js'
|
||||
import { createLocalVideoChannelWithoutKeys, federateAllVideosOfChannel } from '../../lib/video-channel.js'
|
||||
import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../../helpers/audit-logger.js'
|
||||
import { resetSequelizeInstance } from '../../../helpers/database-utils.js'
|
||||
import { buildNSFWFilters, createReqFiles, getCountVideos, isUserAbleToSearchRemoteURI } from '../../../helpers/express-utils.js'
|
||||
import { logger } from '../../../helpers/logger.js'
|
||||
import { getFormattedObjects } from '../../../helpers/utils.js'
|
||||
import { MIMETYPES } from '../../../initializers/constants.js'
|
||||
import { sequelizeTypescript } from '../../../initializers/database.js'
|
||||
import { sendUpdateActor } from '../../../lib/activitypub/send/index.js'
|
||||
import { JobQueue } from '../../../lib/job-queue/index.js'
|
||||
import { deleteLocalActorImageFile, updateLocalActorImageFiles } from '../../../lib/local-actor.js'
|
||||
import { createLocalVideoChannelWithoutKeys, federateAllVideosOfChannel } from '../../../lib/video-channel.js'
|
||||
import {
|
||||
apiRateLimiter,
|
||||
asyncMiddleware,
|
||||
|
@ -41,8 +41,8 @@ import {
|
|||
videoChannelsSortValidator,
|
||||
videoChannelsUpdateValidator,
|
||||
videoPlaylistsSortValidator
|
||||
} from '../../middlewares/index.js'
|
||||
import { updateAvatarValidator, updateBannerValidator } from '../../middlewares/validators/actor-image.js'
|
||||
} from '../../../middlewares/index.js'
|
||||
import { updateAvatarValidator, updateBannerValidator } from '../../../middlewares/validators/actor-image.js'
|
||||
import {
|
||||
ensureChannelOwnerCanUpload,
|
||||
videoChannelImportVideosValidator,
|
||||
|
@ -50,16 +50,17 @@ import {
|
|||
videoChannelsHandleValidatorFactory,
|
||||
videoChannelsListValidator,
|
||||
videosSortValidator
|
||||
} from '../../middlewares/validators/index.js'
|
||||
} from '../../../middlewares/validators/index.js'
|
||||
import {
|
||||
commonVideoPlaylistFiltersValidator,
|
||||
videoPlaylistsReorderInChannelValidator
|
||||
} from '../../middlewares/validators/videos/video-playlists.js'
|
||||
import { AccountModel } from '../../models/account/account.js'
|
||||
import { guessAdditionalAttributesFromQuery } from '../../models/video/formatter/index.js'
|
||||
import { VideoChannelModel } from '../../models/video/video-channel.js'
|
||||
import { VideoPlaylistModel } from '../../models/video/video-playlist.js'
|
||||
import { VideoModel } from '../../models/video/video.js'
|
||||
} from '../../../middlewares/validators/videos/video-playlists.js'
|
||||
import { AccountModel } from '../../../models/account/account.js'
|
||||
import { guessAdditionalAttributesFromQuery } from '../../../models/video/formatter/index.js'
|
||||
import { VideoChannelModel } from '../../../models/video/video-channel.js'
|
||||
import { VideoPlaylistModel } from '../../../models/video/video-playlist.js'
|
||||
import { VideoModel } from '../../../models/video/video.js'
|
||||
import { channelCollaborators } from './video-channel-collaborators.js'
|
||||
|
||||
const auditLogger = auditLoggerFactory('channels')
|
||||
const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT)
|
||||
|
@ -68,6 +69,7 @@ const reqBannerFile = createReqFiles([ 'bannerfile' ], MIMETYPES.IMAGE.MIMETYPE_
|
|||
const videoChannelRouter = express.Router()
|
||||
|
||||
videoChannelRouter.use(apiRateLimiter)
|
||||
videoChannelRouter.use(channelCollaborators)
|
||||
|
||||
videoChannelRouter.get(
|
||||
'/',
|
||||
|
@ -85,7 +87,7 @@ videoChannelRouter.post(
|
|||
'/:handle/avatar/pick',
|
||||
authenticate,
|
||||
reqAvatarFile,
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: true })),
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: true, checkIsOwner: false })),
|
||||
updateAvatarValidator,
|
||||
asyncMiddleware(updateVideoChannelAvatar)
|
||||
)
|
||||
|
@ -94,7 +96,7 @@ videoChannelRouter.post(
|
|||
'/:handle/banner/pick',
|
||||
authenticate,
|
||||
reqBannerFile,
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: true })),
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: true, checkIsOwner: false })),
|
||||
updateBannerValidator,
|
||||
asyncMiddleware(updateVideoChannelBanner)
|
||||
)
|
||||
|
@ -102,21 +104,21 @@ videoChannelRouter.post(
|
|||
videoChannelRouter.delete(
|
||||
'/:handle/avatar',
|
||||
authenticate,
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: true })),
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: true, checkIsOwner: false })),
|
||||
asyncMiddleware(deleteVideoChannelAvatar)
|
||||
)
|
||||
|
||||
videoChannelRouter.delete(
|
||||
'/:handle/banner',
|
||||
authenticate,
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: true })),
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: true, checkIsOwner: false })),
|
||||
asyncMiddleware(deleteVideoChannelBanner)
|
||||
)
|
||||
|
||||
videoChannelRouter.put(
|
||||
'/:handle',
|
||||
authenticate,
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: true })),
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: true, checkIsOwner: false })),
|
||||
videoChannelsUpdateValidator,
|
||||
asyncRetryTransactionMiddleware(updateVideoChannel)
|
||||
)
|
||||
|
@ -124,14 +126,14 @@ videoChannelRouter.put(
|
|||
videoChannelRouter.delete(
|
||||
'/:handle',
|
||||
authenticate,
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: true })),
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: true, checkIsOwner: true })),
|
||||
asyncMiddleware(videoChannelsRemoveValidator),
|
||||
asyncRetryTransactionMiddleware(removeVideoChannel)
|
||||
)
|
||||
|
||||
videoChannelRouter.get(
|
||||
'/:handle',
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: false, checkManage: false })),
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: false, checkCanManage: false, checkIsOwner: false })),
|
||||
asyncMiddleware(getVideoChannel)
|
||||
)
|
||||
|
||||
|
@ -140,7 +142,7 @@ videoChannelRouter.get(
|
|||
videoChannelRouter.get(
|
||||
'/:handle/video-playlists',
|
||||
optionalAuthenticate,
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: false, checkManage: false })),
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: false, checkCanManage: false, checkIsOwner: false })),
|
||||
paginationValidator,
|
||||
videoPlaylistsSortValidator,
|
||||
setDefaultSort,
|
||||
|
@ -152,7 +154,7 @@ videoChannelRouter.get(
|
|||
videoChannelRouter.post(
|
||||
'/:handle/video-playlists/reorder',
|
||||
authenticate,
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: true })),
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: true, checkIsOwner: false })),
|
||||
asyncMiddleware(videoPlaylistsReorderInChannelValidator),
|
||||
asyncRetryTransactionMiddleware(reorderPlaylistsInChannel)
|
||||
)
|
||||
|
@ -161,7 +163,7 @@ videoChannelRouter.post(
|
|||
|
||||
videoChannelRouter.get(
|
||||
'/:handle/videos',
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: false, checkManage: false })),
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: false, checkCanManage: false, checkIsOwner: false })),
|
||||
paginationValidator,
|
||||
videosSortValidator,
|
||||
setDefaultVideosSort,
|
||||
|
@ -174,7 +176,7 @@ videoChannelRouter.get(
|
|||
videoChannelRouter.get(
|
||||
'/:handle/followers',
|
||||
authenticate,
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: false, checkManage: true })),
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: false, checkCanManage: true, checkIsOwner: false })),
|
||||
paginationValidator,
|
||||
videoChannelsFollowersSortValidator,
|
||||
setDefaultSort,
|
||||
|
@ -185,7 +187,7 @@ videoChannelRouter.get(
|
|||
videoChannelRouter.post(
|
||||
'/:handle/import-videos',
|
||||
authenticate,
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: true })),
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: true, checkIsOwner: false })),
|
||||
asyncMiddleware(videoChannelImportVideosValidator),
|
||||
asyncMiddleware(ensureChannelOwnerCanUpload),
|
||||
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,
|
||||
setDefaultPagination,
|
||||
optionalAuthenticate,
|
||||
asyncMiddleware(getVideoPlaylistVideos)
|
||||
asyncMiddleware(listVideosOfPlaylist)
|
||||
)
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
async function getVideoPlaylistVideos (req: express.Request, res: express.Response) {
|
||||
async function listVideosOfPlaylist (req: express.Request, res: express.Response) {
|
||||
const videoPlaylistInstance = res.locals.videoPlaylistSummary
|
||||
const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
|
||||
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 { LocalVideoCreator } from '@server/lib/local-video-creator.js'
|
||||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||
import { checkCanManageVideo } from '@server/middlewares/validators/shared/videos.js'
|
||||
import {
|
||||
videoLiveAddValidator,
|
||||
videoLiveFindReplaySessionValidator,
|
||||
|
@ -44,21 +45,30 @@ liveRouter.get(
|
|||
'/live/:videoId/sessions',
|
||||
authenticate,
|
||||
asyncMiddleware(videoLiveGetValidator),
|
||||
videoLiveListSessionsValidator,
|
||||
asyncMiddleware(videoLiveListSessionsValidator),
|
||||
asyncMiddleware(getLiveVideoSessions)
|
||||
)
|
||||
|
||||
liveRouter.get('/live/:videoId', optionalAuthenticate, asyncMiddleware(videoLiveGetValidator), getLiveVideo)
|
||||
liveRouter.get(
|
||||
'/live/:videoId',
|
||||
optionalAuthenticate,
|
||||
asyncMiddleware(videoLiveGetValidator),
|
||||
asyncMiddleware(getLiveVideo)
|
||||
)
|
||||
|
||||
liveRouter.put(
|
||||
'/live/:videoId',
|
||||
authenticate,
|
||||
asyncMiddleware(videoLiveGetValidator),
|
||||
videoLiveUpdateValidator,
|
||||
asyncMiddleware(videoLiveUpdateValidator),
|
||||
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
|
||||
|
||||
return res.json(videoLive.toFormattedJSON(canSeePrivateLiveInformation(res)))
|
||||
return res.json(videoLive.toFormattedJSON(await canSeePrivateLiveInformation(req, res)))
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
function canSeePrivateLiveInformation (res: express.Response) {
|
||||
const user = res.locals.oauth?.token.User
|
||||
if (!user) return false
|
||||
|
||||
if (user.hasRight(UserRight.GET_ANY_LIVE)) return true
|
||||
|
||||
const video = res.locals.videoAll
|
||||
return video.VideoChannel.Account.userId === user.id
|
||||
function canSeePrivateLiveInformation (req: express.Request, res: express.Response) {
|
||||
return checkCanManageVideo({
|
||||
user: res.locals.oauth?.token.User,
|
||||
video: res.locals.videoAll,
|
||||
right: UserRight.GET_ANY_LIVE,
|
||||
req,
|
||||
res: null,
|
||||
checkIsLocal: true,
|
||||
checkIsOwner: false
|
||||
})
|
||||
}
|
||||
|
||||
async function updateLiveVideo (req: express.Request, res: express.Response) {
|
||||
|
|
|
@ -12,7 +12,7 @@ servicesRouter.use('/oembed', cors(), apiRateLimiter, asyncMiddleware(oembedVali
|
|||
servicesRouter.use(
|
||||
'/redirect/accounts/:handle',
|
||||
apiRateLimiter,
|
||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: false, checkManage: false })),
|
||||
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: false, checkCanManage: false })),
|
||||
redirectToAccountUrl
|
||||
)
|
||||
|
||||
|
|
|
@ -1,20 +1,25 @@
|
|||
import { Response } from 'express'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { MUserId } from '@server/types/models/index.js'
|
||||
import { HttpStatusCode, UserRight } from '@peertube/peertube-models'
|
||||
import { checkCanManageAccount } from '@server/middlewares/validators/shared/users.js'
|
||||
import { MUserAccountId } from '@server/types/models/index.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) {
|
||||
if (videoChangeOwnership.NextOwner.userId === user.id) {
|
||||
return true
|
||||
export function checkCanTerminateOwnershipChange (options: {
|
||||
user: MUserAccountId
|
||||
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({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: req.t('Cannot terminate an ownership change of another user')
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Cannot terminate an ownership change of another user'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
export {
|
||||
checkUserCanTerminateOwnershipChange
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import { REMOTE_SCHEME, WEBSERVER } from '../../initializers/constants.js'
|
|||
import { sanitizeHost } from '../core-utils.js'
|
||||
import { exists } from './misc.js'
|
||||
|
||||
function isWebfingerLocalResourceValid (value: string) {
|
||||
export function isWebfingerLocalResourceValid (value: string) {
|
||||
if (!exists(value)) return false
|
||||
if (value.startsWith('acct:') === false) return false
|
||||
|
||||
|
@ -13,9 +13,3 @@ function isWebfingerLocalResourceValid (value: string) {
|
|||
const host = actorParts[1]
|
||||
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 { 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>,
|
||||
arg1: A,
|
||||
arg2: B,
|
||||
|
@ -13,29 +13,29 @@ function retryTransactionWrapper<T, A, B, C, D> (
|
|||
arg4: D
|
||||
): Promise<T>
|
||||
|
||||
function retryTransactionWrapper<T, A, B, C> (
|
||||
export function retryTransactionWrapper<T, A, B, C> (
|
||||
functionToRetry: (arg1: A, arg2: B, arg3: C) => Promise<T>,
|
||||
arg1: A,
|
||||
arg2: B,
|
||||
arg3: C
|
||||
): Promise<T>
|
||||
|
||||
function retryTransactionWrapper<T, A, B> (
|
||||
export function retryTransactionWrapper<T, A, B> (
|
||||
functionToRetry: (arg1: A, arg2: B) => Promise<T>,
|
||||
arg1: A,
|
||||
arg2: B
|
||||
): Promise<T>
|
||||
|
||||
function retryTransactionWrapper<T, A> (
|
||||
export function retryTransactionWrapper<T, A> (
|
||||
functionToRetry: (arg1: A) => Promise<T>,
|
||||
arg1: A
|
||||
): Promise<T>
|
||||
|
||||
function retryTransactionWrapper<T> (
|
||||
export function retryTransactionWrapper<T> (
|
||||
functionToRetry: () => Promise<T> | Bluebird<T>
|
||||
): Promise<T>
|
||||
|
||||
function retryTransactionWrapper<T> (
|
||||
export function retryTransactionWrapper<T> (
|
||||
functionToRetry: (...args: any[]) => Promise<T>,
|
||||
...args: any[]
|
||||
): 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) => {
|
||||
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() || []
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
function filterNonExistingModels<T extends { hasSameUniqueKeysThan(other: T): boolean }> (
|
||||
export function filterNonExistingModels<T extends { hasSameUniqueKeysThan(other: T): boolean }> (
|
||||
fromDatabase: T[],
|
||||
newModels: T[]
|
||||
) {
|
||||
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 })))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
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 }
|
||||
|
||||
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())
|
||||
|
||||
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
|
||||
|
||||
try {
|
||||
if (!this.initReadersPromise) this.initReadersPromise = this.initReadersIfNeeded()
|
||||
if (this.initReadersPromise === undefined) {
|
||||
this.initReadersPromise = this.initReadersIfNeeded()
|
||||
}
|
||||
|
||||
await this.initReadersPromise
|
||||
this.initReadersPromise = undefined
|
||||
|
||||
|
|
|
@ -3,13 +3,13 @@ import { WEBSERVER } from '@server/initializers/constants.js'
|
|||
import { actorNameAlphabet } from './custom-validators/activitypub/actor.js'
|
||||
import { regexpCapture } from './regexp.js'
|
||||
|
||||
export function extractMentions (text: string, isOwned: boolean) {
|
||||
export function extractMentions (text: string, isLocal: boolean) {
|
||||
let result: string[] = []
|
||||
|
||||
const localMention = `@(${actorNameAlphabet}+)`
|
||||
const remoteMention = `${localMention}@${WEBSERVER.HOST}`
|
||||
|
||||
const mentionRegex = isOwned
|
||||
const mentionRegex = isLocal
|
||||
? '(?:(?:' + remoteMention + ')|(?:' + localMention + '))' // Include local mentions?
|
||||
: '(?:' + remoteMention + ')'
|
||||
|
||||
|
@ -20,16 +20,14 @@ export function extractMentions (text: string, isOwned: boolean) {
|
|||
result = result.concat(
|
||||
regexpCapture(text, firstMentionRegex)
|
||||
.map(([ , username1, username2 ]) => username1 || username2),
|
||||
|
||||
regexpCapture(text, endMentionRegex)
|
||||
.map(([ , username1, username2 ]) => username1 || username2),
|
||||
|
||||
regexpCapture(text, remoteMentionsRegex)
|
||||
.map(([ , username ]) => username)
|
||||
)
|
||||
|
||||
// Include local mentions
|
||||
if (isOwned) {
|
||||
if (isLocal) {
|
||||
const localMentionsRegex = new RegExp(' ' + localMention + ' ', 'g')
|
||||
|
||||
result = result.concat(
|
||||
|
|
|
@ -20,6 +20,8 @@ import {
|
|||
UserImportStateType,
|
||||
UserRegistrationState,
|
||||
UserRegistrationStateType,
|
||||
VideoChannelCollaboratorState,
|
||||
VideoChannelCollaboratorStateType,
|
||||
VideoChannelSyncState,
|
||||
VideoChannelSyncStateType,
|
||||
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'
|
||||
}
|
||||
|
||||
export const CHANNEL_COLLABORATOR_STATE: { [id in VideoChannelCollaboratorStateType]: string } = {
|
||||
[VideoChannelCollaboratorState.ACCEPTED]: 'Accepted',
|
||||
[VideoChannelCollaboratorState.PENDING]: 'Pending'
|
||||
}
|
||||
|
||||
export const MIMETYPES = {
|
||||
AUDIO: {
|
||||
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 { UserVideoHistoryModel } from '@server/models/user/user-video-history.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 { VideoChannelCollaboratorModel } from '@server/models/video/video-channel-collaborator.js'
|
||||
import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync.js'
|
||||
import { VideoChapterModel } from '@server/models/video/video-chapter.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 { VideoViewModel } from '../models/view/video-view.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
|
||||
|
||||
|
@ -191,7 +192,8 @@ export async function initDatabaseModels (silent: boolean) {
|
|||
AccountAutomaticTagPolicyModel,
|
||||
UploadImageModel,
|
||||
VideoLiveScheduleModel,
|
||||
PlayerSettingModel
|
||||
PlayerSettingModel,
|
||||
VideoChannelCollaboratorModel
|
||||
])
|
||||
|
||||
// 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 { VideoPlaylistModel } from '@server/models/video/video-playlist.js'
|
||||
import { FilteredModelAttributes } from '@server/types/index.js'
|
||||
import {
|
||||
MAccountHost,
|
||||
MThumbnail,
|
||||
MVideoPlaylist,
|
||||
MVideoPlaylistFull,
|
||||
MVideoPlaylistVideosLength
|
||||
} from '@server/types/models/index.js'
|
||||
import { MAccountHost, MThumbnail, MVideoPlaylist, MVideoPlaylistFull, MVideoPlaylistVideosLength } from '@server/types/models/index.js'
|
||||
import Bluebird from 'bluebird'
|
||||
import { getAPId } from '../activity.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')
|
||||
|
||||
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 => {
|
||||
if (!checkUrlsSameHost(playlistUrl, account.Actor.url)) {
|
||||
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}`)
|
||||
}
|
||||
|
||||
logger.debug(`Creating or updating playlist ${playlistObject.id}`, lTags(playlistObject.id))
|
||||
|
||||
const playlistAttributes = playlistObjectToDBAttributes(playlistObject, to || playlistObject.to)
|
||||
|
||||
const channel = await getRemotePlaylistChannel(playlistObject)
|
||||
|
|
|
@ -51,7 +51,7 @@ async function processVideoShare (actorAnnouncer: MActorSignature, activity: Act
|
|||
transaction: t
|
||||
})
|
||||
|
||||
if (video.isOwned() && created === true) {
|
||||
if (video.isLocal() && created === true) {
|
||||
// Don't resend the activity to the sender
|
||||
const exceptions = [ actorAnnouncer ]
|
||||
|
||||
|
|
|
@ -91,7 +91,7 @@ async function processCreateCacheFile (
|
|||
|
||||
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`)
|
||||
return
|
||||
}
|
||||
|
@ -100,7 +100,7 @@ async function processCreateCacheFile (
|
|||
return createOrUpdateCacheFile(cacheFile, video, byActor, t)
|
||||
})
|
||||
|
||||
if (video.isOwned()) {
|
||||
if (video.isLocal()) {
|
||||
// Don't resend the activity to the sender
|
||||
const exceptions = [ byActor ]
|
||||
await forwardVideoRelatedActivity(activity, undefined, exceptions, video)
|
||||
|
@ -152,7 +152,7 @@ async function processCreateVideoComment (
|
|||
}
|
||||
|
||||
// Try to not forward unwanted comments on our videos
|
||||
if (video.isOwned()) {
|
||||
if (video.isLocal()) {
|
||||
if (!canVideoBeFederated(video)) {
|
||||
logger.info('Skip comment forward on non federated video' + video.url)
|
||||
return
|
||||
|
|
|
@ -55,7 +55,7 @@ async function processDeleteActivity (options: APProcessorOptions<ActivityDelete
|
|||
{
|
||||
const videoInstance = await VideoModel.loadByUrlAndPopulateAccountAndFiles(objectUrl)
|
||||
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)
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ async function processDeleteActivity (options: APProcessorOptions<ActivityDelete
|
|||
{
|
||||
const videoPlaylist = await VideoPlaylistModel.loadByUrlAndPopulateAccount(objectUrl)
|
||||
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)
|
||||
}
|
||||
|
@ -144,7 +144,7 @@ function processDeleteVideoComment (byActor: MActorSignature, videoComment: MCom
|
|||
|
||||
await videoComment.save({ transaction: t })
|
||||
|
||||
if (videoComment.Video.isOwned()) {
|
||||
if (videoComment.Video.isLocal()) {
|
||||
// Don't resend the activity to the sender
|
||||
const exceptions = [ byActor ]
|
||||
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)
|
||||
|
||||
const { video: onlyVideo } = await maybeGetOrCreateAPVideo({ videoObject: videoUrl, fetchType: 'only-video-and-blacklist' })
|
||||
if (!onlyVideo?.isOwned()) return
|
||||
if (!onlyVideo?.isLocal()) return
|
||||
|
||||
if (!canVideoBeFederated(onlyVideo)) {
|
||||
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)
|
||||
|
||||
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 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)
|
||||
|
||||
const { video: onlyVideo } = await maybeGetOrCreateAPVideo({ videoObject: videoUrl, fetchType: 'only-video-and-blacklist' })
|
||||
if (!onlyVideo?.isOwned()) return
|
||||
if (!onlyVideo?.isLocal()) return
|
||||
|
||||
if (!canVideoBeFederated(onlyVideo)) {
|
||||
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`)
|
||||
}
|
||||
|
||||
if (comment.isOwned() !== true) {
|
||||
if (comment.isLocal() !== true) {
|
||||
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 { video: onlyVideo } = await maybeGetOrCreateAPVideo({ videoObject: likeActivity.object })
|
||||
if (!onlyVideo?.isOwned()) return
|
||||
if (!onlyVideo?.isLocal()) return
|
||||
|
||||
return sequelizeTypescript.transaction(async t => {
|
||||
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 { video: onlyVideo } = await maybeGetOrCreateAPVideo({ videoObject: dislikeActivity.object })
|
||||
if (!onlyVideo?.isOwned()) return
|
||||
if (!onlyVideo?.isLocal()) return
|
||||
|
||||
return sequelizeTypescript.transaction(async t => {
|
||||
if (!byActor.Account) throw new Error('Unknown account ' + byActor.url)
|
||||
|
@ -132,7 +132,7 @@ async function processUndoCacheFile (
|
|||
|
||||
await cacheFile.destroy({ transaction: t })
|
||||
|
||||
if (video.isOwned()) {
|
||||
if (video.isLocal()) {
|
||||
// Don't resend the activity to the sender
|
||||
const exceptions = [ byActor ]
|
||||
|
||||
|
@ -153,7 +153,7 @@ function processUndoAnnounce (byActor: MActorSignature, announceActivity: Activi
|
|||
|
||||
await share.destroy({ transaction: t })
|
||||
|
||||
if (share.Video.isOwned()) {
|
||||
if (share.Video.isLocal()) {
|
||||
// Don't resend the activity to the sender
|
||||
const exceptions = [ byActor ]
|
||||
|
||||
|
|
|
@ -103,7 +103,7 @@ async function processUpdateCacheFile (
|
|||
|
||||
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`)
|
||||
return
|
||||
}
|
||||
|
@ -112,7 +112,7 @@ async function processUpdateCacheFile (
|
|||
await createOrUpdateCacheFile(cacheFileObject, video, byActor, t)
|
||||
})
|
||||
|
||||
if (video.isOwned()) {
|
||||
if (video.isLocal()) {
|
||||
// Don't resend the activity to the sender
|
||||
const exceptions = [ byActor ]
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ async function processCreateView (activity: ActivityView, byActor: MActorSignatu
|
|||
viewerResultCounter: getViewerResultCounter(activity)
|
||||
})
|
||||
|
||||
if (video.isOwned()) {
|
||||
if (video.isLocal()) {
|
||||
// Forward the view but don't resend the activity to the sender
|
||||
const exceptions = [ byActor ]
|
||||
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) {
|
||||
const isOrigin = comment.Video.isOwned()
|
||||
const isOrigin = comment.Video.isLocal()
|
||||
|
||||
if (isOrigin) {
|
||||
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) {
|
||||
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 videoAccount = await AccountModel.load(videoComment.Video.VideoChannel.Account.id, transaction)
|
||||
|
||||
const byActor = videoComment.isOwned()
|
||||
const byActor = videoComment.isLocal()
|
||||
? videoComment.Account.Actor
|
||||
: videoAccount.Actor
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAud
|
|||
const { byActor, video, transaction, contextType, parallelizable } = options
|
||||
|
||||
// Send to origin
|
||||
if (video.isOwned() === false) {
|
||||
if (video.isLocal() === false) {
|
||||
return sendVideoActivityToOrigin(activityBuilder, options)
|
||||
}
|
||||
|
||||
|
@ -62,7 +62,7 @@ async function sendVideoActivityToOrigin (activityBuilder: (audience: ActivityAu
|
|||
}) {
|
||||
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
|
||||
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 { 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')
|
||||
}
|
||||
|
||||
|
@ -169,7 +169,7 @@ async function getAutomaticTagsAndAssignReview (comment: MComment, video: MVideo
|
|||
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
|
||||
if (video.isOwned() || comment.isOwned()) {
|
||||
if (video.isLocal() || comment.isLocal()) {
|
||||
comment.heldForReview = await shouldCommentBeHeldForReview({ user: null, video, automaticTags })
|
||||
} else {
|
||||
comment.heldForReview = false
|
||||
|
|
|
@ -13,7 +13,7 @@ async function sendVideoRateChange (
|
|||
dislikes: number,
|
||||
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)
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ async function sendVideoRateChangeToOrigin (
|
|||
t: Transaction
|
||||
) {
|
||||
// Local video, we don't need to send like
|
||||
if (video.isOwned()) return
|
||||
if (video.isLocal()) return
|
||||
|
||||
const actor = account.Actor
|
||||
|
||||
|
|
|
@ -490,7 +490,7 @@ export class Emailer {
|
|||
}
|
||||
|
||||
private initHandlebarsIfNeeded () {
|
||||
if (this.registeringHandlebars) return this.registeringHandlebars
|
||||
if (this.registeringHandlebars !== undefined) return this.registeringHandlebars
|
||||
|
||||
this.registeringHandlebars = this._initHandlebarsIfNeeded()
|
||||
|
||||
|
|
|
@ -12,13 +12,13 @@ type ImageModel = {
|
|||
filename: string
|
||||
onDisk: boolean
|
||||
|
||||
isOwned (): boolean
|
||||
getPath (): string
|
||||
isLocal(): boolean
|
||||
getPath(): string
|
||||
|
||||
save (): Promise<Model>
|
||||
save(): Promise<Model>
|
||||
}
|
||||
|
||||
export abstract class AbstractPermanentFileCache <M extends ImageModel> {
|
||||
export abstract class AbstractPermanentFileCache<M extends ImageModel> {
|
||||
// Unsafe because it can return paths that do not exist anymore
|
||||
private readonly filenameToPathUnsafeCache = new LRUCache<string, string>({
|
||||
max: LRU_CACHE.FILENAME_TO_PATH_PERMANENT_FILE_CACHE.MAX_SIZE
|
||||
|
@ -28,7 +28,6 @@ export abstract class AbstractPermanentFileCache <M extends ImageModel> {
|
|||
protected abstract loadModel (filename: string): Promise<M>
|
||||
|
||||
constructor (private readonly directory: string) {
|
||||
|
||||
}
|
||||
|
||||
async lazyServe (options: {
|
||||
|
@ -102,7 +101,7 @@ export abstract class AbstractPermanentFileCache <M extends ImageModel> {
|
|||
const { err, image, filename, next } = options
|
||||
|
||||
// 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 })
|
||||
|
||||
this.filenameToPathUnsafeCache.delete(filename)
|
||||
|
|
|
@ -2,10 +2,9 @@ import { remove } from 'fs-extra/esm'
|
|||
import { logger } from '../../../helpers/logger.js'
|
||||
import memoizee from 'memoizee'
|
||||
|
||||
type GetFilePathResult = { isOwned: boolean, path: string, downloadName?: string } | undefined
|
||||
|
||||
export abstract class AbstractSimpleFileCache <T> {
|
||||
type GetFilePathResult = { isLocal: boolean, path: string, downloadName?: string } | undefined
|
||||
|
||||
export abstract class AbstractSimpleFileCache<T> {
|
||||
getFilePath: (params: T) => Promise<GetFilePathResult>
|
||||
|
||||
abstract getFilePathImpl (params: T): Promise<GetFilePathResult>
|
||||
|
@ -19,7 +18,7 @@ export abstract class AbstractSimpleFileCache <T> {
|
|||
max,
|
||||
promise: true,
|
||||
dispose: (result?: GetFilePathResult) => {
|
||||
if (result && result.isOwned !== true) {
|
||||
if (result && result.isLocal !== true) {
|
||||
remove(result.path)
|
||||
.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 }))
|
||||
|
|
|
@ -21,8 +21,8 @@ class VideoCaptionsSimpleFileCache extends AbstractSimpleFileCache<string> {
|
|||
const videoCaption = await VideoCaptionModel.loadWithVideoByFilename(filename)
|
||||
if (!videoCaption) return undefined
|
||||
|
||||
if (videoCaption.isOwned()) {
|
||||
return { isOwned: true, path: videoCaption.getFSFilePath() }
|
||||
if (videoCaption.isLocal()) {
|
||||
return { isLocal: true, path: videoCaption.getFSFilePath() }
|
||||
}
|
||||
|
||||
return this.loadRemoteFile(filename)
|
||||
|
@ -33,7 +33,7 @@ class VideoCaptionsSimpleFileCache extends AbstractSimpleFileCache<string> {
|
|||
const videoCaption = await VideoCaptionModel.loadWithVideoByFilename(key)
|
||||
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
|
||||
const video = await VideoModel.loadFull(videoCaption.videoId)
|
||||
|
@ -45,7 +45,7 @@ class VideoCaptionsSimpleFileCache extends AbstractSimpleFileCache<string> {
|
|||
try {
|
||||
await doRequestAndSaveToFile(remoteUrl, destPath)
|
||||
|
||||
return { isOwned: false, path: destPath }
|
||||
return { isLocal: false, path: destPath }
|
||||
} catch (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 { FILES_CACHE } from '../../initializers/constants.js'
|
||||
import { VideoModel } from '../../models/video/video.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 constructor () {
|
||||
|
@ -23,7 +22,7 @@ class VideoPreviewsSimpleFileCache extends AbstractSimpleFileCache <string> {
|
|||
const thumbnail = await ThumbnailModel.loadWithVideoByFilename(filename, ThumbnailType.PREVIEW)
|
||||
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)
|
||||
}
|
||||
|
@ -33,7 +32,7 @@ class VideoPreviewsSimpleFileCache extends AbstractSimpleFileCache <string> {
|
|||
const video = await VideoModel.loadFull(key)
|
||||
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 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)
|
||||
|
||||
return { isOwned: false, path: destPath }
|
||||
return { isLocal: false, path: destPath }
|
||||
} catch (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 { doRequestAndSaveToFile } from '@server/helpers/requests.js'
|
||||
import { StoryboardModel } from '@server/models/video/storyboard.js'
|
||||
import { join } from 'path'
|
||||
import { FILES_CACHE } from '../../initializers/constants.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 constructor () {
|
||||
|
@ -21,7 +20,7 @@ class VideoStoryboardsSimpleFileCache extends AbstractSimpleFileCache <string> {
|
|||
const storyboard = await StoryboardModel.loadWithVideoByFilename(filename)
|
||||
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)
|
||||
}
|
||||
|
@ -39,7 +38,7 @@ class VideoStoryboardsSimpleFileCache extends AbstractSimpleFileCache <string> {
|
|||
|
||||
logger.debug('Fetched remote storyboard %s to %s.', remoteUrl, destPath)
|
||||
|
||||
return { isOwned: false, path: destPath }
|
||||
return { isLocal: false, path: destPath }
|
||||
} catch (err) {
|
||||
logger.info('Cannot fetch remote storyboard file %s.', remoteUrl, { err })
|
||||
|
||||
|
|
|
@ -8,8 +8,7 @@ import { FILES_CACHE } from '../../initializers/constants.js'
|
|||
import { VideoModel } from '../../models/video/video.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 constructor () {
|
||||
|
@ -24,10 +23,10 @@ class VideoTorrentsSimpleFileCache extends AbstractSimpleFileCache <string> {
|
|||
const file = await VideoFileModel.loadWithVideoOrPlaylistByTorrentFilename(filename)
|
||||
if (!file) return undefined
|
||||
|
||||
if (file.getVideo().isOwned()) {
|
||||
if (file.getVideo().isLocal()) {
|
||||
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)
|
||||
|
@ -38,7 +37,7 @@ class VideoTorrentsSimpleFileCache extends AbstractSimpleFileCache <string> {
|
|||
const file = await VideoFileModel.loadWithVideoOrPlaylistByTorrentFilename(key)
|
||||
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
|
||||
const video = await VideoModel.loadFull(file.getVideo().id)
|
||||
|
@ -52,7 +51,7 @@ class VideoTorrentsSimpleFileCache extends AbstractSimpleFileCache <string> {
|
|||
|
||||
const downloadName = this.buildDownloadName(video, file)
|
||||
|
||||
return { isOwned: false, path: destPath, downloadName }
|
||||
return { isLocal: false, path: destPath, downloadName }
|
||||
} catch (err) {
|
||||
logger.info('Cannot fetch remote torrent file %s.', remoteUrl, { err })
|
||||
|
||||
|
|
|
@ -108,7 +108,7 @@ export class ActorHtml {
|
|||
updatedAt: entity.updatedAt
|
||||
},
|
||||
|
||||
forbidIndexation: !entity.Actor.isOwned(),
|
||||
forbidIndexation: !entity.Actor.isLocal(),
|
||||
embedIndexation: false,
|
||||
|
||||
rssFeeds: getRSSFeeds(entity)
|
||||
|
|
|
@ -113,7 +113,7 @@ export class PlaylistHtml {
|
|||
|
||||
forbidIndexation: isEmbed
|
||||
? playlist.privacy !== VideoPlaylistPrivacy.PUBLIC && playlist.privacy !== VideoPlaylistPrivacy.UNLISTED
|
||||
: !playlist.isOwned() || playlist.privacy !== VideoPlaylistPrivacy.PUBLIC,
|
||||
: !playlist.isLocal() || playlist.privacy !== VideoPlaylistPrivacy.PUBLIC,
|
||||
|
||||
embedIndexation: isEmbed,
|
||||
|
||||
|
|
|
@ -438,7 +438,10 @@ class MuxingSession extends EventEmitter implements MuxingSession {
|
|||
setTimeout(() => {
|
||||
// Wait latest segments generation, and close watchers
|
||||
|
||||
const promise = this.filesWatcher?.close() || Promise.resolve()
|
||||
const promise = this.filesWatcher
|
||||
? this.filesWatcher.close()
|
||||
: Promise.resolve()
|
||||
|
||||
promise
|
||||
.then(() => {
|
||||
// Process remaining segments hash
|
||||
|
|
|
@ -129,7 +129,7 @@ async function createVideoAbuse (options: {
|
|||
videoAbuseInstance.Video = videoInstance
|
||||
abuseInstance.VideoAbuse = videoAbuseInstance
|
||||
|
||||
return { isOwned: videoInstance.isOwned() }
|
||||
return { isLocal: videoInstance.isLocal() }
|
||||
}
|
||||
|
||||
return createAbuse({
|
||||
|
@ -160,7 +160,7 @@ function createVideoCommentAbuse (options: {
|
|||
commentAbuseInstance.VideoComment = commentInstance
|
||||
abuseInstance.VideoCommentAbuse = commentAbuseInstance
|
||||
|
||||
return { isOwned: commentInstance.isOwned() }
|
||||
return { isLocal: commentInstance.isLocal() }
|
||||
}
|
||||
|
||||
return createAbuse({
|
||||
|
@ -183,7 +183,7 @@ function createAccountAbuse (options: {
|
|||
const { baseAbuse, accountInstance, transaction, reporterAccount, skipNotification } = options
|
||||
|
||||
const associateFun = () => {
|
||||
return Promise.resolve({ isOwned: accountInstance.isOwned() })
|
||||
return Promise.resolve({ isLocal: accountInstance.isLocal() })
|
||||
}
|
||||
|
||||
return createAbuse({
|
||||
|
@ -217,7 +217,7 @@ async function createAbuse (options: {
|
|||
base: FilteredModelAttributes<AbuseModel>
|
||||
reporterAccount: MAccountDefault
|
||||
flaggedAccount: MAccountLight
|
||||
associateFun: (abuseInstance: MAbuseFull) => Promise<{ isOwned: boolean }>
|
||||
associateFun: (abuseInstance: MAbuseFull) => Promise<{ isLocal: boolean }>
|
||||
skipNotification: boolean
|
||||
transaction: Transaction
|
||||
}) {
|
||||
|
@ -230,9 +230,9 @@ async function createAbuse (options: {
|
|||
abuseInstance.ReporterAccount = reporterAccount
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,9 @@ import {
|
|||
MAbuseMessage,
|
||||
MActorFollowFull,
|
||||
MApplication,
|
||||
MChannelAccountDefault,
|
||||
MChannelCollaboratorAccount,
|
||||
MChannelDefault,
|
||||
MCommentOwnerVideo,
|
||||
MPlugin,
|
||||
MVideoAccountLight,
|
||||
|
@ -17,6 +20,9 @@ import {
|
|||
import { JobQueue } from '../job-queue/index.js'
|
||||
import { PeerTubeSocket } from '../peertube-socket.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 {
|
||||
AbstractNotification,
|
||||
AbuseStateChangeForReporter,
|
||||
|
@ -71,7 +77,10 @@ class Notifier {
|
|||
newPeertubeVersion: [ NewPeerTubeVersionForAdmins ],
|
||||
newPluginVersion: [ NewPluginVersionForAdmins ],
|
||||
videoStudioEditionFinished: [ StudioEditionFinishedForOwner ],
|
||||
videoTranscriptionGenerated: [ VideoTranscriptionGeneratedForOwner ]
|
||||
videoTranscriptionGenerated: [ VideoTranscriptionGeneratedForOwner ],
|
||||
channelCollaboratorInvitation: [ InvitedToCollaborateToChannel ],
|
||||
channelCollaborationAccepted: [ AcceptedToCollaborateToChannel ],
|
||||
channelCollaborationRefused: [ RefusedToCollaborateToChannel ]
|
||||
}
|
||||
|
||||
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 }))
|
||||
}
|
||||
|
||||
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>) {
|
||||
await object.prepare()
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ export class AbuseStateChangeForReporter extends AbstractNotification<MAbuseFull
|
|||
|
||||
async prepare () {
|
||||
const reporter = this.abuse.ReporterAccount
|
||||
if (reporter.isOwned() !== true) return
|
||||
if (reporter.isLocal() !== true) return
|
||||
|
||||
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