* pub-relay

* relay actorをApplicationにする

* Disable koa-compress

* Homeはリレーに送らない

* Disable debug

* UI

* cleanupなど
This commit is contained in:
MeiMei 2020-05-10 18:42:31 +09:00 committed by GitHub
parent ddb7d90bd1
commit fe841db55a
27 changed files with 510 additions and 12 deletions

View File

@ -502,6 +502,10 @@ sidebar: "サイドバー"
divider: "分割線" divider: "分割線"
addItem: "項目を追加" addItem: "項目を追加"
rooms: "ルーム" rooms: "ルーム"
relays: "リレー"
addRelay: "リレーの追加"
inboxUrl: "inboxのURL"
addedRelays: "追加済みのリレー"
_theme: _theme:
explore: "テーマを探す" explore: "テーマを探す"
@ -1090,3 +1094,8 @@ _pages:
enviromentVariables: "環境変数" enviromentVariables: "環境変数"
pageVariables: "ページ要素" pageVariables: "ページ要素"
argVariables: "入力スロット" argVariables: "入力スロット"
_relayStatus:
requesting: "承認待ち"
accepted: "承認済み"
rejected: "拒否済み"

View File

@ -0,0 +1,18 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class pubRelay1589023282116 implements MigrationInterface {
name = 'pubRelay1589023282116'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TYPE "relay_status_enum" AS ENUM('requesting', 'accepted', 'rejected')`, undefined);
await queryRunner.query(`CREATE TABLE "relay" ("id" character varying(32) NOT NULL, "inbox" character varying(512) NOT NULL, "status" "relay_status_enum" NOT NULL, CONSTRAINT "PK_78ebc9cfddf4292633b7ba57aee" PRIMARY KEY ("id"))`, undefined);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0d9a1738f2cf7f3b1c3334dfab" ON "relay" ("inbox") `, undefined);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_0d9a1738f2cf7f3b1c3334dfab"`, undefined);
await queryRunner.query(`DROP TABLE "relay"`, undefined);
await queryRunner.query(`DROP TYPE "relay_status_enum"`, undefined);
}
}

View File

@ -132,7 +132,7 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import { faGripVertical, faChevronLeft, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faListUl, faPlus, faUserClock, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer, faInfoCircle, faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; import { faGripVertical, faChevronLeft, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faListUl, faPlus, faUserClock, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer, faInfoCircle, faQuestionCircle, faProjectDiagram } from '@fortawesome/free-solid-svg-icons';
import { faBell, faEnvelope, faLaugh, faComments } from '@fortawesome/free-regular-svg-icons'; import { faBell, faEnvelope, faLaugh, faComments } from '@fortawesome/free-regular-svg-icons';
import { ResizeObserver } from '@juggle/resize-observer'; import { ResizeObserver } from '@juggle/resize-observer';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
@ -169,7 +169,7 @@ export default Vue.extend({
isDesktop: window.innerWidth >= DESKTOP_THRESHOLD, isDesktop: window.innerWidth >= DESKTOP_THRESHOLD,
canBack: false, canBack: false,
wallpaper: localStorage.getItem('wallpaper') != null, wallpaper: localStorage.getItem('wallpaper') != null,
faGripVertical, faChevronLeft, faComments, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faBell, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faEnvelope, faListUl, faPlus, faUserClock, faLaugh, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer faGripVertical, faChevronLeft, faComments, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faBell, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faEnvelope, faListUl, faPlus, faUserClock, faLaugh, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer, faProjectDiagram
}; };
}, },
@ -413,6 +413,11 @@ export default Vue.extend({
text: this.$t('federation'), text: this.$t('federation'),
to: '/instance/federation', to: '/instance/federation',
icon: faGlobe, icon: faGlobe,
}, {
type: 'link',
text: this.$t('relays'),
to: '/instance/relays',
icon: faProjectDiagram,
}, { }, {
type: 'link', type: 'link',
text: this.$t('announcements'), text: this.$t('announcements'),

View File

@ -0,0 +1,93 @@
<template>
<div class="relaycxt">
<portal to="icon"><fa :icon="faProjectDiagram"/></portal>
<portal to="title">{{ $t('relays') }}</portal>
<section class="_card add">
<div class="_title"><fa :icon="faPlus"/> {{ $t('addRelay') }}</div>
<div class="_content">
<mk-input v-model="inbox">
<span>{{ $t('inboxUrl') }}</span>
</mk-input>
<mk-button @click="add(inbox)" primary><fa :icon="faPlus"/> {{ $t('add') }}</mk-button>
</div>
</section>
<section class="_card relays">
<div class="_title"><fa :icon="faProjectDiagram"/> {{ $t('addedRelays') }}</div>
<div class="_content relay" v-for="relay in relays" :key="relay.inbox">
<div>{{ relay.inbox }}</div>
<div>{{ $t(`_relayStatus.${relay.status}`) }}</div>
<mk-button class="button" inline @click="remove(relay.inbox)"><fa :icon="faTrashAlt"/> {{ $t('remove') }}</mk-button>
</div>
</section>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { faPlus, faProjectDiagram } from '@fortawesome/free-solid-svg-icons';
import { faSave, faTrashAlt } from '@fortawesome/free-regular-svg-icons';
import i18n from '../../i18n';
import MkButton from '../../components/ui/button.vue';
import MkInput from '../../components/ui/input.vue';
export default Vue.extend({
i18n,
metaInfo() {
return {
title: this.$t('relays') as string
};
},
components: {
MkButton,
MkInput,
},
data() {
return {
relays: [],
inbox: '',
faPlus, faProjectDiagram, faSave, faTrashAlt
}
},
created() {
this.refresh();
},
methods: {
add(inbox: string) {
this.$root.api('admin/relays/add', {
inbox
}).then((relay: any) => {
this.refresh();
});
},
remove(inbox: string) {
this.$root.api('admin/relays/remove', {
inbox
}).then(() => {
this.refresh();
});
},
refresh() {
this.$root.api('admin/relays/list').then((relays: any) => {
this.relays = relays;
});
}
}
});
</script>
<style lang="scss" scoped>
._content.relay {
div {
margin: 0.5em 0;
}
}
</style>

View File

@ -58,6 +58,7 @@ export const router = new VueRouter({
{ path: '/instance/queue', component: page('instance/queue') }, { path: '/instance/queue', component: page('instance/queue') },
{ path: '/instance/settings', component: page('instance/settings') }, { path: '/instance/settings', component: page('instance/settings') },
{ path: '/instance/federation', component: page('instance/federation') }, { path: '/instance/federation', component: page('instance/federation') },
{ path: '/instance/relays', component: page('instance/relays') },
{ path: '/instance/announcements', component: page('instance/announcements') }, { path: '/instance/announcements', component: page('instance/announcements') },
{ path: '/notes/:note', name: 'note', component: page('note') }, { path: '/notes/:note', name: 'note', component: page('note') },
{ path: '/tags/:tag', component: page('tag') }, { path: '/tags/:tag', component: page('tag') },

View File

@ -58,6 +58,7 @@ import { AntennaNote } from '../models/entities/antenna-note';
import { PromoNote } from '../models/entities/promo-note'; import { PromoNote } from '../models/entities/promo-note';
import { PromoRead } from '../models/entities/promo-read'; import { PromoRead } from '../models/entities/promo-read';
import { program } from '../argv'; import { program } from '../argv';
import { Relay } from '../models/entities/relay';
const sqlLogger = dbLogger.createSubLogger('sql', 'white', false); const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
@ -149,6 +150,7 @@ export const entities = [
PromoRead, PromoRead,
ReversiGame, ReversiGame,
ReversiMatching, ReversiMatching,
Relay,
...charts as any ...charts as any
]; ];

36
src/misc/gen-key-pair.ts Normal file
View File

@ -0,0 +1,36 @@
import * as crypto from 'crypto';
import * as util from 'util';
const generateKeyPair = util.promisify(crypto.generateKeyPair);
export async function genRsaKeyPair(modulusLength = 2048) {
return await generateKeyPair('rsa', {
modulusLength,
publicKeyEncoding: {
type: 'spki',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem',
cipher: undefined,
passphrase: undefined
}
});
}
export async function genEcKeyPair(namedCurve: 'prime256v1' | 'secp384r1' | 'secp521r1' | 'curve25519' = 'prime256v1') {
return await generateKeyPair('ec', {
namedCurve,
publicKeyEncoding: {
type: 'spki',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem',
cipher: undefined,
passphrase: undefined
}
});
}

View File

@ -0,0 +1,19 @@
import { PrimaryColumn, Entity, Index, Column } from 'typeorm';
import { id } from '../id';
@Entity()
export class Relay {
@PrimaryColumn(id())
public id: string;
@Index({ unique: true })
@Column('varchar', {
length: 512, nullable: false,
})
public inbox: string;
@Column('enum', {
enum: ['requesting', 'accepted', 'rejected'],
})
public status: 'requesting' | 'accepted' | 'rejected';
}

View File

@ -52,6 +52,7 @@ import { AntennaNote } from './entities/antenna-note';
import { PromoNote } from './entities/promo-note'; import { PromoNote } from './entities/promo-note';
import { PromoRead } from './entities/promo-read'; import { PromoRead } from './entities/promo-read';
import { EmojiRepository } from './repositories/emoji'; import { EmojiRepository } from './repositories/emoji';
import { RelayRepository } from './repositories/relay';
export const Announcements = getRepository(Announcement); export const Announcements = getRepository(Announcement);
export const AnnouncementReads = getRepository(AnnouncementRead); export const AnnouncementReads = getRepository(AnnouncementRead);
@ -106,3 +107,4 @@ export const Antennas = getCustomRepository(AntennaRepository);
export const AntennaNotes = getRepository(AntennaNote); export const AntennaNotes = getRepository(AntennaNote);
export const PromoNotes = getRepository(PromoNote); export const PromoNotes = getRepository(PromoNote);
export const PromoReads = getRepository(PromoRead); export const PromoReads = getRepository(PromoRead);
export const Relays = getCustomRepository(RelayRepository);

View File

@ -0,0 +1,6 @@
import { EntityRepository, Repository } from 'typeorm';
import { Relay } from '../entities/relay';
@EntityRepository(Relay)
export class RelayRepository extends Repository<Relay> {
}

View File

@ -56,12 +56,10 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
} }
// HTTP-Signatureの検証 // HTTP-Signatureの検証
if (!httpSignature.verifySignature(signature, authUser.key.keyPem)) { const httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem);
return 'signature verification failed';
}
// signatureのsignerは、activity.actorと一致する必要がある // また、signatureのsignerは、activity.actorと一致する必要がある
if (authUser.user.uri !== activity.actor) { if (!httpSignatureValidated || authUser.user.uri !== activity.actor) {
// 一致しなくても、でもLD-Signatureがありそうならそっちも見る // 一致しなくても、でもLD-Signatureがありそうならそっちも見る
if (activity.signature) { if (activity.signature) {
if (activity.signature.type !== 'RsaSignature2017') { if (activity.signature.type !== 'RsaSignature2017') {
@ -93,7 +91,7 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
return `skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`; return `skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`;
} }
} else { } else {
return 'signature verification failed'; throw `skip: http-signature verification failed.`;
} }
} }

View File

@ -2,6 +2,7 @@ import { IRemoteUser } from '../../../../models/entities/user';
import accept from '../../../../services/following/requests/accept'; import accept from '../../../../services/following/requests/accept';
import { IFollow } from '../../type'; import { IFollow } from '../../type';
import DbResolver from '../../db-resolver'; import DbResolver from '../../db-resolver';
import { relayAccepted } from '../../../../services/relay';
export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => { export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => {
// ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある // ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある
@ -17,6 +18,12 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<string> =>
return `skip: follower is not a local user`; return `skip: follower is not a local user`;
} }
// relay
const match = activity.id?.match(/follow-relay\/(\w+)/);
if (match) {
return await relayAccepted(match[1]);
}
await accept(actor, follower); await accept(actor, follower);
return `ok`; return `ok`;
}; };

View File

@ -2,6 +2,7 @@ import { IRemoteUser } from '../../../../models/entities/user';
import reject from '../../../../services/following/requests/reject'; import reject from '../../../../services/following/requests/reject';
import { IFollow } from '../../type'; import { IFollow } from '../../type';
import DbResolver from '../../db-resolver'; import DbResolver from '../../db-resolver';
import { relayRejected } from '../../../../services/relay';
export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => { export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => {
// ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある // ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある
@ -17,6 +18,12 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<string> =>
return `skip: follower is not a local user`; return `skip: follower is not a local user`;
} }
// relay
const match = activity.id?.match(/follow-relay\/(\w+)/);
if (match) {
return await relayRejected(match[1]);
}
await reject(actor, follower); await reject(actor, follower);
return `ok`; return `ok`;
}; };

View File

@ -70,6 +70,7 @@ export class LdSignature {
const transformedData = { ...data }; const transformedData = { ...data };
delete transformedData['signature']; delete transformedData['signature'];
const cannonidedData = await this.normalize(transformedData); const cannonidedData = await this.normalize(transformedData);
if (this.debug) console.debug(`cannonidedData: ${cannonidedData}`);
const documentHash = this.sha256(cannonidedData); const documentHash = this.sha256(cannonidedData);
const verifyData = `${optionsHash}${documentHash}`; const verifyData = `${optionsHash}${documentHash}`;
return verifyData; return verifyData;

View File

@ -0,0 +1,14 @@
import config from '../../../config';
import { Relay } from '../../../models/entities/relay';
import { ILocalUser } from '../../../models/entities/user';
export function renderFollowRelay(relay: Relay, relayActor: ILocalUser) {
const follow = {
id: `${config.url}/activities/follow-relay/${relay.id}`,
type: 'Follow',
actor: `${config.url}/users/${relayActor.id}`,
object: 'https://www.w3.org/ns/activitystreams#Public'
};
return follow;
}

View File

@ -1,7 +1,12 @@
import config from '../../../config'; import config from '../../../config';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { IActivity } from '../type';
import { LdSignature } from '../misc/ld-signature';
import { ILocalUser } from '../../../models/entities/user';
import { UserKeypairs } from '../../../models';
import { ensure } from '../../../prelude/ensure';
export const renderActivity = (x: any) => { export const renderActivity = (x: any): IActivity | null => {
if (x == null) return null; if (x == null) return null;
if (x !== null && typeof x === 'object' && x.id == null) { if (x !== null && typeof x === 'object' && x.id == null) {
@ -11,8 +16,46 @@ export const renderActivity = (x: any) => {
return Object.assign({ return Object.assign({
'@context': [ '@context': [
'https://www.w3.org/ns/activitystreams', 'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1', 'https://w3id.org/security/v1'
{ Hashtag: 'as:Hashtag' }
] ]
}, x); }, x);
}; };
export const attachLdSignature = async (activity: any, user: ILocalUser): Promise<IActivity | null> => {
if (activity == null) return null;
const keypair = await UserKeypairs.findOne({
userId: user.id
}).then(ensure);
const obj = {
// as non-standards
manuallyApprovesFollowers: 'as:manuallyApprovesFollowers',
sensitive: 'as:sensitive',
Hashtag: 'as:Hashtag',
quoteUrl: 'as:quoteUrl',
// Mastodon
toot: 'http://joinmastodon.org/ns#',
Emoji: 'toot:Emoji',
featured: 'toot:featured',
// schema
schema: 'http://schema.org#',
PropertyValue: 'schema:PropertyValue',
value: 'schema:value',
// Misskey
misskey: `${config.url}/ns#`,
'_misskey_content': 'misskey:_misskey_content',
'_misskey_quote': 'misskey:_misskey_quote',
'_misskey_reaction': 'misskey:_misskey_reaction',
'_misskey_votes': 'misskey:_misskey_votes',
'_misskey_talk': 'misskey:_misskey_talk',
};
activity['@context'].push(obj);
const ldSignature = new LdSignature();
ldSignature.debug = false;
activity = await ldSignature.signRsaSignature2017(activity, keypair.privateKey, `${config.url}/users/${user.id}#main-key`);
return activity;
};

View File

@ -13,6 +13,7 @@ import { ensure } from '../../../prelude/ensure';
export async function renderPerson(user: ILocalUser) { export async function renderPerson(user: ILocalUser) {
const id = `${config.url}/users/${user.id}`; const id = `${config.url}/users/${user.id}`;
const isSystem = !!user.username.match(/\./);
const [avatar, banner, profile] = await Promise.all([ const [avatar, banner, profile] = await Promise.all([
user.avatarId ? DriveFiles.findOne(user.avatarId) : Promise.resolve(undefined), user.avatarId ? DriveFiles.findOne(user.avatarId) : Promise.resolve(undefined),
@ -52,7 +53,7 @@ export async function renderPerson(user: ILocalUser) {
const keypair = await UserKeypairs.findOne(user.id).then(ensure); const keypair = await UserKeypairs.findOne(user.id).then(ensure);
return { return {
type: user.isBot ? 'Service' : 'Person', type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person',
id, id,
inbox: `${id}/inbox`, inbox: `${id}/inbox`,
outbox: `${id}/outbox`, outbox: `${id}/outbox`,

View File

@ -0,0 +1,24 @@
import $ from 'cafy';
import define from '../../../define';
import { addRelay } from '../../../../../services/relay';
export const meta = {
desc: {
'ja-JP': 'Add relay'
},
tags: ['admin'],
requireCredential: true as const,
requireModerator: true as const,
params: {
inbox: {
validator: $.str
},
},
};
export default define(meta, async (ps, user) => {
return await addRelay(ps.inbox);
});

View File

@ -0,0 +1,20 @@
import define from '../../../define';
import { listRelay } from '../../../../../services/relay';
export const meta = {
desc: {
'ja-JP': 'List relay'
},
tags: ['admin'],
requireCredential: true as const,
requireModerator: true as const,
params: {
},
};
export default define(meta, async (ps, user) => {
return await listRelay();
});

View File

@ -0,0 +1,24 @@
import $ from 'cafy';
import define from '../../../define';
import { removeRelay } from '../../../../../services/relay';
export const meta = {
desc: {
'ja-JP': 'Remove relay'
},
tags: ['admin'],
requireCredential: true as const,
requireModerator: true as const,
params: {
inbox: {
validator: $.str
},
},
};
export default define(meta, async (ps, user) => {
return await removeRelay(ps.inbox);
});

View File

@ -0,0 +1,59 @@
import * as bcrypt from 'bcryptjs';
import { v4 as uuid } from 'uuid';
import generateNativeUserToken from '../server/api/common/generate-native-user-token';
import { genRsaKeyPair } from '../misc/gen-key-pair';
import { User } from '../models/entities/user';
import { UserProfile } from '../models/entities/user-profile';
import { getConnection } from 'typeorm';
import { genId } from '../misc/gen-id';
import { UserKeypair } from '../models/entities/user-keypair';
import { UsedUsername } from '../models/entities/used-username';
export async function createSystemUser(username: string) {
const password = uuid();
// Generate hash of password
const salt = await bcrypt.genSalt(8);
const hash = await bcrypt.hash(password, salt);
// Generate secret
const secret = generateNativeUserToken();
const keyPair = await genRsaKeyPair(4096);
let account!: User;
// Start transaction
await getConnection().transaction(async transactionalEntityManager => {
account = await transactionalEntityManager.save(new User({
id: genId(),
createdAt: new Date(),
username: username,
usernameLower: username.toLowerCase(),
host: null,
token: secret,
isAdmin: false,
isLocked: true,
isBot: true,
}));
await transactionalEntityManager.save(new UserKeypair({
publicKey: keyPair.publicKey,
privateKey: keyPair.privateKey,
userId: account.id
}));
await transactionalEntityManager.save(new UserProfile({
userId: account.id,
autoAcceptFollowed: false,
password: hash,
}));
await transactionalEntityManager.save(new UsedUsername({
createdAt: new Date(),
username: username.toLowerCase(),
}));
});
return account;
}

View File

@ -9,6 +9,7 @@ import { Notes, UserNotePinings, Users } from '../../models';
import { UserNotePining } from '../../models/entities/user-note-pinings'; import { UserNotePining } from '../../models/entities/user-note-pinings';
import { genId } from '../../misc/gen-id'; import { genId } from '../../misc/gen-id';
import { deliverToFollowers } from '../../remote/activitypub/deliver-manager'; import { deliverToFollowers } from '../../remote/activitypub/deliver-manager';
import { deliverToRelays } from '../relay';
/** /**
* 稿 * 稿
@ -87,4 +88,5 @@ export async function deliverPinnedChange(userId: User['id'], noteId: Note['id']
const content = renderActivity(isAddition ? renderAdd(user, target, item) : renderRemove(user, target, item)); const content = renderActivity(isAddition ? renderAdd(user, target, item) : renderRemove(user, target, item));
deliverToFollowers(user, content); deliverToFollowers(user, content);
deliverToRelays(user, content);
} }

View File

@ -4,6 +4,7 @@ import { Users } from '../../models';
import { User } from '../../models/entities/user'; import { User } from '../../models/entities/user';
import { renderPerson } from '../../remote/activitypub/renderer/person'; import { renderPerson } from '../../remote/activitypub/renderer/person';
import { deliverToFollowers } from '../../remote/activitypub/deliver-manager'; import { deliverToFollowers } from '../../remote/activitypub/deliver-manager';
import { deliverToRelays } from '../relay';
export async function publishToFollowers(userId: User['id']) { export async function publishToFollowers(userId: User['id']) {
const user = await Users.findOne(userId); const user = await Users.findOne(userId);
@ -13,5 +14,6 @@ export async function publishToFollowers(userId: User['id']) {
if (Users.isLocalUser(user)) { if (Users.isLocalUser(user)) {
const content = renderActivity(renderUpdate(await renderPerson(user), user)); const content = renderActivity(renderUpdate(await renderPerson(user), user));
deliverToFollowers(user, content); deliverToFollowers(user, content);
deliverToRelays(user, content);
} }
} }

View File

@ -31,6 +31,7 @@ import { ensure } from '../../prelude/ensure';
import { checkHitAntenna } from '../../misc/check-hit-antenna'; import { checkHitAntenna } from '../../misc/check-hit-antenna';
import { addNoteToAntenna } from '../add-note-to-antenna'; import { addNoteToAntenna } from '../add-note-to-antenna';
import { countSameRenotes } from '../../misc/count-same-renotes'; import { countSameRenotes } from '../../misc/count-same-renotes';
import { deliverToRelays } from '../relay';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@ -349,6 +350,10 @@ export default async (user: User, data: Option, silent = false) => new Promise<N
dm.addFollowersRecipe(); dm.addFollowersRecipe();
} }
if (['public'].includes(note.visibility)) {
deliverToRelays(user, noteActivity);
}
dm.execute(); dm.execute();
})(); })();
} }

View File

@ -12,6 +12,7 @@ import { Notes, Users, Instances } from '../../models';
import { notesChart, perUserNotesChart, instanceChart } from '../chart'; import { notesChart, perUserNotesChart, instanceChart } from '../chart';
import { deliverToFollowers } from '../../remote/activitypub/deliver-manager'; import { deliverToFollowers } from '../../remote/activitypub/deliver-manager';
import { countSameRenotes } from '../../misc/count-same-renotes'; import { countSameRenotes } from '../../misc/count-same-renotes';
import { deliverToRelays } from '../relay';
/** /**
* 稿 * 稿
@ -48,6 +49,7 @@ export default async function(user: User, note: Note, quiet = false) {
: renderDelete(renderTombstone(`${config.url}/notes/${note.id}`), user)); : renderDelete(renderTombstone(`${config.url}/notes/${note.id}`), user));
deliverToFollowers(user, content); deliverToFollowers(user, content);
deliverToRelays(user, content);
} }
// also deliever delete activity to cascaded notes // also deliever delete activity to cascaded notes

View File

@ -4,6 +4,7 @@ import renderNote from '../../../remote/activitypub/renderer/note';
import { Users, Notes } from '../../../models'; import { Users, Notes } from '../../../models';
import { Note } from '../../../models/entities/note'; import { Note } from '../../../models/entities/note';
import { deliverToFollowers } from '../../../remote/activitypub/deliver-manager'; import { deliverToFollowers } from '../../../remote/activitypub/deliver-manager';
import { deliverToRelays } from '../../relay';
export async function deliverQuestionUpdate(noteId: Note['id']) { export async function deliverQuestionUpdate(noteId: Note['id']) {
const note = await Notes.findOne(noteId); const note = await Notes.findOne(noteId);
@ -16,5 +17,6 @@ export async function deliverQuestionUpdate(noteId: Note['id']) {
const content = renderActivity(renderUpdate(await renderNote(note, false), user)); const content = renderActivity(renderUpdate(await renderNote(note, false), user));
deliverToFollowers(user, content); deliverToFollowers(user, content);
deliverToRelays(user, content);
} }
} }

96
src/services/relay.ts Normal file
View File

@ -0,0 +1,96 @@
import { createSystemUser } from './create-system-user';
import { renderFollowRelay } from '../remote/activitypub/renderer/follow-relay';
import { renderActivity, attachLdSignature } from '../remote/activitypub/renderer';
import renderUndo from '../remote/activitypub/renderer/undo';
import { deliver } from '../queue';
import { ILocalUser } from '../models/entities/user';
import { Users, Relays } from '../models';
import { genId } from '../misc/gen-id';
const ACTOR_USERNAME = 'relay.actor' as const;
export async function getRelayActor(): Promise<ILocalUser> {
const user = await Users.findOne({
host: null,
username: ACTOR_USERNAME
});
if (user) return user as ILocalUser;
const created = await createSystemUser(ACTOR_USERNAME);
return created as ILocalUser;
}
export async function addRelay(inbox: string) {
const relay = await Relays.save({
id: genId(),
inbox,
status: 'requesting'
});
const relayActor = await getRelayActor();
const follow = await renderFollowRelay(relay, relayActor);
const activity = renderActivity(follow);
deliver(relayActor, activity, relay.inbox);
return relay;
}
export async function removeRelay(inbox: string) {
const relay = await Relays.findOne({
inbox
});
if (relay == null) {
throw 'relay not found';
}
const relayActor = await getRelayActor();
const follow = renderFollowRelay(relay, relayActor);
const undo = renderUndo(follow, relayActor);
const activity = renderActivity(undo);
deliver(relayActor, activity, relay.inbox);
await Relays.delete(relay.id);
}
export async function listRelay() {
const relays = await Relays.find();
return relays;
}
export async function relayAccepted(id: string) {
const result = await Relays.update(id, {
status: 'accepted'
});
return JSON.stringify(result);
}
export async function relayRejected(id: string) {
const result = await Relays.update(id, {
status: 'rejected'
});
return JSON.stringify(result);
}
export async function deliverToRelays(user: ILocalUser, activity: any) {
if (activity == null) return;
const relays = await Relays.find({
status: 'accepted'
});
if (relays.length === 0) return;
const relayActor = await getRelayActor();
const copy = JSON.parse(JSON.stringify(activity));
if (!copy.to) copy.to = ['https://www.w3.org/ns/activitystreams#Public'];
const signed = await attachLdSignature(copy, user);
for (const relay of relays) {
deliver(relayActor, signed, relay.inbox);
}
}