From afe22daee5a4715bbe9c1bc27eb0a1e170ffded0 Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Tue, 1 Dec 2020 18:45:24 -0500 Subject: [PATCH] Get skins by tweet id --- .../api/__tests__/router.test.ts | 19 +++++-- packages/skin-database/data/SkinModel.ts | 11 +++- packages/skin-database/data/TweetModel.ts | 51 +++++++++++++++++++ .../data/__tests__/SkinModel.test.ts | 8 +++ .../data/__tests__/skins.test.ts | 33 ++++++++---- packages/skin-database/seeds/test_data.ts | 12 +++++ packages/skin-database/utils.ts | 1 + 7 files changed, 119 insertions(+), 16 deletions(-) diff --git a/packages/skin-database/api/__tests__/router.test.ts b/packages/skin-database/api/__tests__/router.test.ts index 98e46f22..eb34d1a8 100644 --- a/packages/skin-database/api/__tests__/router.test.ts +++ b/packages/skin-database/api/__tests__/router.test.ts @@ -82,7 +82,7 @@ describe("/skins/", () => { const { body } = await request(app).get("/skins/"); expect(body).toMatchInlineSnapshot(` Object { - "skinCount": 5, + "skinCount": 6, "skins": Array [ Object { "fileName": "Zelda_Amp_3.wsz", @@ -94,6 +94,11 @@ describe("/skins/", () => { "md5": "a_fake_md5", "nsfw": false, }, + Object { + "fileName": "tweeted.wsz", + "md5": "a_tweeted_md5", + "nsfw": false, + }, Object { "fileName": "approved.wsz", "md5": "an_approved_md5", @@ -119,7 +124,7 @@ describe("/skins/", () => { .query({ first: 2, offset: 1 }); expect(body).toMatchInlineSnapshot(` Object { - "skinCount": 5, + "skinCount": 6, "skins": Array [ Object { "fileName": "path.wsz", @@ -127,8 +132,8 @@ describe("/skins/", () => { "nsfw": false, }, Object { - "fileName": "approved.wsz", - "md5": "an_approved_md5", + "fileName": "tweeted.wsz", + "md5": "a_tweeted_md5", "nsfw": false, }, ], @@ -212,7 +217,7 @@ test("/skins/a_md5_that_does_not_exist/reject (404)", async () => { expect(handler).not.toHaveBeenCalled(); }); -test.only("/skins/a_fake_md5/nsfw", async () => { +test("/skins/a_fake_md5/nsfw", async () => { const ctx = new UserContext(); const { body } = await request(app) .post("/skins/a_fake_md5/nsfw") @@ -338,6 +343,10 @@ test("/stylegan.json", async () => { "fileName": "rejected.wsz", "url": "https://cdn.webampskins.org/screenshots/a_rejected_md5.png", }, + Object { + "fileName": "tweeted.wsz", + "url": "https://cdn.webampskins.org/screenshots/a_tweeted_md5.png", + }, Object { "fileName": "approved.wsz", "url": "https://cdn.webampskins.org/screenshots/an_approved_md5.png", diff --git a/packages/skin-database/data/SkinModel.ts b/packages/skin-database/data/SkinModel.ts index 9e62e99a..fbc333b7 100644 --- a/packages/skin-database/data/SkinModel.ts +++ b/packages/skin-database/data/SkinModel.ts @@ -4,7 +4,7 @@ import UserContext, { ctxWeakMapMemoize } from "./UserContext"; import TweetModel, { TweetDebugData } from "./TweetModel"; import IaItemModel from "./IaItemModel"; import FileModel, { FileDebugData } from "./FileModel"; -import { MD5_REGEX } from "../utils"; +import { MD5_REGEX, TWEET_ID_REGEX } from "../utils"; import DataLoader from "dataloader"; import { knex } from "../db"; @@ -32,7 +32,14 @@ export default class SkinModel { } } const iaItem = await IaItemModel.fromAnything(ctx, anything); - return iaItem?.getSkin() ?? null; + if (iaItem != null) { + return iaItem.getSkin(); + } + const tweet = await TweetModel.fromAnything(ctx, anything); + if (tweet != null) { + return tweet.getSkin(); + } + return null; } static async exists(ctx: UserContext, md5: string): Promise { diff --git a/packages/skin-database/data/TweetModel.ts b/packages/skin-database/data/TweetModel.ts index 596a23b9..e72a700d 100644 --- a/packages/skin-database/data/TweetModel.ts +++ b/packages/skin-database/data/TweetModel.ts @@ -2,6 +2,8 @@ import UserContext, { ctxWeakMapMemoize } from "./UserContext"; import { TweetRow } from "../types"; import DataLoader from "dataloader"; import { knex } from "../db"; +import { TWEET_SNOWFLAKE_REGEX } from "../utils"; +import SkinModel from "./SkinModel"; export type TweetDebugData = { row: TweetRow; @@ -15,6 +17,37 @@ export default class TweetModel { return rows.map((row) => new TweetModel(ctx, row)); } + static async fromUrl( + ctx: UserContext, + url: string + ): Promise { + const row = await getTweetByTweetUrlLoader(ctx).load(url); + if (row == null) { + return null; + } + return new TweetModel(ctx, row); + } + + static async fromAnything( + ctx: UserContext, + anything: string + ): Promise { + const snowflakeMatch = anything.match(TWEET_SNOWFLAKE_REGEX); + if (snowflakeMatch == null) { + return null; + } + const snowflake = snowflakeMatch[1]; + const found = await TweetModel.fromUrl( + ctx, + `https://twitter.com/winampskins/status/${snowflake}` + ); + return found || null; + } + + getMd5(): string { + return this.row.skin_md5; + } + getUrl(): string { return this.row.url; } @@ -25,6 +58,14 @@ export default class TweetModel { return this.row.retweets; } + async getSkin(): Promise { + const skin = await SkinModel.fromMd5(this.ctx, this.getMd5()); + if (skin == null) { + throw new Error(`Could not find skin for md5 "${this.getMd5()}"`); + } + return skin; + } + async debug(): Promise { return { row: this.row, @@ -39,3 +80,13 @@ const getTweetsLoader = ctxWeakMapMemoize>( return md5s.map((md5) => rows.filter((x) => x.skin_md5 === md5)); }) ); + +const getTweetByTweetUrlLoader = ctxWeakMapMemoize< + DataLoader +>( + () => + new DataLoader(async (urls) => { + const rows = await knex("tweets").whereIn("url", urls).select(); + return urls.map((url) => rows.find((x) => x.url === url)); + }) +); diff --git a/packages/skin-database/data/__tests__/SkinModel.test.ts b/packages/skin-database/data/__tests__/SkinModel.test.ts index 726a0440..bd780123 100644 --- a/packages/skin-database/data/__tests__/SkinModel.test.ts +++ b/packages/skin-database/data/__tests__/SkinModel.test.ts @@ -51,4 +51,12 @@ describe("fromAnything", () => { ); expect(skin?.getMd5()).toBe("a_fake_md5"); }); + test("Tweet URL", async () => { + const ctx = new UserContext(); + const skin = await SkinModel.fromAnything( + ctx, + "https://twitter.com/winampskins/status/1333893671326871552" + ); + expect(skin?.getMd5()).toBe("a_tweeted_md5"); + }); }); diff --git a/packages/skin-database/data/__tests__/skins.test.ts b/packages/skin-database/data/__tests__/skins.test.ts index 5f71cadd..320bfde3 100644 --- a/packages/skin-database/data/__tests__/skins.test.ts +++ b/packages/skin-database/data/__tests__/skins.test.ts @@ -52,6 +52,10 @@ describe("seeded", () => { "fileName": "rejected.wsz", "md5": "a_rejected_md5", }, + Object { + "fileName": "tweeted.wsz", + "md5": "a_tweeted_md5", + }, Object { "fileName": "approved.wsz", "md5": "an_approved_md5", @@ -60,7 +64,7 @@ describe("seeded", () => { `); }); test("getClassicSkinCount", async () => { - expect(await Skins.getClassicSkinCount()).toBe(5); + expect(await Skins.getClassicSkinCount()).toBe(6); }); test("getTweetableSkinCount", async () => { expect(await Skins.getTweetableSkinCount()).toBe(1); @@ -84,6 +88,10 @@ describe("seeded", () => { "fileName": "rejected.wsz", "url": "https://cdn.webampskins.org/screenshots/a_rejected_md5.png", }, + Object { + "fileName": "tweeted.wsz", + "url": "https://cdn.webampskins.org/screenshots/a_tweeted_md5.png", + }, Object { "fileName": "approved.wsz", "url": "https://cdn.webampskins.org/screenshots/an_approved_md5.png", @@ -105,6 +113,11 @@ describe("seeded", () => { "md5": "a_fake_md5", "nsfw": false, }, + Object { + "fileName": "tweeted.wsz", + "md5": "a_tweeted_md5", + "nsfw": false, + }, Object { "fileName": "approved.wsz", "md5": "an_approved_md5", @@ -124,14 +137,16 @@ describe("seeded", () => { `); }); test("getStats", async () => { - expect(await Skins.getStats()).toEqual({ - approved: 1, - rejected: 1, - nsfw: 1, - tweeted: 0, - tweetable: 1, - webUploads: 0, - }); + expect(await Skins.getStats()).toMatchInlineSnapshot(` + Object { + "approved": 2, + "nsfw": 1, + "rejected": 1, + "tweetable": 1, + "tweeted": 1, + "webUploads": 0, + } + `); }); test("getSkinToTweet", async () => { expect(await Skins.getSkinToTweet()).toEqual({ diff --git a/packages/skin-database/seeds/test_data.ts b/packages/skin-database/seeds/test_data.ts index c6dc8ab7..166b0fe0 100644 --- a/packages/skin-database/seeds/test_data.ts +++ b/packages/skin-database/seeds/test_data.ts @@ -6,6 +6,7 @@ export async function seed(knex: Knex): Promise { await knex("files").del(); await knex("skin_reviews").del(); await knex("ia_items").del(); + await knex("tweets").del(); // Inserts seed entries await knex("skins").insert([ { md5: "a_fake_md5", skin_type: 1, emails: "" }, @@ -13,6 +14,7 @@ export async function seed(knex: Knex): Promise { { md5: "an_approved_md5", skin_type: 1, emails: "" }, { md5: "a_rejected_md5", skin_type: 1, emails: "" }, { md5: "a_nsfw_md5", skin_type: 1, emails: "" }, + { md5: "a_tweeted_md5", skin_type: 1, emails: "" }, { md5: "48bbdbbeb03d347e59b1eebda4d352d0", skin_type: 1, emails: "" }, ]); await knex("files").insert([ @@ -21,6 +23,7 @@ export async function seed(knex: Knex): Promise { { skin_md5: "an_approved_md5", file_path: "/a/fake/approved.wsz" }, { skin_md5: "a_rejected_md5", file_path: "/a/fake/rejected.wsz" }, { skin_md5: "a_nsfw_md5", file_path: "/a/fake/nsfw.wsz" }, + { skin_md5: "a_tweeted_md5", file_path: "/a/fake/tweeted.wsz" }, { skin_md5: "48bbdbbeb03d347e59b1eebda4d352d0", file_path: "/a/fake/Zelda_Amp_3.wsz", @@ -29,10 +32,19 @@ export async function seed(knex: Knex): Promise { await knex("skin_reviews").insert([ { skin_md5: "an_approved_md5", review: "APPROVED" }, { skin_md5: "a_rejected_md5", review: "REJECTED" }, + { skin_md5: "a_tweeted_md5", review: "APPROVED" }, { skin_md5: "a_nsfw_md5", review: "NSFW" }, ]); await knex("ia_items").insert([ { skin_md5: "a_fake_md5", identifier: "a_fake_ia_identifier" }, ]); + + await knex("tweets").insert([ + { + skin_md5: "a_tweeted_md5", + url: "https://twitter.com/winampskins/status/1333893671326871552", + tweet_id: "1333893671326871552", + }, + ]); } diff --git a/packages/skin-database/utils.ts b/packages/skin-database/utils.ts index 504f1572..b8d37b2b 100644 --- a/packages/skin-database/utils.ts +++ b/packages/skin-database/utils.ts @@ -12,3 +12,4 @@ export function truncate(str: string, len: number): string { } export const MD5_REGEX = /([a-fA-F0-9]{32})/; +export const TWEET_SNOWFLAKE_REGEX = /([0-9]{19})/;