Status are sent to mastodon

This commit is contained in:
Chocobozzz 2017-12-19 10:34:56 +01:00
parent ce33ee01cd
commit e12a009254
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
20 changed files with 133 additions and 111 deletions

View File

@ -1,8 +1,8 @@
import * as validator from 'validator' import * as validator from 'validator'
import { CONSTRAINTS_FIELDS } from '../../../initializers' import { CONSTRAINTS_FIELDS } from '../../../initializers'
import { isAccountNameValid } from '../accounts' import { isAccountNameValid } from '../accounts'
import { exists, isUUIDValid } from '../misc' import { exists } from '../misc'
import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../video-channels' import { isVideoChannelNameValid } from '../video-channels'
import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc'
function isActorEndpointsObjectValid (endpointObject: any) { function isActorEndpointsObjectValid (endpointObject: any) {
@ -23,41 +23,39 @@ function isActorPublicKeyValid (publicKey: string) {
return exists(publicKey) && return exists(publicKey) &&
typeof publicKey === 'string' && typeof publicKey === 'string' &&
publicKey.startsWith('-----BEGIN PUBLIC KEY-----') && publicKey.startsWith('-----BEGIN PUBLIC KEY-----') &&
publicKey.endsWith('-----END PUBLIC KEY-----') && publicKey.indexOf('-----END PUBLIC KEY-----') !== -1 &&
validator.isLength(publicKey, CONSTRAINTS_FIELDS.ACTOR.PUBLIC_KEY) validator.isLength(publicKey, CONSTRAINTS_FIELDS.ACTOR.PUBLIC_KEY)
} }
const actorNameRegExp = new RegExp('[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_]+')
function isActorPreferredUsernameValid (preferredUsername: string) { function isActorPreferredUsernameValid (preferredUsername: string) {
return isAccountNameValid(preferredUsername) || isVideoChannelNameValid(preferredUsername) return exists(preferredUsername) && validator.matches(preferredUsername, actorNameRegExp)
} }
const actorNameRegExp = new RegExp('[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_]+')
function isActorNameValid (name: string) { function isActorNameValid (name: string) {
return exists(name) && validator.matches(name, actorNameRegExp) return isAccountNameValid(name) || isVideoChannelNameValid(name)
} }
function isActorPrivateKeyValid (privateKey: string) { function isActorPrivateKeyValid (privateKey: string) {
return exists(privateKey) && return exists(privateKey) &&
typeof privateKey === 'string' && typeof privateKey === 'string' &&
privateKey.startsWith('-----BEGIN RSA PRIVATE KEY-----') && privateKey.startsWith('-----BEGIN RSA PRIVATE KEY-----') &&
privateKey.endsWith('-----END RSA PRIVATE KEY-----') && // Sometimes there is a \n at the end, so just assert the string contains the end mark
privateKey.indexOf('-----END RSA PRIVATE KEY-----') !== -1 &&
validator.isLength(privateKey, CONSTRAINTS_FIELDS.ACTOR.PRIVATE_KEY) validator.isLength(privateKey, CONSTRAINTS_FIELDS.ACTOR.PRIVATE_KEY)
} }
function isRemoteActorValid (remoteActor: any) { function isRemoteActorValid (remoteActor: any) {
return isActivityPubUrlValid(remoteActor.id) && return isActivityPubUrlValid(remoteActor.id) &&
isUUIDValid(remoteActor.uuid) &&
isActorTypeValid(remoteActor.type) && isActorTypeValid(remoteActor.type) &&
isActivityPubUrlValid(remoteActor.following) && isActivityPubUrlValid(remoteActor.following) &&
isActivityPubUrlValid(remoteActor.followers) && isActivityPubUrlValid(remoteActor.followers) &&
isActivityPubUrlValid(remoteActor.inbox) && isActivityPubUrlValid(remoteActor.inbox) &&
isActivityPubUrlValid(remoteActor.outbox) && isActivityPubUrlValid(remoteActor.outbox) &&
isActorNameValid(remoteActor.name) &&
isActorPreferredUsernameValid(remoteActor.preferredUsername) && isActorPreferredUsernameValid(remoteActor.preferredUsername) &&
isActivityPubUrlValid(remoteActor.url) && isActivityPubUrlValid(remoteActor.url) &&
isActorPublicKeyObjectValid(remoteActor.publicKey) && isActorPublicKeyObjectValid(remoteActor.publicKey) &&
isActorEndpointsObjectValid(remoteActor.endpoints) && isActorEndpointsObjectValid(remoteActor.endpoints) &&
(!remoteActor.summary || isVideoChannelDescriptionValid(remoteActor.summary)) &&
setValidAttributedTo(remoteActor) && setValidAttributedTo(remoteActor) &&
// If this is not an account, it should be attributed to an account // If this is not an account, it should be attributed to an account
// In PeerTube we use this to attach a video channel to a specific account // In PeerTube we use this to attach a video channel to a specific account

View File

@ -316,7 +316,7 @@ const CACHE = {
} }
} }
const ACCEPT_HEADERS = ACTIVITY_PUB.POTENTIAL_ACCEPT_HEADERS.concat('html', 'application/json') const ACCEPT_HEADERS = [ 'html', 'application/json' ].concat(ACTIVITY_PUB.POTENTIAL_ACCEPT_HEADERS)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -22,7 +22,7 @@ async function up (utils: {
id integer NOT NULL, id integer NOT NULL,
type enum_actor_type NOT NULL, type enum_actor_type NOT NULL,
uuid uuid NOT NULL, uuid uuid NOT NULL,
name character varying(255) NOT NULL, "preferredUsername" character varying(255) NOT NULL,
url character varying(2000) NOT NULL, url character varying(2000) NOT NULL,
"publicKey" character varying(5000), "publicKey" character varying(5000),
"privateKey" character varying(5000), "privateKey" character varying(5000),
@ -50,7 +50,7 @@ async function up (utils: {
`ALTER SEQUENCE actor_id_seq OWNED BY actor.id`, `ALTER SEQUENCE actor_id_seq OWNED BY actor.id`,
`ALTER TABLE ONLY actor ALTER COLUMN id SET DEFAULT nextval('actor_id_seq'::regclass)`, `ALTER TABLE ONLY actor ALTER COLUMN id SET DEFAULT nextval('actor_id_seq'::regclass)`,
`ALTER TABLE ONLY actor ADD CONSTRAINT actor_pkey PRIMARY KEY (id);`, `ALTER TABLE ONLY actor ADD CONSTRAINT actor_pkey PRIMARY KEY (id);`,
`CREATE UNIQUE INDEX actor_name_server_id ON actor USING btree (name, "serverId")`, `CREATE UNIQUE INDEX actor_preferred_username_server_id ON actor USING btree ("preferredUsername", "serverId")`,
`ALTER TABLE ONLY actor `ALTER TABLE ONLY actor
ADD CONSTRAINT "actor_avatarId_fkey" FOREIGN KEY ("avatarId") REFERENCES avatar(id) ON UPDATE CASCADE ON DELETE CASCADE`, ADD CONSTRAINT "actor_avatarId_fkey" FOREIGN KEY ("avatarId") REFERENCES avatar(id) ON UPDATE CASCADE ON DELETE CASCADE`,
`ALTER TABLE ONLY actor `ALTER TABLE ONLY actor
@ -68,7 +68,7 @@ async function up (utils: {
` `
INSERT INTO "actor" INSERT INTO "actor"
( (
type, uuid, name, url, "publicKey", "privateKey", "followersCount", "followingCount", "inboxUrl", "outboxUrl", type, uuid, "preferredUsername", url, "publicKey", "privateKey", "followersCount", "followingCount", "inboxUrl", "outboxUrl",
"sharedInboxUrl", "followersUrl", "followingUrl", "avatarId", "serverId", "createdAt", "updatedAt" "sharedInboxUrl", "followersUrl", "followingUrl", "avatarId", "serverId", "createdAt", "updatedAt"
) )
SELECT SELECT
@ -83,7 +83,7 @@ async function up (utils: {
` `
INSERT INTO "actor" INSERT INTO "actor"
( (
type, uuid, name, url, "publicKey", "privateKey", "followersCount", "followingCount", "inboxUrl", "outboxUrl", type, uuid, "preferredUsername", url, "publicKey", "privateKey", "followersCount", "followingCount", "inboxUrl", "outboxUrl",
"sharedInboxUrl", "followersUrl", "followingUrl", "avatarId", "serverId", "createdAt", "updatedAt" "sharedInboxUrl", "followersUrl", "followingUrl", "avatarId", "serverId", "createdAt", "updatedAt"
) )
SELECT SELECT
@ -119,7 +119,7 @@ async function up (utils: {
const query = ` const query = `
INSERT INTO actor INSERT INTO actor
( (
type, uuid, name, url, "publicKey", "privateKey", "followersCount", "followingCount", "inboxUrl", "outboxUrl", type, uuid, "preferredUsername", url, "publicKey", "privateKey", "followersCount", "followingCount", "inboxUrl", "outboxUrl",
"sharedInboxUrl", "followersUrl", "followingUrl", "avatarId", "serverId", "createdAt", "updatedAt" "sharedInboxUrl", "followersUrl", "followingUrl", "avatarId", "serverId", "createdAt", "updatedAt"
) )
SELECT SELECT

View File

@ -11,7 +11,7 @@ import { ActorModel } from '../../models/activitypub/actor'
import { ServerModel } from '../../models/server/server' import { ServerModel } from '../../models/server/server'
import { VideoChannelModel } from '../../models/video/video-channel' import { VideoChannelModel } from '../../models/video/video-channel'
// Set account keys, this could be long so process after the account creation and do not block the client // Set account keys, this could be long so process after the account creation and do not block the client
function setAsyncActorKeys (actor: ActorModel) { function setAsyncActorKeys (actor: ActorModel) {
return createPrivateAndPublicKeys() return createPrivateAndPublicKeys()
.then(({ publicKey, privateKey }) => { .then(({ publicKey, privateKey }) => {
@ -107,7 +107,7 @@ function saveActorAndServerAndModelIfNotExist (
type FetchRemoteActorResult = { type FetchRemoteActorResult = {
actor: ActorModel actor: ActorModel
preferredUsername: string name: string
summary: string summary: string
attributedTo: ActivityPubAttributedTo[] attributedTo: ActivityPubAttributedTo[]
} }
@ -142,8 +142,8 @@ async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResu
const actor = new ActorModel({ const actor = new ActorModel({
type: actorJSON.type, type: actorJSON.type,
uuid: actorJSON.uuid, uuid: actorJSON.uuid,
name: actorJSON.name, preferredUsername: actorJSON.preferredUsername,
url: actorJSON.url, url: actorJSON.id,
publicKey: actorJSON.publicKey.publicKeyPem, publicKey: actorJSON.publicKey.publicKeyPem,
privateKey: null, privateKey: null,
followersCount: followersCount, followersCount: followersCount,
@ -155,19 +155,20 @@ async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResu
followingUrl: actorJSON.following followingUrl: actorJSON.following
}) })
const name = actorJSON.name || actorJSON.preferredUsername
return { return {
actor, actor,
preferredUsername: actorJSON.preferredUsername, name,
summary: actorJSON.summary, summary: actorJSON.summary,
attributedTo: actorJSON.attributedTo attributedTo: actorJSON.attributedTo
} }
} }
function buildActorInstance (type: ActivityPubActorType, url: string, name: string, uuid?: string) { function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string, uuid?: string) {
return new ActorModel({ return new ActorModel({
type, type,
url, url,
name, preferredUsername,
uuid, uuid,
publicKey: null, publicKey: null,
privateKey: null, privateKey: null,
@ -210,7 +211,7 @@ async function fetchActorTotalItems (url: string) {
function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) { function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) {
const account = new AccountModel({ const account = new AccountModel({
name: result.preferredUsername, name: result.name,
actorId: actor.id actorId: actor.id
}) })
@ -219,7 +220,7 @@ function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Tran
async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResult, ownerActor: ActorModel, t: Transaction) { async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResult, ownerActor: ActorModel, t: Transaction) {
const videoChannel = new VideoChannelModel({ const videoChannel = new VideoChannelModel({
name: result.preferredUsername, name: result.name,
description: result.summary, description: result.summary,
actorId: actor.id, actorId: actor.id,
accountId: ownerActor.Account.id accountId: ownerActor.Account.id

View File

@ -1,5 +1,5 @@
import { Transaction } from 'sequelize' import { Transaction } from 'sequelize'
import { Activity } from '../../../../shared/models/activitypub' import { Activity, ActivityAudience } from '../../../../shared/models/activitypub'
import { logger } from '../../../helpers' import { logger } from '../../../helpers'
import { ACTIVITY_PUB } from '../../../initializers' import { ACTIVITY_PUB } from '../../../initializers'
import { ActorModel } from '../../../models/activitypub/actor' import { ActorModel } from '../../../models/activitypub/actor'
@ -116,6 +116,10 @@ async function getAudience (actorSender: ActorModel, t: Transaction, isPublic =
return { to, cc } return { to, cc }
} }
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[], followersException: ActorModel[], t: Transaction) {
const toActorFollowerIds = toActorFollower.map(a => a.id) const toActorFollowerIds = toActorFollower.map(a => a.id)
@ -133,5 +137,6 @@ export {
getOriginVideoAudience, getOriginVideoAudience,
getActorsInvolvedInVideo, getActorsInvolvedInVideo,
getObjectFollowersAudience, getObjectFollowersAudience,
forwardActivity forwardActivity,
audiencify
} }

View File

@ -1,16 +1,20 @@
import { Transaction } from 'sequelize' import { Transaction } from 'sequelize'
import { ActivityAccept } from '../../../../shared/models/activitypub' import { ActivityAccept, ActivityFollow } from '../../../../shared/models/activitypub'
import { ActorModel } from '../../../models/activitypub/actor' import { ActorModel } from '../../../models/activitypub/actor'
import { ActorFollowModel } from '../../../models/activitypub/actor-follow' import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
import { getActorFollowAcceptActivityPubUrl } from '../url' import { getActorFollowAcceptActivityPubUrl, getActorFollowActivityPubUrl } from '../url'
import { unicastTo } from './misc' import { unicastTo } from './misc'
import { followActivityData } from './send-follow'
async function sendAccept (actorFollow: ActorFollowModel, t: Transaction) { async function sendAccept (actorFollow: ActorFollowModel, t: Transaction) {
const follower = actorFollow.ActorFollower const follower = actorFollow.ActorFollower
const me = actorFollow.ActorFollowing const me = actorFollow.ActorFollowing
const followUrl = getActorFollowActivityPubUrl(actorFollow)
const followData = followActivityData(followUrl, follower, me)
const url = getActorFollowAcceptActivityPubUrl(actorFollow) const url = getActorFollowAcceptActivityPubUrl(actorFollow)
const data = acceptActivityData(url, me) const data = acceptActivityData(url, me, followData)
return unicastTo(data, me, follower.inboxUrl, t) return unicastTo(data, me, follower.inboxUrl, t)
} }
@ -23,10 +27,11 @@ export {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function acceptActivityData (url: string, byActor: ActorModel): ActivityAccept { function acceptActivityData (url: string, byActor: ActorModel, followActivityData: ActivityFollow): ActivityAccept {
return { return {
type: 'Accept', type: 'Accept',
id: url, id: url,
actor: byActor.url actor: byActor.url,
object: followActivityData
} }
} }

View File

@ -7,6 +7,7 @@ import { VideoModel } from '../../../models/video/video'
import { VideoAbuseModel } from '../../../models/video/video-abuse' import { VideoAbuseModel } from '../../../models/video/video-abuse'
import { getVideoAbuseActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoViewActivityPubUrl } from '../url' import { getVideoAbuseActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoViewActivityPubUrl } from '../url'
import { import {
audiencify,
broadcastToFollowers, broadcastToFollowers,
getActorsInvolvedInVideo, getActorsInvolvedInVideo,
getAudience, getAudience,
@ -16,9 +17,11 @@ import {
} from './misc' } from './misc'
async function sendCreateVideo (video: VideoModel, t: Transaction) { async function sendCreateVideo (video: VideoModel, t: Transaction) {
const byActor = video.VideoChannel.Account.Actor if (video.privacy === VideoPrivacy.PRIVATE) return
const byActor = video.VideoChannel.Account.Actor
const videoObject = video.toActivityPubObject() const videoObject = video.toActivityPubObject()
const audience = await getAudience(byActor, t, video.privacy === VideoPrivacy.PUBLIC) const audience = await getAudience(byActor, t, video.privacy === VideoPrivacy.PUBLIC)
const data = await createActivityData(video.url, byActor, videoObject, t, audience) const data = await createActivityData(video.url, byActor, videoObject, t, audience)
@ -93,14 +96,12 @@ async function createActivityData (
audience = await getAudience(byActor, t) audience = await getAudience(byActor, t)
} }
return { return audiencify({
type: 'Create', type: 'Create',
id: url, id: url,
actor: byActor.url, actor: byActor.url,
to: audience.to, object: audiencify(object, audience)
cc: audience.cc, }, audience)
object
}
} }
function createDislikeActivityData (byActor: ActorModel, video: VideoModel) { function createDislikeActivityData (byActor: ActorModel, video: VideoModel) {

View File

@ -4,6 +4,7 @@ import { ActorModel } from '../../../models/activitypub/actor'
import { VideoModel } from '../../../models/video/video' import { VideoModel } from '../../../models/video/video'
import { getVideoLikeActivityPubUrl } from '../url' import { getVideoLikeActivityPubUrl } from '../url'
import { import {
audiencify,
broadcastToFollowers, broadcastToFollowers,
getActorsInvolvedInVideo, getActorsInvolvedInVideo,
getAudience, getAudience,
@ -44,14 +45,12 @@ async function likeActivityData (
audience = await getAudience(byActor, t) audience = await getAudience(byActor, t)
} }
return { return audiencify({
type: 'Like', type: 'Like',
id: url, id: url,
actor: byActor.url, actor: byActor.url,
to: audience.to,
cc: audience.cc,
object: video.url object: video.url
} }, audience)
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -11,6 +11,7 @@ import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
import { VideoModel } from '../../../models/video/video' import { VideoModel } from '../../../models/video/video'
import { getActorFollowActivityPubUrl, getUndoActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from '../url' import { getActorFollowActivityPubUrl, getUndoActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from '../url'
import { import {
audiencify,
broadcastToFollowers, broadcastToFollowers,
getActorsInvolvedInVideo, getActorsInvolvedInVideo,
getAudience, getAudience,
@ -112,12 +113,10 @@ async function undoActivityData (
audience = await getAudience(byActor, t) audience = await getAudience(byActor, t)
} }
return { return audiencify({
type: 'Undo', type: 'Undo',
id: url, id: url,
actor: byActor.url, actor: byActor.url,
to: audience.to,
cc: audience.cc,
object object
} }, audience)
} }

View File

@ -5,7 +5,7 @@ import { ActorModel } from '../../../models/activitypub/actor'
import { VideoModel } from '../../../models/video/video' import { VideoModel } from '../../../models/video/video'
import { VideoShareModel } from '../../../models/video/video-share' import { VideoShareModel } from '../../../models/video/video-share'
import { getUpdateActivityPubUrl } from '../url' import { getUpdateActivityPubUrl } from '../url'
import { broadcastToFollowers, getAudience } from './misc' import { audiencify, broadcastToFollowers, getAudience } from './misc'
async function sendUpdateVideo (video: VideoModel, t: Transaction) { async function sendUpdateVideo (video: VideoModel, t: Transaction) {
const byActor = video.VideoChannel.Account.Actor const byActor = video.VideoChannel.Account.Actor
@ -41,12 +41,10 @@ async function updateActivityData (
audience = await getAudience(byActor, t) audience = await getAudience(byActor, t)
} }
return { return audiencify({
type: 'Update', type: 'Update',
id: url, id: url,
actor: byActor.url, actor: byActor.url,
to: audience.to, object: audiencify(object, audience)
cc: audience.cc, }, audience)
object
}
} }

View File

@ -1,10 +1,13 @@
import { Transaction } from 'sequelize' import { Transaction } from 'sequelize'
import { VideoPrivacy } from '../../../shared/models/videos'
import { getServerActor } from '../../helpers' import { getServerActor } from '../../helpers'
import { VideoModel } from '../../models/video/video' import { VideoModel } from '../../models/video/video'
import { VideoShareModel } from '../../models/video/video-share' import { VideoShareModel } from '../../models/video/video-share'
import { sendVideoAnnounceToFollowers } from './send' import { sendVideoAnnounceToFollowers } from './send'
async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction) { async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction) {
if (video.privacy === VideoPrivacy.PRIVATE) return
const serverActor = await getServerActor() const serverActor = await getServerActor()
const serverShare = VideoShareModel.create({ const serverShare = VideoShareModel.create({

View File

@ -1,15 +1,17 @@
import { doRequest, logger } from '../../../helpers' import { doRequest, logger } from '../../../helpers'
import { ActivityPubHttpPayload, computeBody, maybeRetryRequestLater } from './activitypub-http-job-scheduler' import { ActivityPubHttpPayload, buildSignedRequestOptions, computeBody, maybeRetryRequestLater } from './activitypub-http-job-scheduler'
async function process (payload: ActivityPubHttpPayload, jobId: number) { async function process (payload: ActivityPubHttpPayload, jobId: number) {
logger.info('Processing ActivityPub broadcast in job %d.', jobId) logger.info('Processing ActivityPub broadcast in job %d.', jobId)
const body = await computeBody(payload) const body = await computeBody(payload)
const httpSignatureOptions = await buildSignedRequestOptions(payload)
const options = { const options = {
method: 'POST', method: 'POST',
uri: '', uri: '',
json: body json: body,
httpSignature: httpSignatureOptions
} }
for (const uri of payload.uris) { for (const uri of payload.uris) {

View File

@ -1,5 +1,5 @@
import { JobCategory } from '../../../../shared' import { JobCategory } from '../../../../shared'
import { buildSignedActivity, logger } from '../../../helpers' import { buildSignedActivity, getServerActor, logger } from '../../../helpers'
import { ACTIVITY_PUB } from '../../../initializers' import { ACTIVITY_PUB } from '../../../initializers'
import { ActorModel } from '../../../models/activitypub/actor' import { ActorModel } from '../../../models/activitypub/actor'
import { JobHandler, JobScheduler } from '../job-scheduler' import { JobHandler, JobScheduler } from '../job-scheduler'
@ -46,16 +46,36 @@ async function computeBody (payload: ActivityPubHttpPayload) {
if (payload.signatureActorId) { if (payload.signatureActorId) {
const actorSignature = await ActorModel.load(payload.signatureActorId) const actorSignature = await ActorModel.load(payload.signatureActorId)
if (!actorSignature) throw new Error('Unknown signature account id.') if (!actorSignature) throw new Error('Unknown signature actor id.')
body = await buildSignedActivity(actorSignature, payload.body) body = await buildSignedActivity(actorSignature, payload.body)
} }
return body return body
} }
async function buildSignedRequestOptions (payload: ActivityPubHttpPayload) {
let actor: ActorModel
if (payload.signatureActorId) {
actor = await ActorModel.load(payload.signatureActorId)
if (!actor) throw new Error('Unknown signature actor id.')
} else {
// We need to sign the request, so use the server
actor = await getServerActor()
}
const keyId = actor.getWebfingerUrl()
return {
algorithm: 'rsa-sha256',
authorizationHeaderName: 'Signature',
keyId,
key: actor.privateKey
}
}
export { export {
ActivityPubHttpPayload, ActivityPubHttpPayload,
activitypubHttpJobScheduler, activitypubHttpJobScheduler,
maybeRetryRequestLater, maybeRetryRequestLater,
computeBody computeBody,
buildSignedRequestOptions
} }

View File

@ -1,16 +1,18 @@
import { doRequest, logger } from '../../../helpers' import { doRequest, logger } from '../../../helpers'
import { ActivityPubHttpPayload, computeBody, maybeRetryRequestLater } from './activitypub-http-job-scheduler' import { ActivityPubHttpPayload, buildSignedRequestOptions, computeBody, maybeRetryRequestLater } from './activitypub-http-job-scheduler'
async function process (payload: ActivityPubHttpPayload, jobId: number) { async function process (payload: ActivityPubHttpPayload, jobId: number) {
logger.info('Processing ActivityPub unicast in job %d.', jobId) logger.info('Processing ActivityPub unicast in job %d.', jobId)
const body = await computeBody(payload) const body = await computeBody(payload)
const httpSignatureOptions = await buildSignedRequestOptions(payload)
const uri = payload.uris[0] const uri = payload.uris[0]
const options = { const options = {
method: 'POST', method: 'POST',
uri, uri,
json: body json: body,
httpSignature: httpSignatureOptions
} }
try { try {

View File

@ -1,4 +1,5 @@
import * as Bluebird from 'bluebird' import * as Bluebird from 'bluebird'
import { VideoPrivacy } from '../../../../shared/models/videos'
import { computeResolutionsToTranscode, logger } from '../../../helpers' import { computeResolutionsToTranscode, logger } from '../../../helpers'
import { sequelizeTypescript } from '../../../initializers' import { sequelizeTypescript } from '../../../initializers'
import { VideoModel } from '../../../models/video/video' import { VideoModel } from '../../../models/video/video'
@ -35,9 +36,11 @@ async function onSuccess (jobId: number, video: VideoModel, jobScheduler: JobSch
// Video does not exist anymore // Video does not exist anymore
if (!videoDatabase) return undefined if (!videoDatabase) return undefined
// Now we'll add the video's meta data to our followers if (video.privacy !== VideoPrivacy.PRIVATE) {
await sendCreateVideo(video, undefined) // Now we'll add the video's meta data to our followers
await shareVideoByServerAndChannel(video, undefined) await sendCreateVideo(video, undefined)
await shareVideoByServerAndChannel(video, undefined)
}
const originalFileHeight = await videoDatabase.getOriginalFileHeight() const originalFileHeight = await videoDatabase.getOriginalFileHeight()

View File

@ -1,4 +1,5 @@
import { VideoResolution } from '../../../../shared' import { VideoResolution } from '../../../../shared'
import { VideoPrivacy } from '../../../../shared/models/videos'
import { logger } from '../../../helpers' import { logger } from '../../../helpers'
import { VideoModel } from '../../../models/video/video' import { VideoModel } from '../../../models/video/video'
import { sendUpdateVideo } from '../../activitypub/send' import { sendUpdateVideo } from '../../activitypub/send'
@ -31,7 +32,9 @@ async function onSuccess (jobId: number, video: VideoModel) {
// Video does not exist anymore // Video does not exist anymore
if (!videoDatabase) return undefined if (!videoDatabase) return undefined
await sendUpdateVideo(video, undefined) if (video.privacy !== VideoPrivacy.PRIVATE) {
await sendUpdateVideo(video, undefined)
}
return undefined return undefined
} }

View File

@ -9,11 +9,13 @@ import { ActorModel } from '../models/activitypub/actor'
async function checkSignature (req: Request, res: Response, next: NextFunction) { async function checkSignature (req: Request, res: Response, next: NextFunction) {
const signatureObject: ActivityPubSignature = req.body.signature const signatureObject: ActivityPubSignature = req.body.signature
logger.debug('Checking signature of actor %s...', signatureObject.creator) const [ creator ] = signatureObject.creator.split('#')
logger.debug('Checking signature of actor %s...', creator)
let actor: ActorModel let actor: ActorModel
try { try {
actor = await getOrCreateActorAndServerAndModel(signatureObject.creator) actor = await getOrCreateActorAndServerAndModel(creator)
} catch (err) { } catch (err) {
logger.error('Cannot create remote actor and check signature.', err) logger.error('Cannot create remote actor and check signature.', err)
return res.sendStatus(403) return res.sendStatus(403)
@ -32,6 +34,7 @@ async function checkSignature (req: Request, res: Response, next: NextFunction)
function executeIfActivityPub (fun: RequestHandler | RequestHandler[]) { function executeIfActivityPub (fun: RequestHandler | RequestHandler[]) {
return (req: Request, res: Response, next: NextFunction) => { return (req: Request, res: Response, next: NextFunction) => {
const accepted = req.accepts(ACCEPT_HEADERS) const accepted = req.accepts(ACCEPT_HEADERS)
console.log(accepted)
if (accepted === false || ACTIVITY_PUB.POTENTIAL_ACCEPT_HEADERS.indexOf(accepted) === -1) { if (accepted === false || ACTIVITY_PUB.POTENTIAL_ACCEPT_HEADERS.indexOf(accepted) === -1) {
return next() return next()
} }

View File

@ -2,32 +2,15 @@ import { values } from 'lodash'
import { join } from 'path' import { join } from 'path'
import * as Sequelize from 'sequelize' import * as Sequelize from 'sequelize'
import { import {
AllowNull, AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, DefaultScope, ForeignKey, HasMany, HasOne, Is, IsUUID, Model, Scopes,
BelongsTo, Table, UpdatedAt
Column,
CreatedAt,
DataType,
Default, DefaultScope,
ForeignKey,
HasMany,
HasOne,
Is,
IsUUID,
Model,
Scopes,
Table,
UpdatedAt
} from 'sequelize-typescript' } from 'sequelize-typescript'
import { ActivityPubActorType } from '../../../shared/models/activitypub' import { ActivityPubActorType } from '../../../shared/models/activitypub'
import { Avatar } from '../../../shared/models/avatars/avatar.model' import { Avatar } from '../../../shared/models/avatars/avatar.model'
import { activityPubContextify } from '../../helpers' import { activityPubContextify } from '../../helpers'
import { import {
isActivityPubUrlValid, isActivityPubUrlValid, isActorFollowersCountValid, isActorFollowingCountValid, isActorPreferredUsernameValid,
isActorFollowersCountValid, isActorPrivateKeyValid, isActorPublicKeyValid
isActorFollowingCountValid,
isActorNameValid,
isActorPrivateKeyValid,
isActorPublicKeyValid
} from '../../helpers/custom-validators/activitypub' } from '../../helpers/custom-validators/activitypub'
import { ACTIVITY_PUB_ACTOR_TYPES, AVATARS_DIR, CONFIG, CONSTRAINTS_FIELDS } from '../../initializers' import { ACTIVITY_PUB_ACTOR_TYPES, AVATARS_DIR, CONFIG, CONSTRAINTS_FIELDS } from '../../initializers'
import { AccountModel } from '../account/account' import { AccountModel } from '../account/account'
@ -71,7 +54,7 @@ enum ScopeNames {
tableName: 'actor', tableName: 'actor',
indexes: [ indexes: [
{ {
fields: [ 'name', 'serverId' ], fields: [ 'preferredUsername', 'serverId' ],
unique: true unique: true
} }
] ]
@ -89,9 +72,9 @@ export class ActorModel extends Model<ActorModel> {
uuid: string uuid: string
@AllowNull(false) @AllowNull(false)
@Is('ActorName', value => throwIfNotValid(value, isActorNameValid, 'actor name')) @Is('ActorPreferredUsername', value => throwIfNotValid(value, isActorPreferredUsernameValid, 'actor preferred username'))
@Column @Column
name: string preferredUsername: string
@AllowNull(false) @AllowNull(false)
@Is('ActorUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url')) @Is('ActorUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
@ -212,16 +195,6 @@ export class ActorModel extends Model<ActorModel> {
return ActorModel.scope(ScopeNames.FULL).findById(id) return ActorModel.scope(ScopeNames.FULL).findById(id)
} }
static loadByUUID (uuid: string) {
const query = {
where: {
uuid
}
}
return ActorModel.scope(ScopeNames.FULL).findOne(query)
}
static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction) { static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction) {
const query = { const query = {
where: { where: {
@ -235,10 +208,10 @@ export class ActorModel extends Model<ActorModel> {
return ActorModel.scope(ScopeNames.FULL).findAll(query) return ActorModel.scope(ScopeNames.FULL).findAll(query)
} }
static loadLocalByName (name: string) { static loadLocalByName (preferredUsername: string) {
const query = { const query = {
where: { where: {
name, preferredUsername,
serverId: null serverId: null
} }
} }
@ -246,10 +219,10 @@ export class ActorModel extends Model<ActorModel> {
return ActorModel.scope(ScopeNames.FULL).findOne(query) return ActorModel.scope(ScopeNames.FULL).findOne(query)
} }
static loadByNameAndHost (name: string, host: string) { static loadByNameAndHost (preferredUsername: string, host: string) {
const query = { const query = {
where: { where: {
name preferredUsername
}, },
include: [ include: [
{ {
@ -286,17 +259,15 @@ export class ActorModel extends Model<ActorModel> {
} }
} }
let host = CONFIG.WEBSERVER.HOST
let score: number let score: number
if (this.Server) { if (this.Server) {
host = this.Server.host
score = this.Server.score score = this.Server.score
} }
return { return {
id: this.id, id: this.id,
uuid: this.uuid, uuid: this.uuid,
host, host: this.getHost(),
score, score,
followingCount: this.followingCount, followingCount: this.followingCount,
followersCount: this.followersCount, followersCount: this.followersCount,
@ -304,7 +275,7 @@ export class ActorModel extends Model<ActorModel> {
} }
} }
toActivityPubObject (preferredUsername: string, type: 'Account' | 'Application' | 'VideoChannel') { toActivityPubObject (name: string, type: 'Account' | 'Application' | 'VideoChannel') {
let activityPubType let activityPubType
if (type === 'Account') { if (type === 'Account') {
activityPubType = 'Person' as 'Person' activityPubType = 'Person' as 'Person'
@ -321,9 +292,9 @@ export class ActorModel extends Model<ActorModel> {
followers: this.getFollowersUrl(), followers: this.getFollowersUrl(),
inbox: this.inboxUrl, inbox: this.inboxUrl,
outbox: this.outboxUrl, outbox: this.outboxUrl,
preferredUsername, preferredUsername: this.preferredUsername,
url: this.url, url: this.url,
name: this.name, name,
endpoints: { endpoints: {
sharedInbox: this.sharedInboxUrl sharedInbox: this.sharedInboxUrl
}, },
@ -373,4 +344,12 @@ export class ActorModel extends Model<ActorModel> {
isOwned () { isOwned () {
return this.serverId === null return this.serverId === null
} }
getWebfingerUrl () {
return 'acct:' + this.preferredUsername + '@' + this.getHost()
}
getHost () {
return this.Server ? this.Server.host : CONFIG.WEBSERVER.HOST
}
} }

View File

@ -46,6 +46,7 @@ export interface ActivityFollow extends BaseActivity {
export interface ActivityAccept extends BaseActivity { export interface ActivityAccept extends BaseActivity {
type: 'Accept' type: 'Accept'
object: ActivityFollow
} }
export interface ActivityAnnounce extends BaseActivity { export interface ActivityAnnounce extends BaseActivity {

View File

@ -2378,9 +2378,9 @@ jsonify@~0.0.0:
version "0.0.0" version "0.0.0"
resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
"jsonld-signatures@https://github.com/digitalbazaar/jsonld-signatures#rsa2017": "jsonld-signatures@https://github.com/Chocobozzz/jsonld-signatures#rsa2017":
version "1.2.2-2" version "1.2.2-2"
resolved "https://github.com/digitalbazaar/jsonld-signatures#ccb5ca156d72d7632131080d6ef564681791391e" resolved "https://github.com/Chocobozzz/jsonld-signatures#77660963e722eb4541d2d255f9d9d4216329665f"
dependencies: dependencies:
bitcore-message "github:CoMakery/bitcore-message#dist" bitcore-message "github:CoMakery/bitcore-message#dist"
jsonld "^0.5.12" jsonld "^0.5.12"