@import '_variables';
@import '_mixins';
+form {
+ margin-bottom: 30px;
+}
+
.avatar-and-textarea {
display: flex;
margin-bottom: 10px;
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'
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)
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)
}
})
}
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)
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 })
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)
}
// ---------------------------------------------------------------------------
getObjectFollowersAudience,
forwardActivity,
audiencify,
- getOriginVideoCommentAudience
+ getOriginVideoCommentAudience,
+ computeUris,
+ broadcastToActors
}
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'
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)
}
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) {
// 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) {
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 (
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'