Compare commits
26 Commits
fc73a3853f
...
abd99b1433
Author | SHA1 | Date |
---|---|---|
鴇峰 朔華 | abd99b1433 | |
syuilo | 0f59adc436 | |
syuilo | 9fdabe3666 | |
rectcoordsystem | 090e9392cd | |
Julia | b9cb949eb1 | |
Julia | 5f675201f2 | |
syuilo | 1c284c8154 | |
Sayamame-beans | aa48a0e207 | |
syuilo | f0c3a4cc0b | |
鴇峰 朔華 | 6236c936f1 | |
鴇峰 朔華 | fd9b7edeff | |
鴇峰 朔華 | ac95b12f0b | |
鴇峰 朔華 | a96ae92f47 | |
鴇峰 朔華 | 292809a324 | |
鴇峰 朔華 | 34da11f371 | |
鴇峰 朔華 | da94dbee00 | |
鴇峰 朔華 | 0301e86aff | |
鴇峰 朔華 | 26652949cb | |
鴇峰 朔華 | 24792e09b5 | |
鴇峰 朔華 | 3ea69b6203 | |
鴇峰 朔華 | 202fceed22 | |
鴇峰 朔華 | 9ef2dbbd30 | |
鴇峰 朔華 | 1b0ac28825 | |
鴇峰 朔華 | 51a2a7d81c | |
鴇峰 朔華 | 37627bb0e6 | |
鴇峰 朔華 | 3dd5af3003 |
|
@ -30,6 +30,7 @@
|
|||
(Based on https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/663)
|
||||
- Enhance: サイドバーを簡単に展開・折りたたみできるように ( #14981 )
|
||||
- Enhance: リノートメニューに「リノートの詳細」を追加
|
||||
- Enhance: 非ログイン状態でMisskeyを開いた際のパフォーマンスを向上
|
||||
- Fix: 通知の範囲指定の設定項目が必要ない通知設定でも範囲指定の設定がでている問題を修正
|
||||
- Fix: Turnstileが失敗・期限切れした際にも成功扱いとなってしまう問題を修正
|
||||
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/768)
|
||||
|
@ -65,6 +66,7 @@
|
|||
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/709)
|
||||
- Fix: User Webhookテスト機能のMock Payloadを修正
|
||||
- Fix: アカウント削除のモデレーションログが動作していないのを修正 (#14996)
|
||||
- Fix: リノートミュートが新規投稿通知に対して作用していなかった問題を修正
|
||||
|
||||
### Misskey.js
|
||||
- Fix: Stream初期化時、別途WebSocketを指定する場合の型定義を修正
|
||||
|
|
|
@ -586,6 +586,7 @@ masterVolume: "Volum principal"
|
|||
notUseSound: "Sense so"
|
||||
useSoundOnlyWhenActive: "Reproduir sons només quan Misskey estigui actiu"
|
||||
details: "Detalls"
|
||||
renoteDetails: "Més informació sobre l'impuls "
|
||||
chooseEmoji: "Tria un emoji"
|
||||
unableToProcess: "L'operació no pot ser completada "
|
||||
recentUsed: "Utilitzat recentment"
|
||||
|
|
|
@ -1242,6 +1242,7 @@ keepOriginalFilenameDescription: "Wenn diese Einstellung deaktiviert ist, wird d
|
|||
noDescription: "Keine Beschreibung vorhanden"
|
||||
tryAgain: "Bitte später erneut versuchen"
|
||||
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"
|
||||
createdAntennas: "Erstellte Antennen"
|
||||
fromX: "Von {x}"
|
||||
|
@ -1253,6 +1254,8 @@ thereAreNChanges: "Es gibt {n} Änderung(en)"
|
|||
signinWithPasskey: "Mit Passkey anmelden"
|
||||
passkeyVerificationFailed: "Die Passkey-Verifizierung ist fehlgeschlagen."
|
||||
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"
|
||||
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"
|
||||
|
@ -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."
|
||||
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"
|
||||
makeNotesHiddenBefore: "Frühere Notizen privat machen"
|
||||
mayNotEffectForFederatedNotes: "Dies hat möglicherweise keine Auswirkungen auf Notizen, die an andere Server föderiert werden."
|
||||
_abuseUserReport:
|
||||
forward: "Weiterleiten"
|
||||
|
@ -1274,6 +1278,7 @@ _delivery:
|
|||
stop: "Gesperrt"
|
||||
_type:
|
||||
none: "Wird veröffentlicht"
|
||||
manuallySuspended: "Manuell gesperrt"
|
||||
_bubbleGame:
|
||||
howToPlay: "Wie man spielt"
|
||||
hold: "Halten"
|
||||
|
|
|
@ -586,6 +586,7 @@ masterVolume: "Master volume"
|
|||
notUseSound: "Disable sound"
|
||||
useSoundOnlyWhenActive: "Output sounds only if Misskey is active."
|
||||
details: "Details"
|
||||
renoteDetails: "Renote details"
|
||||
chooseEmoji: "Select an emoji"
|
||||
unableToProcess: "The operation could not be completed"
|
||||
recentUsed: "Recently used"
|
||||
|
|
|
@ -606,6 +606,14 @@ export interface Locale extends ILocale {
|
|||
* ブロック解除
|
||||
*/
|
||||
"unblock": string;
|
||||
/**
|
||||
* リアクションをブロック
|
||||
*/
|
||||
"blockReactionUser": string;
|
||||
/**
|
||||
* リアクションのブロックを解除
|
||||
*/
|
||||
"unblockReactionUser": string;
|
||||
/**
|
||||
* 凍結
|
||||
*/
|
||||
|
@ -622,6 +630,14 @@ export interface Locale extends ILocale {
|
|||
* ブロック解除しますか?
|
||||
*/
|
||||
"unblockConfirm": string;
|
||||
/**
|
||||
* リアクションをブロックしますか?
|
||||
*/
|
||||
"blockReactionUserConfirm": string;
|
||||
/**
|
||||
* リアクションのブロックを解除しますか?
|
||||
*/
|
||||
"unblockReactionUserConfirm": string;
|
||||
/**
|
||||
* 凍結しますか?
|
||||
*/
|
||||
|
@ -994,6 +1010,10 @@ export interface Locale extends ILocale {
|
|||
* ブロックしたユーザー
|
||||
*/
|
||||
"blockedUsers": string;
|
||||
/**
|
||||
* リアクションをブロックしたユーザー
|
||||
*/
|
||||
"reactionBlockedUsers": string;
|
||||
/**
|
||||
* ユーザーはいません
|
||||
*/
|
||||
|
|
|
@ -147,10 +147,14 @@ renoteMute: "リノートをミュート"
|
|||
renoteUnmute: "リノートのミュートを解除"
|
||||
block: "ブロック"
|
||||
unblock: "ブロック解除"
|
||||
blockReactionUser: "リアクションをブロック"
|
||||
unblockReactionUser: "リアクションのブロックを解除"
|
||||
suspend: "凍結"
|
||||
unsuspend: "解凍"
|
||||
blockConfirm: "ブロックしますか?"
|
||||
unblockConfirm: "ブロック解除しますか?"
|
||||
blockReactionUserConfirm: "リアクションをブロックしますか?"
|
||||
unblockReactionUserConfirm: "リアクションのブロックを解除しますか?"
|
||||
suspendConfirm: "凍結しますか?"
|
||||
unsuspendConfirm: "解凍しますか?"
|
||||
selectList: "リストを選択"
|
||||
|
@ -244,6 +248,7 @@ federationAllowedHostsDescription: "連合を許可するサーバーのホス
|
|||
muteAndBlock: "ミュートとブロック"
|
||||
mutedUsers: "ミュートしたユーザー"
|
||||
blockedUsers: "ブロックしたユーザー"
|
||||
reactionBlockedUsers: "リアクションをブロックしたユーザー"
|
||||
noUsers: "ユーザーはいません"
|
||||
editProfile: "プロフィールを編集"
|
||||
noteDeleteConfirm: "このノートを削除しますか?"
|
||||
|
|
|
@ -586,6 +586,7 @@ masterVolume: "마스터 볼륨"
|
|||
notUseSound: "음소거 하기"
|
||||
useSoundOnlyWhenActive: "Misskey를 활성화한 때에만 소리를 출력하기"
|
||||
details: "자세히"
|
||||
renoteDetails: "리노트 상세 내용"
|
||||
chooseEmoji: "이모지 선택"
|
||||
unableToProcess: "작업을 완료할 수 없습니다"
|
||||
recentUsed: "최근 사용"
|
||||
|
@ -1299,6 +1300,7 @@ thisContentsAreMarkedAsSigninRequiredByAuthor: "게시자에 의해 로그인해
|
|||
lockdown: "잠금"
|
||||
pleaseSelectAccount: "계정을 선택해주세요."
|
||||
availableRoles: "사용 가능한 역할"
|
||||
acknowledgeNotesAndEnable: "활성화 하기 전에 주의 사항을 확인했습니다."
|
||||
_accountSettings:
|
||||
requireSigninToViewContents: "콘텐츠 열람을 위해 로그인으 필수로 설정하기"
|
||||
requireSigninToViewContentsDescription1: "자신이 작성한 모든 노트 등의 콘텐츠를 보기 위해 로그인을 필수로 설정합니다. 크롤러가 정보 수집하는 것을 방지하는 효과를 기대할 수 있습니다."
|
||||
|
@ -1455,6 +1457,8 @@ _serverSettings:
|
|||
reactionsBufferingDescription: "활성화 한 경우, 리액션 작성 퍼포먼스가 대폭 향상되어 DB의 부하를 줄일 수 있으나, Redis의 메모리 사용량이 많아집니다."
|
||||
inquiryUrl: "문의처 URL"
|
||||
inquiryUrlDescription: "서버 운영자에게 보내는 문의 양식의 URL이나 운영자의 연락처 등이 적힌 웹 페이지의 URL을 설정합니다."
|
||||
openRegistration: "회원 가입을 활성화 하기"
|
||||
openRegistrationWarning: "회원 가입을 개방하는 것은 리스크가 따릅니다. 서버를 항상 감시할 수 있고, 문제가 발생했을 때 바로 대응할 수 있는 상태에서만 활성화 하는 것을 권장합니다."
|
||||
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "일정 기간동안 모더레이터의 활동이 감지되지 않는 경우, 스팸 방지를 위해 이 설정은 자동으로 꺼집니다."
|
||||
_accountMigration:
|
||||
moveFrom: "다른 계정에서 이 계정으로 이사"
|
||||
|
@ -2737,3 +2741,6 @@ _selfXssPrevention:
|
|||
description1: "여기에 무언가를 붙여넣으면 악의적인 사용자에게 계정을 탈취당하거나 개인정보를 도용당할 수 있습니다."
|
||||
description2: "붙여 넣으려는 항목이 무엇인지 정확히 이해하지 못하는 경우, %c지금 바로 작업을 중단하고 이 창을 닫으십시오."
|
||||
description3: "자세한 내용은 여기를 확인해 주세요. {link}"
|
||||
_followRequest:
|
||||
recieved: "받은 신청"
|
||||
sent: "보낸 신청"
|
||||
|
|
|
@ -1707,9 +1707,9 @@ _achievements:
|
|||
description: "在元旦登入"
|
||||
flavor: "今年也请对本服务器多多指教!"
|
||||
_cookieClicked:
|
||||
title: "点击饼干小游戏"
|
||||
title: "饼干点点乐"
|
||||
description: "点击了饼干"
|
||||
flavor: "用错软件了?"
|
||||
flavor: "穿越了?"
|
||||
_brainDiver:
|
||||
title: "Brain Diver"
|
||||
description: "发布了包含 Brain Diver 链接的帖子"
|
||||
|
|
|
@ -586,6 +586,7 @@ masterVolume: "主音量"
|
|||
notUseSound: "關閉音效"
|
||||
useSoundOnlyWhenActive: "瀏覽器在前景運作時,Misskey 才會發出音效"
|
||||
details: "詳細資訊"
|
||||
renoteDetails: "轉發貼文的細節"
|
||||
chooseEmoji: "選擇您的表情符號"
|
||||
unableToProcess: "操作無法完成"
|
||||
recentUsed: "最近使用"
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class AddBlockingReactionUser1731932268436 {
|
||||
name = 'AddBlockingReactionUser1731932268436'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "blocking" ADD "blockType" character varying NOT NULL DEFAULT 'user'`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "blocking"."blockType" IS 'Block type.'`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_cd38e7ea08163899a2d1f4427d" ON "blocking" ("blockType") `);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`DELETE FROM blocking WHERE "blockType" = 'reaction'`); // blockingテーブルのblockTypeがreactionの行を削除
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_cd38e7ea08163899a2d1f4427d"`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "blocking"."blockType" IS 'Block type.'`);
|
||||
await queryRunner.query(`ALTER TABLE "blocking" DROP COLUMN "blockType"`);
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ import * as Redis from 'ioredis';
|
|||
import type { BlockingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository, MiFollowing } from '@/models/_.js';
|
||||
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
|
||||
import type { MiLocalUser, MiUser } from '@/models/User.js';
|
||||
import { MiBlockingType } from '@/models/Blocking.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
@ -24,6 +25,8 @@ export class CacheService implements OnApplicationShutdown {
|
|||
public userMutingsCache: RedisKVCache<Set<string>>;
|
||||
public userBlockingCache: RedisKVCache<Set<string>>;
|
||||
public userBlockedCache: RedisKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ
|
||||
public userReactionBlockingCache: RedisKVCache<Set<string>>; // NOTE: リアクションBlockキャッシュ
|
||||
public userReactionBlockedCache: RedisKVCache<Set<string>>; // NOTE: 「被」リアクションBlockキャッシュ
|
||||
public renoteMutingsCache: RedisKVCache<Set<string>>;
|
||||
public userFollowingsCache: RedisKVCache<Record<string, Pick<MiFollowing, 'withReplies'> | undefined>>;
|
||||
|
||||
|
@ -80,7 +83,7 @@ export class CacheService implements OnApplicationShutdown {
|
|||
this.userBlockingCache = new RedisKVCache<Set<string>>(this.redisClient, 'userBlocking', {
|
||||
lifetime: 1000 * 60 * 30, // 30m
|
||||
memoryCacheLifetime: 1000 * 60, // 1m
|
||||
fetcher: (key) => this.blockingsRepository.find({ where: { blockerId: key }, select: ['blockeeId'] }).then(xs => new Set(xs.map(x => x.blockeeId))),
|
||||
fetcher: (key) => this.blockingsRepository.find({ where: { blockerId: key, blockType: MiBlockingType.User }, select: ['blockeeId'] }).then(xs => new Set(xs.map(x => x.blockeeId))),
|
||||
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
|
||||
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
||||
});
|
||||
|
@ -88,7 +91,23 @@ export class CacheService implements OnApplicationShutdown {
|
|||
this.userBlockedCache = new RedisKVCache<Set<string>>(this.redisClient, 'userBlocked', {
|
||||
lifetime: 1000 * 60 * 30, // 30m
|
||||
memoryCacheLifetime: 1000 * 60, // 1m
|
||||
fetcher: (key) => this.blockingsRepository.find({ where: { blockeeId: key }, select: ['blockerId'] }).then(xs => new Set(xs.map(x => x.blockerId))),
|
||||
fetcher: (key) => this.blockingsRepository.find({ where: { blockeeId: key, blockType: MiBlockingType.User }, select: ['blockerId'] }).then(xs => new Set(xs.map(x => x.blockerId))),
|
||||
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
|
||||
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
||||
});
|
||||
|
||||
this.userReactionBlockingCache = new RedisKVCache<Set<string>>(this.redisClient, 'userReactionBlocking', {
|
||||
lifetime: 1000 * 60 * 30, // 30m
|
||||
memoryCacheLifetime: 1000 * 60, // 1m
|
||||
fetcher: (key) => this.blockingsRepository.find({ where: { blockerId: key, blockType: MiBlockingType.Reaction }, select: ['blockeeId'] }).then(xs => new Set(xs.map(x => x.blockeeId))),
|
||||
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
|
||||
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
||||
});
|
||||
|
||||
this.userReactionBlockedCache = new RedisKVCache<Set<string>>(this.redisClient, 'userReactionBlocked', {
|
||||
lifetime: 1000 * 60 * 30, // 30m
|
||||
memoryCacheLifetime: 1000 * 60, // 1m
|
||||
fetcher: (key) => this.blockingsRepository.find({ where: { blockeeId: key, blockType: MiBlockingType.Reaction }, select: ['blockerId'] }).then(xs => new Set(xs.map(x => x.blockerId))),
|
||||
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
|
||||
fromRedisConverter: (value) => new Set(JSON.parse(value)),
|
||||
});
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
import * as fs from 'node:fs';
|
||||
import * as stream from 'node:stream/promises';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import ipaddr from 'ipaddr.js';
|
||||
import chalk from 'chalk';
|
||||
import got, * as Got from 'got';
|
||||
import { parse } from 'content-disposition';
|
||||
|
@ -70,13 +69,6 @@ export class DownloadService {
|
|||
},
|
||||
enableUnixSockets: false,
|
||||
}).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'];
|
||||
if (contentLength != null) {
|
||||
const size = Number(contentLength);
|
||||
|
@ -139,18 +131,4 @@ export class DownloadService {
|
|||
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';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -312,6 +312,7 @@ export class EmailService {
|
|||
Accept: 'application/json',
|
||||
Authorization: truemailAuthKey,
|
||||
},
|
||||
isLocalAddressAllowed: true,
|
||||
});
|
||||
|
||||
const json = (await res.json()) as {
|
||||
|
|
|
@ -223,6 +223,8 @@ export interface InternalEventTypes {
|
|||
unfollow: { followerId: MiUser['id']; followeeId: MiUser['id']; };
|
||||
blockingCreated: { blockerId: MiUser['id']; blockeeId: MiUser['id']; };
|
||||
blockingDeleted: { blockerId: MiUser['id']; blockeeId: MiUser['id']; };
|
||||
blockingReactionCreated: { blockerId: MiUser['id']; blockeeId: MiUser['id']; };
|
||||
blockingReactionDeleted: { blockerId: MiUser['id']; blockeeId: MiUser['id']; };
|
||||
policiesUpdated: MiRole['policies'];
|
||||
roleCreated: MiRole;
|
||||
roleDeleted: MiRole;
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
import * as http from 'node:http';
|
||||
import * as https from 'node:https';
|
||||
import * as net from 'node:net';
|
||||
import ipaddr from 'ipaddr.js';
|
||||
import CacheableLookup from 'cacheable-lookup';
|
||||
import fetch from 'node-fetch';
|
||||
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
||||
|
@ -15,6 +16,7 @@ import type { Config } from '@/config.js';
|
|||
import { StatusError } from '@/misc/status-error.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
|
||||
import { assertActivityMatchesUrls } from '@/core/activitypub/misc/check-against-url.js';
|
||||
import type { IObject } from '@/core/activitypub/type.js';
|
||||
import type { Response } from 'node-fetch';
|
||||
import type { URL } from 'node:url';
|
||||
|
@ -24,8 +26,102 @@ export type HttpRequestSendOptions = {
|
|||
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()
|
||||
export class HttpRequestService {
|
||||
/**
|
||||
* Get http non-proxy agent (without local address filtering)
|
||||
*/
|
||||
private httpNative: http.Agent;
|
||||
|
||||
/**
|
||||
* Get https non-proxy agent (without local address filtering)
|
||||
*/
|
||||
private httpsNative: https.Agent;
|
||||
|
||||
/**
|
||||
* Get http non-proxy agent
|
||||
*/
|
||||
|
@ -56,19 +152,20 @@ export class HttpRequestService {
|
|||
lookup: false, // nativeのdns.lookupにfallbackしない
|
||||
});
|
||||
|
||||
this.http = new http.Agent({
|
||||
const agentOption = {
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 30 * 1000,
|
||||
lookup: cache.lookup as unknown as net.LookupFunction,
|
||||
localAddress: config.outgoingAddress,
|
||||
});
|
||||
};
|
||||
|
||||
this.https = new https.Agent({
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 30 * 1000,
|
||||
lookup: cache.lookup as unknown as net.LookupFunction,
|
||||
localAddress: config.outgoingAddress,
|
||||
});
|
||||
this.httpNative = new http.Agent(agentOption);
|
||||
|
||||
this.httpsNative = new https.Agent(agentOption);
|
||||
|
||||
this.http = new HttpRequestServiceAgent(config, agentOption);
|
||||
|
||||
this.https = new HttpsRequestServiceAgent(config, agentOption);
|
||||
|
||||
const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128);
|
||||
|
||||
|
@ -103,16 +200,22 @@ export class HttpRequestService {
|
|||
* @param bypassProxy Allways bypass proxy
|
||||
*/
|
||||
@bindThis
|
||||
public getAgentByUrl(url: URL, bypassProxy = false): http.Agent | https.Agent {
|
||||
public getAgentByUrl(url: URL, bypassProxy = false, isLocalAddressAllowed = false): http.Agent | https.Agent {
|
||||
if (bypassProxy || (this.config.proxyBypassHosts ?? []).includes(url.hostname)) {
|
||||
if (isLocalAddressAllowed) {
|
||||
return url.protocol === 'http:' ? this.httpNative : this.httpsNative;
|
||||
}
|
||||
return url.protocol === 'http:' ? this.http : this.https;
|
||||
} else {
|
||||
if (isLocalAddressAllowed && (!this.config.proxy)) {
|
||||
return url.protocol === 'http:' ? this.httpNative : this.httpsNative;
|
||||
}
|
||||
return url.protocol === 'http:' ? this.httpAgent : this.httpsAgent;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getActivityJson(url: string): Promise<IObject> {
|
||||
public async getActivityJson(url: string, isLocalAddressAllowed = false): Promise<IObject> {
|
||||
const res = await this.send(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
|
@ -120,16 +223,22 @@ export class HttpRequestService {
|
|||
},
|
||||
timeout: 5000,
|
||||
size: 1024 * 256,
|
||||
isLocalAddressAllowed: isLocalAddressAllowed,
|
||||
}, {
|
||||
throwErrorWhenResponseNotOk: true,
|
||||
validators: [validateContentTypeSetAsActivityPub],
|
||||
});
|
||||
|
||||
return await res.json() as IObject;
|
||||
const finalUrl = res.url; // redirects may have been involved
|
||||
const activity = await res.json() as IObject;
|
||||
|
||||
assertActivityMatchesUrls(activity, [finalUrl]);
|
||||
|
||||
return activity;
|
||||
}
|
||||
|
||||
@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, {
|
||||
method: 'GET',
|
||||
headers: Object.assign({
|
||||
|
@ -137,19 +246,21 @@ export class HttpRequestService {
|
|||
}, headers ?? {}),
|
||||
timeout: 5000,
|
||||
size: 1024 * 256,
|
||||
isLocalAddressAllowed: isLocalAddressAllowed,
|
||||
});
|
||||
|
||||
return await res.json() as T;
|
||||
}
|
||||
|
||||
@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, {
|
||||
method: 'GET',
|
||||
headers: Object.assign({
|
||||
Accept: accept,
|
||||
}, headers ?? {}),
|
||||
timeout: 5000,
|
||||
isLocalAddressAllowed: isLocalAddressAllowed,
|
||||
});
|
||||
|
||||
return await res.text();
|
||||
|
@ -164,6 +275,7 @@ export class HttpRequestService {
|
|||
headers?: Record<string, string>,
|
||||
timeout?: number,
|
||||
size?: number,
|
||||
isLocalAddressAllowed?: boolean,
|
||||
} = {},
|
||||
extra: HttpRequestSendOptions = {
|
||||
throwErrorWhenResponseNotOk: true,
|
||||
|
@ -177,6 +289,8 @@ export class HttpRequestService {
|
|||
controller.abort();
|
||||
}, timeout);
|
||||
|
||||
const isLocalAddressAllowed = args.isLocalAddressAllowed ?? false;
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: args.method ?? 'GET',
|
||||
headers: {
|
||||
|
@ -185,7 +299,7 @@ export class HttpRequestService {
|
|||
},
|
||||
body: args.body,
|
||||
size: args.size ?? 10 * 1024 * 1024,
|
||||
agent: (url) => this.getAgentByUrl(url),
|
||||
agent: (url) => this.getAgentByUrl(url, false, isLocalAddressAllowed),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
|
|
|
@ -56,6 +56,7 @@ import { isReply } from '@/misc/is-reply.js';
|
|||
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { CollapsedQueue } from '@/misc/collapsed-queue.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
|
||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||
|
||||
|
@ -217,6 +218,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
private instanceChart: InstanceChart,
|
||||
private utilityService: UtilityService,
|
||||
private userBlockingService: UserBlockingService,
|
||||
private cacheService: CacheService,
|
||||
) {
|
||||
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({
|
||||
followeeId: user.id,
|
||||
notify: 'normal',
|
||||
}).then(followings => {
|
||||
}).then(async followings => {
|
||||
if (note.visibility !== 'specified') {
|
||||
const isPureRenote = this.isRenote(data) && !this.isQuote(data) ? true : false;
|
||||
for (const following of followings) {
|
||||
// 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', {
|
||||
noteId: note.id,
|
||||
}, user.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import type { MiUser } from '@/models/User.js';
|
|||
import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository, RenoteMutingsRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { MiBlockingType } from '@/models/Blocking.js';
|
||||
import type { SelectQueryBuilder } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
|
@ -72,7 +73,8 @@ export class QueryService {
|
|||
public generateBlockedUserQuery(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void {
|
||||
const blockingQuery = this.blockingsRepository.createQueryBuilder('blocking')
|
||||
.select('blocking.blockerId')
|
||||
.where('blocking.blockeeId = :blockeeId', { blockeeId: me.id });
|
||||
.where('blocking.blockeeId = :blockeeId', { blockeeId: me.id })
|
||||
.andWhere('blocking.blockType = :blockType', { blockType: MiBlockingType.User });
|
||||
|
||||
// 投稿の作者にブロックされていない かつ
|
||||
// 投稿の返信先の作者にブロックされていない かつ
|
||||
|
@ -97,7 +99,8 @@ export class QueryService {
|
|||
public generateBlockQueryForUsers(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void {
|
||||
const blockingQuery = this.blockingsRepository.createQueryBuilder('blocking')
|
||||
.select('blocking.blockeeId')
|
||||
.where('blocking.blockerId = :blockerId', { blockerId: me.id });
|
||||
.where('blocking.blockerId = :blockerId', { blockerId: me.id })
|
||||
.andWhere('blocking.blockType = :blockType', { blockType: MiBlockingType.User });
|
||||
|
||||
const blockedQuery = this.blockingsRepository.createQueryBuilder('blocking')
|
||||
.select('blocking.blockerId')
|
||||
|
|
|
@ -107,7 +107,8 @@ export class ReactionService {
|
|||
// Check blocking
|
||||
if (note.userId !== user.id) {
|
||||
const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
|
||||
if (blocked) {
|
||||
const reactionBlocked = await this.userBlockingService.checkReactionBlocked(note.userId, user.id);
|
||||
if (blocked || reactionBlocked) {
|
||||
throw new IdentifiableError('e70412a4-7197-4726-8e74-f3e0deb92aa7');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,9 +54,9 @@ export class RemoteUserResolveService {
|
|||
}) as MiLocalUser;
|
||||
}
|
||||
|
||||
host = this.utilityService.toPuny(host);
|
||||
host = this.utilityService.punyHost(host);
|
||||
|
||||
if (this.config.host === host) {
|
||||
if (host === this.utilityService.toPuny(this.config.host)) {
|
||||
this.logger.info(`return local user: ${usernameLower}`);
|
||||
return await this.usersRepository.findOneBy({ usernameLower, host: IsNull() }).then(u => {
|
||||
if (u == null) {
|
||||
|
|
|
@ -8,10 +8,16 @@ import { ModuleRef } from '@nestjs/core';
|
|||
import { IdService } from '@/core/IdService.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import type { MiBlocking } from '@/models/Blocking.js';
|
||||
import { MiBlockingType } from '@/models/Blocking.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListMembershipsRepository } from '@/models/_.js';
|
||||
import type {
|
||||
BlockingsRepository,
|
||||
FollowRequestsRepository,
|
||||
UserListMembershipsRepository,
|
||||
UserListsRepository,
|
||||
} from '@/models/_.js';
|
||||
import Logger from '@/logger.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
|
@ -67,14 +73,27 @@ export class UserBlockingService implements OnModuleInit {
|
|||
this.removeFromList(blockee, blocker),
|
||||
]);
|
||||
|
||||
const blocking = {
|
||||
const blocking = await this.blockingsRepository.findOneBy({
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
}).then(blocking => {
|
||||
if (blocking) {
|
||||
return blocking;
|
||||
}
|
||||
return {
|
||||
id: this.idService.gen(),
|
||||
blocker,
|
||||
blockerId: blocker.id,
|
||||
blockee,
|
||||
blockeeId: blockee.id,
|
||||
blockType: MiBlockingType.User,
|
||||
} as MiBlocking;
|
||||
});
|
||||
|
||||
if (blocking.blockType === MiBlockingType.Reaction) {
|
||||
await this.reactionUnblock(blocker, blockee);
|
||||
}
|
||||
blocking.blockType = MiBlockingType.User;
|
||||
await this.blockingsRepository.insert(blocking);
|
||||
|
||||
this.cacheService.userBlockingCache.refresh(blocker.id);
|
||||
|
@ -160,6 +179,7 @@ export class UserBlockingService implements OnModuleInit {
|
|||
const blocking = await this.blockingsRepository.findOneBy({
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
blockType: MiBlockingType.User,
|
||||
});
|
||||
|
||||
if (blocking == null) {
|
||||
|
@ -169,8 +189,9 @@ export class UserBlockingService implements OnModuleInit {
|
|||
|
||||
// Since we already have the blocker and blockee, we do not need to fetch
|
||||
// them in the query above and can just manually insert them here.
|
||||
blocking.blocker = blocker;
|
||||
blocking.blockee = blockee;
|
||||
// But we don't need to do this because we are not using them in this function.
|
||||
// blocking.blocker = blocker;
|
||||
// blocking.blockee = blockee;
|
||||
|
||||
await this.blockingsRepository.delete(blocking.id);
|
||||
|
||||
|
@ -193,4 +214,73 @@ export class UserBlockingService implements OnModuleInit {
|
|||
public async checkBlocked(blockerId: MiUser['id'], blockeeId: MiUser['id']): Promise<boolean> {
|
||||
return (await this.cacheService.userBlockingCache.fetch(blockerId)).has(blockeeId);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async reactionBlock(blocker: MiUser, blockee: MiUser, silent = false) {
|
||||
const blocking = await this.blockingsRepository.findOneBy({
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
}).then(blocking => {
|
||||
if (blocking) {
|
||||
return blocking;
|
||||
}
|
||||
return {
|
||||
id: this.idService.gen(),
|
||||
blocker,
|
||||
blockerId: blocker.id,
|
||||
blockee,
|
||||
blockeeId: blockee.id,
|
||||
blockType: MiBlockingType.Reaction,
|
||||
} as MiBlocking;
|
||||
});
|
||||
|
||||
if (blocking.blockType === MiBlockingType.User) {
|
||||
await this.unblock(blocker, blockee);
|
||||
}
|
||||
blocking.blockType = MiBlockingType.Reaction;
|
||||
await this.blockingsRepository.insert(blocking);
|
||||
|
||||
this.cacheService.userReactionBlockingCache.refresh(blocker.id);
|
||||
this.cacheService.userReactionBlockedCache.refresh(blockee.id);
|
||||
|
||||
this.globalEventService.publishInternalEvent('blockingReactionCreated', {
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async reactionUnblock(blocker: MiUser, blockee: MiUser) {
|
||||
const blocking = await this.blockingsRepository.findOneBy({
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
blockType: MiBlockingType.Reaction,
|
||||
});
|
||||
|
||||
if (blocking == null) {
|
||||
this.logger.warn('Unblock requested, but the target was not blocked.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Since we already have the blocker and blockee, we do not need to fetch
|
||||
// them in the query above and can just manually insert them here.
|
||||
blocking.blocker = blocker;
|
||||
blocking.blockee = blockee;
|
||||
|
||||
await this.blockingsRepository.delete(blocking.id);
|
||||
|
||||
this.cacheService.userReactionBlockingCache.refresh(blocker.id);
|
||||
this.cacheService.userReactionBlockedCache.refresh(blockee.id);
|
||||
|
||||
this.globalEventService.publishInternalEvent('blockingReactionDeleted', {
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async checkReactionBlocked(blockerId: MiUser['id'], blockeeId: MiUser['id']): Promise<boolean> {
|
||||
return (await this.cacheService.userReactionBlockingCache.fetch(blockerId)).has(blockeeId);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -34,6 +34,11 @@ export class UtilityService {
|
|||
return this.toPuny(this.config.host) === this.toPuny(host);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public isUriLocal(uri: string): boolean {
|
||||
return this.punyHost(uri) === this.toPuny(this.config.host);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public isBlockedHost(blockedHosts: string[], host: string | null): boolean {
|
||||
if (host == null) return false;
|
||||
|
@ -96,7 +101,7 @@ export class UtilityService {
|
|||
@bindThis
|
||||
public extractDbHost(uri: string): string {
|
||||
const url = new URL(uri);
|
||||
return this.toPuny(url.hostname);
|
||||
return this.toPuny(url.host);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -110,6 +115,13 @@ export class UtilityService {
|
|||
return toASCII(host.toLowerCase());
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public punyHost(url: string): string {
|
||||
const urlObj = new URL(url);
|
||||
const host = `${this.toPuny(urlObj.hostname)}${urlObj.port.length > 0 ? ':' + urlObj.port : ''}`;
|
||||
return host;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public isFederationAllowedHost(host: string): boolean {
|
||||
if (this.meta.federation === 'none') return false;
|
||||
|
|
|
@ -246,14 +246,12 @@ export class WebAuthnService {
|
|||
|
||||
@bindThis
|
||||
public async verifyAuthentication(userId: MiUser['id'], response: AuthenticationResponseJSON): Promise<boolean> {
|
||||
const challenge = await this.redisClient.get(`webauthn:challenge:${userId}`);
|
||||
const challenge = await this.redisClient.getdel(`webauthn:challenge:${userId}`);
|
||||
|
||||
if (!challenge) {
|
||||
throw new IdentifiableError('2d16e51c-007b-4edd-afd2-f7dd02c947f6', 'challenge not found');
|
||||
}
|
||||
|
||||
await this.redisClient.del(`webauthn:challenge:${userId}`);
|
||||
|
||||
const key = await this.userSecurityKeysRepository.findOneBy({
|
||||
id: response.id,
|
||||
userId: userId,
|
||||
|
|
|
@ -10,6 +10,7 @@ import type { Config } from '@/config.js';
|
|||
import { MemoryKVCache } from '@/misc/cache.js';
|
||||
import type { MiUserPublickey } from '@/models/UserPublickey.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import type { MiNote } from '@/models/Note.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MiLocalUser, MiRemoteUser } from '@/models/User.js';
|
||||
|
@ -53,6 +54,7 @@ export class ApDbResolverService implements OnApplicationShutdown {
|
|||
|
||||
private cacheService: CacheService,
|
||||
private apPersonService: ApPersonService,
|
||||
private utilityService: UtilityService,
|
||||
) {
|
||||
this.publicKeyCache = new MemoryKVCache<MiUserPublickey | null>(1000 * 60 * 60 * 12); // 12h
|
||||
this.publicKeyByUserIdCache = new MemoryKVCache<MiUserPublickey | null>(1000 * 60 * 60 * 12); // 12h
|
||||
|
@ -63,7 +65,9 @@ export class ApDbResolverService implements OnApplicationShutdown {
|
|||
const separator = '/';
|
||||
|
||||
const uri = new URL(getApId(value));
|
||||
if (uri.origin !== this.config.url) return { local: false, uri: uri.href };
|
||||
if (this.utilityService.toPuny(uri.host) !== this.utilityService.toPuny(this.config.host)) {
|
||||
return { local: false, uri: uri.href };
|
||||
}
|
||||
|
||||
const [, type, id, ...rest] = uri.pathname.split(separator);
|
||||
return {
|
||||
|
|
|
@ -89,15 +89,26 @@ export class ApInboxService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async performActivity(actor: MiRemoteUser, activity: IObject): Promise<string | void> {
|
||||
public async performActivity(actor: MiRemoteUser, activity: IObject, resolver?: Resolver): Promise<string | void> {
|
||||
let result = undefined as string | void;
|
||||
if (isCollectionOrOrderedCollection(activity)) {
|
||||
const results = [] as [string, string | void][];
|
||||
const resolver = this.apResolverService.createResolver();
|
||||
for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
resolver ??= this.apResolverService.createResolver();
|
||||
|
||||
const items = toArray(isCollection(activity) ? activity.items : activity.orderedItems);
|
||||
if (items.length >= resolver.getRecursionLimit()) {
|
||||
throw new Error(`skipping activity: collection would surpass recursion limit: ${this.utilityService.extractDbHost(actor.uri)}`);
|
||||
}
|
||||
|
||||
for (const item of items) {
|
||||
const act = await resolver.resolve(item);
|
||||
if (act.id == null || this.utilityService.extractDbHost(act.id) !== this.utilityService.extractDbHost(actor.uri)) {
|
||||
this.logger.debug('skipping activity: activity id is null or mismatching');
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
results.push([getApId(item), await this.performOneActivity(actor, act)]);
|
||||
results.push([getApId(item), await this.performOneActivity(actor, act, resolver)]);
|
||||
} catch (err) {
|
||||
if (err instanceof Error || typeof err === 'string') {
|
||||
this.logger.error(err);
|
||||
|
@ -112,7 +123,7 @@ export class ApInboxService {
|
|||
result = results.map(([id, reason]) => `${id}: ${reason}`).join('\n');
|
||||
}
|
||||
} else {
|
||||
result = await this.performOneActivity(actor, activity);
|
||||
result = await this.performOneActivity(actor, activity, resolver);
|
||||
}
|
||||
|
||||
// ついでにリモートユーザーの情報が古かったら更新しておく
|
||||
|
@ -127,37 +138,37 @@ export class ApInboxService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async performOneActivity(actor: MiRemoteUser, activity: IObject): Promise<string | void> {
|
||||
public async performOneActivity(actor: MiRemoteUser, activity: IObject, resolver?: Resolver): Promise<string | void> {
|
||||
if (actor.isSuspended) return;
|
||||
|
||||
if (isCreate(activity)) {
|
||||
return await this.create(actor, activity);
|
||||
return await this.create(actor, activity, resolver);
|
||||
} else if (isDelete(activity)) {
|
||||
return await this.delete(actor, activity);
|
||||
} else if (isUpdate(activity)) {
|
||||
return await this.update(actor, activity);
|
||||
return await this.update(actor, activity, resolver);
|
||||
} else if (isFollow(activity)) {
|
||||
return await this.follow(actor, activity);
|
||||
} else if (isAccept(activity)) {
|
||||
return await this.accept(actor, activity);
|
||||
return await this.accept(actor, activity, resolver);
|
||||
} else if (isReject(activity)) {
|
||||
return await this.reject(actor, activity);
|
||||
return await this.reject(actor, activity, resolver);
|
||||
} else if (isAdd(activity)) {
|
||||
return await this.add(actor, activity);
|
||||
return await this.add(actor, activity, resolver);
|
||||
} else if (isRemove(activity)) {
|
||||
return await this.remove(actor, activity);
|
||||
return await this.remove(actor, activity, resolver);
|
||||
} else if (isAnnounce(activity)) {
|
||||
return await this.announce(actor, activity);
|
||||
return await this.announce(actor, activity, resolver);
|
||||
} else if (isLike(activity)) {
|
||||
return await this.like(actor, activity);
|
||||
} else if (isUndo(activity)) {
|
||||
return await this.undo(actor, activity);
|
||||
return await this.undo(actor, activity, resolver);
|
||||
} else if (isBlock(activity)) {
|
||||
return await this.block(actor, activity);
|
||||
} else if (isFlag(activity)) {
|
||||
return await this.flag(actor, activity);
|
||||
} else if (isMove(activity)) {
|
||||
return await this.move(actor, activity);
|
||||
return await this.move(actor, activity, resolver);
|
||||
} else {
|
||||
return `unrecognized activity type: ${activity.type}`;
|
||||
}
|
||||
|
@ -199,12 +210,13 @@ export class ApInboxService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
private async accept(actor: MiRemoteUser, activity: IAccept): Promise<string> {
|
||||
private async accept(actor: MiRemoteUser, activity: IAccept, resolver?: Resolver): Promise<string> {
|
||||
const uri = activity.id ?? activity;
|
||||
|
||||
this.logger.info(`Accept: ${uri}`);
|
||||
|
||||
const resolver = this.apResolverService.createResolver();
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
resolver ??= this.apResolverService.createResolver();
|
||||
|
||||
const object = await resolver.resolve(activity.object).catch(err => {
|
||||
this.logger.error(`Resolution failed: ${err}`);
|
||||
|
@ -241,7 +253,7 @@ export class ApInboxService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
private async add(actor: MiRemoteUser, activity: IAdd): Promise<string | void> {
|
||||
private async add(actor: MiRemoteUser, activity: IAdd, resolver?: Resolver): Promise<string | void> {
|
||||
if (actor.uri !== activity.actor) {
|
||||
return 'invalid actor';
|
||||
}
|
||||
|
@ -251,7 +263,7 @@ export class ApInboxService {
|
|||
}
|
||||
|
||||
if (activity.target === actor.featured) {
|
||||
const note = await this.apNoteService.resolveNote(activity.object);
|
||||
const note = await this.apNoteService.resolveNote(activity.object, { resolver });
|
||||
if (note == null) return 'note not found';
|
||||
await this.notePiningService.addPinned(actor, note.id);
|
||||
return;
|
||||
|
@ -261,12 +273,13 @@ export class ApInboxService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
private async announce(actor: MiRemoteUser, activity: IAnnounce): Promise<string | void> {
|
||||
private async announce(actor: MiRemoteUser, activity: IAnnounce, resolver?: Resolver): Promise<string | void> {
|
||||
const uri = getApId(activity);
|
||||
|
||||
this.logger.info(`Announce: ${uri}`);
|
||||
|
||||
const resolver = this.apResolverService.createResolver();
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
resolver ??= this.apResolverService.createResolver();
|
||||
|
||||
if (!activity.object) return 'skip: activity has no object property';
|
||||
const targetUri = getApId(activity.object);
|
||||
|
@ -283,7 +296,7 @@ export class ApInboxService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
private async announceNote(actor: MiRemoteUser, activity: IAnnounce, target: IPost): Promise<string | void> {
|
||||
private async announceNote(actor: MiRemoteUser, activity: IAnnounce, target: IPost, resolver?: Resolver): Promise<string | void> {
|
||||
const uri = getApId(activity);
|
||||
|
||||
if (actor.isSuspended) {
|
||||
|
@ -305,7 +318,7 @@ export class ApInboxService {
|
|||
// Announce対象をresolve
|
||||
let renote;
|
||||
try {
|
||||
renote = await this.apNoteService.resolveNote(target);
|
||||
renote = await this.apNoteService.resolveNote(target, { resolver });
|
||||
if (renote == null) return 'announce target is null';
|
||||
} catch (err) {
|
||||
// 対象が4xxならスキップ
|
||||
|
@ -324,7 +337,7 @@ export class ApInboxService {
|
|||
|
||||
this.logger.info(`Creating the (Re)Note: ${uri}`);
|
||||
|
||||
const activityAudience = await this.apAudienceService.parseAudience(actor, activity.to, activity.cc);
|
||||
const activityAudience = await this.apAudienceService.parseAudience(actor, activity.to, activity.cc, resolver);
|
||||
const createdAt = activity.published ? new Date(activity.published) : null;
|
||||
|
||||
if (createdAt && createdAt < this.idService.parse(renote.id).date) {
|
||||
|
@ -362,7 +375,7 @@ export class ApInboxService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
private async create(actor: MiRemoteUser, activity: ICreate): Promise<string | void> {
|
||||
private async create(actor: MiRemoteUser, activity: ICreate, resolver?: Resolver): Promise<string | void> {
|
||||
const uri = getApId(activity);
|
||||
|
||||
this.logger.info(`Create: ${uri}`);
|
||||
|
@ -387,7 +400,8 @@ export class ApInboxService {
|
|||
activity.object.attributedTo = activity.actor;
|
||||
}
|
||||
|
||||
const resolver = this.apResolverService.createResolver();
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
resolver ??= this.apResolverService.createResolver();
|
||||
|
||||
const object = await resolver.resolve(activity.object).catch(e => {
|
||||
this.logger.error(`Resolution failed: ${e}`);
|
||||
|
@ -414,6 +428,8 @@ export class ApInboxService {
|
|||
if (this.utilityService.extractDbHost(actor.uri) !== this.utilityService.extractDbHost(note.id)) {
|
||||
return 'skip: host in actor.uri !== note.id';
|
||||
}
|
||||
} else {
|
||||
return 'skip: note.id is not a string';
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -423,7 +439,7 @@ export class ApInboxService {
|
|||
const exist = await this.apNoteService.fetchNote(note);
|
||||
if (exist) return 'skip: note exists';
|
||||
|
||||
await this.apNoteService.createNote(note, resolver, silent);
|
||||
await this.apNoteService.createNote(note, actor, resolver, silent);
|
||||
return 'ok';
|
||||
} catch (err) {
|
||||
if (err instanceof StatusError && !err.isRetryable) {
|
||||
|
@ -555,12 +571,13 @@ export class ApInboxService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
private async reject(actor: MiRemoteUser, activity: IReject): Promise<string> {
|
||||
private async reject(actor: MiRemoteUser, activity: IReject, resolver?: Resolver): Promise<string> {
|
||||
const uri = activity.id ?? activity;
|
||||
|
||||
this.logger.info(`Reject: ${uri}`);
|
||||
|
||||
const resolver = this.apResolverService.createResolver();
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
resolver ??= this.apResolverService.createResolver();
|
||||
|
||||
const object = await resolver.resolve(activity.object).catch(e => {
|
||||
this.logger.error(`Resolution failed: ${e}`);
|
||||
|
@ -597,7 +614,7 @@ export class ApInboxService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
private async remove(actor: MiRemoteUser, activity: IRemove): Promise<string | void> {
|
||||
private async remove(actor: MiRemoteUser, activity: IRemove, resolver?: Resolver): Promise<string | void> {
|
||||
if (actor.uri !== activity.actor) {
|
||||
return 'invalid actor';
|
||||
}
|
||||
|
@ -607,7 +624,7 @@ export class ApInboxService {
|
|||
}
|
||||
|
||||
if (activity.target === actor.featured) {
|
||||
const note = await this.apNoteService.resolveNote(activity.object);
|
||||
const note = await this.apNoteService.resolveNote(activity.object, { resolver });
|
||||
if (note == null) return 'note not found';
|
||||
await this.notePiningService.removePinned(actor, note.id);
|
||||
return;
|
||||
|
@ -617,7 +634,7 @@ export class ApInboxService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
private async undo(actor: MiRemoteUser, activity: IUndo): Promise<string> {
|
||||
private async undo(actor: MiRemoteUser, activity: IUndo, resolver?: Resolver): Promise<string> {
|
||||
if (actor.uri !== activity.actor) {
|
||||
return 'invalid actor';
|
||||
}
|
||||
|
@ -626,7 +643,8 @@ export class ApInboxService {
|
|||
|
||||
this.logger.info(`Undo: ${uri}`);
|
||||
|
||||
const resolver = this.apResolverService.createResolver();
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
resolver ??= this.apResolverService.createResolver();
|
||||
|
||||
const object = await resolver.resolve(activity.object).catch(e => {
|
||||
this.logger.error(`Resolution failed: ${e}`);
|
||||
|
@ -750,14 +768,15 @@ export class ApInboxService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
private async update(actor: MiRemoteUser, activity: IUpdate): Promise<string> {
|
||||
private async update(actor: MiRemoteUser, activity: IUpdate, resolver?: Resolver): Promise<string> {
|
||||
if (actor.uri !== activity.actor) {
|
||||
return 'skip: invalid actor';
|
||||
}
|
||||
|
||||
this.logger.debug('Update');
|
||||
|
||||
const resolver = this.apResolverService.createResolver();
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
resolver ??= this.apResolverService.createResolver();
|
||||
|
||||
const object = await resolver.resolve(activity.object).catch(e => {
|
||||
this.logger.error(`Resolution failed: ${e}`);
|
||||
|
@ -768,7 +787,7 @@ export class ApInboxService {
|
|||
await this.apPersonService.updatePerson(actor.uri, resolver, object);
|
||||
return 'ok: Person updated';
|
||||
} 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';
|
||||
} else {
|
||||
return `skip: Unknown type: ${getApType(object)}`;
|
||||
|
@ -776,11 +795,11 @@ export class ApInboxService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
private async move(actor: MiRemoteUser, activity: IMove): Promise<string> {
|
||||
private async move(actor: MiRemoteUser, activity: IMove, resolver?: Resolver): Promise<string> {
|
||||
// fetch the new and old accounts
|
||||
const targetUri = getApHrefNullable(activity.target);
|
||||
if (!targetUri) return 'skip: invalid activity target';
|
||||
|
||||
return await this.apPersonService.updatePerson(actor.uri) ?? 'skip: nothing to do';
|
||||
return await this.apPersonService.updatePerson(actor.uri, resolver) ?? 'skip: nothing to do';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,11 +11,14 @@ import { DI } from '@/di-symbols.js';
|
|||
import type { Config } from '@/config.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { UserKeypairService } from '@/core/UserKeypairService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type Logger from '@/logger.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 = {
|
||||
url: string;
|
||||
|
@ -145,6 +148,7 @@ export class ApRequestService {
|
|||
private userKeypairService: UserKeypairService,
|
||||
private httpRequestService: HttpRequestService,
|
||||
private loggerService: LoggerService,
|
||||
private utilityService: UtilityService,
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
this.logger = this.loggerService?.getLogger('ap-request'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる
|
||||
|
@ -238,7 +242,7 @@ export class ApRequestService {
|
|||
const alternate = document.querySelector('head > link[rel="alternate"][type="application/activity+json"]');
|
||||
if (alternate) {
|
||||
const href = alternate.getAttribute('href');
|
||||
if (href) {
|
||||
if (href && this.utilityService.punyHost(url) === this.utilityService.punyHost(href)) {
|
||||
return await this.signedGet(href, user, false);
|
||||
}
|
||||
}
|
||||
|
@ -251,7 +255,11 @@ export class ApRequestService {
|
|||
//#endregion
|
||||
|
||||
validateContentTypeSetAsActivityPub(res);
|
||||
const finalUrl = res.url; // redirects may have been involved
|
||||
const activity = await res.json() as IObject;
|
||||
|
||||
return await res.json();
|
||||
assertActivityMatchesUrls(activity, [finalUrl]);
|
||||
|
||||
return activity;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ export class Resolver {
|
|||
private apRendererService: ApRendererService,
|
||||
private apDbResolverService: ApDbResolverService,
|
||||
private loggerService: LoggerService,
|
||||
private recursionLimit = 100,
|
||||
private recursionLimit = 256,
|
||||
) {
|
||||
this.history = new Set();
|
||||
this.logger = this.loggerService.getLogger('ap-resolve');
|
||||
|
@ -52,6 +52,11 @@ export class Resolver {
|
|||
return Array.from(this.history);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public getRecursionLimit(): number {
|
||||
return this.recursionLimit;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async resolveCollection(value: string | IObject): Promise<ICollection | IOrderedCollection> {
|
||||
const collection = typeof value === 'string'
|
||||
|
@ -113,6 +118,18 @@ export class Resolver {
|
|||
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');
|
||||
}
|
||||
|
||||
if (this.utilityService.punyHost(object.id) !== this.utilityService.punyHost(value)) {
|
||||
throw new Error(`invalid AP object ${value}: id ${object.id} has different host`);
|
||||
}
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: dakkar and sharkey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { IObject } from '../type.js';
|
||||
|
||||
export function assertActivityMatchesUrls(activity: IObject, urls: string[]) {
|
||||
const idOk = activity.id !== undefined && urls.includes(activity.id);
|
||||
|
||||
// technically `activity.url` could be an `ApObject = IObject |
|
||||
// string | (IObject | string)[]`, but if it's a complicated thing
|
||||
// and the `activity.id` doesn't match, I think we're fine
|
||||
// rejecting the activity
|
||||
const urlOk = typeof(activity.url) === 'string' && urls.includes(activity.url);
|
||||
|
||||
if (!idOk && !urlOk) {
|
||||
throw new Error(`bad Activity: neither id(${activity?.id}) nor url(${activity?.url}) match location(${urls})`);
|
||||
}
|
||||
}
|
|
@ -77,7 +77,7 @@ export class ApNoteService {
|
|||
}
|
||||
|
||||
@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 apType = getApType(object);
|
||||
|
||||
|
@ -98,6 +98,14 @@ export class ApNoteService {
|
|||
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;
|
||||
}
|
||||
|
||||
|
@ -115,14 +123,14 @@ export class ApNoteService {
|
|||
* Noteを作成します。
|
||||
*/
|
||||
@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 entryUri = getApId(value);
|
||||
const err = this.validateNote(object, entryUri);
|
||||
const err = this.validateNote(object, entryUri, actor);
|
||||
if (err) {
|
||||
this.logger.error(err.message, {
|
||||
resolver: { history: resolver.getHistory() },
|
||||
|
@ -136,16 +144,26 @@ export class ApNoteService {
|
|||
|
||||
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);
|
||||
}
|
||||
|
||||
const url = getOneApHrefNullable(note.url);
|
||||
|
||||
if (url && !checkHttps(url)) {
|
||||
if (url != null) {
|
||||
if (!checkHttps(url)) {
|
||||
throw new Error('unexpected schema of note url: ' + url);
|
||||
}
|
||||
|
||||
if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(note.id)) {
|
||||
throw new Error(`note url & uri host mismatch: note url: ${url}, note uri: ${note.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.info(`Creating the Note: ${note.id}`);
|
||||
|
||||
// 投稿者をフェッチ
|
||||
|
@ -156,8 +174,9 @@ export class ApNoteService {
|
|||
const uri = getOneApId(note.attributedTo);
|
||||
|
||||
// ローカルで投稿者を検索し、もし凍結されていたらスキップ
|
||||
const cachedActor = await this.apPersonService.fetchPerson(uri) as MiRemoteUser;
|
||||
if (cachedActor && cachedActor.isSuspended) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
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');
|
||||
}
|
||||
|
||||
|
@ -189,7 +208,8 @@ export class ApNoteService {
|
|||
}
|
||||
//#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) {
|
||||
|
@ -348,7 +368,7 @@ export class ApNoteService {
|
|||
if (exist) return exist;
|
||||
//#endregion
|
||||
|
||||
if (uri.startsWith(this.config.url)) {
|
||||
if (this.utilityService.isUriLocal(uri)) {
|
||||
throw new StatusError('cannot resolve local note', 400, 'cannot resolve local note');
|
||||
}
|
||||
|
||||
|
@ -356,7 +376,7 @@ export class ApNoteService {
|
|||
// ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにノートが生成されるが
|
||||
// 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。
|
||||
const createFrom = options.sentFrom?.origin === new URL(uri).origin ? value : uri;
|
||||
return await this.createNote(createFrom, options.resolver, true);
|
||||
return await this.createNote(createFrom, undefined, options.resolver, true);
|
||||
} finally {
|
||||
unlock();
|
||||
}
|
||||
|
|
|
@ -129,12 +129,6 @@ export class ApPersonService implements OnModuleInit {
|
|||
this.logger = this.apLoggerService.logger;
|
||||
}
|
||||
|
||||
private punyHost(url: string): string {
|
||||
const urlObj = new URL(url);
|
||||
const host = `${this.utilityService.toPuny(urlObj.hostname)}${urlObj.port.length > 0 ? ':' + urlObj.port : ''}`;
|
||||
return host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and convert to actor object
|
||||
* @param x Fetched object
|
||||
|
@ -142,7 +136,7 @@ export class ApPersonService implements OnModuleInit {
|
|||
*/
|
||||
@bindThis
|
||||
private validateActor(x: IObject, uri: string): IActor {
|
||||
const expectHost = this.punyHost(uri);
|
||||
const expectHost = this.utilityService.punyHost(uri);
|
||||
|
||||
if (!isActor(x)) {
|
||||
throw new Error(`invalid Actor type '${x.type}'`);
|
||||
|
@ -156,6 +150,29 @@ export class ApPersonService implements OnModuleInit {
|
|||
throw new Error('invalid Actor: wrong inbox');
|
||||
}
|
||||
|
||||
if (this.utilityService.punyHost(x.inbox) !== expectHost) {
|
||||
throw new Error('invalid Actor: inbox has different host');
|
||||
}
|
||||
|
||||
const sharedInboxObject = x.sharedInbox ?? (x.endpoints ? x.endpoints.sharedInbox : undefined);
|
||||
if (sharedInboxObject != null) {
|
||||
const sharedInbox = getApId(sharedInboxObject);
|
||||
if (!(typeof sharedInbox === 'string' && sharedInbox.length > 0 && this.utilityService.punyHost(sharedInbox) === expectHost)) {
|
||||
throw new Error('invalid Actor: wrong shared inbox');
|
||||
}
|
||||
}
|
||||
|
||||
for (const collection of ['outbox', 'followers', 'following'] as (keyof IActor)[]) {
|
||||
const collectionUri = getApId((x as IActor)[collection]);
|
||||
if (typeof collectionUri === 'string' && collectionUri.length > 0) {
|
||||
if (this.utilityService.punyHost(collectionUri) !== expectHost) {
|
||||
throw new Error(`invalid Actor: ${collection} has different host`);
|
||||
}
|
||||
} else if (collectionUri != null) {
|
||||
throw new Error(`invalid Actor: wrong ${collection}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!(typeof x.preferredUsername === 'string' && x.preferredUsername.length > 0 && x.preferredUsername.length <= 128 && /^\w([\w-.]*\w)?$/.test(x.preferredUsername))) {
|
||||
throw new Error('invalid Actor: wrong username');
|
||||
}
|
||||
|
@ -179,7 +196,7 @@ export class ApPersonService implements OnModuleInit {
|
|||
x.summary = truncate(x.summary, summaryLength);
|
||||
}
|
||||
|
||||
const idHost = this.punyHost(x.id);
|
||||
const idHost = this.utilityService.punyHost(x.id);
|
||||
if (idHost !== expectHost) {
|
||||
throw new Error('invalid Actor: id has different host');
|
||||
}
|
||||
|
@ -189,7 +206,7 @@ export class ApPersonService implements OnModuleInit {
|
|||
throw new Error('invalid Actor: publicKey.id is not a string');
|
||||
}
|
||||
|
||||
const publicKeyIdHost = this.punyHost(x.publicKey.id);
|
||||
const publicKeyIdHost = this.utilityService.punyHost(x.publicKey.id);
|
||||
if (publicKeyIdHost !== expectHost) {
|
||||
throw new Error('invalid Actor: publicKey.id has different host');
|
||||
}
|
||||
|
@ -280,7 +297,8 @@ export class ApPersonService implements OnModuleInit {
|
|||
public async createPerson(uri: string, resolver?: Resolver): Promise<MiRemoteUser> {
|
||||
if (typeof uri !== 'string') throw new Error('uri is not string');
|
||||
|
||||
if (uri.startsWith(this.config.url)) {
|
||||
const host = this.utilityService.punyHost(uri);
|
||||
if (host === this.utilityService.toPuny(this.config.host)) {
|
||||
throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user');
|
||||
}
|
||||
|
||||
|
@ -294,8 +312,6 @@ export class ApPersonService implements OnModuleInit {
|
|||
|
||||
this.logger.info(`Creating the Person: ${person.id}`);
|
||||
|
||||
const host = this.punyHost(object.id);
|
||||
|
||||
const fields = this.analyzeAttachments(person.attachment ?? []);
|
||||
|
||||
const tags = extractApHashtags(person.tag).map(normalizeForSearch).splice(0, 32);
|
||||
|
@ -321,10 +337,20 @@ export class ApPersonService implements OnModuleInit {
|
|||
|
||||
const url = getOneApHrefNullable(person.url);
|
||||
|
||||
if (url && !checkHttps(url)) {
|
||||
if (person.id == null) {
|
||||
throw new Error('Refusing to create person without id');
|
||||
}
|
||||
|
||||
if (url != null) {
|
||||
if (!checkHttps(url)) {
|
||||
throw new Error('unexpected schema of person url: ' + url);
|
||||
}
|
||||
|
||||
if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(person.id)) {
|
||||
throw new Error(`person url <> uri host mismatch: ${url} <> ${person.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Create user
|
||||
let user: MiRemoteUser | null = null;
|
||||
|
||||
|
@ -465,7 +491,7 @@ export class ApPersonService implements OnModuleInit {
|
|||
if (typeof uri !== 'string') throw new Error('uri is not string');
|
||||
|
||||
// URIがこのサーバーを指しているならスキップ
|
||||
if (uri.startsWith(`${this.config.url}/`)) return;
|
||||
if (this.utilityService.isUriLocal(uri)) return;
|
||||
|
||||
//#region このサーバーに既に登録されているか
|
||||
const exist = await this.fetchPerson(uri) as MiRemoteUser | null;
|
||||
|
@ -514,10 +540,20 @@ export class ApPersonService implements OnModuleInit {
|
|||
|
||||
const url = getOneApHrefNullable(person.url);
|
||||
|
||||
if (url && !checkHttps(url)) {
|
||||
if (person.id == null) {
|
||||
throw new Error('Refusing to update person without id');
|
||||
}
|
||||
|
||||
if (url != null) {
|
||||
if (!checkHttps(url)) {
|
||||
throw new Error('unexpected schema of person url: ' + url);
|
||||
}
|
||||
|
||||
if (this.utilityService.punyHost(url) !== this.utilityService.punyHost(person.id)) {
|
||||
throw new Error(`person url <> uri host mismatch: ${url} <> ${person.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
const updates = {
|
||||
lastFetchedAt: new Date(),
|
||||
inbox: person.inbox,
|
||||
|
@ -728,7 +764,7 @@ export class ApPersonService implements OnModuleInit {
|
|||
await this.updatePerson(src.movedToUri, undefined, undefined, [...movePreventUris, src.uri]);
|
||||
dst = await this.fetchPerson(src.movedToUri) ?? dst;
|
||||
} else {
|
||||
if (src.movedToUri.startsWith(`${this.config.url}/`)) {
|
||||
if (this.utilityService.isUriLocal(src.movedToUri)) {
|
||||
// ローカルユーザーっぽいのにfetchPersonで見つからないということはmovedToUriが間違っている
|
||||
return 'failed: movedTo is local but not found';
|
||||
}
|
||||
|
|
|
@ -5,16 +5,18 @@
|
|||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
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 { IPoll } from '@/models/Poll.js';
|
||||
import type { MiRemoteUser } from '@/models/User.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { isQuestion } from '../type.js';
|
||||
import { getOneApId, isQuestion } from '../type.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { ApLoggerService } from '../ApLoggerService.js';
|
||||
import { ApResolverService } from '../ApResolverService.js';
|
||||
import type { Resolver } from '../ApResolverService.js';
|
||||
import type { IObject, IQuestion } from '../type.js';
|
||||
import type { IObject } from '../type.js';
|
||||
|
||||
@Injectable()
|
||||
export class ApQuestionService {
|
||||
|
@ -24,6 +26,9 @@ export class ApQuestionService {
|
|||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
|
||||
|
@ -32,6 +37,7 @@ export class ApQuestionService {
|
|||
|
||||
private apResolverService: ApResolverService,
|
||||
private apLoggerService: ApLoggerService,
|
||||
private utilityService: UtilityService,
|
||||
) {
|
||||
this.logger = this.apLoggerService.logger;
|
||||
}
|
||||
|
@ -65,12 +71,12 @@ export class ApQuestionService {
|
|||
* @returns true if updated
|
||||
*/
|
||||
@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;
|
||||
if (uri == null) throw new Error('uri is null');
|
||||
|
||||
// URIがこのサーバーを指しているならスキップ
|
||||
if (uri.startsWith(this.config.url + '/')) throw new Error('uri points local');
|
||||
if (this.utilityService.isUriLocal(uri)) throw new Error('uri points local');
|
||||
|
||||
//#region このサーバーに既に登録されているか
|
||||
const note = await this.notesRepository.findOneBy({ uri });
|
||||
|
@ -78,15 +84,26 @@ export class ApQuestionService {
|
|||
|
||||
const poll = await this.pollsRepository.findOneBy({ noteId: note.id });
|
||||
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
|
||||
|
||||
// resolve new Question object
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
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)}`);
|
||||
|
||||
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;
|
||||
if (apChoices == null) throw new Error('invalid apChoices: ' + apChoices);
|
||||
|
@ -96,7 +113,7 @@ export class ApQuestionService {
|
|||
for (const choice of poll.choices) {
|
||||
const oldCount = poll.votes[poll.choices.indexOf(choice)];
|
||||
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) {
|
||||
changed = true;
|
||||
|
|
|
@ -39,6 +39,7 @@ import type {
|
|||
UserSecurityKeysRepository,
|
||||
UsersRepository,
|
||||
} from '@/models/_.js';
|
||||
import { MiBlockingType } from '@/models/Blocking.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
||||
|
@ -76,6 +77,8 @@ export type UserRelation = {
|
|||
hasPendingFollowRequestToYou: boolean
|
||||
isBlocking: boolean
|
||||
isBlocked: boolean
|
||||
isReactionBlocking: boolean
|
||||
isReactionBlocked: boolean
|
||||
isMuted: boolean
|
||||
isRenoteMuted: boolean
|
||||
}
|
||||
|
@ -169,6 +172,8 @@ export class UserEntityService implements OnModuleInit {
|
|||
hasPendingFollowRequestToYou,
|
||||
isBlocking,
|
||||
isBlocked,
|
||||
isReactionBlocking,
|
||||
isReactionBlocked,
|
||||
isMuted,
|
||||
isRenoteMuted,
|
||||
] = await Promise.all([
|
||||
|
@ -198,12 +203,28 @@ export class UserEntityService implements OnModuleInit {
|
|||
where: {
|
||||
blockerId: me,
|
||||
blockeeId: target,
|
||||
blockType: MiBlockingType.User,
|
||||
},
|
||||
}),
|
||||
this.blockingsRepository.exists({
|
||||
where: {
|
||||
blockerId: target,
|
||||
blockeeId: me,
|
||||
blockType: MiBlockingType.User,
|
||||
},
|
||||
}),
|
||||
this.blockingsRepository.exists({
|
||||
where: {
|
||||
blockerId: me,
|
||||
blockeeId: target,
|
||||
blockType: MiBlockingType.Reaction,
|
||||
},
|
||||
}),
|
||||
this.blockingsRepository.exists({
|
||||
where: {
|
||||
blockerId: target,
|
||||
blockeeId: me,
|
||||
blockType: MiBlockingType.Reaction,
|
||||
},
|
||||
}),
|
||||
this.mutingsRepository.exists({
|
||||
|
@ -229,6 +250,8 @@ export class UserEntityService implements OnModuleInit {
|
|||
hasPendingFollowRequestToYou,
|
||||
isBlocking,
|
||||
isBlocked,
|
||||
isReactionBlocking,
|
||||
isReactionBlocked,
|
||||
isMuted,
|
||||
isRenoteMuted,
|
||||
};
|
||||
|
@ -243,6 +266,8 @@ export class UserEntityService implements OnModuleInit {
|
|||
followeesRequests,
|
||||
blockers,
|
||||
blockees,
|
||||
reactionBlockers,
|
||||
reactionBlockees,
|
||||
muters,
|
||||
renoteMuters,
|
||||
] = await Promise.all([
|
||||
|
@ -266,11 +291,25 @@ export class UserEntityService implements OnModuleInit {
|
|||
this.blockingsRepository.createQueryBuilder('b')
|
||||
.select('b.blockeeId')
|
||||
.where('b.blockerId = :me', { me })
|
||||
.andWhere('b.blockType = :type', { type: MiBlockingType.User })
|
||||
.getRawMany<{ b_blockeeId: string }>()
|
||||
.then(it => it.map(it => it.b_blockeeId)),
|
||||
this.blockingsRepository.createQueryBuilder('b')
|
||||
.select('b.blockerId')
|
||||
.where('b.blockeeId = :me', { me })
|
||||
.andWhere('b.blockType = :type', { type: MiBlockingType.User })
|
||||
.getRawMany<{ b_blockerId: string }>()
|
||||
.then(it => it.map(it => it.b_blockerId)),
|
||||
this.blockingsRepository.createQueryBuilder('b')
|
||||
.select('b.blockeeId')
|
||||
.where('b.blockerId = :me', { me })
|
||||
.andWhere('b.blockType = :type', { type: MiBlockingType.Reaction })
|
||||
.getRawMany<{ b_blockeeId: string }>()
|
||||
.then(it => it.map(it => it.b_blockeeId)),
|
||||
this.blockingsRepository.createQueryBuilder('b')
|
||||
.select('b.blockerId')
|
||||
.where('b.blockeeId = :me', { me })
|
||||
.andWhere('b.blockType = :type', { type: MiBlockingType.Reaction })
|
||||
.getRawMany<{ b_blockerId: string }>()
|
||||
.then(it => it.map(it => it.b_blockerId)),
|
||||
this.mutingsRepository.createQueryBuilder('m')
|
||||
|
@ -300,6 +339,8 @@ export class UserEntityService implements OnModuleInit {
|
|||
hasPendingFollowRequestToYou: followeesRequests.includes(target),
|
||||
isBlocking: blockers.includes(target),
|
||||
isBlocked: blockees.includes(target),
|
||||
isReactionBlocking: reactionBlockers.includes(target),
|
||||
isReactionBlocked: reactionBlockees.includes(target),
|
||||
isMuted: muters.includes(target),
|
||||
isRenoteMuted: renoteMuters.includes(target),
|
||||
},
|
||||
|
@ -638,6 +679,8 @@ export class UserEntityService implements OnModuleInit {
|
|||
hasPendingFollowRequestToYou: relation.hasPendingFollowRequestToYou,
|
||||
isBlocking: relation.isBlocking,
|
||||
isBlocked: relation.isBlocked,
|
||||
isReactionBlocking: relation.isReactionBlocking,
|
||||
isReactionBlocked: relation.isReactionBlocked,
|
||||
isMuted: relation.isMuted,
|
||||
isRenoteMuted: relation.isRenoteMuted,
|
||||
notify: relation.following?.notify ?? 'none',
|
||||
|
|
|
@ -7,6 +7,11 @@ import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typ
|
|||
import { id } from './util/id.js';
|
||||
import { MiUser } from './User.js';
|
||||
|
||||
export enum MiBlockingType {
|
||||
User = 'user',
|
||||
Reaction = 'reaction',
|
||||
}
|
||||
|
||||
@Entity('blocking')
|
||||
@Index(['blockerId', 'blockeeId'], { unique: true })
|
||||
export class MiBlocking {
|
||||
|
@ -38,4 +43,11 @@ export class MiBlocking {
|
|||
})
|
||||
@JoinColumn()
|
||||
public blocker: MiUser | null;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
comment: 'Block type.',
|
||||
default: MiBlockingType.User,
|
||||
})
|
||||
public blockType: MiBlockingType;
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ import { MiAntenna } from '@/models/Antenna.js';
|
|||
import { MiApp } from '@/models/App.js';
|
||||
import { MiAvatarDecoration } from '@/models/AvatarDecoration.js';
|
||||
import { MiAuthSession } from '@/models/AuthSession.js';
|
||||
import { MiBlocking } from '@/models/Blocking.js';
|
||||
import { MiBlocking, MiBlockingType } from '@/models/Blocking.js';
|
||||
import { MiChannelFollowing } from '@/models/ChannelFollowing.js';
|
||||
import { MiChannelFavorite } from '@/models/ChannelFavorite.js';
|
||||
import { MiClip } from '@/models/Clip.js';
|
||||
|
@ -136,6 +136,7 @@ export {
|
|||
MiAvatarDecoration,
|
||||
MiAuthSession,
|
||||
MiBlocking,
|
||||
MiBlockingType,
|
||||
MiChannelFollowing,
|
||||
MiChannelFavorite,
|
||||
MiClip,
|
||||
|
|
|
@ -27,5 +27,10 @@ export const packedBlockingSchema = {
|
|||
optional: false, nullable: false,
|
||||
ref: 'UserDetailedNotMe',
|
||||
},
|
||||
blockType: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
enum: ['user', 'reaction'],
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
|
|
@ -416,6 +416,14 @@ export const packedUserDetailedNotMeOnlySchema = {
|
|||
type: 'boolean',
|
||||
nullable: false, optional: true,
|
||||
},
|
||||
isReactionBlocking: {
|
||||
type: 'boolean',
|
||||
nullable: false, optional: true,
|
||||
},
|
||||
isReactionBlocked: {
|
||||
type: 'boolean',
|
||||
nullable: false, optional: true,
|
||||
},
|
||||
isMuted: {
|
||||
type: 'boolean',
|
||||
nullable: false, optional: true,
|
||||
|
|
|
@ -190,6 +190,8 @@ export class InboxProcessorService implements OnApplicationShutdown {
|
|||
if (signerHost !== 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();
|
||||
|
|
|
@ -105,7 +105,7 @@ export class ActivityPubServerService {
|
|||
let signature;
|
||||
|
||||
try {
|
||||
signature = httpSignature.parseRequest(request.raw, { 'headers': [] });
|
||||
signature = httpSignature.parseRequest(request.raw, { 'headers': ['(request-target)', 'host', 'date'], authorizationHeaderName: 'signature' });
|
||||
} catch (e) {
|
||||
reply.code(401);
|
||||
return;
|
||||
|
|
|
@ -231,6 +231,9 @@ import * as ep___i_favorites from './endpoints/i/favorites.js';
|
|||
import * as ep___i_gallery_likes from './endpoints/i/gallery/likes.js';
|
||||
import * as ep___i_gallery_posts from './endpoints/i/gallery/posts.js';
|
||||
import * as ep___i_importBlocking from './endpoints/i/import-blocking.js';
|
||||
import * as ep___blocking_reaction_user_create from './endpoints/blocking-reaction-user/create.js';
|
||||
import * as ep___blocking_reaction_user_delete from './endpoints/blocking-reaction-user/delete.js';
|
||||
import * as ep___blocking_reaction_user_list from './endpoints/blocking-reaction-user/list.js';
|
||||
import * as ep___i_importFollowing from './endpoints/i/import-following.js';
|
||||
import * as ep___i_importMuting from './endpoints/i/import-muting.js';
|
||||
import * as ep___i_importUserLists from './endpoints/i/import-user-lists.js';
|
||||
|
@ -502,6 +505,9 @@ const $auth_session_userkey: Provider = { provide: 'ep:auth/session/userkey', us
|
|||
const $blocking_create: Provider = { provide: 'ep:blocking/create', useClass: ep___blocking_create.default };
|
||||
const $blocking_delete: Provider = { provide: 'ep:blocking/delete', useClass: ep___blocking_delete.default };
|
||||
const $blocking_list: Provider = { provide: 'ep:blocking/list', useClass: ep___blocking_list.default };
|
||||
const $blocking_reaction_user_create: Provider = { provide: 'ep:blocking-reaction-user/create', useClass: ep___blocking_reaction_user_create.default };
|
||||
const $blocking_reaction_user_delete: Provider = { provide: 'ep:blocking-reaction-user/delete', useClass: ep___blocking_reaction_user_delete.default };
|
||||
const $blocking_reaction_user_list: Provider = { provide: 'ep:blocking-reaction-user/list', useClass: ep___blocking_reaction_user_list.default };
|
||||
const $channels_create: Provider = { provide: 'ep:channels/create', useClass: ep___channels_create.default };
|
||||
const $channels_featured: Provider = { provide: 'ep:channels/featured', useClass: ep___channels_featured.default };
|
||||
const $channels_follow: Provider = { provide: 'ep:channels/follow', useClass: ep___channels_follow.default };
|
||||
|
@ -894,6 +900,9 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
|||
$blocking_create,
|
||||
$blocking_delete,
|
||||
$blocking_list,
|
||||
$blocking_reaction_user_create,
|
||||
$blocking_reaction_user_delete,
|
||||
$blocking_reaction_user_list,
|
||||
$channels_create,
|
||||
$channels_featured,
|
||||
$channels_follow,
|
||||
|
|
|
@ -120,6 +120,9 @@ import * as ep___auth_session_userkey from './endpoints/auth/session/userkey.js'
|
|||
import * as ep___blocking_create from './endpoints/blocking/create.js';
|
||||
import * as ep___blocking_delete from './endpoints/blocking/delete.js';
|
||||
import * as ep___blocking_list from './endpoints/blocking/list.js';
|
||||
import * as ep___blocking_reaction_user_create from './endpoints/blocking-reaction-user/create.js';
|
||||
import * as ep___blocking_reaction_user_delete from './endpoints/blocking-reaction-user/delete.js';
|
||||
import * as ep___blocking_reaction_user_list from './endpoints/blocking-reaction-user/list.js';
|
||||
import * as ep___channels_create from './endpoints/channels/create.js';
|
||||
import * as ep___channels_featured from './endpoints/channels/featured.js';
|
||||
import * as ep___channels_follow from './endpoints/channels/follow.js';
|
||||
|
@ -506,6 +509,9 @@ const eps = [
|
|||
['blocking/create', ep___blocking_create],
|
||||
['blocking/delete', ep___blocking_delete],
|
||||
['blocking/list', ep___blocking_list],
|
||||
['blocking-reaction-user/create', ep___blocking_reaction_user_create],
|
||||
['blocking-reaction-user/delete', ep___blocking_reaction_user_delete],
|
||||
['blocking-reaction-user/list', ep___blocking_reaction_user_list],
|
||||
['channels/create', ep___channels_create],
|
||||
['channels/featured', ep___channels_featured],
|
||||
['channels/follow', ep___channels_follow],
|
||||
|
|
|
@ -11,6 +11,7 @@ import { ApResolverService } from '@/core/activitypub/ApResolverService.js';
|
|||
export const meta = {
|
||||
tags: ['federation'],
|
||||
|
||||
requireAdmin: true,
|
||||
requireCredential: true,
|
||||
kind: 'read:federation',
|
||||
|
||||
|
|
|
@ -118,6 +118,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
]));
|
||||
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 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(
|
||||
me,
|
||||
isActor(object) ? await this.apPersonService.createPerson(getApId(object)) : null,
|
||||
isPost(object) ? await this.apNoteService.createNote(getApId(object), undefined, true) : null,
|
||||
isPost(object) ? await this.apNoteService.createNote(getApId(object), undefined, resolver) : null,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import ms from 'ms';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { BlockingsRepository, UsersRepository } from '@/models/_.js';
|
||||
import { MiBlockingType } from '@/models/Blocking.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { GetterService } from '@/server/api/GetterService.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account'],
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
max: 20,
|
||||
},
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:blocks',
|
||||
|
||||
errors: {
|
||||
noSuchUser: {
|
||||
message: 'No such user.',
|
||||
code: 'NO_SUCH_USER',
|
||||
id: '7cc4f851-e2f1-4621-9633-ec9e1d00c01e',
|
||||
},
|
||||
|
||||
blockeeIsYourself: {
|
||||
message: 'Blockee is yourself.',
|
||||
code: 'BLOCKEE_IS_YOURSELF',
|
||||
id: '88b19138-f28d-42c0-8499-6a31bbd0fdc6',
|
||||
},
|
||||
|
||||
alreadyBlocking: {
|
||||
message: 'You are already blocking that user.',
|
||||
code: 'ALREADY_BLOCKING',
|
||||
id: '787fed64-acb9-464a-82eb-afbd745b9614',
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'UserDetailedNotMe',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['userId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.blockingsRepository)
|
||||
private blockingsRepository: BlockingsRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private getterService: GetterService,
|
||||
private userBlockingService: UserBlockingService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const blocker = await this.usersRepository.findOneByOrFail({ id: me.id });
|
||||
|
||||
// 自分自身
|
||||
if (me.id === ps.userId) {
|
||||
throw new ApiError(meta.errors.blockeeIsYourself);
|
||||
}
|
||||
|
||||
// Get blockee
|
||||
const blockee = await this.getterService.getUser(ps.userId).catch(err => {
|
||||
if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
||||
throw err;
|
||||
});
|
||||
|
||||
// Check if already blocking
|
||||
const exist = await this.blockingsRepository.exists({
|
||||
where: {
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
blockType: MiBlockingType.Reaction,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist) {
|
||||
throw new ApiError(meta.errors.alreadyBlocking);
|
||||
}
|
||||
|
||||
await this.userBlockingService.reactionBlock(blocker, blockee);
|
||||
|
||||
return await this.userEntityService.pack(blockee.id, blocker, {
|
||||
schema: 'UserDetailedNotMe',
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import ms from 'ms';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { UsersRepository, BlockingsRepository } from '@/models/_.js';
|
||||
import { MiBlockingType } from '@/models/Blocking.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { GetterService } from '@/server/api/GetterService.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account'],
|
||||
|
||||
limit: {
|
||||
duration: ms('1hour'),
|
||||
max: 100,
|
||||
},
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:blocks',
|
||||
|
||||
errors: {
|
||||
noSuchUser: {
|
||||
message: 'No such user.',
|
||||
code: 'NO_SUCH_USER',
|
||||
id: '8621d8bf-c358-4303-a066-5ea78610eb3f',
|
||||
},
|
||||
|
||||
blockeeIsYourself: {
|
||||
message: 'Blockee is yourself.',
|
||||
code: 'BLOCKEE_IS_YOURSELF',
|
||||
id: '06f6fac6-524b-473c-a354-e97a40ae6eac',
|
||||
},
|
||||
|
||||
notBlocking: {
|
||||
message: 'You are not blocking that user.',
|
||||
code: 'NOT_BLOCKING',
|
||||
id: '291b2efa-60c6-45c0-9f6a-045c8f9b02cd',
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'UserDetailedNotMe',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['userId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.blockingsRepository)
|
||||
private blockingsRepository: BlockingsRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private getterService: GetterService,
|
||||
private userBlockingService: UserBlockingService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const blocker = await this.usersRepository.findOneByOrFail({ id: me.id });
|
||||
|
||||
// Check if the blockee is yourself
|
||||
if (me.id === ps.userId) {
|
||||
throw new ApiError(meta.errors.blockeeIsYourself);
|
||||
}
|
||||
|
||||
// Get blockee
|
||||
const blockee = await this.getterService.getUser(ps.userId).catch(err => {
|
||||
if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
||||
throw err;
|
||||
});
|
||||
|
||||
// Check not blocking
|
||||
const exist = await this.blockingsRepository.exists({
|
||||
where: {
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
blockType: MiBlockingType.Reaction,
|
||||
},
|
||||
});
|
||||
|
||||
if (!exist) {
|
||||
throw new ApiError(meta.errors.notBlocking);
|
||||
}
|
||||
|
||||
// Delete blocking
|
||||
await this.userBlockingService.reactionUnblock(blocker, blockee);
|
||||
|
||||
return await this.userEntityService.pack(blockee.id, blocker, {
|
||||
schema: 'UserDetailedNotMe',
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { BlockingsRepository } from '@/models/_.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { BlockingEntityService } from '@/core/entities/BlockingEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { MiBlockingType } from '@/models/Blocking.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:blocks',
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'Blocking',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.blockingsRepository)
|
||||
private blockingsRepository: BlockingsRepository,
|
||||
|
||||
private blockingEntityService: BlockingEntityService,
|
||||
private queryService: QueryService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.queryService.makePaginationQuery(this.blockingsRepository.createQueryBuilder('blocking'), ps.sinceId, ps.untilId)
|
||||
.andWhere('blocking.blockerId = :meId', { meId: me.id })
|
||||
.andWhere('blocking.blockType = :blockType', { blockType: MiBlockingType.Reaction });
|
||||
|
||||
const blockings = await query
|
||||
.limit(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return await this.blockingEntityService.packMany(blockings, me);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -6,7 +6,8 @@
|
|||
import ms from 'ms';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { UsersRepository, BlockingsRepository } from '@/models/_.js';
|
||||
import type { BlockingsRepository, UsersRepository } from '@/models/_.js';
|
||||
import { MiBlockingType } from '@/models/Blocking.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
@ -92,6 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
where: {
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
blockType: MiBlockingType.User,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
import ms from 'ms';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { UsersRepository, BlockingsRepository } from '@/models/_.js';
|
||||
import type { BlockingsRepository, UsersRepository } from '@/models/_.js';
|
||||
import { MiBlockingType } from '@/models/Blocking.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
@ -92,6 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
where: {
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
blockType: MiBlockingType.User,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import type { BlockingsRepository } from '@/models/_.js';
|
|||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { BlockingEntityService } from '@/core/entities/BlockingEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { MiBlockingType } from '@/models/Blocking.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account'],
|
||||
|
@ -49,7 +50,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.queryService.makePaginationQuery(this.blockingsRepository.createQueryBuilder('blocking'), ps.sinceId, ps.untilId)
|
||||
.andWhere('blocking.blockerId = :meId', { meId: me.id });
|
||||
.andWhere('blocking.blockerId = :meId', { meId: me.id })
|
||||
.andWhere('blocking.blockType = :blockType', { blockType: MiBlockingType.User });
|
||||
|
||||
const blockings = await query
|
||||
.limit(ps.limit)
|
||||
|
|
|
@ -559,7 +559,7 @@ export class ClientServerService {
|
|||
}
|
||||
});
|
||||
|
||||
//#region SSR (for crawlers)
|
||||
//#region SSR
|
||||
// User
|
||||
fastify.get<{ Params: { user: string; sub?: string; } }>('/@:user/:sub?', async (request, reply) => {
|
||||
const { username, host } = Acct.parse(request.params.user);
|
||||
|
@ -584,11 +584,17 @@ export class ClientServerService {
|
|||
reply.header('X-Robots-Tag', 'noimageai');
|
||||
reply.header('X-Robots-Tag', 'noai');
|
||||
}
|
||||
|
||||
const _user = await this.userEntityService.pack(user);
|
||||
|
||||
return await reply.view('user', {
|
||||
user, profile, me,
|
||||
avatarUrl: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user),
|
||||
sub: request.params.sub,
|
||||
...await this.generateCommonPugData(this.meta),
|
||||
clientCtx: htmlSafeJsonStringify({
|
||||
user: _user,
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
// リモートユーザーなので
|
||||
|
@ -641,6 +647,9 @@ export class ClientServerService {
|
|||
// TODO: Let locale changeable by instance setting
|
||||
summary: getNoteSummary(_note),
|
||||
...await this.generateCommonPugData(this.meta),
|
||||
clientCtx: htmlSafeJsonStringify({
|
||||
note: _note,
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
return await renderBase(reply);
|
||||
|
@ -729,6 +738,9 @@ export class ClientServerService {
|
|||
profile,
|
||||
avatarUrl: _clip.user.avatarUrl,
|
||||
...await this.generateCommonPugData(this.meta),
|
||||
clientCtx: htmlSafeJsonStringify({
|
||||
clip: _clip,
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
return await renderBase(reply);
|
||||
|
|
|
@ -145,6 +145,6 @@ export class UrlPreviewService {
|
|||
contentLengthRequired: meta.urlPreviewRequireContentLength,
|
||||
});
|
||||
|
||||
return this.httpRequestService.getJson<SummalyResult>(`${proxy}?${queryStr}`);
|
||||
return this.httpRequestService.getJson<SummalyResult>(`${proxy}?${queryStr}`, 'application/json, */*', undefined, true);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -74,6 +74,9 @@ html
|
|||
script(type='application/json' id='misskey_meta' data-generated-at=now)
|
||||
!= metaJson
|
||||
|
||||
script(type='application/json' id='misskey_clientCtx' data-generated-at=now)
|
||||
!= clientCtx
|
||||
|
||||
script
|
||||
include ../boot.js
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import { secureRndstr } from '@/misc/secure-rndstr.js';
|
|||
import { genAidx } from '@/misc/id/aidx.js';
|
||||
import {
|
||||
BlockingsRepository,
|
||||
FollowingsRepository, FollowRequestsRepository,
|
||||
FollowingsRepository, FollowRequestsRepository, MiBlockingType,
|
||||
MiUserProfile, MutingsRepository, RenoteMutingsRepository,
|
||||
UserMemoRepository,
|
||||
UserProfilesRepository,
|
||||
|
@ -115,6 +115,16 @@ describe('UserEntityService', () => {
|
|||
id: genAidx(Date.now()),
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
blockType: MiBlockingType.User,
|
||||
});
|
||||
}
|
||||
|
||||
async function blockReaction(blocker: MiUser, blockee: MiUser) {
|
||||
await blockingRepository.insert({
|
||||
id: genAidx(Date.now()),
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
blockType: MiBlockingType.Reaction,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -260,6 +270,8 @@ describe('UserEntityService', () => {
|
|||
expect(actual.hasPendingFollowRequestToYou).toBe(false);
|
||||
expect(actual.isBlocking).toBe(false);
|
||||
expect(actual.isBlocked).toBe(false);
|
||||
expect(actual.isReactionBlocking).toBe(false);
|
||||
expect(actual.isReactionBlocked).toBe(false);
|
||||
expect(actual.isMuted).toBe(false);
|
||||
expect(actual.isRenoteMuted).toBe(false);
|
||||
}
|
||||
|
@ -275,6 +287,8 @@ describe('UserEntityService', () => {
|
|||
expect(actual.hasPendingFollowRequestToYou).toBe(false);
|
||||
expect(actual.isBlocking).toBe(false);
|
||||
expect(actual.isBlocked).toBe(false);
|
||||
expect(actual.isReactionBlocking).toBe(false);
|
||||
expect(actual.isReactionBlocked).toBe(false);
|
||||
expect(actual.isMuted).toBe(false);
|
||||
expect(actual.isRenoteMuted).toBe(false);
|
||||
}
|
||||
|
@ -290,6 +304,8 @@ describe('UserEntityService', () => {
|
|||
expect(actual.hasPendingFollowRequestToYou).toBe(false);
|
||||
expect(actual.isBlocking).toBe(false);
|
||||
expect(actual.isBlocked).toBe(false);
|
||||
expect(actual.isReactionBlocking).toBe(false);
|
||||
expect(actual.isReactionBlocked).toBe(false);
|
||||
expect(actual.isMuted).toBe(false);
|
||||
expect(actual.isRenoteMuted).toBe(false);
|
||||
}
|
||||
|
@ -305,6 +321,8 @@ describe('UserEntityService', () => {
|
|||
expect(actual.hasPendingFollowRequestToYou).toBe(true);
|
||||
expect(actual.isBlocking).toBe(false);
|
||||
expect(actual.isBlocked).toBe(false);
|
||||
expect(actual.isReactionBlocking).toBe(false);
|
||||
expect(actual.isReactionBlocked).toBe(false);
|
||||
expect(actual.isMuted).toBe(false);
|
||||
expect(actual.isRenoteMuted).toBe(false);
|
||||
}
|
||||
|
@ -320,6 +338,8 @@ describe('UserEntityService', () => {
|
|||
expect(actual.hasPendingFollowRequestToYou).toBe(false);
|
||||
expect(actual.isBlocking).toBe(true);
|
||||
expect(actual.isBlocked).toBe(false);
|
||||
expect(actual.isReactionBlocking).toBe(false);
|
||||
expect(actual.isReactionBlocked).toBe(false);
|
||||
expect(actual.isMuted).toBe(false);
|
||||
expect(actual.isRenoteMuted).toBe(false);
|
||||
}
|
||||
|
@ -339,6 +359,41 @@ describe('UserEntityService', () => {
|
|||
expect(actual.isRenoteMuted).toBe(false);
|
||||
}
|
||||
|
||||
|
||||
// meがリアクションをブロックしてる人たち
|
||||
const reactionBlockingYou = await Promise.all(randomIntRange().map(() => createUser()));
|
||||
for (const who of reactionBlockingYou) {
|
||||
await blockReaction(me, who);
|
||||
const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any;
|
||||
expect(actual.isFollowing).toBe(false);
|
||||
expect(actual.isFollowed).toBe(false);
|
||||
expect(actual.hasPendingFollowRequestFromYou).toBe(false);
|
||||
expect(actual.hasPendingFollowRequestToYou).toBe(false);
|
||||
expect(actual.isBlocking).toBe(false);
|
||||
expect(actual.isBlocked).toBe(false);
|
||||
expect(actual.isReactionBlocking).toBe(true);
|
||||
expect(actual.isReactionBlocked).toBe(false);
|
||||
expect(actual.isMuted).toBe(false);
|
||||
expect(actual.isRenoteMuted).toBe(false);
|
||||
}
|
||||
|
||||
// meのリアクションをブロックしてる人たち
|
||||
const reactionBlockingMe = await Promise.all(randomIntRange().map(() => createUser()));
|
||||
for (const who of reactionBlockingMe) {
|
||||
await blockReaction(who, me);
|
||||
const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any;
|
||||
expect(actual.isFollowing).toBe(false);
|
||||
expect(actual.isFollowed).toBe(false);
|
||||
expect(actual.hasPendingFollowRequestFromYou).toBe(false);
|
||||
expect(actual.hasPendingFollowRequestToYou).toBe(false);
|
||||
expect(actual.isBlocking).toBe(false);
|
||||
expect(actual.isBlocked).toBe(false);
|
||||
expect(actual.isReactionBlocking).toBe(false);
|
||||
expect(actual.isReactionBlocked).toBe(true);
|
||||
expect(actual.isMuted).toBe(false);
|
||||
expect(actual.isRenoteMuted).toBe(false);
|
||||
}
|
||||
|
||||
// meがミュートしてる人たち
|
||||
const muters = await Promise.all(randomIntRange().map(() => createUser()));
|
||||
for (const who of muters) {
|
||||
|
@ -350,6 +405,8 @@ describe('UserEntityService', () => {
|
|||
expect(actual.hasPendingFollowRequestToYou).toBe(false);
|
||||
expect(actual.isBlocking).toBe(false);
|
||||
expect(actual.isBlocked).toBe(false);
|
||||
expect(actual.isReactionBlocking).toBe(false);
|
||||
expect(actual.isReactionBlocked).toBe(false);
|
||||
expect(actual.isMuted).toBe(true);
|
||||
expect(actual.isRenoteMuted).toBe(false);
|
||||
}
|
||||
|
@ -365,6 +422,8 @@ describe('UserEntityService', () => {
|
|||
expect(actual.hasPendingFollowRequestToYou).toBe(false);
|
||||
expect(actual.isBlocking).toBe(false);
|
||||
expect(actual.isBlocked).toBe(false);
|
||||
expect(actual.isReactionBlocking).toBe(false);
|
||||
expect(actual.isReactionBlocked).toBe(false);
|
||||
expect(actual.isMuted).toBe(false);
|
||||
expect(actual.isRenoteMuted).toBe(true);
|
||||
}
|
||||
|
|
|
@ -33,25 +33,28 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<script lang="ts" setup>
|
||||
import { computed, watch, provide, ref } from 'vue';
|
||||
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 { $i } from '@/account.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { url } from '@@/js/config.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { clipsCache } from '@/cache.js';
|
||||
import { isSupportShare } from '@/scripts/navigator.js';
|
||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.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<{
|
||||
clipId: string,
|
||||
}>();
|
||||
|
||||
const clip = ref<Misskey.entities.Clip | null>(null);
|
||||
const clip = ref<Misskey.entities.Clip | null>(CTX_CLIP);
|
||||
const favorited = ref(false);
|
||||
const pagination = {
|
||||
endpoint: 'clips/notes' as const,
|
||||
|
@ -64,6 +67,11 @@ const pagination = {
|
|||
const isOwned = computed<boolean | null>(() => $i && clip.value && ($i.id === clip.value.userId));
|
||||
|
||||
watch(() => props.clipId, async () => {
|
||||
if (CTX_CLIP && CTX_CLIP.id === props.clipId) {
|
||||
clip.value = CTX_CLIP;
|
||||
return;
|
||||
}
|
||||
|
||||
clip.value = await misskeyApi('clips/show', {
|
||||
clipId: props.clipId,
|
||||
});
|
||||
|
|
|
@ -62,13 +62,16 @@ import { dateString } from '@/filters/date.js';
|
|||
import MkClipPreview from '@/components/MkClipPreview.vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { pleaseLogin } from '@/scripts/please-login.js';
|
||||
import { getServerContext } from '@/server-context.js';
|
||||
|
||||
const CTX_NOTE = getServerContext('note');
|
||||
|
||||
const props = defineProps<{
|
||||
noteId: 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 showPrev = ref<'user' | 'channel' | false>(false);
|
||||
const showNext = ref<'user' | 'channel' | false>(false);
|
||||
|
@ -116,6 +119,12 @@ function fetchNote() {
|
|||
showPrev.value = false;
|
||||
showNext.value = false;
|
||||
note.value = null;
|
||||
|
||||
if (CTX_NOTE && CTX_NOTE.id === props.noteId) {
|
||||
note.value = CTX_NOTE;
|
||||
return;
|
||||
}
|
||||
|
||||
misskeyApi('notes/show', {
|
||||
noteId: props.noteId,
|
||||
}).then(res => {
|
||||
|
|
|
@ -122,6 +122,39 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
</MkPagination>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder>
|
||||
<template #icon><i class="ti ti-ban"></i></template>
|
||||
<template #label>{{ i18n.ts.reactionBlockedUsers }}</template>
|
||||
|
||||
<MkPagination :pagination="blockingReactionUserPagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img :src="infoImageUrl" class="_ghost"/>
|
||||
<div>{{ i18n.ts.noUsers }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #default="{ items }">
|
||||
<div class="_gaps_s">
|
||||
<div v-for="item in items" :key="item.blockee.id" :class="[$style.userItem, { [$style.userItemOpend]: expandedBlockItems.includes(item.id) }]">
|
||||
<div :class="$style.userItemMain">
|
||||
<MkA :class="$style.userItemMainBody" :to="userPage(item.blockee)">
|
||||
<MkUserCardMini :user="item.blockee"/>
|
||||
</MkA>
|
||||
<button class="_button" :class="$style.userToggle" @click="toggleBlockItem(item)"><i :class="$style.chevron" class="ti ti-chevron-down"></i></button>
|
||||
<button class="_button" :class="$style.remove" @click="unblockReactionUser(item.blockee, $event)"><i class="ti ti-x"></i></button>
|
||||
</div>
|
||||
<div v-if="expandedBlockItems.includes(item.id)" :class="$style.userItemSub">
|
||||
<div>Blocked at: <MkTime :time="item.createdAt" mode="detail"/></div>
|
||||
<div v-if="item.expiresAt">Period: {{ new Date(item.expiresAt).toLocaleString() }}</div>
|
||||
<div v-else>Period: {{ i18n.ts.indefinitely }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</MkFolder>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -157,6 +190,11 @@ const blockingPagination = {
|
|||
limit: 10,
|
||||
};
|
||||
|
||||
const blockingReactionUserPagination = {
|
||||
endpoint: 'blocking-reaction-user/list' as const,
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
const expandedRenoteMuteItems = ref([]);
|
||||
const expandedMuteItems = ref([]);
|
||||
const expandedBlockItems = ref([]);
|
||||
|
@ -194,6 +232,16 @@ async function unblock(user, ev) {
|
|||
}], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
async function unblockReactionUser(user, ev) {
|
||||
os.popupMenu([{
|
||||
text: i18n.ts.unblock,
|
||||
icon: 'ti ti-x',
|
||||
action: async () => {
|
||||
await os.apiWithDialog('blocking-reaction-user/delete', { userId: user.id });
|
||||
},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
async function toggleRenoteMuteItem(item) {
|
||||
if (expandedRenoteMuteItems.value.includes(item.id)) {
|
||||
expandedRenoteMuteItems.value = expandedRenoteMuteItems.value.filter(x => x !== item.id);
|
||||
|
|
|
@ -39,6 +39,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { $i } from '@/account.js';
|
||||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||
import { getServerContext } from '@/server-context.js';
|
||||
|
||||
const XHome = defineAsyncComponent(() => import('./home.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 XRaw = defineAsyncComponent(() => import('./raw.vue'));
|
||||
|
||||
const CTX_USER = getServerContext('user');
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
acct: string;
|
||||
page?: string;
|
||||
|
@ -61,13 +64,24 @@ const props = withDefaults(defineProps<{
|
|||
|
||||
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);
|
||||
|
||||
function fetchUser(): void {
|
||||
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;
|
||||
misskeyApi('users/show', Misskey.acct.parse(props.acct)).then(u => {
|
||||
misskeyApi('users/show', {
|
||||
username,
|
||||
host,
|
||||
}).then(u => {
|
||||
user.value = u;
|
||||
}).catch(err => {
|
||||
error.value = err;
|
||||
|
|
|
@ -84,6 +84,16 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
|
|||
});
|
||||
}
|
||||
|
||||
async function toggleReactionBlock() {
|
||||
if (!await getConfirmed(user.isReactionBlocking ? i18n.ts.unblockReactionUserConfirm : i18n.ts.blockReactionUserConfirm)) return;
|
||||
|
||||
os.apiWithDialog(user.isReactionBlocking ? 'blocking-reaction-user/delete' : 'blocking-reaction-user/create', {
|
||||
userId: user.id,
|
||||
}).then(() => {
|
||||
user.isReactionBlocking = !user.isReactionBlocking;
|
||||
});
|
||||
}
|
||||
|
||||
async function toggleNotify() {
|
||||
os.apiWithDialog('following/update', {
|
||||
userId: user.id,
|
||||
|
@ -373,6 +383,10 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
|
|||
icon: 'ti ti-ban',
|
||||
text: user.isBlocking ? i18n.ts.unblock : i18n.ts.block,
|
||||
action: toggleBlock,
|
||||
}, {
|
||||
icon: 'ti ti-ban',
|
||||
text: user.isReactionBlocking ? i18n.ts.unblockReactionUser : i18n.ts.blockReactionUser,
|
||||
action: toggleReactionBlock,
|
||||
});
|
||||
|
||||
if (user.isFollowed) {
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -551,6 +551,24 @@ type BlockingListRequest = operations['blocking___list']['requestBody']['content
|
|||
// @public (undocumented)
|
||||
type BlockingListResponse = operations['blocking___list']['responses']['200']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type BlockingReactionUserCreateRequest = operations['blocking-reaction-user___create']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type BlockingReactionUserCreateResponse = operations['blocking-reaction-user___create']['responses']['200']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type BlockingReactionUserDeleteRequest = operations['blocking-reaction-user___delete']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type BlockingReactionUserDeleteResponse = operations['blocking-reaction-user___delete']['responses']['200']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type BlockingReactionUserListRequest = operations['blocking-reaction-user___list']['requestBody']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type BlockingReactionUserListResponse = operations['blocking-reaction-user___list']['responses']['200']['content']['application/json'];
|
||||
|
||||
// @public (undocumented)
|
||||
type BubbleGameRankingRequest = operations['bubble-game___ranking']['requestBody']['content']['application/json'];
|
||||
|
||||
|
@ -1381,6 +1399,12 @@ declare namespace entities {
|
|||
BlockingDeleteResponse,
|
||||
BlockingListRequest,
|
||||
BlockingListResponse,
|
||||
BlockingReactionUserCreateRequest,
|
||||
BlockingReactionUserCreateResponse,
|
||||
BlockingReactionUserDeleteRequest,
|
||||
BlockingReactionUserDeleteResponse,
|
||||
BlockingReactionUserListRequest,
|
||||
BlockingReactionUserListResponse,
|
||||
ChannelsCreateRequest,
|
||||
ChannelsCreateResponse,
|
||||
ChannelsFeaturedResponse,
|
||||
|
|
|
@ -1204,6 +1204,39 @@ declare module '../api.js' {
|
|||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:blocks*
|
||||
*/
|
||||
request<E extends 'blocking-reaction-user/create', P extends Endpoints[E]['req']>(
|
||||
endpoint: E,
|
||||
params: P,
|
||||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:blocks*
|
||||
*/
|
||||
request<E extends 'blocking-reaction-user/delete', P extends Endpoints[E]['req']>(
|
||||
endpoint: E,
|
||||
params: P,
|
||||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *read:blocks*
|
||||
*/
|
||||
request<E extends 'blocking-reaction-user/list', P extends Endpoints[E]['req']>(
|
||||
endpoint: E,
|
||||
params: P,
|
||||
credential?: string | null,
|
||||
): Promise<SwitchCaseResponseType<E, P>>;
|
||||
|
||||
/**
|
||||
* No description provided.
|
||||
*
|
||||
|
|
|
@ -156,6 +156,12 @@ import type {
|
|||
BlockingDeleteResponse,
|
||||
BlockingListRequest,
|
||||
BlockingListResponse,
|
||||
BlockingReactionUserCreateRequest,
|
||||
BlockingReactionUserCreateResponse,
|
||||
BlockingReactionUserDeleteRequest,
|
||||
BlockingReactionUserDeleteResponse,
|
||||
BlockingReactionUserListRequest,
|
||||
BlockingReactionUserListResponse,
|
||||
ChannelsCreateRequest,
|
||||
ChannelsCreateResponse,
|
||||
ChannelsFeaturedResponse,
|
||||
|
@ -690,6 +696,9 @@ export type Endpoints = {
|
|||
'blocking/create': { req: BlockingCreateRequest; res: BlockingCreateResponse };
|
||||
'blocking/delete': { req: BlockingDeleteRequest; res: BlockingDeleteResponse };
|
||||
'blocking/list': { req: BlockingListRequest; res: BlockingListResponse };
|
||||
'blocking-reaction-user/create': { req: BlockingReactionUserCreateRequest; res: BlockingReactionUserCreateResponse };
|
||||
'blocking-reaction-user/delete': { req: BlockingReactionUserDeleteRequest; res: BlockingReactionUserDeleteResponse };
|
||||
'blocking-reaction-user/list': { req: BlockingReactionUserListRequest; res: BlockingReactionUserListResponse };
|
||||
'channels/create': { req: ChannelsCreateRequest; res: ChannelsCreateResponse };
|
||||
'channels/featured': { req: EmptyRequest; res: ChannelsFeaturedResponse };
|
||||
'channels/follow': { req: ChannelsFollowRequest; res: EmptyResponse };
|
||||
|
|
|
@ -159,6 +159,12 @@ export type BlockingDeleteRequest = operations['blocking___delete']['requestBody
|
|||
export type BlockingDeleteResponse = operations['blocking___delete']['responses']['200']['content']['application/json'];
|
||||
export type BlockingListRequest = operations['blocking___list']['requestBody']['content']['application/json'];
|
||||
export type BlockingListResponse = operations['blocking___list']['responses']['200']['content']['application/json'];
|
||||
export type BlockingReactionUserCreateRequest = operations['blocking-reaction-user___create']['requestBody']['content']['application/json'];
|
||||
export type BlockingReactionUserCreateResponse = operations['blocking-reaction-user___create']['responses']['200']['content']['application/json'];
|
||||
export type BlockingReactionUserDeleteRequest = operations['blocking-reaction-user___delete']['requestBody']['content']['application/json'];
|
||||
export type BlockingReactionUserDeleteResponse = operations['blocking-reaction-user___delete']['responses']['200']['content']['application/json'];
|
||||
export type BlockingReactionUserListRequest = operations['blocking-reaction-user___list']['requestBody']['content']['application/json'];
|
||||
export type BlockingReactionUserListResponse = operations['blocking-reaction-user___list']['responses']['200']['content']['application/json'];
|
||||
export type ChannelsCreateRequest = operations['channels___create']['requestBody']['content']['application/json'];
|
||||
export type ChannelsCreateResponse = operations['channels___create']['responses']['200']['content']['application/json'];
|
||||
export type ChannelsFeaturedResponse = operations['channels___featured']['responses']['200']['content']['application/json'];
|
||||
|
|
|
@ -997,6 +997,33 @@ export type paths = {
|
|||
*/
|
||||
post: operations['blocking___list'];
|
||||
};
|
||||
'/blocking-reaction-user/create': {
|
||||
/**
|
||||
* blocking-reaction-user/create
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:blocks*
|
||||
*/
|
||||
post: operations['blocking-reaction-user___create'];
|
||||
};
|
||||
'/blocking-reaction-user/delete': {
|
||||
/**
|
||||
* blocking-reaction-user/delete
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:blocks*
|
||||
*/
|
||||
post: operations['blocking-reaction-user___delete'];
|
||||
};
|
||||
'/blocking-reaction-user/list': {
|
||||
/**
|
||||
* blocking-reaction-user/list
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *read:blocks*
|
||||
*/
|
||||
post: operations['blocking-reaction-user___list'];
|
||||
};
|
||||
'/channels/create': {
|
||||
/**
|
||||
* channels/create
|
||||
|
@ -3825,6 +3852,8 @@ export type components = {
|
|||
hasPendingFollowRequestToYou?: boolean;
|
||||
isBlocking?: boolean;
|
||||
isBlocked?: boolean;
|
||||
isReactionBlocking?: boolean;
|
||||
isReactionBlocked?: boolean;
|
||||
isMuted?: boolean;
|
||||
isRenoteMuted?: boolean;
|
||||
/** @enum {string} */
|
||||
|
@ -4486,6 +4515,8 @@ export type components = {
|
|||
/** Format: id */
|
||||
blockeeId: string;
|
||||
blockee: components['schemas']['UserDetailedNotMe'];
|
||||
/** @enum {string} */
|
||||
blockType: 'user' | 'reaction';
|
||||
};
|
||||
Hashtag: {
|
||||
/** @example misskey */
|
||||
|
@ -11705,6 +11736,184 @@ export type operations = {
|
|||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* blocking-reaction-user/create
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:blocks*
|
||||
*/
|
||||
'blocking-reaction-user___create': {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
/** Format: misskey:id */
|
||||
userId: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK (with results) */
|
||||
200: {
|
||||
content: {
|
||||
'application/json': components['schemas']['UserDetailedNotMe'];
|
||||
};
|
||||
};
|
||||
/** @description Client error */
|
||||
400: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Authentication error */
|
||||
401: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Forbidden error */
|
||||
403: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description I'm Ai */
|
||||
418: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description To many requests */
|
||||
429: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Internal server error */
|
||||
500: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* blocking-reaction-user/delete
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *write:blocks*
|
||||
*/
|
||||
'blocking-reaction-user___delete': {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
/** Format: misskey:id */
|
||||
userId: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK (with results) */
|
||||
200: {
|
||||
content: {
|
||||
'application/json': components['schemas']['UserDetailedNotMe'];
|
||||
};
|
||||
};
|
||||
/** @description Client error */
|
||||
400: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Authentication error */
|
||||
401: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Forbidden error */
|
||||
403: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description I'm Ai */
|
||||
418: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description To many requests */
|
||||
429: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Internal server error */
|
||||
500: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* blocking-reaction-user/list
|
||||
* @description No description provided.
|
||||
*
|
||||
* **Credential required**: *Yes* / **Permission**: *read:blocks*
|
||||
*/
|
||||
'blocking-reaction-user___list': {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
/** @default 30 */
|
||||
limit?: number;
|
||||
/** Format: misskey:id */
|
||||
sinceId?: string;
|
||||
/** Format: misskey:id */
|
||||
untilId?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK (with results) */
|
||||
200: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Blocking'][];
|
||||
};
|
||||
};
|
||||
/** @description Client error */
|
||||
400: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Authentication error */
|
||||
401: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Forbidden error */
|
||||
403: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description I'm Ai */
|
||||
418: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
/** @description Internal server error */
|
||||
500: {
|
||||
content: {
|
||||
'application/json': components['schemas']['Error'];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* channels/create
|
||||
* @description No description provided.
|
||||
|
|
Loading…
Reference in New Issue