···11-import { slackApp } from "../../index";
11+import { eq } from "drizzle-orm";
22+import { db, slackApp } from "../../index";
33+import { users as usersTable } from "../../libs/schema";
44+import { generatePassphrase } from "../../libs/words";
55+import { getUser } from "../../libs/hackernews";
2637export async function linkUserSetup() {
48 try {
···610 "/hn-alerts-link",
711 () => Promise.resolve(),
812 async ({ payload, context }) => {
1313+ const userInput = payload.text?.trim() || null;
1414+ let hnUsername = userInput;
1515+1616+ const userFromDB = await db
1717+ .select()
1818+ .from(usersTable)
1919+ .where(eq(usersTable.id, payload.user_id))
2020+ .then((user) => user[0]);
2121+2222+ 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+ );
2929+3030+ 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+ }
3737+3838+ await db.update(usersTable).set({ verified: true });
3939+4040+ await context.respond({
4141+ text: "Your Hacker News account has been verified :yay:",
4242+ response_type: "ephemeral",
4343+ });
4444+ return;
4545+ }
4646+4747+ await context.respond({
4848+ text: "You are already linked to a Hacker News account.",
4949+ response_type: "ephemeral",
5050+ });
5151+ return;
5252+ }
5353+5454+ // 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+ }
6464+6565+ const verificationPhrase = generatePassphrase(3);
6666+6767+ await db.insert(usersTable).values({
6868+ id: payload.user_id,
6969+ hackernewsUsername: hnUsername,
7070+ challenge: verificationPhrase,
7171+ });
7272+973 await context.respond({
1010- text: "Linking successful!",
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`",
1177 response_type: "ephemeral",
1278 });
1379 },
+46
src/libs/hackernews.ts
···11+export interface User {
22+ id: string;
33+ created: number;
44+ karma: number;
55+ about?: string;
66+ submitted?: number[];
77+}
88+99+/**
1010+ * Fetches user data by user ID from the Hacker News API.
1111+ * Only users with public activity (comments or story submissions) are available.
1212+ *
1313+ * @param userId - The user's unique username (case-sensitive)
1414+ * @returns Promise resolving to the user data
1515+ * @throws Error if the user cannot be found or if there's a network error
1616+ */
1717+export async function getUser(userId: string): Promise<User> {
1818+ if (!userId) {
1919+ throw new Error("User ID is required");
2020+ }
2121+2222+ try {
2323+ const response = await fetch(
2424+ `https://hacker-news.firebaseio.com/v0/user/${userId}.json`,
2525+ );
2626+2727+ if (!response.ok) {
2828+ throw new Error(
2929+ `Failed to fetch user with ID ${userId}: ${response.statusText}`,
3030+ );
3131+ }
3232+3333+ const userData = await response.json();
3434+3535+ if (!userData) {
3636+ throw new Error(`User with ID ${userId} not found`);
3737+ }
3838+3939+ return userData as User;
4040+ } catch (error) {
4141+ if (error instanceof Error) {
4242+ throw error;
4343+ }
4444+ throw new Error(`Failed to fetch user with ID ${userId}: ${String(error)}`);
4545+ }
4646+}
+3-1
src/libs/schema.ts
···11-import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
11+import { boolean, pgTable, text, timestamp } from "drizzle-orm/pg-core";
22import type { Pool } from "pg";
3344// Define the users table
55export const users = pgTable("users", {
66 id: text("id").primaryKey(),
77 hackernewsUsername: text("hackernews_username"),
88+ challenge: text("challenge"),
99+ verified: boolean("verified").default(false),
810 createdAt: timestamp("created_at")
911 .$defaultFn(() => new Date())
1012 .notNull(),