Compare commits
14 commits
313313fc2e
...
bd7e204fe0
Author | SHA1 | Date | |
---|---|---|---|
bd7e204fe0 | |||
|
ce08d2c827 | ||
|
a3ad95c058 | ||
|
3b804799c3 | ||
|
323de25075 | ||
|
c427e10f17 | ||
|
329995f4a3 | ||
|
f0a754eaa8 | ||
|
504ead526a | ||
e01e82aa65 | |||
5b6e8cc110 | |||
9b8d02d1c3 | |||
|
4603ab67bb | ||
|
763c708253 |
36 changed files with 616 additions and 224 deletions
|
@ -58,6 +58,7 @@ PgroongaのCWサーチ (github.com/paricafe/misskey#d30db97b59d264450901c1dd8680
|
||||||
(Based on https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/663)
|
(Based on https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/663)
|
||||||
- Enhance: サイドバーを簡単に展開・折りたたみできるように ( #14981 )
|
- Enhance: サイドバーを簡単に展開・折りたたみできるように ( #14981 )
|
||||||
- Enhance: リノートメニューに「リノートの詳細」を追加
|
- Enhance: リノートメニューに「リノートの詳細」を追加
|
||||||
|
- Enhance: 非ログイン状態でMisskeyを開いた際のパフォーマンスを向上
|
||||||
- Fix: 通知の範囲指定の設定項目が必要ない通知設定でも範囲指定の設定がでている問題を修正
|
- Fix: 通知の範囲指定の設定項目が必要ない通知設定でも範囲指定の設定がでている問題を修正
|
||||||
- Fix: Turnstileが失敗・期限切れした際にも成功扱いとなってしまう問題を修正
|
- Fix: Turnstileが失敗・期限切れした際にも成功扱いとなってしまう問題を修正
|
||||||
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/768)
|
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/768)
|
||||||
|
@ -92,6 +93,7 @@ PgroongaのCWサーチ (github.com/paricafe/misskey#d30db97b59d264450901c1dd8680
|
||||||
- Fix: FTT無効時にユーザーリストタイムラインが使用できない問題を修正
|
- Fix: FTT無効時にユーザーリストタイムラインが使用できない問題を修正
|
||||||
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/709)
|
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/709)
|
||||||
- Fix: User Webhookテスト機能のMock Payloadを修正
|
- Fix: User Webhookテスト機能のMock Payloadを修正
|
||||||
|
- Fix: アカウント削除のモデレーションログが動作していないのを修正 (#14996)
|
||||||
|
|
||||||
### Misskey.js
|
### Misskey.js
|
||||||
- Fix: Stream初期化時、別途WebSocketを指定する場合の型定義を修正
|
- Fix: Stream初期化時、別途WebSocketを指定する場合の型定義を修正
|
||||||
|
|
|
@ -586,6 +586,7 @@ masterVolume: "Volum principal"
|
||||||
notUseSound: "Sense so"
|
notUseSound: "Sense so"
|
||||||
useSoundOnlyWhenActive: "Reproduir sons només quan Misskey estigui actiu"
|
useSoundOnlyWhenActive: "Reproduir sons només quan Misskey estigui actiu"
|
||||||
details: "Detalls"
|
details: "Detalls"
|
||||||
|
renoteDetails: "Més informació sobre l'impuls "
|
||||||
chooseEmoji: "Tria un emoji"
|
chooseEmoji: "Tria un emoji"
|
||||||
unableToProcess: "L'operació no pot ser completada "
|
unableToProcess: "L'operació no pot ser completada "
|
||||||
recentUsed: "Utilitzat recentment"
|
recentUsed: "Utilitzat recentment"
|
||||||
|
|
|
@ -1242,6 +1242,7 @@ keepOriginalFilenameDescription: "Wenn diese Einstellung deaktiviert ist, wird d
|
||||||
noDescription: "Keine Beschreibung vorhanden"
|
noDescription: "Keine Beschreibung vorhanden"
|
||||||
tryAgain: "Bitte später erneut versuchen"
|
tryAgain: "Bitte später erneut versuchen"
|
||||||
confirmWhenRevealingSensitiveMedia: "Das Anzeigen von sensiblen Medien bestätigen"
|
confirmWhenRevealingSensitiveMedia: "Das Anzeigen von sensiblen Medien bestätigen"
|
||||||
|
sensitiveMediaRevealConfirm: "Es könnte sich um sensible Medien handeln. Möchtest du sie anzeigen?"
|
||||||
createdLists: "Erstellte Listen"
|
createdLists: "Erstellte Listen"
|
||||||
createdAntennas: "Erstellte Antennen"
|
createdAntennas: "Erstellte Antennen"
|
||||||
fromX: "Von {x}"
|
fromX: "Von {x}"
|
||||||
|
@ -1253,6 +1254,8 @@ thereAreNChanges: "Es gibt {n} Änderung(en)"
|
||||||
signinWithPasskey: "Mit Passkey anmelden"
|
signinWithPasskey: "Mit Passkey anmelden"
|
||||||
passkeyVerificationFailed: "Die Passkey-Verifizierung ist fehlgeschlagen."
|
passkeyVerificationFailed: "Die Passkey-Verifizierung ist fehlgeschlagen."
|
||||||
passkeyVerificationSucceededButPasswordlessLoginDisabled: "Die Verifizierung des Passkeys war erfolgreich, aber die passwortlose Anmeldung ist deaktiviert."
|
passkeyVerificationSucceededButPasswordlessLoginDisabled: "Die Verifizierung des Passkeys war erfolgreich, aber die passwortlose Anmeldung ist deaktiviert."
|
||||||
|
messageToFollower: "Nachricht an die Follower"
|
||||||
|
testCaptchaWarning: "Diese Funktion ist für CAPTCHA-Testzwecke gedacht.\n<strong>Nicht in einer Produktivumgebung verwenden.</strong>"
|
||||||
prohibitedWordsForNameOfUser: "Verbotene Begriffe für Benutzernamen"
|
prohibitedWordsForNameOfUser: "Verbotene Begriffe für Benutzernamen"
|
||||||
prohibitedWordsForNameOfUserDescription: "Wenn eine Zeichenfolge aus dieser Liste im Namen eines Benutzers enthalten ist, wird der Benutzername abgelehnt. Benutzer mit Moderatorenrechten sind von dieser Einschränkung nicht betroffen."
|
prohibitedWordsForNameOfUserDescription: "Wenn eine Zeichenfolge aus dieser Liste im Namen eines Benutzers enthalten ist, wird der Benutzername abgelehnt. Benutzer mit Moderatorenrechten sind von dieser Einschränkung nicht betroffen."
|
||||||
yourNameContainsProhibitedWords: "Dein Name enthält einen verbotenen Begriff"
|
yourNameContainsProhibitedWords: "Dein Name enthält einen verbotenen Begriff"
|
||||||
|
@ -1264,6 +1267,7 @@ _accountSettings:
|
||||||
requireSigninToViewContentsDescription1: "Erfordere eine Anmeldung, um alle Notizen und andere Inhalte anzuzeigen, die du erstellt hast. Dadurch wird verhindert, dass Crawler deine Informationen sammeln."
|
requireSigninToViewContentsDescription1: "Erfordere eine Anmeldung, um alle Notizen und andere Inhalte anzuzeigen, die du erstellt hast. Dadurch wird verhindert, dass Crawler deine Informationen sammeln."
|
||||||
requireSigninToViewContentsDescription3: "Diese Einschränkungen gelten möglicherweise nicht für föderierte Inhalte von anderen Servern."
|
requireSigninToViewContentsDescription3: "Diese Einschränkungen gelten möglicherweise nicht für föderierte Inhalte von anderen Servern."
|
||||||
makeNotesFollowersOnlyBefore: "Macht frühere Notizen nur für Follower sichtbar"
|
makeNotesFollowersOnlyBefore: "Macht frühere Notizen nur für Follower sichtbar"
|
||||||
|
makeNotesHiddenBefore: "Frühere Notizen privat machen"
|
||||||
mayNotEffectForFederatedNotes: "Dies hat möglicherweise keine Auswirkungen auf Notizen, die an andere Server föderiert werden."
|
mayNotEffectForFederatedNotes: "Dies hat möglicherweise keine Auswirkungen auf Notizen, die an andere Server föderiert werden."
|
||||||
_abuseUserReport:
|
_abuseUserReport:
|
||||||
forward: "Weiterleiten"
|
forward: "Weiterleiten"
|
||||||
|
@ -1274,6 +1278,7 @@ _delivery:
|
||||||
stop: "Gesperrt"
|
stop: "Gesperrt"
|
||||||
_type:
|
_type:
|
||||||
none: "Wird veröffentlicht"
|
none: "Wird veröffentlicht"
|
||||||
|
manuallySuspended: "Manuell gesperrt"
|
||||||
_bubbleGame:
|
_bubbleGame:
|
||||||
howToPlay: "Wie man spielt"
|
howToPlay: "Wie man spielt"
|
||||||
hold: "Halten"
|
hold: "Halten"
|
||||||
|
|
|
@ -586,6 +586,7 @@ masterVolume: "Master volume"
|
||||||
notUseSound: "Disable sound"
|
notUseSound: "Disable sound"
|
||||||
useSoundOnlyWhenActive: "Output sounds only if Misskey is active."
|
useSoundOnlyWhenActive: "Output sounds only if Misskey is active."
|
||||||
details: "Details"
|
details: "Details"
|
||||||
|
renoteDetails: "Renote details"
|
||||||
chooseEmoji: "Select an emoji"
|
chooseEmoji: "Select an emoji"
|
||||||
unableToProcess: "The operation could not be completed"
|
unableToProcess: "The operation could not be completed"
|
||||||
recentUsed: "Recently used"
|
recentUsed: "Recently used"
|
||||||
|
|
|
@ -586,6 +586,7 @@ masterVolume: "마스터 볼륨"
|
||||||
notUseSound: "음소거 하기"
|
notUseSound: "음소거 하기"
|
||||||
useSoundOnlyWhenActive: "Misskey를 활성화한 때에만 소리를 출력하기"
|
useSoundOnlyWhenActive: "Misskey를 활성화한 때에만 소리를 출력하기"
|
||||||
details: "자세히"
|
details: "자세히"
|
||||||
|
renoteDetails: "리노트 상세 내용"
|
||||||
chooseEmoji: "이모지 선택"
|
chooseEmoji: "이모지 선택"
|
||||||
unableToProcess: "작업을 완료할 수 없습니다"
|
unableToProcess: "작업을 완료할 수 없습니다"
|
||||||
recentUsed: "최근 사용"
|
recentUsed: "최근 사용"
|
||||||
|
@ -1299,6 +1300,7 @@ thisContentsAreMarkedAsSigninRequiredByAuthor: "게시자에 의해 로그인해
|
||||||
lockdown: "잠금"
|
lockdown: "잠금"
|
||||||
pleaseSelectAccount: "계정을 선택해주세요."
|
pleaseSelectAccount: "계정을 선택해주세요."
|
||||||
availableRoles: "사용 가능한 역할"
|
availableRoles: "사용 가능한 역할"
|
||||||
|
acknowledgeNotesAndEnable: "활성화 하기 전에 주의 사항을 확인했습니다."
|
||||||
_accountSettings:
|
_accountSettings:
|
||||||
requireSigninToViewContents: "콘텐츠 열람을 위해 로그인으 필수로 설정하기"
|
requireSigninToViewContents: "콘텐츠 열람을 위해 로그인으 필수로 설정하기"
|
||||||
requireSigninToViewContentsDescription1: "자신이 작성한 모든 노트 등의 콘텐츠를 보기 위해 로그인을 필수로 설정합니다. 크롤러가 정보 수집하는 것을 방지하는 효과를 기대할 수 있습니다."
|
requireSigninToViewContentsDescription1: "자신이 작성한 모든 노트 등의 콘텐츠를 보기 위해 로그인을 필수로 설정합니다. 크롤러가 정보 수집하는 것을 방지하는 효과를 기대할 수 있습니다."
|
||||||
|
@ -1455,6 +1457,8 @@ _serverSettings:
|
||||||
reactionsBufferingDescription: "활성화 한 경우, 리액션 작성 퍼포먼스가 대폭 향상되어 DB의 부하를 줄일 수 있으나, Redis의 메모리 사용량이 많아집니다."
|
reactionsBufferingDescription: "활성화 한 경우, 리액션 작성 퍼포먼스가 대폭 향상되어 DB의 부하를 줄일 수 있으나, Redis의 메모리 사용량이 많아집니다."
|
||||||
inquiryUrl: "문의처 URL"
|
inquiryUrl: "문의처 URL"
|
||||||
inquiryUrlDescription: "서버 운영자에게 보내는 문의 양식의 URL이나 운영자의 연락처 등이 적힌 웹 페이지의 URL을 설정합니다."
|
inquiryUrlDescription: "서버 운영자에게 보내는 문의 양식의 URL이나 운영자의 연락처 등이 적힌 웹 페이지의 URL을 설정합니다."
|
||||||
|
openRegistration: "회원 가입을 활성화 하기"
|
||||||
|
openRegistrationWarning: "회원 가입을 개방하는 것은 리스크가 따릅니다. 서버를 항상 감시할 수 있고, 문제가 발생했을 때 바로 대응할 수 있는 상태에서만 활성화 하는 것을 권장합니다."
|
||||||
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "일정 기간동안 모더레이터의 활동이 감지되지 않는 경우, 스팸 방지를 위해 이 설정은 자동으로 꺼집니다."
|
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "일정 기간동안 모더레이터의 활동이 감지되지 않는 경우, 스팸 방지를 위해 이 설정은 자동으로 꺼집니다."
|
||||||
_accountMigration:
|
_accountMigration:
|
||||||
moveFrom: "다른 계정에서 이 계정으로 이사"
|
moveFrom: "다른 계정에서 이 계정으로 이사"
|
||||||
|
@ -2737,3 +2741,6 @@ _selfXssPrevention:
|
||||||
description1: "여기에 무언가를 붙여넣으면 악의적인 사용자에게 계정을 탈취당하거나 개인정보를 도용당할 수 있습니다."
|
description1: "여기에 무언가를 붙여넣으면 악의적인 사용자에게 계정을 탈취당하거나 개인정보를 도용당할 수 있습니다."
|
||||||
description2: "붙여 넣으려는 항목이 무엇인지 정확히 이해하지 못하는 경우, %c지금 바로 작업을 중단하고 이 창을 닫으십시오."
|
description2: "붙여 넣으려는 항목이 무엇인지 정확히 이해하지 못하는 경우, %c지금 바로 작업을 중단하고 이 창을 닫으십시오."
|
||||||
description3: "자세한 내용은 여기를 확인해 주세요. {link}"
|
description3: "자세한 내용은 여기를 확인해 주세요. {link}"
|
||||||
|
_followRequest:
|
||||||
|
recieved: "받은 신청"
|
||||||
|
sent: "보낸 신청"
|
||||||
|
|
|
@ -1707,9 +1707,9 @@ _achievements:
|
||||||
description: "在元旦登入"
|
description: "在元旦登入"
|
||||||
flavor: "今年也请对本服务器多多指教!"
|
flavor: "今年也请对本服务器多多指教!"
|
||||||
_cookieClicked:
|
_cookieClicked:
|
||||||
title: "点击饼干小游戏"
|
title: "饼干点点乐"
|
||||||
description: "点击了饼干"
|
description: "点击了饼干"
|
||||||
flavor: "用错软件了?"
|
flavor: "穿越了?"
|
||||||
_brainDiver:
|
_brainDiver:
|
||||||
title: "Brain Diver"
|
title: "Brain Diver"
|
||||||
description: "发布了包含 Brain Diver 链接的帖子"
|
description: "发布了包含 Brain Diver 链接的帖子"
|
||||||
|
|
|
@ -586,6 +586,7 @@ masterVolume: "主音量"
|
||||||
notUseSound: "關閉音效"
|
notUseSound: "關閉音效"
|
||||||
useSoundOnlyWhenActive: "瀏覽器在前景運作時,Misskey 才會發出音效"
|
useSoundOnlyWhenActive: "瀏覽器在前景運作時,Misskey 才會發出音效"
|
||||||
details: "詳細資訊"
|
details: "詳細資訊"
|
||||||
|
renoteDetails: "轉發貼文的細節"
|
||||||
chooseEmoji: "選擇您的表情符號"
|
chooseEmoji: "選擇您的表情符號"
|
||||||
unableToProcess: "操作無法完成"
|
unableToProcess: "操作無法完成"
|
||||||
recentUsed: "最近使用"
|
recentUsed: "最近使用"
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
import * as stream from 'node:stream/promises';
|
import * as stream from 'node:stream/promises';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import ipaddr from 'ipaddr.js';
|
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import got, * as Got from 'got';
|
import got, * as Got from 'got';
|
||||||
import { parse } from 'content-disposition';
|
import { parse } from 'content-disposition';
|
||||||
|
@ -61,7 +60,6 @@ export class DownloadService {
|
||||||
request: operationTimeout, // whole operation timeout
|
request: operationTimeout, // whole operation timeout
|
||||||
},
|
},
|
||||||
agent: {
|
agent: {
|
||||||
http: this.httpRequestService.httpAgent,
|
|
||||||
https: this.httpRequestService.httpsAgent,
|
https: this.httpRequestService.httpsAgent,
|
||||||
},
|
},
|
||||||
http2: false, // default
|
http2: false, // default
|
||||||
|
@ -70,13 +68,6 @@ export class DownloadService {
|
||||||
},
|
},
|
||||||
enableUnixSockets: false,
|
enableUnixSockets: false,
|
||||||
}).on('response', (res: Got.Response) => {
|
}).on('response', (res: Got.Response) => {
|
||||||
if ((process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') && !this.config.proxy && res.ip) {
|
|
||||||
if (this.isPrivateIp(res.ip)) {
|
|
||||||
this.logger.warn(`Blocked address: ${res.ip}`);
|
|
||||||
req.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const contentLength = res.headers['content-length'];
|
const contentLength = res.headers['content-length'];
|
||||||
if (contentLength != null) {
|
if (contentLength != null) {
|
||||||
const size = Number(contentLength);
|
const size = Number(contentLength);
|
||||||
|
@ -139,18 +130,4 @@ export class DownloadService {
|
||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
|
||||||
private isPrivateIp(ip: string): boolean {
|
|
||||||
const parsedIp = ipaddr.parse(ip);
|
|
||||||
|
|
||||||
for (const net of this.config.allowedPrivateNetworks ?? []) {
|
|
||||||
const cidr = ipaddr.parseCIDR(net);
|
|
||||||
if (cidr[0].kind() === parsedIp.kind() && parsedIp.match(ipaddr.parseCIDR(net))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsedIp.range() !== 'unicast';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -313,6 +313,7 @@ export class EmailService {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
Authorization: truemailAuthKey,
|
Authorization: truemailAuthKey,
|
||||||
},
|
},
|
||||||
|
isLocalAddressAllowed: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const json = (await res.json()) as {
|
const json = (await res.json()) as {
|
||||||
|
|
|
@ -6,9 +6,10 @@
|
||||||
import * as http from 'node:http';
|
import * as http from 'node:http';
|
||||||
import * as https from 'node:https';
|
import * as https from 'node:https';
|
||||||
import * as net from 'node:net';
|
import * as net from 'node:net';
|
||||||
|
import ipaddr from 'ipaddr.js';
|
||||||
import CacheableLookup from 'cacheable-lookup';
|
import CacheableLookup from 'cacheable-lookup';
|
||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
import { HttpsProxyAgent } from 'hpagent';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
|
@ -18,30 +19,109 @@ import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/val
|
||||||
import { assertActivityMatchesUrls } from '@/core/activitypub/misc/check-against-url.js';
|
import { assertActivityMatchesUrls } from '@/core/activitypub/misc/check-against-url.js';
|
||||||
import type { IObject } from '@/core/activitypub/type.js';
|
import type { IObject } from '@/core/activitypub/type.js';
|
||||||
import type { Response } from 'node-fetch';
|
import type { Response } from 'node-fetch';
|
||||||
import type { URL } from 'node:url';
|
import { URL } from 'node:url';
|
||||||
|
|
||||||
export type HttpRequestSendOptions = {
|
export type HttpRequestSendOptions = {
|
||||||
throwErrorWhenResponseNotOk: boolean;
|
throwErrorWhenResponseNotOk: boolean;
|
||||||
validators?: ((res: Response) => void)[];
|
validators?: ((res: Response) => void)[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
declare module 'node:http' {
|
||||||
|
interface Agent {
|
||||||
|
createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HttpRequestServiceAgent extends http.Agent {
|
||||||
|
constructor(
|
||||||
|
private config: Config,
|
||||||
|
options?: http.AgentOptions,
|
||||||
|
) {
|
||||||
|
super(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket {
|
||||||
|
const socket = super.createConnection(options, callback)
|
||||||
|
.on('connect', () => {
|
||||||
|
const address = socket.remoteAddress;
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
if (address && ipaddr.isValid(address)) {
|
||||||
|
if (this.isPrivateIp(address)) {
|
||||||
|
socket.destroy(new Error(`Blocked address: ${address}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return socket;
|
||||||
|
};
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private isPrivateIp(ip: string): boolean {
|
||||||
|
const parsedIp = ipaddr.parse(ip);
|
||||||
|
|
||||||
|
for (const net of this.config.allowedPrivateNetworks ?? []) {
|
||||||
|
const cidr = ipaddr.parseCIDR(net);
|
||||||
|
if (cidr[0].kind() === parsedIp.kind() && parsedIp.match(ipaddr.parseCIDR(net))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedIp.range() !== 'unicast';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HttpsRequestServiceAgent extends https.Agent {
|
||||||
|
constructor(
|
||||||
|
private config: Config,
|
||||||
|
options?: https.AgentOptions,
|
||||||
|
) {
|
||||||
|
super(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket {
|
||||||
|
const socket = super.createConnection(options, callback)
|
||||||
|
.on('connect', () => {
|
||||||
|
const address = socket.remoteAddress;
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
if (address && ipaddr.isValid(address)) {
|
||||||
|
if (this.isPrivateIp(address)) {
|
||||||
|
socket.destroy(new Error(`Blocked address: ${address}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return socket;
|
||||||
|
};
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private isPrivateIp(ip: string): boolean {
|
||||||
|
const parsedIp = ipaddr.parse(ip);
|
||||||
|
|
||||||
|
for (const net of this.config.allowedPrivateNetworks ?? []) {
|
||||||
|
const cidr = ipaddr.parseCIDR(net);
|
||||||
|
if (cidr[0].kind() === parsedIp.kind() && parsedIp.match(ipaddr.parseCIDR(net))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedIp.range() !== 'unicast';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class HttpRequestService {
|
export class HttpRequestService {
|
||||||
/**
|
/**
|
||||||
* Get http non-proxy agent
|
* Get https non-proxy agent (without local address filtering)
|
||||||
*/
|
*/
|
||||||
private http: http.Agent;
|
private httpsNative: https.Agent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get https non-proxy agent
|
* Get https non-proxy agent
|
||||||
*/
|
*/
|
||||||
private https: https.Agent;
|
private https: https.Agent;
|
||||||
|
|
||||||
/**
|
|
||||||
* Get http proxy or non-proxy agent
|
|
||||||
*/
|
|
||||||
public httpAgent: http.Agent;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get https proxy or non-proxy agent
|
* Get https proxy or non-proxy agent
|
||||||
*/
|
*/
|
||||||
|
@ -57,34 +137,21 @@ export class HttpRequestService {
|
||||||
lookup: false, // nativeのdns.lookupにfallbackしない
|
lookup: false, // nativeのdns.lookupにfallbackしない
|
||||||
});
|
});
|
||||||
|
|
||||||
this.http = new http.Agent({
|
const agentOption = {
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
keepAliveMsecs: 30 * 1000,
|
keepAliveMsecs: 30 * 1000,
|
||||||
lookup: cache.lookup as unknown as net.LookupFunction,
|
lookup: cache.lookup as unknown as net.LookupFunction,
|
||||||
localAddress: config.outgoingAddress,
|
localAddress: config.outgoingAddress,
|
||||||
});
|
minVersion: 'TLSv1.2' as const,
|
||||||
|
};
|
||||||
|
|
||||||
this.https = new https.Agent({
|
|
||||||
keepAlive: true,
|
this.httpsNative = new https.Agent(agentOption);
|
||||||
keepAliveMsecs: 30 * 1000,
|
|
||||||
lookup: cache.lookup as unknown as net.LookupFunction,
|
this.https = new HttpsRequestServiceAgent(config, agentOption);
|
||||||
localAddress: config.outgoingAddress,
|
|
||||||
});
|
|
||||||
|
|
||||||
const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128);
|
const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128);
|
||||||
|
|
||||||
this.httpAgent = config.proxy
|
|
||||||
? new HttpProxyAgent({
|
|
||||||
keepAlive: true,
|
|
||||||
keepAliveMsecs: 30 * 1000,
|
|
||||||
maxSockets,
|
|
||||||
maxFreeSockets: 256,
|
|
||||||
scheduling: 'lifo',
|
|
||||||
proxy: config.proxy,
|
|
||||||
localAddress: config.outgoingAddress,
|
|
||||||
})
|
|
||||||
: this.http;
|
|
||||||
|
|
||||||
this.httpsAgent = config.proxy
|
this.httpsAgent = config.proxy
|
||||||
? new HttpsProxyAgent({
|
? new HttpsProxyAgent({
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
|
@ -104,16 +171,22 @@ export class HttpRequestService {
|
||||||
* @param bypassProxy Allways bypass proxy
|
* @param bypassProxy Allways bypass proxy
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public getAgentByUrl(url: URL, bypassProxy = false): http.Agent | https.Agent {
|
public getAgentByUrl(url: URL, bypassProxy = false): https.Agent {
|
||||||
|
if (url.protocol !== 'https:') {
|
||||||
|
throw new Error('Invalid protocol');
|
||||||
|
}
|
||||||
|
if (url.port && url.port !== '443') {
|
||||||
|
throw new Error('Invalid port');
|
||||||
|
}
|
||||||
if (bypassProxy || (this.config.proxyBypassHosts ?? []).includes(url.hostname)) {
|
if (bypassProxy || (this.config.proxyBypassHosts ?? []).includes(url.hostname)) {
|
||||||
return url.protocol === 'http:' ? this.http : this.https;
|
return this.https;
|
||||||
} else {
|
} else {
|
||||||
return url.protocol === 'http:' ? this.httpAgent : this.httpsAgent;
|
return this.httpsAgent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getActivityJson(url: string): Promise<IObject> {
|
public async getActivityJson(url: string, isLocalAddressAllowed = false): Promise<IObject> {
|
||||||
const res = await this.send(url, {
|
const res = await this.send(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -121,6 +194,7 @@ export class HttpRequestService {
|
||||||
},
|
},
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
size: 1024 * 256,
|
size: 1024 * 256,
|
||||||
|
isLocalAddressAllowed: isLocalAddressAllowed,
|
||||||
}, {
|
}, {
|
||||||
throwErrorWhenResponseNotOk: true,
|
throwErrorWhenResponseNotOk: true,
|
||||||
validators: [validateContentTypeSetAsActivityPub],
|
validators: [validateContentTypeSetAsActivityPub],
|
||||||
|
@ -129,13 +203,13 @@ export class HttpRequestService {
|
||||||
const finalUrl = res.url; // redirects may have been involved
|
const finalUrl = res.url; // redirects may have been involved
|
||||||
const activity = await res.json() as IObject;
|
const activity = await res.json() as IObject;
|
||||||
|
|
||||||
assertActivityMatchesUrls(activity, [url, finalUrl]);
|
assertActivityMatchesUrls(activity, [finalUrl]);
|
||||||
|
|
||||||
return activity;
|
return activity;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getJson<T = unknown>(url: string, accept = 'application/json, */*', headers?: Record<string, string>): Promise<T> {
|
public async getJson<T = unknown>(url: string, accept = 'application/json, */*', headers?: Record<string, string>, isLocalAddressAllowed = false): Promise<T> {
|
||||||
const res = await this.send(url, {
|
const res = await this.send(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: Object.assign({
|
headers: Object.assign({
|
||||||
|
@ -143,19 +217,21 @@ export class HttpRequestService {
|
||||||
}, headers ?? {}),
|
}, headers ?? {}),
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
size: 1024 * 256,
|
size: 1024 * 256,
|
||||||
|
isLocalAddressAllowed: isLocalAddressAllowed,
|
||||||
});
|
});
|
||||||
|
|
||||||
return await res.json() as T;
|
return await res.json() as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getHtml(url: string, accept = 'text/html, */*', headers?: Record<string, string>): Promise<string> {
|
public async getHtml(url: string, accept = 'text/html, */*', headers?: Record<string, string>, isLocalAddressAllowed = false): Promise<string> {
|
||||||
const res = await this.send(url, {
|
const res = await this.send(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: Object.assign({
|
headers: Object.assign({
|
||||||
Accept: accept,
|
Accept: accept,
|
||||||
}, headers ?? {}),
|
}, headers ?? {}),
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
|
isLocalAddressAllowed: isLocalAddressAllowed,
|
||||||
});
|
});
|
||||||
|
|
||||||
return await res.text();
|
return await res.text();
|
||||||
|
@ -170,6 +246,7 @@ export class HttpRequestService {
|
||||||
headers?: Record<string, string>,
|
headers?: Record<string, string>,
|
||||||
timeout?: number,
|
timeout?: number,
|
||||||
size?: number,
|
size?: number,
|
||||||
|
isLocalAddressAllowed?: boolean,
|
||||||
} = {},
|
} = {},
|
||||||
extra: HttpRequestSendOptions = {
|
extra: HttpRequestSendOptions = {
|
||||||
throwErrorWhenResponseNotOk: true,
|
throwErrorWhenResponseNotOk: true,
|
||||||
|
@ -183,6 +260,16 @@ export class HttpRequestService {
|
||||||
controller.abort();
|
controller.abort();
|
||||||
}, timeout);
|
}, timeout);
|
||||||
|
|
||||||
|
const urlParsed = new URL(url);
|
||||||
|
|
||||||
|
if (urlParsed.protocol !== 'https:') {
|
||||||
|
throw new Error('Invalid protocol');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (urlParsed.port && urlParsed.port !== '443') {
|
||||||
|
throw new Error('Invalid port');
|
||||||
|
}
|
||||||
|
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
method: args.method ?? 'GET',
|
method: args.method ?? 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -191,7 +278,7 @@ export class HttpRequestService {
|
||||||
},
|
},
|
||||||
body: args.body,
|
body: args.body,
|
||||||
size: args.size ?? 10 * 1024 * 1024,
|
size: args.size ?? 10 * 1024 * 1024,
|
||||||
agent: (url) => this.getAgentByUrl(url),
|
agent: (url) => this.getAgentByUrl(url, false),
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -56,6 +56,7 @@ import { isReply } from '@/misc/is-reply.js';
|
||||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
import { CollapsedQueue } from '@/misc/collapsed-queue.js';
|
import { CollapsedQueue } from '@/misc/collapsed-queue.js';
|
||||||
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
|
|
||||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||||
|
|
||||||
|
@ -217,6 +218,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
private instanceChart: InstanceChart,
|
private instanceChart: InstanceChart,
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private userBlockingService: UserBlockingService,
|
private userBlockingService: UserBlockingService,
|
||||||
|
private cacheService: CacheService,
|
||||||
) {
|
) {
|
||||||
this.updateNotesCountQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseNotesCount, this.performUpdateNotesCount);
|
this.updateNotesCountQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseNotesCount, this.performUpdateNotesCount);
|
||||||
}
|
}
|
||||||
|
@ -543,15 +545,23 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
this.followingsRepository.findBy({
|
this.followingsRepository.findBy({
|
||||||
followeeId: user.id,
|
followeeId: user.id,
|
||||||
notify: 'normal',
|
notify: 'normal',
|
||||||
}).then(followings => {
|
}).then(async followings => {
|
||||||
if (note.visibility !== 'specified') {
|
if (note.visibility !== 'specified') {
|
||||||
|
const isPureRenote = this.isRenote(data) && !this.isQuote(data) ? true : false;
|
||||||
for (const following of followings) {
|
for (const following of followings) {
|
||||||
// TODO: ワードミュート考慮
|
// TODO: ワードミュート考慮
|
||||||
|
let isRenoteMuted = false;
|
||||||
|
if (isPureRenote) {
|
||||||
|
const userIdsWhoMeMutingRenotes = await this.cacheService.renoteMutingsCache.fetch(following.followerId);
|
||||||
|
isRenoteMuted = userIdsWhoMeMutingRenotes.has(user.id);
|
||||||
|
}
|
||||||
|
if (!isRenoteMuted) {
|
||||||
this.notificationService.createNotification(following.followerId, 'note', {
|
this.notificationService.createNotification(following.followerId, 'note', {
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
}, user.id);
|
}, user.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { MiLocalUser, MiRemoteUser } from '@/models/User.js';
|
||||||
import { getApId } from './type.js';
|
import { getApId } from './type.js';
|
||||||
import { ApPersonService } from './models/ApPersonService.js';
|
import { ApPersonService } from './models/ApPersonService.js';
|
||||||
import type { IObject } from './type.js';
|
import type { IObject } from './type.js';
|
||||||
|
import { toASCII } from 'node:punycode';
|
||||||
|
|
||||||
export type UriParseResult = {
|
export type UriParseResult = {
|
||||||
/** wether the URI was generated by us */
|
/** wether the URI was generated by us */
|
||||||
|
@ -63,7 +64,9 @@ export class ApDbResolverService implements OnApplicationShutdown {
|
||||||
const separator = '/';
|
const separator = '/';
|
||||||
|
|
||||||
const uri = new URL(getApId(value));
|
const uri = new URL(getApId(value));
|
||||||
if (uri.origin !== this.config.url) return { local: false, uri: uri.href };
|
if (toASCII(uri.host) !== toASCII(this.config.host)) {
|
||||||
|
return { local: false, uri: uri.href };
|
||||||
|
}
|
||||||
|
|
||||||
const [, type, id, ...rest] = uri.pathname.split(separator);
|
const [, type, id, ...rest] = uri.pathname.split(separator);
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -29,7 +29,7 @@ import { bindThis } from '@/decorators.js';
|
||||||
import type { MiRemoteUser } from '@/models/User.js';
|
import type { MiRemoteUser } from '@/models/User.js';
|
||||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||||
import { AbuseReportService } from '@/core/AbuseReportService.js';
|
import { AbuseReportService } from '@/core/AbuseReportService.js';
|
||||||
import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost, yumeDowncastAccept, yumeDowncastAdd, yumeDowncastAnnounce, yumeDowncastBlock, yumeDowncastCreate, yumeDowncastDelete, yumeDowncastFlag, yumeDowncastFollow, yumeDowncastLike, yumeDowncastMove, yumeDowncastReject, yumeDowncastRemove, yumeDowncastUndo, yumeDowncastUpdate } from './type.js';
|
import { getApHrefNullable, getApId, getApIds, getApType, isActor, isCollection, isCollectionOrOrderedCollection, isPost, isTombstone, validActor, validPost, yumeDowncastAccept, yumeDowncastAdd, yumeDowncastAnnounce, yumeDowncastBlock, yumeDowncastCreate, yumeDowncastDelete, yumeDowncastFlag, yumeDowncastFollow, yumeDowncastLike, yumeDowncastMove, yumeDowncastReject, yumeDowncastRemove, yumeDowncastUndo, yumeDowncastUpdate } from './type.js';
|
||||||
import { ApNoteService } from './models/ApNoteService.js';
|
import { ApNoteService } from './models/ApNoteService.js';
|
||||||
import { ApLoggerService } from './ApLoggerService.js';
|
import { ApLoggerService } from './ApLoggerService.js';
|
||||||
import { ApDbResolverService } from './ApDbResolverService.js';
|
import { ApDbResolverService } from './ApDbResolverService.js';
|
||||||
|
@ -274,7 +274,8 @@ export class ApInboxService {
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isFollow(object)) return await this.acceptFollow(actor, object);
|
const follow = yumeDowncastFollow(object);
|
||||||
|
if (follow) return await this.acceptFollow(actor, follow);
|
||||||
|
|
||||||
return `skip: Unknown Accept type: ${getApType(object)}`;
|
return `skip: Unknown Accept type: ${getApType(object)}`;
|
||||||
}
|
}
|
||||||
|
@ -486,7 +487,7 @@ export class ApInboxService {
|
||||||
const exist = await this.apNoteService.fetchNote(note);
|
const exist = await this.apNoteService.fetchNote(note);
|
||||||
if (exist) return 'skip: note exists';
|
if (exist) return 'skip: note exists';
|
||||||
|
|
||||||
await this.apNoteService.createNote(note, resolver, silent);
|
await this.apNoteService.createNote(note, actor, resolver, silent);
|
||||||
return 'ok';
|
return 'ok';
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof StatusError && !err.isRetryable) {
|
if (err instanceof StatusError && !err.isRetryable) {
|
||||||
|
@ -623,7 +624,8 @@ export class ApInboxService {
|
||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isFollow(object)) return await this.rejectFollow(actor, object);
|
const follow = yumeDowncastFollow(object);
|
||||||
|
if (follow) return await this.rejectFollow(actor, follow);
|
||||||
|
|
||||||
return `skip: Unknown Reject type: ${getApType(object)}`;
|
return `skip: Unknown Reject type: ${getApType(object)}`;
|
||||||
}
|
}
|
||||||
|
@ -690,11 +692,20 @@ export class ApInboxService {
|
||||||
});
|
});
|
||||||
|
|
||||||
// don't queue because the sender may attempt again when timeout
|
// don't queue because the sender may attempt again when timeout
|
||||||
if (isFollow(object)) return await this.undoFollow(actor, object);
|
const follow = yumeDowncastFollow(object);
|
||||||
if (isBlock(object)) return await this.undoBlock(actor, object);
|
if (follow) return await this.undoFollow(actor, follow);
|
||||||
if (isLike(object)) return await this.undoLike(actor, object);
|
|
||||||
if (isAnnounce(object)) return await this.undoAnnounce(actor, object);
|
const block = yumeDowncastBlock(object);
|
||||||
if (isAccept(object)) return await this.undoAccept(actor, object);
|
if (block) return await this.undoBlock(actor, block);
|
||||||
|
|
||||||
|
const like = yumeDowncastLike(object);
|
||||||
|
if (like) return await this.undoLike(actor, like);
|
||||||
|
|
||||||
|
const announce = yumeDowncastAnnounce(object);
|
||||||
|
if (announce) return await this.undoAnnounce(actor, announce);
|
||||||
|
|
||||||
|
const accept = yumeDowncastAccept(object);
|
||||||
|
if (accept) return await this.undoAccept(actor, accept);
|
||||||
|
|
||||||
return `skip: unknown object type ${getApType(object)}`;
|
return `skip: unknown object type ${getApType(object)}`;
|
||||||
}
|
}
|
||||||
|
@ -824,7 +835,7 @@ export class ApInboxService {
|
||||||
await this.apPersonService.updatePerson(actor.uri, resolver, object);
|
await this.apPersonService.updatePerson(actor.uri, resolver, object);
|
||||||
return 'ok: Person updated';
|
return 'ok: Person updated';
|
||||||
} else if (getApType(object) === 'Question') {
|
} else if (getApType(object) === 'Question') {
|
||||||
await this.apQuestionService.updateQuestion(object, resolver).catch(err => console.error(err));
|
await this.apQuestionService.updateQuestion(object, actor, resolver).catch(err => console.error(err));
|
||||||
return 'ok: Question updated';
|
return 'ok: Question updated';
|
||||||
} else {
|
} else {
|
||||||
return `skip: Unknown type: ${getApType(object)}`;
|
return `skip: Unknown type: ${getApType(object)}`;
|
||||||
|
|
|
@ -11,11 +11,14 @@ import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type { MiUser } from '@/models/User.js';
|
import type { MiUser } from '@/models/User.js';
|
||||||
import { UserKeypairService } from '@/core/UserKeypairService.js';
|
import { UserKeypairService } from '@/core/UserKeypairService.js';
|
||||||
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
|
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
|
||||||
|
import { assertActivityMatchesUrls } from '@/core/activitypub/misc/check-against-url.js';
|
||||||
|
import type { IObject } from './type.js';
|
||||||
|
|
||||||
type Request = {
|
type Request = {
|
||||||
url: string;
|
url: string;
|
||||||
|
@ -145,6 +148,7 @@ export class ApRequestService {
|
||||||
private userKeypairService: UserKeypairService,
|
private userKeypairService: UserKeypairService,
|
||||||
private httpRequestService: HttpRequestService,
|
private httpRequestService: HttpRequestService,
|
||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
|
private utilityService: UtilityService,
|
||||||
) {
|
) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
this.logger = this.loggerService?.getLogger('ap-request'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる
|
this.logger = this.loggerService?.getLogger('ap-request'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる
|
||||||
|
@ -251,7 +255,11 @@ export class ApRequestService {
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
validateContentTypeSetAsActivityPub(res);
|
validateContentTypeSetAsActivityPub(res);
|
||||||
|
const finalUrl = res.url; // redirects may have been involved
|
||||||
|
const activity = await res.json() as IObject;
|
||||||
|
|
||||||
return await res.json();
|
assertActivityMatchesUrls(activity, [url, finalUrl]);
|
||||||
|
|
||||||
|
return activity;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,8 @@ import { ApDbResolverService } from './ApDbResolverService.js';
|
||||||
import { ApRendererService } from './ApRendererService.js';
|
import { ApRendererService } from './ApRendererService.js';
|
||||||
import { ApRequestService } from './ApRequestService.js';
|
import { ApRequestService } from './ApRequestService.js';
|
||||||
import type { IObject, ICollection, IOrderedCollection, IUnsanitizedObject } from './type.js';
|
import type { IObject, ICollection, IOrderedCollection, IUnsanitizedObject } from './type.js';
|
||||||
|
import { toASCII } from 'node:punycode';
|
||||||
|
import { yumeAssertAcceptableURL } from './misc/validator.js';
|
||||||
|
|
||||||
export class Resolver {
|
export class Resolver {
|
||||||
private history: Set<string>;
|
private history: Set<string>;
|
||||||
|
@ -53,6 +55,11 @@ export class Resolver {
|
||||||
return Array.from(this.history);
|
return Array.from(this.history);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public getRecursionLimit(): number {
|
||||||
|
return this.recursionLimit;
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async resolveCollection(value: string | IObject): Promise<ICollection | IOrderedCollection> {
|
public async resolveCollection(value: string | IObject): Promise<ICollection | IOrderedCollection> {
|
||||||
const collection = typeof value === 'string'
|
const collection = typeof value === 'string'
|
||||||
|
@ -103,8 +110,8 @@ export class Resolver {
|
||||||
}
|
}
|
||||||
|
|
||||||
const object = (this.user
|
const object = (this.user
|
||||||
? await this.apRequestService.signedGet(value, this.user) as IObject
|
? await this.apRequestService.signedGet(value, this.user) as IUnsanitizedObject
|
||||||
: await this.httpRequestService.getActivityJson(value)) as IObject;
|
: await this.httpRequestService.getActivityJson(value)) as IUnsanitizedObject;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
Array.isArray(object['@context']) ?
|
Array.isArray(object['@context']) ?
|
||||||
|
@ -114,6 +121,21 @@ export class Resolver {
|
||||||
throw new Error('invalid response');
|
throw new Error('invalid response');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HttpRequestService / ApRequestService have already checked that
|
||||||
|
// `object.id` or `object.url` matches the URL used to fetch the
|
||||||
|
// object after redirects; here we double-check that no redirects
|
||||||
|
// bounced between hosts
|
||||||
|
if (object.id == null) {
|
||||||
|
throw new Error('invalid AP object: missing id');
|
||||||
|
}
|
||||||
|
|
||||||
|
const idURL = yumeAssertAcceptableURL(object.id);
|
||||||
|
const valueURL = yumeAssertAcceptableURL(value);
|
||||||
|
|
||||||
|
if (toASCII(idURL.host) !== toASCII(valueURL.host)) {
|
||||||
|
throw new Bull.UnrecoverableError(`invalid AP object ${value}: id ${object.id} has different host`);
|
||||||
|
}
|
||||||
|
|
||||||
return object;
|
return object;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,28 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Response } from 'node-fetch';
|
import type { Response } from 'node-fetch';
|
||||||
|
import * as Bull from 'bullmq';
|
||||||
|
import { toASCII } from 'node:punycode';
|
||||||
|
|
||||||
|
export function yumeAssertAcceptableURL(url: string | URL): URL {
|
||||||
|
const urlParsed = url instanceof URL ? url : new URL(url);
|
||||||
|
|
||||||
|
if (urlParsed.search.length + urlParsed.pathname.length > 1024) {
|
||||||
|
throw new Bull.UnrecoverableError('URL is too long');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (urlParsed.protocol !== 'https:') {
|
||||||
|
throw new Bull.UnrecoverableError('URL protocol is not https');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (urlParsed.port && urlParsed.port !== '443') {
|
||||||
|
throw new Bull.UnrecoverableError('URL port is not 443');
|
||||||
|
}
|
||||||
|
|
||||||
|
urlParsed.hostname = toASCII(urlParsed.hostname);
|
||||||
|
|
||||||
|
return urlParsed;
|
||||||
|
}
|
||||||
|
|
||||||
export function validateContentTypeSetAsActivityPub(response: Response): void {
|
export function validateContentTypeSetAsActivityPub(response: Response): void {
|
||||||
const contentType = (response.headers.get('content-type') ?? '').toLowerCase();
|
const contentType = (response.headers.get('content-type') ?? '').toLowerCase();
|
||||||
|
|
|
@ -36,6 +36,7 @@ import { ApQuestionService } from './ApQuestionService.js';
|
||||||
import { ApImageService } from './ApImageService.js';
|
import { ApImageService } from './ApImageService.js';
|
||||||
import type { Resolver } from '../ApResolverService.js';
|
import type { Resolver } from '../ApResolverService.js';
|
||||||
import type { IObject, IPost } from '../type.js';
|
import type { IObject, IPost } from '../type.js';
|
||||||
|
import { yumeAssertAcceptableURL } from '../misc/validator.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApNoteService {
|
export class ApNoteService {
|
||||||
|
@ -77,7 +78,7 @@ export class ApNoteService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public validateNote(object: IObject, uri: string): Error | null {
|
public validateNote(object: IObject, uri: string, actor?: MiRemoteUser): Error | null {
|
||||||
const expectHost = this.utilityService.extractDbHost(uri);
|
const expectHost = this.utilityService.extractDbHost(uri);
|
||||||
const apType = getApType(object);
|
const apType = getApType(object);
|
||||||
|
|
||||||
|
@ -98,6 +99,14 @@ export class ApNoteService {
|
||||||
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', 'invalid Note: published timestamp is malformed');
|
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', 'invalid Note: published timestamp is malformed');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (actor) {
|
||||||
|
const attribution = (object.attributedTo) ? getOneApId(object.attributedTo) : actor.uri;
|
||||||
|
|
||||||
|
if (attribution !== actor.uri) {
|
||||||
|
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: attribution does not match the actor that send it. attribution: ${attribution}, actor: ${actor.uri}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,11 +124,14 @@ export class ApNoteService {
|
||||||
* Noteを作成します。
|
* Noteを作成します。
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async createNote(value: string | IObject, resolver: Resolver, silent = false): Promise<MiNote | null> {
|
public async createNote(value: string | IObject, actor?: MiRemoteUser, resolver?: Resolver, silent = false): Promise<MiNote | null> {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
if (resolver == null) resolver = this.apResolverService.createResolver();
|
||||||
|
|
||||||
const object = await resolver.resolve(value);
|
const object = await resolver.resolve(value);
|
||||||
|
|
||||||
const entryUri = getApId(value);
|
const entryUri = getApId(value);
|
||||||
const err = this.validateNote(object, entryUri);
|
const err = this.validateNote(object, entryUri, actor);
|
||||||
if (err) {
|
if (err) {
|
||||||
this.logger.error(err.message, {
|
this.logger.error(err.message, {
|
||||||
resolver: { history: resolver.getHistory() },
|
resolver: { history: resolver.getHistory() },
|
||||||
|
@ -133,16 +145,29 @@ export class ApNoteService {
|
||||||
|
|
||||||
this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
|
this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
|
||||||
|
|
||||||
if (note.id && !checkHttps(note.id)) {
|
if (note.id == null) {
|
||||||
|
throw new Error('Refusing to create note without id');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!checkHttps(note.id)) {
|
||||||
throw new Error('unexpected schema of note.id: ' + note.id);
|
throw new Error('unexpected schema of note.id: ' + note.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = getOneApHrefNullable(note.url);
|
const url = getOneApHrefNullable(note.url);
|
||||||
|
|
||||||
if (url && !checkHttps(url)) {
|
if (url != null) {
|
||||||
|
if (!checkHttps(url)) {
|
||||||
throw new Error('unexpected schema of note url: ' + url);
|
throw new Error('unexpected schema of note url: ' + url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const actUrl = yumeAssertAcceptableURL(url);
|
||||||
|
const noteUrl = yumeAssertAcceptableURL(note.id);
|
||||||
|
|
||||||
|
if (noteUrl.host !== actUrl.host) {
|
||||||
|
throw new Error(`note url & uri host mismatch: note url: ${url}, note uri: ${note.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.logger.info(`Creating the Note: ${note.id}`);
|
this.logger.info(`Creating the Note: ${note.id}`);
|
||||||
|
|
||||||
// 投稿者をフェッチ
|
// 投稿者をフェッチ
|
||||||
|
@ -153,8 +178,9 @@ export class ApNoteService {
|
||||||
const uri = getOneApId(note.attributedTo);
|
const uri = getOneApId(note.attributedTo);
|
||||||
|
|
||||||
// ローカルで投稿者を検索し、もし凍結されていたらスキップ
|
// ローカルで投稿者を検索し、もし凍結されていたらスキップ
|
||||||
const cachedActor = await this.apPersonService.fetchPerson(uri) as MiRemoteUser;
|
// eslint-disable-next-line no-param-reassign
|
||||||
if (cachedActor && cachedActor.isSuspended) {
|
actor ??= await this.apPersonService.fetchPerson(uri) as MiRemoteUser | undefined;
|
||||||
|
if (actor && actor.isSuspended) {
|
||||||
throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended');
|
throw new IdentifiableError('85ab9bd7-3a41-4530-959d-f07073900109', 'actor has been suspended');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,7 +212,8 @@ export class ApNoteService {
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
const actor = cachedActor ?? await this.apPersonService.resolvePerson(uri, resolver) as MiRemoteUser;
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
actor ??= await this.apPersonService.resolvePerson(uri, resolver) as MiRemoteUser;
|
||||||
|
|
||||||
// 解決した投稿者が凍結されていたらスキップ
|
// 解決した投稿者が凍結されていたらスキップ
|
||||||
if (actor.isSuspended) {
|
if (actor.isSuspended) {
|
||||||
|
@ -345,15 +372,11 @@ export class ApNoteService {
|
||||||
if (exist) return exist;
|
if (exist) return exist;
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
if (uri.startsWith(this.config.url)) {
|
|
||||||
throw new StatusError('cannot resolve local note', 400, 'cannot resolve local note');
|
|
||||||
}
|
|
||||||
|
|
||||||
// リモートサーバーからフェッチしてきて登録
|
// リモートサーバーからフェッチしてきて登録
|
||||||
// ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにノートが生成されるが
|
// ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにノートが生成されるが
|
||||||
// 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。
|
// 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。
|
||||||
const createFrom = options.sentFrom?.origin === new URL(uri).origin ? value : uri;
|
const createFrom = options.sentFrom?.origin === new URL(uri).origin ? value : uri;
|
||||||
return await this.createNote(createFrom, this.apResolverService.createResolver(), true);
|
return await this.createNote(createFrom, undefined, options.resolver, true);
|
||||||
} finally {
|
} finally {
|
||||||
unlock();
|
unlock();
|
||||||
}
|
}
|
||||||
|
|
|
@ -661,7 +661,7 @@ export class ApPersonService implements OnModuleInit {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async updateFeatured(userId: MiUser['id'], resolver?: Resolver): Promise<void> {
|
public async updateFeatured(userId: MiUser['id'], resolver?: Resolver): Promise<void> {
|
||||||
const user = await this.usersRepository.findOneByOrFail({ id: userId, isDeleted: false });
|
const user = await this.usersRepository.findOneByOrFail({ id: userId });
|
||||||
if (!this.userEntityService.isRemoteUser(user)) return;
|
if (!this.userEntityService.isRemoteUser(user)) return;
|
||||||
if (!user.featured) return;
|
if (!user.featured) return;
|
||||||
|
|
||||||
|
|
|
@ -5,16 +5,19 @@
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { NotesRepository, PollsRepository } from '@/models/_.js';
|
import type { UsersRepository, NotesRepository, PollsRepository } from '@/models/_.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type { IPoll } from '@/models/Poll.js';
|
import type { IPoll } from '@/models/Poll.js';
|
||||||
|
import type { MiRemoteUser } from '@/models/User.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { isQuestion } from '../type.js';
|
import { getOneApId, isQuestion } from '../type.js';
|
||||||
import { ApLoggerService } from '../ApLoggerService.js';
|
import { ApLoggerService } from '../ApLoggerService.js';
|
||||||
import { ApResolverService } from '../ApResolverService.js';
|
import { ApResolverService } from '../ApResolverService.js';
|
||||||
import type { Resolver } from '../ApResolverService.js';
|
import type { Resolver } from '../ApResolverService.js';
|
||||||
import type { IObject, IQuestion } from '../type.js';
|
import type { IObject } from '../type.js';
|
||||||
|
import { yumeAssertAcceptableURL } from '../misc/validator.js';
|
||||||
|
import { toASCII } from 'punycode';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApQuestionService {
|
export class ApQuestionService {
|
||||||
|
@ -24,6 +27,9 @@ export class ApQuestionService {
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
|
@Inject(DI.usersRepository)
|
||||||
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
@Inject(DI.notesRepository)
|
@Inject(DI.notesRepository)
|
||||||
private notesRepository: NotesRepository,
|
private notesRepository: NotesRepository,
|
||||||
|
|
||||||
|
@ -65,28 +71,41 @@ export class ApQuestionService {
|
||||||
* @returns true if updated
|
* @returns true if updated
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async updateQuestion(value: string | IObject, resolver?: Resolver): Promise<boolean> {
|
public async updateQuestion(value: string | IObject, actor?: MiRemoteUser, resolver?: Resolver): Promise<boolean> {
|
||||||
const uri = typeof value === 'string' ? value : value.id;
|
const uriIn = typeof value === 'string' ? value : value.id;
|
||||||
if (uri == null) throw new Error('uri is null');
|
if (uriIn == null) throw new Error('uri is null');
|
||||||
|
|
||||||
// URIがこのサーバーを指しているならスキップ
|
// URIがこのサーバーを指しているならスキップ
|
||||||
if (uri.startsWith(this.config.url + '/')) throw new Error('uri points local');
|
const uri = yumeAssertAcceptableURL(uriIn);
|
||||||
|
|
||||||
|
if (toASCII(this.config.host) === uri.host) throw new Error('uri points local');
|
||||||
|
|
||||||
//#region このサーバーに既に登録されているか
|
//#region このサーバーに既に登録されているか
|
||||||
const note = await this.notesRepository.findOneBy({ uri });
|
const note = await this.notesRepository.findOneBy({ uri: uriIn });
|
||||||
if (note == null) throw new Error('Question is not registered');
|
if (note == null) throw new Error('Question is not registered');
|
||||||
|
|
||||||
const poll = await this.pollsRepository.findOneBy({ noteId: note.id });
|
const poll = await this.pollsRepository.findOneBy({ noteId: note.id });
|
||||||
if (poll == null) throw new Error('Question is not registered');
|
if (poll == null) throw new Error('Question is not registered');
|
||||||
|
|
||||||
|
const user = await this.usersRepository.findOneBy({ id: poll.userId });
|
||||||
|
if (user == null) throw new Error('Question is not registered');
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
// resolve new Question object
|
// resolve new Question object
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
if (resolver == null) resolver = this.apResolverService.createResolver();
|
if (resolver == null) resolver = this.apResolverService.createResolver();
|
||||||
const question = await resolver.resolve(value) as IQuestion;
|
const question = await resolver.resolve(value);
|
||||||
this.logger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`);
|
this.logger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`);
|
||||||
|
|
||||||
if (question.type !== 'Question') throw new Error('object is not a Question');
|
if (!isQuestion(question)) throw new Error('object is not a Question');
|
||||||
|
|
||||||
|
const attribution = (question.attributedTo) ? getOneApId(question.attributedTo) : user.uri;
|
||||||
|
const attributionMatchesExisting = attribution === user.uri;
|
||||||
|
const actorMatchesAttribution = (actor) ? attribution === actor.uri : true;
|
||||||
|
|
||||||
|
if (!attributionMatchesExisting || !actorMatchesAttribution) {
|
||||||
|
throw new Error('Refusing to ingest update for poll by different user');
|
||||||
|
}
|
||||||
|
|
||||||
const apChoices = question.oneOf ?? question.anyOf;
|
const apChoices = question.oneOf ?? question.anyOf;
|
||||||
if (apChoices == null) throw new Error('invalid apChoices: ' + apChoices);
|
if (apChoices == null) throw new Error('invalid apChoices: ' + apChoices);
|
||||||
|
@ -96,7 +115,7 @@ export class ApQuestionService {
|
||||||
for (const choice of poll.choices) {
|
for (const choice of poll.choices) {
|
||||||
const oldCount = poll.votes[poll.choices.indexOf(choice)];
|
const oldCount = poll.votes[poll.choices.indexOf(choice)];
|
||||||
const newCount = apChoices.filter(ap => ap.name === choice).at(0)?.replies?.totalItems;
|
const newCount = apChoices.filter(ap => ap.name === choice).at(0)?.replies?.totalItems;
|
||||||
if (newCount == null) throw new Error('invalid newCount: ' + newCount);
|
if (newCount == null || !(Number.isInteger(newCount) && newCount >= 0)) throw new Error('invalid newCount: ' + newCount);
|
||||||
|
|
||||||
if (oldCount !== newCount) {
|
if (oldCount !== newCount) {
|
||||||
changed = true;
|
changed = true;
|
||||||
|
|
|
@ -3,22 +3,45 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { target } from "happy-dom/lib/PropertySymbol.js";
|
||||||
import { toASCII } from "node:punycode";
|
import { toASCII } from "node:punycode";
|
||||||
|
import * as bull from "bullmq";
|
||||||
|
|
||||||
export type Obj = { [x: string]: any };
|
export type Obj = { [x: string]: any };
|
||||||
export type ApObject = IObject | string | (IObject | string)[];
|
export type ApObject = IObject | string | (IObject | string)[];
|
||||||
|
|
||||||
export interface IUnsanitizedObject {
|
export interface MisskeyVendorKeys {
|
||||||
|
_misskey_summary: string;
|
||||||
|
_misskey_followedMessage: string | null;
|
||||||
|
_misskey_requireSigninToViewContents: boolean;
|
||||||
|
_misskey_makeNotesFollowersOnlyBefore: number | null;
|
||||||
|
_misskey_makeNotesHiddenBefore: number | null;
|
||||||
|
_misskey_quote: string;
|
||||||
|
_misskey_content: string;
|
||||||
|
_misskey_reaction: string;
|
||||||
|
_misskey_votes: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractMisskeyVendorKeys(object: IObject): Partial<MisskeyVendorKeys> {
|
||||||
|
return {
|
||||||
|
_misskey_summary: object._misskey_summary,
|
||||||
|
_misskey_followedMessage: object._misskey_followedMessage,
|
||||||
|
_misskey_requireSigninToViewContents: object._misskey_requireSigninToViewContents,
|
||||||
|
_misskey_makeNotesFollowersOnlyBefore: object._misskey_makeNotesFollowersOnlyBefore,
|
||||||
|
_misskey_makeNotesHiddenBefore: object._misskey_makeNotesHiddenBefore,
|
||||||
|
_misskey_quote: object._misskey_quote,
|
||||||
|
_misskey_content: object._misskey_content,
|
||||||
|
_misskey_reaction: object._misskey_reaction,
|
||||||
|
_misskey_votes: object._misskey_votes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUnsanitizedObject extends Partial<MisskeyVendorKeys> {
|
||||||
'@context'?: string | string[] | Obj | Obj[];
|
'@context'?: string | string[] | Obj | Obj[];
|
||||||
type: string | string[];
|
type: string | string[];
|
||||||
id?: string;
|
id?: string;
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
summary?: string;
|
summary?: string;
|
||||||
_misskey_summary?: string;
|
|
||||||
_misskey_followedMessage?: string | null;
|
|
||||||
_misskey_requireSigninToViewContents?: boolean;
|
|
||||||
_misskey_makeNotesFollowersOnlyBefore?: number | null;
|
|
||||||
_misskey_makeNotesHiddenBefore?: number | null;
|
|
||||||
published?: string;
|
published?: string;
|
||||||
cc?: ApObject;
|
cc?: ApObject;
|
||||||
to?: ApObject;
|
to?: ApObject;
|
||||||
|
@ -36,6 +59,10 @@ export interface IUnsanitizedObject {
|
||||||
href?: string;
|
href?: string;
|
||||||
tag?: IObject | IObject[];
|
tag?: IObject | IObject[];
|
||||||
sensitive?: boolean;
|
sensitive?: boolean;
|
||||||
|
|
||||||
|
visibility?: string;
|
||||||
|
mentionedUsers?: any[];
|
||||||
|
visibleUsers?: any[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IObject extends IUnsanitizedObject {
|
export interface IObject extends IUnsanitizedObject {
|
||||||
|
@ -55,11 +82,11 @@ export function yumeNormalizeURL(url: string): string {
|
||||||
u.hash = '';
|
u.hash = '';
|
||||||
u.host = toASCII(u.host);
|
u.host = toASCII(u.host);
|
||||||
if (u.protocol && u.protocol !== 'https:') {
|
if (u.protocol && u.protocol !== 'https:') {
|
||||||
throw new Error('protocol is not https');
|
throw new bull.UnrecoverableError('protocol is not https');
|
||||||
}
|
}
|
||||||
u.protocol = 'https:';
|
u.protocol = 'https:';
|
||||||
if (u.port && u.port !== '443') {
|
if (u.port && u.port !== '443') {
|
||||||
throw new Error('port is not 443');
|
throw new bull.UnrecoverableError('port is not 443');
|
||||||
}
|
}
|
||||||
return u.toString();
|
return u.toString();
|
||||||
}
|
}
|
||||||
|
@ -67,7 +94,7 @@ export function yumeNormalizeURL(url: string): string {
|
||||||
export function yumeNormalizeRecursive<O extends IUnsanitizedObject | string | (IUnsanitizedObject | string)[]>(object: O, depth = 0):
|
export function yumeNormalizeRecursive<O extends IUnsanitizedObject | string | (IUnsanitizedObject | string)[]>(object: O, depth = 0):
|
||||||
IObject | string | (IObject | string)[] {
|
IObject | string | (IObject | string)[] {
|
||||||
if (depth > 16) {
|
if (depth > 16) {
|
||||||
throw new Error('recursion limit exceeded');
|
throw new bull.UnrecoverableError('recursion limit exceeded');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof object === 'string') {
|
if (typeof object === 'string') {
|
||||||
|
@ -75,7 +102,7 @@ export function yumeNormalizeRecursive<O extends IUnsanitizedObject | string | (
|
||||||
}
|
}
|
||||||
if (Array.isArray(object)) {
|
if (Array.isArray(object)) {
|
||||||
if (object.length > 64) {
|
if (object.length > 64) {
|
||||||
throw new Error('array length limit exceeded');
|
throw new bull.UnrecoverableError('array length limit exceeded');
|
||||||
}
|
}
|
||||||
return object.flatMap(yumeNormalizeRecursive);
|
return object.flatMap(yumeNormalizeRecursive);
|
||||||
}
|
}
|
||||||
|
@ -87,12 +114,6 @@ export function yumeNormalizeObject(object: IUnsanitizedObject): IObject {
|
||||||
if (object.cc) {
|
if (object.cc) {
|
||||||
object.cc = yumeNormalizeRecursive(object.cc);
|
object.cc = yumeNormalizeRecursive(object.cc);
|
||||||
}
|
}
|
||||||
if (object.to) {
|
|
||||||
object.to = yumeNormalizeRecursive(object.to);
|
|
||||||
}
|
|
||||||
if (object.attributedTo) {
|
|
||||||
object.attributedTo = yumeNormalizeRecursive(object.attributedTo);
|
|
||||||
}
|
|
||||||
if (object.id) {
|
if (object.id) {
|
||||||
object.id = yumeNormalizeURL(object.id);
|
object.id = yumeNormalizeURL(object.id);
|
||||||
}
|
}
|
||||||
|
@ -101,18 +122,11 @@ export function yumeNormalizeObject(object: IUnsanitizedObject): IObject {
|
||||||
object.url = yumeNormalizeRecursive(object.url);
|
object.url = yumeNormalizeRecursive(object.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (object.attachment) {
|
|
||||||
object.attachment = object.attachment.map(yumeNormalizeRecursive);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (object.inReplyTo) {
|
if (object.inReplyTo) {
|
||||||
object.inReplyTo = yumeNormalizeRecursive(object.inReplyTo);
|
object.inReplyTo = yumeNormalizeRecursive(object.inReplyTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return object as IObject;
|
||||||
...object,
|
|
||||||
__yume_normalized_object: true,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -180,6 +194,24 @@ export interface IActivity extends IObject {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SafeList {
|
||||||
|
id: string;
|
||||||
|
published: string;
|
||||||
|
visibility: string;
|
||||||
|
mentionedUsers: any[];
|
||||||
|
visibleUsers: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractSafe(object: IObject): Partial<SafeList> {
|
||||||
|
return {
|
||||||
|
id: object.id,
|
||||||
|
published: object.published,
|
||||||
|
visibility: object.visibility,
|
||||||
|
mentionedUsers: object.mentionedUsers,
|
||||||
|
visibleUsers: object.visibleUsers,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface ICollection extends IObject {
|
export interface ICollection extends IObject {
|
||||||
type: 'Collection';
|
type: 'Collection';
|
||||||
totalItems: number;
|
totalItems: number;
|
||||||
|
@ -361,99 +393,83 @@ export const isDocument = (object: IObject): object is IApDocument => {
|
||||||
return type != null && validDocumentTypes.includes(type);
|
return type != null && validDocumentTypes.includes(type);
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IApImage extends IApDocument {
|
export interface IApImage extends IApDocument, Partial<SafeList> {
|
||||||
type: 'Image';
|
type: 'Image';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICreate extends IActivity {
|
export interface ICreate extends IActivity, Partial<SafeList> {
|
||||||
type: 'Create';
|
type: 'Create';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IDelete extends IActivity {
|
export interface IDelete extends IActivity, Partial<SafeList> {
|
||||||
type: 'Delete';
|
type: 'Delete';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUpdate extends IActivity {
|
export interface IUpdate extends IActivity, Partial<SafeList> {
|
||||||
type: 'Update';
|
type: 'Update';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IRead extends IActivity {
|
export interface IRead extends IActivity, Partial<SafeList> {
|
||||||
type: 'Read';
|
type: 'Read';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUndo extends IActivity {
|
export interface IUndo extends IActivity, Partial<SafeList> {
|
||||||
type: 'Undo';
|
type: 'Undo';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IFollow extends IActivity {
|
export interface IFollow extends IActivity, Partial<SafeList> {
|
||||||
type: 'Follow';
|
type: 'Follow';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAccept extends IActivity {
|
export interface IAccept extends IActivity, Partial<SafeList> {
|
||||||
type: 'Accept';
|
type: 'Accept';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IReject extends IActivity {
|
export interface IReject extends IActivity, Partial<SafeList> {
|
||||||
type: 'Reject';
|
type: 'Reject';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAdd extends IActivity {
|
export interface IAdd extends IActivity, Partial<SafeList> {
|
||||||
type: 'Add';
|
type: 'Add';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IRemove extends IActivity {
|
export interface IRemove extends IActivity, Partial<SafeList> {
|
||||||
type: 'Remove';
|
type: 'Remove';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ILike extends IActivity {
|
export interface ILike extends IActivity, Partial<SafeList> {
|
||||||
type: 'Like' | 'EmojiReaction' | 'EmojiReact';
|
type: 'Like' | 'EmojiReaction' | 'EmojiReact';
|
||||||
_misskey_reaction?: string;
|
_misskey_reaction?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAnnounce extends IActivity {
|
export interface IAnnounce extends IActivity, Partial<SafeList> {
|
||||||
type: 'Announce';
|
type: 'Announce';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBlock extends IActivity {
|
export interface IBlock extends IActivity, Partial<SafeList> {
|
||||||
type: 'Block';
|
type: 'Block';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IFlag extends IActivity {
|
export interface IFlag extends IActivity, Partial<SafeList> {
|
||||||
type: 'Flag';
|
type: 'Flag';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IMove extends IActivity {
|
export interface IMove extends IActivity, Partial<SafeList> {
|
||||||
type: 'Move';
|
type: 'Move';
|
||||||
target: IObject | string;
|
target: IObject | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isCreate = (object: IObject): object is ICreate => getApType(object) === 'Create';
|
|
||||||
export const isDelete = (object: IObject): object is IDelete => getApType(object) === 'Delete';
|
|
||||||
export const isUpdate = (object: IObject): object is IUpdate => getApType(object) === 'Update';
|
|
||||||
export const isRead = (object: IObject): object is IRead => getApType(object) === 'Read';
|
|
||||||
export const isUndo = (object: IObject): object is IUndo => getApType(object) === 'Undo';
|
|
||||||
export const isFollow = (object: IObject): object is IFollow => getApType(object) === 'Follow';
|
|
||||||
export const isAccept = (object: IObject): object is IAccept => getApType(object) === 'Accept';
|
|
||||||
export const isReject = (object: IObject): object is IReject => getApType(object) === 'Reject';
|
|
||||||
export const isAdd = (object: IObject): object is IAdd => getApType(object) === 'Add';
|
|
||||||
export const isRemove = (object: IObject): object is IRemove => getApType(object) === 'Remove';
|
|
||||||
export const isLike = (object: IObject): object is ILike => {
|
|
||||||
const type = getApType(object);
|
|
||||||
return type != null && ['Like', 'EmojiReaction', 'EmojiReact'].includes(type);
|
|
||||||
};
|
|
||||||
export const isAnnounce = (object: IObject): object is IAnnounce => getApType(object) === 'Announce';
|
|
||||||
export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block';
|
|
||||||
export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag';
|
|
||||||
export const isMove = (object: IObject): object is IMove => getApType(object) === 'Move';
|
|
||||||
export const isNote = (object: IObject): object is IPost => getApType(object) === 'Note';
|
|
||||||
|
|
||||||
export function yumeDowncastCreate(object: IObject): ICreate | null {
|
export function yumeDowncastCreate(object: IObject): ICreate | null {
|
||||||
if (getApType(object) !== 'Create') return null;
|
if (getApType(object) !== 'Create') return null;
|
||||||
const obj = object as ICreate;
|
const obj = object as ICreate;
|
||||||
|
if (!obj.actor || !obj.object) return null;
|
||||||
return {
|
return {
|
||||||
|
...extractMisskeyVendorKeys(object),
|
||||||
|
...extractSafe(object),
|
||||||
type: 'Create',
|
type: 'Create',
|
||||||
actor: obj.actor,
|
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
|
||||||
object: obj.object,
|
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
|
||||||
|
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
|
||||||
__yume_normalized_object: true,
|
__yume_normalized_object: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -461,10 +477,14 @@ export function yumeDowncastCreate(object: IObject): ICreate | null {
|
||||||
export function yumeDowncastDelete(object: IObject): IDelete | null {
|
export function yumeDowncastDelete(object: IObject): IDelete | null {
|
||||||
if (getApType(object) !== 'Delete') return null;
|
if (getApType(object) !== 'Delete') return null;
|
||||||
const obj = object as IDelete;
|
const obj = object as IDelete;
|
||||||
|
if (!obj.actor || !obj.object) return null;
|
||||||
return {
|
return {
|
||||||
|
...extractMisskeyVendorKeys(object),
|
||||||
|
...extractSafe(object),
|
||||||
type: 'Delete',
|
type: 'Delete',
|
||||||
actor: obj.actor,
|
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
|
||||||
object: obj.object,
|
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
|
||||||
|
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
|
||||||
__yume_normalized_object: true,
|
__yume_normalized_object: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -472,10 +492,14 @@ export function yumeDowncastDelete(object: IObject): IDelete | null {
|
||||||
export function yumeDowncastUpdate(object: IObject): IUpdate | null {
|
export function yumeDowncastUpdate(object: IObject): IUpdate | null {
|
||||||
if (getApType(object) !== 'Update') return null;
|
if (getApType(object) !== 'Update') return null;
|
||||||
const obj = object as IUpdate;
|
const obj = object as IUpdate;
|
||||||
|
if (!obj.actor || !obj.object) return null;
|
||||||
return {
|
return {
|
||||||
|
...extractMisskeyVendorKeys(object),
|
||||||
|
...extractSafe(object),
|
||||||
type: 'Update',
|
type: 'Update',
|
||||||
actor: obj.actor,
|
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
|
||||||
object: obj.object,
|
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
|
||||||
|
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
|
||||||
__yume_normalized_object: true,
|
__yume_normalized_object: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -483,10 +507,14 @@ export function yumeDowncastUpdate(object: IObject): IUpdate | null {
|
||||||
export function yumeDowncastRead(object: IObject): IRead | null {
|
export function yumeDowncastRead(object: IObject): IRead | null {
|
||||||
if (getApType(object) !== 'Read') return null;
|
if (getApType(object) !== 'Read') return null;
|
||||||
const obj = object as IRead;
|
const obj = object as IRead;
|
||||||
|
if (!obj.actor || !obj.object) return null;
|
||||||
return {
|
return {
|
||||||
|
...extractMisskeyVendorKeys(object),
|
||||||
|
...extractSafe(object),
|
||||||
type: 'Read',
|
type: 'Read',
|
||||||
actor: obj.actor,
|
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
|
||||||
object: obj.object,
|
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
|
||||||
|
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
|
||||||
__yume_normalized_object: true,
|
__yume_normalized_object: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -494,10 +522,14 @@ export function yumeDowncastRead(object: IObject): IRead | null {
|
||||||
export function yumeDowncastUndo(object: IObject): IUndo | null {
|
export function yumeDowncastUndo(object: IObject): IUndo | null {
|
||||||
if (getApType(object) !== 'Undo') return null;
|
if (getApType(object) !== 'Undo') return null;
|
||||||
const obj = object as IUndo;
|
const obj = object as IUndo;
|
||||||
|
if (!obj.actor || !obj.object) return null;
|
||||||
return {
|
return {
|
||||||
|
...extractMisskeyVendorKeys(object),
|
||||||
|
...extractSafe(object),
|
||||||
type: 'Undo',
|
type: 'Undo',
|
||||||
actor: obj.actor,
|
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
|
||||||
object: obj.object,
|
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
|
||||||
|
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
|
||||||
__yume_normalized_object: true,
|
__yume_normalized_object: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -505,10 +537,14 @@ export function yumeDowncastUndo(object: IObject): IUndo | null {
|
||||||
export function yumeDowncastFollow(object: IObject): IFollow | null {
|
export function yumeDowncastFollow(object: IObject): IFollow | null {
|
||||||
if (getApType(object) !== 'Follow') return null;
|
if (getApType(object) !== 'Follow') return null;
|
||||||
const obj = object as IFollow;
|
const obj = object as IFollow;
|
||||||
|
if (!obj.actor || !obj.object) return null;
|
||||||
return {
|
return {
|
||||||
|
...extractMisskeyVendorKeys(object),
|
||||||
|
...extractSafe(object),
|
||||||
type: 'Follow',
|
type: 'Follow',
|
||||||
actor: obj.actor,
|
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
|
||||||
object: obj.object,
|
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
|
||||||
|
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
|
||||||
__yume_normalized_object: true,
|
__yume_normalized_object: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -516,10 +552,14 @@ export function yumeDowncastFollow(object: IObject): IFollow | null {
|
||||||
export function yumeDowncastAccept(object: IObject): IAccept | null {
|
export function yumeDowncastAccept(object: IObject): IAccept | null {
|
||||||
if (getApType(object) !== 'Accept') return null;
|
if (getApType(object) !== 'Accept') return null;
|
||||||
const obj = object as IAccept;
|
const obj = object as IAccept;
|
||||||
|
if (!obj.actor || !obj.object) return null;
|
||||||
return {
|
return {
|
||||||
|
...extractMisskeyVendorKeys(object),
|
||||||
|
...extractSafe(object),
|
||||||
type: 'Accept',
|
type: 'Accept',
|
||||||
actor: obj.actor,
|
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
|
||||||
object: obj.object,
|
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
|
||||||
|
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
|
||||||
__yume_normalized_object: true,
|
__yume_normalized_object: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -527,10 +567,14 @@ export function yumeDowncastAccept(object: IObject): IAccept | null {
|
||||||
export function yumeDowncastReject(object: IObject): IReject | null {
|
export function yumeDowncastReject(object: IObject): IReject | null {
|
||||||
if (getApType(object) !== 'Reject') return null;
|
if (getApType(object) !== 'Reject') return null;
|
||||||
const obj = object as IReject;
|
const obj = object as IReject;
|
||||||
|
if (!obj.actor || !obj.object) return null;
|
||||||
return {
|
return {
|
||||||
|
...extractMisskeyVendorKeys(object),
|
||||||
|
...extractSafe(object),
|
||||||
type: 'Reject',
|
type: 'Reject',
|
||||||
actor: obj.actor,
|
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
|
||||||
object: obj.object,
|
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
|
||||||
|
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
|
||||||
__yume_normalized_object: true,
|
__yume_normalized_object: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -538,11 +582,14 @@ export function yumeDowncastReject(object: IObject): IReject | null {
|
||||||
export function yumeDowncastAdd(object: IObject): IAdd | null {
|
export function yumeDowncastAdd(object: IObject): IAdd | null {
|
||||||
if (getApType(object) !== 'Add') return null;
|
if (getApType(object) !== 'Add') return null;
|
||||||
const obj = object as IAdd;
|
const obj = object as IAdd;
|
||||||
|
if (!obj.actor || !obj.object ) return null;
|
||||||
return {
|
return {
|
||||||
|
...extractMisskeyVendorKeys(object),
|
||||||
|
...extractSafe(object),
|
||||||
type: 'Add',
|
type: 'Add',
|
||||||
actor: obj.actor,
|
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
|
||||||
object: obj.object,
|
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
|
||||||
target: obj.target,
|
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
|
||||||
__yume_normalized_object: true,
|
__yume_normalized_object: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -550,11 +597,14 @@ export function yumeDowncastAdd(object: IObject): IAdd | null {
|
||||||
export function yumeDowncastRemove(object: IObject): IRemove | null {
|
export function yumeDowncastRemove(object: IObject): IRemove | null {
|
||||||
if (getApType(object) !== 'Remove') return null;
|
if (getApType(object) !== 'Remove') return null;
|
||||||
const obj = object as IRemove;
|
const obj = object as IRemove;
|
||||||
|
if (!obj.actor || !obj.object) return null;
|
||||||
return {
|
return {
|
||||||
|
...extractMisskeyVendorKeys(object),
|
||||||
|
...extractSafe(object),
|
||||||
type: 'Remove',
|
type: 'Remove',
|
||||||
actor: obj.actor,
|
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
|
||||||
object: obj.object,
|
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
|
||||||
target: obj.target,
|
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
|
||||||
__yume_normalized_object: true,
|
__yume_normalized_object: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -562,10 +612,14 @@ export function yumeDowncastRemove(object: IObject): IRemove | null {
|
||||||
export function yumeDowncastLike(object: IObject): ILike | null {
|
export function yumeDowncastLike(object: IObject): ILike | null {
|
||||||
if (getApType(object) !== 'Like') return null;
|
if (getApType(object) !== 'Like') return null;
|
||||||
const obj = object as ILike;
|
const obj = object as ILike;
|
||||||
|
if (!obj.actor || !obj.object || !obj.target) return null;
|
||||||
return {
|
return {
|
||||||
|
...extractMisskeyVendorKeys(object),
|
||||||
|
...extractSafe(object),
|
||||||
type: 'Like',
|
type: 'Like',
|
||||||
actor: obj.actor,
|
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
|
||||||
object: obj.object,
|
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
|
||||||
|
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
|
||||||
__yume_normalized_object: true,
|
__yume_normalized_object: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -573,10 +627,14 @@ export function yumeDowncastLike(object: IObject): ILike | null {
|
||||||
export function yumeDowncastAnnounce(object: IObject): IAnnounce | null {
|
export function yumeDowncastAnnounce(object: IObject): IAnnounce | null {
|
||||||
if (getApType(object) !== 'Announce') return null;
|
if (getApType(object) !== 'Announce') return null;
|
||||||
const obj = object as IAnnounce;
|
const obj = object as IAnnounce;
|
||||||
|
if (!obj.actor || !obj.object) return null;
|
||||||
return {
|
return {
|
||||||
|
// ...extractMisskeyVendorKeys(object),
|
||||||
|
...extractSafe(object),
|
||||||
type: 'Announce',
|
type: 'Announce',
|
||||||
actor: obj.actor,
|
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
|
||||||
object: obj.object,
|
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
|
||||||
|
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
|
||||||
__yume_normalized_object: true,
|
__yume_normalized_object: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -584,10 +642,14 @@ export function yumeDowncastAnnounce(object: IObject): IAnnounce | null {
|
||||||
export function yumeDowncastBlock(object: IObject): IBlock | null {
|
export function yumeDowncastBlock(object: IObject): IBlock | null {
|
||||||
if (getApType(object) !== 'Block') return null;
|
if (getApType(object) !== 'Block') return null;
|
||||||
const obj = object as IBlock;
|
const obj = object as IBlock;
|
||||||
|
if (!obj.actor || !obj.object) return null;
|
||||||
return {
|
return {
|
||||||
|
...extractMisskeyVendorKeys(object),
|
||||||
|
...extractSafe(object),
|
||||||
type: 'Block',
|
type: 'Block',
|
||||||
actor: obj.actor,
|
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
|
||||||
object: obj.object,
|
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
|
||||||
|
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
|
||||||
__yume_normalized_object: true,
|
__yume_normalized_object: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -595,10 +657,14 @@ export function yumeDowncastBlock(object: IObject): IBlock | null {
|
||||||
export function yumeDowncastFlag(object: IObject): IFlag | null {
|
export function yumeDowncastFlag(object: IObject): IFlag | null {
|
||||||
if (getApType(object) !== 'Flag') return null;
|
if (getApType(object) !== 'Flag') return null;
|
||||||
const obj = object as IFlag;
|
const obj = object as IFlag;
|
||||||
|
if (!obj.actor || !obj.object) return null;
|
||||||
return {
|
return {
|
||||||
|
...extractMisskeyVendorKeys(object),
|
||||||
|
...extractSafe(object),
|
||||||
type: 'Flag',
|
type: 'Flag',
|
||||||
actor: obj.actor,
|
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
|
||||||
object: obj.object,
|
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
|
||||||
|
target: obj.target ? (typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target)) : undefined,
|
||||||
__yume_normalized_object: true,
|
__yume_normalized_object: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -606,17 +672,20 @@ export function yumeDowncastFlag(object: IObject): IFlag | null {
|
||||||
export function yumeDowncastMove(object: IObject): IMove | null {
|
export function yumeDowncastMove(object: IObject): IMove | null {
|
||||||
if (getApType(object) !== 'Move') return null;
|
if (getApType(object) !== 'Move') return null;
|
||||||
const obj = object as IMove;
|
const obj = object as IMove;
|
||||||
|
if (!obj.actor || !obj.object || !obj.target) return null;
|
||||||
return {
|
return {
|
||||||
|
...extractMisskeyVendorKeys(object),
|
||||||
|
...extractSafe(object),
|
||||||
type: 'Move',
|
type: 'Move',
|
||||||
actor: obj.actor,
|
actor: typeof obj.actor === 'string' ? yumeNormalizeURL(obj.actor) : yumeNormalizeObject(obj.actor),
|
||||||
object: obj.object,
|
object: typeof obj.object === 'string' ? yumeNormalizeURL(obj.object) : yumeNormalizeObject(obj.object),
|
||||||
target: obj.target,
|
target: typeof obj.target === 'string' ? yumeNormalizeURL(obj.target) : yumeNormalizeObject(obj.target),
|
||||||
__yume_normalized_object: true,
|
__yume_normalized_object: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
export function yumeDowncastMention(object: IObject): IApMention {
|
export function yumeDowncastMention(object: IObject): IApMention | null {
|
||||||
if (getApType(object) !== 'Mention') {
|
if (getApType(object) !== 'Mention') {
|
||||||
throw new Error('not a mention');
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const href = getApHrefNullable(object);
|
const href = getApHrefNullable(object);
|
||||||
|
|
|
@ -255,6 +255,8 @@ export class InboxProcessorService implements OnApplicationShutdown {
|
||||||
incCounter(mIncomingApReject, 'host_signature_mismatch');
|
incCounter(mIncomingApReject, 'host_signature_mismatch');
|
||||||
throw new Bull.UnrecoverableError(`skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`);
|
throw new Bull.UnrecoverableError(`skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
throw new Bull.UnrecoverableError('skip: activity id is not a string');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.apRequestChart.inbox();
|
this.apRequestChart.inbox();
|
||||||
|
@ -307,7 +309,7 @@ export class InboxProcessorService implements OnApplicationShutdown {
|
||||||
}
|
}
|
||||||
const end = +new Date();
|
const end = +new Date();
|
||||||
observeHistogram(mIncomingApProcessingTime, { success: 'false' }, (end - begin) / 1000);
|
observeHistogram(mIncomingApProcessingTime, { success: 'false' }, (end - begin) / 1000);
|
||||||
incCounter(mincomingApProcessingError, { reason: 'unknown' });
|
incCounter(mincomingApProcessingError, {});
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
observeHistogram(mIncomingApProcessingTime, { success: 'true' }, (+new Date() - begin) / 1000);
|
observeHistogram(mIncomingApProcessingTime, { success: 'true' }, (+new Date() - begin) / 1000);
|
||||||
|
|
|
@ -105,7 +105,7 @@ export class ActivityPubServerService {
|
||||||
let signature;
|
let signature;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
signature = httpSignature.parseRequest(request.raw, { 'headers': [] });
|
signature = httpSignature.parseRequest(request.raw, { 'headers': ['(request-target)', 'host', 'date'], authorizationHeaderName: 'signature' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reply.code(401);
|
reply.code(401);
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -46,7 +46,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
throw new Error('cannot delete a root account');
|
throw new Error('cannot delete a root account');
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.deleteAccoountService.deleteAccount(user);
|
await this.deleteAccoountService.deleteAccount(user, me);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,13 +33,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
private deleteAccountService: DeleteAccountService,
|
private deleteAccountService: DeleteAccountService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const user = await this.usersRepository.findOneByOrFail({ id: ps.userId });
|
const user = await this.usersRepository.findOneByOrFail({ id: ps.userId });
|
||||||
if (user.isDeleted) {
|
if (user.isDeleted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.deleteAccountService.deleteAccount(user);
|
await this.deleteAccountService.deleteAccount(user, me);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { ApResolverService } from '@/core/activitypub/ApResolverService.js';
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['federation'],
|
tags: ['federation'],
|
||||||
|
|
||||||
|
requireAdmin: true,
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
kind: 'read:federation',
|
kind: 'read:federation',
|
||||||
|
|
||||||
|
|
|
@ -118,6 +118,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
]));
|
]));
|
||||||
if (local != null) return local;
|
if (local != null) return local;
|
||||||
|
|
||||||
|
const host = this.utilityService.extractDbHost(uri);
|
||||||
|
|
||||||
|
// local object, not found in db? fail
|
||||||
|
if (this.utilityService.isSelfHost(host)) return null;
|
||||||
|
|
||||||
// リモートから一旦オブジェクトフェッチ
|
// リモートから一旦オブジェクトフェッチ
|
||||||
const resolver = this.apResolverService.createResolver();
|
const resolver = this.apResolverService.createResolver();
|
||||||
const object = await resolver.resolve(uri) as any;
|
const object = await resolver.resolve(uri) as any;
|
||||||
|
@ -135,7 +140,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
return await this.mergePack(
|
return await this.mergePack(
|
||||||
me,
|
me,
|
||||||
isActor(object) ? await this.apPersonService.createPerson(getApId(object), resolver) : null,
|
isActor(object) ? await this.apPersonService.createPerson(getApId(object), resolver) : null,
|
||||||
isPost(object) ? await this.apNoteService.createNote(getApId(object), resolver, true) : null,
|
isPost(object) ? await this.apNoteService.createNote(getApId(object), undefined, resolver, true) : null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -611,7 +611,7 @@ export class ClientServerService {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
//#region SSR (for crawlers)
|
//#region SSR
|
||||||
// User
|
// User
|
||||||
fastify.get<{ Params: { user: string; sub?: string; } }>('/@:user/:sub?', async (request, reply) => {
|
fastify.get<{ Params: { user: string; sub?: string; } }>('/@:user/:sub?', async (request, reply) => {
|
||||||
const { username, host } = Acct.parse(request.params.user);
|
const { username, host } = Acct.parse(request.params.user);
|
||||||
|
@ -636,11 +636,17 @@ export class ClientServerService {
|
||||||
reply.header('X-Robots-Tag', 'noimageai');
|
reply.header('X-Robots-Tag', 'noimageai');
|
||||||
reply.header('X-Robots-Tag', 'noai');
|
reply.header('X-Robots-Tag', 'noai');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const _user = await this.userEntityService.pack(user);
|
||||||
|
|
||||||
return await reply.view('user', {
|
return await reply.view('user', {
|
||||||
user, profile, me,
|
user, profile, me,
|
||||||
avatarUrl: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user),
|
avatarUrl: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user),
|
||||||
sub: request.params.sub,
|
sub: request.params.sub,
|
||||||
...await this.generateCommonPugData(this.meta),
|
...await this.generateCommonPugData(this.meta),
|
||||||
|
clientCtx: htmlSafeJsonStringify({
|
||||||
|
user: _user,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// リモートユーザーなので
|
// リモートユーザーなので
|
||||||
|
@ -693,6 +699,9 @@ export class ClientServerService {
|
||||||
// TODO: Let locale changeable by instance setting
|
// TODO: Let locale changeable by instance setting
|
||||||
summary: getNoteSummary(_note),
|
summary: getNoteSummary(_note),
|
||||||
...await this.generateCommonPugData(this.meta),
|
...await this.generateCommonPugData(this.meta),
|
||||||
|
clientCtx: htmlSafeJsonStringify({
|
||||||
|
note: _note,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return await renderBase(reply);
|
return await renderBase(reply);
|
||||||
|
@ -781,6 +790,9 @@ export class ClientServerService {
|
||||||
profile,
|
profile,
|
||||||
avatarUrl: _clip.user.avatarUrl,
|
avatarUrl: _clip.user.avatarUrl,
|
||||||
...await this.generateCommonPugData(this.meta),
|
...await this.generateCommonPugData(this.meta),
|
||||||
|
clientCtx: htmlSafeJsonStringify({
|
||||||
|
clip: _clip,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return await renderBase(reply);
|
return await renderBase(reply);
|
||||||
|
|
|
@ -118,7 +118,6 @@ export class UrlPreviewService {
|
||||||
private fetchSummary(url: string, meta: MiMeta, lang?: string): Promise<SummalyResult> {
|
private fetchSummary(url: string, meta: MiMeta, lang?: string): Promise<SummalyResult> {
|
||||||
const agent = this.config.proxy
|
const agent = this.config.proxy
|
||||||
? {
|
? {
|
||||||
http: this.httpRequestService.httpAgent,
|
|
||||||
https: this.httpRequestService.httpsAgent,
|
https: this.httpRequestService.httpsAgent,
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
@ -145,6 +144,6 @@ export class UrlPreviewService {
|
||||||
contentLengthRequired: meta.urlPreviewRequireContentLength,
|
contentLengthRequired: meta.urlPreviewRequireContentLength,
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.httpRequestService.getJson<SummalyResult>(`${proxy}?${queryStr}`);
|
return this.httpRequestService.getJson<SummalyResult>(`${proxy}?${queryStr}`, 'application/json, */*', undefined, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,6 +73,9 @@ html
|
||||||
script(type='application/json' id='misskey_meta' data-generated-at=now)
|
script(type='application/json' id='misskey_meta' data-generated-at=now)
|
||||||
!= metaJson
|
!= metaJson
|
||||||
|
|
||||||
|
script(type='application/json' id='misskey_clientCtx' data-generated-at=now)
|
||||||
|
!= clientCtx
|
||||||
|
|
||||||
script(integrity=bootJS.integrity) !{bootJS.content}
|
script(integrity=bootJS.integrity) !{bootJS.content}
|
||||||
|
|
||||||
body
|
body
|
||||||
|
|
|
@ -167,7 +167,7 @@ describe('ActivityPub', () => {
|
||||||
|
|
||||||
const punnyPost = {
|
const punnyPost = {
|
||||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
id: `https://あ.com/users/${secureRndstr(8)}`,
|
id: `https://あ.com/users/あ`,
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
attributedTo: actor.id,
|
attributedTo: actor.id,
|
||||||
to: 'https://www.w3.org/ns/activitystreams#Public',
|
to: 'https://www.w3.org/ns/activitystreams#Public',
|
||||||
|
@ -207,7 +207,7 @@ describe('ActivityPub', () => {
|
||||||
resolver.register(actor.id, actor);
|
resolver.register(actor.id, actor);
|
||||||
resolver.register(post.id, post);
|
resolver.register(post.id, post);
|
||||||
|
|
||||||
const note = await noteService.createNote(post.id, resolver, true);
|
const note = await noteService.createNote(post.id, undefined, resolver, true);
|
||||||
|
|
||||||
assert.deepStrictEqual(note?.uri, post.id);
|
assert.deepStrictEqual(note?.uri, post.id);
|
||||||
assert.deepStrictEqual(note.visibility, 'public');
|
assert.deepStrictEqual(note.visibility, 'public');
|
||||||
|
@ -370,7 +370,7 @@ describe('ActivityPub', () => {
|
||||||
resolver.register(actor.featured, featured);
|
resolver.register(actor.featured, featured);
|
||||||
resolver.register(firstNote.id, firstNote);
|
resolver.register(firstNote.id, firstNote);
|
||||||
|
|
||||||
const note = await noteService.createNote(firstNote.id as string, resolver);
|
const note = await noteService.createNote(firstNote.id as string, undefined, resolver);
|
||||||
assert.strictEqual(note?.uri, firstNote.id);
|
assert.strictEqual(note?.uri, firstNote.id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -25,17 +25,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, inject, ref } from 'vue';
|
import { computed, defineAsyncComponent, inject, ref } from 'vue';
|
||||||
|
import type { MenuItem } from '@/types/menu.js';
|
||||||
import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy.js';
|
import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { customEmojisMap } from '@/custom-emojis.js';
|
import { customEmojisMap } from '@/custom-emojis.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApiGet } from '@/scripts/misskey-api.js';
|
import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
|
||||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||||
import * as sound from '@/scripts/sound.js';
|
import * as sound from '@/scripts/sound.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';
|
import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';
|
||||||
import type { MenuItem } from '@/types/menu.js';
|
import { $i } from '@/account.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -125,9 +126,31 @@ function onClick(ev: MouseEvent) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if ($i?.isModerator ?? $i?.isAdmin) {
|
||||||
|
menuItems.push({
|
||||||
|
text: i18n.ts.edit,
|
||||||
|
icon: 'ti ti-pencil',
|
||||||
|
action: async () => {
|
||||||
|
await edit(props.name);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
|
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function edit(name: string) {
|
||||||
|
const emoji = await misskeyApi('emoji', {
|
||||||
|
name: name,
|
||||||
|
});
|
||||||
|
const { dispose } = os.popup(defineAsyncComponent(() => import('@/pages/emoji-edit-dialog.vue')), {
|
||||||
|
emoji: emoji,
|
||||||
|
}, {
|
||||||
|
closed: () => dispose(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
@ -33,25 +33,28 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, watch, provide, ref } from 'vue';
|
import { computed, watch, provide, ref } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { url } from '@@/js/config.js';
|
||||||
|
import type { MenuItem } from '@/types/menu.js';
|
||||||
import MkNotes from '@/components/MkNotes.vue';
|
import MkNotes from '@/components/MkNotes.vue';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import { url } from '@@/js/config.js';
|
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import { clipsCache } from '@/cache.js';
|
import { clipsCache } from '@/cache.js';
|
||||||
import { isSupportShare } from '@/scripts/navigator.js';
|
import { isSupportShare } from '@/scripts/navigator.js';
|
||||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||||
import { genEmbedCode } from '@/scripts/get-embed-code.js';
|
import { genEmbedCode } from '@/scripts/get-embed-code.js';
|
||||||
import type { MenuItem } from '@/types/menu.js';
|
import { getServerContext } from '@/server-context.js';
|
||||||
|
|
||||||
|
const CTX_CLIP = getServerContext('clip');
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
clipId: string,
|
clipId: string,
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const clip = ref<Misskey.entities.Clip | null>(null);
|
const clip = ref<Misskey.entities.Clip | null>(CTX_CLIP);
|
||||||
const favorited = ref(false);
|
const favorited = ref(false);
|
||||||
const pagination = {
|
const pagination = {
|
||||||
endpoint: 'clips/notes' as const,
|
endpoint: 'clips/notes' as const,
|
||||||
|
@ -64,6 +67,11 @@ const pagination = {
|
||||||
const isOwned = computed<boolean | null>(() => $i && clip.value && ($i.id === clip.value.userId));
|
const isOwned = computed<boolean | null>(() => $i && clip.value && ($i.id === clip.value.userId));
|
||||||
|
|
||||||
watch(() => props.clipId, async () => {
|
watch(() => props.clipId, async () => {
|
||||||
|
if (CTX_CLIP && CTX_CLIP.id === props.clipId) {
|
||||||
|
clip.value = CTX_CLIP;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
clip.value = await misskeyApi('clips/show', {
|
clip.value = await misskeyApi('clips/show', {
|
||||||
clipId: props.clipId,
|
clipId: props.clipId,
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,18 +15,22 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { defineAsyncComponent } from 'vue';
|
||||||
|
import type { MenuItem } from '@/types/menu.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApiGet } from '@/scripts/misskey-api.js';
|
import { misskeyApiGet } from '@/scripts/misskey-api.js';
|
||||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';
|
import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';
|
||||||
|
import { $i } from '@/account.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
emoji: Misskey.entities.EmojiSimple;
|
emoji: Misskey.entities.EmojiSimple;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
function menu(ev) {
|
function menu(ev) {
|
||||||
os.popupMenu([{
|
const menuItems: MenuItem[] = [];
|
||||||
|
menuItems.push({
|
||||||
type: 'label',
|
type: 'label',
|
||||||
text: ':' + props.emoji.name + ':',
|
text: ':' + props.emoji.name + ':',
|
||||||
}, {
|
}, {
|
||||||
|
@ -48,8 +52,28 @@ function menu(ev) {
|
||||||
closed: () => dispose(),
|
closed: () => dispose(),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}], ev.currentTarget ?? ev.target);
|
});
|
||||||
|
|
||||||
|
if ($i?.isModerator ?? $i?.isAdmin) {
|
||||||
|
menuItems.push({
|
||||||
|
text: i18n.ts.edit,
|
||||||
|
icon: 'ti ti-pencil',
|
||||||
|
action: () => {
|
||||||
|
edit(props.emoji);
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
|
||||||
|
}
|
||||||
|
|
||||||
|
const edit = async (emoji) => {
|
||||||
|
const { dispose } = os.popup(defineAsyncComponent(() => import('@/pages/emoji-edit-dialog.vue')), {
|
||||||
|
emoji: emoji,
|
||||||
|
}, {
|
||||||
|
closed: () => dispose(),
|
||||||
|
});
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
@ -62,13 +62,16 @@ import { dateString } from '@/filters/date.js';
|
||||||
import MkClipPreview from '@/components/MkClipPreview.vue';
|
import MkClipPreview from '@/components/MkClipPreview.vue';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { pleaseLogin } from '@/scripts/please-login.js';
|
import { pleaseLogin } from '@/scripts/please-login.js';
|
||||||
|
import { getServerContext } from '@/server-context.js';
|
||||||
|
|
||||||
|
const CTX_NOTE = getServerContext('note');
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
noteId: string;
|
noteId: string;
|
||||||
initialTab?: string;
|
initialTab?: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const note = ref<null | Misskey.entities.Note>();
|
const note = ref<null | Misskey.entities.Note>(CTX_NOTE);
|
||||||
const clips = ref<Misskey.entities.Clip[]>();
|
const clips = ref<Misskey.entities.Clip[]>();
|
||||||
const showPrev = ref<'user' | 'channel' | false>(false);
|
const showPrev = ref<'user' | 'channel' | false>(false);
|
||||||
const showNext = ref<'user' | 'channel' | false>(false);
|
const showNext = ref<'user' | 'channel' | false>(false);
|
||||||
|
@ -116,6 +119,12 @@ function fetchNote() {
|
||||||
showPrev.value = false;
|
showPrev.value = false;
|
||||||
showNext.value = false;
|
showNext.value = false;
|
||||||
note.value = null;
|
note.value = null;
|
||||||
|
|
||||||
|
if (CTX_NOTE && CTX_NOTE.id === props.noteId) {
|
||||||
|
note.value = CTX_NOTE;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
misskeyApi('notes/show', {
|
misskeyApi('notes/show', {
|
||||||
noteId: props.noteId,
|
noteId: props.noteId,
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
|
|
|
@ -39,6 +39,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||||
|
import { getServerContext } from '@/server-context.js';
|
||||||
|
|
||||||
const XHome = defineAsyncComponent(() => import('./home.vue'));
|
const XHome = defineAsyncComponent(() => import('./home.vue'));
|
||||||
const XTimeline = defineAsyncComponent(() => import('./index.timeline.vue'));
|
const XTimeline = defineAsyncComponent(() => import('./index.timeline.vue'));
|
||||||
|
@ -52,6 +53,8 @@ const XFlashs = defineAsyncComponent(() => import('./flashs.vue'));
|
||||||
const XGallery = defineAsyncComponent(() => import('./gallery.vue'));
|
const XGallery = defineAsyncComponent(() => import('./gallery.vue'));
|
||||||
const XRaw = defineAsyncComponent(() => import('./raw.vue'));
|
const XRaw = defineAsyncComponent(() => import('./raw.vue'));
|
||||||
|
|
||||||
|
const CTX_USER = getServerContext('user');
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
acct: string;
|
acct: string;
|
||||||
page?: string;
|
page?: string;
|
||||||
|
@ -61,13 +64,24 @@ const props = withDefaults(defineProps<{
|
||||||
|
|
||||||
const tab = ref(props.page);
|
const tab = ref(props.page);
|
||||||
|
|
||||||
const user = ref<null | Misskey.entities.UserDetailed>(null);
|
const user = ref<null | Misskey.entities.UserDetailed>(CTX_USER);
|
||||||
const error = ref<any>(null);
|
const error = ref<any>(null);
|
||||||
|
|
||||||
function fetchUser(): void {
|
function fetchUser(): void {
|
||||||
if (props.acct == null) return;
|
if (props.acct == null) return;
|
||||||
|
|
||||||
|
const { username, host } = Misskey.acct.parse(props.acct);
|
||||||
|
|
||||||
|
if (CTX_USER && CTX_USER.username === username && CTX_USER.host === host) {
|
||||||
|
user.value = CTX_USER;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
user.value = null;
|
user.value = null;
|
||||||
misskeyApi('users/show', Misskey.acct.parse(props.acct)).then(u => {
|
misskeyApi('users/show', {
|
||||||
|
username,
|
||||||
|
host,
|
||||||
|
}).then(u => {
|
||||||
user.value = u;
|
user.value = u;
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
error.value = err;
|
error.value = err;
|
||||||
|
|
23
packages/frontend/src/server-context.ts
Normal file
23
packages/frontend/src/server-context.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { $i } from '@/account.js';
|
||||||
|
|
||||||
|
const providedContextEl = document.getElementById('misskey_clientCtx');
|
||||||
|
|
||||||
|
export type ServerContext = {
|
||||||
|
clip?: Misskey.entities.Clip;
|
||||||
|
note?: Misskey.entities.Note;
|
||||||
|
user?: Misskey.entities.UserLite;
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
export const serverContext: ServerContext = (providedContextEl && providedContextEl.textContent) ? JSON.parse(providedContextEl.textContent) : null;
|
||||||
|
|
||||||
|
export function getServerContext<K extends keyof NonNullable<ServerContext>>(entity: K): Required<Pick<NonNullable<ServerContext>, K>> | null {
|
||||||
|
// contextは非ログイン状態の情報しかないためログイン時は利用できない
|
||||||
|
if ($i) return null;
|
||||||
|
|
||||||
|
return serverContext ? (serverContext[entity] ?? null) : null;
|
||||||
|
}
|
Loading…
Reference in a new issue