forked from FoundKeyGang/FoundKey
pub-relay (#6341)
* pub-relay * relay actorをApplicationにする * Disable koa-compress * Homeはリレーに送らない * Disable debug * UI * cleanupなど
This commit is contained in:
parent
be183206e6
commit
145389768d
27 changed files with 510 additions and 12 deletions
|
@ -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: "拒否済み"
|
||||||
|
|
18
migration/1589023282116-pubRelay.ts
Normal file
18
migration/1589023282116-pubRelay.ts
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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'),
|
||||||
|
|
93
src/client/pages/instance/relays.vue
Normal file
93
src/client/pages/instance/relays.vue
Normal 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>
|
|
@ -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') },
|
||||||
|
|
|
@ -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
36
src/misc/gen-key-pair.ts
Normal 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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
19
src/models/entities/relay.ts
Normal file
19
src/models/entities/relay.ts
Normal 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';
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
6
src/models/repositories/relay.ts
Normal file
6
src/models/repositories/relay.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { EntityRepository, Repository } from 'typeorm';
|
||||||
|
import { Relay } from '../entities/relay';
|
||||||
|
|
||||||
|
@EntityRepository(Relay)
|
||||||
|
export class RelayRepository extends Repository<Relay> {
|
||||||
|
}
|
|
@ -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.`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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`;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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`;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
|
14
src/remote/activitypub/renderer/follow-relay.ts
Normal file
14
src/remote/activitypub/renderer/follow-relay.ts
Normal 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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
|
@ -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`,
|
||||||
|
|
24
src/server/api/endpoints/admin/relays/add.ts
Normal file
24
src/server/api/endpoints/admin/relays/add.ts
Normal 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);
|
||||||
|
});
|
20
src/server/api/endpoints/admin/relays/list.ts
Normal file
20
src/server/api/endpoints/admin/relays/list.ts
Normal 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();
|
||||||
|
});
|
24
src/server/api/endpoints/admin/relays/remove.ts
Normal file
24
src/server/api/endpoints/admin/relays/remove.ts
Normal 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);
|
||||||
|
});
|
59
src/services/create-system-user.ts
Normal file
59
src/services/create-system-user.ts
Normal 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;
|
||||||
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
96
src/services/relay.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue