summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMeiMei <30769358+mei23@users.noreply.github.com>2020-05-10 18:42:31 +0900
committerGitHub <noreply@github.com>2020-05-10 18:42:31 +0900
commit145389768d434c46bd24662488294eead7d3addb (patch)
tree1530f59a5c189d25500c18fc5ef21b3173b1321f /src
parentfix(server): Remove koa-compress (diff)
downloadmisskey-145389768d434c46bd24662488294eead7d3addb.tar.gz
misskey-145389768d434c46bd24662488294eead7d3addb.tar.bz2
misskey-145389768d434c46bd24662488294eead7d3addb.zip
pub-relay (#6341)
* pub-relay * relay actorをApplicationにする * Disable koa-compress * Homeはリレーに送らない * Disable debug * UI * cleanupなど
Diffstat (limited to 'src')
-rw-r--r--src/client/app.vue9
-rw-r--r--src/client/pages/instance/relays.vue93
-rw-r--r--src/client/router.ts1
-rw-r--r--src/db/postgre.ts2
-rw-r--r--src/misc/gen-key-pair.ts36
-rw-r--r--src/models/entities/relay.ts19
-rw-r--r--src/models/index.ts2
-rw-r--r--src/models/repositories/relay.ts6
-rw-r--r--src/queue/processors/inbox.ts10
-rw-r--r--src/remote/activitypub/kernel/accept/follow.ts7
-rw-r--r--src/remote/activitypub/kernel/reject/follow.ts7
-rw-r--r--src/remote/activitypub/misc/ld-signature.ts1
-rw-r--r--src/remote/activitypub/renderer/follow-relay.ts14
-rw-r--r--src/remote/activitypub/renderer/index.ts49
-rw-r--r--src/remote/activitypub/renderer/person.ts3
-rw-r--r--src/server/api/endpoints/admin/relays/add.ts24
-rw-r--r--src/server/api/endpoints/admin/relays/list.ts20
-rw-r--r--src/server/api/endpoints/admin/relays/remove.ts24
-rw-r--r--src/services/create-system-user.ts59
-rw-r--r--src/services/i/pin.ts2
-rw-r--r--src/services/i/update.ts2
-rw-r--r--src/services/note/create.ts5
-rw-r--r--src/services/note/delete.ts2
-rw-r--r--src/services/note/polls/update.ts2
-rw-r--r--src/services/relay.ts96
25 files changed, 483 insertions, 12 deletions
diff --git a/src/client/app.vue b/src/client/app.vue
index 170ba9365d..5e7396205b 100644
--- a/src/client/app.vue
+++ b/src/client/app.vue
@@ -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
};
},
@@ -415,6 +415,11 @@ export default Vue.extend({
icon: faGlobe,
}, {
type: 'link',
+ text: this.$t('relays'),
+ to: '/instance/relays',
+ icon: faProjectDiagram,
+ }, {
+ type: 'link',
text: this.$t('announcements'),
to: '/instance/announcements',
icon: faBroadcastTower,
diff --git a/src/client/pages/instance/relays.vue b/src/client/pages/instance/relays.vue
new file mode 100644
index 0000000000..568f5edd71
--- /dev/null
+++ b/src/client/pages/instance/relays.vue
@@ -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>
diff --git a/src/client/router.ts b/src/client/router.ts
index e997d2db99..cf98c57bd7 100644
--- a/src/client/router.ts
+++ b/src/client/router.ts
@@ -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') },
diff --git a/src/db/postgre.ts b/src/db/postgre.ts
index 9e3eb3f7d6..81fb92f684 100644
--- a/src/db/postgre.ts
+++ b/src/db/postgre.ts
@@ -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
];
diff --git a/src/misc/gen-key-pair.ts b/src/misc/gen-key-pair.ts
new file mode 100644
index 0000000000..d4a8fa7534
--- /dev/null
+++ b/src/misc/gen-key-pair.ts
@@ -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
+ }
+ });
+}
diff --git a/src/models/entities/relay.ts b/src/models/entities/relay.ts
new file mode 100644
index 0000000000..4c82ccb125
--- /dev/null
+++ b/src/models/entities/relay.ts
@@ -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';
+}
diff --git a/src/models/index.ts b/src/models/index.ts
index c3b329f4f8..e1389e7353 100644
--- a/src/models/index.ts
+++ b/src/models/index.ts
@@ -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);
diff --git a/src/models/repositories/relay.ts b/src/models/repositories/relay.ts
new file mode 100644
index 0000000000..601bb5eb39
--- /dev/null
+++ b/src/models/repositories/relay.ts
@@ -0,0 +1,6 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Relay } from '../entities/relay';
+
+@EntityRepository(Relay)
+export class RelayRepository extends Repository<Relay> {
+}
diff --git a/src/queue/processors/inbox.ts b/src/queue/processors/inbox.ts
index f37f663ed5..3a0bdbe28a 100644
--- a/src/queue/processors/inbox.ts
+++ b/src/queue/processors/inbox.ts
@@ -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.`;
}
}
diff --git a/src/remote/activitypub/kernel/accept/follow.ts b/src/remote/activitypub/kernel/accept/follow.ts
index c067f7622a..71c1bed9de 100644
--- a/src/remote/activitypub/kernel/accept/follow.ts
+++ b/src/remote/activitypub/kernel/accept/follow.ts
@@ -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`;
};
diff --git a/src/remote/activitypub/kernel/reject/follow.ts b/src/remote/activitypub/kernel/reject/follow.ts
index 49e82c7afc..d97ced46b3 100644
--- a/src/remote/activitypub/kernel/reject/follow.ts
+++ b/src/remote/activitypub/kernel/reject/follow.ts
@@ -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`;
};
diff --git a/src/remote/activitypub/misc/ld-signature.ts b/src/remote/activitypub/misc/ld-signature.ts
index d61b430f7a..070e39edfb 100644
--- a/src/remote/activitypub/misc/ld-signature.ts
+++ b/src/remote/activitypub/misc/ld-signature.ts
@@ -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;
diff --git a/src/remote/activitypub/renderer/follow-relay.ts b/src/remote/activitypub/renderer/follow-relay.ts
new file mode 100644
index 0000000000..58bc0c90c3
--- /dev/null
+++ b/src/remote/activitypub/renderer/follow-relay.ts
@@ -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;
+}
diff --git a/src/remote/activitypub/renderer/index.ts b/src/remote/activitypub/renderer/index.ts
index 63447b0c43..e84a7d90ac 100644
--- a/src/remote/activitypub/renderer/index.ts
+++ b/src/remote/activitypub/renderer/index.ts
@@ -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;
+};
diff --git a/src/remote/activitypub/renderer/person.ts b/src/remote/activitypub/renderer/person.ts
index 56ff10319a..bc8a462d2e 100644
--- a/src/remote/activitypub/renderer/person.ts
+++ b/src/remote/activitypub/renderer/person.ts
@@ -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`,
diff --git a/src/server/api/endpoints/admin/relays/add.ts b/src/server/api/endpoints/admin/relays/add.ts
new file mode 100644
index 0000000000..3ea6bcc73b
--- /dev/null
+++ b/src/server/api/endpoints/admin/relays/add.ts
@@ -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);
+});
diff --git a/src/server/api/endpoints/admin/relays/list.ts b/src/server/api/endpoints/admin/relays/list.ts
new file mode 100644
index 0000000000..3b132f73b3
--- /dev/null
+++ b/src/server/api/endpoints/admin/relays/list.ts
@@ -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();
+});
diff --git a/src/server/api/endpoints/admin/relays/remove.ts b/src/server/api/endpoints/admin/relays/remove.ts
new file mode 100644
index 0000000000..df95e0329a
--- /dev/null
+++ b/src/server/api/endpoints/admin/relays/remove.ts
@@ -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);
+});
diff --git a/src/services/create-system-user.ts b/src/services/create-system-user.ts
new file mode 100644
index 0000000000..7f59efb448
--- /dev/null
+++ b/src/services/create-system-user.ts
@@ -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;
+}
diff --git a/src/services/i/pin.ts b/src/services/i/pin.ts
index 9fd7263ffc..fcddc50636 100644
--- a/src/services/i/pin.ts
+++ b/src/services/i/pin.ts
@@ -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);
}
diff --git a/src/services/i/update.ts b/src/services/i/update.ts
index ae72e91345..8d40b08a85 100644
--- a/src/services/i/update.ts
+++ b/src/services/i/update.ts
@@ -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);
}
}
diff --git a/src/services/note/create.ts b/src/services/note/create.ts
index f506337924..60a62dcdff 100644
--- a/src/services/note/create.ts
+++ b/src/services/note/create.ts
@@ -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();
})();
}
diff --git a/src/services/note/delete.ts b/src/services/note/delete.ts
index dc8d23134a..11b52cd135 100644
--- a/src/services/note/delete.ts
+++ b/src/services/note/delete.ts
@@ -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
diff --git a/src/services/note/polls/update.ts b/src/services/note/polls/update.ts
index c076d13040..a33efab668 100644
--- a/src/services/note/polls/update.ts
+++ b/src/services/note/polls/update.ts
@@ -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);
}
}
diff --git a/src/services/relay.ts b/src/services/relay.ts
new file mode 100644
index 0000000000..aa3179675d
--- /dev/null
+++ b/src/services/relay.ts
@@ -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);
+ }
+}