2020-08-16 23:28:41 -07:00
|
|
|
const { gql, makeExecutableSchema } = require("apollo-server");
|
2020-08-17 18:49:37 -07:00
|
|
|
import { addBeelineToSchema, beelinePlugin } from "./lib/beeline-graphql";
|
2020-04-22 11:51:36 -07:00
|
|
|
|
|
|
|
const connectToDb = require("./db");
|
2020-04-23 14:23:46 -07:00
|
|
|
const buildLoaders = require("./loaders");
|
2020-04-25 06:50:34 -07:00
|
|
|
const neopets = require("./neopets");
|
2020-05-23 12:47:06 -07:00
|
|
|
const {
|
|
|
|
capitalize,
|
|
|
|
getPoseFromPetState,
|
2020-08-31 00:32:17 -07:00
|
|
|
getPetStateFieldsFromPose,
|
2020-05-23 13:55:59 -07:00
|
|
|
getPoseFromPetData,
|
2020-05-23 12:47:06 -07:00
|
|
|
getEmotion,
|
|
|
|
getGenderPresentation,
|
2020-08-31 00:32:17 -07:00
|
|
|
getPoseName,
|
2020-08-31 19:23:56 -07:00
|
|
|
getRestrictedZoneIds,
|
2020-08-20 23:23:33 -07:00
|
|
|
loadBodyName,
|
2020-08-20 22:25:41 -07:00
|
|
|
logToDiscord,
|
2020-08-20 23:16:59 -07:00
|
|
|
normalizeRow,
|
2020-05-23 12:47:06 -07:00
|
|
|
} = require("./util");
|
2020-04-22 11:51:36 -07:00
|
|
|
|
|
|
|
const typeDefs = gql`
|
2020-08-16 23:28:41 -07:00
|
|
|
directive @cacheControl(maxAge: Int!) on FIELD_DEFINITION | OBJECT
|
|
|
|
|
2020-04-23 01:08:00 -07:00
|
|
|
enum LayerImageSize {
|
|
|
|
SIZE_600
|
|
|
|
SIZE_300
|
|
|
|
SIZE_150
|
|
|
|
}
|
|
|
|
|
2020-05-23 12:47:06 -07:00
|
|
|
"""
|
|
|
|
The poses a PetAppearance can take!
|
|
|
|
"""
|
|
|
|
enum Pose {
|
|
|
|
HAPPY_MASC
|
|
|
|
SAD_MASC
|
|
|
|
SICK_MASC
|
|
|
|
HAPPY_FEM
|
|
|
|
SAD_FEM
|
|
|
|
SICK_FEM
|
|
|
|
UNCONVERTED
|
|
|
|
UNKNOWN # for when we have the data, but we don't know what it is
|
|
|
|
}
|
|
|
|
|
2020-05-02 16:49:57 -07:00
|
|
|
"""
|
|
|
|
A pet's gender presentation: masculine or feminine.
|
|
|
|
|
|
|
|
Neopets calls these "male" and "female", and I think that's silly and not wise
|
|
|
|
to propagate further, especially in the context of a strictly visual app like
|
|
|
|
Dress to Impress! This description isn't altogether correct either, but idk
|
|
|
|
what's better :/
|
|
|
|
"""
|
|
|
|
enum GenderPresentation {
|
|
|
|
MASCULINE
|
|
|
|
FEMININE
|
|
|
|
}
|
|
|
|
|
|
|
|
"""
|
|
|
|
A pet's emotion: happy, sad, or sick.
|
|
|
|
|
|
|
|
Note that we don't ever show the angry emotion on Dress to Impress, because
|
|
|
|
we don't have the data: it's impossible for a pet's passive emotion on the
|
|
|
|
pet lookup to be angry!
|
|
|
|
"""
|
|
|
|
enum Emotion {
|
|
|
|
HAPPY
|
|
|
|
SAD
|
|
|
|
SICK
|
|
|
|
}
|
|
|
|
|
2020-04-22 11:51:36 -07:00
|
|
|
type Item {
|
|
|
|
id: ID!
|
|
|
|
name: String!
|
2020-04-25 22:40:28 -07:00
|
|
|
description: String!
|
2020-04-22 14:55:12 -07:00
|
|
|
thumbnailUrl: String!
|
2020-05-31 15:56:40 -07:00
|
|
|
rarityIndex: Int!
|
|
|
|
isNc: Boolean!
|
2020-08-28 00:10:00 -07:00
|
|
|
|
|
|
|
# How this item appears on the given species/color combo. If it does not
|
|
|
|
# fit the pet, we'll return an empty ItemAppearance with no layers.
|
|
|
|
appearanceOn(speciesId: ID!, colorId: ID!): ItemAppearance!
|
2020-07-31 22:31:28 -07:00
|
|
|
|
|
|
|
# This is set manually by Support users, when the pet is only for e.g.
|
|
|
|
# Maraquan pets, and our usual auto-detection isn't working. We provide
|
|
|
|
# this for the Support UI; it's not very helpful for most users, because it
|
|
|
|
# can be empty even if the item _has_ an auto-detected special color.
|
|
|
|
manualSpecialColor: Color
|
2020-08-14 21:12:13 -07:00
|
|
|
|
|
|
|
# This is set manually by Support users, when the item _seems_ to fit all
|
|
|
|
# pets the same because of its zones, but it actually doesn't - e.g.,
|
|
|
|
# the Dug Up Dirt Foreground actually looks different for each body. We
|
|
|
|
# provide this for the Support UI; it's not very helpful for most users,
|
|
|
|
# because it's only used at modeling time. This value does not change how
|
|
|
|
# layer data from this API should be interpreted!
|
|
|
|
explicitlyBodySpecific: Boolean!
|
2020-04-23 01:08:00 -07:00
|
|
|
}
|
|
|
|
|
2020-07-22 23:08:28 -07:00
|
|
|
# Cache for 1 week (unlikely to change)
|
|
|
|
type PetAppearance @cacheControl(maxAge: 604800) {
|
2020-05-02 20:48:32 -07:00
|
|
|
id: ID!
|
2020-06-24 19:05:07 -07:00
|
|
|
species: Species!
|
|
|
|
color: Color!
|
2020-05-23 12:47:06 -07:00
|
|
|
pose: Pose!
|
2020-06-24 19:05:07 -07:00
|
|
|
bodyId: ID!
|
|
|
|
|
2020-05-02 16:49:57 -07:00
|
|
|
layers: [AppearanceLayer!]!
|
2020-08-31 23:18:30 -07:00
|
|
|
restrictedZones: [Zone!]!
|
2020-08-31 19:23:56 -07:00
|
|
|
|
2020-08-27 21:32:22 -07:00
|
|
|
petStateId: ID! # Deprecated, an alias for id
|
2020-08-28 22:58:39 -07:00
|
|
|
# Whether this PetAppearance is known to look incorrect. This is a manual
|
|
|
|
# flag that we set, in the case where this glitchy PetAppearance really did
|
|
|
|
# appear on Neopets.com, and has since been fixed.
|
|
|
|
isGlitched: Boolean!
|
2020-05-02 16:49:57 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
type ItemAppearance {
|
2020-08-28 00:10:00 -07:00
|
|
|
id: ID!
|
|
|
|
item: Item!
|
|
|
|
bodyId: ID!
|
|
|
|
layers: [AppearanceLayer!]
|
2020-04-23 14:44:06 -07:00
|
|
|
restrictedZones: [Zone!]!
|
2020-04-23 01:08:00 -07:00
|
|
|
}
|
|
|
|
|
2020-07-22 23:08:28 -07:00
|
|
|
# Cache for 1 week (unlikely to change)
|
|
|
|
type AppearanceLayer @cacheControl(maxAge: 604800) {
|
2020-08-14 22:09:52 -07:00
|
|
|
# The DTI ID. Guaranteed unique across all layers of all types.
|
2020-04-23 01:08:00 -07:00
|
|
|
id: ID!
|
2020-08-14 22:09:52 -07:00
|
|
|
|
|
|
|
# The Neopets ID. Guaranteed unique across layers of the _same_ type, but
|
|
|
|
# not of different types. That is, it's allowed and common for an item
|
|
|
|
# layer and a pet layer to have the same remoteId.
|
|
|
|
remoteId: ID!
|
|
|
|
|
2020-04-23 01:08:00 -07:00
|
|
|
zone: Zone!
|
|
|
|
imageUrl(size: LayerImageSize): String
|
2020-05-11 21:19:34 -07:00
|
|
|
|
|
|
|
"""
|
|
|
|
This layer as a single SVG, if available.
|
|
|
|
|
|
|
|
This might not be available if the asset isn't converted yet by Neopets,
|
|
|
|
or if it's not as simple as a single SVG (e.g. animated).
|
|
|
|
"""
|
|
|
|
svgUrl: String
|
2020-08-01 14:12:57 -07:00
|
|
|
|
2020-08-01 22:54:30 -07:00
|
|
|
"""
|
|
|
|
This layer as a single SWF, if available.
|
|
|
|
|
|
|
|
At time of writing, all layers have SWFs. But I've marked this nullable
|
|
|
|
because I'm not sure this will continue to be true after the HTML5
|
|
|
|
migration, and I'd like clients to guard against it.
|
|
|
|
"""
|
|
|
|
swfUrl: String
|
|
|
|
|
2020-08-01 14:12:57 -07:00
|
|
|
"""
|
|
|
|
This layer can fit on PetAppearances with the same bodyId. "0" is a
|
|
|
|
special body ID that indicates it fits all PetAppearances.
|
|
|
|
"""
|
|
|
|
bodyId: ID!
|
2020-08-01 15:30:26 -07:00
|
|
|
|
|
|
|
"""
|
|
|
|
The item this layer is for, if any. (For pet layers, this is null.)
|
|
|
|
"""
|
|
|
|
item: Item
|
2020-08-31 19:23:56 -07:00
|
|
|
|
|
|
|
"""
|
|
|
|
The zones that this layer restricts, if any. Note that, for item layers,
|
|
|
|
this is generally empty and the restriction is on the ItemAppearance, not
|
|
|
|
the individual layers. For pet layers, this is generally used for
|
|
|
|
Unconverted pets.
|
2020-08-31 23:18:30 -07:00
|
|
|
|
|
|
|
Deprecated, aggregated into PetAppearance for a simpler API.
|
2020-08-31 19:23:56 -07:00
|
|
|
"""
|
|
|
|
restrictedZones: [Zone!]!
|
2020-04-23 01:08:00 -07:00
|
|
|
}
|
|
|
|
|
2020-07-22 23:08:28 -07:00
|
|
|
# Cache for 1 week (unlikely to change)
|
|
|
|
type Zone @cacheControl(maxAge: 604800) {
|
2020-04-23 01:08:00 -07:00
|
|
|
id: ID!
|
|
|
|
depth: Int!
|
|
|
|
label: String!
|
2020-04-22 11:51:36 -07:00
|
|
|
}
|
|
|
|
|
2020-04-25 01:55:48 -07:00
|
|
|
type ItemSearchResult {
|
|
|
|
query: String!
|
|
|
|
items: [Item!]!
|
|
|
|
}
|
|
|
|
|
2020-07-22 23:08:28 -07:00
|
|
|
# Cache for 1 week (unlikely to change)
|
|
|
|
type Color @cacheControl(maxAge: 604800) {
|
2020-04-25 03:42:05 -07:00
|
|
|
id: ID!
|
|
|
|
name: String!
|
2020-07-31 22:11:32 -07:00
|
|
|
isStandard: Boolean!
|
2020-04-25 03:42:05 -07:00
|
|
|
}
|
|
|
|
|
2020-07-22 23:08:28 -07:00
|
|
|
# Cache for 1 week (unlikely to change)
|
|
|
|
type Species @cacheControl(maxAge: 604800) {
|
2020-04-25 03:42:05 -07:00
|
|
|
id: ID!
|
|
|
|
name: String!
|
2020-08-31 18:25:42 -07:00
|
|
|
|
|
|
|
# The bodyId for PetAppearances that use this species and a standard color.
|
|
|
|
# We use this to preload the standard body IDs, so that items stay when
|
|
|
|
# switching between standard colors.
|
|
|
|
standardBodyId: ID!
|
2020-04-25 03:42:05 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
type SpeciesColorPair {
|
|
|
|
species: Species!
|
|
|
|
color: Color!
|
|
|
|
}
|
|
|
|
|
2020-04-25 06:50:34 -07:00
|
|
|
type Outfit {
|
2020-06-24 19:05:07 -07:00
|
|
|
id: ID!
|
|
|
|
name: String!
|
|
|
|
petAppearance: PetAppearance!
|
|
|
|
wornItems: [Item!]!
|
|
|
|
closetedItems: [Item!]!
|
|
|
|
|
|
|
|
species: Species! # to be deprecated? can use petAppearance? 🤔
|
|
|
|
color: Color! # to be deprecated? can use petAppearance? 🤔
|
|
|
|
pose: Pose! # to be deprecated? can use petAppearance? 🤔
|
|
|
|
items: [Item!]! # deprecated alias for wornItems
|
2020-04-25 06:50:34 -07:00
|
|
|
}
|
|
|
|
|
2020-04-22 11:51:36 -07:00
|
|
|
type Query {
|
2020-07-22 23:08:28 -07:00
|
|
|
allColors: [Color!]! @cacheControl(maxAge: 10800) # Cache for 3 hours (we might add more!)
|
|
|
|
allSpecies: [Species!]! @cacheControl(maxAge: 10800) # Cache for 3 hours (we might add more!)
|
2020-05-03 01:52:39 -07:00
|
|
|
allValidSpeciesColorPairs: [SpeciesColorPair!]! # deprecated
|
2020-07-31 22:31:28 -07:00
|
|
|
item(id: ID!): Item
|
2020-04-22 11:51:36 -07:00
|
|
|
items(ids: [ID!]!): [Item!]!
|
2020-04-25 01:55:48 -07:00
|
|
|
itemSearch(query: String!): ItemSearchResult!
|
|
|
|
itemSearchToFit(
|
|
|
|
query: String!
|
|
|
|
speciesId: ID!
|
|
|
|
colorId: ID!
|
|
|
|
offset: Int
|
|
|
|
limit: Int
|
|
|
|
): ItemSearchResult!
|
2020-08-28 22:58:39 -07:00
|
|
|
|
|
|
|
petAppearanceById(id: ID!): PetAppearance @cacheControl(maxAge: 10800) # Cache for 3 hours (Support might edit!)
|
|
|
|
# The canonical pet appearance for the given species, color, and pose.
|
|
|
|
# Null if we don't have any data for this combination.
|
2020-05-23 12:47:06 -07:00
|
|
|
petAppearance(speciesId: ID!, colorId: ID!, pose: Pose!): PetAppearance
|
2020-08-28 22:58:39 -07:00
|
|
|
@cacheControl(maxAge: 10800) # Cache for 3 hours (we might model more!)
|
|
|
|
# All pet appearances we've ever seen for the given species and color. Note
|
|
|
|
# that this might include multiple copies for the same pose, and they might
|
2020-08-31 01:17:18 -07:00
|
|
|
# even be glitched data. We use this for Support tools, and we don't cache
|
|
|
|
# it to make sure that Support users are always seeing the most up-to-date
|
|
|
|
# version here (even if the standard pose picker is still showing outdated
|
|
|
|
# cached canonical poses).
|
2020-05-02 16:49:57 -07:00
|
|
|
petAppearances(speciesId: ID!, colorId: ID!): [PetAppearance!]!
|
2020-06-24 19:05:07 -07:00
|
|
|
outfit(id: ID!): Outfit
|
|
|
|
|
2020-08-31 18:25:42 -07:00
|
|
|
color(id: ID!): Color
|
|
|
|
species(id: ID!): Species
|
|
|
|
|
2020-04-25 06:50:34 -07:00
|
|
|
petOnNeopetsDotCom(petName: String!): Outfit
|
2020-04-22 11:51:36 -07:00
|
|
|
}
|
2020-08-01 00:04:11 -07:00
|
|
|
|
2020-08-20 21:40:05 -07:00
|
|
|
type RemoveLayerFromItemMutationResult {
|
|
|
|
layer: AppearanceLayer!
|
|
|
|
item: Item!
|
|
|
|
}
|
|
|
|
|
2020-08-01 00:04:11 -07:00
|
|
|
type Mutation {
|
|
|
|
setManualSpecialColor(
|
|
|
|
itemId: ID!
|
|
|
|
colorId: ID
|
|
|
|
supportSecret: String!
|
|
|
|
): Item!
|
2020-08-01 15:30:26 -07:00
|
|
|
|
2020-08-14 22:01:27 -07:00
|
|
|
setItemExplicitlyBodySpecific(
|
|
|
|
itemId: ID!
|
|
|
|
explicitlyBodySpecific: Boolean!
|
|
|
|
supportSecret: String!
|
|
|
|
): Item!
|
|
|
|
|
2020-08-01 15:30:26 -07:00
|
|
|
setLayerBodyId(
|
|
|
|
layerId: ID!
|
|
|
|
bodyId: ID!
|
|
|
|
supportSecret: String!
|
|
|
|
): AppearanceLayer!
|
2020-08-20 21:40:05 -07:00
|
|
|
|
|
|
|
removeLayerFromItem(
|
|
|
|
layerId: ID!
|
|
|
|
itemId: ID!
|
|
|
|
supportSecret: String!
|
|
|
|
): RemoveLayerFromItemMutationResult!
|
2020-08-31 00:32:17 -07:00
|
|
|
|
|
|
|
setPetAppearancePose(
|
|
|
|
appearanceId: ID!
|
|
|
|
pose: Pose!
|
|
|
|
supportSecret: String!
|
|
|
|
): PetAppearance!
|
2020-08-31 00:48:54 -07:00
|
|
|
|
|
|
|
setPetAppearanceIsGlitched(
|
|
|
|
appearanceId: ID!
|
|
|
|
isGlitched: Boolean!
|
|
|
|
supportSecret: String!
|
|
|
|
): PetAppearance!
|
2020-08-01 00:04:11 -07:00
|
|
|
}
|
2020-04-22 11:51:36 -07:00
|
|
|
`;
|
|
|
|
|
|
|
|
const resolvers = {
|
|
|
|
Item: {
|
2020-07-02 14:33:47 -07:00
|
|
|
name: async ({ id, name }, _, { itemTranslationLoader }) => {
|
|
|
|
if (name) return name;
|
|
|
|
const translation = await itemTranslationLoader.load(id);
|
2020-04-22 11:51:36 -07:00
|
|
|
return translation.name;
|
|
|
|
},
|
2020-07-02 14:33:47 -07:00
|
|
|
description: async ({ id, description }, _, { itemTranslationLoader }) => {
|
|
|
|
if (description) return description;
|
|
|
|
const translation = await itemTranslationLoader.load(id);
|
2020-04-25 22:40:28 -07:00
|
|
|
return translation.description;
|
|
|
|
},
|
2020-07-02 14:33:47 -07:00
|
|
|
thumbnailUrl: async ({ id, thumbnailUrl }, _, { itemLoader }) => {
|
|
|
|
if (thumbnailUrl) return thumbnailUrl;
|
|
|
|
const item = await itemLoader.load(id);
|
|
|
|
return item.thumbnailUrl;
|
|
|
|
},
|
|
|
|
rarityIndex: async ({ id, rarityIndex }, _, { itemLoader }) => {
|
|
|
|
if (rarityIndex) return rarityIndex;
|
|
|
|
const item = await itemLoader.load(id);
|
|
|
|
return item.rarityIndex;
|
|
|
|
},
|
|
|
|
isNc: async ({ id, rarityIndex }, _, { itemLoader }) => {
|
2020-07-02 20:06:04 -07:00
|
|
|
if (rarityIndex != null) return rarityIndex === 500 || rarityIndex === 0;
|
|
|
|
const item = await itemLoader.load(id);
|
2020-07-02 14:33:47 -07:00
|
|
|
return item.rarityIndex === 500 || item.rarityIndex === 0;
|
|
|
|
},
|
2020-04-23 14:23:46 -07:00
|
|
|
appearanceOn: async (
|
2020-07-02 14:33:47 -07:00
|
|
|
{ id },
|
2020-04-23 14:23:46 -07:00
|
|
|
{ speciesId, colorId },
|
2020-08-28 00:10:00 -07:00
|
|
|
{ petTypeBySpeciesAndColorLoader }
|
2020-04-23 14:23:46 -07:00
|
|
|
) => {
|
2020-06-24 19:05:07 -07:00
|
|
|
const petType = await petTypeBySpeciesAndColorLoader.load({
|
2020-08-28 00:10:00 -07:00
|
|
|
speciesId,
|
|
|
|
colorId,
|
2020-04-23 01:08:00 -07:00
|
|
|
});
|
2020-08-28 00:10:00 -07:00
|
|
|
return { item: { id }, bodyId: petType.bodyId };
|
2020-04-23 01:08:00 -07:00
|
|
|
},
|
2020-07-31 22:31:28 -07:00
|
|
|
manualSpecialColor: async ({ id }, _, { itemLoader }) => {
|
|
|
|
const item = await itemLoader.load(id);
|
|
|
|
return item.manualSpecialColorId != null
|
|
|
|
? { id: item.manualSpecialColorId }
|
|
|
|
: null;
|
|
|
|
},
|
2020-08-14 21:12:13 -07:00
|
|
|
explicitlyBodySpecific: async ({ id }, _, { itemLoader }) => {
|
|
|
|
const item = await itemLoader.load(id);
|
|
|
|
return item.explicitlyBodySpecific;
|
|
|
|
},
|
2020-04-23 01:08:00 -07:00
|
|
|
},
|
2020-08-28 00:10:00 -07:00
|
|
|
ItemAppearance: {
|
|
|
|
id: ({ item, bodyId }) => `item-${item.id}-body-${bodyId}`,
|
|
|
|
layers: async ({ item, bodyId }, _, { itemSwfAssetLoader }) => {
|
|
|
|
const allSwfAssets = await itemSwfAssetLoader.load({
|
|
|
|
itemId: item.id,
|
|
|
|
bodyId,
|
|
|
|
});
|
|
|
|
|
|
|
|
return allSwfAssets.filter((sa) => sa.url.endsWith(".swf"));
|
|
|
|
},
|
|
|
|
restrictedZones: async ({ item: { id: itemId } }, _, { itemLoader }) => {
|
|
|
|
const item = await itemLoader.load(itemId);
|
2020-08-31 19:23:56 -07:00
|
|
|
return getRestrictedZoneIds(item.zonesRestrict).map((id) => ({ id }));
|
2020-08-28 00:10:00 -07:00
|
|
|
},
|
|
|
|
},
|
2020-05-02 16:49:57 -07:00
|
|
|
PetAppearance: {
|
2020-08-27 21:32:22 -07:00
|
|
|
color: async ({ id }, _, { petStateLoader, petTypeLoader }) => {
|
|
|
|
const petState = await petStateLoader.load(id);
|
2020-06-24 19:05:07 -07:00
|
|
|
const petType = await petTypeLoader.load(petState.petTypeId);
|
|
|
|
return { id: petType.colorId };
|
2020-05-02 20:48:32 -07:00
|
|
|
},
|
2020-08-27 21:32:22 -07:00
|
|
|
species: async ({ id }, _, { petStateLoader, petTypeLoader }) => {
|
|
|
|
const petState = await petStateLoader.load(id);
|
2020-06-24 19:05:07 -07:00
|
|
|
const petType = await petTypeLoader.load(petState.petTypeId);
|
|
|
|
return { id: petType.speciesId };
|
|
|
|
},
|
2020-08-27 21:32:22 -07:00
|
|
|
bodyId: async ({ id }, _, { petStateLoader, petTypeLoader }) => {
|
|
|
|
const petState = await petStateLoader.load(id);
|
2020-06-24 19:05:07 -07:00
|
|
|
const petType = await petTypeLoader.load(petState.petTypeId);
|
|
|
|
return petType.bodyId;
|
|
|
|
},
|
2020-08-27 21:32:22 -07:00
|
|
|
pose: async ({ id }, _, { petStateLoader }) => {
|
|
|
|
const petState = await petStateLoader.load(id);
|
2020-06-24 19:05:07 -07:00
|
|
|
return getPoseFromPetState(petState);
|
|
|
|
},
|
2020-08-27 21:32:22 -07:00
|
|
|
layers: async ({ id }, _, { petSwfAssetLoader }) => {
|
|
|
|
const swfAssets = await petSwfAssetLoader.load(id);
|
2020-05-02 16:49:57 -07:00
|
|
|
return swfAssets;
|
|
|
|
},
|
2020-08-31 23:18:30 -07:00
|
|
|
restrictedZones: async ({ id }, _, { petSwfAssetLoader }) => {
|
|
|
|
// The restricted zones are defined on the layers. Load them and aggegate
|
|
|
|
// the zones, then uniquify and sort them for ease of use.
|
|
|
|
const swfAssets = await petSwfAssetLoader.load(id);
|
|
|
|
let restrictedZoneIds = swfAssets
|
|
|
|
.map((sa) => getRestrictedZoneIds(sa.zonesRestrict))
|
|
|
|
.flat();
|
|
|
|
restrictedZoneIds = [...new Set(restrictedZoneIds)];
|
|
|
|
restrictedZoneIds.sort((a, b) => parseInt(a) - parseInt(b));
|
|
|
|
return restrictedZoneIds.map((id) => ({ id }));
|
|
|
|
},
|
2020-08-27 21:32:22 -07:00
|
|
|
petStateId: ({ id }) => id,
|
2020-08-28 22:58:39 -07:00
|
|
|
isGlitched: async ({ id }, _, { petStateLoader }) => {
|
|
|
|
const petState = await petStateLoader.load(id);
|
|
|
|
return petState.glitched;
|
|
|
|
},
|
2020-05-02 16:49:57 -07:00
|
|
|
},
|
2020-04-23 14:23:46 -07:00
|
|
|
AppearanceLayer: {
|
2020-08-01 15:30:26 -07:00
|
|
|
bodyId: async ({ id }, _, { swfAssetLoader }) => {
|
2020-08-14 22:09:52 -07:00
|
|
|
const layer = await swfAssetLoader.load(id);
|
|
|
|
return layer.remoteId;
|
|
|
|
},
|
|
|
|
bodyId: async ({ id }, _, { swfAssetLoader }) => {
|
2020-08-01 15:30:26 -07:00
|
|
|
const layer = await swfAssetLoader.load(id);
|
|
|
|
return layer.bodyId;
|
|
|
|
},
|
2020-08-17 18:49:37 -07:00
|
|
|
zone: async ({ id }, _, { swfAssetLoader, zoneLoader }) => {
|
2020-08-01 15:30:26 -07:00
|
|
|
const layer = await swfAssetLoader.load(id);
|
2020-08-19 17:50:05 -07:00
|
|
|
return { id: layer.zoneId };
|
2020-04-23 01:08:00 -07:00
|
|
|
},
|
2020-08-31 19:23:56 -07:00
|
|
|
restrictedZones: async ({ id }, _, { swfAssetLoader }) => {
|
|
|
|
const layer = await swfAssetLoader.load(id);
|
|
|
|
return getRestrictedZoneIds(layer.zonesRestrict).map((id) => ({ id }));
|
|
|
|
},
|
2020-08-01 22:54:30 -07:00
|
|
|
swfUrl: async ({ id }, _, { swfAssetLoader }) => {
|
|
|
|
const layer = await swfAssetLoader.load(id);
|
|
|
|
return layer.url;
|
|
|
|
},
|
2020-08-01 15:30:26 -07:00
|
|
|
imageUrl: async ({ id }, { size }, { swfAssetLoader }) => {
|
|
|
|
const layer = await swfAssetLoader.load(id);
|
|
|
|
|
2020-04-23 01:08:00 -07:00
|
|
|
if (!layer.hasImage) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
const sizeNum = size.split("_")[1];
|
|
|
|
|
|
|
|
const rid = layer.remoteId;
|
|
|
|
const paddedId = rid.padStart(12, "0");
|
|
|
|
const rid1 = paddedId.slice(0, 3);
|
|
|
|
const rid2 = paddedId.slice(3, 6);
|
|
|
|
const rid3 = paddedId.slice(6, 9);
|
|
|
|
const time = Number(new Date(layer.convertedAt));
|
|
|
|
|
2020-04-23 14:23:46 -07:00
|
|
|
return (
|
|
|
|
`https://impress-asset-images.s3.amazonaws.com/${layer.type}` +
|
2020-04-24 21:38:18 -07:00
|
|
|
`/${rid1}/${rid2}/${rid3}/${rid}/${sizeNum}x${sizeNum}.png?v2-${time}`
|
2020-04-23 14:23:46 -07:00
|
|
|
);
|
2020-04-23 01:08:00 -07:00
|
|
|
},
|
2020-08-17 17:50:01 -07:00
|
|
|
svgUrl: async ({ id }, _, { db, swfAssetLoader, svgLogger }) => {
|
2020-08-01 15:30:26 -07:00
|
|
|
const layer = await swfAssetLoader.load(id);
|
2020-08-17 17:50:01 -07:00
|
|
|
let manifest = layer.manifest && JSON.parse(layer.manifest);
|
|
|
|
|
|
|
|
// When the manifest is specifically null, that means we don't know if
|
|
|
|
// it exists yet. Load it to find out!
|
|
|
|
if (manifest === null) {
|
|
|
|
manifest = await neopets.loadAssetManifest(layer.url);
|
|
|
|
|
|
|
|
// Then, write the new manifest. We make sure to write an empty string
|
|
|
|
// if there was no manifest, to signify that it doesn't exist, so we
|
|
|
|
// don't need to bother looking it up again.
|
|
|
|
//
|
|
|
|
// TODO: Someday the manifests will all exist, right? So we'll want to
|
|
|
|
// reload all the missing ones at that time.
|
|
|
|
manifest = manifest || "";
|
|
|
|
const [
|
|
|
|
result,
|
|
|
|
] = await db.execute(
|
|
|
|
`UPDATE swf_assets SET manifest = ? WHERE id = ? LIMIT 1;`,
|
|
|
|
[manifest, layer.id]
|
|
|
|
);
|
|
|
|
if (result.affectedRows !== 1) {
|
|
|
|
throw new Error(
|
|
|
|
`Expected to affect 1 asset, but affected ${result.affectedRows}`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
console.log(
|
|
|
|
`Loaded and saved manifest for ${layer.type} ${layer.remoteId}. ` +
|
|
|
|
`DTI ID: ${layer.id}. Exists?: ${Boolean(manifest)}`
|
|
|
|
);
|
|
|
|
}
|
2020-08-01 15:30:26 -07:00
|
|
|
|
2020-05-11 21:19:34 -07:00
|
|
|
if (!manifest) {
|
2020-05-23 11:32:05 -07:00
|
|
|
svgLogger.log("no-manifest");
|
2020-05-11 21:19:34 -07:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (manifest.assets.length !== 1) {
|
2020-05-23 11:32:05 -07:00
|
|
|
svgLogger.log(`wrong-asset-count:${manifest.assets.length}!=1`);
|
2020-05-11 21:19:34 -07:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
const asset = manifest.assets[0];
|
|
|
|
if (asset.format !== "vector") {
|
2020-05-23 11:32:05 -07:00
|
|
|
svgLogger.log(`wrong-asset-format:${asset.format}`);
|
2020-05-11 21:19:34 -07:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (asset.assetData.length !== 1) {
|
2020-05-23 11:32:05 -07:00
|
|
|
svgLogger.log(`wrong-assetData-length:${asset.assetData.length}!=1`);
|
2020-05-11 21:19:34 -07:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2020-05-23 11:32:05 -07:00
|
|
|
svgLogger.log("success");
|
2020-05-11 21:19:34 -07:00
|
|
|
const assetDatum = asset.assetData[0];
|
|
|
|
const url = new URL(assetDatum.path, "http://images.neopets.com");
|
|
|
|
return url.toString();
|
|
|
|
},
|
2020-08-01 15:30:26 -07:00
|
|
|
item: async ({ id }, _, { db }) => {
|
|
|
|
// TODO: If this becomes a popular request, we'll definitely need to
|
|
|
|
// loaderize this! I'm cheating for now because it's just Support, one at
|
|
|
|
// a time.
|
|
|
|
const [rows] = await db.query(
|
|
|
|
`
|
|
|
|
SELECT parent_id FROM parents_swf_assets
|
|
|
|
WHERE swf_asset_id = ? AND parent_type = "Item" LIMIT 1;
|
|
|
|
`,
|
|
|
|
[id]
|
|
|
|
);
|
|
|
|
|
|
|
|
if (rows.length === 0) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return { id: String(rows[0].parent_id) };
|
|
|
|
},
|
2020-04-23 01:08:00 -07:00
|
|
|
},
|
|
|
|
Zone: {
|
2020-08-17 18:49:37 -07:00
|
|
|
depth: async ({ id }, _, { zoneLoader }) => {
|
|
|
|
// TODO: Should we extend this loader-in-field pattern elsewhere? I like
|
|
|
|
// that we avoid the fetch in cases where we only want the zone ID,
|
|
|
|
// but it adds complexity 🤔
|
|
|
|
const zone = await zoneLoader.load(id);
|
|
|
|
return zone.depth;
|
|
|
|
},
|
|
|
|
label: async ({ id }, _, { zoneTranslationLoader }) => {
|
|
|
|
const zoneTranslation = await zoneTranslationLoader.load(id);
|
|
|
|
return zoneTranslation.label;
|
|
|
|
},
|
2020-04-22 11:51:36 -07:00
|
|
|
},
|
2020-04-25 03:42:05 -07:00
|
|
|
Color: {
|
2020-06-24 19:05:07 -07:00
|
|
|
name: async ({ id }, _, { colorTranslationLoader }) => {
|
|
|
|
const colorTranslation = await colorTranslationLoader.load(id);
|
2020-04-25 04:33:05 -07:00
|
|
|
return capitalize(colorTranslation.name);
|
2020-04-25 03:42:05 -07:00
|
|
|
},
|
2020-07-31 22:11:32 -07:00
|
|
|
isStandard: async ({ id }, _, { colorLoader }) => {
|
|
|
|
const color = await colorLoader.load(id);
|
|
|
|
return color.standard ? true : false;
|
|
|
|
},
|
2020-04-25 03:42:05 -07:00
|
|
|
},
|
|
|
|
Species: {
|
2020-06-24 19:05:07 -07:00
|
|
|
name: async ({ id }, _, { speciesTranslationLoader }) => {
|
|
|
|
const speciesTranslation = await speciesTranslationLoader.load(id);
|
2020-04-25 04:33:05 -07:00
|
|
|
return capitalize(speciesTranslation.name);
|
2020-04-25 03:42:05 -07:00
|
|
|
},
|
2020-08-31 18:25:42 -07:00
|
|
|
standardBodyId: async ({ id }, _, { petTypeBySpeciesAndColorLoader }) => {
|
|
|
|
const petType = await petTypeBySpeciesAndColorLoader.load({
|
|
|
|
speciesId: id,
|
|
|
|
colorId: "8", // Blue
|
|
|
|
});
|
|
|
|
return petType.bodyId;
|
|
|
|
},
|
2020-04-25 03:42:05 -07:00
|
|
|
},
|
2020-06-24 19:05:07 -07:00
|
|
|
Outfit: {
|
|
|
|
name: async ({ id }, _, { outfitLoader }) => {
|
|
|
|
const outfit = await outfitLoader.load(id);
|
|
|
|
return outfit.name;
|
|
|
|
},
|
|
|
|
petAppearance: async ({ id }, _, { outfitLoader }) => {
|
|
|
|
const outfit = await outfitLoader.load(id);
|
2020-08-27 21:32:22 -07:00
|
|
|
return { id: outfit.petStateId };
|
2020-06-24 19:05:07 -07:00
|
|
|
},
|
|
|
|
wornItems: async ({ id }, _, { itemOutfitRelationshipsLoader }) => {
|
|
|
|
const relationships = await itemOutfitRelationshipsLoader.load(id);
|
|
|
|
return relationships
|
|
|
|
.filter((oir) => oir.isWorn)
|
|
|
|
.map((oir) => ({ id: oir.itemId }));
|
|
|
|
},
|
|
|
|
closetedItems: async ({ id }, _, { itemOutfitRelationshipsLoader }) => {
|
|
|
|
const relationships = await itemOutfitRelationshipsLoader.load(id);
|
|
|
|
return relationships
|
|
|
|
.filter((oir) => !oir.isWorn)
|
|
|
|
.map((oir) => ({ id: oir.itemId }));
|
|
|
|
},
|
|
|
|
},
|
2020-04-22 11:51:36 -07:00
|
|
|
Query: {
|
2020-07-31 22:11:32 -07:00
|
|
|
allColors: async (_, { ids }, { colorLoader }) => {
|
|
|
|
const allColors = await colorLoader.loadAll();
|
2020-04-25 03:42:05 -07:00
|
|
|
return allColors;
|
|
|
|
},
|
2020-08-31 18:25:42 -07:00
|
|
|
allSpecies: async (_, { ids }, { speciesLoader }) => {
|
|
|
|
const allSpecies = await speciesLoader.loadAll();
|
2020-04-25 03:42:05 -07:00
|
|
|
return allSpecies;
|
|
|
|
},
|
|
|
|
allValidSpeciesColorPairs: async (_, __, { loadAllPetTypes }) => {
|
|
|
|
const allPetTypes = await loadAllPetTypes();
|
|
|
|
const allPairs = allPetTypes.map((pt) => ({
|
|
|
|
color: { id: pt.colorId },
|
|
|
|
species: { id: pt.speciesId },
|
|
|
|
}));
|
|
|
|
return allPairs;
|
|
|
|
},
|
2020-07-31 22:31:28 -07:00
|
|
|
item: (_, { id }) => ({ id }),
|
2020-07-02 20:06:04 -07:00
|
|
|
items: (_, { ids }) => {
|
2020-07-22 23:08:28 -07:00
|
|
|
return ids.map((id) => ({ id }));
|
2020-04-24 21:17:03 -07:00
|
|
|
},
|
|
|
|
itemSearch: async (_, { query }, { itemSearchLoader }) => {
|
2020-05-31 15:52:54 -07:00
|
|
|
const items = await itemSearchLoader.load(query.trim());
|
2020-04-25 01:55:48 -07:00
|
|
|
return { query, items };
|
2020-04-25 00:43:01 -07:00
|
|
|
},
|
|
|
|
itemSearchToFit: async (
|
|
|
|
_,
|
2020-04-25 01:55:48 -07:00
|
|
|
{ query, speciesId, colorId, offset, limit },
|
2020-06-24 19:05:07 -07:00
|
|
|
{ petTypeBySpeciesAndColorLoader, itemSearchToFitLoader }
|
2020-04-25 00:43:01 -07:00
|
|
|
) => {
|
2020-06-24 19:05:07 -07:00
|
|
|
const petType = await petTypeBySpeciesAndColorLoader.load({
|
|
|
|
speciesId,
|
|
|
|
colorId,
|
|
|
|
});
|
2020-04-25 00:43:01 -07:00
|
|
|
const { bodyId } = petType;
|
2020-04-25 01:55:48 -07:00
|
|
|
const items = await itemSearchToFitLoader.load({
|
2020-05-31 15:52:54 -07:00
|
|
|
query: query.trim(),
|
2020-04-25 01:55:48 -07:00
|
|
|
bodyId,
|
|
|
|
offset,
|
|
|
|
limit,
|
|
|
|
});
|
|
|
|
return { query, items };
|
2020-04-23 01:08:00 -07:00
|
|
|
},
|
2020-08-28 22:58:39 -07:00
|
|
|
petAppearanceById: (_, { id }) => ({ id }),
|
2020-04-23 14:23:46 -07:00
|
|
|
petAppearance: async (
|
|
|
|
_,
|
2020-05-23 12:47:06 -07:00
|
|
|
{ speciesId, colorId, pose },
|
2020-06-24 19:05:07 -07:00
|
|
|
{ petTypeBySpeciesAndColorLoader, petStatesForPetTypeLoader }
|
2020-04-23 14:23:46 -07:00
|
|
|
) => {
|
2020-06-24 19:05:07 -07:00
|
|
|
const petType = await petTypeBySpeciesAndColorLoader.load({
|
2020-04-23 14:23:46 -07:00
|
|
|
speciesId,
|
|
|
|
colorId,
|
|
|
|
});
|
2020-05-02 22:32:08 -07:00
|
|
|
|
2020-08-28 22:58:39 -07:00
|
|
|
// TODO: We could query for this more directly, instead of loading all
|
|
|
|
// appearances 🤔
|
2020-06-24 19:05:07 -07:00
|
|
|
const petStates = await petStatesForPetTypeLoader.load(petType.id);
|
2020-08-31 00:37:12 -07:00
|
|
|
const petState = petStates.find((ps) => getPoseFromPetState(ps) === pose);
|
2020-05-02 22:32:08 -07:00
|
|
|
if (!petState) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2020-08-27 21:32:22 -07:00
|
|
|
return { id: petState.id };
|
2020-05-02 16:49:57 -07:00
|
|
|
},
|
|
|
|
petAppearances: async (
|
|
|
|
_,
|
|
|
|
{ speciesId, colorId },
|
2020-06-24 19:05:07 -07:00
|
|
|
{ petTypeBySpeciesAndColorLoader, petStatesForPetTypeLoader }
|
2020-05-02 16:49:57 -07:00
|
|
|
) => {
|
2020-06-24 19:05:07 -07:00
|
|
|
const petType = await petTypeBySpeciesAndColorLoader.load({
|
2020-05-02 16:49:57 -07:00
|
|
|
speciesId,
|
|
|
|
colorId,
|
|
|
|
});
|
2020-06-24 19:05:07 -07:00
|
|
|
const petStates = await petStatesForPetTypeLoader.load(petType.id);
|
2020-08-27 21:32:22 -07:00
|
|
|
return petStates.map((petState) => ({ id: petState.id }));
|
2020-04-23 14:23:46 -07:00
|
|
|
},
|
2020-06-24 19:05:07 -07:00
|
|
|
outfit: (_, { id }) => ({ id }),
|
2020-04-25 06:50:34 -07:00
|
|
|
petOnNeopetsDotCom: async (_, { petName }) => {
|
2020-05-23 13:55:59 -07:00
|
|
|
const [petMetaData, customPetData] = await Promise.all([
|
|
|
|
neopets.loadPetMetaData(petName),
|
|
|
|
neopets.loadCustomPetData(petName),
|
|
|
|
]);
|
2020-04-25 06:50:34 -07:00
|
|
|
const outfit = {
|
2020-06-24 19:05:07 -07:00
|
|
|
// TODO: This isn't a fully-working Outfit object. It works for the
|
|
|
|
// client as currently implemented, but we'll probably want to
|
|
|
|
// move the client and this onto our more generic fields!
|
2020-05-23 13:55:59 -07:00
|
|
|
species: { id: customPetData.custom_pet.species_id },
|
|
|
|
color: { id: customPetData.custom_pet.color_id },
|
|
|
|
pose: getPoseFromPetData(petMetaData, customPetData),
|
|
|
|
items: Object.values(customPetData.object_info_registry).map((o) => ({
|
2020-04-25 06:50:34 -07:00
|
|
|
id: o.obj_info_id,
|
2020-07-02 14:33:47 -07:00
|
|
|
name: o.name,
|
|
|
|
description: o.description,
|
|
|
|
thumbnailUrl: o.thumbnail_url,
|
|
|
|
rarityIndex: o.rarity_index,
|
2020-04-25 06:50:34 -07:00
|
|
|
})),
|
|
|
|
};
|
|
|
|
return outfit;
|
|
|
|
},
|
2020-08-31 18:25:42 -07:00
|
|
|
color: async (_, { id }, { colorLoader }) => {
|
|
|
|
const color = await colorLoader.load(id);
|
|
|
|
if (!color) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return { id };
|
|
|
|
},
|
|
|
|
species: async (_, { id }, { speciesLoader }) => {
|
|
|
|
const species = await speciesLoader.load(id);
|
|
|
|
if (!species) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return { id };
|
|
|
|
},
|
2020-04-22 11:51:36 -07:00
|
|
|
},
|
2020-08-01 00:04:11 -07:00
|
|
|
Mutation: {
|
|
|
|
setManualSpecialColor: async (
|
|
|
|
_,
|
|
|
|
{ itemId, colorId, supportSecret },
|
2020-08-20 22:25:41 -07:00
|
|
|
{ itemLoader, itemTranslationLoader, colorTranslationLoader, db }
|
2020-08-01 00:04:11 -07:00
|
|
|
) => {
|
|
|
|
if (supportSecret !== process.env["SUPPORT_SECRET"]) {
|
|
|
|
throw new Error(`Support secret is incorrect. Try setting up again?`);
|
|
|
|
}
|
|
|
|
|
2020-08-20 23:16:59 -07:00
|
|
|
const oldItem = await itemLoader.load(itemId);
|
2020-08-20 22:25:41 -07:00
|
|
|
|
2020-08-01 00:04:11 -07:00
|
|
|
const [
|
|
|
|
result,
|
|
|
|
] = await db.execute(
|
|
|
|
`UPDATE items SET manual_special_color_id = ? WHERE id = ? LIMIT 1`,
|
|
|
|
[colorId, itemId]
|
|
|
|
);
|
|
|
|
|
|
|
|
if (result.affectedRows !== 1) {
|
|
|
|
throw new Error(
|
|
|
|
`Expected to affect 1 item, but affected ${result.affectedRows}`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-08-20 23:16:59 -07:00
|
|
|
itemLoader.clear(itemId); // we changed the item, so clear it from cache
|
|
|
|
|
2020-08-20 22:25:41 -07:00
|
|
|
if (process.env["SUPPORT_TOOLS_DISCORD_WEBHOOK_URL"]) {
|
|
|
|
try {
|
|
|
|
const [
|
|
|
|
itemTranslation,
|
|
|
|
oldColorTranslation,
|
|
|
|
newColorTranslation,
|
|
|
|
] = await Promise.all([
|
|
|
|
itemTranslationLoader.load(itemId),
|
2020-08-20 23:16:59 -07:00
|
|
|
oldItem.manualSpecialColorId
|
|
|
|
? colorTranslationLoader.load(oldItem.manualSpecialColorId)
|
2020-08-20 22:25:41 -07:00
|
|
|
: Promise.resolve(null),
|
|
|
|
colorId
|
|
|
|
? colorTranslationLoader.load(colorId)
|
|
|
|
: Promise.resolve(null),
|
|
|
|
]);
|
|
|
|
|
|
|
|
const oldColorName = oldColorTranslation
|
|
|
|
? capitalize(oldColorTranslation.name)
|
|
|
|
: "Auto-detect";
|
|
|
|
const newColorName = newColorTranslation
|
|
|
|
? capitalize(newColorTranslation.name)
|
|
|
|
: "Auto-detect";
|
|
|
|
await logToDiscord({
|
|
|
|
embeds: [
|
|
|
|
{
|
|
|
|
title: `🛠 ${itemTranslation.name}`,
|
|
|
|
thumbnail: {
|
2020-08-20 23:16:59 -07:00
|
|
|
url: oldItem.thumbnailUrl,
|
2020-08-20 22:25:41 -07:00
|
|
|
height: 80,
|
|
|
|
width: 80,
|
|
|
|
},
|
|
|
|
fields: [
|
|
|
|
{
|
|
|
|
name: "Special color",
|
|
|
|
value: `${oldColorName} → **${newColorName}**`,
|
|
|
|
},
|
|
|
|
],
|
|
|
|
timestamp: new Date().toISOString(),
|
2020-08-20 23:16:59 -07:00
|
|
|
url: `https://impress.openneo.net/items/${oldItem.id}`,
|
2020-08-20 22:25:41 -07:00
|
|
|
},
|
|
|
|
],
|
|
|
|
});
|
|
|
|
} catch (e) {
|
|
|
|
console.error("Error sending Discord support log", e);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
console.warn("No Discord support webhook provided, skipping");
|
|
|
|
}
|
|
|
|
|
2020-08-01 00:04:11 -07:00
|
|
|
return { id: itemId };
|
|
|
|
},
|
2020-08-01 15:30:26 -07:00
|
|
|
|
2020-08-14 22:01:27 -07:00
|
|
|
setItemExplicitlyBodySpecific: async (
|
|
|
|
_,
|
|
|
|
{ itemId, explicitlyBodySpecific, supportSecret },
|
2020-08-20 23:16:59 -07:00
|
|
|
{ itemLoader, itemTranslationLoader, db }
|
2020-08-14 22:01:27 -07:00
|
|
|
) => {
|
|
|
|
if (supportSecret !== process.env["SUPPORT_SECRET"]) {
|
|
|
|
throw new Error(`Support secret is incorrect. Try setting up again?`);
|
|
|
|
}
|
|
|
|
|
2020-08-20 23:16:59 -07:00
|
|
|
const oldItem = await itemLoader.load(itemId);
|
|
|
|
|
2020-08-14 22:01:27 -07:00
|
|
|
const [
|
|
|
|
result,
|
|
|
|
] = await db.execute(
|
|
|
|
`UPDATE items SET explicitly_body_specific = ? WHERE id = ? LIMIT 1`,
|
|
|
|
[explicitlyBodySpecific ? 1 : 0, itemId]
|
|
|
|
);
|
|
|
|
|
|
|
|
if (result.affectedRows !== 1) {
|
|
|
|
throw new Error(
|
|
|
|
`Expected to affect 1 item, but affected ${result.affectedRows}`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-08-20 23:16:59 -07:00
|
|
|
itemLoader.clear(itemId); // we changed the item, so clear it from cache
|
|
|
|
|
|
|
|
if (process.env["SUPPORT_TOOLS_DISCORD_WEBHOOK_URL"]) {
|
|
|
|
try {
|
|
|
|
const itemTranslation = await itemTranslationLoader.load(itemId);
|
|
|
|
const oldRuleName = oldItem.explicitlyBodySpecific
|
|
|
|
? "Body specific"
|
|
|
|
: "Auto-detect";
|
|
|
|
const newRuleName = explicitlyBodySpecific
|
|
|
|
? "Body specific"
|
|
|
|
: "Auto-detect";
|
|
|
|
await logToDiscord({
|
|
|
|
embeds: [
|
|
|
|
{
|
|
|
|
title: `🛠 ${itemTranslation.name}`,
|
|
|
|
thumbnail: {
|
|
|
|
url: oldItem.thumbnailUrl,
|
|
|
|
height: 80,
|
|
|
|
width: 80,
|
|
|
|
},
|
|
|
|
fields: [
|
|
|
|
{
|
|
|
|
name: "Pet compatibility rule",
|
|
|
|
value: `${oldRuleName} → **${newRuleName}**`,
|
|
|
|
},
|
|
|
|
],
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
url: `https://impress.openneo.net/items/${oldItem.id}`,
|
|
|
|
},
|
|
|
|
],
|
|
|
|
});
|
|
|
|
} catch (e) {
|
|
|
|
console.error("Error sending Discord support log", e);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
console.warn("No Discord support webhook provided, skipping");
|
|
|
|
}
|
|
|
|
|
2020-08-14 22:01:27 -07:00
|
|
|
return { id: itemId };
|
|
|
|
},
|
|
|
|
|
2020-08-20 23:16:59 -07:00
|
|
|
setLayerBodyId: async (
|
|
|
|
_,
|
|
|
|
{ layerId, bodyId, supportSecret },
|
|
|
|
{
|
|
|
|
itemLoader,
|
|
|
|
itemTranslationLoader,
|
|
|
|
swfAssetLoader,
|
|
|
|
zoneTranslationLoader,
|
|
|
|
db,
|
|
|
|
}
|
|
|
|
) => {
|
2020-08-01 15:30:26 -07:00
|
|
|
if (supportSecret !== process.env["SUPPORT_SECRET"]) {
|
|
|
|
throw new Error(`Support secret is incorrect. Try setting up again?`);
|
|
|
|
}
|
|
|
|
|
2020-08-20 23:16:59 -07:00
|
|
|
const oldSwfAsset = await swfAssetLoader.load(layerId);
|
|
|
|
|
2020-08-01 15:30:26 -07:00
|
|
|
const [
|
|
|
|
result,
|
|
|
|
] = await db.execute(
|
|
|
|
`UPDATE swf_assets SET body_id = ? WHERE id = ? LIMIT 1`,
|
|
|
|
[bodyId, layerId]
|
|
|
|
);
|
|
|
|
|
|
|
|
if (result.affectedRows !== 1) {
|
|
|
|
throw new Error(
|
|
|
|
`Expected to affect 1 layer, but affected ${result.affectedRows}`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-08-20 23:16:59 -07:00
|
|
|
swfAssetLoader.clear(layerId); // we changed it, so clear it from cache
|
|
|
|
|
|
|
|
if (process.env["SUPPORT_TOOLS_DISCORD_WEBHOOK_URL"]) {
|
|
|
|
try {
|
|
|
|
const itemId = await db
|
|
|
|
.execute(
|
|
|
|
`SELECT parent_id FROM parents_swf_assets
|
|
|
|
WHERE swf_asset_id = ? AND parent_type = "Item" LIMIT 1;`,
|
|
|
|
[layerId]
|
|
|
|
)
|
|
|
|
.then(([rows]) => normalizeRow(rows[0]).parentId);
|
|
|
|
|
|
|
|
const [
|
|
|
|
item,
|
|
|
|
itemTranslation,
|
|
|
|
zoneTranslation,
|
|
|
|
oldBodyName,
|
|
|
|
newBodyName,
|
|
|
|
] = await Promise.all([
|
|
|
|
itemLoader.load(itemId),
|
|
|
|
itemTranslationLoader.load(itemId),
|
|
|
|
zoneTranslationLoader.load(oldSwfAsset.zoneId),
|
2020-08-20 23:23:33 -07:00
|
|
|
loadBodyName(oldSwfAsset.bodyId, db),
|
|
|
|
loadBodyName(bodyId, db),
|
2020-08-20 23:16:59 -07:00
|
|
|
]);
|
|
|
|
|
|
|
|
await logToDiscord({
|
|
|
|
embeds: [
|
|
|
|
{
|
|
|
|
title: `🛠 ${itemTranslation.name}`,
|
|
|
|
thumbnail: {
|
|
|
|
url: item.thumbnailUrl,
|
|
|
|
height: 80,
|
|
|
|
width: 80,
|
|
|
|
},
|
|
|
|
fields: [
|
|
|
|
{
|
|
|
|
name:
|
|
|
|
`Layer ${layerId} (${zoneTranslation.label}): ` +
|
|
|
|
`Pet compatibility`,
|
|
|
|
value: `${oldBodyName} → **${newBodyName}**`,
|
|
|
|
},
|
|
|
|
],
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
url: `https://impress.openneo.net/items/${itemId}`,
|
|
|
|
},
|
|
|
|
],
|
|
|
|
});
|
|
|
|
} catch (e) {
|
|
|
|
console.error("Error sending Discord support log", e);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
console.warn("No Discord support webhook provided, skipping");
|
|
|
|
}
|
|
|
|
|
2020-08-01 15:30:26 -07:00
|
|
|
return { id: layerId };
|
|
|
|
},
|
2020-08-20 21:40:05 -07:00
|
|
|
|
|
|
|
removeLayerFromItem: async (
|
|
|
|
_,
|
|
|
|
{ layerId, itemId, supportSecret },
|
2020-08-20 23:16:59 -07:00
|
|
|
{
|
|
|
|
itemLoader,
|
|
|
|
itemTranslationLoader,
|
|
|
|
swfAssetLoader,
|
|
|
|
zoneTranslationLoader,
|
|
|
|
db,
|
|
|
|
}
|
2020-08-20 21:40:05 -07:00
|
|
|
) => {
|
|
|
|
if (supportSecret !== process.env["SUPPORT_SECRET"]) {
|
|
|
|
throw new Error(`Support secret is incorrect. Try setting up again?`);
|
|
|
|
}
|
|
|
|
|
2020-08-20 23:16:59 -07:00
|
|
|
const oldSwfAsset = await swfAssetLoader.load(layerId);
|
|
|
|
|
2020-08-20 21:40:05 -07:00
|
|
|
const [result] = await db.execute(
|
|
|
|
`DELETE FROM parents_swf_assets ` +
|
|
|
|
`WHERE swf_asset_id = ? AND parent_type = "Item" AND parent_id = ? ` +
|
|
|
|
`LIMIT 1`,
|
|
|
|
[layerId, itemId]
|
|
|
|
);
|
|
|
|
|
|
|
|
if (result.affectedRows !== 1) {
|
|
|
|
throw new Error(
|
|
|
|
`Expected to affect 1 layer, but affected ${result.affectedRows}`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-08-20 23:16:59 -07:00
|
|
|
swfAssetLoader.clear(layerId); // we changed it, so clear it from cache
|
|
|
|
|
|
|
|
if (process.env["SUPPORT_TOOLS_DISCORD_WEBHOOK_URL"]) {
|
|
|
|
try {
|
2020-08-20 23:23:33 -07:00
|
|
|
const [
|
|
|
|
item,
|
|
|
|
itemTranslation,
|
|
|
|
zoneTranslation,
|
|
|
|
bodyName,
|
|
|
|
] = await Promise.all([
|
2020-08-20 23:16:59 -07:00
|
|
|
itemLoader.load(itemId),
|
|
|
|
itemTranslationLoader.load(itemId),
|
|
|
|
zoneTranslationLoader.load(oldSwfAsset.zoneId),
|
2020-08-20 23:23:33 -07:00
|
|
|
loadBodyName(oldSwfAsset.bodyId, db),
|
2020-08-20 23:16:59 -07:00
|
|
|
]);
|
|
|
|
|
|
|
|
await logToDiscord({
|
|
|
|
embeds: [
|
|
|
|
{
|
|
|
|
title: `🛠 ${itemTranslation.name}`,
|
|
|
|
thumbnail: {
|
|
|
|
url: item.thumbnailUrl,
|
|
|
|
height: 80,
|
|
|
|
width: 80,
|
|
|
|
},
|
|
|
|
fields: [
|
|
|
|
{
|
|
|
|
name: `Layer ${layerId} (${zoneTranslation.label})`,
|
2020-08-20 23:23:33 -07:00
|
|
|
value: `❌ Removed from ${bodyName}`,
|
2020-08-20 23:16:59 -07:00
|
|
|
},
|
|
|
|
],
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
url: `https://impress.openneo.net/items/${itemId}`,
|
|
|
|
},
|
|
|
|
],
|
|
|
|
});
|
|
|
|
} catch (e) {
|
|
|
|
console.error("Error sending Discord support log", e);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
console.warn("No Discord support webhook provided, skipping");
|
|
|
|
}
|
|
|
|
|
2020-08-20 21:40:05 -07:00
|
|
|
return { layer: { id: layerId }, item: { id: itemId } };
|
|
|
|
},
|
2020-08-31 00:32:17 -07:00
|
|
|
|
|
|
|
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}`,
|
2020-08-31 00:48:54 -07:00
|
|
|
},
|
|
|
|
],
|
|
|
|
});
|
|
|
|
} catch (e) {
|
|
|
|
console.error("Error sending Discord support log", e);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
console.warn("No Discord support webhook provided, skipping");
|
|
|
|
}
|
|
|
|
|
|
|
|
return { id: appearanceId };
|
|
|
|
},
|
|
|
|
|
|
|
|
setPetAppearanceIsGlitched: async (
|
|
|
|
_,
|
|
|
|
{ appearanceId, isGlitched, 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 [
|
|
|
|
result,
|
|
|
|
] = await db.execute(
|
|
|
|
`UPDATE pet_states SET glitched = ? WHERE id = ? LIMIT 1`,
|
|
|
|
[isGlitched, 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 colorName = capitalize(colorTranslation.name);
|
|
|
|
const speciesName = capitalize(speciesTranslation.name);
|
|
|
|
|
|
|
|
const pose = getPoseFromPetState(oldPetState);
|
|
|
|
const oldGlitchinessState =
|
|
|
|
String(oldPetState.glitched) === "1" ? "Glitched" : "Valid";
|
|
|
|
const newGlitchinessState = isGlitched ? "Glitched" : "Valid";
|
|
|
|
|
|
|
|
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}`,
|
|
|
|
value: `${oldGlitchinessState} → **${newGlitchinessState}**`,
|
|
|
|
},
|
|
|
|
],
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
url: `https://impress-2020.openneo.net/outfits/new?species=${petType.speciesId}&color=${petType.colorId}&pose=${pose}&state=${appearanceId}`,
|
2020-08-31 00:32:17 -07:00
|
|
|
},
|
|
|
|
],
|
|
|
|
});
|
|
|
|
} catch (e) {
|
|
|
|
console.error("Error sending Discord support log", e);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
console.warn("No Discord support webhook provided, skipping");
|
|
|
|
}
|
|
|
|
|
|
|
|
return { id: appearanceId };
|
|
|
|
},
|
2020-08-01 00:04:11 -07:00
|
|
|
},
|
2020-04-22 11:51:36 -07:00
|
|
|
};
|
|
|
|
|
2020-05-23 11:32:05 -07:00
|
|
|
let lastSvgLogger = null;
|
|
|
|
const svgLogging = {
|
|
|
|
requestDidStart() {
|
|
|
|
return {
|
|
|
|
willSendResponse({ operationName }) {
|
|
|
|
const logEntries = lastSvgLogger.entries;
|
|
|
|
if (logEntries.length === 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log(`[svgLogger] Operation: ${operationName}`);
|
|
|
|
|
|
|
|
const logEntryCounts = {};
|
|
|
|
for (const logEntry of logEntries) {
|
|
|
|
logEntryCounts[logEntry] = (logEntryCounts[logEntry] || 0) + 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
const logEntriesSortedByCount = Object.entries(logEntryCounts).sort(
|
|
|
|
(a, b) => b[1] - a[1]
|
|
|
|
);
|
|
|
|
for (const [logEntry, count] of logEntriesSortedByCount) {
|
|
|
|
console.log(`[svgLogger] - ${logEntry}: ${count}`);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
};
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2020-08-16 23:28:41 -07:00
|
|
|
const schema = makeExecutableSchema({ typeDefs, resolvers });
|
2020-08-17 01:27:05 -07:00
|
|
|
const plugins = [svgLogging];
|
|
|
|
|
|
|
|
if (process.env["NODE_ENV"] !== "test") {
|
|
|
|
addBeelineToSchema(schema);
|
|
|
|
plugins.push(beelinePlugin);
|
|
|
|
}
|
2020-08-16 23:28:41 -07:00
|
|
|
|
2020-04-22 13:03:32 -07:00
|
|
|
const config = {
|
2020-08-16 23:28:41 -07:00
|
|
|
schema,
|
2020-04-22 11:51:36 -07:00
|
|
|
context: async () => {
|
|
|
|
const db = await connectToDb();
|
2020-05-23 11:32:05 -07:00
|
|
|
|
|
|
|
const svgLogger = {
|
|
|
|
entries: [],
|
|
|
|
log(entry) {
|
|
|
|
this.entries.push(entry);
|
|
|
|
},
|
|
|
|
};
|
|
|
|
lastSvgLogger = svgLogger;
|
|
|
|
|
2020-04-22 12:00:52 -07:00
|
|
|
return {
|
2020-05-23 11:32:05 -07:00
|
|
|
svgLogger,
|
2020-08-01 00:04:11 -07:00
|
|
|
db,
|
2020-04-23 14:23:46 -07:00
|
|
|
...buildLoaders(db),
|
2020-04-22 12:00:52 -07:00
|
|
|
};
|
2020-04-22 11:51:36 -07:00
|
|
|
},
|
2020-04-23 01:09:17 -07:00
|
|
|
|
2020-08-17 01:27:05 -07:00
|
|
|
plugins,
|
2020-05-23 11:32:05 -07:00
|
|
|
|
2020-04-23 01:09:17 -07:00
|
|
|
// Enable Playground in production :)
|
|
|
|
introspection: true,
|
2020-04-23 01:12:52 -07:00
|
|
|
playground: {
|
|
|
|
endpoint: "/api/graphql",
|
|
|
|
},
|
2020-04-22 13:03:32 -07:00
|
|
|
};
|
2020-04-22 11:51:36 -07:00
|
|
|
|
|
|
|
if (require.main === module) {
|
2020-04-22 13:03:32 -07:00
|
|
|
const { ApolloServer } = require("apollo-server");
|
|
|
|
const server = new ApolloServer(config);
|
2020-04-22 11:51:36 -07:00
|
|
|
server.listen().then(({ url }) => {
|
|
|
|
console.log(`🚀 Server ready at ${url}`);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-04-22 13:03:32 -07:00
|
|
|
module.exports = { config };
|