diff --git a/setup-mysql-user.sql b/setup-mysql-user.sql index db9a0db..af00878 100644 --- a/setup-mysql-user.sql +++ b/setup-mysql-user.sql @@ -14,8 +14,9 @@ GRANT SELECT ON openneo_impress.zone_translations TO impress2020; -- Public data tables: write GRANT UPDATE ON openneo_impress.items TO impress2020; -GRANT UPDATE ON openneo_impress.swf_assets TO impress2020; GRANT DELETE ON openneo_impress.parents_swf_assets TO impress2020; +GRANT UPDATE ON openneo_impress.pet_states TO impress2020; +GRANT UPDATE ON openneo_impress.swf_assets TO impress2020; -- User data tables GRANT SELECT ON openneo_impress.item_outfit_relationships TO impress2020; diff --git a/src/app/WardrobePage/support/PosePickerSupport.js b/src/app/WardrobePage/support/PosePickerSupport.js index 72a34e8..3af7acb 100644 --- a/src/app/WardrobePage/support/PosePickerSupport.js +++ b/src/app/WardrobePage/support/PosePickerSupport.js @@ -1,11 +1,16 @@ import React from "react"; import gql from "graphql-tag"; -import { useQuery } from "@apollo/client"; -import { Box, IconButton, Select, Switch } from "@chakra-ui/core"; -import { ArrowBackIcon, ArrowForwardIcon } from "@chakra-ui/icons"; +import { useMutation, useQuery } from "@apollo/client"; +import { Box, IconButton, Select, Spinner, Switch } from "@chakra-ui/core"; +import { + ArrowBackIcon, + ArrowForwardIcon, + CheckCircleIcon, +} from "@chakra-ui/icons"; import HangerSpinner from "../../components/HangerSpinner"; import Metadata, { MetadataLabel, MetadataValue } from "./Metadata"; +import useSupport from "./useSupport"; function PosePickerSupport({ speciesId, @@ -30,56 +35,9 @@ function PosePickerSupport({ } } - happyMasc: petAppearance( - speciesId: $speciesId - colorId: $colorId - pose: HAPPY_MASC - ) { - id - } - sadMasc: petAppearance( - speciesId: $speciesId - colorId: $colorId - pose: SAD_MASC - ) { - id - } - sickMasc: petAppearance( - speciesId: $speciesId - colorId: $colorId - pose: SICK_MASC - ) { - id - } - happyFem: petAppearance( - speciesId: $speciesId - colorId: $colorId - pose: HAPPY_FEM - ) { - id - } - sadFem: petAppearance( - speciesId: $speciesId - colorId: $colorId - pose: SAD_FEM - ) { - id - } - sickFem: petAppearance( - speciesId: $speciesId - colorId: $colorId - pose: SICK_FEM - ) { - id - } - unknown: petAppearance( - speciesId: $speciesId - colorId: $colorId - pose: UNKNOWN - ) { - id - } + ...CanonicalPetAppearances } + ${canonicalPetAppearancesFragment} `, { variables: { speciesId, colorId } } ); @@ -121,6 +79,7 @@ function PosePickerSupport({ HAPPY_FEM: data.happyFem?.id, SAD_FEM: data.sadFem?.id, SICK_FEM: data.sickFem?.id, + UNCONVERTED: data.unconverted?.id, UNKNOWN: data.unknown?.id, }; const canonicalAppearanceIds = Object.values( @@ -150,37 +109,21 @@ function PosePickerSupport({ canonicalAppearanceIds={canonicalAppearanceIds} dispatchToOutfit={dispatchToOutfit} /> - + DTI ID: {appearanceId} Pose: - - - - + Zones: @@ -267,6 +210,97 @@ function PosePickerSupportNavigator({ ); } +function PosePickerSupportPoseFields({ petAppearance, speciesId, colorId }) { + const { supportSecret } = useSupport(); + + const [mutate, { loading, error, data }] = useMutation( + gql` + mutation PosePickerSupportSetPetAppearancePose( + $appearanceId: ID! + $pose: Pose! + $supportSecret: String! + ) { + setPetAppearancePose( + appearanceId: $appearanceId + pose: $pose + supportSecret: $supportSecret + ) { + id + pose + } + } + `, + { + refetchQueries: [ + { + query: gql` + query PosePickerSupportRefetchCanonicalAppearances( + $speciesId: ID! + $colorId: ID! + ) { + ...CanonicalPetAppearances + } + ${canonicalPetAppearancesFragment} + `, + variables: { speciesId, colorId }, + }, + ], + } + ); + + return ( + + + + + + {error && {error.message}} + + ); +} + export function PosePickerSupportSwitch({ isChecked, onChange }) { return ( @@ -297,4 +331,72 @@ const POSE_NAMES = { UNKNOWN: "Unknown", }; +const canonicalPetAppearancesFragment = gql` + fragment CanonicalPetAppearances on Query { + happyMasc: petAppearance( + speciesId: $speciesId + colorId: $colorId + pose: HAPPY_MASC + ) { + id + } + + sadMasc: petAppearance( + speciesId: $speciesId + colorId: $colorId + pose: SAD_MASC + ) { + id + } + + sickMasc: petAppearance( + speciesId: $speciesId + colorId: $colorId + pose: SICK_MASC + ) { + id + } + + happyFem: petAppearance( + speciesId: $speciesId + colorId: $colorId + pose: HAPPY_FEM + ) { + id + } + + sadFem: petAppearance( + speciesId: $speciesId + colorId: $colorId + pose: SAD_FEM + ) { + id + } + + sickFem: petAppearance( + speciesId: $speciesId + colorId: $colorId + pose: SICK_FEM + ) { + id + } + + unconverted: petAppearance( + speciesId: $speciesId + colorId: $colorId + pose: UNCONVERTED + ) { + id + } + + unknown: petAppearance( + speciesId: $speciesId + colorId: $colorId + pose: UNKNOWN + ) { + id + } + } +`; + export default PosePickerSupport; diff --git a/src/server/index.js b/src/server/index.js index de1257f..cb58f80 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -7,9 +7,11 @@ const neopets = require("./neopets"); const { capitalize, getPoseFromPetState, + getPetStateFieldsFromPose, getPoseFromPetData, getEmotion, getGenderPresentation, + getPoseName, loadBodyName, logToDiscord, normalizeRow, @@ -259,6 +261,12 @@ const typeDefs = gql` itemId: ID! supportSecret: String! ): RemoveLayerFromItemMutationResult! + + setPetAppearancePose( + appearanceId: ID! + pose: Pose! + supportSecret: String! + ): PetAppearance! } `; @@ -939,6 +947,88 @@ const resolvers = { return { layer: { id: layerId }, item: { id: itemId } }; }, + + setPetAppearancePose: async ( + _, + { appearanceId, pose, supportSecret }, + { + colorTranslationLoader, + speciesTranslationLoader, + petStateLoader, + petTypeLoader, + db, + } + ) => { + if (supportSecret !== process.env["SUPPORT_SECRET"]) { + throw new Error(`Support secret is incorrect. Try setting up again?`); + } + + const oldPetState = await petStateLoader.load(appearanceId); + + const { moodId, female, unconverted } = getPetStateFieldsFromPose(pose); + + const [result] = await db.execute( + `UPDATE pet_states SET mood_id = ?, female = ?, unconverted = ? + WHERE id = ? LIMIT 1`, + [moodId, female, unconverted, appearanceId] + ); + + if (result.affectedRows !== 1) { + throw new Error( + `Expected to affect 1 layer, but affected ${result.affectedRows}` + ); + } + + // we changed it, so clear it from cache + petStateLoader.clear(appearanceId); + + if (process.env["SUPPORT_TOOLS_DISCORD_WEBHOOK_URL"]) { + try { + const petType = await petTypeLoader.load(oldPetState.petTypeId); + const [colorTranslation, speciesTranslation] = await Promise.all([ + colorTranslationLoader.load(petType.colorId), + speciesTranslationLoader.load(petType.speciesId), + ]); + + const oldPose = getPoseFromPetState(oldPetState); + const colorName = capitalize(colorTranslation.name); + const speciesName = capitalize(speciesTranslation.name); + + await logToDiscord({ + embeds: [ + { + title: `🛠 ${colorName} ${speciesName}`, + thumbnail: { + url: `http://pets.neopets.com/cp/${ + petType.basicImageHash || petType.imageHash + }/1/6.png`, + height: 150, + width: 150, + }, + fields: [ + { + name: `Appearance ${appearanceId}: Pose`, + value: `${getPoseName(oldPose)} → **${getPoseName(pose)}**`, + }, + { + name: "As a reminder…", + value: "…the thumbnail might not match!", + }, + ], + timestamp: new Date().toISOString(), + url: `https://impress-2020.openneo.net/outfits/new?species=${petType.speciesId}&color=${petType.colorId}&pose=${pose}&state=${appearanceId}`, + }, + ], + }); + } catch (e) { + console.error("Error sending Discord support log", e); + } + } else { + console.warn("No Discord support webhook provided, skipping"); + } + + return { id: appearanceId }; + }, }, }; diff --git a/src/server/loaders.js b/src/server/loaders.js index a9872d8..8185c69 100644 --- a/src/server/loaders.js +++ b/src/server/loaders.js @@ -355,7 +355,8 @@ const buildPetStatesForPetTypeLoader = (db, loaders) => const [rows, _] = await db.execute( `SELECT * FROM pet_states WHERE pet_type_id IN (${qs}) - ORDER BY (mood_id IS NULL) ASC, mood_id ASC, female DESC, id DESC`, + ORDER BY (mood_id IS NULL) ASC, mood_id ASC, female DESC, + unconverted DESC, id DESC`, petTypeIds ); diff --git a/src/server/util.js b/src/server/util.js index 7444740..bd4d4a9 100644 --- a/src/server/util.js +++ b/src/server/util.js @@ -21,9 +21,9 @@ function getEmotion(pose) { function getGenderPresentation(pose) { if (["HAPPY_MASC", "SAD_MASC", "SICK_MASC"].includes(pose)) { - return "MASCULINE"; + return "MASC"; } else if (["HAPPY_FEM", "SAD_FEM", "SICK_FEM"].includes(pose)) { - return "MASCULINE"; + return "FEM"; } else if (["UNCONVERTED", "UNKNOWN"].includes(pose)) { return null; } else { @@ -60,6 +60,28 @@ function getPoseFromPetState(petState) { } } +function getPetStateFieldsFromPose(pose) { + if (pose === "UNCONVERTED") { + return { moodId: null, female: null, unconverted: true }; + } else if (pose === "UNKNOWN") { + return { moodId: null, female: null, unconverted: false }; + } else if (pose === "HAPPY_MASC") { + return { moodId: "1", female: false, unconverted: false }; + } else if (pose === "HAPPY_FEM") { + return { moodId: "1", female: true, unconverted: false }; + } else if (pose === "SAD_MASC") { + return { moodId: "2", female: false, unconverted: false }; + } else if (pose === "SAD_FEM") { + return { moodId: "2", female: true, unconverted: false }; + } else if (pose === "SICK_MASC") { + return { moodId: "3", female: false, unconverted: false }; + } else if (pose === "SICK_FEM") { + return { moodId: "3", female: true, unconverted: false }; + } else { + throw new Error(`unexpected pose ${pose}`); + } +} + function getPoseFromPetData(petMetaData, petCustomData) { // TODO: Use custom data to decide if Unconverted. const moodId = petMetaData.mood; @@ -85,6 +107,21 @@ function getPoseFromPetData(petMetaData, petCustomData) { } } +const POSE_NAMES = { + HAPPY_MASC: "Happy Masc", + SAD_MASC: "Sad Masc", + SICK_MASC: "Sick Masc", + HAPPY_FEM: "Happy Fem", + SAD_FEM: "Sad Fem", + SICK_FEM: "Sick Fem", + UNCONVERTED: "Unconverted", + UNKNOWN: "Unknown", +}; + +function getPoseName(pose) { + return POSE_NAMES[pose]; +} + async function loadBodyName(bodyId, db) { if (String(bodyId) === "0") { return "All bodies"; @@ -148,7 +185,9 @@ module.exports = { getEmotion, getGenderPresentation, getPoseFromPetState, + getPetStateFieldsFromPose, getPoseFromPetData, + getPoseName, loadBodyName, logToDiscord, normalizeRow,