From 4c170b1341041f304d408ef83cba030222ff781f Mon Sep 17 00:00:00 2001 From: Erik Zuuring Date: Tue, 26 Nov 2024 11:53:40 +0200 Subject: [PATCH 1/6] Implement embeds by issueId ### Notes This implements a new feature to embed linear links based off of the `issueId` if it's placed in a message, such as `THESIS-123`. Additionally this adds a function to delete the embed if the original message link is deleted / changed. Still need to implement editing the original embed if it changes. --- discord-scripts/fix-linear-embed.ts | 88 +++++++++++++++++++++++++---- 1 file changed, 77 insertions(+), 11 deletions(-) diff --git a/discord-scripts/fix-linear-embed.ts b/discord-scripts/fix-linear-embed.ts index 3138753a..b0d7b1b6 100644 --- a/discord-scripts/fix-linear-embed.ts +++ b/discord-scripts/fix-linear-embed.ts @@ -13,6 +13,16 @@ const { LINEAR_API_TOKEN } = process.env // track processed message to avoid duplicates if original message is edited const processedMessages = new Map>() +// let us also track sent embeds to delete them if the original message is deleted or edited WIP +const sentEmbeds = new Map() + +const ISSUE_PREFIXES = ["THESIS", "MEZO", "ENG"] +const issueTagRegex = new RegExp( + `\\b(${ISSUE_PREFIXES.join("|")})-\\d+\\b`, + "g", +) +const issueUrlRegex = + /https:\/\/linear\.app\/([a-zA-Z0-9-]+)\/issue\/([a-zA-Z0-9-]+)(?:.*#comment-([a-zA-Z0-9]+))?/g function truncateToWords( content: string | undefined, @@ -132,32 +142,55 @@ async function processLinearEmbeds( logger: Log, linearClient: LinearClient, ) { - const issueUrlRegex = - /https:\/\/linear\.app\/([a-zA-Z0-9-]+)\/issue\/([a-zA-Z0-9-]+)(?:.*#comment-([a-zA-Z0-9]+))?/g - - const matches = Array.from(message.matchAll(issueUrlRegex)) + const urlMatches = Array.from(message.matchAll(issueUrlRegex)) + const issueMatches = Array.from(message.matchAll(issueTagRegex)) - if (matches.length === 0) { + if (urlMatches.length === 0 && issueMatches.length === 0) { return } const processedIssues = processedMessages.get(messageId) || new Set() processedMessages.set(messageId, processedIssues) - const embedPromises = matches.map(async (match) => { + const uniqueMatches = new Set() + + urlMatches.forEach((match) => { const teamName = match[1] const issueId = match[2] const commentId = match[3] || undefined - const uniqueKey = `${issueId}-${commentId}` + const uniqueKey = `${issueId}-${commentId || ""}` + + if (!processedIssues.has(uniqueKey)) { + processedIssues.add(uniqueKey) + uniqueMatches.add(JSON.stringify({ issueId, commentId, teamName })) + } + }) + + issueMatches.forEach((match) => { + const issueId = match[0] - if (processedIssues.has(uniqueKey)) { - return null + if ( + Array.from(uniqueMatches).some( + (uniqueMatch) => JSON.parse(uniqueMatch).issueId === issueId, + ) + ) { + return } - processedIssues.add(uniqueKey) + const uniqueKey = `${issueId}` + if (!processedIssues.has(uniqueKey)) { + processedIssues.add(uniqueKey) + uniqueMatches.add( + JSON.stringify({ issueId, commentId: undefined, teamName: undefined }), + ) + } + }) + + const embedPromises = Array.from(uniqueMatches).map(async (matchString) => { + const { issueId, commentId, teamName } = JSON.parse(matchString) logger.info( - `Processing team: ${teamName}, issue: ${issueId}, comment: ${commentId}`, + `Processing issue: ${issueId}, comment: ${commentId}, team: ${teamName}`, ) const embed = await createLinearEmbed( @@ -169,6 +202,7 @@ async function processLinearEmbeds( return { embed, issueId } }) + const results = await Promise.all(embedPromises) results @@ -180,6 +214,9 @@ async function processLinearEmbeds( if (embed) { channel .send({ embeds: [embed] }) + .then((sentMessage) => { + sentEmbeds.set(messageId, sentMessage) + }) .catch((error) => logger.error( `Failed to send embed for issue ID: ${issueId}: ${error}`, @@ -229,6 +266,23 @@ export default function linearEmbeds(discordClient: Client, robot: Robot) { return } + const matches = + Array.from(newMessage.content.matchAll(issueTagRegex)).length > 0 || + Array.from(newMessage.content.matchAll(issueUrlRegex)).length > 0 + + if (!matches) { + const embedMessage = sentEmbeds.get(newMessage.id) + if (embedMessage) { + await embedMessage.delete().catch((error) => { + robot.logger.error( + `Failed to delete embed for message ID: ${newMessage.id}: ${error}`, + ) + }) + sentEmbeds.delete(newMessage.id) + } + return + } + robot.logger.info( `Processing updated message: ${newMessage.content} (was: ${oldMessage?.content})`, ) @@ -240,4 +294,16 @@ export default function linearEmbeds(discordClient: Client, robot: Robot) { linearClient, ) }) + + discordClient.on("messageDelete", async (message) => { + const embedMessage = sentEmbeds.get(message.id) + if (embedMessage) { + await embedMessage.delete().catch((error) => { + robot.logger.error( + `Failed to delete embed for message ID: ${message.id}: ${error}`, + ) + }) + sentEmbeds.delete(message.id) + } + }) } From d7860d2142bb725512bdddbe9c67120574815ce2 Mon Sep 17 00:00:00 2001 From: Erik Zuuring Date: Wed, 27 Nov 2024 15:46:48 +0200 Subject: [PATCH 2/6] Add support for dynamically pulling in issue tags This will now pull in the issue tags dynamically from the Linear API rather than having to define it in `ISSUE_PREFIXES` --- discord-scripts/fix-linear-embed.ts | 37 +++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/discord-scripts/fix-linear-embed.ts b/discord-scripts/fix-linear-embed.ts index b0d7b1b6..dc498ca6 100644 --- a/discord-scripts/fix-linear-embed.ts +++ b/discord-scripts/fix-linear-embed.ts @@ -16,11 +16,27 @@ const processedMessages = new Map>() // let us also track sent embeds to delete them if the original message is deleted or edited WIP const sentEmbeds = new Map() -const ISSUE_PREFIXES = ["THESIS", "MEZO", "ENG"] -const issueTagRegex = new RegExp( - `\\b(${ISSUE_PREFIXES.join("|")})-\\d+\\b`, - "g", -) +let issueTagRegex: RegExp | null = null + +async function fetchIssuePrefixes(linearClient: LinearClient): Promise { + try { + const teams = await linearClient.teams() + return teams.nodes.map((team) => team.key) + } catch (error) { + console.error("Failed to fetch issue prefixes:", error) + return [] + } +} + +function updateIssueTagRegex(prefixes: string[]) { + issueTagRegex = new RegExp(`\\b(${prefixes.join("|")})-\\d+\\b`, "g") +} + +async function initializeIssueTagRegex(linearClient: LinearClient) { + const prefixes = await fetchIssuePrefixes(linearClient) + updateIssueTagRegex(prefixes) +} + const issueUrlRegex = /https:\/\/linear\.app\/([a-zA-Z0-9-]+)\/issue\/([a-zA-Z0-9-]+)(?:.*#comment-([a-zA-Z0-9]+))?/g @@ -142,6 +158,11 @@ async function processLinearEmbeds( logger: Log, linearClient: LinearClient, ) { + if (!issueTagRegex) { + logger.error("IssueTagRegex is not initialized.") + return + } + const urlMatches = Array.from(message.matchAll(issueUrlRegex)) const issueMatches = Array.from(message.matchAll(issueTagRegex)) @@ -228,9 +249,11 @@ async function processLinearEmbeds( }) } -export default function linearEmbeds(discordClient: Client, robot: Robot) { +export default async function linearEmbeds(discordClient: Client, robot: Robot) { const linearClient = new LinearClient({ apiKey: LINEAR_API_TOKEN }) + await initializeIssueTagRegex(linearClient) + discordClient.on("messageCreate", async (message: Message) => { if ( message.author.bot || @@ -267,7 +290,7 @@ export default function linearEmbeds(discordClient: Client, robot: Robot) { } const matches = - Array.from(newMessage.content.matchAll(issueTagRegex)).length > 0 || + Array.from(newMessage.content.matchAll(issueTagRegex || /./)).length > 0 || Array.from(newMessage.content.matchAll(issueUrlRegex)).length > 0 if (!matches) { From bc6fd55a63bdc38aed4c767e860fef053092d1c0 Mon Sep 17 00:00:00 2001 From: Erik Zuuring Date: Wed, 27 Nov 2024 16:13:01 +0200 Subject: [PATCH 3/6] Prettier fixes --- discord-scripts/fix-linear-embed.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/discord-scripts/fix-linear-embed.ts b/discord-scripts/fix-linear-embed.ts index dc498ca6..cde1c0a8 100644 --- a/discord-scripts/fix-linear-embed.ts +++ b/discord-scripts/fix-linear-embed.ts @@ -18,7 +18,9 @@ const sentEmbeds = new Map() let issueTagRegex: RegExp | null = null -async function fetchIssuePrefixes(linearClient: LinearClient): Promise { +async function fetchIssuePrefixes( + linearClient: LinearClient, +): Promise { try { const teams = await linearClient.teams() return teams.nodes.map((team) => team.key) @@ -249,7 +251,10 @@ async function processLinearEmbeds( }) } -export default async function linearEmbeds(discordClient: Client, robot: Robot) { +export default async function linearEmbeds( + discordClient: Client, + robot: Robot, +) { const linearClient = new LinearClient({ apiKey: LINEAR_API_TOKEN }) await initializeIssueTagRegex(linearClient) @@ -290,8 +295,8 @@ export default async function linearEmbeds(discordClient: Client, robot: Robot) } const matches = - Array.from(newMessage.content.matchAll(issueTagRegex || /./)).length > 0 || - Array.from(newMessage.content.matchAll(issueUrlRegex)).length > 0 + Array.from(newMessage.content.matchAll(issueTagRegex || /./)).length > + 0 || Array.from(newMessage.content.matchAll(issueUrlRegex)).length > 0 if (!matches) { const embedMessage = sentEmbeds.get(newMessage.id) From dd12951dd49a1a9b96ae33e09a39c3a986395b5e Mon Sep 17 00:00:00 2001 From: Erik Zuuring Date: Fri, 29 Nov 2024 11:08:02 +0200 Subject: [PATCH 4/6] Move loggers to debug Let's switch the `robot.logger.info` into `robot.logger.debug` --- discord-scripts/fix-linear-embed.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/discord-scripts/fix-linear-embed.ts b/discord-scripts/fix-linear-embed.ts index cde1c0a8..48699ed3 100644 --- a/discord-scripts/fix-linear-embed.ts +++ b/discord-scripts/fix-linear-embed.ts @@ -212,7 +212,7 @@ async function processLinearEmbeds( const embedPromises = Array.from(uniqueMatches).map(async (matchString) => { const { issueId, commentId, teamName } = JSON.parse(matchString) - logger.info( + logger.debug( `Processing issue: ${issueId}, comment: ${commentId}, team: ${teamName}`, ) @@ -271,7 +271,7 @@ export default async function linearEmbeds( return } - robot.logger.info(`Processing message: ${message.content}`) + robot.logger.debug(`Processing message: ${message.content}`) await processLinearEmbeds( message.content, message.id, @@ -311,7 +311,7 @@ export default async function linearEmbeds( return } - robot.logger.info( + robot.logger.debug( `Processing updated message: ${newMessage.content} (was: ${oldMessage?.content})`, ) await processLinearEmbeds( From 07b77a9ec0ba01daa66fc8df54c38e01e539c564 Mon Sep 17 00:00:00 2001 From: Erik Zuuring Date: Fri, 29 Nov 2024 14:13:28 +0200 Subject: [PATCH 5/6] Working edit embeds This adds support for editing the embeds if the original message is updated with changed issues. Ready to roll out for testing. Adds support to `issueTagRegex` for different cased issues like `iSsUe-111` or `ISSUE-111` --- discord-scripts/fix-linear-embed.ts | 59 +++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/discord-scripts/fix-linear-embed.ts b/discord-scripts/fix-linear-embed.ts index 48699ed3..70fdf448 100644 --- a/discord-scripts/fix-linear-embed.ts +++ b/discord-scripts/fix-linear-embed.ts @@ -31,7 +31,7 @@ async function fetchIssuePrefixes( } function updateIssueTagRegex(prefixes: string[]) { - issueTagRegex = new RegExp(`\\b(${prefixes.join("|")})-\\d+\\b`, "g") + issueTagRegex = new RegExp(`\\b(${prefixes.join("|")})-\\d+\\b`, "gi") } async function initializeIssueTagRegex(linearClient: LinearClient) { @@ -294,12 +294,13 @@ export default async function linearEmbeds( return } - const matches = - Array.from(newMessage.content.matchAll(issueTagRegex || /./)).length > - 0 || Array.from(newMessage.content.matchAll(issueUrlRegex)).length > 0 + const embedMessage = sentEmbeds.get(newMessage.id) + const urlMatches = Array.from(newMessage.content.matchAll(issueUrlRegex)) + const issueMatches = issueTagRegex + ? Array.from(newMessage.content.matchAll(issueTagRegex)) + : [] - if (!matches) { - const embedMessage = sentEmbeds.get(newMessage.id) + if (urlMatches.length === 0 && issueMatches.length === 0) { if (embedMessage) { await embedMessage.delete().catch((error) => { robot.logger.error( @@ -311,16 +312,42 @@ export default async function linearEmbeds( return } - robot.logger.debug( - `Processing updated message: ${newMessage.content} (was: ${oldMessage?.content})`, - ) - await processLinearEmbeds( - newMessage.content, - newMessage.id, - newMessage.channel, - robot.logger, - linearClient, - ) + const match = urlMatches[0] || issueMatches[0] + const teamName = match[1] || undefined + const issueId = match[2] || match[0] + const commentId = urlMatches.length > 0 ? match[3] || undefined : undefined + + if (embedMessage) { + // we will then update the existing embed + try { + const embed = await createLinearEmbed( + linearClient, + issueId, + commentId, + teamName, + ) + if (embed) { + await embedMessage.edit({ embeds: [embed] }) + robot.logger.debug(`Updated embed for message ID: ${newMessage.id}`) + } else { + robot.logger.error( + `Failed to create embed for updated message ID: ${newMessage.id}`, + ) + } + } catch (error) { + robot.logger.error( + `Failed to edit embed for message ID: ${newMessage.id}: ${error}`, + ) + } + } else { + await processLinearEmbeds( + newMessage.content, + newMessage.id, + newMessage.channel as TextChannel | ThreadChannel, + robot.logger, + linearClient, + ) + } }) discordClient.on("messageDelete", async (message) => { From 21788f1735e6b52a7e635776299f13754832af85 Mon Sep 17 00:00:00 2001 From: Erik Zuuring Date: Wed, 4 Dec 2024 12:12:43 +0200 Subject: [PATCH 6/6] Refactor + review fixes This resolves the issues with excessive json calls and fixing up the regex patterns as per review! --- discord-scripts/fix-linear-embed.ts | 91 +++++++++++------------------ 1 file changed, 35 insertions(+), 56 deletions(-) diff --git a/discord-scripts/fix-linear-embed.ts b/discord-scripts/fix-linear-embed.ts index 70fdf448..8b996528 100644 --- a/discord-scripts/fix-linear-embed.ts +++ b/discord-scripts/fix-linear-embed.ts @@ -12,31 +12,19 @@ import { LinearClient } from "@linear/sdk" const { LINEAR_API_TOKEN } = process.env // track processed message to avoid duplicates if original message is edited -const processedMessages = new Map>() +const processedMessages = new Map< + string, + Map +>() + // let us also track sent embeds to delete them if the original message is deleted or edited WIP const sentEmbeds = new Map() let issueTagRegex: RegExp | null = null -async function fetchIssuePrefixes( - linearClient: LinearClient, -): Promise { - try { - const teams = await linearClient.teams() - return teams.nodes.map((team) => team.key) - } catch (error) { - console.error("Failed to fetch issue prefixes:", error) - return [] - } -} - -function updateIssueTagRegex(prefixes: string[]) { - issueTagRegex = new RegExp(`\\b(${prefixes.join("|")})-\\d+\\b`, "gi") -} - -async function initializeIssueTagRegex(linearClient: LinearClient) { - const prefixes = await fetchIssuePrefixes(linearClient) - updateIssueTagRegex(prefixes) +function initializeIssueTagRegex() { + issueTagRegex = + /(?() + const processedIssues = + processedMessages.get(messageId) ?? + new Map< + string, + { issueId: string; commentId?: string; teamName?: string } + >() processedMessages.set(messageId, processedIssues) - const uniqueMatches = new Set() - urlMatches.forEach((match) => { const teamName = match[1] const issueId = match[2] @@ -184,54 +175,43 @@ async function processLinearEmbeds( const uniqueKey = `${issueId}-${commentId || ""}` if (!processedIssues.has(uniqueKey)) { - processedIssues.add(uniqueKey) - uniqueMatches.add(JSON.stringify({ issueId, commentId, teamName })) + processedIssues.set(uniqueKey, { issueId, commentId, teamName }) } }) issueMatches.forEach((match) => { const issueId = match[0] - - if ( - Array.from(uniqueMatches).some( - (uniqueMatch) => JSON.parse(uniqueMatch).issueId === issueId, - ) - ) { - return - } - const uniqueKey = `${issueId}` + if (!processedIssues.has(uniqueKey)) { - processedIssues.add(uniqueKey) - uniqueMatches.add( - JSON.stringify({ issueId, commentId: undefined, teamName: undefined }), - ) + processedIssues.set(uniqueKey, { issueId }) } }) - const embedPromises = Array.from(uniqueMatches).map(async (matchString) => { - const { issueId, commentId, teamName } = JSON.parse(matchString) - - logger.debug( - `Processing issue: ${issueId}, comment: ${commentId}, team: ${teamName}`, - ) - - const embed = await createLinearEmbed( - linearClient, - issueId, - commentId, - teamName, - ) + const embedPromises = Array.from(processedIssues.values()).map( + async ({ issueId, commentId, teamName }) => { + logger.debug( + `Processing issue: ${issueId}, comment: ${commentId ?? "N/A"}, team: ${ + teamName ?? "N/A" + }`, + ) - return { embed, issueId } - }) + const embed = await createLinearEmbed( + linearClient, + issueId, + commentId, + teamName, + ) + return { embed, issueId } + }, + ) const results = await Promise.all(embedPromises) results .filter( (result): result is { embed: EmbedBuilder; issueId: string } => - result !== null, + result.embed !== null, ) .forEach(({ embed, issueId }) => { if (embed) { @@ -256,8 +236,7 @@ export default async function linearEmbeds( robot: Robot, ) { const linearClient = new LinearClient({ apiKey: LINEAR_API_TOKEN }) - - await initializeIssueTagRegex(linearClient) + initializeIssueTagRegex() discordClient.on("messageCreate", async (message: Message) => { if (