/** * Chat plugin to view GitHub events in a chatroom. * By Mia, with design / html from xfix's original bot, https://github.com/xfix/GitHub-Bot-Legacy/ * @author mia-pi-git */ import { FS, Utils } from '../../lib'; const STAFF_REPOS = Config.staffrepos || [ 'pokemon-showdown', 'pokemon-showdown-client', 'Pokemon-Showdown-Dex', 'pokemon-showdown-loginserver', ]; const COOLDOWN = 10 * 60 * 1000; export const gitData: GitData = JSON.parse(FS("config/chat-plugins/github.json").readIfExistsSync() || "{}"); interface GitHookHandler { on(event: 'pull_request', callback: (repo: string, ref: string | undefined, result: PullRequest) => void): void; on(event: 'push', callback: (repo: string, ref: string, result: Push) => void): void; on(event: string, callback: (repo: string, ref: string | undefined, result: any) => void): void; listen(): void; server: import('http').Server; } interface Push { commits: Commit[]; sender: { login: string }; compare: string; } interface PullRequest { action: string; number: number; pull_request: { url: string, html_url: string, title: string, user: { login: string, html_url: string, }, merge_commit_sha: string, }; sender: { login: string }; } interface Commit { id: string; message: string; author: { name: string, avatar_url: string }; url: string; } interface GitData { usernames?: { [username: string]: string }; bans?: { [username: string]: string }; } export const GitHub = new class { readonly hook: GitHookHandler | null = null; updates: { [k: string]: number } = Object.create(null); constructor() { // config.github: https://github.com/nlf/node-github-hook#readme if (!Config.github) return; try { this.hook = require('githubhook')({ logger: { log: (line: string) => Monitor.debug(line), error: (line: string) => Monitor.notice(line), }, ...Config.github, }); } catch (err) { Monitor.crashlog(err, "GitHub hook"); } this.listen(); } listen() { if (!this.hook) return; this.hook.listen(); this.hook.on('push', (repo, ref, result) => this.handlePush(repo, ref, result)); this.hook.on('pull_request', (repo, ref, result) => this.handlePull(repo, ref, result)); } private getRepoName(repo: string) { switch (repo) { case 'pokemon-showdown': return 'server'; case 'pokemon-showdown-client': return 'client'; case 'Pokemon-Showdown-Dex': return 'dex'; default: return repo.toLowerCase(); } } handlePush(repo: string, ref: string, result: Push) { const branch = /[^/]+$/.exec(ref)?.[0] || ""; if (branch !== 'master') return; const messages: { [k: string]: string[] } = { staff: [], development: [], }; for (const commit of result.commits) { const { message, url } = commit; const [shortMessage] = message.split('\n\n'); const username = this.getUsername(commit.author.name); const repoName = this.getRepoName(repo); const id = commit.id.substring(0, 6); messages.development.push( Utils.html`[${repoName}] ${id} ${shortMessage} (${username})` ); messages.staff.push(Utils.html`[${repoName}] ${shortMessage} (${username})`); } for (const k in messages) { this.report(k as RoomID, repo, messages[k as RoomID]); } } handlePull(repo: string, ref: string | undefined, result: PullRequest) { if (this.isRateLimited(result.number)) return; if (this.isGitbanned(result)) return; const url = result.pull_request.html_url; const action = this.isValidAction(result.action); if (!action) return; const repoName = this.getRepoName(repo); const userName = this.getUsername(result.sender.login); const title = result.pull_request.title; let buf = Utils.html`[${repoName}] ${userName} `; buf += Utils.html`${action} PR#${result.number}: ${title}`; this.report('development', repo, buf); } report(roomid: RoomID, repo: string, messages: string[] | string) { if (!STAFF_REPOS.includes(repo) && roomid === 'staff') return; if (Array.isArray(messages)) messages = messages.join('
'); Rooms.get(roomid)?.add(`|html|
${messages}
`).update(); } isGitbanned(result: PullRequest) { if (!gitData.bans) return false; return gitData.bans[result.sender.login] || gitData.bans[result.pull_request.user.login]; } isRateLimited(prNumber: number) { if (this.updates[prNumber]) { if (this.updates[prNumber] + COOLDOWN > Date.now()) return true; this.updates[prNumber] = Date.now(); return false; } this.updates[prNumber] = Date.now(); return false; } isValidAction(action: string) { if (action === 'synchronize') return 'updated'; if (action === 'review_requested') { return 'requested a review for'; } else if (action === 'review_request_removed') { return 'removed a review request for'; } if (['ready_for_review', 'labeled', 'unlabeled', 'converted_to_draft'].includes(action)) { return null; } return action; } getUsername(name: string) { return gitData.usernames?.[toID(name)] || name; } save() { FS("config/chat-plugins/github.json").writeUpdate(() => JSON.stringify(gitData)); } }; export const commands: Chat.ChatCommands = { gh: 'github', github: { ''() { return this.parse('/help github'); }, ban(target, room, user) { room = this.requireRoom('development'); this.checkCan('mute', null, room); const [username, reason] = Utils.splitFirst(target, ',').map(u => u.trim()); if (!toID(target)) return this.parse(`/help github`); if (!toID(username)) return this.errorReply("Provide a username."); if (room.auth.has(toID(GitHub.getUsername(username)))) { return this.errorReply("That user is Dev roomauth. If you need to do this, demote them and try again."); } if (!gitData.bans) gitData.bans = {}; if (gitData.bans[toID(username)]) { return this.errorReply(`${username} is already gitbanned.`); } gitData.bans[toID(username)] = reason || " "; // to ensure it's truthy GitHub.save(); this.privateModAction(`${user.name} banned the GitHub user ${username} from having their GitHub actions reported to this server.`); this.modlog('GITHUB BAN', username, reason); }, unban(target, room, user) { room = this.requireRoom('development'); this.checkCan('mute', null, room); target = toID(target); if (!target) return this.parse('/help github'); if (!gitData.bans?.[target]) return this.errorReply("That user is not gitbanned."); delete gitData.bans[target]; if (!Object.keys(gitData.bans).length) delete gitData.bans; GitHub.save(); this.privateModAction(`${user.name} allowed the GitHub user ${target} to have their GitHub actions reported to this server.`); this.modlog('GITHUB UNBAN', target); }, bans() { const room = this.requireRoom('development'); this.checkCan('mute', null, room); return this.parse('/j view-github-bans'); }, setname: 'addusername', addusername(target, room, user) { room = this.requireRoom('development'); this.checkCan('mute', null, room); const [gitName, username] = Utils.splitFirst(target, ',').map(u => u.trim()); if (!toID(gitName) || !toID(username)) return this.parse(`/help github`); if (!gitData.usernames) gitData.usernames = {}; gitData.usernames[toID(gitName)] = username; GitHub.save(); this.privateModAction(`${user.name} set ${gitName}'s name on reported GitHub actions to be ${username}.`); this.modlog('GITHUB SETNAME', null, `'${gitName}' to '${username}'`); }, clearname: 'removeusername', removeusername(target, room, user) { room = this.requireRoom('development'); this.checkCan('mute', null, room); target = toID(target); if (!target) return this.parse(`/help github`); const name = gitData.usernames?.[target]; if (!name) return this.errorReply(`${target} is not a GitHub username on our list.`); delete gitData.usernames?.[target]; if (!Object.keys(gitData.usernames || {}).length) delete gitData.usernames; GitHub.save(); this.privateModAction(`${user.name} removed ${target}'s name from the GitHub username list.`); this.modlog('GITHUB CLEARNAME', target, `from the name ${name}`); }, names() { return this.parse('/j view-github-names'); }, }, githubhelp: [ `/github ban [username], [reason] - Bans a GitHub user from having their GitHub actions reported to Dev room. Requires: % @ # ~`, `/github unban [username] - Unbans a GitHub user from having their GitHub actions reported to Dev room. Requires: % @ # ~`, `/github bans - Lists all GitHub users that are currently gitbanned. Requires: % @ # ~`, `/github setname [username], [name] - Sets a GitHub user's name on reported GitHub actions to be [name]. Requires: % @ # ~`, `/github clearname [username] - Removes a GitHub user's name from the GitHub username list. Requires: % @ # ~`, `/github names - Lists all GitHub usernames that are currently on our list.`, ], }; export const pages: Chat.PageTable = { github: { bans(query, user) { const room = Rooms.get('development'); if (!room) return this.errorReply("No Development room found."); this.checkCan('mute', null, room); if (!gitData.bans) return this.errorReply("There are no gitbans at this time."); let buf = `

Current Gitbans:


    `; for (const [username, reason] of Object.entries(gitData.bans)) { buf += `
  1. ${username} - ${reason.trim() || '(No reason found)'}
  2. `; } buf += `
`; return buf; }, names() { if (!gitData.usernames) return this.errorReply("There are no GitHub usernames in the list."); let buf = `

Current GitHub username mappings:


    `; for (const [username, name] of Object.entries(gitData.usernames)) { buf += `
  1. ${username} - ${name}
  2. `; } buf += `
`; return buf; }, }, }; export function destroy() { GitHub.hook?.server.close(); }