Compare commits

...

12 commits

Author SHA1 Message Date
fly_mc
a0fe3feb48 backend: fix duplicated import 2024-11-21 22:47:28 +08:00
fly_mc
48683b5557 sw: use StorageAPI and better cache management 2024-11-21 22:31:52 +08:00
fly_mc
0c3e70b5c3 frontend: move showActionsOnlyHover to bottom 2024-11-21 22:10:34 +08:00
fly_mc
5a3f3862f8 frontend: removed renote counts auto increase 2024-11-21 22:03:49 +08:00
fly_mc
be80b377cc frontend: removed auto count+1 in MkPostForm 2024-11-21 22:01:42 +08:00
fly_mc
d194d4e247 Merge branch 'develop' into pari-dev 2024-11-21 21:57:03 +08:00
fly_mc
1372e005a5 Revert "do not use media proxy if emoji is local"
This reverts commit e2471b85dd.
2024-11-21 21:55:50 +08:00
github-actions[bot]
752606fe88 Bump version to 2024.11.0-beta.4 2024-11-21 08:21:54 +00:00
かっこかり
7f0ae038d4
Update CHANGELOG.md 2024-11-21 17:16:06 +09:00
syuilo
9871035597
Update CHANGELOG.md 2024-11-21 15:41:01 +09:00
github-actions[bot]
a21a2c52d7 Bump version to 2024.11.0-alpha.3 2024-11-21 06:27:16 +00:00
かっこかり
c1f19fad1e
fix(backend): fix apResolver (#15010)
* fix(backend): fix apResolver

* fix

* add comments

* tweak comment
2024-11-21 14:36:24 +09:00
13 changed files with 150 additions and 106 deletions

View file

@ -8,9 +8,9 @@
### General ### General
- Feat: コンテンツの表示にログインを必須にできるように - Feat: コンテンツの表示にログインを必須にできるように
- Feat: 過去のノートを非公開化/フォロワーのみ表示可能にできるように - Feat: 過去のノートを非公開化/フォロワーのみ表示可能にできるように
- Fix: お知らせ作成時に画像URL入力欄を空欄に変更できないのを修正 ( #14976 )
- Enhance: 依存関係の更新 - Enhance: 依存関係の更新
- Enhance: l10nの更新 - Enhance: l10nの更新
- Fix: お知らせ作成時に画像URL入力欄を空欄に変更できないのを修正 ( #14976 )
### Client ### Client
- Enhance: Bull DashboardでRelationship Queueの状態も確認できるように - Enhance: Bull DashboardでRelationship Queueの状態も確認できるように
@ -67,6 +67,7 @@
- Fix: User Webhookテスト機能のMock Payloadを修正 - Fix: User Webhookテスト機能のMock Payloadを修正
- Fix: アカウント削除のモデレーションログが動作していないのを修正 (#14996) - Fix: アカウント削除のモデレーションログが動作していないのを修正 (#14996)
- Fix: リノートミュートが新規投稿通知に対して作用していなかった問題を修正 - Fix: リノートミュートが新規投稿通知に対して作用していなかった問題を修正
- Fix: セキュリティに関する修正
### Misskey.js ### Misskey.js
- Fix: Stream初期化時、別途WebSocketを指定する場合の型定義を修正 - Fix: Stream初期化時、別途WebSocketを指定する場合の型定義を修正

View file

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "2024.11.0-pari.27", "version": "2024.11.0-pari.28",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -58,7 +58,6 @@ import { trackPromise } from '@/misc/promise-tracker.js';
import { isUserRelated } from '@/misc/is-user-related.js'; import { isUserRelated } from '@/misc/is-user-related.js';
import { IdentifiableError } from '@/misc/identifiable-error.js'; import { IdentifiableError } from '@/misc/identifiable-error.js';
import { CollapsedQueue } from '@/misc/collapsed-queue.js'; import { CollapsedQueue } from '@/misc/collapsed-queue.js';
import { CacheService } from '@/core/CacheService.js';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';

View file

@ -132,7 +132,8 @@ export class ApInboxService {
if (actor.uri) { if (actor.uri) {
if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) { if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
setImmediate(() => { setImmediate(() => {
this.apPersonService.updatePerson(actor.uri, resolver); // 同一ユーザーの情報を再度処理するので、使用済みのresolverを再利用してはいけない
this.apPersonService.updatePerson(actor.uri);
}); });
} }
} }

View file

@ -11,39 +11,15 @@ import type { } from '@/models/Blocking.js';
import type { MiEmoji } from '@/models/Emoji.js'; import type { MiEmoji } from '@/models/Emoji.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { In } from 'typeorm'; import { In } from 'typeorm';
import type { Config } from '@/config.js';
@Injectable() @Injectable()
export class EmojiEntityService { export class EmojiEntityService {
constructor( constructor(
@Inject(DI.emojisRepository) @Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository, private emojisRepository: EmojisRepository,
@Inject(DI.config)
private config: Config,
) { ) {
} }
private stripProxyIfOrigin(url: string): string {
try {
const u = new URL(url);
let origin = u.origin;
if (u.origin === new URL(this.config.mediaProxy).origin) {
const innerUrl = u.searchParams.get('url');
if (innerUrl) {
origin = new URL(innerUrl).origin;
}
}
if (origin === u.origin) {
return url;
}
} catch (e) {
return url;
}
return url;
}
@bindThis @bindThis
public packSimpleNoQuery( public packSimpleNoQuery(
emoji: MiEmoji, emoji: MiEmoji,
@ -53,7 +29,7 @@ export class EmojiEntityService {
name: emoji.name, name: emoji.name,
category: emoji.category, category: emoji.category,
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ) // || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url: this.stripProxyIfOrigin(emoji.publicUrl || emoji.originalUrl), url: emoji.publicUrl || emoji.originalUrl,
localOnly: emoji.localOnly ? true : undefined, localOnly: emoji.localOnly ? true : undefined,
isSensitive: emoji.isSensitive ? true : undefined, isSensitive: emoji.isSensitive ? true : undefined,
roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined, roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined,
@ -96,7 +72,7 @@ export class EmojiEntityService {
category: emoji.category, category: emoji.category,
host: emoji.host, host: emoji.host,
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ) // || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url: this.stripProxyIfOrigin(emoji.publicUrl || emoji.originalUrl), url: emoji.publicUrl || emoji.originalUrl,
license: emoji.license, license: emoji.license,
isSensitive: emoji.isSensitive, isSensitive: emoji.isSensitive,
localOnly: emoji.localOnly, localOnly: emoji.localOnly,

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne, ViewEntity } from 'typeorm'; import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { id } from './util/id.js'; import { id } from './util/id.js';
import { MiUser } from './User.js'; import { MiUser } from './User.js';
@ -98,4 +98,3 @@ export class MiFollowing {
public followeeSharedInbox: string | null; public followeeSharedInbox: string | null;
//#endregion //#endregion
} }

View file

@ -35,11 +35,6 @@ import { makeHstsHook } from './hsts.js';
const _dirname = fileURLToPath(new URL('.', import.meta.url)); const _dirname = fileURLToPath(new URL('.', import.meta.url));
// This function is used to determine if a path is safe to redirect to.
function redirectSafePath(path: string): boolean {
return ['/files/', '/identicon/', '/proxy/', '/static-assets/', '/vite/', '/embed_vite/'].some(prefix => path.startsWith(prefix));
}
@Injectable() @Injectable()
export class ServerService implements OnApplicationShutdown { export class ServerService implements OnApplicationShutdown {
private logger: Logger; private logger: Logger;
@ -144,7 +139,7 @@ export class ServerService implements OnApplicationShutdown {
name: name, name: name,
}); });
reply.header('Content-Security-Policy', 'default-src \'none\''); reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\'');
if (emoji == null) { if (emoji == null) {
if ('fallback' in request.query) { if ('fallback' in request.query) {
@ -155,26 +150,16 @@ export class ServerService implements OnApplicationShutdown {
} }
} }
const dbUrl = emoji?.publicUrl || emoji?.originalUrl;
const dbUrlParsed = new URL(dbUrl);
const instanceUrl = new URL(this.config.url);
if (dbUrlParsed.origin === instanceUrl.origin) {
if (!redirectSafePath(dbUrlParsed.pathname)) {
return await reply.status(508);
}
return await reply.redirect(dbUrl, 301);
}
let url: URL; let url: URL;
if ('badge' in request.query) { if ('badge' in request.query) {
url = new URL(`${this.config.mediaProxy}/emoji.png`); url = new URL(`${this.config.mediaProxy}/emoji.png`);
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ) // || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url.searchParams.set('url', dbUrl); url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl);
url.searchParams.set('badge', '1'); url.searchParams.set('badge', '1');
} else { } else {
url = new URL(`${this.config.mediaProxy}/emoji.webp`); url = new URL(`${this.config.mediaProxy}/emoji.webp`);
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ) // || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url.searchParams.set('url', dbUrl); url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl);
url.searchParams.set('emoji', '1'); url.searchParams.set('emoji', '1');
if ('static' in request.query) url.searchParams.set('static', '1'); if ('static' in request.query) url.searchParams.set('static', '1');
} }
@ -198,16 +183,6 @@ export class ServerService implements OnApplicationShutdown {
reply.header('Cache-Control', 'public, max-age=86400'); reply.header('Cache-Control', 'public, max-age=86400');
if (user) { if (user) {
const dbUrl = user?.avatarUrl ?? this.userEntityService.getIdenticonUrl(user);
const dbUrlParsed = new URL(dbUrl);
const instanceUrl = new URL(this.config.url);
if (dbUrlParsed.origin === instanceUrl.origin) {
if (!redirectSafePath(dbUrlParsed.pathname)) {
return await reply.status(508);
}
return await reply.redirect(dbUrl, 301);
}
reply.redirect(user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user)); reply.redirect(user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user));
} else { } else {
reply.redirect('/static-assets/user-unknown.png'); reply.redirect('/static-assets/user-unknown.png');

View file

@ -137,10 +137,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (local != null) return local; if (local != null) return local;
} }
// 同一ユーザーの情報を再度処理するので、使用済みのresolverを再利用してはいけない
return await this.mergePack( return await this.mergePack(
me, me,
isActor(object) ? await this.apPersonService.createPerson(getApId(object), resolver) : null, isActor(object) ? await this.apPersonService.createPerson(getApId(object)) : null,
isPost(object) ? await this.apNoteService.createNote(getApId(object), undefined, resolver, true) : null, isPost(object) ? await this.apNoteService.createNote(getApId(object), undefined, undefined, true) : null,
); );
} }

View file

@ -779,8 +779,8 @@ function emitUpdReaction(emoji: string, delta: number) {
.footer { .footer {
visibility: hidden; visibility: hidden;
position: absolute; position: absolute;
top: 0px; bottom: 1px;
right: 0px; right: 1px;
padding: 0 4px; padding: 0 4px;
margin-bottom: 0 !important; margin-bottom: 0 !important;
background: var(--MI_THEME-popup); background: var(--MI_THEME-popup);

View file

@ -916,12 +916,6 @@ async function post(ev?: MouseEvent) {
} }
nextTick(() => { nextTick(() => {
deleteDraft(); deleteDraft();
if (props.reply) {
props.reply.repliesCount = (props.reply.repliesCount || 0) + 1;
}
if (props.quote) {
props.renote.renoteCount = (props.renote.renoteCount || 0) + 1;
}
emit('posted'); emit('posted');
if (postAccount.value != null ? postAccount.value.id : null) { if (postAccount.value != null ? postAccount.value.id : null) {
postAccount.value = null; postAccount.value = null;

View file

@ -576,7 +576,6 @@ export function getRenoteMenu(props: {
channelId: appearNote.channelId, channelId: appearNote.channelId,
}).then(() => { }).then(() => {
os.toast(i18n.ts.renoted); os.toast(i18n.ts.renoted);
appearNote.renoteCount = (appearNote.renoteCount || 0) + 1;
}); });
} }
}, },
@ -625,7 +624,6 @@ export function getRenoteMenu(props: {
renoteId: appearNote.id, renoteId: appearNote.id,
}).then(() => { }).then(() => {
os.toast(i18n.ts.renoted); os.toast(i18n.ts.renoted);
appearNote.renoteCount = (appearNote.renoteCount || 0) + 1;
}); });
} }
}, },
@ -667,7 +665,6 @@ export function getRenoteMenu(props: {
channelId: channel.id, channelId: channel.id,
}).then(() => { }).then(() => {
os.toast(i18n.tsx.renotedToX({ name: channel.name })); os.toast(i18n.tsx.renotedToX({ name: channel.name }));
appearNote.renoteCount = (appearNote.renoteCount || 0) + 1;
}); });
} }
}, },

View file

@ -1,7 +1,7 @@
{ {
"type": "module", "type": "module",
"name": "misskey-js", "name": "misskey-js",
"version": "2024.11.0-alpha.2", "version": "2024.11.0-beta.4",
"description": "Misskey SDK for JavaScript", "description": "Misskey SDK for JavaScript",
"license": "MIT", "license": "MIT",
"main": "./built/index.js", "main": "./built/index.js",

View file

@ -14,21 +14,121 @@ import * as swos from '@/scripts/operations.js';
const STATIC_CACHE_NAME = `misskey-static-${_VERSION_}`; const STATIC_CACHE_NAME = `misskey-static-${_VERSION_}`;
const PATHS_TO_CACHE = ['/assets/', '/static-assets/', '/emoji/', '/twemoji/', '/fluent-emoji/', '/vite/']; const PATHS_TO_CACHE = ['/assets/', '/static-assets/', '/emoji/', '/twemoji/', '/fluent-emoji/', '/vite/'];
const STORAGE_QUOTA = 2 * 1024 * 1024 * 1024; // 2GB in bytes
const EMOJI_PATH = '/emoji/';
async function requestStorageQuota() {
try {
if (!('storage' in navigator)) {
throw new Error('Storage API not supported');
}
if ('persist' in navigator) {
const isPersisted = await navigator.storage.persist();
console.log(`Persisted storage granted: ${isPersisted}`);
}
if ('estimate' in navigator.storage) {
const estimate = await navigator.storage.estimate();
const currentQuota = estimate.quota || 0;
const currentUsage = estimate.usage || 0;
console.log(`Current storage: ${currentUsage} of ${currentQuota} bytes used`);
if ('requestQuota' in navigator.storage) {
try {
const grantedQuota = await navigator.storage.requestQuota(STORAGE_QUOTA);
console.log(`Granted quota: ${grantedQuota} bytes`);
} catch (quotaError) {
console.warn('Failed to request additional quota:', quotaError);
}
}
return {
quota: currentQuota,
usage: currentUsage
};
} else {
console.warn('Storage estimate API not supported');
return {
quota: 0,
usage: 0
};
}
} catch (error) {
console.error('Failed to request storage quota:', error);
return {
quota: 0,
usage: 0
};
}
}
async function manageStorageSpace(newRequestSize = 0) {
try {
if (!('storage' in navigator)) {
console.warn('Storage API not supported');
return;
}
const estimate = await navigator.storage.estimate();
const currentUsage = estimate.usage || 0;
const currentQuota = estimate.quota || STORAGE_QUOTA;
if (currentUsage + newRequestSize > currentQuota) {
console.log(`Storage space needed. Current usage: ${currentUsage}, Need: ${newRequestSize}, Quota: ${currentQuota}`);
const cache = await caches.open(STATIC_CACHE_NAME);
const keys = await cache.keys();
const emojiKeys = keys.filter(request => request.url.includes(EMOJI_PATH));
console.log(`Found ${emojiKeys.length} emoji caches to manage`);
if (emojiKeys.length > 0) {
for (const key of emojiKeys) {
await cache.delete(key);
console.log(`Deleted cache for: ${key.url}`);
const newEstimate = await navigator.storage.estimate();
const newUsage = newEstimate.usage || 0;
if (newUsage + newRequestSize <= currentQuota) {
console.log(`Sufficient space cleared. New usage: ${newUsage}`);
break;
}
}
} else {
console.warn('No emoji caches available for cleanup');
}
}
} catch (error) {
console.error('Failed to manage storage space:', error);
}
}
async function cacheWithFallback(cache, paths) { async function cacheWithFallback(cache, paths) {
for (const path of paths) { for (const path of paths) {
try { try {
await cache.add(new Request(path, { credentials: 'same-origin' })); const response = await fetch(new Request(path, { credentials: 'same-origin' }));
} catch (error) {} const blob = await response.clone().blob();
}
await manageStorageSpace(blob.size);
await cache.put(new Request(path, { credentials: 'same-origin' }), response);
} catch (error) {
console.error(`Failed to cache ${path}:`, error);
}
}
} }
globalThis.addEventListener('install', (ev) => { globalThis.addEventListener('install', (ev) => {
ev.waitUntil((async () => { ev.waitUntil((async () => {
const cache = await caches.open(STATIC_CACHE_NAME); await requestStorageQuota();
await cacheWithFallback(cache, PATHS_TO_CACHE);
await globalThis.skipWaiting(); const cache = await caches.open(STATIC_CACHE_NAME);
})()); await cacheWithFallback(cache, PATHS_TO_CACHE);
await globalThis.skipWaiting();
})());
}); });
globalThis.addEventListener('activate', ev => { globalThis.addEventListener('activate', ev => {
@ -55,27 +155,31 @@ async function offlineContentHTML() {
} }
globalThis.addEventListener('fetch', ev => { globalThis.addEventListener('fetch', ev => {
const shouldCache = PATHS_TO_CACHE.some(path => ev.request.url.includes(path)); const shouldCache = PATHS_TO_CACHE.some(path => ev.request.url.includes(path));
if (shouldCache) { if (shouldCache) {
ev.respondWith( ev.respondWith(
caches.match(ev.request) caches.match(ev.request)
.then(response => { .then(async response => {
if (response) return response; if (response) return response;
return fetch(ev.request).then(response => { const fetchResponse = await fetch(ev.request);
if (!response || response.status !== 200 || response.type !== 'basic') return response; if (!fetchResponse || fetchResponse.status !== 200 || fetchResponse.type !== 'basic') {
const responseToCache = response.clone(); return fetchResponse;
caches.open(STATIC_CACHE_NAME) }
.then(cache => {
cache.put(ev.request, responseToCache); const blob = await fetchResponse.clone().blob();
}); await manageStorageSpace(blob.size);
return response;
}); const responseToCache = fetchResponse.clone();
}) const cache = await caches.open(STATIC_CACHE_NAME);
); await cache.put(ev.request, responseToCache);
return;
} return fetchResponse;
})
);
return;
}
let isHTMLRequest = false; let isHTMLRequest = false;
if (ev.request.headers.get('sec-fetch-dest') === 'document') { if (ev.request.headers.get('sec-fetch-dest') === 'document') {
@ -168,9 +272,6 @@ globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEv
case 'showFollowRequests': case 'showFollowRequests':
client = await swos.openClient('push', '/my/follow-requests', loginId); client = await swos.openClient('push', '/my/follow-requests', loginId);
break; break;
case 'edited':
if ('note' in data.body) client = await swos.openPost({ reply: data.body.note }, loginId);
break;
default: default:
switch (data.body.type) { switch (data.body.type) {
case 'receiveFollowRequest': case 'receiveFollowRequest':