Server changes for skin museum

This commit is contained in:
Jordan Eldredge 2020-09-16 08:26:44 -04:00
parent 55e9bd102e
commit 6343a2156a
6 changed files with 192 additions and 37 deletions

View file

@ -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<SkinType> {
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<Result> {
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<Result> {
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" };
}

View file

@ -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<boolean> {
}
// TODO: SQLITE
// TODO: Handle modern skins
export async function getSkinByMd5_DEPRECATED(
md5: string
): Promise<SkinRecord | null> {
@ -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]
);

View file

@ -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);
}

View file

@ -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.");
});

View file

@ -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);

View file

@ -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;