import * as validator from 'validator'
import { CONSTRAINTS_FIELDS } from '../../../initializers'
import { isAccountNameValid } from '../accounts'
-import { exists, isUUIDValid } from '../misc'
-import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../video-channels'
+import { exists } from '../misc'
+import { isVideoChannelNameValid } from '../video-channels'
import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc'
function isActorEndpointsObjectValid (endpointObject: any) {
return exists(publicKey) &&
typeof publicKey === 'string' &&
publicKey.startsWith('-----BEGIN PUBLIC KEY-----') &&
- publicKey.endsWith('-----END PUBLIC KEY-----') &&
+ publicKey.indexOf('-----END PUBLIC KEY-----') !== -1 &&
validator.isLength(publicKey, CONSTRAINTS_FIELDS.ACTOR.PUBLIC_KEY)
}
+const actorNameRegExp = new RegExp('[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_]+')
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) {
- return exists(name) && validator.matches(name, actorNameRegExp)
+ return isAccountNameValid(name) || isVideoChannelNameValid(name)
}
function isActorPrivateKeyValid (privateKey: string) {
return exists(privateKey) &&
typeof privateKey === 'string' &&
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)
}
function isRemoteActorValid (remoteActor: any) {
return isActivityPubUrlValid(remoteActor.id) &&
- isUUIDValid(remoteActor.uuid) &&
isActorTypeValid(remoteActor.type) &&
isActivityPubUrlValid(remoteActor.following) &&
isActivityPubUrlValid(remoteActor.followers) &&
isActivityPubUrlValid(remoteActor.inbox) &&
isActivityPubUrlValid(remoteActor.outbox) &&
- isActorNameValid(remoteActor.name) &&
isActorPreferredUsernameValid(remoteActor.preferredUsername) &&
isActivityPubUrlValid(remoteActor.url) &&
isActorPublicKeyObjectValid(remoteActor.publicKey) &&
isActorEndpointsObjectValid(remoteActor.endpoints) &&
- (!remoteActor.summary || isVideoChannelDescriptionValid(remoteActor.summary)) &&
setValidAttributedTo(remoteActor) &&
// 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
}
}
-const ACCEPT_HEADERS = ACTIVITY_PUB.POTENTIAL_ACCEPT_HEADERS.concat('html', 'application/json')
+const ACCEPT_HEADERS = [ 'html', 'application/json' ].concat(ACTIVITY_PUB.POTENTIAL_ACCEPT_HEADERS)
// ---------------------------------------------------------------------------
id integer NOT NULL,
type enum_actor_type NOT NULL,
uuid uuid NOT NULL,
- name character varying(255) NOT NULL,
+ "preferredUsername" character varying(255) NOT NULL,
url character varying(2000) NOT NULL,
"publicKey" character varying(5000),
"privateKey" character varying(5000),
`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 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
ADD CONSTRAINT "actor_avatarId_fkey" FOREIGN KEY ("avatarId") REFERENCES avatar(id) ON UPDATE CASCADE ON DELETE CASCADE`,
`ALTER TABLE ONLY 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"
)
SELECT
`
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"
)
SELECT
const query = `
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"
)
SELECT
import { ServerModel } from '../../models/server/server'
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) {
return createPrivateAndPublicKeys()
.then(({ publicKey, privateKey }) => {
type FetchRemoteActorResult = {
actor: ActorModel
- preferredUsername: string
+ name: string
summary: string
attributedTo: ActivityPubAttributedTo[]
}
const actor = new ActorModel({
type: actorJSON.type,
uuid: actorJSON.uuid,
- name: actorJSON.name,
- url: actorJSON.url,
+ preferredUsername: actorJSON.preferredUsername,
+ url: actorJSON.id,
publicKey: actorJSON.publicKey.publicKeyPem,
privateKey: null,
followersCount: followersCount,
followingUrl: actorJSON.following
})
+ const name = actorJSON.name || actorJSON.preferredUsername
return {
actor,
- preferredUsername: actorJSON.preferredUsername,
+ name,
summary: actorJSON.summary,
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({
type,
url,
- name,
+ preferredUsername,
uuid,
publicKey: null,
privateKey: null,
function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) {
const account = new AccountModel({
- name: result.preferredUsername,
+ name: result.name,
actorId: actor.id
})
async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResult, ownerActor: ActorModel, t: Transaction) {
const videoChannel = new VideoChannelModel({
- name: result.preferredUsername,
+ name: result.name,
description: result.summary,
actorId: actor.id,
accountId: ownerActor.Account.id
import { Transaction } from 'sequelize'
-import { Activity } from '../../../../shared/models/activitypub'
+import { Activity, ActivityAudience } from '../../../../shared/models/activitypub'
import { logger } from '../../../helpers'
import { ACTIVITY_PUB } from '../../../initializers'
import { ActorModel } from '../../../models/activitypub/actor'
return { to, cc }
}
+function audiencify (object: any, audience: ActivityAudience) {
+ return Object.assign(object, audience)
+}
+
async function computeFollowerUris (toActorFollower: ActorModel[], followersException: ActorModel[], t: Transaction) {
const toActorFollowerIds = toActorFollower.map(a => a.id)
getOriginVideoAudience,
getActorsInvolvedInVideo,
getObjectFollowersAudience,
- forwardActivity
+ forwardActivity,
+ audiencify
}
import { Transaction } from 'sequelize'
-import { ActivityAccept } from '../../../../shared/models/activitypub'
+import { ActivityAccept, ActivityFollow } from '../../../../shared/models/activitypub'
import { ActorModel } from '../../../models/activitypub/actor'
import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
-import { getActorFollowAcceptActivityPubUrl } from '../url'
+import { getActorFollowAcceptActivityPubUrl, getActorFollowActivityPubUrl } from '../url'
import { unicastTo } from './misc'
+import { followActivityData } from './send-follow'
async function sendAccept (actorFollow: ActorFollowModel, t: Transaction) {
const follower = actorFollow.ActorFollower
const me = actorFollow.ActorFollowing
+ const followUrl = getActorFollowActivityPubUrl(actorFollow)
+ const followData = followActivityData(followUrl, follower, me)
+
const url = getActorFollowAcceptActivityPubUrl(actorFollow)
- const data = acceptActivityData(url, me)
+ const data = acceptActivityData(url, me, followData)
return unicastTo(data, me, follower.inboxUrl, t)
}
// ---------------------------------------------------------------------------
-function acceptActivityData (url: string, byActor: ActorModel): ActivityAccept {
+function acceptActivityData (url: string, byActor: ActorModel, followActivityData: ActivityFollow): ActivityAccept {
return {
type: 'Accept',
id: url,
- actor: byActor.url
+ actor: byActor.url,
+ object: followActivityData
}
}
import { VideoAbuseModel } from '../../../models/video/video-abuse'
import { getVideoAbuseActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoViewActivityPubUrl } from '../url'
import {
+ audiencify,
broadcastToFollowers,
getActorsInvolvedInVideo,
getAudience,
} from './misc'
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 audience = await getAudience(byActor, t, video.privacy === VideoPrivacy.PUBLIC)
const data = await createActivityData(video.url, byActor, videoObject, t, audience)
audience = await getAudience(byActor, t)
}
- return {
+ return audiencify({
type: 'Create',
id: url,
actor: byActor.url,
- to: audience.to,
- cc: audience.cc,
- object
- }
+ object: audiencify(object, audience)
+ }, audience)
}
function createDislikeActivityData (byActor: ActorModel, video: VideoModel) {
import { VideoModel } from '../../../models/video/video'
import { getVideoLikeActivityPubUrl } from '../url'
import {
+ audiencify,
broadcastToFollowers,
getActorsInvolvedInVideo,
getAudience,
audience = await getAudience(byActor, t)
}
- return {
+ return audiencify({
type: 'Like',
id: url,
actor: byActor.url,
- to: audience.to,
- cc: audience.cc,
object: video.url
- }
+ }, audience)
}
// ---------------------------------------------------------------------------
import { VideoModel } from '../../../models/video/video'
import { getActorFollowActivityPubUrl, getUndoActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from '../url'
import {
+ audiencify,
broadcastToFollowers,
getActorsInvolvedInVideo,
getAudience,
audience = await getAudience(byActor, t)
}
- return {
+ return audiencify({
type: 'Undo',
id: url,
actor: byActor.url,
- to: audience.to,
- cc: audience.cc,
object
- }
+ }, audience)
}
import { VideoModel } from '../../../models/video/video'
import { VideoShareModel } from '../../../models/video/video-share'
import { getUpdateActivityPubUrl } from '../url'
-import { broadcastToFollowers, getAudience } from './misc'
+import { audiencify, broadcastToFollowers, getAudience } from './misc'
async function sendUpdateVideo (video: VideoModel, t: Transaction) {
const byActor = video.VideoChannel.Account.Actor
audience = await getAudience(byActor, t)
}
- return {
+ return audiencify({
type: 'Update',
id: url,
actor: byActor.url,
- to: audience.to,
- cc: audience.cc,
- object
- }
+ object: audiencify(object, audience)
+ }, audience)
}
import { Transaction } from 'sequelize'
+import { VideoPrivacy } from '../../../shared/models/videos'
import { getServerActor } from '../../helpers'
import { VideoModel } from '../../models/video/video'
import { VideoShareModel } from '../../models/video/video-share'
import { sendVideoAnnounceToFollowers } from './send'
async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction) {
+ if (video.privacy === VideoPrivacy.PRIVATE) return
+
const serverActor = await getServerActor()
const serverShare = VideoShareModel.create({
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) {
logger.info('Processing ActivityPub broadcast in job %d.', jobId)
const body = await computeBody(payload)
+ const httpSignatureOptions = await buildSignedRequestOptions(payload)
const options = {
method: 'POST',
uri: '',
- json: body
+ json: body,
+ httpSignature: httpSignatureOptions
}
for (const uri of payload.uris) {
import { JobCategory } from '../../../../shared'
-import { buildSignedActivity, logger } from '../../../helpers'
+import { buildSignedActivity, getServerActor, logger } from '../../../helpers'
import { ACTIVITY_PUB } from '../../../initializers'
import { ActorModel } from '../../../models/activitypub/actor'
import { JobHandler, JobScheduler } from '../job-scheduler'
if (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)
}
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 {
ActivityPubHttpPayload,
activitypubHttpJobScheduler,
maybeRetryRequestLater,
- computeBody
+ computeBody,
+ buildSignedRequestOptions
}
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) {
logger.info('Processing ActivityPub unicast in job %d.', jobId)
const body = await computeBody(payload)
+ const httpSignatureOptions = await buildSignedRequestOptions(payload)
const uri = payload.uris[0]
const options = {
method: 'POST',
uri,
- json: body
+ json: body,
+ httpSignature: httpSignatureOptions
}
try {
import * as Bluebird from 'bluebird'
+import { VideoPrivacy } from '../../../../shared/models/videos'
import { computeResolutionsToTranscode, logger } from '../../../helpers'
import { sequelizeTypescript } from '../../../initializers'
import { VideoModel } from '../../../models/video/video'
// Video does not exist anymore
if (!videoDatabase) return undefined
- // Now we'll add the video's meta data to our followers
- await sendCreateVideo(video, undefined)
- await shareVideoByServerAndChannel(video, undefined)
+ if (video.privacy !== VideoPrivacy.PRIVATE) {
+ // Now we'll add the video's meta data to our followers
+ await sendCreateVideo(video, undefined)
+ await shareVideoByServerAndChannel(video, undefined)
+ }
const originalFileHeight = await videoDatabase.getOriginalFileHeight()
import { VideoResolution } from '../../../../shared'
+import { VideoPrivacy } from '../../../../shared/models/videos'
import { logger } from '../../../helpers'
import { VideoModel } from '../../../models/video/video'
import { sendUpdateVideo } from '../../activitypub/send'
// Video does not exist anymore
if (!videoDatabase) return undefined
- await sendUpdateVideo(video, undefined)
+ if (video.privacy !== VideoPrivacy.PRIVATE) {
+ await sendUpdateVideo(video, undefined)
+ }
return undefined
}
async function checkSignature (req: Request, res: Response, next: NextFunction) {
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
try {
- actor = await getOrCreateActorAndServerAndModel(signatureObject.creator)
+ actor = await getOrCreateActorAndServerAndModel(creator)
} catch (err) {
logger.error('Cannot create remote actor and check signature.', err)
return res.sendStatus(403)
function executeIfActivityPub (fun: RequestHandler | RequestHandler[]) {
return (req: Request, res: Response, next: NextFunction) => {
const accepted = req.accepts(ACCEPT_HEADERS)
+ console.log(accepted)
if (accepted === false || ACTIVITY_PUB.POTENTIAL_ACCEPT_HEADERS.indexOf(accepted) === -1) {
return next()
}
import { join } from 'path'
import * as Sequelize from 'sequelize'
import {
- AllowNull,
- BelongsTo,
- Column,
- CreatedAt,
- DataType,
- Default, DefaultScope,
- ForeignKey,
- HasMany,
- HasOne,
- Is,
- IsUUID,
- Model,
- Scopes,
- Table,
- UpdatedAt
+ AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, DefaultScope, ForeignKey, HasMany, HasOne, Is, IsUUID, Model, Scopes,
+ Table, UpdatedAt
} from 'sequelize-typescript'
import { ActivityPubActorType } from '../../../shared/models/activitypub'
import { Avatar } from '../../../shared/models/avatars/avatar.model'
import { activityPubContextify } from '../../helpers'
import {
- isActivityPubUrlValid,
- isActorFollowersCountValid,
- isActorFollowingCountValid,
- isActorNameValid,
- isActorPrivateKeyValid,
- isActorPublicKeyValid
+ isActivityPubUrlValid, isActorFollowersCountValid, isActorFollowingCountValid, isActorPreferredUsernameValid,
+ isActorPrivateKeyValid, isActorPublicKeyValid
} from '../../helpers/custom-validators/activitypub'
import { ACTIVITY_PUB_ACTOR_TYPES, AVATARS_DIR, CONFIG, CONSTRAINTS_FIELDS } from '../../initializers'
import { AccountModel } from '../account/account'
tableName: 'actor',
indexes: [
{
- fields: [ 'name', 'serverId' ],
+ fields: [ 'preferredUsername', 'serverId' ],
unique: true
}
]
uuid: string
@AllowNull(false)
- @Is('ActorName', value => throwIfNotValid(value, isActorNameValid, 'actor name'))
+ @Is('ActorPreferredUsername', value => throwIfNotValid(value, isActorPreferredUsernameValid, 'actor preferred username'))
@Column
- name: string
+ preferredUsername: string
@AllowNull(false)
@Is('ActorUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
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) {
const query = {
where: {
return ActorModel.scope(ScopeNames.FULL).findAll(query)
}
- static loadLocalByName (name: string) {
+ static loadLocalByName (preferredUsername: string) {
const query = {
where: {
- name,
+ preferredUsername,
serverId: null
}
}
return ActorModel.scope(ScopeNames.FULL).findOne(query)
}
- static loadByNameAndHost (name: string, host: string) {
+ static loadByNameAndHost (preferredUsername: string, host: string) {
const query = {
where: {
- name
+ preferredUsername
},
include: [
{
}
}
- let host = CONFIG.WEBSERVER.HOST
let score: number
if (this.Server) {
- host = this.Server.host
score = this.Server.score
}
return {
id: this.id,
uuid: this.uuid,
- host,
+ host: this.getHost(),
score,
followingCount: this.followingCount,
followersCount: this.followersCount,
}
}
- toActivityPubObject (preferredUsername: string, type: 'Account' | 'Application' | 'VideoChannel') {
+ toActivityPubObject (name: string, type: 'Account' | 'Application' | 'VideoChannel') {
let activityPubType
if (type === 'Account') {
activityPubType = 'Person' as 'Person'
followers: this.getFollowersUrl(),
inbox: this.inboxUrl,
outbox: this.outboxUrl,
- preferredUsername,
+ preferredUsername: this.preferredUsername,
url: this.url,
- name: this.name,
+ name,
endpoints: {
sharedInbox: this.sharedInboxUrl
},
isOwned () {
return this.serverId === null
}
+
+ getWebfingerUrl () {
+ return 'acct:' + this.preferredUsername + '@' + this.getHost()
+ }
+
+ getHost () {
+ return this.Server ? this.Server.host : CONFIG.WEBSERVER.HOST
+ }
}
export interface ActivityAccept extends BaseActivity {
type: 'Accept'
+ object: ActivityFollow
}
export interface ActivityAnnounce extends BaseActivity {
version "0.0.0"
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"
- resolved "https://github.com/digitalbazaar/jsonld-signatures#ccb5ca156d72d7632131080d6ef564681791391e"
+ resolved "https://github.com/Chocobozzz/jsonld-signatures#77660963e722eb4541d2d255f9d9d4216329665f"
dependencies:
bitcore-message "github:CoMakery/bitcore-message#dist"
jsonld "^0.5.12"