this is good enough
Some checks are pending
CI / test (push) Waiting to run

This commit is contained in:
Corey Johnson 2025-11-13 10:59:49 -08:00
parent e2feb9cc1a
commit f156e91bcd
6 changed files with 349 additions and 332 deletions

View File

@ -21,13 +21,13 @@ const config = {
dev: "/Users/corey/code/tmp/data",
prod: "/var/data",
},
userMappings: {
giteaToDiscordUserMappings: {
dev: {
probablycorey: "corey",
} as Record<string, string>,
prod: {
probablycorey: "probablycorey",
defunkt: "defunkt.gg",
probablycorey: "corey",
defunkt: "defunkt",
} as Record<string, string>,
},
}

View File

@ -18,21 +18,19 @@ db.run(`
CREATE TABLE IF NOT EXISTS comments (
gitea_comment_id INTEGER PRIMARY KEY,
discord_message_id TEXT NOT NULL,
gitea_pr_id INTEGER NOT NULL,
parent_comment_id INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (gitea_pr_id) REFERENCES prs(gitea_pr_id)
);
CREATE TABLE IF NOT EXISTS participants (
id INTEGER PRIMARY KEY AUTOINCREMENT,
gitea_pr_id INTEGER NOT NULL,
discord_user_id TEXT NOT NULL,
UNIQUE(gitea_pr_id, discord_user_id),
FOREIGN KEY (gitea_pr_id) REFERENCES prs(gitea_pr_id)
discord_thread_id TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`)
// Migration: Add discord_thread_id to existing comments tables
try {
db.run(`ALTER TABLE comments ADD COLUMN discord_thread_id TEXT`)
console.log(`📊 Migration: Added discord_thread_id column to comments table`)
} catch (e) {
// Column already exists, ignore error
}
console.log(`📊 Database initialized at ${dbPath}`)
// PR operations
@ -63,9 +61,10 @@ export const getPRByGiteaId = (giteaPrId: number) => {
export const getPRByDiscordThreadId = (discordThreadId: string) => {
const row = db
.query<{ gitea_pr_id: number; repo: string; pr_number: number }, string>(
`SELECT gitea_pr_id, repo, pr_number FROM prs WHERE discord_thread_id = ?`
)
.query<
{ gitea_pr_id: number; repo: string; pr_number: number; discord_thread_id: string },
string
>(`SELECT gitea_pr_id, repo, pr_number, discord_thread_id FROM prs WHERE discord_thread_id = ?`)
.get(discordThreadId)
// Don't throw - we use this to check if it's a PR thread
@ -76,43 +75,21 @@ export const getPRByDiscordThreadId = (discordThreadId: string) => {
export const insertComment = (
giteaCommentId: number,
discordMessageId: string,
giteaPrId: number,
parentCommentId?: number
discordThreadId: string
) => {
db.run(
`INSERT INTO comments (gitea_comment_id, discord_message_id, gitea_pr_id, parent_comment_id)
VALUES (?, ?, ?, ?)`,
[giteaCommentId, discordMessageId, giteaPrId, parentCommentId || null]
`INSERT INTO comments (gitea_comment_id, discord_message_id, discord_thread_id)
VALUES (?, ?, ?)`,
[giteaCommentId, discordMessageId, discordThreadId]
)
}
export const getCommentByGiteaId = (giteaCommentId: number) => {
const row = db
.query<{ discord_message_id: string }, number>(
`SELECT discord_message_id FROM comments WHERE gitea_comment_id = ?`
.query<{ discord_message_id: string; discord_thread_id: string }, number>(
`SELECT discord_message_id, discord_thread_id FROM comments WHERE gitea_comment_id = ?`
)
.get(giteaCommentId)
if (!row) {
throw new Error(`No database entry found for Gitea comment ID ${giteaCommentId}`)
}
return row
}
// Participant operations
export const addParticipant = (giteaPrId: number, discordUserId: string) => {
db.run(
`INSERT OR IGNORE INTO participants (gitea_pr_id, discord_user_id)
VALUES (?, ?)`,
[giteaPrId, discordUserId]
)
}
export const getParticipants = (giteaPrId: number) => {
return db
.query<{ discord_user_id: string }, number>(
`SELECT discord_user_id FROM participants WHERE gitea_pr_id = ?`
)
.all(giteaPrId)
}

View File

@ -2,7 +2,7 @@ import { ActivityType, type Client } from "discord.js"
import { runCommand } from "./commands"
import { getPRByDiscordThreadId } from "../db"
import { createPRComment } from "../gitea/api"
import { getConfig } from "../config"
import { convertUsername, convertDiscordMentionsToGitea } from "../gitea/helpers"
export const listenForEvents = (client: Client) => {
client.on("interactionCreate", async (interaction) => {
@ -16,7 +16,9 @@ export const listenForEvents = (client: Client) => {
// Check if this is a PR thread
const prData = getPRByDiscordThreadId(msg.channel.id)
if (prData) {
const message = `**${mapDiscordUserToGitea(msg.author.username)}**: ${msg.content}`
const username = convertUsername({ discordUsername: msg.author.displayName })
const messageContent = await convertDiscordMentionsToGitea(msg.content)
const message = `**${username}**: ${messageContent}`
await createPRComment(prData, message, msg.id)
}
@ -51,14 +53,3 @@ export const listenForEvents = (client: Client) => {
console.warn("Discord client warning:", info)
})
}
const mapDiscordUserToGitea = (username: string): string => {
const userMappings = getConfig("userMappings")
for (const [giteaUser, discordUser] of Object.entries(userMappings)) {
if (discordUser === username) {
return giteaUser
}
}
return username
}

View File

@ -14,7 +14,7 @@ type CreateCommentResponse = {
}
export const createPRComment = async (
prData: { gitea_pr_id: number; repo: string; pr_number: number },
prData: { gitea_pr_id: number; repo: string; pr_number: number; discord_thread_id: string },
body: string,
discordMessageId: string
): Promise<CreateCommentResponse> => {
@ -44,7 +44,7 @@ export const createPRComment = async (
const comment = (await response.json()) as CreateCommentResponse
// Store the mapping immediately so webhook handler knows this came from Discord
insertComment(comment.id, discordMessageId, prData.gitea_pr_id)
insertComment(comment.id, discordMessageId, prData.discord_thread_id)
return comment
}

View File

@ -0,0 +1,150 @@
import { client } from "../discord/index"
import { getPRByGiteaId } from "../db"
import { getConfig } from "../config"
import { ChannelType, TextChannel } from "discord.js"
import { assertNever } from "@workshop/shared/utils"
import { fetchPR } from "./api"
import type { Gitea } from "./types"
import { PRHandler } from "./webhook-handler"
export const ensurePRCreatedOnDiscord = async (
pullRequest: Gitea.PullRequest | Gitea.Issue,
repository: Gitea.Repository
) => {
const prNumber = pullRequest.number
const exists = getPRByGiteaId(pullRequest.id)
if (exists) return true
const pr = await fetchPR(repository.full_name, prNumber)
await PRHandler.handleOpened(pr, repository)
return true
}
export const getThread = async (pullRequest: Gitea.PullRequest | Gitea.Issue) => {
const row = getPRByGiteaId(pullRequest.id)
if (!row) {
return
}
const thread = await client.channels.fetch(row.discord_thread_id)
if (!thread) {
return
}
if (!thread.isThread()) {
throw new Error(`Discord channel ${row.discord_thread_id} is not a thread for PR #${pullRequest.number}`)
}
return thread
}
type PullRequestState = "closed" | "merged" | "opened"
const prState = (pullRequest: Gitea.PullRequest): { icon: string; label: string } => {
let state: PullRequestState = "opened"
if (pullRequest.merged) {
state = "merged"
} else if (pullRequest.state === "closed") {
state = "closed"
}
let stateInfo: { icon: string; label: string }
if (state === "merged") {
stateInfo = { icon: "🟣", label: "merged" }
} else if (state === "closed") {
stateInfo = { icon: "🔴", label: "closed" }
} else if (state === "opened") {
stateInfo = { icon: "🟢", label: "opened" }
} else {
assertNever(state, `Unhandled pull request state: ${state}`)
}
return stateInfo
}
export const threadName = (pullRequest: Gitea.PullRequest, repository: Gitea.Repository): string => {
const stateInfo = prState(pullRequest)
const name = `${stateInfo.icon} [${repository.name}] ${pullRequest.title}`
return name.slice(0, 100)
}
export const getWebhook = async () => {
const channel = await getChannel()
const webhooks = await channel.fetchWebhooks()
let webhook = webhooks.find((wh) => wh.owner?.id === client.user?.id)
if (!webhook) {
webhook = await channel.createWebhook({
name: "Gitea Bridge",
reason: "Proxy Gitea comments to Discord",
})
}
return webhook
}
export const getDiscordAvatarUrl = async (discordUsername: string) => {
const discordUser = await getDiscordUser(discordUsername)
return discordUser?.avatarURL() || undefined
}
export const convertUsername = (opts: { discordUsername: string } | { giteaUsername: string }): string => {
const giteaToDiscordUserMappings = getConfig("giteaToDiscordUserMappings")
let convertedUsername: string | undefined
let username: string
if ("giteaUsername" in opts) {
username = opts.giteaUsername
convertedUsername = giteaToDiscordUserMappings[username]
} else {
username = opts.discordUsername
const entries = Object.entries(giteaToDiscordUserMappings)
const result = entries.find(([_, discordUsername]) => discordUsername === username)
if (result) convertedUsername = result[0]
}
return convertedUsername || `💥${username}💥`
}
export const getDiscordUser = async (discordUsername: string) => {
const cachedUser = client.users.cache.find((user) => user.username === discordUsername)
if (cachedUser) return cachedUser
const channel = await getChannel()
const members = await channel.guild.members.fetch({ query: discordUsername, limit: 1 })
const member = members.first()
return member?.user ?? undefined
}
export const convertDiscordMentionsToGitea = async (content: string): Promise<string> => {
const mentionRegex = /<@(\d+)>/g
const mentions = [...content.matchAll(mentionRegex)]
let result = content
for (const match of mentions) {
const userId = match[1]!
const user = await client.users.fetch(userId).catch(() => null)
if (!user) continue
const giteaUsername = convertUsername({ discordUsername: user.displayName })
console.log(`🌭`, { userId, user, giteaUsername })
result = result.replace(match[0], `@${giteaUsername}`)
}
return result
}
let channel: TextChannel | null = null
const getChannel = async (): Promise<TextChannel> => {
if (channel) return channel
const channelId = getConfig("channelId")
const foundChannel = await client.channels.fetch(channelId)
if (foundChannel?.type !== ChannelType.GuildText) {
throw new Error(
`Discord channel ${channelId} (from config.ts) is type ${foundChannel?.type}, expected GuildText (0)`
)
}
channel = foundChannel as TextChannel
return channel
}

View File

@ -1,304 +1,203 @@
import { client } from "../discord/index"
import { insertPR, getPRByGiteaId, insertComment, getCommentByGiteaId } from "../db"
import { getConfig } from "../config"
import { ChannelType, ThreadChannel, type Channel } from "discord.js"
import { assertNever } from "@workshop/shared/utils"
import { fetchPR } from "./api"
import { insertPR, insertComment, getCommentByGiteaId } from "../db"
import {
ensurePRCreatedOnDiscord as ensureThreadExists,
getDiscordUser,
getWebhook,
getThread,
getDiscordAvatarUrl,
convertUsername,
threadName,
} from "./helpers"
import type { Gitea } from "./types"
import { getConfig } from "../config"
export const handleGiteaWebhook = async (payload: Gitea.Webhook) => {
const isDev = process.env.NODE_ENV !== "production"
if (!isDev && payload.repository.name === "ignore-me") return
if ("pull_request" in payload) {
const { pull_request: pullRequest, repository } = payload
if (PRHandler.canHandle(payload)) {
await PRHandler.handle(payload)
} else if (CommentHandler.canHandle(payload)) {
await CommentHandler.handle(payload)
}
}
if (payload.action === "opened") {
await handlePullRequestOpened(pullRequest, repository)
} else if (payload.action === "edited") {
await ensurePRCreatedOnDiscord(pullRequest, repository)
await handlePullRequestEdited(pullRequest, repository)
} else if (payload.action === "closed" || payload.action === "reopened") {
await ensurePRCreatedOnDiscord(pullRequest, repository)
await handlePullRequestStateChange(pullRequest, repository)
export class PRHandler {
static canHandle(payload: Gitea.Webhook): payload is Gitea.PullRequestWebhook {
return "pull_request" in payload
}
static async handle(payload: Gitea.PullRequestWebhook) {
const { pull_request: pullRequest, repository, action } = payload
if (action === "opened") {
await this.handleOpened(pullRequest, repository)
} else if (action === "edited") {
await this.handleEdited(pullRequest, repository)
} else if (action === "closed" || action === "reopened") {
await this.handleStateChange(pullRequest, repository)
}
console.log(`✅ pull request webhook action: ${payload.action} #${payload.number}`)
} else if ("comment" in payload && "issue" in payload) {
const { issue, comment, repository } = payload
console.log(`✅ pull request webhook action: ${action} #${payload.number}`)
}
static async handleOpened(pullRequest: Gitea.PullRequest, repository: Gitea.Repository) {
// Post the PR details as first message in thread
const webhook = await getWebhook()
const prMessage = this.formatPrBody(pullRequest, repository.full_name)
const discordUsername = convertUsername({ giteaUsername: pullRequest.user.login })
const message = await webhook.send({
content: prMessage,
username: discordUsername,
avatarURL: await getDiscordAvatarUrl(discordUsername),
})
const thread = await message.startThread({
name: threadName(pullRequest, repository),
reason: `PR #${pullRequest.number} opened`,
})
insertPR(pullRequest.id, repository.full_name, pullRequest.number, thread.id, message.id)
}
static async handleEdited(pullRequest: Gitea.PullRequest, repository: Gitea.Repository) {
const thread = await getThread(pullRequest)
if (!thread) {
this.handleOpened(pullRequest, repository)
return
}
const prBody = await thread.fetchStarterMessage()
if (!prBody) {
throw new Error(`Could not fetch PR body for #${pullRequest.number}`)
}
const updatedMessage = this.formatPrBody(pullRequest, repository.full_name)
thread.setName(threadName(pullRequest, repository))
const webhook = await getWebhook()
await webhook.editMessage(prBody, { content: updatedMessage })
}
static async handleStateChange(pullRequest: Gitea.PullRequest, repository: Gitea.Repository) {
const thread = await getThread(pullRequest)
if (!thread) {
this.handleOpened(pullRequest, repository)
return
}
await thread.setName(threadName(pullRequest, repository))
}
static formatPrBody(pullRequest: Gitea.PullRequest, repositoryFullName: string): string {
const body = pullRequest.body || "_empty_"
return `
> ### [${pullRequest.title}](<${pullRequest.html_url}>)
> **${repositoryFullName}**
>
${body
.split("\n")
.map((line) => `> ${line}`)
.join("\n")}
`
}
}
class CommentHandler {
static canHandle(payload: Gitea.Webhook): payload is Gitea.IssueCommentWebhook {
return "comment" in payload && "issue" in payload
}
static async handle(payload: Gitea.IssueCommentWebhook) {
const { issue, comment, repository, action } = payload
if (!issue.pull_request) return
if (payload.action === "created") {
await ensurePRCreatedOnDiscord(issue, repository)
await handleIssueCommentCreated(issue, comment)
} else if (payload.action === "edited") {
await ensurePRCreatedOnDiscord(issue, repository)
await handleIssueCommentEdited(issue, comment)
} else if (payload.action === "deleted") {
await ensurePRCreatedOnDiscord(issue, repository)
await handleIssueDeleted(issue, comment)
if (action === "created") {
await ensureThreadExists(issue, repository)
await this.handleCreated(issue, comment)
} else if (action === "edited") {
await ensureThreadExists(issue, repository)
await this.handleEdited(issue, comment)
} else if (action === "deleted") {
await ensureThreadExists(issue, repository)
await this.handleDeleted(comment)
}
console.log(`✅ comment webhook action: ${payload.action} on #${payload.issue.number}`)
} else {
// console.log(`🙈 Unhandled Gitea webhook payload`, { payload: JSON.stringify(payload, null, 2) })
}
}
const channelId = getConfig("channelId")
const channel = await client.channels.fetch(channelId)
if (channel?.type !== ChannelType.GuildText) {
throw new Error(
`Discord channel ${channelId} (from config.ts) is type ${channel?.type}, expected GuildText (0)`
)
}
const handlePullRequestOpened = async (pullRequest: Gitea.PullRequest, repository: Gitea.Repository) => {
// Post the PR details as first message in thread
const webhook = await getOrCreateWebhook(channel)
const prMessage = formatPrBody(pullRequest, repository.full_name)
const message = await webhook.send({
content: prMessage,
username: mapGiteaUserToDiscord(pullRequest.user.login),
avatarURL: await mapGiteaUserToAvatarURL(pullRequest.user.login),
})
const thread = await message.startThread({
name: threadName(pullRequest, repository),
reason: `PR #${pullRequest.number} opened`,
})
insertPR(pullRequest.id, repository.full_name, pullRequest.number, thread.id, message.id)
}
const handlePullRequestEdited = async (pullRequest: Gitea.PullRequest, repository: Gitea.Repository) => {
const thread = await getThread(pullRequest)
const prBody = await thread.fetchStarterMessage()
if (!prBody) {
throw new Error(`Could not fetch PR body for #${pullRequest.number}`)
console.log(`✅ comment webhook action: ${action} on #${issue.number}`)
}
const updatedMessage = formatPrBody(pullRequest, repository.full_name)
console.log(`🌭`, { title: pullRequest.title })
thread.setName(threadName(pullRequest, repository))
const webhook = await getOrCreateWebhook(thread)
await webhook.editMessage(prBody, { content: updatedMessage })
}
static async handleCreated(issue: Gitea.Issue, comment: Gitea.Comment) {
const commentRow = getCommentByGiteaId(comment.id)
if (commentRow) {
return // Comment already exists, skip
}
const handleIssueCommentCreated = async (issue: Gitea.Issue, comment: Gitea.Comment) => {
// Check if this comment already exists (came from Discord originally)
try {
getCommentByGiteaId(comment.id)
return
} catch {
// Comment doesn't exist - this is a new Gitea comment, post to Discord
const discordUsername = convertUsername({ giteaUsername: comment.user.login })
const thread = await getThread(issue)
if (!thread) {
throw new Error(`No thread found for PR #${issue.number} when handling new comment`)
}
const webhook = await getWebhook()
const message = await webhook.send({
content: await this.formatComment(comment.body),
username: discordUsername,
avatarURL: await getDiscordAvatarUrl(discordUsername),
threadId: thread.id,
})
insertComment(comment.id, message.id, thread.id)
}
const thread = await getThread(issue)
const webhook = await getOrCreateWebhook(thread)
const message = await webhook.send({
content: await formatComment(comment),
username: mapGiteaUserToDiscord(comment.user.login),
avatarURL: await mapGiteaUserToAvatarURL(comment.user.login),
threadId: thread.id,
})
static async handleEdited(issue: Gitea.Issue, comment: Gitea.Comment) {
const commentRow = getCommentByGiteaId(comment.id)
if (!commentRow) {
this.handleCreated(issue, comment)
return
}
insertComment(comment.id, message.id, issue.id)
}
const handleIssueCommentEdited = async (issue: Gitea.Issue, comment: Gitea.Comment) => {
const commentRow = getCommentByGiteaId(comment.id)
const thread = await getThread(issue)
const discordMessage = await thread.messages.fetch(commentRow.discord_message_id)
if (!discordMessage) {
throw new Error(`Discord message ${commentRow.discord_message_id} not found`)
}
const webhook = await getOrCreateWebhook(thread)
const content = await formatComment(comment)
await webhook.editMessage(commentRow.discord_message_id, { content, threadId: thread.id })
}
const handleIssueDeleted = async (issue: Gitea.Issue, comment: Gitea.Comment) => {
const commentRow = getCommentByGiteaId(comment.id)
const thread = await getThread(issue)
const discordMessage = await thread.messages.fetch(commentRow.discord_message_id)
if (!discordMessage) {
throw new Error(`Discord message ${commentRow.discord_message_id} not found`)
}
const webhook = await getOrCreateWebhook(thread)
await webhook.editMessage(commentRow.discord_message_id, { content: `[DELETED]`, threadId: thread.id })
}
const ensurePRCreatedOnDiscord = async (
pullRequest: Gitea.PullRequest | Gitea.Issue,
repository: Gitea.Repository
) => {
const prNumber = pullRequest.number
const exists = getPRByGiteaId(pullRequest.id)
if (exists) return true
const pr = await fetchPR(repository.full_name, prNumber)
await handlePullRequestOpened(pr, repository)
return true
}
const getThread = async (pullRequest: Gitea.PullRequest | Gitea.Issue) => {
const row = getPRByGiteaId(pullRequest.id)
if (!row) {
throw new Error(`No PR found in database for Gitea PR ID ${pullRequest.id}`)
}
const thread = await client.channels.fetch(row.discord_thread_id)
if (!thread?.isThread()) {
throw new Error(`Discord channel ${row.discord_thread_id} is not a thread for PR #${pullRequest.number}`)
}
return thread
}
const prState = (pullRequest: Gitea.PullRequest): { icon: string; label: string } => {
let state: PullRequestState = "opened"
if (pullRequest.merged) {
state = "merged"
} else if (pullRequest.state === "closed") {
state = "closed"
}
console.log(`🌭 `, { state, merged: pullRequest.merged })
let stateInfo: { icon: string; label: string }
if (state === "merged") {
stateInfo = { icon: "🟣", label: "merged" }
} else if (state === "closed") {
stateInfo = { icon: "🔴", label: "closed" }
} else if (state === "opened") {
stateInfo = { icon: "🟢", label: "opened" }
} else {
assertNever(state, `Unhandled pull request state: ${state}`)
}
return stateInfo
}
const threadName = (pullRequest: Gitea.PullRequest, repository: Gitea.Repository): string => {
const stateInfo = prState(pullRequest)
const name = `${stateInfo.icon} [${repository.name}] ${pullRequest.title}`
return name.slice(0, 100)
}
type PullRequestState = "closed" | "merged" | "opened"
const handlePullRequestStateChange = async (pullRequest: Gitea.PullRequest, repository: Gitea.Repository) => {
const thread = await getThread(pullRequest)
await thread.setName(threadName(pullRequest, repository))
}
const getOrCreateWebhook = async (threadOrChannel: ThreadChannel | Channel) => {
const channel = threadOrChannel.isThread() ? threadOrChannel.parent : threadOrChannel
if (!channel || channel.type !== ChannelType.GuildText) {
throw new Error(`Channel ${threadOrChannel.id} is not a text channel`)
}
const webhooks = await channel.fetchWebhooks()
let webhook = webhooks.find((wh) => wh.owner?.id === client.user?.id)
if (!webhook) {
webhook = await channel.createWebhook({
name: "Gitea Bridge",
reason: "Proxy Gitea comments to Discord",
const webhook = await getWebhook()
const content = await this.formatComment(comment.body)
await webhook.editMessage(commentRow.discord_message_id, {
content,
threadId: commentRow.discord_thread_id,
})
}
return webhook
}
static async handleDeleted(comment: Gitea.Comment) {
const commentRow = getCommentByGiteaId(comment.id)
if (!commentRow) {
return // Comment doesn't exist, skip
}
const formatPrBody = (pullRequest: Gitea.PullRequest, repositoryFullName: string): string => {
const body = pullRequest.body || "_empty_"
return `
> ### [${pullRequest.title}](<${pullRequest.html_url}>)
> **${repositoryFullName}**
>
${body
.split("\n")
.map((line) => `> ${line}`)
.join("\n")}
`
}
const formatComment = async (comment: { body: string; user: Gitea.User }) => {
return convertMentionsToDiscord(comment.body)
}
const mapGiteaUserToAvatarURL = async (username: string) => {
const discordUserName = mapGiteaUserToDiscord(username)
const discordUser = await getDiscordUser(discordUserName)
return discordUser?.avatarURL() || undefined
}
const mapGiteaUserToDiscord = (username: string): string => {
const userMappings = getConfig("userMappings")
const discordUserId = userMappings[username]
return discordUserId || `💥${username}💥`
}
const getDiscordUser = async (discordUsername: string) => {
const cachedUser = client.users.cache.find((user) => user.username === discordUsername)
if (cachedUser) return cachedUser
const members = await channel.guild.members.fetch({ query: discordUsername, limit: 1 })
const member = members.first()
return member?.user ?? undefined
}
// Cache Discord username -> user ID
const discordUserIdCache = new Map<string, string>()
const getDiscordUserId = async (discordUsername: string): Promise<string | null> => {
// Check cache first
if (discordUserIdCache.has(discordUsername)) {
return discordUserIdCache.get(discordUsername)!
const webhook = await getWebhook()
await webhook.editMessage(commentRow.discord_message_id, {
content: `[DELETED]`,
threadId: commentRow.discord_thread_id,
})
}
// Fetch from Discord
const members = await channel.guild.members.fetch({ query: discordUsername, limit: 1 })
const member = members.first()
static async formatComment(comment: string) {
const giteaToDiscordUserMappings = getConfig("giteaToDiscordUserMappings")
if (member) {
discordUserIdCache.set(discordUsername, member.user.id)
return member.user.id
}
// Find all @username mentions
const mentionRegex = /@(\w+)/g
const mentions = [...comment.matchAll(mentionRegex)]
return null
}
let result = comment
for (const match of mentions) {
const giteaUsername = match[1]!
const discordUsername = giteaToDiscordUserMappings[giteaUsername]
const convertMentionsToDiscord = async (text: string): Promise<string> => {
const userMappings = getConfig("userMappings")
// Find all @username mentions
const mentionRegex = /@(\w+)/g
const mentions = [...text.matchAll(mentionRegex)]
let result = text
for (const match of mentions) {
const giteaUsername = match[1]!
const discordUsername = userMappings[giteaUsername]
if (discordUsername) {
const discordId = await getDiscordUserId(discordUsername)
if (discordId) {
result = result.replace(match[0], `<@${discordId}>`)
if (discordUsername) {
const discordUser = await getDiscordUser(discordUsername)
if (discordUser) {
result = result.replace(match[0], `<@${discordUser.id}>`)
}
}
}
}
return result
return result
}
}