From 1daab571757cf7fcfdac539cba423ff459bac377 Mon Sep 17 00:00:00 2001 From: mattyatea Date: Sun, 4 Oct 2026 08:45:14 +0000 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20Calls=E3=83=99=E3=83=BC=E3=82=B9?= =?UTF-8?q?=E3=81=AE=E7=B5=B5=E3=83=81=E3=83=A3=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/ja-JP.yml | 28 + packages/backend/src/core/CoreModule.ts | 3 + .../backend/src/core/GlobalEventService.ts | 9 + .../src/core/drawing/DrawingService.ts | 146 ++++ packages/backend/src/server/ServerModule.ts | 2 + .../backend/src/server/api/endpoint-list.ts | 10 + .../server/api/endpoints/drawing/_shared.ts | 61 ++ .../src/server/api/endpoints/drawing/clear.ts | 24 + .../src/server/api/endpoints/drawing/end.ts | 24 + .../src/server/api/endpoints/drawing/join.ts | 24 + .../src/server/api/endpoints/drawing/kick.ts | 24 + .../src/server/api/endpoints/drawing/leave.ts | 24 + .../server/api/endpoints/drawing/message.ts | 24 + .../src/server/api/endpoints/drawing/show.ts | 25 + .../src/server/api/endpoints/drawing/start.ts | 24 + .../server/api/endpoints/drawing/stroke.ts | 24 + .../src/server/api/stream/Connection.ts | 2 + .../src/server/api/stream/channels/drawing.ts | 45 + packages/backend/test/e2e/calls.ts | 46 ++ .../test/unit/core/drawing/DrawingService.ts | 127 +++ .../src/components/MkCallsRoomWindow.vue | 5 + packages/frontend/src/pages/drawing/canvas.ts | 42 + packages/frontend/src/pages/drawing/room.vue | 320 ++++++++ packages/frontend/src/router.definition.ts | 4 + packages/i18n/src/autogen/locale.ts | 106 +++ packages/misskey-js/etc/misskey-js.api.md | 56 +- .../misskey-js/src/autogen/apiClientJSDoc.ts | 99 +++ packages/misskey-js/src/autogen/endpoint.ts | 19 + packages/misskey-js/src/autogen/entities.ts | 10 + packages/misskey-js/src/autogen/types.ts | 770 ++++++++++++++++++ packages/misskey-js/src/streaming.types.ts | 12 + specs/drawing-chat/screenshots/desktop.png | Bin 0 -> 114115 bytes specs/drawing-chat/screenshots/mobile.png | Bin 0 -> 53107 bytes specs/drawing-chat/spec.md | 17 + specs/drawing-chat/verification.md | 25 + 35 files changed, 2180 insertions(+), 1 deletion(-) create mode 100644 packages/backend/src/core/drawing/DrawingService.ts create mode 100644 packages/backend/src/server/api/endpoints/drawing/_shared.ts create mode 100644 packages/backend/src/server/api/endpoints/drawing/clear.ts create mode 100644 packages/backend/src/server/api/endpoints/drawing/end.ts create mode 100644 packages/backend/src/server/api/endpoints/drawing/join.ts create mode 100644 packages/backend/src/server/api/endpoints/drawing/kick.ts create mode 100644 packages/backend/src/server/api/endpoints/drawing/leave.ts create mode 100644 packages/backend/src/server/api/endpoints/drawing/message.ts create mode 100644 packages/backend/src/server/api/endpoints/drawing/show.ts create mode 100644 packages/backend/src/server/api/endpoints/drawing/start.ts create mode 100644 packages/backend/src/server/api/endpoints/drawing/stroke.ts create mode 100644 packages/backend/src/server/api/stream/channels/drawing.ts create mode 100644 packages/backend/test/unit/core/drawing/DrawingService.ts create mode 100644 packages/frontend/src/pages/drawing/canvas.ts create mode 100644 packages/frontend/src/pages/drawing/room.vue create mode 100644 specs/drawing-chat/screenshots/desktop.png create mode 100644 specs/drawing-chat/screenshots/mobile.png create mode 100644 specs/drawing-chat/spec.md create mode 100644 specs/drawing-chat/verification.md diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 3e06082cdec..eb56d21e5ff 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -3876,3 +3876,31 @@ _events: tagsCaption: "カンマ区切りで入力" rateLimitReached: "イベントの作成上限に達しました(1日5件まで)" unknownError: "エラーが発生しました" + +_drawing: + title: "絵チャ" + start: "キャンバスを開く" + join: "絵チャに参加" + leave: "絵チャから退出" + scope: "参加できる人" + public: "公開ルームにアクセスできる人" + chatRoom: "ChatRoomのメンバー" + calls: "Callsに参加している人" + pen: "ペン" + eraser: "消しゴム" + color: "色" + width: "太さ" + clear: "全消去" + clearConfirm: "キャンバスを全消去しますか? 全員の絵が消えます。" + end: "絵チャを終了" + endConfirm: "絵チャを終了しますか? 終了後は編集できません。必要なら先に画像をドライブへ保存してください。" + ended: "この絵チャは終了しました。画像の保存・添付は引き続きできます。" + unavailable: "この絵チャにはアクセスできません。参加範囲とCallsの参加状態を確認してください。" + waiting: "ホストがキャンバスを開くと参加できます。" + save: "ドライブに保存" + attach: "画像を添付して投稿" + remove: "絵チャから退出させる" + removeConfirm: "この参加者を絵チャから退出させますか? 再参加できなくなります。" + canvasFull: "キャンバスの容量に達しました。画像を保存してから、ホストが全消去してください。" + description: "1280×720、同時参加16人まで。音声に接続せずに描くこともできます。" + disconnected: "再接続を待っています。" diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index e90295f8183..0045149ea68 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { DrawingService } from './drawing/DrawingService.js'; import { Module } from '@nestjs/common'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; import { AbuseReportService } from '@/core/AbuseReportService.js'; @@ -343,6 +344,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting QueueModule, ], providers: [ + DrawingService, LoggerService, AbuseReportService, AbuseReportNotificationService, @@ -674,6 +676,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting //#endregion ], exports: [ + DrawingService, QueueModule, LoggerService, AbuseReportService, diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index 7c8ef935e49..ac4a54b96d3 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -20,6 +20,7 @@ import type { MiPage } from '@/models/Page.js'; import type { MiWebhook } from '@/models/Webhook.js'; import type { MiSystemWebhook } from '@/models/SystemWebhook.js'; import type { MiMeta } from '@/models/Meta.js'; +import type { DrawingEvent } from './drawing/DrawingService.js'; import { MiAvatarDecoration, MiChatMessage, MiChatRoom, MiReversiGame, MiRole, MiRoleAssignment } from '@/models/_.js'; import type { MiCallsRoom } from '@/models/CallsRoom.js'; import type { Packed } from '@/misc/json-schema.js'; @@ -334,6 +335,10 @@ export type GlobalEvents = { name: `chatRoomStream:${MiChatRoom['id']}`; payload: EventTypesToEventPayload; }; + drawing: { + name: `drawingStream:${string}`; + payload: { type: 'updated'; body: DrawingEvent }; + }; callsRoom: { name: `callsRoomStream:${MiCallsRoom['id']}`; payload: EventTypesToEventPayload; @@ -453,6 +458,10 @@ export class GlobalEventService { this.publish(`chatRoomStream:${toRoomId}`, type, typeof value === 'undefined' ? null : value); } + public publishDrawingStream(roomId: string, event: DrawingEvent): void { + this.publish(`drawingStream:${roomId}`, 'updated', event); + } + @bindThis public publishCallsRoomStream(roomId: MiCallsRoom['id'], type: K, value: CallsRoomEventTypes[K]): void { this.publish(`callsRoomStream:${roomId}`, type, value); diff --git a/packages/backend/src/core/drawing/DrawingService.ts b/packages/backend/src/core/drawing/DrawingService.ts new file mode 100644 index 00000000000..efe1f5606ca --- /dev/null +++ b/packages/backend/src/core/drawing/DrawingService.ts @@ -0,0 +1,146 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { randomUUID } from 'node:crypto'; +import { Inject, Injectable } from '@nestjs/common'; +import * as Redis from 'ioredis'; +import { DI } from '@/di-symbols.js'; +import type { CallsParticipantsRepository, MiUser } from '@/models/_.js'; +import { CallsRoomService } from '@/core/calls/CallsRoomService.js'; +import { CallsLiveConnectionService } from '@/core/calls/CallsLiveConnectionService.js'; +import { ChatService } from '@/core/ChatService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; + +export type DrawingScope = 'public' | 'chatRoom' | 'calls'; +export type DrawingStroke = { color: string; width: number; eraser: boolean; points: number[][] }; +export type DrawingMessage = { userId: string; text: string; id: string }; +export type DrawingControl = { + canvasId: string; + version: number; + scope: DrawingScope; + ended: boolean; + participantIds: string[]; + removedIds: string[]; + lastSeen: Record; +}; +export type DrawingSnapshot = Omit & { strokes: DrawingStroke[]; messages: DrawingMessage[] }; +export type DrawingEvent = Omit & { action: string; stroke: DrawingStroke | null; message: DrawingMessage | null }; +export type DrawingAction = 'start' | 'join' | 'leave' | 'stroke' | 'clear' | 'end' | 'kick' | 'message'; +export class DrawingError extends Error { + constructor(public readonly code: 'accessDenied' | 'invalidState' | 'roomFull' | 'canvasFull') { super(code); } +} + +@Injectable() +export class DrawingService { + constructor( + @Inject(DI.redis) private redis: Redis.Redis, + @Inject(DI.callsParticipantsRepository) private callsParticipants: CallsParticipantsRepository, + private callsRoomService: CallsRoomService, + private liveConnections: CallsLiveConnectionService, + private chatService: ChatService, + private events: GlobalEventService, + ) {} + + private key(roomId: string) { return `drawing:${roomId}`; } + + private async control(roomId: string): Promise { + const raw = await this.redis.get(this.key(roomId)); + return raw == null ? null : JSON.parse(raw) as DrawingControl; + } + + public async assertAccess(user: MiUser, roomId: string, control?: DrawingControl | null, write = false) { + const room = await this.callsRoomService.getRoom(roomId); + await this.callsRoomService.assertCanAccess(user, room); + const availability = await this.chatService.getChatAvailability(user.id); + if (!availability[write ? 'write' : 'read']) throw new DrawingError('accessDenied'); + const state = control === undefined ? await this.control(roomId) : control; + if (state?.removedIds.includes(user.id)) throw new DrawingError('accessDenied'); + const participant = await this.callsParticipants.findOneBy({ roomId, userId: user.id }); + if (participant?.state === 'removed') throw new DrawingError('accessDenied'); + if (state?.scope === 'calls') { + if (participant?.state !== 'active' || await this.liveConnections.get(participant.id) == null) throw new DrawingError('accessDenied'); + } + // Calls moderators may inspect a ChatRoom, but drawing is limited to its members. + if (state?.scope === 'chatRoom') { + const chatRoom = room.chatRoomId == null ? null : await this.chatService.findRoomById(room.chatRoomId); + if (chatRoom == null || !await this.chatService.isRoomMember(chatRoom, user.id)) throw new DrawingError('accessDenied'); + } + return room; + } + + public async snapshot(user: MiUser, roomId: string): Promise { + return this.liveConnections.withRoomLock(roomId, async () => { + const control = await this.control(roomId); + await this.assertAccess(user, roomId, control); + if (control == null) return null; + const { removedIds, lastSeen, ...visible } = control; + const [strokes, messages] = await Promise.all([ + this.redis.lrange(`${this.key(roomId)}:strokes`, 0, -1), + this.redis.lrange(`${this.key(roomId)}:messages`, 0, -1), + ]); + const room = await this.callsRoomService.getRoom(roomId); + return { ...visible, ended: visible.ended || room.state !== 'open', participantIds: visible.participantIds.filter(id => (control.lastSeen[id] ?? 0) > Date.now() - 90000), strokes: strokes.map(x => JSON.parse(x) as DrawingStroke), messages: messages.map(x => JSON.parse(x) as DrawingMessage) }; + }); + } + + public async update(user: MiUser, roomId: string, action: DrawingAction, params: { canvasId?: string; scope?: DrawingScope; userId?: string; stroke?: DrawingStroke; text?: string }): Promise { + await this.liveConnections.withRoomLock(roomId, async assertHeld => { + let control = await this.control(roomId); + const room = await this.assertAccess(user, roomId, control, true); + if (room.state !== 'open') throw new DrawingError('invalidState'); + const ownerAction = ['start', 'clear', 'end', 'kick'].includes(action); + if (ownerAction && room.ownerUserId !== user.id) throw new DrawingError('accessDenied'); + const transaction = this.redis.multi(); + const key = this.key(roomId); + if (action === 'start') { + if (control != null || params.scope == null) throw new DrawingError('invalidState'); + if (params.scope === 'public' && (room.attachmentType !== 'personal' || room.visibility !== 'public')) throw new DrawingError('accessDenied'); + if (params.scope === 'chatRoom' && room.chatRoomId == null) throw new DrawingError('accessDenied'); + control = { canvasId: randomUUID(), version: 0, scope: params.scope, ended: false, participantIds: [user.id], removedIds: [], lastSeen: { [user.id]: Date.now() } }; + await this.assertAccess(user, roomId, control, true); + } else { + if (control == null || control.ended || control.canvasId !== params.canvasId) throw new DrawingError('invalidState'); + control.participantIds = control.participantIds.filter(id => (control!.lastSeen[id] ?? 0) > Date.now() - 90000); + if (action === 'join') { + control.lastSeen[user.id] = Date.now(); + if (!control.participantIds.includes(user.id)) { + if (control.participantIds.length >= 16) throw new DrawingError('roomFull'); + control.participantIds.push(user.id); + } + } else if (action === 'leave') { + control.participantIds = control.participantIds.filter(id => id !== user.id); + } else if (action === 'kick') { + if (params.userId == null || params.userId === room.ownerUserId || !control.participantIds.includes(params.userId)) throw new DrawingError('invalidState'); + control.participantIds = control.participantIds.filter(id => id !== params.userId); + control.removedIds.push(params.userId); + } else if (action === 'clear') { + control.canvasId = randomUUID(); + transaction.del(`${key}:strokes`); + } else if (action === 'end') { + control.ended = true; + } else { + if (!control.participantIds.includes(user.id)) throw new DrawingError('accessDenied'); + if (action === 'stroke') { + if (params.stroke == null) throw new DrawingError('invalidState'); + if (await this.redis.llen(`${key}:strokes`) >= 10000) throw new DrawingError('canvasFull'); + transaction.rpush(`${key}:strokes`, JSON.stringify(params.stroke)); + } else if (action === 'message') { + if (params.text == null || params.text.trim().length === 0) throw new DrawingError('invalidState'); + } + } + } + const message = action === 'message' ? { userId: user.id, text: params.text!.trim(), id: randomUUID() } : null; + if (message != null) transaction.rpush(`${key}:messages`, JSON.stringify(message)).ltrim(`${key}:messages`, -50, -1); + control.version++; + await assertHeld(); + transaction.set(key, JSON.stringify(control)); + const result = await transaction.exec(); + const error = result?.find(([err]) => err != null)?.[0]; + if (error != null) throw error; + const { removedIds, lastSeen, ...visible } = control; + this.events.publishDrawingStream(roomId, { ...visible, action, stroke: action === 'stroke' ? params.stroke! : null, message }); + }); + } +} diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts index dc360e2fe54..1308c663178 100644 --- a/packages/backend/src/server/ServerModule.ts +++ b/packages/backend/src/server/ServerModule.ts @@ -46,6 +46,7 @@ import { ServerStatsChannel } from './api/stream/channels/server-stats.js'; import { UserListChannel } from './api/stream/channels/user-list.js'; import { RoleTimelineChannel } from './api/stream/channels/role-timeline.js'; import { ChatUserChannel } from './api/stream/channels/chat-user.js'; +import { DrawingChannel } from '@/server/api/stream/channels/drawing.js'; import { ChatRoomChannel } from './api/stream/channels/chat-room.js'; import { CallsRoomChannel } from './api/stream/channels/calls-room.js'; import { CallsRoomsChannel } from './api/stream/channels/calls-rooms.js'; @@ -93,6 +94,7 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j RoleTimelineChannel, ChatUserChannel, ChatRoomChannel, + DrawingChannel, CallsRoomChannel, CallsRoomsChannel, ReversiChannel, diff --git a/packages/backend/src/server/api/endpoint-list.ts b/packages/backend/src/server/api/endpoint-list.ts index 1e8b8451047..e764ad0ed47 100644 --- a/packages/backend/src/server/api/endpoint-list.ts +++ b/packages/backend/src/server/api/endpoint-list.ts @@ -516,3 +516,13 @@ export * as 'admin/inbox-rule/edit' from './endpoints/admin/inbox-rule/edit.js'; export * as 'admin/inbox-rule/set' from './endpoints/admin/inbox-rule/set.js'; export * as 'admin/inbox-rule/delete' from './endpoints/admin/inbox-rule/delete.js'; export * as 'admin/inbox-rule/list' from './endpoints/admin/inbox-rule/list.js'; + +export * as 'drawing/show' from './endpoints/drawing/show.js'; +export * as 'drawing/start' from './endpoints/drawing/start.js'; +export * as 'drawing/join' from './endpoints/drawing/join.js'; +export * as 'drawing/leave' from './endpoints/drawing/leave.js'; +export * as 'drawing/stroke' from './endpoints/drawing/stroke.js'; +export * as 'drawing/clear' from './endpoints/drawing/clear.js'; +export * as 'drawing/end' from './endpoints/drawing/end.js'; +export * as 'drawing/kick' from './endpoints/drawing/kick.js'; +export * as 'drawing/message' from './endpoints/drawing/message.js'; diff --git a/packages/backend/src/server/api/endpoints/drawing/_shared.ts b/packages/backend/src/server/api/endpoints/drawing/_shared.ts new file mode 100644 index 00000000000..3f50f201d66 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/drawing/_shared.ts @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { DrawingError } from '@/core/drawing/DrawingService.js'; +import { ApiError } from '@/server/api/error.js'; +import { callsApiError, callsErrors } from '../calls/_shared.js'; + +export const drawingErrors = { + ...callsErrors, + drawingAccessDenied: { message: 'You cannot access this canvas.', code: 'DRAWING_ACCESS_DENIED', id: '3e6830dc-0b80-41fe-84d3-dc7d4fe0d725' }, + drawingInvalidState: { message: 'The canvas is not in the required state.', code: 'DRAWING_INVALID_STATE', id: '175cdb1a-5156-472b-8387-a94f64e2cc12' }, + drawingRoomFull: { message: 'The canvas has reached its participant limit.', code: 'DRAWING_ROOM_FULL', id: '5489fcf0-205d-4800-b460-4bba2929da29' }, + drawingCanvasFull: { message: 'Export or clear the canvas before continuing.', code: 'DRAWING_CANVAS_FULL', id: 'f47a90a1-766c-4c4c-a7c3-78ace3ca86ed' }, +} as const; + +export function drawingApiError(error: unknown): never { + if (error instanceof DrawingError) { + const definition = { + accessDenied: drawingErrors.drawingAccessDenied, + invalidState: drawingErrors.drawingInvalidState, + roomFull: drawingErrors.drawingRoomFull, + canvasFull: drawingErrors.drawingCanvasFull, + }[error.code]; + throw new ApiError(definition); + } + return callsApiError(error); +} + +export const strokeSchema = { + type: 'object', optional: false, nullable: false, + properties: { + color: { type: 'string', optional: false, nullable: false, pattern: '^#[0-9a-fA-F]{6}$' }, + width: { type: 'number', optional: false, nullable: false, minimum: 1, maximum: 64 }, + eraser: { type: 'boolean', optional: false, nullable: false }, + points: { type: 'array', optional: false, nullable: false, minItems: 1, maxItems: 64, items: { + type: 'array', optional: false, nullable: false, minItems: 2, maxItems: 2, + items: { type: 'number', optional: false, nullable: false, minimum: 0, maximum: 1280 }, + } }, + }, required: ['color', 'width', 'eraser', 'points'], additionalProperties: false, +} as const; + +export const messageSchema = { type: 'object', optional: false, nullable: false, properties: { + id: { type: 'string', optional: false, nullable: false }, + userId: { type: 'string', optional: false, nullable: false }, + text: { type: 'string', optional: false, nullable: false }, +} } as const; + +export const snapshotSchema = { + type: 'object', optional: false, nullable: true, + properties: { + canvasId: { type: 'string', optional: false, nullable: false }, + version: { type: 'number', optional: false, nullable: false }, + scope: { type: 'string', optional: false, nullable: false, enum: ['public', 'chatRoom', 'calls'] }, + ended: { type: 'boolean', optional: false, nullable: false }, + participantIds: { type: 'array', optional: false, nullable: false, items: { type: 'string', optional: false, nullable: false } }, + strokes: { type: 'array', optional: false, nullable: false, items: strokeSchema }, + messages: { type: 'array', optional: false, nullable: false, items: messageSchema }, + }, +} as const; diff --git a/packages/backend/src/server/api/endpoints/drawing/clear.ts b/packages/backend/src/server/api/endpoints/drawing/clear.ts new file mode 100644 index 00000000000..0076a998b16 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/drawing/clear.ts @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { DrawingService } from '@/core/drawing/DrawingService.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { drawingApiError, drawingErrors } from './_shared.js'; + +export const meta = { + tags: ['calls'], requireCredential: true, prohibitMoved: true, kind: 'write:calls', + limit: { duration: 1000 * 60, max: 60 }, errors: drawingErrors, +} as const; +export const paramDef = { type: 'object', properties: { roomId: { type: 'string', format: 'misskey:id' }, canvasId: { type: 'string', pattern: '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' } }, required: ['roomId', 'canvasId'] } as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor(drawingService: DrawingService) { + super(meta, paramDef, async (ps, me) => { + try { await drawingService.update(me, ps.roomId, 'clear', { canvasId: ps.canvasId }); } catch (error) { drawingApiError(error); } + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/drawing/end.ts b/packages/backend/src/server/api/endpoints/drawing/end.ts new file mode 100644 index 00000000000..3c7b71e7d4d --- /dev/null +++ b/packages/backend/src/server/api/endpoints/drawing/end.ts @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { DrawingService } from '@/core/drawing/DrawingService.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { drawingApiError, drawingErrors } from './_shared.js'; + +export const meta = { + tags: ['calls'], requireCredential: true, prohibitMoved: true, kind: 'write:calls', + limit: { duration: 1000 * 60, max: 60 }, errors: drawingErrors, +} as const; +export const paramDef = { type: 'object', properties: { roomId: { type: 'string', format: 'misskey:id' }, canvasId: { type: 'string', pattern: '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' } }, required: ['roomId', 'canvasId'] } as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor(drawingService: DrawingService) { + super(meta, paramDef, async (ps, me) => { + try { await drawingService.update(me, ps.roomId, 'end', { canvasId: ps.canvasId }); } catch (error) { drawingApiError(error); } + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/drawing/join.ts b/packages/backend/src/server/api/endpoints/drawing/join.ts new file mode 100644 index 00000000000..1b1b36d2542 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/drawing/join.ts @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { DrawingService } from '@/core/drawing/DrawingService.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { drawingApiError, drawingErrors } from './_shared.js'; + +export const meta = { + tags: ['calls'], requireCredential: true, prohibitMoved: true, kind: 'write:calls', + limit: { duration: 1000 * 60, max: 60 }, errors: drawingErrors, +} as const; +export const paramDef = { type: 'object', properties: { roomId: { type: 'string', format: 'misskey:id' }, canvasId: { type: 'string', pattern: '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' } }, required: ['roomId', 'canvasId'] } as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor(drawingService: DrawingService) { + super(meta, paramDef, async (ps, me) => { + try { await drawingService.update(me, ps.roomId, 'join', { canvasId: ps.canvasId }); } catch (error) { drawingApiError(error); } + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/drawing/kick.ts b/packages/backend/src/server/api/endpoints/drawing/kick.ts new file mode 100644 index 00000000000..03768682b04 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/drawing/kick.ts @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { DrawingService } from '@/core/drawing/DrawingService.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { drawingApiError, drawingErrors } from './_shared.js'; + +export const meta = { + tags: ['calls'], requireCredential: true, prohibitMoved: true, kind: 'write:calls', + limit: { duration: 1000 * 60, max: 60 }, errors: drawingErrors, +} as const; +export const paramDef = { type: 'object', properties: { roomId: { type: 'string', format: 'misskey:id' }, canvasId: { type: 'string', pattern: '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' }, userId: { type: 'string', format: 'misskey:id' } }, required: ['roomId', 'canvasId', 'userId'] } as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor(drawingService: DrawingService) { + super(meta, paramDef, async (ps, me) => { + try { await drawingService.update(me, ps.roomId, 'kick', { canvasId: ps.canvasId, userId: ps.userId }); } catch (error) { drawingApiError(error); } + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/drawing/leave.ts b/packages/backend/src/server/api/endpoints/drawing/leave.ts new file mode 100644 index 00000000000..fee5cf05ea5 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/drawing/leave.ts @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { DrawingService } from '@/core/drawing/DrawingService.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { drawingApiError, drawingErrors } from './_shared.js'; + +export const meta = { + tags: ['calls'], requireCredential: true, prohibitMoved: true, kind: 'write:calls', + limit: { duration: 1000 * 60, max: 60 }, errors: drawingErrors, +} as const; +export const paramDef = { type: 'object', properties: { roomId: { type: 'string', format: 'misskey:id' }, canvasId: { type: 'string', pattern: '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' } }, required: ['roomId', 'canvasId'] } as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor(drawingService: DrawingService) { + super(meta, paramDef, async (ps, me) => { + try { await drawingService.update(me, ps.roomId, 'leave', { canvasId: ps.canvasId }); } catch (error) { drawingApiError(error); } + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/drawing/message.ts b/packages/backend/src/server/api/endpoints/drawing/message.ts new file mode 100644 index 00000000000..85fd0d50c43 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/drawing/message.ts @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { DrawingService } from '@/core/drawing/DrawingService.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { drawingApiError, drawingErrors } from './_shared.js'; + +export const meta = { + tags: ['calls'], requireCredential: true, prohibitMoved: true, kind: 'write:calls', + limit: { duration: 1000 * 60, max: 20 }, errors: drawingErrors, +} as const; +export const paramDef = { type: 'object', properties: { roomId: { type: 'string', format: 'misskey:id' }, canvasId: { type: 'string', pattern: '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' }, text: { type: 'string', minLength: 1, maxLength: 500 } }, required: ['roomId', 'canvasId', 'text'] } as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor(drawingService: DrawingService) { + super(meta, paramDef, async (ps, me) => { + try { await drawingService.update(me, ps.roomId, 'message', { canvasId: ps.canvasId, text: ps.text }); } catch (error) { drawingApiError(error); } + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/drawing/show.ts b/packages/backend/src/server/api/endpoints/drawing/show.ts new file mode 100644 index 00000000000..bcc19b0eef8 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/drawing/show.ts @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { DrawingService } from '@/core/drawing/DrawingService.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { drawingApiError, drawingErrors, snapshotSchema } from './_shared.js'; + +export const meta = { + tags: ['calls'], requireCredential: true, kind: 'read:calls', + limit: { duration: 1000 * 60, max: 60 }, errors: drawingErrors, + res: { type: 'object', optional: false, nullable: false, properties: { canvas: snapshotSchema } }, +} as const; +export const paramDef = { type: 'object', properties: { roomId: { type: 'string', format: 'misskey:id' } }, required: ['roomId'] } as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor(drawingService: DrawingService) { + super(meta, paramDef, async (ps, me) => { + try { return { canvas: await drawingService.snapshot(me, ps.roomId) }; } catch (error) { drawingApiError(error); } + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/drawing/start.ts b/packages/backend/src/server/api/endpoints/drawing/start.ts new file mode 100644 index 00000000000..d50212c7618 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/drawing/start.ts @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { DrawingService } from '@/core/drawing/DrawingService.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { drawingApiError, drawingErrors } from './_shared.js'; + +export const meta = { + tags: ['calls'], requireCredential: true, prohibitMoved: true, kind: 'write:calls', + limit: { duration: 1000 * 60, max: 60 }, errors: drawingErrors, +} as const; +export const paramDef = { type: 'object', properties: { roomId: { type: 'string', format: 'misskey:id' }, scope: { type: 'string', enum: ['public', 'chatRoom', 'calls'] } }, required: ['roomId', 'scope'] } as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor(drawingService: DrawingService) { + super(meta, paramDef, async (ps, me) => { + try { await drawingService.update(me, ps.roomId, 'start', { scope: ps.scope }); } catch (error) { drawingApiError(error); } + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/drawing/stroke.ts b/packages/backend/src/server/api/endpoints/drawing/stroke.ts new file mode 100644 index 00000000000..43521b7dc4e --- /dev/null +++ b/packages/backend/src/server/api/endpoints/drawing/stroke.ts @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { DrawingService, DrawingError } from '@/core/drawing/DrawingService.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { drawingApiError, drawingErrors, strokeSchema } from './_shared.js'; + +export const meta = { + tags: ['calls'], requireCredential: true, prohibitMoved: true, kind: 'write:calls', + limit: { duration: 1000 * 60, max: 300 }, errors: drawingErrors, +} as const; +export const paramDef = { type: 'object', properties: { roomId: { type: 'string', format: 'misskey:id' }, canvasId: { type: 'string', pattern: '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' }, stroke: strokeSchema }, required: ['roomId', 'canvasId', 'stroke'] } as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor(drawingService: DrawingService) { + super(meta, paramDef, async (ps, me) => { + try { if (ps.stroke.points.some(point => point[1] > 720)) throw new DrawingError('invalidState'); await drawingService.update(me, ps.roomId, 'stroke', { canvasId: ps.canvasId, stroke: ps.stroke }); } catch (error) { drawingApiError(error); } + }); + } +} diff --git a/packages/backend/src/server/api/stream/Connection.ts b/packages/backend/src/server/api/stream/Connection.ts index 9b0bc36cae3..df07dd42cd9 100644 --- a/packages/backend/src/server/api/stream/Connection.ts +++ b/packages/backend/src/server/api/stream/Connection.ts @@ -33,6 +33,7 @@ import { ServerStatsChannel } from '@/server/api/stream/channels/server-stats.js import { QueueStatsChannel } from '@/server/api/stream/channels/queue-stats.js'; import { AdminChannel } from '@/server/api/stream/channels/admin.js'; import { ChatUserChannel } from '@/server/api/stream/channels/chat-user.js'; +import { DrawingChannel } from '@/server/api/stream/channels/drawing.js'; import { ChatRoomChannel } from '@/server/api/stream/channels/chat-room.js'; import { CallsRoomChannel } from '@/server/api/stream/channels/calls-room.js'; import { CallsRoomsChannel } from '@/server/api/stream/channels/calls-rooms.js'; @@ -340,6 +341,7 @@ export default class Connection { case 'admin': return AdminChannel; case 'chatUser': return ChatUserChannel; case 'chatRoom': return ChatRoomChannel; + case 'drawing': return DrawingChannel; case 'callsRoom': return CallsRoomChannel; case 'callsRooms': return CallsRoomsChannel; case 'reversi': return ReversiChannel; diff --git a/packages/backend/src/server/api/stream/channels/drawing.ts b/packages/backend/src/server/api/stream/channels/drawing.ts new file mode 100644 index 00000000000..4ac529ebf5c --- /dev/null +++ b/packages/backend/src/server/api/stream/channels/drawing.ts @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable, Scope } from '@nestjs/common'; +import { REQUEST } from '@nestjs/core'; +import { bindThis } from '@/decorators.js'; +import type { GlobalEvents } from '@/core/GlobalEventService.js'; +import { DrawingService } from '@/core/drawing/DrawingService.js'; +import type { JsonObject } from '@/misc/json-value.js'; +import Channel, { type ChannelRequest } from '../channel.js'; + +@Injectable({ scope: Scope.TRANSIENT }) +export class DrawingChannel extends Channel { + public readonly chName = 'drawing'; + public static shouldShare = false; + public static requireCredential = true as const; + public static kind = 'read:calls'; + private roomId: string; + + constructor(@Inject(REQUEST) request: ChannelRequest, private drawingService: DrawingService) { super(request); } + + @bindThis + public async init(params: JsonObject): Promise { + if (typeof params.roomId !== 'string' || this.user == null) return false; + this.roomId = params.roomId; + try { await this.drawingService.assertAccess(this.user, this.roomId); } catch { return false; } + this.subscriber.on(`drawingStream:${this.roomId}`, this.onEvent); + return true; + } + + @bindThis + private async onEvent(data: GlobalEvents['drawing']['payload']) { + try { await this.drawingService.assertAccess(this.user!, this.roomId); } catch { + this.send('revoked', null); + this.dispose(); + return; + } + this.send(data.type, data.body); + } + + @bindThis + public dispose() { this.subscriber.off(`drawingStream:${this.roomId}`, this.onEvent); } +} diff --git a/packages/backend/test/e2e/calls.ts b/packages/backend/test/e2e/calls.ts index 278f7a871aa..8fb9fd7614b 100644 --- a/packages/backend/test/e2e/calls.ts +++ b/packages/backend/test/e2e/calls.ts @@ -6,6 +6,7 @@ process.env.NODE_ENV = 'test'; import { beforeAll, describe, expect, test } from 'vitest'; +import { loadConfig } from '@/config.js'; import { api, castAsError, signup } from '../utils.js'; import type * as misskey from 'misskey-js'; @@ -60,4 +61,49 @@ describe('Calls', () => { expect(castAsError(response.body ?? {}).error.code).toBe('CALLS_INVALID_STATE'); }); }); + + describe('drawing/stroke', () => { + test.each([ + { color: '#112233', width: 4, eraser: false, points: [] }, + { color: '#112233', width: 65, eraser: false, points: [[10, 20]] }, + { color: 'invalid', width: 4, eraser: false, points: [[10, 20]] }, + ])('描画入力のサイズと色を検証する (%j)', async stroke => { + const response = await api('drawing/stroke', { + roomId: '9a000000000000000000000000000000', + canvasId: '11111111-1111-4111-8111-111111111111', + stroke, + }, alice); + expect(response.status).toBe(400); + expect(castAsError(response.body ?? {}).error.code).toBe('INVALID_PARAM'); + }); + }); + + describe.runIf(loadConfig().cloudflareRealtime?.enabled === true)('Drawing room lifecycle', () => { + test('公開キャンバスで参加・描画・文字チャット・退出・消去・終了を同期する', async () => { + const bob = await signup({ username: 'drawing_bob' }); + const created = await api('calls/rooms/create', { attachmentType: 'personal', title: 'Drawing', visibility: 'public' }, alice); + expect(created.status).toBe(200); + const roomId = created.body.id; + await api('calls/rooms/open', { roomId, expectedRevision: created.body.revision }, alice); + expect((await api('drawing/show', { roomId }, alice)).body.canvas).toBeNull(); + expect((await api('drawing/start', { roomId, scope: 'public' }, alice)).status).toBe(204); + const canvasId = (await api('drawing/show', { roomId }, alice)).body.canvas!.canvasId; + expect((await api('drawing/join', { roomId, canvasId }, bob)).status).toBe(204); + const stroke = { color: '#112233', width: 4, eraser: false, points: [[10, 20], [30, 40]] }; + expect((await api('drawing/stroke', { roomId, canvasId, stroke }, bob)).status).toBe(204); + expect((await api('drawing/message', { roomId, canvasId, text: 'hello' }, bob)).status).toBe(204); + expect((await api('drawing/show', { roomId }, alice)).body.canvas).toMatchObject({ strokes: [stroke], messages: [{ userId: bob.id, text: 'hello' }] }); + await api('drawing/leave', { roomId, canvasId }, bob); + expect(castAsError((await api('drawing/stroke', { roomId, canvasId, stroke }, bob)).body ?? {}).error.code).toBe('DRAWING_ACCESS_DENIED'); + await api('drawing/join', { roomId, canvasId }, bob); + await api('drawing/kick', { roomId, canvasId, userId: bob.id }, alice); + expect(castAsError((await api('drawing/show', { roomId }, bob)).body ?? {}).error.code).toBe('DRAWING_ACCESS_DENIED'); + await api('drawing/clear', { roomId, canvasId }, alice); + const cleared = (await api('drawing/show', { roomId }, alice)).body.canvas!; + expect(cleared.strokes).toEqual([]); + expect(cleared.canvasId).not.toBe(canvasId); + await api('drawing/end', { roomId, canvasId: cleared.canvasId }, alice); + expect((await api('drawing/show', { roomId }, alice)).body.canvas?.ended).toBe(true); + }); + }); }); diff --git a/packages/backend/test/unit/core/drawing/DrawingService.ts b/packages/backend/test/unit/core/drawing/DrawingService.ts new file mode 100644 index 00000000000..b0cd52bf368 --- /dev/null +++ b/packages/backend/test/unit/core/drawing/DrawingService.ts @@ -0,0 +1,127 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { describe, expect, test, vi } from 'vitest'; +import { DrawingService } from '@/core/drawing/DrawingService.js'; +import type { MiUser } from '@/models/_.js'; + +class MemoryRedis { + public values = new Map(); + public lists = new Map(); + public async get(key: string) { return this.values.get(key) ?? null; } + public async llen(key: string) { return this.lists.get(key)?.length ?? 0; } + public async lrange(key: string) { return this.lists.get(key) ?? []; } + public multi() { + const commands: (() => void)[] = []; + const transaction = { + set: (key: string, value: string) => { commands.push(() => { this.values.set(key, value); }); return transaction; }, + del: (key: string) => { commands.push(() => { this.lists.delete(key); }); return transaction; }, + rpush: (key: string, value: string) => { commands.push(() => { this.lists.set(key, [...(this.lists.get(key) ?? []), value]); }); return transaction; }, + ltrim: (key: string, start: number) => { commands.push(() => { this.lists.set(key, (this.lists.get(key) ?? []).slice(start)); }); return transaction; }, + exec: async () => { commands.forEach(command => command()); return [[null, 'OK']]; }, + }; + return transaction; + } +} + +const owner = { id: 'owner', host: null } as MiUser; +const member = { id: 'member', host: null } as MiUser; +const outsider = { id: 'outsider', host: null } as MiUser; +const stroke = { color: '#123456', width: 4, eraser: false, points: [[20, 30], [40, 50]] }; +function fixture() { + const redis = new MemoryRedis(); + const room = { id: 'room', ownerUserId: owner.id, attachmentType: 'personal', visibility: 'public', state: 'open', chatRoomId: null as string | null }; + const participants = new Map(); + const live = new Set(); + const memberships = new Set([owner.id, member.id]); + const events = vi.fn(); + const service = new DrawingService(redis as never, { findOneBy: async ({ userId }: { userId: string }) => participants.get(userId) ?? null } as never, + { getRoom: async () => room, assertCanAccess: async () => {} } as never, + { withRoomLock: async (_id: string, callback: (assertHeld: () => Promise) => Promise) => callback(async () => {}), get: async (id: string) => live.has(id) ? {} : null } as never, + { getChatAvailability: async () => ({ read: true, write: true }), findRoomById: async () => ({ id: 'chat' }), isRoomMember: async (_room: unknown, id: string) => memberships.has(id) } as never, + { publishDrawingStream: events } as never); + return { service, redis, room, participants, live, memberships, events }; +} + +async function start(f: ReturnType, scope: 'public' | 'chatRoom' | 'calls' = 'public') { + await f.service.update(owner, 'room', 'start', { scope }); + return (await f.service.snapshot(owner, 'room'))!; +} + +describe('DrawingService', () => { + test('two members share strokes and late/reloaded clients receive the current canvas', async () => { + const f = fixture(); + const state = await start(f); + await f.service.update(member, 'room', 'join', { canvasId: state.canvasId }); + await f.service.update(owner, 'room', 'stroke', { canvasId: state.canvasId, stroke }); + await f.service.update(member, 'room', 'stroke', { canvasId: state.canvasId, stroke: { ...stroke, eraser: true } }); + expect(await f.service.snapshot(outsider, 'room')).toEqual(await f.service.snapshot(member, 'room')); + expect((await f.service.snapshot(member, 'room'))?.strokes).toEqual([stroke, { ...stroke, eraser: true }]); + expect(f.events.mock.calls.at(-1)?.[1]).toMatchObject({ version: 4, action: 'stroke' }); + }); + + test('only the owner can clear, end or remove participants; removed users lose read and write access', async () => { + const f = fixture(); + const { canvasId } = await start(f); + await f.service.update(member, 'room', 'join', { canvasId }); + for (const action of ['clear', 'end', 'kick'] as const) await expect(f.service.update(member, 'room', action, { canvasId, userId: owner.id })).rejects.toMatchObject({ code: 'accessDenied' }); + await f.service.update(owner, 'room', 'kick', { canvasId, userId: member.id }); + await expect(f.service.snapshot(member, 'room')).rejects.toMatchObject({ code: 'accessDenied' }); + await expect(f.service.update(member, 'room', 'stroke', { canvasId, stroke })).rejects.toMatchObject({ code: 'accessDenied' }); + await expect(f.service.update(member, 'room', 'join', { canvasId })).rejects.toMatchObject({ code: 'accessDenied' }); + }); + + test('clear invalidates queued strokes and end preserves the exportable canvas without reopening editing', async () => { + const f = fixture(); + const { canvasId } = await start(f); + await f.service.update(owner, 'room', 'stroke', { canvasId, stroke }); + await f.service.update(owner, 'room', 'clear', { canvasId }); + const cleared = (await f.service.snapshot(owner, 'room'))!; + expect(cleared.strokes).toEqual([]); + await expect(f.service.update(owner, 'room', 'stroke', { canvasId, stroke })).rejects.toMatchObject({ code: 'invalidState' }); + await f.service.update(owner, 'room', 'stroke', { canvasId: cleared.canvasId, stroke }); + await f.service.update(owner, 'room', 'end', { canvasId: cleared.canvasId }); + expect(await f.service.snapshot(owner, 'room')).toMatchObject({ ended: true, strokes: [stroke] }); + await expect(f.service.update(owner, 'room', 'stroke', { canvasId: cleared.canvasId, stroke })).rejects.toMatchObject({ code: 'invalidState' }); + await expect(f.service.update(owner, 'room', 'start', { scope: 'public' })).rejects.toMatchObject({ code: 'invalidState' }); + }); + + test('ChatRoom mode checks actual membership for reads and every write', async () => { + const f = fixture(); + f.room.attachmentType = 'chatRoom'; f.room.chatRoomId = 'chat'; + const { canvasId } = await start(f, 'chatRoom'); + await expect(f.service.snapshot(outsider, 'room')).rejects.toMatchObject({ code: 'accessDenied' }); + await f.service.update(member, 'room', 'join', { canvasId }); + f.memberships.delete(member.id); + await expect(f.service.update(member, 'room', 'stroke', { canvasId, stroke })).rejects.toMatchObject({ code: 'accessDenied' }); + }); + + test('Calls mode requires a live participant, including the host, and rejects departed or removed participants', async () => { + const f = fixture(); + await expect(start(f, 'calls')).rejects.toMatchObject({ code: 'accessDenied' }); + f.participants.set(owner.id, { id: 'host', state: 'active' }); f.live.add('host'); + const { canvasId } = await start(f, 'calls'); + await expect(f.service.snapshot(member, 'room')).rejects.toMatchObject({ code: 'accessDenied' }); + f.participants.set(member.id, { id: 'guest', state: 'active' }); f.live.add('guest'); + await f.service.update(member, 'room', 'join', { canvasId }); + f.live.delete('guest'); + await expect(f.service.update(member, 'room', 'stroke', { canvasId, stroke })).rejects.toMatchObject({ code: 'accessDenied' }); + f.live.add('guest'); f.participants.set(member.id, { id: 'guest', state: 'removed' }); + await expect(f.service.snapshot(member, 'room')).rejects.toMatchObject({ code: 'accessDenied' }); + }); + + test('limits participant slots and releases expired slots while retaining the canvas', async () => { + const f = fixture(); + const { canvasId } = await start(f); + for (let index = 0; index < 15; index++) await f.service.update({ id: `guest${index}` } as MiUser, 'room', 'join', { canvasId }); + await expect(f.service.update(member, 'room', 'join', { canvasId })).rejects.toMatchObject({ code: 'roomFull' }); + const key = 'drawing:room'; + const control = JSON.parse(f.redis.values.get(key)!); + control.lastSeen.guest0 = Date.now() - 100000; + f.redis.values.set(key, JSON.stringify(control)); + await f.service.update(member, 'room', 'join', { canvasId }); + expect((await f.service.snapshot(owner, 'room'))?.participantIds).toHaveLength(16); + }); +}); diff --git a/packages/frontend/src/components/MkCallsRoomWindow.vue b/packages/frontend/src/components/MkCallsRoomWindow.vue index 072d613c5fe..342e6a302b9 100644 --- a/packages/frontend/src/components/MkCallsRoomWindow.vue +++ b/packages/frontend/src/components/MkCallsRoomWindow.vue @@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ room?.title ?? i18n.ts._calls.title }}

+
@@ -125,6 +126,10 @@ function cleanupPopout(): void { emit('popout', null); } +function openDrawing(): void { + window.open(`/calls/${props.roomId}/drawing`, '_blank', 'noopener'); +} + function closeWindow(): void { const wasPoppedOut = popoutTarget.value != null; dialog.value?.close(); diff --git a/packages/frontend/src/pages/drawing/canvas.ts b/packages/frontend/src/pages/drawing/canvas.ts new file mode 100644 index 00000000000..f139bcb252a --- /dev/null +++ b/packages/frontend/src/pages/drawing/canvas.ts @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type { entities } from 'misskey-js'; +export type DrawingSnapshot = NonNullable; +export type DrawingStroke = DrawingSnapshot['strokes'][number]; + +export const DRAWING_PAPER_COLOR = '#ffffff'; + +export function paintStroke(context: CanvasRenderingContext2D, stroke: DrawingStroke) { + context.save(); + context.globalCompositeOperation = stroke.eraser ? 'destination-out' : 'source-over'; + context.strokeStyle = stroke.color; + context.fillStyle = stroke.color; + context.lineWidth = stroke.width; + context.lineCap = 'round'; + context.lineJoin = 'round'; + context.beginPath(); + context.moveTo(stroke.points[0][0], stroke.points[0][1]); + for (const point of stroke.points.slice(1)) context.lineTo(point[0], point[1]); + if (stroke.points.length === 1) { + context.arc(stroke.points[0][0], stroke.points[0][1], stroke.width / 2, 0, Math.PI * 2); + context.fill(); + } else { + context.stroke(); + } + context.restore(); +} + +export function canvasBlob(canvas: HTMLCanvasElement): Promise { + // The paper remains white in exported images regardless of the current UI theme. + const image = window.document.createElement('canvas'); + image.width = canvas.width; + image.height = canvas.height; + const context = image.getContext('2d')!; + context.fillStyle = DRAWING_PAPER_COLOR; + context.fillRect(0, 0, image.width, image.height); + context.drawImage(canvas, 0, 0); + return new Promise((resolve, reject) => image.toBlob(blob => blob == null ? reject(new Error('Canvas export failed')) : resolve(blob), 'image/png')); +} diff --git a/packages/frontend/src/pages/drawing/room.vue b/packages/frontend/src/pages/drawing/room.vue new file mode 100644 index 00000000000..9c137c2472d --- /dev/null +++ b/packages/frontend/src/pages/drawing/room.vue @@ -0,0 +1,320 @@ + + + + + + + diff --git a/packages/frontend/src/router.definition.ts b/packages/frontend/src/router.definition.ts index a0cf2e70ca9..07e3bc658e6 100644 --- a/packages/frontend/src/router.definition.ts +++ b/packages/frontend/src/router.definition.ts @@ -72,6 +72,10 @@ export const ROUTE_DEF = [{ path: '/calls', component: page(() => import('@/pages/calls.vue')), loginRequired: true, +}, { + path: '/calls/:roomId/drawing', + component: page(() => import('@/pages/drawing/room.vue')), + loginRequired: true, }, { path: '/calls/:roomId', component: page(() => import('@/pages/calls.vue')), diff --git a/packages/i18n/src/autogen/locale.ts b/packages/i18n/src/autogen/locale.ts index 34da1e39598..06785cf8322 100644 --- a/packages/i18n/src/autogen/locale.ts +++ b/packages/i18n/src/autogen/locale.ts @@ -14536,4 +14536,110 @@ export interface Locale extends ILocale { */ "unknownError": string; }; + "_drawing": { + /** + * 絵チャ + */ + "title": string; + /** + * キャンバスを開く + */ + "start": string; + /** + * 絵チャに参加 + */ + "join": string; + /** + * 絵チャから退出 + */ + "leave": string; + /** + * 参加できる人 + */ + "scope": string; + /** + * 公開ルームにアクセスできる人 + */ + "public": string; + /** + * ChatRoomのメンバー + */ + "chatRoom": string; + /** + * Callsに参加している人 + */ + "calls": string; + /** + * ペン + */ + "pen": string; + /** + * 消しゴム + */ + "eraser": string; + /** + * 色 + */ + "color": string; + /** + * 太さ + */ + "width": string; + /** + * 全消去 + */ + "clear": string; + /** + * キャンバスを全消去しますか? 全員の絵が消えます。 + */ + "clearConfirm": string; + /** + * 絵チャを終了 + */ + "end": string; + /** + * 絵チャを終了しますか? 終了後は編集できません。必要なら先に画像をドライブへ保存してください。 + */ + "endConfirm": string; + /** + * この絵チャは終了しました。画像の保存・添付は引き続きできます。 + */ + "ended": string; + /** + * この絵チャにはアクセスできません。参加範囲とCallsの参加状態を確認してください。 + */ + "unavailable": string; + /** + * ホストがキャンバスを開くと参加できます。 + */ + "waiting": string; + /** + * ドライブに保存 + */ + "save": string; + /** + * 画像を添付して投稿 + */ + "attach": string; + /** + * 絵チャから退出させる + */ + "remove": string; + /** + * この参加者を絵チャから退出させますか? 再参加できなくなります。 + */ + "removeConfirm": string; + /** + * キャンバスの容量に達しました。画像を保存してから、ホストが全消去してください。 + */ + "canvasFull": string; + /** + * 1280×720、同時参加16人まで。音声に接続せずに描くこともできます。 + */ + "description": string; + /** + * 再接続を待っています。 + */ + "disconnected": string; + }; } diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 2ce2340139c..c08c4afba56 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -1162,6 +1162,20 @@ export type Channels = { }; }; }; + drawing: { + params: { + roomId: string; + }; + events: { + updated: (payload: Omit, 'strokes' | 'messages'> & { + action: string; + stroke: (NonNullable)['strokes'][number] | null; + message: (NonNullable)['messages'][number] | null; + }) => void; + revoked: () => void; + }; + receives: Record; + }; chatRoom: { params: { roomId: string; @@ -1612,6 +1626,36 @@ type ClipsUpdateResponse = operations['clips___update']['responses']['200']['con // @public (undocumented) type DateString = string; +// @public (undocumented) +type DrawingClearRequest = operations['drawing___clear']['requestBody']['content']['application/json']; + +// @public (undocumented) +type DrawingEndRequest = operations['drawing___end']['requestBody']['content']['application/json']; + +// @public (undocumented) +type DrawingJoinRequest = operations['drawing___join']['requestBody']['content']['application/json']; + +// @public (undocumented) +type DrawingKickRequest = operations['drawing___kick']['requestBody']['content']['application/json']; + +// @public (undocumented) +type DrawingLeaveRequest = operations['drawing___leave']['requestBody']['content']['application/json']; + +// @public (undocumented) +type DrawingMessageRequest = operations['drawing___message']['requestBody']['content']['application/json']; + +// @public (undocumented) +type DrawingShowRequest = operations['drawing___show']['requestBody']['content']['application/json']; + +// @public (undocumented) +type DrawingShowResponse = operations['drawing___show']['responses']['200']['content']['application/json']; + +// @public (undocumented) +type DrawingStartRequest = operations['drawing___start']['requestBody']['content']['application/json']; + +// @public (undocumented) +type DrawingStrokeRequest = operations['drawing___stroke']['requestBody']['content']['application/json']; + // @public (undocumented) type DriveFile = components['schemas']['DriveFile']; @@ -2290,6 +2334,16 @@ declare namespace entities { ClipsUnfavoriteRequest, ClipsUpdateRequest, ClipsUpdateResponse, + DrawingClearRequest, + DrawingEndRequest, + DrawingJoinRequest, + DrawingKickRequest, + DrawingLeaveRequest, + DrawingMessageRequest, + DrawingShowRequest, + DrawingShowResponse, + DrawingStartRequest, + DrawingStrokeRequest, DriveResponse, DriveFilesRequest, DriveFilesResponse, @@ -4415,7 +4469,7 @@ type VerifyEmailRequest = operations['verify-email']['requestBody']['content'][' // src/streaming.ts:57:3 - (ae-forgotten-export) The symbol "ReconnectingWebSocket" needs to be exported by the entry point index.d.ts // src/streaming.types.ts:236:4 - (ae-forgotten-export) The symbol "ReversiUpdateKey" needs to be exported by the entry point index.d.ts // src/streaming.types.ts:251:4 - (ae-forgotten-export) The symbol "ReversiUpdateSettings" needs to be exported by the entry point index.d.ts -// src/streaming.types.ts:308:4 - (ae-forgotten-export) The symbol "CallsRoomEventBase" needs to be exported by the entry point index.d.ts +// src/streaming.types.ts:320:4 - (ae-forgotten-export) The symbol "CallsRoomEventBase" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index d1208d8fc78..1da833a71a8 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -2547,6 +2547,105 @@ declare module '../api.js' { credential?: string | null, ): Promise>; + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:calls* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:calls* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:calls* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:calls* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:calls* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:calls* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:calls* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:calls* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + + /** + * No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:calls* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + /** * No description provided. * diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index 594dbe5c6f2..caa8f5e74b7 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -355,6 +355,16 @@ import type { ClipsUnfavoriteRequest, ClipsUpdateRequest, ClipsUpdateResponse, + DrawingClearRequest, + DrawingEndRequest, + DrawingJoinRequest, + DrawingKickRequest, + DrawingLeaveRequest, + DrawingMessageRequest, + DrawingShowRequest, + DrawingShowResponse, + DrawingStartRequest, + DrawingStrokeRequest, DriveResponse, DriveFilesRequest, DriveFilesResponse, @@ -1016,6 +1026,15 @@ export type Endpoints = { 'clips/show': { req: ClipsShowRequest; res: ClipsShowResponse }; 'clips/unfavorite': { req: ClipsUnfavoriteRequest; res: EmptyResponse }; 'clips/update': { req: ClipsUpdateRequest; res: ClipsUpdateResponse }; + 'drawing/clear': { req: DrawingClearRequest; res: EmptyResponse }; + 'drawing/end': { req: DrawingEndRequest; res: EmptyResponse }; + 'drawing/join': { req: DrawingJoinRequest; res: EmptyResponse }; + 'drawing/kick': { req: DrawingKickRequest; res: EmptyResponse }; + 'drawing/leave': { req: DrawingLeaveRequest; res: EmptyResponse }; + 'drawing/message': { req: DrawingMessageRequest; res: EmptyResponse }; + 'drawing/show': { req: DrawingShowRequest; res: DrawingShowResponse }; + 'drawing/start': { req: DrawingStartRequest; res: EmptyResponse }; + 'drawing/stroke': { req: DrawingStrokeRequest; res: EmptyResponse }; 'drive': { req: EmptyRequest; res: DriveResponse }; 'drive/files': { req: DriveFilesRequest; res: DriveFilesResponse }; 'drive/files/attached-chat-messages': { req: DriveFilesAttachedChatMessagesRequest; res: DriveFilesAttachedChatMessagesResponse }; diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index 59d26854570..cc1048fe30f 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -358,6 +358,16 @@ export type ClipsShowResponse = operations['clips___show']['responses']['200'][' export type ClipsUnfavoriteRequest = operations['clips___unfavorite']['requestBody']['content']['application/json']; export type ClipsUpdateRequest = operations['clips___update']['requestBody']['content']['application/json']; export type ClipsUpdateResponse = operations['clips___update']['responses']['200']['content']['application/json']; +export type DrawingClearRequest = operations['drawing___clear']['requestBody']['content']['application/json']; +export type DrawingEndRequest = operations['drawing___end']['requestBody']['content']['application/json']; +export type DrawingJoinRequest = operations['drawing___join']['requestBody']['content']['application/json']; +export type DrawingKickRequest = operations['drawing___kick']['requestBody']['content']['application/json']; +export type DrawingLeaveRequest = operations['drawing___leave']['requestBody']['content']['application/json']; +export type DrawingMessageRequest = operations['drawing___message']['requestBody']['content']['application/json']; +export type DrawingShowRequest = operations['drawing___show']['requestBody']['content']['application/json']; +export type DrawingShowResponse = operations['drawing___show']['responses']['200']['content']['application/json']; +export type DrawingStartRequest = operations['drawing___start']['requestBody']['content']['application/json']; +export type DrawingStrokeRequest = operations['drawing___stroke']['requestBody']['content']['application/json']; export type DriveResponse = operations['drive']['responses']['200']['content']['application/json']; export type DriveFilesRequest = operations['drive___files']['requestBody']['content']['application/json']; export type DriveFilesResponse = operations['drive___files']['responses']['200']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index fc1391a4426..70757ce0249 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -2088,6 +2088,87 @@ export type paths = { */ post: operations['clips___update']; }; + '/drawing/clear': { + /** + * drawing/clear + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:calls* + */ + post: operations['drawing___clear']; + }; + '/drawing/end': { + /** + * drawing/end + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:calls* + */ + post: operations['drawing___end']; + }; + '/drawing/join': { + /** + * drawing/join + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:calls* + */ + post: operations['drawing___join']; + }; + '/drawing/kick': { + /** + * drawing/kick + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:calls* + */ + post: operations['drawing___kick']; + }; + '/drawing/leave': { + /** + * drawing/leave + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:calls* + */ + post: operations['drawing___leave']; + }; + '/drawing/message': { + /** + * drawing/message + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:calls* + */ + post: operations['drawing___message']; + }; + '/drawing/show': { + /** + * drawing/show + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *read:calls* + */ + post: operations['drawing___show']; + }; + '/drawing/start': { + /** + * drawing/start + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:calls* + */ + post: operations['drawing___start']; + }; + '/drawing/stroke': { + /** + * drawing/stroke + * @description No description provided. + * + * **Credential required**: *Yes* / **Permission**: *write:calls* + */ + post: operations['drawing___stroke']; + }; '/drive': { /** * drive @@ -23257,6 +23338,695 @@ export interface operations { }; }; }; + drawing___clear: { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + roomId: string; + canvasId: string; + }; + }; + }; + responses: { + /** @description OK (without any results) */ + 204: { + headers: { + [name: string]: unknown; + }; + }; + /** @description Client error */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Too many requests */ + 429: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + drawing___end: { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + roomId: string; + canvasId: string; + }; + }; + }; + responses: { + /** @description OK (without any results) */ + 204: { + headers: { + [name: string]: unknown; + }; + }; + /** @description Client error */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Too many requests */ + 429: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + drawing___join: { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + roomId: string; + canvasId: string; + }; + }; + }; + responses: { + /** @description OK (without any results) */ + 204: { + headers: { + [name: string]: unknown; + }; + }; + /** @description Client error */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Too many requests */ + 429: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + drawing___kick: { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + roomId: string; + canvasId: string; + /** Format: misskey:id */ + userId: string; + }; + }; + }; + responses: { + /** @description OK (without any results) */ + 204: { + headers: { + [name: string]: unknown; + }; + }; + /** @description Client error */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Too many requests */ + 429: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + drawing___leave: { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + roomId: string; + canvasId: string; + }; + }; + }; + responses: { + /** @description OK (without any results) */ + 204: { + headers: { + [name: string]: unknown; + }; + }; + /** @description Client error */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Too many requests */ + 429: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + drawing___message: { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + roomId: string; + canvasId: string; + text: string; + }; + }; + }; + responses: { + /** @description OK (without any results) */ + 204: { + headers: { + [name: string]: unknown; + }; + }; + /** @description Client error */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Too many requests */ + 429: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + drawing___show: { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + roomId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + canvas: { + canvasId: string; + version: number; + /** @enum {string} */ + scope: 'public' | 'chatRoom' | 'calls'; + ended: boolean; + participantIds: string[]; + strokes: { + color: string; + width: number; + eraser: boolean; + points: number[][]; + }[]; + messages: { + id: string; + userId: string; + text: string; + }[]; + } | null; + }; + }; + }; + /** @description Client error */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Too many requests */ + 429: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + drawing___start: { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + roomId: string; + /** @enum {string} */ + scope: 'public' | 'chatRoom' | 'calls'; + }; + }; + }; + responses: { + /** @description OK (without any results) */ + 204: { + headers: { + [name: string]: unknown; + }; + }; + /** @description Client error */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Too many requests */ + 429: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; + drawing___stroke: { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + roomId: string; + canvasId: string; + stroke: { + color: string; + width: number; + eraser: boolean; + points: number[][]; + }; + }; + }; + }; + responses: { + /** @description OK (without any results) */ + 204: { + headers: { + [name: string]: unknown; + }; + }; + /** @description Client error */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Too many requests */ + 429: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; drive: { responses: { /** @description OK (with results) */ diff --git a/packages/misskey-js/src/streaming.types.ts b/packages/misskey-js/src/streaming.types.ts index 404ba2e7a54..cfb83c27bba 100644 --- a/packages/misskey-js/src/streaming.types.ts +++ b/packages/misskey-js/src/streaming.types.ts @@ -276,6 +276,18 @@ export type Channels = { }; }; }; + drawing: { + params: { roomId: string }; + events: { + updated: (payload: Omit, 'strokes' | 'messages'> & { + action: string; + stroke: (NonNullable)['strokes'][number] | null; + message: (NonNullable)['messages'][number] | null; + }) => void; + revoked: () => void; + }; + receives: Record; + }; chatRoom: { params: { roomId: string; diff --git a/specs/drawing-chat/screenshots/desktop.png b/specs/drawing-chat/screenshots/desktop.png new file mode 100644 index 0000000000000000000000000000000000000000..88c210177a00f928180111165c5f16a89e0a0b11 GIT binary patch literal 114115 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� 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 z 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 literal 0 HcmV?d00001 diff --git a/specs/drawing-chat/screenshots/mobile.png b/specs/drawing-chat/screenshots/mobile.png new file mode 100644 index 0000000000000000000000000000000000000000..62369bf7f262664392a95c7227625377ad5afeb5 GIT binary patch literal 53107 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

9s)mTHAM-iH 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 TPx}ab3j$C~P82Hq?brVT-io^_ literal 0 HcmV?d00001 diff --git a/specs/drawing-chat/spec.md b/specs/drawing-chat/spec.md new file mode 100644 index 00000000000..6772650b228 --- /dev/null +++ b/specs/drawing-chat/spec.md @@ -0,0 +1,17 @@ +# T07 絵チャ + +[みゅーさん](https://freeski.msnis.net/notes/arx2uv0drtfz01nc)、[白うささん](https://freeski.msnis.net/notes/arx2y5z7rtfz020i)、[ちょあよさん](https://freeski.msnis.net/notes/arx31ebirtfz027f)の提案をまとめた共同描画機能。2026年10月4日の回答に従い、Callsを土台に、公開ルーム・ChatRoomメンバー・Calls参加者の3つの参加範囲を扱う。 + +Callsルームのブラシボタンから絵チャを開く。ホストが最初に参加範囲を選ぶ。公開は公開の個人Calls、ChatRoomはChatRoomに紐づくCallsで選択できる。Calls参加者限定ではホストを含め、有効なCalls接続が必要。Calls自体の閲覧権限とChatの利用ポリシーも維持する。公開・ChatRoomでは音声接続は不要で、予約状態のCallsを開いて絵チャを開始できる。 + +キャンバスは1280×720、同時参加は16人。ペン、色、太さ、消しゴム、参加者一覧、短い文字チャットを提供する。ChatRoomでは既存のチャット履歴・投稿フォームを使い、それ以外では絵チャ内の直近50メッセージを保持する。描画イベントは文字チャット履歴へ保存しない。 + +現在の絵はRedisに保持する。途中参加、再読込、streaming再接続時はサーバーのキャンバスを取得する。Callsの既存ルームロックで、描画・消去・参加・終了と取得を直列化する。参加枠は30秒の更新と90秒の失効で管理し、画面を閉じた参加者が枠を占有し続けないようにする。キャンバス自体には参加者不在による有効期限を設けない。Redisの永続化・退避の設定は既存の運用に従う。 + +ホストは参加者を絵チャから退出させ、確認後に全消去または終了できる。退出させられた人は再参加・取得・描画できない。Callsから退出またはChatRoomから除外された人にも権限を再確認する。全消去でキャンバス識別子を更新し、送信待ちの古い線を拒否する。終了後は編集を再開できず、最後の絵を保存・投稿に添付できる。終了の確認に保存案内を表示する。 + +描画は最大64点の線分を約250ミリ秒ごとに同期し、現在の絵を構成する線分は10,000件まで保持する。上限時は画像保存とホストによる全消去を案内する。PNGは白い紙面で書き出し、既存のドライブアップロードと投稿フォームへの添付を使う。 + +レイヤー、無制限の履歴、終了後の編集再開、作品一覧、動画録画、筆圧対応は含めない。Cloudflareの音声・映像処理は変更しない。 + +完了条件は、2人での同期と途中参加・再読込の一致、参加範囲・退出に応じた取得と描画の制限、画像保存・添付、確認付き全消去の全員への反映。 diff --git a/specs/drawing-chat/verification.md b/specs/drawing-chat/verification.md new file mode 100644 index 00000000000..c6619ab5784 --- /dev/null +++ b/specs/drawing-chat/verification.md @@ -0,0 +1,25 @@ +# T07 動作確認 + +2026-10-04、Calls の `d1f0e95cfb0ba67cf44e2e047fb83a5d5d9b815b` を基点とした専用 worktree で検証。専用 PostgreSQL 18 / Redis 7、ローカル Misskey、Chromium の独立した2つのログイン状態を使用した。 + +| 検証 | 結果 | +| --- | --- | +| DrawingService unit(同期、権限、消去・終了、参加枠、ChatRoom、Calls 接続) | PASS: 6件 | +| `pnpm --filter backend test:e2e --run test/e2e/calls.ts` | PASS: 8件。Calls 有効の試験設定で公開キャンバスの lifecycle も実行 | +| backend / frontend typecheck | PASS | +| `pnpm build-misskey-js-with-types` | PASS | +| frontend build | PASS | +| 変更ファイルの `eslint --quiet` / locale safety | PASS | +| SPDX 全体検査 | BASELINE: 基点にある migration 2件のヘッダー欠落。今回の追加ファイルは欠落なし | + +ブラウザーでは次を確認した。2人がマウスで描画した後のキャンバス画像が一致し、再読込でも一致する。消しゴムと文字チャットが相手へ反映される。全消去をキャンセルすると線が残り、確定すると両画面で白紙になる。PNG をドライブに保存すると 1280×720 の画像が作られ、投稿フォームには画像が添付される。投稿は実送信しない。390px 幅でもキャンバスが画面内に収まる。ホストによる退出で相手の画面と API のアクセスが拒否され、終了後は描画が無効になり保存は有効のままになる。ページの未処理 JavaScript エラーは0件。 + +API では ChatRoom の招待・参加から絵チャを開き、外部ユーザーの拒否、既存 ChatRoom 文字チャット、退室後の取得・描画拒否を確認した。Calls 限定では試験用の有効な participant / Redis 接続状態を用い、接続ありのホストだけが開始・取得でき、未接続メンバーと接続失効後のホストは拒否されることを確認した。 + +Cloudflare の実音声接続は試験用認証情報のため SKIPPED。既存の音声・映像処理は変更していない。Redis の再起動後の保持は既存の永続化設定に依存する。 + +SPDX の既存違反は `packages/backend/migration/1774789240317-event.js` と `packages/backend/migration/1778352600000-hashtagFollowing.js`。マージ済 migration は変更していない。 + +![デスクトップ](screenshots/desktop.png) + +![スマホ幅](screenshots/mobile.png) From 6ff770c78a0c012e912d9047a01fdf4e6b0e5f74 Mon Sep 17 00:00:00 2001 From: mattyatea Date: Sun, 4 Oct 2026 08:53:37 +0000 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20=E7=B5=B5=E3=83=81=E3=83=A3=E3=81=AE?= =?UTF-8?q?=E9=80=9A=E4=BF=A1=E5=BE=A9=E6=97=A7=E3=81=A8=E9=96=B2=E8=A6=A7?= =?UTF-8?q?=E7=8A=B6=E6=85=8B=E3=82=92=E7=B6=AD=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/ja-JP.yml | 1 + packages/frontend/src/pages/drawing/room.vue | 20 +++++++++++++++++--- packages/i18n/src/autogen/locale.ts | 4 ++++ specs/drawing-chat/verification.md | 2 ++ 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index eb56d21e5ff..043b03304ea 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -3901,6 +3901,7 @@ _drawing: attach: "画像を添付して投稿" remove: "絵チャから退出させる" removeConfirm: "この参加者を絵チャから退出させますか? 再参加できなくなります。" + cannotJoin: "絵チャに参加できません。利用権限・参加範囲・空き人数を確認してください。画像の閲覧と保存は引き続きできます。" canvasFull: "キャンバスの容量に達しました。画像を保存してから、ホストが全消去してください。" description: "1280×720、同時参加16人まで。音声に接続せずに描くこともできます。" disconnected: "再接続を待っています。" diff --git a/packages/frontend/src/pages/drawing/room.vue b/packages/frontend/src/pages/drawing/room.vue index 9c137c2472d..295f4ec1dbc 100644 --- a/packages/frontend/src/pages/drawing/room.vue +++ b/packages/frontend/src/pages/drawing/room.vue @@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ i18n.ts._drawing.description }}
{{ error }} {{ i18n.ts._drawing.disconnected }} + {{ participationError }}
@@ -107,6 +107,7 @@ const connected = ref(stream.state === 'connected'); const isHost = computed(() => room.value?.attachment.ownerUserId === $i.id); const joined = computed(() => state.value?.participantIds.includes($i.id) === true); const canDraw = computed(() => joined.value && state.value?.ended === false && connected.value && !error.value); +const canSend = computed(() => canDraw.value && !busy.value && text.value.trim().length > 0 && [...text.value].length <= 500); let channel: Misskey.IChannelConnection | null = null; let chatChannel: Misskey.IChannelConnection | null = null; let poll: number | undefined; @@ -135,9 +136,14 @@ function showError(cause: unknown) { console.error(cause); } else if (failure.code === 'DRAWING_CANVAS_FULL') { void os.alert({ type: 'warning', text: i18n.ts._drawing.canvasFull }); - } else { + } else if (failure.code === 'DRAWING_INVALID_STATE') { + void refresh(); + } else if (['DRAWING_ACCESS_DENIED', 'CALLS_ACCESS_DENIED', 'CALLS_ROOM_NOT_FOUND', 'CALLS_ATTACHMENT_NOT_FOUND', 'CALLS_FEATURE_DISABLED', 'FORBIDDEN'].includes(failure.code)) { error.value = i18n.ts._drawing.unavailable; stopPointer(); + } else { + stopPointer(); + void os.alert({ type: 'warning', text: i18n.ts.somethingHappened }); } } @@ -154,10 +160,11 @@ async function refresh() { try { const { canvas: snapshot } = await misskeyApi('drawing/show', { roomId: props.roomId }); if (!active) return; + const recovered = error.value !== '' || !connected.value; + error.value = ''; + connected.value = stream.state === 'connected'; if (snapshot && (!state.value || snapshot.version >= state.value.version)) { - const changed = state.value?.version !== snapshot.version || error.value !== '' || !connected.value; - error.value = ''; - connected.value = stream.state === 'connected'; + const changed = state.value?.version !== snapshot.version || recovered; state.value = snapshot; await nextTick(); if (changed) drawSnapshot(); @@ -167,16 +174,18 @@ async function refresh() { } async function update(action: 'join' | 'leave' | 'clear' | 'end' | 'kick' | 'message', params: { userId?: string; text?: string } = {}) { - if (!state.value) return; + if (!state.value) return false; busy.value = true; if (action === 'join') participationError.value = ''; try { await misskeyApi(`drawing/${action}`, { roomId: props.roomId, canvasId: state.value.canvasId, ...params } as never); await refresh(); + return true; } catch (cause) { if (action === 'join' && (cause as { code?: string })?.code) { participationError.value = i18n.ts._drawing.cannotJoin; } else { showError(cause); } + return false; } finally { busy.value = false; } } @@ -201,7 +210,7 @@ async function end() { if (!(await os.confirm({ type: 'warning', text: i18n.ts._ async function kick(userId: string) { if (!(await os.confirm({ type: 'warning', text: i18n.ts._drawing.removeConfirm })).canceled) await update('kick', { userId }); } -async function sendMessage() { await update('message', { text: text.value }); if (!error.value) text.value = ''; } +async function sendMessage() { if (canSend.value && await update('message', { text: text.value })) text.value = ''; } function point(event: PointerEvent) { const rect = canvas.value!.getBoundingClientRect(); @@ -294,6 +303,13 @@ async function activate() { chatChannel = stream.useChannel('chatRoom', { roomId: chat.id }); chatChannel.on('message', message => { chatMessages.value.push(message as Misskey.entities.ChatMessage); }); chatChannel.on('deleted', id => { chatMessages.value = chatMessages.value.filter(message => message.id !== id); }); + chatChannel.on('react', event => { + chatMessages.value.find(message => message.id === event.messageId)?.reactions.push({ reaction: event.reaction, user: event.user! }); + }); + chatChannel.on('unreact', event => { + const message = chatMessages.value.find(message => message.id === event.messageId); + if (message) message.reactions = message.reactions.filter(reaction => reaction.reaction !== event.reaction || reaction.user.id !== event.user!.id); + }); } await refresh(); await nextTick(); diff --git a/packages/i18n/src/autogen/locale.ts b/packages/i18n/src/autogen/locale.ts index 0f52a805afa..73e9db26957 100644 --- a/packages/i18n/src/autogen/locale.ts +++ b/packages/i18n/src/autogen/locale.ts @@ -14629,6 +14629,10 @@ export interface Locale extends ILocale { * この参加者を絵チャから退出させますか? 再参加できなくなります。 */ "removeConfirm": string; + /** + * 500文字まで + */ + "messageLimit": string; /** * 絵チャに参加できません。利用権限・参加範囲・空き人数を確認してください。画像の閲覧と保存は引き続きできます。 */ diff --git a/specs/drawing-chat/verification.md b/specs/drawing-chat/verification.md index 77badecc383..1999d70b670 100644 --- a/specs/drawing-chat/verification.md +++ b/specs/drawing-chat/verification.md @@ -16,6 +16,8 @@ PR 前レビューで見つかった表示の復帰と参加拒否の扱いを修正後、追加のブラウザー確認も PASS。描画 API の送信を一時的に abort し、キャンバスを保持したまま再読込なしで復帰することを確認した。自動参加の応答を `DRAWING_ACCESS_DENIED`(読み取り専用相当)・`DRAWING_ROOM_FULL` に置き換えた場合も、取得済みの絵と保存ボタンは維持し、描画だけを無効にすることを確認した。 +最終修正の追加確認も PASS。未開始の `canvas: null` 応答で通信状態と開始ボタンが復帰する。古い heartbeat の `DRAWING_INVALID_STATE` 応答は再取得で復帰し、絵と保存ボタンを維持する。501文字の入力では送信を止め、入力 API の拒否応答でも文章とキャンバスを保持する。ChatRoom のリアクション追加・取消は再読込なしで表示へ反映する。短い線の終了時にも送信するため描画上限を毎分600件へ調整し、専用サーバーを `NODE_ENV=production` で起動して毎分300件を超える340件の要求が通ることを確認した。 + API では ChatRoom の招待・参加から絵チャを開き、外部ユーザーの拒否、既存 ChatRoom 文字チャット、退室後の取得・描画拒否を確認した。Calls 限定では試験用の有効な participant / Redis 接続状態を用い、接続ありのホストだけが開始・取得でき、未接続メンバーと接続失効後のホストは拒否されることを確認した。 Cloudflare の実音声接続は試験用認証情報のため SKIPPED。既存の音声・映像処理は変更していない。Redis の再起動後の保持は既存の永続化設定に依存する。 From 842a13caf1875e638e78853ab43211986d136006 Mon Sep 17 00:00:00 2001 From: mattyatea Date: Sun, 4 Oct 2026 09:19:13 +0000 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20=E7=B5=B5=E3=83=81=E3=83=A3=E3=81=AE?= =?UTF-8?q?=E9=96=B2=E8=A6=A7=E6=A8=A9=E9=99=90=E3=81=A8=E7=94=BB=E9=9D=A2?= =?UTF-8?q?=E3=83=A9=E3=82=A4=E3=83=95=E3=82=B5=E3=82=A4=E3=82=AF=E3=83=AB?= =?UTF-8?q?=E3=82=92=E7=B6=AD=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/frontend/src/pages/drawing/room.vue | 11 +++++++++-- specs/drawing-chat/verification.md | 2 ++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/frontend/src/pages/drawing/room.vue b/packages/frontend/src/pages/drawing/room.vue index cef141f1073..15e4b5a2c0b 100644 --- a/packages/frontend/src/pages/drawing/room.vue +++ b/packages/frontend/src/pages/drawing/room.vue @@ -106,7 +106,7 @@ const pending = ref(0); const connected = ref(stream.state === 'connected'); const isHost = computed(() => room.value?.attachment.ownerUserId === $i.id); const joined = computed(() => state.value?.participantIds.includes($i.id) === true); -const canDraw = computed(() => joined.value && state.value?.ended === false && connected.value && !error.value); +const canDraw = computed(() => joined.value && state.value?.ended === false && connected.value && !error.value && !participationError.value && $i.policies.chatAvailability === 'available'); const canSend = computed(() => canDraw.value && !busy.value && text.value.trim().length > 0 && [...text.value].length <= 500); let channel: Misskey.IChannelConnection | null = null; let chatChannel: Misskey.IChannelConnection | null = null; @@ -295,13 +295,18 @@ async function activate() { stream.on('_disconnected_', onDisconnected); try { room.value = (await misskeyApi('calls/rooms/show', { roomId: props.roomId })).room; + if (!active) return; scope.value = room.value.attachment.type === 'chatRoom' ? 'chatRoom' : room.value.visibility === 'public' ? 'public' : 'calls'; if (room.value.attachment.type === 'chatRoom') { const chat = await misskeyApi('chat/rooms/show', { roomId: room.value.attachment.chatRoomId }); chatRoom.value = chat; chatMessages.value = (await misskeyApi('chat/messages/room-timeline', { roomId: chat.id, limit: 20 })).reverse(); + if (!active) return; chatChannel = stream.useChannel('chatRoom', { roomId: chat.id }); - chatChannel.on('message', message => { chatMessages.value.push(message as Misskey.entities.ChatMessage); }); + chatChannel.on('message', message => { + chatMessages.value.push(message as Misskey.entities.ChatMessage); + if (message.fromUserId !== $i.id && active && !window.document.hidden) chatChannel?.send('read', { id: message.id }); + }); chatChannel.on('deleted', id => { chatMessages.value = chatMessages.value.filter(message => message.id !== id); }); chatChannel.on('react', event => { chatMessages.value.find(message => message.id === event.messageId)?.reactions.push({ reaction: event.reaction, user: event.user! }); @@ -313,9 +318,11 @@ async function activate() { } await refresh(); await nextTick(); + if (!active) return; drawSnapshot(); if (state.value && !state.value.ended) await join(); } catch (cause) { showError(cause); } + if (!active) return; poll = window.setInterval(() => { if (!error.value) void refresh(); }, 15000); heartbeat = window.setInterval(() => { if (canDraw.value) void misskeyApi('drawing/join', { roomId: props.roomId, canvasId: state.value!.canvasId }).catch(showError); }, 30000); } diff --git a/specs/drawing-chat/verification.md b/specs/drawing-chat/verification.md index 1999d70b670..acdcc426ab1 100644 --- a/specs/drawing-chat/verification.md +++ b/specs/drawing-chat/verification.md @@ -18,6 +18,8 @@ PR 前レビューで見つかった表示の復帰と参加拒否の扱いを 最終修正の追加確認も PASS。未開始の `canvas: null` 応答で通信状態と開始ボタンが復帰する。古い heartbeat の `DRAWING_INVALID_STATE` 応答は再取得で復帰し、絵と保存ボタンを維持する。501文字の入力では送信を止め、入力 API の拒否応答でも文章とキャンバスを保持する。ChatRoom のリアクション追加・取消は再読込なしで表示へ反映する。短い線の終了時にも送信するため描画上限を毎分600件へ調整し、専用サーバーを `NODE_ENV=production` で起動して毎分300件を超える340件の要求が通ることを確認した。 +追加の修正確認も PASS。参加枠に本人が残る snapshot と参加拒否を組み合わせても、描画・heartbeat は無効で、31秒後も閲覧・保存を維持する。表示中に受信した ChatRoom メッセージの既読通知を streaming で送ることを確認した。Calls 情報の応答を遅延させた状態で別ページへ移動しても、応答後に絵チャへ戻らない。型チェック・frontend build・変更ファイル lint は修正後にも実行した。 + API では ChatRoom の招待・参加から絵チャを開き、外部ユーザーの拒否、既存 ChatRoom 文字チャット、退室後の取得・描画拒否を確認した。Calls 限定では試験用の有効な participant / Redis 接続状態を用い、接続ありのホストだけが開始・取得でき、未接続メンバーと接続失効後のホストは拒否されることを確認した。 Cloudflare の実音声接続は試験用認証情報のため SKIPPED。既存の音声・映像処理は変更していない。Redis の再起動後の保持は既存の永続化設定に依存する。 From eee7ed4786f6b189e22c47b0f15d0bb5931f8b31 Mon Sep 17 00:00:00 2001 From: mattyatea Date: Tue, 6 Oct 2026 14:01:44 +0000 Subject: [PATCH 5/5] =?UTF-8?q?feat(calls):=20=E3=82=A2=E3=82=AF=E3=83=86?= =?UTF-8?q?=E3=82=A3=E3=83=93=E3=83=86=E3=82=A3=E3=81=AE=E5=9C=9F=E5=8F=B0?= =?UTF-8?q?=E3=82=92=E7=B5=B5=E3=83=81=E3=83=A3=E3=81=8B=E3=82=89=E5=88=86?= =?UTF-8?q?=E9=9B=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/ja-JP.yml | 3 ++ packages/frontend/.storybook/generate.tsx | 1 + .../src/components/MkCallsRoomWindow.vue | 31 +++++++---- .../calls/MkCallsActivities.stories.impl.ts | 15 ++++++ .../components/calls/MkCallsActivities.vue | 54 +++++++++++++++++++ .../test/unit/calls-activities.test.ts | 31 +++++++++++ .../test/unit/calls-join-confirmation.test.ts | 30 ++++++++++- packages/i18n/src/autogen/locale.ts | 12 +++++ 8 files changed, 167 insertions(+), 10 deletions(-) create mode 100644 packages/frontend/src/components/calls/MkCallsActivities.stories.impl.ts create mode 100644 packages/frontend/src/components/calls/MkCallsActivities.vue create mode 100644 packages/frontend/test/unit/calls-activities.test.ts diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index a25c05ec48c..86b9fde6ad7 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -2721,6 +2721,9 @@ _permissions: "write:calls": "Misskey Callsへの参加と音声通話を操作する" _calls: + activities: "アクティビティ" + backToActivities: "アクティビティ一覧に戻る" + noActivities: "利用できるアクティビティはまだありません" participationNotAllowed: "このアカウントではコールへの参加が許可されていません。" cameraSettings: "カメラの設定" screenSettings: "画面共有の設定" diff --git a/packages/frontend/.storybook/generate.tsx b/packages/frontend/.storybook/generate.tsx index 4274839e016..00352656bb4 100644 --- a/packages/frontend/.storybook/generate.tsx +++ b/packages/frontend/.storybook/generate.tsx @@ -464,6 +464,7 @@ function toStories(component: string): Promise { globSync('src/components/MkTagItem.vue'), globSync('src/components/MkRoleSelectDialog.vue'), globSync('src/components/grid/MkGrid.vue'), + globSync('src/components/calls/MkCallsActivities.vue'), globSync('src/pages/admin/custom-emojis-manager2.vue'), globSync('src/pages/admin/overview.ap-requests.vue'), globSync('src/pages/user/home.vue'), diff --git a/packages/frontend/src/components/MkCallsRoomWindow.vue b/packages/frontend/src/components/MkCallsRoomWindow.vue index b4fe50bdda9..5d09fc92ee2 100644 --- a/packages/frontend/src/components/MkCallsRoomWindow.vue +++ b/packages/frontend/src/components/MkCallsRoomWindow.vue @@ -13,6 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ room?.title ?? i18n.ts._calls.title }}

{{ session.elapsedTime.value }}
+ @@ -20,22 +21,20 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.notFound }}