···44import { generatePassphrase } from "../../libs/words";
55import { getUser } from "../../libs/hackernews";
6677-export async function linkUserSetup() {
88- try {
99- slackApp.command(
1010- "/hn-alerts-link",
1111- () => Promise.resolve(),
1212- async ({ payload, context }) => {
1313- const userInput = payload.text?.trim() || null;
1414- let hnUsername = userInput;
77+// Helper functions for each command action
88+async function handleLinkRequest(
99+ userId: string,
1010+ userInput: string | null,
1111+): Promise<string> {
1212+ let hnUsername = userInput;
15131616- const userFromDB = await db
1717- .select()
1818- .from(usersTable)
1919- .where(eq(usersTable.id, payload.user_id))
2020- .then((user) => user[0]);
1414+ // Extract username from URL if provided
1515+ if (userInput?.includes("news.ycombinator.com/user?id=")) {
1616+ try {
1717+ const cleanedInput = userInput.replace(/[<>]/g, "");
1818+ const username = new URL(cleanedInput).searchParams.get("id");
1919+ if (username) hnUsername = username;
2020+ } catch (e) {
2121+ console.log("Failed to parse URL, using raw input", e);
2222+ }
2323+ }
21242222- if (userFromDB) {
2323- if (!userFromDB.verified) {
2424- const res = await getUser(
2525- userFromDB.hackernewsUsername as string,
2626- ).then((user) =>
2727- user?.about?.includes(userFromDB.challenge as string),
2828- );
2525+ if (!hnUsername) {
2626+ return "Please provide your Hacker News username: `/hn-alerts-link your_username`";
2727+ }
29283030- if (!res) {
3131- await context.respond({
3232- text: `Your Hacker News account is not verified. Add \`${userFromDB.challenge}\` to your <https://news.ycombinator.com/user?id=${userFromDB.hackernewsUsername}|profile>.`,
3333- response_type: "ephemeral",
3434- });
3535- return;
3636- }
2929+ const verificationPhrase = generatePassphrase(3);
37303838- await db.update(usersTable).set({ verified: true });
3131+ await db.insert(usersTable).values({
3232+ id: userId,
3333+ hackernewsUsername: hnUsername,
3434+ challenge: verificationPhrase,
3535+ });
3636+3737+ return `Please verify your Hacker News username: <https://news.ycombinator.com/user?id=${hnUsername}|\`${hnUsername}\`> by adding the verification phrase: \`${verificationPhrase}\`. When you're done, type \`/hn-alerts-link verify\` to complete the process.`;
3838+}
3939+4040+async function handleVerify(
4141+ userId: string,
4242+ hackernewsUsername: string,
4343+ challenge: string,
4444+): Promise<string> {
4545+ const res = await getUser(hackernewsUsername as string).then((user) =>
4646+ user?.about?.includes(challenge as string),
4747+ );
39484040- await context.respond({
4141- text: "Your Hacker News account has been verified :yay:",
4242- response_type: "ephemeral",
4343- });
4444- return;
4545- }
4949+ if (!res) {
5050+ return `Your Hacker News account is not verified. Add \`${challenge}\` to your <https://news.ycombinator.com/user?id=${hackernewsUsername}|profile>.`;
5151+ }
46524747- await context.respond({
4848- text: "You are already linked to a Hacker News account.",
4949- response_type: "ephemeral",
5050- });
5151- return;
5252- }
5353+ await db
5454+ .update(usersTable)
5555+ .set({ verified: true })
5656+ .where(eq(usersTable.id, userId));
53575454- // Extract username from URL if provided
5555- if (userInput?.includes("news.ycombinator.com/user?id=")) {
5656- try {
5757- const cleanedInput = userInput.replace(/[<>]/g, "");
5858- const username = new URL(cleanedInput).searchParams.get("id");
5959- if (username) hnUsername = username;
6060- } catch (e) {
6161- console.log("Failed to parse URL, using raw input", e);
6262- }
6363- }
5858+ return "Your Hacker News account has been verified :yay:";
5959+}
64606565- const verificationPhrase = generatePassphrase(3);
6161+async function handleUnlink(userId: string): Promise<string> {
6262+ await db.delete(usersTable).where(eq(usersTable.id, userId));
66636767- await db.insert(usersTable).values({
6868- id: payload.user_id,
6969- hackernewsUsername: hnUsername,
7070- challenge: verificationPhrase,
7171- });
6464+ return "Your Hacker News account has been unlinked successfully.";
6565+}
72667373- await context.respond({
7474- text: hnUsername
7575- ? `Please verify your Hacker News username: <https://news.ycombinator.com/user?id=${hnUsername}|\`${hnUsername}\`> by adding the verification phrase: \`${verificationPhrase}\`. When you're done, type \`/hn-alerts-link\` to complete the process.`
7676- : "Please provide your Hacker News username: `/hn-alerts-link your_username`",
7777- response_type: "ephemeral",
7878- });
7979- },
8080- );
8181- } catch (error) {
8282- console.error("Error setting up linking", error);
8383- }
6767+function handleHelp(): string {
6868+ return (
6969+ "Available commands:\n" +
7070+ "• `/hn-alerts-link your_username` - Link your account\n" +
7171+ "• `/hn-alerts-link verify` - Verify your Hacker News account\n" +
7272+ "• `/hn-alerts-link unlink` - Remove your linked account\n" +
7373+ "• `/hn-alerts-link help` - Show this help message"
7474+ );
8475}
7676+7777+export { handleVerify, handleUnlink, handleHelp, handleLinkRequest };
+2-2
src/features/index.ts
···11-import { linkUserSetup } from "./handler/linking";
11+import { commandSetup } from "./routing/commad";
2233export default async function setup() {
44- linkUserSetup();
44+ commandSetup();
55}
+86
src/features/routing/commad.ts
···11+import { eq } from "drizzle-orm";
22+import { slackApp, db } from "../../index";
33+import { users as usersTable } from "../../libs/schema";
44+import {
55+ handleVerify,
66+ handleUnlink,
77+ handleHelp,
88+ handleLinkRequest,
99+} from "../handler/linking";
1010+1111+export async function commandSetup() {
1212+ try {
1313+ slackApp.command(
1414+ "/hn-alerts-link",
1515+ () => Promise.resolve(),
1616+ async ({ payload, context }) => {
1717+ const input = payload.text?.trim() || "";
1818+ const userId = payload.user_id;
1919+ const command = input.split(" ")[0]?.toLowerCase() ?? "";
2020+2121+ const userFromDB = await db
2222+ .select()
2323+ .from(usersTable)
2424+ .where(eq(usersTable.id, userId))
2525+ .then((user) => user[0]);
2626+2727+ let responseText = "";
2828+2929+ // Handle commands using a switch statement
3030+ switch (command) {
3131+ case "verify":
3232+ if (!userFromDB) {
3333+ responseText =
3434+ "You don't have a pending verification. Use `/hn-alerts-link your_username` first.";
3535+ } else if (userFromDB.verified) {
3636+ responseText = "Your account is already verified.";
3737+ } else {
3838+ responseText = await handleVerify(
3939+ userId,
4040+ userFromDB.hackernewsUsername as string,
4141+ userFromDB.challenge as string,
4242+ );
4343+ }
4444+ break;
4545+4646+ case "unlink":
4747+ if (!userFromDB) {
4848+ responseText = "You don't have a linked Hacker News account.";
4949+ } else {
5050+ responseText = await handleUnlink(userId);
5151+ }
5252+ break;
5353+5454+ case "help":
5555+ responseText = handleHelp();
5656+ break;
5757+5858+ default:
5959+ // If the user is already linked and verified
6060+ if (userFromDB?.verified) {
6161+ responseText = `You are already linked to the <https://news.ycombinator.com/user?id=${userFromDB.hackernewsUsername}|\`${userFromDB.hackernewsUsername}\`> Hacker News account. Use \`/hn-alerts-link unlink\` to remove the link.`;
6262+ }
6363+ // If there's a pending verification
6464+ else if (userFromDB) {
6565+ responseText = await handleVerify(
6666+ userId,
6767+ userFromDB.hackernewsUsername as string,
6868+ userFromDB.challenge as string,
6969+ );
7070+ }
7171+ // Handle new link request (when no command specified, treat input as username)
7272+ else {
7373+ responseText = await handleLinkRequest(userId, input);
7474+ }
7575+ }
7676+7777+ await context.respond({
7878+ text: responseText,
7979+ response_type: "ephemeral",
8080+ });
8181+ },
8282+ );
8383+ } catch (error) {
8484+ console.error("Error setting up linking", error);
8585+ }
8686+}
+10-9
src/libs/db.ts
···11-import { drizzle } from "drizzle-orm/node-postgres";
22-import { Pool } from "pg";
11+import { drizzle } from "drizzle-orm/bun-sqlite";
22+import { Database } from "bun:sqlite";
33import * as schema from "./schema";
4455-const pool = new Pool({
66- connectionString: process.env.DATABASE_URL,
77-});
55+// Use environment variable for the database path in production
66+const dbPath = process.env.DATABASE_PATH || "./local.db";
8799-export const db = drizzle(pool, { schema });
88+// Create a SQLite database instance using Bun's built-in driver
99+const sqlite = new Database(dbPath);
10101111-// Set up triggers when initializing the database
1212-schema.setupTriggers(pool).catch(console.error);
1111+// Create a Drizzle instance with the database and schema
1212+export const db = drizzle(sqlite, { schema });
13131414-export { pool, schema };
1414+// Export the sqlite instance and schema for use in other files
1515+export { sqlite, schema };
+3-31
src/libs/schema.ts
···11-import { boolean, pgTable, text, timestamp } from "drizzle-orm/pg-core";
22-import type { Pool } from "pg";
11+import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
3243// Define the users table
55-export const users = pgTable("users", {
44+export const users = sqliteTable("users", {
65 id: text("id").primaryKey(),
76 hackernewsUsername: text("hackernews_username"),
87 challenge: text("challenge"),
99- verified: boolean("verified").default(false),
1010- createdAt: timestamp("created_at")
1111- .$defaultFn(() => new Date())
1212- .notNull(),
1313- updatedAt: timestamp("updated_at")
1414- .$defaultFn(() => new Date())
1515- .notNull(),
88+ verified: integer("verified", { mode: "boolean" }).default(false).notNull(),
169});
1717-1818-export async function setupTriggers(pool: Pool) {
1919- await pool.query(`
2020- -- Create or replace the update function
2121- CREATE OR REPLACE FUNCTION update_user_updated_at()
2222- RETURNS TRIGGER AS $$
2323- BEGIN
2424- NEW.updated_at = NOW();
2525- RETURN NEW;
2626- END;
2727- $$ LANGUAGE plpgsql;
2828-2929- -- Drop trigger if exists and create a new one
3030- DROP TRIGGER IF EXISTS update_user_updated_at_trigger ON users;
3131-3232- CREATE TRIGGER update_user_updated_at_trigger
3333- BEFORE UPDATE ON users
3434- FOR EACH ROW
3535- EXECUTE FUNCTION update_user_updated_at();
3636- `);
3737-}