This commit is contained in:
Jordan Eldredge 2020-06-11 14:25:54 -04:00
parent a0ec8a4f82
commit a8b33f23c8
8 changed files with 148 additions and 20 deletions

View file

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

View file

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

View file

@ -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<void> {
return S3.markAsTweeted(md5);
}
export async function markAsNSFW(md5: string): Promise<void> {
await skins.findOneAndUpdate({ md5 }, { $set: { nsfw: true } });
}
export async function getStatus(md5: string): Promise<TweetStatus> {
const skin = await skins.findOne({ md5 });
if (skin.tweeted) {
@ -204,6 +220,27 @@ export async function reject(md5: string): Promise<void> {
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<SkinRecord | null> {
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<{

View file

@ -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<void> {
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: "[<md5>]",
description: "Tweet an approved skin",
};

View file

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

View file

@ -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": {

View file

@ -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<string> {
return new Promise((resolve, reject) => {
@ -31,12 +32,43 @@ function spawnPromise(command: string, args: string[]): Promise<string> {
});
}
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}`);
}

View file

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