diff options
-rw-r--r-- | server/lib/activitypub/cache-file.ts | 91 | ||||
-rw-r--r-- | server/lib/activitypub/playlist.ts | 204 | ||||
-rw-r--r-- | server/lib/activitypub/playlists/create-update.ts | 146 | ||||
-rw-r--r-- | server/lib/activitypub/playlists/index.ts | 2 | ||||
-rw-r--r-- | server/lib/activitypub/playlists/refresh.ts | 44 | ||||
-rw-r--r-- | server/lib/activitypub/playlists/shared/index.ts | 2 | ||||
-rw-r--r-- | server/lib/activitypub/playlists/shared/object-to-model-attributes.ts | 40 | ||||
-rw-r--r-- | server/lib/activitypub/playlists/shared/url-to-object.ts | 47 | ||||
-rw-r--r-- | server/lib/activitypub/process/process-create.ts | 4 | ||||
-rw-r--r-- | server/lib/activitypub/process/process-update.ts | 2 | ||||
-rw-r--r-- | server/lib/activitypub/share.ts | 38 | ||||
-rw-r--r-- | server/lib/activitypub/video-comments.ts | 33 | ||||
-rw-r--r-- | server/lib/activitypub/video-rates.ts | 56 | ||||
-rw-r--r-- | server/lib/job-queue/handlers/activitypub-http-fetcher.ts | 2 | ||||
-rw-r--r-- | server/lib/job-queue/handlers/activitypub-refresher.ts | 2 | ||||
-rw-r--r-- | server/models/video/video-playlist.ts | 5 | ||||
-rw-r--r-- | shared/models/activitypub/objects/index.ts | 4 |
17 files changed, 407 insertions, 315 deletions
diff --git a/server/lib/activitypub/cache-file.ts b/server/lib/activitypub/cache-file.ts index 2e6dd34e0..a16d2cd93 100644 --- a/server/lib/activitypub/cache-file.ts +++ b/server/lib/activitypub/cache-file.ts | |||
@@ -1,54 +1,27 @@ | |||
1 | import { CacheFileObject } from '../../../shared/index' | ||
2 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' | ||
3 | import { Transaction } from 'sequelize' | 1 | import { Transaction } from 'sequelize' |
4 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' | ||
5 | import { MActorId, MVideoRedundancy, MVideoWithAllFiles } from '@server/types/models' | 2 | import { MActorId, MVideoRedundancy, MVideoWithAllFiles } from '@server/types/models' |
3 | import { CacheFileObject } from '../../../shared/index' | ||
4 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' | ||
5 | import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' | ||
6 | 6 | ||
7 | function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject, video: MVideoWithAllFiles, byActor: MActorId) { | 7 | async function createOrUpdateCacheFile (cacheFileObject: CacheFileObject, video: MVideoWithAllFiles, byActor: MActorId, t: Transaction) { |
8 | 8 | const redundancyModel = await VideoRedundancyModel.loadByUrl(cacheFileObject.id, t) | |
9 | if (cacheFileObject.url.mediaType === 'application/x-mpegURL') { | ||
10 | const url = cacheFileObject.url | ||
11 | |||
12 | const playlist = video.VideoStreamingPlaylists.find(t => t.type === VideoStreamingPlaylistType.HLS) | ||
13 | if (!playlist) throw new Error('Cannot find HLS playlist of video ' + video.url) | ||
14 | 9 | ||
15 | return { | 10 | if (redundancyModel) { |
16 | expiresOn: cacheFileObject.expires ? new Date(cacheFileObject.expires) : null, | 11 | return updateCacheFile(cacheFileObject, redundancyModel, video, byActor, t) |
17 | url: cacheFileObject.id, | ||
18 | fileUrl: url.href, | ||
19 | strategy: null, | ||
20 | videoStreamingPlaylistId: playlist.id, | ||
21 | actorId: byActor.id | ||
22 | } | ||
23 | } | 12 | } |
24 | 13 | ||
25 | const url = cacheFileObject.url | 14 | return createCacheFile(cacheFileObject, video, byActor, t) |
26 | const videoFile = video.VideoFiles.find(f => { | ||
27 | return f.resolution === url.height && f.fps === url.fps | ||
28 | }) | ||
29 | |||
30 | if (!videoFile) throw new Error(`Cannot find video file ${url.height} ${url.fps} of video ${video.url}`) | ||
31 | |||
32 | return { | ||
33 | expiresOn: cacheFileObject.expires ? new Date(cacheFileObject.expires) : null, | ||
34 | url: cacheFileObject.id, | ||
35 | fileUrl: url.href, | ||
36 | strategy: null, | ||
37 | videoFileId: videoFile.id, | ||
38 | actorId: byActor.id | ||
39 | } | ||
40 | } | 15 | } |
41 | 16 | ||
42 | async function createOrUpdateCacheFile (cacheFileObject: CacheFileObject, video: MVideoWithAllFiles, byActor: MActorId, t: Transaction) { | 17 | // --------------------------------------------------------------------------- |
43 | const redundancyModel = await VideoRedundancyModel.loadByUrl(cacheFileObject.id, t) | ||
44 | 18 | ||
45 | if (!redundancyModel) { | 19 | export { |
46 | await createCacheFile(cacheFileObject, video, byActor, t) | 20 | createOrUpdateCacheFile |
47 | } else { | ||
48 | await updateCacheFile(cacheFileObject, redundancyModel, video, byActor, t) | ||
49 | } | ||
50 | } | 21 | } |
51 | 22 | ||
23 | // --------------------------------------------------------------------------- | ||
24 | |||
52 | function createCacheFile (cacheFileObject: CacheFileObject, video: MVideoWithAllFiles, byActor: MActorId, t: Transaction) { | 25 | function createCacheFile (cacheFileObject: CacheFileObject, video: MVideoWithAllFiles, byActor: MActorId, t: Transaction) { |
53 | const attributes = cacheFileActivityObjectToDBAttributes(cacheFileObject, video, byActor) | 26 | const attributes = cacheFileActivityObjectToDBAttributes(cacheFileObject, video, byActor) |
54 | 27 | ||
@@ -74,9 +47,37 @@ function updateCacheFile ( | |||
74 | return redundancyModel.save({ transaction: t }) | 47 | return redundancyModel.save({ transaction: t }) |
75 | } | 48 | } |
76 | 49 | ||
77 | export { | 50 | function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject, video: MVideoWithAllFiles, byActor: MActorId) { |
78 | createOrUpdateCacheFile, | 51 | |
79 | createCacheFile, | 52 | if (cacheFileObject.url.mediaType === 'application/x-mpegURL') { |
80 | updateCacheFile, | 53 | const url = cacheFileObject.url |
81 | cacheFileActivityObjectToDBAttributes | 54 | |
55 | const playlist = video.VideoStreamingPlaylists.find(t => t.type === VideoStreamingPlaylistType.HLS) | ||
56 | if (!playlist) throw new Error('Cannot find HLS playlist of video ' + video.url) | ||
57 | |||
58 | return { | ||
59 | expiresOn: cacheFileObject.expires ? new Date(cacheFileObject.expires) : null, | ||
60 | url: cacheFileObject.id, | ||
61 | fileUrl: url.href, | ||
62 | strategy: null, | ||
63 | videoStreamingPlaylistId: playlist.id, | ||
64 | actorId: byActor.id | ||
65 | } | ||
66 | } | ||
67 | |||
68 | const url = cacheFileObject.url | ||
69 | const videoFile = video.VideoFiles.find(f => { | ||
70 | return f.resolution === url.height && f.fps === url.fps | ||
71 | }) | ||
72 | |||
73 | if (!videoFile) throw new Error(`Cannot find video file ${url.height} ${url.fps} of video ${video.url}`) | ||
74 | |||
75 | return { | ||
76 | expiresOn: cacheFileObject.expires ? new Date(cacheFileObject.expires) : null, | ||
77 | url: cacheFileObject.id, | ||
78 | fileUrl: url.href, | ||
79 | strategy: null, | ||
80 | videoFileId: videoFile.id, | ||
81 | actorId: byActor.id | ||
82 | } | ||
82 | } | 83 | } |
diff --git a/server/lib/activitypub/playlist.ts b/server/lib/activitypub/playlist.ts deleted file mode 100644 index 8fe6e79f2..000000000 --- a/server/lib/activitypub/playlist.ts +++ /dev/null | |||
@@ -1,204 +0,0 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
3 | import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object' | ||
4 | import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' | ||
5 | import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' | ||
6 | import { checkUrlsSameHost } from '../../helpers/activitypub' | ||
7 | import { isPlaylistElementObjectValid, isPlaylistObjectValid } from '../../helpers/custom-validators/activitypub/playlist' | ||
8 | import { isArray } from '../../helpers/custom-validators/misc' | ||
9 | import { logger } from '../../helpers/logger' | ||
10 | import { doJSONRequest, PeerTubeRequestError } from '../../helpers/requests' | ||
11 | import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' | ||
12 | import { sequelizeTypescript } from '../../initializers/database' | ||
13 | import { VideoPlaylistModel } from '../../models/video/video-playlist' | ||
14 | import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element' | ||
15 | import { MAccountDefault, MAccountId, MVideoId } from '../../types/models' | ||
16 | import { MVideoPlaylist, MVideoPlaylistId, MVideoPlaylistOwner } from '../../types/models/video/video-playlist' | ||
17 | import { FilteredModelAttributes } from '../../types/sequelize' | ||
18 | import { createPlaylistMiniatureFromUrl } from '../thumbnail' | ||
19 | import { getOrCreateActorAndServerAndModel } from './actor' | ||
20 | import { crawlCollectionPage } from './crawl' | ||
21 | import { getOrCreateAPVideo } from './videos' | ||
22 | |||
23 | function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: MAccountId, to: string[]) { | ||
24 | const privacy = to.includes(ACTIVITY_PUB.PUBLIC) | ||
25 | ? VideoPlaylistPrivacy.PUBLIC | ||
26 | : VideoPlaylistPrivacy.UNLISTED | ||
27 | |||
28 | return { | ||
29 | name: playlistObject.name, | ||
30 | description: playlistObject.content, | ||
31 | privacy, | ||
32 | url: playlistObject.id, | ||
33 | uuid: playlistObject.uuid, | ||
34 | ownerAccountId: byAccount.id, | ||
35 | videoChannelId: null, | ||
36 | createdAt: new Date(playlistObject.published), | ||
37 | updatedAt: new Date(playlistObject.updated) | ||
38 | } | ||
39 | } | ||
40 | |||
41 | function playlistElementObjectToDBAttributes (elementObject: PlaylistElementObject, videoPlaylist: MVideoPlaylistId, video: MVideoId) { | ||
42 | return { | ||
43 | position: elementObject.position, | ||
44 | url: elementObject.id, | ||
45 | startTimestamp: elementObject.startTimestamp || null, | ||
46 | stopTimestamp: elementObject.stopTimestamp || null, | ||
47 | videoPlaylistId: videoPlaylist.id, | ||
48 | videoId: video.id | ||
49 | } | ||
50 | } | ||
51 | |||
52 | async function createAccountPlaylists (playlistUrls: string[], account: MAccountDefault) { | ||
53 | await Bluebird.map(playlistUrls, async playlistUrl => { | ||
54 | try { | ||
55 | const exists = await VideoPlaylistModel.doesPlaylistExist(playlistUrl) | ||
56 | if (exists === true) return | ||
57 | |||
58 | // Fetch url | ||
59 | const { body } = await doJSONRequest<PlaylistObject>(playlistUrl, { activityPub: true }) | ||
60 | |||
61 | if (!isPlaylistObjectValid(body)) { | ||
62 | throw new Error(`Invalid playlist object when fetch account playlists: ${JSON.stringify(body)}`) | ||
63 | } | ||
64 | |||
65 | if (!isArray(body.to)) { | ||
66 | throw new Error('Playlist does not have an audience.') | ||
67 | } | ||
68 | |||
69 | return createOrUpdateVideoPlaylist(body, account, body.to) | ||
70 | } catch (err) { | ||
71 | logger.warn('Cannot add playlist element %s.', playlistUrl, { err }) | ||
72 | } | ||
73 | }, { concurrency: CRAWL_REQUEST_CONCURRENCY }) | ||
74 | } | ||
75 | |||
76 | async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, byAccount: MAccountId, to: string[]) { | ||
77 | const playlistAttributes = playlistObjectToDBAttributes(playlistObject, byAccount, to) | ||
78 | |||
79 | if (isArray(playlistObject.attributedTo) && playlistObject.attributedTo.length === 1) { | ||
80 | const actor = await getOrCreateActorAndServerAndModel(playlistObject.attributedTo[0]) | ||
81 | |||
82 | if (actor.VideoChannel) { | ||
83 | playlistAttributes.videoChannelId = actor.VideoChannel.id | ||
84 | } else { | ||
85 | logger.warn('Attributed to of video playlist %s is not a video channel.', playlistObject.id, { playlistObject }) | ||
86 | } | ||
87 | } | ||
88 | |||
89 | const [ playlist ] = await VideoPlaylistModel.upsert<MVideoPlaylist>(playlistAttributes, { returning: true }) | ||
90 | |||
91 | let accItems: string[] = [] | ||
92 | await crawlCollectionPage<string>(playlistObject.id, items => { | ||
93 | accItems = accItems.concat(items) | ||
94 | |||
95 | return Promise.resolve() | ||
96 | }) | ||
97 | |||
98 | const refreshedPlaylist = await VideoPlaylistModel.loadWithAccountAndChannel(playlist.id, null) | ||
99 | |||
100 | if (playlistObject.icon) { | ||
101 | try { | ||
102 | const thumbnailModel = await createPlaylistMiniatureFromUrl({ downloadUrl: playlistObject.icon.url, playlist: refreshedPlaylist }) | ||
103 | await refreshedPlaylist.setAndSaveThumbnail(thumbnailModel, undefined) | ||
104 | } catch (err) { | ||
105 | logger.warn('Cannot generate thumbnail of %s.', playlistObject.id, { err }) | ||
106 | } | ||
107 | } else if (refreshedPlaylist.hasThumbnail()) { | ||
108 | await refreshedPlaylist.Thumbnail.destroy() | ||
109 | refreshedPlaylist.Thumbnail = null | ||
110 | } | ||
111 | |||
112 | return resetVideoPlaylistElements(accItems, refreshedPlaylist) | ||
113 | } | ||
114 | |||
115 | async function refreshVideoPlaylistIfNeeded (videoPlaylist: MVideoPlaylistOwner): Promise<MVideoPlaylistOwner> { | ||
116 | if (!videoPlaylist.isOutdated()) return videoPlaylist | ||
117 | |||
118 | try { | ||
119 | const { playlistObject } = await fetchRemoteVideoPlaylist(videoPlaylist.url) | ||
120 | |||
121 | if (playlistObject === undefined) { | ||
122 | logger.warn('Cannot refresh remote playlist %s: invalid body.', videoPlaylist.url) | ||
123 | |||
124 | await videoPlaylist.setAsRefreshed() | ||
125 | return videoPlaylist | ||
126 | } | ||
127 | |||
128 | const byAccount = videoPlaylist.OwnerAccount | ||
129 | await createOrUpdateVideoPlaylist(playlistObject, byAccount, playlistObject.to) | ||
130 | |||
131 | return videoPlaylist | ||
132 | } catch (err) { | ||
133 | if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) { | ||
134 | logger.info('Cannot refresh remote video playlist %s: it does not exist anymore. Deleting it.', videoPlaylist.url) | ||
135 | |||
136 | await videoPlaylist.destroy() | ||
137 | return undefined | ||
138 | } | ||
139 | |||
140 | logger.warn('Cannot refresh video playlist %s.', videoPlaylist.url, { err }) | ||
141 | |||
142 | await videoPlaylist.setAsRefreshed() | ||
143 | return videoPlaylist | ||
144 | } | ||
145 | } | ||
146 | |||
147 | // --------------------------------------------------------------------------- | ||
148 | |||
149 | export { | ||
150 | createAccountPlaylists, | ||
151 | playlistObjectToDBAttributes, | ||
152 | playlistElementObjectToDBAttributes, | ||
153 | createOrUpdateVideoPlaylist, | ||
154 | refreshVideoPlaylistIfNeeded | ||
155 | } | ||
156 | |||
157 | // --------------------------------------------------------------------------- | ||
158 | |||
159 | async function resetVideoPlaylistElements (elementUrls: string[], playlist: MVideoPlaylist) { | ||
160 | const elementsToCreate: FilteredModelAttributes<VideoPlaylistElementModel>[] = [] | ||
161 | |||
162 | await Bluebird.map(elementUrls, async elementUrl => { | ||
163 | try { | ||
164 | const { body } = await doJSONRequest<PlaylistElementObject>(elementUrl, { activityPub: true }) | ||
165 | |||
166 | if (!isPlaylistElementObjectValid(body)) throw new Error(`Invalid body in video get playlist element ${elementUrl}`) | ||
167 | |||
168 | if (checkUrlsSameHost(body.id, elementUrl) !== true) { | ||
169 | throw new Error(`Playlist element url ${elementUrl} host is different from the AP object id ${body.id}`) | ||
170 | } | ||
171 | |||
172 | const { video } = await getOrCreateAPVideo({ videoObject: { id: body.url }, fetchType: 'only-video' }) | ||
173 | |||
174 | elementsToCreate.push(playlistElementObjectToDBAttributes(body, playlist, video)) | ||
175 | } catch (err) { | ||
176 | logger.warn('Cannot add playlist element %s.', elementUrl, { err }) | ||
177 | } | ||
178 | }, { concurrency: CRAWL_REQUEST_CONCURRENCY }) | ||
179 | |||
180 | await sequelizeTypescript.transaction(async t => { | ||
181 | await VideoPlaylistElementModel.deleteAllOf(playlist.id, t) | ||
182 | |||
183 | for (const element of elementsToCreate) { | ||
184 | await VideoPlaylistElementModel.create(element, { transaction: t }) | ||
185 | } | ||
186 | }) | ||
187 | |||
188 | logger.info('Reset playlist %s with %s elements.', playlist.url, elementsToCreate.length) | ||
189 | |||
190 | return undefined | ||
191 | } | ||
192 | |||
193 | async function fetchRemoteVideoPlaylist (playlistUrl: string): Promise<{ statusCode: number, playlistObject: PlaylistObject }> { | ||
194 | logger.info('Fetching remote playlist %s.', playlistUrl) | ||
195 | |||
196 | const { body, statusCode } = await doJSONRequest<any>(playlistUrl, { activityPub: true }) | ||
197 | |||
198 | if (isPlaylistObjectValid(body) === false || checkUrlsSameHost(body.id, playlistUrl) !== true) { | ||
199 | logger.debug('Remote video playlist JSON is not valid.', { body }) | ||
200 | return { statusCode, playlistObject: undefined } | ||
201 | } | ||
202 | |||
203 | return { statusCode, playlistObject: body } | ||
204 | } | ||
diff --git a/server/lib/activitypub/playlists/create-update.ts b/server/lib/activitypub/playlists/create-update.ts new file mode 100644 index 000000000..886b1f288 --- /dev/null +++ b/server/lib/activitypub/playlists/create-update.ts | |||
@@ -0,0 +1,146 @@ | |||
1 | import { isArray } from '@server/helpers/custom-validators/misc' | ||
2 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | ||
3 | import { CRAWL_REQUEST_CONCURRENCY } from '@server/initializers/constants' | ||
4 | import { sequelizeTypescript } from '@server/initializers/database' | ||
5 | import { createPlaylistMiniatureFromUrl } from '@server/lib/thumbnail' | ||
6 | import { VideoPlaylistModel } from '@server/models/video/video-playlist' | ||
7 | import { VideoPlaylistElementModel } from '@server/models/video/video-playlist-element' | ||
8 | import { FilteredModelAttributes } from '@server/types' | ||
9 | import { MAccountDefault, MAccountId, MVideoPlaylist, MVideoPlaylistFull } from '@server/types/models' | ||
10 | import { AttributesOnly } from '@shared/core-utils' | ||
11 | import { PlaylistObject } from '@shared/models' | ||
12 | import { getOrCreateActorAndServerAndModel } from '../actor' | ||
13 | import { crawlCollectionPage } from '../crawl' | ||
14 | import { getOrCreateAPVideo } from '../videos' | ||
15 | import { | ||
16 | fetchRemotePlaylistElement, | ||
17 | fetchRemoteVideoPlaylist, | ||
18 | playlistElementObjectToDBAttributes, | ||
19 | playlistObjectToDBAttributes | ||
20 | } from './shared' | ||
21 | |||
22 | import Bluebird = require('bluebird') | ||
23 | |||
24 | const lTags = loggerTagsFactory('ap', 'video-playlist') | ||
25 | |||
26 | async function createAccountPlaylists (playlistUrls: string[], account: MAccountDefault) { | ||
27 | await Bluebird.map(playlistUrls, async playlistUrl => { | ||
28 | try { | ||
29 | const exists = await VideoPlaylistModel.doesPlaylistExist(playlistUrl) | ||
30 | if (exists === true) return | ||
31 | |||
32 | const { playlistObject } = await fetchRemoteVideoPlaylist(playlistUrl) | ||
33 | |||
34 | if (playlistObject === undefined) { | ||
35 | throw new Error(`Cannot refresh remote playlist ${playlistUrl}: invalid body.`) | ||
36 | } | ||
37 | |||
38 | return createOrUpdateVideoPlaylist(playlistObject, account, playlistObject.to) | ||
39 | } catch (err) { | ||
40 | logger.warn('Cannot add playlist element %s.', playlistUrl, { err, ...lTags(playlistUrl) }) | ||
41 | } | ||
42 | }, { concurrency: CRAWL_REQUEST_CONCURRENCY }) | ||
43 | } | ||
44 | |||
45 | async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, byAccount: MAccountId, to: string[]) { | ||
46 | const playlistAttributes = playlistObjectToDBAttributes(playlistObject, byAccount, to) | ||
47 | |||
48 | await setVideoChannelIfNeeded(playlistObject, playlistAttributes) | ||
49 | |||
50 | const [ upsertPlaylist ] = await VideoPlaylistModel.upsert<MVideoPlaylist>(playlistAttributes, { returning: true }) | ||
51 | |||
52 | const playlistElementUrls = await fetchElementUrls(playlistObject) | ||
53 | |||
54 | // Refetch playlist from DB since elements fetching could be long in time | ||
55 | const playlist = await VideoPlaylistModel.loadWithAccountAndChannel(upsertPlaylist.id, null) | ||
56 | |||
57 | try { | ||
58 | await updatePlaylistThumbnail(playlistObject, playlist) | ||
59 | } catch (err) { | ||
60 | logger.warn('Cannot update thumbnail of %s.', playlistObject.id, { err, ...lTags(playlistObject.id, playlist.uuid, playlist.url) }) | ||
61 | } | ||
62 | |||
63 | return rebuildVideoPlaylistElements(playlistElementUrls, playlist) | ||
64 | } | ||
65 | |||
66 | // --------------------------------------------------------------------------- | ||
67 | |||
68 | export { | ||
69 | createAccountPlaylists, | ||
70 | createOrUpdateVideoPlaylist | ||
71 | } | ||
72 | |||
73 | // --------------------------------------------------------------------------- | ||
74 | |||
75 | async function setVideoChannelIfNeeded (playlistObject: PlaylistObject, playlistAttributes: AttributesOnly<VideoPlaylistModel>) { | ||
76 | if (!isArray(playlistObject.attributedTo) || playlistObject.attributedTo.length !== 1) return | ||
77 | |||
78 | const actor = await getOrCreateActorAndServerAndModel(playlistObject.attributedTo[0]) | ||
79 | |||
80 | if (!actor.VideoChannel) { | ||
81 | logger.warn('Playlist "attributedTo" %s is not a video channel.', playlistObject.id, { playlistObject, ...lTags(playlistObject.id) }) | ||
82 | return | ||
83 | } | ||
84 | |||
85 | playlistAttributes.videoChannelId = actor.VideoChannel.id | ||
86 | } | ||
87 | |||
88 | async function fetchElementUrls (playlistObject: PlaylistObject) { | ||
89 | let accItems: string[] = [] | ||
90 | await crawlCollectionPage<string>(playlistObject.id, items => { | ||
91 | accItems = accItems.concat(items) | ||
92 | |||
93 | return Promise.resolve() | ||
94 | }) | ||
95 | |||
96 | return accItems | ||
97 | } | ||
98 | |||
99 | async function updatePlaylistThumbnail (playlistObject: PlaylistObject, playlist: MVideoPlaylistFull) { | ||
100 | if (playlistObject.icon) { | ||
101 | const thumbnailModel = await createPlaylistMiniatureFromUrl({ downloadUrl: playlistObject.icon.url, playlist }) | ||
102 | await playlist.setAndSaveThumbnail(thumbnailModel, undefined) | ||
103 | |||
104 | return | ||
105 | } | ||
106 | |||
107 | // Playlist does not have an icon, destroy existing one | ||
108 | if (playlist.hasThumbnail()) { | ||
109 | await playlist.Thumbnail.destroy() | ||
110 | playlist.Thumbnail = null | ||
111 | } | ||
112 | } | ||
113 | |||
114 | async function rebuildVideoPlaylistElements (elementUrls: string[], playlist: MVideoPlaylist) { | ||
115 | const elementsToCreate = await buildElementsDBAttributes(elementUrls, playlist) | ||
116 | |||
117 | await sequelizeTypescript.transaction(async t => { | ||
118 | await VideoPlaylistElementModel.deleteAllOf(playlist.id, t) | ||
119 | |||
120 | for (const element of elementsToCreate) { | ||
121 | await VideoPlaylistElementModel.create(element, { transaction: t }) | ||
122 | } | ||
123 | }) | ||
124 | |||
125 | logger.info('Rebuilt playlist %s with %s elements.', playlist.url, elementsToCreate.length, lTags(playlist.uuid, playlist.url)) | ||
126 | |||
127 | return undefined | ||
128 | } | ||
129 | |||
130 | async function buildElementsDBAttributes (elementUrls: string[], playlist: MVideoPlaylist) { | ||
131 | const elementsToCreate: FilteredModelAttributes<VideoPlaylistElementModel>[] = [] | ||
132 | |||
133 | await Bluebird.map(elementUrls, async elementUrl => { | ||
134 | try { | ||
135 | const { elementObject } = await fetchRemotePlaylistElement(elementUrl) | ||
136 | |||
137 | const { video } = await getOrCreateAPVideo({ videoObject: { id: elementObject.url }, fetchType: 'only-video' }) | ||
138 | |||
139 | elementsToCreate.push(playlistElementObjectToDBAttributes(elementObject, playlist, video)) | ||
140 | } catch (err) { | ||
141 | logger.warn('Cannot add playlist element %s.', elementUrl, { err, ...lTags(playlist.uuid, playlist.url) }) | ||
142 | } | ||
143 | }, { concurrency: CRAWL_REQUEST_CONCURRENCY }) | ||
144 | |||
145 | return elementsToCreate | ||
146 | } | ||
diff --git a/server/lib/activitypub/playlists/index.ts b/server/lib/activitypub/playlists/index.ts new file mode 100644 index 000000000..2885830b4 --- /dev/null +++ b/server/lib/activitypub/playlists/index.ts | |||
@@ -0,0 +1,2 @@ | |||
1 | export * from './create-update' | ||
2 | export * from './refresh' | ||
diff --git a/server/lib/activitypub/playlists/refresh.ts b/server/lib/activitypub/playlists/refresh.ts new file mode 100644 index 000000000..ff9e5471a --- /dev/null +++ b/server/lib/activitypub/playlists/refresh.ts | |||
@@ -0,0 +1,44 @@ | |||
1 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | ||
2 | import { PeerTubeRequestError } from '@server/helpers/requests' | ||
3 | import { MVideoPlaylistOwner } from '@server/types/models' | ||
4 | import { HttpStatusCode } from '@shared/core-utils' | ||
5 | import { createOrUpdateVideoPlaylist } from './create-update' | ||
6 | import { fetchRemoteVideoPlaylist } from './shared' | ||
7 | |||
8 | async function refreshVideoPlaylistIfNeeded (videoPlaylist: MVideoPlaylistOwner): Promise<MVideoPlaylistOwner> { | ||
9 | if (!videoPlaylist.isOutdated()) return videoPlaylist | ||
10 | |||
11 | const lTags = loggerTagsFactory('ap', 'video-playlist', 'refresh', videoPlaylist.uuid, videoPlaylist.url) | ||
12 | |||
13 | try { | ||
14 | const { playlistObject } = await fetchRemoteVideoPlaylist(videoPlaylist.url) | ||
15 | |||
16 | if (playlistObject === undefined) { | ||
17 | logger.warn('Cannot refresh remote playlist %s: invalid body.', videoPlaylist.url, lTags()) | ||
18 | |||
19 | await videoPlaylist.setAsRefreshed() | ||
20 | return videoPlaylist | ||
21 | } | ||
22 | |||
23 | const byAccount = videoPlaylist.OwnerAccount | ||
24 | await createOrUpdateVideoPlaylist(playlistObject, byAccount, playlistObject.to) | ||
25 | |||
26 | return videoPlaylist | ||
27 | } catch (err) { | ||
28 | if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) { | ||
29 | logger.info('Cannot refresh not existing playlist %s. Deleting it.', videoPlaylist.url, lTags()) | ||
30 | |||
31 | await videoPlaylist.destroy() | ||
32 | return undefined | ||
33 | } | ||
34 | |||
35 | logger.warn('Cannot refresh video playlist %s.', videoPlaylist.url, { err, ...lTags() }) | ||
36 | |||
37 | await videoPlaylist.setAsRefreshed() | ||
38 | return videoPlaylist | ||
39 | } | ||
40 | } | ||
41 | |||
42 | export { | ||
43 | refreshVideoPlaylistIfNeeded | ||
44 | } | ||
diff --git a/server/lib/activitypub/playlists/shared/index.ts b/server/lib/activitypub/playlists/shared/index.ts new file mode 100644 index 000000000..a217f2291 --- /dev/null +++ b/server/lib/activitypub/playlists/shared/index.ts | |||
@@ -0,0 +1,2 @@ | |||
1 | export * from './object-to-model-attributes' | ||
2 | export * from './url-to-object' | ||
diff --git a/server/lib/activitypub/playlists/shared/object-to-model-attributes.ts b/server/lib/activitypub/playlists/shared/object-to-model-attributes.ts new file mode 100644 index 000000000..6ec44485e --- /dev/null +++ b/server/lib/activitypub/playlists/shared/object-to-model-attributes.ts | |||
@@ -0,0 +1,40 @@ | |||
1 | import { ACTIVITY_PUB } from '@server/initializers/constants' | ||
2 | import { VideoPlaylistModel } from '@server/models/video/video-playlist' | ||
3 | import { VideoPlaylistElementModel } from '@server/models/video/video-playlist-element' | ||
4 | import { MAccountId, MVideoId, MVideoPlaylistId } from '@server/types/models' | ||
5 | import { AttributesOnly } from '@shared/core-utils' | ||
6 | import { PlaylistElementObject, PlaylistObject, VideoPlaylistPrivacy } from '@shared/models' | ||
7 | |||
8 | function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: MAccountId, to: string[]) { | ||
9 | const privacy = to.includes(ACTIVITY_PUB.PUBLIC) | ||
10 | ? VideoPlaylistPrivacy.PUBLIC | ||
11 | : VideoPlaylistPrivacy.UNLISTED | ||
12 | |||
13 | return { | ||
14 | name: playlistObject.name, | ||
15 | description: playlistObject.content, | ||
16 | privacy, | ||
17 | url: playlistObject.id, | ||
18 | uuid: playlistObject.uuid, | ||
19 | ownerAccountId: byAccount.id, | ||
20 | videoChannelId: null, | ||
21 | createdAt: new Date(playlistObject.published), | ||
22 | updatedAt: new Date(playlistObject.updated) | ||
23 | } as AttributesOnly<VideoPlaylistModel> | ||
24 | } | ||
25 | |||
26 | function playlistElementObjectToDBAttributes (elementObject: PlaylistElementObject, videoPlaylist: MVideoPlaylistId, video: MVideoId) { | ||
27 | return { | ||
28 | position: elementObject.position, | ||
29 | url: elementObject.id, | ||
30 | startTimestamp: elementObject.startTimestamp || null, | ||
31 | stopTimestamp: elementObject.stopTimestamp || null, | ||
32 | videoPlaylistId: videoPlaylist.id, | ||
33 | videoId: video.id | ||
34 | } as AttributesOnly<VideoPlaylistElementModel> | ||
35 | } | ||
36 | |||
37 | export { | ||
38 | playlistObjectToDBAttributes, | ||
39 | playlistElementObjectToDBAttributes | ||
40 | } | ||
diff --git a/server/lib/activitypub/playlists/shared/url-to-object.ts b/server/lib/activitypub/playlists/shared/url-to-object.ts new file mode 100644 index 000000000..ec8c01255 --- /dev/null +++ b/server/lib/activitypub/playlists/shared/url-to-object.ts | |||
@@ -0,0 +1,47 @@ | |||
1 | import { isArray } from 'lodash' | ||
2 | import { checkUrlsSameHost } from '@server/helpers/activitypub' | ||
3 | import { isPlaylistElementObjectValid, isPlaylistObjectValid } from '@server/helpers/custom-validators/activitypub/playlist' | ||
4 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | ||
5 | import { doJSONRequest } from '@server/helpers/requests' | ||
6 | import { PlaylistElementObject, PlaylistObject } from '@shared/models' | ||
7 | |||
8 | async function fetchRemoteVideoPlaylist (playlistUrl: string): Promise<{ statusCode: number, playlistObject: PlaylistObject }> { | ||
9 | const lTags = loggerTagsFactory('ap', 'video-playlist', playlistUrl) | ||
10 | |||
11 | logger.info('Fetching remote playlist %s.', playlistUrl, lTags()) | ||
12 | |||
13 | const { body, statusCode } = await doJSONRequest<any>(playlistUrl, { activityPub: true }) | ||
14 | |||
15 | if (isPlaylistObjectValid(body) === false || checkUrlsSameHost(body.id, playlistUrl) !== true) { | ||
16 | logger.debug('Remote video playlist JSON is not valid.', { body, ...lTags() }) | ||
17 | return { statusCode, playlistObject: undefined } | ||
18 | } | ||
19 | |||
20 | if (!isArray(body.to)) { | ||
21 | logger.debug('Remote video playlist JSON does not have a valid audience.', { body, ...lTags() }) | ||
22 | return { statusCode, playlistObject: undefined } | ||
23 | } | ||
24 | |||
25 | return { statusCode, playlistObject: body } | ||
26 | } | ||
27 | |||
28 | async function fetchRemotePlaylistElement (elementUrl: string): Promise<{ statusCode: number, elementObject: PlaylistElementObject }> { | ||
29 | const lTags = loggerTagsFactory('ap', 'video-playlist', 'element', elementUrl) | ||
30 | |||
31 | logger.debug('Fetching remote playlist element %s.', elementUrl, lTags()) | ||
32 | |||
33 | const { body, statusCode } = await doJSONRequest<PlaylistElementObject>(elementUrl, { activityPub: true }) | ||
34 | |||
35 | if (!isPlaylistElementObjectValid(body)) throw new Error(`Invalid body in fetch playlist element ${elementUrl}`) | ||
36 | |||
37 | if (checkUrlsSameHost(body.id, elementUrl) !== true) { | ||
38 | throw new Error(`Playlist element url ${elementUrl} host is different from the AP object id ${body.id}`) | ||
39 | } | ||
40 | |||
41 | return { statusCode, elementObject: body } | ||
42 | } | ||
43 | |||
44 | export { | ||
45 | fetchRemoteVideoPlaylist, | ||
46 | fetchRemotePlaylistElement | ||
47 | } | ||
diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts index ef5a3100e..6b7f5aae8 100644 --- a/server/lib/activitypub/process/process-create.ts +++ b/server/lib/activitypub/process/process-create.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | import { isBlockedByServerOrAccount } from '@server/lib/blocklist' | ||
1 | import { isRedundancyAccepted } from '@server/lib/redundancy' | 2 | import { isRedundancyAccepted } from '@server/lib/redundancy' |
2 | import { ActivityCreate, CacheFileObject, VideoObject } from '../../../../shared' | 3 | import { ActivityCreate, CacheFileObject, VideoObject } from '../../../../shared' |
3 | import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object' | 4 | import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object' |
@@ -9,11 +10,10 @@ import { APProcessorOptions } from '../../../types/activitypub-processor.model' | |||
9 | import { MActorSignature, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../../types/models' | 10 | import { MActorSignature, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../../types/models' |
10 | import { Notifier } from '../../notifier' | 11 | import { Notifier } from '../../notifier' |
11 | import { createOrUpdateCacheFile } from '../cache-file' | 12 | import { createOrUpdateCacheFile } from '../cache-file' |
12 | import { createOrUpdateVideoPlaylist } from '../playlist' | 13 | import { createOrUpdateVideoPlaylist } from '../playlists' |
13 | import { forwardVideoRelatedActivity } from '../send/utils' | 14 | import { forwardVideoRelatedActivity } from '../send/utils' |
14 | import { resolveThread } from '../video-comments' | 15 | import { resolveThread } from '../video-comments' |
15 | import { getOrCreateAPVideo } from '../videos' | 16 | import { getOrCreateAPVideo } from '../videos' |
16 | import { isBlockedByServerOrAccount } from '@server/lib/blocklist' | ||
17 | 17 | ||
18 | async function processCreateActivity (options: APProcessorOptions<ActivityCreate>) { | 18 | async function processCreateActivity (options: APProcessorOptions<ActivityCreate>) { |
19 | const { activity, byActor } = options | 19 | const { activity, byActor } = options |
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts index be3f6acac..d2b63c901 100644 --- a/server/lib/activitypub/process/process-update.ts +++ b/server/lib/activitypub/process/process-update.ts | |||
@@ -15,7 +15,7 @@ import { APProcessorOptions } from '../../../types/activitypub-processor.model' | |||
15 | import { MActorSignature } from '../../../types/models' | 15 | import { MActorSignature } from '../../../types/models' |
16 | import { getImageInfoIfExists, updateActorImageInstance, updateActorInstance } from '../actor' | 16 | import { getImageInfoIfExists, updateActorImageInstance, updateActorInstance } from '../actor' |
17 | import { createOrUpdateCacheFile } from '../cache-file' | 17 | import { createOrUpdateCacheFile } from '../cache-file' |
18 | import { createOrUpdateVideoPlaylist } from '../playlist' | 18 | import { createOrUpdateVideoPlaylist } from '../playlists' |
19 | import { forwardVideoRelatedActivity } from '../send/utils' | 19 | import { forwardVideoRelatedActivity } from '../send/utils' |
20 | import { APVideoUpdater, getOrCreateAPVideo } from '../videos' | 20 | import { APVideoUpdater, getOrCreateAPVideo } from '../videos' |
21 | 21 | ||
diff --git a/server/lib/activitypub/share.ts b/server/lib/activitypub/share.ts index c22fa0893..327955dd2 100644 --- a/server/lib/activitypub/share.ts +++ b/server/lib/activitypub/share.ts | |||
@@ -40,23 +40,7 @@ async function changeVideoChannelShare ( | |||
40 | async function addVideoShares (shareUrls: string[], video: MVideoId) { | 40 | async function addVideoShares (shareUrls: string[], video: MVideoId) { |
41 | await Bluebird.map(shareUrls, async shareUrl => { | 41 | await Bluebird.map(shareUrls, async shareUrl => { |
42 | try { | 42 | try { |
43 | const { body } = await doJSONRequest<any>(shareUrl, { activityPub: true }) | 43 | await addVideoShare(shareUrl, video) |
44 | if (!body || !body.actor) throw new Error('Body or body actor is invalid') | ||
45 | |||
46 | const actorUrl = getAPId(body.actor) | ||
47 | if (checkUrlsSameHost(shareUrl, actorUrl) !== true) { | ||
48 | throw new Error(`Actor url ${actorUrl} has not the same host than the share url ${shareUrl}`) | ||
49 | } | ||
50 | |||
51 | const actor = await getOrCreateActorAndServerAndModel(actorUrl) | ||
52 | |||
53 | const entry = { | ||
54 | actorId: actor.id, | ||
55 | videoId: video.id, | ||
56 | url: shareUrl | ||
57 | } | ||
58 | |||
59 | await VideoShareModel.upsert(entry) | ||
60 | } catch (err) { | 44 | } catch (err) { |
61 | logger.warn('Cannot add share %s.', shareUrl, { err }) | 45 | logger.warn('Cannot add share %s.', shareUrl, { err }) |
62 | } | 46 | } |
@@ -71,6 +55,26 @@ export { | |||
71 | 55 | ||
72 | // --------------------------------------------------------------------------- | 56 | // --------------------------------------------------------------------------- |
73 | 57 | ||
58 | async function addVideoShare (shareUrl: string, video: MVideoId) { | ||
59 | const { body } = await doJSONRequest<any>(shareUrl, { activityPub: true }) | ||
60 | if (!body || !body.actor) throw new Error('Body or body actor is invalid') | ||
61 | |||
62 | const actorUrl = getAPId(body.actor) | ||
63 | if (checkUrlsSameHost(shareUrl, actorUrl) !== true) { | ||
64 | throw new Error(`Actor url ${actorUrl} has not the same host than the share url ${shareUrl}`) | ||
65 | } | ||
66 | |||
67 | const actor = await getOrCreateActorAndServerAndModel(actorUrl) | ||
68 | |||
69 | const entry = { | ||
70 | actorId: actor.id, | ||
71 | videoId: video.id, | ||
72 | url: shareUrl | ||
73 | } | ||
74 | |||
75 | await VideoShareModel.upsert(entry) | ||
76 | } | ||
77 | |||
74 | async function shareByServer (video: MVideo, t: Transaction) { | 78 | async function shareByServer (video: MVideo, t: Transaction) { |
75 | const serverActor = await getServerActor() | 79 | const serverActor = await getServerActor() |
76 | 80 | ||
diff --git a/server/lib/activitypub/video-comments.ts b/server/lib/activitypub/video-comments.ts index 722147b69..760da719d 100644 --- a/server/lib/activitypub/video-comments.ts +++ b/server/lib/activitypub/video-comments.ts | |||
@@ -29,10 +29,11 @@ async function addVideoComments (commentUrls: string[]) { | |||
29 | 29 | ||
30 | async function resolveThread (params: ResolveThreadParams): ResolveThreadResult { | 30 | async function resolveThread (params: ResolveThreadParams): ResolveThreadResult { |
31 | const { url, isVideo } = params | 31 | const { url, isVideo } = params |
32 | |||
32 | if (params.commentCreated === undefined) params.commentCreated = false | 33 | if (params.commentCreated === undefined) params.commentCreated = false |
33 | if (params.comments === undefined) params.comments = [] | 34 | if (params.comments === undefined) params.comments = [] |
34 | 35 | ||
35 | // If it is not a video, or if we don't know if it's a video | 36 | // If it is not a video, or if we don't know if it's a video, try to get the thread from DB |
36 | if (isVideo === false || isVideo === undefined) { | 37 | if (isVideo === false || isVideo === undefined) { |
37 | const result = await resolveCommentFromDB(params) | 38 | const result = await resolveCommentFromDB(params) |
38 | if (result) return result | 39 | if (result) return result |
@@ -42,7 +43,7 @@ async function resolveThread (params: ResolveThreadParams): ResolveThreadResult | |||
42 | // If it is a video, or if we don't know if it's a video | 43 | // If it is a video, or if we don't know if it's a video |
43 | if (isVideo === true || isVideo === undefined) { | 44 | if (isVideo === true || isVideo === undefined) { |
44 | // Keep await so we catch the exception | 45 | // Keep await so we catch the exception |
45 | return await tryResolveThreadFromVideo(params) | 46 | return await tryToResolveThreadFromVideo(params) |
46 | } | 47 | } |
47 | } catch (err) { | 48 | } catch (err) { |
48 | logger.debug('Cannot resolve thread from video %s, maybe because it was not a video', url, { err }) | 49 | logger.debug('Cannot resolve thread from video %s, maybe because it was not a video', url, { err }) |
@@ -62,28 +63,26 @@ async function resolveCommentFromDB (params: ResolveThreadParams) { | |||
62 | const { url, comments, commentCreated } = params | 63 | const { url, comments, commentCreated } = params |
63 | 64 | ||
64 | const commentFromDatabase = await VideoCommentModel.loadByUrlAndPopulateReplyAndVideoUrlAndAccount(url) | 65 | const commentFromDatabase = await VideoCommentModel.loadByUrlAndPopulateReplyAndVideoUrlAndAccount(url) |
65 | if (commentFromDatabase) { | 66 | if (!commentFromDatabase) return undefined |
66 | let parentComments = comments.concat([ commentFromDatabase ]) | ||
67 | 67 | ||
68 | // Speed up things and resolve directly the thread | 68 | let parentComments = comments.concat([ commentFromDatabase ]) |
69 | if (commentFromDatabase.InReplyToVideoComment) { | ||
70 | const data = await VideoCommentModel.listThreadParentComments(commentFromDatabase, undefined, 'DESC') | ||
71 | 69 | ||
72 | parentComments = parentComments.concat(data) | 70 | // Speed up things and resolve directly the thread |
73 | } | 71 | if (commentFromDatabase.InReplyToVideoComment) { |
72 | const data = await VideoCommentModel.listThreadParentComments(commentFromDatabase, undefined, 'DESC') | ||
74 | 73 | ||
75 | return resolveThread({ | 74 | parentComments = parentComments.concat(data) |
76 | url: commentFromDatabase.Video.url, | ||
77 | comments: parentComments, | ||
78 | isVideo: true, | ||
79 | commentCreated | ||
80 | }) | ||
81 | } | 75 | } |
82 | 76 | ||
83 | return undefined | 77 | return resolveThread({ |
78 | url: commentFromDatabase.Video.url, | ||
79 | comments: parentComments, | ||
80 | isVideo: true, | ||
81 | commentCreated | ||
82 | }) | ||
84 | } | 83 | } |
85 | 84 | ||
86 | async function tryResolveThreadFromVideo (params: ResolveThreadParams) { | 85 | async function tryToResolveThreadFromVideo (params: ResolveThreadParams) { |
87 | const { url, comments, commentCreated } = params | 86 | const { url, comments, commentCreated } = params |
88 | 87 | ||
89 | // Maybe it's a reply to a video? | 88 | // Maybe it's a reply to a video? |
diff --git a/server/lib/activitypub/video-rates.ts b/server/lib/activitypub/video-rates.ts index f40c07fea..091f4ec23 100644 --- a/server/lib/activitypub/video-rates.ts +++ b/server/lib/activitypub/video-rates.ts | |||
@@ -15,30 +15,7 @@ import { getVideoDislikeActivityPubUrlByLocalActor, getVideoLikeActivityPubUrlBy | |||
15 | async function createRates (ratesUrl: string[], video: MVideo, rate: VideoRateType) { | 15 | async function createRates (ratesUrl: string[], video: MVideo, rate: VideoRateType) { |
16 | await Bluebird.map(ratesUrl, async rateUrl => { | 16 | await Bluebird.map(ratesUrl, async rateUrl => { |
17 | try { | 17 | try { |
18 | // Fetch url | 18 | await createRate(rateUrl, video, rate) |
19 | const { body } = await doJSONRequest<any>(rateUrl, { activityPub: true }) | ||
20 | if (!body || !body.actor) throw new Error('Body or body actor is invalid') | ||
21 | |||
22 | const actorUrl = getAPId(body.actor) | ||
23 | if (checkUrlsSameHost(actorUrl, rateUrl) !== true) { | ||
24 | throw new Error(`Rate url ${rateUrl} has not the same host than actor url ${actorUrl}`) | ||
25 | } | ||
26 | |||
27 | if (checkUrlsSameHost(body.id, rateUrl) !== true) { | ||
28 | throw new Error(`Rate url ${rateUrl} host is different from the AP object id ${body.id}`) | ||
29 | } | ||
30 | |||
31 | const actor = await getOrCreateActorAndServerAndModel(actorUrl) | ||
32 | |||
33 | const entry = { | ||
34 | videoId: video.id, | ||
35 | accountId: actor.Account.id, | ||
36 | type: rate, | ||
37 | url: body.id | ||
38 | } | ||
39 | |||
40 | // Video "likes"/"dislikes" will be updated by the caller | ||
41 | await AccountVideoRateModel.upsert(entry) | ||
42 | } catch (err) { | 19 | } catch (err) { |
43 | logger.warn('Cannot add rate %s.', rateUrl, { err }) | 20 | logger.warn('Cannot add rate %s.', rateUrl, { err }) |
44 | } | 21 | } |
@@ -73,8 +50,39 @@ function getLocalRateUrl (rateType: VideoRateType, actor: MActorUrl, video: MVid | |||
73 | : getVideoDislikeActivityPubUrlByLocalActor(actor, video) | 50 | : getVideoDislikeActivityPubUrlByLocalActor(actor, video) |
74 | } | 51 | } |
75 | 52 | ||
53 | // --------------------------------------------------------------------------- | ||
54 | |||
76 | export { | 55 | export { |
77 | getLocalRateUrl, | 56 | getLocalRateUrl, |
78 | createRates, | 57 | createRates, |
79 | sendVideoRateChange | 58 | sendVideoRateChange |
80 | } | 59 | } |
60 | |||
61 | // --------------------------------------------------------------------------- | ||
62 | |||
63 | async function createRate (rateUrl: string, video: MVideo, rate: VideoRateType) { | ||
64 | // Fetch url | ||
65 | const { body } = await doJSONRequest<any>(rateUrl, { activityPub: true }) | ||
66 | if (!body || !body.actor) throw new Error('Body or body actor is invalid') | ||
67 | |||
68 | const actorUrl = getAPId(body.actor) | ||
69 | if (checkUrlsSameHost(actorUrl, rateUrl) !== true) { | ||
70 | throw new Error(`Rate url ${rateUrl} has not the same host than actor url ${actorUrl}`) | ||
71 | } | ||
72 | |||
73 | if (checkUrlsSameHost(body.id, rateUrl) !== true) { | ||
74 | throw new Error(`Rate url ${rateUrl} host is different from the AP object id ${body.id}`) | ||
75 | } | ||
76 | |||
77 | const actor = await getOrCreateActorAndServerAndModel(actorUrl) | ||
78 | |||
79 | const entry = { | ||
80 | videoId: video.id, | ||
81 | accountId: actor.Account.id, | ||
82 | type: rate, | ||
83 | url: body.id | ||
84 | } | ||
85 | |||
86 | // Video "likes"/"dislikes" will be updated by the caller | ||
87 | await AccountVideoRateModel.upsert(entry) | ||
88 | } | ||
diff --git a/server/lib/job-queue/handlers/activitypub-http-fetcher.ts b/server/lib/job-queue/handlers/activitypub-http-fetcher.ts index e210ac3ef..04b25f955 100644 --- a/server/lib/job-queue/handlers/activitypub-http-fetcher.ts +++ b/server/lib/job-queue/handlers/activitypub-http-fetcher.ts | |||
@@ -8,7 +8,7 @@ import { VideoCommentModel } from '../../../models/video/video-comment' | |||
8 | import { VideoShareModel } from '../../../models/video/video-share' | 8 | import { VideoShareModel } from '../../../models/video/video-share' |
9 | import { MAccountDefault, MVideoFullLight } from '../../../types/models' | 9 | import { MAccountDefault, MVideoFullLight } from '../../../types/models' |
10 | import { crawlCollectionPage } from '../../activitypub/crawl' | 10 | import { crawlCollectionPage } from '../../activitypub/crawl' |
11 | import { createAccountPlaylists } from '../../activitypub/playlist' | 11 | import { createAccountPlaylists } from '../../activitypub/playlists' |
12 | import { processActivities } from '../../activitypub/process' | 12 | import { processActivities } from '../../activitypub/process' |
13 | import { addVideoShares } from '../../activitypub/share' | 13 | import { addVideoShares } from '../../activitypub/share' |
14 | import { addVideoComments } from '../../activitypub/video-comments' | 14 | import { addVideoComments } from '../../activitypub/video-comments' |
diff --git a/server/lib/job-queue/handlers/activitypub-refresher.ts b/server/lib/job-queue/handlers/activitypub-refresher.ts index a120e4ea8..10e6895da 100644 --- a/server/lib/job-queue/handlers/activitypub-refresher.ts +++ b/server/lib/job-queue/handlers/activitypub-refresher.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import * as Bull from 'bull' | 1 | import * as Bull from 'bull' |
2 | import { refreshVideoPlaylistIfNeeded } from '@server/lib/activitypub/playlist' | 2 | import { refreshVideoPlaylistIfNeeded } from '@server/lib/activitypub/playlists' |
3 | import { refreshVideoIfNeeded } from '@server/lib/activitypub/videos' | 3 | import { refreshVideoIfNeeded } from '@server/lib/activitypub/videos' |
4 | import { RefreshPayload } from '@shared/models' | 4 | import { RefreshPayload } from '@shared/models' |
5 | import { logger } from '../../../helpers/logger' | 5 | import { logger } from '../../../helpers/logger' |
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts index 98cea1b64..1a05f8d42 100644 --- a/server/models/video/video-playlist.ts +++ b/server/models/video/video-playlist.ts | |||
@@ -18,6 +18,7 @@ import { | |||
18 | UpdatedAt | 18 | UpdatedAt |
19 | } from 'sequelize-typescript' | 19 | } from 'sequelize-typescript' |
20 | import { v4 as uuidv4 } from 'uuid' | 20 | import { v4 as uuidv4 } from 'uuid' |
21 | import { setAsUpdated } from '@server/helpers/database-utils' | ||
21 | import { MAccountId, MChannelId } from '@server/types/models' | 22 | import { MAccountId, MChannelId } from '@server/types/models' |
22 | import { AttributesOnly } from '@shared/core-utils' | 23 | import { AttributesOnly } from '@shared/core-utils' |
23 | import { ActivityIconObject } from '../../../shared/models/activitypub/objects' | 24 | import { ActivityIconObject } from '../../../shared/models/activitypub/objects' |
@@ -531,9 +532,7 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli | |||
531 | } | 532 | } |
532 | 533 | ||
533 | setAsRefreshed () { | 534 | setAsRefreshed () { |
534 | this.changed('updatedAt', true) | 535 | return setAsUpdated('videoPlaylist', this.id) |
535 | |||
536 | return this.save() | ||
537 | } | 536 | } |
538 | 537 | ||
539 | isOwned () { | 538 | isOwned () { |
diff --git a/shared/models/activitypub/objects/index.ts b/shared/models/activitypub/objects/index.ts index a6a20e87a..9e2c6b728 100644 --- a/shared/models/activitypub/objects/index.ts +++ b/shared/models/activitypub/objects/index.ts | |||
@@ -2,5 +2,9 @@ export * from './abuse-object' | |||
2 | export * from './cache-file-object' | 2 | export * from './cache-file-object' |
3 | export * from './common-objects' | 3 | export * from './common-objects' |
4 | export * from './dislike-object' | 4 | export * from './dislike-object' |
5 | export * from './object.model' | ||
6 | export * from './playlist-element-object' | ||
7 | export * from './playlist-object' | ||
8 | export * from './video-comment-object' | ||
5 | export * from './video-torrent-object' | 9 | export * from './video-torrent-object' |
6 | export * from './view-object' | 10 | export * from './view-object' |