Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions lib/data-retrieval/extract-issue-bounty.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
interface BountySource {
body?: string | null
labels?: Array<string | { name?: string | null }>
}

interface IssueComment {
body?: string | null
user?: {
login?: string | null
} | null
}

function formatUsd(amount: string) {
const normalized = amount.replace(/,/g, "")
const numeric = Number(normalized)

if (!Number.isFinite(numeric)) {
return `$${amount}`
}

return `$${Number.isInteger(numeric) ? numeric : numeric.toFixed(2)}`
}

function getLabelName(label: string | { name?: string | null }) {
return typeof label === "string" ? label : label.name || ""
}

export function extractIssueBounty(
issue: BountySource,
comments: IssueComment[] = [],
) {
const bodyBounty = issue.body?.match(
/(?:^|\n)\s*\/bounty\s+\$?\s*([0-9][0-9,]*(?:\.[0-9]{1,2})?)\s*(?:\$|usd)?\b/i,
)
if (bodyBounty?.[1]) {
return formatUsd(bodyBounty[1])
}

for (const comment of comments) {
const commentAuthor = comment.user?.login
if (commentAuthor !== "algora-pbc" && commentAuthor !== "algora-pbc[bot]") {
continue
}

const algoraBounty = comment.body?.match(
/##\s*.*?\$?([0-9][0-9,]*(?:\.[0-9]{1,2})?)\s+bounty/i,
)
if (algoraBounty?.[1]) {
return formatUsd(algoraBounty[1])
}
}

for (const label of issue.labels || []) {
const labelBounty = getLabelName(label).match(
/^\$([0-9][0-9,]*(?:\.[0-9]{1,2})?)$/,
)
if (labelBounty?.[1]) {
return formatUsd(labelBounty[1])
}
}

return null
}
43 changes: 31 additions & 12 deletions scripts/issue-notifications.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { WebhookClient, type MessageCreateOptions } from "discord.js"
import { type MessageCreateOptions, WebhookClient } from "discord.js"
import { EXCLUDED_BOTS } from "lib/constants"
import { extractIssueBounty } from "lib/data-retrieval/extract-issue-bounty"
import { getRepos } from "lib/data-retrieval/getRepos"
import { octokit } from "lib/sdks"
import { EXCLUDED_BOTS } from "lib/constants"

const discordWebhook = new WebhookClient({
url: process.env.ISSUES_DISCORD_WEBHOOK_URL || "",
Expand All @@ -15,6 +16,9 @@ interface Issue {
login: string
}
created_at: string
body?: string | null
labels?: Array<string | { name?: string | null }>
bounty?: string | null
}

function getUTCDateTime(): string {
Expand Down Expand Up @@ -47,11 +51,27 @@ async function getRecentIssues(repo: string): Promise<Issue[]> {
issue.user.login as (typeof EXCLUDED_BOTS)[number],
),
) as Issue[]

const issuesWithBounties = await Promise.all(
filteredIssues.map(async (issue) => {
const { data: comments } = await octokit.issues.listComments({
owner,
repo: repoName,
issue_number: issue.number,
})

return {
...issue,
bounty: extractIssueBounty(issue, comments),
}
}),
)

console.log(
`[${getUTCDateTime()}] Found ${filteredIssues.length} new issues in ${repo}`,
`[${getUTCDateTime()}] Found ${issuesWithBounties.length} new issues in ${repo}`,
)

return filteredIssues
return issuesWithBounties
}

async function notifyDiscord(issues: Issue[], repo: string) {
Expand All @@ -61,14 +81,13 @@ async function notifyDiscord(issues: Issue[], repo: string) {
`[${getUTCDateTime()}] Sending notification for ${issues.length} issues from ${repo} to Discord`,
)

const messageContent =
`New issues in ${repo}:\n` +
issues
.map(
(issue) =>
`• #${issue.number} ${issue.title} by ${issue.user.login} - <${issue.html_url}>`,
)
.join("\n")
const issueLines = issues
.map((issue) => {
const bounty = issue.bounty ? ` [${issue.bounty} bounty]` : ""
return `- #${issue.number}${bounty} ${issue.title} by ${issue.user.login} - <${issue.html_url}>`
})
.join("\n")
const messageContent = `New issues in ${repo}:\n${issueLines}`

const messageOptions: MessageCreateOptions = {
content: messageContent,
Expand Down
35 changes: 35 additions & 0 deletions tests/extract-issue-bounty.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { describe, expect, it } from "bun:test"
import { extractIssueBounty } from "lib/data-retrieval/extract-issue-bounty"

describe("extractIssueBounty", () => {
it("extracts bounty commands from issue bodies", () => {
expect(extractIssueBounty({ body: "Build this\n\n/bounty $12" })).toBe(
"$12",
)
expect(extractIssueBounty({ body: "/bounty 50$" })).toBe("$50")
})

it("extracts bounty amounts from Algora comments", () => {
expect(
extractIssueBounty({ body: "" }, [
{
user: { login: "algora-pbc" },
body: "## $8 bounty [tscircuit](https://algora.io/tscircuit)",
},
]),
).toBe("$8")
})

it("uses dollar labels as a fallback", () => {
expect(extractIssueBounty({ labels: [{ name: "$10" }] })).toBe("$10")
})

it("ignores non-bounty comments and labels", () => {
expect(
extractIssueBounty(
{ body: "No bounty here", labels: [{ name: "help wanted" }] },
[{ user: { login: "someone" }, body: "## $100 bounty" }],
),
).toBeNull()
})
})
Loading