Skip to content

Commit

Permalink
[from now] 2024/03/03 12:49:55
Browse files Browse the repository at this point in the history
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
kawamataryo committed Mar 3, 2024
1 parent 608edd8 commit 28b3c67
Show file tree
Hide file tree
Showing 10 changed files with 116 additions and 19 deletions.
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
13 changes: 8 additions & 5 deletions src/app.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 }>();

Expand All @@ -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:
Expand Down
45 changes: 43 additions & 2 deletions src/clients/discord.ts
Original file line number Diff line number Diff line change
@@ -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/";
Expand All @@ -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}`,
);
}
}
}
10 changes: 9 additions & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
@@ -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/";
6 changes: 5 additions & 1 deletion src/interactions/handleApplicationCommands.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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;
Expand All @@ -31,6 +34,7 @@ export const handleApplicationCommands = async ({
return command.handler({
intentObj,
repositories,
env,
});
}
}
Expand Down
5 changes: 3 additions & 2 deletions src/repositories/baseRepository.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
}
23 changes: 17 additions & 6 deletions src/repositories/eventsRepository.ts
Original file line number Diff line number Diff line change
@@ -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({
Expand Down
24 changes: 23 additions & 1 deletion src/schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { sql } from "drizzle-orm";
import { relations, sql } from "drizzle-orm";
import {
index,
integer,
Expand Down Expand Up @@ -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],
}),
}),
);
6 changes: 6 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,9 @@ export type Repositories = {
eventsRepository: EventsRepository;
eventsToCheckinsRepository: EventsToCheckinsRepository;
};

export type Bindings = {
DB: D1Database;
DISCORD_TOKEN: string;
MOKUMOKU_CHANNEL_ID: string;
};

0 comments on commit 28b3c67

Please sign in to comment.