/**
* 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 += `- ${username} - ${reason.trim() || '(No reason found)'}
`;
}
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 += `- ${username} - ${name}
`;
}
buf += `
`;
return buf;
},
},
};
export function destroy() {
GitHub.hook?.server.close();
}