Compare commits

...

32 commits

Author SHA1 Message Date
Mizah a858ee31c6 Added doc comment to alert()
Some checks failed
Check SPDX-License-Identifier / check-spdx-license-id (push) Has been cancelled
Check copyright year / check_copyright_year (push) Has been cancelled
Publish Docker image (develop) / Build (linux/amd64) (push) Has been cancelled
Publish Docker image (develop) / Build (linux/arm64) (push) Has been cancelled
Dockle / dockle (push) Has been cancelled
Lint / pnpm_install (push) Has been cancelled
Storybook / build (push) Has been cancelled
Test (frontend) / vitest (20.16.0) (push) Has been cancelled
Test (frontend) / e2e (chrome, 20.16.0) (push) Has been cancelled
Test (production install and build) / production (20.16.0) (push) Has been cancelled
Publish Docker image (develop) / merge (push) Has been cancelled
Lint / lint (backend) (push) Has been cancelled
Lint / lint (frontend) (push) Has been cancelled
Lint / lint (frontend-embed) (push) Has been cancelled
Lint / lint (frontend-shared) (push) Has been cancelled
Lint / lint (misskey-bubble-game) (push) Has been cancelled
Lint / lint (misskey-js) (push) Has been cancelled
Lint / lint (misskey-reversi) (push) Has been cancelled
Lint / lint (sw) (push) Has been cancelled
Lint / typecheck (backend) (push) Has been cancelled
Lint / typecheck (misskey-js) (push) Has been cancelled
Lint / typecheck (sw) (push) Has been cancelled
2024-11-17 14:34:54 +02:00
Mizah 0bd7ed8191 Added doc comment to toast() 2024-11-17 14:34:47 +02:00
Mizah ee574ae154 Added doc comment to pageWindow 2024-11-17 14:34:39 +02:00
Mizah a505f36252 Added doc comment to popup 2024-11-17 14:34:31 +02:00
Mizah a516383c66 Added internal comments to popup() 2024-11-17 14:34:21 +02:00
Mizah 1613dafc39 Added docs to z-index generator. 2024-11-17 14:23:44 +02:00
Mizah 8457fa9b3b Added docs to popup list and popup id counter. 2024-11-17 14:23:31 +02:00
Mizah 2e51e779e7 Added docs idb proxy.
Some checks are pending
Check SPDX-License-Identifier / check-spdx-license-id (push) Waiting to run
Check copyright year / check_copyright_year (push) Waiting to run
Publish Docker image (develop) / Build (linux/amd64) (push) Waiting to run
Publish Docker image (develop) / Build (linux/arm64) (push) Waiting to run
Publish Docker image (develop) / merge (push) Blocked by required conditions
Dockle / dockle (push) Waiting to run
Storybook / build (push) Waiting to run
Test (production install and build) / production (20.16.0) (push) Waiting to run
2024-11-17 14:02:17 +02:00
Mizah 7f6b486976 Added docs to Keys for localStorage. 2024-11-17 14:02:11 +02:00
Mizah 8c1508fae4 Added docs to miLocalStorage. 2024-11-17 14:02:08 +02:00
Mizah aeb568664d Added docs (and observations) to notes count. 2024-11-17 14:02:02 +02:00
Mizah ecb990fb77 Added docs to $i, iAmModerator, iAmAdmin. 2024-11-17 14:01:53 +02:00
饺子w (Yumechi) a11b77a415
fix(backend): Webhook Test一致性 (#14863)
* fix(backend): Webhook Test一致性

Signed-off-by: eternal-flame-AD <yume@yumechi.jp>

* UserWebhookPayload<'followed'> 修正

Signed-off-by: eternal-flame-AD <yume@yumechi.jp>

---------

Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
2024-11-12 09:51:18 +09:00
syuilo 1bc4f400c0 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2024-11-11 16:35:23 +09:00
syuilo 458c72c153 Update about-misskey.vue 2024-11-11 16:35:13 +09:00
syuilo 6bd3ed2074
Update CHANGELOG.md 2024-11-10 15:10:04 +09:00
かっこかり 31e5f0bd09
fix(frontend): メールアドレス登録有効化時の「完了」ダイアログボックスの表示条件を修正 (#14928)
* fix(frontend): メールアドレス登録有効化時の「完了」ダイアログボックスの表示条件を修正

* Update MkSignupDialog.form.vue

* fix condition
2024-11-10 15:08:58 +09:00
かっこかり e0a83e9c9e
Update CHANGELOG.md (書き方を揃える) 2024-11-09 15:57:10 +09:00
かっこかり 1496700b37
Update CHANGELOG.md
たぶんリリースワークフローはこうしないと認識してくれない
2024-11-09 15:51:49 +09:00
syuilo 00cbf9fe80
Update CONTRIBUTING.md 2024-11-09 14:09:02 +09:00
github-actions[bot] cf09aa21f0 Bump version to 2024.11.0-alpha.0 2024-11-09 02:28:02 +00:00
github-actions[bot] 9f7d41eb47 Bump version to 2024.10.2-alpha.3 2024-11-09 02:25:42 +00:00
かっこかり 4a62051ce7
fix(backend): ローカルユーザーへのメンションを含むノートが連合される際に正しいURLに変換されないことがある問題を修正 (#14879)
* fix: make sure mentions of local users get rendered correctly during AP delivery (resolves #645)

* Update Changelog

* indent

---------

Co-authored-by: Laura Hausmann <laura@hausmann.dev>
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2024-11-09 10:58:09 +09:00
かっこかり 3a421837bf
refactor(frontend): 動画UIのフルスクリーン周りの調整 (#14877)
* refactor(frontend): フルスクリーン周りの調整

(cherry picked from commit 783032caec5853d78d5af3391e29cf364f2282e8)

* refactor(frontend): deviceKindの循環参照を除去

(cherry picked from commit 1ca471f57e968a1a6e2259bde4a7c6da1fe0c54e)

* fix

---------

Co-authored-by: taiyme <53635909+taiyme@users.noreply.github.com>
2024-11-09 10:57:04 +09:00
momoirodouhu a4c5ce1413
enhance(backend) : リモートユーザーの照会をオリジナルにリダイレクトするように (#12892) (#14897)
* enhance(backend) : リモートユーザーの照会をオリジナルにリダイレクトするように (#12892)

* オリジンリダイレクトのテストをtodoとして追加。

e2eテストにリモートユーザー考慮のテストがなさそうなので。

次のコマンドで動くことは確認済みです。
curl "http://localhost:3000/@foo@bar" -H "accept: application/activity+json" -L

* Acctのパースを既存のパーサーでするように修正

* lint
2024-11-09 10:54:44 +09:00
かっこかり e75b62f3f5
enhance(frontend): 個別お知らせページではmetaタグを出力するように (#14902)
* enhance(frontend): 個別お知らせページではmetaタグを出力するように

* Update Changelog
2024-11-09 10:53:09 +09:00
かっこかり 5b60ae810b
fix(frontend): 外部URLへのリダイレクトのバリデーションを強化 (#14919)
* Fix code scanning alert no. 25: Incomplete URL scheme check (MisskeyIO#799)

* Fix code scanning alert no. 26: Incomplete URL scheme check

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* Fix code scanning alert no. 25: Incomplete URL scheme check

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
(cherry picked from commit 7d7552e076c0152a5966e919be0e9a60b3736208)

* ✌️

---------

Co-authored-by: あわわわとーにゅ <17376330+u1-liquid@users.noreply.github.com>
2024-11-09 10:52:07 +09:00
かっこかり 98b4717c45
fix(backend): SQLのサニタイズを強化 (#14920)
* Fix code scanning alert no. 28: Incomplete string escaping or encoding (MisskeyIO#800)

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
(cherry picked from commit 443335c662b14f609d6a81a8f3807e95709aebc1)

* ✌️

---------

Co-authored-by: あわわわとーにゅ <17376330+u1-liquid@users.noreply.github.com>
2024-11-09 10:51:28 +09:00
syuilo 8a4ce16e90
Update CONTRIBUTING.md 2024-11-08 18:00:55 +09:00
4ster1sk 794cb9ffe2
fix(backend): followedMessageではなくdescriptionになっていたのを修正 (#14908) 2024-11-07 17:16:51 +09:00
syuilo 0b976064ca
Update CHANGELOG.md 2024-11-07 15:10:38 +09:00
4ster1sk bca690f256
fix(backend): フォロワーへのメッセージの絵文字をemojisに含めるように (#14904) 2024-11-07 15:10:10 +09:00
32 changed files with 406 additions and 84 deletions

View file

@ -1,8 +1,10 @@
## 2024.10.2 ## 2024.11.0
### General ### General
- Feat: コンテンツの表示にログインを必須にできるように - Feat: コンテンツの表示にログインを必須にできるように
- Feat: 過去のノートを非公開化/フォロワーのみ表示可能にできるように - Feat: 過去のノートを非公開化/フォロワーのみ表示可能にできるように
- Enhance: 依存関係の更新
- Enhance: l10nの更新
### Client ### Client
- Enhance: Bull DashboardでRelationship Queueの状態も確認できるように - Enhance: Bull DashboardでRelationship Queueの状態も確認できるように
@ -15,7 +17,8 @@
- どのアカウントで認証しようとしているのかがわかるように - どのアカウントで認証しようとしているのかがわかるように
- 認証するアカウントを切り替えられるように - 認証するアカウントを切り替えられるように
- Enhance: Self-XSS防止用の警告を追加 - Enhance: Self-XSS防止用の警告を追加
- Enhance: カタルーニャ語 (ca-ES) に対応 - Enhance: カタルーニャ語 (ca-ES) に対応
- Enhance: 個別お知らせページではMetaタグを出力するように
- Fix: 通知の範囲指定の設定項目が必要ない通知設定でも範囲指定の設定がでている問題を修正 - Fix: 通知の範囲指定の設定項目が必要ない通知設定でも範囲指定の設定がでている問題を修正
- Fix: Turnstileが失敗・期限切れした際にも成功扱いとなってしまう問題を修正 - Fix: Turnstileが失敗・期限切れした際にも成功扱いとなってしまう問題を修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/768) (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/768)
@ -24,19 +27,25 @@
- Fix: リンク切れを修正 - Fix: リンク切れを修正
= Fix: ノート投稿ボタンにホバー時のスタイルが適用されていないのを修正 = Fix: ノート投稿ボタンにホバー時のスタイルが適用されていないのを修正
(Cherry-picked from https://github.com/taiyme/misskey/pull/305) (Cherry-picked from https://github.com/taiyme/misskey/pull/305)
- Fix: メールアドレス登録有効化時の「完了」ダイアログボックスの表示条件を修正
### Server ### Server
- Enhance: 起動前の疎通チェックで、DBとメイン以外のRedisの疎通確認も行うように - Enhance: 起動前の疎通チェックで、DBとメイン以外のRedisの疎通確認も行うように
(Based on https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/588) (Based on https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/588)
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/715) (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/715)
- Enhance: リモートユーザーの照会をオリジナルにリダイレクトするように
- Fix: フォロワーへのメッセージの絵文字をemojisに含めるように
- Fix: Nested proxy requestsを検出した際にブロックするように - Fix: Nested proxy requestsを検出した際にブロックするように
[ghsa-gq5q-c77c-v236](https://github.com/misskey-dev/misskey/security/advisories/ghsa-gq5q-c77c-v236) [ghsa-gq5q-c77c-v236](https://github.com/misskey-dev/misskey/security/advisories/ghsa-gq5q-c77c-v236)
- Fix: 招待コードの発行可能な残り数算出に使用すべきロールポリシーの値が違う問題を修正 - Fix: 招待コードの発行可能な残り数算出に使用すべきロールポリシーの値が違う問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/706) (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/706)
- Fix: 連合への配信時に、acctの大小文字が区別されてしまい正しくメンションが処理されないことがある問題を修正 - Fix: 連合への配信時に、acctの大小文字が区別されてしまい正しくメンションが処理されないことがある問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/711) (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/711)
- Fix: ローカルユーザーへのメンションを含むートが連合される際に正しいURLに変換されないことがある問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/712)
- Fix: FTT無効時にユーザーリストタイムラインが使用できない問題を修正 - Fix: FTT無効時にユーザーリストタイムラインが使用できない問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/709) (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/709)
- Fix: User Webhookテスト機能のMock Payloadを修正
### Misskey.js ### Misskey.js
- Fix: Stream初期化時、別途WebSocketを指定する場合の型定義を修正 - Fix: Stream初期化時、別途WebSocketを指定する場合の型定義を修正

View file

@ -83,6 +83,10 @@ One should not add property that has defined before by other implementation, or
## Reviewers guide ## Reviewers guide
Be willing to comment on the good points and not just the things you want fixed 💯 Be willing to comment on the good points and not just the things you want fixed 💯
読んでおくといいやつ
- https://blog.lacolaco.net/posts/1e2cf439b3c2/
- https://konifar-zatsu.hatenadiary.jp/entry/2024/11/05/192421
### Review perspective ### Review perspective
- Scope - Scope
- Are the goals of the PR clear? - Are the goals of the PR clear?

View file

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "2024.10.2-alpha.2", "version": "2024.11.0-alpha.0",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -407,7 +407,9 @@ export class MfmService {
const a = doc.createElement('a'); const a = doc.createElement('a');
const { username, host, acct } = node.props; const { username, host, acct } = node.props;
const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username.toLowerCase() === username.toLowerCase() && remoteUser.host?.toLowerCase() === host?.toLowerCase()); const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username.toLowerCase() === username.toLowerCase() && remoteUser.host?.toLowerCase() === host?.toLowerCase());
a.setAttribute('href', remoteUserInfo ? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri) : `${this.config.url}/${acct}`); a.setAttribute('href', remoteUserInfo
? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri)
: `${this.config.url}/${acct.endsWith(`@${this.config.url}`) ? acct.substring(0, acct.length - this.config.url.length - 1) : acct}`);
a.className = 'u-url mention'; a.className = 'u-url mention';
a.textContent = acct; a.textContent = acct;
return a; return a;

View file

@ -7,7 +7,7 @@ import { randomUUID } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import type { IActivity } from '@/core/activitypub/type.js'; import type { IActivity } from '@/core/activitypub/type.js';
import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiWebhook, webhookEventTypes } from '@/models/Webhook.js'; import type { MiWebhook, WebhookEventTypes, webhookEventTypes } from '@/models/Webhook.js';
import type { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js'; import type { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
@ -35,6 +35,7 @@ import type {
} from './QueueModule.js'; } from './QueueModule.js';
import type httpSignature from '@peertube/http-signature'; import type httpSignature from '@peertube/http-signature';
import type * as Bull from 'bullmq'; import type * as Bull from 'bullmq';
import { type UserWebhookPayload } from './UserWebhookService.js';
@Injectable() @Injectable()
export class QueueService { export class QueueService {
@ -468,10 +469,10 @@ export class QueueService {
* @see UserWebhookDeliverProcessorService * @see UserWebhookDeliverProcessorService
*/ */
@bindThis @bindThis
public userWebhookDeliver( public userWebhookDeliver<T extends WebhookEventTypes>(
webhook: MiWebhook, webhook: MiWebhook,
type: typeof webhookEventTypes[number], type: T,
content: unknown, content: UserWebhookPayload<T>,
opts?: { attempts?: number }, opts?: { attempts?: number },
) { ) {
const data: UserWebhookDeliverJobData = { const data: UserWebhookDeliverJobData = {

View file

@ -6,11 +6,23 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
import { type WebhooksRepository } from '@/models/_.js'; import { type WebhooksRepository } from '@/models/_.js';
import { MiWebhook } from '@/models/Webhook.js'; import { MiWebhook, WebhookEventTypes } from '@/models/Webhook.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { GlobalEvents } from '@/core/GlobalEventService.js'; import { GlobalEvents } from '@/core/GlobalEventService.js';
import type { OnApplicationShutdown } from '@nestjs/common'; import type { OnApplicationShutdown } from '@nestjs/common';
import type { Packed } from '@/misc/json-schema.js';
export type UserWebhookPayload<T extends WebhookEventTypes> =
T extends 'note' | 'reply' | 'renote' |'mention' ? {
note: Packed<'Note'>,
} :
T extends 'follow' | 'unfollow' ? {
user: Packed<'UserDetailedNotMe'>,
} :
T extends 'followed' ? {
user: Packed<'UserLite'>,
} : never;
@Injectable() @Injectable()
export class UserWebhookService implements OnApplicationShutdown { export class UserWebhookService implements OnApplicationShutdown {

View file

@ -10,7 +10,7 @@ import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWeb
import { SystemWebhookService } from '@/core/SystemWebhookService.js'; import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { Packed } from '@/misc/json-schema.js'; import { Packed } from '@/misc/json-schema.js';
import { type WebhookEventTypes } from '@/models/Webhook.js'; import { type WebhookEventTypes } from '@/models/Webhook.js';
import { UserWebhookService } from '@/core/UserWebhookService.js'; import { type UserWebhookPayload, UserWebhookService } from '@/core/UserWebhookService.js';
import { QueueService } from '@/core/QueueService.js'; import { QueueService } from '@/core/QueueService.js';
import { ModeratorInactivityRemainingTime } from '@/queue/processors/CheckModeratorsActivityProcessorService.js'; import { ModeratorInactivityRemainingTime } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
@ -306,10 +306,10 @@ export class WebhookTestService {
* - on * - on
*/ */
@bindThis @bindThis
public async testUserWebhook( public async testUserWebhook<T extends WebhookEventTypes>(
params: { params: {
webhookId: MiWebhook['id'], webhookId: MiWebhook['id'],
type: WebhookEventTypes, type: T,
override?: Partial<Omit<MiWebhook, 'id'>>, override?: Partial<Omit<MiWebhook, 'id'>>,
}, },
sender: MiUser | null, sender: MiUser | null,
@ -321,7 +321,7 @@ export class WebhookTestService {
} }
const webhook = webhooks[0]; const webhook = webhooks[0];
const send = (contents: unknown) => { const send = <U extends WebhookEventTypes>(type: U, contents: UserWebhookPayload<U>) => {
const merged = { const merged = {
...webhook, ...webhook,
...params.override, ...params.override,
@ -329,7 +329,7 @@ export class WebhookTestService {
// テスト目的なのでUserWebhookServiceの機能を経由せず直接キューに追加するチェック処理などをスキップする意図. // テスト目的なのでUserWebhookServiceの機能を経由せず直接キューに追加するチェック処理などをスキップする意図.
// また、Jobの試行回数も1回だけ. // また、Jobの試行回数も1回だけ.
this.queueService.userWebhookDeliver(merged, params.type, contents, { attempts: 1 }); this.queueService.userWebhookDeliver(merged, type, contents, { attempts: 1 });
}; };
const dummyNote1 = generateDummyNote({ const dummyNote1 = generateDummyNote({
@ -361,33 +361,40 @@ export class WebhookTestService {
switch (params.type) { switch (params.type) {
case 'note': { case 'note': {
send(toPackedNote(dummyNote1)); send('note', { note: toPackedNote(dummyNote1) });
break; break;
} }
case 'reply': { case 'reply': {
send(toPackedNote(dummyReply1)); send('reply', { note: toPackedNote(dummyReply1) });
break; break;
} }
case 'renote': { case 'renote': {
send(toPackedNote(dummyRenote1)); send('renote', { note: toPackedNote(dummyRenote1) });
break; break;
} }
case 'mention': { case 'mention': {
send(toPackedNote(dummyMention1)); send('mention', { note: toPackedNote(dummyMention1) });
break; break;
} }
case 'follow': { case 'follow': {
send(toPackedUserDetailedNotMe(dummyUser1)); send('follow', { user: toPackedUserDetailedNotMe(dummyUser1) });
break; break;
} }
case 'followed': { case 'followed': {
send(toPackedUserLite(dummyUser2)); send('followed', { user: toPackedUserLite(dummyUser2) });
break; break;
} }
case 'unfollow': { case 'unfollow': {
send(toPackedUserDetailedNotMe(dummyUser3)); send('unfollow', { user: toPackedUserDetailedNotMe(dummyUser3) });
break; break;
} }
// まだ実装されていない (#9485)
case 'reaction': return;
default: {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _exhaustiveAssertion: never = params.type;
return;
}
} }
} }

View file

@ -4,5 +4,5 @@
*/ */
export function sqlLikeEscape(s: string) { export function sqlLikeEscape(s: string) {
return s.replace(/([%_])/g, '\\$1'); return s.replace(/([\\%_])/g, '\\$1');
} }

View file

@ -29,6 +29,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { IActivity } from '@/core/activitypub/type.js'; import { IActivity } from '@/core/activitypub/type.js';
import { isQuote, isRenote } from '@/misc/is-renote.js'; import { isQuote, isRenote } from '@/misc/is-renote.js';
import * as Acct from '@/misc/acct.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify'; import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify';
import type { FindOptionsWhere } from 'typeorm'; import type { FindOptionsWhere } from 'typeorm';
@ -486,6 +487,16 @@ export class ActivityPubServerService {
return; return;
} }
// リモートだったらリダイレクト
if (user.host != null) {
if (user.uri == null || this.utilityService.isSelfHost(user.host)) {
reply.code(500);
return;
}
reply.redirect(user.uri, 301);
return;
}
reply.header('Cache-Control', 'public, max-age=180'); reply.header('Cache-Control', 'public, max-age=180');
this.setResponseType(request, reply); this.setResponseType(request, reply);
return (this.apRendererService.addContext(await this.apRendererService.renderPerson(user as MiLocalUser))); return (this.apRendererService.addContext(await this.apRendererService.renderPerson(user as MiLocalUser)));
@ -654,19 +665,20 @@ export class ActivityPubServerService {
const user = await this.usersRepository.findOneBy({ const user = await this.usersRepository.findOneBy({
id: userId, id: userId,
host: IsNull(),
isSuspended: false, isSuspended: false,
}); });
return await this.userInfo(request, reply, user); return await this.userInfo(request, reply, user);
}); });
fastify.get<{ Params: { user: string; } }>('/@:user', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => { fastify.get<{ Params: { acct: string; } }>('/@:acct', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => {
vary(reply.raw, 'Accept'); vary(reply.raw, 'Accept');
const acct = Acct.parse(request.params.acct);
const user = await this.usersRepository.findOneBy({ const user = await this.usersRepository.findOneBy({
usernameLower: request.params.user.toLowerCase(), usernameLower: acct.username,
host: IsNull(), host: acct.host ?? IsNull(),
isSuspended: false, isSuspended: false,
}); });

View file

@ -465,6 +465,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const newName = updates.name === undefined ? user.name : updates.name; const newName = updates.name === undefined ? user.name : updates.name;
const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description; const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description;
const newFields = profileUpdates.fields === undefined ? profile.fields : profileUpdates.fields; const newFields = profileUpdates.fields === undefined ? profile.fields : profileUpdates.fields;
const newFollowedMessage = profileUpdates.followedMessage === undefined ? profile.followedMessage : profileUpdates.followedMessage;
if (newName != null) { if (newName != null) {
let hasProhibitedWords = false; let hasProhibitedWords = false;
@ -494,6 +495,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
]); ]);
} }
if (newFollowedMessage != null) {
const tokens = mfm.parse(newFollowedMessage);
emojis = emojis.concat(extractCustomEmojisFromMfm(tokens));
}
updates.emojis = emojis; updates.emojis = emojis;
updates.tags = tags; updates.tags = tags;

View file

@ -42,13 +42,26 @@ import { MetaEntityService } from '@/core/entities/MetaEntityService.js';
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
import type { ChannelsRepository, ClipsRepository, FlashsRepository, GalleryPostsRepository, MiMeta, NotesRepository, PagesRepository, ReversiGamesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import type {
AnnouncementsRepository,
ChannelsRepository,
ClipsRepository,
FlashsRepository,
GalleryPostsRepository,
MiMeta,
NotesRepository,
PagesRepository,
ReversiGamesRepository,
UserProfilesRepository,
UsersRepository,
} from '@/models/_.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js'; import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { FlashEntityService } from '@/core/entities/FlashEntityService.js'; import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js'; import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js';
import { FeedService } from './FeedService.js'; import { FeedService } from './FeedService.js';
import { UrlPreviewService } from './UrlPreviewService.js'; import { UrlPreviewService } from './UrlPreviewService.js';
import { ClientLoggerService } from './ClientLoggerService.js'; import { ClientLoggerService } from './ClientLoggerService.js';
@ -103,6 +116,9 @@ export class ClientServerService {
@Inject(DI.reversiGamesRepository) @Inject(DI.reversiGamesRepository)
private reversiGamesRepository: ReversiGamesRepository, private reversiGamesRepository: ReversiGamesRepository,
@Inject(DI.announcementsRepository)
private announcementsRepository: AnnouncementsRepository,
private flashEntityService: FlashEntityService, private flashEntityService: FlashEntityService,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
@ -112,6 +128,7 @@ export class ClientServerService {
private clipEntityService: ClipEntityService, private clipEntityService: ClipEntityService,
private channelEntityService: ChannelEntityService, private channelEntityService: ChannelEntityService,
private reversiGameEntityService: ReversiGameEntityService, private reversiGameEntityService: ReversiGameEntityService,
private announcementEntityService: AnnouncementEntityService,
private urlPreviewService: UrlPreviewService, private urlPreviewService: UrlPreviewService,
private feedService: FeedService, private feedService: FeedService,
private roleService: RoleService, private roleService: RoleService,
@ -776,6 +793,24 @@ export class ClientServerService {
return await renderBase(reply); return await renderBase(reply);
} }
}); });
// 個別お知らせページ
fastify.get<{ Params: { announcementId: string; } }>('/announcements/:announcementId', async (request, reply) => {
const announcement = await this.announcementsRepository.findOneBy({
id: request.params.announcementId,
});
if (announcement) {
const _announcement = await this.announcementEntityService.pack(announcement);
reply.header('Cache-Control', 'public, max-age=3600');
return await reply.view('announcement', {
announcement: _announcement,
...await this.generateCommonPugData(this.meta),
});
} else {
return await renderBase(reply);
}
});
//#endregion //#endregion
//#region noindex pages //#region noindex pages

View file

@ -0,0 +1,21 @@
extends ./base
block vars
- const title = announcement.title;
- const description = announcement.text.length > 100 ? announcement.text.slice(0, 100) + '…' : announcement.text;
- const url = `${config.url}/announcements/${announcement.id}`;
block title
= `${title} | ${instanceName}`
block desc
meta(name='description' content=description)
block og
meta(property='og:type' content='article')
meta(property='og:title' content= title)
meta(property='og:description' content= description)
meta(property='og:url' content= url)
if announcement.imageUrl
meta(property='og:image' content=announcement.imageUrl)
meta(property='twitter:card' content='summary_large_image')

View file

@ -2,6 +2,7 @@ block vars
block loadClientEntry block loadClientEntry
- const entry = config.frontendEntry; - const entry = config.frontendEntry;
- const baseUrl = config.url;
doctype html doctype html
@ -32,7 +33,7 @@ html
link(rel='icon' href= icon || '/favicon.ico') link(rel='icon' href= icon || '/favicon.ico')
link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png') link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png')
link(rel='manifest' href='/manifest.json') link(rel='manifest' href='/manifest.json')
link(rel='search' type='application/opensearchdescription+xml' title=(title || "Misskey") href=`${url}/opensearch.xml`) link(rel='search' type='application/opensearchdescription+xml' title=(title || "Misskey") href=`${baseUrl}/opensearch.xml`)
link(rel='prefetch' href=serverErrorImageUrl) link(rel='prefetch' href=serverErrorImageUrl)
link(rel='prefetch' href=infoImageUrl) link(rel='prefetch' href=infoImageUrl)
link(rel='prefetch' href=notFoundImageUrl) link(rel='prefetch' href=notFoundImageUrl)

View file

@ -230,6 +230,7 @@ describe('Webリソース', () => {
path: path('xxxxxxxxxx'), path: path('xxxxxxxxxx'),
type: HTML, type: HTML,
})); }));
test.todo('HTMLとしてGETできる。(リモートユーザーでもリダイレクトせず)');
}); });
describe.each([ describe.each([
@ -249,6 +250,7 @@ describe('Webリソース', () => {
path: path('xxxxxxxxxx'), path: path('xxxxxxxxxx'),
accept, accept,
})); }));
test.todo('はオリジナルにリダイレクトされる。(リモートユーザー)');
}); });
}); });

View file

@ -7,7 +7,7 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { beforeAll, describe, jest } from '@jest/globals'; import { beforeAll, describe, jest } from '@jest/globals';
import { WebhookTestService } from '@/core/WebhookTestService.js'; import { WebhookTestService } from '@/core/WebhookTestService.js';
import { UserWebhookService } from '@/core/UserWebhookService.js'; import { UserWebhookPayload, UserWebhookService } from '@/core/UserWebhookService.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js'; import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { GlobalModule } from '@/GlobalModule.js'; import { GlobalModule } from '@/GlobalModule.js';
import { MiSystemWebhook, MiUser, MiWebhook, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import { MiSystemWebhook, MiUser, MiWebhook, UserProfilesRepository, UsersRepository } from '@/models/_.js';
@ -122,7 +122,7 @@ describe('WebhookTestService', () => {
const calls = queueService.userWebhookDeliver.mock.calls[0]; const calls = queueService.userWebhookDeliver.mock.calls[0];
expect((calls[0] as any).id).toBe('dummy-webhook'); expect((calls[0] as any).id).toBe('dummy-webhook');
expect(calls[1]).toBe('note'); expect(calls[1]).toBe('note');
expect((calls[2] as any).id).toBe('dummy-note-1'); expect((calls[2] as UserWebhookPayload<'note'>).note.id).toBe('dummy-note-1');
}); });
test('reply', async () => { test('reply', async () => {
@ -131,7 +131,7 @@ describe('WebhookTestService', () => {
const calls = queueService.userWebhookDeliver.mock.calls[0]; const calls = queueService.userWebhookDeliver.mock.calls[0];
expect((calls[0] as any).id).toBe('dummy-webhook'); expect((calls[0] as any).id).toBe('dummy-webhook');
expect(calls[1]).toBe('reply'); expect(calls[1]).toBe('reply');
expect((calls[2] as any).id).toBe('dummy-reply-1'); expect((calls[2] as UserWebhookPayload<'reply'>).note.id).toBe('dummy-reply-1');
}); });
test('renote', async () => { test('renote', async () => {
@ -140,7 +140,7 @@ describe('WebhookTestService', () => {
const calls = queueService.userWebhookDeliver.mock.calls[0]; const calls = queueService.userWebhookDeliver.mock.calls[0];
expect((calls[0] as any).id).toBe('dummy-webhook'); expect((calls[0] as any).id).toBe('dummy-webhook');
expect(calls[1]).toBe('renote'); expect(calls[1]).toBe('renote');
expect((calls[2] as any).id).toBe('dummy-renote-1'); expect((calls[2] as UserWebhookPayload<'renote'>).note.id).toBe('dummy-renote-1');
}); });
test('mention', async () => { test('mention', async () => {
@ -149,7 +149,7 @@ describe('WebhookTestService', () => {
const calls = queueService.userWebhookDeliver.mock.calls[0]; const calls = queueService.userWebhookDeliver.mock.calls[0];
expect((calls[0] as any).id).toBe('dummy-webhook'); expect((calls[0] as any).id).toBe('dummy-webhook');
expect(calls[1]).toBe('mention'); expect(calls[1]).toBe('mention');
expect((calls[2] as any).id).toBe('dummy-mention-1'); expect((calls[2] as UserWebhookPayload<'mention'>).note.id).toBe('dummy-mention-1');
}); });
test('follow', async () => { test('follow', async () => {
@ -158,7 +158,7 @@ describe('WebhookTestService', () => {
const calls = queueService.userWebhookDeliver.mock.calls[0]; const calls = queueService.userWebhookDeliver.mock.calls[0];
expect((calls[0] as any).id).toBe('dummy-webhook'); expect((calls[0] as any).id).toBe('dummy-webhook');
expect(calls[1]).toBe('follow'); expect(calls[1]).toBe('follow');
expect((calls[2] as any).id).toBe('dummy-user-1'); expect((calls[2] as UserWebhookPayload<'follow'>).user.id).toBe('dummy-user-1');
}); });
test('followed', async () => { test('followed', async () => {
@ -167,7 +167,7 @@ describe('WebhookTestService', () => {
const calls = queueService.userWebhookDeliver.mock.calls[0]; const calls = queueService.userWebhookDeliver.mock.calls[0];
expect((calls[0] as any).id).toBe('dummy-webhook'); expect((calls[0] as any).id).toBe('dummy-webhook');
expect(calls[1]).toBe('followed'); expect(calls[1]).toBe('followed');
expect((calls[2] as any).id).toBe('dummy-user-2'); expect((calls[2] as UserWebhookPayload<'followed'>).user.id).toBe('dummy-user-2');
}); });
test('unfollow', async () => { test('unfollow', async () => {
@ -176,7 +176,7 @@ describe('WebhookTestService', () => {
const calls = queueService.userWebhookDeliver.mock.calls[0]; const calls = queueService.userWebhookDeliver.mock.calls[0];
expect((calls[0] as any).id).toBe('dummy-webhook'); expect((calls[0] as any).id).toBe('dummy-webhook');
expect(calls[1]).toBe('unfollow'); expect(calls[1]).toBe('unfollow');
expect((calls[2] as any).id).toBe('dummy-user-3'); expect((calls[2] as UserWebhookPayload<'unfollow'>).user.id).toBe('dummy-user-3');
}); });
describe('NoSuchWebhookError', () => { describe('NoSuchWebhookError', () => {

View file

@ -22,23 +22,66 @@ type Account = Misskey.entities.MeDetailed & { token: string };
const accountData = miLocalStorage.getItem('account'); const accountData = miLocalStorage.getItem('account');
// TODO: 外部からはreadonlyに // TODO: 外部からはreadonlyに
/**
* Reactive state for the current account. "I" as in "I am logged in".
* Initialized from local storage if available, otherwise null.
*
* @type {Account | null}
*/
export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null; export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null;
/**
* Whether the current account is a moderator.
*
* @type {boolean}
*/
export const iAmModerator = $i != null && ($i.isAdmin === true || $i.isModerator === true); export const iAmModerator = $i != null && ($i.isAdmin === true || $i.isModerator === true);
/**
* Whether the current account is an administrator.
*
* @type {boolean}
*/
export const iAmAdmin = $i != null && $i.isAdmin; export const iAmAdmin = $i != null && $i.isAdmin;
/**
* Whether it is necessary to sign in; checks if the current
* account is null and throws an error if so.
*
* @throws {Error} If the current account is null
* @returns {Account} The current account
*/
export function signinRequired() { export function signinRequired() {
if ($i == null) throw new Error('signin required'); if ($i == null) throw new Error('signin required');
return $i; return $i;
} }
/**
* Extracts the current number of notes from the current account.
*
* Note: This appears to only be used for the "notes1" achievement.
*
* Also, separating it like this might cause counts to get out-of-sync.
*/
export let notesCount = $i == null ? 0 : $i.notesCount; export let notesCount = $i == null ? 0 : $i.notesCount;
/**
* Increments the number of notes by one.
*
* Documentation TODO: What about $i.notesCount? Why not increment that?
*/
export function incNotesCount() { export function incNotesCount() {
notesCount++; notesCount++;
} }
export async function signout() { export async function signout() {
if (!$i) return;
// If we're not signed in, there's nothing to do.
if (!$i) {
// Error log:
console.error('signout() called when not signed in');
return;
}
waiting(); waiting();
miLocalStorage.removeItem('account'); miLocalStorage.removeItem('account');

View file

@ -15,7 +15,7 @@ import { updateI18n, i18n } from '@/i18n.js';
import { $i, refreshAccount, login } from '@/account.js'; import { $i, refreshAccount, login } from '@/account.js';
import { defaultStore, ColdDeviceStorage } from '@/store.js'; import { defaultStore, ColdDeviceStorage } from '@/store.js';
import { fetchInstance, instance } from '@/instance.js'; import { fetchInstance, instance } from '@/instance.js';
import { deviceKind } from '@/scripts/device-kind.js'; import { deviceKind, updateDeviceKind } from '@/scripts/device-kind.js';
import { reloadChannel } from '@/scripts/unison-reload.js'; import { reloadChannel } from '@/scripts/unison-reload.js';
import { getUrlWithoutLoginId } from '@/scripts/login-id.js'; import { getUrlWithoutLoginId } from '@/scripts/login-id.js';
import { getAccountFromId } from '@/scripts/get-account-from-id.js'; import { getAccountFromId } from '@/scripts/get-account-from-id.js';
@ -185,6 +185,10 @@ export async function common(createVue: () => App<Element>) {
} }
}); });
watch(defaultStore.reactiveState.overridedDeviceKind, (kind) => {
updateDeviceKind(kind);
}, { immediate: true });
watch(defaultStore.reactiveState.useBlurEffectForModal, v => { watch(defaultStore.reactiveState.useBlurEffectForModal, v => {
document.documentElement.style.setProperty('--MI-modalBgFilter', v ? 'blur(4px)' : 'none'); document.documentElement.style.setProperty('--MI-modalBgFilter', v ? 'blur(4px)' : 'none');
}, { immediate: true }); }, { immediate: true });

View file

@ -118,7 +118,7 @@ import { hms } from '@/filters/hms.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { isFullscreenNotSupported } from '@/scripts/device-kind.js'; import { exitFullscreen, requestFullscreen } from '@/scripts/fullscreen.js';
import hasAudio from '@/scripts/media-has-audio.js'; import hasAudio from '@/scripts/media-has-audio.js';
import MkMediaRange from '@/components/MkMediaRange.vue'; import MkMediaRange from '@/components/MkMediaRange.vue';
import { $i, iAmModerator } from '@/account.js'; import { $i, iAmModerator } from '@/account.js';
@ -334,26 +334,21 @@ function togglePlayPause() {
} }
function toggleFullscreen() { function toggleFullscreen() {
if (isFullscreenNotSupported && videoEl.value) { if (playerEl.value == null || videoEl.value == null) return;
if (isFullscreen.value) { if (isFullscreen.value) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment exitFullscreen({
//@ts-ignore videoEl: videoEl.value,
videoEl.value.webkitExitFullscreen(); });
isFullscreen.value = false; isFullscreen.value = false;
} else { } else {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment requestFullscreen({
//@ts-ignore videoEl: videoEl.value,
videoEl.value.webkitEnterFullscreen(); playerEl: playerEl.value,
isFullscreen.value = true; options: {
} navigationUI: 'hide',
} else if (playerEl.value) { },
if (isFullscreen.value) { });
document.exitFullscreen(); isFullscreen.value = true;
isFullscreen.value = false;
} else {
playerEl.value.requestFullscreen({ navigationUI: 'hide' });
isFullscreen.value = true;
}
} }
} }
@ -454,8 +449,10 @@ watch(loop, (to) => {
}); });
watch(hide, (to) => { watch(hide, (to) => {
if (to && isFullscreen.value) { if (videoEl.value && to && isFullscreen.value) {
document.exitFullscreen(); exitFullscreen({
videoEl: videoEl.value,
});
isFullscreen.value = false; isFullscreen.value = false;
} }
}); });

View file

@ -277,7 +277,7 @@ async function onSubmit(): Promise<void> {
return null; return null;
}); });
if (res) { if (res && res.ok) {
if (res.status === 204 || instance.emailRequiredForSignup) { if (res.status === 204 || instance.emailRequiredForSignup) {
os.alert({ os.alert({
type: 'success', type: 'success',
@ -295,6 +295,8 @@ async function onSubmit(): Promise<void> {
await login(resJson.token); await login(resJson.token);
} }
} }
} else {
onSignupApiError();
} }
submitting.value = false; submitting.value = false;

View file

@ -3,6 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
/**
* A typesafe enum of keys for localStorage.
*/
export type Keys = export type Keys =
'v' | 'v' |
'lastVersion' | 'lastVersion' |
@ -44,16 +47,45 @@ export type Keys =
// セッション毎に廃棄されるLocalStorage代替セーフモードなどで使用できそう // セッション毎に廃棄されるLocalStorage代替セーフモードなどで使用できそう
//const safeSessionStorage = new Map<Keys, string>(); //const safeSessionStorage = new Map<Keys, string>();
/**
* A utility object for interacting with the browser's localStorage.
*
* It's mostly a small wrapper around window.localStorage, but it validates
* keys with a typesafe enum, and provides a few convenience methods for JSON.
*/
export const miLocalStorage = { export const miLocalStorage = {
/**
* Retrieves an item from localStorage.
* @param {Keys} key - The key of the item to retrieve.
* @returns {string | null} The value of the item, or null if the item does not exist.
*/
getItem: (key: Keys): string | null => { getItem: (key: Keys): string | null => {
return window.localStorage.getItem(key); return window.localStorage.getItem(key);
}, },
/**
* Stores an item in localStorage.
* @param {Keys} key - The key of the item to store.
* @param {string} value - The value of the item to store.
*/
setItem: (key: Keys, value: string): void => { setItem: (key: Keys, value: string): void => {
window.localStorage.setItem(key, value); window.localStorage.setItem(key, value);
}, },
/**
* Removes an item from localStorage.
* @param {Keys} key - The key of the item to remove.
*/
removeItem: (key: Keys): void => { removeItem: (key: Keys): void => {
window.localStorage.removeItem(key); window.localStorage.removeItem(key);
}, },
/**
* Retrieves an item from localStorage and parses it as JSON.
* @param {Keys} key - The key of the item to retrieve.
* @returns {any | undefined} The parsed value of the item, or undefined if the item does not exist.
*/
getItemAsJson: (key: Keys): any | undefined => { getItemAsJson: (key: Keys): any | undefined => {
const item = miLocalStorage.getItem(key); const item = miLocalStorage.getItem(key);
if (item === null) { if (item === null) {
@ -61,6 +93,12 @@ export const miLocalStorage = {
} }
return JSON.parse(item); return JSON.parse(item);
}, },
/**
* Stores an item in localStorage as a JSON string.
* @param {Keys} key - The key of the item to store.
* @param {any} value - The value of the item to store.
*/
setItemAsJson: (key: Keys, value: any): void => { setItemAsJson: (key: Keys, value: any): void => {
miLocalStorage.setItem(key, JSON.stringify(value)); miLocalStorage.setItem(key, JSON.stringify(value));
}, },

View file

@ -136,7 +136,16 @@ export function promiseDialog<T extends Promise<any>>(
return promise; return promise;
} }
/**
* Counter for generating unique popup IDs.
* @type {number}
*/
let popupIdCount = 0; let popupIdCount = 0;
/**
* A reactive list of the currently opened popups. This is used in a Vue component
* in a v-for loop to render the popups.
*/
export const popups = ref<{ export const popups = ref<{
id: number; id: number;
component: Component; component: Component;
@ -144,12 +153,23 @@ export const popups = ref<{
events: Record<string, any>; events: Record<string, any>;
}[]>([]); }[]>([]);
/**
* An object containing z-index values for different priority levels.
*/
const zIndexes = { const zIndexes = {
veryLow: 500000, veryLow: 500000,
low: 1000000, low: 1000000,
middle: 2000000, middle: 2000000,
high: 3000000, high: 3000000,
}; };
/**
* Claims a z-index value for a given priority level.
* Increments the z-index value for the specified priority by 100 and returns the new value.
*
* @param {keyof typeof zIndexes} [priority='low'] - The priority level for which to claim a z-index.
* @returns {number} The new z-index value for the specified priority.
*/
export function claimZIndex(priority: keyof typeof zIndexes = 'low'): number { export function claimZIndex(priority: keyof typeof zIndexes = 'low'): number {
zIndexes[priority] += 100; zIndexes[priority] += 100;
return zIndexes[priority]; return zIndexes[priority];
@ -177,6 +197,15 @@ type EmitsExtractor<T> = {
[K in keyof T as K extends `onVnode${string}` ? never : K extends `on${infer E}` ? Uncapitalize<E> : K extends string ? never : K]: T[K]; [K in keyof T as K extends `onVnode${string}` ? never : K extends `on${infer E}` ? Uncapitalize<E> : K extends string ? never : K]: T[K];
}; };
/**
* Opens a popup with the specified component, props, and events.
*
* @template T - The type of the component.
* @param {T} component - The Vue component to display in the popup.
* @param {ComponentProps<T>} props - The props to pass to the component.
* @param {ComponentEmit<T>} [events={}] - The events to bind to the component.
* @returns {{ dispose: () => void }} An object containing a dispose function to close the popup.
*/
export function popup<T extends Component>( export function popup<T extends Component>(
component: T, component: T,
props: ComponentProps<T>, props: ComponentProps<T>,
@ -184,13 +213,18 @@ export function popup<T extends Component>(
): { dispose: () => void } { ): { dispose: () => void } {
markRaw(component); markRaw(component);
// Generate a unique ID for this popup.
const id = ++popupIdCount; const id = ++popupIdCount;
// On disposal, remove this popup from the list of open popups.
const dispose = () => { const dispose = () => {
// このsetTimeoutが無いと挙動がおかしくなる(autocompleteが閉じなくなる)。Vueのバグ // このsetTimeoutが無いと挙動がおかしくなる(autocompleteが閉じなくなる)。Vueのバグ
window.setTimeout(() => { window.setTimeout(() => {
popups.value = popups.value.filter(p => p.id !== id); popups.value = popups.value.filter(p => p.id !== id);
}, 0); }, 0);
}; };
// Bundle the component, props, and events into a state object.
const state = { const state = {
component, component,
props, props,
@ -198,13 +232,19 @@ export function popup<T extends Component>(
id, id,
}; };
// Add the popup to the list of open popups.
popups.value.push(state); popups.value.push(state);
// Return a function that can be called to close the popup.
return { return {
dispose, dispose,
}; };
} }
/**
* Open the page with the given path in a pop-up window.
* @param path The path of the page to open.
*/
export function pageWindow(path: string) { export function pageWindow(path: string) {
const { dispose } = popup(MkPageWindow, { const { dispose } = popup(MkPageWindow, {
initialPath: path, initialPath: path,
@ -213,6 +253,11 @@ export function pageWindow(path: string) {
}); });
} }
/**
* Displays a toast message to the user.
*
* @param {string} message - The message to display in the toast.
*/
export function toast(message: string) { export function toast(message: string) {
const { dispose } = popup(MkToast, { const { dispose } = popup(MkToast, {
message, message,
@ -221,6 +266,15 @@ export function toast(message: string) {
}); });
} }
/**
* Displays an alert dialog to the user.
*
* @param {Object} props - The properties for the alert dialog.
* @param {'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question'} [props.type] - The type of the alert.
* @param {string} [props.title] - The title of the alert dialog.
* @param {string} [props.text] - The text content of the alert dialog.
* @returns {Promise<void>} A promise that resolves when the alert dialog is closed.
*/
export function alert(props: { export function alert(props: {
type?: 'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question'; type?: 'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question';
title?: string; title?: string;

View file

@ -272,6 +272,9 @@ const patronsWithIcon = [{
}, { }, {
name: 'Yatoigawa', name: 'Yatoigawa',
icon: 'https://assets.misskey-hub.net/patrons/505e3568885a4a488431a8f22b4553d0.jpg', icon: 'https://assets.misskey-hub.net/patrons/505e3568885a4a488431a8f22b4553d0.jpg',
}, {
name: '秋瀬カヲル',
icon: 'https://assets.misskey-hub.net/patrons/0f22aeb866484f4fa51db6721e3f9847.jpg',
}]; }];
const patrons = [ const patrons = [
@ -380,6 +383,7 @@ const patrons = [
'ケモナーのケシン', 'ケモナーのケシン',
'こまつぶり', 'こまつぶり',
'まゆつな空高', 'まゆつな空高',
'asata',
]; ];
const thereIsTreasure = ref($i && !claimedAchievements.includes('foundTreasure')); const thereIsTreasure = ref($i && !claimedAchievements.includes('foundTreasure'));

View file

@ -103,7 +103,7 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []); const headerTabs = computed(() => []);
definePageMetadata(() => ({ definePageMetadata(() => ({
title: announcement.value ? `${i18n.ts.announcements}: ${announcement.value.title}` : i18n.ts.announcements, title: announcement.value ? announcement.value.title : i18n.ts.announcements,
icon: 'ti ti-speakerphone', icon: 'ti ti-speakerphone',
})); }));
</script> </script>

View file

@ -62,7 +62,7 @@ function accepted() {
state.value = 'accepted'; state.value = 'accepted';
if (session.value && session.value.app.callbackUrl) { if (session.value && session.value.app.callbackUrl) {
const url = new URL(session.value.app.callbackUrl); const url = new URL(session.value.app.callbackUrl);
if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(url.protocol)) throw new Error('invalid url'); if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:', 'vbscript:'].includes(url.protocol)) throw new Error('invalid url');
location.href = `${session.value.app.callbackUrl}?token=${session.value.token}`; location.href = `${session.value.app.callbackUrl}?token=${session.value.token}`;
} }
} }

View file

@ -65,7 +65,7 @@ async function onAccept(token: string) {
if (props.callback && props.callback !== '') { if (props.callback && props.callback !== '') {
const cbUrl = new URL(props.callback); const cbUrl = new URL(props.callback);
if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(cbUrl.protocol)) throw new Error('invalid url'); if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:', 'vbscript:'].includes(cbUrl.protocol)) throw new Error('invalid url');
cbUrl.searchParams.set('session', props.session); cbUrl.searchParams.set('session', props.session);
location.href = cbUrl.toString(); location.href = cbUrl.toString();
} else { } else {

View file

@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton transparent :class="$style.testButton" :disabled="!(active && event_renote)" @click="test('renote')"><i class="ti ti-send"></i></MkButton> <MkButton transparent :class="$style.testButton" :disabled="!(active && event_renote)" @click="test('renote')"><i class="ti ti-send"></i></MkButton>
</div> </div>
<div :class="$style.switchBox"> <div :class="$style.switchBox">
<MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch> <MkSwitch v-model="event_reaction" :disabled="true">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch>
<MkButton transparent :class="$style.testButton" :disabled="!(active && event_reaction)" @click="test('reaction')"><i class="ti ti-send"></i></MkButton> <MkButton transparent :class="$style.testButton" :disabled="!(active && event_reaction)" @click="test('reaction')"><i class="ti ti-send"></i></MkButton>
</div> </div>
<div :class="$style.switchBox"> <div :class="$style.switchBox">

View file

@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="event_note">{{ i18n.ts._webhookSettings._events.note }}</MkSwitch> <MkSwitch v-model="event_note">{{ i18n.ts._webhookSettings._events.note }}</MkSwitch>
<MkSwitch v-model="event_reply">{{ i18n.ts._webhookSettings._events.reply }}</MkSwitch> <MkSwitch v-model="event_reply">{{ i18n.ts._webhookSettings._events.reply }}</MkSwitch>
<MkSwitch v-model="event_renote">{{ i18n.ts._webhookSettings._events.renote }}</MkSwitch> <MkSwitch v-model="event_renote">{{ i18n.ts._webhookSettings._events.renote }}</MkSwitch>
<MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch> <MkSwitch v-model="event_reaction" :disabled="true">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch>
<MkSwitch v-model="event_mention">{{ i18n.ts._webhookSettings._events.mention }}</MkSwitch> <MkSwitch v-model="event_mention">{{ i18n.ts._webhookSettings._events.mention }}</MkSwitch>
</div> </div>
</FormSection> </FormSection>

View file

@ -3,22 +3,22 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { defaultStore } from '@/store.js'; export type DeviceKind = 'smartphone' | 'tablet' | 'desktop';
await defaultStore.ready;
const ua = navigator.userAgent.toLowerCase(); const ua = navigator.userAgent.toLowerCase();
const isTablet = /ipad/.test(ua) || (/mobile|iphone|android/.test(ua) && window.innerWidth > 700); const isTablet = /ipad/.test(ua) || (/mobile|iphone|android/.test(ua) && window.innerWidth > 700);
const isSmartphone = !isTablet && /mobile|iphone|android/.test(ua); const isSmartphone = !isTablet && /mobile|iphone|android/.test(ua);
const isIPhone = /iphone|ipod/gi.test(ua) && navigator.maxTouchPoints > 1; export const DEFAULT_DEVICE_KIND: DeviceKind = (
// navigator.platform may be deprecated but this check is still required isSmartphone
const isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1; ? 'smartphone'
const isIos = /ipad|iphone|ipod/gi.test(ua) && navigator.maxTouchPoints > 1; : isTablet
? 'tablet'
: 'desktop'
);
export const isFullscreenNotSupported = isIPhone || isIos; export let deviceKind: DeviceKind = DEFAULT_DEVICE_KIND;
export const deviceKind: 'smartphone' | 'tablet' | 'desktop' = defaultStore.state.overridedDeviceKind ? defaultStore.state.overridedDeviceKind export function updateDeviceKind(kind: DeviceKind | null) {
: isSmartphone ? 'smartphone' deviceKind = kind ?? DEFAULT_DEVICE_KIND;
: isTablet ? 'tablet' }
: 'desktop';

View file

@ -0,0 +1,46 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
type PartiallyPartial<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type VideoEl = PartiallyPartial<HTMLVideoElement, 'requestFullscreen'> & {
webkitEnterFullscreen?(): void;
webkitExitFullscreen?(): void;
};
type PlayerEl = PartiallyPartial<HTMLElement, 'requestFullscreen'>;
type RequestFullscreenProps = {
readonly videoEl: VideoEl;
readonly playerEl: PlayerEl;
readonly options?: FullscreenOptions | null;
};
type ExitFullscreenProps = {
readonly videoEl: VideoEl;
};
export const requestFullscreen = ({ videoEl, playerEl, options }: RequestFullscreenProps) => {
if (playerEl.requestFullscreen != null) {
playerEl.requestFullscreen(options ?? undefined);
return;
}
if (videoEl.webkitEnterFullscreen != null) {
videoEl.webkitEnterFullscreen();
return;
}
};
export const exitFullscreen = ({ videoEl }: ExitFullscreenProps) => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (document.exitFullscreen != null) {
document.exitFullscreen();
return;
}
if (videoEl.webkitExitFullscreen != null) {
videoEl.webkitExitFullscreen();
return;
}
};

View file

@ -26,6 +26,7 @@ if (window.Cypress) {
console.log('Cypress detected. It will use localStorage.'); console.log('Cypress detected. It will use localStorage.');
} }
// Check for the availability of indexedDB.
if (idbAvailable) { if (idbAvailable) {
await iset('idb-test', 'test') await iset('idb-test', 'test')
.catch(err => { .catch(err => {
@ -37,16 +38,36 @@ if (idbAvailable) {
console.error('indexedDB is unavailable. It will use localStorage.'); console.error('indexedDB is unavailable. It will use localStorage.');
} }
/**
* Get a value from indexedDB (or localStorage as a fallback).
*
* @param key The key of the item to retrieve.
*
* @returns The value of the item.
*/
export async function get(key: string) { export async function get(key: string) {
if (idbAvailable) return iget(key); if (idbAvailable) return iget(key);
return miLocalStorage.getItemAsJson(`${PREFIX}${key}`); return miLocalStorage.getItemAsJson(`${PREFIX}${key}`);
} }
/**
* Set a value in indexedDB (or localStorage as a fallback).
*
* @param {string} key - The key of the item to set.
* @param {any} val - The value of the item to set.
* @returns {Promise<void>} - A promise that resolves when the value has been set.`
*/
export async function set(key: string, val: any) { export async function set(key: string, val: any) {
if (idbAvailable) return iset(key, val); if (idbAvailable) return iset(key, val);
return miLocalStorage.setItemAsJson(`${PREFIX}${key}`, val); return miLocalStorage.setItemAsJson(`${PREFIX}${key}`, val);
} }
/**
* Delete a value from indexedDB (or localStorage as a fallback).
*
* @param {string} key - The key of the item to delete.
* @returns {Promise<void>} - A promise that resolves when the value has been deleted.
*/
export async function del(key: string) { export async function del(key: string) {
if (idbAvailable) return idel(key); if (idbAvailable) return idel(key);
return miLocalStorage.removeItem(`${PREFIX}${key}`); return miLocalStorage.removeItem(`${PREFIX}${key}`);

View file

@ -8,8 +8,9 @@ import * as Misskey from 'misskey-js';
import { hemisphere } from '@@/js/intl-const.js'; import { hemisphere } from '@@/js/intl-const.js';
import lightTheme from '@@/themes/l-light.json5'; import lightTheme from '@@/themes/l-light.json5';
import darkTheme from '@@/themes/d-green-lime.json5'; import darkTheme from '@@/themes/d-green-lime.json5';
import { miLocalStorage } from './local-storage.js';
import type { SoundType } from '@/scripts/sound.js'; import type { SoundType } from '@/scripts/sound.js';
import { DEFAULT_DEVICE_KIND, type DeviceKind } from '@/scripts/device-kind.js';
import { miLocalStorage } from '@/local-storage.js';
import { Storage } from '@/pizzax.js'; import { Storage } from '@/pizzax.js';
import type { Ast } from '@syuilo/aiscript'; import type { Ast } from '@syuilo/aiscript';
@ -207,7 +208,7 @@ export const defaultStore = markRaw(new Storage('base', {
overridedDeviceKind: { overridedDeviceKind: {
where: 'device', where: 'device',
default: null as null | 'smartphone' | 'tablet' | 'desktop', default: null as DeviceKind | null,
}, },
serverDisconnectedBehavior: { serverDisconnectedBehavior: {
where: 'device', where: 'device',
@ -263,11 +264,11 @@ export const defaultStore = markRaw(new Storage('base', {
}, },
useBlurEffectForModal: { useBlurEffectForModal: {
where: 'device', where: 'device',
default: !/mobile|iphone|android/.test(navigator.userAgent.toLowerCase()), // 循環参照するのでdevice-kind.tsは参照できない default: DEFAULT_DEVICE_KIND === 'desktop',
}, },
useBlurEffect: { useBlurEffect: {
where: 'device', where: 'device',
default: !/mobile|iphone|android/.test(navigator.userAgent.toLowerCase()), // 循環参照するのでdevice-kind.tsは参照できない default: DEFAULT_DEVICE_KIND === 'desktop',
}, },
showFixedPostForm: { showFixedPostForm: {
where: 'device', where: 'device',

View file

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