diff --git a/config/formats.ts b/config/formats.ts index 49220cde96b3a..de0f91e8a0122 100644 --- a/config/formats.ts +++ b/config/formats.ts @@ -165,6 +165,23 @@ export const Formats: FormatList = [ 'Lugia', 'Lunala', 'Magearna', 'Marshadow', 'Mewtwo', 'Naganadel', 'Necrozma-Dawn-Wings', 'Necrozma-Dusk-Mane', 'Palkia', 'Pheromosa', 'Rayquaza', 'Reshiram', 'Solgaleo', 'Xerneas', 'Yveltal', 'Zacian', 'Zamazenta', 'Zekrom', 'Zygarde-Base', 'Moody', 'Power Construct', 'Shadow Tag', 'Terrain Extender', 'Baton Pass', + 'Damp Rock', 'Smooth Rock', 'Moody', 'Shadow Tag', 'Baton Pass', + ], + }, + { + name: "[Gen 8] Anything Goes", + threads: [ + `• Anything Goes`, + ], + + mod: 'gen8', + searchShow: false, + ruleset: ['Obtainable', 'Team Preview', 'HP Percentage Mod', 'Cancel Mod', 'Endless Battle Clause'], + }, + { + name: "[Gen 8] ZU", + threads: [ + `• ZU Metagame Discussion`, ], }, { diff --git a/data/abilities.ts b/data/abilities.ts index b3f0f0f9eb032..6397e21bf74ef 100644 --- a/data/abilities.ts +++ b/data/abilities.ts @@ -2816,7 +2816,7 @@ export const Abilities: {[abilityid: string]: AbilityData} = { } }, name: "Quick Draw", - rating: 2.5, + rating: 2, num: 259, }, quickfeet: { diff --git a/lib/utils.ts b/lib/utils.ts index 6ce0cc8d60c6f..7bf6cf35c2c43 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -14,7 +14,8 @@ * - A lot of Chat functions are kind of iffy, but I'm going to say for now * that if it's English-specific, it should be left out of here. */ - +import {Net} from './net'; +import * as crypto from 'crypto'; type Comparable = number | string | boolean | Comparable[] | {reverse: Comparable}; export const Utils = new class Utils { @@ -302,4 +303,68 @@ export const Utils = new class Utils { // Step 7 return d[n][m]; } + async namecolor(name: string): Promise { + const config = Net('https://play.pokemonshowdown.com/config/config.js'); + const customcolors = await config.get().then(res => { + const idx = res.indexOf(' = {'); + res = res.slice(idx + 4); + const colors: {[k: string]: string} = {}; + for (const line of res.split('\n')) { + const [name, base] = line.split(':') + .map(item => item.split('//')[0]) + .map(toID).filter(Boolean); + if (!base || !name) continue; + colors[name] = base; + } + return colors; + }); + const HSLToRGB = (H: number, S: number, L: number) => { + let C = (100 - Math.abs(2 * L - 100)) * S / 100 / 100; + let X = C * (1 - Math.abs((H / 60) % 2 - 1)); + let m = L / 100 - C / 2; + + let R1; + let G1; + let B1; + switch (Math.floor(H / 60)) { + case 1: R1 = X; G1 = C; B1 = 0; break; + case 2: R1 = 0; G1 = C; B1 = X; break; + case 3: R1 = 0; G1 = X; B1 = C; break; + case 4: R1 = X; G1 = 0; B1 = C; break; + case 5: R1 = C; G1 = 0; B1 = X; break; + case 0: default: R1 = C; G1 = X; B1 = 0; break; + } + let R = R1 + m; + let G = G1 + m; + let B = B1 + m; + return {R, G, B}; + } + if (customcolors[name]) return this.namecolor(customcolors[name]); + let hash = crypto.createHash('md5').update(name).digest('hex'); + let H = parseInt(hash.substr(4, 4), 16) % 360; // 0 to 360 + let S = parseInt(hash.substr(0, 4), 16) % 50 + 40; // 40 to 89 + let L = Math.floor(parseInt(hash.substr(8, 4), 16) % 20 + 30); // 30 to 49 + + let {R, G, B} = HSLToRGB(H, S, L); + let lum = R * R * R * 0.2126 + G * G * G * 0.7152 + B * B * B * 0.0722; // 0.013 (dark blue) to 0.737 (yellow) + + let HLmod = (lum - 0.2) * -150; // -80 (yellow) to 28 (dark blue) + if (HLmod > 18) HLmod = (HLmod - 18) * 2.5; + else if (HLmod < 0) HLmod = (HLmod - 0) / 3; + else HLmod = 0; + // let mod = ';border-right: ' + Math.abs(HLmod) + 'px solid ' + (HLmod > 0 ? 'red' : '#0088FF'); + let Hdist = Math.min(Math.abs(180 - H), Math.abs(240 - H)); + if (Hdist < 15) { + HLmod += (15 - Hdist) / 3; + } + + L += HLmod; + + let {R: r, G: g, B: b} = HSLToRGB(H, S, L); + const toHex = (x: number) => { + const hex = Math.round(x * 255).toString(16); + return hex.length === 1 ? '0' + hex : hex; + }; + return `#${toHex(r)}${toHex(g)}${toHex(b)}`; + } }; diff --git a/package.json b/package.json index fd50a53c21dd5..883d929b60bf8 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ }, "optionalDependencies": { "cloud-env": "^0.2.3", + "githubhook": "^1.9.3", + "node-oom-heapdump": "^1.2.0", "node-static": "^0.7.11", "nodemailer": "^6.4.6", "permessage-deflate": "^0.1.7", diff --git a/server/chat-commands/info.ts b/server/chat-commands/info.ts index a35b4115b8f67..b9c0471fa897a 100644 --- a/server/chat-commands/info.ts +++ b/server/chat-commands/info.ts @@ -1623,7 +1623,6 @@ export const commands: ChatCommands = { }, bugreport: 'bugs', - bugreports: 'bugs', bugs(target, room, user) { if (!this.runBroadcast()) return; if (room?.battle) { diff --git a/server/chat-plugins/github.ts b/server/chat-plugins/github.ts new file mode 100644 index 0000000000000..b7e4a63f2c939 --- /dev/null +++ b/server/chat-plugins/github.ts @@ -0,0 +1,192 @@ +/** + * Plugin to notify given rooms / the Development room on Pokemon Showdown + * of github updates. + * Some html / design from xfix's github bot, plugin by Mia. + * @author mia-pi-git + */ +// @ts-ignore old enough module it has no types +import * as githubhook from 'githubhook'; +import {Utils} from '../../lib/utils'; +import {FS} from '../../lib/fs'; + +const cooldown = 15 * 60 * 1000; +const rooms: RoomID[] = Config.github?.rooms ? Config.github.rooms : ['staff', 'upperstaff', 'development']; + +export const github = githubhook({ + port: (Config.github?.port || Config.port + 1), + secret: (Config.github?.secret || ''), + callback: '/server/chat-plugins/github.ts', +}); + +interface GithubData { + names: {[k: string]: string}; + bans: string[]; + repos: {[k: string]: string}; +} + +export let gitData: GithubData = { + bans: ['dependabot-preview[bot]'], + names: {}, + repos: { + 'pokemon-showdown': 'server', + 'pokemon-showdown-client': 'client', + 'Pokemon-Showdown-Dex': 'dex', + }, +}; + +try { + gitData = JSON.parse(FS('config/chat-plugins/github.json').readIfExistsSync()); +} catch (e) {}; + +github.on('push', + (repo: string, ref: string, result: AnyObject) => GithubParser.push(repo, ref, result) +); + +github.on( + 'pull_request', + (repo: string, ref: string, result: AnyObject) => GithubParser.pull(repo, ref, result) +); + +export const GithubParser = new class { + pushes: AnyObject; + constructor() { + this.pushes = {}; + } + shouldShow(id: string, repo: string) { + if (gitData.bans.includes(id)) return false; + if (!this.pushes[repo]) this.pushes[repo] = {}; + if (Date.now() - this.pushes[repo][id] > cooldown) return true; + if (Date.now() - this.pushes[repo][id] < cooldown) this.pushes[repo][id] = Date.now(); + return false; + } + repository(repo: string) { + if (gitData.repos[repo]) return gitData.repos[repo]; + return repo; + } + save() { + FS('config/chat-plugins/github.json').writeUpdate(() => JSON.stringify(gitData)); + } + report(html: string) { + Rooms.global.notifyRooms(rooms, `|c|&|/uhtml github,
${html}
`); + } + push(repo: string, ref: string, result: AnyObject) { + const url = result.compare; + const buffer = result.commits.map((commit: AnyObject) => { + const message = commit.message; + const shortMessage = /.+/.exec(message)![0]; + const id = commit.id.slice(0, 6); + const formattedRepo = `[${this.repository(repo)}]`; + const userName = Utils.html`(${this.getName(result.sender.login)})`; + return `${formattedRepo} ${id} ${shortMessage} ${userName}`; + }).join('
'); + if (!this.pushes[repo]) this.pushes[repo] = {}; + if (!this.shouldShow(result.sender.login, repo)) return; + this.pushes[repo][result.pusher.name] = Date.now(); + this.report(buffer); + } + pull(repo: string, ref: string, result: AnyObject) { + const committer = toID(result.sender.login); + if (gitData.bans.includes(committer)) return; + const num = result.pull_request.number; + const url = result.pull_request.html_url; + const title = result.pull_request.title; + const name = this.getName(result.sender.login); + if (!this.shouldShow(result.sender.login, repo)) return; + let action = result.action; + if (action === 'synchronize') { + action = 'updated'; + } + if (action === 'review_requested') { + action = 'requested a review for'; + } + const blacklisted = ['converted_to_draft', 'ready_for_review', 'converted_to_draft']; + if (blacklisted.includes(action)) { + return; + } + this.report( + `[${repo}] ${name} ` + + `${action} PR#${num}: ${title}` + ); + } + getName(name: string) { + if (gitData.names[name]) return gitData.names[name]; + return name; + } +}; + +export const commands: ChatCommands = { + git: 'github', + gh: 'github', + github: { + ban(target, room, user) { + room = this.requireRoom('development'); + this.checkCan('mute', null, room); + if (gitData.bans.includes(target)) return this.errorReply(`${target} is already gitbanned.`); + gitData.bans.push(target); + GithubParser.save(); + this.privateModAction(`(${user.name} prevented ${target} from being reported by the plugin.)`); + return this.modlog('GITBAN', null, target); + }, + unban(target, room, user) { + room = this.requireRoom('development'); + this.checkCan('mute', null, room); + const index = gitData.bans.indexOf(target); + if (index < 0) return this.errorReply(`${target} is not gitbanned.`); + gitData.bans.splice(index, 1); + GithubParser.save(); + this.privateModAction(`(${user.name} allowed ${target} to be reported by the plugin.)`); + return this.modlog('UNGITBAN', null, target); + }, + bans(target, room, user) { + room = this.requireRoom('development'); + this.checkCan('mute', null, room); + let buf = `IDs banned from being reported by the Github Plugin:
`; + for (const id of gitData.bans) { + buf += `- ${id}
`; + } + return this.sendReplyBox(buf); + }, + setname(target, room, user) { + room = this.requireRoom('development'); + this.checkCan('mute', null, room); + const [oldID, newName] = target.split(','); + if (!target || !oldID || !newName) return this.errorReply(`Specify a GitHub username and new name.`); + if (gitData.names[oldID] === newName) return this.errorReply("That Git ID is already set to that name."); + gitData.names[oldID] = newName; + GithubParser.save(); + this.privateModAction(`(${user.name} set ${oldID}'s name to be ${newName} in the Github Plugin.)`); + this.modlog(`GIT NAME`, null, `"${oldID}" to "${newName}"`); + }, + resetname(target, room, user) { + room = this.requireRoom('development'); + this.checkCan('mute', null, room); + if (!target) return this.errorReply(`Specify a name.`); + if (!gitData.names[target]) return this.errorReply("That Git ID does not have a name set."); + delete gitData.names[target]; + GithubParser.save(); + this.privateModAction(`(${user.name} reset ${target}'s name in the Github Plugin.)`); + return this.modlog(`GIT RESETNAME`, null, `${target}`); + }, + reponame(target, room, user) { + room = this.requireRoom('development'); + this.checkCan('mute', null, room); + const [oldRepo, newName] = Utils.splitFirst(target, ','); + if (!oldRepo || !newName) return this.errorReply("Specify a repo name and a new name to display."); + gitData.repos[oldRepo] = newName; + GithubParser.save(); + this.privateModAction(`(${user.name} set the repo ${oldRepo}'s name in the Github Plugin to ${newName}.)`); + return this.modlog(`GIT REPONAME`, null, `${target}`); + }, + resetreponame(target, room, user) { + room = this.requireRoom('development'); + this.checkCan('mute', null, room); + if (!gitData.repos[target]) return this.errorReply(`${target} does not have a name in the Github Plugin.`); + delete gitData.repos[target]; + GithubParser.save(); + this.privateModAction(`(${user.name} reset the repo ${target}'s name in the Github Plugin.)`); + return this.modlog(`GIT RESETREPONAME`, null, `${target}`); + }, + }, +}; + +github.listen(); diff --git a/server/rooms.ts b/server/rooms.ts index 9040533778fb0..81ddfebfaa590 100644 --- a/server/rooms.ts +++ b/server/rooms.ts @@ -803,10 +803,8 @@ export abstract class BasicRoom { if (user.named) { this.reportJoin('j', user.getIdentityWithStatus(this.roomid), user); } - const staffIntro = this.getStaffIntroMessage(user); if (staffIntro) this.sendUser(user, staffIntro); - this.users[user.id] = user; this.userCount++;