Send comment to followers and parents
authorChocobozzz <me@florianbigard.com>
Mon, 8 Jan 2018 09:00:35 +0000 (10:00 +0100)
committerChocobozzz <me@florianbigard.com>
Mon, 8 Jan 2018 09:15:27 +0000 (10:15 +0100)
client/src/app/videos/+video-watch/comment/video-comment-add.component.scss
server/lib/activitypub/process/process-create.ts
server/lib/activitypub/send/misc.ts
server/lib/activitypub/send/send-create.ts
server/models/account/account.ts

index 37097da724db20cf795f73c66554530c75d995da..e586880fce11b30debc39d56b74f1a4d9aa603c0 100644 (file)
@@ -1,6 +1,10 @@
 @import '_variables';
 @import '_mixins';
 
+form {
+  margin-bottom: 30px;
+}
+
 .avatar-and-textarea {
   display: flex;
   margin-bottom: 10px;
index ffd20fe7433622aeaacac65381855c364e349f7c..a97f6ae83b440d82c913d985a2087d113ee990cb 100644 (file)
@@ -13,8 +13,9 @@ import { VideoModel } from '../../../models/video/video'
 import { VideoAbuseModel } from '../../../models/video/video-abuse'
 import { VideoCommentModel } from '../../../models/video/video-comment'
 import { VideoFileModel } from '../../../models/video/video-file'
+import { VideoShareModel } from '../../../models/video/video-share'
 import { getOrCreateActorAndServerAndModel } from '../actor'
-import { forwardActivity } from '../send/misc'
+import { forwardActivity, getActorsInvolvedInVideo } from '../send/misc'
 import { generateThumbnailFromUrl } from '../videos'
 import { addVideoComments, addVideoShares, videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc'
 
@@ -266,18 +267,19 @@ function createVideoComment (byActor: ActorModel, activity: ActivityCreate) {
   if (!byAccount) throw new Error('Cannot create video comment with the non account actor ' + byActor.url)
 
   return sequelizeTypescript.transaction(async t => {
-    let video = await VideoModel.loadByUrl(comment.inReplyTo, t)
+    let video = await VideoModel.loadByUrlAndPopulateAccount(comment.inReplyTo, t)
+    let objectToCreate
 
     // This is a new thread
     if (video) {
-      await VideoCommentModel.create({
+      objectToCreate = {
         url: comment.id,
         text: comment.content,
         originCommentId: null,
         inReplyToComment: null,
         videoId: video.id,
         accountId: byAccount.id
-      }, { transaction: t })
+      }
     } else {
       const inReplyToComment = await VideoCommentModel.loadByUrl(comment.inReplyTo, t)
       if (!inReplyToComment) throw new Error('Unknown replied comment ' + comment.inReplyTo)
@@ -285,20 +287,34 @@ function createVideoComment (byActor: ActorModel, activity: ActivityCreate) {
       video = await VideoModel.load(inReplyToComment.videoId)
 
       const originCommentId = inReplyToComment.originCommentId || inReplyToComment.id
-      await VideoCommentModel.create({
+      objectToCreate = {
         url: comment.id,
         text: comment.content,
         originCommentId,
         inReplyToCommentId: inReplyToComment.id,
         videoId: video.id,
         accountId: byAccount.id
-      }, { transaction: t })
+      }
     }
 
-    if (video.isOwned()) {
+    const options = {
+      where: {
+        url: objectToCreate.url
+      },
+      defaults: objectToCreate,
+      transaction: t
+    }
+    const [ ,created ] = await VideoCommentModel.findOrCreate(options)
+
+    if (video.isOwned() && created === true) {
       // Don't resend the activity to the sender
       const exceptions = [ byActor ]
-      await forwardActivity(activity, t, exceptions)
+
+      // Mastodon does not add our announces in audience, so we forward to them manually
+      const additionalActors = await getActorsInvolvedInVideo(video, t)
+      const additionalFollowerUrls = additionalActors.map(a => a.followersUrl)
+
+      await forwardActivity(activity, t, exceptions, additionalFollowerUrls)
     }
   })
 }
index 4aa514c150acef8fbcf45325d523b8e353e05c30..2a9f4cae868ac0bf0b8c8181d52433a326b9ad72 100644 (file)
@@ -12,12 +12,13 @@ import { activitypubHttpJobScheduler, ActivityPubHttpPayload } from '../../jobs/
 async function forwardActivity (
   activity: Activity,
   t: Transaction,
-  followersException: ActorModel[] = []
+  followersException: ActorModel[] = [],
+  additionalFollowerUrls: string[] = []
 ) {
   const to = activity.to || []
   const cc = activity.cc || []
 
-  const followersUrls: string[] = []
+  const followersUrls = additionalFollowerUrls
   for (const dest of to.concat(cc)) {
     if (dest.endsWith('/followers')) {
       followersUrls.push(dest)
@@ -47,13 +48,25 @@ async function broadcastToFollowers (
   byActor: ActorModel,
   toActorFollowers: ActorModel[],
   t: Transaction,
-  followersException: ActorModel[] = []
+  actorsException: ActorModel[] = []
 ) {
-  const uris = await computeFollowerUris(toActorFollowers, followersException, t)
-  if (uris.length === 0) {
-    logger.info('0 followers for %s, no broadcasting.', toActorFollowers.map(a => a.id).join(', '))
-    return undefined
-  }
+  const uris = await computeFollowerUris(toActorFollowers, actorsException, t)
+  return broadcastTo(uris, data, byActor, t)
+}
+
+async function broadcastToActors (
+  data: any,
+  byActor: ActorModel,
+  toActors: ActorModel[],
+  t: Transaction,
+  actorsException: ActorModel[] = []
+) {
+  const uris = await computeUris(toActors, actorsException)
+  return broadcastTo(uris, data, byActor, t)
+}
+
+async function broadcastTo (uris: string[], data: any, byActor: ActorModel, t: Transaction) {
+  if (uris.length === 0) return undefined
 
   logger.debug('Creating broadcast job.', { uris })
 
@@ -149,12 +162,20 @@ function audiencify (object: any, audience: ActivityAudience) {
   return Object.assign(object, audience)
 }
 
-async function computeFollowerUris (toActorFollower: ActorModel[], followersException: ActorModel[], t: Transaction) {
+async function computeFollowerUris (toActorFollower: ActorModel[], actorsException: ActorModel[], t: Transaction) {
   const toActorFollowerIds = toActorFollower.map(a => a.id)
 
   const result = await ActorFollowModel.listAcceptedFollowerSharedInboxUrls(toActorFollowerIds, t)
-  const followersSharedInboxException = followersException.map(f => f.sharedInboxUrl)
-  return result.data.filter(sharedInbox => followersSharedInboxException.indexOf(sharedInbox) === -1)
+  const sharedInboxesException = actorsException.map(f => f.sharedInboxUrl)
+  return result.data.filter(sharedInbox => sharedInboxesException.indexOf(sharedInbox) === -1)
+}
+
+async function computeUris (toActors: ActorModel[], actorsException: ActorModel[] = []) {
+  const toActorSharedInboxesSet = new Set(toActors.map(a => a.sharedInboxUrl))
+
+  const sharedInboxesException = actorsException.map(f => f.sharedInboxUrl)
+  return Array.from(toActorSharedInboxesSet)
+    .filter(sharedInbox => sharedInboxesException.indexOf(sharedInbox) === -1)
 }
 
 // ---------------------------------------------------------------------------
@@ -168,5 +189,7 @@ export {
   getObjectFollowersAudience,
   forwardActivity,
   audiencify,
-  getOriginVideoCommentAudience
+  getOriginVideoCommentAudience,
+  computeUris,
+  broadcastToActors
 }
index e2ee639d9cea3e378ebbda95d1b3e4081c31138e..9db663be198c78a63c4494cc252697925b7a32f6 100644 (file)
@@ -8,7 +8,7 @@ import { VideoAbuseModel } from '../../../models/video/video-abuse'
 import { VideoCommentModel } from '../../../models/video/video-comment'
 import { getVideoAbuseActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoViewActivityPubUrl } from '../url'
 import {
-  audiencify, broadcastToFollowers, getActorsInvolvedInVideo, getAudience, getObjectFollowersAudience,
+  audiencify, broadcastToActors, broadcastToFollowers, getActorsInvolvedInVideo, getAudience, getObjectFollowersAudience,
   getOriginVideoAudience, getOriginVideoCommentAudience,
   unicastTo
 } from './misc'
@@ -39,11 +39,20 @@ async function sendCreateVideoCommentToOrigin (comment: VideoCommentModel, t: Tr
   const threadParentComments = await VideoCommentModel.listThreadParentComments(comment, t)
   const commentObject = comment.toActivityPubObject(threadParentComments)
 
-  const actorsInvolvedInVideo = await getActorsInvolvedInVideo(comment.Video, t)
-  const audience = getOriginVideoCommentAudience(comment, threadParentComments, actorsInvolvedInVideo)
+  const actorsInvolvedInComment = await getActorsInvolvedInVideo(comment.Video, t)
+  actorsInvolvedInComment.push(byActor)
+  const audience = getOriginVideoCommentAudience(comment, threadParentComments, actorsInvolvedInComment)
 
   const data = await createActivityData(comment.url, byActor, commentObject, t, audience)
 
+  // This was a reply, send it to the parent actors
+  const actorsException = [ byActor ]
+  await broadcastToActors(data, byActor, threadParentComments.map(c => c.Account.Actor), t, actorsException)
+
+  // Broadcast to our followers
+  await broadcastToFollowers(data, byActor, [ byActor ], t)
+
+  // Send to origin
   return unicastTo(data, byActor, comment.Video.VideoChannel.Account.Actor.sharedInboxUrl, t)
 }
 
@@ -52,12 +61,21 @@ async function sendCreateVideoCommentToVideoFollowers (comment: VideoCommentMode
   const threadParentComments = await VideoCommentModel.listThreadParentComments(comment, t)
   const commentObject = comment.toActivityPubObject(threadParentComments)
 
-  const actorsInvolvedInVideo = await getActorsInvolvedInVideo(comment.Video, t)
-  const audience = getOriginVideoCommentAudience(comment, threadParentComments, actorsInvolvedInVideo)
+  const actorsInvolvedInComment = await getActorsInvolvedInVideo(comment.Video, t)
+  actorsInvolvedInComment.push(byActor)
+
+  const audience = getOriginVideoCommentAudience(comment, threadParentComments, actorsInvolvedInComment)
   const data = await createActivityData(comment.url, byActor, commentObject, t, audience)
 
-  const followersException = [ byActor ]
-  return broadcastToFollowers(data, byActor, actorsInvolvedInVideo, t, followersException)
+  // This was a reply, send it to the parent actors
+  const actorsException = [ byActor ]
+  await broadcastToActors(data, byActor, threadParentComments.map(c => c.Account.Actor), t, actorsException)
+
+  // Broadcast to our followers
+  await broadcastToFollowers(data, byActor, [ byActor ], t)
+
+  // Send to actors involved in the comment
+  return broadcastToFollowers(data, byActor, actorsInvolvedInComment, t, actorsException)
 }
 
 async function sendCreateViewToOrigin (byActor: ActorModel, video: VideoModel, t: Transaction) {
@@ -81,8 +99,8 @@ async function sendCreateViewToVideoFollowers (byActor: ActorModel, video: Video
 
   // Use the server actor to send the view
   const serverActor = await getServerActor()
-  const followersException = [ byActor ]
-  return broadcastToFollowers(data, serverActor, actorsToForwardView, t, followersException)
+  const actorsException = [ byActor ]
+  return broadcastToFollowers(data, serverActor, actorsToForwardView, t, actorsException)
 }
 
 async function sendCreateDislikeToOrigin (byActor: ActorModel, video: VideoModel, t: Transaction) {
@@ -104,8 +122,8 @@ async function sendCreateDislikeToVideoFollowers (byActor: ActorModel, video: Vi
   const audience = getObjectFollowersAudience(actorsToForwardView)
   const data = await createActivityData(url, byActor, dislikeActivityData, t, audience)
 
-  const followersException = [ byActor ]
-  return broadcastToFollowers(data, byActor, actorsToForwardView, t, followersException)
+  const actorsException = [ byActor ]
+  return broadcastToFollowers(data, byActor, actorsToForwardView, t, actorsException)
 }
 
 async function createActivityData (
index c85d12824d5394ee04fb4d135d01201ccd095f4e..47336d1e017af9c1cbd82233872fb1ec74b543a5 100644 (file)
@@ -1,26 +1,15 @@
 import * as Sequelize from 'sequelize'
 import {
-  AfterDestroy,
-  AllowNull,
-  BelongsTo,
-  Column,
-  CreatedAt,
-  DefaultScope,
-  ForeignKey,
-  HasMany,
-  Is,
-  Model,
-  Table,
+  AfterDestroy, AllowNull, BelongsTo, Column, CreatedAt, DefaultScope, ForeignKey, HasMany, Model, Table,
   UpdatedAt
 } from 'sequelize-typescript'
 import { Account } from '../../../shared/models/actors'
-import { isUserUsernameValid } from '../../helpers/custom-validators/users'
 import { sendDeleteActor } from '../../lib/activitypub/send'
 import { ActorModel } from '../activitypub/actor'
 import { ApplicationModel } from '../application/application'
 import { AvatarModel } from '../avatar/avatar'
 import { ServerModel } from '../server/server'
-import { getSort, throwIfNotValid } from '../utils'
+import { getSort } from '../utils'
 import { VideoChannelModel } from '../video/video-channel'
 import { UserModel } from './user'