Fix federation with some actors
authorChocobozzz <me@florianbigard.com>
Wed, 23 Oct 2019 09:33:53 +0000 (11:33 +0200)
committerChocobozzz <me@florianbigard.com>
Wed, 23 Oct 2019 09:33:53 +0000 (11:33 +0200)
That don't have a shared inbox, or a URL

13 files changed:
server/controllers/activitypub/inbox.ts
server/helpers/custom-validators/activitypub/actor.ts
server/initializers/constants.ts
server/initializers/migrations/0445-shared-inbox-optional.ts [new file with mode: 0644]
server/lib/activitypub/actor.ts
server/lib/activitypub/send/send-create.ts
server/lib/activitypub/send/send-delete.ts
server/lib/activitypub/send/send-flag.ts
server/lib/activitypub/send/utils.ts
server/models/activitypub/actor-follow.ts
server/models/activitypub/actor.ts
server/typings/models/account/actor.ts
shared/models/activitypub/activitypub-actor.ts

index d9df253aa28f05bece936b5f8347016b15992bf8..ca42106b8b27ba19d194b73d3330540cfc3ffeb6 100644 (file)
@@ -6,7 +6,6 @@ import { processActivities } from '../../lib/activitypub/process/process'
 import { asyncMiddleware, checkSignature, localAccountValidator, localVideoChannelValidator, signatureValidator } from '../../middlewares'
 import { activityPubValidator } from '../../middlewares/validators/activitypub/activity'
 import { queue } from 'async'
-import { ActorModel } from '../../models/activitypub/actor'
 import { MActorDefault, MActorSignature } from '../../typings/models'
 
 const inboxRouter = express.Router()
index 4e9aabf0ed6f722599aad044ffa3fb538fb8acfa..ec4dd6f96a151c38983d43f507d0818bff6492b4 100644 (file)
@@ -6,7 +6,12 @@ import { isHostValid } from '../servers'
 import { peertubeTruncate } from '@server/helpers/core-utils'
 
 function isActorEndpointsObjectValid (endpointObject: any) {
-  return isActivityPubUrlValid(endpointObject.sharedInbox)
+  if (endpointObject && endpointObject.sharedInbox) {
+    return isActivityPubUrlValid(endpointObject.sharedInbox)
+  }
+
+  // Shared inbox is optional
+  return true
 }
 
 function isActorPublicKeyObjectValid (publicKeyObject: any) {
@@ -16,7 +21,7 @@ function isActorPublicKeyObjectValid (publicKeyObject: any) {
 }
 
 function isActorTypeValid (type: string) {
-  return type === 'Person' || type === 'Application' || type === 'Group'
+  return type === 'Person' || type === 'Application' || type === 'Group' || type === 'Service' || type === 'Organization'
 }
 
 function isActorPublicKeyValid (publicKey: string) {
@@ -81,9 +86,11 @@ function sanitizeAndCheckActorObject (object: any) {
 }
 
 function normalizeActor (actor: any) {
-  if (!actor || !actor.url) return
+  if (!actor) return
 
-  if (typeof actor.url !== 'string') {
+  if (!actor.url) {
+    actor.url = actor.id
+  } else if (typeof actor.url !== 'string') {
     actor.url = actor.url.href || actor.url.url
   }
 
index 10f95f5abf2b0038a3d139a8912bbcaf2202b0cd..190fd427aa00020d63c4fcfa329e4ea65e5bc0bf 100644 (file)
@@ -14,7 +14,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
 
 // ---------------------------------------------------------------------------
 
-const LAST_MIGRATION_VERSION = 440
+const LAST_MIGRATION_VERSION = 445
 
 // ---------------------------------------------------------------------------
 
@@ -459,7 +459,9 @@ const ACTIVITY_PUB = {
 const ACTIVITY_PUB_ACTOR_TYPES: { [ id: string ]: ActivityPubActorType } = {
   GROUP: 'Group',
   PERSON: 'Person',
-  APPLICATION: 'Application'
+  APPLICATION: 'Application',
+  ORGANIZATION: 'Organization',
+  SERVICE: 'Service'
 }
 
 const HTTP_SIGNATURE = {
diff --git a/server/initializers/migrations/0445-shared-inbox-optional.ts b/server/initializers/migrations/0445-shared-inbox-optional.ts
new file mode 100644 (file)
index 0000000..dad2d65
--- /dev/null
@@ -0,0 +1,26 @@
+import * as Sequelize from 'sequelize'
+
+async function up (utils: {
+  transaction: Sequelize.Transaction,
+  queryInterface: Sequelize.QueryInterface,
+  sequelize: Sequelize.Sequelize,
+  db: any
+}): Promise<void> {
+  {
+    const data = {
+      type: Sequelize.STRING,
+      allowNull: true
+    }
+
+    await utils.queryInterface.changeColumn('actor', 'sharedInboxUrl', data)
+  }
+}
+
+function down (options) {
+  throw new Error('Not implemented.')
+}
+
+export {
+  up,
+  down
+}
index 13b73077e8ca6d6e757f09d92ac264fb3f5d97ec..cad9af5e0c32f29ed65c85c3a081f6ee206b5f8b 100644 (file)
@@ -163,9 +163,12 @@ async function updateActorInstance (actorInstance: ActorModel, attributes: Activ
   actorInstance.followingCount = followingCount
   actorInstance.inboxUrl = attributes.inbox
   actorInstance.outboxUrl = attributes.outbox
-  actorInstance.sharedInboxUrl = attributes.endpoints.sharedInbox
   actorInstance.followersUrl = attributes.followers
   actorInstance.followingUrl = attributes.following
+
+  if (attributes.endpoints && attributes.endpoints.sharedInbox) {
+    actorInstance.sharedInboxUrl = attributes.endpoints.sharedInbox
+  }
 }
 
 type AvatarInfo = { name: string, onDisk: boolean, fileUrl: string }
@@ -437,9 +440,12 @@ async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: numbe
     followingCount: followingCount,
     inboxUrl: actorJSON.inbox,
     outboxUrl: actorJSON.outbox,
-    sharedInboxUrl: actorJSON.endpoints.sharedInbox,
     followersUrl: actorJSON.followers,
-    followingUrl: actorJSON.following
+    followingUrl: actorJSON.following,
+
+    sharedInboxUrl: actorJSON.endpoints && actorJSON.endpoints.sharedInbox
+      ? actorJSON.endpoints.sharedInbox
+      : null,
   })
 
   const avatarInfo = await getAvatarInfoIfExists(actorJSON)
index 26ec3e94873ea619796754fd5ffc50ce3525bb9e..edbc14a73e73fb66466bb5405ab0ad47d9879bad 100644 (file)
@@ -100,7 +100,7 @@ async function sendCreateVideoComment (comment: MCommentOwnerVideo, t: Transacti
   if (isOrigin) return broadcastToFollowers(createActivity, byActor, actorsInvolvedInComment, t, actorsException)
 
   // Send to origin
-  t.afterCommit(() => unicastTo(createActivity, byActor, comment.Video.VideoChannel.Account.Actor.sharedInboxUrl))
+  t.afterCommit(() => unicastTo(createActivity, byActor, comment.Video.VideoChannel.Account.Actor.getSharedInbox()))
 }
 
 function buildCreateActivity (url: string, byActor: MActorLight, object: any, audience?: ActivityAudience): ActivityCreate {
index 4b1ff8dc5739345aff971016c31e2010563bca02..a91756ff4577084c156c29b807891823a2af3931 100644 (file)
@@ -71,7 +71,7 @@ async function sendDeleteVideoComment (videoComment: MCommentOwnerVideoReply, t:
   if (isVideoOrigin) return broadcastToFollowers(activity, byActor, actorsInvolvedInComment, t, actorsException)
 
   // Send to origin
-  t.afterCommit(() => unicastTo(activity, byActor, videoComment.Video.VideoChannel.Account.Actor.sharedInboxUrl))
+  t.afterCommit(() => unicastTo(activity, byActor, videoComment.Video.VideoChannel.Account.Actor.getSharedInbox()))
 }
 
 async function sendDeleteVideoPlaylist (videoPlaylist: MVideoPlaylistFullSummary, t: Transaction) {
index 5ae1614ab63d8c5f6467249bef01efc6617c91f6..da7638a7bb04d6640880b3db4c9a30b6f7c55b43 100644 (file)
@@ -18,7 +18,7 @@ async function sendVideoAbuse (byActor: MActor, videoAbuse: MVideoAbuseVideo, vi
   const audience = { to: [ video.VideoChannel.Account.Actor.url ], cc: [] }
   const flagActivity = buildFlagActivity(url, byActor, videoAbuse, audience)
 
-  t.afterCommit(() => unicastTo(flagActivity, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl))
+  t.afterCommit(() => unicastTo(flagActivity, byActor, video.VideoChannel.Account.Actor.getSharedInbox()))
 }
 
 function buildFlagActivity (url: string, byActor: MActor, videoAbuse: MVideoAbuseVideo, audience: ActivityAudience): ActivityFlag {
index 8129ab32a8f617310b5b0ffcebd3722e1e6d410d..77b7234796b6c89c55d444ffebb7cb649bd4055c 100644 (file)
@@ -7,7 +7,7 @@ import { JobQueue } from '../../job-queue'
 import { getActorsInvolvedInVideo, getAudienceFromFollowersOf, getRemoteVideoAudience } from '../audience'
 import { getServerActor } from '../../../helpers/utils'
 import { afterCommitIfTransaction } from '../../../helpers/database-utils'
-import { MActorFollowerException, MActor, MActorId, MActorLight, MVideo, MVideoAccountLight } from '../../../typings/models'
+import { MActorWithInboxes, MActor, MActorId, MActorLight, MVideo, MVideoAccountLight } from '../../../typings/models'
 
 async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAudience) => Activity, options: {
   byActor: MActorLight,
@@ -24,7 +24,7 @@ async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAud
     const activity = activityBuilder(audience)
 
     return afterCommitIfTransaction(transaction, () => {
-      return unicastTo(activity, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl)
+      return unicastTo(activity, byActor, video.VideoChannel.Account.Actor.getSharedInbox())
     })
   }
 
@@ -40,7 +40,7 @@ async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAud
 async function forwardVideoRelatedActivity (
   activity: Activity,
   t: Transaction,
-  followersException: MActorFollowerException[] = [],
+  followersException: MActorWithInboxes[] = [],
   video: MVideo
 ) {
   // Mastodon does not add our announces in audience, so we forward to them manually
@@ -53,7 +53,7 @@ async function forwardVideoRelatedActivity (
 async function forwardActivity (
   activity: Activity,
   t: Transaction,
-  followersException: MActorFollowerException[] = [],
+  followersException: MActorWithInboxes[] = [],
   additionalFollowerUrls: string[] = []
 ) {
   logger.info('Forwarding activity %s.', activity.id)
@@ -90,7 +90,7 @@ async function broadcastToFollowers (
   byActor: MActorId,
   toFollowersOf: MActorId[],
   t: Transaction,
-  actorsException: MActorFollowerException[] = []
+  actorsException: MActorWithInboxes[] = []
 ) {
   const uris = await computeFollowerUris(toFollowersOf, actorsException, t)
 
@@ -102,7 +102,7 @@ async function broadcastToActors (
   byActor: MActorId,
   toActors: MActor[],
   t?: Transaction,
-  actorsException: MActorFollowerException[] = []
+  actorsException: MActorWithInboxes[] = []
 ) {
   const uris = await computeUris(toActors, actorsException)
   return afterCommitIfTransaction(t, () => broadcastTo(uris, data, byActor))
@@ -147,7 +147,7 @@ export {
 
 // ---------------------------------------------------------------------------
 
-async function computeFollowerUris (toFollowersOf: MActorId[], actorsException: MActorFollowerException[], t: Transaction) {
+async function computeFollowerUris (toFollowersOf: MActorId[], actorsException: MActorWithInboxes[], t: Transaction) {
   const toActorFollowerIds = toFollowersOf.map(a => a.id)
 
   const result = await ActorFollowModel.listAcceptedFollowerSharedInboxUrls(toActorFollowerIds, t)
@@ -156,11 +156,11 @@ async function computeFollowerUris (toFollowersOf: MActorId[], actorsException:
   return result.data.filter(sharedInbox => sharedInboxesException.indexOf(sharedInbox) === -1)
 }
 
-async function computeUris (toActors: MActor[], actorsException: MActorFollowerException[] = []) {
+async function computeUris (toActors: MActor[], actorsException: MActorWithInboxes[] = []) {
   const serverActor = await getServerActor()
   const targetUrls = toActors
     .filter(a => a.id !== serverActor.id) // Don't send to ourselves
-    .map(a => a.sharedInboxUrl || a.inboxUrl)
+    .map(a => a.getSharedInbox())
 
   const toActorSharedInboxesSet = new Set(targetUrls)
 
@@ -169,10 +169,10 @@ async function computeUris (toActors: MActor[], actorsException: MActorFollowerE
               .filter(sharedInbox => sharedInboxesException.indexOf(sharedInbox) === -1)
 }
 
-async function buildSharedInboxesException (actorsException: MActorFollowerException[]) {
+async function buildSharedInboxesException (actorsException: MActorWithInboxes[]) {
   const serverActor = await getServerActor()
 
   return actorsException
-    .map(f => f.sharedInboxUrl || f.inboxUrl)
+    .map(f => f.getSharedInbox())
     .concat([ serverActor.sharedInboxUrl ])
 }
index 8498692f0fcba3b049ebf03782673b36445fd489..fb3c4ef9d82cb94fd75280a7aeedc0b95b35eb0c 100644 (file)
@@ -574,8 +574,8 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
     }
 
     const selections: string[] = []
-    if (distinct === true) selections.push('DISTINCT("Follows"."' + columnUrl + '") AS "url"')
-    else selections.push('"Follows"."' + columnUrl + '" AS "url"')
+    if (distinct === true) selections.push('DISTINCT("Follows"."' + columnUrl + '") AS "selectionUrl"')
+    else selections.push('"Follows"."' + columnUrl + '" AS "selectionUrl"')
 
     selections.push('COUNT(*) AS "total"')
 
@@ -585,7 +585,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
       let query = 'SELECT ' + selection + ' FROM "actor" ' +
         'INNER JOIN "actorFollow" ON "actorFollow"."' + firstJoin + '" = "actor"."id" ' +
         'INNER JOIN "actor" AS "Follows" ON "actorFollow"."' + secondJoin + '" = "Follows"."id" ' +
-        'WHERE "actor"."id" = ANY ($actorIds) AND "actorFollow"."state" = \'accepted\' '
+        'WHERE "actor"."id" = ANY ($actorIds) AND "actorFollow"."state" = \'accepted\' AND "selectionUrl" IS NOT NULL '
 
       if (count !== undefined) query += 'LIMIT ' + count
       if (start !== undefined) query += ' OFFSET ' + start
@@ -599,7 +599,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
     }
 
     const [ followers, [ dataTotal ] ] = await Promise.all(tasks)
-    const urls: string[] = followers.map(f => f.url)
+    const urls: string[] = followers.map(f => f.selectionUrl)
 
     return {
       data: urls,
index 535ebd79200d26fabcd10bf7dc3fed19d15f9db5..42a24b58380ed2aa0ae00dfc5b96f4a58bfece44 100644 (file)
@@ -44,7 +44,8 @@ import {
   MActorFull,
   MActorHost,
   MActorServer,
-  MActorSummaryFormattable
+  MActorSummaryFormattable,
+  MActorWithInboxes
 } from '../../typings/models'
 import * as Bluebird from 'bluebird'
 
@@ -179,8 +180,8 @@ export class ActorModel extends Model<ActorModel> {
   @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
   outboxUrl: string
 
-  @AllowNull(false)
-  @Is('ActorSharedInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'shared inbox url'))
+  @AllowNull(true)
+  @Is('ActorSharedInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'shared inbox url', true))
   @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
   sharedInboxUrl: string
 
@@ -402,6 +403,10 @@ export class ActorModel extends Model<ActorModel> {
     })
   }
 
+  getSharedInbox (this: MActorWithInboxes) {
+    return this.sharedInboxUrl || this.inboxUrl
+  }
+
   toFormattedSummaryJSON (this: MActorSummaryFormattable) {
     let avatar: Avatar = null
     if (this.Avatar) {
index bcacb83516680d4c1107573aa0bef8d8e72f4a14..ee4ece755b1fbd52ccc75245fc2d1b2d1f2574d4 100644 (file)
@@ -19,7 +19,7 @@ export type MActorUsername = Pick<MActor, 'preferredUsername'>
 
 export type MActorFollowersUrl = Pick<MActor, 'followersUrl'>
 export type MActorAudience = MActorUrl & MActorFollowersUrl
-export type MActorFollowerException = Pick<ActorModel, 'sharedInboxUrl' | 'inboxUrl'>
+export type MActorWithInboxes = Pick<ActorModel, 'sharedInboxUrl' | 'inboxUrl' | 'getSharedInbox'>
 export type MActorSignature = MActorAccountChannelId
 
 export type MActorLight = Omit<MActor, 'privateKey' | 'privateKey'>
index 53ec579bcccafdac02815e105ea890362ea789eb..b8a2dc92515c2fb0d3f68eb5a6ec1d1e0ee89a5c 100644 (file)
@@ -1,6 +1,6 @@
 import { ActivityPubAttributedTo } from './objects/common-objects'
 
-export type ActivityPubActorType = 'Person' | 'Application' | 'Group'
+export type ActivityPubActorType = 'Person' | 'Application' | 'Group' | 'Service' | 'Organization'
 
 export interface ActivityPubActor {
   '@context': any[]