diff options
Diffstat (limited to 'server/lib/activitypub')
-rw-r--r-- | server/lib/activitypub/misc.ts | 46 | ||||
-rw-r--r-- | server/lib/activitypub/process-add.ts | 22 | ||||
-rw-r--r-- | server/lib/activitypub/process-announce.ts | 29 | ||||
-rw-r--r-- | server/lib/activitypub/process-create.ts | 16 | ||||
-rw-r--r-- | server/lib/activitypub/send-request.ts | 40 |
5 files changed, 84 insertions, 69 deletions
diff --git a/server/lib/activitypub/misc.ts b/server/lib/activitypub/misc.ts index 43d26c328..13838fc4c 100644 --- a/server/lib/activitypub/misc.ts +++ b/server/lib/activitypub/misc.ts | |||
@@ -7,6 +7,21 @@ import { VIDEO_MIMETYPE_EXT } from '../../initializers/constants' | |||
7 | import { VideoChannelInstance } from '../../models/video/video-channel-interface' | 7 | import { VideoChannelInstance } from '../../models/video/video-channel-interface' |
8 | import { VideoFileAttributes } from '../../models/video/video-file-interface' | 8 | import { VideoFileAttributes } from '../../models/video/video-file-interface' |
9 | import { VideoAttributes, VideoInstance } from '../../models/video/video-interface' | 9 | import { VideoAttributes, VideoInstance } from '../../models/video/video-interface' |
10 | import { VideoChannelObject } from '../../../shared/models/activitypub/objects/video-channel-object' | ||
11 | import { AccountInstance } from '../../models/account/account-interface' | ||
12 | |||
13 | function videoChannelActivityObjectToDBAttributes (videoChannelObject: VideoChannelObject, account: AccountInstance) { | ||
14 | return { | ||
15 | name: videoChannelObject.name, | ||
16 | description: videoChannelObject.content, | ||
17 | uuid: videoChannelObject.uuid, | ||
18 | url: videoChannelObject.id, | ||
19 | createdAt: new Date(videoChannelObject.published), | ||
20 | updatedAt: new Date(videoChannelObject.updated), | ||
21 | remote: true, | ||
22 | accountId: account.id | ||
23 | } | ||
24 | } | ||
10 | 25 | ||
11 | async function videoActivityObjectToDBAttributes ( | 26 | async function videoActivityObjectToDBAttributes ( |
12 | videoChannel: VideoChannelInstance, | 27 | videoChannel: VideoChannelInstance, |
@@ -45,26 +60,32 @@ async function videoActivityObjectToDBAttributes ( | |||
45 | } | 60 | } |
46 | 61 | ||
47 | function videoFileActivityUrlToDBAttributes (videoCreated: VideoInstance, videoObject: VideoTorrentObject) { | 62 | function videoFileActivityUrlToDBAttributes (videoCreated: VideoInstance, videoObject: VideoTorrentObject) { |
48 | const fileUrls = videoObject.url | 63 | const mimeTypes = Object.keys(VIDEO_MIMETYPE_EXT) |
49 | .filter(u => Object.keys(VIDEO_MIMETYPE_EXT).indexOf(u.mimeType) !== -1 && u.url.startsWith('video/')) | 64 | const fileUrls = videoObject.url.filter(u => { |
65 | return mimeTypes.indexOf(u.mimeType) !== -1 && u.mimeType.startsWith('video/') | ||
66 | }) | ||
67 | |||
68 | if (fileUrls.length === 0) { | ||
69 | throw new Error('Cannot find video files for ' + videoCreated.url) | ||
70 | } | ||
50 | 71 | ||
51 | const attributes: VideoFileAttributes[] = [] | 72 | const attributes: VideoFileAttributes[] = [] |
52 | for (const url of fileUrls) { | 73 | for (const fileUrl of fileUrls) { |
53 | // Fetch associated magnet uri | 74 | // Fetch associated magnet uri |
54 | const magnet = videoObject.url | 75 | const magnet = videoObject.url.find(u => { |
55 | .find(u => { | 76 | return u.mimeType === 'application/x-bittorrent;x-scheme-handler/magnet' && u.width === fileUrl.width |
56 | return u.mimeType === 'application/x-bittorrent;x-scheme-handler/magnet' && u.width === url.width | 77 | }) |
57 | }) | 78 | |
58 | if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + url.url) | 79 | if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.url) |
59 | 80 | ||
60 | const parsed = magnetUtil.decode(magnet.url) | 81 | const parsed = magnetUtil.decode(magnet.url) |
61 | if (!parsed || isVideoFileInfoHashValid(parsed.infoHash) === false) throw new Error('Cannot parse magnet URI ' + magnet.url) | 82 | if (!parsed || isVideoFileInfoHashValid(parsed.infoHash) === false) throw new Error('Cannot parse magnet URI ' + magnet.url) |
62 | 83 | ||
63 | const attribute = { | 84 | const attribute = { |
64 | extname: VIDEO_MIMETYPE_EXT[url.mimeType], | 85 | extname: VIDEO_MIMETYPE_EXT[fileUrl.mimeType], |
65 | infoHash: parsed.infoHash, | 86 | infoHash: parsed.infoHash, |
66 | resolution: url.width, | 87 | resolution: fileUrl.width, |
67 | size: url.size, | 88 | size: fileUrl.size, |
68 | videoId: videoCreated.id | 89 | videoId: videoCreated.id |
69 | } | 90 | } |
70 | attributes.push(attribute) | 91 | attributes.push(attribute) |
@@ -77,5 +98,6 @@ function videoFileActivityUrlToDBAttributes (videoCreated: VideoInstance, videoO | |||
77 | 98 | ||
78 | export { | 99 | export { |
79 | videoFileActivityUrlToDBAttributes, | 100 | videoFileActivityUrlToDBAttributes, |
80 | videoActivityObjectToDBAttributes | 101 | videoActivityObjectToDBAttributes, |
102 | videoChannelActivityObjectToDBAttributes | ||
81 | } | 103 | } |
diff --git a/server/lib/activitypub/process-add.ts b/server/lib/activitypub/process-add.ts index 98e414dbb..df7139d46 100644 --- a/server/lib/activitypub/process-add.ts +++ b/server/lib/activitypub/process-add.ts | |||
@@ -5,6 +5,8 @@ import { database as db } from '../../initializers' | |||
5 | import { AccountInstance } from '../../models/account/account-interface' | 5 | import { AccountInstance } from '../../models/account/account-interface' |
6 | import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc' | 6 | import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc' |
7 | import Bluebird = require('bluebird') | 7 | import Bluebird = require('bluebird') |
8 | import { getOrCreateVideoChannel } from '../../helpers/activitypub' | ||
9 | import { VideoChannelInstance } from '../../models/video/video-channel-interface' | ||
8 | 10 | ||
9 | async function processAddActivity (activity: ActivityAdd) { | 11 | async function processAddActivity (activity: ActivityAdd) { |
10 | const activityObject = activity.object | 12 | const activityObject = activity.object |
@@ -12,7 +14,10 @@ async function processAddActivity (activity: ActivityAdd) { | |||
12 | const account = await getOrCreateAccount(activity.actor) | 14 | const account = await getOrCreateAccount(activity.actor) |
13 | 15 | ||
14 | if (activityType === 'Video') { | 16 | if (activityType === 'Video') { |
15 | return processAddVideo(account, activity.id, activityObject as VideoTorrentObject) | 17 | const videoChannelUrl = activity.target |
18 | const videoChannel = await getOrCreateVideoChannel(account, videoChannelUrl) | ||
19 | |||
20 | return processAddVideo(account, videoChannel, activityObject as VideoTorrentObject) | ||
16 | } | 21 | } |
17 | 22 | ||
18 | logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id }) | 23 | logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id }) |
@@ -27,16 +32,16 @@ export { | |||
27 | 32 | ||
28 | // --------------------------------------------------------------------------- | 33 | // --------------------------------------------------------------------------- |
29 | 34 | ||
30 | function processAddVideo (account: AccountInstance, videoChannelUrl: string, video: VideoTorrentObject) { | 35 | function processAddVideo (account: AccountInstance, videoChannel: VideoChannelInstance, video: VideoTorrentObject) { |
31 | const options = { | 36 | const options = { |
32 | arguments: [ account, videoChannelUrl, video ], | 37 | arguments: [ account, videoChannel, video ], |
33 | errorMessage: 'Cannot insert the remote video with many retries.' | 38 | errorMessage: 'Cannot insert the remote video with many retries.' |
34 | } | 39 | } |
35 | 40 | ||
36 | return retryTransactionWrapper(addRemoteVideo, options) | 41 | return retryTransactionWrapper(addRemoteVideo, options) |
37 | } | 42 | } |
38 | 43 | ||
39 | async function addRemoteVideo (account: AccountInstance, videoChannelUrl: string, videoToCreateData: VideoTorrentObject) { | 44 | function addRemoteVideo (account: AccountInstance, videoChannel: VideoChannelInstance, videoToCreateData: VideoTorrentObject) { |
40 | logger.debug('Adding remote video %s.', videoToCreateData.url) | 45 | logger.debug('Adding remote video %s.', videoToCreateData.url) |
41 | 46 | ||
42 | return db.sequelize.transaction(async t => { | 47 | return db.sequelize.transaction(async t => { |
@@ -44,9 +49,6 @@ async function addRemoteVideo (account: AccountInstance, videoChannelUrl: string | |||
44 | transaction: t | 49 | transaction: t |
45 | } | 50 | } |
46 | 51 | ||
47 | const videoChannel = await db.VideoChannel.loadByUrl(videoChannelUrl, t) | ||
48 | if (!videoChannel) throw new Error('Video channel not found.') | ||
49 | |||
50 | if (videoChannel.Account.id !== account.id) throw new Error('Video channel is not owned by this account.') | 52 | if (videoChannel.Account.id !== account.id) throw new Error('Video channel is not owned by this account.') |
51 | 53 | ||
52 | const videoData = await videoActivityObjectToDBAttributes(videoChannel, videoToCreateData, t) | 54 | const videoData = await videoActivityObjectToDBAttributes(videoChannel, videoToCreateData, t) |
@@ -59,8 +61,11 @@ async function addRemoteVideo (account: AccountInstance, videoChannelUrl: string | |||
59 | const videoCreated = await video.save(sequelizeOptions) | 61 | const videoCreated = await video.save(sequelizeOptions) |
60 | 62 | ||
61 | const videoFileAttributes = await videoFileActivityUrlToDBAttributes(videoCreated, videoToCreateData) | 63 | const videoFileAttributes = await videoFileActivityUrlToDBAttributes(videoCreated, videoToCreateData) |
64 | if (videoFileAttributes.length === 0) { | ||
65 | throw new Error('Cannot find valid files for video %s ' + videoToCreateData.url) | ||
66 | } | ||
62 | 67 | ||
63 | const tasks: Bluebird<any>[] = videoFileAttributes.map(f => db.VideoFile.create(f)) | 68 | const tasks: Bluebird<any>[] = videoFileAttributes.map(f => db.VideoFile.create(f, { transaction: t })) |
64 | await Promise.all(tasks) | 69 | await Promise.all(tasks) |
65 | 70 | ||
66 | const tags = videoToCreateData.tag.map(t => t.name) | 71 | const tags = videoToCreateData.tag.map(t => t.name) |
@@ -71,5 +76,4 @@ async function addRemoteVideo (account: AccountInstance, videoChannelUrl: string | |||
71 | 76 | ||
72 | return videoCreated | 77 | return videoCreated |
73 | }) | 78 | }) |
74 | |||
75 | } | 79 | } |
diff --git a/server/lib/activitypub/process-announce.ts b/server/lib/activitypub/process-announce.ts index d67958aec..f9674e095 100644 --- a/server/lib/activitypub/process-announce.ts +++ b/server/lib/activitypub/process-announce.ts | |||
@@ -10,38 +10,33 @@ import { VideoChannelInstance } from '../../models/video/video-channel-interface | |||
10 | import { VideoInstance } from '../../models/index' | 10 | import { VideoInstance } from '../../models/index' |
11 | 11 | ||
12 | async function processAnnounceActivity (activity: ActivityAnnounce) { | 12 | async function processAnnounceActivity (activity: ActivityAnnounce) { |
13 | const activityType = activity.object.type | 13 | const announcedActivity = activity.object |
14 | const accountAnnouncer = await getOrCreateAccount(activity.actor) | 14 | const accountAnnouncer = await getOrCreateAccount(activity.actor) |
15 | 15 | ||
16 | if (activityType === 'VideoChannel') { | 16 | if (announcedActivity.type === 'Create' && announcedActivity.object.type === 'VideoChannel') { |
17 | const activityCreate = Object.assign(activity, { | ||
18 | type: 'Create' as 'Create', | ||
19 | actor: activity.object.actor, | ||
20 | object: activity.object as VideoChannelObject | ||
21 | }) | ||
22 | |||
23 | // Add share entry | 17 | // Add share entry |
24 | const videoChannel: VideoChannelInstance = await processCreateActivity(activityCreate) | 18 | const videoChannel: VideoChannelInstance = await processCreateActivity(announcedActivity) |
25 | await db.VideoChannelShare.create({ | 19 | await db.VideoChannelShare.create({ |
26 | accountId: accountAnnouncer.id, | 20 | accountId: accountAnnouncer.id, |
27 | videoChannelId: videoChannel.id | 21 | videoChannelId: videoChannel.id |
28 | }) | 22 | }) |
29 | } else if (activityType === 'Video') { | ||
30 | const activityAdd = Object.assign(activity, { | ||
31 | type: 'Add' as 'Add', | ||
32 | actor: activity.object.actor, | ||
33 | object: activity.object as VideoTorrentObject | ||
34 | }) | ||
35 | 23 | ||
24 | return undefined | ||
25 | } else if (announcedActivity.type === 'Add' && announcedActivity.object.type === 'Video') { | ||
36 | // Add share entry | 26 | // Add share entry |
37 | const video: VideoInstance = await processAddActivity(activityAdd) | 27 | const video: VideoInstance = await processAddActivity(announcedActivity) |
38 | await db.VideoShare.create({ | 28 | await db.VideoShare.create({ |
39 | accountId: accountAnnouncer.id, | 29 | accountId: accountAnnouncer.id, |
40 | videoId: video.id | 30 | videoId: video.id |
41 | }) | 31 | }) |
32 | |||
33 | return undefined | ||
42 | } | 34 | } |
43 | 35 | ||
44 | logger.warn('Unknown activity object type %s when announcing activity.', activityType, { activity: activity.id }) | 36 | logger.warn( |
37 | 'Unknown activity object type %s -> %s when announcing activity.', announcedActivity.type, announcedActivity.object.type, | ||
38 | { activity: activity.id } | ||
39 | ) | ||
45 | return Promise.resolve(undefined) | 40 | return Promise.resolve(undefined) |
46 | } | 41 | } |
47 | 42 | ||
diff --git a/server/lib/activitypub/process-create.ts b/server/lib/activitypub/process-create.ts index 4e4c9f703..c4706a66b 100644 --- a/server/lib/activitypub/process-create.ts +++ b/server/lib/activitypub/process-create.ts | |||
@@ -4,6 +4,7 @@ import { logger, retryTransactionWrapper } from '../../helpers' | |||
4 | import { getActivityPubUrl, getOrCreateAccount } from '../../helpers/activitypub' | 4 | import { getActivityPubUrl, getOrCreateAccount } from '../../helpers/activitypub' |
5 | import { database as db } from '../../initializers' | 5 | import { database as db } from '../../initializers' |
6 | import { AccountInstance } from '../../models/account/account-interface' | 6 | import { AccountInstance } from '../../models/account/account-interface' |
7 | import { videoChannelActivityObjectToDBAttributes } from './misc' | ||
7 | 8 | ||
8 | async function processCreateActivity (activity: ActivityCreate) { | 9 | async function processCreateActivity (activity: ActivityCreate) { |
9 | const activityObject = activity.object | 10 | const activityObject = activity.object |
@@ -37,23 +38,14 @@ function processCreateVideoChannel (account: AccountInstance, videoChannelToCrea | |||
37 | return retryTransactionWrapper(addRemoteVideoChannel, options) | 38 | return retryTransactionWrapper(addRemoteVideoChannel, options) |
38 | } | 39 | } |
39 | 40 | ||
40 | async function addRemoteVideoChannel (account: AccountInstance, videoChannelToCreateData: VideoChannelObject) { | 41 | function addRemoteVideoChannel (account: AccountInstance, videoChannelToCreateData: VideoChannelObject) { |
41 | logger.debug('Adding remote video channel "%s".', videoChannelToCreateData.uuid) | 42 | logger.debug('Adding remote video channel "%s".', videoChannelToCreateData.uuid) |
42 | 43 | ||
43 | return db.sequelize.transaction(async t => { | 44 | return db.sequelize.transaction(async t => { |
44 | let videoChannel = await db.VideoChannel.loadByUUIDOrUrl(videoChannelToCreateData.uuid, videoChannelToCreateData.id, t) | 45 | let videoChannel = await db.VideoChannel.loadByUUIDOrUrl(videoChannelToCreateData.uuid, videoChannelToCreateData.id, t) |
45 | if (videoChannel) throw new Error('Video channel with this URL/UUID already exists.') | 46 | if (videoChannel) throw new Error('Video channel with this URL/UUID already exists.') |
46 | 47 | ||
47 | const videoChannelData = { | 48 | const videoChannelData = videoChannelActivityObjectToDBAttributes(videoChannelToCreateData, account) |
48 | name: videoChannelToCreateData.name, | ||
49 | description: videoChannelToCreateData.content, | ||
50 | uuid: videoChannelToCreateData.uuid, | ||
51 | createdAt: new Date(videoChannelToCreateData.published), | ||
52 | updatedAt: new Date(videoChannelToCreateData.updated), | ||
53 | remote: true, | ||
54 | accountId: account.id | ||
55 | } | ||
56 | |||
57 | videoChannel = db.VideoChannel.build(videoChannelData) | 49 | videoChannel = db.VideoChannel.build(videoChannelData) |
58 | videoChannel.url = getActivityPubUrl('videoChannel', videoChannel.uuid) | 50 | videoChannel.url = getActivityPubUrl('videoChannel', videoChannel.uuid) |
59 | 51 | ||
@@ -73,7 +65,7 @@ function processCreateVideoAbuse (account: AccountInstance, videoAbuseToCreateDa | |||
73 | return retryTransactionWrapper(addRemoteVideoAbuse, options) | 65 | return retryTransactionWrapper(addRemoteVideoAbuse, options) |
74 | } | 66 | } |
75 | 67 | ||
76 | async function addRemoteVideoAbuse (account: AccountInstance, videoAbuseToCreateData: VideoAbuseObject) { | 68 | function addRemoteVideoAbuse (account: AccountInstance, videoAbuseToCreateData: VideoAbuseObject) { |
77 | logger.debug('Reporting remote abuse for video %s.', videoAbuseToCreateData.object) | 69 | logger.debug('Reporting remote abuse for video %s.', videoAbuseToCreateData.object) |
78 | 70 | ||
79 | return db.sequelize.transaction(async t => { | 71 | return db.sequelize.transaction(async t => { |
diff --git a/server/lib/activitypub/send-request.ts b/server/lib/activitypub/send-request.ts index 664b9d826..f9b72f2a8 100644 --- a/server/lib/activitypub/send-request.ts +++ b/server/lib/activitypub/send-request.ts | |||
@@ -59,24 +59,21 @@ async function sendDeleteAccount (account: AccountInstance, t: Sequelize.Transac | |||
59 | return broadcastToFollowers(data, [ account ], t) | 59 | return broadcastToFollowers(data, [ account ], t) |
60 | } | 60 | } |
61 | 61 | ||
62 | async function sendAnnounce (byAccount: AccountInstance, instance: VideoInstance | VideoChannelInstance, t: Sequelize.Transaction) { | 62 | async function sendVideoChannelAnnounce (byAccount: AccountInstance, videoChannel: VideoChannelInstance, t: Sequelize.Transaction) { |
63 | const object = instance.toActivityPubObject() | 63 | const url = getActivityPubUrl('videoChannel', videoChannel.uuid) + '#announce' |
64 | 64 | const announcedActivity = await createActivityData(url, videoChannel.Account, videoChannel.toActivityPubObject(), true) | |
65 | let url = '' | 65 | |
66 | let objectActorUrl: string | 66 | const data = await announceActivityData(url, byAccount, announcedActivity) |
67 | if ((instance as any).VideoChannel !== undefined) { | 67 | return broadcastToFollowers(data, [ byAccount ], t) |
68 | objectActorUrl = (instance as VideoInstance).VideoChannel.Account.url | 68 | } |
69 | url = getActivityPubUrl('video', instance.uuid) + '#announce' | ||
70 | } else { | ||
71 | objectActorUrl = (instance as VideoChannelInstance).Account.url | ||
72 | url = getActivityPubUrl('videoChannel', instance.uuid) + '#announce' | ||
73 | } | ||
74 | 69 | ||
75 | const objectWithActor = Object.assign(object, { | 70 | async function sendVideoAnnounce (byAccount: AccountInstance, video: VideoInstance, t: Sequelize.Transaction) { |
76 | actor: objectActorUrl | 71 | const url = getActivityPubUrl('video', video.uuid) + '#announce' |
77 | }) | ||
78 | 72 | ||
79 | const data = await announceActivityData(url, byAccount, objectWithActor) | 73 | const videoChannel = video.VideoChannel |
74 | const announcedActivity = await addActivityData(url, videoChannel.Account, videoChannel.url, video.toActivityPubObject(), true) | ||
75 | |||
76 | const data = await announceActivityData(url, byAccount, announcedActivity) | ||
80 | return broadcastToFollowers(data, [ byAccount ], t) | 77 | return broadcastToFollowers(data, [ byAccount ], t) |
81 | } | 78 | } |
82 | 79 | ||
@@ -117,7 +114,8 @@ export { | |||
117 | sendAccept, | 114 | sendAccept, |
118 | sendFollow, | 115 | sendFollow, |
119 | sendVideoAbuse, | 116 | sendVideoAbuse, |
120 | sendAnnounce | 117 | sendVideoChannelAnnounce, |
118 | sendVideoAnnounce | ||
121 | } | 119 | } |
122 | 120 | ||
123 | // --------------------------------------------------------------------------- | 121 | // --------------------------------------------------------------------------- |
@@ -159,7 +157,7 @@ async function getPublicActivityTo (account: AccountInstance) { | |||
159 | return inboxUrls.concat('https://www.w3.org/ns/activitystreams#Public') | 157 | return inboxUrls.concat('https://www.w3.org/ns/activitystreams#Public') |
160 | } | 158 | } |
161 | 159 | ||
162 | async function createActivityData (url: string, byAccount: AccountInstance, object: any) { | 160 | async function createActivityData (url: string, byAccount: AccountInstance, object: any, raw = false) { |
163 | const to = await getPublicActivityTo(byAccount) | 161 | const to = await getPublicActivityTo(byAccount) |
164 | const base = { | 162 | const base = { |
165 | type: 'Create', | 163 | type: 'Create', |
@@ -169,6 +167,8 @@ async function createActivityData (url: string, byAccount: AccountInstance, obje | |||
169 | object | 167 | object |
170 | } | 168 | } |
171 | 169 | ||
170 | if (raw === true) return base | ||
171 | |||
172 | return buildSignedActivity(byAccount, base) | 172 | return buildSignedActivity(byAccount, base) |
173 | } | 173 | } |
174 | 174 | ||
@@ -195,7 +195,7 @@ async function deleteActivityData (url: string, byAccount: AccountInstance) { | |||
195 | return buildSignedActivity(byAccount, base) | 195 | return buildSignedActivity(byAccount, base) |
196 | } | 196 | } |
197 | 197 | ||
198 | async function addActivityData (url: string, byAccount: AccountInstance, target: string, object: any) { | 198 | async function addActivityData (url: string, byAccount: AccountInstance, target: string, object: any, raw = false) { |
199 | const to = await getPublicActivityTo(byAccount) | 199 | const to = await getPublicActivityTo(byAccount) |
200 | const base = { | 200 | const base = { |
201 | type: 'Add', | 201 | type: 'Add', |
@@ -206,6 +206,8 @@ async function addActivityData (url: string, byAccount: AccountInstance, target: | |||
206 | target | 206 | target |
207 | } | 207 | } |
208 | 208 | ||
209 | if (raw === true) return base | ||
210 | |||
209 | return buildSignedActivity(byAccount, base) | 211 | return buildSignedActivity(byAccount, base) |
210 | } | 212 | } |
211 | 213 | ||