Add public links to AP representation
See https://github.com/Chocobozzz/PeerTube/issues/6389
This commit is contained in:
parent
de5adc09b2
commit
e81b6eba74
12
.dprint.json
12
.dprint.json
|
@ -12,8 +12,7 @@
|
|||
"singleBodyPosition": "maintain",
|
||||
"nextControlFlowPosition": "sameLine",
|
||||
"trailingCommas": "never",
|
||||
"operatorPosition": "sameLine",
|
||||
"conditionalExpression.operatorPosition": "nextLine",
|
||||
"operatorPosition": "maintain",
|
||||
"preferHanging": false,
|
||||
"preferSingleLine": false,
|
||||
"arrowFunction.useParentheses": "preferNone",
|
||||
|
@ -64,12 +63,9 @@
|
|||
"arrayExpression.spaceAround": true,
|
||||
"arrayPattern.spaceAround": true
|
||||
},
|
||||
"json": {
|
||||
},
|
||||
"markdown": {
|
||||
},
|
||||
"toml": {
|
||||
},
|
||||
"json": {},
|
||||
"markdown": {},
|
||||
"toml": {},
|
||||
"excludes": [
|
||||
"**/node_modules",
|
||||
"**/*-lock.json",
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
"SwitchCase": 1,
|
||||
"MemberExpression": "off",
|
||||
// https://github.com/eslint/eslint/issues/15299
|
||||
"ignoredNodes": ["PropertyDefinition", "TSTypeParameterInstantiation"]
|
||||
"ignoredNodes": ["PropertyDefinition", "TSTypeParameterInstantiation", "TSConditionalType *"]
|
||||
}
|
||||
],
|
||||
"@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'
|
||||
|
||||
|
@ -12,7 +12,7 @@ export interface ActivityPubActor {
|
|||
inbox: string
|
||||
outbox: string
|
||||
preferredUsername: string
|
||||
url: string
|
||||
url: ActivityUrlObject[] | string
|
||||
name: string
|
||||
endpoints: {
|
||||
sharedInbox: string
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { processViewersStats } from '@tests/shared/views.js'
|
||||
import { HttpStatusCode, VideoComment, VideoPlaylistPrivacy, WatchActionObject } from '@peertube/peertube-models'
|
||||
import {
|
||||
ActivityPubActor,
|
||||
HttpStatusCode,
|
||||
VideoComment,
|
||||
VideoObject,
|
||||
VideoPlaylistPrivacy,
|
||||
WatchActionObject
|
||||
} from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createMultipleServers,
|
||||
|
@ -12,8 +17,10 @@ import {
|
|||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel
|
||||
} 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 video: { 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) {
|
||||
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.id).to.equal(servers[0].url + '/accounts/root')
|
||||
expect(object.name).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) {
|
||||
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.id).to.equal(servers[0].url + '/video-channels/root_channel')
|
||||
expect(object.name).to.equal('Main 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) {
|
||||
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.id).to.equal(servers[0].url + '/videos/watch/' + video.uuid)
|
||||
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) {
|
||||
|
|
|
@ -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 { ActivityPubActor } from 'packages/models/src/activitypub/activitypub-actor.js'
|
||||
import validator from 'validator'
|
||||
import { CONSTRAINTS_FIELDS } from '../../../initializers/constants.js'
|
||||
import { exists, isArray, isDateValid } from '../misc.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) {
|
||||
if (endpointObject?.sharedInbox) {
|
||||
|
@ -62,7 +63,7 @@ export function isActorDeleteActivityValid (activity: any) {
|
|||
return isBaseActivityValid(activity, 'Delete')
|
||||
}
|
||||
|
||||
export function sanitizeAndCheckActorObject (actor: any) {
|
||||
export function sanitizeAndCheckActorObject (actor: ActivityPubActor) {
|
||||
if (!isActorTypeValid(actor.type)) return false
|
||||
|
||||
normalizeActor(actor)
|
||||
|
@ -71,43 +72,16 @@ export function sanitizeAndCheckActorObject (actor: any) {
|
|||
isActivityPubUrlValid(actor.id) &&
|
||||
isActivityPubUrlValid(actor.inbox) &&
|
||||
isActorPreferredUsernameValid(actor.preferredUsername) &&
|
||||
isActivityPubUrlValid(actor.url) &&
|
||||
isActorPublicKeyObjectValid(actor.publicKey) &&
|
||||
isActorEndpointsObjectValid(actor.endpoints) &&
|
||||
|
||||
(!actor.outbox || isActivityPubUrlValid(actor.outbox)) &&
|
||||
(!actor.following || isActivityPubUrlValid(actor.following)) &&
|
||||
(!actor.followers || isActivityPubUrlValid(actor.followers)) &&
|
||||
|
||||
setValidAttributedTo(actor) &&
|
||||
setValidDescription(actor) &&
|
||||
// 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
|
||||
(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) {
|
||||
if (!exists(handle)) return false
|
||||
|
||||
|
@ -121,8 +95,38 @@ export function areValidActorHandles (handles: string[]) {
|
|||
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 { ActivityHtmlUrlObject } from 'packages/models/src/activitypub/index.js'
|
||||
import validator from 'validator'
|
||||
import { CONSTRAINTS_FIELDS } from '../../../initializers/constants.js'
|
||||
import { exists } from '../misc.js'
|
||||
|
||||
function isUrlValid (url: string) {
|
||||
export function isUrlValid (url: string) {
|
||||
const isURLOptions = {
|
||||
require_host: true,
|
||||
require_tld: true,
|
||||
|
@ -20,11 +21,11 @@ function isUrlValid (url: string) {
|
|||
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)
|
||||
}
|
||||
|
||||
function isBaseActivityValid (activity: any, type: string) {
|
||||
export function isBaseActivityValid (activity: any, type: string) {
|
||||
return activity.type === type &&
|
||||
isActivityPubUrlValid(activity.id) &&
|
||||
isObjectValid(activity.actor) &&
|
||||
|
@ -32,19 +33,26 @@ function isBaseActivityValid (activity: any, type: string) {
|
|||
isUrlCollectionValid(activity.cc)
|
||||
}
|
||||
|
||||
function isUrlCollectionValid (collection: any) {
|
||||
export function isUrlCollectionValid (collection: any) {
|
||||
return collection === undefined ||
|
||||
(Array.isArray(collection) && collection.every(t => isActivityPubUrlValid(t)))
|
||||
}
|
||||
|
||||
function isObjectValid (object: any) {
|
||||
export function isObjectValid (object: any) {
|
||||
return exists(object) &&
|
||||
(
|
||||
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) {
|
||||
obj.attributedTo = []
|
||||
return true
|
||||
|
@ -58,19 +66,10 @@ function setValidAttributedTo (obj: any) {
|
|||
return true
|
||||
}
|
||||
|
||||
function isActivityPubVideoDurationValid (value: string) {
|
||||
export function isActivityPubVideoDurationValid (value: string) {
|
||||
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration
|
||||
return exists(value) &&
|
||||
typeof value === 'string' &&
|
||||
value.startsWith('PT') &&
|
||||
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
|
||||
if (!actor) {
|
||||
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
|
||||
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
|
||||
|
||||
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 schedulePlaylistFetchIfNeeded(actor, created, accountPlaylistsUrl)
|
||||
|
@ -75,10 +75,10 @@ async function getOrCreateAPActor (
|
|||
return actorRefreshed
|
||||
}
|
||||
|
||||
async function getOrCreateAPOwner (actorObject: ActivityPubActor, actorUrl: string) {
|
||||
const accountAttributedTo = await findOwner(actorUrl, actorObject.attributedTo, 'Person')
|
||||
async function getOrCreateAPOwner (actorObject: ActivityPubActor, actorId: string) {
|
||||
const accountAttributedTo = await findOwner(actorId, actorObject.attributedTo, 'Person')
|
||||
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 {
|
||||
|
@ -86,22 +86,22 @@ async function getOrCreateAPOwner (actorObject: ActivityPubActor, actorUrl: stri
|
|||
const recurseIfNeeded = false
|
||||
return getOrCreateAPActor(accountAttributedTo, 'all', recurseIfNeeded)
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
|
||||
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)) {
|
||||
const actorObject = await fetchAPObjectIfNeeded<ActivityPubActor>(getAPId(actorToCheck))
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if (checkUrlsSameHost(actorObject.id, rootUrl) !== true) {
|
||||
logger.warn(`Account attributed to ${actorObject.id} does not have the same host than owner actor url ${rootUrl}`)
|
||||
if (checkUrlsSameHost(actorObject.id, rootId) !== true) {
|
||||
logger.warn(`Account attributed to ${actorObject.id} does not have the same host than owner actor url ${rootId}`)
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
import { ActivityPubActor, ActorImageType } from '@peertube/peertube-models'
|
||||
import { resetSequelizeInstance, runInReadCommittedTransaction } from '@server/helpers/database-utils.js'
|
||||
import { logger } from '@server/helpers/logger.js'
|
||||
import { AccountModel } from '@server/models/account/account.js'
|
||||
import { VideoChannelModel } from '@server/models/video/video-channel.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 { updateActorImages } from './image.js'
|
||||
import { fetchActorFollowsCount } from './shared/index.js'
|
||||
import { getImagesInfoFromObject } from './shared/object-to-model-attributes.js'
|
||||
|
||||
export class APActorUpdater {
|
||||
|
||||
private readonly accountOrChannel: MAccount | MChannel
|
||||
|
||||
constructor (
|
||||
|
@ -32,7 +31,7 @@ export class APActorUpdater {
|
|||
this.accountOrChannel.description = this.actorObject.summary
|
||||
|
||||
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.Account = owner.Account as AccountModel
|
||||
|
||||
|
@ -49,7 +48,8 @@ export class APActorUpdater {
|
|||
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) {
|
||||
if (this.actor !== undefined) {
|
||||
await resetSequelizeInstance(this.actor)
|
||||
|
|
|
@ -113,7 +113,7 @@ async function processUpdateCacheFile (
|
|||
}
|
||||
|
||||
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)
|
||||
return updater.update()
|
||||
|
|
|
@ -18,9 +18,8 @@ async function processActivityPubFollow (job: Job) {
|
|||
const payload = job.data as ActivitypubFollowPayload
|
||||
const host = payload.host
|
||||
|
||||
const handle = host
|
||||
? `${payload.name}@${host}`
|
||||
: payload.name
|
||||
const identifier = [ payload.name || SERVER_ACTOR_NAME, host ]
|
||||
.filter(v => !!v).join('@')
|
||||
|
||||
logger.info('Processing ActivityPub follow in job %s.', job.id)
|
||||
|
||||
|
@ -40,18 +39,18 @@ async function processActivityPubFollow (job: Job) {
|
|||
|
||||
targetActor = await getOrCreateAPActor(actorUrl, 'all')
|
||||
} 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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -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 { FindOptions, IncludeOptions, Includeable, Op, Transaction, WhereOptions, literal } from 'sequelize'
|
||||
import {
|
||||
AfterDestroy,
|
||||
AllowNull,
|
||||
BeforeDestroy,
|
||||
BelongsTo, Column,
|
||||
BelongsTo,
|
||||
Column,
|
||||
CreatedAt,
|
||||
DataType,
|
||||
Default,
|
||||
DefaultScope,
|
||||
ForeignKey,
|
||||
HasMany,
|
||||
Is, Scopes,
|
||||
Is,
|
||||
Scopes,
|
||||
Table,
|
||||
UpdatedAt
|
||||
} 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 { sendDeleteActor } from '../../lib/activitypub/send/send-delete.js'
|
||||
import {
|
||||
MAccount, MAccountAP,
|
||||
MAccount,
|
||||
MAccountAP,
|
||||
MAccountDefault,
|
||||
MAccountFormattable,
|
||||
MAccountHost,
|
||||
MAccountIdHost,
|
||||
MAccountSummaryFormattable,
|
||||
MChannelHost
|
||||
MChannelIdHost
|
||||
} from '../../types/models/index.js'
|
||||
import { ActorFollowModel } from '../actor/actor-follow.js'
|
||||
import { ActorImageModel } from '../actor/actor-image.js'
|
||||
|
@ -145,7 +149,6 @@ export type SummaryOptions = {
|
|||
]
|
||||
})
|
||||
export class AccountModel extends SequelizeModel<AccountModel> {
|
||||
|
||||
@AllowNull(false)
|
||||
@Column
|
||||
name: string
|
||||
|
@ -423,7 +426,7 @@ export class AccountModel extends SequelizeModel<AccountModel> {
|
|||
|
||||
static listLocalsForSitemap (sort: string): Promise<MAccountHost[]> {
|
||||
return AccountModel.unscoped().findAll({
|
||||
attributes: [ ],
|
||||
attributes: [],
|
||||
offset: 0,
|
||||
order: getSort(sort),
|
||||
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)
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
|
@ -495,8 +518,12 @@ export class AccountModel extends SequelizeModel<AccountModel> {
|
|||
}
|
||||
|
||||
// Avoid error when running this method on MAccount... | MChannel...
|
||||
getClientUrl (this: MAccountHost | MChannelHost) {
|
||||
return WEBSERVER.URL + '/a/' + this.Actor.getIdentifier() + '/video-channels'
|
||||
getClientUrl (this: MAccountIdHost | MChannelIdHost, channelsSuffix = true) {
|
||||
const suffix = channelsSuffix
|
||||
? '/video-channels'
|
||||
: ''
|
||||
|
||||
return WEBSERVER.URL + '/a/' + this.Actor.getIdentifier() + suffix
|
||||
}
|
||||
|
||||
isBlocked () {
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
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 { activityPubContextify } from '@server/helpers/activity-pub-utils.js'
|
||||
import { getContextFilter } from '@server/lib/activitypub/context.js'
|
||||
|
@ -15,7 +20,8 @@ import {
|
|||
ForeignKey,
|
||||
HasMany,
|
||||
HasOne,
|
||||
Is, Scopes,
|
||||
Is,
|
||||
Scopes,
|
||||
Table,
|
||||
UpdatedAt
|
||||
} from 'sequelize-typescript'
|
||||
|
@ -31,7 +37,8 @@ import { isActivityPubUrlValid } from '../../helpers/custom-validators/activityp
|
|||
import {
|
||||
ACTIVITY_PUB,
|
||||
ACTIVITY_PUB_ACTOR_TYPES,
|
||||
CONSTRAINTS_FIELDS, SERVER_ACTOR_NAME,
|
||||
CONSTRAINTS_FIELDS,
|
||||
SERVER_ACTOR_NAME,
|
||||
WEBSERVER
|
||||
} from '../../initializers/constants.js'
|
||||
import {
|
||||
|
@ -159,7 +166,6 @@ export const unusedActorAttributesForAPI: (keyof AttributesOnly<ActorModel>)[] =
|
|||
]
|
||||
})
|
||||
export class ActorModel extends SequelizeModel<ActorModel> {
|
||||
|
||||
@AllowNull(false)
|
||||
@Column(DataType.ENUM(...Object.values(ACTIVITY_PUB_ACTOR_TYPES)))
|
||||
type: ActivityPubActorType
|
||||
|
@ -346,10 +352,10 @@ export class ActorModel extends SequelizeModel<ActorModel> {
|
|||
|
||||
static loadAccountActorFollowerUrlByVideoId (videoId: number, transaction: Transaction) {
|
||||
const query = `SELECT "actor"."id" AS "id", "actor"."followersUrl" AS "followersUrl" ` +
|
||||
`FROM "actor" ` +
|
||||
`INNER JOIN "account" ON "actor"."id" = "account"."actorId" ` +
|
||||
`INNER JOIN "videoChannel" ON "videoChannel"."accountId" = "account"."id" ` +
|
||||
`INNER JOIN "video" ON "video"."channelId" = "videoChannel"."id" AND "video"."id" = :videoId`
|
||||
`FROM "actor" ` +
|
||||
`INNER JOIN "account" ON "actor"."id" = "account"."actorId" ` +
|
||||
`INNER JOIN "videoChannel" ON "videoChannel"."accountId" = "account"."id" ` +
|
||||
`INNER JOIN "video" ON "video"."channelId" = "videoChannel"."id" AND "video"."id" = :videoId`
|
||||
|
||||
const options = {
|
||||
type: QueryTypes.SELECT as QueryTypes.SELECT,
|
||||
|
@ -584,7 +590,6 @@ export class ActorModel extends SequelizeModel<ActorModel> {
|
|||
inbox: this.inboxUrl,
|
||||
outbox: this.outboxUrl,
|
||||
preferredUsername: this.preferredUsername,
|
||||
url: this.url,
|
||||
name,
|
||||
endpoints: {
|
||||
sharedInbox: this.sharedInboxUrl
|
||||
|
|
|
@ -4,7 +4,9 @@ import {
|
|||
ActivityPubStoryboard,
|
||||
ActivityTagObject,
|
||||
ActivityTrackerUrlObject,
|
||||
ActivityUrlObject, VideoCommentPolicy, VideoObject
|
||||
ActivityUrlObject,
|
||||
VideoCommentPolicy,
|
||||
VideoObject
|
||||
} from '@peertube/peertube-models'
|
||||
import { getAPPublicValue } from '@server/helpers/activity-pub-utils.js'
|
||||
import { isArray } from '@server/helpers/custom-validators/misc.js'
|
||||
|
@ -41,7 +43,13 @@ export function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
|
|||
{
|
||||
type: 'Link',
|
||||
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,
|
||||
|
||||
...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 { CONFIG } from '@server/initializers/config.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 {
|
||||
AfterCreate,
|
||||
|
@ -18,7 +18,8 @@ import {
|
|||
DefaultScope,
|
||||
ForeignKey,
|
||||
HasMany,
|
||||
Is, Scopes,
|
||||
Is,
|
||||
Scopes,
|
||||
Sequelize,
|
||||
Table,
|
||||
UpdatedAt
|
||||
|
@ -36,6 +37,7 @@ import {
|
|||
MChannelDefault,
|
||||
MChannelFormattable,
|
||||
MChannelHost,
|
||||
MChannelIdHost,
|
||||
MChannelSummaryFormattable,
|
||||
type MChannel
|
||||
} from '../../types/models/video/index.js'
|
||||
|
@ -142,7 +144,7 @@ export type SummaryOptions = {
|
|||
`(` +
|
||||
`LOWER("preferredUsername") = ${sanitizedPreferredUsername} ` +
|
||||
`AND "host" = ${sanitizedHost}` +
|
||||
`)`
|
||||
`)`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -303,33 +305,33 @@ export type SummaryOptions = {
|
|||
[
|
||||
literal(
|
||||
'(' +
|
||||
`SELECT string_agg(concat_ws('|', t.day, t.views), ',') ` +
|
||||
'FROM ( ' +
|
||||
`SELECT string_agg(concat_ws('|', t.day, t.views), ',') ` +
|
||||
'FROM ( ' +
|
||||
'WITH ' +
|
||||
'days AS ( ' +
|
||||
`SELECT generate_series(date_trunc('day', now()) - '${daysPrior} day'::interval, ` +
|
||||
`date_trunc('day', now()), '1 day'::interval) AS day ` +
|
||||
') ' +
|
||||
'SELECT days.day AS day, COALESCE(SUM("videoView".views), 0) AS views ' +
|
||||
'FROM days ' +
|
||||
'LEFT JOIN (' +
|
||||
'"videoView" INNER JOIN "video" ON "videoView"."videoId" = "video"."id" ' +
|
||||
'AND "video"."channelId" = "VideoChannelModel"."id"' +
|
||||
`) ON date_trunc('day', "videoView"."startDate") = date_trunc('day', days.day) ` +
|
||||
'GROUP BY day ' +
|
||||
'ORDER BY day ' +
|
||||
'days AS ( ' +
|
||||
`SELECT generate_series(date_trunc('day', now()) - '${daysPrior} day'::interval, ` +
|
||||
`date_trunc('day', now()), '1 day'::interval) AS day ` +
|
||||
') ' +
|
||||
'SELECT days.day AS day, COALESCE(SUM("videoView".views), 0) AS views ' +
|
||||
'FROM days ' +
|
||||
'LEFT JOIN (' +
|
||||
'"videoView" INNER JOIN "video" ON "videoView"."videoId" = "video"."id" ' +
|
||||
'AND "video"."channelId" = "VideoChannelModel"."id"' +
|
||||
`) ON date_trunc('day', "videoView"."startDate") = date_trunc('day', days.day) ` +
|
||||
'GROUP BY day ' +
|
||||
'ORDER BY day ' +
|
||||
') t' +
|
||||
')'
|
||||
')'
|
||||
),
|
||||
'viewsPerDay'
|
||||
],
|
||||
[
|
||||
literal(
|
||||
'(' +
|
||||
'SELECT COALESCE(SUM("video".views), 0) AS totalViews ' +
|
||||
'FROM "video" ' +
|
||||
'WHERE "video"."channelId" = "VideoChannelModel"."id"' +
|
||||
')'
|
||||
'SELECT COALESCE(SUM("video".views), 0) AS totalViews ' +
|
||||
'FROM "video" ' +
|
||||
'WHERE "video"."channelId" = "VideoChannelModel"."id"' +
|
||||
')'
|
||||
),
|
||||
'totalViews'
|
||||
]
|
||||
|
@ -352,7 +354,6 @@ export type SummaryOptions = {
|
|||
]
|
||||
})
|
||||
export class VideoChannelModel extends SequelizeModel<VideoChannelModel> {
|
||||
|
||||
@AllowNull(false)
|
||||
@Is('VideoChannelName', value => throwIfNotValid(value, isVideoChannelDisplayNameValid, 'name'))
|
||||
@Column
|
||||
|
@ -471,7 +472,6 @@ export class VideoChannelModel extends SequelizeModel<VideoChannelModel> {
|
|||
}
|
||||
|
||||
static async getStats () {
|
||||
|
||||
function getLocalVideoChannelStats (days?: number) {
|
||||
const options = {
|
||||
type: QueryTypes.SELECT as QueryTypes.SELECT,
|
||||
|
@ -480,7 +480,7 @@ export class VideoChannelModel extends SequelizeModel<VideoChannelModel> {
|
|||
|
||||
const videoJoin = days
|
||||
? `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 = `
|
||||
|
@ -492,7 +492,7 @@ export class VideoChannelModel extends SequelizeModel<VideoChannelModel> {
|
|||
AND "Account->Actor"."serverId" IS NULL`
|
||||
|
||||
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()
|
||||
|
@ -512,7 +512,7 @@ export class VideoChannelModel extends SequelizeModel<VideoChannelModel> {
|
|||
|
||||
static listLocalsForSitemap (sort: string): Promise<MChannelHost[]> {
|
||||
const query = {
|
||||
attributes: [ ],
|
||||
attributes: [],
|
||||
offset: 0,
|
||||
order: getSort(sort),
|
||||
include: [
|
||||
|
@ -536,11 +536,13 @@ export class VideoChannelModel extends SequelizeModel<VideoChannelModel> {
|
|||
.findAll(query)
|
||||
}
|
||||
|
||||
static listForApi (parameters: Pick<AvailableForListOptions, 'actorId'> & {
|
||||
start: number
|
||||
count: number
|
||||
sort: string
|
||||
}) {
|
||||
static listForApi (
|
||||
parameters: Pick<AvailableForListOptions, 'actorId'> & {
|
||||
start: number
|
||||
count: number
|
||||
sort: string
|
||||
}
|
||||
) {
|
||||
const { actorId } = parameters
|
||||
|
||||
const query = {
|
||||
|
@ -559,11 +561,13 @@ export class VideoChannelModel extends SequelizeModel<VideoChannelModel> {
|
|||
]).then(([ total, data ]) => ({ total, data }))
|
||||
}
|
||||
|
||||
static searchForApi (options: Pick<AvailableForListOptions, 'actorId' | 'search' | 'host' | 'handles'> & {
|
||||
start: number
|
||||
count: number
|
||||
sort: string
|
||||
}) {
|
||||
static searchForApi (
|
||||
options: Pick<AvailableForListOptions, 'actorId' | 'search' | 'host' | 'handles'> & {
|
||||
start: number
|
||||
count: number
|
||||
sort: string
|
||||
}
|
||||
) {
|
||||
let attributesInclude: any[] = [ literal('0 as similarity') ]
|
||||
let where: WhereOptions
|
||||
|
||||
|
@ -597,7 +601,8 @@ export class VideoChannelModel extends SequelizeModel<VideoChannelModel> {
|
|||
const getScope = (forCount: boolean) => {
|
||||
return {
|
||||
method: [
|
||||
ScopeNames.FOR_API, {
|
||||
ScopeNames.FOR_API,
|
||||
{
|
||||
...pick(options, [ 'actorId', 'host', 'handles' ]),
|
||||
|
||||
forCount
|
||||
|
@ -846,6 +851,27 @@ export class VideoChannelModel extends SequelizeModel<VideoChannelModel> {
|
|||
return {
|
||||
...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,
|
||||
support: this.support,
|
||||
postingRestrictedToMods: true,
|
||||
|
@ -859,8 +885,12 @@ export class VideoChannelModel extends SequelizeModel<VideoChannelModel> {
|
|||
}
|
||||
|
||||
// Avoid error when running this method on MAccount... | MChannel...
|
||||
getClientUrl (this: MAccountHost | MChannelHost) {
|
||||
return WEBSERVER.URL + '/c/' + this.Actor.getIdentifier() + '/videos'
|
||||
getClientUrl (this: MAccountIdHost | MChannelIdHost, videosSuffix = true) {
|
||||
const suffix = videosSuffix
|
||||
? '/videos'
|
||||
: ''
|
||||
|
||||
return WEBSERVER.URL + '/c/' + this.Actor.getIdentifier() + suffix
|
||||
}
|
||||
|
||||
getDisplayName () {
|
||||
|
|
|
@ -69,6 +69,10 @@ export type MAccountActor =
|
|||
& MAccount
|
||||
& Use<'Actor', MActor>
|
||||
|
||||
export type MAccountIdHost =
|
||||
& MAccountId
|
||||
& Use<'Actor', MActorHost>
|
||||
|
||||
export type MAccountHost =
|
||||
& MAccount
|
||||
& Use<'Actor', MActorHost>
|
||||
|
@ -105,5 +109,5 @@ export type MAccountFormattable =
|
|||
& Use<'Actor', MActorFormattable>
|
||||
|
||||
export type MAccountAP =
|
||||
& Pick<MAccount, 'name' | 'description'>
|
||||
& Pick<MAccount, 'id' | 'name' | 'description' | 'getClientUrl'>
|
||||
& Use<'Actor', MActorAPAccount>
|
||||
|
|
|
@ -170,6 +170,7 @@ export type MActorFormattable =
|
|||
|
||||
type MActorAPBase =
|
||||
& MActor
|
||||
& MActorHost
|
||||
& Use<'Avatars', MActorImage[]>
|
||||
|
||||
export type MActorAPAccount = MActorAPBase
|
||||
|
|
|
@ -88,6 +88,10 @@ export type MChannelHost =
|
|||
& MChannel
|
||||
& Use<'Actor', MActorHost>
|
||||
|
||||
export type MChannelIdHost =
|
||||
& MChannelId
|
||||
& Use<'Actor', MActorHost>
|
||||
|
||||
export type MChannelHostOnly =
|
||||
& MChannelId
|
||||
& Use<'Actor', MActorHostOnly>
|
||||
|
@ -155,6 +159,6 @@ export type MChannelFormattable =
|
|||
& PickWithOpt<VideoChannelModel, 'Account', MAccountFormattable>
|
||||
|
||||
export type MChannelAP =
|
||||
& Pick<MChannel, 'name' | 'description' | 'support'>
|
||||
& Pick<MChannel, 'id' | 'name' | 'description' | 'support' | 'getClientUrl'>
|
||||
& Use<'Actor', MActorAPChannel>
|
||||
& Use<'Account', MAccountUrl>
|
||||
|
|
Loading…
Reference in New Issue