diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d2c98ad2..925a6f505 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,16 @@ ### Client - Feat: 新しいゲームを追加 +- Feat: 音声・映像プレイヤーを追加 +- Feat: 絵文字の詳細ダイアログを追加 +- Feat: 枠線をつけるMFM`$[border.width=1,style=solid,color=fff,radius=0 ...]`を追加 - Enhance: ハッシュタグ入力時に、本文の末尾の行に何も書かれていない場合は新たにスペースを追加しないように - Enhance: チャンネルノートのピン留めをノートのメニューからできるように - Enhance: 管理者の場合はAPI tokenの発行画面で管理機能に関する権限を付与できるように - Enhance: AiScriptを0.17.0に更新 [CHANGELOG](https://github.com/aiscript-dev/aiscript/blob/bb89d132b633a622d3cb0eff0d0cc7e476c0cfdd/CHANGELOG.md) - 配列の範囲外・非整数のインデックスへの代入が完全禁止になるので注意 - Enhance: 絵文字ピッカー・オートコンプリートで、完全一致した絵文字を優先的に表示するように +- Enhance: Playの説明欄にMFMを使えるように - Fix: ネイティブモードの絵文字がモノクロにならないように - Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正 - Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正 @@ -21,7 +25,12 @@ - Enhance: 連合先のレートリミットに引っかかった際にリトライするようになりました - Enhance: ActivityPub Deliver queueでBodyを事前処理するように (#12916) - Enhance: クリップをエクスポートできるように +- Enhance: `/files`のファイルに対してHTTP Rangeリクエストを行えるように +- Enhance: `api.json`のOpenAPI Specificationを3.1.0に更新 - Fix: `drive/files/update`でファイル名のバリデーションが機能していない問題を修正 +- Fix: `notes/create`で、`text`が空白文字のみで構成されているか`null`であって、かつ`text`だけであるリクエストに対するレスポンスが400になるように変更 +- Fix: `notes/create`で、`text`が空白文字のみで構成されていてかつリノート、ファイルまたは投票を含んでいるリクエストに対するレスポンスの`text`が`""`から`null`になるように変更 +- Fix: ipv4とipv6の両方が利用可能な環境でallowedPrivateNetworksが設定されていた場合プライベートipの検証ができていなかった問題を修正 ## 2023.12.2 diff --git a/locales/index.d.ts b/locales/index.d.ts index e36ff1983..0afd93a20 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1096,6 +1096,8 @@ export interface Locale { "noteIdOrUrl": string; "video": string; "videos": string; + "audio": string; + "audioFiles": string; "dataSaver": string; "accountMigration": string; "accountMoved": string; @@ -1243,6 +1245,8 @@ export interface Locale { "replay": string; "replaying": string; "ranking": string; + "lastNDays": string; + "backToTitle": string; "_bubbleGame": { "howToPlay": string; "_howToPlay": { diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 5609cacb2..6cffb8195 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1093,6 +1093,8 @@ limitWidthOfReaction: "リアクションの最大横幅を制限し、縮小し noteIdOrUrl: "ノートIDまたはURL" video: "動画" videos: "動画" +audio: "音声" +audioFiles: "音声" dataSaver: "データセーバー" accountMigration: "アカウントの移行" accountMoved: "このユーザーは新しいアカウントに移行しました:" @@ -1240,6 +1242,8 @@ showReplay: "リプレイを見る" replay: "リプレイ" replaying: "リプレイ中" ranking: "ランキング" +lastNDays: "直近{n}日" +backToTitle: "タイトルへ" _bubbleGame: howToPlay: "遊び方" diff --git a/packages/backend/migration/1705222772858-optimize-note-index-for-array-column.js b/packages/backend/migration/1705222772858-optimize-note-index-for-array-column.js new file mode 100644 index 000000000..571bd8e8f --- /dev/null +++ b/packages/backend/migration/1705222772858-optimize-note-index-for-array-column.js @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class OptimizeNoteIndexForArrayColumns1705222772858 { + name = 'OptimizeNoteIndexForArrayColumns1705222772858' + + async up(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_796a8c03959361f97dc2be1d5c"`); + await queryRunner.query(`DROP INDEX "public"."IDX_54ebcb6d27222913b908d56fd8"`); + await queryRunner.query(`DROP INDEX "public"."IDX_88937d94d7443d9a99a76fa5c0"`); + await queryRunner.query(`DROP INDEX "public"."IDX_51c063b6a133a9cb87145450f5"`); + await queryRunner.query(`CREATE INDEX "IDX_NOTE_FILE_IDS" ON "note" using gin ("fileIds")`) + } + + async down(queryRunner) { + await queryRunner.query(`DROP INDEX "IDX_NOTE_FILE_IDS"`) + await queryRunner.query(`CREATE INDEX "IDX_51c063b6a133a9cb87145450f5" ON "note" ("fileIds") `); + await queryRunner.query(`CREATE INDEX "IDX_88937d94d7443d9a99a76fa5c0" ON "note" ("tags") `); + await queryRunner.query(`CREATE INDEX "IDX_54ebcb6d27222913b908d56fd8" ON "note" ("mentions") `); + await queryRunner.query(`CREATE INDEX "IDX_796a8c03959361f97dc2be1d5c" ON "note" ("visibleUserIds") `); + } +} diff --git a/packages/backend/src/core/DownloadService.ts b/packages/backend/src/core/DownloadService.ts index 5474272b0..5e196e83d 100644 --- a/packages/backend/src/core/DownloadService.ts +++ b/packages/backend/src/core/DownloadService.ts @@ -145,7 +145,8 @@ export class DownloadService { const parsedIp = ipaddr.parse(ip); for (const net of this.config.allowedPrivateNetworks ?? []) { - if (parsedIp.match(ipaddr.parseCIDR(net))) { + const cidr = ipaddr.parseCIDR(net); + if (cidr[0].kind() === parsedIp.kind() && parsedIp.match(ipaddr.parseCIDR(net))) { return false; } } diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 58f3d3559..345367f05 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -494,6 +494,9 @@ export class NoteCreateService implements OnApplicationShutdown { data.text = data.text.slice(0, DB_MAX_NOTE_TEXT_LENGTH); } data.text = data.text.trim(); + if (data.text === '') { + data.text = null; + } } else { data.text = null; } diff --git a/packages/backend/src/core/QueryService.ts b/packages/backend/src/core/QueryService.ts index f006ed494..13d8a6759 100644 --- a/packages/backend/src/core/QueryService.ts +++ b/packages/backend/src/core/QueryService.ts @@ -212,8 +212,8 @@ export class QueryService { // または 自分自身 .orWhere('note.userId = :meId') // または 自分宛て - .orWhere(':meId = ANY(note.visibleUserIds)') - .orWhere(':meId = ANY(note.mentions)') + .orWhere(':meIdAsList <@ note.visibleUserIds') + .orWhere(':meIdAsList <@ note.mentions') .orWhere(new Brackets(qb => { qb // または フォロワー宛ての投稿であり、 @@ -228,7 +228,7 @@ export class QueryService { })); })); - q.setParameters({ meId: me.id }); + q.setParameters({ meId: me.id, meIdAsList: [me.id] }); } } diff --git a/packages/backend/src/models/Note.ts b/packages/backend/src/models/Note.ts index 270528288..13b6b1ce9 100644 --- a/packages/backend/src/models/Note.ts +++ b/packages/backend/src/models/Note.ts @@ -11,9 +11,6 @@ import { MiChannel } from './Channel.js'; import type { MiDriveFile } from './DriveFile.js'; @Entity('note') -@Index('IDX_NOTE_TAGS', { synchronize: false }) -@Index('IDX_NOTE_MENTIONS', { synchronize: false }) -@Index('IDX_NOTE_VISIBLE_USER_IDS', { synchronize: false }) export class MiNote { @PrimaryColumn(id()) public id: string; @@ -139,7 +136,7 @@ export class MiNote { }) public url: string | null; - @Index() + @Index('IDX_NOTE_FILE_IDS', { synchronize: false }) @Column({ ...id(), array: true, default: '{}', @@ -151,14 +148,14 @@ export class MiNote { }) public attachedFileTypes: string[]; - @Index() + @Index('IDX_NOTE_VISIBLE_USER_IDS', { synchronize: false }) @Column({ ...id(), array: true, default: '{}', }) public visibleUserIds: MiUser['id'][]; - @Index() + @Index('IDX_NOTE_MENTIONS', { synchronize: false }) @Column({ ...id(), array: true, default: '{}', @@ -180,7 +177,7 @@ export class MiNote { }) public emojis: string[]; - @Index() + @Index('IDX_NOTE_TAGS', { synchronize: false }) @Column('varchar', { length: 128, array: true, default: '{}', }) diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts index 61e8e8c84..e8de8b7a5 100644 --- a/packages/backend/src/server/FileServerService.ts +++ b/packages/backend/src/server/FileServerService.ts @@ -166,11 +166,35 @@ export class FileServerService { } if (!image) { - image = { - data: fs.createReadStream(file.path), - ext: file.ext, - type: file.mime, - }; + if (request.headers.range && file.file.size > 0) { + const range = request.headers.range as string; + const parts = range.replace(/bytes=/, '').split('-'); + const start = parseInt(parts[0], 10); + let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1; + if (end > file.file.size) { + end = file.file.size - 1; + } + const chunksize = end - start + 1; + + image = { + data: fs.createReadStream(file.path, { + start, + end, + }), + ext: file.ext, + type: file.mime, + }; + + reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`); + reply.header('Accept-Ranges', 'bytes'); + reply.header('Content-Length', chunksize); + } else { + image = { + data: fs.createReadStream(file.path), + ext: file.ext, + type: file.mime, + }; + } } if ('pipe' in image.data && typeof image.data.pipe === 'function') { @@ -201,11 +225,54 @@ export class FileServerService { reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.mime) ? file.mime : 'application/octet-stream'); reply.header('Cache-Control', 'max-age=31536000, immutable'); reply.header('Content-Disposition', contentDisposition('inline', filename)); + + if (request.headers.range && file.file.size > 0) { + const range = request.headers.range as string; + const parts = range.replace(/bytes=/, '').split('-'); + const start = parseInt(parts[0], 10); + let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1; + if (end > file.file.size) { + end = file.file.size - 1; + } + const chunksize = end - start + 1; + const fileStream = fs.createReadStream(file.path, { + start, + end, + }); + reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`); + reply.header('Accept-Ranges', 'bytes'); + reply.header('Content-Length', chunksize); + reply.code(206); + return fileStream; + } + return fs.createReadStream(file.path); } else { reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.file.type) ? file.file.type : 'application/octet-stream'); reply.header('Cache-Control', 'max-age=31536000, immutable'); reply.header('Content-Disposition', contentDisposition('inline', file.filename)); + + if (request.headers.range && file.file.size > 0) { + const range = request.headers.range as string; + const parts = range.replace(/bytes=/, '').split('-'); + const start = parseInt(parts[0], 10); + let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1; + console.log(end); + if (end > file.file.size) { + end = file.file.size - 1; + } + const chunksize = end - start + 1; + const fileStream = fs.createReadStream(file.path, { + start, + end, + }); + reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`); + reply.header('Accept-Ranges', 'bytes'); + reply.header('Content-Length', chunksize); + reply.code(206); + return fileStream; + } + return fs.createReadStream(file.path); } } catch (e) { @@ -338,11 +405,35 @@ export class FileServerService { } if (!image) { - image = { - data: fs.createReadStream(file.path), - ext: file.ext, - type: file.mime, - }; + if (request.headers.range && file.file && file.file.size > 0) { + const range = request.headers.range as string; + const parts = range.replace(/bytes=/, '').split('-'); + const start = parseInt(parts[0], 10); + let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1; + if (end > file.file.size) { + end = file.file.size - 1; + } + const chunksize = end - start + 1; + + image = { + data: fs.createReadStream(file.path, { + start, + end, + }), + ext: file.ext, + type: file.mime, + }; + + reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`); + reply.header('Accept-Ranges', 'bytes'); + reply.header('Content-Length', chunksize); + } else { + image = { + data: fs.createReadStream(file.path), + ext: file.ext, + type: file.mime, + }; + } } if ('cleanup' in file) { diff --git a/packages/backend/src/server/api/endpoints/bubble-game/ranking.ts b/packages/backend/src/server/api/endpoints/bubble-game/ranking.ts index 0cba129a0..9c057760c 100644 --- a/packages/backend/src/server/api/endpoints/bubble-game/ranking.ts +++ b/packages/backend/src/server/api/endpoints/bubble-game/ranking.ts @@ -11,8 +11,6 @@ import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; export const meta = { - tags: [], - allowGet: true, cacheSec: 60, diff --git a/packages/backend/src/server/api/endpoints/bubble-game/register.ts b/packages/backend/src/server/api/endpoints/bubble-game/register.ts index af0f69e4a..f092d16a7 100644 --- a/packages/backend/src/server/api/endpoints/bubble-game/register.ts +++ b/packages/backend/src/server/api/endpoints/bubble-game/register.ts @@ -12,8 +12,6 @@ import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; export const meta = { - tags: [], - requireCredential: true, kind: 'write:account', diff --git a/packages/backend/src/server/api/endpoints/drive/files.ts b/packages/backend/src/server/api/endpoints/drive/files.ts index b7e9d12e9..0ca31dc99 100644 --- a/packages/backend/src/server/api/endpoints/drive/files.ts +++ b/packages/backend/src/server/api/endpoints/drive/files.ts @@ -36,7 +36,7 @@ export const paramDef = { untilId: { type: 'string', format: 'misskey:id' }, folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null }, type: { type: 'string', nullable: true, pattern: /^[a-zA-Z\/\-*]+$/.toString().slice(1, -1) }, - sort: { type: 'string', nullable: true, enum: ['+createdAt', '-createdAt', '+name', '-name', '+size', '-size'] }, + sort: { type: 'string', nullable: true, enum: ['+createdAt', '-createdAt', '+name', '-name', '+size', '-size', null] }, }, required: [], } as const; diff --git a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts index 14a13b09c..7a0b8b441 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts @@ -74,7 +74,7 @@ export default class extends Endpoint { // eslint- } const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId); - query.andWhere(':file = ANY(note.fileIds)', { file: file.id }); + query.andWhere(':file <@ note.fileIds', { file: [file.id] }); const notes = await query.limit(ps.limit).getMany(); diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts index fcb4fc565..6d8ae7c02 100644 --- a/packages/backend/src/server/api/endpoints/federation/instances.ts +++ b/packages/backend/src/server/api/endpoints/federation/instances.ts @@ -62,6 +62,7 @@ export const paramDef = { '-firstRetrievedAt', '+latestRequestReceivedAt', '-latestRequestReceivedAt', + null, ], }, }, diff --git a/packages/backend/src/server/api/endpoints/hashtags/users.ts b/packages/backend/src/server/api/endpoints/hashtags/users.ts index 50aea7994..8302d2380 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/users.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/users.ts @@ -6,6 +6,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { UsersRepository } from '@/models/_.js'; +import { safeForSql } from "@/misc/safe-for-sql.js"; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -47,8 +48,9 @@ export default class extends Endpoint { // eslint- private userEntityService: UserEntityService, ) { super(meta, paramDef, async (ps, me) => { + if (!safeForSql(normalizeForSearch(ps.tag))) throw new Error('Injection'); const query = this.usersRepository.createQueryBuilder('user') - .where(':tag = ANY(user.tags)', { tag: normalizeForSearch(ps.tag) }) + .where(':tag <@ user.tags', { tag: [normalizeForSearch(ps.tag)] }) .andWhere('user.isSuspended = FALSE'); const recent = new Date(Date.now() - (1000 * 60 * 60 * 24 * 5)); diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts index 325d54d19..7c4665857 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts @@ -104,13 +104,13 @@ export const meta = { items: { type: 'string', enum: [ - "ble", - "cable", - "hybrid", - "internal", - "nfc", - "smart-card", - "usb", + 'ble', + 'cable', + 'hybrid', + 'internal', + 'nfc', + 'smart-card', + 'usb', ], }, }, @@ -124,8 +124,8 @@ export const meta = { authenticatorAttachment: { type: 'string', enum: [ - "cross-platform", - "platform", + 'cross-platform', + 'platform', ], }, requireResidentKey: { @@ -134,9 +134,9 @@ export const meta = { userVerification: { type: 'string', enum: [ - "discouraged", - "preferred", - "required", + 'discouraged', + 'preferred', + 'required', ], }, }, @@ -145,10 +145,11 @@ export const meta = { type: 'string', nullable: true, enum: [ - "direct", - "enterprise", - "indirect", - "none", + 'direct', + 'enterprise', + 'indirect', + 'none', + null, ], }, extensions: { diff --git a/packages/backend/src/server/api/endpoints/notes/create.test.ts b/packages/backend/src/server/api/endpoints/notes/create.test.ts index 0de5a14a9..c204f4af9 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.test.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.test.ts @@ -34,11 +34,10 @@ describe('api:notes/create', () => { .toBe(VALID); }); - // TODO - //test('null post', () => { - // expect(v({ text: null })) - // .toBe(INVALID); - //}); + test('null post', () => { + expect(v({ text: null })) + .toBe(INVALID); + }); test('0 characters post', () => { expect(v({ text: '' })) @@ -49,6 +48,11 @@ describe('api:notes/create', () => { expect(v({ text: await tooLong })) .toBe(INVALID); }); + + test('whitespace-only post', () => { + expect(v({ text: ' ' })) + .toBe(INVALID); + }); }); describe('cw', () => { diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 3fef0e6aa..44ca1b9c1 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -177,13 +177,32 @@ export const paramDef = { }, }, // (re)note with text, files and poll are optional - anyOf: [ - { required: ['text'] }, - { required: ['renoteId'] }, - { required: ['fileIds'] }, - { required: ['mediaIds'] }, - { required: ['poll'] }, - ], + if: { + properties: { + renoteId: { + type: 'null', + }, + fileIds: { + type: 'null', + }, + mediaIds: { + type: 'null', + }, + poll: { + type: 'null', + }, + }, + }, + then: { + properties: { + text: { + type: 'string', + minLength: 1, + pattern: '[^\\s]+', + }, + }, + required: ['text'], + }, } as const; @Injectable() diff --git a/packages/backend/src/server/api/endpoints/notes/mentions.ts b/packages/backend/src/server/api/endpoints/notes/mentions.ts index 2317f8f7b..323c6c946 100644 --- a/packages/backend/src/server/api/endpoints/notes/mentions.ts +++ b/packages/backend/src/server/api/endpoints/notes/mentions.ts @@ -61,9 +61,9 @@ export default class extends Endpoint { // eslint- const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId) .andWhere(new Brackets(qb => { - qb - .where(`'{"${me.id}"}' <@ note.mentions`) - .orWhere(`'{"${me.id}"}' <@ note.visibleUserIds`); + qb // このmeIdAsListパラメータはqueryServiceのgenerateVisibilityQueryでセットされる + .where(':meIdAsList <@ note.mentions') + .orWhere(':meIdAsList <@ note.visibleUserIds'); })) // Avoid scanning primary key index .orderBy('CONCAT(note.id)', 'DESC') diff --git a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts index 89e05fd57..53544149e 100644 --- a/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts +++ b/packages/backend/src/server/api/endpoints/notes/search-by-tag.ts @@ -104,14 +104,14 @@ export default class extends Endpoint { // eslint- try { if (ps.tag) { if (!safeForSql(normalizeForSearch(ps.tag))) throw new Error('Injection'); - query.andWhere(`'{"${normalizeForSearch(ps.tag)}"}' <@ note.tags`); + query.andWhere(':tag <@ note.tags', { tag: [normalizeForSearch(ps.tag)] }); } else { query.andWhere(new Brackets(qb => { for (const tags of ps.query!) { qb.orWhere(new Brackets(qb => { for (const tag of tags) { if (!safeForSql(normalizeForSearch(tag))) throw new Error('Injection'); - qb.andWhere(`'{"${normalizeForSearch(tag)}"}' <@ note.tags`); + qb.andWhere(':tag <@ note.tags', { tag: [normalizeForSearch(tag)] }); } })); } diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts index 0e71510b4..971a6116b 100644 --- a/packages/backend/src/server/api/openapi/gen-spec.ts +++ b/packages/backend/src/server/api/openapi/gen-spec.ts @@ -10,7 +10,7 @@ import { schemas, convertSchemaToOpenApiSchema } from './schemas.js'; export function genOpenapiSpec(config: Config) { const spec = { - openapi: '3.0.0', + openapi: '3.1.0', info: { version: config.version, @@ -56,7 +56,7 @@ export function genOpenapiSpec(config: Config) { } } - const resSchema = endpoint.meta.res ? convertSchemaToOpenApiSchema(endpoint.meta.res) : {}; + const resSchema = endpoint.meta.res ? convertSchemaToOpenApiSchema(endpoint.meta.res, 'res') : {}; let desc = (endpoint.meta.description ? endpoint.meta.description : 'No description provided.') + '\n\n'; @@ -71,7 +71,7 @@ export function genOpenapiSpec(config: Config) { } const requestType = endpoint.meta.requireFile ? 'multipart/form-data' : 'application/json'; - const schema = { ...endpoint.params }; + const schema = { ...convertSchemaToOpenApiSchema(endpoint.params, 'param') }; if (endpoint.meta.requireFile) { schema.properties = { @@ -210,7 +210,9 @@ export function genOpenapiSpec(config: Config) { }; spec.paths['/' + endpoint.name] = { - ...(endpoint.meta.allowGet ? { get: info } : {}), + ...(endpoint.meta.allowGet ? { + get: info, + } : {}), post: info, }; } diff --git a/packages/backend/src/server/api/openapi/schemas.ts b/packages/backend/src/server/api/openapi/schemas.ts index 2716f5f16..a862a7b74 100644 --- a/packages/backend/src/server/api/openapi/schemas.ts +++ b/packages/backend/src/server/api/openapi/schemas.ts @@ -6,32 +6,35 @@ import type { Schema } from '@/misc/json-schema.js'; import { refs } from '@/misc/json-schema.js'; -export function convertSchemaToOpenApiSchema(schema: Schema) { - // optional, refはスキーマ定義に含まれないので分離しておく +export function convertSchemaToOpenApiSchema(schema: Schema, type: 'param' | 'res') { + // optional, nullable, refはスキーマ定義に含まれないので分離しておく // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { optional, ref, ...res }: any = schema; + const { optional, nullable, ref, ...res }: any = schema; if (schema.type === 'object' && schema.properties) { - const required = Object.entries(schema.properties).filter(([k, v]) => !v.optional).map(([k]) => k); - if (required.length > 0) { + if (type === 'res') { + const required = Object.entries(schema.properties).filter(([k, v]) => !v.optional).map(([k]) => k); + if (required.length > 0) { // 空配列は許可されない - res.required = required; + res.required = required; + } } for (const k of Object.keys(schema.properties)) { - res.properties[k] = convertSchemaToOpenApiSchema(schema.properties[k]); + res.properties[k] = convertSchemaToOpenApiSchema(schema.properties[k], type); } } if (schema.type === 'array' && schema.items) { - res.items = convertSchemaToOpenApiSchema(schema.items); + res.items = convertSchemaToOpenApiSchema(schema.items, type); } - if (schema.anyOf) res.anyOf = schema.anyOf.map(convertSchemaToOpenApiSchema); - if (schema.oneOf) res.oneOf = schema.oneOf.map(convertSchemaToOpenApiSchema); - if (schema.allOf) res.allOf = schema.allOf.map(convertSchemaToOpenApiSchema); + for (const o of ['anyOf', 'oneOf', 'allOf'] as const) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + if (o in schema) res[o] = schema[o]!.map(schema => convertSchemaToOpenApiSchema(schema, type)); + } - if (schema.ref) { + if (type === 'res' && schema.ref) { const $ref = `#/components/schemas/${schema.ref}`; if (schema.nullable || schema.optional) { res.allOf = [{ $ref }]; @@ -40,6 +43,14 @@ export function convertSchemaToOpenApiSchema(schema: Schema) { } } + if (schema.nullable) { + if (Array.isArray(schema.type) && !schema.type.includes('null')) { + res.type.push('null'); + } else if (typeof schema.type === 'string') { + res.type = [res.type, 'null']; + } + } + return res; } @@ -72,6 +83,6 @@ export const schemas = { }, ...Object.fromEntries( - Object.entries(refs).map(([key, schema]) => [key, convertSchemaToOpenApiSchema(schema)]), + Object.entries(refs).map(([key, schema]) => [key, convertSchemaToOpenApiSchema(schema, 'res')]), ), }; diff --git a/packages/backend/test/e2e/drive.ts b/packages/backend/test/e2e/drive.ts new file mode 100644 index 000000000..3a84961fc --- /dev/null +++ b/packages/backend/test/e2e/drive.ts @@ -0,0 +1,95 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import { MiNote } from '@/models/Note.js'; +import { api, initTestDb, makeStreamCatcher, post, signup, uploadFile } from '../utils.js'; +import type * as misskey from 'misskey-js'; +import type{ Repository } from 'typeorm' +import type { Packed } from '@/misc/json-schema.js'; + + +describe('Drive', () => { + let Notes: Repository; + + let alice: misskey.entities.SignupResponse; + let bob: misskey.entities.SignupResponse; + + beforeAll(async () => { + const connection = await initTestDb(true); + Notes = connection.getRepository(MiNote); + alice = await signup({ username: 'alice' }); + bob = await signup({ username: 'bob' }); + }, 1000 * 60 * 2); + + test('ファイルURLからアップロードできる', async () => { + // utils.js uploadUrl の処理だがAPIレスポンスも見るためここで同様の処理を書いている + + const marker = Math.random().toString(); + + const url = 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg' + + const catcher = makeStreamCatcher( + alice, + 'main', + (msg) => msg.type === 'urlUploadFinished' && msg.body.marker === marker, + (msg) => msg.body.file as Packed<'DriveFile'>, + 10 * 1000); + + const res = await api('drive/files/upload-from-url', { + url, + marker, + force: true, + }, alice); + + const file = await catcher; + + assert.strictEqual(res.status, 204); + assert.strictEqual(file.name, 'Lenna.jpg'); + assert.strictEqual(file.type, 'image/jpeg'); + }) + + test('ローカルからアップロードできる', async () => { + // APIレスポンスを直接使用するので utils.js uploadFile が通過することで成功とする + + const res = await uploadFile(alice, { path: 'Lenna.jpg', name: 'テスト画像' }); + + assert.strictEqual(res.body?.name, 'テスト画像.jpg'); + assert.strictEqual(res.body?.type, 'image/jpeg'); + }) + + test('添付ノート一覧を取得できる', async () => { + const ids = (await Promise.all([uploadFile(alice), uploadFile(alice), uploadFile(alice)])).map(elm => elm.body!.id) + + const note0 = await post(alice, { fileIds: [ids[0]] }); + const note1 = await post(alice, { fileIds: [ids[0], ids[1]] }); + + const attached0 = await api('drive/files/attached-notes', { fileId: ids[0] }, alice); + assert.strictEqual(attached0.body.length, 2); + assert.strictEqual(attached0.body[0].id, note1.id) + assert.strictEqual(attached0.body[1].id, note0.id) + + const attached1 = await api('drive/files/attached-notes', { fileId: ids[1] }, alice); + assert.strictEqual(attached1.body.length, 1); + assert.strictEqual(attached1.body[0].id, note1.id) + + const attached2 = await api('drive/files/attached-notes', { fileId: ids[2] }, alice); + assert.strictEqual(attached2.body.length, 0) + }) + + test('添付ノート一覧は他の人から見えない', async () => { + const file = await uploadFile(alice); + + await post(alice, { fileIds: [file.body!.id] }); + + const res = await api('drive/files/attached-notes', { fileId: file.body!.id }, bob); + assert.strictEqual(res.status, 400); + assert.strictEqual('error' in res.body, true); + + }) +}); + diff --git a/packages/backend/test/e2e/note.ts b/packages/backend/test/e2e/note.ts index 0f2e08e67..0280b051f 100644 --- a/packages/backend/test/e2e/note.ts +++ b/packages/backend/test/e2e/note.ts @@ -136,6 +136,19 @@ describe('Note', () => { assert.strictEqual(res.body.createdNote.renote.text, bobPost.text); }); + test('引用renoteで空白文字のみで構成されたtextにするとレスポンスがtext: nullになる', async () => { + const bobPost = await post(bob, { + text: 'test', + }); + const res = await api('/notes/create', { + text: ' ', + renoteId: bobPost.id, + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(res.body.createdNote.text, null); + }); + test('visibility: followersでrenoteできる', async () => { const createRes = await api('/notes/create', { text: 'test', diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index 2b232a0a5..a41002cc8 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -16,6 +16,7 @@ import { DEFAULT_POLICIES } from '@/core/RoleService.js'; import { entities } from '../src/postgres.js'; import { loadConfig } from '../src/config.js'; import type * as misskey from 'misskey-js'; +import { Packed } from '@/misc/json-schema.js'; export { server as startServer, jobQueue as startJobQueue } from '@/boot/common.js'; @@ -114,6 +115,20 @@ export function randomString(chars = 'abcdefghijklmnopqrstuvwxyz0123456789', len return randomString; } +/** + * @brief プロミスにタイムアウト追加 + * @param p 待ち対象プロミス + * @param timeout 待機ミリ秒 + */ +function timeoutPromise(p: Promise, timeout: number): Promise { + return Promise.race([ + p, + new Promise((reject) =>{ + setTimeout(() => { reject(new Error('timed out')); }, timeout) + }) as never + ]); +} + export const signup = async (params?: Partial): Promise> => { const q = Object.assign({ username: randomString(), @@ -320,17 +335,16 @@ export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadO }; }; -export const uploadUrl = async (user: UserToken, url: string) => { - let resolve: unknown; - const file = new Promise(ok => resolve = ok); +export const uploadUrl = async (user: UserToken, url: string): Promise> => { const marker = Math.random().toString(); - const ws = await connectStream(user, 'main', (msg) => { - if (msg.type === 'urlUploadFinished' && msg.body.marker === marker) { - ws.close(); - resolve(msg.body.file); - } - }); + const catcher = makeStreamCatcher( + user, + 'main', + (msg) => msg.type === 'urlUploadFinished' && msg.body.marker === marker, + (msg) => msg.body.file as Packed<'DriveFile'>, + 60 * 1000 + ); await api('drive/files/upload-from-url', { url, @@ -338,7 +352,7 @@ export const uploadUrl = async (user: UserToken, url: string) => { force: true, }, user); - return file; + return catcher; }; export function connectStream(user: UserToken, channel: string, listener: (message: Record) => any, params?: any): Promise { @@ -410,6 +424,35 @@ export const waitFire = async (user: UserToken, channel: string, trgr: () => any }); }; +/** + * @brief WebSocketストリームから特定条件の通知を拾うプロミスを生成 + * @param user ユーザー認証情報 + * @param channel チャンネル + * @param cond 条件 + * @param extractor 取り出し処理 + * @param timeout ミリ秒タイムアウト + * @returns 時間内に正常に処理できた場合に通知からextractorを通した値を得る + */ +export function makeStreamCatcher( + user: UserToken, + channel: string, + cond: (message: Record) => boolean, + extractor: (message: Record) => T, + timeout = 60 * 1000): Promise { + let ws: WebSocket + const p = new Promise(async (resolve) => { + ws = await connectStream(user, channel, (msg) => { + if (cond(msg)) { + resolve(extractor(msg)) + } + }); + }).finally(() => { + ws?.close(); + }); + + return timeoutPromise(p, timeout); +} + export type SimpleGetResponse = { status: number, body: any | JSDOM | null, diff --git a/packages/frontend/assets/drop-and-fusion/poi1.mp3 b/packages/frontend/assets/drop-and-fusion/collision.mp3 similarity index 100% rename from packages/frontend/assets/drop-and-fusion/poi1.mp3 rename to packages/frontend/assets/drop-and-fusion/collision.mp3 diff --git a/packages/frontend/assets/drop-and-fusion/collision_yen.mp3 b/packages/frontend/assets/drop-and-fusion/collision_yen.mp3 new file mode 100644 index 000000000..6737357f6 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/collision_yen.mp3 differ diff --git a/packages/frontend/assets/drop-and-fusion/poi2.mp3 b/packages/frontend/assets/drop-and-fusion/drop.mp3 similarity index 100% rename from packages/frontend/assets/drop-and-fusion/poi2.mp3 rename to packages/frontend/assets/drop-and-fusion/drop.mp3 diff --git a/packages/frontend/assets/drop-and-fusion/drop_yen.mp3 b/packages/frontend/assets/drop-and-fusion/drop_yen.mp3 new file mode 100644 index 000000000..bbf385f15 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/drop_yen.mp3 differ diff --git a/packages/frontend/assets/drop-and-fusion/bubble2.mp3 b/packages/frontend/assets/drop-and-fusion/fusion.mp3 similarity index 100% rename from packages/frontend/assets/drop-and-fusion/bubble2.mp3 rename to packages/frontend/assets/drop-and-fusion/fusion.mp3 diff --git a/packages/frontend/assets/drop-and-fusion/fusion_yen.mp3 b/packages/frontend/assets/drop-and-fusion/fusion_yen.mp3 new file mode 100644 index 000000000..e8d203fb5 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/fusion_yen.mp3 differ diff --git a/packages/frontend/assets/drop-and-fusion/gameover_yen.mp3 b/packages/frontend/assets/drop-and-fusion/gameover_yen.mp3 new file mode 100644 index 000000000..c7fdcb5c8 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/gameover_yen.mp3 differ diff --git a/packages/frontend/assets/drop-and-fusion/go.png b/packages/frontend/assets/drop-and-fusion/go.png new file mode 100644 index 000000000..37468f139 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/go.png differ diff --git a/packages/frontend/assets/drop-and-fusion/cold_face.png b/packages/frontend/assets/drop-and-fusion/normal_monos/cold_face.png similarity index 100% rename from packages/frontend/assets/drop-and-fusion/cold_face.png rename to packages/frontend/assets/drop-and-fusion/normal_monos/cold_face.png diff --git a/packages/frontend/assets/drop-and-fusion/exploding_head.png b/packages/frontend/assets/drop-and-fusion/normal_monos/exploding_head.png similarity index 100% rename from packages/frontend/assets/drop-and-fusion/exploding_head.png rename to packages/frontend/assets/drop-and-fusion/normal_monos/exploding_head.png diff --git a/packages/frontend/assets/drop-and-fusion/face_with_open_mouth.png b/packages/frontend/assets/drop-and-fusion/normal_monos/face_with_open_mouth.png similarity index 100% rename from packages/frontend/assets/drop-and-fusion/face_with_open_mouth.png rename to packages/frontend/assets/drop-and-fusion/normal_monos/face_with_open_mouth.png diff --git a/packages/frontend/assets/drop-and-fusion/face_with_symbols_on_mouth.png b/packages/frontend/assets/drop-and-fusion/normal_monos/face_with_symbols_on_mouth.png similarity index 100% rename from packages/frontend/assets/drop-and-fusion/face_with_symbols_on_mouth.png rename to packages/frontend/assets/drop-and-fusion/normal_monos/face_with_symbols_on_mouth.png diff --git a/packages/frontend/assets/drop-and-fusion/grinning_squinting_face.png b/packages/frontend/assets/drop-and-fusion/normal_monos/grinning_squinting_face.png similarity index 100% rename from packages/frontend/assets/drop-and-fusion/grinning_squinting_face.png rename to packages/frontend/assets/drop-and-fusion/normal_monos/grinning_squinting_face.png diff --git a/packages/frontend/assets/drop-and-fusion/heart_suit.png b/packages/frontend/assets/drop-and-fusion/normal_monos/heart_suit.png similarity index 100% rename from packages/frontend/assets/drop-and-fusion/heart_suit.png rename to packages/frontend/assets/drop-and-fusion/normal_monos/heart_suit.png diff --git a/packages/frontend/assets/drop-and-fusion/pleading_face.png b/packages/frontend/assets/drop-and-fusion/normal_monos/pleading_face.png similarity index 100% rename from packages/frontend/assets/drop-and-fusion/pleading_face.png rename to packages/frontend/assets/drop-and-fusion/normal_monos/pleading_face.png diff --git a/packages/frontend/assets/drop-and-fusion/smiling_face_with_hearts.png b/packages/frontend/assets/drop-and-fusion/normal_monos/smiling_face_with_hearts.png similarity index 100% rename from packages/frontend/assets/drop-and-fusion/smiling_face_with_hearts.png rename to packages/frontend/assets/drop-and-fusion/normal_monos/smiling_face_with_hearts.png diff --git a/packages/frontend/assets/drop-and-fusion/smiling_face_with_sunglasses.png b/packages/frontend/assets/drop-and-fusion/normal_monos/smiling_face_with_sunglasses.png similarity index 100% rename from packages/frontend/assets/drop-and-fusion/smiling_face_with_sunglasses.png rename to packages/frontend/assets/drop-and-fusion/normal_monos/smiling_face_with_sunglasses.png diff --git a/packages/frontend/assets/drop-and-fusion/zany_face.png b/packages/frontend/assets/drop-and-fusion/normal_monos/zany_face.png similarity index 100% rename from packages/frontend/assets/drop-and-fusion/zany_face.png rename to packages/frontend/assets/drop-and-fusion/normal_monos/zany_face.png diff --git a/packages/frontend/assets/drop-and-fusion/ready.png b/packages/frontend/assets/drop-and-fusion/ready.png new file mode 100644 index 000000000..10a87fcf5 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/ready.png differ diff --git a/packages/frontend/assets/drop-and-fusion/keycap_1.png b/packages/frontend/assets/drop-and-fusion/square_monos/keycap_1.png similarity index 100% rename from packages/frontend/assets/drop-and-fusion/keycap_1.png rename to packages/frontend/assets/drop-and-fusion/square_monos/keycap_1.png diff --git a/packages/frontend/assets/drop-and-fusion/keycap_10.png b/packages/frontend/assets/drop-and-fusion/square_monos/keycap_10.png similarity index 100% rename from packages/frontend/assets/drop-and-fusion/keycap_10.png rename to packages/frontend/assets/drop-and-fusion/square_monos/keycap_10.png diff --git a/packages/frontend/assets/drop-and-fusion/keycap_2.png b/packages/frontend/assets/drop-and-fusion/square_monos/keycap_2.png similarity index 100% rename from packages/frontend/assets/drop-and-fusion/keycap_2.png rename to packages/frontend/assets/drop-and-fusion/square_monos/keycap_2.png diff --git a/packages/frontend/assets/drop-and-fusion/keycap_3.png b/packages/frontend/assets/drop-and-fusion/square_monos/keycap_3.png similarity index 100% rename from packages/frontend/assets/drop-and-fusion/keycap_3.png rename to packages/frontend/assets/drop-and-fusion/square_monos/keycap_3.png diff --git a/packages/frontend/assets/drop-and-fusion/keycap_4.png b/packages/frontend/assets/drop-and-fusion/square_monos/keycap_4.png similarity index 100% rename from packages/frontend/assets/drop-and-fusion/keycap_4.png rename to packages/frontend/assets/drop-and-fusion/square_monos/keycap_4.png diff --git a/packages/frontend/assets/drop-and-fusion/keycap_5.png b/packages/frontend/assets/drop-and-fusion/square_monos/keycap_5.png similarity index 100% rename from packages/frontend/assets/drop-and-fusion/keycap_5.png rename to packages/frontend/assets/drop-and-fusion/square_monos/keycap_5.png diff --git a/packages/frontend/assets/drop-and-fusion/keycap_6.png b/packages/frontend/assets/drop-and-fusion/square_monos/keycap_6.png similarity index 100% rename from packages/frontend/assets/drop-and-fusion/keycap_6.png rename to packages/frontend/assets/drop-and-fusion/square_monos/keycap_6.png diff --git a/packages/frontend/assets/drop-and-fusion/keycap_7.png b/packages/frontend/assets/drop-and-fusion/square_monos/keycap_7.png similarity index 100% rename from packages/frontend/assets/drop-and-fusion/keycap_7.png rename to packages/frontend/assets/drop-and-fusion/square_monos/keycap_7.png diff --git a/packages/frontend/assets/drop-and-fusion/keycap_8.png b/packages/frontend/assets/drop-and-fusion/square_monos/keycap_8.png similarity index 100% rename from packages/frontend/assets/drop-and-fusion/keycap_8.png rename to packages/frontend/assets/drop-and-fusion/square_monos/keycap_8.png diff --git a/packages/frontend/assets/drop-and-fusion/keycap_9.png b/packages/frontend/assets/drop-and-fusion/square_monos/keycap_9.png similarity index 100% rename from packages/frontend/assets/drop-and-fusion/keycap_9.png rename to packages/frontend/assets/drop-and-fusion/square_monos/keycap_9.png diff --git a/packages/frontend/assets/drop-and-fusion/sweets_monos/candy_color.svg b/packages/frontend/assets/drop-and-fusion/sweets_monos/candy_color.svg new file mode 100644 index 000000000..6eab3ca49 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/sweets_monos/candy_color.svg differ diff --git a/packages/frontend/assets/drop-and-fusion/sweets_monos/chocolate_bar_color.svg b/packages/frontend/assets/drop-and-fusion/sweets_monos/chocolate_bar_color.svg new file mode 100644 index 000000000..eea5fec18 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/sweets_monos/chocolate_bar_color.svg differ diff --git a/packages/frontend/assets/drop-and-fusion/sweets_monos/cookie_color.svg b/packages/frontend/assets/drop-and-fusion/sweets_monos/cookie_color.svg new file mode 100644 index 000000000..42b628cca Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/sweets_monos/cookie_color.svg differ diff --git a/packages/frontend/assets/drop-and-fusion/sweets_monos/custard_color.svg b/packages/frontend/assets/drop-and-fusion/sweets_monos/custard_color.svg new file mode 100644 index 000000000..d967fec7f Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/sweets_monos/custard_color.svg differ diff --git a/packages/frontend/assets/drop-and-fusion/sweets_monos/doughnut_color.svg b/packages/frontend/assets/drop-and-fusion/sweets_monos/doughnut_color.svg new file mode 100644 index 000000000..e8e225bc0 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/sweets_monos/doughnut_color.svg differ diff --git a/packages/frontend/assets/drop-and-fusion/sweets_monos/lollipop_color.svg b/packages/frontend/assets/drop-and-fusion/sweets_monos/lollipop_color.svg new file mode 100644 index 000000000..ad90ac6f5 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/sweets_monos/lollipop_color.svg differ diff --git a/packages/frontend/assets/drop-and-fusion/sweets_monos/pancakes_color.svg b/packages/frontend/assets/drop-and-fusion/sweets_monos/pancakes_color.svg new file mode 100644 index 000000000..94b2ab69b Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/sweets_monos/pancakes_color.svg differ diff --git a/packages/frontend/assets/drop-and-fusion/sweets_monos/shaved_ice_color.svg b/packages/frontend/assets/drop-and-fusion/sweets_monos/shaved_ice_color.svg new file mode 100644 index 000000000..64dfef8e0 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/sweets_monos/shaved_ice_color.svg differ diff --git a/packages/frontend/assets/drop-and-fusion/sweets_monos/shortcake_color.svg b/packages/frontend/assets/drop-and-fusion/sweets_monos/shortcake_color.svg new file mode 100644 index 000000000..66e8f91f1 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/sweets_monos/shortcake_color.svg differ diff --git a/packages/frontend/assets/drop-and-fusion/sweets_monos/soft_ice_cream_color.svg b/packages/frontend/assets/drop-and-fusion/sweets_monos/soft_ice_cream_color.svg new file mode 100644 index 000000000..37be9c0cb Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/sweets_monos/soft_ice_cream_color.svg differ diff --git a/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/candy_color.svg b/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/candy_color.svg new file mode 100644 index 000000000..e673f430f Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/candy_color.svg differ diff --git a/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/chocolate_bar_color.svg b/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/chocolate_bar_color.svg new file mode 100644 index 000000000..5cd39cc9e Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/chocolate_bar_color.svg differ diff --git a/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/custard_color.svg b/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/custard_color.svg new file mode 100644 index 000000000..dd4870dd7 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/custard_color.svg differ diff --git a/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/doughnut_color.svg b/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/doughnut_color.svg new file mode 100644 index 000000000..a8d5557f5 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/doughnut_color.svg differ diff --git a/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/lollipop_color.svg b/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/lollipop_color.svg new file mode 100644 index 000000000..d3778737a Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/lollipop_color.svg differ diff --git a/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/pancakes_color.svg b/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/pancakes_color.svg new file mode 100644 index 000000000..b1a1a322e Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/pancakes_color.svg differ diff --git a/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/shaved_ice_color.svg b/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/shaved_ice_color.svg new file mode 100644 index 000000000..00872c7a0 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/shaved_ice_color.svg differ diff --git a/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/shortcake_color.svg b/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/shortcake_color.svg new file mode 100644 index 000000000..e6ed1fbbf Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/shortcake_color.svg differ diff --git a/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/soft_ice_cream_color.svg b/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/soft_ice_cream_color.svg new file mode 100644 index 000000000..b77e0c365 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/sweets_monos/verts/soft_ice_cream_color.svg differ diff --git a/packages/frontend/assets/drop-and-fusion/yen_monos/10000yen.png b/packages/frontend/assets/drop-and-fusion/yen_monos/10000yen.png new file mode 100644 index 000000000..bda777719 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/yen_monos/10000yen.png differ diff --git a/packages/frontend/assets/drop-and-fusion/yen_monos/1000yen.png b/packages/frontend/assets/drop-and-fusion/yen_monos/1000yen.png new file mode 100644 index 000000000..4c462fb1f Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/yen_monos/1000yen.png differ diff --git a/packages/frontend/assets/drop-and-fusion/yen_monos/100yen.png b/packages/frontend/assets/drop-and-fusion/yen_monos/100yen.png new file mode 100644 index 000000000..8911543af Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/yen_monos/100yen.png differ diff --git a/packages/frontend/assets/drop-and-fusion/yen_monos/10yen.png b/packages/frontend/assets/drop-and-fusion/yen_monos/10yen.png new file mode 100644 index 000000000..041f77389 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/yen_monos/10yen.png differ diff --git a/packages/frontend/assets/drop-and-fusion/yen_monos/1yen.png b/packages/frontend/assets/drop-and-fusion/yen_monos/1yen.png new file mode 100644 index 000000000..cc6dcfd74 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/yen_monos/1yen.png differ diff --git a/packages/frontend/assets/drop-and-fusion/yen_monos/2000yen.png b/packages/frontend/assets/drop-and-fusion/yen_monos/2000yen.png new file mode 100644 index 000000000..6048b7c99 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/yen_monos/2000yen.png differ diff --git a/packages/frontend/assets/drop-and-fusion/yen_monos/5000yen.png b/packages/frontend/assets/drop-and-fusion/yen_monos/5000yen.png new file mode 100644 index 000000000..b0fe26db1 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/yen_monos/5000yen.png differ diff --git a/packages/frontend/assets/drop-and-fusion/yen_monos/500yen.png b/packages/frontend/assets/drop-and-fusion/yen_monos/500yen.png new file mode 100644 index 000000000..9e3d2b766 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/yen_monos/500yen.png differ diff --git a/packages/frontend/assets/drop-and-fusion/yen_monos/50yen.png b/packages/frontend/assets/drop-and-fusion/yen_monos/50yen.png new file mode 100644 index 000000000..c8ef08997 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/yen_monos/50yen.png differ diff --git a/packages/frontend/assets/drop-and-fusion/yen_monos/5yen.png b/packages/frontend/assets/drop-and-fusion/yen_monos/5yen.png new file mode 100644 index 000000000..b120bdca3 Binary files /dev/null and b/packages/frontend/assets/drop-and-fusion/yen_monos/5yen.png differ diff --git a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue new file mode 100644 index 000000000..c53bbca37 --- /dev/null +++ b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue @@ -0,0 +1,102 @@ + + + + + + + diff --git a/packages/frontend/src/components/MkFlashPreview.vue b/packages/frontend/src/components/MkFlashPreview.vue index ab435585d..1be8a6d8f 100644 --- a/packages/frontend/src/components/MkFlashPreview.vue +++ b/packages/frontend/src/components/MkFlashPreview.vue @@ -9,7 +9,9 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ flash.title }}

-

{{ flash.summary.length > 85 ? flash.summary.slice(0, 85) + '…' : flash.summary }}

+

+ +