1
0
Fork 0
mirror of https://github.com/Chocobozzz/PeerTube.git synced 2025-10-03 09:49:20 +02:00

Add channel collaborators feature

This commit is contained in:
Chocobozzz 2025-09-16 14:36:37 +02:00
parent 94e55dfc6c
commit b30ded66f6
No known key found for this signature in database
GPG key ID: 583A612D890159BE
192 changed files with 5534 additions and 2642 deletions

View file

@ -23,11 +23,11 @@ import { ConfigManager } from '../../../shared/config-manager.js'
import { logger } from '../../../shared/index.js' import { logger } from '../../../shared/index.js'
import { buildFFmpegLive, ProcessOptions } from './common.js' import { buildFFmpegLive, ProcessOptions } from './common.js'
type CustomLiveRTMPHLSTranscodingUpdatePayload = type CustomLiveRTMPHLSTranscodingUpdatePayload = Omit<LiveRTMPHLSTranscodingUpdatePayload, 'resolutionPlaylistFile'> & {
Omit<LiveRTMPHLSTranscodingUpdatePayload, 'resolutionPlaylistFile'> & { resolutionPlaylistFile?: [ Buffer, string ] | Blob | string } resolutionPlaylistFile?: [Buffer, string] | Blob | string
}
export class ProcessLiveRTMPHLSTranscoding { export class ProcessLiveRTMPHLSTranscoding {
private readonly outputPath: string private readonly outputPath: string
private readonly fsWatchers: FSWatcher[] = [] private readonly fsWatchers: FSWatcher[] = []
@ -326,7 +326,7 @@ export class ProcessLiveRTMPHLSTranscoding {
const p = payloadBuilder().then(p => this.updateWithRetry(p)) const p = payloadBuilder().then(p => this.updateWithRetry(p))
if (!sequentialPromises) sequentialPromises = p if (sequentialPromises === undefined) sequentialPromises = p
else sequentialPromises = sequentialPromises.then(() => p) else sequentialPromises = sequentialPromises.then(() => p)
} }

View file

@ -140,7 +140,8 @@ export class MyVideoChannelsComponent {
})), })),
switchMap(options => this.videoChannelService.listAccountVideoChannels(options)) switchMap(options => this.videoChannelService.listAccountVideoChannels(options))
) )
.subscribe(res => { .subscribe({
next: res => {
this.videoChannels = this.videoChannels.concat(res.data) this.videoChannels = this.videoChannels.concat(res.data)
this.pagination.totalItems = res.total this.pagination.totalItems = res.total
@ -167,6 +168,9 @@ export class MyVideoChannelsComponent {
this.buildChartOptions() this.buildChartOptions()
this.onChannelDataSubject.next(res.data) this.onChannelDataSubject.next(res.data)
},
error: err => this.notifier.error(err.message)
}) })
} }

View file

@ -7,6 +7,7 @@ import {
ActorInfo, ActorInfo,
FollowState, FollowState,
PluginType_Type, PluginType_Type,
UserNotificationData,
UserNotification as UserNotificationServer, UserNotification as UserNotificationServer,
UserNotificationType, UserNotificationType,
UserNotificationType_Type, UserNotificationType_Type,
@ -21,6 +22,7 @@ export class UserNotification implements UserNotificationServer {
id: number id: number
type: UserNotificationType_Type type: UserNotificationType_Type
read: boolean read: boolean
data: UserNotificationData
video?: VideoInfo & { video?: VideoInfo & {
channel: ActorInfo & { avatarUrl?: string } channel: ActorInfo & { avatarUrl?: string }

View file

@ -69,7 +69,7 @@ export class PeerTubeEmbed {
this.peertubePlugin = new PeerTubePlugin(this.http) this.peertubePlugin = new PeerTubePlugin(this.http)
this.peertubeTheme = new PeerTubeTheme(this.peertubePlugin) this.peertubeTheme = new PeerTubeTheme(this.peertubePlugin)
this.playerHTML = new PlayerHTML(videoWrapperId) this.playerHTML = new PlayerHTML(videoWrapperId)
this.playerOptionsBuilder = new PlayerOptionsBuilder(this.playerHTML, this.videoFetcher, this.peertubePlugin, this.config) this.playerOptionsBuilder = new PlayerOptionsBuilder(this.playerHTML, this.videoFetcher, this.peertubePlugin)
this.liveManager = new LiveManager(this.playerHTML) this.liveManager = new LiveManager(this.playerHTML)
this.requiresPassword = false this.requiresPassword = false

View file

@ -65,8 +65,7 @@ export class PlayerOptionsBuilder {
constructor ( constructor (
private readonly playerHTML: PlayerHTML, private readonly playerHTML: PlayerHTML,
private readonly videoFetcher: VideoFetcher, private readonly videoFetcher: VideoFetcher,
private readonly peertubePlugin: PeerTubePlugin, private readonly peertubePlugin: PeerTubePlugin
private readonly serverConfig: HTMLServerConfig
) {} ) {}
hasAPIEnabled () { hasAPIEnabled () {

View file

@ -85,7 +85,11 @@ export default defineConfig([
'@typescript-eslint/no-unnecessary-boolean-literal-compare': 'off', '@typescript-eslint/no-unnecessary-boolean-literal-compare': 'off',
'@typescript-eslint/strict-boolean-expressions': 'off', '@typescript-eslint/strict-boolean-expressions': 'off',
'@typescript-eslint/consistent-type-definitions': 'off', '@typescript-eslint/consistent-type-definitions': 'off',
'@typescript-eslint/no-misused-promises': 'off', '@typescript-eslint/no-misused-promises': [ 'error', {
checksConditionals: true,
checksSpreads: true,
checksVoidReturn: false
} ],
'@typescript-eslint/no-namespace': 'off', '@typescript-eslint/no-namespace': 'off',
'@typescript-eslint/no-empty-interface': 'off', '@typescript-eslint/no-empty-interface': 'off',
'@typescript-eslint/no-extraneous-class': 'off', '@typescript-eslint/no-extraneous-class': 'off',

View file

@ -4,6 +4,7 @@ export * from './user-create-result.model.js'
export * from './user-create.model.js' export * from './user-create.model.js'
export * from './user-flag.model.js' export * from './user-flag.model.js'
export * from './user-login.model.js' export * from './user-login.model.js'
export * from './user-notification-data.model.js'
export * from './user-notification-list-query.model.js' export * from './user-notification-list-query.model.js'
export * from './user-notification-setting.model.js' export * from './user-notification-setting.model.js'
export * from './user-notification.model.js' export * from './user-notification.model.js'

View file

@ -0,0 +1,12 @@
export type UserNotificationData = UserNotificationDataCollaborationRejected
export interface UserNotificationDataCollaborationRejected {
channelDisplayName: string
channelHandle: string
channelOwnerDisplayName: string
channelOwnerHandle: string
collaboratorDisplayName: string
collaboratorHandle: string
}

View file

@ -1,8 +1,10 @@
import { FollowState } from '../actors/index.js' import { FollowState } from '../actors/index.js'
import { AbuseStateType } from '../moderation/index.js' import { AbuseStateType } from '../moderation/index.js'
import { PluginType_Type } from '../plugins/index.js' import { PluginType_Type } from '../plugins/index.js'
import { VideoChannelCollaboratorStateType } from '../videos/index.js'
import { VideoConstant } from '../videos/video-constant.model.js' import { VideoConstant } from '../videos/video-constant.model.js'
import { VideoStateType } from '../videos/video-state.enum.js' import { VideoStateType } from '../videos/video-state.enum.js'
import { UserNotificationData } from './user-notification-data.model.js'
export const UserNotificationType = { export const UserNotificationType = {
NEW_VIDEO_FROM_SUBSCRIPTION: 1, NEW_VIDEO_FROM_SUBSCRIPTION: 1,
@ -40,7 +42,11 @@ export const UserNotificationType = {
NEW_LIVE_FROM_SUBSCRIPTION: 21, NEW_LIVE_FROM_SUBSCRIPTION: 21,
MY_VIDEO_TRANSCRIPTION_GENERATED: 22 MY_VIDEO_TRANSCRIPTION_GENERATED: 22,
INVITED_TO_COLLABORATE_TO_CHANNEL: 23,
ACCEPTED_TO_COLLABORATE_TO_CHANNEL: 24,
REFUSED_TO_COLLABORATE_TO_CHANNEL: 25
} as const } as const
export type UserNotificationType_Type = typeof UserNotificationType[keyof typeof UserNotificationType] export type UserNotificationType_Type = typeof UserNotificationType[keyof typeof UserNotificationType]
@ -79,6 +85,7 @@ export interface UserNotification {
id: number id: number
type: UserNotificationType_Type type: UserNotificationType_Type
read: boolean read: boolean
data: UserNotificationData
video?: VideoInfo & { video?: VideoInfo & {
channel: ActorInfo channel: ActorInfo
@ -156,6 +163,15 @@ export interface UserNotification {
video: VideoInfo video: VideoInfo
} }
videoChannelCollaborator?: {
id: number
state: VideoConstant<VideoChannelCollaboratorStateType>
channel: ActorInfo
account: ActorInfo
}
createdAt: string createdAt: string
updatedAt: string updatedAt: string
} }

View file

@ -1,3 +1,4 @@
export * from './video-channel-collaborator.model.js'
export * from './video-channel-create-result.model.js' export * from './video-channel-create-result.model.js'
export * from './video-channel-create.model.js' export * from './video-channel-create.model.js'
export * from './video-channel-update.model.js' export * from './video-channel-update.model.js'

View file

@ -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
}

View file

@ -31,6 +31,7 @@ import {
BlacklistCommand, BlacklistCommand,
CaptionsCommand, CaptionsCommand,
ChangeOwnershipCommand, ChangeOwnershipCommand,
ChannelCollaboratorsCommand,
ChannelSyncsCommand, ChannelSyncsCommand,
ChannelsCommand, ChannelsCommand,
ChaptersCommand, ChaptersCommand,
@ -82,6 +83,7 @@ export class PeerTubeServer {
parallel?: boolean parallel?: boolean
internalServerNumber: number internalServerNumber: number
adminEmail: string
serverNumber?: number serverNumber?: number
customConfigFile?: string customConfigFile?: string
@ -170,6 +172,8 @@ export class PeerTubeServer {
watchedWordsLists?: WatchedWordsCommand watchedWordsLists?: WatchedWordsCommand
autoTags?: AutomaticTagsCommand autoTags?: AutomaticTagsCommand
channelCollaborators?: ChannelCollaboratorsCommand
constructor (options: { serverNumber: number } | { url: string }) { constructor (options: { serverNumber: number } | { url: string }) {
if ((options as any).url) { if ((options as any).url) {
this.setUrl((options as any).url) this.setUrl((options as any).url)
@ -188,6 +192,7 @@ export class PeerTubeServer {
} }
} }
this.adminEmail = this.buildEmail()
this.assignCommands() this.assignCommands()
} }
@ -406,7 +411,7 @@ export class PeerTubeServer {
well_known: this.getDirectoryPath('well-known') + '/' well_known: this.getDirectoryPath('well-known') + '/'
}, },
admin: { admin: {
email: `admin${this.internalServerNumber}@example.com` email: this.buildEmail()
}, },
live: { live: {
rtmp: { rtmp: {
@ -477,5 +482,11 @@ export class PeerTubeServer {
this.watchedWordsLists = new WatchedWordsCommand(this) this.watchedWordsLists = new WatchedWordsCommand(this)
this.autoTags = new AutomaticTagsCommand(this) this.autoTags = new AutomaticTagsCommand(this)
this.channelCollaborators = new ChannelCollaboratorsCommand(this)
}
private buildEmail () {
return `admin${this.internalServerNumber}@example.com`
} }
} }

View file

@ -12,7 +12,8 @@ import {
UserUpdate, UserUpdate,
UserUpdateMe, UserUpdateMe,
UserVideoQuota, UserVideoQuota,
UserVideoRate UserVideoRate,
VideoChannel
} from '@peertube/peertube-models' } from '@peertube/peertube-models'
import { unwrapBody } from '../requests/index.js' import { unwrapBody } from '../requests/index.js'
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js' import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
@ -282,6 +283,20 @@ export class UsersCommand extends AbstractCommand {
}) })
} }
listMyChannels (options: OverrideCommandOptions = {}) {
const path = '/api/v1/users/me/video-channels'
return this.getRequestBody<ResultList<VideoChannel>>({
...options,
path,
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.OK_200
})
}
// ---------------------------------------------------------------------------
deleteMe (options: OverrideCommandOptions = {}) { deleteMe (options: OverrideCommandOptions = {}) {
const path = '/api/v1/users/me' const path = '/api/v1/users/me'

View file

@ -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
})
}
}

View file

@ -39,6 +39,7 @@ export class ChannelsCommand extends AbstractCommand {
sort?: string sort?: string
withStats?: boolean withStats?: boolean
search?: string search?: string
includeCollaborations?: boolean
} }
) { ) {
const { accountName, sort = 'createdAt' } = options const { accountName, sort = 'createdAt' } = options
@ -48,7 +49,7 @@ export class ChannelsCommand extends AbstractCommand {
...options, ...options,
path, path,
query: { sort, ...pick(options, [ 'start', 'count', 'withStats', 'search' ]) }, query: { sort, ...pick(options, [ 'start', 'count', 'withStats', 'search', 'includeCollaborations' ]) },
implicitToken: false, implicitToken: false,
defaultExpectedStatus: HttpStatusCode.OK_200 defaultExpectedStatus: HttpStatusCode.OK_200
}) })

View file

@ -23,7 +23,6 @@ type ListForAdminOrAccountCommonOptions = {
} }
export class CommentsCommand extends AbstractCommand { export class CommentsCommand extends AbstractCommand {
private lastVideoId: number | string private lastVideoId: number | string
private lastThreadId: number private lastThreadId: number
private lastReplyId: number private lastReplyId: number
@ -51,6 +50,7 @@ export class CommentsCommand extends AbstractCommand {
listCommentsOnMyVideos (options: OverrideCommandOptions & ListForAdminOrAccountCommonOptions & { listCommentsOnMyVideos (options: OverrideCommandOptions & ListForAdminOrAccountCommonOptions & {
isHeldForReview?: boolean isHeldForReview?: boolean
includeCollaborations?: boolean
} = {}) { } = {}) {
const path = '/api/v1/users/me/videos/comments' const path = '/api/v1/users/me/videos/comments'
@ -61,7 +61,7 @@ export class CommentsCommand extends AbstractCommand {
query: { query: {
...this.buildListForAdminOrAccountQuery(options), ...this.buildListForAdminOrAccountQuery(options),
isHeldForReview: options.isHeldForReview ...pick(options, [ 'isHeldForReview', 'includeCollaborations' ])
}, },
implicitToken: true, implicitToken: true,
defaultExpectedStatus: HttpStatusCode.OK_200 defaultExpectedStatus: HttpStatusCode.OK_200
@ -78,13 +78,15 @@ export class CommentsCommand extends AbstractCommand {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
listThreads (options: OverrideCommandOptions & { listThreads (
options: OverrideCommandOptions & {
videoId: number | string videoId: number | string
videoPassword?: string videoPassword?: string
start?: number start?: number
count?: number count?: number
sort?: string sort?: string
}) { }
) {
const { start, count, sort, videoId, videoPassword } = options const { start, count, sort, videoId, videoPassword } = options
const path = '/api/v1/videos/' + videoId + '/comment-threads' const path = '/api/v1/videos/' + videoId + '/comment-threads'
@ -99,10 +101,12 @@ export class CommentsCommand extends AbstractCommand {
}) })
} }
getThread (options: OverrideCommandOptions & { getThread (
options: OverrideCommandOptions & {
videoId: number | string videoId: number | string
threadId: number threadId: number
}) { }
) {
const { videoId, threadId } = options const { videoId, threadId } = options
const path = '/api/v1/videos/' + videoId + '/comment-threads/' + threadId const path = '/api/v1/videos/' + videoId + '/comment-threads/' + threadId
@ -115,21 +119,25 @@ export class CommentsCommand extends AbstractCommand {
}) })
} }
async getThreadOf (options: OverrideCommandOptions & { async getThreadOf (
options: OverrideCommandOptions & {
videoId: number | string videoId: number | string
text: string text: string
}) { }
) {
const { videoId, text } = options const { videoId, text } = options
const threadId = await this.findCommentId({ videoId, text }) const threadId = await this.findCommentId({ videoId, text })
return this.getThread({ ...options, videoId, threadId }) return this.getThread({ ...options, videoId, threadId })
} }
async createThread (options: OverrideCommandOptions & { async createThread (
options: OverrideCommandOptions & {
videoId: number | string videoId: number | string
text: string text: string
videoPassword?: string videoPassword?: string
}) { }
) {
const { videoId, text, videoPassword } = options const { videoId, text, videoPassword } = options
const path = '/api/v1/videos/' + videoId + '/comment-threads' const path = '/api/v1/videos/' + videoId + '/comment-threads'
@ -149,12 +157,14 @@ export class CommentsCommand extends AbstractCommand {
return body.comment return body.comment
} }
async addReply (options: OverrideCommandOptions & { async addReply (
options: OverrideCommandOptions & {
videoId: number | string videoId: number | string
toCommentId: number toCommentId: number
text: string text: string
videoPassword?: string videoPassword?: string
}) { }
) {
const { videoId, toCommentId, text, videoPassword } = options const { videoId, toCommentId, text, videoPassword } = options
const path = '/api/v1/videos/' + videoId + '/comments/' + toCommentId const path = '/api/v1/videos/' + videoId + '/comments/' + toCommentId
@ -173,22 +183,28 @@ export class CommentsCommand extends AbstractCommand {
return body.comment return body.comment
} }
async addReplyToLastReply (options: OverrideCommandOptions & { async addReplyToLastReply (
options: OverrideCommandOptions & {
text: string text: string
}) { }
) {
return this.addReply({ ...options, videoId: this.lastVideoId, toCommentId: this.lastReplyId }) return this.addReply({ ...options, videoId: this.lastVideoId, toCommentId: this.lastReplyId })
} }
async addReplyToLastThread (options: OverrideCommandOptions & { async addReplyToLastThread (
options: OverrideCommandOptions & {
text: string text: string
}) { }
) {
return this.addReply({ ...options, videoId: this.lastVideoId, toCommentId: this.lastThreadId }) return this.addReply({ ...options, videoId: this.lastVideoId, toCommentId: this.lastThreadId })
} }
async findCommentId (options: OverrideCommandOptions & { async findCommentId (
options: OverrideCommandOptions & {
videoId: number | string videoId: number | string
text: string text: string
}) { }
) {
const { videoId, text } = options const { videoId, text } = options
const { data } = await this.listForAdmin({ videoId, count: 25, sort: '-createdAt' }) const { data } = await this.listForAdmin({ videoId, count: 25, sort: '-createdAt' })
@ -197,10 +213,12 @@ export class CommentsCommand extends AbstractCommand {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
delete (options: OverrideCommandOptions & { delete (
options: OverrideCommandOptions & {
videoId: number | string videoId: number | string
commentId: number commentId: number
}) { }
) {
const { videoId, commentId } = options const { videoId, commentId } = options
const path = '/api/v1/videos/' + videoId + '/comments/' + commentId const path = '/api/v1/videos/' + videoId + '/comments/' + commentId
@ -213,9 +231,11 @@ export class CommentsCommand extends AbstractCommand {
}) })
} }
async deleteAllComments (options: OverrideCommandOptions & { async deleteAllComments (
options: OverrideCommandOptions & {
videoUUID: string videoUUID: string
}) { }
) {
const { data } = await this.listForAdmin({ ...options, start: 0, count: 20 }) const { data } = await this.listForAdmin({ ...options, start: 0, count: 20 })
for (const comment of data) { for (const comment of data) {
@ -227,10 +247,12 @@ export class CommentsCommand extends AbstractCommand {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
approve (options: OverrideCommandOptions & { approve (
options: OverrideCommandOptions & {
videoId: number | string videoId: number | string
commentId: number commentId: number
}) { }
) {
const { videoId, commentId } = options const { videoId, commentId } = options
const path = '/api/v1/videos/' + videoId + '/comments/' + commentId + '/approve' const path = '/api/v1/videos/' + videoId + '/comments/' + commentId + '/approve'

View file

@ -1,5 +1,6 @@
export * from './blacklist-command.js' export * from './blacklist-command.js'
export * from './captions-command.js' export * from './captions-command.js'
export * from './channel-collaborators-command.js'
export * from './change-ownership-command.js' export * from './change-ownership-command.js'
export * from './channels.js' export * from './channels.js'
export * from './channels-command.js' export * from './channels-command.js'

View file

@ -71,10 +71,11 @@ export class PlaylistsCommand extends AbstractCommand {
sort?: string sort?: string
search?: string search?: string
playlistType?: VideoPlaylistType_Type playlistType?: VideoPlaylistType_Type
includeCollaborations?: boolean
} }
) { ) {
const path = '/api/v1/accounts/' + options.handle + '/video-playlists' const path = '/api/v1/accounts/' + options.handle + '/video-playlists'
const query = pick(options, [ 'start', 'count', 'sort', 'search', 'playlistType' ]) const query = pick(options, [ 'start', 'count', 'sort', 'search', 'playlistType', 'includeCollaborations' ])
return this.getRequestBody<ResultList<VideoPlaylist>>({ return this.getRequestBody<ResultList<VideoPlaylist>>({
...options, ...options,

View file

@ -229,14 +229,18 @@ export class VideosCommand extends AbstractCommand {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
listMyVideos (options: OverrideCommandOptions & VideosCommonQuery & { channelId?: number, channelNameOneOf?: string[] } = {}) { listMyVideos (options: OverrideCommandOptions & VideosCommonQuery & {
channelId?: number
channelNameOneOf?: string[]
includeCollaborations?: boolean
} = {}) {
const path = '/api/v1/users/me/videos' const path = '/api/v1/users/me/videos'
return this.getRequestBody<ResultList<Video>>({ return this.getRequestBody<ResultList<Video>>({
...options, ...options,
path, path,
query: { ...this.buildListQuery(options), ...pick(options, [ 'channelId', 'channelNameOneOf' ]) }, query: { ...this.buildListQuery(options), ...pick(options, [ 'channelId', 'channelNameOneOf', 'includeCollaborations' ]) },
implicitToken: true, implicitToken: true,
defaultExpectedStatus: HttpStatusCode.OK_200 defaultExpectedStatus: HttpStatusCode.OK_200
}) })

View 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 ])
})
})

View file

@ -1,8 +1,7 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import { FIXTURE_URLS } from '@tests/shared/fixture-urls.js'
import { areHttpImportTestsDisabled } from '@peertube/peertube-node-utils'
import { HttpStatusCode } from '@peertube/peertube-models' import { HttpStatusCode } from '@peertube/peertube-models'
import { areHttpImportTestsDisabled } from '@peertube/peertube-node-utils'
import { import {
ChannelsCommand, ChannelsCommand,
cleanupTests, cleanupTests,
@ -11,6 +10,7 @@ import {
setAccessTokensToServers, setAccessTokensToServers,
setDefaultVideoChannel setDefaultVideoChannel
} from '@peertube/peertube-server-commands' } from '@peertube/peertube-server-commands'
import { FIXTURE_URLS } from '@tests/shared/fixture-urls.js'
describe('Test videos import in a channel API validator', function () { describe('Test videos import in a channel API validator', function () {
let server: PeerTubeServer let server: PeerTubeServer

View file

@ -186,7 +186,7 @@ describe('Test video lives API validator', function () {
it('Should fail with a bad channel', async function () { it('Should fail with a bad channel', async function () {
const fields = { ...baseCorrectParams, channelId: 545454 } const fields = { ...baseCorrectParams, channelId: 545454 }
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
}) })
it('Should fail with a bad privacy for replay settings', async function () { it('Should fail with a bad privacy for replay settings', async function () {
@ -208,7 +208,7 @@ describe('Test video lives API validator', function () {
const fields = { ...baseCorrectParams, channelId: customChannelId } const fields = { ...baseCorrectParams, channelId: customChannelId }
await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields }) await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
}) })
it('Should fail with too many tags', async function () { it('Should fail with too many tags', async function () {

View file

@ -1,9 +1,7 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import { expect } from 'chai'
import { omit } from '@peertube/peertube-core-utils' import { omit } from '@peertube/peertube-core-utils'
import { HttpStatusCode, VideoChannelUpdate } from '@peertube/peertube-models' import { HttpStatusCode, VideoChannelUpdate } from '@peertube/peertube-models'
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils' import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
import { import {
ChannelsCommand, ChannelsCommand,
@ -16,6 +14,8 @@ import {
PeerTubeServer, PeerTubeServer,
setAccessTokensToServers setAccessTokensToServers
} from '@peertube/peertube-server-commands' } from '@peertube/peertube-server-commands'
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
import { expect } from 'chai'
describe('Test video channels API validator', function () { describe('Test video channels API validator', function () {
const videoChannelPath = '/api/v1/video-channels' const videoChannelPath = '/api/v1/video-channels'

View file

@ -229,7 +229,7 @@ describe('Test video imports API validator', function () {
it('Should fail with a bad channel', async function () { it('Should fail with a bad channel', async function () {
const fields = { ...baseCorrectParams, channelId: 545454 } const fields = { ...baseCorrectParams, channelId: 545454 }
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
}) })
it('Should fail with another user channel', async function () { it('Should fail with another user channel', async function () {
@ -245,7 +245,7 @@ describe('Test video imports API validator', function () {
const fields = { ...baseCorrectParams, channelId: customChannelId } const fields = { ...baseCorrectParams, channelId: customChannelId }
await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields }) await makePostBodyRequest({ url: server.url, path, token: userAccessToken, fields, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
}) })
it('Should fail with too many tags', async function () { it('Should fail with too many tags', async function () {

View file

@ -260,7 +260,7 @@ describe('Test video playlists API validator', function () {
}) })
it('Should fail with an unknown video channel id', async function () { it('Should fail with an unknown video channel id', async function () {
const params = getBase({ videoChannelId: 42 }, { expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) const params = getBase({ videoChannelId: 42 }, { expectedStatus: HttpStatusCode.NOT_FOUND_404 })
await command.create(params) await command.create(params)
await command.update(getUpdate(params, playlist.shortUUID)) await command.update(getUpdate(params, playlist.shortUUID))
@ -307,7 +307,7 @@ describe('Test video playlists API validator', function () {
}) })
it('Should fail to set a playlist to a channel owned by another user', async function () { it('Should fail to set a playlist to a channel owned by another user', async function () {
const params = getBase({}, { token: userAccessToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) const params = getBase({}, { token: userAccessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
await command.create(params) await command.create(params)
await command.update(getUpdate(params, userPlaylist.shortUUID)) await command.update(getUpdate(params, userPlaylist.shortUUID))

View file

@ -358,7 +358,11 @@ describe('Test videos API validator', function () {
const fields = { ...baseCorrectParams, channelId: 545454 } const fields = { ...baseCorrectParams, channelId: 545454 }
const attaches = baseCorrectAttaches const attaches = baseCorrectAttaches
await checkUploadVideoParam({ ...baseOptions(), attributes: { ...fields, ...attaches } }) await checkUploadVideoParam({
...baseOptions(),
attributes: { ...fields, ...attaches },
expectedStatus: HttpStatusCode.NOT_FOUND_404
})
}) })
it('Should fail with another user channel', async function () { it('Should fail with another user channel', async function () {
@ -378,7 +382,8 @@ describe('Test videos API validator', function () {
await checkUploadVideoParam({ await checkUploadVideoParam({
...baseOptions(), ...baseOptions(),
token: userAccessToken, token: userAccessToken,
attributes: { ...fields, ...attaches } attributes: { ...fields, ...attaches },
expectedStatus: HttpStatusCode.FORBIDDEN_403
}) })
}) })
@ -680,7 +685,13 @@ describe('Test videos API validator', function () {
it('Should fail with a bad channel', async function () { it('Should fail with a bad channel', async function () {
const fields = { ...baseCorrectParams, channelId: 545454 } const fields = { ...baseCorrectParams, channelId: 545454 }
await makePutBodyRequest({ url: server.url, path: path + video.shortUUID, token: server.accessToken, fields }) await makePutBodyRequest({
url: server.url,
path: path + video.shortUUID,
token: server.accessToken,
fields,
expectedStatus: HttpStatusCode.NOT_FOUND_404
})
}) })
it('Should fail with too many tags', async function () { it('Should fail with too many tags', async function () {

View file

@ -1,13 +1,15 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import { expect } from 'chai'
import { wait } from '@peertube/peertube-core-utils' import { wait } from '@peertube/peertube-core-utils'
import { PluginType, UserNotification, UserNotificationType } from '@peertube/peertube-models' import { PluginType, UserNotification, UserNotificationType } from '@peertube/peertube-models'
import { cleanupTests, PeerTubeServer } from '@peertube/peertube-server-commands' import { cleanupTests, PeerTubeServer } from '@peertube/peertube-server-commands'
import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js' import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
import { MockJoinPeerTubeVersions } from '@tests/shared/mock-servers/mock-joinpeertube-versions.js' import { MockJoinPeerTubeVersions } from '@tests/shared/mock-servers/mock-joinpeertube-versions.js'
import { CheckerBaseParams, prepareNotificationsTest, checkNewPeerTubeVersion, checkNewPluginVersion } from '@tests/shared/notifications.js' import { checkNewPeerTubeVersion, checkNewPluginVersion } from '@tests/shared/notifications/check-admin-notifications.js'
import { prepareNotificationsTest } from '@tests/shared/notifications/notifications-common.js'
import { CheckerBaseParams } from '@tests/shared/notifications/shared/notification-checker.js'
import { SQLCommand } from '@tests/shared/sql-command.js' import { SQLCommand } from '@tests/shared/sql-command.js'
import { expect } from 'chai'
describe('Test admin notifications', function () { describe('Test admin notifications', function () {
let server: PeerTubeServer let server: PeerTubeServer

View file

@ -3,7 +3,10 @@
import { UserNotification } from '@peertube/peertube-models' import { UserNotification } from '@peertube/peertube-models'
import { PeerTubeServer, cleanupTests, waitJobs } from '@peertube/peertube-server-commands' import { PeerTubeServer, cleanupTests, waitJobs } from '@peertube/peertube-server-commands'
import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js' import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
import { CheckerBaseParams, checkMyVideoTranscriptionGenerated, prepareNotificationsTest } from '@tests/shared/notifications.js' import { checkMyVideoTranscriptionGenerated } from '@tests/shared/notifications/check-video-notifications.js'
import { prepareNotificationsTest } from '@tests/shared/notifications/notifications-common.js'
import { CheckerBaseParams } from '@tests/shared/notifications/shared/notification-checker.js'
import { join } from 'path' import { join } from 'path'
describe('Test caption notifications', function () { describe('Test caption notifications', function () {

View file

@ -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 ])
})
})

View file

@ -3,7 +3,9 @@
import { UserNotification, UserNotificationType, VideoCommentPolicy } from '@peertube/peertube-models' import { UserNotification, UserNotificationType, VideoCommentPolicy } from '@peertube/peertube-models'
import { PeerTubeServer, cleanupTests, setDefaultAccountAvatar, waitJobs } from '@peertube/peertube-server-commands' import { PeerTubeServer, cleanupTests, setDefaultAccountAvatar, waitJobs } from '@peertube/peertube-server-commands'
import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js' import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
import { CheckerBaseParams, checkCommentMention, checkNewCommentOnMyVideo, prepareNotificationsTest } from '@tests/shared/notifications.js' import { checkCommentMention, checkNewCommentOnMyVideo } from '@tests/shared/notifications/check-comment-notifications.js'
import { prepareNotificationsTest } from '@tests/shared/notifications/notifications-common.js'
import { CheckerBaseParams } from '@tests/shared/notifications/shared/notification-checker.js'
import { expect } from 'chai' import { expect } from 'chai'
describe('Test comments notifications', function () { describe('Test comments notifications', function () {

View file

@ -1,5 +1,6 @@
import './admin-notifications.js' import './admin-notifications.js'
import './caption-notifications.js' import './caption-notifications.js'
import './channel-collaborators-notification.js'
import './comments-notifications.js' import './comments-notifications.js'
import './moderation-notifications.js' import './moderation-notifications.js'
import './notifications-api.js' import './notifications-api.js'

View file

@ -6,21 +6,19 @@ import { buildUUID } from '@peertube/peertube-node-utils'
import { cleanupTests, PeerTubeServer, waitJobs } from '@peertube/peertube-server-commands' import { cleanupTests, PeerTubeServer, waitJobs } from '@peertube/peertube-server-commands'
import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js' import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
import { MockInstancesIndex } from '@tests/shared/mock-servers/mock-instances-index.js' import { MockInstancesIndex } from '@tests/shared/mock-servers/mock-instances-index.js'
import { checkAutoInstanceFollowing, checkNewInstanceFollower } from '@tests/shared/notifications/check-follow-notifications.js'
import { import {
prepareNotificationsTest,
CheckerBaseParams,
checkNewVideoAbuseForModerators,
checkNewCommentAbuseForModerators,
checkNewAccountAbuseForModerators,
checkAbuseStateChange, checkAbuseStateChange,
checkNewAbuseMessage, checkNewAbuseMessage,
checkNewAccountAbuseForModerators,
checkNewBlacklistOnMyVideo, checkNewBlacklistOnMyVideo,
checkNewInstanceFollower, checkNewCommentAbuseForModerators,
checkAutoInstanceFollowing, checkNewVideoAbuseForModerators,
checkVideoAutoBlacklistForModerators, checkVideoAutoBlacklistForModerators
checkMyVideoIsPublished, } from '@tests/shared/notifications/check-moderation-notifications.js'
checkNewVideoFromSubscription import { checkMyVideoIsPublished, checkNewVideoFromSubscription } from '@tests/shared/notifications/check-video-notifications.js'
} from '@tests/shared/notifications.js' import { prepareNotificationsTest } from '@tests/shared/notifications/notifications-common.js'
import { CheckerBaseParams } from '@tests/shared/notifications/shared/notification-checker.js'
describe('Test moderation notifications', function () { describe('Test moderation notifications', function () {
let servers: PeerTubeServer[] = [] let servers: PeerTubeServer[] = []

View file

@ -1,15 +1,12 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import { expect } from 'chai'
import { UserNotification, UserNotificationSettingValue, UserNotificationType } from '@peertube/peertube-models' import { UserNotification, UserNotificationSettingValue, UserNotificationType } from '@peertube/peertube-models'
import { cleanupTests, PeerTubeServer, waitJobs } from '@peertube/peertube-server-commands' import { cleanupTests, PeerTubeServer, waitJobs } from '@peertube/peertube-server-commands'
import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js' import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
import { import { checkNewVideoFromSubscription } from '@tests/shared/notifications/check-video-notifications.js'
prepareNotificationsTest, import { getAllNotificationsSettings, prepareNotificationsTest } from '@tests/shared/notifications/notifications-common.js'
CheckerBaseParams, import { CheckerBaseParams } from '@tests/shared/notifications/shared/notification-checker.js'
getAllNotificationsSettings, import { expect } from 'chai'
checkNewVideoFromSubscription
} from '@tests/shared/notifications.js'
describe('Test notifications API', function () { describe('Test notifications API', function () {
let server: PeerTubeServer let server: PeerTubeServer

View file

@ -3,7 +3,9 @@
import { UserNotification } from '@peertube/peertube-models' import { UserNotification } from '@peertube/peertube-models'
import { cleanupTests, PeerTubeServer, waitJobs } from '@peertube/peertube-server-commands' import { cleanupTests, PeerTubeServer, waitJobs } from '@peertube/peertube-server-commands'
import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js' import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
import { CheckerBaseParams, prepareNotificationsTest, checkUserRegistered, checkRegistrationRequest } from '@tests/shared/notifications.js' import { checkRegistrationRequest, checkUserRegistered } from '@tests/shared/notifications/check-moderation-notifications.js'
import { prepareNotificationsTest } from '@tests/shared/notifications/notifications-common.js'
import { CheckerBaseParams } from '@tests/shared/notifications/shared/notification-checker.js'
describe('Test registrations notifications', function () { describe('Test registrations notifications', function () {
let server: PeerTubeServer let server: PeerTubeServer

View file

@ -6,17 +6,16 @@ import { buildUUID } from '@peertube/peertube-node-utils'
import { cleanupTests, findExternalSavedVideo, PeerTubeServer, stopFfmpeg, waitJobs } from '@peertube/peertube-server-commands' import { cleanupTests, findExternalSavedVideo, PeerTubeServer, stopFfmpeg, waitJobs } from '@peertube/peertube-server-commands'
import { FIXTURE_URLS } from '@tests/shared/fixture-urls.js' import { FIXTURE_URLS } from '@tests/shared/fixture-urls.js'
import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js' import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
import { checkNewActorFollow } from '@tests/shared/notifications/check-follow-notifications.js'
import { import {
CheckerBaseParams,
checkMyVideoImportIsFinished, checkMyVideoImportIsFinished,
checkMyVideoIsPublished, checkMyVideoIsPublished,
checkNewActorFollow,
checkNewLiveFromSubscription, checkNewLiveFromSubscription,
checkNewVideoFromSubscription, checkNewVideoFromSubscription,
checkVideoStudioEditionIsFinished, checkVideoStudioEditionIsFinished
prepareNotificationsTest, } from '@tests/shared/notifications/check-video-notifications.js'
waitUntilNotification import { prepareNotificationsTest, waitUntilNotification } from '@tests/shared/notifications/notifications-common.js'
} from '@tests/shared/notifications.js' import { CheckerBaseParams } from '@tests/shared/notifications/shared/notification-checker.js'
import { uploadRandomVideoOnServers } from '@tests/shared/videos.js' import { uploadRandomVideoOnServers } from '@tests/shared/videos.js'
import { expect } from 'chai' import { expect } from 'chai'

View 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)
})
})

View file

@ -21,8 +21,8 @@ import './video-schedule-update.js'
import './video-source.js' import './video-source.js'
import './video-static-file-privacy.js' import './video-static-file-privacy.js'
import './video-storyboard.js' import './video-storyboard.js'
import './video-storyboard-remote-runner.js'
import './video-transcription.js' import './video-transcription.js'
import './videos-common-filters.js' import './videos-common-filters.js'
import './videos-history.js' import './videos-history.js'
import './videos-overview.js' import './videos-overview.js'
import './channel-collaborators.js'

View file

@ -8,9 +8,9 @@ describe('Comment model', function () {
const text = '@florian @jean@localhost:9000 @flo @another@localhost:9000 @flo2@jean.com hello ' + const text = '@florian @jean@localhost:9000 @flo @another@localhost:9000 @flo2@jean.com hello ' +
'email@localhost:9000 coucou.com no? @chocobozzz @chocobozzz @end' 'email@localhost:9000 coucou.com no? @chocobozzz @chocobozzz @end'
const isOwned = true const isLocal = true
const result = extractMentions(text, isOwned).sort((a, b) => a.localeCompare(b)) const result = extractMentions(text, isLocal).sort((a, b) => a.localeCompare(b))
expect(result).to.deep.equal([ 'another', 'chocobozzz', 'end', 'flo', 'florian', 'jean' ]) expect(result).to.deep.equal([ 'another', 'chocobozzz', 'end', 'flo', 'florian', 'jean' ])
}) })

View file

@ -1,12 +1,12 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import { Account, AccountSummary, HttpStatusCode, VideoChannel, VideoChannelSummary } from '@peertube/peertube-models'
import { makeRawRequest, PeerTubeServer } from '@peertube/peertube-server-commands'
import { expect } from 'chai' import { expect } from 'chai'
import { pathExists } from 'fs-extra/esm' import { pathExists } from 'fs-extra/esm'
import { readdir } from 'fs/promises' import { readdir } from 'fs/promises'
import { Account, VideoChannel } from '@peertube/peertube-models'
import { PeerTubeServer } from '@peertube/peertube-server-commands'
async function expectChannelsFollows (options: { export async function expectChannelsFollows (options: {
server: PeerTubeServer server: PeerTubeServer
handle: string handle: string
followers: number followers: number
@ -18,7 +18,7 @@ async function expectChannelsFollows (options: {
return expectActorFollow({ ...options, data }) return expectActorFollow({ ...options, data })
} }
async function expectAccountFollows (options: { export async function expectAccountFollows (options: {
server: PeerTubeServer server: PeerTubeServer
handle: string handle: string
followers: number followers: number
@ -30,7 +30,7 @@ async function expectAccountFollows (options: {
return expectActorFollow({ ...options, data }) return expectActorFollow({ ...options, data })
} }
async function checkActorFilesWereRemoved (filename: string, server: PeerTubeServer) { export async function checkActorFilesWereRemoved (filename: string, server: PeerTubeServer) {
for (const directory of [ 'avatars' ]) { for (const directory of [ 'avatars' ]) {
const directoryPath = server.getDirectoryPath(directory) const directoryPath = server.getDirectoryPath(directory)
@ -44,12 +44,22 @@ async function checkActorFilesWereRemoved (filename: string, server: PeerTubeSer
} }
} }
export { export async function checkActorImage (actor: AccountSummary | VideoChannelSummary) {
expectAccountFollows, expect(actor.avatars).to.have.lengthOf(4)
expectChannelsFollows,
checkActorFilesWereRemoved for (const avatar of actor.avatars) {
expect(avatar.createdAt).to.exist
expect(avatar.fileUrl).to.exist
expect(avatar.height).to.be.greaterThan(0)
expect(avatar.width).to.be.greaterThan(0)
expect(avatar.updatedAt).to.exist
await makeRawRequest({ url: avatar.fileUrl, expectedStatus: HttpStatusCode.OK_200 })
}
} }
// ---------------------------------------------------------------------------
// Private
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function expectActorFollow (options: { function expectActorFollow (options: {

View file

@ -33,7 +33,7 @@ import { tmpdir } from 'os'
import { basename, join, resolve } from 'path' import { basename, join, resolve } from 'path'
import { testFileExistsOnFSOrNot } from './checks.js' import { testFileExistsOnFSOrNot } from './checks.js'
import { MockSmtpServer } from './mock-servers/mock-email.js' import { MockSmtpServer } from './mock-servers/mock-email.js'
import { getAllNotificationsSettings } from './notifications.js' import { getAllNotificationsSettings } from './notifications/notifications-common.js'
type ExportOutbox = ActivityPubOrderedCollection<ActivityCreate<VideoObject | VideoCommentObject>> type ExportOutbox = ActivityPubOrderedCollection<ActivityCreate<VideoObject | VideoCommentObject>>

View file

@ -66,7 +66,7 @@ class MockSmtpServer {
async kill () { async kill () {
if (!this.maildev) return if (!this.maildev) return
if (this.relayingEmail) { if (this.relayingEmail !== undefined) {
await this.relayingEmail await this.relayingEmail
} }

File diff suppressed because it is too large Load diff

View file

@ -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 })
}

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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 })
}

View file

@ -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 })
}

View file

@ -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 })
}

View 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
}
}

View file

@ -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)
}

View file

@ -61,28 +61,28 @@ activityPubClientRouter.get(
[ '/accounts?/:handle', '/accounts?/:handle/video-channels', '/a/:handle', '/a/:handle/video-channels' ], [ '/accounts?/:handle', '/accounts?/:handle/video-channels', '/a/:handle', '/a/:handle/video-channels' ],
executeIfActivityPub, executeIfActivityPub,
activityPubRateLimiter, activityPubRateLimiter,
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkManage: false })), asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkCanManage: false })),
asyncMiddleware(accountController) asyncMiddleware(accountController)
) )
activityPubClientRouter.get( activityPubClientRouter.get(
'/accounts?/:handle/followers', '/accounts?/:handle/followers',
executeIfActivityPub, executeIfActivityPub,
activityPubRateLimiter, activityPubRateLimiter,
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkManage: false })), asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkCanManage: false })),
asyncMiddleware(accountFollowersController) asyncMiddleware(accountFollowersController)
) )
activityPubClientRouter.get( activityPubClientRouter.get(
'/accounts?/:handle/following', '/accounts?/:handle/following',
executeIfActivityPub, executeIfActivityPub,
activityPubRateLimiter, activityPubRateLimiter,
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkManage: false })), asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkCanManage: false })),
asyncMiddleware(accountFollowingController) asyncMiddleware(accountFollowingController)
) )
activityPubClientRouter.get( activityPubClientRouter.get(
'/accounts?/:handle/playlists', '/accounts?/:handle/playlists',
executeIfActivityPub, executeIfActivityPub,
activityPubRateLimiter, activityPubRateLimiter,
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkManage: false })), asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkCanManage: false })),
asyncMiddleware(accountPlaylistsController) asyncMiddleware(accountPlaylistsController)
) )
activityPubClientRouter.get( activityPubClientRouter.get(
@ -212,35 +212,35 @@ activityPubClientRouter.get(
[ '/video-channels/:handle', '/video-channels/:handle/videos', '/c/:handle', '/c/:handle/videos' ], [ '/video-channels/:handle', '/video-channels/:handle/videos', '/c/:handle', '/c/:handle/videos' ],
executeIfActivityPub, executeIfActivityPub,
activityPubRateLimiter, activityPubRateLimiter,
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: false })), asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: false, checkIsOwner: false })),
asyncMiddleware(videoChannelController) asyncMiddleware(videoChannelController)
) )
activityPubClientRouter.get( activityPubClientRouter.get(
'/video-channels/:handle/followers', '/video-channels/:handle/followers',
executeIfActivityPub, executeIfActivityPub,
activityPubRateLimiter, activityPubRateLimiter,
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: false })), asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: false, checkIsOwner: false })),
asyncMiddleware(videoChannelFollowersController) asyncMiddleware(videoChannelFollowersController)
) )
activityPubClientRouter.get( activityPubClientRouter.get(
'/video-channels/:handle/following', '/video-channels/:handle/following',
executeIfActivityPub, executeIfActivityPub,
activityPubRateLimiter, activityPubRateLimiter,
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: false })), asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: false, checkIsOwner: false })),
asyncMiddleware(videoChannelFollowingController) asyncMiddleware(videoChannelFollowingController)
) )
activityPubClientRouter.get( activityPubClientRouter.get(
'/video-channels/:handle/playlists', '/video-channels/:handle/playlists',
executeIfActivityPub, executeIfActivityPub,
activityPubRateLimiter, activityPubRateLimiter,
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: false })), asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: false, checkIsOwner: false })),
asyncMiddleware(videoChannelPlaylistsController) asyncMiddleware(videoChannelPlaylistsController)
) )
activityPubClientRouter.get( activityPubClientRouter.get(
'/video-channels/:handle/player-settings', '/video-channels/:handle/player-settings',
executeIfActivityPub, executeIfActivityPub,
activityPubRateLimiter, activityPubRateLimiter,
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: false })), asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: false, checkIsOwner: false })),
asyncMiddleware(channelPlayerSettingsController) asyncMiddleware(channelPlayerSettingsController)
) )
@ -462,7 +462,7 @@ async function videoCommentController (req: express.Request, res: express.Respon
const videoComment = res.locals.videoCommentFull const videoComment = res.locals.videoCommentFull
if (redirectIfNotOwned(videoComment.url, res)) return if (redirectIfNotOwned(videoComment.url, res)) return
if (videoComment.Video.isOwned() && videoComment.heldForReview === true) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) if (videoComment.Video.isLocal() && videoComment.heldForReview === true) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
const threadParentComments = await VideoCommentModel.listThreadParentComments({ comment: videoComment }) const threadParentComments = await VideoCommentModel.listThreadParentComments({ comment: videoComment })
@ -484,7 +484,7 @@ async function videoCommentController (req: express.Request, res: express.Respon
async function videoCommentApprovedController (req: express.Request, res: express.Response) { async function videoCommentApprovedController (req: express.Request, res: express.Response) {
const comment = res.locals.videoCommentFull const comment = res.locals.videoCommentFull
if (!comment.Video.isOwned() || comment.heldForReview === true) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) if (!comment.Video.isLocal() || comment.heldForReview === true) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
const activity = buildApprovalActivity({ comment, type: 'ApproveReply' }) const activity = buildApprovalActivity({ comment, type: 'ApproveReply' })

View file

@ -29,7 +29,7 @@ inboxRouter.post(
activityPubRateLimiter, activityPubRateLimiter,
signatureValidator, signatureValidator,
asyncMiddleware(checkSignature), asyncMiddleware(checkSignature),
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkManage: false })), asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkCanManage: false })),
asyncMiddleware(activityPubValidator), asyncMiddleware(activityPubValidator),
inboxController inboxController
) )
@ -39,7 +39,7 @@ inboxRouter.post(
activityPubRateLimiter, activityPubRateLimiter,
signatureValidator, signatureValidator,
asyncMiddleware(checkSignature), asyncMiddleware(checkSignature),
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: false })), asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: false, checkIsOwner: false })),
asyncMiddleware(activityPubValidator), asyncMiddleware(activityPubValidator),
inboxController inboxController
) )

View file

@ -23,7 +23,7 @@ outboxRouter.get(
'/accounts/:handle/outbox', '/accounts/:handle/outbox',
activityPubRateLimiter, activityPubRateLimiter,
apPaginationValidator, apPaginationValidator,
accountHandleGetValidatorFactory({ checkIsLocal: true, checkManage: false }), accountHandleGetValidatorFactory({ checkIsLocal: true, checkCanManage: false }),
asyncMiddleware(outboxController) asyncMiddleware(outboxController)
) )
@ -31,7 +31,7 @@ outboxRouter.get(
'/video-channels/:handle/outbox', '/video-channels/:handle/outbox',
activityPubRateLimiter, activityPubRateLimiter,
apPaginationValidator, apPaginationValidator,
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: false })), asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: false, checkIsOwner: false })),
asyncMiddleware(outboxController) asyncMiddleware(outboxController)
) )

View file

@ -26,12 +26,16 @@ import {
accountHandleGetValidatorFactory, accountHandleGetValidatorFactory,
accountsFollowersSortValidator, accountsFollowersSortValidator,
accountsSortValidator, accountsSortValidator,
listAccountChannelsValidator,
videoChannelsSortValidator, videoChannelsSortValidator,
videoChannelStatsValidator,
videoChannelSyncsSortValidator, videoChannelSyncsSortValidator,
videosSortValidator videosSortValidator
} from '../../middlewares/validators/index.js' } from '../../middlewares/validators/index.js'
import { commonVideoPlaylistFiltersValidator, videoPlaylistsSearchValidator } from '../../middlewares/validators/videos/video-playlists.js' import {
commonVideoPlaylistFiltersValidator,
videoPlaylistsAccountValidator,
videoPlaylistsSearchValidator
} from '../../middlewares/validators/videos/video-playlists.js'
import { AccountVideoRateModel } from '../../models/account/account-video-rate.js' import { AccountVideoRateModel } from '../../models/account/account-video-rate.js'
import { AccountModel } from '../../models/account/account.js' import { AccountModel } from '../../models/account/account.js'
import { guessAdditionalAttributesFromQuery } from '../../models/video/formatter/index.js' import { guessAdditionalAttributesFromQuery } from '../../models/video/formatter/index.js'
@ -54,13 +58,13 @@ accountsRouter.get(
accountsRouter.get( accountsRouter.get(
'/:handle', '/:handle',
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: false, checkManage: false })), asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: false, checkCanManage: false })),
getAccount getAccount
) )
accountsRouter.get( accountsRouter.get(
'/:handle/videos', '/:handle/videos',
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: false, checkManage: false })), asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: false, checkCanManage: false })),
paginationValidator, paginationValidator,
videosSortValidator, videosSortValidator,
setDefaultVideosSort, setDefaultVideosSort,
@ -72,8 +76,8 @@ accountsRouter.get(
accountsRouter.get( accountsRouter.get(
'/:handle/video-channels', '/:handle/video-channels',
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: false, checkManage: false })), asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: false, checkCanManage: false })),
videoChannelStatsValidator, listAccountChannelsValidator,
paginationValidator, paginationValidator,
videoChannelsSortValidator, videoChannelsSortValidator,
setDefaultSort, setDefaultSort,
@ -84,20 +88,21 @@ accountsRouter.get(
accountsRouter.get( accountsRouter.get(
'/:handle/video-playlists', '/:handle/video-playlists',
optionalAuthenticate, optionalAuthenticate,
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: false, checkManage: false })), asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: false, checkCanManage: false })),
paginationValidator, paginationValidator,
videoPlaylistsSortValidator, videoPlaylistsSortValidator,
setDefaultSort, setDefaultSort,
setDefaultPagination, setDefaultPagination,
commonVideoPlaylistFiltersValidator, commonVideoPlaylistFiltersValidator,
videoPlaylistsSearchValidator, videoPlaylistsSearchValidator,
videoPlaylistsAccountValidator,
asyncMiddleware(listAccountPlaylists) asyncMiddleware(listAccountPlaylists)
) )
accountsRouter.get( accountsRouter.get(
'/:handle/video-channel-syncs', '/:handle/video-channel-syncs',
authenticate, authenticate,
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkManage: true })), asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkCanManage: true })),
paginationValidator, paginationValidator,
videoChannelSyncsSortValidator, videoChannelSyncsSortValidator,
setDefaultSort, setDefaultSort,
@ -108,7 +113,7 @@ accountsRouter.get(
accountsRouter.get( accountsRouter.get(
'/:handle/ratings', '/:handle/ratings',
authenticate, authenticate,
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkManage: true })), asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkCanManage: true })),
paginationValidator, paginationValidator,
videoRatesSortValidator, videoRatesSortValidator,
setDefaultSort, setDefaultSort,
@ -120,7 +125,7 @@ accountsRouter.get(
accountsRouter.get( accountsRouter.get(
'/:handle/followers', '/:handle/followers',
authenticate, authenticate,
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkManage: true })), asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: true, checkCanManage: true })),
paginationValidator, paginationValidator,
accountsFollowersSortValidator, accountsFollowersSortValidator,
setDefaultSort, setDefaultSort,
@ -153,16 +158,15 @@ async function listAccounts (req: express.Request, res: express.Response) {
} }
async function listAccountChannels (req: express.Request, res: express.Response) { async function listAccountChannels (req: express.Request, res: express.Response) {
const options = { const resultList = await VideoChannelModel.listByAccountForAPI({
accountId: res.locals.account.id, accountId: res.locals.account.id,
start: req.query.start, start: req.query.start,
count: req.query.count, count: req.query.count,
sort: req.query.sort, sort: req.query.sort,
withStats: req.query.withStats, withStats: req.query.withStats,
includeCollaborations: req.query.includeCollaborations,
search: req.query.search search: req.query.search
} })
const resultList = await VideoChannelModel.listByAccountForAPI(options)
return res.json(getFormattedObjects(resultList.data, resultList.total)) return res.json(getFormattedObjects(resultList.data, resultList.total))
} }
@ -183,7 +187,7 @@ async function listAccountChannelsSync (req: express.Request, res: express.Respo
async function listAccountPlaylists (req: express.Request, res: express.Response) { async function listAccountPlaylists (req: express.Request, res: express.Response) {
const serverActor = await getServerActor() const serverActor = await getServerActor()
const query = req.query as VideoPlaylistsListQuery const query = req.query as VideoPlaylistsListQuery & { includeCollaborations?: boolean }
// Allow users to see their private/unlisted video playlists // Allow users to see their private/unlisted video playlists
let listMyPlaylists = false let listMyPlaylists = false
@ -204,7 +208,9 @@ async function listAccountPlaylists (req: express.Request, res: express.Response
sort: query.sort, sort: query.sort,
search: query.search, search: query.search,
type: query.playlistType type: query.playlistType,
includeCollaborations: query.includeCollaborations
}) })
return res.json(getFormattedObjects(resultList.data, resultList.total)) return res.json(getFormattedObjects(resultList.data, resultList.total))
@ -260,7 +266,7 @@ async function listAccountRatings (req: express.Request, res: express.Response)
async function listAccountFollowers (req: express.Request, res: express.Response) { async function listAccountFollowers (req: express.Request, res: express.Response) {
const account = res.locals.account const account = res.locals.account
const channels = await VideoChannelModel.listAllByAccount(account.id) const channels = await VideoChannelModel.listAllOwnedByAccount(account.id)
const actorIds = [ account.actorId ].concat(channels.map(c => c.actorId)) const actorIds = [ account.actorId ].concat(channels.map(c => c.actorId))
const resultList = await ActorFollowModel.listFollowersForApi({ const resultList = await ActorFollowModel.listFollowersForApi({

View file

@ -21,7 +21,7 @@ import { searchRouter } from './search/index.js'
import { serverRouter } from './server/index.js' import { serverRouter } from './server/index.js'
import { usersRouter } from './users/index.js' import { usersRouter } from './users/index.js'
import { videoChannelSyncRouter } from './video-channel-sync.js' import { videoChannelSyncRouter } from './video-channel-sync.js'
import { videoChannelRouter } from './video-channel.js' import { videoChannelRouter } from './video-channels/index.js'
import { videoPlaylistRouter } from './video-playlist.js' import { videoPlaylistRouter } from './video-playlist.js'
import { videosRouter } from './videos/index.js' import { videosRouter } from './videos/index.js'
import { watchedWordsRouter } from './watched-words.js' import { watchedWordsRouter } from './watched-words.js'

View file

@ -1,4 +1,5 @@
import { PlayerChannelSettingsUpdate, PlayerVideoSettingsUpdate } from '@peertube/peertube-models' import { PlayerChannelSettingsUpdate, PlayerVideoSettingsUpdate } from '@peertube/peertube-models'
import { sendUpdateChannelPlayerSettings, sendUpdateVideoPlayerSettings } from '@server/lib/activitypub/send/send-update.js'
import { upsertPlayerSettings } from '@server/lib/player-settings.js' import { upsertPlayerSettings } from '@server/lib/player-settings.js'
import { import {
getChannelPlayerSettingsValidator, getChannelPlayerSettingsValidator,
@ -15,7 +16,6 @@ import {
optionalAuthenticate, optionalAuthenticate,
videoChannelsHandleValidatorFactory videoChannelsHandleValidatorFactory
} from '../../middlewares/index.js' } from '../../middlewares/index.js'
import { sendUpdateChannelPlayerSettings, sendUpdateVideoPlayerSettings } from '@server/lib/activitypub/send/send-update.js'
const playerSettingsRouter = express.Router() const playerSettingsRouter = express.Router()
@ -39,7 +39,7 @@ playerSettingsRouter.put(
playerSettingsRouter.get( playerSettingsRouter.get(
'/video-channels/:handle', '/video-channels/:handle',
optionalAuthenticate, optionalAuthenticate,
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: false, checkManage: false })), asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: false, checkCanManage: false, checkIsOwner: false })),
getChannelPlayerSettingsValidator, getChannelPlayerSettingsValidator,
asyncMiddleware(getChannelPlayerSettings) asyncMiddleware(getChannelPlayerSettings)
) )
@ -47,7 +47,7 @@ playerSettingsRouter.get(
playerSettingsRouter.put( playerSettingsRouter.put(
'/video-channels/:handle', '/video-channels/:handle',
authenticate, authenticate,
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: true })), asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: true, checkIsOwner: false })),
updatePlayerSettingsValidatorFactory('channel'), updatePlayerSettingsValidatorFactory('channel'),
asyncMiddleware(updateChannelPlayerSettings) asyncMiddleware(updateChannelPlayerSettings)
) )

View file

@ -29,7 +29,8 @@ import { searchLocalUrl } from './shared/index.js'
const searchChannelsRouter = express.Router() const searchChannelsRouter = express.Router()
searchChannelsRouter.get('/video-channels', searchChannelsRouter.get(
'/video-channels',
openapiOperationDoc({ operationId: 'searchChannels' }), openapiOperationDoc({ operationId: 'searchChannels' }),
paginationValidator, paginationValidator,
setDefaultPagination, setDefaultPagination,
@ -102,7 +103,7 @@ async function searchVideoChannelsDB (query: VideoChannelsSearchQueryAfterSaniti
}, 'filter:api.search.video-channels.local.list.params') }, 'filter:api.search.video-channels.local.list.params')
const resultList = await Hooks.wrapPromiseFun( const resultList = await Hooks.wrapPromiseFun(
VideoChannelModel.searchForApi.bind(VideoChannelModel), VideoChannelModel.listForApi.bind(VideoChannelModel),
apiOptions, apiOptions,
'filter:api.search.video-channels.local.list.result' 'filter:api.search.video-channels.local.list.result'
) )

View file

@ -56,10 +56,10 @@ const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_
const meRouter = express.Router() const meRouter = express.Router()
meRouter.get('/me', authenticate, asyncMiddleware(getUserInformation)) meRouter.get('/me', authenticate, asyncMiddleware(getMyInformation))
meRouter.delete('/me', authenticate, deleteMeValidator, asyncMiddleware(deleteMe)) meRouter.delete('/me', authenticate, deleteMeValidator, asyncMiddleware(deleteMe))
meRouter.get('/me/video-quota-used', authenticate, asyncMiddleware(getUserVideoQuotaUsed)) meRouter.get('/me/video-quota-used', authenticate, asyncMiddleware(getMyVideoQuotaUsed))
meRouter.get( meRouter.get(
'/me/videos/imports', '/me/videos/imports',
@ -69,7 +69,7 @@ meRouter.get(
setDefaultSort, setDefaultSort,
setDefaultPagination, setDefaultPagination,
getMyVideoImportsValidator, getMyVideoImportsValidator,
asyncMiddleware(getUserVideoImports) asyncMiddleware(listMyVideoImports)
) )
meRouter.get( meRouter.get(
@ -92,14 +92,14 @@ meRouter.get(
setDefaultPagination, setDefaultPagination,
commonVideosFiltersValidator, commonVideosFiltersValidator,
asyncMiddleware(usersVideosValidator), asyncMiddleware(usersVideosValidator),
asyncMiddleware(listUserVideos) asyncMiddleware(listMyVideos)
) )
meRouter.get( meRouter.get(
'/me/videos/:videoId/rating', '/me/videos/:videoId/rating',
authenticate, authenticate,
asyncMiddleware(usersVideoRatingValidator), asyncMiddleware(usersVideoRatingValidator),
asyncMiddleware(getUserVideoRating) asyncMiddleware(getMyVideoRating)
) )
meRouter.put( meRouter.put(
@ -131,14 +131,15 @@ export {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
async function listUserVideos (req: express.Request, res: express.Response) { async function listMyVideos (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User const user = res.locals.oauth.token.User
const countVideos = getCountVideos(req) const countVideos = getCountVideos(req)
const query = pickCommonVideoQuery(req.query) const query = pickCommonVideoQuery(req.query)
const include = (query.include || VideoInclude.NONE) | VideoInclude.BLACKLISTED | VideoInclude.NOT_PUBLISHED_STATE const include = (query.include || VideoInclude.NONE) | VideoInclude.BLACKLISTED | VideoInclude.NOT_PUBLISHED_STATE
const apiOptions = await Hooks.wrapObject({ const apiOptions = await Hooks.wrapObject(
{
privacyOneOf: getAllPrivacies(), privacyOneOf: getAllPrivacies(),
...query, ...query,
@ -152,11 +153,14 @@ async function listUserVideos (req: express.Request, res: express.Response) {
videoChannelId: res.locals.videoChannel?.id, videoChannelId: res.locals.videoChannel?.id,
channelNameOneOf: req.query.channelNameOneOf, channelNameOneOf: req.query.channelNameOneOf,
includeCollaborations: req.query.includeCollaborations || false,
countVideos, countVideos,
include include
}, 'filter:api.user.me.videos.list.params') } satisfies Parameters<typeof VideoModel.listForApi>[0],
'filter:api.user.me.videos.list.params'
)
const resultList = await Hooks.wrapPromiseFun( const resultList = await Hooks.wrapPromiseFun(
VideoModel.listForApi.bind(VideoModel), VideoModel.listForApi.bind(VideoModel),
@ -170,7 +174,7 @@ async function listUserVideos (req: express.Request, res: express.Response) {
async function listCommentsOnUserVideos (req: express.Request, res: express.Response) { async function listCommentsOnUserVideos (req: express.Request, res: express.Response) {
const userAccount = res.locals.oauth.token.User.Account const userAccount = res.locals.oauth.token.User.Account
const options = { const resultList = await VideoCommentModel.listCommentsForApi({
...pick(req.query, [ ...pick(req.query, [
'start', 'start',
'count', 'count',
@ -182,14 +186,15 @@ async function listCommentsOnUserVideos (req: express.Request, res: express.Resp
]), ]),
autoTagOfAccountId: userAccount.id, autoTagOfAccountId: userAccount.id,
videoAccountOwnerId: userAccount.id, videoAccountOwnerId: userAccount.id,
videoAccountOwnerIncludeCollaborations: req.query.includeCollaborations || false,
heldForReview: req.query.isHeldForReview, heldForReview: req.query.isHeldForReview,
videoChannelOwnerId: res.locals.videoChannel?.id, videoChannelOwnerId: res.locals.videoChannel?.id,
videoId: res.locals.videoAll?.id videoId: res.locals.videoAll?.id
} })
const resultList = await VideoCommentModel.listCommentsForApi(options)
return res.json({ return res.json({
total: resultList.total, total: resultList.total,
@ -197,7 +202,7 @@ async function listCommentsOnUserVideos (req: express.Request, res: express.Resp
}) })
} }
async function getUserVideoImports (req: express.Request, res: express.Response) { async function listMyVideoImports (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User const user = res.locals.oauth.token.User
const resultList = await VideoImportModel.listUserVideoImportsForApi({ const resultList = await VideoImportModel.listUserVideoImportsForApi({
userId: user.id, userId: user.id,
@ -208,7 +213,7 @@ async function getUserVideoImports (req: express.Request, res: express.Response)
return res.json(getFormattedObjects(resultList.data, resultList.total)) return res.json(getFormattedObjects(resultList.data, resultList.total))
} }
async function getUserInformation (req: express.Request, res: express.Response) { async function getMyInformation (req: express.Request, res: express.Response) {
// We did not load channels in res.locals.user // We did not load channels in res.locals.user
const user = await UserModel.loadForMeAPI(res.locals.oauth.token.user.id) const user = await UserModel.loadForMeAPI(res.locals.oauth.token.user.id)
@ -221,7 +226,7 @@ async function getUserInformation (req: express.Request, res: express.Response)
return res.json(result) return res.json(result)
} }
async function getUserVideoQuotaUsed (req: express.Request, res: express.Response) { async function getMyVideoQuotaUsed (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.user const user = res.locals.oauth.token.user
const videoQuotaUsed = await getOriginalVideoFileTotalFromUser(user) const videoQuotaUsed = await getOriginalVideoFileTotalFromUser(user)
const videoQuotaUsedDaily = await getOriginalVideoFileTotalDailyFromUser(user) const videoQuotaUsedDaily = await getOriginalVideoFileTotalDailyFromUser(user)
@ -233,7 +238,7 @@ async function getUserVideoQuotaUsed (req: express.Request, res: express.Respons
return res.json(data) return res.json(data)
} }
async function getUserVideoRating (req: express.Request, res: express.Response) { async function getMyVideoRating (req: express.Request, res: express.Response) {
const videoId = res.locals.videoId.id const videoId = res.locals.videoId.id
const accountId = +res.locals.oauth.token.User.Account.id const accountId = +res.locals.oauth.token.User.Account.id

View file

@ -14,17 +14,17 @@ import { ActorFollowModel } from '@server/models/actor/actor-follow.js'
import { getServerActor } from '@server/models/application/application.js' import { getServerActor } from '@server/models/application/application.js'
import { MChannelBannerAccountDefault } from '@server/types/models/index.js' import { MChannelBannerAccountDefault } from '@server/types/models/index.js'
import express from 'express' import express from 'express'
import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger.js' import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../../helpers/audit-logger.js'
import { resetSequelizeInstance } from '../../helpers/database-utils.js' import { resetSequelizeInstance } from '../../../helpers/database-utils.js'
import { buildNSFWFilters, createReqFiles, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils.js' import { buildNSFWFilters, createReqFiles, getCountVideos, isUserAbleToSearchRemoteURI } from '../../../helpers/express-utils.js'
import { logger } from '../../helpers/logger.js' import { logger } from '../../../helpers/logger.js'
import { getFormattedObjects } from '../../helpers/utils.js' import { getFormattedObjects } from '../../../helpers/utils.js'
import { MIMETYPES } from '../../initializers/constants.js' import { MIMETYPES } from '../../../initializers/constants.js'
import { sequelizeTypescript } from '../../initializers/database.js' import { sequelizeTypescript } from '../../../initializers/database.js'
import { sendUpdateActor } from '../../lib/activitypub/send/index.js' import { sendUpdateActor } from '../../../lib/activitypub/send/index.js'
import { JobQueue } from '../../lib/job-queue/index.js' import { JobQueue } from '../../../lib/job-queue/index.js'
import { deleteLocalActorImageFile, updateLocalActorImageFiles } from '../../lib/local-actor.js' import { deleteLocalActorImageFile, updateLocalActorImageFiles } from '../../../lib/local-actor.js'
import { createLocalVideoChannelWithoutKeys, federateAllVideosOfChannel } from '../../lib/video-channel.js' import { createLocalVideoChannelWithoutKeys, federateAllVideosOfChannel } from '../../../lib/video-channel.js'
import { import {
apiRateLimiter, apiRateLimiter,
asyncMiddleware, asyncMiddleware,
@ -41,8 +41,8 @@ import {
videoChannelsSortValidator, videoChannelsSortValidator,
videoChannelsUpdateValidator, videoChannelsUpdateValidator,
videoPlaylistsSortValidator videoPlaylistsSortValidator
} from '../../middlewares/index.js' } from '../../../middlewares/index.js'
import { updateAvatarValidator, updateBannerValidator } from '../../middlewares/validators/actor-image.js' import { updateAvatarValidator, updateBannerValidator } from '../../../middlewares/validators/actor-image.js'
import { import {
ensureChannelOwnerCanUpload, ensureChannelOwnerCanUpload,
videoChannelImportVideosValidator, videoChannelImportVideosValidator,
@ -50,16 +50,17 @@ import {
videoChannelsHandleValidatorFactory, videoChannelsHandleValidatorFactory,
videoChannelsListValidator, videoChannelsListValidator,
videosSortValidator videosSortValidator
} from '../../middlewares/validators/index.js' } from '../../../middlewares/validators/index.js'
import { import {
commonVideoPlaylistFiltersValidator, commonVideoPlaylistFiltersValidator,
videoPlaylistsReorderInChannelValidator videoPlaylistsReorderInChannelValidator
} from '../../middlewares/validators/videos/video-playlists.js' } from '../../../middlewares/validators/videos/video-playlists.js'
import { AccountModel } from '../../models/account/account.js' import { AccountModel } from '../../../models/account/account.js'
import { guessAdditionalAttributesFromQuery } from '../../models/video/formatter/index.js' import { guessAdditionalAttributesFromQuery } from '../../../models/video/formatter/index.js'
import { VideoChannelModel } from '../../models/video/video-channel.js' import { VideoChannelModel } from '../../../models/video/video-channel.js'
import { VideoPlaylistModel } from '../../models/video/video-playlist.js' import { VideoPlaylistModel } from '../../../models/video/video-playlist.js'
import { VideoModel } from '../../models/video/video.js' import { VideoModel } from '../../../models/video/video.js'
import { channelCollaborators } from './video-channel-collaborators.js'
const auditLogger = auditLoggerFactory('channels') const auditLogger = auditLoggerFactory('channels')
const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT) const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT)
@ -68,6 +69,7 @@ const reqBannerFile = createReqFiles([ 'bannerfile' ], MIMETYPES.IMAGE.MIMETYPE_
const videoChannelRouter = express.Router() const videoChannelRouter = express.Router()
videoChannelRouter.use(apiRateLimiter) videoChannelRouter.use(apiRateLimiter)
videoChannelRouter.use(channelCollaborators)
videoChannelRouter.get( videoChannelRouter.get(
'/', '/',
@ -85,7 +87,7 @@ videoChannelRouter.post(
'/:handle/avatar/pick', '/:handle/avatar/pick',
authenticate, authenticate,
reqAvatarFile, reqAvatarFile,
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: true })), asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: true, checkIsOwner: false })),
updateAvatarValidator, updateAvatarValidator,
asyncMiddleware(updateVideoChannelAvatar) asyncMiddleware(updateVideoChannelAvatar)
) )
@ -94,7 +96,7 @@ videoChannelRouter.post(
'/:handle/banner/pick', '/:handle/banner/pick',
authenticate, authenticate,
reqBannerFile, reqBannerFile,
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: true })), asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: true, checkIsOwner: false })),
updateBannerValidator, updateBannerValidator,
asyncMiddleware(updateVideoChannelBanner) asyncMiddleware(updateVideoChannelBanner)
) )
@ -102,21 +104,21 @@ videoChannelRouter.post(
videoChannelRouter.delete( videoChannelRouter.delete(
'/:handle/avatar', '/:handle/avatar',
authenticate, authenticate,
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: true })), asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: true, checkIsOwner: false })),
asyncMiddleware(deleteVideoChannelAvatar) asyncMiddleware(deleteVideoChannelAvatar)
) )
videoChannelRouter.delete( videoChannelRouter.delete(
'/:handle/banner', '/:handle/banner',
authenticate, authenticate,
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: true })), asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: true, checkIsOwner: false })),
asyncMiddleware(deleteVideoChannelBanner) asyncMiddleware(deleteVideoChannelBanner)
) )
videoChannelRouter.put( videoChannelRouter.put(
'/:handle', '/:handle',
authenticate, authenticate,
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: true })), asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: true, checkIsOwner: false })),
videoChannelsUpdateValidator, videoChannelsUpdateValidator,
asyncRetryTransactionMiddleware(updateVideoChannel) asyncRetryTransactionMiddleware(updateVideoChannel)
) )
@ -124,14 +126,14 @@ videoChannelRouter.put(
videoChannelRouter.delete( videoChannelRouter.delete(
'/:handle', '/:handle',
authenticate, authenticate,
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: true })), asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: true, checkIsOwner: true })),
asyncMiddleware(videoChannelsRemoveValidator), asyncMiddleware(videoChannelsRemoveValidator),
asyncRetryTransactionMiddleware(removeVideoChannel) asyncRetryTransactionMiddleware(removeVideoChannel)
) )
videoChannelRouter.get( videoChannelRouter.get(
'/:handle', '/:handle',
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: false, checkManage: false })), asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: false, checkCanManage: false, checkIsOwner: false })),
asyncMiddleware(getVideoChannel) asyncMiddleware(getVideoChannel)
) )
@ -140,7 +142,7 @@ videoChannelRouter.get(
videoChannelRouter.get( videoChannelRouter.get(
'/:handle/video-playlists', '/:handle/video-playlists',
optionalAuthenticate, optionalAuthenticate,
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: false, checkManage: false })), asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: false, checkCanManage: false, checkIsOwner: false })),
paginationValidator, paginationValidator,
videoPlaylistsSortValidator, videoPlaylistsSortValidator,
setDefaultSort, setDefaultSort,
@ -152,7 +154,7 @@ videoChannelRouter.get(
videoChannelRouter.post( videoChannelRouter.post(
'/:handle/video-playlists/reorder', '/:handle/video-playlists/reorder',
authenticate, authenticate,
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: true })), asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: true, checkIsOwner: false })),
asyncMiddleware(videoPlaylistsReorderInChannelValidator), asyncMiddleware(videoPlaylistsReorderInChannelValidator),
asyncRetryTransactionMiddleware(reorderPlaylistsInChannel) asyncRetryTransactionMiddleware(reorderPlaylistsInChannel)
) )
@ -161,7 +163,7 @@ videoChannelRouter.post(
videoChannelRouter.get( videoChannelRouter.get(
'/:handle/videos', '/:handle/videos',
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: false, checkManage: false })), asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: false, checkCanManage: false, checkIsOwner: false })),
paginationValidator, paginationValidator,
videosSortValidator, videosSortValidator,
setDefaultVideosSort, setDefaultVideosSort,
@ -174,7 +176,7 @@ videoChannelRouter.get(
videoChannelRouter.get( videoChannelRouter.get(
'/:handle/followers', '/:handle/followers',
authenticate, authenticate,
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: false, checkManage: true })), asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: false, checkCanManage: true, checkIsOwner: false })),
paginationValidator, paginationValidator,
videoChannelsFollowersSortValidator, videoChannelsFollowersSortValidator,
setDefaultSort, setDefaultSort,
@ -185,7 +187,7 @@ videoChannelRouter.get(
videoChannelRouter.post( videoChannelRouter.post(
'/:handle/import-videos', '/:handle/import-videos',
authenticate, authenticate,
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkManage: true })), asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: true, checkIsOwner: false })),
asyncMiddleware(videoChannelImportVideosValidator), asyncMiddleware(videoChannelImportVideosValidator),
asyncMiddleware(ensureChannelOwnerCanUpload), asyncMiddleware(ensureChannelOwnerCanUpload),
asyncMiddleware(importVideosInChannel) asyncMiddleware(importVideosInChannel)

View file

@ -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)
}

View file

@ -109,7 +109,7 @@ videoPlaylistRouter.get(
paginationValidator, paginationValidator,
setDefaultPagination, setDefaultPagination,
optionalAuthenticate, optionalAuthenticate,
asyncMiddleware(getVideoPlaylistVideos) asyncMiddleware(listVideosOfPlaylist)
) )
videoPlaylistRouter.post( videoPlaylistRouter.post(
@ -517,7 +517,7 @@ async function reorderVideosOfPlaylist (req: express.Request, res: express.Respo
return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end() return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end()
} }
async function getVideoPlaylistVideos (req: express.Request, res: express.Response) { async function listVideosOfPlaylist (req: express.Request, res: express.Response) {
const videoPlaylistInstance = res.locals.videoPlaylistSummary const videoPlaylistInstance = res.locals.videoPlaylistSummary
const user = res.locals.oauth ? res.locals.oauth.token.User : undefined const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
const server = await getServerActor() const server = await getServerActor()

View file

@ -10,6 +10,7 @@ import { sequelizeTypescript } from '@server/initializers/database.js'
import { federateVideoIfNeeded } from '@server/lib/activitypub/videos/index.js' import { federateVideoIfNeeded } from '@server/lib/activitypub/videos/index.js'
import { LocalVideoCreator } from '@server/lib/local-video-creator.js' import { LocalVideoCreator } from '@server/lib/local-video-creator.js'
import { Hooks } from '@server/lib/plugins/hooks.js' import { Hooks } from '@server/lib/plugins/hooks.js'
import { checkCanManageVideo } from '@server/middlewares/validators/shared/videos.js'
import { import {
videoLiveAddValidator, videoLiveAddValidator,
videoLiveFindReplaySessionValidator, videoLiveFindReplaySessionValidator,
@ -44,21 +45,30 @@ liveRouter.get(
'/live/:videoId/sessions', '/live/:videoId/sessions',
authenticate, authenticate,
asyncMiddleware(videoLiveGetValidator), asyncMiddleware(videoLiveGetValidator),
videoLiveListSessionsValidator, asyncMiddleware(videoLiveListSessionsValidator),
asyncMiddleware(getLiveVideoSessions) asyncMiddleware(getLiveVideoSessions)
) )
liveRouter.get('/live/:videoId', optionalAuthenticate, asyncMiddleware(videoLiveGetValidator), getLiveVideo) liveRouter.get(
'/live/:videoId',
optionalAuthenticate,
asyncMiddleware(videoLiveGetValidator),
asyncMiddleware(getLiveVideo)
)
liveRouter.put( liveRouter.put(
'/live/:videoId', '/live/:videoId',
authenticate, authenticate,
asyncMiddleware(videoLiveGetValidator), asyncMiddleware(videoLiveGetValidator),
videoLiveUpdateValidator, asyncMiddleware(videoLiveUpdateValidator),
asyncRetryTransactionMiddleware(updateLiveVideo) asyncRetryTransactionMiddleware(updateLiveVideo)
) )
liveRouter.get('/:videoId/live-session', asyncMiddleware(videoLiveFindReplaySessionValidator), getLiveReplaySession) liveRouter.get(
'/:videoId/live-session',
asyncMiddleware(videoLiveFindReplaySessionValidator),
getLiveReplaySession
)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -68,10 +78,10 @@ export {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function getLiveVideo (req: express.Request, res: express.Response) { async function getLiveVideo (req: express.Request, res: express.Response) {
const videoLive = res.locals.videoLive const videoLive = res.locals.videoLive
return res.json(videoLive.toFormattedJSON(canSeePrivateLiveInformation(res))) return res.json(videoLive.toFormattedJSON(await canSeePrivateLiveInformation(req, res)))
} }
function getLiveReplaySession (req: express.Request, res: express.Response) { function getLiveReplaySession (req: express.Request, res: express.Response) {
@ -88,14 +98,16 @@ async function getLiveVideoSessions (req: express.Request, res: express.Response
return res.json(getFormattedObjects(data, data.length)) return res.json(getFormattedObjects(data, data.length))
} }
function canSeePrivateLiveInformation (res: express.Response) { function canSeePrivateLiveInformation (req: express.Request, res: express.Response) {
const user = res.locals.oauth?.token.User return checkCanManageVideo({
if (!user) return false user: res.locals.oauth?.token.User,
video: res.locals.videoAll,
if (user.hasRight(UserRight.GET_ANY_LIVE)) return true right: UserRight.GET_ANY_LIVE,
req,
const video = res.locals.videoAll res: null,
return video.VideoChannel.Account.userId === user.id checkIsLocal: true,
checkIsOwner: false
})
} }
async function updateLiveVideo (req: express.Request, res: express.Response) { async function updateLiveVideo (req: express.Request, res: express.Response) {

View file

@ -12,7 +12,7 @@ servicesRouter.use('/oembed', cors(), apiRateLimiter, asyncMiddleware(oembedVali
servicesRouter.use( servicesRouter.use(
'/redirect/accounts/:handle', '/redirect/accounts/:handle',
apiRateLimiter, apiRateLimiter,
asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: false, checkManage: false })), asyncMiddleware(accountHandleGetValidatorFactory({ checkIsLocal: false, checkCanManage: false })),
redirectToAccountUrl redirectToAccountUrl
) )

View file

@ -1,20 +1,25 @@
import { Response } from 'express' import { HttpStatusCode, UserRight } from '@peertube/peertube-models'
import { HttpStatusCode } from '@peertube/peertube-models' import { checkCanManageAccount } from '@server/middlewares/validators/shared/users.js'
import { MUserId } from '@server/types/models/index.js' import { MUserAccountId } from '@server/types/models/index.js'
import { MVideoChangeOwnershipFull } from '@server/types/models/video/video-change-ownership.js' import { MVideoChangeOwnershipFull } from '@server/types/models/video/video-change-ownership.js'
import { Request, Response } from 'express'
function checkUserCanTerminateOwnershipChange (user: MUserId, videoChangeOwnership: MVideoChangeOwnershipFull, res: Response) { export function checkCanTerminateOwnershipChange (options: {
if (videoChangeOwnership.NextOwner.userId === user.id) { user: MUserAccountId
return true videoChangeOwnership: MVideoChangeOwnershipFull
} req: Request
res: Response
}) {
const { user, videoChangeOwnership, req, res } = options
if (!checkCanManageAccount({ user, account: videoChangeOwnership.NextOwner, req, res: null, specialRight: UserRight.MANAGE_USERS })) {
res.fail({ res.fail({
status: HttpStatusCode.FORBIDDEN_403, status: HttpStatusCode.FORBIDDEN_403,
message: 'Cannot terminate an ownership change of another user' message: req.t('Cannot terminate an ownership change of another user')
}) })
return false return false
} }
export { return true
checkUserCanTerminateOwnershipChange
} }

View file

@ -2,7 +2,7 @@ import { REMOTE_SCHEME, WEBSERVER } from '../../initializers/constants.js'
import { sanitizeHost } from '../core-utils.js' import { sanitizeHost } from '../core-utils.js'
import { exists } from './misc.js' import { exists } from './misc.js'
function isWebfingerLocalResourceValid (value: string) { export function isWebfingerLocalResourceValid (value: string) {
if (!exists(value)) return false if (!exists(value)) return false
if (value.startsWith('acct:') === false) return false if (value.startsWith('acct:') === false) return false
@ -13,9 +13,3 @@ function isWebfingerLocalResourceValid (value: string) {
const host = actorParts[1] const host = actorParts[1]
return sanitizeHost(host, REMOTE_SCHEME.HTTP) === WEBSERVER.HOST return sanitizeHost(host, REMOTE_SCHEME.HTTP) === WEBSERVER.HOST
} }
// ---------------------------------------------------------------------------
export {
isWebfingerLocalResourceValid
}

View file

@ -5,7 +5,7 @@ import { Model } from 'sequelize-typescript'
import { sequelizeTypescript } from '@server/initializers/database.js' import { sequelizeTypescript } from '@server/initializers/database.js'
import { logger } from './logger.js' import { logger } from './logger.js'
function retryTransactionWrapper<T, A, B, C, D> ( export function retryTransactionWrapper<T, A, B, C, D> (
functionToRetry: (arg1: A, arg2: B, arg3: C, arg4: D) => Promise<T>, functionToRetry: (arg1: A, arg2: B, arg3: C, arg4: D) => Promise<T>,
arg1: A, arg1: A,
arg2: B, arg2: B,
@ -13,29 +13,29 @@ function retryTransactionWrapper<T, A, B, C, D> (
arg4: D arg4: D
): Promise<T> ): Promise<T>
function retryTransactionWrapper<T, A, B, C> ( export function retryTransactionWrapper<T, A, B, C> (
functionToRetry: (arg1: A, arg2: B, arg3: C) => Promise<T>, functionToRetry: (arg1: A, arg2: B, arg3: C) => Promise<T>,
arg1: A, arg1: A,
arg2: B, arg2: B,
arg3: C arg3: C
): Promise<T> ): Promise<T>
function retryTransactionWrapper<T, A, B> ( export function retryTransactionWrapper<T, A, B> (
functionToRetry: (arg1: A, arg2: B) => Promise<T>, functionToRetry: (arg1: A, arg2: B) => Promise<T>,
arg1: A, arg1: A,
arg2: B arg2: B
): Promise<T> ): Promise<T>
function retryTransactionWrapper<T, A> ( export function retryTransactionWrapper<T, A> (
functionToRetry: (arg1: A) => Promise<T>, functionToRetry: (arg1: A) => Promise<T>,
arg1: A arg1: A
): Promise<T> ): Promise<T>
function retryTransactionWrapper<T> ( export function retryTransactionWrapper<T> (
functionToRetry: () => Promise<T> | Bluebird<T> functionToRetry: () => Promise<T> | Bluebird<T>
): Promise<T> ): Promise<T>
function retryTransactionWrapper<T> ( export function retryTransactionWrapper<T> (
functionToRetry: (...args: any[]) => Promise<T>, functionToRetry: (...args: any[]) => Promise<T>,
...args: any[] ...args: any[]
): Promise<T> { ): Promise<T> {
@ -50,7 +50,7 @@ function retryTransactionWrapper<T> (
}) })
} }
function transactionRetryer<T> (func: (err: any, data: T) => any) { export function transactionRetryer<T> (func: (err: any, data: T) => any) {
return new Promise<T>((res, rej) => { return new Promise<T>((res, rej) => {
retry( retry(
{ {
@ -68,7 +68,7 @@ function transactionRetryer<T> (func: (err: any, data: T) => any) {
}) })
} }
function saveInTransactionWithRetries<T extends Pick<Model, 'save' | 'changed'>> (model: T) { export function saveInTransactionWithRetries<T extends Pick<Model, 'save' | 'changed'>> (model: T) {
const changedKeys = model.changed() || [] const changedKeys = model.changed() || []
return retryTransactionWrapper(() => { return retryTransactionWrapper(() => {
@ -87,46 +87,41 @@ function saveInTransactionWithRetries<T extends Pick<Model, 'save' | 'changed'>>
}) })
} }
export function deleteInTransactionWithRetries<T extends Pick<Model, 'destroy'>> (model: T) {
return retryTransactionWrapper(() => {
return sequelizeTypescript.transaction(async transaction => {
await model.destroy({ transaction })
})
})
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function resetSequelizeInstance<T> (instance: Model<T>) { export function resetSequelizeInstance<T> (instance: Model<T>) {
return instance.reload() return instance.reload()
} }
function filterNonExistingModels<T extends { hasSameUniqueKeysThan(other: T): boolean }> ( export function filterNonExistingModels<T extends { hasSameUniqueKeysThan(other: T): boolean }> (
fromDatabase: T[], fromDatabase: T[],
newModels: T[] newModels: T[]
) { ) {
return fromDatabase.filter(f => !newModels.find(newModel => newModel.hasSameUniqueKeysThan(f))) return fromDatabase.filter(f => !newModels.find(newModel => newModel.hasSameUniqueKeysThan(f)))
} }
function deleteAllModels<T extends Pick<Model, 'destroy'>> (models: T[], transaction: Transaction) { export function deleteAllModels<T extends Pick<Model, 'destroy'>> (models: T[], transaction: Transaction) {
return Promise.all(models.map(f => f.destroy({ transaction }))) return Promise.all(models.map(f => f.destroy({ transaction })))
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function runInReadCommittedTransaction<T> (fn: (t: Transaction) => Promise<T>) { export function runInReadCommittedTransaction<T> (fn: (t: Transaction) => Promise<T>) {
const options = { isolationLevel: Transaction.ISOLATION_LEVELS.READ_COMMITTED } const options = { isolationLevel: Transaction.ISOLATION_LEVELS.READ_COMMITTED }
return sequelizeTypescript.transaction(options, t => fn(t)) return sequelizeTypescript.transaction(options, t => fn(t))
} }
function afterCommitIfTransaction (t: Transaction, fn: Function) { export function afterCommitIfTransaction (t: Transaction, fn: Function) {
if (t) return t.afterCommit(() => fn()) if (t) return t.afterCommit(() => fn())
return fn() return fn()
} }
// ---------------------------------------------------------------------------
export {
resetSequelizeInstance,
retryTransactionWrapper,
transactionRetryer,
saveInTransactionWithRetries,
afterCommitIfTransaction,
filterNonExistingModels,
deleteAllModels,
runInReadCommittedTransaction
}

View file

@ -30,7 +30,10 @@ export class GeoIP {
if (CONFIG.GEO_IP.ENABLED === false) return emptyResult if (CONFIG.GEO_IP.ENABLED === false) return emptyResult
try { try {
if (!this.initReadersPromise) this.initReadersPromise = this.initReadersIfNeeded() if (this.initReadersPromise === undefined) {
this.initReadersPromise = this.initReadersIfNeeded()
}
await this.initReadersPromise await this.initReadersPromise
this.initReadersPromise = undefined this.initReadersPromise = undefined

View file

@ -3,13 +3,13 @@ import { WEBSERVER } from '@server/initializers/constants.js'
import { actorNameAlphabet } from './custom-validators/activitypub/actor.js' import { actorNameAlphabet } from './custom-validators/activitypub/actor.js'
import { regexpCapture } from './regexp.js' import { regexpCapture } from './regexp.js'
export function extractMentions (text: string, isOwned: boolean) { export function extractMentions (text: string, isLocal: boolean) {
let result: string[] = [] let result: string[] = []
const localMention = `@(${actorNameAlphabet}+)` const localMention = `@(${actorNameAlphabet}+)`
const remoteMention = `${localMention}@${WEBSERVER.HOST}` const remoteMention = `${localMention}@${WEBSERVER.HOST}`
const mentionRegex = isOwned const mentionRegex = isLocal
? '(?:(?:' + remoteMention + ')|(?:' + localMention + '))' // Include local mentions? ? '(?:(?:' + remoteMention + ')|(?:' + localMention + '))' // Include local mentions?
: '(?:' + remoteMention + ')' : '(?:' + remoteMention + ')'
@ -20,16 +20,14 @@ export function extractMentions (text: string, isOwned: boolean) {
result = result.concat( result = result.concat(
regexpCapture(text, firstMentionRegex) regexpCapture(text, firstMentionRegex)
.map(([ , username1, username2 ]) => username1 || username2), .map(([ , username1, username2 ]) => username1 || username2),
regexpCapture(text, endMentionRegex) regexpCapture(text, endMentionRegex)
.map(([ , username1, username2 ]) => username1 || username2), .map(([ , username1, username2 ]) => username1 || username2),
regexpCapture(text, remoteMentionsRegex) regexpCapture(text, remoteMentionsRegex)
.map(([ , username ]) => username) .map(([ , username ]) => username)
) )
// Include local mentions // Include local mentions
if (isOwned) { if (isLocal) {
const localMentionsRegex = new RegExp(' ' + localMention + ' ', 'g') const localMentionsRegex = new RegExp(' ' + localMention + ' ', 'g')
result = result.concat( result = result.concat(

View file

@ -20,6 +20,8 @@ import {
UserImportStateType, UserImportStateType,
UserRegistrationState, UserRegistrationState,
UserRegistrationStateType, UserRegistrationStateType,
VideoChannelCollaboratorState,
VideoChannelCollaboratorStateType,
VideoChannelSyncState, VideoChannelSyncState,
VideoChannelSyncStateType, VideoChannelSyncStateType,
VideoCommentPolicy, VideoCommentPolicy,
@ -52,7 +54,7 @@ import { CONFIG, registerConfigChangedHandler } from './config.js'
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export const LAST_MIGRATION_VERSION = 930 export const LAST_MIGRATION_VERSION = 935
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -690,6 +692,11 @@ export const VIDEO_COMMENTS_POLICY: { [id in VideoCommentPolicyType]: string } =
[VideoCommentPolicy.REQUIRES_APPROVAL]: 'Requires approval' [VideoCommentPolicy.REQUIRES_APPROVAL]: 'Requires approval'
} }
export const CHANNEL_COLLABORATOR_STATE: { [id in VideoChannelCollaboratorStateType]: string } = {
[VideoChannelCollaboratorState.ACCEPTED]: 'Accepted',
[VideoChannelCollaboratorState.PENDING]: 'Pending'
}
export const MIMETYPES = { export const MIMETYPES = {
AUDIO: { AUDIO: {
MIMETYPE_EXT: { MIMETYPE_EXT: {

View file

@ -16,7 +16,9 @@ import { UserNotificationModel } from '@server/models/user/user-notification.js'
import { UserRegistrationModel } from '@server/models/user/user-registration.js' import { UserRegistrationModel } from '@server/models/user/user-registration.js'
import { UserVideoHistoryModel } from '@server/models/user/user-video-history.js' import { UserVideoHistoryModel } from '@server/models/user/user-video-history.js'
import { UserModel } from '@server/models/user/user.js' import { UserModel } from '@server/models/user/user.js'
import { PlayerSettingModel } from '@server/models/video/player-setting.js'
import { StoryboardModel } from '@server/models/video/storyboard.js' import { StoryboardModel } from '@server/models/video/storyboard.js'
import { VideoChannelCollaboratorModel } from '@server/models/video/video-channel-collaborator.js'
import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync.js' import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync.js'
import { VideoChapterModel } from '@server/models/video/video-chapter.js' import { VideoChapterModel } from '@server/models/video/video-chapter.js'
import { VideoJobInfoModel } from '@server/models/video/video-job-info.js' import { VideoJobInfoModel } from '@server/models/video/video-job-info.js'
@ -69,7 +71,6 @@ import { VideoTagModel } from '../models/video/video-tag.js'
import { VideoModel } from '../models/video/video.js' import { VideoModel } from '../models/video/video.js'
import { VideoViewModel } from '../models/view/video-view.js' import { VideoViewModel } from '../models/view/video-view.js'
import { CONFIG } from './config.js' import { CONFIG } from './config.js'
import { PlayerSettingModel } from '@server/models/video/player-setting.js'
pg.defaults.parseInt8 = true // Avoid BIGINT to be converted to string pg.defaults.parseInt8 = true // Avoid BIGINT to be converted to string
@ -191,7 +192,8 @@ export async function initDatabaseModels (silent: boolean) {
AccountAutomaticTagPolicyModel, AccountAutomaticTagPolicyModel,
UploadImageModel, UploadImageModel,
VideoLiveScheduleModel, VideoLiveScheduleModel,
PlayerSettingModel PlayerSettingModel,
VideoChannelCollaboratorModel
]) ])
// Check extensions exist in the database // Check extensions exist in the database

View file

@ -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
}

View file

@ -9,13 +9,7 @@ import { updateRemotePlaylistMiniatureFromUrl } from '@server/lib/thumbnail.js'
import { VideoPlaylistElementModel } from '@server/models/video/video-playlist-element.js' import { VideoPlaylistElementModel } from '@server/models/video/video-playlist-element.js'
import { VideoPlaylistModel } from '@server/models/video/video-playlist.js' import { VideoPlaylistModel } from '@server/models/video/video-playlist.js'
import { FilteredModelAttributes } from '@server/types/index.js' import { FilteredModelAttributes } from '@server/types/index.js'
import { import { MAccountHost, MThumbnail, MVideoPlaylist, MVideoPlaylistFull, MVideoPlaylistVideosLength } from '@server/types/models/index.js'
MAccountHost,
MThumbnail,
MVideoPlaylist,
MVideoPlaylistFull,
MVideoPlaylistVideosLength
} from '@server/types/models/index.js'
import Bluebird from 'bluebird' import Bluebird from 'bluebird'
import { getAPId } from '../activity.js' import { getAPId } from '../activity.js'
import { getOrCreateAPActor } from '../actors/index.js' import { getOrCreateAPActor } from '../actors/index.js'
@ -33,6 +27,11 @@ import { isActivityPubUrlValid } from '@server/helpers/custom-validators/activit
const lTags = loggerTagsFactory('ap', 'video-playlist') const lTags = loggerTagsFactory('ap', 'video-playlist')
export async function createAccountPlaylists (playlistUrls: string[], account: MAccountHost) { export async function createAccountPlaylists (playlistUrls: string[], account: MAccountHost) {
logger.info(
`Creating or updating ${playlistUrls.length} playlists for account ${account.Actor.preferredUsername}`,
lTags()
)
await Bluebird.map(playlistUrls, async playlistUrl => { await Bluebird.map(playlistUrls, async playlistUrl => {
if (!checkUrlsSameHost(playlistUrl, account.Actor.url)) { if (!checkUrlsSameHost(playlistUrl, account.Actor.url)) {
logger.warn(`Playlist ${playlistUrl} is not on the same host as owner account ${account.Actor.url}`, lTags(playlistUrl)) logger.warn(`Playlist ${playlistUrl} is not on the same host as owner account ${account.Actor.url}`, lTags(playlistUrl))
@ -69,6 +68,8 @@ export async function createOrUpdateVideoPlaylist (options: {
throw new Error(`Playlist ${playlistObject.id} is not on the same host as context URL ${contextUrl}`) throw new Error(`Playlist ${playlistObject.id} is not on the same host as context URL ${contextUrl}`)
} }
logger.debug(`Creating or updating playlist ${playlistObject.id}`, lTags(playlistObject.id))
const playlistAttributes = playlistObjectToDBAttributes(playlistObject, to || playlistObject.to) const playlistAttributes = playlistObjectToDBAttributes(playlistObject, to || playlistObject.to)
const channel = await getRemotePlaylistChannel(playlistObject) const channel = await getRemotePlaylistChannel(playlistObject)

View file

@ -51,7 +51,7 @@ async function processVideoShare (actorAnnouncer: MActorSignature, activity: Act
transaction: t transaction: t
}) })
if (video.isOwned() && created === true) { if (video.isLocal() && created === true) {
// Don't resend the activity to the sender // Don't resend the activity to the sender
const exceptions = [ actorAnnouncer ] const exceptions = [ actorAnnouncer ]

View file

@ -91,7 +91,7 @@ async function processCreateCacheFile (
const { video } = await getOrCreateAPVideo({ videoObject: cacheFile.object }) const { video } = await getOrCreateAPVideo({ videoObject: cacheFile.object })
if (video.isOwned() && !canVideoBeFederated(video)) { if (video.isLocal() && !canVideoBeFederated(video)) {
logger.warn(`Do not process create cache file ${cacheFile.object} on a video that cannot be federated`) logger.warn(`Do not process create cache file ${cacheFile.object} on a video that cannot be federated`)
return return
} }
@ -100,7 +100,7 @@ async function processCreateCacheFile (
return createOrUpdateCacheFile(cacheFile, video, byActor, t) return createOrUpdateCacheFile(cacheFile, video, byActor, t)
}) })
if (video.isOwned()) { if (video.isLocal()) {
// Don't resend the activity to the sender // Don't resend the activity to the sender
const exceptions = [ byActor ] const exceptions = [ byActor ]
await forwardVideoRelatedActivity(activity, undefined, exceptions, video) await forwardVideoRelatedActivity(activity, undefined, exceptions, video)
@ -152,7 +152,7 @@ async function processCreateVideoComment (
} }
// Try to not forward unwanted comments on our videos // Try to not forward unwanted comments on our videos
if (video.isOwned()) { if (video.isLocal()) {
if (!canVideoBeFederated(video)) { if (!canVideoBeFederated(video)) {
logger.info('Skip comment forward on non federated video' + video.url) logger.info('Skip comment forward on non federated video' + video.url)
return return

View file

@ -55,7 +55,7 @@ async function processDeleteActivity (options: APProcessorOptions<ActivityDelete
{ {
const videoInstance = await VideoModel.loadByUrlAndPopulateAccountAndFiles(objectUrl) const videoInstance = await VideoModel.loadByUrlAndPopulateAccountAndFiles(objectUrl)
if (videoInstance) { if (videoInstance) {
if (videoInstance.isOwned()) throw new Error(`Remote instance cannot delete owned video ${videoInstance.url}.`) if (videoInstance.isLocal()) throw new Error(`Remote instance cannot delete owned video ${videoInstance.url}.`)
return retryTransactionWrapper(processDeleteVideo, byActor, videoInstance) return retryTransactionWrapper(processDeleteVideo, byActor, videoInstance)
} }
@ -64,7 +64,7 @@ async function processDeleteActivity (options: APProcessorOptions<ActivityDelete
{ {
const videoPlaylist = await VideoPlaylistModel.loadByUrlAndPopulateAccount(objectUrl) const videoPlaylist = await VideoPlaylistModel.loadByUrlAndPopulateAccount(objectUrl)
if (videoPlaylist) { if (videoPlaylist) {
if (videoPlaylist.isOwned()) throw new Error(`Remote instance cannot delete owned playlist ${videoPlaylist.url}.`) if (videoPlaylist.isLocal()) throw new Error(`Remote instance cannot delete owned playlist ${videoPlaylist.url}.`)
return retryTransactionWrapper(processDeleteVideoPlaylist, byActor, videoPlaylist) return retryTransactionWrapper(processDeleteVideoPlaylist, byActor, videoPlaylist)
} }
@ -144,7 +144,7 @@ function processDeleteVideoComment (byActor: MActorSignature, videoComment: MCom
await videoComment.save({ transaction: t }) await videoComment.save({ transaction: t })
if (videoComment.Video.isOwned()) { if (videoComment.Video.isLocal()) {
// Don't resend the activity to the sender // Don't resend the activity to the sender
const exceptions = [ byActor ] const exceptions = [ byActor ]
await forwardVideoRelatedActivity(activity, t, exceptions, videoComment.Video) await forwardVideoRelatedActivity(activity, t, exceptions, videoComment.Video)

View file

@ -28,7 +28,7 @@ async function processDislike (activity: ActivityDislike, byActor: MActorSignatu
if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url) if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url)
const { video: onlyVideo } = await maybeGetOrCreateAPVideo({ videoObject: videoUrl, fetchType: 'only-video-and-blacklist' }) const { video: onlyVideo } = await maybeGetOrCreateAPVideo({ videoObject: videoUrl, fetchType: 'only-video-and-blacklist' })
if (!onlyVideo?.isOwned()) return if (!onlyVideo?.isLocal()) return
if (!canVideoBeFederated(onlyVideo)) { if (!canVideoBeFederated(onlyVideo)) {
logger.warn(`Do not process dislike on video ${videoUrl} that cannot be federated`) logger.warn(`Do not process dislike on video ${videoUrl} that cannot be federated`)

View file

@ -38,7 +38,7 @@ async function processFollow (byActor: MActorSignature, activityId: string, targ
const targetActor = await ActorModel.loadByUrlAndPopulateAccountAndChannel(targetActorURL, t) const targetActor = await ActorModel.loadByUrlAndPopulateAccountAndChannel(targetActorURL, t)
if (!targetActor) throw new Error('Unknown actor') if (!targetActor) throw new Error('Unknown actor')
if (targetActor.isOwned() === false) throw new Error('This is not a local actor.') if (targetActor.isLocal() === false) throw new Error('This is not a local actor.')
if (await rejectIfInstanceFollowDisabled(byActor, activityId, targetActor)) return { actorFollow: undefined } if (await rejectIfInstanceFollowDisabled(byActor, activityId, targetActor)) return { actorFollow: undefined }
if (await rejectIfMuted(byActor, activityId, targetActor)) return { actorFollow: undefined } if (await rejectIfMuted(byActor, activityId, targetActor)) return { actorFollow: undefined }

View file

@ -30,7 +30,7 @@ async function processLikeVideo (byActor: MActorSignature, activity: ActivityLik
if (!byAccount) throw new Error('Cannot create like with the non account actor ' + byActor.url) if (!byAccount) throw new Error('Cannot create like with the non account actor ' + byActor.url)
const { video: onlyVideo } = await maybeGetOrCreateAPVideo({ videoObject: videoUrl, fetchType: 'only-video-and-blacklist' }) const { video: onlyVideo } = await maybeGetOrCreateAPVideo({ videoObject: videoUrl, fetchType: 'only-video-and-blacklist' })
if (!onlyVideo?.isOwned()) return if (!onlyVideo?.isLocal()) return
if (!canVideoBeFederated(onlyVideo)) { if (!canVideoBeFederated(onlyVideo)) {
logger.warn(`Do not process like on video ${videoUrl} that cannot be federated`) logger.warn(`Do not process like on video ${videoUrl} that cannot be federated`)

View file

@ -16,7 +16,7 @@ export function processReplyApprovalFactory (type: Extract<ActivityType, 'Approv
throw new Error(`Cannot process reply approval on comment ${comment.url} that doesn't exist`) throw new Error(`Cannot process reply approval on comment ${comment.url} that doesn't exist`)
} }
if (comment.isOwned() !== true) { if (comment.isLocal() !== true) {
throw new Error(`Cannot process reply approval on non-owned comment ${comment.url}`) throw new Error(`Cannot process reply approval on non-owned comment ${comment.url}`)
} }

View file

@ -68,7 +68,7 @@ async function processUndoLike (byActor: MActorSignature, activity: ActivityUndo
const likeActivity = activity.object const likeActivity = activity.object
const { video: onlyVideo } = await maybeGetOrCreateAPVideo({ videoObject: likeActivity.object }) const { video: onlyVideo } = await maybeGetOrCreateAPVideo({ videoObject: likeActivity.object })
if (!onlyVideo?.isOwned()) return if (!onlyVideo?.isLocal()) return
return sequelizeTypescript.transaction(async t => { return sequelizeTypescript.transaction(async t => {
if (!byActor.Account) throw new Error('Unknown account ' + byActor.url) if (!byActor.Account) throw new Error('Unknown account ' + byActor.url)
@ -92,7 +92,7 @@ async function processUndoDislike (byActor: MActorSignature, activity: ActivityU
const dislikeActivity = activity.object const dislikeActivity = activity.object
const { video: onlyVideo } = await maybeGetOrCreateAPVideo({ videoObject: dislikeActivity.object }) const { video: onlyVideo } = await maybeGetOrCreateAPVideo({ videoObject: dislikeActivity.object })
if (!onlyVideo?.isOwned()) return if (!onlyVideo?.isLocal()) return
return sequelizeTypescript.transaction(async t => { return sequelizeTypescript.transaction(async t => {
if (!byActor.Account) throw new Error('Unknown account ' + byActor.url) if (!byActor.Account) throw new Error('Unknown account ' + byActor.url)
@ -132,7 +132,7 @@ async function processUndoCacheFile (
await cacheFile.destroy({ transaction: t }) await cacheFile.destroy({ transaction: t })
if (video.isOwned()) { if (video.isLocal()) {
// Don't resend the activity to the sender // Don't resend the activity to the sender
const exceptions = [ byActor ] const exceptions = [ byActor ]
@ -153,7 +153,7 @@ function processUndoAnnounce (byActor: MActorSignature, announceActivity: Activi
await share.destroy({ transaction: t }) await share.destroy({ transaction: t })
if (share.Video.isOwned()) { if (share.Video.isLocal()) {
// Don't resend the activity to the sender // Don't resend the activity to the sender
const exceptions = [ byActor ] const exceptions = [ byActor ]

View file

@ -103,7 +103,7 @@ async function processUpdateCacheFile (
const { video } = await getOrCreateAPVideo({ videoObject: cacheFileObject.object }) const { video } = await getOrCreateAPVideo({ videoObject: cacheFileObject.object })
if (video.isOwned() && !canVideoBeFederated(video)) { if (video.isLocal() && !canVideoBeFederated(video)) {
logger.warn(`Do not process update cache file on video ${activity.object} that cannot be federated`) logger.warn(`Do not process update cache file on video ${activity.object} that cannot be federated`)
return return
} }
@ -112,7 +112,7 @@ async function processUpdateCacheFile (
await createOrUpdateCacheFile(cacheFileObject, video, byActor, t) await createOrUpdateCacheFile(cacheFileObject, video, byActor, t)
}) })
if (video.isOwned()) { if (video.isLocal()) {
// Don't resend the activity to the sender // Don't resend the activity to the sender
const exceptions = [ byActor ] const exceptions = [ byActor ]

View file

@ -38,7 +38,7 @@ async function processCreateView (activity: ActivityView, byActor: MActorSignatu
viewerResultCounter: getViewerResultCounter(activity) viewerResultCounter: getViewerResultCounter(activity)
}) })
if (video.isOwned()) { if (video.isLocal()) {
// Forward the view but don't resend the activity to the sender // Forward the view but don't resend the activity to the sender
const exceptions = [ byActor ] const exceptions = [ byActor ]
await forwardVideoRelatedActivity(activity, undefined, exceptions, video) await forwardVideoRelatedActivity(activity, undefined, exceptions, video)

View file

@ -110,7 +110,7 @@ export async function sendCreateVideoPlaylist (playlist: MVideoPlaylistFull, tra
} }
export async function sendCreateVideoCommentIfNeeded (comment: MCommentOwnerVideoReply, transaction: Transaction) { export async function sendCreateVideoCommentIfNeeded (comment: MCommentOwnerVideoReply, transaction: Transaction) {
const isOrigin = comment.Video.isOwned() const isOrigin = comment.Video.isLocal()
if (isOrigin) { if (isOrigin) {
const videoWithBlacklist = await VideoModel.loadWithBlacklist(comment.Video.id) const videoWithBlacklist = await VideoModel.loadWithBlacklist(comment.Video.id)

View file

@ -53,13 +53,13 @@ async function sendDeleteActor (byActor: ActorModel, transaction: Transaction) {
async function sendDeleteVideoComment (videoComment: MCommentOwnerVideo, transaction: Transaction) { async function sendDeleteVideoComment (videoComment: MCommentOwnerVideo, transaction: Transaction) {
logger.info('Creating job to send delete of comment %s.', videoComment.url) logger.info('Creating job to send delete of comment %s.', videoComment.url)
const isVideoOrigin = videoComment.Video.isOwned() const isVideoOrigin = videoComment.Video.isLocal()
const url = getDeleteActivityPubUrl(videoComment.url) const url = getDeleteActivityPubUrl(videoComment.url)
const videoAccount = await AccountModel.load(videoComment.Video.VideoChannel.Account.id, transaction) const videoAccount = await AccountModel.load(videoComment.Video.VideoChannel.Account.id, transaction)
const byActor = videoComment.isOwned() const byActor = videoComment.isLocal()
? videoComment.Account.Actor ? videoComment.Account.Actor
: videoAccount.Actor : videoAccount.Actor

View file

@ -29,7 +29,7 @@ async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAud
const { byActor, video, transaction, contextType, parallelizable } = options const { byActor, video, transaction, contextType, parallelizable } = options
// Send to origin // Send to origin
if (video.isOwned() === false) { if (video.isLocal() === false) {
return sendVideoActivityToOrigin(activityBuilder, options) return sendVideoActivityToOrigin(activityBuilder, options)
} }
@ -62,7 +62,7 @@ async function sendVideoActivityToOrigin (activityBuilder: (audience: ActivityAu
}) { }) {
const { byActor, video, actorsInvolvedInVideo, transaction, contextType } = options const { byActor, video, actorsInvolvedInVideo, transaction, contextType } = options
if (video.isOwned()) throw new Error('Cannot send activity to owned video origin ' + video.url) if (video.isLocal()) throw new Error('Cannot send activity to owned video origin ' + video.url)
let accountActor: MActorLight = (video as MVideoAccountLight).VideoChannel?.Account?.Actor let accountActor: MActorLight = (video as MVideoAccountLight).VideoChannel?.Account?.Actor
if (!accountActor) accountActor = await ActorModel.loadAccountActorByVideoId(video.id, transaction) if (!accountActor) accountActor = await ActorModel.loadAccountActorByVideoId(video.id, transaction)

View file

@ -109,7 +109,7 @@ async function tryToResolveThreadFromVideo (params: ResolveThreadParams) {
const syncParam = { rates: true, shares: true, comments: false, refreshVideo: false } const syncParam = { rates: true, shares: true, comments: false, refreshVideo: false }
const { video } = await getOrCreateAPVideo({ videoObject: url, syncParam }) const { video } = await getOrCreateAPVideo({ videoObject: url, syncParam })
if (video.isOwned() && !canVideoBeFederated(video)) { if (video.isLocal() && !canVideoBeFederated(video)) {
throw new Error('Cannot resolve thread of video that is not compatible with federation') throw new Error('Cannot resolve thread of video that is not compatible with federation')
} }
@ -169,7 +169,7 @@ async function getAutomaticTagsAndAssignReview (comment: MComment, video: MVideo
const automaticTags = await new AutomaticTagger().buildCommentsAutomaticTags({ ownerAccount, text: comment.text }) const automaticTags = await new AutomaticTagger().buildCommentsAutomaticTags({ ownerAccount, text: comment.text })
// Third parties rely on origin, so if origin has the comment it's not held for review // Third parties rely on origin, so if origin has the comment it's not held for review
if (video.isOwned() || comment.isOwned()) { if (video.isLocal() || comment.isLocal()) {
comment.heldForReview = await shouldCommentBeHeldForReview({ user: null, video, automaticTags }) comment.heldForReview = await shouldCommentBeHeldForReview({ user: null, video, automaticTags })
} else { } else {
comment.heldForReview = false comment.heldForReview = false

View file

@ -13,7 +13,7 @@ async function sendVideoRateChange (
dislikes: number, dislikes: number,
t: Transaction t: Transaction
) { ) {
if (video.isOwned()) return federateVideoIfNeeded(video, false, t) if (video.isLocal()) return federateVideoIfNeeded(video, false, t)
return sendVideoRateChangeToOrigin(account, video, likes, dislikes, t) return sendVideoRateChangeToOrigin(account, video, likes, dislikes, t)
} }
@ -41,7 +41,7 @@ async function sendVideoRateChangeToOrigin (
t: Transaction t: Transaction
) { ) {
// Local video, we don't need to send like // Local video, we don't need to send like
if (video.isOwned()) return if (video.isLocal()) return
const actor = account.Actor const actor = account.Actor

View file

@ -490,7 +490,7 @@ export class Emailer {
} }
private initHandlebarsIfNeeded () { private initHandlebarsIfNeeded () {
if (this.registeringHandlebars) return this.registeringHandlebars if (this.registeringHandlebars !== undefined) return this.registeringHandlebars
this.registeringHandlebars = this._initHandlebarsIfNeeded() this.registeringHandlebars = this._initHandlebarsIfNeeded()

View file

@ -12,7 +12,7 @@ type ImageModel = {
filename: string filename: string
onDisk: boolean onDisk: boolean
isOwned (): boolean isLocal(): boolean
getPath(): string getPath(): string
save(): Promise<Model> save(): Promise<Model>
@ -28,7 +28,6 @@ export abstract class AbstractPermanentFileCache <M extends ImageModel> {
protected abstract loadModel (filename: string): Promise<M> protected abstract loadModel (filename: string): Promise<M>
constructor (private readonly directory: string) { constructor (private readonly directory: string) {
} }
async lazyServe (options: { async lazyServe (options: {
@ -102,7 +101,7 @@ export abstract class AbstractPermanentFileCache <M extends ImageModel> {
const { err, image, filename, next } = options const { err, image, filename, next } = options
// It seems this actor image is not on the disk anymore // It seems this actor image is not on the disk anymore
if (err.status === HttpStatusCode.NOT_FOUND_404 && !image.isOwned()) { if (err.status === HttpStatusCode.NOT_FOUND_404 && !image.isLocal()) {
logger.error('Cannot lazy serve image %s.', filename, { err }) logger.error('Cannot lazy serve image %s.', filename, { err })
this.filenameToPathUnsafeCache.delete(filename) this.filenameToPathUnsafeCache.delete(filename)

View file

@ -2,10 +2,9 @@ import { remove } from 'fs-extra/esm'
import { logger } from '../../../helpers/logger.js' import { logger } from '../../../helpers/logger.js'
import memoizee from 'memoizee' import memoizee from 'memoizee'
type GetFilePathResult = { isOwned: boolean, path: string, downloadName?: string } | undefined type GetFilePathResult = { isLocal: boolean, path: string, downloadName?: string } | undefined
export abstract class AbstractSimpleFileCache<T> { export abstract class AbstractSimpleFileCache<T> {
getFilePath: (params: T) => Promise<GetFilePathResult> getFilePath: (params: T) => Promise<GetFilePathResult>
abstract getFilePathImpl (params: T): Promise<GetFilePathResult> abstract getFilePathImpl (params: T): Promise<GetFilePathResult>
@ -19,7 +18,7 @@ export abstract class AbstractSimpleFileCache <T> {
max, max,
promise: true, promise: true,
dispose: (result?: GetFilePathResult) => { dispose: (result?: GetFilePathResult) => {
if (result && result.isOwned !== true) { if (result && result.isLocal !== true) {
remove(result.path) remove(result.path)
.then(() => logger.debug('%s removed from %s', result.path, this.constructor.name)) .then(() => logger.debug('%s removed from %s', result.path, this.constructor.name))
.catch(err => logger.error('Cannot remove %s from cache %s.', result.path, this.constructor.name, { err })) .catch(err => logger.error('Cannot remove %s from cache %s.', result.path, this.constructor.name, { err }))

View file

@ -21,8 +21,8 @@ class VideoCaptionsSimpleFileCache extends AbstractSimpleFileCache<string> {
const videoCaption = await VideoCaptionModel.loadWithVideoByFilename(filename) const videoCaption = await VideoCaptionModel.loadWithVideoByFilename(filename)
if (!videoCaption) return undefined if (!videoCaption) return undefined
if (videoCaption.isOwned()) { if (videoCaption.isLocal()) {
return { isOwned: true, path: videoCaption.getFSFilePath() } return { isLocal: true, path: videoCaption.getFSFilePath() }
} }
return this.loadRemoteFile(filename) return this.loadRemoteFile(filename)
@ -33,7 +33,7 @@ class VideoCaptionsSimpleFileCache extends AbstractSimpleFileCache<string> {
const videoCaption = await VideoCaptionModel.loadWithVideoByFilename(key) const videoCaption = await VideoCaptionModel.loadWithVideoByFilename(key)
if (!videoCaption) return undefined if (!videoCaption) return undefined
if (videoCaption.isOwned()) throw new Error('Cannot load remote caption of owned video.') if (videoCaption.isLocal()) throw new Error('Cannot load remote caption of owned video.')
// Used to fetch the path // Used to fetch the path
const video = await VideoModel.loadFull(videoCaption.videoId) const video = await VideoModel.loadFull(videoCaption.videoId)
@ -45,7 +45,7 @@ class VideoCaptionsSimpleFileCache extends AbstractSimpleFileCache<string> {
try { try {
await doRequestAndSaveToFile(remoteUrl, destPath) await doRequestAndSaveToFile(remoteUrl, destPath)
return { isOwned: false, path: destPath } return { isLocal: false, path: destPath }
} catch (err) { } catch (err) {
logger.info('Cannot fetch remote caption file %s.', remoteUrl, { err }) logger.info('Cannot fetch remote caption file %s.', remoteUrl, { err })

View file

@ -1,14 +1,13 @@
import { ThumbnailType } from '@peertube/peertube-models'
import { logger } from '@server/helpers/logger.js'
import { doRequestAndSaveToFile } from '@server/helpers/requests.js'
import { ThumbnailModel } from '@server/models/video/thumbnail.js'
import { join } from 'path' import { join } from 'path'
import { FILES_CACHE } from '../../initializers/constants.js' import { FILES_CACHE } from '../../initializers/constants.js'
import { VideoModel } from '../../models/video/video.js' import { VideoModel } from '../../models/video/video.js'
import { AbstractSimpleFileCache } from './shared/abstract-simple-file-cache.js' import { AbstractSimpleFileCache } from './shared/abstract-simple-file-cache.js'
import { doRequestAndSaveToFile } from '@server/helpers/requests.js'
import { ThumbnailModel } from '@server/models/video/thumbnail.js'
import { ThumbnailType } from '@peertube/peertube-models'
import { logger } from '@server/helpers/logger.js'
class VideoPreviewsSimpleFileCache extends AbstractSimpleFileCache<string> { class VideoPreviewsSimpleFileCache extends AbstractSimpleFileCache<string> {
private static instance: VideoPreviewsSimpleFileCache private static instance: VideoPreviewsSimpleFileCache
private constructor () { private constructor () {
@ -23,7 +22,7 @@ class VideoPreviewsSimpleFileCache extends AbstractSimpleFileCache <string> {
const thumbnail = await ThumbnailModel.loadWithVideoByFilename(filename, ThumbnailType.PREVIEW) const thumbnail = await ThumbnailModel.loadWithVideoByFilename(filename, ThumbnailType.PREVIEW)
if (!thumbnail) return undefined if (!thumbnail) return undefined
if (thumbnail.Video.isOwned()) return { isOwned: true, path: thumbnail.getPath() } if (thumbnail.Video.isLocal()) return { isLocal: true, path: thumbnail.getPath() }
return this.loadRemoteFile(thumbnail.Video.uuid) return this.loadRemoteFile(thumbnail.Video.uuid)
} }
@ -33,7 +32,7 @@ class VideoPreviewsSimpleFileCache extends AbstractSimpleFileCache <string> {
const video = await VideoModel.loadFull(key) const video = await VideoModel.loadFull(key)
if (!video) return undefined if (!video) return undefined
if (video.isOwned()) throw new Error('Cannot load remote preview of owned video.') if (video.isLocal()) throw new Error('Cannot load remote preview of owned video.')
const preview = video.getPreview() const preview = video.getPreview()
const destPath = join(FILES_CACHE.PREVIEWS.DIRECTORY, preview.filename) const destPath = join(FILES_CACHE.PREVIEWS.DIRECTORY, preview.filename)
@ -44,7 +43,7 @@ class VideoPreviewsSimpleFileCache extends AbstractSimpleFileCache <string> {
logger.debug('Fetched remote preview %s to %s.', remoteUrl, destPath) logger.debug('Fetched remote preview %s to %s.', remoteUrl, destPath)
return { isOwned: false, path: destPath } return { isLocal: false, path: destPath }
} catch (err) { } catch (err) {
logger.info('Cannot fetch remote preview file %s.', remoteUrl, { err }) logger.info('Cannot fetch remote preview file %s.', remoteUrl, { err })

View file

@ -1,12 +1,11 @@
import { join } from 'path'
import { logger } from '@server/helpers/logger.js' import { logger } from '@server/helpers/logger.js'
import { doRequestAndSaveToFile } from '@server/helpers/requests.js' import { doRequestAndSaveToFile } from '@server/helpers/requests.js'
import { StoryboardModel } from '@server/models/video/storyboard.js' import { StoryboardModel } from '@server/models/video/storyboard.js'
import { join } from 'path'
import { FILES_CACHE } from '../../initializers/constants.js' import { FILES_CACHE } from '../../initializers/constants.js'
import { AbstractSimpleFileCache } from './shared/abstract-simple-file-cache.js' import { AbstractSimpleFileCache } from './shared/abstract-simple-file-cache.js'
class VideoStoryboardsSimpleFileCache extends AbstractSimpleFileCache<string> { class VideoStoryboardsSimpleFileCache extends AbstractSimpleFileCache<string> {
private static instance: VideoStoryboardsSimpleFileCache private static instance: VideoStoryboardsSimpleFileCache
private constructor () { private constructor () {
@ -21,7 +20,7 @@ class VideoStoryboardsSimpleFileCache extends AbstractSimpleFileCache <string> {
const storyboard = await StoryboardModel.loadWithVideoByFilename(filename) const storyboard = await StoryboardModel.loadWithVideoByFilename(filename)
if (!storyboard) return undefined if (!storyboard) return undefined
if (storyboard.Video.isOwned()) return { isOwned: true, path: storyboard.getPath() } if (storyboard.Video.isLocal()) return { isLocal: true, path: storyboard.getPath() }
return this.loadRemoteFile(storyboard.filename) return this.loadRemoteFile(storyboard.filename)
} }
@ -39,7 +38,7 @@ class VideoStoryboardsSimpleFileCache extends AbstractSimpleFileCache <string> {
logger.debug('Fetched remote storyboard %s to %s.', remoteUrl, destPath) logger.debug('Fetched remote storyboard %s to %s.', remoteUrl, destPath)
return { isOwned: false, path: destPath } return { isLocal: false, path: destPath }
} catch (err) { } catch (err) {
logger.info('Cannot fetch remote storyboard file %s.', remoteUrl, { err }) logger.info('Cannot fetch remote storyboard file %s.', remoteUrl, { err })

View file

@ -9,7 +9,6 @@ import { VideoModel } from '../../models/video/video.js'
import { AbstractSimpleFileCache } from './shared/abstract-simple-file-cache.js' import { AbstractSimpleFileCache } from './shared/abstract-simple-file-cache.js'
class VideoTorrentsSimpleFileCache extends AbstractSimpleFileCache<string> { class VideoTorrentsSimpleFileCache extends AbstractSimpleFileCache<string> {
private static instance: VideoTorrentsSimpleFileCache private static instance: VideoTorrentsSimpleFileCache
private constructor () { private constructor () {
@ -24,10 +23,10 @@ class VideoTorrentsSimpleFileCache extends AbstractSimpleFileCache <string> {
const file = await VideoFileModel.loadWithVideoOrPlaylistByTorrentFilename(filename) const file = await VideoFileModel.loadWithVideoOrPlaylistByTorrentFilename(filename)
if (!file) return undefined if (!file) return undefined
if (file.getVideo().isOwned()) { if (file.getVideo().isLocal()) {
const downloadName = this.buildDownloadName(file.getVideo(), file) const downloadName = this.buildDownloadName(file.getVideo(), file)
return { isOwned: true, path: join(CONFIG.STORAGE.TORRENTS_DIR, file.torrentFilename), downloadName } return { isLocal: true, path: join(CONFIG.STORAGE.TORRENTS_DIR, file.torrentFilename), downloadName }
} }
return this.loadRemoteFile(filename) return this.loadRemoteFile(filename)
@ -38,7 +37,7 @@ class VideoTorrentsSimpleFileCache extends AbstractSimpleFileCache <string> {
const file = await VideoFileModel.loadWithVideoOrPlaylistByTorrentFilename(key) const file = await VideoFileModel.loadWithVideoOrPlaylistByTorrentFilename(key)
if (!file) return undefined if (!file) return undefined
if (file.getVideo().isOwned()) throw new Error('Cannot load remote file of owned video.') if (file.getVideo().isLocal()) throw new Error('Cannot load remote file of owned video.')
// Used to fetch the path // Used to fetch the path
const video = await VideoModel.loadFull(file.getVideo().id) const video = await VideoModel.loadFull(file.getVideo().id)
@ -52,7 +51,7 @@ class VideoTorrentsSimpleFileCache extends AbstractSimpleFileCache <string> {
const downloadName = this.buildDownloadName(video, file) const downloadName = this.buildDownloadName(video, file)
return { isOwned: false, path: destPath, downloadName } return { isLocal: false, path: destPath, downloadName }
} catch (err) { } catch (err) {
logger.info('Cannot fetch remote torrent file %s.', remoteUrl, { err }) logger.info('Cannot fetch remote torrent file %s.', remoteUrl, { err })

View file

@ -108,7 +108,7 @@ export class ActorHtml {
updatedAt: entity.updatedAt updatedAt: entity.updatedAt
}, },
forbidIndexation: !entity.Actor.isOwned(), forbidIndexation: !entity.Actor.isLocal(),
embedIndexation: false, embedIndexation: false,
rssFeeds: getRSSFeeds(entity) rssFeeds: getRSSFeeds(entity)

View file

@ -113,7 +113,7 @@ export class PlaylistHtml {
forbidIndexation: isEmbed forbidIndexation: isEmbed
? playlist.privacy !== VideoPlaylistPrivacy.PUBLIC && playlist.privacy !== VideoPlaylistPrivacy.UNLISTED ? playlist.privacy !== VideoPlaylistPrivacy.PUBLIC && playlist.privacy !== VideoPlaylistPrivacy.UNLISTED
: !playlist.isOwned() || playlist.privacy !== VideoPlaylistPrivacy.PUBLIC, : !playlist.isLocal() || playlist.privacy !== VideoPlaylistPrivacy.PUBLIC,
embedIndexation: isEmbed, embedIndexation: isEmbed,

View file

@ -438,7 +438,10 @@ class MuxingSession extends EventEmitter implements MuxingSession {
setTimeout(() => { setTimeout(() => {
// Wait latest segments generation, and close watchers // Wait latest segments generation, and close watchers
const promise = this.filesWatcher?.close() || Promise.resolve() const promise = this.filesWatcher
? this.filesWatcher.close()
: Promise.resolve()
promise promise
.then(() => { .then(() => {
// Process remaining segments hash // Process remaining segments hash

View file

@ -129,7 +129,7 @@ async function createVideoAbuse (options: {
videoAbuseInstance.Video = videoInstance videoAbuseInstance.Video = videoInstance
abuseInstance.VideoAbuse = videoAbuseInstance abuseInstance.VideoAbuse = videoAbuseInstance
return { isOwned: videoInstance.isOwned() } return { isLocal: videoInstance.isLocal() }
} }
return createAbuse({ return createAbuse({
@ -160,7 +160,7 @@ function createVideoCommentAbuse (options: {
commentAbuseInstance.VideoComment = commentInstance commentAbuseInstance.VideoComment = commentInstance
abuseInstance.VideoCommentAbuse = commentAbuseInstance abuseInstance.VideoCommentAbuse = commentAbuseInstance
return { isOwned: commentInstance.isOwned() } return { isLocal: commentInstance.isLocal() }
} }
return createAbuse({ return createAbuse({
@ -183,7 +183,7 @@ function createAccountAbuse (options: {
const { baseAbuse, accountInstance, transaction, reporterAccount, skipNotification } = options const { baseAbuse, accountInstance, transaction, reporterAccount, skipNotification } = options
const associateFun = () => { const associateFun = () => {
return Promise.resolve({ isOwned: accountInstance.isOwned() }) return Promise.resolve({ isLocal: accountInstance.isLocal() })
} }
return createAbuse({ return createAbuse({
@ -217,7 +217,7 @@ async function createAbuse (options: {
base: FilteredModelAttributes<AbuseModel> base: FilteredModelAttributes<AbuseModel>
reporterAccount: MAccountDefault reporterAccount: MAccountDefault
flaggedAccount: MAccountLight flaggedAccount: MAccountLight
associateFun: (abuseInstance: MAbuseFull) => Promise<{ isOwned: boolean }> associateFun: (abuseInstance: MAbuseFull) => Promise<{ isLocal: boolean }>
skipNotification: boolean skipNotification: boolean
transaction: Transaction transaction: Transaction
}) { }) {
@ -230,9 +230,9 @@ async function createAbuse (options: {
abuseInstance.ReporterAccount = reporterAccount abuseInstance.ReporterAccount = reporterAccount
abuseInstance.FlaggedAccount = flaggedAccount abuseInstance.FlaggedAccount = flaggedAccount
const { isOwned } = await associateFun(abuseInstance) const { isLocal } = await associateFun(abuseInstance)
if (isOwned === false) { if (isLocal === false) {
sendAbuse(reporterAccount.Actor, abuseInstance, abuseInstance.FlaggedAccount, transaction) sendAbuse(reporterAccount.Actor, abuseInstance, abuseInstance.FlaggedAccount, transaction)
} }

View file

@ -8,6 +8,9 @@ import {
MAbuseMessage, MAbuseMessage,
MActorFollowFull, MActorFollowFull,
MApplication, MApplication,
MChannelAccountDefault,
MChannelCollaboratorAccount,
MChannelDefault,
MCommentOwnerVideo, MCommentOwnerVideo,
MPlugin, MPlugin,
MVideoAccountLight, MVideoAccountLight,
@ -17,6 +20,9 @@ import {
import { JobQueue } from '../job-queue/index.js' import { JobQueue } from '../job-queue/index.js'
import { PeerTubeSocket } from '../peertube-socket.js' import { PeerTubeSocket } from '../peertube-socket.js'
import { Hooks } from '../plugins/hooks.js' import { Hooks } from '../plugins/hooks.js'
import { AcceptedToCollaborateToChannel } from './shared/channel/accepted-to-collaborate-to-channel.js'
import { InvitedToCollaborateToChannel } from './shared/channel/invited-to-collaborate-to-channel.js'
import { RefusedToCollaborateToChannel } from './shared/channel/refused-to-collaborate-to-channel.js'
import { import {
AbstractNotification, AbstractNotification,
AbuseStateChangeForReporter, AbuseStateChangeForReporter,
@ -71,7 +77,10 @@ class Notifier {
newPeertubeVersion: [ NewPeerTubeVersionForAdmins ], newPeertubeVersion: [ NewPeerTubeVersionForAdmins ],
newPluginVersion: [ NewPluginVersionForAdmins ], newPluginVersion: [ NewPluginVersionForAdmins ],
videoStudioEditionFinished: [ StudioEditionFinishedForOwner ], videoStudioEditionFinished: [ StudioEditionFinishedForOwner ],
videoTranscriptionGenerated: [ VideoTranscriptionGeneratedForOwner ] videoTranscriptionGenerated: [ VideoTranscriptionGeneratedForOwner ],
channelCollaboratorInvitation: [ InvitedToCollaborateToChannel ],
channelCollaborationAccepted: [ AcceptedToCollaborateToChannel ],
channelCollaborationRefused: [ RefusedToCollaborateToChannel ]
} }
private static instance: Notifier private static instance: Notifier
@ -287,6 +296,44 @@ class Notifier {
.catch(err => logger.error('Cannot notify on generated video transcription %s of video %s.', caption.language, video.url, { err })) .catch(err => logger.error('Cannot notify on generated video transcription %s of video %s.', caption.language, video.url, { err }))
} }
notifyOfChannelCollaboratorInvitation (collaborator: MChannelCollaboratorAccount, channel: MChannelAccountDefault) {
const models = this.notificationModels.channelCollaboratorInvitation
const channelName = channel.Actor.preferredUsername
const collaboratorName = collaborator.Account.Actor.preferredUsername
logger.debug('Notify on channel collaborator invitation', { channelName, collaboratorName, ...lTags() })
this.sendNotifications(models, { channel, collaborator })
.catch(err => logger.error(`Cannot notify ${collaboratorName} of invitation to collaborate to channel ${channelName}`, { err }))
}
notifyOfAcceptedChannelCollaborator (collaborator: MChannelCollaboratorAccount, channel: MChannelDefault) {
const models = this.notificationModels.channelCollaborationAccepted
const channelName = channel.Actor.preferredUsername
const channelOwner = collaborator.Account.Actor.preferredUsername
logger.debug('Notify of accepted channel collaboration invitation', { channelName, channelOwner, ...lTags() })
this.sendNotifications(models, { channel, collaborator })
.catch(err => logger.error(`Cannot notify ${channelOwner} of accepted invitation to collaborate to channel ${channelName}`, { err }))
}
notifyOfRefusedChannelCollaborator (collaborator: MChannelCollaboratorAccount, channel: MChannelDefault) {
const models = this.notificationModels.channelCollaborationRefused
const channelName = channel.Actor.preferredUsername
const channelOwner = collaborator.Account.Actor.preferredUsername
logger.debug('Notify of refused channel collaboration invitation', { channelName, channelOwner, ...lTags() })
this.sendNotifications(models, { channel, collaborator })
.catch(err => logger.error(`Cannot notify ${channelOwner} of refused invitation to collaborate to channel ${channelName}`, { err }))
}
// ---------------------------------------------------------------------------
private async notify<T> (object: AbstractNotification<T>) { private async notify<T> (object: AbstractNotification<T>) {
await object.prepare() await object.prepare()

View file

@ -13,7 +13,7 @@ export class AbuseStateChangeForReporter extends AbstractNotification<MAbuseFull
async prepare () { async prepare () {
const reporter = this.abuse.ReporterAccount const reporter = this.abuse.ReporterAccount
if (reporter.isOwned() !== true) return if (reporter.isLocal() !== true) return
this.user = await UserModel.loadByAccountActorId(this.abuse.ReporterAccount.actorId) this.user = await UserModel.loadByAccountActorId(this.abuse.ReporterAccount.actorId)
} }

Some files were not shown because too many files have changed in this diff Show more