1
0
Fork 0
mirror of https://github.com/Chocobozzz/PeerTube.git synced 2025-10-03 01:39:37 +02:00

Add channel collaborators feature

This commit is contained in:
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 { buildFFmpegLive, ProcessOptions } from './common.js'
type CustomLiveRTMPHLSTranscodingUpdatePayload =
Omit<LiveRTMPHLSTranscodingUpdatePayload, 'resolutionPlaylistFile'> & { resolutionPlaylistFile?: [ Buffer, string ] | Blob | string }
type CustomLiveRTMPHLSTranscodingUpdatePayload = Omit<LiveRTMPHLSTranscodingUpdatePayload, 'resolutionPlaylistFile'> & {
resolutionPlaylistFile?: [Buffer, string] | Blob | string
}
export class ProcessLiveRTMPHLSTranscoding {
private readonly outputPath: string
private readonly fsWatchers: FSWatcher[] = []
@ -326,7 +326,7 @@ export class ProcessLiveRTMPHLSTranscoding {
const p = payloadBuilder().then(p => this.updateWithRetry(p))
if (!sequentialPromises) sequentialPromises = p
if (sequentialPromises === undefined) sequentialPromises = p
else sequentialPromises = sequentialPromises.then(() => p)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

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

View file

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

View file

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

View file

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

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'
return this.getRequestBody<ResultList<Video>>({
...options,
path,
query: { ...this.buildListQuery(options), ...pick(options, [ 'channelId', 'channelNameOneOf' ]) },
query: { ...this.buildListQuery(options), ...pick(options, [ 'channelId', 'channelNameOneOf', 'includeCollaborations' ]) },
implicitToken: true,
defaultExpectedStatus: HttpStatusCode.OK_200
})

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 */
import { FIXTURE_URLS } from '@tests/shared/fixture-urls.js'
import { areHttpImportTestsDisabled } from '@peertube/peertube-node-utils'
import { HttpStatusCode } from '@peertube/peertube-models'
import { areHttpImportTestsDisabled } from '@peertube/peertube-node-utils'
import {
ChannelsCommand,
cleanupTests,
@ -11,6 +10,7 @@ import {
setAccessTokensToServers,
setDefaultVideoChannel
} from '@peertube/peertube-server-commands'
import { FIXTURE_URLS } from '@tests/shared/fixture-urls.js'
describe('Test videos import in a channel API validator', function () {
let server: PeerTubeServer

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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 { PeerTubeServer, cleanupTests, setDefaultAccountAvatar, waitJobs } from '@peertube/peertube-server-commands'
import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
import { CheckerBaseParams, checkCommentMention, checkNewCommentOnMyVideo, prepareNotificationsTest } from '@tests/shared/notifications.js'
import { checkCommentMention, checkNewCommentOnMyVideo } from '@tests/shared/notifications/check-comment-notifications.js'
import { prepareNotificationsTest } from '@tests/shared/notifications/notifications-common.js'
import { CheckerBaseParams } from '@tests/shared/notifications/shared/notification-checker.js'
import { expect } from 'chai'
describe('Test comments notifications', function () {

View file

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

View file

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

View file

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

View file

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

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 { FIXTURE_URLS } from '@tests/shared/fixture-urls.js'
import { MockSmtpServer } from '@tests/shared/mock-servers/mock-email.js'
import { checkNewActorFollow } from '@tests/shared/notifications/check-follow-notifications.js'
import {
CheckerBaseParams,
checkMyVideoImportIsFinished,
checkMyVideoIsPublished,
checkNewActorFollow,
checkNewLiveFromSubscription,
checkNewVideoFromSubscription,
checkVideoStudioEditionIsFinished,
prepareNotificationsTest,
waitUntilNotification
} from '@tests/shared/notifications.js'
checkVideoStudioEditionIsFinished
} from '@tests/shared/notifications/check-video-notifications.js'
import { prepareNotificationsTest, waitUntilNotification } from '@tests/shared/notifications/notifications-common.js'
import { CheckerBaseParams } from '@tests/shared/notifications/shared/notification-checker.js'
import { uploadRandomVideoOnServers } from '@tests/shared/videos.js'
import { expect } from 'chai'

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-static-file-privacy.js'
import './video-storyboard.js'
import './video-storyboard-remote-runner.js'
import './video-transcription.js'
import './videos-common-filters.js'
import './videos-history.js'
import './videos-overview.js'
import './channel-collaborators.js'

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

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)
const { video: onlyVideo } = await maybeGetOrCreateAPVideo({ videoObject: videoUrl, fetchType: 'only-video-and-blacklist' })
if (!onlyVideo?.isOwned()) return
if (!onlyVideo?.isLocal()) return
if (!canVideoBeFederated(onlyVideo)) {
logger.warn(`Do not process dislike on video ${videoUrl} that cannot be federated`)

View file

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

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)
const { video: onlyVideo } = await maybeGetOrCreateAPVideo({ videoObject: videoUrl, fetchType: 'only-video-and-blacklist' })
if (!onlyVideo?.isOwned()) return
if (!onlyVideo?.isLocal()) return
if (!canVideoBeFederated(onlyVideo)) {
logger.warn(`Do not process like on video ${videoUrl} that cannot be federated`)

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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