Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
diff --git a/.vscode/settings.json b/.vscode/settings.json index 779c22d..7dad9a1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,6 @@ "editor.defaultFormatter": "biomejs.biome", "editor.formatOnPaste": true, "editor.formatOnSave": true, - "cSpell.words": ["CHECKIN", "checkins", "Mokumoku"], + "cSpell.words": ["CHECKIN", "checkins", "Connpass", "Mokumoku"], "editor.formatOnType": true } diff --git a/package.json b/package.json index b2819a2..46c4e93 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "discord-api-types": "^0.37.70", "discord-interactions": "^3.4.0", "drizzle-orm": "^0.29.4", + "form-data": "^4.0.0", "hono": "^4.0.5", "lefthook": "^1.6.3" }, diff --git a/src/app.ts b/src/app.ts index 3ee67c6..1b2b04f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,6 +1,7 @@ import { InteractionType } from "discord-interactions"; import { Hono } from "hono"; import checkinCommand from "./interactions/applicationCommands/checkin"; +import generateEventDescription from "./interactions/applicationCommands/generateEventDescription"; import mokumokuStartCommand from "./interactions/applicationCommands/mokumokuStart"; import { handleApplicationCommands } from "./interactions/handleApplicationCommands"; import { handleModalSubmits } from "./interactions/handleModalSubmit"; @@ -11,10 +12,7 @@ import { EventsRepository } from "./repositories/eventsRepository"; import { EventsToCheckinsRepository } from "./repositories/eventsToCheckinsRepository"; import { UsersRepository } from "./repositories/usersRepository"; import { errorResponse } from "./responses/errorResponse"; - -type Bindings = { - DB: D1Database; -}; +import { Bindings } from "./types"; const app = new Hono<{ Bindings: Bindings }>(); @@ -35,7 +33,12 @@ app.post("/interaction", verifyDiscordInteraction, async (c) => { await handleApplicationCommands({ repositories, intentObj: body, - commands: [checkinCommand, mokumokuStartCommand], + env: c.env, + commands: [ + checkinCommand, + mokumokuStartCommand, + generateEventDescription, + ], }), ); case InteractionType.MODAL_SUBMIT: diff --git a/src/clients/discord.ts b/src/clients/discord.ts index b86f640..c3b1b47 100644 --- a/src/clients/discord.ts +++ b/src/clients/discord.ts @@ -1,4 +1,9 @@ -import { RESTPostAPIChannelMessageJSONBody } from "discord-api-types/v10"; +import { + RESTPatchAPIChannelMessageFormDataBody, + RESTPostAPIChannelMessageFormDataBody, + RESTPostAPIChannelMessageJSONBody, +} from "discord-api-types/v10"; +import FormData from "form-data"; export class DiscordClient { private BASE_URL = "https://discord.com/api/v10/"; @@ -17,10 +22,46 @@ export class DiscordClient { channelId, body, }: { channelId: string; body: RESTPostAPIChannelMessageJSONBody }) { - await fetch(`${this.BASE_URL}/channels/${channelId}/messages`, { + const res = await fetch(`${this.BASE_URL}/channels/${channelId}/messages`, { method: "POST", body: JSON.stringify(body), headers: this.config.headers, }); + if (!res.ok) { + throw new Error( + `Failed to send message: ${res.status} ${res.statusText}`, + ); + } + } + + async sendTextFile({ + channelId, + file, + }: { + channelId: string; + file: { + content: string; + name: string; + }; + }) { + const form = new FormData(); + form.append( + "file", + new Blob([file.content], { type: "text/plain" }), + file.name, + ); + + const res = await fetch(`${this.BASE_URL}/channels/${channelId}/messages`, { + method: "POST", + body: form as unknown as BodyInit, + headers: { + Authorization: this.config.headers.Authorization, + }, + }); + if (!res.ok) { + throw new Error( + `Failed to send message: ${res.status} ${res.statusText}`, + ); + } } } diff --git a/src/constants.ts b/src/constants.ts index 7b1bec0..294530b 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,3 +1,11 @@ export const CHECKIN_COMMAND_NAME = "checkin"; -export const MOKUMOKU_START_COMMAND_NAME = "mokumoku-start"; + export const CHECKIN_MODAL_CUSTOM_ID = "checkin"; + +export const MOKUMOKU_START_COMMAND_NAME = "mokumoku-start"; + +export const GENERATE_EVENT_DESCRIPTION_COMMAND_NAME = + "generate-event-description"; + +export const CONNPASS_EVENT_PAGE_URL = + "https://mito-web-engineer.connpass.com/"; diff --git a/src/interactions/handleApplicationCommands.ts b/src/interactions/handleApplicationCommands.ts index ee4957d..c86caff 100644 --- a/src/interactions/handleApplicationCommands.ts +++ b/src/interactions/handleApplicationCommands.ts @@ -1,5 +1,5 @@ import { APIBaseInteraction, InteractionType } from "discord-api-types/v10"; -import { Repositories } from "../types"; +import { Bindings, Repositories } from "../types"; export type ApplicationCommandObj = APIBaseInteraction< InteractionType.ApplicationCommand, @@ -11,15 +11,18 @@ export type ApplicationCommandObj = APIBaseInteraction< export const handleApplicationCommands = async ({ intentObj, repositories, + env, commands, }: { intentObj: ApplicationCommandObj; repositories: Repositories; + env: Bindings; commands: { commandName: string; handler: (args: { intentObj: ApplicationCommandObj; repositories: Repositories; + env: Bindings; }) => Promise<{ type: number; data: unknown; @@ -31,6 +34,7 @@ export const handleApplicationCommands = async ({ return command.handler({ intentObj, repositories, + env, }); } } diff --git a/src/repositories/baseRepository.ts b/src/repositories/baseRepository.ts index cbc3888..cc49aac 100644 --- a/src/repositories/baseRepository.ts +++ b/src/repositories/baseRepository.ts @@ -1,9 +1,10 @@ import { DrizzleD1Database, drizzle } from "drizzle-orm/d1"; +import * as schema from "../schema"; export class BaseRepository { - protected db: DrizzleD1Database<Record<string, never>>; + protected db: DrizzleD1Database<typeof schema>; constructor(d1db: D1Database) { - this.db = drizzle(d1db); + this.db = drizzle(d1db, { schema }); } } diff --git a/src/repositories/eventsRepository.ts b/src/repositories/eventsRepository.ts index 9ad8c4d..0e13693 100644 --- a/src/repositories/eventsRepository.ts +++ b/src/repositories/eventsRepository.ts @@ -1,15 +1,26 @@ import dayjs from "dayjs"; -import { eq } from "drizzle-orm"; +import { desc, eq } from "drizzle-orm"; import { events } from "../schema"; import { BaseRepository } from "./baseRepository"; export class EventsRepository extends BaseRepository { async findTodayEvent() { - const targetEvents = await this.db - .select() - .from(events) - .where(eq(events.date, dayjs().tz().format("YYYY-MM-DD"))); - return targetEvents.length > 0 ? targetEvents[0] : null; + return await this.db.query.events.findFirst({ + where: eq(events.date, dayjs().tz().format("YYYY-MM-DD")), + }); + } + + async findLatestEventWithCheckins() { + return await this.db.query.events.findFirst({ + orderBy: [desc(events.createdAt)], + with: { + eventsToCheckins: { + with: { + checkin: true, + }, + }, + }, + }); } async create({ diff --git a/src/schema.ts b/src/schema.ts index f33328a..bb0fb68 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -1,4 +1,4 @@ -import { sql } from "drizzle-orm"; +import { relations, sql } from "drizzle-orm"; import { index, integer, @@ -59,3 +59,25 @@ export const events = sqliteTable("events", { date: text("date").notNull(), createdAt: text("created_at").default(sql`CURRENT_TIMESTAMP`), }); + +export const eventsRelations = relations(events, ({ many }) => ({ + eventsToCheckins: many(eventsToCheckins), +})); + +export const checkinsRelations = relations(checkins, ({ many }) => ({ + eventsToCheckins: many(eventsToCheckins), +})); + +export const eventsToCheckinsRelations = relations( + eventsToCheckins, + ({ one }) => ({ + event: one(events, { + fields: [eventsToCheckins.eventId], + references: [events.id], + }), + checkin: one(checkins, { + fields: [eventsToCheckins.checkinId], + references: [checkins.id], + }), + }), +); diff --git a/src/types.ts b/src/types.ts index 403e37d..24bcb38 100644 --- a/src/types.ts +++ b/src/types.ts @@ -9,3 +9,9 @@ export type Repositories = { eventsRepository: EventsRepository; eventsToCheckinsRepository: EventsToCheckinsRepository; }; + +export type Bindings = { + DB: D1Database; + DISCORD_TOKEN: string; + MOKUMOKU_CHANNEL_ID: string; +};
- Loading branch information