diff --git a/experiments/skin-database/tasks/migrate.ts b/experiments/skin-database/tasks/migrate.ts new file mode 100644 index 00000000..4a0107dd --- /dev/null +++ b/experiments/skin-database/tasks/migrate.ts @@ -0,0 +1,16 @@ +const db = require("../db"); +const iaItems = db.get("internetArchiveItems"); +const skins = db.get("skins"); + +async function main() { + const item = await iaItems.findOne({ + migrated: { $exists: false }, + md5: { $exists: true }, + }); + + skins.update({ md5: item.md5 }); + + console.log(item); +} + +export default main; diff --git a/packages/skin-database/cli.ts b/packages/skin-database/cli.ts index fc2b1297..ce4dc778 100755 --- a/packages/skin-database/cli.ts +++ b/packages/skin-database/cli.ts @@ -4,6 +4,7 @@ import fs from "fs"; import { argv } from "yargs"; import fetchInternetArchiveMetadata from "./tasks/fetchInternetArchiveMetadata"; import ensureInternetArchiveItemsIndexByMd5 from "./tasks/ensureInternetArchiveItemsIndexByMd5"; +import migrate from "./tasks/migrate"; import logger from "./logger"; import DiscordWinstonTransport from "./DiscordWinstonTransport"; import * as Skins from "./data/skins"; @@ -36,7 +37,7 @@ async function main() { break; case "tweet": - await tweet(client); + await tweet(client, null); break; case "fetch-metadata": console.log("Going to download metadata from the Internet Archive"); @@ -52,7 +53,8 @@ async function main() { break; } case "reconcile": { - await Skins.reconcile(); + console.log("Reconcile"); + console.log(await Skins.getSkinToArchive()); break; } case "skin": { @@ -67,6 +69,10 @@ async function main() { console.log(await addSkinFromBuffer(buffer, filePath, "cli-user")); break; } + case "migrate": { + await migrate(); + } + default: console.log(`Unknown command ${argv._[0]}`); } diff --git a/packages/skin-database/data/skins.ts b/packages/skin-database/data/skins.ts index 0441eaf4..9ca5f7ad 100644 --- a/packages/skin-database/data/skins.ts +++ b/packages/skin-database/data/skins.ts @@ -35,6 +35,10 @@ function getSkinRecord(skin: DBSkinRecord): SkinRecord { filePaths, imageHash, uploader, + tweeted, + rejected, + approved, + nsfw, } = skin; const fileNames = filePaths.map((p) => path.basename(p)); const skinUrl = `https://s3.amazonaws.com/webamp-uploaded-skins/skins/${md5}.wsz`; @@ -52,6 +56,10 @@ function getSkinRecord(skin: DBSkinRecord): SkinRecord { readmeText, imageHash, uploader, + tweeted, + rejected, + approved, + nsfw, }; } @@ -163,6 +171,10 @@ export async function getMd5sMatchingImageHash(imageHash: string) { return skins.find({ imageHash }, { md5: 1 }); } +export async function getUnarchived() { + return skins.find({ itemName: null }, { md5: 1 }); +} + export function getInternetArchiveUrl(itemName: string | null): string | null { return itemName == null ? null : `https://archive.org/details/${itemName}`; } @@ -180,6 +192,10 @@ export async function markAsTweeted(md5: string): Promise { return S3.markAsTweeted(md5); } +export async function markAsNSFW(md5: string): Promise { + await skins.findOneAndUpdate({ md5 }, { $set: { nsfw: true } }); +} + export async function getStatus(md5: string): Promise { const skin = await skins.findOne({ md5 }); if (skin.tweeted) { @@ -204,6 +220,27 @@ export async function reject(md5: string): Promise { return S3.reject(md5); } +export async function getSkinToArchive(): Promise<{ + filename: string | null; + md5: string; +}> { + const reviewable = await skins.aggregate([ + { $match: CLASSIC_QUERY }, + { + $lookup: { + from: "internetArchiveItems", + localField: "md5", + foreignField: "md5", + as: "archive", + }, + }, + { $count: "5" }, + ]); + const skin = reviewable[0]; + const { canonicalFilename, md5 } = getSkinRecord(skin); + return { filename: canonicalFilename, md5 }; +} + export async function getSkinToReview(): Promise<{ filename: string | null; md5: string; @@ -217,10 +254,7 @@ export async function getSkinToReview(): Promise<{ return { filename: canonicalFilename, md5 }; } -export async function getSkinToTweet(): Promise<{ - filename: string | null; - md5: string; -} | null> { +export async function getSkinToTweet(): Promise { const tweetables = await skins.aggregate([ { $match: TWEETABLE_QUERY }, { $sample: { size: 1 } }, @@ -229,8 +263,7 @@ export async function getSkinToTweet(): Promise<{ if (skin == null) { return null; } - const { canonicalFilename, md5 } = getSkinRecord(skin); - return { filename: canonicalFilename, md5 }; + return getSkinRecord(skin); } export async function getStats(): Promise<{ diff --git a/packages/skin-database/discord-bot/commands/tweet.ts b/packages/skin-database/discord-bot/commands/tweet.ts index f0cd6d1e..5ac4eb1c 100644 --- a/packages/skin-database/discord-bot/commands/tweet.ts +++ b/packages/skin-database/discord-bot/commands/tweet.ts @@ -3,19 +3,20 @@ import { Message } from "discord.js"; import { tweet } from "../../tasks/tweet"; import { CAPTBARITONE_USER_ID } from "../../config"; -async function handler(message: Message): Promise { +async function handler(message: Message, args: [string]) { + const [anything] = args; if (message.author.id !== CAPTBARITONE_USER_ID) { await message.channel.send( `Currently only @captbaritone can use this command` ); return; } - await tweet(message.client); + await tweet(message.client, anything || null); } module.exports = { command: "tweet", handler, - usage: "", + usage: "[]", description: "Tweet an approved skin", }; diff --git a/packages/skin-database/discord-bot/utils.ts b/packages/skin-database/discord-bot/utils.ts index 731eadb3..9e2c8cd5 100644 --- a/packages/skin-database/discord-bot/utils.ts +++ b/packages/skin-database/discord-bot/utils.ts @@ -20,7 +20,7 @@ const filter = (reaction: MessageReaction): boolean => { return ( hasNonBot && - ["👍", "👎", "👏", "😔"].some((name) => reaction.emoji.name === name) + ["👍", "👎", "👏", "😔", "🔞"].some((name) => reaction.emoji.name === name) ); }; @@ -52,6 +52,7 @@ export async function postSkin({ internetArchiveUrl, internetArchiveItemName, readmeText, + nsfw, } = skin; const title = _title ? _title(canonicalFilename) : canonicalFilename; @@ -59,8 +60,18 @@ export async function postSkin({ .setTitle(title) .addField("Try Online", `[webamp.org](${webampUrl})`, true) .addField("Download", `[${canonicalFilename}](${skinUrl})`, true) - .addField("Md5", md5, true) - .setImage(screenshotUrl); + .addField("Md5", md5, true); + + if (nsfw) { + embed.addField("NSFW", `🔞`, true); + } + + // @ts-ignore + if (nsfw && !(dest.type === "text" && dest.nsfw)) { + embed.addField("Screenshot", `[Screenshot](${screenshotUrl})`, true); + } else { + embed.setImage(screenshotUrl); + } if (readmeText) { // Trim the readme since Discord will reject it otherwise. @@ -93,7 +104,9 @@ export async function postSkin({ embed.addField("Tweet Status", `[Tweeted](${tweetUrl}) ${likes}🐦`, true); } else { if (tweetStatus === "UNREVIEWED") { - embed.setFooter("React with 👍 or 👎 to approve or deny"); + embed.setFooter( + "React with 👍 or 👎 to approve or deny or 🔞 to mark NSFW" + ); } embed.addField("Tweet Status", getPrettyTwitterStatus(tweetStatus), true); } @@ -110,8 +123,9 @@ export async function postSkin({ if (tweetStatus === "TWEETED") { return; } - await Promise.all([msg.react("👍"), msg.react("👎")]); + // Don't await + Promise.all([msg.react("👍"), msg.react("👎"), msg.react("🔞")]); // TODO: Timeout at some point await msg.awaitReactions(filter, { max: 1 }).then(async (collected) => { const vote = collected.first(); @@ -135,6 +149,23 @@ export async function postSkin({ ); msg.react("❌"); break; + case "🔞": + logger.info(`${user.username} marked ${md5} as NSFW`); + await Skins.markAsNSFW(md5); + await msg.channel.send( + `${canonicalFilename} was marked as NSFW by ${user.username}` + ); + await Skins.reject(md5); + logger.info(`${user.username} rejected ${md5}`); + await msg.channel.send( + `${canonicalFilename} was rejected by ${user.username}` + ); + msg.react("❌"); + break; + default: + logger.alert( + `Unknown skin reaction by ${user.username} on ${md5}: ${vote.emoji.name}` + ); } }); } diff --git a/packages/skin-database/package.json b/packages/skin-database/package.json index 1fbedc4e..2af763a3 100644 --- a/packages/skin-database/package.json +++ b/packages/skin-database/package.json @@ -29,7 +29,8 @@ "start": "tsc && node dist/index.js", "tweet": "tsc && ./dist/cli.js tweet", "fetch-metadata": "tsc && .dist/cli.js fetch-metadata", - "bot": "tsc && node ./dist/discord-bot/index.js" + "bot": "tsc && node ./dist/discord-bot/index.js", + "cli": "tsc && node ./dist/cli.js" }, "prettier": {}, "devDependencies": { diff --git a/packages/skin-database/tasks/tweet.ts b/packages/skin-database/tasks/tweet.ts index 0d9d1303..97779c20 100644 --- a/packages/skin-database/tasks/tweet.ts +++ b/packages/skin-database/tasks/tweet.ts @@ -4,6 +4,7 @@ import * as Skins from "../data/skins"; import { TWEET_BOT_CHANNEL_ID } from "../config"; import { spawn } from "child_process"; import { Client } from "discord.js"; +import { SkinRecord } from "../types"; function spawnPromise(command: string, args: string[]): Promise { return new Promise((resolve, reject) => { @@ -31,12 +32,43 @@ function spawnPromise(command: string, args: string[]): Promise { }); } -export async function tweet(discordClient: Client) { +export async function tweet(discordClient: Client, anything: string | null) { const tweetBotChannel = discordClient.channels.get(TWEET_BOT_CHANNEL_ID); if (tweetBotChannel == null) { throw new Error("Could not connect to the #tweet-bot channel"); } - const tweetableSkin = await Skins.getSkinToTweet(); + let tweetableSkin: null | SkinRecord = null; + if (anything != null) { + const _md5 = await Skins.getMd5ByAnything(anything); + if (_md5 == null) { + // @ts-ignore + await tweetBotChannel.send( + `Oops! Could not find a skin matching ${anything}` + ); + return; + } + tweetableSkin = await Skins.getSkinByMd5(_md5); + if (tweetableSkin == null) { + // @ts-ignore + await tweetBotChannel.send( + `Oops! Could not find a skin matching the md5 hash ${_md5}` + ); + logger.info(`Could not find a skin matching hash ${_md5}`); + return; + } + if (tweetableSkin.tweeted) { + // @ts-ignore + await tweetBotChannel.send(`Oops! This skin has alraedy been tweeted.`); + return; + } + if (tweetableSkin.rejected) { + // @ts-ignore + await tweetBotChannel.send(`Oops! Can't tweet a rejected skin.`); + return; + } + } else { + tweetableSkin = await Skins.getSkinToTweet(); + } if (tweetableSkin == null) { // @ts-ignore await tweetBotChannel.send( @@ -46,7 +78,7 @@ export async function tweet(discordClient: Client) { return; } - const { md5, filename } = tweetableSkin; + const { md5, canonicalFilename: filename } = tweetableSkin; if (filename == null) { throw new Error(`Could not find filename for skin with hash ${md5}`); } diff --git a/packages/skin-database/types.ts b/packages/skin-database/types.ts index 82049ea2..96571f8b 100644 --- a/packages/skin-database/types.ts +++ b/packages/skin-database/types.ts @@ -10,6 +10,10 @@ export type DBSkinRecord = { filePaths: string[]; imageHash?: string; uploader?: string; + tweeted?: boolean; + rejected?: boolean; + approved?: boolean; + nsfw?: boolean; }; export type DBIARecord = { @@ -30,4 +34,8 @@ export type SkinRecord = { skinUrl: string; canonicalFilename: string | null; webampUrl: string; + tweeted?: boolean; + rejected?: boolean; + approved?: boolean; + nsfw?: boolean; };