Compare commits

...

19 Commits

Author SHA1 Message Date
anatawa12 bc8a9d54d2
Merge bb921297c7 into 3bf63dd9c5 2024-09-18 02:51:48 +09:00
かっこかり 3bf63dd9c5
fix(frontend): 設定変更時のリロード確認ダイアログが複数個表示されることがある問題を修正 (#14543)
* fix(frontend): reloadAskが同時に複数実行されないように

* Update Changelog

* fix

* フラグ解除が確実に行われるように

* reloadAskを汎用化、理由を受け取るように

* fix
2024-09-17 22:18:06 +09:00
かっこかり ce95323e49
fix(antenna): src=list && userListId=null の場合クエリータイムアウトが発生する問題を修正 (MisskeyIO#721) (#14568)
(cherry picked from commit 47b6b97c9c6d9583dd1b11acbf8f94059e81ebaf)

Co-authored-by: まっちゃとーにゅ <17376330+u1-liquid@users.noreply.github.com>
2024-09-17 22:02:34 +09:00
Sayamame-beans bb921297c7
Merge branch 'develop' into reimplement-rate-limit 2024-07-17 19:44:52 +09:00
syuilo 0265d913e5
Merge branch 'develop' into reimplement-rate-limit 2024-06-22 19:42:11 +09:00
Kisaragi 31daf10f87
fix lint error 2024-06-22 14:32:39 +09:00
anatawa12 d8941558c3
fix: レートリミットのfactorが二回適用されて二乗の効果がある問題を修正 2024-06-22 13:40:23 +09:00
anatawa12 e77bcf5ff7
Merge branch 'develop' into reimplement-rate-limit 2024-06-22 13:39:48 +09:00
anatawa12 f9dd3aca78
refactor: return rate limit instead of throwing an object 2024-06-22 13:34:52 +09:00
anatawa12 fce656b5e9
Merge branch 'develop' into reimplement-rate-limit 2024-06-15 11:08:06 +09:00
anatawa12 0ef7f14fa9
refactor: remove unnecessary max/min function 2024-06-14 17:49:52 +09:00
anatawa12 4af9c90d12
refactor: remove unnecessary return in min/max function 2024-06-14 17:49:52 +09:00
anatawa12 3eb1875804
refactor: check if there is rate limit inside min/max function 2024-06-14 17:49:52 +09:00
anatawa12 f72c768407
refactor: do not check long term limit inside min 2024-06-14 17:49:52 +09:00
anatawa12 b3561ce425
refactor: reimplement limit with async 2024-06-14 17:49:52 +09:00
anatawa12 1d6d90d62d
refactor: reimplement max/min with async 2024-06-14 17:49:52 +09:00
anatawa12 d8844aed6f
refactor: wrap ratelimiter with promise 2024-06-14 17:49:52 +09:00
anatawa12 28c8d318e1
fix: long term / short term limitがないときでもそれぞれ用のnew Limiterとlimiter.getが呼ばれる問題 2024-06-14 17:49:51 +09:00
anatawa12 03c2919b69
fix rate limit check never ends 2024-06-14 15:36:58 +09:00
12 changed files with 126 additions and 138 deletions

View File

@ -14,6 +14,7 @@
- Fix: 月の違う同じ日はセパレータが表示されないのを修正
- Fix: 縦横比が極端なカスタム絵文字を表示する際にレイアウトが崩れる箇所があるのを修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/725)
- Fix: 設定変更時のリロード確認ダイアログが複数個表示されることがある問題を修正
### Server
- Fix: アンテナの書き込み時にキーワードが与えられなかった場合のエラーをApiErrorとして投げるように
@ -149,6 +150,7 @@
- Fix: アンテナ・クリップ・リスト・ウェブフックがロールポリシーの上限より一つ多く作れてしまうのを修正 (#14036)
- Fix: notRespondingSinceが実装される前に不通になったインスタンスが自動的に配信停止にならない (#14059)
- Fix: FTT有効時、タイムライン用エンドポイントで`sinceId`にキャッシュ内最古のものより古いものを指定した場合に正しく結果が返ってこない問題を修正
- Fix: レートリミットのfactorが二回適用されて二乗の効果がある問題を修正 (#13997)
- Fix: 自分以外のクリップ内のノート個数が見えることがあるのを修正
- Fix: 空文字列のリアクションはフォールバックされるように
- Fix: リノートにリアクションできないように

2
locales/index.d.ts vendored
View File

@ -3121,7 +3121,7 @@ export interface Locale extends ILocale {
*/
"narrow": string;
/**
*
*
*/
"reloadToApplySetting": string;
/**

View File

@ -778,7 +778,7 @@ left: "左"
center: "中央"
wide: "広い"
narrow: "狭い"
reloadToApplySetting: "設定はページリロード後に反映されます。今すぐリロードしますか?"
reloadToApplySetting: "設定はページリロード後に反映されます。"
needReloadToApply: "反映には再起動が必要です。"
showTitlebar: "タイトルバーを表示する"
clearCache: "キャッシュをクリア"

View File

@ -123,11 +123,14 @@ export class AntennaService implements OnApplicationShutdown {
if (antenna.src === 'home') {
// TODO
} else if (antenna.src === 'list') {
const listUsers = (await this.userListMembershipsRepository.findBy({
userListId: antenna.userListId!,
})).map(x => x.userId);
if (!listUsers.includes(note.userId)) return false;
if (antenna.userListId == null) return false;
const exists = await this.userListMembershipsRepository.exists({
where: {
userListId: antenna.userListId,
userId: note.userId,
},
});
if (!exists) return false;
} else if (antenna.src === 'users') {
const accts = antenna.users.map(x => {
const { username, host } = Acct.parse(x);

View File

@ -326,19 +326,15 @@ export class ApiCallService implements OnApplicationShutdown {
if (factor > 0) {
// Rate limit
await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor).catch(err => {
if ('info' in err) {
// errはLimiter.LimiterInfoであることが期待される
const rateLimit = await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor);
if (rateLimit != null) {
throw new ApiError({
message: 'Rate limit exceeded. Please try again later.',
code: 'RATE_LIMIT_EXCEEDED',
id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
httpStatusCode: 429,
}, err.info);
} else {
throw new TypeError('information must be a rate-limiter information.');
}, rateLimit.info);
}
});
}
}

View File

@ -12,6 +12,14 @@ import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
import type { IEndpointMeta } from './endpoints.js';
type RateLimitInfo = {
code: 'BRIEF_REQUEST_INTERVAL',
info: Limiter.LimiterInfo,
} | {
code: 'RATE_LIMIT_EXCEEDED',
info: Limiter.LimiterInfo,
}
@Injectable()
export class RateLimiterService {
private logger: Logger;
@ -31,77 +39,57 @@ export class RateLimiterService {
}
@bindThis
public limit(limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string, factor = 1) {
{
private checkLimiter(options: Limiter.LimiterOption): Promise<Limiter.LimiterInfo> {
return new Promise<Limiter.LimiterInfo>((resolve, reject) => {
new Limiter(options).get((err, info) => {
if (err) {
return reject(err);
}
resolve(info);
});
});
}
@bindThis
public async limit(limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string, factor = 1): Promise<RateLimitInfo | null> {
if (this.disabled) {
return Promise.resolve();
return null;
}
// Short-term limit
const min = new Promise<void>((ok, reject) => {
const minIntervalLimiter = new Limiter({
if (limitation.minInterval != null) {
const info = await this.checkLimiter({
id: `${actor}:${limitation.key}:min`,
duration: limitation.minInterval! * factor,
duration: limitation.minInterval * factor,
max: 1,
db: this.redisClient,
});
minIntervalLimiter.get((err, info) => {
if (err) {
return reject({ code: 'ERR', info });
}
this.logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`);
if (info.remaining === 0) {
return reject({ code: 'BRIEF_REQUEST_INTERVAL', info });
} else {
if (hasLongTermLimit) {
return max.then(ok, reject);
} else {
return ok();
// eslint-disable-next-line no-throw-literal
return { code: 'BRIEF_REQUEST_INTERVAL', info };
}
}
});
});
// Long term limit
const max = new Promise<void>((ok, reject) => {
const limiter = new Limiter({
if (limitation.duration != null && limitation.max != null) {
const info = await this.checkLimiter({
id: `${actor}:${limitation.key}`,
duration: limitation.duration! * factor,
max: limitation.max! / factor,
duration: limitation.duration,
max: limitation.max / factor,
db: this.redisClient,
});
limiter.get((err, info) => {
if (err) {
return reject({ code: 'ERR', info });
}
this.logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`);
if (info.remaining === 0) {
return reject({ code: 'RATE_LIMIT_EXCEEDED', info });
} else {
return ok();
// eslint-disable-next-line no-throw-literal
return { code: 'RATE_LIMIT_EXCEEDED', info };
}
}
});
});
const hasShortTermLimit = typeof limitation.minInterval === 'number';
const hasLongTermLimit =
typeof limitation.duration === 'number' &&
typeof limitation.max === 'number';
if (hasShortTermLimit) {
return min;
} else if (hasLongTermLimit) {
return max;
} else {
return Promise.resolve();
}
}
return null;
}
}

View File

@ -73,10 +73,9 @@ export class SigninApiService {
return { error };
}
try {
// not more than 1 attempt per second and not more than 10 attempts per hour
await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip));
} catch (err) {
const rateLimit = await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip));
if (rateLimit != null) {
reply.code(429);
return {
error: {

View File

@ -258,7 +258,7 @@ import { langs } from '@@/js/config.js';
import { defaultStore } from '@/store.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { unisonReload } from '@/scripts/unison-reload.js';
import { reloadAsk } from '@/scripts/reload-ask.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { miLocalStorage } from '@/local-storage.js';
@ -270,16 +270,6 @@ const fontSize = ref(miLocalStorage.getItem('fontSize'));
const useSystemFont = ref(miLocalStorage.getItem('useSystemFont') != null);
const dataSaver = ref(defaultStore.state.dataSaver);
async function reloadAsk() {
const { canceled } = await os.confirm({
type: 'info',
text: i18n.ts.reloadToApplySetting,
});
if (canceled) return;
unisonReload();
}
const hemisphere = computed(defaultStore.makeGetterSetter('hemisphere'));
const overridedDeviceKind = computed(defaultStore.makeGetterSetter('overridedDeviceKind'));
const serverDisconnectedBehavior = computed(defaultStore.makeGetterSetter('serverDisconnectedBehavior'));
@ -369,7 +359,7 @@ watch([
confirmWhenRevealingSensitiveMedia,
contextMenu,
], async () => {
await reloadAsk();
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
});
const emojiIndexLangs = ['en-US', 'ja-JP', 'ja-JP_hira'] as const;

View File

@ -54,7 +54,7 @@ import MkContainer from '@/components/MkContainer.vue';
import * as os from '@/os.js';
import { navbarItemDef } from '@/navbar.js';
import { defaultStore } from '@/store.js';
import { unisonReload } from '@/scripts/unison-reload.js';
import { reloadAsk } from '@/scripts/reload-ask.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
@ -67,16 +67,6 @@ const items = ref(defaultStore.state.menu.map(x => ({
const menuDisplay = computed(defaultStore.makeGetterSetter('menuDisplay'));
async function reloadAsk() {
const { canceled } = await os.confirm({
type: 'info',
text: i18n.ts.reloadToApplySetting,
});
if (canceled) return;
unisonReload();
}
async function addItem() {
const menu = Object.keys(navbarItemDef).filter(k => !defaultStore.state.menu.includes(k));
const { canceled, result: item } = await os.select({
@ -100,7 +90,7 @@ function removeItem(index: number) {
async function save() {
defaultStore.set('menu', items.value.map(x => x.type));
await reloadAsk();
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
}
function reset() {
@ -111,7 +101,7 @@ function reset() {
}
watch(menuDisplay, async () => {
await reloadAsk();
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
});
const headerActions = computed(() => []);

View File

@ -98,7 +98,7 @@ import { defaultStore } from '@/store.js';
import { signout, signinRequired } from '@/account.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { unisonReload } from '@/scripts/unison-reload.js';
import { reloadAsk } from '@/scripts/reload-ask.js';
import FormSection from '@/components/form/section.vue';
const $i = signinRequired();
@ -132,16 +132,6 @@ async function deleteAccount() {
await signout();
}
async function reloadAsk() {
const { canceled } = await os.confirm({
type: 'info',
text: i18n.ts.reloadToApplySetting,
});
if (canceled) return;
unisonReload();
}
async function updateRepliesAll(withReplies: boolean) {
const { canceled } = await os.confirm({
type: 'warning',
@ -155,7 +145,7 @@ async function updateRepliesAll(withReplies: boolean) {
watch([
enableCondensedLineForAcct,
], async () => {
await reloadAsk();
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
});
const headerActions = computed(() => []);

View File

@ -88,19 +88,9 @@ import { uniqueBy } from '@/scripts/array.js';
import { fetchThemes, getThemes } from '@/theme-store.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { miLocalStorage } from '@/local-storage.js';
import { unisonReload } from '@/scripts/unison-reload.js';
import { reloadAsk } from '@/scripts/reload-ask.js';
import * as os from '@/os.js';
async function reloadAsk() {
const { canceled } = await os.confirm({
type: 'info',
text: i18n.ts.reloadToApplySetting,
});
if (canceled) return;
unisonReload();
}
const installedThemes = ref(getThemes());
const builtinThemes = getBuiltinThemesRef();
@ -148,13 +138,13 @@ watch(syncDeviceDarkMode, () => {
}
});
watch(wallpaper, () => {
watch(wallpaper, async () => {
if (wallpaper.value == null) {
miLocalStorage.removeItem('wallpaper');
} else {
miLocalStorage.setItem('wallpaper', wallpaper.value);
}
reloadAsk();
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
});
onActivated(() => {

View File

@ -0,0 +1,40 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import { unisonReload } from '@/scripts/unison-reload.js';
let isReloadConfirming = false;
export async function reloadAsk(opts: {
unison?: boolean;
reason?: string;
}) {
if (isReloadConfirming) {
return;
}
isReloadConfirming = true;
const { canceled } = await os.confirm(opts.reason == null ? {
type: 'info',
text: i18n.ts.reloadConfirm,
} : {
type: 'info',
title: i18n.ts.reloadConfirm,
text: opts.reason,
}).finally(() => {
isReloadConfirming = false;
});
if (canceled) return;
if (opts.unison) {
unisonReload();
} else {
location.reload();
}
}