diff --git a/experiments/skin-database/addSkin.ts b/experiments/skin-database/addSkin.ts new file mode 100644 index 00000000..ed9b1798 --- /dev/null +++ b/experiments/skin-database/addSkin.ts @@ -0,0 +1,46 @@ +import * as Skins from "./data/skins"; +import fs from "fs"; +import md5Buffer from "md5"; +import * as S3 from "./s3"; +import Shooter from "./shooter"; +import _temp from "temp"; +import * as Analyser from "./analyser"; + +const temp = _temp.track(); + +// TODO +// Extract the readme +// Extract the emails +// Upload to Internet Archive +// Store the Internet Archive item name +// Construct IA Webamp UR +type Result = + | { md5: string; status: "FOUND" } + | { md5: string; status: "ADDED"; averageColor: string }; + +export async function addSkinFromBuffer( + buffer: Buffer, + filePath: string, + uploader: string +): Promise { + const md5 = md5Buffer(buffer); + const skin = await Skins.getSkinByMd5(md5); + if (skin != null) { + return { md5, status: "FOUND" }; + } + const tempFile = temp.path({ suffix: ".wsz" }); + fs.writeFileSync(tempFile, buffer); + const tempScreenshotPath = temp.path({ suffix: ".png" }); + + await Shooter.withShooter((shooter) => + shooter.takeScreenshot(tempFile, tempScreenshotPath, { + minify: true, + }) + ); + + const averageColor = await Analyser.getColor(tempScreenshotPath); + await S3.putScreenshot(md5, fs.readFileSync(tempScreenshotPath)); + await S3.putSkin(md5, buffer); + await Skins.addSkin({ md5, filePath, uploader, averageColor }); + return { md5, status: "ADDED", averageColor }; +} diff --git a/experiments/skin-database/analyser.ts b/experiments/skin-database/analyser.ts new file mode 100644 index 00000000..7e5ef440 --- /dev/null +++ b/experiments/skin-database/analyser.ts @@ -0,0 +1,18 @@ +// Functions for deriving information from skins + +import { exec } from "child_process"; +import shellescape from "shell-escape"; + +export function getColor(imgPath: string): Promise { + return new Promise((resolve, reject) => { + const excapedImgPath = shellescape([imgPath]); + const command = `convert ${excapedImgPath} -scale 1x1\! -format '%[pixel:u]' info:-`; + exec(command, (error, stdout) => { + if (error !== null) { + reject(error); + return; + } + resolve(stdout.slice(1)); + }); + }); +} diff --git a/experiments/skin-database/discord-bot/commands/archive.js b/experiments/skin-database/discord-bot/commands/archive.js deleted file mode 100644 index c9261bd4..00000000 --- a/experiments/skin-database/discord-bot/commands/archive.js +++ /dev/null @@ -1,98 +0,0 @@ -const fs = require("fs"); -const fetch = require("node-fetch"); -const md5Buffer = require("md5"); -const Skins = require("../../data/skins"); -const Utils = require("../utils"); -const S3 = require("../../s3"); -const Shooter = require("../../shooter"); -const temp = require("temp").track(); - -async function handler(message) { - const { attachments } = message; - if (attachments.length < 1) { - await message.channel.send("Could not archive. No attachment found."); - return; - } - const files = await Promise.all( - attachments.map(async (attachment) => { - const { filename, url } = attachment; - const response = await fetch(url); - console.log("got response"); - const buffer = await response.buffer(); - console.log("got buffer"); - const md5 = md5Buffer(buffer); - console.log("got md5", md5); - return { filename, buffer, md5 }; - }) - ); - - // TODO - // Extract the readme - // Extract the emails - // Extract the average color - // Upload to Internet Archive - // Store the Internet Archive item name - // Construct IA Webamp URL - const shooter = new Shooter("https://webamp.org"); - - try { - for (const file of files) { - const skin = await Skins.getSkinByMd5(file.md5); - if (skin != null) { - await message.channel.send(`This skin is already in our collection.`); - await Utils.postSkin({ - md5: file.md5, - dest: message.channel, - }); - } else { - await message.channel.send( - `Thanks! ${file.filename} is a brand new skin. 👏 Uploading to our server and taking a screenshot...` - ); - const tempFile = temp.path({ suffix: ".wsz" }); - fs.writeFileSync(tempFile, file.buffer); - const tempScreenshotPath = temp.path({ suffix: ".png" }); - - try { - await shooter.takeScreenshot(tempFile, tempScreenshotPath, { - minify: true, - }); - } catch (e) { - console.error(e); - await message.channel.send( - `Something went wrong taking the screenshot of ${file.filename}. Sorry.` - ); - continue; - } - - await S3.putScreenshot(file.md5, fs.readFileSync(tempScreenshotPath)); - await S3.putSkin(file.md5, file.buffer); - await Skins.addSkin({ - md5: file.md5, - filePath: file.filename, - uploader: message.author.username, - }); - await message.channel.send( - `Done! ${file.filename} has been queued for archiving. In the mean time, here's a screenshot and a link to it on Webamp.` - ); - await Utils.postSkin({ - md5: file.md5, - dest: message.channel, - }); - } - } - } catch (e) { - console.error(e); - message.channel.send( - "There was an error archiving your skin. Please ping @captbaritone." - ); - } - shooter.dispose(); -} - -module.exports = { - command: "archive", - usage: "", - description: - "Queues the accompanying uploaded skin for inclusion in the Archive", - handler, -}; diff --git a/experiments/skin-database/discord-bot/commands/archive.ts b/experiments/skin-database/discord-bot/commands/archive.ts new file mode 100644 index 00000000..1c1f8bb3 --- /dev/null +++ b/experiments/skin-database/discord-bot/commands/archive.ts @@ -0,0 +1,55 @@ +import { Message } from "discord.js"; +import fetch from "node-fetch"; +import * as Utils from "../utils"; +import { addSkinFromBuffer } from "../../addSkin"; + +async function handler(message: Message) { + const { attachments } = message; + if (attachments.array().length < 1) { + await message.channel.send("Could not archive. No attachment found."); + return; + } + const files = await Promise.all( + attachments.map(async (attachment) => { + const { filename, url } = attachment; + const response = await fetch(url); + console.log("got response"); + const buffer = await response.buffer(); + console.log("got buffer"); + return { filename, buffer }; + }) + ); + + for (const file of files) { + let result; + try { + result = await addSkinFromBuffer( + file.buffer, + file.filename, + message.author.username + ); + } catch (e) { + console.error(e); + message.channel.send( + "There was an error archiving your skin. Please ping @captbaritone." + ); + throw e; + } + if (result.status === "FOUND") { + await message.channel.send(`This skin is already in our collection.`); + } else if (result.status === "ADDED") { + await message.channel.send( + `Thanks! ${file.filename} is a brand new skin. 👏 It has been queued for archiving. In the mean time, here's a screenshot and a link to it on Webamp.` + ); + } + await Utils.postSkin({ md5: result.md5, dest: message.channel }); + } +} + +module.exports = { + command: "archive", + usage: "", + description: + "Queues the accompanying uploaded skin for inclusion in the Archive", + handler, +}; diff --git a/experiments/skin-database/discord-bot/utils.ts b/experiments/skin-database/discord-bot/utils.ts index 320631ae..9c8b1997 100644 --- a/experiments/skin-database/discord-bot/utils.ts +++ b/experiments/skin-database/discord-bot/utils.ts @@ -1,4 +1,11 @@ -import { RichEmbed, User, MessageReaction, Channel } from "discord.js"; +import { + RichEmbed, + User, + MessageReaction, + TextChannel, + DMChannel, + GroupDMChannel, +} from "discord.js"; import rgbHex from "rgb-hex"; import * as Skins from "../data/skins"; import logger from "../logger"; @@ -17,14 +24,14 @@ const filter = (reaction: MessageReaction): boolean => { ); }; -async function postSkin({ +export async function postSkin({ md5, title: _title, dest, }: { md5: string; - title: (filename: string | null) => string; - dest: Channel; + title?: (filename: string | null) => string; + dest: TextChannel | DMChannel | GroupDMChannel; }) { const skin = await Skins.getSkinByMd5(md5); if (skin == null) { @@ -141,5 +148,3 @@ function getPrettyTwitterStatus(status: TweetStatus): string { return "Tweeted 🐦"; } } - -module.exports = { postSkin }; diff --git a/experiments/skin-database/shooter.js b/experiments/skin-database/shooter.js index 80364fe6..64545fe4 100644 --- a/experiments/skin-database/shooter.js +++ b/experiments/skin-database/shooter.js @@ -15,6 +15,15 @@ class Shooter { this._url = url; } + static async withShooter(cb) { + const shooter = new Shooter("https://webamp.org"); + try { + await cb(shooter); + } finally { + shooter.dispose(); + } + } + async init() { this._browser = await puppeteer.launch(); this._page = await this._browser.newPage();