Skip to content

Commit 731a570

Browse files
authored
Add linear embed support (#323)
### Notes This adds a new `fix-linear-embed` script which will take linear links and generate an embed for it. So far this supports two types of embeds: - Linear Issues: Display information on the issue like description, assignee, priority and link - Linear Issue Comments: Display comment text, issue information, link to issue In order to function, this requires an env `LINEAR_API_TOKEN` to be added.
2 parents d55b232 + a706000 commit 731a570

File tree

3 files changed

+221
-0
lines changed

3 files changed

+221
-0
lines changed

discord-scripts/fix-linear-embed.ts

+181
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import { Client, EmbedBuilder, TextChannel } from "discord.js"
2+
import { Log, Robot } from "hubot"
3+
import { LinearClient } from "@linear/sdk"
4+
5+
const { LINEAR_API_TOKEN } = process.env
6+
7+
async function createLinearEmbed(
8+
linearClient: LinearClient,
9+
issueId: string,
10+
commentId?: string,
11+
teamName?: string,
12+
) {
13+
try {
14+
const issue = await linearClient.issue(issueId)
15+
16+
const project = issue.project ? await issue.project : null
17+
const state = issue.state ? await issue.state : null
18+
const assignee = issue.assignee ? await issue.assignee : null
19+
const comments = await issue.comments()
20+
const comment = commentId
21+
? comments.nodes.find((c) => c.id.startsWith(commentId))
22+
: null
23+
24+
const embed = new EmbedBuilder()
25+
26+
if (comment) {
27+
// Comment-focused embed
28+
embed
29+
.setTitle(`Comment on Issue: ${issue.title}`)
30+
.setURL(
31+
`https://linear.app/${teamName}/issue/${issue.identifier}#comment-${commentId}`,
32+
)
33+
.setDescription(comment.body || "No comment body available.")
34+
.addFields(
35+
{
36+
name: "Issue",
37+
value: `${issue.title} (${state?.name || "No status"})`,
38+
inline: false,
39+
},
40+
{
41+
name: "Assignee",
42+
value: assignee?.name.toString() || "Unassigned",
43+
inline: true,
44+
},
45+
{
46+
name: "Priority",
47+
value: issue.priority?.toString() || "None",
48+
inline: true,
49+
},
50+
)
51+
.setFooter({ text: `Project: ${project?.name || "No project"}` })
52+
} else {
53+
// Issue-focused embed
54+
embed
55+
.setTitle(`Issue: ${issue.title}`)
56+
.setURL(`https://linear.app/${teamName}/issue/${issue.identifier}`)
57+
.setDescription(issue.description || "No description available.")
58+
.addFields(
59+
{ name: "Status", value: state?.name || "No status", inline: true },
60+
{
61+
name: "Assignee",
62+
value: assignee?.name.toString() || "Unassigned",
63+
inline: true,
64+
},
65+
{
66+
name: "Priority",
67+
value: issue.priority?.toString() || "None",
68+
inline: true,
69+
},
70+
)
71+
.setFooter({ text: `Project: ${project?.name || "No project"}` })
72+
73+
if (comments.nodes.length > 0) {
74+
embed.addFields({
75+
name: "Recent Comment",
76+
value: comments.nodes[0].body,
77+
})
78+
}
79+
}
80+
81+
if (issue.updatedAt) {
82+
embed.setTimestamp(new Date(issue.updatedAt))
83+
}
84+
85+
return embed
86+
} catch (error) {
87+
console.error("Error creating Linear embed:", error)
88+
return null
89+
}
90+
}
91+
92+
async function processLinearEmbeds(
93+
message: string,
94+
channel: TextChannel,
95+
logger: Log,
96+
linearClient: LinearClient,
97+
) {
98+
const issueUrlRegex =
99+
/https:\/\/linear\.app\/([a-zA-Z0-9-]+)\/issue\/([a-zA-Z0-9-]+)(?:.*#comment-([a-zA-Z0-9]+))?/g
100+
101+
const matches = Array.from(message.matchAll(issueUrlRegex))
102+
103+
if (matches.length === 0) {
104+
logger.info("No Linear issue links found in message.")
105+
return
106+
}
107+
108+
const embedPromises = matches.map(async (match) => {
109+
const teamName = match[1]
110+
const issueId = match[2]
111+
const commentId = match[3] || undefined
112+
113+
logger.info(
114+
`Processing team: ${teamName}, issue: ${issueId}, comment: ${commentId}`,
115+
)
116+
117+
const embed = await createLinearEmbed(
118+
linearClient,
119+
issueId,
120+
commentId,
121+
teamName,
122+
)
123+
124+
return { embed, issueId }
125+
})
126+
127+
const results = await Promise.all(embedPromises)
128+
129+
results.forEach(({ embed, issueId }) => {
130+
if (embed) {
131+
channel
132+
.send({ embeds: [embed] })
133+
.catch((error) =>
134+
logger.error(
135+
`Failed to send embed for issue ID: ${issueId}: ${error}`,
136+
),
137+
)
138+
} else {
139+
logger.error(`Failed to create embed for issue ID: ${issueId}`)
140+
}
141+
})
142+
}
143+
144+
export default function linearEmbeds(discordClient: Client, robot: Robot) {
145+
const linearClient = new LinearClient({ apiKey: LINEAR_API_TOKEN })
146+
147+
discordClient.on("messageCreate", async (message) => {
148+
if (message.author.bot || !(message.channel instanceof TextChannel)) {
149+
return
150+
}
151+
152+
robot.logger.info(`Processing message: ${message.content}`)
153+
await processLinearEmbeds(
154+
message.content,
155+
message.channel,
156+
robot.logger,
157+
linearClient,
158+
)
159+
})
160+
161+
discordClient.on("messageUpdate", async (oldMessage, newMessage) => {
162+
if (
163+
!newMessage.content ||
164+
!newMessage.channel ||
165+
newMessage.author?.bot ||
166+
!(newMessage.channel instanceof TextChannel)
167+
) {
168+
return
169+
}
170+
171+
robot.logger.info(
172+
`Processing updated message: ${newMessage.content} (was: ${oldMessage?.content})`,
173+
)
174+
await processLinearEmbeds(
175+
newMessage.content,
176+
newMessage.channel,
177+
robot.logger,
178+
linearClient,
179+
)
180+
})
181+
}

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"author": "Antonio Salazar Cardozo <[email protected]>",
77
"description": "Heimdall can see and hear your every need, and keeps watch for the onset of Ragnarok",
88
"dependencies": {
9+
"@linear/sdk": "^32.0.0",
910
"@types/cookie": "^0.3.1",
1011
"@types/cookie-parser": "^1.4.3",
1112
"@types/hubot": "^3.3.2",

yarn.lock

+39
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,11 @@
473473
resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.0.tgz#0709e9f4cb252351c609c6e6d8d6779a8d25edff"
474474
integrity sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==
475475

476+
"@graphql-typed-document-node/core@^3.1.0":
477+
version "3.2.0"
478+
resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861"
479+
integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==
480+
476481
"@humanwhocodes/config-array@^0.11.8":
477482
version "0.11.8"
478483
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9"
@@ -748,6 +753,15 @@
748753
"@jridgewell/resolve-uri" "3.1.0"
749754
"@jridgewell/sourcemap-codec" "1.4.14"
750755

756+
"@linear/sdk@^32.0.0":
757+
version "32.0.0"
758+
resolved "https://registry.yarnpkg.com/@linear/sdk/-/sdk-32.0.0.tgz#1882e89321d4572e266cb39463cdd9408b994fe4"
759+
integrity sha512-ZENFrq3JIztYSEmHRmt+HYgfEAqCd/vIVJNDNp3vpedz+0M/FZZSnxe1qTOQToIASvxWbaJtc2M6QT/cIcXRyA==
760+
dependencies:
761+
"@graphql-typed-document-node/core" "^3.1.0"
762+
graphql "^15.4.0"
763+
isomorphic-unfetch "^3.1.0"
764+
751765
"@mapbox/node-pre-gyp@^1.0.0":
752766
version "1.0.10"
753767
resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz#8e6735ccebbb1581e5a7e652244cadc8a844d03c"
@@ -3354,6 +3368,11 @@ graphemer@^1.4.0:
33543368
resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
33553369
integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
33563370

3371+
graphql@^15.4.0:
3372+
version "15.9.0"
3373+
resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.9.0.tgz#4e8ca830cfd30b03d44d3edd9cac2b0690304b53"
3374+
integrity sha512-GCOQdvm7XxV1S4U4CGrsdlEN37245eC8P9zaYCMr6K1BG0IPGy5lUwmJsEOGyl1GD6HXjOtl2keCP9asRBwNvA==
3375+
33573376
har-schema@^2.0.0:
33583377
version "2.0.0"
33593378
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
@@ -3969,6 +3988,14 @@ isexe@^2.0.0:
39693988
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
39703989
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
39713990

3991+
isomorphic-unfetch@^3.1.0:
3992+
version "3.1.0"
3993+
resolved "https://registry.yarnpkg.com/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz#87341d5f4f7b63843d468438128cb087b7c3e98f"
3994+
integrity sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==
3995+
dependencies:
3996+
node-fetch "^2.6.1"
3997+
unfetch "^4.2.0"
3998+
39723999
isstream@~0.1.2:
39734000
version "0.1.2"
39744001
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
@@ -4924,6 +4951,13 @@ nice-try@^1.0.4:
49244951
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
49254952
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
49264953

4954+
node-fetch@^2.6.1:
4955+
version "2.7.0"
4956+
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
4957+
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
4958+
dependencies:
4959+
whatwg-url "^5.0.0"
4960+
49274961
node-fetch@^2.6.7:
49284962
version "2.6.9"
49294963
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6"
@@ -6467,6 +6501,11 @@ undici@^5.20.0:
64676501
dependencies:
64686502
"@fastify/busboy" "^2.0.0"
64696503

6504+
unfetch@^4.2.0:
6505+
version "4.2.0"
6506+
resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be"
6507+
integrity sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==
6508+
64706509
unhomoglyph@^1.0.6:
64716510
version "1.0.6"
64726511
resolved "https://registry.yarnpkg.com/unhomoglyph/-/unhomoglyph-1.0.6.tgz#ea41f926d0fcf598e3b8bb2980c2ddac66b081d3"

0 commit comments

Comments
 (0)