diff --git a/packages/skin-database/addSkin.ts b/packages/skin-database/addSkin.ts index d63faba9..fb91ab36 100644 --- a/packages/skin-database/addSkin.ts +++ b/packages/skin-database/addSkin.ts @@ -5,6 +5,19 @@ import * as S3 from "./s3"; import Shooter from "./shooter"; import _temp from "temp"; import * as Analyser from "./analyser"; +import { SkinType } from "./types"; +import JSZip from "jszip"; + +export async function getSkinType(file: Buffer): Promise { + const zip = await JSZip.loadAsync(file); + if (zip.file(/main\.bmp$/i).length > 0) { + return "CLASSIC"; + } + if (zip.file(/skin\.xml$/i).length > 0) { + return "MODERN"; + } + throw new Error("Not a skin"); +} // TODO Move this into the function so that we clean up on each run? const temp = _temp.track(); @@ -17,7 +30,7 @@ const temp = _temp.track(); // Construct IA Webamp UR type Result = | { md5: string; status: "FOUND" } - | { md5: string; status: "ADDED"; averageColor: string }; + | { md5: string; status: "ADDED"; averageColor?: string; skinType: SkinType }; export async function addSkinFromBuffer( buffer: Buffer, @@ -29,6 +42,39 @@ export async function addSkinFromBuffer( if (exists) { return { md5, status: "FOUND" }; } + + // Note: This will thrown on invalid skins. + const skinType = await getSkinType(buffer); + + switch (skinType) { + case "CLASSIC": + return addClassicSkinFromBuffer(buffer, md5, filePath, uploader); + case "MODERN": + return addModernSkinFromBuffer(buffer, md5, filePath, uploader); + } +} + +async function addModernSkinFromBuffer( + buffer: Buffer, + md5: string, + filePath: string, + uploader: string +): Promise { + const tempFile = temp.path({ suffix: ".wal" }); + fs.writeFileSync(tempFile, buffer); + await S3.putSkin(md5, buffer, "wal"); + + await Skins.addSkin({ md5, filePath, uploader, modern: true }); + + return { md5, status: "ADDED", skinType: "MODERN" }; +} + +async function addClassicSkinFromBuffer( + buffer: Buffer, + md5: string, + filePath: string, + uploader: string +): Promise { const tempFile = temp.path({ suffix: ".wsz" }); fs.writeFileSync(tempFile, buffer); const tempScreenshotPath = temp.path({ suffix: ".png" }); @@ -41,7 +87,9 @@ export async function addSkinFromBuffer( 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 }; + await S3.putSkin(md5, buffer, "wsz"); + await Skins.addSkin({ md5, filePath, uploader, averageColor, modern: false }); + + await Skins.updateSearchIndex(md5); + return { md5, status: "ADDED", averageColor, skinType: "CLASSIC" }; } diff --git a/packages/skin-database/data/skins.ts b/packages/skin-database/data/skins.ts index 0f8c4a78..0068ff2a 100644 --- a/packages/skin-database/data/skins.ts +++ b/packages/skin-database/data/skins.ts @@ -56,10 +56,26 @@ function getWebampUrl(md5: string): string { return `https://webamp.org?skinUrl=${getSkinUrl(md5)}`; } -export async function addSkin({ md5, filePath, uploader, averageColor }) { +function getMuseumUrl(md5: string): string { + return `https://skins.webamp.org/skin/${md5}`; +} + +export async function addSkin({ + md5, + filePath, + uploader, + averageColor, + modern, +}: { + md5: string; + filePath: string; + uploader: string; + averageColor?: string; + modern: boolean; +}) { skins_CONVERTED.insert({ md5, - type: "CLASSIC", + type: modern ? "MODERN" : "CLASSIC", filePaths: [filePath], uploader, averageColor, @@ -67,7 +83,7 @@ export async function addSkin({ md5, filePath, uploader, averageColor }) { await knex("skins").insert( { md5, - skin_type: SKIN_TYPE.CLASSIC, + skin_type: modern ? SKIN_TYPE.MODERN : SKIN_TYPE.CLASSIC, average_color: averageColor, }, [] @@ -114,6 +130,7 @@ export async function skinExists(md5: string): Promise { } // TODO: SQLITE +// TODO: Handle modern skins export async function getSkinByMd5_DEPRECATED( md5: string ): Promise { @@ -147,6 +164,7 @@ export async function getSkinByMd5_DEPRECATED( imageHash: _skin.imageHash, nsfwPredictions: _skin.nsfwPredictions, approved: _skin.approved, + rejected: _skin.rejected, averageColor: _skin.averageColor, emails: _skin.emails, tweetStatus, @@ -155,6 +173,7 @@ export async function getSkinByMd5_DEPRECATED( fileNames: getFilenames(_skin), canonicalFilename: getCanonicalFilename(_skin), webampUrl: getWebampUrl(_skin.md5), + museumUrl: getMuseumUrl(_skin.md5), internetArchiveItemName, internetArchiveUrl, }; @@ -495,22 +514,31 @@ export async function getMuseumPageSql({ > { const skins = await knex.raw( ` -SELECT skins.md5, - skins.average_color, - skin_reviews.review = 'NSFW' AS nsfw, - skin_reviews.review = 'APPROVED' AS approved, - skin_reviews.review = 'REJECTED' AS rejected, - tweets.likes + SELECT skins.md5, + skins.average_color, + skin_reviews.review = 'NSFW' AS nsfw, + skin_reviews.review = 'APPROVED' AS approved, + skin_reviews.review = 'REJECTED' AS rejected, + tweets.likes, + CASE skins.md5 + WHEN "5e4f10275dcb1fb211d4a8b4f1bda236" THEN 0 -- Base + WHEN "cd251187a5e6ff54ce938d26f1f2de02" THEN 1 -- Winamp3 Classified + WHEN "b0fb83cc20af3abe264291bb17fb2a13" THEN 2 -- Winamp5 Classified + WHEN "d6010aa35bed659bc1311820daa4b341" THEN 3 -- Bento Classified + ELSE 1000 + END priority FROM skins - LEFT JOIN tweets - ON tweets.skin_md5 = skins.md5 - LEFT JOIN skin_reviews - ON skin_reviews.skin_md5 = skins.md5 + LEFT JOIN tweets + ON tweets.skin_md5 = skins.md5 + LEFT JOIN skin_reviews + ON skin_reviews.skin_md5 = skins.md5 WHERE skin_type = 1 -ORDER BY tweets.likes DESC, - nsfw ASC, - approved DESC, - rejected ASC +ORDER BY + priority ASC, + tweets.likes DESC, + nsfw ASC, + approved DESC, + rejected ASC LIMIT ? offset ?`, [first, offset] ); diff --git a/packages/skin-database/discord-bot/utils.ts b/packages/skin-database/discord-bot/utils.ts index 314d4570..f6dada09 100644 --- a/packages/skin-database/discord-bot/utils.ts +++ b/packages/skin-database/discord-bot/utils.ts @@ -44,7 +44,6 @@ export async function postSkin({ canonicalFilename, screenshotUrl, skinUrl, - webampUrl, averageColor, emails, tweetUrl, @@ -54,12 +53,13 @@ export async function postSkin({ internetArchiveItemName, readmeText, nsfw, + museumUrl, } = skin; const title = _title ? _title(canonicalFilename) : canonicalFilename; const embed = new RichEmbed() .setTitle(title) - .addField("Try Online", `[webamp.org](${webampUrl})`, true) + .addField("Try Online", `[skins.webamp.org](${museumUrl})`, true) .addField("Download", `[${canonicalFilename}](${skinUrl})`, true) .addField("Md5", md5, true); @@ -121,7 +121,7 @@ export async function postSkin({ // @ts-ignore WAT? const msg = await dest.send(embed); - if (tweetStatus === "TWEETED") { + if (tweetStatus !== "UNREVIEWED") { return; } @@ -183,3 +183,32 @@ function getPrettyTwitterStatus(status: TweetStatus): string { return "Tweeted 🐦"; } } + +export async function sendAlreadyReviewed({ + md5, + dest, +}: { + md5: string; + dest: TextChannel | DMChannel | GroupDMChannel; +}) { + const skin = await Skins.getSkinByMd5_DEPRECATED(md5); + if (skin == null) { + console.warn("Could not find skin for md5", { md5, alert: true }); + logger.warn("Could not find skin for md5", { md5, alert: true }); + return; + } + const { canonicalFilename, museumUrl, tweetStatus, nsfw } = skin; + + const embed = new RichEmbed() + .setTitle( + `Someone flagged "${canonicalFilename}", but it's already been reviwed.` + ) + .addField("Status", getPrettyTwitterStatus(tweetStatus), true) + .addField("Museum", `[${canonicalFilename}](${museumUrl})`, true); + + if (nsfw) { + embed.addField("NSFW", `🔞`, true); + } + + dest.send(embed); +} diff --git a/packages/skin-database/index.js b/packages/skin-database/index.js index e1e71aaa..a45a9e09 100644 --- a/packages/skin-database/index.js +++ b/packages/skin-database/index.js @@ -80,6 +80,7 @@ app.get("/skins/", async (req, res) => { }); app.post("/skins/missing", async (req, res) => { + console.log("Checking for missing skins."); const missing = []; const found = []; for (const md5 of req.body.hashes) { @@ -89,25 +90,55 @@ app.post("/skins/missing", async (req, res) => { found.push(md5); } } + console.log( + `${found.length} skins are found and ${missing.length} are missing.` + ); res.json({ missing, found }); }); app.post("/skins/", async (req, res) => { + const client = new Discord.Client(); + await client.login(config.discordToken); + const dest = client.channels.get(config.SKIN_UPLOADS_CHANNEL_ID); + const files = req.files; if (files == null) { + dest.send("Someone hit the upload endpoint with no files attached."); res.status(500).send({ error: "No file supplied" }); return; } const upload = req.files.skin; if (upload == null) { + dest.send("Someone hit the upload endpoint with no files attached."); res.status(500).send({ error: "No file supplied" }); return; } - const result = await addSkinFromBuffer(upload.data, upload.name, "Web API"); + let result; + + try { + result = await addSkinFromBuffer(upload.data, upload.name, "Web API"); + } catch (e) { + console.error(e); + dest.send(`Encountered an error uploading a skin: ${e.message}`); + res.status(500).send({ error: `Error adding skin: ${e.message}` }); + return; + } if (result.status === "ADDED") { - console.log(`Updating index for ${result.md5}.`); - await Skins.updateSearchIndex(result.md5); + // await new Promise((resolve) => setTimeout(resolve, 3000)); + if (result.skinType === "CLASSIC") { + console.log(`Going to post new skin to discord: ${result.md5}`); + // Don't await + Utils.postSkin({ + md5: result.md5, + title: (filename) => `New skin uploaded: ${filename}`, + dest, + }); + } else if (result.skinType === "MODERN") { + dest.send( + `Someone uploaded a new modern skin: ${upload.name} (${result.md5})` + ); + } } res.json({ ...result, filename: upload.name }); @@ -139,12 +170,19 @@ app.post("/skins/:md5/report", async (req, res) => { await client.login(config.discordToken); const dest = client.channels.get(config.NSFW_SKIN_CHANNEL_ID); - // Don't await - Utils.postSkin({ - md5, - title: (filename) => `Review: ${filename}`, - dest, - }); + const skin = await Skins.getSkinByMd5_DEPRECATED(md5); + + if (skin.tweetStatus === "UNREVIEWED") { + // Don't await + Utils.postSkin({ + md5, + title: (filename) => `Review: ${filename}`, + dest, + }); + } else { + Utils.sendAlreadyReviewed({ md5, dest }); + } + res.send("The skin has been reported and will be reviewed shortly."); }); diff --git a/packages/skin-database/s3.js b/packages/skin-database/s3.js index 8fc96062..d748f89f 100644 --- a/packages/skin-database/s3.js +++ b/packages/skin-database/s3.js @@ -3,12 +3,17 @@ AWS.config.update({ region: "us-west-2" }); const s3 = new AWS.S3(); -function putSkin(md5, buffer) { +function putSkin(md5, buffer, ext = "wsz") { return new Promise((resolve, rejectPromise) => { const bucketName = "cdn.webampskins.org"; - const key = `skins/${md5}.wsz`; + const key = `skins/${md5}.${ext}`; s3.putObject( - { Bucket: bucketName, Key: key, Body: buffer, ACL: "public-read" }, + { + Bucket: bucketName, + Key: key, + Body: buffer, + ACL: "public-read", + }, (err) => { if (err) { rejectPromise(err); @@ -26,7 +31,12 @@ function putScreenshot(md5, buffer) { const bucketName = "cdn.webampskins.org"; const key = `screenshots/${md5}.png`; s3.putObject( - { Bucket: bucketName, Key: key, Body: buffer, ACL: "public-read" }, + { + Bucket: bucketName, + Key: key, + Body: buffer, + ACL: "public-read", + }, (err) => { if (err) { rejectPromise(err); diff --git a/packages/skin-database/types.ts b/packages/skin-database/types.ts index 92d9e940..14f663f4 100644 --- a/packages/skin-database/types.ts +++ b/packages/skin-database/types.ts @@ -1,6 +1,7 @@ import { NsfwPrediction } from "./nsfwImage"; export type TweetStatus = "APPROVED" | "REJECTED" | "TWEETED" | "UNREVIEWED"; +export type SkinType = "MODERN" | "CLASSIC"; export type DBSkinRecord = { md5: string; @@ -35,6 +36,7 @@ export type SkinRecord = { skinUrl: string; canonicalFilename: string | null; webampUrl: string; + museumUrl: string; tweeted?: boolean; rejected?: boolean; approved?: boolean;