mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2025-10-03 09:49:20 +02:00
Add public links to AP representation
See https://github.com/Chocobozzz/PeerTube/issues/6389
This commit is contained in:
parent
de5adc09b2
commit
e81b6eba74
17 changed files with 267 additions and 152 deletions
12
.dprint.json
12
.dprint.json
|
@ -12,8 +12,7 @@
|
||||||
"singleBodyPosition": "maintain",
|
"singleBodyPosition": "maintain",
|
||||||
"nextControlFlowPosition": "sameLine",
|
"nextControlFlowPosition": "sameLine",
|
||||||
"trailingCommas": "never",
|
"trailingCommas": "never",
|
||||||
"operatorPosition": "sameLine",
|
"operatorPosition": "maintain",
|
||||||
"conditionalExpression.operatorPosition": "nextLine",
|
|
||||||
"preferHanging": false,
|
"preferHanging": false,
|
||||||
"preferSingleLine": false,
|
"preferSingleLine": false,
|
||||||
"arrowFunction.useParentheses": "preferNone",
|
"arrowFunction.useParentheses": "preferNone",
|
||||||
|
@ -64,12 +63,9 @@
|
||||||
"arrayExpression.spaceAround": true,
|
"arrayExpression.spaceAround": true,
|
||||||
"arrayPattern.spaceAround": true
|
"arrayPattern.spaceAround": true
|
||||||
},
|
},
|
||||||
"json": {
|
"json": {},
|
||||||
},
|
"markdown": {},
|
||||||
"markdown": {
|
"toml": {},
|
||||||
},
|
|
||||||
"toml": {
|
|
||||||
},
|
|
||||||
"excludes": [
|
"excludes": [
|
||||||
"**/node_modules",
|
"**/node_modules",
|
||||||
"**/*-lock.json",
|
"**/*-lock.json",
|
||||||
|
|
|
@ -50,7 +50,7 @@
|
||||||
"SwitchCase": 1,
|
"SwitchCase": 1,
|
||||||
"MemberExpression": "off",
|
"MemberExpression": "off",
|
||||||
// https://github.com/eslint/eslint/issues/15299
|
// https://github.com/eslint/eslint/issues/15299
|
||||||
"ignoredNodes": ["PropertyDefinition", "TSTypeParameterInstantiation"]
|
"ignoredNodes": ["PropertyDefinition", "TSTypeParameterInstantiation", "TSConditionalType *"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"@typescript-eslint/consistent-type-assertions": [
|
"@typescript-eslint/consistent-type-assertions": [
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ActivityIconObject, ActivityPubAttributedTo } from './objects/common-objects.js'
|
import { ActivityIconObject, ActivityPubAttributedTo, ActivityUrlObject } from './objects/common-objects.js'
|
||||||
|
|
||||||
export type ActivityPubActorType = 'Person' | 'Application' | 'Group' | 'Service' | 'Organization'
|
export type ActivityPubActorType = 'Person' | 'Application' | 'Group' | 'Service' | 'Organization'
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ export interface ActivityPubActor {
|
||||||
inbox: string
|
inbox: string
|
||||||
outbox: string
|
outbox: string
|
||||||
preferredUsername: string
|
preferredUsername: string
|
||||||
url: string
|
url: ActivityUrlObject[] | string
|
||||||
name: string
|
name: string
|
||||||
endpoints: {
|
endpoints: {
|
||||||
sharedInbox: string
|
sharedInbox: string
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
|
|
||||||
import { expect } from 'chai'
|
import {
|
||||||
import { processViewersStats } from '@tests/shared/views.js'
|
ActivityPubActor,
|
||||||
import { HttpStatusCode, VideoComment, VideoPlaylistPrivacy, WatchActionObject } from '@peertube/peertube-models'
|
HttpStatusCode,
|
||||||
|
VideoComment,
|
||||||
|
VideoObject,
|
||||||
|
VideoPlaylistPrivacy,
|
||||||
|
WatchActionObject
|
||||||
|
} from '@peertube/peertube-models'
|
||||||
import {
|
import {
|
||||||
cleanupTests,
|
cleanupTests,
|
||||||
createMultipleServers,
|
createMultipleServers,
|
||||||
|
@ -12,8 +17,10 @@ import {
|
||||||
setAccessTokensToServers,
|
setAccessTokensToServers,
|
||||||
setDefaultVideoChannel
|
setDefaultVideoChannel
|
||||||
} from '@peertube/peertube-server-commands'
|
} from '@peertube/peertube-server-commands'
|
||||||
|
import { processViewersStats } from '@tests/shared/views.js'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
|
||||||
describe('Test activitypub', function () {
|
describe('Test ActivityPub', function () {
|
||||||
let servers: PeerTubeServer[] = []
|
let servers: PeerTubeServer[] = []
|
||||||
let video: { id: number, uuid: string, shortUUID: string }
|
let video: { id: number, uuid: string, shortUUID: string }
|
||||||
let playlist: { id: number, uuid: string, shortUUID: string }
|
let playlist: { id: number, uuid: string, shortUUID: string }
|
||||||
|
@ -21,31 +28,62 @@ describe('Test activitypub', function () {
|
||||||
|
|
||||||
async function testAccount (path: string) {
|
async function testAccount (path: string) {
|
||||||
const res = await makeActivityPubGetRequest(servers[0].url, path)
|
const res = await makeActivityPubGetRequest(servers[0].url, path)
|
||||||
const object = res.body
|
const object = res.body as ActivityPubActor
|
||||||
|
|
||||||
expect(object.type).to.equal('Person')
|
expect(object.type).to.equal('Person')
|
||||||
expect(object.id).to.equal(servers[0].url + '/accounts/root')
|
expect(object.id).to.equal(servers[0].url + '/accounts/root')
|
||||||
expect(object.name).to.equal('root')
|
expect(object.name).to.equal('root')
|
||||||
expect(object.preferredUsername).to.equal('root')
|
expect(object.preferredUsername).to.equal('root')
|
||||||
|
|
||||||
|
// TODO: enable in v8
|
||||||
|
// const htmlURLs = [
|
||||||
|
// servers[0].url + '/accounts/root',
|
||||||
|
// servers[0].url + '/a/root',
|
||||||
|
// servers[0].url + '/a/root/video-channels'
|
||||||
|
// ]
|
||||||
|
|
||||||
|
// for (const htmlURL of htmlURLs) {
|
||||||
|
// expect(object.url.find(u => u.href === htmlURL), htmlURL).to.exist
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function testChannel (path: string) {
|
async function testChannel (path: string) {
|
||||||
const res = await makeActivityPubGetRequest(servers[0].url, path)
|
const res = await makeActivityPubGetRequest(servers[0].url, path)
|
||||||
const object = res.body
|
const object = res.body as ActivityPubActor
|
||||||
|
|
||||||
expect(object.type).to.equal('Group')
|
expect(object.type).to.equal('Group')
|
||||||
expect(object.id).to.equal(servers[0].url + '/video-channels/root_channel')
|
expect(object.id).to.equal(servers[0].url + '/video-channels/root_channel')
|
||||||
expect(object.name).to.equal('Main root channel')
|
expect(object.name).to.equal('Main root channel')
|
||||||
expect(object.preferredUsername).to.equal('root_channel')
|
expect(object.preferredUsername).to.equal('root_channel')
|
||||||
|
|
||||||
|
// TODO: enable in v8
|
||||||
|
// const htmlURLs = [
|
||||||
|
// servers[0].url + '/video-channels/root_channel',
|
||||||
|
// servers[0].url + '/c/root_channel',
|
||||||
|
// servers[0].url + '/c/root_channel/videos'
|
||||||
|
// ]
|
||||||
|
|
||||||
|
// for (const htmlURL of htmlURLs) {
|
||||||
|
// expect(object.url.find(u => u.href === htmlURL), htmlURL).to.exist
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function testVideo (path: string) {
|
async function testVideo (path: string) {
|
||||||
const res = await makeActivityPubGetRequest(servers[0].url, path)
|
const res = await makeActivityPubGetRequest(servers[0].url, path)
|
||||||
const object = res.body
|
const object = res.body as VideoObject
|
||||||
|
|
||||||
expect(object.type).to.equal('Video')
|
expect(object.type).to.equal('Video')
|
||||||
expect(object.id).to.equal(servers[0].url + '/videos/watch/' + video.uuid)
|
expect(object.id).to.equal(servers[0].url + '/videos/watch/' + video.uuid)
|
||||||
expect(object.name).to.equal('video')
|
expect(object.name).to.equal('video')
|
||||||
|
|
||||||
|
const htmlURLs = [
|
||||||
|
servers[0].url + '/videos/watch/' + video.uuid,
|
||||||
|
servers[0].url + '/w/' + video.shortUUID
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const htmlURL of htmlURLs) {
|
||||||
|
expect(object.url.find(u => u.href === htmlURL), htmlURL).to.exist
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function testComment (path: string) {
|
async function testComment (path: string) {
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { unarray } from '@peertube/peertube-core-utils'
|
import { arrayify } from '@peertube/peertube-core-utils'
|
||||||
import { peertubeTruncate } from '@server/helpers/core-utils.js'
|
import { peertubeTruncate } from '@server/helpers/core-utils.js'
|
||||||
|
import { ActivityPubActor } from 'packages/models/src/activitypub/activitypub-actor.js'
|
||||||
import validator from 'validator'
|
import validator from 'validator'
|
||||||
import { CONSTRAINTS_FIELDS } from '../../../initializers/constants.js'
|
import { CONSTRAINTS_FIELDS } from '../../../initializers/constants.js'
|
||||||
import { exists, isArray, isDateValid } from '../misc.js'
|
import { exists, isArray, isDateValid } from '../misc.js'
|
||||||
import { isHostValid } from '../servers.js'
|
import { isHostValid } from '../servers.js'
|
||||||
import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc.js'
|
import { isActivityPubHTMLUrlValid, isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc.js'
|
||||||
|
|
||||||
export function isActorEndpointsObjectValid (endpointObject: any) {
|
export function isActorEndpointsObjectValid (endpointObject: any) {
|
||||||
if (endpointObject?.sharedInbox) {
|
if (endpointObject?.sharedInbox) {
|
||||||
|
@ -62,7 +63,7 @@ export function isActorDeleteActivityValid (activity: any) {
|
||||||
return isBaseActivityValid(activity, 'Delete')
|
return isBaseActivityValid(activity, 'Delete')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sanitizeAndCheckActorObject (actor: any) {
|
export function sanitizeAndCheckActorObject (actor: ActivityPubActor) {
|
||||||
if (!isActorTypeValid(actor.type)) return false
|
if (!isActorTypeValid(actor.type)) return false
|
||||||
|
|
||||||
normalizeActor(actor)
|
normalizeActor(actor)
|
||||||
|
@ -71,43 +72,16 @@ export function sanitizeAndCheckActorObject (actor: any) {
|
||||||
isActivityPubUrlValid(actor.id) &&
|
isActivityPubUrlValid(actor.id) &&
|
||||||
isActivityPubUrlValid(actor.inbox) &&
|
isActivityPubUrlValid(actor.inbox) &&
|
||||||
isActorPreferredUsernameValid(actor.preferredUsername) &&
|
isActorPreferredUsernameValid(actor.preferredUsername) &&
|
||||||
isActivityPubUrlValid(actor.url) &&
|
|
||||||
isActorPublicKeyObjectValid(actor.publicKey) &&
|
isActorPublicKeyObjectValid(actor.publicKey) &&
|
||||||
isActorEndpointsObjectValid(actor.endpoints) &&
|
isActorEndpointsObjectValid(actor.endpoints) &&
|
||||||
|
|
||||||
(!actor.outbox || isActivityPubUrlValid(actor.outbox)) &&
|
(!actor.outbox || isActivityPubUrlValid(actor.outbox)) &&
|
||||||
(!actor.following || isActivityPubUrlValid(actor.following)) &&
|
(!actor.following || isActivityPubUrlValid(actor.following)) &&
|
||||||
(!actor.followers || isActivityPubUrlValid(actor.followers)) &&
|
(!actor.followers || isActivityPubUrlValid(actor.followers)) &&
|
||||||
|
|
||||||
setValidAttributedTo(actor) &&
|
|
||||||
setValidDescription(actor) &&
|
|
||||||
// If this is a group (a channel), it should be attributed to an account
|
// If this is a group (a channel), it should be attributed to an account
|
||||||
// In PeerTube we use this to attach a video channel to a specific account
|
// In PeerTube we use this to attach a video channel to a specific account
|
||||||
(actor.type !== 'Group' || actor.attributedTo.length !== 0)
|
(actor.type !== 'Group' || actor.attributedTo.length !== 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeActor (actor: any) {
|
|
||||||
if (!actor) return
|
|
||||||
|
|
||||||
if (!actor.url) {
|
|
||||||
actor.url = actor.id
|
|
||||||
} else if (isArray(actor.url)) {
|
|
||||||
actor.url = unarray(actor.url)
|
|
||||||
} else if (typeof actor.url !== 'string') {
|
|
||||||
actor.url = actor.url.href || actor.url.url
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isDateValid(actor.published)) actor.published = undefined
|
|
||||||
|
|
||||||
if (actor.summary && typeof actor.summary === 'string') {
|
|
||||||
actor.summary = peertubeTruncate(actor.summary, { length: CONSTRAINTS_FIELDS.USERS.DESCRIPTION.max })
|
|
||||||
|
|
||||||
if (actor.summary.length < CONSTRAINTS_FIELDS.USERS.DESCRIPTION.min) {
|
|
||||||
actor.summary = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isValidActorHandle (handle: string) {
|
export function isValidActorHandle (handle: string) {
|
||||||
if (!exists(handle)) return false
|
if (!exists(handle)) return false
|
||||||
|
|
||||||
|
@ -121,8 +95,38 @@ export function areValidActorHandles (handles: string[]) {
|
||||||
return isArray(handles) && handles.every(h => isValidActorHandle(h))
|
return isArray(handles) && handles.every(h => isValidActorHandle(h))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setValidDescription (obj: any) {
|
// ---------------------------------------------------------------------------
|
||||||
if (!obj.summary) obj.summary = null
|
// Private
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
return true
|
function normalizeActor (actor: ActivityPubActor) {
|
||||||
|
if (!actor) return
|
||||||
|
|
||||||
|
setValidUrls(actor)
|
||||||
|
setValidAttributedTo(actor)
|
||||||
|
setValidDescription(actor)
|
||||||
|
|
||||||
|
if (!isDateValid(actor.published)) actor.published = undefined
|
||||||
|
|
||||||
|
if (actor.summary && typeof actor.summary === 'string') {
|
||||||
|
actor.summary = peertubeTruncate(actor.summary, { length: CONSTRAINTS_FIELDS.USERS.DESCRIPTION.max })
|
||||||
|
|
||||||
|
if (actor.summary.length < CONSTRAINTS_FIELDS.USERS.DESCRIPTION.min) {
|
||||||
|
actor.summary = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setValidDescription (actor: ActivityPubActor) {
|
||||||
|
if (!actor.summary) actor.summary = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function setValidUrls (actor: any) {
|
||||||
|
if (!actor.url) {
|
||||||
|
actor.url = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
actor.url = arrayify(actor.url)
|
||||||
|
.filter(u => isActivityPubHTMLUrlValid(u))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import validator from 'validator'
|
|
||||||
import { CONFIG } from '@server/initializers/config.js'
|
import { CONFIG } from '@server/initializers/config.js'
|
||||||
|
import { ActivityHtmlUrlObject } from 'packages/models/src/activitypub/index.js'
|
||||||
|
import validator from 'validator'
|
||||||
import { CONSTRAINTS_FIELDS } from '../../../initializers/constants.js'
|
import { CONSTRAINTS_FIELDS } from '../../../initializers/constants.js'
|
||||||
import { exists } from '../misc.js'
|
import { exists } from '../misc.js'
|
||||||
|
|
||||||
function isUrlValid (url: string) {
|
export function isUrlValid (url: string) {
|
||||||
const isURLOptions = {
|
const isURLOptions = {
|
||||||
require_host: true,
|
require_host: true,
|
||||||
require_tld: true,
|
require_tld: true,
|
||||||
|
@ -20,11 +21,11 @@ function isUrlValid (url: string) {
|
||||||
return exists(url) && validator.default.isURL('' + url, isURLOptions)
|
return exists(url) && validator.default.isURL('' + url, isURLOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isActivityPubUrlValid (url: string) {
|
export function isActivityPubUrlValid (url: string) {
|
||||||
return isUrlValid(url) && validator.default.isLength('' + url, CONSTRAINTS_FIELDS.ACTORS.URL)
|
return isUrlValid(url) && validator.default.isLength('' + url, CONSTRAINTS_FIELDS.ACTORS.URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isBaseActivityValid (activity: any, type: string) {
|
export function isBaseActivityValid (activity: any, type: string) {
|
||||||
return activity.type === type &&
|
return activity.type === type &&
|
||||||
isActivityPubUrlValid(activity.id) &&
|
isActivityPubUrlValid(activity.id) &&
|
||||||
isObjectValid(activity.actor) &&
|
isObjectValid(activity.actor) &&
|
||||||
|
@ -32,19 +33,26 @@ function isBaseActivityValid (activity: any, type: string) {
|
||||||
isUrlCollectionValid(activity.cc)
|
isUrlCollectionValid(activity.cc)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isUrlCollectionValid (collection: any) {
|
export function isUrlCollectionValid (collection: any) {
|
||||||
return collection === undefined ||
|
return collection === undefined ||
|
||||||
(Array.isArray(collection) && collection.every(t => isActivityPubUrlValid(t)))
|
(Array.isArray(collection) && collection.every(t => isActivityPubUrlValid(t)))
|
||||||
}
|
}
|
||||||
|
|
||||||
function isObjectValid (object: any) {
|
export function isObjectValid (object: any) {
|
||||||
return exists(object) &&
|
return exists(object) &&
|
||||||
(
|
(
|
||||||
isActivityPubUrlValid(object) || isActivityPubUrlValid(object.id)
|
isActivityPubUrlValid(object) || isActivityPubUrlValid(object.id)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setValidAttributedTo (obj: any) {
|
export function isActivityPubHTMLUrlValid (url: ActivityHtmlUrlObject) {
|
||||||
|
return url &&
|
||||||
|
url.type === 'Link' &&
|
||||||
|
url.mediaType === 'text/html' &&
|
||||||
|
isActivityPubUrlValid(url.href)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setValidAttributedTo (obj: any) {
|
||||||
if (Array.isArray(obj.attributedTo) === false) {
|
if (Array.isArray(obj.attributedTo) === false) {
|
||||||
obj.attributedTo = []
|
obj.attributedTo = []
|
||||||
return true
|
return true
|
||||||
|
@ -58,19 +66,10 @@ function setValidAttributedTo (obj: any) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
function isActivityPubVideoDurationValid (value: string) {
|
export function isActivityPubVideoDurationValid (value: string) {
|
||||||
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration
|
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration
|
||||||
return exists(value) &&
|
return exists(value) &&
|
||||||
typeof value === 'string' &&
|
typeof value === 'string' &&
|
||||||
value.startsWith('PT') &&
|
value.startsWith('PT') &&
|
||||||
value.endsWith('S')
|
value.endsWith('S')
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
|
||||||
isUrlValid,
|
|
||||||
isActivityPubUrlValid,
|
|
||||||
isBaseActivityValid,
|
|
||||||
setValidAttributedTo,
|
|
||||||
isObjectValid,
|
|
||||||
isActivityPubVideoDurationValid
|
|
||||||
}
|
|
||||||
|
|
|
@ -45,7 +45,7 @@ async function getOrCreateAPActor (
|
||||||
// We don't have this actor in our database, fetch it on remote
|
// We don't have this actor in our database, fetch it on remote
|
||||||
if (!actor) {
|
if (!actor) {
|
||||||
const { actorObject } = await fetchRemoteActor(actorUrl)
|
const { actorObject } = await fetchRemoteActor(actorUrl)
|
||||||
if (actorObject === undefined) throw new Error('Cannot fetch remote actor ' + actorUrl)
|
if (actorObject === undefined) throw new Error(`Cannot fetch remote actor ${actorUrl}`)
|
||||||
|
|
||||||
// actorUrl is just an alias/redirection, so process object id instead
|
// actorUrl is just an alias/redirection, so process object id instead
|
||||||
if (actorObject.id !== actorUrl) return getOrCreateAPActor(actorObject, 'all', recurseIfNeeded, updateCollections)
|
if (actorObject.id !== actorUrl) return getOrCreateAPActor(actorObject, 'all', recurseIfNeeded, updateCollections)
|
||||||
|
@ -67,7 +67,7 @@ async function getOrCreateAPActor (
|
||||||
if (actor.VideoChannel) (actor as MActorAccountChannelIdActor).VideoChannel.Actor = actor
|
if (actor.VideoChannel) (actor as MActorAccountChannelIdActor).VideoChannel.Actor = actor
|
||||||
|
|
||||||
const { actor: actorRefreshed, refreshed } = await refreshActorIfNeeded({ actor, fetchedType: fetchType })
|
const { actor: actorRefreshed, refreshed } = await refreshActorIfNeeded({ actor, fetchedType: fetchType })
|
||||||
if (!actorRefreshed) throw new Error('Actor ' + actor.url + ' does not exist anymore.')
|
if (!actorRefreshed) throw new Error(`Actor ${actor.url} does not exist anymore.`)
|
||||||
|
|
||||||
await scheduleOutboxFetchIfNeeded(actor, created, refreshed, updateCollections)
|
await scheduleOutboxFetchIfNeeded(actor, created, refreshed, updateCollections)
|
||||||
await schedulePlaylistFetchIfNeeded(actor, created, accountPlaylistsUrl)
|
await schedulePlaylistFetchIfNeeded(actor, created, accountPlaylistsUrl)
|
||||||
|
@ -75,10 +75,10 @@ async function getOrCreateAPActor (
|
||||||
return actorRefreshed
|
return actorRefreshed
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getOrCreateAPOwner (actorObject: ActivityPubActor, actorUrl: string) {
|
async function getOrCreateAPOwner (actorObject: ActivityPubActor, actorId: string) {
|
||||||
const accountAttributedTo = await findOwner(actorUrl, actorObject.attributedTo, 'Person')
|
const accountAttributedTo = await findOwner(actorId, actorObject.attributedTo, 'Person')
|
||||||
if (!accountAttributedTo) {
|
if (!accountAttributedTo) {
|
||||||
throw new Error(`Cannot find account attributed to video channel ${actorUrl}`)
|
throw new Error(`Cannot find account attributed to video channel ${actorId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -86,22 +86,22 @@ async function getOrCreateAPOwner (actorObject: ActivityPubActor, actorUrl: stri
|
||||||
const recurseIfNeeded = false
|
const recurseIfNeeded = false
|
||||||
return getOrCreateAPActor(accountAttributedTo, 'all', recurseIfNeeded)
|
return getOrCreateAPActor(accountAttributedTo, 'all', recurseIfNeeded)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('Cannot get or create account attributed to video channel ' + actorUrl)
|
logger.error(`Cannot get or create account attributed to video channel ${actorId}`)
|
||||||
throw new Error(err)
|
throw new Error(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function findOwner (rootUrl: string, attributedTo: APObjectId[] | APObjectId, type: 'Person' | 'Group') {
|
async function findOwner (rootId: string, attributedTo: APObjectId[] | APObjectId, type: 'Person' | 'Group') {
|
||||||
for (const actorToCheck of arrayify(attributedTo)) {
|
for (const actorToCheck of arrayify(attributedTo)) {
|
||||||
const actorObject = await fetchAPObjectIfNeeded<ActivityPubActor>(getAPId(actorToCheck))
|
const actorObject = await fetchAPObjectIfNeeded<ActivityPubActor>(getAPId(actorToCheck))
|
||||||
|
|
||||||
if (!actorObject) {
|
if (!actorObject) {
|
||||||
logger.warn('Unknown attributed to actor %s for owner %s', actorToCheck, rootUrl)
|
logger.warn(`Unknown attributed to actor ${actorToCheck} for owner ${rootId}`)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (checkUrlsSameHost(actorObject.id, rootUrl) !== true) {
|
if (checkUrlsSameHost(actorObject.id, rootId) !== true) {
|
||||||
logger.warn(`Account attributed to ${actorObject.id} does not have the same host than owner actor url ${rootUrl}`)
|
logger.warn(`Account attributed to ${actorObject.id} does not have the same host than owner actor url ${rootId}`)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
|
import { ActivityPubActor, ActorImageType } from '@peertube/peertube-models'
|
||||||
import { resetSequelizeInstance, runInReadCommittedTransaction } from '@server/helpers/database-utils.js'
|
import { resetSequelizeInstance, runInReadCommittedTransaction } from '@server/helpers/database-utils.js'
|
||||||
import { logger } from '@server/helpers/logger.js'
|
import { logger } from '@server/helpers/logger.js'
|
||||||
import { AccountModel } from '@server/models/account/account.js'
|
import { AccountModel } from '@server/models/account/account.js'
|
||||||
import { VideoChannelModel } from '@server/models/video/video-channel.js'
|
import { VideoChannelModel } from '@server/models/video/video-channel.js'
|
||||||
import { MAccount, MActor, MActorFull, MChannel } from '@server/types/models/index.js'
|
import { MAccount, MActor, MActorFull, MChannel } from '@server/types/models/index.js'
|
||||||
import { ActivityPubActor, ActorImageType } from '@peertube/peertube-models'
|
|
||||||
import { getOrCreateAPOwner } from './get.js'
|
import { getOrCreateAPOwner } from './get.js'
|
||||||
import { updateActorImages } from './image.js'
|
import { updateActorImages } from './image.js'
|
||||||
import { fetchActorFollowsCount } from './shared/index.js'
|
import { fetchActorFollowsCount } from './shared/index.js'
|
||||||
import { getImagesInfoFromObject } from './shared/object-to-model-attributes.js'
|
import { getImagesInfoFromObject } from './shared/object-to-model-attributes.js'
|
||||||
|
|
||||||
export class APActorUpdater {
|
export class APActorUpdater {
|
||||||
|
|
||||||
private readonly accountOrChannel: MAccount | MChannel
|
private readonly accountOrChannel: MAccount | MChannel
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
|
@ -32,7 +31,7 @@ export class APActorUpdater {
|
||||||
this.accountOrChannel.description = this.actorObject.summary
|
this.accountOrChannel.description = this.actorObject.summary
|
||||||
|
|
||||||
if (this.accountOrChannel instanceof VideoChannelModel) {
|
if (this.accountOrChannel instanceof VideoChannelModel) {
|
||||||
const owner = await getOrCreateAPOwner(this.actorObject, this.actorObject.url)
|
const owner = await getOrCreateAPOwner(this.actorObject, this.actorObject.id)
|
||||||
this.accountOrChannel.accountId = owner.Account.id
|
this.accountOrChannel.accountId = owner.Account.id
|
||||||
this.accountOrChannel.Account = owner.Account as AccountModel
|
this.accountOrChannel.Account = owner.Account as AccountModel
|
||||||
|
|
||||||
|
@ -49,7 +48,8 @@ export class APActorUpdater {
|
||||||
await this.accountOrChannel.save({ transaction: t })
|
await this.accountOrChannel.save({ transaction: t })
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.info('Remote account %s updated', this.actorObject.url)
|
// Update the following line to template string
|
||||||
|
logger.info(`Remote account ${this.actorObject.id} updated`)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (this.actor !== undefined) {
|
if (this.actor !== undefined) {
|
||||||
await resetSequelizeInstance(this.actor)
|
await resetSequelizeInstance(this.actor)
|
||||||
|
|
|
@ -113,7 +113,7 @@ async function processUpdateCacheFile (
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processUpdateActor (actor: MActorFull, actorObject: ActivityPubActor) {
|
async function processUpdateActor (actor: MActorFull, actorObject: ActivityPubActor) {
|
||||||
logger.debug('Updating remote account "%s".', actorObject.url)
|
logger.debug(`Updating remote account "${actorObject.id}".`)
|
||||||
|
|
||||||
const updater = new APActorUpdater(actorObject, actor)
|
const updater = new APActorUpdater(actorObject, actor)
|
||||||
return updater.update()
|
return updater.update()
|
||||||
|
|
|
@ -18,9 +18,8 @@ async function processActivityPubFollow (job: Job) {
|
||||||
const payload = job.data as ActivitypubFollowPayload
|
const payload = job.data as ActivitypubFollowPayload
|
||||||
const host = payload.host
|
const host = payload.host
|
||||||
|
|
||||||
const handle = host
|
const identifier = [ payload.name || SERVER_ACTOR_NAME, host ]
|
||||||
? `${payload.name}@${host}`
|
.filter(v => !!v).join('@')
|
||||||
: payload.name
|
|
||||||
|
|
||||||
logger.info('Processing ActivityPub follow in job %s.', job.id)
|
logger.info('Processing ActivityPub follow in job %s.', job.id)
|
||||||
|
|
||||||
|
@ -40,18 +39,18 @@ async function processActivityPubFollow (job: Job) {
|
||||||
|
|
||||||
targetActor = await getOrCreateAPActor(actorUrl, 'all')
|
targetActor = await getOrCreateAPActor(actorUrl, 'all')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn(`Do not follow ${handle} because we could not find the actor URL (in database or using webfinger)`, { err })
|
logger.warn(`Do not follow ${identifier} because we could not find the actor URL (in database or using webfinger)`, { err })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!targetActor) {
|
if (!targetActor) {
|
||||||
logger.warn(`Do not follow ${handle} because we could not fetch/load the actor`)
|
logger.warn(`Do not follow ${identifier} because we could not fetch/load the actor`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload.assertIsChannel && !targetActor.VideoChannel) {
|
if (payload.assertIsChannel && !targetActor.VideoChannel) {
|
||||||
logger.warn(`Do not follow ${handle} because it is not a channel.`)
|
logger.warn(`Do not follow ${identifier} because it is not a channel.`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,20 @@
|
||||||
import { Account, AccountSummary, VideoPrivacy } from '@peertube/peertube-models'
|
import { Account, AccountSummary, ActivityPubActor, VideoPrivacy } from '@peertube/peertube-models'
|
||||||
import { ModelCache } from '@server/models/shared/model-cache.js'
|
import { ModelCache } from '@server/models/shared/model-cache.js'
|
||||||
import { FindOptions, IncludeOptions, Includeable, Op, Transaction, WhereOptions, literal } from 'sequelize'
|
import { FindOptions, IncludeOptions, Includeable, Op, Transaction, WhereOptions, literal } from 'sequelize'
|
||||||
import {
|
import {
|
||||||
AfterDestroy,
|
AfterDestroy,
|
||||||
AllowNull,
|
AllowNull,
|
||||||
BeforeDestroy,
|
BeforeDestroy,
|
||||||
BelongsTo, Column,
|
BelongsTo,
|
||||||
|
Column,
|
||||||
CreatedAt,
|
CreatedAt,
|
||||||
DataType,
|
DataType,
|
||||||
Default,
|
Default,
|
||||||
DefaultScope,
|
DefaultScope,
|
||||||
ForeignKey,
|
ForeignKey,
|
||||||
HasMany,
|
HasMany,
|
||||||
Is, Scopes,
|
Is,
|
||||||
|
Scopes,
|
||||||
Table,
|
Table,
|
||||||
UpdatedAt
|
UpdatedAt
|
||||||
} from 'sequelize-typescript'
|
} from 'sequelize-typescript'
|
||||||
|
@ -20,12 +22,14 @@ import { isAccountDescriptionValid } from '../../helpers/custom-validators/accou
|
||||||
import { CONSTRAINTS_FIELDS, SERVER_ACTOR_NAME, WEBSERVER } from '../../initializers/constants.js'
|
import { CONSTRAINTS_FIELDS, SERVER_ACTOR_NAME, WEBSERVER } from '../../initializers/constants.js'
|
||||||
import { sendDeleteActor } from '../../lib/activitypub/send/send-delete.js'
|
import { sendDeleteActor } from '../../lib/activitypub/send/send-delete.js'
|
||||||
import {
|
import {
|
||||||
MAccount, MAccountAP,
|
MAccount,
|
||||||
|
MAccountAP,
|
||||||
MAccountDefault,
|
MAccountDefault,
|
||||||
MAccountFormattable,
|
MAccountFormattable,
|
||||||
MAccountHost,
|
MAccountHost,
|
||||||
|
MAccountIdHost,
|
||||||
MAccountSummaryFormattable,
|
MAccountSummaryFormattable,
|
||||||
MChannelHost
|
MChannelIdHost
|
||||||
} from '../../types/models/index.js'
|
} from '../../types/models/index.js'
|
||||||
import { ActorFollowModel } from '../actor/actor-follow.js'
|
import { ActorFollowModel } from '../actor/actor-follow.js'
|
||||||
import { ActorImageModel } from '../actor/actor-image.js'
|
import { ActorImageModel } from '../actor/actor-image.js'
|
||||||
|
@ -145,7 +149,6 @@ export type SummaryOptions = {
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class AccountModel extends SequelizeModel<AccountModel> {
|
export class AccountModel extends SequelizeModel<AccountModel> {
|
||||||
|
|
||||||
@AllowNull(false)
|
@AllowNull(false)
|
||||||
@Column
|
@Column
|
||||||
name: string
|
name: string
|
||||||
|
@ -423,7 +426,7 @@ export class AccountModel extends SequelizeModel<AccountModel> {
|
||||||
|
|
||||||
static listLocalsForSitemap (sort: string): Promise<MAccountHost[]> {
|
static listLocalsForSitemap (sort: string): Promise<MAccountHost[]> {
|
||||||
return AccountModel.unscoped().findAll({
|
return AccountModel.unscoped().findAll({
|
||||||
attributes: [ ],
|
attributes: [],
|
||||||
offset: 0,
|
offset: 0,
|
||||||
order: getSort(sort),
|
order: getSort(sort),
|
||||||
include: [
|
include: [
|
||||||
|
@ -474,10 +477,30 @@ export class AccountModel extends SequelizeModel<AccountModel> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async toActivityPubObject (this: MAccountAP) {
|
async toActivityPubObject (this: MAccountAP): Promise<ActivityPubActor> {
|
||||||
const obj = await this.Actor.toActivityPubObject(this.name)
|
const obj = await this.Actor.toActivityPubObject(this.name)
|
||||||
|
|
||||||
return Object.assign(obj, {
|
return Object.assign(obj, {
|
||||||
|
// // TODO: Uncomment in v8 for backward compatibility
|
||||||
|
// url: [
|
||||||
|
// {
|
||||||
|
// type: 'Link',
|
||||||
|
// mediaType: 'text/html',
|
||||||
|
// href: this.getClientUrl(true)
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// type: 'Link',
|
||||||
|
// mediaType: 'text/html',
|
||||||
|
// href: this.getClientUrl(false)
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// type: 'Link',
|
||||||
|
// mediaType: 'text/html',
|
||||||
|
// href: this.Actor.url
|
||||||
|
// }
|
||||||
|
// ] as ActivityUrlObject[],
|
||||||
|
|
||||||
|
url: this.Actor.url,
|
||||||
summary: this.description
|
summary: this.description
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -495,8 +518,12 @@ export class AccountModel extends SequelizeModel<AccountModel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Avoid error when running this method on MAccount... | MChannel...
|
// Avoid error when running this method on MAccount... | MChannel...
|
||||||
getClientUrl (this: MAccountHost | MChannelHost) {
|
getClientUrl (this: MAccountIdHost | MChannelIdHost, channelsSuffix = true) {
|
||||||
return WEBSERVER.URL + '/a/' + this.Actor.getIdentifier() + '/video-channels'
|
const suffix = channelsSuffix
|
||||||
|
? '/video-channels'
|
||||||
|
: ''
|
||||||
|
|
||||||
|
return WEBSERVER.URL + '/a/' + this.Actor.getIdentifier() + suffix
|
||||||
}
|
}
|
||||||
|
|
||||||
isBlocked () {
|
isBlocked () {
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
import { forceNumber, maxBy } from '@peertube/peertube-core-utils'
|
import { forceNumber, maxBy } from '@peertube/peertube-core-utils'
|
||||||
import { ActivityIconObject, ActorImageType, ActorImageType_Type, type ActivityPubActorType } from '@peertube/peertube-models'
|
import {
|
||||||
|
ActivityIconObject,
|
||||||
|
ActorImageType,
|
||||||
|
ActorImageType_Type,
|
||||||
|
type ActivityPubActorType
|
||||||
|
} from '@peertube/peertube-models'
|
||||||
import { AttributesOnly } from '@peertube/peertube-typescript-utils'
|
import { AttributesOnly } from '@peertube/peertube-typescript-utils'
|
||||||
import { activityPubContextify } from '@server/helpers/activity-pub-utils.js'
|
import { activityPubContextify } from '@server/helpers/activity-pub-utils.js'
|
||||||
import { getContextFilter } from '@server/lib/activitypub/context.js'
|
import { getContextFilter } from '@server/lib/activitypub/context.js'
|
||||||
|
@ -15,7 +20,8 @@ import {
|
||||||
ForeignKey,
|
ForeignKey,
|
||||||
HasMany,
|
HasMany,
|
||||||
HasOne,
|
HasOne,
|
||||||
Is, Scopes,
|
Is,
|
||||||
|
Scopes,
|
||||||
Table,
|
Table,
|
||||||
UpdatedAt
|
UpdatedAt
|
||||||
} from 'sequelize-typescript'
|
} from 'sequelize-typescript'
|
||||||
|
@ -31,7 +37,8 @@ import { isActivityPubUrlValid } from '../../helpers/custom-validators/activityp
|
||||||
import {
|
import {
|
||||||
ACTIVITY_PUB,
|
ACTIVITY_PUB,
|
||||||
ACTIVITY_PUB_ACTOR_TYPES,
|
ACTIVITY_PUB_ACTOR_TYPES,
|
||||||
CONSTRAINTS_FIELDS, SERVER_ACTOR_NAME,
|
CONSTRAINTS_FIELDS,
|
||||||
|
SERVER_ACTOR_NAME,
|
||||||
WEBSERVER
|
WEBSERVER
|
||||||
} from '../../initializers/constants.js'
|
} from '../../initializers/constants.js'
|
||||||
import {
|
import {
|
||||||
|
@ -159,7 +166,6 @@ export const unusedActorAttributesForAPI: (keyof AttributesOnly<ActorModel>)[] =
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class ActorModel extends SequelizeModel<ActorModel> {
|
export class ActorModel extends SequelizeModel<ActorModel> {
|
||||||
|
|
||||||
@AllowNull(false)
|
@AllowNull(false)
|
||||||
@Column(DataType.ENUM(...Object.values(ACTIVITY_PUB_ACTOR_TYPES)))
|
@Column(DataType.ENUM(...Object.values(ACTIVITY_PUB_ACTOR_TYPES)))
|
||||||
type: ActivityPubActorType
|
type: ActivityPubActorType
|
||||||
|
@ -346,10 +352,10 @@ export class ActorModel extends SequelizeModel<ActorModel> {
|
||||||
|
|
||||||
static loadAccountActorFollowerUrlByVideoId (videoId: number, transaction: Transaction) {
|
static loadAccountActorFollowerUrlByVideoId (videoId: number, transaction: Transaction) {
|
||||||
const query = `SELECT "actor"."id" AS "id", "actor"."followersUrl" AS "followersUrl" ` +
|
const query = `SELECT "actor"."id" AS "id", "actor"."followersUrl" AS "followersUrl" ` +
|
||||||
`FROM "actor" ` +
|
`FROM "actor" ` +
|
||||||
`INNER JOIN "account" ON "actor"."id" = "account"."actorId" ` +
|
`INNER JOIN "account" ON "actor"."id" = "account"."actorId" ` +
|
||||||
`INNER JOIN "videoChannel" ON "videoChannel"."accountId" = "account"."id" ` +
|
`INNER JOIN "videoChannel" ON "videoChannel"."accountId" = "account"."id" ` +
|
||||||
`INNER JOIN "video" ON "video"."channelId" = "videoChannel"."id" AND "video"."id" = :videoId`
|
`INNER JOIN "video" ON "video"."channelId" = "videoChannel"."id" AND "video"."id" = :videoId`
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
type: QueryTypes.SELECT as QueryTypes.SELECT,
|
type: QueryTypes.SELECT as QueryTypes.SELECT,
|
||||||
|
@ -584,7 +590,6 @@ export class ActorModel extends SequelizeModel<ActorModel> {
|
||||||
inbox: this.inboxUrl,
|
inbox: this.inboxUrl,
|
||||||
outbox: this.outboxUrl,
|
outbox: this.outboxUrl,
|
||||||
preferredUsername: this.preferredUsername,
|
preferredUsername: this.preferredUsername,
|
||||||
url: this.url,
|
|
||||||
name,
|
name,
|
||||||
endpoints: {
|
endpoints: {
|
||||||
sharedInbox: this.sharedInboxUrl
|
sharedInbox: this.sharedInboxUrl
|
||||||
|
|
|
@ -4,7 +4,9 @@ import {
|
||||||
ActivityPubStoryboard,
|
ActivityPubStoryboard,
|
||||||
ActivityTagObject,
|
ActivityTagObject,
|
||||||
ActivityTrackerUrlObject,
|
ActivityTrackerUrlObject,
|
||||||
ActivityUrlObject, VideoCommentPolicy, VideoObject
|
ActivityUrlObject,
|
||||||
|
VideoCommentPolicy,
|
||||||
|
VideoObject
|
||||||
} from '@peertube/peertube-models'
|
} from '@peertube/peertube-models'
|
||||||
import { getAPPublicValue } from '@server/helpers/activity-pub-utils.js'
|
import { getAPPublicValue } from '@server/helpers/activity-pub-utils.js'
|
||||||
import { isArray } from '@server/helpers/custom-validators/misc.js'
|
import { isArray } from '@server/helpers/custom-validators/misc.js'
|
||||||
|
@ -41,7 +43,13 @@ export function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
|
||||||
{
|
{
|
||||||
type: 'Link',
|
type: 'Link',
|
||||||
mediaType: 'text/html',
|
mediaType: 'text/html',
|
||||||
href: WEBSERVER.URL + '/videos/watch/' + video.uuid
|
href: WEBSERVER.URL + video.getWatchStaticPath()
|
||||||
|
} as ActivityUrlObject,
|
||||||
|
|
||||||
|
{
|
||||||
|
type: 'Link',
|
||||||
|
mediaType: 'text/html',
|
||||||
|
href: video.url
|
||||||
} as ActivityUrlObject,
|
} as ActivityUrlObject,
|
||||||
|
|
||||||
...buildVideoFileUrls({ video, files: video.VideoFiles }),
|
...buildVideoFileUrls({ video, files: video.VideoFiles }),
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { forceNumber, pick } from '@peertube/peertube-core-utils'
|
||||||
import { ActivityPubActor, VideoChannel, VideoChannelSummary, VideoPrivacy } from '@peertube/peertube-models'
|
import { ActivityPubActor, VideoChannel, VideoChannelSummary, VideoPrivacy } from '@peertube/peertube-models'
|
||||||
import { CONFIG } from '@server/initializers/config.js'
|
import { CONFIG } from '@server/initializers/config.js'
|
||||||
import { InternalEventEmitter } from '@server/lib/internal-event-emitter.js'
|
import { InternalEventEmitter } from '@server/lib/internal-event-emitter.js'
|
||||||
import { MAccountHost } from '@server/types/models/index.js'
|
import { MAccountIdHost } from '@server/types/models/index.js'
|
||||||
import { FindOptions, Includeable, literal, Op, QueryTypes, ScopeOptions, Transaction, WhereOptions } from 'sequelize'
|
import { FindOptions, Includeable, literal, Op, QueryTypes, ScopeOptions, Transaction, WhereOptions } from 'sequelize'
|
||||||
import {
|
import {
|
||||||
AfterCreate,
|
AfterCreate,
|
||||||
|
@ -18,7 +18,8 @@ import {
|
||||||
DefaultScope,
|
DefaultScope,
|
||||||
ForeignKey,
|
ForeignKey,
|
||||||
HasMany,
|
HasMany,
|
||||||
Is, Scopes,
|
Is,
|
||||||
|
Scopes,
|
||||||
Sequelize,
|
Sequelize,
|
||||||
Table,
|
Table,
|
||||||
UpdatedAt
|
UpdatedAt
|
||||||
|
@ -36,6 +37,7 @@ import {
|
||||||
MChannelDefault,
|
MChannelDefault,
|
||||||
MChannelFormattable,
|
MChannelFormattable,
|
||||||
MChannelHost,
|
MChannelHost,
|
||||||
|
MChannelIdHost,
|
||||||
MChannelSummaryFormattable,
|
MChannelSummaryFormattable,
|
||||||
type MChannel
|
type MChannel
|
||||||
} from '../../types/models/video/index.js'
|
} from '../../types/models/video/index.js'
|
||||||
|
@ -142,7 +144,7 @@ export type SummaryOptions = {
|
||||||
`(` +
|
`(` +
|
||||||
`LOWER("preferredUsername") = ${sanitizedPreferredUsername} ` +
|
`LOWER("preferredUsername") = ${sanitizedPreferredUsername} ` +
|
||||||
`AND "host" = ${sanitizedHost}` +
|
`AND "host" = ${sanitizedHost}` +
|
||||||
`)`
|
`)`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -303,33 +305,33 @@ export type SummaryOptions = {
|
||||||
[
|
[
|
||||||
literal(
|
literal(
|
||||||
'(' +
|
'(' +
|
||||||
`SELECT string_agg(concat_ws('|', t.day, t.views), ',') ` +
|
`SELECT string_agg(concat_ws('|', t.day, t.views), ',') ` +
|
||||||
'FROM ( ' +
|
'FROM ( ' +
|
||||||
'WITH ' +
|
'WITH ' +
|
||||||
'days AS ( ' +
|
'days AS ( ' +
|
||||||
`SELECT generate_series(date_trunc('day', now()) - '${daysPrior} day'::interval, ` +
|
`SELECT generate_series(date_trunc('day', now()) - '${daysPrior} day'::interval, ` +
|
||||||
`date_trunc('day', now()), '1 day'::interval) AS day ` +
|
`date_trunc('day', now()), '1 day'::interval) AS day ` +
|
||||||
') ' +
|
') ' +
|
||||||
'SELECT days.day AS day, COALESCE(SUM("videoView".views), 0) AS views ' +
|
'SELECT days.day AS day, COALESCE(SUM("videoView".views), 0) AS views ' +
|
||||||
'FROM days ' +
|
'FROM days ' +
|
||||||
'LEFT JOIN (' +
|
'LEFT JOIN (' +
|
||||||
'"videoView" INNER JOIN "video" ON "videoView"."videoId" = "video"."id" ' +
|
'"videoView" INNER JOIN "video" ON "videoView"."videoId" = "video"."id" ' +
|
||||||
'AND "video"."channelId" = "VideoChannelModel"."id"' +
|
'AND "video"."channelId" = "VideoChannelModel"."id"' +
|
||||||
`) ON date_trunc('day', "videoView"."startDate") = date_trunc('day', days.day) ` +
|
`) ON date_trunc('day', "videoView"."startDate") = date_trunc('day', days.day) ` +
|
||||||
'GROUP BY day ' +
|
'GROUP BY day ' +
|
||||||
'ORDER BY day ' +
|
'ORDER BY day ' +
|
||||||
') t' +
|
') t' +
|
||||||
')'
|
')'
|
||||||
),
|
),
|
||||||
'viewsPerDay'
|
'viewsPerDay'
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
literal(
|
literal(
|
||||||
'(' +
|
'(' +
|
||||||
'SELECT COALESCE(SUM("video".views), 0) AS totalViews ' +
|
'SELECT COALESCE(SUM("video".views), 0) AS totalViews ' +
|
||||||
'FROM "video" ' +
|
'FROM "video" ' +
|
||||||
'WHERE "video"."channelId" = "VideoChannelModel"."id"' +
|
'WHERE "video"."channelId" = "VideoChannelModel"."id"' +
|
||||||
')'
|
')'
|
||||||
),
|
),
|
||||||
'totalViews'
|
'totalViews'
|
||||||
]
|
]
|
||||||
|
@ -352,7 +354,6 @@ export type SummaryOptions = {
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class VideoChannelModel extends SequelizeModel<VideoChannelModel> {
|
export class VideoChannelModel extends SequelizeModel<VideoChannelModel> {
|
||||||
|
|
||||||
@AllowNull(false)
|
@AllowNull(false)
|
||||||
@Is('VideoChannelName', value => throwIfNotValid(value, isVideoChannelDisplayNameValid, 'name'))
|
@Is('VideoChannelName', value => throwIfNotValid(value, isVideoChannelDisplayNameValid, 'name'))
|
||||||
@Column
|
@Column
|
||||||
|
@ -471,7 +472,6 @@ export class VideoChannelModel extends SequelizeModel<VideoChannelModel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getStats () {
|
static async getStats () {
|
||||||
|
|
||||||
function getLocalVideoChannelStats (days?: number) {
|
function getLocalVideoChannelStats (days?: number) {
|
||||||
const options = {
|
const options = {
|
||||||
type: QueryTypes.SELECT as QueryTypes.SELECT,
|
type: QueryTypes.SELECT as QueryTypes.SELECT,
|
||||||
|
@ -480,7 +480,7 @@ export class VideoChannelModel extends SequelizeModel<VideoChannelModel> {
|
||||||
|
|
||||||
const videoJoin = days
|
const videoJoin = days
|
||||||
? `INNER JOIN "video" AS "Videos" ON "VideoChannelModel"."id" = "Videos"."channelId" ` +
|
? `INNER JOIN "video" AS "Videos" ON "VideoChannelModel"."id" = "Videos"."channelId" ` +
|
||||||
`AND ("Videos"."publishedAt" > Now() - interval '${days}d')`
|
`AND ("Videos"."publishedAt" > Now() - interval '${days}d')`
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
const query = `
|
const query = `
|
||||||
|
@ -492,7 +492,7 @@ export class VideoChannelModel extends SequelizeModel<VideoChannelModel> {
|
||||||
AND "Account->Actor"."serverId" IS NULL`
|
AND "Account->Actor"."serverId" IS NULL`
|
||||||
|
|
||||||
return VideoChannelModel.sequelize.query<{ count: string }>(query, options)
|
return VideoChannelModel.sequelize.query<{ count: string }>(query, options)
|
||||||
.then(r => parseInt(r[0].count, 10))
|
.then(r => parseInt(r[0].count, 10))
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalLocalVideoChannels = await getLocalVideoChannelStats()
|
const totalLocalVideoChannels = await getLocalVideoChannelStats()
|
||||||
|
@ -512,7 +512,7 @@ export class VideoChannelModel extends SequelizeModel<VideoChannelModel> {
|
||||||
|
|
||||||
static listLocalsForSitemap (sort: string): Promise<MChannelHost[]> {
|
static listLocalsForSitemap (sort: string): Promise<MChannelHost[]> {
|
||||||
const query = {
|
const query = {
|
||||||
attributes: [ ],
|
attributes: [],
|
||||||
offset: 0,
|
offset: 0,
|
||||||
order: getSort(sort),
|
order: getSort(sort),
|
||||||
include: [
|
include: [
|
||||||
|
@ -536,11 +536,13 @@ export class VideoChannelModel extends SequelizeModel<VideoChannelModel> {
|
||||||
.findAll(query)
|
.findAll(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
static listForApi (parameters: Pick<AvailableForListOptions, 'actorId'> & {
|
static listForApi (
|
||||||
start: number
|
parameters: Pick<AvailableForListOptions, 'actorId'> & {
|
||||||
count: number
|
start: number
|
||||||
sort: string
|
count: number
|
||||||
}) {
|
sort: string
|
||||||
|
}
|
||||||
|
) {
|
||||||
const { actorId } = parameters
|
const { actorId } = parameters
|
||||||
|
|
||||||
const query = {
|
const query = {
|
||||||
|
@ -559,11 +561,13 @@ export class VideoChannelModel extends SequelizeModel<VideoChannelModel> {
|
||||||
]).then(([ total, data ]) => ({ total, data }))
|
]).then(([ total, data ]) => ({ total, data }))
|
||||||
}
|
}
|
||||||
|
|
||||||
static searchForApi (options: Pick<AvailableForListOptions, 'actorId' | 'search' | 'host' | 'handles'> & {
|
static searchForApi (
|
||||||
start: number
|
options: Pick<AvailableForListOptions, 'actorId' | 'search' | 'host' | 'handles'> & {
|
||||||
count: number
|
start: number
|
||||||
sort: string
|
count: number
|
||||||
}) {
|
sort: string
|
||||||
|
}
|
||||||
|
) {
|
||||||
let attributesInclude: any[] = [ literal('0 as similarity') ]
|
let attributesInclude: any[] = [ literal('0 as similarity') ]
|
||||||
let where: WhereOptions
|
let where: WhereOptions
|
||||||
|
|
||||||
|
@ -597,7 +601,8 @@ export class VideoChannelModel extends SequelizeModel<VideoChannelModel> {
|
||||||
const getScope = (forCount: boolean) => {
|
const getScope = (forCount: boolean) => {
|
||||||
return {
|
return {
|
||||||
method: [
|
method: [
|
||||||
ScopeNames.FOR_API, {
|
ScopeNames.FOR_API,
|
||||||
|
{
|
||||||
...pick(options, [ 'actorId', 'host', 'handles' ]),
|
...pick(options, [ 'actorId', 'host', 'handles' ]),
|
||||||
|
|
||||||
forCount
|
forCount
|
||||||
|
@ -846,6 +851,27 @@ export class VideoChannelModel extends SequelizeModel<VideoChannelModel> {
|
||||||
return {
|
return {
|
||||||
...obj,
|
...obj,
|
||||||
|
|
||||||
|
// // TODO: Uncomment in v8 for backward compatibility
|
||||||
|
// url: [
|
||||||
|
// {
|
||||||
|
// type: 'Link',
|
||||||
|
// mediaType: 'text/html',
|
||||||
|
// href: this.getClientUrl(true)
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// type: 'Link',
|
||||||
|
// mediaType: 'text/html',
|
||||||
|
// href: this.getClientUrl(false)
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// type: 'Link',
|
||||||
|
// mediaType: 'text/html',
|
||||||
|
// href: this.Actor.url
|
||||||
|
// }
|
||||||
|
// ] as ActivityUrlObject[],
|
||||||
|
|
||||||
|
url: this.Actor.url,
|
||||||
|
|
||||||
summary: this.description,
|
summary: this.description,
|
||||||
support: this.support,
|
support: this.support,
|
||||||
postingRestrictedToMods: true,
|
postingRestrictedToMods: true,
|
||||||
|
@ -859,8 +885,12 @@ export class VideoChannelModel extends SequelizeModel<VideoChannelModel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Avoid error when running this method on MAccount... | MChannel...
|
// Avoid error when running this method on MAccount... | MChannel...
|
||||||
getClientUrl (this: MAccountHost | MChannelHost) {
|
getClientUrl (this: MAccountIdHost | MChannelIdHost, videosSuffix = true) {
|
||||||
return WEBSERVER.URL + '/c/' + this.Actor.getIdentifier() + '/videos'
|
const suffix = videosSuffix
|
||||||
|
? '/videos'
|
||||||
|
: ''
|
||||||
|
|
||||||
|
return WEBSERVER.URL + '/c/' + this.Actor.getIdentifier() + suffix
|
||||||
}
|
}
|
||||||
|
|
||||||
getDisplayName () {
|
getDisplayName () {
|
||||||
|
|
|
@ -69,6 +69,10 @@ export type MAccountActor =
|
||||||
& MAccount
|
& MAccount
|
||||||
& Use<'Actor', MActor>
|
& Use<'Actor', MActor>
|
||||||
|
|
||||||
|
export type MAccountIdHost =
|
||||||
|
& MAccountId
|
||||||
|
& Use<'Actor', MActorHost>
|
||||||
|
|
||||||
export type MAccountHost =
|
export type MAccountHost =
|
||||||
& MAccount
|
& MAccount
|
||||||
& Use<'Actor', MActorHost>
|
& Use<'Actor', MActorHost>
|
||||||
|
@ -105,5 +109,5 @@ export type MAccountFormattable =
|
||||||
& Use<'Actor', MActorFormattable>
|
& Use<'Actor', MActorFormattable>
|
||||||
|
|
||||||
export type MAccountAP =
|
export type MAccountAP =
|
||||||
& Pick<MAccount, 'name' | 'description'>
|
& Pick<MAccount, 'id' | 'name' | 'description' | 'getClientUrl'>
|
||||||
& Use<'Actor', MActorAPAccount>
|
& Use<'Actor', MActorAPAccount>
|
||||||
|
|
|
@ -170,6 +170,7 @@ export type MActorFormattable =
|
||||||
|
|
||||||
type MActorAPBase =
|
type MActorAPBase =
|
||||||
& MActor
|
& MActor
|
||||||
|
& MActorHost
|
||||||
& Use<'Avatars', MActorImage[]>
|
& Use<'Avatars', MActorImage[]>
|
||||||
|
|
||||||
export type MActorAPAccount = MActorAPBase
|
export type MActorAPAccount = MActorAPBase
|
||||||
|
|
|
@ -88,6 +88,10 @@ export type MChannelHost =
|
||||||
& MChannel
|
& MChannel
|
||||||
& Use<'Actor', MActorHost>
|
& Use<'Actor', MActorHost>
|
||||||
|
|
||||||
|
export type MChannelIdHost =
|
||||||
|
& MChannelId
|
||||||
|
& Use<'Actor', MActorHost>
|
||||||
|
|
||||||
export type MChannelHostOnly =
|
export type MChannelHostOnly =
|
||||||
& MChannelId
|
& MChannelId
|
||||||
& Use<'Actor', MActorHostOnly>
|
& Use<'Actor', MActorHostOnly>
|
||||||
|
@ -155,6 +159,6 @@ export type MChannelFormattable =
|
||||||
& PickWithOpt<VideoChannelModel, 'Account', MAccountFormattable>
|
& PickWithOpt<VideoChannelModel, 'Account', MAccountFormattable>
|
||||||
|
|
||||||
export type MChannelAP =
|
export type MChannelAP =
|
||||||
& Pick<MChannel, 'name' | 'description' | 'support'>
|
& Pick<MChannel, 'id' | 'name' | 'description' | 'support' | 'getClientUrl'>
|
||||||
& Use<'Actor', MActorAPChannel>
|
& Use<'Actor', MActorAPChannel>
|
||||||
& Use<'Account', MAccountUrl>
|
& Use<'Account', MAccountUrl>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue