From ce0d614cd03ec9c0eabb4476325e73b9e359b7c3 Mon Sep 17 00:00:00 2001 From: kozakura913 <98575220+kozakura913@users.noreply.github.com> Date: Mon, 16 Sep 2024 17:17:00 +0900 Subject: [PATCH 1/5] =?UTF-8?q?=E3=83=AA=E3=83=A2=E3=83=BC=E3=83=88?= =?UTF-8?q?=E3=82=AF=E3=83=AA=E3=83=83=E3=83=97=E3=82=92=E3=81=8A=E6=B0=97?= =?UTF-8?q?=E3=81=AB=E5=85=A5=E3=82=8A=E3=81=99=E3=82=8B=E6=99=82=E3=81=AB?= =?UTF-8?q?=E4=BD=9C=E8=80=85=E3=82=92=E4=BF=9D=E6=8C=81=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../1726460877945-ClipFavoriteRemoteAuthor.js | 16 +++ packages/backend/src/core/ClipService.ts | 28 +++- packages/backend/src/misc/remote-api-utils.ts | 129 ++++++++++++++++++ .../backend/src/models/ClipFavoriteRemote.ts | 8 ++ .../server/api/endpoints/clips/favorite.ts | 3 +- .../api/endpoints/clips/my-favorites.ts | 9 +- .../frontend/src/components/MkClipPreview.vue | 40 +++++- 7 files changed, 227 insertions(+), 6 deletions(-) create mode 100644 packages/backend/migration/1726460877945-ClipFavoriteRemoteAuthor.js create mode 100644 packages/backend/src/misc/remote-api-utils.ts diff --git a/packages/backend/migration/1726460877945-ClipFavoriteRemoteAuthor.js b/packages/backend/migration/1726460877945-ClipFavoriteRemoteAuthor.js new file mode 100644 index 0000000000..915d6e3afc --- /dev/null +++ b/packages/backend/migration/1726460877945-ClipFavoriteRemoteAuthor.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project, yojo-art team + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class clipFavoriteRemoteAuthor1726460877945 { + name = 'clipFavoriteRemoteAuthor1726460877945' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "clip_favorite_remote" ADD "authorId" character varying(32)`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "clip_favorite_remote" DROP COLUMN "authorId"`); + } +} diff --git a/packages/backend/src/core/ClipService.ts b/packages/backend/src/core/ClipService.ts index b2ef5f080a..0ed580f399 100644 --- a/packages/backend/src/core/ClipService.ts +++ b/packages/backend/src/core/ClipService.ts @@ -18,7 +18,7 @@ import { bindThis } from '@/decorators.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; import { RoleService } from '@/core/RoleService.js'; import { IdService } from '@/core/IdService.js'; -import type { MiLocalUser } from '@/models/User.js'; +import type { MiLocalUser, MiUser } from '@/models/User.js'; import { Packed } from '@/misc/json-schema.js'; @Injectable() @@ -172,6 +172,32 @@ export class ClipService { this.notesRepository.decrement({ id: noteId }, 'clippedCount', 1); } @bindThis + async showRemoteOrDummy(clipId: string, author: MiUser|null) : Promise> { + if (author == null) { + throw new Error(); + } + try { + if (author.host == null) { + throw new Error(); + } + return await this.showRemote(clipId, author.host); + } catch { + return await awaitAll({ + id: clipId + '@' + (author.host ? author.host : ''), + createdAt: new Date(0).toISOString(), + lastClippedAt: new Date(0).toISOString(), + userId: author.id, + user: this.userEntityService.pack(author), + name: 'Unavailable', + description: '', + isPublic: true, + favoritedCount: 0, + isFavorited: false, + notesCount: 0, + }); + } + } + @bindThis public async showRemote( clipId:string, host:string, diff --git a/packages/backend/src/misc/remote-api-utils.ts b/packages/backend/src/misc/remote-api-utils.ts new file mode 100644 index 0000000000..bd8ad1d82b --- /dev/null +++ b/packages/backend/src/misc/remote-api-utils.ts @@ -0,0 +1,129 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project, yojo-art team + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import Redis from 'ioredis'; +import got, * as Got from 'got'; +import type { Config } from '@/config.js'; +import { HttpRequestService } from '@/core/HttpRequestService.js'; +import { MiUser } from '@/models/User.js'; + +export type FetchRemoteApiOpts={ + /** リモートで割り当てられているid */ + userId?:string, + limit?:number, + sinceId?:string, + untilId?:string, +}; + +export async function fetch_remote_api( + config: Config, httpRequestService: HttpRequestService, host: string, endpoint: string, opts: FetchRemoteApiOpts, +) { + const url = 'https://' + host + endpoint; + const sinceIdRemote = opts.sinceId ? opts.sinceId.split('@')[0] : undefined; + const untilIdRemote = opts.untilId ? opts.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: opts.userId, + limit: opts.limit, + sinceId: sinceIdRemote, + untilId: untilIdRemote, + }), + }); + return await res.text(); +} +/** userがリモートで割り当てられているidを取得 */ +export async function fetch_remote_user_id( + config:Config, + httpRequestService: HttpRequestService, + redisForRemoteApis: Redis.Redis, + user:MiUser, +) { + //ローカルのIDからリモートのIDを割り出す + const cache_key = 'remote-userId:' + user.id; + const id = await redisForRemoteApis.get(cache_key); + if (id !== null) { + if (id === '__NOT_MISSKEY') { + return null; + } + if (id === '__INTERVAL') { + return null; + } + //アクセス時に有効期限を更新 + redisForRemoteApis.expire(cache_key, 7 * 24 * 60 * 60); + return id; + } + try { + const url = 'https://' + user.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: user.username, + }), + }); + const text = await res.text(); + const json = JSON.parse(text); + if (json.id != null) { + const redisPipeline = redisForRemoteApis.pipeline(); + redisPipeline.set(cache_key, json.id); + //キャッシュ期限1週間 + redisPipeline.expire(cache_key, 7 * 24 * 60 * 60); + await redisPipeline.exec(); + return json.id as string; + } + } catch { + const redisPipeline = redisForRemoteApis.pipeline(); + redisPipeline.set(cache_key, '__INTERVAL'); + redisPipeline.expire(cache_key, 60 * 60); + await redisPipeline.exec(); + } + return null; +} diff --git a/packages/backend/src/models/ClipFavoriteRemote.ts b/packages/backend/src/models/ClipFavoriteRemote.ts index ead069d9c4..3d6ef7107c 100644 --- a/packages/backend/src/models/ClipFavoriteRemote.ts +++ b/packages/backend/src/models/ClipFavoriteRemote.ts @@ -23,6 +23,14 @@ export class MiClipFavoriteRemote { @JoinColumn() public user: MiUser | null; + @Column(id()) + public authorId: MiUser['id']; + @ManyToOne(type => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public author: MiUser | null; + @Column('varchar', { length: 32, }) diff --git a/packages/backend/src/server/api/endpoints/clips/favorite.ts b/packages/backend/src/server/api/endpoints/clips/favorite.ts index 9a60223875..50071c9df7 100644 --- a/packages/backend/src/server/api/endpoints/clips/favorite.ts +++ b/packages/backend/src/server/api/endpoints/clips/favorite.ts @@ -75,7 +75,7 @@ export default class extends Endpoint { // eslint- const host = clipIdArray.length > 1 ? clipIdArray[1] : null; if (host) { const clipId = clipIdArray[0]; - await clipService.showRemote(clipId, host); + const clip = await clipService.showRemote(clipId, host); const exist = await this.clipFavoritesRemoteRepository.exists({ where: { @@ -94,6 +94,7 @@ export default class extends Endpoint { // eslint- clipId: clipId, host: host, userId: me.id, + authorId: clip.userId, }); return; } diff --git a/packages/backend/src/server/api/endpoints/clips/my-favorites.ts b/packages/backend/src/server/api/endpoints/clips/my-favorites.ts index 2814954c52..b38315c5bb 100644 --- a/packages/backend/src/server/api/endpoints/clips/my-favorites.ts +++ b/packages/backend/src/server/api/endpoints/clips/my-favorites.ts @@ -63,10 +63,15 @@ export default class extends Endpoint { // eslint- } if (ps.withRemote) { const query = this.clipFavoritesRemoteRepository.createQueryBuilder('favorite') - .andWhere('favorite.userId = :meId', { meId: me.id }); + .andWhere('favorite.userId = :meId', { meId: me.id }) + .leftJoinAndSelect('favorite.author', 'author'); const favorites = await query.getMany(); - const remoteFavorites = await Promise.all(favorites.map(e => clipService.showRemote(e.clipId, e.host))); + let remoteFavorites = await Promise.all(favorites.map(e => clipService.showRemoteOrDummy(e.clipId, e.author))); + remoteFavorites = remoteFavorites.map(clip => { + clip.isFavorited = true; + return clip; + }); myFavorites = myFavorites.concat(remoteFavorites); } return myFavorites.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()); diff --git a/packages/frontend/src/components/MkClipPreview.vue b/packages/frontend/src/components/MkClipPreview.vue index 7fe5deabf9..e2679e3a86 100644 --- a/packages/frontend/src/components/MkClipPreview.vue +++ b/packages/frontend/src/components/MkClipPreview.vue @@ -6,7 +6,11 @@ SPDX-License-Identifier: AGPL-3.0-only