From 7c4e9258f06aa2d19277db96dd74f0486071c46a Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Sat, 1 Apr 2023 23:37:19 -0700 Subject: [PATCH] Schma parity between generated an Grats --- .../api/graphql/ModernSkinsConnection.ts | 14 +- .../api/graphql/SkinsConnection.ts | 17 +- .../api/graphql/TweetsConnection.ts | 7 +- .../api/graphql/grats-schema.graphql | 308 ++++++++++++++++-- .../graphql/resolvers/ArchiveFileResolver.ts | 50 ++- .../graphql/resolvers/ClassicSkinResolver.ts | 84 +++-- .../graphql/resolvers/CommonSkinResolver.ts | 82 +++-- .../resolvers/DatabaseStatisticsResolver.ts | 66 +++- .../resolvers/InternetArchiveItemResolver.ts | 32 +- .../graphql/resolvers/ModernSkinResolver.ts | 77 +++-- .../api/graphql/resolvers/MutationResolver.ts | 75 ++++- .../api/graphql/resolvers/ReviewResolver.ts | 4 +- .../api/graphql/resolvers/RootResolver.ts | 51 ++- .../api/graphql/resolvers/TweetResolver.ts | 27 +- .../skin-database/api/graphql/schema.graphql | 81 ++--- packages/skin-database/types.ts | 5 +- 16 files changed, 770 insertions(+), 210 deletions(-) diff --git a/packages/skin-database/api/graphql/ModernSkinsConnection.ts b/packages/skin-database/api/graphql/ModernSkinsConnection.ts index b9ee85f7..22cd3878 100644 --- a/packages/skin-database/api/graphql/ModernSkinsConnection.ts +++ b/packages/skin-database/api/graphql/ModernSkinsConnection.ts @@ -3,7 +3,9 @@ import SkinModel from "../../data/SkinModel"; import { knex } from "../../db"; import ModernSkinResolver from "./resolvers/ModernSkinResolver"; -/** @gqlType */ +/** + * A collection of "modern" Winamp skins + * @gqlType */ export default class ModernSkinsConnection { _first: number; _offset: number; @@ -16,14 +18,18 @@ export default class ModernSkinsConnection { return query; } - /** @gqlField */ + /** + * The total number of skins matching the filter + * @gqlField */ async count(): Promise { const count = await this._getQuery().count("*", { as: "count" }); return Number(count[0].count); } - /** @gqlField */ - async nodes(_args: never, ctx): Promise { + /** + * The list of skins + * @gqlField */ + async nodes(_args: never, ctx): Promise> { const skins = await this._getQuery() .select() .limit(this._first) diff --git a/packages/skin-database/api/graphql/SkinsConnection.ts b/packages/skin-database/api/graphql/SkinsConnection.ts index 53a840c0..10658b4d 100644 --- a/packages/skin-database/api/graphql/SkinsConnection.ts +++ b/packages/skin-database/api/graphql/SkinsConnection.ts @@ -36,7 +36,10 @@ async function getSkinMuseumPageFromCache(first: number, offset: number) { return skins; } -/** @gqlType */ +/** + * A collection of classic Winamp skins + * @gqlType + */ export default class SkinsConnection { _first: number; _offset: number; @@ -76,7 +79,10 @@ export default class SkinsConnection { return query; } - /** @gqlField */ + /** + * The total number of skins matching the filter + * @gqlField + */ async count(): Promise { if (this._sort === "MUSEUM") { // This is the common case, so serve it from cache. @@ -86,8 +92,11 @@ export default class SkinsConnection { return Number(count[0].count); } - /** @gqlField */ - async nodes(args: never, ctx): Promise { + /** + * The list of skins + * @gqlField + */ + async nodes(args: never, ctx): Promise> { if (this._sort === "MUSEUM") { if (this._filter) { throw new Error( diff --git a/packages/skin-database/api/graphql/TweetsConnection.ts b/packages/skin-database/api/graphql/TweetsConnection.ts index ee370042..11993b3a 100644 --- a/packages/skin-database/api/graphql/TweetsConnection.ts +++ b/packages/skin-database/api/graphql/TweetsConnection.ts @@ -6,7 +6,10 @@ import TweetResolver from "./resolvers/TweetResolver"; /** @gqlEnum */ export type TweetsSortOption = "LIKES" | "RETWEETS"; -/** @gqlType */ +/** + * A collection of tweets made by the @winampskins bot + * @gqlType + */ export default class TweetsConnection { _first: number; _offset: number; @@ -41,7 +44,7 @@ export default class TweetsConnection { * The list of tweets * @gqlField */ - async nodes(args: never, ctx): Promise { + async nodes(args: never, ctx): Promise> { const tweets = await this._getQuery() .select() .limit(this._first) diff --git a/packages/skin-database/api/graphql/grats-schema.graphql b/packages/skin-database/api/graphql/grats-schema.graphql index c75b2e8e..beaf1d52 100644 --- a/packages/skin-database/api/graphql/grats-schema.graphql +++ b/packages/skin-database/api/graphql/grats-schema.graphql @@ -7,89 +7,245 @@ directive @exported(filename: String!, functionName: String!) on FIELD_DEFINITIO directive @methodName(name: String!) on FIELD_DEFINITION +"""A file found within a Winamp Skin's .wsz archive""" type ArchiveFile { + """ + The date on the file inside the archive. Given in simplified extended ISO + format (ISO 8601). + """ date: String + """The md5 hash of the file within the archive""" file_md5: String + """Filename of the file within the archive""" filename: String + """Is the file a directory?""" is_directory: Boolean + """ + The uncompressed size of the file in bytes. + + **Note:** Will be `null` for directories + """ size: Int + """The skin in which this file was found""" skin: Skin + """The content of the file, if it's a text file""" text_content: String + """ + A URL to download the file. **Note:** This is powered by a little + serverless Cloudflare function which tries to exctact the file on the fly. + It may not work for all files. + """ url: String } +"""A classic Winamp skin""" type ClassicSkin implements Node & Skin { - archive_files: [ArchiveFile!] + """List of files contained within the skin's .wsz archive""" + archive_files: [ArchiveFile] + """String representation (rgb usually) of the skin's average color""" average_color: String + """URL to download the skin""" download_url: String - filename(normalize_extension: Boolean = false): String + """ + Filename of skin when uploaded to the Museum. Note: In some cases a skin + has been uploaded under multiple names. Here we just pick one. + """ + filename( + """ + If true, the the correct file extension (.wsz or .wal) will be . + Otherwise, the original user-uploaded file extension will be used. + """ + normalize_extension: Boolean = false + ): String + """Does the skin include sprite sheets for the media library?""" has_media_library: Boolean + """GraphQL ID of the skin""" id: ID! + """The skin's "item" at archive.org""" internet_archive_item: InternetArchiveItem + """ + The date on which this skin was last updated in the Algolia search index. + Given in simplified extended ISO format (ISO 8601). + """ last_algolia_index_update_date: String + """MD5 hash of the skin's file""" md5: String + """URL of the skin on the Winamp Skin Museum""" museum_url: String + """Has the skin been flagged as "not safe for wrok"?""" nsfw: Boolean + """Text of the readme file extracted from the skin""" readme_text: String - reviews: [Review!] + """ + Times that the skin has been reviewed either on the Museum's Tinder-style + reivew page, or via the Discord bot. + """ + reviews: [Review] + """URL of a screenshot of the skin""" screenshot_url: String + """The number of transparent pixels rendered by the skin.""" transparent_pixels: Int + """Has the skin been tweeted?""" tweeted: Boolean - tweets: [Tweet!] + """List of @winampskins tweets that mentioned the skin.""" + tweets: [Tweet] + """URL of webamp.org with the skin loaded""" webamp_url: String } +"""Statistics about the contents of the Museum's database.""" type DatabaseStatistics { + """ + The number of skins that have been approved for tweeting. This includes both + tweeted and untweeted skins. + + **Note:** Skins can be both approved and rejected by different users. + """ approved_skins_count: Int + """ + The number of skins that have been marked as NSFW. + + **Note:** Skins can be approved and rejected by different users. + **Note:** Generally skins that have been marked NSFW are also marked as rejected. + """ nsfw_skins_count: Int + """ + The number of skins that have been rejected for tweeting. + + **Note:** Skins can be both approved and rejected by different users. + **Note:** Generally skins that have been marked NSFW are also marked as rejected. + """ rejected_skins_count: Int + """ + The number of skins that have been approved for tweeting, but not yet tweeted. + """ tweetable_skins_count: Int + """ + The number of skins in the Museum that have been tweeted by @winampskins + """ tweeted_skins_count: Int + """The total number of classic skins in the Museum's database""" unique_classic_skins_count: Int + """The number of skins that have never been reviewed.""" unreviewed_skins_count: Int + """Skins uploads that have errored during processing.""" uploads_in_error_state_count: Int + """ + Skins uplaods awaiting processing. This can happen when there are a large + number of skin uplaods at the same time, or when the skin uploading processing + pipeline gets stuck. + """ uploads_pending_processing_count: Int + """ + Number of skins that have been uploaded to the Museum via the web interface. + """ web_uploads_count: Int } type InternetArchiveItem { + """The Internet Archive's unique identifier for this item""" identifier: String + """ + The date and time that we last scraped this item's metadata. + **Note:** This field is temporary and will be removed in the future. + The date format is just what we get from the database, and it's ambiguous. + """ last_metadata_scrape_date_UNSTABLE: String + """URL to get the Internet Archive's metadata for this item in JSON form.""" metadata_url: String + """ + Our cached version of the metadata avaliable at \`metadata_url\` (above) + """ raw_metadata_json: String + """The skin that this item contains""" skin: Skin + """The URL where this item can be viewed on the Internet Archive""" url: String } +""" +A "modern" Winamp skin. These skins use the `.wal` file extension and are free-form. + +Most functionality in the Winamp Skin Museum is centered around "classic" skins, +which are currently called just `Skin` in this schema. +""" type ModernSkin implements Node & Skin { - archive_files: [ArchiveFile!] - average_color: String + """List of files contained within the skin's .wsz archive""" + archive_files: [ArchiveFile] + average_color: String @deprecated(reason: "Needed for migration") + """URL to download the skin""" download_url: String - filename(normalize_extension: Boolean = false): String + """ + Filename of skin when uploaded to the Museum. Note: In some cases a skin + has been uploaded under multiple names. Here we just pick one. + """ + filename( + """ + If true, the the correct file extension (.wsz or .wal) will be . + Otherwise, the original user-uploaded file extension will be used. + """ + normalize_extension: Boolean = false + ): String + """GraphQL ID of the skin""" id: ID! + """The skin's "item" at archive.org""" internet_archive_item: InternetArchiveItem + """MD5 hash of the skin's file""" md5: String - museum_url: String - nsfw: Boolean - readme_text: String - reviews: [Review!] - screenshot_url: String + museum_url: String @deprecated(reason: "Needed for migration") + nsfw: Boolean @deprecated(reason: "Needed for migration") + readme_text: String @deprecated(reason: "Needed for migration") + """ + Times that the skin has been reviewed either on the Museum's Tinder-style + reivew page, or via the Discord bot. + """ + reviews: [Review] + screenshot_url: String @deprecated(reason: "Needed for migration") + """Has the skin been tweeted?""" tweeted: Boolean - tweets: [Tweet!] - webamp_url: String + """List of @winampskins tweets that mentioned the skin.""" + tweets: [Tweet] + webamp_url: String @deprecated(reason: "Needed for migration") } +"""A collection of "modern" Winamp skins""" type ModernSkinsConnection { + """The total number of skins matching the filter""" count: Int - nodes: [ModernSkin!] + """The list of skins""" + nodes: [ModernSkin] } type Mutation { + """ + Approve skin for tweeting + + **Note:** Requires being logged in + """ approve_skin(md5: String!): Boolean + """ + Mark a skin as NSFW + + **Note:** Requires being logged in + """ mark_skin_nsfw(md5: String!): Boolean + """ + Reject skin for tweeting + + **Note:** Requires being logged in + """ reject_skin(md5: String!): Boolean + """ + Request that an admin check if this skin is NSFW. + Unlike other review mutaiton endpoints, this one does not require being logged + in. + """ request_nsfw_review_for_skin(md5: String!): Boolean - send_feedback(email: String, message: String, url: String): Boolean + """ + Send a message to the admin of the site. Currently this appears in Discord. + """ + send_feedback(email: String, message: String!, url: String): Boolean + """Mutations for the upload flow""" upload: UploadMutations } @@ -105,11 +261,13 @@ interface Node { type Query { """ Fetch archive file by it's MD5 hash + Get information about a file found within a skin's wsz/wal/zip archive. """ fetch_archive_file_by_md5(md5: String!): ArchiveFile """ Get an archive.org item by its identifier. You can find this in the URL: + https://archive.org/details// """ fetch_internet_archive_item_by_identifier(identifier: String!): InternetArchiveItem @@ -119,9 +277,11 @@ type Query { fetch_tweet_by_url(url: String!): Tweet """The currently authenticated user, if any.""" me: User + """All modern skins in the database""" modern_skins(first: Int = 10, offset: Int = 0): ModernSkinsConnection """ Get a globally unique object by its ID. + https://graphql.org/learn/global-object-identification/ """ node(id: ID!): Node @@ -130,17 +290,26 @@ type Query { Useful for locating a particular skin. """ - search_skins(first: Int = 10, offset: Int = 0, query: String!): [Skin!] + search_skins(first: Int = 10, offset: Int = 0, query: String!): [Skin] + """A random skin that needs to be reviewed""" skin_to_review: Skin + """ + All classic skins in the database + + **Note:** We don't currently support combining sorting and filtering. + """ skins(filter: SkinsFilterOption, first: Int = 10, offset: Int = 0, sort: SkinsSortOption): SkinsConnection + """A namespace for statistics about the database""" statistics: DatabaseStatistics """Tweets tweeted by @winampskins""" tweets(first: Int = 10, offset: Int = 0, sort: TweetsSortOption): TweetsConnection - upload_statuses(ids: [String!]!): [SkinUpload!] + """Get the status of a batch of uploads by ids""" + upload_statuses(ids: [String!]!): [SkinUpload] """Get the status of a batch of uploads by md5s""" - upload_statuses_by_md5(md5s: [String!]!): [SkinUpload!] + upload_statuses_by_md5(md5s: [String!]!): [SkinUpload] @deprecated(reason: "Prefer `upload_statuses` instead, were we operate on ids.") } +"""The judgement made about a skin by a moderator""" enum Rating { APPROVED NSFW @@ -155,41 +324,74 @@ type Review { """The rating that the user gave the skin""" rating: Rating """ - The user who made the review (if known). **Note:** In the early days we - didn't track this, so many will be null. + The user who made the review (if known). **Note:** In the early days we didn't + track this, so many will be null. """ reviewer: String """The skin that was reviewed""" skin: Skin } +""" +A Winamp skin. Could be modern or classic. + +**Note**: At some point in the future, this might be renamed to `Skin`. +""" interface Skin { - archive_files: [ArchiveFile!] - average_color: String @deprecated(reason: "Needed for migration to new skin model") + """List of files contained within the skin's .wsz archive""" + archive_files: [ArchiveFile] + average_color: String @deprecated(reason: "Needed for migration") + """URL to download the skin""" download_url: String - filename(normalize_extension: Boolean = false): String + """ + Filename of skin when uploaded to the Museum. Note: In some cases a skin + has been uploaded under multiple names. Here we just pick one. + """ + filename( + """ + If true, the the correct file extension (.wsz or .wal) will be . + Otherwise, the original user-uploaded file extension will be used. + """ + normalize_extension: Boolean = false + ): String + """GraphQL ID of the skin""" id: ID! + """The skin's "item" at archive.org""" internet_archive_item: InternetArchiveItem + """MD5 hash of the skin's file""" md5: String - museum_url: String @deprecated(reason: "Needed for migration to new skin model") - nsfw: Boolean @deprecated(reason: "Needed for migration to new skin model") - readme_text: String @deprecated(reason: "Needed for migration to new skin model") - reviews: [Review!] - screenshot_url: String @deprecated(reason: "Needed for migration to new skin model") + museum_url: String @deprecated(reason: "Needed for migration") + nsfw: Boolean @deprecated(reason: "Needed for migration") + readme_text: String @deprecated(reason: "Needed for migration") + """ + Times that the skin has been reviewed either on the Museum's Tinder-style + reivew page, or via the Discord bot. + """ + reviews: [Review] + screenshot_url: String @deprecated(reason: "Needed for migration") + """Has the skin been tweeted?""" tweeted: Boolean - tweets: [Tweet!] - webamp_url: String @deprecated(reason: "Needed for migration to new skin model") + """List of @winampskins tweets that mentioned the skin.""" + tweets: [Tweet] + webamp_url: String @deprecated(reason: "Needed for migration") } +"""Information about an attempt to upload a skin to the Museum.""" type SkinUpload { id: String + """ + Skin that was uploaded. **Note:** This is null if the skin has not yet been + fully processed. (status == ARCHIVED) + """ skin: Skin status: SkinUploadStatus + """Md5 hash given when requesting the upload URL.""" upload_md5: String } """ The current status of a pending upload. + **Note:** Expect more values here as we try to be more transparent about the status of a pending uploads. """ @@ -201,9 +403,12 @@ enum SkinUploadStatus { URL_REQUESTED } +"""A collection of classic Winamp skins""" type SkinsConnection { + """The total number of skins matching the filter""" count: Int - nodes: [Skin!] + """The list of skins""" + nodes: [Skin] } enum SkinsFilterOption { @@ -217,18 +422,32 @@ enum SkinsSortOption { MUSEUM } +"""A tweet made by @winampskins mentioning a Winamp skin""" type Tweet { + """ + Number of likes the tweet has received. Updated nightly. (Note: Recent likes on older tweets may not be reflected here) + """ likes: Int + """ + Number of retweets the tweet has received. Updated nightly. (Note: Recent retweets on older tweets may not be reflected here) + """ retweets: Int + """The skin featured in this Tweet""" skin: Skin + """ + URL of the tweet. **Note:** Early on in the bot's life we just recorded + _which_ skins were tweeted, not any info about the actual tweet. This means we + don't always know the URL of the tweet. + """ url: String } +"""A collection of tweets made by the @winampskins bot""" type TweetsConnection { """The total number of tweets""" count: Int """The list of tweets""" - nodes: [Tweet!] + nodes: [Tweet] } enum TweetsSortOption { @@ -236,17 +455,38 @@ enum TweetsSortOption { RETWEETS } +""" +Mutations for the upload flow + +1. The user finds the md5 hash of their local files. +2. (`get_upload_urls`) The user requests upload URLs for each of their files. +3. The server returns upload URLs for each of their files which are not already in the collection. +4. The user uploads each of their files to the URLs returned in step 3. +5. (`report_skin_uploaded`) The user notifies the server that they're done uploading. +6. (TODO) The user polls for the status of their uploads. +""" type UploadMutations { - get_upload_urls(files: [UploadUrlRequest!]!): [UploadUrl!] + """ + Get a (possibly incompelte) list of UploadUrls for each of the files. If an + UploadUrl is not returned for a given hash, it means the file is already in + the collection. + """ + get_upload_urls(files: [UploadUrlRequest!]!): [UploadUrl] + """Notify the server that the user is done uploading.""" report_skin_uploaded(id: String!, md5: String!): Boolean } +""" +A URL that the client can use to upload a skin to S3, and then notify the server +when they're done. +""" type UploadUrl { id: String md5: String url: String } +"""Input object used for a user to request an UploadUrl""" input UploadUrlRequest { filename: String! md5: String! diff --git a/packages/skin-database/api/graphql/resolvers/ArchiveFileResolver.ts b/packages/skin-database/api/graphql/resolvers/ArchiveFileResolver.ts index 8d8291f7..41d5e812 100644 --- a/packages/skin-database/api/graphql/resolvers/ArchiveFileResolver.ts +++ b/packages/skin-database/api/graphql/resolvers/ArchiveFileResolver.ts @@ -4,45 +4,77 @@ import SkinModel from "../../../data/SkinModel"; import { ISkin } from "./CommonSkinResolver"; import SkinResolver from "./SkinResolver"; -/** @gqlType ArchiveFile */ +/** + * A file found within a Winamp Skin's .wsz archive + * @gqlType ArchiveFile + */ export default class ArchiveFileResolver { _model: ArchiveFileModel; constructor(model: ArchiveFileModel) { this._model = model; } - /** @gqlField */ + /** + * Filename of the file within the archive + * @gqlField + */ filename(): string { return this._model.getFileName(); } - /** @gqlField */ + /** + * A URL to download the file. **Note:** This is powered by a little + * serverless Cloudflare function which tries to exctact the file on the fly. + * It may not work for all files. + * @gqlField + */ url(): string { return this._model.getUrl(); } - /** @gqlField */ + /** + * The md5 hash of the file within the archive + * @gqlField + */ file_md5(): string { return this._model.getFileMd5(); } - /** @gqlField */ + /** + * The uncompressed size of the file in bytes. + * + * **Note:** Will be `null` for directories + * @gqlField + */ size(): Promise { return this._model.getFileSize(); } - /** @gqlField */ + /** + * The content of the file, if it's a text file + * @gqlField + */ text_content(): Promise { return this._model.getTextContent(); } - /** @gqlField */ + /** + * Is the file a directory? + * @gqlField + */ is_directory(): boolean { return this._model.getIsDirectory(); } - /** @gqlField */ + /** + * The skin in which this file was found + * @gqlField + */ async skin(_: never, { ctx }): Promise { const model = await SkinModel.fromMd5Assert(ctx, this._model.getMd5()); return SkinResolver.fromModel(model); } - /** @gqlField */ + /** + * The date on the file inside the archive. Given in simplified extended ISO + * format (ISO 8601). + * @gqlField + */ date(): string { return this._model.getFileDate().toISOString(); } diff --git a/packages/skin-database/api/graphql/resolvers/ClassicSkinResolver.ts b/packages/skin-database/api/graphql/resolvers/ClassicSkinResolver.ts index c11cf9b1..da0dd158 100644 --- a/packages/skin-database/api/graphql/resolvers/ClassicSkinResolver.ts +++ b/packages/skin-database/api/graphql/resolvers/ClassicSkinResolver.ts @@ -7,23 +7,31 @@ import TweetResolver from "./TweetResolver"; import ArchiveFileResolver from "./ArchiveFileResolver"; import InternetArchiveItemResolver from "./InternetArchiveItemResolver"; -/** @gqlType ClassicSkin */ +/** + * A classic Winamp skin + * @gqlType ClassicSkin */ export default class ClassicSkinResolver extends CommonSkinResolver implements NodeResolver, ISkin { __typename = "ClassicSkin"; /** + * GraphQL ID of the skin * @gqlField * @killsParentOnException */ id(): ID { return toId(this.__typename, this.md5()); } - /** @gqlField */ + /** + * Filename of skin when uploaded to the Museum. Note: In some cases a skin + * has been uploaded under multiple names. Here we just pick one. + * @gqlField */ async filename({ normalize_extension = false, }: { + /** If true, the the correct file extension (.wsz or .wal) will be . + Otherwise, the original user-uploaded file extension will be used. */ normalize_extension?: boolean; }): Promise { const filename = await this._model.getFileName(); @@ -33,67 +41,99 @@ export default class ClassicSkinResolver return filename; } - /** @gqlField */ + /** + * MD5 hash of the skin's file + * @gqlField */ md5(): string { return super.md5(); } - /** @gqlField */ + /** + * URL to download the skin + * @gqlField */ download_url(): string { return super.download_url(); } - /** @gqlField */ + /** + * Has the skin been tweeted? + * @gqlField */ tweeted(): Promise { return super.tweeted(); } - /** @gqlField */ - tweets(): Promise { + /** + * List of @winampskins tweets that mentioned the skin. + * @gqlField */ + tweets(): Promise> { return super.tweets(); } - /** @gqlField */ - archive_files(): Promise { + /** + * List of files contained within the skin's .wsz archive + * @gqlField */ + archive_files(): Promise> { return super.archive_files(); } - /** @gqlField */ + /** + * The skin's "item" at archive.org + * @gqlField */ internet_archive_item(): Promise { return super.internet_archive_item(); } - /** @gqlField */ + /** + * URL of the skin on the Winamp Skin Museum + * @gqlField */ museum_url(): string { return this._model.getMuseumUrl(); } - /** @gqlField */ + /** + * URL of webamp.org with the skin loaded + * @gqlField */ webamp_url(): string { return this._model.getWebampUrl(); } - /** @gqlField */ + /** + * URL of a screenshot of the skin + * @gqlField */ screenshot_url(): string { return this._model.getScreenshotUrl(); } - /** @gqlField */ + /** + * Text of the readme file extracted from the skin + * @gqlField */ readme_text(): Promise { return this._model.getReadme(); } - /** @gqlField */ + /** + * Has the skin been flagged as "not safe for wrok"? + * @gqlField */ nsfw(): Promise { return this._model.getIsNsfw(); } - /** @gqlField */ + /** + * String representation (rgb usually) of the skin's average color + * @gqlField */ average_color(): string { return this._model.getAverageColor(); } - /** @gqlField */ + /** + * Does the skin include sprite sheets for the media library? + * @gqlField */ has_media_library(): Promise { return this._model.hasMediaLibrary(); } - /** @gqlField */ - async reviews(): Promise { + /** + * Times that the skin has been reviewed either on the Museum's Tinder-style + * reivew page, or via the Discord bot. + * @gqlField */ + async reviews(): Promise> { const reviews = await this._model.getReviews(); return reviews.map((row) => new ReviewResolver(row)); } - /** @gqlField */ + /** + * The date on which this skin was last updated in the Algolia search index. + * Given in simplified extended ISO format (ISO 8601). + * @gqlField */ async last_algolia_index_update_date(): Promise { const updates = await this._model.getAlgoliaIndexUpdates(1); if (updates.length < 1) { @@ -102,7 +142,9 @@ export default class ClassicSkinResolver const update = updates[0]; return new Date(update.update_timestamp * 1000).toISOString(); } - /** @gqlField */ + /** + * The number of transparent pixels rendered by the skin. + * @gqlField */ transparent_pixels(): Promise { return this._model.transparentPixels(); } diff --git a/packages/skin-database/api/graphql/resolvers/CommonSkinResolver.ts b/packages/skin-database/api/graphql/resolvers/CommonSkinResolver.ts index 5528c89b..7def80c1 100644 --- a/packages/skin-database/api/graphql/resolvers/CommonSkinResolver.ts +++ b/packages/skin-database/api/graphql/resolvers/CommonSkinResolver.ts @@ -5,77 +5,109 @@ import InternetArchiveItemResolver from "./InternetArchiveItemResolver"; import ReviewResolver from "./ReviewResolver"; import TweetResolver from "./TweetResolver"; -/** @gqlInterface Skin */ +/** + * A Winamp skin. Could be modern or classic. + * + * **Note**: At some point in the future, this might be renamed to `Skin`. + * @gqlInterface Skin + */ export interface ISkin { __typename: string; /** + * GraphQL ID of the skin * @gqlField * @killsParentOnException */ id(): ID; - /** @gqlField */ + /** + * Filename of skin when uploaded to the Museum. Note: In some cases a skin + * has been uploaded under multiple names. Here we just pick one. + * @gqlField + */ filename({ normalize_extension = false, }: { + /** + * If true, the the correct file extension (.wsz or .wal) will be . + * Otherwise, the original user-uploaded file extension will be used. + */ normalize_extension?: boolean; }): Promise; - /** @gqlField */ + /** + * MD5 hash of the skin's file + * @gqlField + */ md5(): string; - /** @gqlField */ + /** + * URL to download the skin + * @gqlField + */ download_url(): string; - /** @gqlField */ + /** + * Has the skin been tweeted? + * @gqlField + */ tweeted(): Promise; - /** @gqlField */ - tweets(): Promise; + /** + * List of @winampskins tweets that mentioned the skin. + * @gqlField + */ + tweets(): Promise>; - /** @gqlField */ - archive_files(): Promise; + /** + * List of files contained within the skin's .wsz archive + * @gqlField + */ + archive_files(): Promise>; - /** @gqlField */ + /** + * The skin's "item" at archive.org + * @gqlField + */ internet_archive_item(): Promise; - /** @gqlField */ - reviews(): Promise; + /** + * Times that the skin has been reviewed either on the Museum's Tinder-style + * reivew page, or via the Discord bot. + * @gqlField + */ + reviews(): Promise>; /** * @gqlField - * @deprecated Needed for migration to new skin model + * @deprecated Needed for migration */ museum_url(): string | null; /** * @gqlField - * @deprecated Needed for migration to new skin model + * @deprecated Needed for migration */ webamp_url(): string | null; /** * @gqlField - * @deprecated Needed for migration to new skin model - */ + * @deprecated Needed for migration */ screenshot_url(): string | null; /** * @gqlField - * @deprecated Needed for migration to new skin model - */ + * @deprecated Needed for migration */ readme_text(): Promise; /** * @gqlField - * @deprecated Needed for migration to new skin model - */ + * @deprecated Needed for migration */ nsfw(): Promise; /** * @gqlField - * @deprecated Needed for migration to new skin model - */ + * @deprecated Needed for migration */ average_color(): string | null; } @@ -94,11 +126,11 @@ export default class CommonSkinResolver { tweeted(): Promise { return this._model.tweeted(); } - async tweets(): Promise { + async tweets(): Promise> { const tweets = await this._model.getTweets(); return tweets.map((tweetModel) => new TweetResolver(tweetModel)); } - async archive_files(): Promise { + async archive_files(): Promise> { const files = await this._model.getArchiveFiles(); return files.map((file) => new ArchiveFileResolver(file)); } @@ -110,7 +142,7 @@ export default class CommonSkinResolver { return new InternetArchiveItemResolver(item); } - async reviews(): Promise { + async reviews(): Promise> { const reviews = await this._model.getReviews(); return reviews.map((row) => new ReviewResolver(row)); } diff --git a/packages/skin-database/api/graphql/resolvers/DatabaseStatisticsResolver.ts b/packages/skin-database/api/graphql/resolvers/DatabaseStatisticsResolver.ts index d09e60ca..d377e381 100644 --- a/packages/skin-database/api/graphql/resolvers/DatabaseStatisticsResolver.ts +++ b/packages/skin-database/api/graphql/resolvers/DatabaseStatisticsResolver.ts @@ -1,45 +1,89 @@ import { Int } from "grats"; import * as Skins from "../../../data/skins"; -/** @gqlType DatabaseStatistics */ +/** + * Statistics about the contents of the Museum's database. + * @gqlType DatabaseStatistics + */ export default class DatabaseStatisticsResolver { - /** @gqlField */ + /** + * The total number of classic skins in the Museum's database + * @gqlField + */ unique_classic_skins_count(): Promise { return Skins.getClassicSkinCount(); } - /** @gqlField */ + /** + * The number of skins in the Museum that have been tweeted by @winampskins + * @gqlField + */ tweeted_skins_count(): Promise { return Skins.getTweetedSkinCount(); } - /** @gqlField */ + /** + * The number of skins that have been approved for tweeting. This includes both + * tweeted and untweeted skins. + * + * **Note:** Skins can be both approved and rejected by different users. + * @gqlField + */ approved_skins_count(): Promise { return Skins.getApprovedSkinCount(); } - /** @gqlField */ + /** + * The number of skins that have been rejected for tweeting. + * + * **Note:** Skins can be both approved and rejected by different users. + * **Note:** Generally skins that have been marked NSFW are also marked as rejected. + * @gqlField + */ rejected_skins_count(): Promise { return Skins.getRejectedSkinCount(); } - /** @gqlField */ + /** + * The number of skins that have been marked as NSFW. + * + * **Note:** Skins can be approved and rejected by different users. + * **Note:** Generally skins that have been marked NSFW are also marked as rejected. + * @gqlField + */ nsfw_skins_count(): Promise { return Skins.getNsfwSkinCount(); } - /** @gqlField */ + /** + * The number of skins that have never been reviewed. + * @gqlField + */ unreviewed_skins_count(): Promise { return Skins.getUnreviewedSkinCount(); } - /** @gqlField */ + /** + * The number of skins that have been approved for tweeting, but not yet tweeted. + * @gqlField + */ tweetable_skins_count(): Promise { return Skins.getTweetableSkinCount(); } - /** @gqlField */ + /** + * Skins uplaods awaiting processing. This can happen when there are a large + * number of skin uplaods at the same time, or when the skin uploading processing + * pipeline gets stuck. + * @gqlField + */ uploads_pending_processing_count(): Promise { return Skins.getUploadsAwaitingProcessingCount(); } - /** @gqlField */ + /** + * Skins uploads that have errored during processing. + * @gqlField + */ uploads_in_error_state_count(): Promise { return Skins.getUploadsErroredCount(); } - /** @gqlField */ + /** + * Number of skins that have been uploaded to the Museum via the web interface. + * @gqlField + */ web_uploads_count(): Promise { return Skins.getWebUploadsCount(); } diff --git a/packages/skin-database/api/graphql/resolvers/InternetArchiveItemResolver.ts b/packages/skin-database/api/graphql/resolvers/InternetArchiveItemResolver.ts index 4d49807d..650d0cd1 100644 --- a/packages/skin-database/api/graphql/resolvers/InternetArchiveItemResolver.ts +++ b/packages/skin-database/api/graphql/resolvers/InternetArchiveItemResolver.ts @@ -8,29 +8,49 @@ export default class InternetArchiveItemResolver { constructor(model: IaItemModel) { this._model = model; } - /** @gqlField */ + /** + * The Internet Archive's unique identifier for this item + * @gqlField + */ identifier(): string { return this._model.getIdentifier(); } - /** @gqlField */ + /** + * The URL where this item can be viewed on the Internet Archive + * @gqlField + */ url(): string { return this._model.getUrl(); } - /** @gqlField */ + /** + * URL to get the Internet Archive's metadata for this item in JSON form. + * @gqlField + */ metadata_url(): string { return this._model.getMetadataUrl(); } - /** @gqlField */ + /** + * The date and time that we last scraped this item's metadata. + * **Note:** This field is temporary and will be removed in the future. + * The date format is just what we get from the database, and it's ambiguous. + * @gqlField + */ last_metadata_scrape_date_UNSTABLE(): string | null { return this._model.getMetadataTimestamp(); } - /** @gqlField */ + /** + * Our cached version of the metadata avaliable at \`metadata_url\` (above) + * @gqlField + */ raw_metadata_json(): string | null { return this._model.getMetadataJSON(); } - /** @gqlField */ + /** + * The skin that this item contains + * @gqlField + */ async skin(): Promise { const skin = await this._model.getSkin(); if (skin == null) { diff --git a/packages/skin-database/api/graphql/resolvers/ModernSkinResolver.ts b/packages/skin-database/api/graphql/resolvers/ModernSkinResolver.ts index 47bd9e04..657f8366 100644 --- a/packages/skin-database/api/graphql/resolvers/ModernSkinResolver.ts +++ b/packages/skin-database/api/graphql/resolvers/ModernSkinResolver.ts @@ -8,17 +8,27 @@ import InternetArchiveItemResolver from "./InternetArchiveItemResolver"; import ArchiveFileResolver from "./ArchiveFileResolver"; import TweetResolver from "./TweetResolver"; -/** @gqlType ModernSkin */ +/** + * A "modern" Winamp skin. These skins use the `.wal` file extension and are free-form. + * + * Most functionality in the Winamp Skin Museum is centered around "classic" skins, + * which are currently called just `Skin` in this schema. + * @gqlType ModernSkin */ export default class ModernSkinResolver extends CommonSkinResolver implements NodeResolver, ISkin { _model: SkinModel; __typename = "ModernSkin"; - /** @gqlField */ + /** + * Filename of skin when uploaded to the Museum. Note: In some cases a skin + * has been uploaded under multiple names. Here we just pick one. + * @gqlField */ async filename({ normalize_extension = false, }: { + /** If true, the the correct file extension (.wsz or .wal) will be . + Otherwise, the original user-uploaded file extension will be used. */ normalize_extension?: boolean; }): Promise { const filename = await this._model.getFileName(); @@ -31,68 +41,95 @@ export default class ModernSkinResolver /* TODO: Get all of these from the parent class/interface */ /** + * GraphQL ID of the skin * @gqlField - * @killsParentOnException - */ + * @killsParentOnException */ id(): ID { return toId(this.__typename, this.md5()); } - /** @gqlField */ + /** + * MD5 hash of the skin's file + * @gqlField */ md5(): string { return super.md5(); } - /** @gqlField */ + /** + * URL to download the skin + * @gqlField */ download_url(): string { return super.download_url(); } - /** @gqlField */ + /** + * Has the skin been tweeted? + * @gqlField */ tweeted(): Promise { return super.tweeted(); } - /** @gqlField */ - async tweets(): Promise { + /** + * List of @winampskins tweets that mentioned the skin. + * @gqlField */ + async tweets(): Promise> { return super.tweets(); } - /** @gqlField */ - async archive_files(): Promise { + /** + * List of files contained within the skin's .wsz archive + * @gqlField */ + async archive_files(): Promise> { return super.archive_files(); } - /** @gqlField */ + /** + * The skin's "item" at archive.org + * @gqlField */ async internet_archive_item(): Promise { return super.internet_archive_item(); } - /** @gqlField */ - async reviews(): Promise { + /** + * Times that the skin has been reviewed either on the Museum's Tinder-style + * reivew page, or via the Discord bot. + * @gqlField */ + async reviews(): Promise> { return super.reviews(); } - /** @gqlField */ + /** + * @gqlField + * @deprecated Needed for migration */ museum_url(): string | null { return null; } - /** @gqlField */ + /** + * @gqlField + * @deprecated Needed for migration */ webamp_url(): string | null { return null; } - /** @gqlField */ + /** + * @gqlField + * @deprecated Needed for migration */ screenshot_url(): string | null { return null; } - /** @gqlField */ + /** + * @gqlField + * @deprecated Needed for migration */ async readme_text(): Promise { return null; } - /** @gqlField */ + /** + * @gqlField + * @deprecated Needed for migration */ async nsfw(): Promise { return null; } - /** @gqlField */ + /** + * @gqlField + * @deprecated Needed for migration */ average_color(): string | null { return null; } diff --git a/packages/skin-database/api/graphql/resolvers/MutationResolver.ts b/packages/skin-database/api/graphql/resolvers/MutationResolver.ts index da727fce..85a8abb2 100644 --- a/packages/skin-database/api/graphql/resolvers/MutationResolver.ts +++ b/packages/skin-database/api/graphql/resolvers/MutationResolver.ts @@ -5,7 +5,11 @@ import * as Skins from "../../../data/skins"; import { processUserUploads } from "../../processUserUploads"; // We don't use a resolver here, just return the value directly. -/** @gqlType */ +/** + * A URL that the client can use to upload a skin to S3, and then notify the server + * when they're done. + * @gqlType + */ type UploadUrl = { /** @gqlField */ id: string; @@ -15,16 +19,34 @@ type UploadUrl = { md5: string; }; -/** @gqlInput */ +/** + * Input object used for a user to request an UploadUrl + * @gqlInput + */ type UploadUrlRequest = { filename: string; md5: string }; -/** @gqlType UploadMutations */ +/** + * Mutations for the upload flow + * + * 1. The user finds the md5 hash of their local files. + * 2. (`get_upload_urls`) The user requests upload URLs for each of their files. + * 3. The server returns upload URLs for each of their files which are not already in the collection. + * 4. The user uploads each of their files to the URLs returned in step 3. + * 5. (`report_skin_uploaded`) The user notifies the server that they're done uploading. + * 6. (TODO) The user polls for the status of their uploads. + * + * @gqlType UploadMutations */ class UploadMutationResolver { - /** @gqlField */ + /** + * Get a (possibly incompelte) list of UploadUrls for each of the files. If an + * UploadUrl is not returned for a given hash, it means the file is already in + * the collection. + * @gqlField + */ async get_upload_urls( { files }: { files: UploadUrlRequest[] }, { ctx } - ): Promise { + ): Promise> { const missing: UploadUrl[] = []; await Parallel.each( files, @@ -41,7 +63,10 @@ class UploadMutationResolver { return missing; } - /** @gqlField */ + /** + * Notify the server that the user is done uploading. + * @gqlField + */ async report_skin_uploaded( { id, md5 }: { id: string; md5: string }, req @@ -64,15 +89,21 @@ function requireAuthed(handler) { }; } -/** @gqlType Mutation */ +/** + * + * @gqlType Mutation */ export default class MutationResolver { - /** @gqlField */ + /** + * Mutations for the upload flow + * @gqlField */ async upload(): Promise { return new UploadMutationResolver(); } - /** @gqlField */ + /** + * Send a message to the admin of the site. Currently this appears in Discord. + * @gqlField */ async send_feedback( - { message, email, url }: { message?: string; email?: string; url?: string }, + { message, email, url }: { message: string; email?: string; url?: string }, req ): Promise { req.notify({ @@ -84,7 +115,11 @@ export default class MutationResolver { return true; } - /** @gqlField */ + /** + * Reject skin for tweeting + * + * **Note:** Requires being logged in + * @gqlField */ reject_skin(args: { md5: string }, req): Promise { return this._reject_skin(args, req); } @@ -100,7 +135,11 @@ export default class MutationResolver { return true; }); - /** @gqlField */ + /** + * Approve skin for tweeting + * + * **Note:** Requires being logged in + * @gqlField */ approve_skin(args: { md5: string }, req): Promise { return this._approve_skin(args, req); } @@ -116,7 +155,11 @@ export default class MutationResolver { return true; }); - /** @gqlField */ + /** + * Mark a skin as NSFW + * + * **Note:** Requires being logged in + * @gqlField */ mark_skin_nsfw(args: { md5: string }, req): Promise { return this._mark_skin_nsfw(args, req); } @@ -132,7 +175,11 @@ export default class MutationResolver { return true; }); - /** @gqlField */ + /** + * Request that an admin check if this skin is NSFW. + * Unlike other review mutaiton endpoints, this one does not require being logged + * in. + * @gqlField */ async request_nsfw_review_for_skin( { md5 }: { md5: string }, req diff --git a/packages/skin-database/api/graphql/resolvers/ReviewResolver.ts b/packages/skin-database/api/graphql/resolvers/ReviewResolver.ts index 55ea12e5..a91196e6 100644 --- a/packages/skin-database/api/graphql/resolvers/ReviewResolver.ts +++ b/packages/skin-database/api/graphql/resolvers/ReviewResolver.ts @@ -21,8 +21,8 @@ export default class ReviewResolver { } /** - * The user who made the review (if known). **Note:** In the early days we - * didn't track this, so many will be null. + * The user who made the review (if known). **Note:** In the early days we didn't + * track this, so many will be null. * @gqlField */ reviewer(): string { diff --git a/packages/skin-database/api/graphql/resolvers/RootResolver.ts b/packages/skin-database/api/graphql/resolvers/RootResolver.ts index 87e6687e..06bf9c50 100644 --- a/packages/skin-database/api/graphql/resolvers/RootResolver.ts +++ b/packages/skin-database/api/graphql/resolvers/RootResolver.ts @@ -29,6 +29,7 @@ const index = client.initIndex("Skins"); class RootResolver extends MutationResolver { /** * Get a globally unique object by its ID. + * * https://graphql.org/learn/global-object-identification/ * @gqlField */ @@ -84,6 +85,7 @@ class RootResolver extends MutationResolver { /** * Get an archive.org item by its identifier. You can find this in the URL: + * * https://archive.org/details// * @gqlField */ @@ -100,6 +102,7 @@ class RootResolver extends MutationResolver { /** * Fetch archive file by it's MD5 hash + * * Get information about a file found within a skin's wsz/wal/zip archive. * @gqlField */ @@ -127,7 +130,7 @@ class RootResolver extends MutationResolver { offset = 0, }: { query: string; first?: Int; offset?: Int }, { ctx } - ): Promise { + ): Promise> { if (first > 1000) { throw new Error("Can only query 1000 records via search."); } @@ -146,7 +149,11 @@ class RootResolver extends MutationResolver { ); } - /** @gqlField */ + /** + * All classic skins in the database + * + * **Note:** We don't currently support combining sorting and filtering. + * @gqlField */ skins({ first = 10, offset = 0, @@ -164,7 +171,9 @@ class RootResolver extends MutationResolver { return new SkinsConnection(first, offset, sort, filter); } - /** @gqlField */ + /** + * All modern skins in the database + * @gqlField */ async modern_skins({ first = 10, offset = 0, @@ -178,7 +187,9 @@ class RootResolver extends MutationResolver { return new ModernSkinsConnection(first, offset); } - /** @gqlField */ + /** + * A random skin that needs to be reviewed + * @gqlField */ async skin_to_review(_args: never, { ctx }): Promise { if (!ctx.authed()) { return null; @@ -218,19 +229,22 @@ class RootResolver extends MutationResolver { /** * Get the status of a batch of uploads by md5s * @gqlField + * @deprecated Prefer `upload_statuses` instead, were we operate on ids. */ async upload_statuses_by_md5( { md5s }: { md5s: string[] }, { ctx } - ): Promise { + ): Promise> { return this._upload_statuses({ keyName: "skin_md5", keys: md5s }, ctx); } - /** @gqlField */ + /** + * Get the status of a batch of uploads by ids + * @gqlField */ async upload_statuses( { ids }: { ids: string[] }, { ctx } - ): Promise { + ): Promise> { return this._upload_statuses({ keyName: "id", keys: ids }, ctx); } @@ -256,27 +270,39 @@ class RootResolver extends MutationResolver { ); } - /** @gqlField */ + /** + * A namespace for statistics about the database + * @gqlField */ statistics(): DatabaseStatisticsResolver { return new DatabaseStatisticsResolver(); } } -/** @gqlType */ +/** + * Information about an attempt to upload a skin to the Museum. + * @gqlType + */ type SkinUpload = { /** @gqlField */ id: string; /** @gqlField */ status: SkinUploadStatus; - /** @gqlField */ + /** + * Skin that was uploaded. **Note:** This is null if the skin has not yet been + * fully processed. (status == ARCHIVED) + * @gqlField + */ skin: ISkin | null; - /** @gqlField */ + /** + * Md5 hash given when requesting the upload URL. + * @gqlField + */ upload_md5: string; }; /** @gqlEnum */ type SkinsSortOption = - /* + /** the Museum's (https://skins.webamp.org) special sorting rules. Roughly speaking, it's: @@ -314,6 +340,7 @@ Only the skins that have been tweeted /** * The current status of a pending upload. + * * **Note:** Expect more values here as we try to be more transparent about * the status of a pending uploads. * @gqlEnum diff --git a/packages/skin-database/api/graphql/resolvers/TweetResolver.ts b/packages/skin-database/api/graphql/resolvers/TweetResolver.ts index 4b32d93c..ef440562 100644 --- a/packages/skin-database/api/graphql/resolvers/TweetResolver.ts +++ b/packages/skin-database/api/graphql/resolvers/TweetResolver.ts @@ -3,26 +3,43 @@ import TweetModel from "../../../data/TweetModel"; import { ISkin } from "./CommonSkinResolver"; import SkinResolver from "./SkinResolver"; -/** @gqlType Tweet */ +/** + * A tweet made by @winampskins mentioning a Winamp skin + * @gqlType Tweet + */ export default class TweetResolver { _model: TweetModel; constructor(model: TweetModel) { this._model = model; } - /** @gqlField */ + /** + * URL of the tweet. **Note:** Early on in the bot's life we just recorded + * _which_ skins were tweeted, not any info about the actual tweet. This means we + * don't always know the URL of the tweet. + * @gqlField + */ url(): string | null { return this._model.getUrl(); } - /** @gqlField */ + /** + * Number of likes the tweet has received. Updated nightly. (Note: Recent likes on older tweets may not be reflected here) + * @gqlField + */ likes(): Int { return this._model.getLikes(); } - /** @gqlField */ + /** + * Number of retweets the tweet has received. Updated nightly. (Note: Recent retweets on older tweets may not be reflected here) + * @gqlField + */ retweets(): Int { return this._model.getRetweets(); } - /** @gqlField */ + /** + * The skin featured in this Tweet + * @gqlField + */ async skin(): Promise { const skin = await this._model.getSkin(); if (skin == null) { diff --git a/packages/skin-database/api/graphql/schema.graphql b/packages/skin-database/api/graphql/schema.graphql index 8a987daa..f663757b 100644 --- a/packages/skin-database/api/graphql/schema.graphql +++ b/packages/skin-database/api/graphql/schema.graphql @@ -449,40 +449,41 @@ type User { } enum SkinsSortOption { - """ - the Museum's (https://skins.webamp.org) special sorting rules. + # TODO: Add this back once Grats supports descriptions + # """ + # the Museum's (https://skins.webamp.org) special sorting rules. - Roughly speaking, it's: + # Roughly speaking, it's: - 1. The four classic default skins - 2. Tweeted skins first (sorted by the number of likes/retweets) - 3. Approved, but not tweeted yet, skins - 4. Unreviwed skins - 5. Rejected skins - 6. NSFW skins - """ + # 1. The four classic default skins + # 2. Tweeted skins first (sorted by the number of likes/retweets) + # 3. Approved, but not tweeted yet, skins + # 4. Unreviwed skins + # 5. Rejected skins + # 6. NSFW skins + # """ MUSEUM } enum SkinsFilterOption { - """ - Only the skins that have been approved for tweeting - """ + # """ + # Only the skins that have been approved for tweeting + # """ APPROVED - """ - Only the skins that have been rejected for tweeting - """ + # """ + # Only the skins that have been rejected for tweeting + # """ REJECTED - """ - Only the skins that have been marked NSFW - """ + # """ + # Only the skins that have been marked NSFW + # """ NSFW - """ - Only the skins that have been tweeted - """ + # """ + # Only the skins that have been tweeted + # """ TWEETED } @@ -493,32 +494,32 @@ The current status of a pending upload. the status of a pending uploads. """ enum SkinUploadStatus { - """ - The user has requested a URL, but the skin has not yet been processed. - """ + # """ + # The user has requested a URL, but the skin has not yet been processed. + # """ URL_REQUESTED - """ - The user has notified us that the skin has been uploaded, but we haven't yet - processed it. - """ + # """ + # The user has notified us that the skin has been uploaded, but we haven't yet + # processed it. + # """ UPLOAD_REPORTED - """ - An error occured processing the skin. Usually this is a transient error, and - the skin will be retried at a later time. - """ + # """ + # An error occured processing the skin. Usually this is a transient error, and + # the skin will be retried at a later time. + # """ ERRORED - """ - An error occured processing the skin, but it was the fault of the server. It - will be processed at a later date. - """ + # """ + # An error occured processing the skin, but it was the fault of the server. It + # will be processed at a later date. + # """ DELAYED - """ - The skin has been successfully added to the Museum. - """ + # """ + # The skin has been successfully added to the Museum. + # """ ARCHIVED } diff --git a/packages/skin-database/types.ts b/packages/skin-database/types.ts index e801c0ac..d8ad0ebb 100644 --- a/packages/skin-database/types.ts +++ b/packages/skin-database/types.ts @@ -5,7 +5,10 @@ export type TweetStatus = | "UNREVIEWED" | "NSFW"; -/** @gqlEnum */ +/** + * The judgement made about a skin by a moderator + * @gqlEnum + */ export type Rating = "APPROVED" | "REJECTED" | "NSFW"; export type SkinType = "MODERN" | "CLASSIC";