Compare commits
16 Commits
2023.10.0-
...
2023.10.0-
Author | SHA1 | Date | |
---|---|---|---|
![]() |
adf9d9c969 | ||
![]() |
8c663f65a8 | ||
![]() |
481ca4ec03 | ||
![]() |
e6ca53c5e1 | ||
![]() |
95dc70021f | ||
![]() |
fd3295eba4 | ||
![]() |
a76cebd897 | ||
![]() |
7d289c1b77 | ||
![]() |
0bdbdba9f8 | ||
![]() |
4489ca3c74 | ||
![]() |
87416710c3 | ||
![]() |
132b01461d | ||
![]() |
dab205edb8 | ||
![]() |
e4dcab8671 | ||
![]() |
780721e9a2 | ||
![]() |
ee483f2dee |
10
CHANGELOG.md
10
CHANGELOG.md
@@ -14,8 +14,9 @@
|
||||
|
||||
## 2023.10.0
|
||||
### NOTE
|
||||
- muted_noteテーブルは使われなくなったため手動で削除を行ってください。
|
||||
- 2023.9.2で導入されたノート編集機能はクオリティの高い実装が困難であることが判明したため撤回されました
|
||||
- アップデート後、アップデートより前の時点にTLを遡ることはできません
|
||||
- アップデート後であっても、今後のアップデートで2023.10.0以前のTLに遡れるようになる可能性はあります
|
||||
|
||||
### Changes
|
||||
- API: users/notes, notes/local-timeline で fileType 指定はできなくなりました
|
||||
@@ -27,13 +28,18 @@
|
||||
- Enhance: ソフトワードミュートとハードワードミュートは統合されました
|
||||
- Enhance: モデレーションログ機能の強化
|
||||
- Enhance: ローカリゼーションの更新
|
||||
- Enhance: 依存関係の更新
|
||||
- Fix: ダイレクト投稿をリノートできてしまう問題を修正
|
||||
- Fix: ユーザーリストTLにチャンネル投稿が含まれる問題を修正
|
||||
|
||||
### Client
|
||||
- Enhance: 二要素認証のバックアップコード一覧をテキストファイルでダウンロード可能に
|
||||
- Fix: リアクションしたユーザ一覧のUIが稀に左上に残ってしまう不具合を修正
|
||||
|
||||
### Server
|
||||
- Enhance: タイムライン取得時のパフォーマンスを改善
|
||||
- Enhance: タイムライン取得時のパフォーマンスを大幅に向上
|
||||
- Enhance: ハイライト取得時のパフォーマンスを大幅に向上
|
||||
- Enhance: 不要なPostgreSQLのインデックスを削除しパフォーマンスを向上
|
||||
|
||||
## 2023.9.3
|
||||
### General
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"version": "2023.10.0-beta.3",
|
||||
"version": "2023.10.0-beta.4",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -47,12 +47,12 @@
|
||||
"cssnano": "6.0.1",
|
||||
"js-yaml": "4.1.0",
|
||||
"postcss": "8.4.31",
|
||||
"terser": "5.20.0",
|
||||
"terser": "5.21.0",
|
||||
"typescript": "5.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "6.7.3",
|
||||
"@typescript-eslint/parser": "6.7.3",
|
||||
"@typescript-eslint/eslint-plugin": "6.7.4",
|
||||
"@typescript-eslint/parser": "6.7.4",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "13.3.0",
|
||||
"eslint": "8.50.0",
|
||||
|
18
packages/backend/migration/1696569742153-clean-up.js
Normal file
18
packages/backend/migration/1696569742153-clean-up.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class CleanUp1696569742153 {
|
||||
name = 'CleanUp1696569742153'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_01f4581f114e0ebd2bbb876f0b"`);
|
||||
await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "score"`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "note" ADD "score" integer NOT NULL DEFAULT '0'`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_01f4581f114e0ebd2bbb876f0b" ON "note_reaction" ("createdAt") `);
|
||||
}
|
||||
}
|
15
packages/backend/migration/1696581429196-clean-up.js
Normal file
15
packages/backend/migration/1696581429196-clean-up.js
Normal file
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class CleanUp1696581429196 {
|
||||
name = 'CleanUp1696581429196'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS "muted_note"`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
}
|
||||
}
|
@@ -71,14 +71,14 @@
|
||||
"@fastify/multipart": "8.0.0",
|
||||
"@fastify/static": "6.11.2",
|
||||
"@fastify/view": "8.2.0",
|
||||
"@nestjs/common": "10.2.6",
|
||||
"@nestjs/core": "10.2.6",
|
||||
"@nestjs/testing": "10.2.6",
|
||||
"@nestjs/common": "10.2.7",
|
||||
"@nestjs/core": "10.2.7",
|
||||
"@nestjs/testing": "10.2.7",
|
||||
"@peertube/http-signature": "1.7.0",
|
||||
"@simplewebauthn/server": "8.2.0",
|
||||
"@sinonjs/fake-timers": "11.1.0",
|
||||
"@swc/cli": "0.1.62",
|
||||
"@swc/core": "1.3.90",
|
||||
"@swc/core": "1.3.92",
|
||||
"accepts": "1.3.8",
|
||||
"ajv": "8.12.0",
|
||||
"archiver": "6.0.1",
|
||||
@@ -86,7 +86,7 @@
|
||||
"bcryptjs": "2.4.3",
|
||||
"blurhash": "2.0.5",
|
||||
"body-parser": "1.20.2",
|
||||
"bullmq": "4.11.4",
|
||||
"bullmq": "4.12.2",
|
||||
"cacheable-lookup": "7.0.0",
|
||||
"cbor": "9.0.1",
|
||||
"chalk": "5.3.0",
|
||||
@@ -155,7 +155,7 @@
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"summaly": "github:misskey-dev/summaly",
|
||||
"systeminformation": "5.21.9",
|
||||
"systeminformation": "5.21.11",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tmp": "0.2.1",
|
||||
"tsc-alias": "1.8.8",
|
||||
@@ -189,7 +189,7 @@
|
||||
"@types/jsrsasign": "10.5.9",
|
||||
"@types/mime-types": "2.1.2",
|
||||
"@types/ms": "0.7.32",
|
||||
"@types/node": "20.7.1",
|
||||
"@types/node": "20.8.2",
|
||||
"@types/node-fetch": "3.0.3",
|
||||
"@types/nodemailer": "6.4.11",
|
||||
"@types/oauth": "0.9.2",
|
||||
@@ -212,8 +212,8 @@
|
||||
"@types/vary": "1.1.1",
|
||||
"@types/web-push": "3.6.1",
|
||||
"@types/ws": "8.5.6",
|
||||
"@typescript-eslint/eslint-plugin": "6.7.3",
|
||||
"@typescript-eslint/parser": "6.7.3",
|
||||
"@typescript-eslint/eslint-plugin": "6.7.4",
|
||||
"@typescript-eslint/parser": "6.7.4",
|
||||
"aws-sdk-client-mock": "3.0.0",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "8.50.0",
|
||||
|
@@ -60,6 +60,7 @@ import { UtilityService } from './UtilityService.js';
|
||||
import { FileInfoService } from './FileInfoService.js';
|
||||
import { SearchService } from './SearchService.js';
|
||||
import { ClipService } from './ClipService.js';
|
||||
import { FeaturedService } from './FeaturedService.js';
|
||||
import { ChartLoggerService } from './chart/ChartLoggerService.js';
|
||||
import FederationChart from './chart/charts/federation.js';
|
||||
import NotesChart from './chart/charts/notes.js';
|
||||
@@ -187,6 +188,7 @@ const $UtilityService: Provider = { provide: 'UtilityService', useExisting: Util
|
||||
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
|
||||
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
|
||||
const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService };
|
||||
const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: FeaturedService };
|
||||
|
||||
const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService };
|
||||
const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart };
|
||||
@@ -318,6 +320,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
FileInfoService,
|
||||
SearchService,
|
||||
ClipService,
|
||||
FeaturedService,
|
||||
ChartLoggerService,
|
||||
FederationChart,
|
||||
NotesChart,
|
||||
@@ -442,6 +445,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$FileInfoService,
|
||||
$SearchService,
|
||||
$ClipService,
|
||||
$FeaturedService,
|
||||
$ChartLoggerService,
|
||||
$FederationChart,
|
||||
$NotesChart,
|
||||
@@ -567,6 +571,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
FileInfoService,
|
||||
SearchService,
|
||||
ClipService,
|
||||
FeaturedService,
|
||||
FederationChart,
|
||||
NotesChart,
|
||||
UsersChart,
|
||||
@@ -690,6 +695,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$FileInfoService,
|
||||
$SearchService,
|
||||
$ClipService,
|
||||
$FeaturedService,
|
||||
$FederationChart,
|
||||
$NotesChart,
|
||||
$UsersChart,
|
||||
|
94
packages/backend/src/core/FeaturedService.ts
Normal file
94
packages/backend/src/core/FeaturedService.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import type { MiNote } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
||||
const GLOBAL_NOTES_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 3; // 3日ごと
|
||||
|
||||
@Injectable()
|
||||
export class FeaturedService {
|
||||
constructor(
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis, // TODO: 専用のRedisサーバーを設定できるようにする
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private getCurrentWindow(windowRange: number): number {
|
||||
const passed = new Date().getTime() - new Date(new Date().getFullYear(), 0, 1).getTime();
|
||||
return Math.floor(passed / windowRange);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async updateRankingOf(name: string, windowRange: number, element: string, score = 1): Promise<void> {
|
||||
const currentWindow = this.getCurrentWindow(windowRange);
|
||||
const redisTransaction = this.redisClient.multi();
|
||||
redisTransaction.zincrby(
|
||||
`${name}:${currentWindow}`,
|
||||
score,
|
||||
element);
|
||||
redisTransaction.expire(
|
||||
`${name}:${currentWindow}`,
|
||||
(windowRange * 3) / 1000,
|
||||
'NX'); // "NX -- Set expiry only when the key has no expiry" = 有効期限がないときだけ設定
|
||||
await redisTransaction.exec();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async getRankingOf(name: string, windowRange: number, limit: number): Promise<string[]> {
|
||||
const currentWindow = this.getCurrentWindow(windowRange);
|
||||
const previousWindow = currentWindow - 1;
|
||||
|
||||
const [currentRankingResult, previousRankingResult] = await Promise.all([
|
||||
this.redisClient.zrange(
|
||||
`${name}:${currentWindow}`, 0, limit, 'REV', 'WITHSCORES'),
|
||||
this.redisClient.zrange(
|
||||
`${name}:${previousWindow}`, 0, limit, 'REV', 'WITHSCORES'),
|
||||
]);
|
||||
|
||||
const ranking = new Map<string, number>();
|
||||
for (let i = 0; i < currentRankingResult.length; i += 2) {
|
||||
const noteId = currentRankingResult[i];
|
||||
const score = parseInt(currentRankingResult[i + 1], 10);
|
||||
ranking.set(noteId, score);
|
||||
}
|
||||
for (let i = 0; i < previousRankingResult.length; i += 2) {
|
||||
const noteId = previousRankingResult[i];
|
||||
const score = parseInt(previousRankingResult[i + 1], 10);
|
||||
const exist = ranking.get(noteId);
|
||||
if (exist != null) {
|
||||
ranking.set(noteId, (exist + score) / 2);
|
||||
} else {
|
||||
ranking.set(noteId, score);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(ranking.keys());
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public updateGlobalNotesRanking(noteId: MiNote['id'], score = 1): Promise<void> {
|
||||
return this.updateRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, noteId, score);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public updateInChannelNotesRanking(noteId: MiNote['id'], channelId: MiNote['channelId'], score = 1): Promise<void> {
|
||||
return this.updateRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, noteId, score);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public getGlobalNotesRanking(limit: number): Promise<MiNote['id'][]> {
|
||||
return this.getRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, limit);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public getInChannelNotesRanking(channelId: MiNote['channelId'], limit: number): Promise<MiNote['id'][]> {
|
||||
return this.getRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, limit);
|
||||
}
|
||||
}
|
@@ -53,6 +53,7 @@ import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { SearchService } from '@/core/SearchService.js';
|
||||
import { FeaturedService } from '@/core/FeaturedService.js';
|
||||
|
||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||
|
||||
@@ -200,6 +201,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
private hashtagService: HashtagService,
|
||||
private antennaService: AntennaService,
|
||||
private webhookService: WebhookService,
|
||||
private featuredService: FeaturedService,
|
||||
private remoteUserResolveService: RemoteUserResolveService,
|
||||
private apDeliverManagerService: ApDeliverManagerService,
|
||||
private apRendererService: ApRendererService,
|
||||
@@ -252,19 +254,30 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
}
|
||||
}
|
||||
|
||||
// Renote対象が「ホームまたは全体」以外の公開範囲ならreject
|
||||
if (data.renote && data.renote.visibility !== 'public' && data.renote.visibility !== 'home' && data.renote.userId !== user.id) {
|
||||
throw new Error('Renote target is not public or home');
|
||||
}
|
||||
if (data.renote) {
|
||||
switch (data.renote.visibility) {
|
||||
case 'public':
|
||||
// public noteは無条件にrenote可能
|
||||
break;
|
||||
case 'home':
|
||||
// home noteはhome以下にrenote可能
|
||||
if (data.visibility === 'public') {
|
||||
data.visibility = 'home';
|
||||
}
|
||||
break;
|
||||
case 'followers':
|
||||
// 他人のfollowers noteはreject
|
||||
if (data.renote.userId !== user.id) {
|
||||
throw new Error('Renote target is not public or home');
|
||||
}
|
||||
|
||||
// Renote対象がpublicではないならhomeにする
|
||||
if (data.renote && data.renote.visibility !== 'public' && data.visibility === 'public') {
|
||||
data.visibility = 'home';
|
||||
}
|
||||
|
||||
// Renote対象がfollowersならfollowersにする
|
||||
if (data.renote && data.renote.visibility === 'followers') {
|
||||
data.visibility = 'followers';
|
||||
// Renote対象がfollowersならfollowersにする
|
||||
data.visibility = 'followers';
|
||||
break;
|
||||
case 'specified':
|
||||
// specified / direct noteはreject
|
||||
throw new Error('Renote target is not public or home');
|
||||
}
|
||||
}
|
||||
|
||||
// 返信対象がpublicではないならhomeにする
|
||||
@@ -508,9 +521,8 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
});
|
||||
}
|
||||
|
||||
// この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき
|
||||
if (data.renote && (await this.noteEntityService.countSameRenotes(user.id, data.renote.id, note.id) === 0)) {
|
||||
if (!user.isBot) this.incRenoteCount(data.renote);
|
||||
if (data.renote && data.renote.userId !== user.id && !user.isBot) {
|
||||
this.incRenoteCount(data.renote);
|
||||
}
|
||||
|
||||
if (data.poll && data.poll.expiresAt) {
|
||||
@@ -710,10 +722,18 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
this.notesRepository.createQueryBuilder().update()
|
||||
.set({
|
||||
renoteCount: () => '"renoteCount" + 1',
|
||||
score: () => '"score" + 1',
|
||||
})
|
||||
.where('id = :id', { id: renote.id })
|
||||
.execute();
|
||||
|
||||
// 30%の確率でハイライト用ランキング更新
|
||||
if (Math.random() < 0.3) {
|
||||
if (renote.channelId != null) {
|
||||
this.featuredService.updateInChannelNotesRanking(renote.id, renote.channelId, 1);
|
||||
} else if (renote.visibility === 'public' && renote.userHost == null) {
|
||||
this.featuredService.updateGlobalNotesRanking(renote.id, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@@ -64,12 +64,6 @@ export class NoteDeleteService {
|
||||
const deletedAt = new Date();
|
||||
const cascadingNotes = await this.findCascadingNotes(note);
|
||||
|
||||
// この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき
|
||||
if (note.renoteId && (await this.noteEntityService.countSameRenotes(user.id, note.renoteId, note.id)) === 0) {
|
||||
this.notesRepository.decrement({ id: note.renoteId }, 'renoteCount', 1);
|
||||
if (!user.isBot) this.notesRepository.decrement({ id: note.renoteId }, 'score', 1);
|
||||
}
|
||||
|
||||
if (note.replyId) {
|
||||
await this.notesRepository.decrement({ id: note.replyId }, 'repliesCount', 1);
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/_.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
@@ -26,6 +27,7 @@ import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { FeaturedService } from '@/core/FeaturedService.js';
|
||||
|
||||
const FALLBACK = '❤';
|
||||
|
||||
@@ -66,6 +68,9 @@ const decodeCustomEmojiRegexp = /^:([\w+-]+)(?:@([\w.-]+))?:$/;
|
||||
@Injectable()
|
||||
export class ReactionService {
|
||||
constructor(
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@@ -86,6 +91,7 @@ export class ReactionService {
|
||||
private noteEntityService: NoteEntityService,
|
||||
private userBlockingService: UserBlockingService,
|
||||
private idService: IdService,
|
||||
private featuredService: FeaturedService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private apRendererService: ApRendererService,
|
||||
private apDeliverManagerService: ApDeliverManagerService,
|
||||
@@ -182,11 +188,19 @@ export class ReactionService {
|
||||
await this.notesRepository.createQueryBuilder().update()
|
||||
.set({
|
||||
reactions: () => sql,
|
||||
... (!user.isBot ? { score: () => '"score" + 1' } : {}),
|
||||
})
|
||||
.where('id = :id', { id: note.id })
|
||||
.execute();
|
||||
|
||||
// 30%の確率でハイライト用ランキング更新
|
||||
if (Math.random() < 0.3 && note.userId !== user.id) {
|
||||
if (note.channelId != null) {
|
||||
this.featuredService.updateInChannelNotesRanking(note.id, note.channelId, 1);
|
||||
} else if (note.visibility === 'public' && note.userHost == null) {
|
||||
this.featuredService.updateGlobalNotesRanking(note.id, 1);
|
||||
}
|
||||
}
|
||||
|
||||
const meta = await this.metaService.fetch();
|
||||
|
||||
if (meta.enableChartsForRemoteUser || (user.host == null)) {
|
||||
@@ -275,8 +289,6 @@ export class ReactionService {
|
||||
.where('id = :id', { id: note.id })
|
||||
.execute();
|
||||
|
||||
if (!user.isBot) this.notesRepository.decrement({ id: note.id }, 'score', 1);
|
||||
|
||||
this.globalEventService.publishNoteStream(note.id, 'unreacted', {
|
||||
reaction: this.decodeReaction(exist.reaction).reaction,
|
||||
userId: user.id,
|
||||
|
@@ -450,19 +450,4 @@ export class NoteEntityService implements OnModuleInit {
|
||||
}
|
||||
return emojis.filter(x => x.name != null && x.host != null) as { name: string; host: string; }[];
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async countSameRenotes(userId: string, renoteId: string, excludeNoteId: string | undefined): Promise<number> {
|
||||
// 指定したユーザーの指定したノートのリノートがいくつあるか数える
|
||||
const query = this.notesRepository.createQueryBuilder('note')
|
||||
.where('note.userId = :userId', { userId })
|
||||
.andWhere('note.renoteId = :renoteId', { renoteId });
|
||||
|
||||
// 指定した投稿を除く
|
||||
if (excludeNoteId) {
|
||||
query.andWhere('note.id != :excludeNoteId', { excludeNoteId });
|
||||
}
|
||||
|
||||
return await query.getCount();
|
||||
}
|
||||
}
|
||||
|
@@ -146,64 +146,76 @@ export class UserEntityService implements OnModuleInit {
|
||||
|
||||
@bindThis
|
||||
public async getRelation(me: MiUser['id'], target: MiUser['id']) {
|
||||
const following = await this.followingsRepository.findOneBy({
|
||||
followerId: me,
|
||||
followeeId: target,
|
||||
});
|
||||
return awaitAll({
|
||||
id: target,
|
||||
const [
|
||||
following,
|
||||
isFollowing: following != null,
|
||||
isFollowed: this.followingsRepository.count({
|
||||
isFollowed,
|
||||
hasPendingFollowRequestFromYou,
|
||||
hasPendingFollowRequestToYou,
|
||||
isBlocking,
|
||||
isBlocked,
|
||||
isMuted,
|
||||
isRenoteMuted,
|
||||
] = await Promise.all([
|
||||
this.followingsRepository.findOneBy({
|
||||
followerId: me,
|
||||
followeeId: target,
|
||||
}),
|
||||
this.followingsRepository.exist({
|
||||
where: {
|
||||
followerId: target,
|
||||
followeeId: me,
|
||||
},
|
||||
take: 1,
|
||||
}).then(n => n > 0),
|
||||
hasPendingFollowRequestFromYou: this.followRequestsRepository.count({
|
||||
}),
|
||||
this.followRequestsRepository.exist({
|
||||
where: {
|
||||
followerId: me,
|
||||
followeeId: target,
|
||||
},
|
||||
take: 1,
|
||||
}).then(n => n > 0),
|
||||
hasPendingFollowRequestToYou: this.followRequestsRepository.count({
|
||||
}),
|
||||
this.followRequestsRepository.exist({
|
||||
where: {
|
||||
followerId: target,
|
||||
followeeId: me,
|
||||
},
|
||||
take: 1,
|
||||
}).then(n => n > 0),
|
||||
isBlocking: this.blockingsRepository.count({
|
||||
}),
|
||||
this.blockingsRepository.exist({
|
||||
where: {
|
||||
blockerId: me,
|
||||
blockeeId: target,
|
||||
},
|
||||
take: 1,
|
||||
}).then(n => n > 0),
|
||||
isBlocked: this.blockingsRepository.count({
|
||||
}),
|
||||
this.blockingsRepository.exist({
|
||||
where: {
|
||||
blockerId: target,
|
||||
blockeeId: me,
|
||||
},
|
||||
take: 1,
|
||||
}).then(n => n > 0),
|
||||
isMuted: this.mutingsRepository.count({
|
||||
}),
|
||||
this.mutingsRepository.exist({
|
||||
where: {
|
||||
muterId: me,
|
||||
muteeId: target,
|
||||
},
|
||||
take: 1,
|
||||
}).then(n => n > 0),
|
||||
isRenoteMuted: this.renoteMutingsRepository.count({
|
||||
}),
|
||||
this.renoteMutingsRepository.exist({
|
||||
where: {
|
||||
muterId: me,
|
||||
muteeId: target,
|
||||
},
|
||||
take: 1,
|
||||
}).then(n => n > 0),
|
||||
});
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
id: target,
|
||||
following,
|
||||
isFollowing: following != null,
|
||||
isFollowed,
|
||||
hasPendingFollowRequestFromYou,
|
||||
hasPendingFollowRequestToYou,
|
||||
isBlocking,
|
||||
isBlocked,
|
||||
isMuted,
|
||||
isRenoteMuted,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -290,24 +302,6 @@ export class UserEntityService implements OnModuleInit {
|
||||
|
||||
const user = typeof src === 'object' ? src : await this.usersRepository.findOneByOrFail({ id: src });
|
||||
|
||||
// migration
|
||||
if (user.avatarId != null && user.avatarUrl === null) {
|
||||
const avatar = await this.driveFilesRepository.findOneByOrFail({ id: user.avatarId });
|
||||
user.avatarUrl = this.driveFileEntityService.getPublicUrl(avatar, 'avatar');
|
||||
this.usersRepository.update(user.id, {
|
||||
avatarUrl: user.avatarUrl,
|
||||
avatarBlurhash: avatar.blurhash,
|
||||
});
|
||||
}
|
||||
if (user.bannerId != null && user.bannerUrl === null) {
|
||||
const banner = await this.driveFilesRepository.findOneByOrFail({ id: user.bannerId });
|
||||
user.bannerUrl = this.driveFileEntityService.getPublicUrl(banner);
|
||||
this.usersRepository.update(user.id, {
|
||||
bannerUrl: user.bannerUrl,
|
||||
bannerBlurhash: banner.blurhash,
|
||||
});
|
||||
}
|
||||
|
||||
const meId = me ? me.id : null;
|
||||
const isMe = meId === user.id;
|
||||
const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false;
|
||||
|
@@ -138,11 +138,6 @@ export class MiNote {
|
||||
})
|
||||
public url: string | null;
|
||||
|
||||
@Column('integer', {
|
||||
default: 0, select: false,
|
||||
})
|
||||
public score: number;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
|
@@ -14,7 +14,6 @@ export class MiNoteReaction {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column('timestamp with time zone', {
|
||||
comment: 'The created date of the NoteReaction.',
|
||||
})
|
||||
|
@@ -85,6 +85,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
isModerator: isModerator,
|
||||
isSilenced: isSilenced,
|
||||
isSuspended: user.isSuspended,
|
||||
isHibernated: user.isHibernated,
|
||||
lastActiveDate: user.lastActiveDate,
|
||||
moderationNote: profile.moderationNote ?? '',
|
||||
signins,
|
||||
|
@@ -57,6 +57,12 @@ export const meta = {
|
||||
id: 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a',
|
||||
},
|
||||
|
||||
cannotRenoteDueToVisibility: {
|
||||
message: 'You can not Renote due to target visibility.',
|
||||
code: 'CANNOT_RENOTE_DUE_TO_VISIBILITY',
|
||||
id: 'be9529e9-fe72-4de0-ae43-0b363c4938af',
|
||||
},
|
||||
|
||||
noSuchReplyTarget: {
|
||||
message: 'No such reply target.',
|
||||
code: 'NO_SUCH_REPLY_TARGET',
|
||||
@@ -231,6 +237,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
throw new ApiError(meta.errors.youHaveBeenBlocked);
|
||||
}
|
||||
}
|
||||
|
||||
if (renote.visibility === 'followers' && renote.userId !== me.id) {
|
||||
// 他人のfollowers noteはreject
|
||||
throw new ApiError(meta.errors.cannotRenoteDueToVisibility);
|
||||
} else if (renote.visibility === 'specified') {
|
||||
// specified / direct noteはreject
|
||||
throw new ApiError(meta.errors.cannotRenoteDueToVisibility);
|
||||
}
|
||||
}
|
||||
|
||||
let reply: MiNote | null = null;
|
||||
|
@@ -6,9 +6,9 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { NotesRepository } from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { FeaturedService } from '@/core/FeaturedService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
@@ -40,41 +40,50 @@ export const paramDef = {
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
private globalNotesRankingCache: string[] = [];
|
||||
private globalNotesRankingCacheLastFetchedAt = 0;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
|
||||
private noteEntityService: NoteEntityService,
|
||||
private queryService: QueryService,
|
||||
private featuredService: FeaturedService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const day = 1000 * 60 * 60 * 24 * 3; // 3日前まで
|
||||
let noteIds: string[];
|
||||
if (ps.channelId) {
|
||||
noteIds = await this.featuredService.getInChannelNotesRanking(ps.channelId, 50);
|
||||
} else {
|
||||
if (this.globalNotesRankingCacheLastFetchedAt !== 0 && (Date.now() - this.globalNotesRankingCacheLastFetchedAt < 1000 * 60 * 30)) {
|
||||
noteIds = this.globalNotesRankingCache;
|
||||
} else {
|
||||
noteIds = await this.featuredService.getGlobalNotesRanking(100);
|
||||
this.globalNotesRankingCache = noteIds;
|
||||
this.globalNotesRankingCacheLastFetchedAt = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
if (noteIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
noteIds.sort((a, b) => a > b ? -1 : 1);
|
||||
noteIds.slice(ps.offset, ps.offset + ps.limit);
|
||||
|
||||
const query = this.notesRepository.createQueryBuilder('note')
|
||||
.addSelect('note.score')
|
||||
.where('note.userHost IS NULL')
|
||||
.andWhere('note.score > 0')
|
||||
.andWhere('note.createdAt > :date', { date: new Date(Date.now() - day) })
|
||||
.andWhere('note.visibility = \'public\'')
|
||||
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('note.channel', 'channel');
|
||||
|
||||
if (ps.channelId) query.andWhere('note.channelId = :channelId', { channelId: ps.channelId });
|
||||
const notes = await query.getMany();
|
||||
notes.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
|
||||
if (me) this.queryService.generateMutedUserQuery(query, me);
|
||||
if (me) this.queryService.generateBlockedUserQuery(query, me);
|
||||
|
||||
let notes = await query
|
||||
.orderBy('note.score', 'DESC')
|
||||
.limit(100)
|
||||
.getMany();
|
||||
|
||||
notes.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
||||
|
||||
notes = notes.slice(ps.offset, ps.offset + ps.limit);
|
||||
// TODO: ミュート等考慮
|
||||
|
||||
return await this.noteEntityService.packMany(notes, me);
|
||||
});
|
||||
|
@@ -746,6 +746,22 @@ describe('Timelines', () => {
|
||||
assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi');
|
||||
});
|
||||
|
||||
test.concurrent('リスインしているユーザーのチャンネルノートが含まれない', async () => {
|
||||
const [alice, bob] = await Promise.all([signup(), signup()]);
|
||||
|
||||
const channel = await api('/channels/create', { name: 'channel' }, bob).then(x => x.body);
|
||||
const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body);
|
||||
await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice);
|
||||
await sleep(1000);
|
||||
const bobNote = await post(bob, { text: 'hi', channelId: channel.id });
|
||||
|
||||
await waitForPushToTl();
|
||||
|
||||
const res = await api('/notes/user-list-timeline', { listId: list.id }, alice);
|
||||
|
||||
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
|
||||
});
|
||||
|
||||
test.concurrent('[withFiles: true] リスインしているユーザーのファイル付きノートのみ含まれる', async () => {
|
||||
const [alice, bob] = await Promise.all([signup(), signup()]);
|
||||
|
||||
|
@@ -18,13 +18,13 @@
|
||||
"dependencies": {
|
||||
"@discordapp/twemoji": "14.1.2",
|
||||
"@github/webauthn-json": "2.1.1",
|
||||
"@rollup/plugin-alias": "5.0.0",
|
||||
"@rollup/plugin-json": "6.0.0",
|
||||
"@rollup/plugin-replace": "5.0.2",
|
||||
"@rollup/pluginutils": "5.0.4",
|
||||
"@rollup/plugin-alias": "5.0.1",
|
||||
"@rollup/plugin-json": "6.0.1",
|
||||
"@rollup/plugin-replace": "5.0.3",
|
||||
"@rollup/pluginutils": "5.0.5",
|
||||
"@syuilo/aiscript": "0.16.0",
|
||||
"@tabler/icons-webfont": "2.37.0",
|
||||
"@vitejs/plugin-vue": "4.3.4",
|
||||
"@vitejs/plugin-vue": "4.4.0",
|
||||
"@vue-macros/reactivity-transform": "0.3.23",
|
||||
"@vue/compiler-sfc": "3.3.4",
|
||||
"astring": "1.8.6",
|
||||
@@ -38,7 +38,7 @@
|
||||
"chartjs-chart-matrix": "2.0.1",
|
||||
"chartjs-plugin-gradient": "0.6.1",
|
||||
"chartjs-plugin-zoom": "2.0.1",
|
||||
"chromatic": "7.2.0",
|
||||
"chromatic": "7.2.2",
|
||||
"compare-versions": "6.1.0",
|
||||
"cropperjs": "2.0.0-beta.4",
|
||||
"date-fns": "2.30.0",
|
||||
@@ -53,13 +53,13 @@
|
||||
"matter-js": "0.19.0",
|
||||
"mfm-js": "0.23.3",
|
||||
"misskey-js": "workspace:*",
|
||||
"photoswipe": "5.4.1",
|
||||
"photoswipe": "5.4.2",
|
||||
"prismjs": "1.29.0",
|
||||
"punycode": "2.3.0",
|
||||
"querystring": "0.2.1",
|
||||
"rollup": "3.29.4",
|
||||
"rollup": "4.0.0",
|
||||
"sanitize-html": "2.11.0",
|
||||
"sass": "1.68.0",
|
||||
"sass": "1.69.0",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"textarea-caret": "3.1.0",
|
||||
"three": "0.157.0",
|
||||
@@ -72,36 +72,36 @@
|
||||
"uuid": "9.0.1",
|
||||
"v-code-diff": "1.7.1",
|
||||
"vanilla-tilt": "1.8.1",
|
||||
"vite": "4.4.9",
|
||||
"vite": "4.4.11",
|
||||
"vue": "3.3.4",
|
||||
"vue-prism-editor": "2.0.0-alpha.2",
|
||||
"vuedraggable": "next"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-actions": "7.4.5",
|
||||
"@storybook/addon-essentials": "7.4.5",
|
||||
"@storybook/addon-interactions": "7.4.5",
|
||||
"@storybook/addon-links": "7.4.5",
|
||||
"@storybook/addon-storysource": "7.4.5",
|
||||
"@storybook/addons": "7.4.5",
|
||||
"@storybook/blocks": "7.4.5",
|
||||
"@storybook/core-events": "7.4.5",
|
||||
"@storybook/jest": "0.2.2",
|
||||
"@storybook/manager-api": "7.4.5",
|
||||
"@storybook/preview-api": "7.4.5",
|
||||
"@storybook/react": "7.4.5",
|
||||
"@storybook/react-vite": "7.4.5",
|
||||
"@storybook/testing-library": "0.2.1",
|
||||
"@storybook/theming": "7.4.5",
|
||||
"@storybook/types": "7.4.5",
|
||||
"@storybook/vue3": "7.4.5",
|
||||
"@storybook/vue3-vite": "7.4.5",
|
||||
"@storybook/addon-actions": "7.4.6",
|
||||
"@storybook/addon-essentials": "7.4.6",
|
||||
"@storybook/addon-interactions": "7.4.6",
|
||||
"@storybook/addon-links": "7.4.6",
|
||||
"@storybook/addon-storysource": "7.4.6",
|
||||
"@storybook/addons": "7.4.6",
|
||||
"@storybook/blocks": "7.4.6",
|
||||
"@storybook/core-events": "7.4.6",
|
||||
"@storybook/jest": "0.2.3",
|
||||
"@storybook/manager-api": "7.4.6",
|
||||
"@storybook/preview-api": "7.4.6",
|
||||
"@storybook/react": "7.4.6",
|
||||
"@storybook/react-vite": "7.4.6",
|
||||
"@storybook/testing-library": "0.2.2",
|
||||
"@storybook/theming": "7.4.6",
|
||||
"@storybook/types": "7.4.6",
|
||||
"@storybook/vue3": "7.4.6",
|
||||
"@storybook/vue3-vite": "7.4.6",
|
||||
"@testing-library/vue": "7.0.0",
|
||||
"@types/escape-regexp": "0.0.1",
|
||||
"@types/estree": "1.0.2",
|
||||
"@types/matter-js": "0.19.1",
|
||||
"@types/micromatch": "4.0.3",
|
||||
"@types/node": "20.7.1",
|
||||
"@types/node": "20.8.2",
|
||||
"@types/punycode": "2.1.0",
|
||||
"@types/sanitize-html": "2.9.1",
|
||||
"@types/throttle-debounce": "5.0.0",
|
||||
@@ -109,9 +109,9 @@
|
||||
"@types/uuid": "9.0.4",
|
||||
"@types/websocket": "1.0.7",
|
||||
"@types/ws": "8.5.6",
|
||||
"@typescript-eslint/eslint-plugin": "6.7.3",
|
||||
"@typescript-eslint/parser": "6.7.3",
|
||||
"@vitest/coverage-v8": "0.34.5",
|
||||
"@typescript-eslint/eslint-plugin": "6.7.4",
|
||||
"@typescript-eslint/parser": "6.7.4",
|
||||
"@vitest/coverage-v8": "0.34.6",
|
||||
"@vue/runtime-core": "3.3.4",
|
||||
"acorn": "8.10.0",
|
||||
"cross-env": "7.0.3",
|
||||
@@ -122,18 +122,18 @@
|
||||
"fast-glob": "3.3.1",
|
||||
"happy-dom": "10.0.3",
|
||||
"micromatch": "4.0.5",
|
||||
"msw": "1.3.1",
|
||||
"msw": "1.3.2",
|
||||
"msw-storybook-addon": "1.8.0",
|
||||
"nodemon": "3.0.1",
|
||||
"prettier": "3.0.3",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"start-server-and-test": "2.0.1",
|
||||
"storybook": "7.4.5",
|
||||
"storybook": "7.4.6",
|
||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||
"summaly": "github:misskey-dev/summaly",
|
||||
"vite-plugin-turbosnap": "1.0.3",
|
||||
"vitest": "0.34.5",
|
||||
"vitest": "0.34.6",
|
||||
"vitest-fetch-mock": "0.2.2",
|
||||
"vue-eslint-parser": "9.3.1",
|
||||
"vue-tsc": "1.8.15"
|
||||
|
@@ -215,7 +215,7 @@ const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false);
|
||||
const translation = ref<any>(null);
|
||||
const translating = ref(false);
|
||||
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
|
||||
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || appearNote.userId === $i.id);
|
||||
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || (appearNote.visibility === 'followers' && appearNote.userId === $i.id));
|
||||
let renoteCollapsed = $ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.userId || $i.id === appearNote.userId)) || (appearNote.myReaction != null)));
|
||||
|
||||
const keymap = {
|
||||
|
@@ -20,12 +20,12 @@
|
||||
"url": "git+https://github.com/misskey-dev/misskey.js.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/api-extractor": "7.37.2",
|
||||
"@microsoft/api-extractor": "7.38.0",
|
||||
"@swc/jest": "0.2.29",
|
||||
"@types/jest": "29.5.5",
|
||||
"@types/node": "20.7.1",
|
||||
"@typescript-eslint/eslint-plugin": "6.7.3",
|
||||
"@typescript-eslint/parser": "6.7.3",
|
||||
"@types/node": "20.8.2",
|
||||
"@typescript-eslint/eslint-plugin": "6.7.4",
|
||||
"@typescript-eslint/parser": "6.7.4",
|
||||
"eslint": "8.50.0",
|
||||
"jest": "29.7.0",
|
||||
"jest-fetch-mock": "3.0.3",
|
||||
@@ -39,7 +39,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@swc/cli": "0.1.62",
|
||||
"@swc/core": "1.3.90",
|
||||
"@swc/core": "1.3.92",
|
||||
"eventemitter3": "5.0.1",
|
||||
"reconnecting-websocket": "4.4.0"
|
||||
}
|
||||
|
@@ -14,7 +14,7 @@
|
||||
"misskey-js": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/parser": "6.7.3",
|
||||
"@typescript-eslint/parser": "6.7.4",
|
||||
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.67",
|
||||
"eslint": "8.50.0",
|
||||
"eslint-plugin-import": "2.28.1",
|
||||
|
1532
pnpm-lock.yaml
generated
1532
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user