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…
	
	Add table
		Add a link
		
	
		Reference in a new issue