forked from FoundKeyGang/FoundKey
Compare commits
40 commits
9c5755cf65
...
77358c8f4b
Author | SHA1 | Date | |
---|---|---|---|
77358c8f4b | |||
62cd1e7ed6 | |||
796adc8599 | |||
ff8b9b6651 | |||
c1268c04f8 | |||
42b555e5e4 | |||
5c3e7c132a | |||
76c8e6b11b | |||
ca24080596 | |||
a12debb7b6 | |||
f760426142 | |||
2f30af1812 | |||
2d46cf7c1e | |||
2ea6daaf7a | |||
597de07465 | |||
9289b0e8ed | |||
b600efae0d | |||
ecca5a164e | |||
1125a623a7 | |||
51a319e8ca | |||
f7904a240a | |||
693dd3ad97 | |||
777b981bf1 | |||
bbd35054e6 | |||
|
e9862d480c | ||
cc0915775b | |||
f181a8805d | |||
ac482e6eec | |||
38786b6999 | |||
680d1f1459 | |||
a0f0bac1ca | |||
af003fc0fe | |||
94cd10365d | |||
0addcddd6c | |||
64c4973eca | |||
7272bde464 | |||
7c9e118ff1 | |||
bed8286175 | |||
f00b3cc378 | |||
21ab8e75ee |
100 changed files with 1587 additions and 1188 deletions
|
@ -153,3 +153,9 @@ redis:
|
|||
# info: /twemoji/1f440.svg
|
||||
# notFound: /twemoji/2049.svg
|
||||
# error: /twemoji/1f480.svg
|
||||
|
||||
# Whether it should be allowed to fetch content in ActivityPub form without HTTP signatures.
|
||||
# It is recommended to leave this as default to improve the effectiveness of instance blocks.s
|
||||
# However, note that while this prevents fetching in ActivityPub form, it could still be scraped
|
||||
# from the API or other representations if the other side is determined to do so.
|
||||
#allowUnsignedFetches: false
|
||||
|
|
2
.mailmap
2
.mailmap
|
@ -6,7 +6,7 @@ Chloe Kudryavtsev <code@toast.bunkerlabs.net> <toast+git@toast.cafe>
|
|||
Chloe Kudryavtsev <code@toast.bunkerlabs.net> <toast@toast.cafe>
|
||||
Dr. Gutfuck LLC <40531868+gutfuckllc@users.noreply.github.com>
|
||||
Ehsan Javadynia <31900907+ehsanjavadynia@users.noreply.github.com> <ehsan.javadynia@gmail.com>
|
||||
Francis Dinh <normandy@biribiri.dev>
|
||||
Norm <normandy@biribiri.dev>
|
||||
Hakaba Hitoyo <tsukadayoshio@gmail.com> Hakaba Hitoyo <example@example.com>
|
||||
Johann150 <johann.galle@protonmail.com> <johann@qwertqwefsday.eu>
|
||||
Michcio <public+git@meekchopp.es> <michcio@noreply.akkoma>
|
||||
|
|
27
CHANGELOG.md
27
CHANGELOG.md
|
@ -11,6 +11,33 @@ Unreleased changes should not be listed in this file.
|
|||
Instead, run `git shortlog --format='%h %s' --group=trailer:changelog <last tag>..` to see unreleased changes; replace `<last tag>` with the tag you wish to compare from.
|
||||
If you are a contributor, please read [CONTRIBUTING.md, section "Changelog Trailer"](./CONTRIBUTING.md#changelog-trailer) on what to do instead.
|
||||
|
||||
## 13.0.0-preview6 - 2023-07-02
|
||||
|
||||
## Added
|
||||
- **BREAKING** activitypub: validate fetch signatures
|
||||
Fetching the ActivityPub representation of something now requires a valid HTTP signature.
|
||||
- client: add MFM functions `position`, `scale`, `fg`, `bg`
|
||||
- server: add webhook stat to nodeinfo
|
||||
- activitypub: handle incoming Update Note activities
|
||||
|
||||
## Changed
|
||||
- client: change followers only icon to closed lock
|
||||
- client: disable sound for received note by default
|
||||
- client: always forbid MFM overflow
|
||||
- make mutes case insensitive
|
||||
- activitypub: improve JSON-LD context
|
||||
The context now properly notes the `@type`s of defined attributes.
|
||||
- docker: only publish port on localhost
|
||||
|
||||
## Fixed
|
||||
- server: fix internal download in emoji import
|
||||
- server: replace unzipper with decompress
|
||||
|
||||
## Removed
|
||||
- migrate note favorites to clips
|
||||
If you previously had favorites they will now be in a clip called "⭐".
|
||||
If you want to add a note as a "favorite" you can use the menu item "Clip".
|
||||
|
||||
## 13.0.0-preview5 - 2023-05-23
|
||||
This release contains 6 breaking changes and 1 security update.
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ services:
|
|||
- redis
|
||||
# - es
|
||||
ports:
|
||||
- "3000:3000"
|
||||
- "127.0.0.1:3000:3000"
|
||||
networks:
|
||||
- internal_network
|
||||
- external_network
|
||||
|
|
|
@ -1309,6 +1309,7 @@ _notification:
|
|||
groupInvited: "Erhaltene Gruppeneinladungen"
|
||||
app: "Benachrichtigungen von Apps"
|
||||
move: Account-Umzüge
|
||||
updated: Beobachtete Notiz wurde bearbeitet
|
||||
_actions:
|
||||
followBack: "folgt dir nun auch"
|
||||
reply: "Antworten"
|
||||
|
|
|
@ -1304,6 +1304,7 @@ _notification:
|
|||
reaction: "Reactions"
|
||||
pollVote: "Votes on polls"
|
||||
pollEnded: "Polls ending"
|
||||
updated: "Watched Note was updated"
|
||||
receiveFollowRequest: "Received follow requests"
|
||||
followRequestAccepted: "Accepted follow requests"
|
||||
groupInvited: "Group invitations"
|
||||
|
|
|
@ -748,6 +748,7 @@ _ffVisibility:
|
|||
followers: "フォロワーだけに公開"
|
||||
private: "非公開"
|
||||
|
||||
nobody: 誰にも見せない (あなたにさえも)
|
||||
_signup:
|
||||
almostThere: "ほとんど完了です"
|
||||
emailAddressInfo: "あなたが使っているメールアドレスを入力してください。メールアドレスが公開されることはありません。"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "foundkey",
|
||||
"version": "13.0.0-preview5",
|
||||
"version": "13.0.0-preview6",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://akkoma.dev/FoundKeyGang/FoundKey.git"
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
{
|
||||
"extension": ["ts","js","cjs","mjs"],
|
||||
"node-option": [
|
||||
"experimental-specifier-resolution=node",
|
||||
"loader=./test/loader.js"
|
||||
"experimental-specifier-resolution=node"
|
||||
],
|
||||
"slow": 1000,
|
||||
"timeout": 30000,
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
export class noteEditing1685997617959 {
|
||||
name = 'noteEditing1685997617959';
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "note" ADD "updatedAt" TIMESTAMP WITH TIME ZONE`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "note"."updatedAt" IS 'The updated date of the Note.'`);
|
||||
|
||||
await queryRunner.query(`ALTER TYPE "public"."notification_type_enum" RENAME TO "notification_type_enum_old"`);
|
||||
await queryRunner.query(`CREATE TYPE "public"."notification_type_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'move', 'app', 'updated')`);
|
||||
await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum" USING "type"::"text"::"public"."notification_type_enum"`);
|
||||
await queryRunner.query(`DROP TYPE "public"."notification_type_enum_old"`);
|
||||
await queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum" RENAME TO "user_profile_mutingnotificationtypes_enum_old"`);
|
||||
await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'move', 'app', 'updated')`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum"[]`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`);
|
||||
await queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum_old"`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum_old" AS ENUM('move', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'pollEnded')`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum_old"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum_old"[]`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`);
|
||||
await queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum"`);
|
||||
await queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum_old" RENAME TO "user_profile_mutingnotificationtypes_enum"`);
|
||||
await queryRunner.query(`CREATE TYPE "public"."notification_type_enum_old" AS ENUM('move', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`);
|
||||
await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum_old" USING "type"::"text"::"public"."notification_type_enum_old"`);
|
||||
await queryRunner.query(`DROP TYPE "public"."notification_type_enum"`);
|
||||
await queryRunner.query(`ALTER TYPE "public"."notification_type_enum_old" RENAME TO "notification_type_enum"`);
|
||||
|
||||
await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "updatedAt"`);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "backend",
|
||||
"version": "13.0.0-preview5",
|
||||
"version": "13.0.0-preview6",
|
||||
"main": "./index.js",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
@ -8,10 +8,10 @@
|
|||
"build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json",
|
||||
"watch": "node watch.mjs",
|
||||
"lint": "tsc --noEmit --skipLibCheck && eslint src --ext .ts",
|
||||
"mocha": "cross-env NODE_ENV=test TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" mocha",
|
||||
"mocha": "NODE_ENV=test mocha",
|
||||
"migrate": "npx typeorm migration:run -d ormconfig.js",
|
||||
"start": "node --experimental-json-modules ./built/index.js",
|
||||
"start:test": "cross-env NODE_ENV=test node --experimental-json-modules ./built/index.js",
|
||||
"start:test": "NODE_ENV=test node --experimental-json-modules ./built/index.js",
|
||||
"test": "npm run mocha"
|
||||
},
|
||||
"dependencies": {
|
||||
|
@ -41,6 +41,7 @@
|
|||
"color-convert": "2.0.1",
|
||||
"content-disposition": "0.5.4",
|
||||
"date-fns": "2.28.0",
|
||||
"decompress": "4.2.1",
|
||||
"deep-email-validator": "0.1.21",
|
||||
"escape-regexp": "0.0.1",
|
||||
"feed": "4.2.2",
|
||||
|
@ -109,7 +110,6 @@
|
|||
"tsconfig-paths": "4.1.0",
|
||||
"twemoji-parser": "14.0.0",
|
||||
"typeorm": "0.3.7",
|
||||
"unzipper": "0.10.11",
|
||||
"uuid": "8.3.2",
|
||||
"web-push": "3.5.0",
|
||||
"ws": "8.8.0",
|
||||
|
@ -164,7 +164,6 @@
|
|||
"@types/ws": "8.5.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.46.1",
|
||||
"@typescript-eslint/parser": "^5.46.1",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "^8.29.0",
|
||||
"eslint-plugin-foundkey-custom-rules": "file:../shared/custom-rules",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
|
|
|
@ -61,6 +61,7 @@ export function loadConfig(): Config {
|
|||
proxyRemoteFiles: false,
|
||||
maxFileSize: 262144000, // 250 MiB
|
||||
maxNoteTextLength: 3000,
|
||||
allowUnsignedFetches: false,
|
||||
}, config);
|
||||
|
||||
mixin.version = meta.version;
|
||||
|
|
|
@ -68,6 +68,8 @@ export type Source = {
|
|||
notFound?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
allowUnsignedFetches?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -20,10 +20,11 @@ export async function checkWordMute(note: NoteLike, me: UserLike | null | undefi
|
|||
const text = ((note.cw ?? '') + '\n' + (note.text ?? '')).trim();
|
||||
|
||||
if (text === '') return false;
|
||||
const textLower = text.toLowerCase();
|
||||
|
||||
const matched = mutedWords.some(filter => {
|
||||
if (Array.isArray(filter)) {
|
||||
return filter.every(keyword => text.includes(keyword));
|
||||
return filter.every(keyword => textLower.includes(keyword.toLowerCase()));
|
||||
} else {
|
||||
// represents RegExp
|
||||
const regexp = filter.match(/^\/(.+)\/(.*)$/);
|
||||
|
|
|
@ -11,7 +11,7 @@ export function isSelfHost(host: string | null): boolean {
|
|||
return toPuny(config.host) === toPuny(host);
|
||||
}
|
||||
|
||||
export function extractDbHost(uri: string): string {
|
||||
export function extractPunyHost(uri: string): string {
|
||||
const url = new URL(uri);
|
||||
return toPuny(url.hostname);
|
||||
}
|
||||
|
|
|
@ -19,6 +19,12 @@ export class Note {
|
|||
})
|
||||
public createdAt: Date;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
nullable: true,
|
||||
comment: 'The updated date of the Note.',
|
||||
})
|
||||
public updatedAt: Date | null;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
|
|
|
@ -169,6 +169,7 @@ export const NoteRepository = db.getRepository(Note).extend({
|
|||
const packed: Packed<'Note'> = await awaitAll({
|
||||
id: note.id,
|
||||
createdAt: note.createdAt.toISOString(),
|
||||
updatedAt: note.updatedAt?.toISOString() ?? null,
|
||||
userId: note.userId,
|
||||
user: Users.pack(note.user ?? note.userId, me, {
|
||||
detail: false,
|
||||
|
|
|
@ -12,6 +12,11 @@ export const packedNoteSchema = {
|
|||
optional: false, nullable: false,
|
||||
format: 'date-time',
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
format: 'date-time',
|
||||
},
|
||||
text: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
|
|
|
@ -32,11 +32,6 @@ export const packedUserLiteSchema = {
|
|||
type: 'any',
|
||||
nullable: true, optional: false,
|
||||
},
|
||||
avatarColor: {
|
||||
type: 'any',
|
||||
nullable: true, optional: false,
|
||||
default: null,
|
||||
},
|
||||
isAdmin: {
|
||||
type: 'boolean',
|
||||
nullable: false, optional: true,
|
||||
|
@ -122,11 +117,6 @@ export const packedUserDetailedNotMeOnlySchema = {
|
|||
type: 'any',
|
||||
nullable: true, optional: false,
|
||||
},
|
||||
bannerColor: {
|
||||
type: 'any',
|
||||
nullable: true, optional: false,
|
||||
default: null,
|
||||
},
|
||||
isLocked: {
|
||||
type: 'boolean',
|
||||
nullable: false, optional: false,
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import * as fs from 'node:fs';
|
||||
import Bull from 'bull';
|
||||
import unzipper from 'unzipper';
|
||||
import decompress from 'decompress';
|
||||
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { createTempDir } from '@/misc/create-temp.js';
|
||||
import { downloadUrl } from '@/misc/download-url.js';
|
||||
import { genId } from '@/misc/gen-id.js';
|
||||
import { DriveFiles, Emojis } from '@/models/index.js';
|
||||
import { DbUserImportJobData } from '@/queue/types.js';
|
||||
import { queueLogger } from '@/queue/logger.js';
|
||||
import { addFile } from '@/services/drive/add-file.js';
|
||||
import { copyFileTo } from '@/services/drive/read-file.js';
|
||||
|
||||
const logger = queueLogger.createSubLogger('import-custom-emojis');
|
||||
|
||||
|
@ -33,7 +33,7 @@ export async function importCustomEmojis(job: Bull.Job<DbUserImportJobData>, don
|
|||
|
||||
try {
|
||||
fs.writeFileSync(destPath, '', 'binary');
|
||||
await downloadUrl(file.url, destPath);
|
||||
await copyFileTo(file, destPath);
|
||||
} catch (e) { // TODO: 何度か再試行
|
||||
if (e instanceof Error || typeof e === 'string') {
|
||||
logger.error(e);
|
||||
|
@ -42,44 +42,41 @@ export async function importCustomEmojis(job: Bull.Job<DbUserImportJobData>, don
|
|||
}
|
||||
|
||||
const outputPath = path + '/emojis';
|
||||
const unzipStream = fs.createReadStream(destPath);
|
||||
const extractor = unzipper.Extract({ path: outputPath });
|
||||
extractor.on('close', async () => {
|
||||
const metaRaw = fs.readFileSync(outputPath + '/meta.json', 'utf-8');
|
||||
const meta = JSON.parse(metaRaw);
|
||||
|
||||
for (const record of meta.emojis) {
|
||||
if (!record.downloaded) continue;
|
||||
if (!/^[a-zA-Z0-9_]+?([a-zA-Z0-9\.]+)?$/.test(record.fileName)) {
|
||||
this.logger.error(`invalid filename: ${record.fileName}, skipping in emoji import`);
|
||||
continue;
|
||||
}
|
||||
const emojiInfo = record.emoji;
|
||||
const emojiPath = outputPath + '/' + record.fileName;
|
||||
await Emojis.delete({
|
||||
name: emojiInfo.name,
|
||||
});
|
||||
const driveFile = await addFile({ user: null, path: emojiPath, name: record.fileName, force: true });
|
||||
await Emojis.insert({
|
||||
id: genId(),
|
||||
updatedAt: new Date(),
|
||||
name: emojiInfo.name,
|
||||
category: emojiInfo.category,
|
||||
host: null,
|
||||
aliases: emojiInfo.aliases,
|
||||
originalUrl: driveFile.url,
|
||||
publicUrl: driveFile.webpublicUrl ?? driveFile.url,
|
||||
type: driveFile.webpublicType ?? driveFile.type,
|
||||
});
|
||||
}
|
||||
|
||||
await db.queryResultCache!.remove(['meta_emojis']);
|
||||
|
||||
cleanup();
|
||||
|
||||
logger.succ('Imported');
|
||||
done();
|
||||
});
|
||||
unzipStream.pipe(extractor);
|
||||
logger.succ(`Unzipping to ${outputPath}`);
|
||||
await decompress(destPath, outputPath);
|
||||
|
||||
const metaRaw = fs.readFileSync(outputPath + '/meta.json', 'utf-8');
|
||||
const meta = JSON.parse(metaRaw);
|
||||
|
||||
for (const record of meta.emojis) {
|
||||
if (!record.downloaded) continue;
|
||||
if (!/^[a-zA-Z0-9_]+?([a-zA-Z0-9\.]+)?$/.test(record.fileName)) {
|
||||
this.logger.error(`invalid filename: ${record.fileName}, skipping in emoji import`);
|
||||
continue;
|
||||
}
|
||||
const emojiInfo = record.emoji;
|
||||
const emojiPath = outputPath + '/' + record.fileName;
|
||||
await Emojis.delete({
|
||||
name: emojiInfo.name,
|
||||
});
|
||||
const driveFile = await addFile({ user: null, path: emojiPath, name: record.fileName, force: true });
|
||||
await Emojis.insert({
|
||||
id: genId(),
|
||||
updatedAt: new Date(),
|
||||
name: emojiInfo.name,
|
||||
category: emojiInfo.category,
|
||||
host: null,
|
||||
aliases: emojiInfo.aliases,
|
||||
originalUrl: driveFile.url,
|
||||
publicUrl: driveFile.webpublicUrl ?? driveFile.url,
|
||||
type: driveFile.webpublicType ?? driveFile.type,
|
||||
});
|
||||
}
|
||||
|
||||
await db.queryResultCache!.remove(['meta_emojis']);
|
||||
|
||||
cleanup();
|
||||
|
||||
logger.succ('Imported');
|
||||
done();
|
||||
}
|
||||
|
|
|
@ -1,27 +1,27 @@
|
|||
import { URL } from 'node:url';
|
||||
import Bull from 'bull';
|
||||
import httpSignature from '@peertube/http-signature';
|
||||
import { perform } from '@/remote/activitypub/perform.js';
|
||||
import Logger from '@/services/logger.js';
|
||||
import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc.js';
|
||||
import { Instances } from '@/models/index.js';
|
||||
import { apRequestChart, federationChart, instanceChart } from '@/services/chart/index.js';
|
||||
import { toPuny, extractDbHost } from '@/misc/convert-host.js';
|
||||
import { extractPunyHost } from '@/misc/convert-host.js';
|
||||
import { getApId } from '@/remote/activitypub/type.js';
|
||||
import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js';
|
||||
import { Resolver } from '@/remote/activitypub/resolver.js';
|
||||
import { LdSignature } from '@/remote/activitypub/misc/ld-signature.js';
|
||||
import { getAuthUser } from '@/remote/activitypub/misc/auth-user.js';
|
||||
import { StatusError } from '@/misc/fetch.js';
|
||||
import { AuthUser, getAuthUser } from '@/remote/activitypub/misc/auth-user.js';
|
||||
import { InboxJobData } from '@/queue/types.js';
|
||||
import { shouldBlockInstance } from '@/misc/should-block-instance.js';
|
||||
import { verifyHttpSignature } from '@/remote/http-signature.js';
|
||||
|
||||
const logger = new Logger('inbox');
|
||||
|
||||
// ユーザーのinboxにアクティビティが届いた時の処理
|
||||
// Processing when an activity arrives in the user's inbox
|
||||
export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
|
||||
const signature = job.data.signature; // HTTP-signature
|
||||
const activity = job.data.activity;
|
||||
const resolver = new Resolver();
|
||||
|
||||
//#region Log
|
||||
const info = Object.assign({}, activity) as any;
|
||||
|
@ -29,46 +29,12 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
|
|||
logger.debug(JSON.stringify(info, null, 2));
|
||||
//#endregion
|
||||
|
||||
const keyIdLower = signature.keyId.toLowerCase();
|
||||
if (keyIdLower.startsWith('acct:')) {
|
||||
return `Old keyId is no longer supported. ${keyIdLower}`;
|
||||
}
|
||||
|
||||
const host = toPuny(new URL(keyIdLower).hostname);
|
||||
|
||||
// Stop if the host is blocked.
|
||||
if (await shouldBlockInstance(host)) {
|
||||
return `Blocked request: ${host}`;
|
||||
}
|
||||
|
||||
const resolver = new Resolver();
|
||||
|
||||
let authUser;
|
||||
try {
|
||||
authUser = await getAuthUser(signature.keyId, getApId(activity.actor), resolver);
|
||||
} catch (e) {
|
||||
if (e instanceof StatusError) {
|
||||
if (e.isClientError) {
|
||||
return `skip: Ignored deleted actors on both ends ${activity.actor} - ${e.statusCode}`;
|
||||
} else {
|
||||
throw new Error(`Error in actor ${activity.actor} - ${e.statusCode || e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (authUser == null) {
|
||||
// Key not found? Unacceptable!
|
||||
return 'skip: failed to resolve user';
|
||||
} else {
|
||||
// Found key!
|
||||
}
|
||||
|
||||
// verify the HTTP Signature
|
||||
const httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem);
|
||||
const validated = await verifyHttpSignature(signature, resolver, getApId(activity.actor));
|
||||
let authUser = validated.authUser;
|
||||
|
||||
// The signature must be valid.
|
||||
// The signature must also match the actor otherwise anyone could sign any activity.
|
||||
if (!httpSignatureValidated || authUser.user.uri !== activity.actor) {
|
||||
if (validated.status !== 'valid' || validated.authUser.user.uri !== activity.actor) {
|
||||
// Last resort: LD-Signature
|
||||
if (activity.signature) {
|
||||
if (activity.signature.type !== 'RsaSignature2017') {
|
||||
|
@ -98,7 +64,7 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
|
|||
}
|
||||
|
||||
// Stop if the host is blocked.
|
||||
const ldHost = extractDbHost(authUser.user.uri);
|
||||
const ldHost = extractPunyHost(authUser.user.uri);
|
||||
if (await shouldBlockInstance(ldHost)) {
|
||||
return `Blocked request: ${ldHost}`;
|
||||
}
|
||||
|
@ -107,15 +73,20 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
|
|||
}
|
||||
}
|
||||
|
||||
// authUser cannot be null at this point:
|
||||
// either it was already not null because the HTTP signature was valid
|
||||
// or, if the LD signature was not verified, this function will already have returned.
|
||||
authUser = authUser as AuthUser;
|
||||
|
||||
// Verify that the actor's host is not blocked
|
||||
const signerHost = extractDbHost(authUser.user.uri!);
|
||||
const signerHost = extractPunyHost(authUser.user.uri!);
|
||||
if (await shouldBlockInstance(signerHost)) {
|
||||
return `Blocked request: ${signerHost}`;
|
||||
}
|
||||
|
||||
if (typeof activity.id === 'string') {
|
||||
// Verify that activity and actor are from the same host.
|
||||
const activityIdHost = extractDbHost(activity.id);
|
||||
const activityIdHost = extractPunyHost(activity.id);
|
||||
if (signerHost !== activityIdHost) {
|
||||
return `skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`;
|
||||
}
|
||||
|
|
|
@ -10,52 +10,49 @@ const logger = queueLogger.createSubLogger('check-expired');
|
|||
export async function checkExpired(job: Bull.Job<Record<string, unknown>>, done: any): Promise<void> {
|
||||
logger.info('Checking expired data...');
|
||||
|
||||
const expiredMutings = await Mutings.createQueryBuilder('muting')
|
||||
.where('muting.expiresAt IS NOT NULL')
|
||||
.andWhere('muting.expiresAt < :now', { now: new Date() })
|
||||
.innerJoinAndSelect('muting.mutee', 'mutee')
|
||||
.getMany();
|
||||
|
||||
if (expiredMutings.length > 0) {
|
||||
await Mutings.delete({
|
||||
id: In(expiredMutings.map(m => m.id)),
|
||||
});
|
||||
|
||||
for (const m of expiredMutings) {
|
||||
publishUserEvent(m.muterId, 'unmute', m.mutee!);
|
||||
}
|
||||
}
|
||||
|
||||
const OlderThan = (millis: number) => {
|
||||
return LessThan(new Date(new Date().getTime() - millis));
|
||||
};
|
||||
|
||||
await Signins.delete({
|
||||
createdAt: OlderThan(2 * MONTH),
|
||||
});
|
||||
await Promise.all([
|
||||
Mutings.createQueryBuilder('muting')
|
||||
.where('muting.expiresAt IS NOT NULL')
|
||||
.andWhere('muting.expiresAt < :now', { now: new Date() })
|
||||
.innerJoinAndSelect('muting.mutee', 'mutee')
|
||||
.getMany()
|
||||
.then(async (expiredMutings) => {
|
||||
if (expiredMutings.length > 0) {
|
||||
await Mutings.delete({
|
||||
id: In(expiredMutings.map(m => m.id)),
|
||||
});
|
||||
|
||||
await AttestationChallenges.delete({
|
||||
createdAt: OlderThan(5 * MINUTE),
|
||||
});
|
||||
|
||||
await PasswordResetRequests.delete({
|
||||
// this timing should be the same as in @/server/api/endpoints/reset-password.ts
|
||||
createdAt: OlderThan(30 * MINUTE),
|
||||
});
|
||||
|
||||
await AuthSessions.delete({
|
||||
createdAt: OlderThan(15 * MINUTE),
|
||||
});
|
||||
|
||||
await Notifications.delete({
|
||||
isRead: true,
|
||||
createdAt: OlderThan(3 * MONTH),
|
||||
});
|
||||
|
||||
await Users.delete({
|
||||
// delete users where the deletion status reference count has come down to zero
|
||||
isDeleted: 0,
|
||||
});
|
||||
for (const m of expiredMutings) {
|
||||
publishUserEvent(m.muterId, 'unmute', m.mutee!);
|
||||
}
|
||||
}
|
||||
}),
|
||||
Signins.delete({
|
||||
createdAt: OlderThan(2 * MONTH),
|
||||
}),
|
||||
AttestationChallenges.delete({
|
||||
createdAt: OlderThan(5 * MINUTE),
|
||||
}),
|
||||
PasswordResetRequests.delete({
|
||||
// this timing should be the same as in @/server/api/endpoints/reset-password.ts
|
||||
createdAt: OlderThan(30 * MINUTE),
|
||||
}),
|
||||
AuthSessions.delete({
|
||||
createdAt: OlderThan(15 * MINUTE),
|
||||
}),
|
||||
Notifications.delete({
|
||||
isRead: true,
|
||||
createdAt: OlderThan(3 * MONTH),
|
||||
}),
|
||||
Users.delete({
|
||||
// delete users where the deletion status reference count has come down to zero
|
||||
isDeleted: 0,
|
||||
}),
|
||||
]);
|
||||
|
||||
logger.succ('Deleted expired data.');
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import post from '@/services/note/create.js';
|
||||
import { IRemoteUser } from '@/models/entities/user.js';
|
||||
import { extractDbHost } from '@/misc/convert-host.js';
|
||||
import { extractPunyHost } from '@/misc/convert-host.js';
|
||||
import { getApLock } from '@/misc/app-lock.js';
|
||||
import { StatusError } from '@/misc/fetch.js';
|
||||
import { Notes } from '@/models/index.js';
|
||||
|
@ -15,7 +15,7 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity:
|
|||
const uri = getApId(activity);
|
||||
|
||||
// Cancel if the announced from host is blocked.
|
||||
if (await shouldBlockInstance(extractDbHost(uri))) return;
|
||||
if (await shouldBlockInstance(extractPunyHost(uri))) return;
|
||||
|
||||
const unlock = await getApLock(uri);
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { IRemoteUser } from '@/models/entities/user.js';
|
||||
import { getApLock } from '@/misc/app-lock.js';
|
||||
import { extractDbHost } from '@/misc/convert-host.js';
|
||||
import { extractPunyHost } from '@/misc/convert-host.js';
|
||||
import { StatusError } from '@/misc/fetch.js';
|
||||
import { Resolver } from '@/remote/activitypub/resolver.js';
|
||||
import { createNote, fetchNote } from '@/remote/activitypub/models/note.js';
|
||||
|
@ -18,7 +18,7 @@ export default async function(resolver: Resolver, actor: IRemoteUser, note: IObj
|
|||
}
|
||||
|
||||
if (typeof note.id === 'string') {
|
||||
if (extractDbHost(actor.uri) !== extractDbHost(note.id)) {
|
||||
if (extractPunyHost(actor.uri) !== extractPunyHost(note.id)) {
|
||||
return 'skip: host in actor.uri !== note.id';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { IRemoteUser } from '@/models/entities/user.js';
|
||||
import { toArray } from '@/prelude/array.js';
|
||||
import { Resolver } from '@/remote/activitypub/resolver.js';
|
||||
import { extractDbHost } from '@/misc/convert-host.js';
|
||||
import { extractPunyHost } from '@/misc/convert-host.js';
|
||||
import { shouldBlockInstance } from '@/misc/should-block-instance.js';
|
||||
import { apLogger } from '../logger.js';
|
||||
import { IObject, isCreate, isDelete, isUpdate, isRead, isFollow, isAccept, isReject, isAdd, isRemove, isAnnounce, isLike, isUndo, isBlock, isCollectionOrOrderedCollection, isCollection, isFlag, isMove, getApId } from '../type.js';
|
||||
|
@ -42,7 +42,7 @@ async function performOneActivity(actor: IRemoteUser, activity: IObject, resolve
|
|||
if (actor.isSuspended) return;
|
||||
|
||||
if (typeof activity.id !== 'undefined') {
|
||||
const host = extractDbHost(getApId(activity));
|
||||
const host = extractPunyHost(getApId(activity));
|
||||
if (await shouldBlockInstance(host)) return;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { IRemoteUser } from '@/models/entities/user.js';
|
||||
import { isSelfHost, extractDbHost } from '@/misc/convert-host.js';
|
||||
import { isSelfHost, extractPunyHost } from '@/misc/convert-host.js';
|
||||
import { MessagingMessages } from '@/models/index.js';
|
||||
import { readUserMessagingMessage } from '@/server/api/common/read-messaging-message.js';
|
||||
import { IRead, getApId } from '../type.js';
|
||||
|
@ -7,7 +7,7 @@ import { IRead, getApId } from '../type.js';
|
|||
export const performReadActivity = async (actor: IRemoteUser, activity: IRead): Promise<string> => {
|
||||
const id = await getApId(activity.object);
|
||||
|
||||
if (!isSelfHost(extractDbHost(id))) {
|
||||
if (!isSelfHost(extractPunyHost(id))) {
|
||||
return `skip: Read to foreign host (${id})`;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { IRemoteUser } from '@/models/entities/user.js';
|
||||
import { getApId, getApType, IUpdate, isActor } from '@/remote/activitypub/type.js';
|
||||
import { getApId, getOneApId, getApType, IUpdate, isActor, isPost } from '@/remote/activitypub/type.js';
|
||||
import { apLogger } from '@/remote/activitypub/logger.js';
|
||||
import { updateQuestion } from '@/remote/activitypub/models/question.js';
|
||||
import { Resolver } from '@/remote/activitypub/resolver.js';
|
||||
import { updatePerson } from '@/remote/activitypub/models/person.js';
|
||||
import { update as updateNote } from '@/remote/activitypub/kernel/update/note.js';
|
||||
|
||||
/**
|
||||
* Updateアクティビティを捌きます
|
||||
|
@ -30,6 +31,8 @@ export default async (actor: IRemoteUser, activity: IUpdate, resolver: Resolver)
|
|||
} else if (getApType(object) === 'Question') {
|
||||
await updateQuestion(object, resolver).catch(e => console.log(e));
|
||||
return 'ok: Question updated';
|
||||
} else if (isPost(object)) {
|
||||
return await updateNote(actor, object, resolver);
|
||||
} else {
|
||||
return `skip: Unknown type: ${getApType(object)}`;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
import { IRemoteUser } from '@/models/entities/user.js';
|
||||
import { getApId } from '@/remote/activitypub/type.js';
|
||||
import { Resolver } from '@/remote/activitypub/resolver.js';
|
||||
import { Notes } from '@/models/index.js';
|
||||
import createNote from '@/remote/activitypub/kernel/create/note.js';
|
||||
import { getApLock } from '@/misc/app-lock.js';
|
||||
import { updateNote } from '@/remote/activitypub/models/note.js';
|
||||
|
||||
export async function update(actor: IRemoteUser, note: IObject, resolver: Resolver): Promise<string> {
|
||||
// check whether note exists
|
||||
const uri = getApId(note);
|
||||
const exists = await Notes.findOneBy({ uri });
|
||||
|
||||
if (exists == null) {
|
||||
// does not yet exist, handle as if this was a create activity
|
||||
// and since this is not a direct creation, handle it silently
|
||||
createNote(resolver, actor, note, true);
|
||||
|
||||
const unlock = await getApLock(uri);
|
||||
try {
|
||||
// if creating was successful...
|
||||
const existsNow = await Notes.findOneByOrFail({ uri });
|
||||
// set the updatedAt timestamp since the note was changed
|
||||
await Notes.update(existsNow.id, { updatedAt: new Date() });
|
||||
return 'ok: unknown note created and marked as updated';
|
||||
} catch (e) {
|
||||
return `skip: updated note unknown and creating rejected: ${e.message}`;
|
||||
} finally {
|
||||
unlock();
|
||||
}
|
||||
} else {
|
||||
// check that actor is authorized to update this note
|
||||
if (actor.id !== exists.userId) {
|
||||
return 'skip: actor not authorized to update Note';
|
||||
}
|
||||
// this does not redo the checks from the Create Note kernel
|
||||
// since if the note made it into the database, we assume
|
||||
// those checks must have been passed before.
|
||||
|
||||
const unlock = await getApLock(uri);
|
||||
try {
|
||||
await updateNote(note, actor, resolver);
|
||||
return 'ok: note updated';
|
||||
} catch (e) {
|
||||
return `skip: update note rejected: ${e.message}`;
|
||||
} finally {
|
||||
unlock();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import { IRemoteUser } from '@/models/entities/user.js';
|
|||
import { UserPublickey } from '@/models/entities/user-publickey.js';
|
||||
import { uriPersonCache, userByIdCache } from '@/services/user-cache.js';
|
||||
import { createPerson } from '@/remote/activitypub/models/person.js';
|
||||
import { Resolver } from '@/remote/activitypub/resolver.js';
|
||||
|
||||
export type AuthUser = {
|
||||
user: IRemoteUser;
|
||||
|
@ -29,8 +30,8 @@ function authUserFromApId(uri: string): Promise<AuthUser | null> {
|
|||
});
|
||||
}
|
||||
|
||||
export async function getAuthUser(keyId: string, actorUri: string, resolver: Resolver): Promise<AuthUser | null> {
|
||||
let authUser = await publicKeyCache.fetch(keyId)
|
||||
export async function authUserFromKeyId(keyId: string): Promise<AuthUser | null> {
|
||||
return await publicKeyCache.fetch(keyId)
|
||||
.then(async key => {
|
||||
if (!key) return null;
|
||||
else return {
|
||||
|
@ -38,6 +39,10 @@ export async function getAuthUser(keyId: string, actorUri: string, resolver: Res
|
|||
key,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAuthUser(keyId: string, actorUri: string, resolver: Resolver): Promise<AuthUser | null> {
|
||||
let authUser = await authUserFromKeyId(keyId);
|
||||
if (authUser != null) return authUser;
|
||||
|
||||
authUser = await authUserFromApId(actorUri);
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import promiseLimit from 'promise-limit';
|
||||
|
||||
import * as foundkey from 'foundkey-js';
|
||||
import config from '@/config/index.js';
|
||||
import post from '@/services/note/create.js';
|
||||
import { IRemoteUser } from '@/models/entities/user.js';
|
||||
import { User, IRemoteUser } from '@/models/entities/user.js';
|
||||
import { unique, toArray, toSingle } from '@/prelude/array.js';
|
||||
import { vote } from '@/services/note/polls/vote.js';
|
||||
import { DriveFile } from '@/models/entities/drive-file.js';
|
||||
import { deliverQuestionUpdate } from '@/services/note/polls/update.js';
|
||||
import { extractDbHost } from '@/misc/convert-host.js';
|
||||
import { Polls, MessagingMessages } from '@/models/index.js';
|
||||
import { extractPunyHost } from '@/misc/convert-host.js';
|
||||
import { Polls, MessagingMessages, Notes } from '@/models/index.js';
|
||||
import { Note } from '@/models/entities/note.js';
|
||||
import { Emoji } from '@/models/entities/emoji.js';
|
||||
import { genId } from '@/misc/gen-id.js';
|
||||
|
@ -27,6 +27,8 @@ import { resolveImage } from './image.js';
|
|||
import { extractApHashtags, extractQuoteUrl, extractEmojis } from './tag.js';
|
||||
import { extractPollFromQuestion } from './question.js';
|
||||
import { extractApMentions } from './mention.js';
|
||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||
import { sideEffects } from '@/services/note/side-effects.js';
|
||||
|
||||
export function validateNote(object: IObject): Error | null {
|
||||
if (object == null) {
|
||||
|
@ -45,9 +47,9 @@ export function validateNote(object: IObject): Error | null {
|
|||
}
|
||||
|
||||
// Check that the server is authorized to act on behalf of this author.
|
||||
const expectHost = extractDbHost(id);
|
||||
const expectHost = extractPunyHost(id);
|
||||
const attributedToHost = object.attributedTo
|
||||
? extractDbHost(getOneApId(object.attributedTo))
|
||||
? extractPunyHost(getOneApId(object.attributedTo))
|
||||
: null;
|
||||
if (attributedToHost !== expectHost) {
|
||||
return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${attributedToHost}`);
|
||||
|
@ -62,7 +64,7 @@ export function validateNote(object: IObject): Error | null {
|
|||
/**
|
||||
* Function to process the content of a note, reusable in createNote and updateNote.
|
||||
*/
|
||||
async function processContent(note: IPost, quoteUri: string | null):
|
||||
async function processContent(actor: IRemoteUser, note: IPost, quoteUri: string | null, resolver: Resolver):
|
||||
Promise<{
|
||||
cw: string | null,
|
||||
files: DriveFile[],
|
||||
|
@ -78,10 +80,10 @@ async function processContent(note: IPost, quoteUri: string | null):
|
|||
const limit = promiseLimit(2);
|
||||
|
||||
const attachments = toArray(note.attachment);
|
||||
const files = note.attachment
|
||||
const files = attachments
|
||||
// If the note is marked as sensitive, the images should be marked sensitive too.
|
||||
.map(attach => attach.sensitive |= note.sensitive)
|
||||
? (await Promise.all(note.attachment.map(x => limit(() => resolveImage(actor, x, resolver)) as Promise<DriveFile>)))
|
||||
? (await Promise.all(attachments.map(x => limit(() => resolveImage(actor, x, resolver)) as Promise<DriveFile>)))
|
||||
.filter(image => image != null)
|
||||
: [];
|
||||
|
||||
|
@ -93,7 +95,7 @@ async function processContent(note: IPost, quoteUri: string | null):
|
|||
text = fromHtml(note.content, quoteUri);
|
||||
}
|
||||
|
||||
const emojis = await extractEmojis(note.tag || [], extractDbHost(getApId(note))).catch(e => {
|
||||
const emojis = await extractEmojis(note.tag || [], extractPunyHost(getApId(note))).catch(e => {
|
||||
apLogger.info(`extractEmojis: ${e}`);
|
||||
return [] as Emoji[];
|
||||
});
|
||||
|
@ -258,7 +260,7 @@ export async function createNote(value: string | IObject, resolver: Resolver, si
|
|||
|
||||
const poll = await extractPollFromQuestion(note, resolver).catch(() => undefined);
|
||||
|
||||
const processedContent = await processContent(note, quote?.uri);
|
||||
const processedContent = await processContent(actor, note, quote?.uri, resolver);
|
||||
|
||||
if (isTalk) {
|
||||
for (const recipient of visibleUsers) {
|
||||
|
@ -299,7 +301,7 @@ export async function resolveNote(value: string | IObject, resolver: Resolver):
|
|||
if (uri == null) throw new Error('missing uri');
|
||||
|
||||
// Interrupt if blocked.
|
||||
if (await shouldBlockInstance(extractDbHost(uri))) throw new StatusError('host blocked', 451, `host ${extractDbHost(uri)} is blocked`);
|
||||
if (await shouldBlockInstance(extractPunyHost(uri))) throw new StatusError('host blocked', 451, `host ${extractPunyHost(uri)} is blocked`);
|
||||
|
||||
const unlock = await getApLock(uri);
|
||||
|
||||
|
@ -324,3 +326,45 @@ export async function resolveNote(value: string | IObject, resolver: Resolver):
|
|||
unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a note.
|
||||
*
|
||||
* If the target Note is not registered, it will be ignored.
|
||||
*/
|
||||
export async function updateNote(value: IPost, actor: User, resolver: Resolver): Promise<Note | null> {
|
||||
const err = validateNote(value);
|
||||
if (err) {
|
||||
apLogger.error(`${err.message}`);
|
||||
throw new Error('invalid updated note');
|
||||
}
|
||||
|
||||
const uri = getApId(value);
|
||||
const exists = await Notes.findOneBy({ uri });
|
||||
if (exists == null) return null;
|
||||
|
||||
let quoteUri = null;
|
||||
if (exists.renoteId && !foundkey.entities.isPureRenote(exists)) {
|
||||
const quote = await Notes.findOneBy({ id: exists.renoteId });
|
||||
quoteUri = quote.uri;
|
||||
}
|
||||
|
||||
// process content and update attached files (e.g. also image descriptions)
|
||||
const processedContent = await processContent(actor, value, quoteUri, resolver);
|
||||
|
||||
// update note content itself
|
||||
await Notes.update(exists.id, {
|
||||
updatedAt: new Date(),
|
||||
|
||||
cw: processedContent.cw,
|
||||
fileIds: processedContent.files.map(file => file.id),
|
||||
attachedFileTypes: processedContent.files.map(file => file.type),
|
||||
text: processedContent.text,
|
||||
emojis: processedContent.apEmoji,
|
||||
tags: processedContent.apHashtags.map(tag => normalizeForSearch(tag)),
|
||||
url: processedContent.url,
|
||||
name: processedContent.name,
|
||||
});
|
||||
|
||||
await sideEffects(actor, await Notes.findOneByOrFail({ id: exists.id }), false, false);
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import { genId } from '@/misc/gen-id.js';
|
|||
import { instanceChart, usersChart } from '@/services/chart/index.js';
|
||||
import { UserPublickey } from '@/models/entities/user-publickey.js';
|
||||
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
||||
import { extractDbHost } from '@/misc/convert-host.js';
|
||||
import { extractPunyHost } from '@/misc/convert-host.js';
|
||||
import { UserProfile } from '@/models/entities/user-profile.js';
|
||||
import { toArray } from '@/prelude/array.js';
|
||||
import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js';
|
||||
|
@ -57,7 +57,7 @@ async function validateActor(x: IObject, resolver: Resolver): Promise<IActor> {
|
|||
|
||||
// This check is security critical.
|
||||
// Without this check, an entry could be inserted into UserPublickey for a local user.
|
||||
if (extractDbHost(uri) === extractDbHost(config.url)) {
|
||||
if (extractPunyHost(uri) === extractPunyHost(config.url)) {
|
||||
throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user');
|
||||
}
|
||||
|
||||
|
@ -108,7 +108,7 @@ async function validateActor(x: IObject, resolver: Resolver): Promise<IActor> {
|
|||
|
||||
// This is a security critical check to not insert or change an entry of
|
||||
// UserPublickey to point to a local key id.
|
||||