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 135d5c7363
No known key found for this signature in database
GPG key ID: 583A612D890159BE
185 changed files with 5457 additions and 2631 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)
}
@ -388,7 +388,7 @@ export class ProcessLiveRTMPHLSTranscoding {
return [
Buffer.from(this.latestFilteredPlaylistContent[playlistName], 'utf-8'),
join(this.outputPath, 'master.m3u8')
] as [ Buffer, string ]
] as [Buffer, string]
}
// ---------------------------------------------------------------------------

View file

@ -140,33 +140,37 @@ export class MyVideoChannelsComponent {
})),
switchMap(options => this.videoChannelService.listAccountVideoChannels(options))
)
.subscribe(res => {
this.videoChannels = this.videoChannels.concat(res.data)
this.pagination.totalItems = res.total
.subscribe({
next: res => {
this.videoChannels = this.videoChannels.concat(res.data)
this.pagination.totalItems = res.total
// chart data
this.videoChannelsChartData = this.videoChannels.map(v => ({
labels: v.viewsPerDay.map(day => day.date.toLocaleDateString()),
datasets: [
{
label: $localize`Views for the day`,
data: v.viewsPerDay.map(day => day.views),
fill: false,
borderColor: '#c6c6c6'
}
],
// chart data
this.videoChannelsChartData = this.videoChannels.map(v => ({
labels: v.viewsPerDay.map(day => day.date.toLocaleDateString()),
datasets: [
{
label: $localize`Views for the day`,
data: v.viewsPerDay.map(day => day.views),
fill: false,
borderColor: '#c6c6c6'
}
],
total: v.viewsPerDay.map(day => day.views)
.reduce((p, c) => p + c, 0),
total: v.viewsPerDay.map(day => day.views)
.reduce((p, c) => p + c, 0),
startDate: v.viewsPerDay.length !== 0
? v.viewsPerDay[0].date.toLocaleDateString()
: ''
}))
startDate: v.viewsPerDay.length !== 0
? v.viewsPerDay[0].date.toLocaleDateString()
: ''
}))
this.buildChartOptions()
this.buildChartOptions()
this.onChannelDataSubject.next(res.data)
this.onChannelDataSubject.next(res.data)
},
error: err => this.notifier.error(err.message)
})
}

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 & {
videoId: number | string
videoPassword?: string
start?: number
count?: number
sort?: string
}) {
listThreads (
options: OverrideCommandOptions & {
videoId: number | string
videoPassword?: string
start?: number
count?: number
sort?: string
}
) {
const { start, count, sort, videoId, videoPassword } = options
const path = '/api/v1/videos/' + videoId + '/comment-threads'
@ -99,10 +101,12 @@ export class CommentsCommand extends AbstractCommand {
})
}
getThread (options: OverrideCommandOptions & {
videoId: number | string
threadId: number
}) {
getThread (
options: OverrideCommandOptions & {
videoId: number | string
threadId: number
}
) {
const { videoId, threadId } = options
const path = '/api/v1/videos/' + videoId + '/comment-threads/' + threadId
@ -115,21 +119,25 @@ export class CommentsCommand extends AbstractCommand {
})
}
async getThreadOf (options: OverrideCommandOptions & {
videoId: number | string
text: string
}) {
async getThreadOf (
options: OverrideCommandOptions & {
videoId: number | string
text: string
}
) {
const { videoId, text } = options
const threadId = await this.findCommentId({ videoId, text })
return this.getThread({ ...options, videoId, threadId })
}
async createThread (options: OverrideCommandOptions & {
videoId: number | string
text: string
videoPassword?: string
}) {
async createThread (
options: OverrideCommandOptions & {
videoId: number | string
text: string
videoPassword?: string
}
) {
const { videoId, text, videoPassword } = options
const path = '/api/v1/videos/' + videoId + '/comment-threads'
@ -149,12 +157,14 @@ export class CommentsCommand extends AbstractCommand {
return body.comment
}
async addReply (options: OverrideCommandOptions & {
videoId: number | string
toCommentId: number
text: string
videoPassword?: string
}) {
async addReply (
options: OverrideCommandOptions & {
videoId: number | string
toCommentId: number
text: string
videoPassword?: string
}
) {
const { videoId, toCommentId, text, videoPassword } = options
const path = '/api/v1/videos/' + videoId + '/comments/' + toCommentId
@ -173,22 +183,28 @@ export class CommentsCommand extends AbstractCommand {
return body.comment
}
async addReplyToLastReply (options: OverrideCommandOptions & {
text: string
}) {
async addReplyToLastReply (
options: OverrideCommandOptions & {
text: string
}
) {
return this.addReply({ ...options, videoId: this.lastVideoId, toCommentId: this.lastReplyId })
}
async addReplyToLastThread (options: OverrideCommandOptions & {
text: string
}) {
async addReplyToLastThread (
options: OverrideCommandOptions & {
text: string
}
) {
return this.addReply({ ...options, videoId: this.lastVideoId, toCommentId: this.lastThreadId })
}
async findCommentId (options: OverrideCommandOptions & {
videoId: number | string
text: string
}) {
async findCommentId (
options: OverrideCommandOptions & {
videoId: number | string
text: string
}
) {
const { videoId, text } = options
const { data } = await this.listForAdmin({ videoId, count: 25, sort: '-createdAt' })
@ -197,10 +213,12 @@ export class CommentsCommand extends AbstractCommand {
// ---------------------------------------------------------------------------
delete (options: OverrideCommandOptions & {
videoId: number | string
commentId: number
}) {
delete (
options: OverrideCommandOptions & {
videoId: number | string
commentId: number
}
) {
const { videoId, commentId } = options
const path = '/api/v1/videos/' + videoId + '/comments/' + commentId
@ -213,9 +231,11 @@ export class CommentsCommand extends AbstractCommand {
})
}
async deleteAllComments (options: OverrideCommandOptions & {
videoUUID: string
}) {
async deleteAllComments (
options: OverrideCommandOptions & {
videoUUID: string
}
) {
const { data } = await this.listForAdmin({ ...options, start: 0, count: 20 })
for (const comment of data) {
@ -227,10 +247,12 @@ export class CommentsCommand extends AbstractCommand {
// ---------------------------------------------------------------------------
approve (options: OverrideCommandOptions & {
videoId: number | string
commentId: number
}) {
approve (
options: OverrideCommandOptions & {
videoId: number | string
commentId: number
}
) {
const { videoId, commentId } = options
const path = '/api/v1/videos/' + videoId + '/comments/' + commentId + '/approve'

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

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

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

@ -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,305 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import { HttpStatusCode, VideoChannelCollaboratorState, VideoPrivacy } from '@peertube/peertube-models'
import {
cleanupTests,
createMultipleServers,
doubleFollow,
makeRawRequest,
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 makeRawRequest({ url: collab.account.url, expectedStatus: HttpStatusCode.OK_200 })
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,32 +131,36 @@ export {
// ---------------------------------------------------------------------------
async function listUserVideos (req: express.Request, res: express.Response) {
async function listMyVideos (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
const countVideos = getCountVideos(req)
const query = pickCommonVideoQuery(req.query)
const include = (query.include || VideoInclude.NONE) | VideoInclude.BLACKLISTED | VideoInclude.NOT_PUBLISHED_STATE
const apiOptions = await Hooks.wrapObject({
privacyOneOf: getAllPrivacies(),
const apiOptions = await Hooks.wrapObject(
{
privacyOneOf: getAllPrivacies(),
...query,
...query,
// Display all
nsfw: null,
// Display all
nsfw: null,
user,
accountId: user.Account.id,
displayOnlyForFollower: null,
user,
accountId: user.Account.id,
displayOnlyForFollower: null,
videoChannelId: res.locals.videoChannel?.id,
channelNameOneOf: req.query.channelNameOneOf,
videoChannelId: res.locals.videoChannel?.id,
channelNameOneOf: req.query.channelNameOneOf,
includeCollaborations: req.query.includeCollaborations || false,
countVideos,
countVideos,
include
}, 'filter:api.user.me.videos.list.params')
include
} satisfies Parameters<typeof VideoModel.listForApi>[0],
'filter:api.user.me.videos.list.params'
)
const resultList = await Hooks.wrapPromiseFun(
VideoModel.listForApi.bind(VideoModel),
@ -170,7 +174,7 @@ async function listUserVideos (req: express.Request, res: express.Response) {
async function listCommentsOnUserVideos (req: express.Request, res: express.Response) {
const userAccount = res.locals.oauth.token.User.Account
const options = {
const resultList = await VideoCommentModel.listCommentsForApi({
...pick(req.query, [
'start',
'count',
@ -182,14 +186,15 @@ async function listCommentsOnUserVideos (req: express.Request, res: express.Resp
]),
autoTagOfAccountId: userAccount.id,
videoAccountOwnerId: userAccount.id,
videoAccountOwnerIncludeCollaborations: req.query.includeCollaborations || false,
heldForReview: req.query.isHeldForReview,
videoChannelOwnerId: res.locals.videoChannel?.id,
videoId: res.locals.videoAll?.id
}
const resultList = await VideoCommentModel.listCommentsForApi(options)
})
return res.json({
total: resultList.total,
@ -197,7 +202,7 @@ async function listCommentsOnUserVideos (req: express.Request, res: express.Resp
})
}
async function getUserVideoImports (req: express.Request, res: express.Response) {
async function listMyVideoImports (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.User
const resultList = await VideoImportModel.listUserVideoImportsForApi({
userId: user.id,
@ -208,7 +213,7 @@ async function getUserVideoImports (req: express.Request, res: express.Response)
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function getUserInformation (req: express.Request, res: express.Response) {
async function getMyInformation (req: express.Request, res: express.Response) {
// We did not load channels in res.locals.user
const user = await UserModel.loadForMeAPI(res.locals.oauth.token.user.id)
@ -221,7 +226,7 @@ async function getUserInformation (req: express.Request, res: express.Response)
return res.json(result)
}
async function getUserVideoQuotaUsed (req: express.Request, res: express.Response) {
async function getMyVideoQuotaUsed (req: express.Request, res: express.Response) {
const user = res.locals.oauth.token.user
const videoQuotaUsed = await getOriginalVideoFileTotalFromUser(user)
const videoQuotaUsedDaily = await getOriginalVideoFileTotalDailyFromUser(user)
@ -233,7 +238,7 @@ async function getUserVideoQuotaUsed (req: express.Request, res: express.Respons
return res.json(data)
}
async function getUserVideoRating (req: express.Request, res: express.Response) {
async function getMyVideoRating (req: express.Request, res: express.Response) {
const videoId = res.locals.videoId.id
const accountId = +res.locals.oauth.token.User.Account.id

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: req.t('Cannot terminate an ownership change of another user')
})
return false
}
res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Cannot terminate an ownership change of another user'
})
return false
}
export {
checkUserCanTerminateOwnershipChange
return true
}

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

@ -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,13 +12,13 @@ type ImageModel = {
filename: string
onDisk: boolean
isOwned (): boolean
getPath (): string
isLocal(): boolean
getPath(): string
save (): Promise<Model>
save(): Promise<Model>
}
export abstract class AbstractPermanentFileCache <M extends ImageModel> {
export abstract class AbstractPermanentFileCache<M extends ImageModel> {
// Unsafe because it can return paths that do not exist anymore
private readonly filenameToPathUnsafeCache = new LRUCache<string, string>({
max: LRU_CACHE.FILENAME_TO_PATH_PERMANENT_FILE_CACHE.MAX_SIZE
@ -28,7 +28,6 @@ export abstract class AbstractPermanentFileCache <M extends ImageModel> {
protected abstract loadModel (filename: string): Promise<M>
constructor (private readonly directory: string) {
}
async lazyServe (options: {
@ -102,7 +101,7 @@ export abstract class AbstractPermanentFileCache <M extends ImageModel> {
const { err, image, filename, next } = options
// It seems this actor image is not on the disk anymore
if (err.status === HttpStatusCode.NOT_FOUND_404 && !image.isOwned()) {
if (err.status === HttpStatusCode.NOT_FOUND_404 && !image.isLocal()) {
logger.error('Cannot lazy serve image %s.', filename, { err })
this.filenameToPathUnsafeCache.delete(filename)

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

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> {
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> {
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

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

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

View file

@ -70,7 +70,7 @@ export class NewAbuseForModerators extends AbstractNotification<NewAbusePayload>
channelDisplayName: channel.getDisplayName(),
channelUrl: channel.getClientUrl(),
reporter: this.payload.reporter,
action: this.buildEmailAction()
action: this.buildEmailAction(to)
}
}
}
@ -85,12 +85,12 @@ export class NewAbuseForModerators extends AbstractNotification<NewAbusePayload>
locals: {
commentUrl: WEBSERVER.URL + comment.getCommentStaticPath(),
videoName: comment.Video.name,
isLocal: comment.isOwned(),
isLocal: comment.isLocal(),
commentCreatedAt: new Date(comment.createdAt).toLocaleString(),
reason: this.payload.abuse.reason,
flaggedAccount: this.payload.abuseInstance.FlaggedAccount.getDisplayName(),
reporter: this.payload.reporter,
action: this.buildEmailAction()
action: this.buildEmailAction(to)
}
}
}
@ -106,17 +106,17 @@ export class NewAbuseForModerators extends AbstractNotification<NewAbusePayload>
locals: {
accountUrl,
accountDisplayName: account.getDisplayName(),
isLocal: account.isOwned(),
isLocal: account.isLocal(),
reason: this.payload.abuse.reason,
reporter: this.payload.reporter,
action: this.buildEmailAction()
action: this.buildEmailAction(to)
}
}
}
private buildEmailAction () {
private buildEmailAction (to: To) {
return {
text: 'View report #' + this.payload.abuseInstance.id,
text: t('View report #' + this.payload.abuseInstance.id, to.language),
url: getAdminAbuseUrl(this.payload.abuseInstance)
}
}

View file

@ -9,7 +9,7 @@ export class NewAbuseMessageForReporter extends AbstractNewAbuseMessage {
async prepare () {
// Only notify our users
if (this.abuse.ReporterAccount.isOwned() !== true) return
if (this.abuse.ReporterAccount.isLocal() !== true) return
await this.loadMessageAccount()

View file

@ -0,0 +1,68 @@
import { UserNotificationSettingValue, UserNotificationType } from '@peertube/peertube-models'
import { t } from '@server/helpers/i18n.js'
import { logger } from '@server/helpers/logger.js'
import { UserModel } from '@server/models/user/user.js'
import { MUserDefault, MUserWithNotificationSetting } from '@server/types/models/index.js'
import { AbstractNotification } from '../common/abstract-notification.js'
import { buildCollaborateToChannelNotification, NotificationCollaboratePayload } from './collaborate-to-channel-utils.js'
export class AcceptedToCollaborateToChannel extends AbstractNotification<NotificationCollaboratePayload> {
private user: MUserDefault
async prepare () {
this.user = await UserModel.loadByAccountId(this.payload.channel.accountId)
}
log () {
logger.info(
`Notifying user ${this.user.username} of accepted invitation to collaborate on channel ${this.payload.channel.Actor.getIdentifier()}`
)
}
isDisabled () {
return false
}
getSetting (_user: MUserWithNotificationSetting) {
// Always notify
return UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
}
getTargetUsers () {
return [ this.user ]
}
createNotification (user: MUserWithNotificationSetting) {
return buildCollaborateToChannelNotification({
user,
payload: this.payload,
notificationType: UserNotificationType.ACCEPTED_TO_COLLABORATE_TO_CHANNEL
})
}
// ---------------------------------------------------------------------------
createEmail (user: MUserWithNotificationSetting) {
const userLanguage = user.getLanguage()
const to = { email: user.email, language: userLanguage }
const { channel, collaborator } = this.payload
const text = t('{collaboratorName} accepted your invitation to become a collaborator of {channelName}', userLanguage, {
collaboratorName: collaborator.Account.getDisplayName(),
channelName: channel.getDisplayName()
})
return {
to,
subject: text,
text,
locals: {
action: {
text: t('Manage your channel', userLanguage),
url: channel.getClientManageUrl()
}
}
}
}
}

View file

@ -0,0 +1,31 @@
import { UserNotificationType_Type } from '@peertube/peertube-models'
import { UserNotificationModel } from '@server/models/user/user-notification.js'
import { MChannelAccountDefault, MChannelCollaboratorAccount } from '@server/types/models/index.js'
import { MUserId, UserNotificationModelForApi } from '@server/types/models/user/index.js'
export type NotificationCollaboratePayload = {
collaborator: MChannelCollaboratorAccount
channel: MChannelAccountDefault
}
export function buildCollaborateToChannelNotification (options: {
user: MUserId
payload: NotificationCollaboratePayload
notificationType: UserNotificationType_Type
}): UserNotificationModelForApi {
const { user, payload, notificationType } = options
const notification = UserNotificationModel.build<UserNotificationModelForApi>({
type: notificationType,
userId: user.id,
channelCollaboratorId: payload.collaborator.id
})
notification.VideoChannelCollaborator = Object.assign(payload.collaborator, {
Account: payload.collaborator.Account,
Channel: payload.channel
})
return notification
}

View file

@ -0,0 +1,69 @@
import { UserNotificationSettingValue, UserNotificationType } from '@peertube/peertube-models'
import { t } from '@server/helpers/i18n.js'
import { logger } from '@server/helpers/logger.js'
import { WEBSERVER } from '@server/initializers/constants.js'
import { UserModel } from '@server/models/user/user.js'
import { MUserDefault, MUserWithNotificationSetting } from '@server/types/models/index.js'
import { AbstractNotification } from '../common/abstract-notification.js'
import { buildCollaborateToChannelNotification, NotificationCollaboratePayload } from './collaborate-to-channel-utils.js'
export class InvitedToCollaborateToChannel extends AbstractNotification<NotificationCollaboratePayload> {
private user: MUserDefault
async prepare () {
this.user = await UserModel.loadByAccountId(this.payload.collaborator.accountId)
}
log () {
logger.info(
`Notifying user ${this.user.username} of invitation to collaborate on channel ${this.payload.channel.Actor.getIdentifier()}`
)
}
isDisabled () {
return false
}
getSetting (_user: MUserWithNotificationSetting) {
// Always notify
return UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
}
getTargetUsers () {
return [ this.user ]
}
createNotification (user: MUserWithNotificationSetting) {
return buildCollaborateToChannelNotification({
user,
payload: this.payload,
notificationType: UserNotificationType.INVITED_TO_COLLABORATE_TO_CHANNEL
})
}
// ---------------------------------------------------------------------------
createEmail (user: MUserWithNotificationSetting) {
const userLanguage = user.getLanguage()
const to = { email: user.email, language: userLanguage }
const { channel } = this.payload
const text = t('{channelOwner} invited you to become a collaborator of channel {channelName}', userLanguage, {
channelOwner: channel.Account.getDisplayName(),
channelName: channel.getDisplayName()
})
return {
to,
subject: text,
text,
locals: {
action: {
text: t('Review the invitation', userLanguage),
url: WEBSERVER.URL + '/my-account/notifications'
}
}
}
}
}

View file

@ -0,0 +1,81 @@
import { UserNotificationDataCollaborationRejected, UserNotificationSettingValue, UserNotificationType } from '@peertube/peertube-models'
import { t } from '@server/helpers/i18n.js'
import { logger } from '@server/helpers/logger.js'
import { UserNotificationModel } from '@server/models/user/user-notification.js'
import { UserModel } from '@server/models/user/user.js'
import { MUserDefault, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models/index.js'
import { AbstractNotification } from '../common/abstract-notification.js'
import { NotificationCollaboratePayload } from './collaborate-to-channel-utils.js'
export class RefusedToCollaborateToChannel extends AbstractNotification<NotificationCollaboratePayload> {
private user: MUserDefault
async prepare () {
this.user = await UserModel.loadByAccountId(this.payload.channel.accountId)
}
log () {
logger.info(
`Notifying user ${this.user.username} of refused invitation to collaborate on channel ${this.payload.channel.Actor.getIdentifier()}`
)
}
isDisabled () {
return false
}
getSetting (_user: MUserWithNotificationSetting) {
// Always notify
return UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
}
getTargetUsers () {
return [ this.user ]
}
createNotification (user: MUserWithNotificationSetting) {
const { channel, collaborator } = this.payload
return UserNotificationModel.build<UserNotificationModelForApi>({
type: UserNotificationType.REFUSED_TO_COLLABORATE_TO_CHANNEL,
userId: user.id,
data: {
channelDisplayName: channel.getDisplayName(),
channelHandle: channel.Actor.getIdentifier(),
channelOwnerDisplayName: this.user.Account.getDisplayName(),
channelOwnerHandle: this.user.Account.Actor.getIdentifier(),
collaboratorDisplayName: collaborator.Account.getDisplayName(),
collaboratorHandle: collaborator.Account.Actor.getIdentifier()
} satisfies UserNotificationDataCollaborationRejected
})
}
// ---------------------------------------------------------------------------
createEmail (user: MUserWithNotificationSetting) {
const userLanguage = user.getLanguage()
const to = { email: user.email, language: userLanguage }
const { channel, collaborator } = this.payload
const text = t('{collaboratorName} refused your invitation to become a collaborator of {channelName}', userLanguage, {
collaboratorName: collaborator.Account.getDisplayName(),
channelName: channel.getDisplayName()
})
return {
to,
subject: text,
text,
locals: {
action: {
text: t('Manage your channel', userLanguage),
url: channel.getClientManageUrl()
}
}
}
}
}

View file

@ -40,7 +40,7 @@ export class CommentMention extends AbstractNotification<MCommentOwnerVideo, MUs
this.users = await UserModel.listByUsernames(extractedUsernames)
if (this.payload.Video.isOwned()) {
if (this.payload.Video.isLocal()) {
const userException = await UserModel.loadByVideoId(this.payload.videoId)
this.users = this.users.filter(u => u.id !== userException.id)
}

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