diff options
author | Chocobozzz <me@florianbigard.com> | 2018-01-26 15:49:57 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2018-01-26 15:49:57 +0100 |
commit | 4ba3b8ea1be85d95a508ac479f26b96ceea15971 (patch) | |
tree | f76de3c6ec591cf885e5000f0e369aab883f3d95 /server | |
parent | 7859b5800c08c15f3380fb3a7e267ce1b3f2df9f (diff) | |
download | PeerTube-4ba3b8ea1be85d95a508ac479f26b96ceea15971.tar.gz PeerTube-4ba3b8ea1be85d95a508ac479f26b96ceea15971.tar.zst PeerTube-4ba3b8ea1be85d95a508ac479f26b96ceea15971.zip |
Don't rehost announced video activities
Diffstat (limited to 'server')
-rw-r--r-- | server/controllers/activitypub/client.ts | 2 | ||||
-rw-r--r-- | server/controllers/activitypub/outbox.ts | 5 | ||||
-rw-r--r-- | server/initializers/constants.ts | 5 | ||||
-rw-r--r-- | server/initializers/migrations/0185-video-share-url.ts | 38 | ||||
-rw-r--r-- | server/lib/activitypub/process/process-announce.ts | 7 | ||||
-rw-r--r-- | server/lib/activitypub/send/send-announce.ts | 35 | ||||
-rw-r--r-- | server/lib/activitypub/share.ts | 24 | ||||
-rw-r--r-- | server/models/video/video-share.ts | 15 | ||||
-rw-r--r-- | server/models/video/video.ts | 46 |
9 files changed, 124 insertions, 53 deletions
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts index 2587ee212..7b60cc311 100644 --- a/server/controllers/activitypub/client.ts +++ b/server/controllers/activitypub/client.ts | |||
@@ -100,7 +100,7 @@ async function videoController (req: express.Request, res: express.Response, nex | |||
100 | 100 | ||
101 | async function videoAnnounceController (req: express.Request, res: express.Response, next: express.NextFunction) { | 101 | async function videoAnnounceController (req: express.Request, res: express.Response, next: express.NextFunction) { |
102 | const share = res.locals.videoShare as VideoShareModel | 102 | const share = res.locals.videoShare as VideoShareModel |
103 | const object = await buildVideoAnnounceToFollowers(share.Actor, res.locals.video, undefined) | 103 | const object = await buildVideoAnnounceToFollowers(share.Actor, share, res.locals.video, undefined) |
104 | 104 | ||
105 | return res.json(activityPubContextify(object)) | 105 | return res.json(activityPubContextify(object)) |
106 | } | 106 | } |
diff --git a/server/controllers/activitypub/outbox.ts b/server/controllers/activitypub/outbox.ts index 41c6ffaeb..86cdcf4cd 100644 --- a/server/controllers/activitypub/outbox.ts +++ b/server/controllers/activitypub/outbox.ts | |||
@@ -5,7 +5,6 @@ import { pageToStartAndCount } from '../../helpers/core-utils' | |||
5 | import { ACTIVITY_PUB } from '../../initializers/constants' | 5 | import { ACTIVITY_PUB } from '../../initializers/constants' |
6 | import { announceActivityData, createActivityData } from '../../lib/activitypub/send' | 6 | import { announceActivityData, createActivityData } from '../../lib/activitypub/send' |
7 | import { buildAudience } from '../../lib/activitypub/send/misc' | 7 | import { buildAudience } from '../../lib/activitypub/send/misc' |
8 | import { getAnnounceActivityPubUrl } from '../../lib/activitypub/url' | ||
9 | import { asyncMiddleware, localAccountValidator } from '../../middlewares' | 8 | import { asyncMiddleware, localAccountValidator } from '../../middlewares' |
10 | import { AccountModel } from '../../models/account/account' | 9 | import { AccountModel } from '../../models/account/account' |
11 | import { ActorModel } from '../../models/activitypub/actor' | 10 | import { ActorModel } from '../../models/activitypub/actor' |
@@ -48,9 +47,9 @@ async function outboxController (req: express.Request, res: express.Response, ne | |||
48 | 47 | ||
49 | // This is a shared video | 48 | // This is a shared video |
50 | if (video.VideoShares !== undefined && video.VideoShares.length !== 0) { | 49 | if (video.VideoShares !== undefined && video.VideoShares.length !== 0) { |
50 | const videoShare = video.VideoShares[0] | ||
51 | const announceAudience = buildAudience(followersMatrix[actor.id]) | 51 | const announceAudience = buildAudience(followersMatrix[actor.id]) |
52 | const url = getAnnounceActivityPubUrl(video.url, actor) | 52 | const announceActivity = await announceActivityData(videoShare.url, actor, video.url, undefined, announceAudience) |
53 | const announceActivity = await announceActivityData(url, actor, video.url, undefined, announceAudience) | ||
54 | 53 | ||
55 | activities.push(announceActivity) | 54 | activities.push(announceActivity) |
56 | } else { | 55 | } else { |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 329d0ffe8..a88f9642c 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -12,7 +12,7 @@ let config: IConfig = require('config') | |||
12 | 12 | ||
13 | // --------------------------------------------------------------------------- | 13 | // --------------------------------------------------------------------------- |
14 | 14 | ||
15 | const LAST_MIGRATION_VERSION = 180 | 15 | const LAST_MIGRATION_VERSION = 185 |
16 | 16 | ||
17 | // --------------------------------------------------------------------------- | 17 | // --------------------------------------------------------------------------- |
18 | 18 | ||
@@ -196,6 +196,9 @@ const CONSTRAINTS_FIELDS = { | |||
196 | VIDEO_COMMENTS: { | 196 | VIDEO_COMMENTS: { |
197 | TEXT: { min: 2, max: 3000 }, // Length | 197 | TEXT: { min: 2, max: 3000 }, // Length |
198 | URL: { min: 3, max: 2000 } // Length | 198 | URL: { min: 3, max: 2000 } // Length |
199 | }, | ||
200 | VIDEO_SHARE: { | ||
201 | URL: { min: 3, max: 2000 } // Length | ||
199 | } | 202 | } |
200 | } | 203 | } |
201 | 204 | ||
diff --git a/server/initializers/migrations/0185-video-share-url.ts b/server/initializers/migrations/0185-video-share-url.ts new file mode 100644 index 000000000..f7eeb0878 --- /dev/null +++ b/server/initializers/migrations/0185-video-share-url.ts | |||
@@ -0,0 +1,38 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction, | ||
5 | queryInterface: Sequelize.QueryInterface, | ||
6 | sequelize: Sequelize.Sequelize | ||
7 | }): Promise<void> { | ||
8 | { | ||
9 | const query = 'DELETE FROM "videoShare" s1 ' + | ||
10 | 'USING (SELECT MIN(id) as id, "actorId", "videoId" FROM "videoShare" GROUP BY "actorId", "videoId" HAVING COUNT(*) > 1) s2 ' + | ||
11 | 'WHERE s1."actorId" = s2."actorId" AND s1."videoId" = s2."videoId" AND s1.id <> s2.id' | ||
12 | await utils.sequelize.query(query) | ||
13 | } | ||
14 | |||
15 | { | ||
16 | const data = { | ||
17 | type: Sequelize.STRING, | ||
18 | allowNull: true, | ||
19 | defaultValue: null | ||
20 | } | ||
21 | await utils.queryInterface.addColumn('videoShare', 'url', data) | ||
22 | |||
23 | const query = `UPDATE "videoShare" SET "url" = (SELECT "url" FROM "video" WHERE "id" = "videoId") || '/announces/' || "actorId"` | ||
24 | await utils.sequelize.query(query) | ||
25 | |||
26 | data.allowNull = false | ||
27 | await utils.queryInterface.changeColumn('videoShare', 'url', data) | ||
28 | } | ||
29 | } | ||
30 | |||
31 | function down (options) { | ||
32 | throw new Error('Not implemented.') | ||
33 | } | ||
34 | |||
35 | export { | ||
36 | up, | ||
37 | down | ||
38 | } | ||
diff --git a/server/lib/activitypub/process/process-announce.ts b/server/lib/activitypub/process/process-announce.ts index 7dafc0593..09f2e80f3 100644 --- a/server/lib/activitypub/process/process-announce.ts +++ b/server/lib/activitypub/process/process-announce.ts | |||
@@ -43,11 +43,14 @@ async function shareVideo (actorAnnouncer: ActorModel, activity: ActivityAnnounc | |||
43 | 43 | ||
44 | const share = { | 44 | const share = { |
45 | actorId: actorAnnouncer.id, | 45 | actorId: actorAnnouncer.id, |
46 | videoId: video.id | 46 | videoId: video.id, |
47 | url: activity.id | ||
47 | } | 48 | } |
48 | 49 | ||
49 | const [ , created ] = await VideoShareModel.findOrCreate({ | 50 | const [ , created ] = await VideoShareModel.findOrCreate({ |
50 | where: share, | 51 | where: { |
52 | url: activity.id | ||
53 | }, | ||
51 | defaults: share, | 54 | defaults: share, |
52 | transaction: t | 55 | transaction: t |
53 | }) | 56 | }) |
diff --git a/server/lib/activitypub/send/send-announce.ts b/server/lib/activitypub/send/send-announce.ts index 76cb3f80c..ed551a2b2 100644 --- a/server/lib/activitypub/send/send-announce.ts +++ b/server/lib/activitypub/send/send-announce.ts | |||
@@ -2,45 +2,23 @@ import { Transaction } from 'sequelize' | |||
2 | import { ActivityAnnounce, ActivityAudience } from '../../../../shared/models/activitypub' | 2 | import { ActivityAnnounce, ActivityAudience } from '../../../../shared/models/activitypub' |
3 | import { ActorModel } from '../../../models/activitypub/actor' | 3 | import { ActorModel } from '../../../models/activitypub/actor' |
4 | import { VideoModel } from '../../../models/video/video' | 4 | import { VideoModel } from '../../../models/video/video' |
5 | import { getAnnounceActivityPubUrl } from '../url' | 5 | import { VideoShareModel } from '../../../models/video/video-share' |
6 | import { | 6 | import { broadcastToFollowers, getActorsInvolvedInVideo, getAudience, getObjectFollowersAudience } from './misc' |
7 | broadcastToFollowers, | ||
8 | getActorsInvolvedInVideo, | ||
9 | getAudience, | ||
10 | getObjectFollowersAudience, | ||
11 | getOriginVideoAudience, | ||
12 | unicastTo | ||
13 | } from './misc' | ||
14 | import { createActivityData } from './send-create' | ||
15 | 7 | ||
16 | async function buildVideoAnnounceToFollowers (byActor: ActorModel, video: VideoModel, t: Transaction) { | 8 | async function buildVideoAnnounceToFollowers (byActor: ActorModel, videoShare: VideoShareModel, video: VideoModel, t: Transaction) { |
17 | const url = getAnnounceActivityPubUrl(video.url, byActor) | ||
18 | const announcedObject = video.url | 9 | const announcedObject = video.url |
19 | 10 | ||
20 | const accountsToForwardView = await getActorsInvolvedInVideo(video, t) | 11 | const accountsToForwardView = await getActorsInvolvedInVideo(video, t) |
21 | const audience = getObjectFollowersAudience(accountsToForwardView) | 12 | const audience = getObjectFollowersAudience(accountsToForwardView) |
22 | return announceActivityData(url, byActor, announcedObject, t, audience) | 13 | return announceActivityData(videoShare.url, byActor, announcedObject, t, audience) |
23 | } | 14 | } |
24 | 15 | ||
25 | async function sendVideoAnnounceToFollowers (byActor: ActorModel, video: VideoModel, t: Transaction) { | 16 | async function sendVideoAnnounceToFollowers (byActor: ActorModel, videoShare: VideoShareModel, video: VideoModel, t: Transaction) { |
26 | const data = await buildVideoAnnounceToFollowers(byActor, video, t) | 17 | const data = await buildVideoAnnounceToFollowers(byActor, videoShare, video, t) |
27 | 18 | ||
28 | return broadcastToFollowers(data, byActor, [ byActor ], t) | 19 | return broadcastToFollowers(data, byActor, [ byActor ], t) |
29 | } | 20 | } |
30 | 21 | ||
31 | async function sendVideoAnnounceToOrigin (byActor: ActorModel, video: VideoModel, t: Transaction) { | ||
32 | const url = getAnnounceActivityPubUrl(video.url, byActor) | ||
33 | |||
34 | const videoObject = video.toActivityPubObject() | ||
35 | const announcedActivity = await createActivityData(url, video.VideoChannel.Account.Actor, videoObject, t) | ||
36 | |||
37 | const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, t) | ||
38 | const audience = getOriginVideoAudience(video, actorsInvolvedInVideo) | ||
39 | const data = await createActivityData(url, byActor, announcedActivity, t, audience) | ||
40 | |||
41 | return unicastTo(data, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl) | ||
42 | } | ||
43 | |||
44 | async function announceActivityData ( | 22 | async function announceActivityData ( |
45 | url: string, | 23 | url: string, |
46 | byActor: ActorModel, | 24 | byActor: ActorModel, |
@@ -66,7 +44,6 @@ async function announceActivityData ( | |||
66 | 44 | ||
67 | export { | 45 | export { |
68 | sendVideoAnnounceToFollowers, | 46 | sendVideoAnnounceToFollowers, |
69 | sendVideoAnnounceToOrigin, | ||
70 | announceActivityData, | 47 | announceActivityData, |
71 | buildVideoAnnounceToFollowers | 48 | buildVideoAnnounceToFollowers |
72 | } | 49 | } |
diff --git a/server/lib/activitypub/share.ts b/server/lib/activitypub/share.ts index fd374d03d..53ecd3dab 100644 --- a/server/lib/activitypub/share.ts +++ b/server/lib/activitypub/share.ts | |||
@@ -4,28 +4,36 @@ import { getServerActor } from '../../helpers/utils' | |||
4 | import { VideoModel } from '../../models/video/video' | 4 | import { VideoModel } from '../../models/video/video' |
5 | import { VideoShareModel } from '../../models/video/video-share' | 5 | import { VideoShareModel } from '../../models/video/video-share' |
6 | import { sendVideoAnnounceToFollowers } from './send' | 6 | import { sendVideoAnnounceToFollowers } from './send' |
7 | import { getAnnounceActivityPubUrl } from './url' | ||
7 | 8 | ||
8 | async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction) { | 9 | async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction) { |
9 | if (video.privacy === VideoPrivacy.PRIVATE) return undefined | 10 | if (video.privacy === VideoPrivacy.PRIVATE) return undefined |
10 | 11 | ||
11 | const serverActor = await getServerActor() | 12 | const serverActor = await getServerActor() |
12 | 13 | ||
13 | const serverShare = VideoShareModel.create({ | 14 | const serverShareUrl = getAnnounceActivityPubUrl(video.url, serverActor) |
15 | const serverSharePromise = VideoShareModel.create({ | ||
14 | actorId: serverActor.id, | 16 | actorId: serverActor.id, |
15 | videoId: video.id | 17 | videoId: video.id, |
18 | url: serverShareUrl | ||
16 | }, { transaction: t }) | 19 | }, { transaction: t }) |
17 | 20 | ||
18 | const videoChannelShare = VideoShareModel.create({ | 21 | const videoChannelShareUrl = getAnnounceActivityPubUrl(video.url, video.VideoChannel.Actor) |
22 | const videoChannelSharePromise = VideoShareModel.create({ | ||
19 | actorId: video.VideoChannel.actorId, | 23 | actorId: video.VideoChannel.actorId, |
20 | videoId: video.id | 24 | videoId: video.id, |
25 | url: videoChannelShareUrl | ||
21 | }, { transaction: t }) | 26 | }, { transaction: t }) |
22 | 27 | ||
23 | await Promise.all([ | 28 | const [ serverShare, videoChannelShare ] = await Promise.all([ |
24 | serverShare, | 29 | serverSharePromise, |
25 | videoChannelShare | 30 | videoChannelSharePromise |
26 | ]) | 31 | ]) |
27 | 32 | ||
28 | return sendVideoAnnounceToFollowers(serverActor, video, t) | 33 | return Promise.all([ |
34 | sendVideoAnnounceToFollowers(serverActor, videoChannelShare, video, t), | ||
35 | sendVideoAnnounceToFollowers(serverActor, serverShare, video, t) | ||
36 | ]) | ||
29 | } | 37 | } |
30 | 38 | ||
31 | export { | 39 | export { |
diff --git a/server/models/video/video-share.ts b/server/models/video/video-share.ts index 56576f98c..48ba68a4a 100644 --- a/server/models/video/video-share.ts +++ b/server/models/video/video-share.ts | |||
@@ -1,7 +1,10 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | 2 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' |
3 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | ||
4 | import { CONSTRAINTS_FIELDS } from '../../initializers' | ||
3 | import { AccountModel } from '../account/account' | 5 | import { AccountModel } from '../account/account' |
4 | import { ActorModel } from '../activitypub/actor' | 6 | import { ActorModel } from '../activitypub/actor' |
7 | import { throwIfNotValid } from '../utils' | ||
5 | import { VideoModel } from './video' | 8 | import { VideoModel } from './video' |
6 | import { VideoChannelModel } from './video-channel' | 9 | import { VideoChannelModel } from './video-channel' |
7 | 10 | ||
@@ -40,10 +43,20 @@ enum ScopeNames { | |||
40 | }, | 43 | }, |
41 | { | 44 | { |
42 | fields: [ 'videoId' ] | 45 | fields: [ 'videoId' ] |
46 | }, | ||
47 | { | ||
48 | fields: [ 'url' ], | ||
49 | unique: true | ||
43 | } | 50 | } |
44 | ] | 51 | ] |
45 | }) | 52 | }) |
46 | export class VideoShareModel extends Model<VideoShareModel> { | 53 | export class VideoShareModel extends Model<VideoShareModel> { |
54 | |||
55 | @AllowNull(false) | ||
56 | @Is('VideoShareUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url')) | ||
57 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_SHARE.URL.max)) | ||
58 | url: string | ||
59 | |||
47 | @CreatedAt | 60 | @CreatedAt |
48 | createdAt: Date | 61 | createdAt: Date |
49 | 62 | ||
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 81d8a64ff..bd834b088 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -5,8 +5,26 @@ import * as parseTorrent from 'parse-torrent' | |||
5 | import { join } from 'path' | 5 | import { join } from 'path' |
6 | import * as Sequelize from 'sequelize' | 6 | import * as Sequelize from 'sequelize' |
7 | import { | 7 | import { |
8 | AfterDestroy, AllowNull, BeforeDestroy, BelongsTo, BelongsToMany, Column, CreatedAt, DataType, Default, ForeignKey, HasMany, | 8 | AfterDestroy, |
9 | IFindOptions, Is, IsInt, IsUUID, Min, Model, Scopes, Table, UpdatedAt | 9 | AllowNull, |
10 | BeforeDestroy, | ||
11 | BelongsTo, | ||
12 | BelongsToMany, | ||
13 | Column, | ||
14 | CreatedAt, | ||
15 | DataType, | ||
16 | Default, | ||
17 | ForeignKey, | ||
18 | HasMany, | ||
19 | IFindOptions, | ||
20 | Is, | ||
21 | IsInt, | ||
22 | IsUUID, | ||
23 | Min, | ||
24 | Model, | ||
25 | Scopes, | ||
26 | Table, | ||
27 | UpdatedAt | ||
10 | } from 'sequelize-typescript' | 28 | } from 'sequelize-typescript' |
11 | import { VideoPrivacy, VideoResolution } from '../../../shared' | 29 | import { VideoPrivacy, VideoResolution } from '../../../shared' |
12 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' | 30 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' |
@@ -16,17 +34,30 @@ import { createTorrentPromise, renamePromise, statPromise, unlinkPromise, writeF | |||
16 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 34 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
17 | import { isBooleanValid } from '../../helpers/custom-validators/misc' | 35 | import { isBooleanValid } from '../../helpers/custom-validators/misc' |
18 | import { | 36 | import { |
19 | isVideoCategoryValid, isVideoDescriptionValid, isVideoDurationValid, isVideoLanguageValid, isVideoLicenceValid, isVideoNameValid, | 37 | isVideoCategoryValid, |
38 | isVideoDescriptionValid, | ||
39 | isVideoDurationValid, | ||
40 | isVideoLanguageValid, | ||
41 | isVideoLicenceValid, | ||
42 | isVideoNameValid, | ||
20 | isVideoPrivacyValid | 43 | isVideoPrivacyValid |
21 | } from '../../helpers/custom-validators/videos' | 44 | } from '../../helpers/custom-validators/videos' |
22 | import { generateImageFromVideoFile, getVideoFileHeight, transcode } from '../../helpers/ffmpeg-utils' | 45 | import { generateImageFromVideoFile, getVideoFileHeight, transcode } from '../../helpers/ffmpeg-utils' |
23 | import { logger } from '../../helpers/logger' | 46 | import { logger } from '../../helpers/logger' |
24 | import { getServerActor } from '../../helpers/utils' | 47 | import { getServerActor } from '../../helpers/utils' |
25 | import { | 48 | import { |
26 | API_VERSION, CONFIG, CONSTRAINTS_FIELDS, PREVIEWS_SIZE, REMOTE_SCHEME, STATIC_PATHS, THUMBNAILS_SIZE, VIDEO_CATEGORIES, | 49 | API_VERSION, |
27 | VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES | 50 | CONFIG, |
51 | CONSTRAINTS_FIELDS, | ||
52 | PREVIEWS_SIZE, | ||
53 | REMOTE_SCHEME, | ||
54 | STATIC_PATHS, | ||
55 | THUMBNAILS_SIZE, | ||
56 | VIDEO_CATEGORIES, | ||
57 | VIDEO_LANGUAGES, | ||
58 | VIDEO_LICENCES, | ||
59 | VIDEO_PRIVACIES | ||
28 | } from '../../initializers' | 60 | } from '../../initializers' |
29 | import { getAnnounceActivityPubUrl } from '../../lib/activitypub' | ||
30 | import { sendDeleteVideo } from '../../lib/activitypub/send' | 61 | import { sendDeleteVideo } from '../../lib/activitypub/send' |
31 | import { AccountModel } from '../account/account' | 62 | import { AccountModel } from '../account/account' |
32 | import { AccountVideoRateModel } from '../account/account-video-rate' | 63 | import { AccountVideoRateModel } from '../account/account-video-rate' |
@@ -936,8 +967,7 @@ export class VideoModel extends Model<VideoModel> { | |||
936 | const shares: string[] = [] | 967 | const shares: string[] = [] |
937 | 968 | ||
938 | for (const videoShare of this.VideoShares) { | 969 | for (const videoShare of this.VideoShares) { |
939 | const shareUrl = getAnnounceActivityPubUrl(this.url, videoShare.Actor) | 970 | shares.push(videoShare.url) |
940 | shares.push(shareUrl) | ||
941 | } | 971 | } |
942 | 972 | ||
943 | sharesObject = activityPubCollection(shares) | 973 | sharesObject = activityPubCollection(shares) |