diff --git a/.env-example b/.env-example index 13701734..910fb9b3 100644 --- a/.env-example +++ b/.env-example @@ -6,3 +6,7 @@ DB_PORT= DB_USER= DB_PASSWORD= DB_DATABASE= + +DATABASE_URL_BASE=mysql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT} +DATABASE_URL=${DATABASE_URL_BASE}/${DB_DATABASE} +#SHADOW_DATABASE_URL=${DATABASE_URL_BASE}/pow-shadow diff --git a/.github/workflows/maintain-quality.yml b/.github/workflows/maintain-quality.yml index 44b7ce07..1010177f 100644 --- a/.github/workflows/maintain-quality.yml +++ b/.github/workflows/maintain-quality.yml @@ -35,6 +35,9 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile --strict-peer-dependencies + - name: Generate Prisma Client + run: pnpm run db:generate + - name: Check all run: pnpm run --aggregate-output "/^(format:check|lint|check:types)$/" env: diff --git a/.vscode/settings.json b/.vscode/settings.json index 21cd3541..6e6f5077 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,8 @@ { "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, - "eslint.experimental.useFlatConfig": true + "eslint.experimental.useFlatConfig": true, + "[prisma]": { + "editor.defaultFormatter": "Prisma.prisma" + } } diff --git a/package.json b/package.json index 9c460695..0e0e1da9 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,10 @@ "scripts": { "start": "node .", "dev": "run-p build:debug:watch dev:run", + "db:generate": "prisma generate", + "db:dev": "prisma migrate dev", + "db:deploy": "prisma migrate deploy", + "db:push": "prisma db push", "dev:run": "node --watch -r dotenv/config . dotenv_config_override=true", "build": "swc src --out-dir dist", "build:debug": "swc --source-maps=true src --out-dir=dist", @@ -27,6 +31,8 @@ "emoji-regex": "10.3.0", "ffmpeg-static": "5.2.0", "mariadb": "3.3.1", + "prisma": "5.17.0", + "@prisma/client": "5.17.0", "sodium-native": "4.1.1" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b08cc1c9..d1e96f29 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@discordjs/voice': specifier: 0.17.0 version: 0.17.0(ffmpeg-static@5.2.0) + '@prisma/client': + specifier: 5.17.0 + version: 5.17.0(prisma@5.17.0) '@sapphire/framework': specifier: 5.2.1 version: 5.2.1 @@ -35,6 +38,9 @@ importers: mariadb: specifier: 3.3.1 version: 3.3.1 + prisma: + specifier: 5.17.0 + version: 5.17.0 sodium-native: specifier: 4.1.1 version: 4.1.1 @@ -179,6 +185,30 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@prisma/client@5.17.0': + resolution: {integrity: sha512-N2tnyKayT0Zf7mHjwEyE8iG7FwTmXDHFZ1GnNhQp0pJUObsuel4ZZ1XwfuAYkq5mRIiC/Kot0kt0tGCfLJ70Jw==} + engines: {node: '>=16.13'} + peerDependencies: + prisma: '*' + peerDependenciesMeta: + prisma: + optional: true + + '@prisma/debug@5.17.0': + resolution: {integrity: sha512-l7+AteR3P8FXiYyo496zkuoiJ5r9jLQEdUuxIxNCN1ud8rdbH3GTxm+f+dCyaSv9l9WY+29L9czaVRXz9mULfg==} + + '@prisma/engines-version@5.17.0-31.393aa359c9ad4a4bb28630fb5613f9c281cde053': + resolution: {integrity: sha512-tUuxZZysZDcrk5oaNOdrBnnkoTtmNQPkzINFDjz7eG6vcs9AVDmA/F6K5Plsb2aQc/l5M2EnFqn3htng9FA4hg==} + + '@prisma/engines@5.17.0': + resolution: {integrity: sha512-+r+Nf+JP210Jur+/X8SIPLtz+uW9YA4QO5IXA+KcSOBe/shT47bCcRMTYCbOESw3FFYFTwe7vU6KTWHKPiwvtg==} + + '@prisma/fetch-engine@5.17.0': + resolution: {integrity: sha512-ESxiOaHuC488ilLPnrv/tM2KrPhQB5TRris/IeIV4ZvUuKeaicCl4Xj/JCQeG9IlxqOgf1cCg5h5vAzlewN91Q==} + + '@prisma/get-platform@5.17.0': + resolution: {integrity: sha512-UlDgbRozCP1rfJ5Tlkf3Cnftb6srGrEQ4Nm3og+1Se2gWmCZ0hmPIi+tQikGDUVLlvOWx3Gyi9LzgRP+HTXV9w==} + '@sapphire/async-queue@1.5.2': resolution: {integrity: sha512-7X7FFAA4DngXUl95+hYbUF19bp1LGiffjJtu7ygrZrbdCSsdDDBaSjB7Akw0ZbOu6k0xpXyljnJ6/RZUvLfRdg==} engines: {node: '>=v14.0.0', npm: '>=7.0.0'} @@ -1136,6 +1166,11 @@ packages: opusscript: optional: true + prisma@5.17.0: + resolution: {integrity: sha512-m4UWkN5lBE6yevqeOxEvmepnL5cNPEjzMw2IqDB59AcEV6w7D8vGljDLd1gPFH+W6gUxw9x7/RmN5dCS/WTPxA==} + engines: {node: '>=16.13'} + hasBin: true + progress@2.0.3: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} @@ -1519,6 +1554,31 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 + '@prisma/client@5.17.0(prisma@5.17.0)': + optionalDependencies: + prisma: 5.17.0 + + '@prisma/debug@5.17.0': {} + + '@prisma/engines-version@5.17.0-31.393aa359c9ad4a4bb28630fb5613f9c281cde053': {} + + '@prisma/engines@5.17.0': + dependencies: + '@prisma/debug': 5.17.0 + '@prisma/engines-version': 5.17.0-31.393aa359c9ad4a4bb28630fb5613f9c281cde053 + '@prisma/fetch-engine': 5.17.0 + '@prisma/get-platform': 5.17.0 + + '@prisma/fetch-engine@5.17.0': + dependencies: + '@prisma/debug': 5.17.0 + '@prisma/engines-version': 5.17.0-31.393aa359c9ad4a4bb28630fb5613f9c281cde053 + '@prisma/get-platform': 5.17.0 + + '@prisma/get-platform@5.17.0': + dependencies: + '@prisma/debug': 5.17.0 + '@sapphire/async-queue@1.5.2': {} '@sapphire/discord-utilities@3.3.0': @@ -2482,6 +2542,10 @@ snapshots: optionalDependencies: ffmpeg-static: 5.2.0 + prisma@5.17.0: + dependencies: + '@prisma/engines': 5.17.0 + progress@2.0.3: {} pseudomap@1.0.2: {} diff --git a/prisma/migrations/0_init/migration.sql b/prisma/migrations/0_init/migration.sql new file mode 100644 index 00000000..14ea1cf1 --- /dev/null +++ b/prisma/migrations/0_init/migration.sql @@ -0,0 +1,21 @@ +-- CreateTable +CREATE TABLE `connectionStates` ( + `voiceChannel` BIGINT UNSIGNED NOT NULL, + `guild` BIGINT UNSIGNED NOT NULL, + `readChannel` BIGINT UNSIGNED NOT NULL, + `skipUser` TEXT NULL, + + PRIMARY KEY (`voiceChannel` ASC) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `userSetting` ( + `id` BIGINT NOT NULL DEFAULT 0, + `speaker` VARCHAR(20) NOT NULL DEFAULT '', + `pitch` INTEGER UNSIGNED NOT NULL, + `speed` INTEGER UNSIGNED NOT NULL, + `isDontRead` TINYINT NOT NULL DEFAULT 0, + + PRIMARY KEY (`id` ASC) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + diff --git a/prisma/migrations/20240722053023_change_to_enum/migration.sql b/prisma/migrations/20240722053023_change_to_enum/migration.sql new file mode 100644 index 00000000..262d327b --- /dev/null +++ b/prisma/migrations/20240722053023_change_to_enum/migration.sql @@ -0,0 +1,18 @@ +/* + Warnings: + + - The primary key for the `userSetting` table will be changed. If it partially fails, the table could be left without primary key constraint. + - You are about to alter the column `id` on the `userSetting` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `UnsignedBigInt`. + - You are about to alter the column `speaker` on the `userSetting` table. The data in that column could be lost. The data in that column will be cast from `VarChar(20)` to `Enum(EnumId(0))`. + - Made the column `skipUser` on table `connectionstates` required. This step will fail if there are existing NULL values in that column. + +*/ +-- AlterTable +ALTER TABLE `connectionStates` MODIFY `skipUser` TEXT NOT NULL; + +-- AlterTable +ALTER TABLE `userSetting` DROP PRIMARY KEY, + MODIFY `id` BIGINT UNSIGNED NOT NULL, + MODIFY `speaker` ENUM('show', 'haruka', 'hikari', 'takeru', 'santa', 'bear') NOT NULL, + MODIFY `isDontRead` BOOLEAN NOT NULL DEFAULT false, + ADD PRIMARY KEY (`id`); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 00000000..e5a788a7 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "mysql" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 00000000..d3259ec9 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,33 @@ +datasource db { + provider = "mysql" + url = env("DATABASE_URL") + shadowDatabaseUrl = env("SHADOW_DATABASE_URL") +} + +generator client { + provider = "prisma-client-js" +} + +enum SpeakerList { + show + haruka + hikari + takeru + santa + bear +} + +model connectionStates { + voiceChannel BigInt @id @db.UnsignedBigInt + guild BigInt @db.UnsignedBigInt + readChannel BigInt @db.UnsignedBigInt + skipUser String @db.Text +} + +model userSetting { + id BigInt @id @db.UnsignedBigInt + speaker SpeakerList + pitch Int @db.UnsignedInt + speed Int @db.UnsignedInt + isDontRead Boolean @default(false) +} diff --git a/src/commands/userSettings.ts b/src/commands/userSettings.ts index 7efe8052..0ebdaba8 100644 --- a/src/commands/userSettings.ts +++ b/src/commands/userSettings.ts @@ -3,9 +3,11 @@ import { getErrorReply, userSettingToString, userSettingToDiff, + randomUserSetting, } from '../utils.js' -import { getUserSetting, randomizeUserSetting, setUserSetting } from '../db.js' +import { getUserSetting, setUserSetting } from '../db.js' import type { InteractionReplyOptions } from 'discord.js' +import { SpeakerList } from '@prisma/client' export class UserSettingsCommand extends Subcommand { public constructor(context: Subcommand.Context, options: Subcommand.Options) { @@ -186,23 +188,22 @@ export class UserSettingsCommand extends Subcommand { } try { - const oldUserSetting = await getUserSetting(interaction.member.id) - const errorMsg: string[] = [] const { options } = interaction const random = options.getBoolean('random') - const speaker = options.getString('speaker') + const speaker = options.getString('speaker') as SpeakerList | null const pitch = options.getInteger('pitch') const speed = options.getInteger('speed') - if (random) { - await randomizeUserSetting(interaction.member.id) - } + const oldUserSetting = await getUserSetting(interaction.member.id) + const newUserSetting = random + ? randomUserSetting(oldUserSetting.id) + : { ...oldUserSetting } if (speaker !== null) { if (allowedVoiceList.includes(speaker)) { - await setUserSetting(interaction.member.id, 'speaker', speaker) + newUserSetting.speaker = speaker } else { errorMsg.push( [ @@ -215,7 +216,7 @@ export class UserSettingsCommand extends Subcommand { if (pitch !== null) { if (pitch > 49 && pitch < 201) { - await setUserSetting(interaction.member.id, 'pitch', pitch) + newUserSetting.pitch = pitch } else { errorMsg.push( `その声の高さ(${pitch}%)は指定できません。指定できる声の高さは、50%~200%です。`, @@ -225,13 +226,15 @@ export class UserSettingsCommand extends Subcommand { if (speed !== null) { if (speed > 49 && speed < 401) { - await setUserSetting(interaction.member.id, 'speed', speed) + newUserSetting.speed = speed } else { errorMsg.push( `その速度(${speed}%)は指定できません。指定できる声の速度は、50%~400%です。`, ) } } + + await setUserSetting(newUserSetting) const userSetting = await getUserSetting(interaction.member.id) if (errorMsg.length === 0) { diff --git a/src/connectionCtx.ts b/src/connectionCtx.ts index a60d7038..4d82fdd0 100644 --- a/src/connectionCtx.ts +++ b/src/connectionCtx.ts @@ -264,7 +264,7 @@ export class ConnectionCtxManager extends Map< } }) } - await deleteState({ voiceChannelId }) + await deleteState({ voiceChannelId: BigInt(voiceChannelId) }) return workerId } } diff --git a/src/db.ts b/src/db.ts index 35eb0dc1..d2673c89 100644 --- a/src/db.ts +++ b/src/db.ts @@ -1,26 +1,16 @@ -import { createPool, SqlError, type PoolConnection } from 'mariadb' +import { + Prisma, + PrismaClient, + type userSetting, + type connectionStates, +} from '@prisma/client' import { DBError } from './errors/index.js' -import type { VoiceBasedChannel } from 'discord.js' import type { ConnectionContext } from './connectionCtx.js' - -export interface UserSetting { - id: bigint - speaker: 'show' | 'haruka' | 'hikari' | 'takeru' | 'santa' | 'bear' - pitch: number - speed: number - isDontRead: 0 | 1 -} - -export interface ConnectionState { - voiceChannel: bigint - guild: bigint - readChannel: bigint - skipUser: string -} +import { randomUserSetting } from './utils.js' const toConnectionState = ( connectionContext: ConnectionContext, -): ConnectionState => { +): connectionStates => { return { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion voiceChannel: BigInt(connectionContext.connection.joinConfig.channelId!), @@ -30,172 +20,136 @@ const toConnectionState = ( } } -const userSettings = new Map() +const cachedUserSettings = new Map() -const pool = createPool({ - host: process.env.DB_HOST, - port: parseInt(process.env.DB_PORT), - user: process.env.DB_USER, - password: process.env.DB_PASSWORD, - database: process.env.DB_DATABASE, - compress: true, -}) +const prisma: PrismaClient = new PrismaClient() -export async function getUserSetting(id: string): Promise { +export async function getUserSetting(id: string): Promise { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - if (userSettings.has(id)) return userSettings.get(id)! - let conn: PoolConnection | undefined = undefined + if (cachedUserSettings.has(id)) return cachedUserSettings.get(id)! try { - conn = await pool.getConnection() - const rows: UserSetting[] = await conn.query( - 'SELECT * FROM userSetting WHERE id = ?', - [id], - ) - if (rows[0] === undefined) { - return await randomizeUserSetting(id) + const userSetting = await prisma.userSetting.findUnique({ + where: { + id: BigInt(id), + }, + }) + + if (userSetting === null) { + return await setUserSetting(randomUserSetting(BigInt(id))) } - userSettings.set(id, rows[0]) - return rows[0] + return userSetting } catch (err) { - if (err instanceof SqlError) { + if ( + err instanceof Prisma.PrismaClientKnownRequestError || + err instanceof Prisma.PrismaClientUnknownRequestError + ) { throw new DBError('ユーザー設定の取得に失敗しました。', { cause: err }) } throw err - } finally { - if (conn) void conn.release() - } -} - -export async function randomizeUserSetting(id: string): Promise { - userSettings.delete(id) - let conn: PoolConnection | undefined = undefined - const voiceList: UserSetting['speaker'][] = [ - 'show', - 'haruka', - 'hikari', - 'takeru', - 'santa', - 'bear', - ] - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const speaker = voiceList[Math.floor(Math.random() * voiceList.length)]! - const pitch = Math.floor(Math.random() * (200 + 1 - 50)) + 50 - const speed = Math.floor(Math.random() * (400 + 1 - 50)) + 50 - const isDontRead = 0 - try { - conn = await pool.getConnection() - await conn.query( - 'INSERT INTO userSetting SET id=?, speaker=?, pitch=?, speed=?, isDontRead=?' + - ' ON DUPLICATE KEY UPDATE speaker=VALUE(speaker), pitch=VALUE(pitch), speed=VALUE(speed), isDontRead=VALUE(isDontRead)', - [id, speaker, pitch, speed, isDontRead], - ) - return { id: BigInt(id), speaker, pitch, speed, isDontRead } - } catch (err) { - if (err instanceof SqlError) { - throw new DBError('ランダム値の設定に失敗しました。', { - cause: err, - }) - } - throw err - } finally { - if (conn) void conn.release() } } export async function setUserSetting( - id: string, - key: string, - value: string | number, -) { - userSettings.delete(id) - let conn: PoolConnection | undefined = undefined + setting: userSetting, +): Promise { + cachedUserSettings.delete(String(setting.id)) try { - conn = await pool.getConnection() - await conn.query(`UPDATE userSetting SET ${key}=? WHERE id = ?`, [ - value, - id, - ]) + const userSetting = await prisma.userSetting.upsert({ + where: { id: BigInt(setting.id) }, + create: { + id: BigInt(setting.id), + speaker: setting.speaker, + pitch: setting.pitch, + speed: setting.speed, + }, + update: { + speaker: setting.speaker, + pitch: setting.pitch, + speed: setting.speed, + }, + }) + + return userSetting } catch (err) { - if (err instanceof SqlError) { - throw new DBError(`${key}の設定に失敗しました。`, { + if ( + err instanceof Prisma.PrismaClientKnownRequestError || + err instanceof Prisma.PrismaClientUnknownRequestError + ) { + throw new DBError(`設定の変更に失敗しました。`, { cause: err, }) } throw err - } finally { - if (conn) void conn.release() } } export function deleteUserCache(id: string) { - userSettings.delete(id) + cachedUserSettings.delete(id) } export async function loadStates() { - let conn: PoolConnection | undefined = undefined try { - conn = await pool.getConnection() - await conn.query( - 'CREATE TABLE IF NOT EXISTS connectionStates (voiceChannel BIGINT UNSIGNED NOT NULL PRIMARY KEY, guild BIGINT UNSIGNED NOT NULL, readChannel BIGINT UNSIGNED NOT NULL, skipUser TEXT)', - ) - const rows: ConnectionState[] = await conn.query( - 'SELECT * FROM connectionStates', - ) - return rows + const connectionStates = prisma.connectionStates.findMany() + return connectionStates } catch (err) { - if (err instanceof SqlError) { + if ( + err instanceof Prisma.PrismaClientKnownRequestError || + err instanceof Prisma.PrismaClientUnknownRequestError || + err instanceof Prisma.PrismaClientInitializationError + ) { throw new DBError(err.message, { cause: err }) } throw err - } finally { - if (conn) void conn.release() } } export async function setState(connectionContext: ConnectionContext) { - const connectionState = toConnectionState(connectionContext) - let conn: PoolConnection | undefined = undefined + const { voiceChannel, guild, readChannel, skipUser } = + toConnectionState(connectionContext) try { - conn = await pool.getConnection() - await conn.query( - 'INSERT IGNORE INTO connectionStates SET voiceChannel=?, guild=?, readChannel=?, skipUser=? ON DUPLICATE KEY UPDATE guild=VALUE(guild), readChannel=VALUE(readChannel), skipUser=VALUE(skipUser)', - [ - connectionState.voiceChannel, - connectionState.guild, - connectionState.readChannel, - connectionState.skipUser, - connectionState.voiceChannel, - ], - ) + await prisma.connectionStates.upsert({ + where: { voiceChannel: voiceChannel }, + create: { + voiceChannel: voiceChannel, + guild, + readChannel, + skipUser, + }, + update: { + guild, + readChannel, + skipUser, + }, + }) } catch (err) { - if (err instanceof SqlError) { + if ( + err instanceof Prisma.PrismaClientKnownRequestError || + err instanceof Prisma.PrismaClientUnknownRequestError + ) { throw new DBError(err.message, { cause: err }) } throw err - } finally { - if (conn) void conn.release() } } export async function deleteState({ - voiceChannel, voiceChannelId, }: { - voiceChannel?: VoiceBasedChannel - voiceChannelId?: string + voiceChannelId: bigint }) { - let conn: PoolConnection | undefined = undefined try { - conn = await pool.getConnection() - await conn.query('DELETE FROM connectionStates WHERE voiceChannel=?', [ - voiceChannel?.id ?? voiceChannelId, - ]) + await prisma.connectionStates.delete({ + where: { + voiceChannel: voiceChannelId, + }, + }) } catch (err) { - if (err instanceof SqlError) { + if ( + err instanceof Prisma.PrismaClientKnownRequestError || + err instanceof Prisma.PrismaClientUnknownRequestError + ) { throw new DBError(err.message, { cause: err }) } throw err - } finally { - if (conn) void conn.release() } } diff --git a/src/load.ts b/src/load.ts index af8a4b18..7505929f 100644 --- a/src/load.ts +++ b/src/load.ts @@ -1,5 +1,6 @@ import type { SapphireClient } from '@sapphire/framework' -import { loadStates, type ConnectionState, deleteState } from './db.js' +import { loadStates, deleteState } from './db.js' +import { type connectionStates } from '@prisma/client' import type { GuildCtxManager } from './guildCtx.js' import type { Client, VoiceBasedChannel } from 'discord.js' import type { @@ -13,7 +14,7 @@ const rejoin = async ({ client, guildCtxManager, }: { - connectionState: ConnectionState + connectionState: connectionStates client: SapphireClient guildCtxManager: GuildCtxManager }) => { @@ -35,7 +36,7 @@ const rejoin = async ({ return guildCtx.join({ voiceChannelId, readChannelId, skipUser }) } catch { return deleteState({ - voiceChannelId: connectionState.voiceChannel.toString(), + voiceChannelId: connectionState.voiceChannel, }) } } diff --git a/src/utils.ts b/src/utils.ts index 2d5a706e..32d1deb3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,7 +1,7 @@ import debug from 'debug' const debug__Queue = debug('utils.js:Queue') import { EventEmitter } from 'events' -import type { UserSetting } from './db.js' +import { SpeakerList, type userSetting } from '@prisma/client' import type { InteractionReplyOptions } from 'discord.js' import { PowError } from './errors/PowError.js' @@ -63,7 +63,27 @@ export class Queue extends EventEmitter { } } -export function userSettingToString(userSetting: UserSetting): string { +export function randomUserSetting(id: bigint): userSetting { + const speakerListValues = Object.values(SpeakerList) + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const speaker = speakerListValues[ + Math.floor(Math.random() * speakerListValues.length) + ]! as SpeakerList + + const pitch = Math.floor(Math.random() * (200 + 1 - 50)) + 50 + const speed = Math.floor(Math.random() * (400 + 1 - 50)) + 50 + + return { + id, + speaker, + pitch, + speed, + isDontRead: false, + } +} + +export function userSettingToString(userSetting: userSetting): string { return [ `speaker: ${userSetting.speaker}`, `pitch: ${userSetting.pitch}`, @@ -73,8 +93,8 @@ export function userSettingToString(userSetting: UserSetting): string { } export const userSettingToDiff = ( - oldUserSetting: UserSetting, - newUserSetting: UserSetting, + oldUserSetting: userSetting, + newUserSetting: userSetting, ) => { return `speaker: ${ oldUserSetting.speaker === newUserSetting.speaker