From fa512bf6c96201a1b4870575d85ac44c2f7fa912 Mon Sep 17 00:00:00 2001 From: kawamataryo Date: Sat, 1 Feb 2025 16:25:24 +0900 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20checkout=E3=82=B3=E3=83=9E?= =?UTF-8?q?=E3=83=B3=E3=83=89=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- drizzle/0004_fantastic_electro.sql | 10 + drizzle/meta/0004_snapshot.json | 284 ++++++++++++++++++ drizzle/meta/_journal.json | 7 + package.json | 2 + scripts/createSlashCommand.ts | 82 ++--- scripts/deleteSlashCommand.ts | 49 +-- src/app.ts | 7 +- src/constants.ts | 4 + .../applicationCommands/checkout.ts | 29 ++ .../modalSubmits/checkoutModal.ts | 41 +++ src/repositories/checkout.ts | 25 ++ src/repositories/usersRepository.ts | 30 +- src/responses/checkinModalSubmitResponse.ts | 22 +- src/responses/checkoutModalResponse.ts | 54 ++++ src/responses/checkoutModalSubmitResponse.ts | 45 +++ src/schema.ts | 23 ++ src/types.ts | 2 + src/utils/getColorFromUsername.ts | 15 + 18 files changed, 655 insertions(+), 76 deletions(-) create mode 100644 drizzle/0004_fantastic_electro.sql create mode 100644 drizzle/meta/0004_snapshot.json create mode 100644 src/interactions/applicationCommands/checkout.ts create mode 100644 src/interactions/modalSubmits/checkoutModal.ts create mode 100644 src/repositories/checkout.ts create mode 100644 src/responses/checkoutModalResponse.ts create mode 100644 src/responses/checkoutModalSubmitResponse.ts create mode 100644 src/utils/getColorFromUsername.ts diff --git a/drizzle/0004_fantastic_electro.sql b/drizzle/0004_fantastic_electro.sql new file mode 100644 index 0000000..fab9d55 --- /dev/null +++ b/drizzle/0004_fantastic_electro.sql @@ -0,0 +1,10 @@ +CREATE TABLE `checkouts` ( + `id` integer PRIMARY KEY NOT NULL, + `date` text DEFAULT CURRENT_DATE, + `user_id` integer NOT NULL, + `content` text NOT NULL, + `created_at` text DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE INDEX `checkouts_userId_idx` ON `checkouts` (`user_id`); \ No newline at end of file diff --git a/drizzle/meta/0004_snapshot.json b/drizzle/meta/0004_snapshot.json new file mode 100644 index 0000000..9f453a6 --- /dev/null +++ b/drizzle/meta/0004_snapshot.json @@ -0,0 +1,284 @@ +{ + "version": "5", + "dialect": "sqlite", + "id": "611abc5e-97a2-4509-8688-404a2523ef23", + "prevId": "a24f9881-d3b1-478b-83f7-10bcdff88b98", + "tables": { + "checkins": { + "name": "checkins", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_DATE" + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "profile": { + "name": "profile", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "todo": { + "name": "todo", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "userId_idx": { + "name": "userId_idx", + "columns": ["user_id"], + "isUnique": false + } + }, + "foreignKeys": { + "checkins_user_id_users_id_fk": { + "name": "checkins_user_id_users_id_fk", + "tableFrom": "checkins", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "checkouts": { + "name": "checkouts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_DATE" + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "checkouts_userId_idx": { + "name": "checkouts_userId_idx", + "columns": ["user_id"], + "isUnique": false + } + }, + "foreignKeys": { + "checkouts_user_id_users_id_fk": { + "name": "checkouts_user_id_users_id_fk", + "tableFrom": "checkouts", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "events": { + "name": "events", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "date": { + "name": "date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "events_to_checkins": { + "name": "events_to_checkins", + "columns": { + "event_id": { + "name": "event_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checkin_id": { + "name": "checkin_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "events_to_checkins_event_id_events_id_fk": { + "name": "events_to_checkins_event_id_events_id_fk", + "tableFrom": "events_to_checkins", + "tableTo": "events", + "columnsFrom": ["event_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "events_to_checkins_checkin_id_checkins_id_fk": { + "name": "events_to_checkins_checkin_id_checkins_id_fk", + "tableFrom": "events_to_checkins", + "tableTo": "checkins", + "columnsFrom": ["checkin_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "events_to_checkins_event_id_checkin_id_pk": { + "columns": ["checkin_id", "event_id"], + "name": "events_to_checkins_event_id_checkin_id_pk" + } + }, + "uniqueConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "discord_user_id": { + "name": "discord_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP" + } + }, + "indexes": { + "users_discord_user_id_unique": { + "name": "users_discord_user_id_unique", + "columns": ["discord_user_id"], + "isUnique": true + }, + "name_idx": { + "name": "name_idx", + "columns": ["name"], + "isUnique": false + }, + "discordUserId_idx": { + "name": "discordUserId_idx", + "columns": ["discord_user_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 3484082..8991017 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -29,6 +29,13 @@ "when": 1709266132201, "tag": "0003_free_dakota_north", "breakpoints": true + }, + { + "idx": 4, + "version": "5", + "when": 1738394221714, + "tag": "0004_fantastic_electro", + "breakpoints": true } ] } diff --git a/package.json b/package.json index 7e31b1a..5660353 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,8 @@ "deploy": "wrangler deploy --minify src/index.ts", "check": "bunx @biomejs/biome check --apply ./src", "generate": "drizzle-kit generate:sqlite --schema=src/schema.ts", + "migrate:local": "wrangler d1 migrations apply prod-mokumoku-bot", + "migrate:prod": "wrangler d1 migrations apply prod-mokumoku-bot --remote", "up": "drizzle-kit up:sqlite --schema=src/schema.ts", "test": "vitest" }, diff --git a/scripts/createSlashCommand.ts b/scripts/createSlashCommand.ts index f8de079..5dd2777 100644 --- a/scripts/createSlashCommand.ts +++ b/scripts/createSlashCommand.ts @@ -1,44 +1,48 @@ import { parseArgs } from "util"; const main = async () => { - const { values } = parseArgs({ - args: Bun.argv, - options: { - name: { - type: 'string', - required: true, - }, - description: { - type: 'string', - required: true, - }, - }, - strict: true, - allowPositionals: true, - }); - - const json = { - name: values.name, - type: 1, - description: values.description, - } - - const headers = { - "Content-Type": "application/json", - Authorization: `Bot ${Bun.env.DISCORD_TOKEN}` - } - - const url = `https://discord.com/api/v10/applications/${Bun.env.DISCORD_APPLICATION_ID}/guilds/1110091489469530132/commands`; - - - const res = await fetch(url, { - method: 'POST', - headers: headers, - body: JSON.stringify(json) - }) - - console.log("Command created โœจ") - console.log(await res.json()) -} + const { values } = parseArgs({ + args: Bun.argv, + options: { + name: { + type: "string", + required: true, + }, + description: { + type: "string", + required: true, + }, + }, + strict: true, + allowPositionals: true, + }); + + const json = { + name: values.name, + type: 1, + description: values.description, + }; + + const headers = { + "Content-Type": "application/json", + Authorization: `Bot ${Bun.env.DISCORD_TOKEN}`, + }; + + const url = `https://discord.com/api/v10/applications/${Bun.env.DISCORD_APPLICATION_ID}/guilds/${Bun.env.DISCORD_GUILD_ID}/commands`; + + const res = await fetch(url, { + method: "POST", + headers: headers, + body: JSON.stringify(json), + }); + + if (!res.ok) { + console.error("Failed to create command", await res.json()); + process.exit(1); + } + + console.log("Command created โœจ"); + console.log(await res.json()); +}; await main(); diff --git a/scripts/deleteSlashCommand.ts b/scripts/deleteSlashCommand.ts index a86934f..96d6294 100644 --- a/scripts/deleteSlashCommand.ts +++ b/scripts/deleteSlashCommand.ts @@ -1,33 +1,36 @@ import { parseArgs } from "util"; const main = async () => { - const { values } = parseArgs({ - args: Bun.argv, - options: { - id: { - type: 'string', - required: true, - }, - }, - strict: true, - allowPositionals: true, - }); + const { values } = parseArgs({ + args: Bun.argv, + options: { + id: { + type: "string", + required: true, + }, + }, + strict: true, + allowPositionals: true, + }); - const headers = { - "Content-Type": "application/json", - Authorization: `Bot ${Bun.env.DISCORD_TOKEN}` - } + const headers = { + "Content-Type": "application/json", + Authorization: `Bot ${Bun.env.DISCORD_TOKEN}`, + }; - const url = `https://discord.com/api/v10/applications/${Bun.env.DISCORD_APPLICATION_ID}/guilds/${Bun.env.DISCORD_GUILD_ID}/commands/${values.id}`; + const url = `https://discord.com/api/v10/applications/${Bun.env.DISCORD_APPLICATION_ID}/guilds/${Bun.env.DISCORD_GUILD_ID}/commands/${values.id}`; + const res = await fetch(url, { + method: "DELETE", + headers: headers, + }); - const res = await fetch(url, { - method: 'DELETE', - headers: headers, - }) + if (!res.ok) { + console.error("Failed to delete command", await res.json()); + process.exit(1); + } - console.log("Command deleted โœจ") - console.log(await res.json()) -} + console.log("Command deleted โœจ"); +}; await main(); diff --git a/src/app.ts b/src/app.ts index cb5c795..ce02e59 100644 --- a/src/app.ts +++ b/src/app.ts @@ -3,13 +3,16 @@ import { Hono } from "hono"; import { ConnpassClient } from "./clients/connpass"; import { DiscordClient } from "./clients/discord"; import checkinCommand from "./interactions/applicationCommands/checkin"; +import checkoutCommand from "./interactions/applicationCommands/checkout"; import generateEventDescription from "./interactions/applicationCommands/generateEventDescription"; import mokumokuStartCommand from "./interactions/applicationCommands/mokumokuStart"; import { handleApplicationCommands } from "./interactions/handleApplicationCommands"; import { handleModalSubmits } from "./interactions/handleModalSubmit"; import checkinModal from "./interactions/modalSubmits/checkinModal"; +import checkoutModal from "./interactions/modalSubmits/checkoutModal"; import { verifyDiscordInteraction } from "./middleware/verifyDiscordInteraction"; import { CheckinsRepository } from "./repositories/checkinsRepository"; +import { CheckoutsRepository } from "./repositories/checkout"; import { EventsRepository } from "./repositories/eventsRepository"; import { EventsToCheckinsRepository } from "./repositories/eventsToCheckinsRepository"; import { UsersRepository } from "./repositories/usersRepository"; @@ -29,6 +32,7 @@ export const interactionRoot = app.post( checkinsRepository: new CheckinsRepository(c.env.DB), eventsRepository: new EventsRepository(c.env.DB), eventsToCheckinsRepository: new EventsToCheckinsRepository(c.env.DB), + checkoutsRepository: new CheckoutsRepository(c.env.DB), }; const clients: Clients = { @@ -48,6 +52,7 @@ export const interactionRoot = app.post( checkinCommand, mokumokuStartCommand, generateEventDescription, + checkoutCommand, ], }), ); @@ -57,7 +62,7 @@ export const interactionRoot = app.post( repositories, clients, modalSubmitObj: body, - modals: [checkinModal], + modals: [checkinModal, checkoutModal], }), ); default: diff --git a/src/constants.ts b/src/constants.ts index 294530b..73ac1e5 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -9,3 +9,7 @@ export const GENERATE_EVENT_DESCRIPTION_COMMAND_NAME = export const CONNPASS_EVENT_PAGE_URL = "https://mito-web-engineer.connpass.com/"; + +export const CHECKOUT_COMMAND_NAME = "checkout"; + +export const CHECKOUT_MODAL_CUSTOM_ID = "checkout"; diff --git a/src/interactions/applicationCommands/checkout.ts b/src/interactions/applicationCommands/checkout.ts new file mode 100644 index 0000000..6a276f5 --- /dev/null +++ b/src/interactions/applicationCommands/checkout.ts @@ -0,0 +1,29 @@ +import { CHECKOUT_COMMAND_NAME } from "../../constants"; +import { buildCheckoutModalResponse } from "../../responses/checkoutModalResponse"; +import { Repositories } from "../../types"; +import { ApplicationCommandObj } from "../handleApplicationCommands"; + +const handler = async ({ + intentObj, + repositories: { usersRepository }, +}: { + intentObj: ApplicationCommandObj; + repositories: Repositories; +}) => { + if (!intentObj.member) { + throw new Error("Invalid interaction"); + } + + const todayCheckin = await usersRepository.findTodayCheckinByDiscordUserId( + intentObj.member.user.id, + ); + + return buildCheckoutModalResponse({ + todo: todayCheckin?.todo || "", + }); +}; + +export default { + commandName: CHECKOUT_COMMAND_NAME, + handler, +}; diff --git a/src/interactions/modalSubmits/checkoutModal.ts b/src/interactions/modalSubmits/checkoutModal.ts new file mode 100644 index 0000000..e6ec450 --- /dev/null +++ b/src/interactions/modalSubmits/checkoutModal.ts @@ -0,0 +1,41 @@ +import { CHECKOUT_MODAL_CUSTOM_ID } from "../../constants"; +import { buildCheckoutModalSubmitResponse } from "../../responses/checkoutModalSubmitResponse"; +import { Repositories } from "../../types"; +import { ModalSubmitObj } from "../handleModalSubmit"; + +const handler = async ({ + modalSubmitObj, + repositories: { usersRepository, checkoutsRepository }, +}: { + modalSubmitObj: ModalSubmitObj; + repositories: Repositories; +}) => { + if (!(modalSubmitObj.member && modalSubmitObj.data)) { + throw new Error("Invalid interaction"); + } + const todo = modalSubmitObj.data.components[0].components[0].value; + const content = modalSubmitObj.data.components[1].components[0].value; + + const user = + (await usersRepository.findUserByDiscordUserId( + modalSubmitObj.member.user.id, + )) ?? + (await usersRepository.create({ + name: modalSubmitObj.member.user.username, + discordUserId: modalSubmitObj.member.user.id, + })); + const checkout = await checkoutsRepository.create({ + userId: user.id, + content, + }); + + return buildCheckoutModalSubmitResponse({ + member: modalSubmitObj.member, + content, + }); +}; + +export default { + customId: CHECKOUT_MODAL_CUSTOM_ID, + handler, +}; diff --git a/src/repositories/checkout.ts b/src/repositories/checkout.ts new file mode 100644 index 0000000..f605ff8 --- /dev/null +++ b/src/repositories/checkout.ts @@ -0,0 +1,25 @@ +import dayjs from "dayjs"; +import { checkouts } from "../schema"; +import { BaseRepository } from "./baseRepository"; + +export class CheckoutsRepository extends BaseRepository { + public async create({ + userId, + content, + }: { + userId: number; + content: string; + }) { + return ( + await this.db + .insert(checkouts) + .values({ + userId, + content, + date: dayjs().tz().format("YYYY-MM-DD"), + createdAt: dayjs().tz().format(), + }) + .returning() + )[0]; + } +} diff --git a/src/repositories/usersRepository.ts b/src/repositories/usersRepository.ts index 25d5c9b..31e9185 100644 --- a/src/repositories/usersRepository.ts +++ b/src/repositories/usersRepository.ts @@ -1,5 +1,5 @@ import dayjs from "dayjs"; -import { desc, eq } from "drizzle-orm"; +import { and, desc, eq } from "drizzle-orm"; import { checkins, users } from "../schema"; import { BaseRepository } from "./baseRepository"; @@ -40,9 +40,35 @@ export class UsersRepository extends BaseRepository { }) .from(users) .leftJoin(checkins, eq(users.id, checkins.userId)) - .where(eq(users.discordUserId, discordUserId)) + .where( + and( + eq(users.discordUserId, discordUserId), + eq(checkins.date, dayjs().format("YYYY-MM-DD")), + ), + ) .orderBy(desc(checkins.createdAt)) .limit(1); return result.length > 0 ? result[0] : undefined; } + + public async findTodayCheckinByDiscordUserId(discordUserId: string) { + const result = await this.db + .select({ + id: checkins.id, + userId: checkins.userId, + todo: checkins.todo, + profile: checkins.profile, + date: checkins.date, + }) + .from(users) + .leftJoin(checkins, eq(users.id, checkins.userId)) + .where( + and( + eq(users.discordUserId, discordUserId), + eq(checkins.date, dayjs().format("YYYY-MM-DD")), + ), + ) + .limit(1); + return result.length > 0 ? result[0] : undefined; + } } diff --git a/src/responses/checkinModalSubmitResponse.ts b/src/responses/checkinModalSubmitResponse.ts index 59a058e..d593785 100644 --- a/src/responses/checkinModalSubmitResponse.ts +++ b/src/responses/checkinModalSubmitResponse.ts @@ -3,20 +3,12 @@ import { APIInteractionResponseChannelMessageWithSource, InteractionResponseType, } from "discord-api-types/v10"; +import { getColorFromUsername } from "../utils/getColorFromUsername"; const buildMemberProfileImageURL = (member: APIInteractionGuildMember) => { return `https://cdn.discordapp.com/avatars/${member.user.id}/${member.user.avatar}.png`; }; -const getRandomColor = () => { - return parseInt( - Math.floor(Math.random() * 0x1000000) - .toString(16) - .padStart(6, "0"), - 16, - ); -}; - export const buildCheckinModalSubmitResponse = ({ member, profile, @@ -33,7 +25,7 @@ export const buildCheckinModalSubmitResponse = ({ embeds: [ { url: "https://discordapp.com", - color: getRandomColor(), + color: getColorFromUsername(member.user.username), thumbnail: { url: thumbnailURL, }, @@ -44,12 +36,20 @@ export const buildCheckinModalSubmitResponse = ({ fields: [ { name: "๐Ÿ‘ค ่‡ชๅทฑ็ดนไป‹", - value: `${profile} \n\n `, + value: profile, + }, + { + name: "", + value: "", }, { name: "๐Ÿ“š ไปŠๆ—ฅใ‚„ใ‚‹ใ“ใจ", value: todo, }, + { + name: "", + value: "", + }, ], }, ], diff --git a/src/responses/checkoutModalResponse.ts b/src/responses/checkoutModalResponse.ts new file mode 100644 index 0000000..2668d41 --- /dev/null +++ b/src/responses/checkoutModalResponse.ts @@ -0,0 +1,54 @@ +import { + APIModalInteractionResponse, + ComponentType, + InteractionResponseType, + TextInputStyle, +} from "discord-api-types/v10"; +import { CHECKOUT_MODAL_CUSTOM_ID } from "../constants"; + +export const buildCheckoutModalResponse = ({ + todo, +}: { + todo: string; +}): APIModalInteractionResponse => { + return { + type: InteractionResponseType.Modal, + data: { + custom_id: CHECKOUT_MODAL_CUSTOM_ID, + title: "ใƒใ‚งใƒƒใ‚ฏใ‚ขใ‚ฆใƒˆ", + components: [ + { + type: ComponentType.ActionRow, + components: [ + { + type: ComponentType.TextInput, + custom_id: "todo", + label: "ไปŠๆ—ฅใ‚„ใ‚‹ใ“ใจ", + style: TextInputStyle.Paragraph, + min_length: 1, + max_length: 512, + required: true, + placeholder: "checkinใงๅ…ฅๅŠ›ใ—ใŸๅ†…ๅฎนใŒ่กจ็คบใ•ใ‚Œใพใ™", + value: todo, + }, + ], + }, + { + type: ComponentType.ActionRow, + components: [ + { + type: ComponentType.TextInput, + custom_id: "content", + label: "ไปŠๆ—ฅใ‚„ใฃใŸใ“ใจ", + style: TextInputStyle.Paragraph, + min_length: 1, + max_length: 512, + required: true, + placeholder: "* ๆ–ฐใ—ใ„ๆฉŸ่ƒฝใŒๅฎŸ่ฃ…ใงใใŸ๐Ÿš€\n* ๆŠ€่ก“ๆ›ธใ‚’่ชญใ‚“ใ ๐Ÿ“š", + }, + ], + }, + ], + }, + }; +}; diff --git a/src/responses/checkoutModalSubmitResponse.ts b/src/responses/checkoutModalSubmitResponse.ts new file mode 100644 index 0000000..e606f2a --- /dev/null +++ b/src/responses/checkoutModalSubmitResponse.ts @@ -0,0 +1,45 @@ +import { + APIInteractionGuildMember, + APIInteractionResponseChannelMessageWithSource, + InteractionResponseType, +} from "discord-api-types/v10"; +import { getColorFromUsername } from "../utils/getColorFromUsername"; + +// todo: ๅ…ฑ้€šๅŒ– +const buildMemberProfileImageURL = (member: APIInteractionGuildMember) => { + return `https://cdn.discordapp.com/avatars/${member.user.id}/${member.user.avatar}.png`; +}; + +export const buildCheckoutModalSubmitResponse = ({ + member, + content, +}: { + member: APIInteractionGuildMember; + content: string; +}): APIInteractionResponseChannelMessageWithSource => { + const thumbnailURL = buildMemberProfileImageURL(member); + return { + type: InteractionResponseType.ChannelMessageWithSource, + data: { + embeds: [ + { + url: "https://discordapp.com", + color: getColorFromUsername(member.user.username), + thumbnail: { + url: thumbnailURL, + }, + footer: { + icon_url: thumbnailURL, + text: `posted by ${member.user.username}`, + }, + fields: [ + { + name: "๐Ÿ’ช ไปŠๆ—ฅใ‚„ใฃใŸใ“ใจ", + value: content, + }, + ], + }, + ], + }, + }; +}; diff --git a/src/schema.ts b/src/schema.ts index 6d62b96..5772484 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -53,6 +53,22 @@ export const eventsToCheckins = sqliteTable( }), ); +export const checkouts = sqliteTable( + "checkouts", + { + id: integer("id").primaryKey(), + date: text("date").default(sql`CURRENT_DATE`), + userId: integer("user_id") + .notNull() + .references(() => users.id), + content: text("content").notNull(), + createdAt: text("created_at").default(sql`CURRENT_TIMESTAMP`), + }, + (checkouts) => ({ + userIdIdx: index("checkouts_userId_idx").on(checkouts.userId), + }), +); + export const events = sqliteTable("events", { id: integer("id").primaryKey(), name: text("name").notNull(), @@ -85,3 +101,10 @@ export const eventsToCheckinsRelations = relations( }), }), ); + +export const checkoutsRelations = relations(checkouts, ({ one }) => ({ + checkin: one(checkins, { + fields: [checkouts.userId], + references: [checkins.userId], + }), +})); diff --git a/src/types.ts b/src/types.ts index 0d5a709..af507c1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,7 @@ import { ConnpassClient } from "./clients/connpass"; import { DiscordClient } from "./clients/discord"; import { CheckinsRepository } from "./repositories/checkinsRepository"; +import { CheckoutsRepository } from "./repositories/checkout"; import { EventsRepository } from "./repositories/eventsRepository"; import { EventsToCheckinsRepository } from "./repositories/eventsToCheckinsRepository"; import { UsersRepository } from "./repositories/usersRepository"; @@ -10,6 +11,7 @@ export type Repositories = { checkinsRepository: CheckinsRepository; eventsRepository: EventsRepository; eventsToCheckinsRepository: EventsToCheckinsRepository; + checkoutsRepository: CheckoutsRepository; }; export type Clients = { diff --git a/src/utils/getColorFromUsername.ts b/src/utils/getColorFromUsername.ts new file mode 100644 index 0000000..e816040 --- /dev/null +++ b/src/utils/getColorFromUsername.ts @@ -0,0 +1,15 @@ +export const getColorFromUsername = (username: string) => { + let hash = 0; + for (let i = 0; i < username.length; i++) { + hash = username.charCodeAt(i) + ((hash << 5) - hash); + } + let color = (hash & 0x00ffffff).toString(16).toUpperCase(); + color = color.padStart(6, "0"); + + // ๆ˜Žใ‚‹ใ•ใ‚’่ชฟๆ•ดใ™ใ‚‹ใŸใ‚ใซใ€ๅ„่‰ฒๆˆๅˆ†ใ‚’ๅฐ‘ใ—ๆ˜Žใ‚‹ใใ™ใ‚‹ + const r = Math.min(255, parseInt(color.substring(0, 2), 16) + 50); + const g = Math.min(255, parseInt(color.substring(2, 4), 16) + 50); + const b = Math.min(255, parseInt(color.substring(4, 6), 16) + 50); + + return (r << 16) + (g << 8) + b; +};