diff options
-rw-r--r-- | client/src/app/videos/+video-watch/comment/video-comment-add.component.scss | 4 | ||||
-rw-r--r-- | server/lib/activitypub/process/process-create.ts | 32 | ||||
-rw-r--r-- | server/lib/activitypub/send/misc.ts | 47 | ||||
-rw-r--r-- | server/lib/activitypub/send/send-create.ts | 40 | ||||
-rw-r--r-- | server/models/account/account.ts | 15 |
5 files changed, 94 insertions, 44 deletions
diff --git a/client/src/app/videos/+video-watch/comment/video-comment-add.component.scss b/client/src/app/videos/+video-watch/comment/video-comment-add.component.scss index 37097da72..e586880fc 100644 --- a/client/src/app/videos/+video-watch/comment/video-comment-add.component.scss +++ b/client/src/app/videos/+video-watch/comment/video-comment-add.component.scss | |||
@@ -1,6 +1,10 @@ | |||
1 | @import '_variables'; | 1 | @import '_variables'; |
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | form { | ||
5 | margin-bottom: 30px; | ||
6 | } | ||
7 | |||
4 | .avatar-and-textarea { | 8 | .avatar-and-textarea { |
5 | display: flex; | 9 | display: flex; |
6 | margin-bottom: 10px; | 10 | margin-bottom: 10px; |
diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts index ffd20fe74..a97f6ae83 100644 --- a/server/lib/activitypub/process/process-create.ts +++ b/server/lib/activitypub/process/process-create.ts | |||
@@ -13,8 +13,9 @@ import { VideoModel } from '../../../models/video/video' | |||
13 | import { VideoAbuseModel } from '../../../models/video/video-abuse' | 13 | import { VideoAbuseModel } from '../../../models/video/video-abuse' |
14 | import { VideoCommentModel } from '../../../models/video/video-comment' | 14 | import { VideoCommentModel } from '../../../models/video/video-comment' |
15 | import { VideoFileModel } from '../../../models/video/video-file' | 15 | import { VideoFileModel } from '../../../models/video/video-file' |
16 | import { VideoShareModel } from '../../../models/video/video-share' | ||
16 | import { getOrCreateActorAndServerAndModel } from '../actor' | 17 | import { getOrCreateActorAndServerAndModel } from '../actor' |
17 | import { forwardActivity } from '../send/misc' | 18 | import { forwardActivity, getActorsInvolvedInVideo } from '../send/misc' |
18 | import { generateThumbnailFromUrl } from '../videos' | 19 | import { generateThumbnailFromUrl } from '../videos' |
19 | import { addVideoComments, addVideoShares, videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc' | 20 | import { addVideoComments, addVideoShares, videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc' |
20 | 21 | ||
@@ -266,18 +267,19 @@ function createVideoComment (byActor: ActorModel, activity: ActivityCreate) { | |||
266 | if (!byAccount) throw new Error('Cannot create video comment with the non account actor ' + byActor.url) | 267 | if (!byAccount) throw new Error('Cannot create video comment with the non account actor ' + byActor.url) |
267 | 268 | ||
268 | return sequelizeTypescript.transaction(async t => { | 269 | return sequelizeTypescript.transaction(async t => { |
269 | let video = await VideoModel.loadByUrl(comment.inReplyTo, t) | 270 | let video = await VideoModel.loadByUrlAndPopulateAccount(comment.inReplyTo, t) |
271 | let objectToCreate | ||
270 | 272 | ||
271 | // This is a new thread | 273 | // This is a new thread |
272 | if (video) { | 274 | if (video) { |
273 | await VideoCommentModel.create({ | 275 | objectToCreate = { |
274 | url: comment.id, | 276 | url: comment.id, |
275 | text: comment.content, | 277 | text: comment.content, |
276 | originCommentId: null, | 278 | originCommentId: null, |
277 | inReplyToComment: null, | 279 | inReplyToComment: null, |
278 | videoId: video.id, | 280 | videoId: video.id, |
279 | accountId: byAccount.id | 281 | accountId: byAccount.id |
280 | }, { transaction: t }) | 282 | } |
281 | } else { | 283 | } else { |
282 | const inReplyToComment = await VideoCommentModel.loadByUrl(comment.inReplyTo, t) | 284 | const inReplyToComment = await VideoCommentModel.loadByUrl(comment.inReplyTo, t) |
283 | if (!inReplyToComment) throw new Error('Unknown replied comment ' + comment.inReplyTo) | 285 | if (!inReplyToComment) throw new Error('Unknown replied comment ' + comment.inReplyTo) |
@@ -285,20 +287,34 @@ function createVideoComment (byActor: ActorModel, activity: ActivityCreate) { | |||
285 | video = await VideoModel.load(inReplyToComment.videoId) | 287 | video = await VideoModel.load(inReplyToComment.videoId) |
286 | 288 | ||
287 | const originCommentId = inReplyToComment.originCommentId || inReplyToComment.id | 289 | const originCommentId = inReplyToComment.originCommentId || inReplyToComment.id |
288 | await VideoCommentModel.create({ | 290 | objectToCreate = { |
289 | url: comment.id, | 291 | url: comment.id, |
290 | text: comment.content, | 292 | text: comment.content, |
291 | originCommentId, | 293 | originCommentId, |
292 | inReplyToCommentId: inReplyToComment.id, | 294 | inReplyToCommentId: inReplyToComment.id, |
293 | videoId: video.id, | 295 | videoId: video.id, |
294 | accountId: byAccount.id | 296 | accountId: byAccount.id |
295 | }, { transaction: t }) | 297 | } |
296 | } | 298 | } |
297 | 299 | ||
298 | if (video.isOwned()) { | 300 | const options = { |
301 | where: { | ||
302 | url: objectToCreate.url | ||
303 | }, | ||
304 | defaults: objectToCreate, | ||
305 | transaction: t | ||
306 | } | ||
307 | const [ ,created ] = await VideoCommentModel.findOrCreate(options) | ||
308 | |||
309 | if (video.isOwned() && created === true) { | ||
299 | // Don't resend the activity to the sender | 310 | // Don't resend the activity to the sender |
300 | const exceptions = [ byActor ] | 311 | const exceptions = [ byActor ] |
301 | await forwardActivity(activity, t, exceptions) | 312 | |
313 | // Mastodon does not add our announces in audience, so we forward to them manually | ||
314 | const additionalActors = await getActorsInvolvedInVideo(video, t) | ||
315 | const additionalFollowerUrls = additionalActors.map(a => a.followersUrl) | ||
316 | |||
317 | await forwardActivity(activity, t, exceptions, additionalFollowerUrls) | ||
302 | } | 318 | } |
303 | }) | 319 | }) |
304 | } | 320 | } |
diff --git a/server/lib/activitypub/send/misc.ts b/server/lib/activitypub/send/misc.ts index 4aa514c15..2a9f4cae8 100644 --- a/server/lib/activitypub/send/misc.ts +++ b/server/lib/activitypub/send/misc.ts | |||
@@ -12,12 +12,13 @@ import { activitypubHttpJobScheduler, ActivityPubHttpPayload } from '../../jobs/ | |||
12 | async function forwardActivity ( | 12 | async function forwardActivity ( |
13 | activity: Activity, | 13 | activity: Activity, |
14 | t: Transaction, | 14 | t: Transaction, |
15 | followersException: ActorModel[] = [] | 15 | followersException: ActorModel[] = [], |
16 | additionalFollowerUrls: string[] = [] | ||
16 | ) { | 17 | ) { |
17 | const to = activity.to || [] | 18 | const to = activity.to || [] |
18 | const cc = activity.cc || [] | 19 | const cc = activity.cc || [] |
19 | 20 | ||
20 | const followersUrls: string[] = [] | 21 | const followersUrls = additionalFollowerUrls |
21 | for (const dest of to.concat(cc)) { | 22 | for (const dest of to.concat(cc)) { |
22 | if (dest.endsWith('/followers')) { | 23 | if (dest.endsWith('/followers')) { |
23 | followersUrls.push(dest) | 24 | followersUrls.push(dest) |
@@ -47,13 +48,25 @@ async function broadcastToFollowers ( | |||
47 | byActor: ActorModel, | 48 | byActor: ActorModel, |
48 | toActorFollowers: ActorModel[], | 49 | toActorFollowers: ActorModel[], |
49 | t: Transaction, | 50 | t: Transaction, |
50 | followersException: ActorModel[] = [] | 51 | actorsException: ActorModel[] = [] |
51 | ) { | 52 | ) { |
52 | const uris = await computeFollowerUris(toActorFollowers, followersException, t) | 53 | const uris = await computeFollowerUris(toActorFollowers, actorsException, t) |
53 | if (uris.length === 0) { | 54 | return broadcastTo(uris, data, byActor, t) |
54 | logger.info('0 followers for %s, no broadcasting.', toActorFollowers.map(a => a.id).join(', ')) | 55 | } |
55 | return undefined | 56 | |
56 | } | 57 | async function broadcastToActors ( |
58 | data: any, | ||
59 | byActor: ActorModel, | ||
60 | toActors: ActorModel[], | ||
61 | t: Transaction, | ||
62 | actorsException: ActorModel[] = [] | ||
63 | ) { | ||
64 | const uris = await computeUris(toActors, actorsException) | ||
65 | return broadcastTo(uris, data, byActor, t) | ||
66 | } | ||
67 | |||
68 | async function broadcastTo (uris: string[], data: any, byActor: ActorModel, t: Transaction) { | ||
69 | if (uris.length === 0) return undefined | ||
57 | 70 | ||
58 | logger.debug('Creating broadcast job.', { uris }) | 71 | logger.debug('Creating broadcast job.', { uris }) |
59 | 72 | ||
@@ -149,12 +162,20 @@ function audiencify (object: any, audience: ActivityAudience) { | |||
149 | return Object.assign(object, audience) | 162 | return Object.assign(object, audience) |
150 | } | 163 | } |
151 | 164 | ||
152 | async function computeFollowerUris (toActorFollower: ActorModel[], followersException: ActorModel[], t: Transaction) { | 165 | async function computeFollowerUris (toActorFollower: ActorModel[], actorsException: ActorModel[], t: Transaction) { |
153 | const toActorFollowerIds = toActorFollower.map(a => a.id) | 166 | const toActorFollowerIds = toActorFollower.map(a => a.id) |
154 | 167 | ||
155 | const result = await ActorFollowModel.listAcceptedFollowerSharedInboxUrls(toActorFollowerIds, t) | 168 | const result = await ActorFollowModel.listAcceptedFollowerSharedInboxUrls(toActorFollowerIds, t) |
156 | const followersSharedInboxException = followersException.map(f => f.sharedInboxUrl) | 169 | const sharedInboxesException = actorsException.map(f => f.sharedInboxUrl) |
157 | return result.data.filter(sharedInbox => followersSharedInboxException.indexOf(sharedInbox) === -1) | 170 | return result.data.filter(sharedInbox => sharedInboxesException.indexOf(sharedInbox) === -1) |
171 | } | ||
172 | |||
173 | async function computeUris (toActors: ActorModel[], actorsException: ActorModel[] = []) { | ||
174 | const toActorSharedInboxesSet = new Set(toActors.map(a => a.sharedInboxUrl)) | ||
175 | |||
176 | const sharedInboxesException = actorsException.map(f => f.sharedInboxUrl) | ||
177 | return Array.from(toActorSharedInboxesSet) | ||
178 | .filter(sharedInbox => sharedInboxesException.indexOf(sharedInbox) === -1) | ||
158 | } | 179 | } |
159 | 180 | ||
160 | // --------------------------------------------------------------------------- | 181 | // --------------------------------------------------------------------------- |
@@ -168,5 +189,7 @@ export { | |||
168 | getObjectFollowersAudience, | 189 | getObjectFollowersAudience, |
169 | forwardActivity, | 190 | forwardActivity, |
170 | audiencify, | 191 | audiencify, |
171 | getOriginVideoCommentAudience | 192 | getOriginVideoCommentAudience, |
193 | computeUris, | ||
194 | broadcastToActors | ||
172 | } | 195 | } |
diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts index e2ee639d9..9db663be1 100644 --- a/server/lib/activitypub/send/send-create.ts +++ b/server/lib/activitypub/send/send-create.ts | |||
@@ -8,7 +8,7 @@ import { VideoAbuseModel } from '../../../models/video/video-abuse' | |||
8 | import { VideoCommentModel } from '../../../models/video/video-comment' | 8 | import { VideoCommentModel } from '../../../models/video/video-comment' |
9 | import { getVideoAbuseActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoViewActivityPubUrl } from '../url' | 9 | import { getVideoAbuseActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoViewActivityPubUrl } from '../url' |
10 | import { | 10 | import { |
11 | audiencify, broadcastToFollowers, getActorsInvolvedInVideo, getAudience, getObjectFollowersAudience, | 11 | audiencify, broadcastToActors, broadcastToFollowers, getActorsInvolvedInVideo, getAudience, getObjectFollowersAudience, |
12 | getOriginVideoAudience, getOriginVideoCommentAudience, | 12 | getOriginVideoAudience, getOriginVideoCommentAudience, |
13 | unicastTo | 13 | unicastTo |
14 | } from './misc' | 14 | } from './misc' |
@@ -39,11 +39,20 @@ async function sendCreateVideoCommentToOrigin (comment: VideoCommentModel, t: Tr | |||
39 | const threadParentComments = await VideoCommentModel.listThreadParentComments(comment, t) | 39 | const threadParentComments = await VideoCommentModel.listThreadParentComments(comment, t) |
40 | const commentObject = comment.toActivityPubObject(threadParentComments) | 40 | const commentObject = comment.toActivityPubObject(threadParentComments) |
41 | 41 | ||
42 | const actorsInvolvedInVideo = await getActorsInvolvedInVideo(comment.Video, t) | 42 | const actorsInvolvedInComment = await getActorsInvolvedInVideo(comment.Video, t) |
43 | const audience = getOriginVideoCommentAudience(comment, threadParentComments, actorsInvolvedInVideo) | 43 | actorsInvolvedInComment.push(byActor) |
44 | const audience = getOriginVideoCommentAudience(comment, threadParentComments, actorsInvolvedInComment) | ||
44 | 45 | ||
45 | const data = await createActivityData(comment.url, byActor, commentObject, t, audience) | 46 | const data = await createActivityData(comment.url, byActor, commentObject, t, audience) |
46 | 47 | ||
48 | // This was a reply, send it to the parent actors | ||
49 | const actorsException = [ byActor ] | ||
50 | await broadcastToActors(data, byActor, threadParentComments.map(c => c.Account.Actor), t, actorsException) | ||
51 | |||
52 | // Broadcast to our followers | ||
53 | await broadcastToFollowers(data, byActor, [ byActor ], t) | ||
54 | |||
55 | // Send to origin | ||
47 | return unicastTo(data, byActor, comment.Video.VideoChannel.Account.Actor.sharedInboxUrl, t) | 56 | return unicastTo(data, byActor, comment.Video.VideoChannel.Account.Actor.sharedInboxUrl, t) |
48 | } | 57 | } |
49 | 58 | ||
@@ -52,12 +61,21 @@ async function sendCreateVideoCommentToVideoFollowers (comment: VideoCommentMode | |||
52 | const threadParentComments = await VideoCommentModel.listThreadParentComments(comment, t) | 61 | const threadParentComments = await VideoCommentModel.listThreadParentComments(comment, t) |
53 | const commentObject = comment.toActivityPubObject(threadParentComments) | 62 | const commentObject = comment.toActivityPubObject(threadParentComments) |
54 | 63 | ||
55 | const actorsInvolvedInVideo = await getActorsInvolvedInVideo(comment.Video, t) | 64 | const actorsInvolvedInComment = await getActorsInvolvedInVideo(comment.Video, t) |
56 | const audience = getOriginVideoCommentAudience(comment, threadParentComments, actorsInvolvedInVideo) | 65 | actorsInvolvedInComment.push(byActor) |
66 | |||
67 | const audience = getOriginVideoCommentAudience(comment, threadParentComments, actorsInvolvedInComment) | ||
57 | const data = await createActivityData(comment.url, byActor, commentObject, t, audience) | 68 | const data = await createActivityData(comment.url, byActor, commentObject, t, audience) |
58 | 69 | ||
59 | const followersException = [ byActor ] | 70 | // This was a reply, send it to the parent actors |
60 | return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, followersException) | 71 | const actorsException = [ byActor ] |
72 | await broadcastToActors(data, byActor, threadParentComments.map(c => c.Account.Actor), t, actorsException) | ||
73 | |||
74 | // Broadcast to our followers | ||
75 | await broadcastToFollowers(data, byActor, [ byActor ], t) | ||
76 | |||
77 | // Send to actors involved in the comment | ||
78 | return broadcastToFollowers(data, byActor, actorsInvolvedInComment, t, actorsException) | ||
61 | } | 79 | } |
62 | 80 | ||
63 | async function sendCreateViewToOrigin (byActor: ActorModel, video: VideoModel, t: Transaction) { | 81 | async function sendCreateViewToOrigin (byActor: ActorModel, video: VideoModel, t: Transaction) { |
@@ -81,8 +99,8 @@ async function sendCreateViewToVideoFollowers (byActor: ActorModel, video: Video | |||
81 | 99 | ||
82 | // Use the server actor to send the view | 100 | // Use the server actor to send the view |
83 | const serverActor = await getServerActor() | 101 | const serverActor = await getServerActor() |
84 | const followersException = [ byActor ] | 102 | const actorsException = [ byActor ] |
85 | return broadcastToFollowers(data, serverActor, actorsToForwardView, t, followersException) | 103 | return broadcastToFollowers(data, serverActor, actorsToForwardView, t, actorsException) |
86 | } | 104 | } |
87 | 105 | ||
88 | async function sendCreateDislikeToOrigin (byActor: ActorModel, video: VideoModel, t: Transaction) { | 106 | async function sendCreateDislikeToOrigin (byActor: ActorModel, video: VideoModel, t: Transaction) { |
@@ -104,8 +122,8 @@ async function sendCreateDislikeToVideoFollowers (byActor: ActorModel, video: Vi | |||
104 | const audience = getObjectFollowersAudience(actorsToForwardView) | 122 | const audience = getObjectFollowersAudience(actorsToForwardView) |
105 | const data = await createActivityData(url, byActor, dislikeActivityData, t, audience) | 123 | const data = await createActivityData(url, byActor, dislikeActivityData, t, audience) |
106 | 124 | ||
107 | const followersException = [ byActor ] | 125 | const actorsException = [ byActor ] |
108 | return broadcastToFollowers(data, byActor, actorsToForwardView, t, followersException) | 126 | return broadcastToFollowers(data, byActor, actorsToForwardView, t, actorsException) |
109 | } | 127 | } |
110 | 128 | ||
111 | async function createActivityData ( | 129 | async function createActivityData ( |
diff --git a/server/models/account/account.ts b/server/models/account/account.ts index c85d12824..47336d1e0 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts | |||
@@ -1,26 +1,15 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import { | 2 | import { |
3 | AfterDestroy, | 3 | AfterDestroy, AllowNull, BelongsTo, Column, CreatedAt, DefaultScope, ForeignKey, HasMany, Model, Table, |
4 | AllowNull, | ||
5 | BelongsTo, | ||
6 | Column, | ||
7 | CreatedAt, | ||
8 | DefaultScope, | ||
9 | ForeignKey, | ||
10 | HasMany, | ||
11 | Is, | ||
12 | Model, | ||
13 | Table, | ||
14 | UpdatedAt | 4 | UpdatedAt |
15 | } from 'sequelize-typescript' | 5 | } from 'sequelize-typescript' |
16 | import { Account } from '../../../shared/models/actors' | 6 | import { Account } from '../../../shared/models/actors' |
17 | import { isUserUsernameValid } from '../../helpers/custom-validators/users' | ||
18 | import { sendDeleteActor } from '../../lib/activitypub/send' | 7 | import { sendDeleteActor } from '../../lib/activitypub/send' |
19 | import { ActorModel } from '../activitypub/actor' | 8 | import { ActorModel } from '../activitypub/actor' |
20 | import { ApplicationModel } from '../application/application' | 9 | import { ApplicationModel } from '../application/application' |
21 | import { AvatarModel } from '../avatar/avatar' | 10 | import { AvatarModel } from '../avatar/avatar' |
22 | import { ServerModel } from '../server/server' | 11 | import { ServerModel } from '../server/server' |
23 | import { getSort, throwIfNotValid } from '../utils' | 12 | import { getSort } from '../utils' |
24 | import { VideoChannelModel } from '../video/video-channel' | 13 | import { VideoChannelModel } from '../video/video-channel' |
25 | import { UserModel } from './user' | 14 | import { UserModel } from './user' |
26 | 15 | ||