This commit is contained in:
parent
1bd8ecba7a
commit
fd939af0b7
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -33,4 +33,6 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
|||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
|
||||
.nano-remix
|
||||
.nano-remix
|
||||
|
||||
local-spike.db
|
||||
|
|
@ -7,10 +7,8 @@
|
|||
1. Install deps with `bun install`.
|
||||
1. Setup a dummy Discord bot and get your DISCORD*TOKEN and CLIENT_ID at https://discord.com/developers/applications *(Corey doesn't remember how to do this all this, so if pair with him if you run into problems. Then document it in here).
|
||||
1. `cp .env.example .env` and fill in the values for everything.
|
||||
1. Find a Discord channel id to use for testing and set it in `.env` as `CHANNEL_ID`.
|
||||
1. There is a very simple auth server you can use to invite the bot to your server. Run it with `bun authServer`.
|
||||
1. Run the bot with `bun bot:discord`
|
||||
|
||||
# Before deploying
|
||||
|
||||
- Add the env var for the gitea token to render
|
||||
1. Find a Discord channel id to use for testing and set it in `.env` as `DISCORD_CLIENT_ID`.
|
||||
1. Run `bun run subdomain:dev` to start the server.
|
||||
1. Visit `localhost:3000/discord/auth` to authorize the bot.
|
||||
1. Use `bun run grok` to start an ngrok tunnel to your local server for webhooks.
|
||||
1. Setup a Gitea webhook to point to your ngrok url at `https://git.nose.space/user/settings/hooks`. **remember the url ends with /gitea/webhook!**
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -8,7 +8,8 @@
|
|||
"bot:discord": "bun run --watch src/discord",
|
||||
"authServer": "bun run --watch src/discord/auth.ts",
|
||||
"subdomain:start": "bun run src/server.tsx",
|
||||
"subdomain:dev": "bun run --hot src/server.tsx"
|
||||
"subdomain:dev": "bun run --hot src/server.tsx",
|
||||
"grok": "ngrok http 3000"
|
||||
},
|
||||
"prettier": {
|
||||
"printWidth": 110,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { client } from "../discord/index"
|
||||
import { insertPR, getPRByGiteaId, insertComment, getCommentByGiteaId } from "../db"
|
||||
import { getConfig } from "../config"
|
||||
import { ChannelType } from "discord.js"
|
||||
import { ChannelType, ThreadChannel, type Channel } from "discord.js"
|
||||
import { assertNever } from "@workshop/shared/utils"
|
||||
|
||||
export const handleGiteaWebhook = async (payload: GiteaWebhook) => {
|
||||
|
|
@ -11,19 +11,20 @@ export const handleGiteaWebhook = async (payload: GiteaWebhook) => {
|
|||
}
|
||||
|
||||
if ("pull_request" in payload) {
|
||||
// TODO: states to handle still "closed" "reopened"
|
||||
if (payload.action === "opened") {
|
||||
await handlePullRequestOpened(payload)
|
||||
} else if (payload.action === "edited") {
|
||||
await handlePullRequestEdited(payload)
|
||||
}
|
||||
console.log(`✅ pull request webhook action: ${payload.action}`)
|
||||
console.log(`✅ pull request webhook action: ${payload.action} #${payload.number}`)
|
||||
} else if ("comment" in payload && "issue" in payload) {
|
||||
if (payload.action === "created") {
|
||||
await handleIssueCommentCreated(payload)
|
||||
} else if (payload.action === "edited") {
|
||||
await handleIssueCommentEdited(payload)
|
||||
}
|
||||
console.log(`✅ comment webhook action: ${payload.action}`)
|
||||
console.log(`✅ comment webhook action: ${payload.action} on #${payload.issue.number}`)
|
||||
} else {
|
||||
assertNever(payload, "Unhandled Gitea webhook payload")
|
||||
}
|
||||
|
|
@ -40,20 +41,21 @@ const handlePullRequestOpened = async (payload: PullRequestWebhook) => {
|
|||
)
|
||||
}
|
||||
|
||||
// Create thread name
|
||||
const authorMention = mapGiteaUserToDiscord(pullRequest.user.login)
|
||||
const threadName = `[${repository.name}] ${pullRequest.title} - ${authorMention}`
|
||||
// Post the PR details as first message in thread
|
||||
const webhook = await getOrCreateWebhook(channel)
|
||||
const prMessage = formatPrMessage(pullRequest, repository.full_name)
|
||||
|
||||
// Create the thread
|
||||
const thread = await channel.threads.create({
|
||||
name: threadName.slice(0, 100), // Discord has 100 char limit on thread names
|
||||
reason: `PR #${pullRequest.number} opened`,
|
||||
const message = await webhook.send({
|
||||
content: prMessage,
|
||||
username: mapGiteaUserToDiscord(pullRequest.user.login),
|
||||
avatarURL: await mapGiteaUserToAvatarURL(pullRequest.user.login),
|
||||
})
|
||||
|
||||
// Post the PR details as first message in thread
|
||||
const prMessage = formatPrMessage(pullRequest)
|
||||
|
||||
const message = await thread.send(prMessage)
|
||||
const threadName = `[${repository.name}] ${pullRequest.title}`
|
||||
const thread = await message.startThread({
|
||||
name: threadName.slice(0, 100),
|
||||
reason: `PR #${pullRequest.number} opened`,
|
||||
})
|
||||
|
||||
insertPR(pullRequest.id, repository.full_name, pullRequest.number, thread.id, message.id)
|
||||
}
|
||||
|
|
@ -77,7 +79,7 @@ const handlePullRequestEdited = async (payload: PullRequestWebhook) => {
|
|||
)
|
||||
}
|
||||
|
||||
const updatedMessage = formatPrMessage(pullRequest)
|
||||
const updatedMessage = formatPrMessage(pullRequest, payload.repository.full_name)
|
||||
await prMessage.edit(updatedMessage)
|
||||
}
|
||||
|
||||
|
|
@ -90,8 +92,6 @@ const handleIssueCommentCreated = async (payload: IssueCommentWebhook) => {
|
|||
// Check if this comment already exists (came from Discord originally)
|
||||
try {
|
||||
getCommentByGiteaId(comment.id)
|
||||
// Comment exists - this came from Discord, skip posting back
|
||||
console.log(`⏭️ Skipping comment ${comment.id} - already exists (came from Discord)`)
|
||||
return
|
||||
} catch {
|
||||
// Comment doesn't exist - this is a new Gitea comment, post to Discord
|
||||
|
|
@ -106,7 +106,13 @@ const handleIssueCommentCreated = async (payload: IssueCommentWebhook) => {
|
|||
)
|
||||
}
|
||||
|
||||
const message = await thread.send(await formatComment(comment))
|
||||
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,
|
||||
})
|
||||
|
||||
insertComment(comment.id, message.id, issue.id)
|
||||
}
|
||||
|
|
@ -130,27 +136,52 @@ const handleIssueCommentEdited = async (payload: IssueCommentWebhook) => {
|
|||
throw new Error(`Discord message ${commentRow.discord_message_id} not found`)
|
||||
}
|
||||
|
||||
await discordMessage.edit(await formatComment(comment))
|
||||
const webhook = await getOrCreateWebhook(thread)
|
||||
const content = await formatComment(comment)
|
||||
await webhook.editMessage(commentRow.discord_message_id, { content })
|
||||
}
|
||||
|
||||
const formatPrMessage = (pullRequest: GiteaPullRequest): string => {
|
||||
const authorMention = mapGiteaUserToDiscord(pullRequest.user.login)
|
||||
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",
|
||||
})
|
||||
}
|
||||
|
||||
return webhook
|
||||
}
|
||||
|
||||
const formatPrMessage = (pullRequest: GiteaPullRequest, repositoryFullName: string): string => {
|
||||
const body = pullRequest.body || "_empty_"
|
||||
return `
|
||||
|
||||
${pullRequest.html_url}
|
||||
**${pullRequest.title}** by __${authorMention}__
|
||||
|
||||
> ${pullRequest.body || "_empty_"}
|
||||
|
||||
> ### [${pullRequest.title}](<${pullRequest.html_url}>)
|
||||
> **${repositoryFullName}**
|
||||
>
|
||||
${body
|
||||
.split("\n")
|
||||
.map((line) => `> ${line}`)
|
||||
.join("\n")}
|
||||
`
|
||||
}
|
||||
|
||||
const formatComment = async (comment: { body: string; user: GiteaUser }): Promise<string> => {
|
||||
const authorName = mapGiteaUserToDiscord(comment.user.login)
|
||||
const messageBody = await convertMentionsToDiscord(comment.body)
|
||||
const formatComment = async (comment: { body: string; user: GiteaUser }) => {
|
||||
return convertMentionsToDiscord(comment.body)
|
||||
}
|
||||
|
||||
return `**${authorName}**: ${messageBody}`
|
||||
const mapGiteaUserToAvatarURL = async (username: string) => {
|
||||
const discordUserName = mapGiteaUserToDiscord(username)
|
||||
const discordUser = await getDiscordUser(discordUserName)
|
||||
|
||||
return discordUser?.avatarURL() || undefined
|
||||
}
|
||||
|
||||
const mapGiteaUserToDiscord = (username: string): string => {
|
||||
|
|
@ -160,6 +191,19 @@ const mapGiteaUserToDiscord = (username: string): string => {
|
|||
return discordUserId || `💥${username}💥`
|
||||
}
|
||||
|
||||
const getDiscordUser = async (discordUsername: string) => {
|
||||
const cachedUser = client.users.cache.find((user) => user.username === discordUsername)
|
||||
if (cachedUser) return cachedUser
|
||||
|
||||
const channelId = getConfig("channelId")
|
||||
const channel = await client.channels.fetch(channelId)
|
||||
if (!channel || !("guild" in channel)) return undefined
|
||||
|
||||
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>()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { serve } from "bun"
|
||||
import { handleGiteaWebhook } from "./gitea/webhook-handler"
|
||||
import "./discord/index" // Initialize Discord bot
|
||||
import "./discord/index" // Make suer the discord client is initialized
|
||||
|
||||
interface ErrorLog {
|
||||
timestamp: string
|
||||
|
|
@ -14,8 +14,9 @@ const errors: ErrorLog[] = []
|
|||
const server = serve({
|
||||
port: parseInt(process.env.PORT || "3000"),
|
||||
routes: {
|
||||
"/": async () => {
|
||||
return new Response("🌵")
|
||||
"/": {
|
||||
GET: () => new Response("🌵"),
|
||||
POST: () => new Response("Use /gitea/webhook for POST requests", { status: 400 }),
|
||||
},
|
||||
"/gitea/webhook": {
|
||||
POST: async (req) => {
|
||||
|
|
@ -46,13 +47,14 @@ const server = serve({
|
|||
},
|
||||
},
|
||||
"/discord/auth": async () => {
|
||||
const permissions = 536870912 // from https://discord.com/developers/applications
|
||||
const authorizeUrl = `https://discord.com/oauth2/authorize?client_id=${process.env.DISCORD_CLIENT_ID}&scope=bot&permissions=${permissions}`
|
||||
|
||||
const html = (
|
||||
<html>
|
||||
<body>
|
||||
<h1>Authenticate spike</h1>
|
||||
<a href="https://discord.com/oauth2/authorize?client_id=${process.env.DISCORD_CLIENT_ID}&scope=bot&permissions=${permissions}">
|
||||
Authorize
|
||||
</a>
|
||||
<a href={authorizeUrl}>Authorize</a>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user