Last active
January 18, 2023 04:15
-
-
Save asportnoy/535178e31a52653d2f4d00f9e8e8ea20 to your computer and use it in GitHub Desktop.
Pylon GitHub Link Detector | https://discord.com/channels/530557949098065930/695065184615792710/921903915950276678
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const API_TOKEN = "YOUR_TOKEN_HERE"; // https://github.com/settings/tokens | No scopes required | |
// Map certain keywords to certain usernames | |
// user keywords do not require a prefix | |
// user/repo keywords must have either a prefix or an issue/pr | |
const SAVED_USERS: { [key: string]: string } = { | |
userKeyword: "username", | |
userRepoKeyword: "username/reponame" | |
}; | |
// Do not trigger in these channels (channel IDs) | |
const IGNORED_CHANNELS: string[] = []; | |
// Process GitHub links as well | |
const INCLUDE_GITHUB_LINKS: boolean = true; | |
// Supress embeds on original message | |
const SUPRESS_EMBEDS: boolean = true; | |
// Code begins here | |
type GHIssueType = 'ISSUE' | 'PULL' | 'UNKNOWN'; | |
class GHMatch { | |
public user: string; | |
public repo: string; | |
public description?: string | null; | |
public issue: { | |
number: string; | |
type: GHIssueType; | |
title?: string; | |
author?: string; | |
state?: 'draft' | 'open' | 'closed' | 'notplanned' | 'merged'; | |
created_at?: Date; | |
} | null; | |
constructor({ | |
user, | |
repo, | |
description, | |
issue, | |
}: { | |
user: string; | |
repo: string; | |
description?: string | null; | |
issue: { number: string; type: GHIssueType } | null; | |
}) { | |
this.user = user; | |
this.repo = repo; | |
this.description = description; | |
if (issue) { | |
this.issue = { | |
number: issue.number, | |
type: issue.type, | |
}; | |
} else { | |
this.issue = null; | |
} | |
} | |
public path( | |
issue: GHIssueType | null | undefined = this.issue?.type | |
): string { | |
let str = `${this.user}/${this.repo}`; | |
if (issue && issue !== 'UNKNOWN' && this.issue) | |
str += `/${issue === 'ISSUE' ? 'issues' : 'pull'}/${this.issue.number}`; | |
return str; | |
} | |
public url(issue?: GHIssueType | null | undefined): string { | |
return `https://github.com/${this.path(issue)}`; | |
} | |
public apiUrl(issue?: boolean): string { | |
return `https://api.github.com/repos/${this.path( | |
issue === false ? null : 'ISSUE' | |
)}`; | |
} | |
public imageUrl(issue?: GHIssueType | null | undefined): string { | |
return `https://opengraph.githubassets.com/_/${this.path(issue)}`; | |
} | |
public clone(): this { | |
return Object.assign(Object.create(Object.getPrototypeOf(this)), this); | |
} | |
} | |
const SAVED_USERS_MAP = new Map(Object.entries(SAVED_USERS)); | |
function matchGitHubTags(text: string): GHMatch[] { | |
let matches: GHMatch[] = []; | |
let regex = INCLUDE_GITHUB_LINKS | |
? /(?<=^|\s|\p{P}|<)((?:https?:\/\/)?(?:www\.)?github\.com\/|(?:gh|github|git|g):)?([\w\.-]+)(?:\/([\w\.-]+))?(?:(?:(?:#|\/(?:issues|pull)\/))?(\d+))?(?=$|\s|\p{P}|<|>)/giu | |
: /(?<=^|\s|\p{P}|<)((?:gh|github|git|g):)?([\w\.-]+)(?:\/([\w\.-]+))?(?:(?:(?:#|\/(?:issues|pull)\/))?(\d+))?(?=$|\s|\p{P}|<|>)/giu; | |
while (true) { | |
let match = regex.exec(text); | |
if (!match) break; | |
let [str, prefix, user, repo, issue] = match; | |
if (SAVED_USERS_MAP.has(user.toLowerCase())) { | |
let target = SAVED_USERS_MAP.get(user.toLowerCase())!; | |
if (target.includes('/')) { | |
if (prefix || issue) [user, repo] = target.split('/'); | |
} else { | |
user = target; | |
} | |
} else if (!prefix) { | |
continue; | |
} | |
matches.push( | |
new GHMatch({ | |
user, | |
repo, | |
issue: issue | |
? { | |
number: issue, | |
type: 'UNKNOWN', | |
} | |
: null, | |
}) | |
); | |
} | |
return matches; | |
} | |
async function getGithubInfo(text: string): Promise<GHMatch[]> { | |
let matches = matchGitHubTags(text); | |
let checked = await Promise.all( | |
matches.map(async (m) => { | |
let res = await fetch(m.apiUrl(false), { | |
headers: { | |
authorization: `token ${API_TOKEN}`, | |
}, | |
}).catch((e) => null); | |
if (!res || !res.ok) return null; | |
let json = await res.json(); | |
m.description = json.description; | |
if (m.issue) { | |
let res = await fetch(m.apiUrl(true), { | |
headers: { | |
authorization: `token ${API_TOKEN}`, | |
}, | |
}).catch((e) => null); | |
if (res && res.ok) { | |
let json = await res.json(); | |
console.log(json); | |
m.issue.type = json.pull_request ? 'PULL' : 'ISSUE'; | |
m.issue.title = json.title; | |
m.issue.author = json.user.login; | |
m.issue.state = json.pull_request?.merged_at ? 'merged' : json.state; | |
m.issue.created_at = new Date(json.created_at); | |
if (m.issue.state === 'open' && json.draft) m.issue.state = 'draft'; | |
if (m.issue.state === 'closed' && json.state_reason === 'not_planned') | |
m.issue.state = 'notplanned'; | |
} else { | |
return null; | |
} | |
} | |
return m; | |
}) | |
); | |
return checked.filter((x) => x) as GHMatch[]; | |
} | |
discord.on('MESSAGE_CREATE', async (message) => { | |
if (IGNORED_CHANNELS.includes(message.channelId)) return; | |
let github = await getGithubInfo(message.content); | |
if (github.length == 0) return; | |
let channel = await message.getChannel(); | |
await channel.triggerTypingIndicator(); | |
let interval = setInterval(channel.triggerTypingIndicator, 10 * 1000); | |
let repos = new Map<string, GHMatch[]>(); | |
github.forEach((r) => { | |
let data = repos.get(`${r.user}/${r.repo}`); | |
if (data) { | |
if (!data.some((x) => x.issue?.number === r.issue?.number)) data.push(r); | |
} else { | |
data = [r]; | |
if (r.issue?.number) { | |
let clone = r.clone(); | |
clone.issue = null; | |
data.unshift(r); | |
} | |
} | |
repos.set(`${r.user}/${r.repo}`, data); | |
}); | |
let string = Array.from(repos).map( | |
([name, links]) => | |
`[${name}](${links[0].url(null)})${ | |
links[0].description ? ` - ${links[0].description}` : '' | |
}${links | |
.slice(1) | |
.map( | |
(x) => | |
`\n> ${stateEmoji(x)} [${ | |
x.issue!.type === 'ISSUE' ? 'Issue' : 'Pull Request' | |
} #${x.issue!.number}](${x.url()}) by [${ | |
x.issue!.author | |
}](https://github.com/${x.issue!.author}) - <t:${Math.floor( | |
x.issue!.created_at!.getTime() / 1000 | |
)}:R> ${x.issue!.title}` | |
) | |
.join('')}` | |
); | |
clearInterval(interval); | |
if (github.length > 0) { | |
await message | |
.inlineReply({ | |
embed: new discord.Embed() | |
.setTitle( | |
`<:github:921844736581595186> ${github.length} GitHub link${ | |
github.length === 1 ? '' : 's' | |
} found` | |
) | |
.setColor(0x2f3136) | |
.setDescription(string.join('\n\n')), | |
allowedMentions: { | |
reply: false, | |
}, | |
}) | |
.catch((e) => null); | |
if (SUPRESS_EMBEDS) | |
message | |
.edit({ | |
flags: (message.flags ?? 0) | discord.Message.Flags.SUPPRESS_EMBEDS, | |
}) | |
.catch((e) => null); | |
} | |
}); | |
function stateEmoji(repo: GHMatch): string { | |
switch (repo.issue?.type) { | |
case 'ISSUE': | |
switch (repo.issue.state) { | |
case 'open': | |
return '<:issueopened:921844108413243442>'; | |
case 'closed': | |
return '<:issueclosed:921844226927497296>'; | |
case 'notplanned': | |
return '<:issuenotplanned:999511547053350942>'; | |
} | |
case 'PULL': | |
switch (repo.issue.state) { | |
case 'draft': | |
return '<:pulldraft:999511555144167474>'; | |
case 'open': | |
return '<:pullopened:921844258489630800>'; | |
case 'merged': | |
return '<:pullmerged:921844280174198915>'; | |
case 'closed': | |
return '<:pullclosed:921844269378060319>'; | |
} | |
} | |
return ''; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment