Revert "リモートのクリップを見る機能 (#179)"

This reverts commit e1e0146d3c.
This commit is contained in:
hijiki 2024-10-25 13:14:58 +09:00
parent e1e0146d3c
commit 5d21dfa6ec
12 changed files with 42 additions and 636 deletions

View File

@ -163,16 +163,6 @@ redis:
# #prefix: example-prefix # #prefix: example-prefix
# #db: 1 # #db: 1
#redisForRemoteClips:
# host: localhost
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
# # You can specify more ioredis options...
# #username: example-username
# ┌───────────────────────────┐ # ┌───────────────────────────┐
#───┘ MeiliSearch configuration └───────────────────────────── #───┘ MeiliSearch configuration └─────────────────────────────

View File

@ -172,16 +172,6 @@ redis:
# # You can specify more ioredis options... # # You can specify more ioredis options...
# #username: example-username # #username: example-username
#redisForRemoteClips:
# host: localhost
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
# # You can specify more ioredis options...
# #username: example-username
# ┌───────────────────────────┐ # ┌───────────────────────────┐
#───┘ MeiliSearch configuration └───────────────────────────── #───┘ MeiliSearch configuration └─────────────────────────────

View File

@ -1,97 +0,0 @@
<!--
## yojo-x.x.x (unreleased)
### Release Date
### General
-
### Client
-
### Server
-
### Misc
-->
## 0.3.0 (unreleased)
### Release Date
### General
- Feat: リモートユーザーのクリップが閲覧できるように
- お気に入りは未実装です
- ログインが必要な投稿は見れません
### Client
-
### Server
- Feat: OpenSearchを利用できるように
- Enhance: 高度な検索に新たな条件を追加(OpenSearchが必要です)
- 添付ファイルのセンシティブ条件(なし/含む/除外)
- 引用ノート除外
- 検索方法の詳細はdoc/Advanced-Search.mdに
- Change:APIのパラメータを変更
- notes/advanced-search の"excludeNsfw"を"excludeCW"に変更
- notes/advanced-search の"channelId"を削除
## 0.2.2
Cherrypick 4.9.0-beta.2
### General
### Client
### Server
- remove: チャンネル機能のAPIを削除
## 0.2.1
Cherrypick 4.9.0-beta.2
### Client
- feat: マスコット画像を表示するウィジェットを追加
## 0.2.0
Cherrypick 4.9.0-beta.2
### General
- enhance: ノートとユーザーの検索時に照会を行うかが選択できるようになりました
- @foo@example.com 形式でユーザ検索した場合に照会ができるようになりました
- remove: チャンネル機能を削除
- add: 公式タグ機能を追加
- インスタンスで利用が推奨されるハッシュタグの一覧です
### Client
- feat: 未ログイン時サーバーで指定されている場合、マスコット画像が表示されます
- enhance: ハッシュタグTLをリアルタイムに更新
- fix: アンケート選択肢にリモート絵文字を表示
### Server
- fix: リモートユーザーにはファイルサイズ制限を適用しない
- fix: メディアタイムライン選択時の上部のアイコンを修正
- add: メディアタイムラインのチュートリアルを追加
- feat: マスコット画像を指定できるように(コントロールパネル/ブランディング)
- デフォルト値(/assets/ai.png)または空欄の場合タイムラインが表示されます
## 0.1.0 (unreleased)
Cherrypick 4.8.0
### General
- enhance: メディアプロキシurlと拡大画像urlを分割
- enhance: 1ファイルの容量をロールでも制限できるように
### Client
- enhance: ノートとユーザーの検索時に照会を行うかが選択できるようになりました
- @foo&#8203;@example.com 形式でユーザ検索した場合に照会ができるようになりました
- add: 通知音を追加 [@mujin-nohuman (無人)](https://github.com/mujin-nohuman)
- fix: "キャッシュをクリア"してもインスタンス情報が更新されない不具合を修正 [#101](https://github.com/yojo-art/cherrypick/issues/101)
### Server
- enhance: remoteProxyエンドポイント設定を追加
- fix: webpublic生成時にドライブの縮小設定を見るように
### Others
- engawaをマージ
- cherrypickからフォーク

View File

@ -77,17 +77,12 @@ const $redisForTimelines: Provider = {
}, },
inject: [DI.config], inject: [DI.config],
}; };
const $redisForRemoteClips: Provider = {
provide: DI.redisForRemoteClips,
useFactory: (config: Config) => {
return new Redis.Redis(config.redisForRemoteClips);
},
inject: [DI.config],
};
@Global() @Global()
@Module({ @Module({
imports: [RepositoryModule], imports: [RepositoryModule],
providers: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines],
exports: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, RepositoryModule],
}) })
export class GlobalModule implements OnApplicationShutdown { export class GlobalModule implements OnApplicationShutdown {
constructor( constructor(
@ -96,7 +91,6 @@ export class GlobalModule implements OnApplicationShutdown {
@Inject(DI.redisForPub) private redisForPub: Redis.Redis, @Inject(DI.redisForPub) private redisForPub: Redis.Redis,
@Inject(DI.redisForSub) private redisForSub: Redis.Redis, @Inject(DI.redisForSub) private redisForSub: Redis.Redis,
@Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis, @Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis,
@Inject(DI.redisForRemoteClips) private redisForRemoteClips: Redis.Redis,
) { } ) { }
public async dispose(): Promise<void> { public async dispose(): Promise<void> {
@ -109,7 +103,6 @@ export class GlobalModule implements OnApplicationShutdown {
this.redisForPub.disconnect(), this.redisForPub.disconnect(),
this.redisForSub.disconnect(), this.redisForSub.disconnect(),
this.redisForTimelines.disconnect(), this.redisForTimelines.disconnect(),
this.redisForRemoteClips.disconnect(),
]); ]);
} }

View File

@ -51,7 +51,6 @@ type Source = {
redisForPubsub?: RedisOptionsSource; redisForPubsub?: RedisOptionsSource;
redisForJobQueue?: RedisOptionsSource; redisForJobQueue?: RedisOptionsSource;
redisForTimelines?: RedisOptionsSource; redisForTimelines?: RedisOptionsSource;
redisForRemoteClips?: RedisOptionsSource;
meilisearch?: { meilisearch?: {
host: string; host: string;
port: string; port: string;
@ -187,7 +186,6 @@ export type Config = {
redisForPubsub: RedisOptions & RedisOptionsSource; redisForPubsub: RedisOptions & RedisOptionsSource;
redisForJobQueue: RedisOptions & RedisOptionsSource; redisForJobQueue: RedisOptions & RedisOptionsSource;
redisForTimelines: RedisOptions & RedisOptionsSource; redisForTimelines: RedisOptions & RedisOptionsSource;
redisForRemoteClips: RedisOptions & RedisOptionsSource;
sentryForBackend: { options: Partial<Sentry.NodeOptions>; enableNodeProfiling: boolean; } | undefined; sentryForBackend: { options: Partial<Sentry.NodeOptions>; enableNodeProfiling: boolean; } | undefined;
sentryForFrontend: { options: Partial<Sentry.NodeOptions> } | undefined; sentryForFrontend: { options: Partial<Sentry.NodeOptions> } | undefined;
perChannelMaxNoteCacheCount: number; perChannelMaxNoteCacheCount: number;
@ -284,7 +282,6 @@ export function loadConfig(): Config {
redisForPubsub: config.redisForPubsub ? convertRedisOptions(config.redisForPubsub, host) : redis, redisForPubsub: config.redisForPubsub ? convertRedisOptions(config.redisForPubsub, host) : redis,
redisForJobQueue: config.redisForJobQueue ? convertRedisOptions(config.redisForJobQueue, host) : redis, redisForJobQueue: config.redisForJobQueue ? convertRedisOptions(config.redisForJobQueue, host) : redis,
redisForTimelines: config.redisForTimelines ? convertRedisOptions(config.redisForTimelines, host) : redis, redisForTimelines: config.redisForTimelines ? convertRedisOptions(config.redisForTimelines, host) : redis,
redisForRemoteClips: config.redisForRemoteClips ? convertRedisOptions(config.redisForRemoteClips, host) : redis,
sentryForBackend: config.sentryForBackend, sentryForBackend: config.sentryForBackend,
sentryForFrontend: config.sentryForFrontend, sentryForFrontend: config.sentryForFrontend,
id: config.id, id: config.id,

View File

@ -11,7 +11,6 @@ export const DI = {
redisForPub: Symbol('redisForPub'), redisForPub: Symbol('redisForPub'),
redisForSub: Symbol('redisForSub'), redisForSub: Symbol('redisForSub'),
redisForTimelines: Symbol('redisForTimelines'), redisForTimelines: Symbol('redisForTimelines'),
redisForRemoteClips: Symbol('redisForRemoteClips'),
//#region Repositories //#region Repositories
usersRepository: Symbol('usersRepository'), usersRepository: Symbol('usersRepository'),

View File

@ -27,9 +27,6 @@ export class HealthServerService {
@Inject(DI.redisForTimelines) @Inject(DI.redisForTimelines)
private redisForTimelines: Redis.Redis, private redisForTimelines: Redis.Redis,
@Inject(DI.redisForRemoteClips)
private redisForRemoteClips: Redis.Redis,
@Inject(DI.db) @Inject(DI.db)
private db: DataSource, private db: DataSource,
@ -46,7 +43,6 @@ export class HealthServerService {
this.redisForPub.ping(), this.redisForPub.ping(),
this.redisForSub.ping(), this.redisForSub.ping(),
this.redisForTimelines.ping(), this.redisForTimelines.ping(),
this.redisForRemoteClips.ping(),
this.db.query('SELECT 1'), this.db.query('SELECT 1'),
...(this.meilisearch ? [this.meilisearch.health()] : []), ...(this.meilisearch ? [this.meilisearch.health()] : []),
]).then(() => 200, () => 503)); ]).then(() => 200, () => 503));

View File

@ -31,18 +31,13 @@ export const meta = {
code: 'ALREADY_FAVORITED', code: 'ALREADY_FAVORITED',
id: '92658936-c625-4273-8326-2d790129256e', id: '92658936-c625-4273-8326-2d790129256e',
}, },
unimplemented: {
message: 'Unimplemented.',
code: 'UNIMPLEMENTED',
id: '37561aed-4ba4-4a53-9efe-a0aa255e9bb3',
},
}, },
} as const; } as const;
export const paramDef = { export const paramDef = {
type: 'object', type: 'object',
properties: { properties: {
clipId: { type: 'string' }, clipId: { type: 'string', format: 'misskey:id' },
}, },
required: ['clipId'], required: ['clipId'],
} as const; } as const;
@ -59,9 +54,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private idService: IdService, private idService: IdService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
if (ps.clipId.split('@').length > 1) {
throw new ApiError(meta.errors.unimplemented);
}
const clip = await this.clipsRepository.findOneBy({ id: ps.clipId }); const clip = await this.clipsRepository.findOneBy({ id: ps.clipId });
if (clip == null) { if (clip == null) {
throw new ApiError(meta.errors.noSuchClip); throw new ApiError(meta.errors.noSuchClip);

View File

@ -4,23 +4,11 @@
*/ */
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import got, * as Got from 'got';
import * as Redis from 'ioredis';
import type { Config } from '@/config.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { NotesRepository, ClipsRepository, ClipNotesRepository, MiNote } from '@/models/_.js'; import type { NotesRepository, ClipsRepository, ClipNotesRepository } from '@/models/_.js';
import { QueryService } from '@/core/QueryService.js'; import { QueryService } from '@/core/QueryService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js';
import { IObject } from '@/core/activitypub/type.js';
import { MetaService } from '@/core/MetaService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { ApLoggerService } from '@/core/activitypub/ApLoggerService.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
export const meta = { export const meta = {
@ -36,16 +24,6 @@ export const meta = {
code: 'NO_SUCH_CLIP', code: 'NO_SUCH_CLIP',
id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00', id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00',
}, },
invalidIdFormat: {
message: 'Invalid id format.',
code: 'INVALID_ID_FORMAT',
id: '42d11fe5-686e-41ee-9e7f-e804ec3a388d',
},
failedToResolveRemoteUser: {
message: 'failedToResolveRemoteUser.',
code: 'FAILED_TO_RESOLVE_REMOTE_USER',
id: 'af0ecffc-8717-409e-a8d4-e1e3a5d2497f',
},
}, },
res: { res: {
@ -62,7 +40,7 @@ export const meta = {
export const paramDef = { export const paramDef = {
type: 'object', type: 'object',
properties: { properties: {
clipId: { type: 'string' }, clipId: { type: 'string', format: 'misskey:id' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' }, sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' },
@ -73,8 +51,6 @@ export const paramDef = {
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.clipsRepository) @Inject(DI.clipsRepository)
private clipsRepository: ClipsRepository, private clipsRepository: ClipsRepository,
@ -83,189 +59,43 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.clipNotesRepository) @Inject(DI.clipNotesRepository)
private clipNotesRepository: ClipNotesRepository, private clipNotesRepository: ClipNotesRepository,
@Inject(DI.redisForRemoteClips)
private redisForRemoteClips: Redis.Redis,
private httpRequestService: HttpRequestService,
private apNoteService: ApNoteService,
private metaService: MetaService,
private utilityService: UtilityService,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
private queryService: QueryService, private queryService: QueryService,
private apLoggerService: ApLoggerService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const parsed_id = ps.clipId.split('@'); const clip = await this.clipsRepository.findOneBy({
let notes = []; id: ps.clipId,
if (parsed_id.length === 2 ) {//is remote });
const url = 'https://' + parsed_id[1] + '/api/clips/notes';
apLoggerService.logger.debug('remote clip ' + url);
notes = await remote(config, httpRequestService, redisForRemoteClips, apNoteService, metaService, utilityService, apLoggerService, url, parsed_id[0], parsed_id[1], ps.clipId, ps.limit, ps.sinceId, ps.untilId);
} else if (parsed_id.length === 1 ) {//is not local
const clip = await this.clipsRepository.findOneBy({
id: ps.clipId,
});
if (clip == null) { if (clip == null) {
throw new ApiError(meta.errors.noSuchClip); throw new ApiError(meta.errors.noSuchClip);
}
if (!clip.isPublic && (me == null || (clip.userId !== me.id))) {
throw new ApiError(meta.errors.noSuchClip);
}
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.innerJoin(this.clipNotesRepository.metadata.targetName, 'clipNote', 'clipNote.noteId = note.id')
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser')
.andWhere('clipNote.clipId = :clipId', { clipId: clip.id });
if (me) {
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateBlockedUserQuery(query, me);
}
notes = await query
.limit(ps.limit)
.getMany();
} else {
throw new ApiError(meta.errors.invalidIdFormat);
} }
if (!clip.isPublic && (me == null || (clip.userId !== me.id))) {
throw new ApiError(meta.errors.noSuchClip);
}
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.innerJoin(this.clipNotesRepository.metadata.targetName, 'clipNote', 'clipNote.noteId = note.id')
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser')
.andWhere('clipNote.clipId = :clipId', { clipId: clip.id });
if (me) {
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateBlockedUserQuery(query, me);
}
const notes = await query
.limit(ps.limit)
.getMany();
return await this.noteEntityService.packMany(notes, me); return await this.noteEntityService.packMany(notes, me);
}); });
} }
} }
async function remote(
config:Config,
httpRequestService: HttpRequestService,
redisForRemoteClips: Redis.Redis,
apNoteService: ApNoteService,
metaService: MetaService,
utilityService: UtilityService,
apLoggerService: ApLoggerService,
url:string,
clipId:string,
host:string,
local_id:string,
limit:number,
sinceId:string|undefined,
untilId:string|undefined,
) {
const cache_key = local_id + '-' + (sinceId ? sinceId : '') + '-' + (untilId ? untilId : '') + limit;
const cache_value = await redisForRemoteClips.get(cache_key);
let remote_json = null;
if (cache_value === null) {
const sinceIdRemote = sinceId ? await redisForRemoteClips.get(sinceId + '@' + host) : undefined;
const untilIdRemote = untilId ? await redisForRemoteClips.get(untilId + '@' + host) : undefined;
const timeout = 30 * 1000;
const operationTimeout = 60 * 1000;
const res = got.post(url, {
headers: {
'User-Agent': config.userAgent,
'Content-Type': 'application/json; charset=utf-8',
},
timeout: {
lookup: timeout,
connect: timeout,
secureConnect: timeout,
socket: timeout, // read timeout
response: timeout,
send: timeout,
request: operationTimeout, // whole operation timeout
},
agent: {
http: httpRequestService.httpAgent,
https: httpRequestService.httpsAgent,
},
http2: true,
retry: {
limit: 1,
},
enableUnixSockets: false,
body: JSON.stringify({
clipId,
limit,
sinceId: sinceIdRemote,
untilId: untilIdRemote,
}),
});
remote_json = await res.text();
redisForRemoteClips.set(cache_key, remote_json);
redisForRemoteClips.expire(cache_key, 10 * 60);
} else {
remote_json = cache_value;
}
const remote_notes = JSON.parse(remote_json);
//リモートに照会する回数の上限
const create_limit = 5;
let create_count = 0;
const notes = [];
for (const note of remote_notes) {
const uri = note.uri ? note.uri : 'https://' + host + '/notes/' + note.id;
if (uri !== null) {
if (create_count > create_limit) {
break;
}
const local_note = await remoteNote(apNoteService, uri, redisForRemoteClips, metaService, utilityService, apLoggerService, host, note.id);
if (local_note !== null) {
if (local_note.is_create) {
create_count++;
}
notes.push(local_note.note);
}
}
}
return notes;
}
class RemoteNote {
note:MiNote;
is_create:boolean;
}
async function remoteNote(
apNoteService: ApNoteService,
uri: string,
redisForRemoteClips: Redis.Redis,
metaService: MetaService,
utilityService: UtilityService,
apLoggerService: ApLoggerService,
host:string,
remote_note_id:string,
): Promise<RemoteNote | null> {
const fetchedMeta = await metaService.fetch();
apLoggerService.logger.debug('remote clip note fetch ' + uri);
let note;
let is_create = false;
try {
//ブロックされたインスタンスの投稿は無かった事にする
if (utilityService.isBlockedHost(fetchedMeta.blockedHosts, utilityService.extractDbHost(uri))) return null;
//ローカルのDBから取得を試みる
note = await apNoteService.fetchNote(uri);
if (note == null) {
//ダメそうなら照会
note = await apNoteService.createNote(uri, undefined, true);
is_create = true;
}
} catch (e) {
apLoggerService.logger.warn(String(e));
//照会失敗した時はクリップ内に無かった事にする
return null;
}
if (note !== null) {
redisForRemoteClips.set(note.id + '@' + host, remote_note_id);
return {
note,
is_create,
};
} else {
return null;
}
}

View File

@ -4,17 +4,10 @@
*/ */
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import got, * as Got from 'got';
import * as Redis from 'ioredis';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { ClipsRepository } from '@/models/_.js'; import type { ClipsRepository } from '@/models/_.js';
import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
export const meta = { export const meta = {
@ -30,16 +23,6 @@ export const meta = {
code: 'NO_SUCH_CLIP', code: 'NO_SUCH_CLIP',
id: 'c3c5fe33-d62c-44d2-9ea5-d997703f5c20', id: 'c3c5fe33-d62c-44d2-9ea5-d997703f5c20',
}, },
invalidIdFormat: {
message: 'Invalid id format.',
code: 'INVALID_ID_FORMAT',
id: 'df45c7d1-cd15-4a35-b3e1-8c9f987c4f5c',
},
failedToResolveRemoteUser: {
message: 'failedToResolveRemoteUser.',
code: 'FAILED_TO_RESOLVE_REMOTE_USER',
id: '56d5e552-d55a-47e3-9f37-6dc85a93ecf9',
},
}, },
res: { res: {
@ -52,7 +35,7 @@ export const meta = {
export const paramDef = { export const paramDef = {
type: 'object', type: 'object',
properties: { properties: {
clipId: { type: 'string' }, clipId: { type: 'string', format: 'misskey:id' },
}, },
required: ['clipId'], required: ['clipId'],
} as const; } as const;
@ -60,28 +43,12 @@ export const paramDef = {
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.clipsRepository) @Inject(DI.clipsRepository)
private clipsRepository: ClipsRepository, private clipsRepository: ClipsRepository,
@Inject(DI.redisForRemoteClips)
private redisForRemoteClips: Redis.Redis,
private httpRequestService: HttpRequestService,
private userEntityService: UserEntityService,
private remoteUserResolveService: RemoteUserResolveService,
private clipEntityService: ClipEntityService, private clipEntityService: ClipEntityService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const parsed_id = ps.clipId.split('@');
if (parsed_id.length === 2 ) {//is remote
const url = 'https://' + parsed_id[1] + '/api/clips/show';
console.log(url);
return remote(config, httpRequestService, userEntityService, remoteUserResolveService, redisForRemoteClips, url, parsed_id[0], parsed_id[1], ps.clipId);
}
if (parsed_id.length !== 1 ) {//is not local
throw new ApiError(meta.errors.invalidIdFormat);
}
// Fetch the clip // Fetch the clip
const clip = await this.clipsRepository.findOneBy({ const clip = await this.clipsRepository.findOneBy({
id: ps.clipId, id: ps.clipId,
@ -99,75 +66,3 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}); });
} }
} }
async function remote(
config:Config,
httpRequestService: HttpRequestService,
userEntityService: UserEntityService,
remoteUserResolveService: RemoteUserResolveService,
redisForRemoteClips: Redis.Redis,
url:string,
clipId:string,
host:string,
local_id:string,
) {
const cache_key = local_id + '-info';
const cache_value = await redisForRemoteClips.get(cache_key);
let remote_json = null;
if (cache_value === null) {
const timeout = 30 * 1000;
const operationTimeout = 60 * 1000;
const res = got.post(url, {
headers: {
'User-Agent': config.userAgent,
'Content-Type': 'application/json; charset=utf-8',
},
timeout: {
lookup: timeout,
connect: timeout,
secureConnect: timeout,
socket: timeout, // read timeout
response: timeout,
send: timeout,
request: operationTimeout, // whole operation timeout
},
agent: {
http: httpRequestService.httpAgent,
https: httpRequestService.httpsAgent,
},
http2: true,
retry: {
limit: 1,
},
enableUnixSockets: false,
body: JSON.stringify({
clipId,
}),
});
remote_json = await res.text();
redisForRemoteClips.set(cache_key, remote_json);
redisForRemoteClips.expire(cache_key, 10 * 60);
} else {
remote_json = cache_value;
}
const remote_clip = JSON.parse(remote_json);
if (remote_clip.user == null || remote_clip.user.username == null) {
throw new ApiError(meta.errors.failedToResolveRemoteUser);
}
const user = await remoteUserResolveService.resolveUser(remote_clip.user.username, host).catch(err => {
throw new ApiError(meta.errors.failedToResolveRemoteUser);
});
return await awaitAll({
id: local_id,
createdAt: remote_clip.createdAt ? remote_clip.createdAt : null,
lastClippedAt: remote_clip.lastClippedAt ? remote_clip.lastClippedAt : null,
userId: user.id,
user: userEntityService.pack(user),
name: remote_clip.name,
description: remote_clip.description,
isPublic: true,
favoritedCount: remote_clip.favoritedCount,
isFavorited: false,
notesCount: remote_clip.notesCount,
});
}

View File

@ -4,18 +4,11 @@
*/ */
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis'; import type { ClipsRepository } from '@/models/_.js';
import got, * as Got from 'got';
import type { ClipsRepository, MiUser, UsersRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js'; import { QueryService } from '@/core/QueryService.js';
import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { Config } from '@/config.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { FindOptionsWhere } from 'typeorm';
export const meta = { export const meta = {
tags: ['users', 'clips'], tags: ['users', 'clips'],
@ -38,8 +31,8 @@ export const paramDef = {
properties: { properties: {
userId: { type: 'string', format: 'misskey:id' }, userId: { type: 'string', format: 'misskey:id' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string' }, sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string' }, untilId: { type: 'string', format: 'misskey:id' },
}, },
required: ['userId'], required: ['userId'],
} as const; } as const;
@ -47,28 +40,13 @@ export const paramDef = {
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.clipsRepository) @Inject(DI.clipsRepository)
private clipsRepository: ClipsRepository, private clipsRepository: ClipsRepository,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.redisForRemoteClips)
private redisForRemoteClips: Redis.Redis,
private httpRequestService: HttpRequestService,
private userEntityService: UserEntityService,
private clipEntityService: ClipEntityService, private clipEntityService: ClipEntityService,
private queryService: QueryService, private queryService: QueryService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const q: FindOptionsWhere<MiUser> = { id: ps.userId };
const user = await this.usersRepository.findOneBy(q);
if (user === null) return [];
if (userEntityService.isRemoteUser(user)) {
return remote(config, httpRequestService, redisForRemoteClips, userEntityService, user, ps.limit, ps.sinceId, ps.untilId);
}
const query = this.queryService.makePaginationQuery(this.clipsRepository.createQueryBuilder('clip'), ps.sinceId, ps.untilId) const query = this.queryService.makePaginationQuery(this.clipsRepository.createQueryBuilder('clip'), ps.sinceId, ps.untilId)
.andWhere('clip.userId = :userId', { userId: ps.userId }) .andWhere('clip.userId = :userId', { userId: ps.userId })
.andWhere('clip.isPublic = true'); .andWhere('clip.isPublic = true');
@ -81,165 +59,3 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}); });
} }
} }
async function remote(
config:Config,
httpRequestService: HttpRequestService,
redisForRemoteClips: Redis.Redis,
userEntityService: UserEntityService,
user:MiUser,
limit:number,
sinceId:string|undefined,
untilId:string|undefined,
) {
const cache_key = user.id + '-' + sinceId + '-' + untilId + '-clips';
const cache_value = await redisForRemoteClips.get(cache_key);
if (cache_value !== null) {
//ステータス格納
if (cache_value.startsWith('__')) {
if (cache_value === '__SKIP_FETCH') return [];
//未定義のステータス
return [];
}
return JSON.parse(cache_value);
}
if (user.host == null) {
//ローカルユーザーではない
return [];
}
const remote_user_id = await fetch_remote_user(config, httpRequestService, redisForRemoteClips, user.username, user.host);
if (remote_user_id === null) {
return [];
}
const remote_json = await fetch_remote_clip(config, httpRequestService, remote_user_id, user.host, limit, sinceId, untilId);
const json = JSON.parse(remote_json);
const clips = [];
for (const remote_clip of json) {
const clip = await awaitAll({
id: remote_clip.id + '@' + user.host,
createdAt: remote_clip.createdAt ? remote_clip.createdAt : null,
lastClippedAt: remote_clip.lastClippedAt ? remote_clip.lastClippedAt : null,
userId: user.id,
user: userEntityService.pack(user),
name: remote_clip.name,
description: remote_clip.description,
isPublic: true,
favoritedCount: remote_clip.favoritedCount,
isFavorited: false,
notesCount: remote_clip.notesCount,
});
clips.push(clip);
}
await redisForRemoteClips.set(cache_key, JSON.stringify(clips));
await redisForRemoteClips.expire(cache_key, 10 * 60);
return clips;
}
async function fetch_remote_user(
config:Config,
httpRequestService: HttpRequestService,
redisForRemoteClips: Redis.Redis,
username:string,
host:string,
) {
const cache_key = username + '@' + host + '-userid';
const id = await redisForRemoteClips.get(cache_key);
if (id !== null) {
if (id === '__NOT_MISSKEY') {
return null;
}
if (id === '__INTERVAL') {
return null;
}
return id;
}
try {
const url = 'https://' + host + '/api/users/show';
const timeout = 30 * 1000;
const operationTimeout = 60 * 1000;
const res = got.post(url, {
headers: {
'User-Agent': config.userAgent,
'Content-Type': 'application/json; charset=utf-8',
},
timeout: {
lookup: timeout,
connect: timeout,
secureConnect: timeout,
socket: timeout, // read timeout
response: timeout,
send: timeout,
request: operationTimeout, // whole operation timeout
},
agent: {
http: httpRequestService.httpAgent,
https: httpRequestService.httpsAgent,
},
http2: true,
retry: {
limit: 1,
},
enableUnixSockets: false,
body: JSON.stringify({
username,
}),
});
const text = await res.text();
const json = JSON.parse(text);
if (json.id != null) {
redisForRemoteClips.set(cache_key, json.id);
return json.id as string;
}
} catch {
redisForRemoteClips.set(cache_key, '__INTERVAL');
await redisForRemoteClips.expire(cache_key, 60 * 60);
}
return null;
}
async function fetch_remote_clip(
config:Config,
httpRequestService: HttpRequestService,
userId:string,
host:string,
limit:number,
sinceId:string|undefined,
untilId:string|undefined,
) {
const url = 'https://' + host + '/api/users/clips';
const sinceIdRemote = sinceId ? sinceId.split('@')[0] : undefined;
const untilIdRemote = untilId ? untilId.split('@')[0] : undefined;
const timeout = 30 * 1000;
const operationTimeout = 60 * 1000;
const res = got.post(url, {
headers: {
'User-Agent': config.userAgent,
'Content-Type': 'application/json; charset=utf-8',
},
timeout: {
lookup: timeout,
connect: timeout,
secureConnect: timeout,
socket: timeout, // read timeout
response: timeout,
send: timeout,
request: operationTimeout, // whole operation timeout
},
agent: {
http: httpRequestService.httpAgent,
https: httpRequestService.httpsAgent,
},
http2: true,
retry: {
limit: 1,
},
enableUnixSockets: false,
body: JSON.stringify({
userId,
limit,
sinceId: sinceIdRemote,
untilId: untilIdRemote,
}),
});
return await res.text();
}

View File

@ -13737,6 +13737,7 @@ export type operations = {
requestBody: { requestBody: {
content: { content: {
'application/json': { 'application/json': {
/** Format: misskey:id */
clipId: string; clipId: string;
/** @default 10 */ /** @default 10 */
limit?: number; limit?: number;
@ -13796,6 +13797,7 @@ export type operations = {
requestBody: { requestBody: {
content: { content: {
'application/json': { 'application/json': {
/** Format: misskey:id */
clipId: string; clipId: string;
}; };
}; };
@ -13906,6 +13908,7 @@ export type operations = {
requestBody: { requestBody: {
content: { content: {
'application/json': { 'application/json': {
/** Format: misskey:id */
clipId: string; clipId: string;
}; };
}; };
@ -26023,7 +26026,9 @@ export type operations = {
userId: string; userId: string;
/** @default 10 */ /** @default 10 */
limit?: number; limit?: number;
/** Format: misskey:id */
sinceId?: string; sinceId?: string;
/** Format: misskey:id */
untilId?: string; untilId?: string;
}; };
}; };