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

View file

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

View file

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

View file

@ -132,7 +132,8 @@ export class ApInboxService {
if (actor.uri) {
if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
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 { bindThis } from '@/decorators.js';
import { In } from 'typeorm';
import type { Config } from '@/config.js';
@Injectable()
export class EmojiEntityService {
constructor(
@Inject(DI.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
public packSimpleNoQuery(
emoji: MiEmoji,
@ -53,7 +29,7 @@ export class EmojiEntityService {
name: emoji.name,
category: emoji.category,
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url: this.stripProxyIfOrigin(emoji.publicUrl || emoji.originalUrl),
url: emoji.publicUrl || emoji.originalUrl,
localOnly: emoji.localOnly ? true : undefined,
isSensitive: emoji.isSensitive ? true : undefined,
roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined,
@ -96,7 +72,7 @@ export class EmojiEntityService {
category: emoji.category,
host: emoji.host,
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url: this.stripProxyIfOrigin(emoji.publicUrl || emoji.originalUrl),
url: emoji.publicUrl || emoji.originalUrl,
license: emoji.license,
isSensitive: emoji.isSensitive,
localOnly: emoji.localOnly,

View file

@ -3,7 +3,7 @@
* 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 { MiUser } from './User.js';
@ -98,4 +98,3 @@ export class MiFollowing {
public followeeSharedInbox: string | null;
//#endregion
}

View file

@ -35,11 +35,6 @@ import { makeHstsHook } from './hsts.js';
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()
export class ServerService implements OnApplicationShutdown {
private logger: Logger;
@ -144,7 +139,7 @@ export class ServerService implements OnApplicationShutdown {
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 ('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;
if ('badge' in request.query) {
url = new URL(`${this.config.mediaProxy}/emoji.png`);
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url.searchParams.set('url', dbUrl);
url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl);
url.searchParams.set('badge', '1');
} else {
url = new URL(`${this.config.mediaProxy}/emoji.webp`);
// || emoji.originalUrl してるのは後方互換性のためpublicUrlはstringなので??はだめ)
url.searchParams.set('url', dbUrl);
url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl);
url.searchParams.set('emoji', '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');
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));
} else {
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;
}
// 同一ユーザーの情報を再度処理するので、使用済みのresolverを再利用してはいけない
return await this.mergePack(
me,
isActor(object) ? await this.apPersonService.createPerson(getApId(object), resolver) : null,
isPost(object) ? await this.apNoteService.createNote(getApId(object), undefined, resolver, true) : null,
isActor(object) ? await this.apPersonService.createPerson(getApId(object)) : 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 {
visibility: hidden;
position: absolute;
top: 0px;
right: 0px;
bottom: 1px;
right: 1px;
padding: 0 4px;
margin-bottom: 0 !important;
background: var(--MI_THEME-popup);

View file

@ -916,12 +916,6 @@ async function post(ev?: MouseEvent) {
}
nextTick(() => {
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');
if (postAccount.value != null ? postAccount.value.id : null) {
postAccount.value = null;

View file

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

View file

@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
"version": "2024.11.0-alpha.2",
"version": "2024.11.0-beta.4",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"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 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) {
for (const path of paths) {
try {
await cache.add(new Request(path, { credentials: 'same-origin' }));
} catch (error) {}
}
for (const path of paths) {
try {
const response = await fetch(new Request(path, { credentials: 'same-origin' }));
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) => {
ev.waitUntil((async () => {
const cache = await caches.open(STATIC_CACHE_NAME);
await cacheWithFallback(cache, PATHS_TO_CACHE);
await globalThis.skipWaiting();
})());
ev.waitUntil((async () => {
await requestStorageQuota();
const cache = await caches.open(STATIC_CACHE_NAME);
await cacheWithFallback(cache, PATHS_TO_CACHE);
await globalThis.skipWaiting();
})());
});
globalThis.addEventListener('activate', ev => {
@ -55,27 +155,31 @@ async function offlineContentHTML() {
}
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) {
ev.respondWith(
caches.match(ev.request)
.then(response => {
if (response) return response;
if (shouldCache) {
ev.respondWith(
caches.match(ev.request)
.then(async response => {
if (response) return response;
return fetch(ev.request).then(response => {
if (!response || response.status !== 200 || response.type !== 'basic') return response;
const responseToCache = response.clone();
caches.open(STATIC_CACHE_NAME)
.then(cache => {
cache.put(ev.request, responseToCache);
});
return response;
});
})
);
return;
}
const fetchResponse = await fetch(ev.request);
if (!fetchResponse || fetchResponse.status !== 200 || fetchResponse.type !== 'basic') {
return fetchResponse;
}
const blob = await fetchResponse.clone().blob();
await manageStorageSpace(blob.size);
const responseToCache = fetchResponse.clone();
const cache = await caches.open(STATIC_CACHE_NAME);
await cache.put(ev.request, responseToCache);
return fetchResponse;
})
);
return;
}
let isHTMLRequest = false;
if (ev.request.headers.get('sec-fetch-dest') === 'document') {
@ -168,9 +272,6 @@ globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEv
case 'showFollowRequests':
client = await swos.openClient('push', '/my/follow-requests', loginId);
break;
case 'edited':
if ('note' in data.body) client = await swos.openPost({ reply: data.body.note }, loginId);
break;
default:
switch (data.body.type) {
case 'receiveFollowRequest':