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: "分割線"
|
||||
addItem: "項目を追加"
|
||||
rooms: "ルーム"
|
||||
relays: "リレー"
|
||||
addRelay: "リレーの追加"
|
||||
inboxUrl: "inboxのURL"
|
||||
addedRelays: "追加済みのリレー"
|
||||
|
||||
_theme:
|
||||
explore: "テーマを探す"
|
||||
|
@ -1090,3 +1094,8 @@ _pages:
|
|||
enviromentVariables: "環境変数"
|
||||
pageVariables: "ページ要素"
|
||||
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">
|
||||
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 { ResizeObserver } from '@juggle/resize-observer';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
@ -169,7 +169,7 @@ export default Vue.extend({
|
|||
isDesktop: window.innerWidth >= DESKTOP_THRESHOLD,
|
||||
canBack: false,
|
||||
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'),
|
||||
to: '/instance/federation',
|
||||
icon: faGlobe,
|
||||
}, {
|
||||
type: 'link',
|
||||
text: this.$t('relays'),
|
||||
to: '/instance/relays',
|
||||
icon: faProjectDiagram,
|
||||
}, {
|
||||
type: 'link',
|
||||
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/settings', component: page('instance/settings') },
|
||||
{ path: '/instance/federation', component: page('instance/federation') },
|
||||
{ path: '/instance/relays', component: page('instance/relays') },
|
||||
{ path: '/instance/announcements', component: page('instance/announcements') },
|
||||
{ path: '/notes/:note', name: 'note', component: page('note') },
|
||||
{ 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 { PromoRead } from '../models/entities/promo-read';
|
||||
import { program } from '../argv';
|
||||
import { Relay } from '../models/entities/relay';
|
||||
|
||||
const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
|
||||
|
||||
|
@ -149,6 +150,7 @@ export const entities = [
|
|||
PromoRead,
|
||||
ReversiGame,
|
||||
ReversiMatching,
|
||||
Relay,
|
||||
...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 { PromoRead } from './entities/promo-read';
|
||||
import { EmojiRepository } from './repositories/emoji';
|
||||
import { RelayRepository } from './repositories/relay';
|
||||
|
||||
export const Announcements = getRepository(Announcement);
|
||||
export const AnnouncementReads = getRepository(AnnouncementRead);
|
||||
|
@ -106,3 +107,4 @@ export const Antennas = getCustomRepository(AntennaRepository);
|
|||
export const AntennaNotes = getRepository(AntennaNote);
|
||||
export const PromoNotes = getRepository(PromoNote);
|
||||
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の検証
|
||||
if (!httpSignature.verifySignature(signature, authUser.key.keyPem)) {
|
||||
return 'signature verification failed';
|
||||
}
|
||||
const httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem);
|
||||
|
||||
// signatureのsignerは、activity.actorと一致する必要がある
|
||||
if (authUser.user.uri !== activity.actor) {
|
||||
// また、signatureのsignerは、activity.actorと一致する必要がある
|
||||
if (!httpSignatureValidated || authUser.user.uri !== activity.actor) {
|
||||
// 一致しなくても、でもLD-Signatureがありそうならそっちも見る
|
||||
if (activity.signature) {
|
||||
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})`;
|
||||
}
|
||||
} 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 { IFollow } from '../../type';
|
||||
import DbResolver from '../../db-resolver';
|
||||
import { relayAccepted } from '../../../../services/relay';
|
||||
|
||||
export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => {
|
||||
// ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある
|
||||
|
@ -17,6 +18,12 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<string> =>
|
|||
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);
|
||||
return `ok`;
|
||||
};
|
||||
|
|
|
@ -2,6 +2,7 @@ import { IRemoteUser } from '../../../../models/entities/user';
|
|||
import reject from '../../../../services/following/requests/reject';
|
||||
import { IFollow } from '../../type';
|
||||
import DbResolver from '../../db-resolver';
|
||||
import { relayRejected } from '../../../../services/relay';
|
||||
|
||||
export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => {
|
||||
// ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある
|
||||
|
@ -17,6 +18,12 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<string> =>
|
|||
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);
|
||||
return `ok`;
|
||||
};
|
||||
|
|
|
@ -70,6 +70,7 @@ export class LdSignature {
|
|||
const transformedData = { ...data };
|
||||
delete transformedData['signature'];
|
||||
const cannonidedData = await this.normalize(transformedData);
|
||||
if (this.debug) console.debug(`cannonidedData: ${cannonidedData}`);
|
||||
const documentHash = this.sha256(cannonidedData);
|
||||
const verifyData = `${optionsHash}${documentHash}`;
|
||||
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 { 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 && typeof x === 'object' && x.id == null) {
|
||||
|
@ -11,8 +16,46 @@ export const renderActivity = (x: any) => {
|
|||
return Object.assign({
|
||||
'@context': [
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
'https://w3id.org/security/v1',
|
||||
{ Hashtag: 'as:Hashtag' }
|
||||
'https://w3id.org/security/v1'
|
||||
]
|
||||
}, 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) {
|
||||
const id = `${config.url}/users/${user.id}`;
|
||||
const isSystem = !!user.username.match(/\./);
|
||||
|
||||
const [avatar, banner, profile] = await Promise.all([
|
||||
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);
|
||||
|
||||
return {
|
||||
type: user.isBot ? 'Service' : 'Person',
|
||||
type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person',
|
||||
id,
|
||||
inbox: `${id}/inbox`,
|
||||
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 { genId } from '../../misc/gen-id';
|
||||
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));
|
||||
|
||||
deliverToFollowers(user, content);
|
||||
deliverToRelays(user, content);
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import { Users } from '../../models';
|
|||
import { User } from '../../models/entities/user';
|
||||
import { renderPerson } from '../../remote/activitypub/renderer/person';
|
||||
import { deliverToFollowers } from '../../remote/activitypub/deliver-manager';
|
||||
import { deliverToRelays } from '../relay';
|
||||
|
||||
export async function publishToFollowers(userId: User['id']) {
|
||||
const user = await Users.findOne(userId);
|
||||
|
@ -13,5 +14,6 @@ export async function publishToFollowers(userId: User['id']) {
|
|||
if (Users.isLocalUser(user)) {
|
||||
const content = renderActivity(renderUpdate(await renderPerson(user), user));
|
||||
deliverToFollowers(user, content);
|
||||
deliverToRelays(user, content);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ import { ensure } from '../../prelude/ensure';
|
|||
import { checkHitAntenna } from '../../misc/check-hit-antenna';
|
||||
import { addNoteToAntenna } from '../add-note-to-antenna';
|
||||
import { countSameRenotes } from '../../misc/count-same-renotes';
|
||||
import { deliverToRelays } from '../relay';
|
||||
|
||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||
|
||||
|
@ -349,6 +350,10 @@ export default async (user: User, data: Option, silent = false) => new Promise<N
|
|||
dm.addFollowersRecipe();
|
||||
}
|
||||
|
||||
if (['public'].includes(note.visibility)) {
|
||||
deliverToRelays(user, noteActivity);
|
||||
}
|
||||
|
||||
dm.execute();
|
||||
})();
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import { Notes, Users, Instances } from '../../models';
|
|||
import { notesChart, perUserNotesChart, instanceChart } from '../chart';
|
||||
import { deliverToFollowers } from '../../remote/activitypub/deliver-manager';
|
||||
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));
|
||||
|
||||
deliverToFollowers(user, content);
|
||||
deliverToRelays(user, content);
|
||||
}
|
||||
|
||||
// also deliever delete activity to cascaded notes
|
||||
|
|
|
@ -4,6 +4,7 @@ import renderNote from '../../../remote/activitypub/renderer/note';
|
|||
import { Users, Notes } from '../../../models';
|
||||
import { Note } from '../../../models/entities/note';
|
||||
import { deliverToFollowers } from '../../../remote/activitypub/deliver-manager';
|
||||
import { deliverToRelays } from '../../relay';
|
||||
|
||||
export async function deliverQuestionUpdate(noteId: Note['id']) {
|
||||
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));
|
||||
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