From 63fdd3aa8b1603c487f0ad8f68547493ea0c4588 Mon Sep 17 00:00:00 2001 From: andrewliu08 <55035762+andrewliu08@users.noreply.github.com> Date: Thu, 15 Aug 2024 11:35:37 -0300 Subject: [PATCH] Clippy: Add commands for creating tags (#1273) * add tags to clippy * add get-tag function * fix bugs * clean up --- apps/daimo-clippy/.env.example | 6 ++- apps/daimo-clippy/src/handlers.ts | 64 +++++++++++++++++++++++ packages/daimo-api/src/api/tagRedirect.ts | 15 ++++-- packages/daimo-api/src/db/db.ts | 38 ++++++++++---- packages/daimo-api/src/server/router.ts | 27 ++++++++-- 5 files changed, 130 insertions(+), 20 deletions(-) diff --git a/apps/daimo-clippy/.env.example b/apps/daimo-clippy/.env.example index de900d5c7..53a5966f5 100644 --- a/apps/daimo-clippy/.env.example +++ b/apps/daimo-clippy/.env.example @@ -1,4 +1,6 @@ DAIMO_API_URL='http://localhost:3000' DAIMO_CHAIN='baseSepolia' -SLACK_COMMAND_TOKEN='' -DAIMO_API_KEY='' \ No newline at end of file +SLACK_BOT_TOKEN='' +SLACK_SIGNING_SECRET='' +DAIMO_API_KEY='' +CLIPPY_TAG_UPDATE_TOKEN='' \ No newline at end of file diff --git a/apps/daimo-clippy/src/handlers.ts b/apps/daimo-clippy/src/handlers.ts index 7833f711d..b1aef9472 100644 --- a/apps/daimo-clippy/src/handlers.ts +++ b/apps/daimo-clippy/src/handlers.ts @@ -14,6 +14,7 @@ import { rpc } from "./rpc"; import { getJSONblock, parseKwargs, unfurlLink } from "./utils"; const apiKey = assertNotNull(process.env.DAIMO_API_KEY); +const clippyTagUpdateToken = assertNotNull(process.env.CLIPPY_TAG_UPDATE_TOKEN); export async function handleCommand(text: string): Promise { console.log(`[SLACK-BOT] Handling command ${text}`); @@ -77,6 +78,14 @@ const commands: Record = { help: "Gets the best swap quote for fromToken to toToken. Args: [fromAmount=1.23, fromToken=DAI, toToken=USDC]", fn: getSwapQuote, }, + "get-tag": { + help: "Get the links previously associated with a tag. Args: [tag]", + fn: getTag, + }, + "set-tag": { + help: "Create or update a tag. Associates https://daimo.com/l/t/ with a link. Args: [tag, link, updateLink]", + fn: setTag, + }, health: { help: "Checks that the API is up", fn: health, @@ -270,6 +279,61 @@ export async function createInviteCode( )}`; } +export async function getTag(kwargs: Map): Promise { + const tag = assertNotNull(kwargs.get("tag")); + + console.log(`[SLACK-BOT] getting tag: ${tag}`); + const res = await rpc.getTagHistory.query({ tag }); + + return `Successfully got tag: ${getJSONblock(res)}`; +} + +export async function setTag(kwargs: Map) { + const tag = assertNotNull(kwargs.get("tag")); + const link = kwargs.get("link"); + const updateLink = kwargs.get("updateLink"); + + if (!link && !updateLink) return "Must specify link or updateLink"; + if (link && updateLink) return "Cannot specify both link and updateLink"; + + if (link) { + console.log(`[SLACK-BOT] creating tag: ${tag}, link: ${link}`); + const existingTag = await rpc.getTagRedirect.query({ tag }); + if (existingTag) { + return `Tag ${tag} already exists. To update the tag, use the updateLink parameter instead of link`; + } + + try { + const res = await rpc.createTagRedirect.mutate({ + apiKey, + tag, + link, + updateToken: clippyTagUpdateToken, + }); + return `Successfully created tag: ${getJSONblock(res)}`; + } catch (e) { + console.error(`[SLACK-BOT] Error setting tag: ${e}`); + return `Error setting tag: ${e}`; + } + } else { + console.log(`[SLACK-BOT] updating tag: ${tag}, updateLink: ${updateLink}`); + try { + const res = await rpc.updateTagRedirect.mutate({ + tag, + link: updateLink!, + updateToken: clippyTagUpdateToken, + }); + return `Successfully updated tag: ${getJSONblock(res)}`; + } catch (e: any) { + console.error(`[SLACK-BOT] Error updating tag: ${e}`); + if (e.message === "Tag does not exist") { + return `Tag ${tag} does not exist. To create a new tag, set the link parameter instead of updateLink`; + } + return `Error updating tag: ${e}`; + } + } +} + async function viewInviteStatus(kwargs: Map): Promise { const url = await (async () => { if (kwargs.has("link")) { diff --git a/packages/daimo-api/src/api/tagRedirect.ts b/packages/daimo-api/src/api/tagRedirect.ts index 840cc0849..2f155ee4c 100644 --- a/packages/daimo-api/src/api/tagRedirect.ts +++ b/packages/daimo-api/src/api/tagRedirect.ts @@ -12,7 +12,16 @@ export async function getTagRedirect( // Returns tag redirect URL, or null if tag does not exist. export async function getTagRedirectHist(tag: string, db: DB) { - return db.loadTagRedirectHist(tag); + return await db.loadTagRedirectHist(tag); +} + +export async function createTagRedirect( + tag: string, + link: string, + updateToken: string, + db: DB +) { + return await db.saveTagRedirect(tag, link, updateToken); } // Updates tag redirect URL, authenticatd by updateToken. @@ -21,9 +30,9 @@ export async function setTagRedirect( link: string, updateToken: string, db: DB -): Promise { +) { await verifyTagUpdateToken(tag, updateToken, db); - return db.saveTagRedirect(tag, link); + return await db.saveTagRedirect(tag, link); } export async function verifyTagUpdateToken( diff --git a/packages/daimo-api/src/db/db.ts b/packages/daimo-api/src/db/db.ts index f41d408bc..3d11db2e0 100644 --- a/packages/daimo-api/src/db/db.ts +++ b/packages/daimo-api/src/db/db.ts @@ -235,19 +235,37 @@ export class DB { })); } - async saveTagRedirect(tag: string, link: string) { + async saveTagRedirect( + tag: string, + link: string, + updateToken?: string + ): Promise { console.log(`[DB] inserting tag redirect: ${tag} -> ${link}`); - const res = await this.pool.query( - `INSERT INTO tag_redirect (tag, link) VALUES ($1, $2) - ON CONFLICT (tag) DO UPDATE SET link = $2`, - [tag, link] - ); + const queryParams = [tag, link]; + let query = `INSERT INTO tag_redirect (tag, link) VALUES ($1, $2) + ON CONFLICT (tag) DO UPDATE SET link = $2 + RETURNING *`; + + if (updateToken !== undefined) { + query = `INSERT INTO tag_redirect (tag, link, update_token) VALUES ($1, $2, $3) + ON CONFLICT (tag) DO UPDATE SET link = $2, update_token = $3 + RETURNING *`; + queryParams.push(updateToken); + } + + const res = await this.pool.query(query, queryParams); + if (res.rowCount && res.rowCount > 0) { - await this.pool.query( - `INSERT INTO tag_redirect_history (tag, link) VALUES ($1, $2)`, - [tag, link] - ); + let historyQuery = `INSERT INTO tag_redirect_history (tag, link) VALUES ($1, $2)`; + + if (updateToken !== undefined) { + historyQuery = `INSERT INTO tag_redirect_history (tag, link, update_token) VALUES ($1, $2, $3)`; + } + + await this.pool.query(historyQuery, queryParams); } + + return res.rows[0]; } async saveOffchainAction(row: OffchainActionRow) { diff --git a/packages/daimo-api/src/server/router.ts b/packages/daimo-api/src/server/router.ts index d8b5c9ef2..3bd4f43b2 100644 --- a/packages/daimo-api/src/server/router.ts +++ b/packages/daimo-api/src/server/router.ts @@ -43,6 +43,7 @@ import { search } from "../api/search"; import { sendUserOpV2 } from "../api/sendUserOpV2"; import { submitWaitlist } from "../api/submitWaitlist"; import { + createTagRedirect, getTagRedirect, getTagRedirectHist, setTagRedirect, @@ -586,7 +587,6 @@ export function createRouter( return profileCache.updateProfileLinks(addr, actionJSON, signature); }), - // @deprecated, remove by 2024 Q4 getTagRedirect: publicProcedure .input(z.object({ tag: z.string() })) .query(async (opts) => { @@ -594,22 +594,39 @@ export function createRouter( return getTagRedirect(tag, db); }), - // @deprecated, remove by 2024 Q4 + createTagRedirect: publicProcedure + .input( + z.object({ + apiKey: z.string(), + tag: z.string(), + link: z.string(), + updateToken: z.string(), + }) + ) + .mutation(async (opts) => { + const { apiKey, tag, link, updateToken } = opts.input; + authorize(apiKey); + + const res = await createTagRedirect(tag, link, updateToken, db); + return { tag: res.tag, link: res.link }; + }), + updateTagRedirect: publicProcedure .input( z.object({ tag: z.string(), link: z.string(), updateToken: z.string() }) ) .mutation(async (opts) => { const { tag, link, updateToken } = opts.input; - return setTagRedirect(tag, link, updateToken, db); + + const res = await setTagRedirect(tag, link, updateToken, db); + return { tag: res.tag, link: res.link }; }), - // @deprecated, remove by 2024 Q4 getTagHistory: publicProcedure .input(z.object({ tag: z.string() })) .query(async (opts) => { const { tag } = opts.input; - return getTagRedirectHist(tag, db); + return await getTagRedirectHist(tag, db); }), // @deprecated, remove by 2024 Q4