Greasy Fork is available in English.
Get information from Greasy Fork and do actions in it.
This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://update.greasyfork.org/scripts/445697/1748148/Greasy%20Fork%20API.js
// ==UserScript== // @name Greasy Fork API // @namespace - // @version 3.1.0 // @description Get data from Greasy Fork, or/and do actions on Greasy Fork // @author NotYou // @license LGPL-3.0 // @connect greasyfork.org // @connect sleazyfork.org // @grant GM.xmlHttpRequest // @grant GM.openInTab // @require https://unpkg.com/[email protected]/lib/index.umd.js // ==/UserScript== !function (z) { 'use strict'; class Schemas { static Id = z.union([ z.number().int().positive(), z.string().regex(/^(?!0)\d+$/) ]) static Query = z.string().trim().optional() static Page = z.number().int().optional() static FilterLocale = z.boolean().optional() static get ScriptsQuery() { return z.object({ q: this.Query, page: this.Page, filter_locale: this.FilterLocale, sort: z.union([ z.literal('total_installs'), z.literal('ratings'), z.literal('created'), z.literal('updated'), z.literal('name'), ]).optional(), by: this.Id.optional(), language: z.union([ z.literal('js'), z.literal('css'), ]).optional() }) } static get AdvancedScriptsQuery() { const Operator = z.union([ z.literal('lt'), z.literal('gt'), z.literal('eq') ]).default('gt') const Installs = z.number().int().nonnegative().default(0) const Datetime = z.union([z.string().datetime(), z.custom(value => value === '')]).default('') return this.ScriptsQuery.extend({ total_installs_operator: Operator, total_installs: Installs, daily_installs_operator: Operator, daily_installs: Installs, ratings_operator: Operator, ratings: z.number().min(0).max(1).default(0), created_operator: Operator, created: Datetime, updated_operator: Operator, updated: Datetime, entry_locales: z.array(z.number()).optional(), tz: z.string().regex(/^[A-Za-z0-9_+-]+\/[A-Za-z0-9_+-]+(?:\/[A-Za-z0-9_+-]+)?$/).optional() }) } static get ScriptsBySiteQuery() { const HostnameFormat = z.custom(value => { try { new URL(`https://${value}:80/`) return true } catch { return false } }) return this.ScriptsQuery.extend({ site: z.union([ z.literal('*'), z.string().ip().trim(), HostnameFormat ]) }) } static get ScriptSetQuery() { return this.ScriptsQuery.extend({ set: this.Id }).omit({ by: true }) } static get LibrariesQuery() { return z.object({ q: this.Query, page: this.Page, filter_locale: this.FilterLocale, sort: z.union([ z.literal('created'), z.literal('updated'), z.literal('name') ]).optional(), by: this.Id.optional() }) } static get UsersQuery() { return z.object({ q: this.Query, page: this.Page, sort: z.union([ z.literal('name'), z.literal('daily_installs'), z.literal('total_installs'), z.literal('ratings'), z.literal('scripts'), z.literal('created_scripts'), z.literal('updated_scripts'), ]).optional(), author: z.boolean().optional() }) } static GetResponse = z.object({ params: z.array( z.tuple([ z.custom(value => value instanceof z.ZodSchema), z.any() ]) ), getUrl: z.function() .args(z.any().array()) .returns(z.string()), type: z.union([ z.literal('json'), z.literal('text') ]) }) static get Install() { return z.object({ id: this.Id, type: z.union([z.literal('js'), z.literal('css')]).default('js') }) } } class GreasyFork { constructor(isSleazyfork = false) { if (isSleazyfork) { this.hostname = 'api.sleazyfork.org' } else { this.hostname = 'api.greasyfork.org' } } Schemas = Schemas _formatZodError(zodError) { if (!(zodError instanceof z.ZodError)) { throw new Error('Provided value is not a ZodError') } const justDisplayMessage = issue => issue.message const formatPath = path => path.map(pathItem => { if (typeof pathItem === 'number') { return `[${pathItem}]` } return pathItem.toString() }).join('.') const formatIssue = issue => { const issueFormatter = { "invalid_type": issue => `${issue.message} at path: "${formatPath(issue.path)}"`, "invalid_literal": issue => `${issue.message}, but got "${issue.received}"`, "custom": justDisplayMessage, "invalid_union": issue => { const expectedValues = issue.unionErrors.map(unionError => `"${unionError.issues[0].expected}"`).join(' | ') return `${issue.message} "${formatPath(issue.path)}", expected these values: ${expectedValues}` }, "invalid_union_discriminator": justDisplayMessage, "invalid_enum_value": justDisplayMessage, "unrecognized_keys": justDisplayMessage, "invalid_arguments": justDisplayMessage, "invalid_return_type": justDisplayMessage, "invalid_date": justDisplayMessage, "invalid_string": issue => `Invalid string format, validation failed at "${issue.validation}"`, "too_small": justDisplayMessage, "too_big": justDisplayMessage, "invalid_intersection_types": justDisplayMessage, "not_multiple_of": justDisplayMessage, "not_finite": justDisplayMessage }[issue.code] if (typeof issueFormatter === 'function') { return issueFormatter(issue) } return `Got unrecognised error! Code: ${issue.code ?? 'undefined'}; Message: ${issue.message ?? 'undefined'}` } return zodError.issues.map(formatIssue).join('\n\n') } _getUrl(path) { return 'https://' + this.hostname + '/' + path } _formatHttpError(response) { if (typeof response !== 'object' || typeof response.status !== 'number' || typeof response.finalUrl !== 'string') { throw new Error('Provided object is not a response-like object') } const statusText = { 400: 'Bad Request', 401: 'Unauthorized', 402: 'Payment Required', 403: 'Forbidden', 404: 'Not Found', 405: 'Method Not Allowed', 406: 'Not Acceptable', 407: 'Proxy Authentication Required', 408: 'Request Timeout', 500: 'Internal Server Error', 501: 'Not Implemented', 502: 'Bad Gateway', 503: 'Service Unavailable', 504: 'Gateway Timeout', }[response.status] ?? `https://developer.mozilla.org/docs/Web/HTTP/Reference/Status/${status}` return `HTTP Error "${response.finalUrl}": ${response.status} ${statusText}` } _request(path, options = {}) { return new Promise((resolve, reject) => { GM.xmlHttpRequest({ url: this._getUrl(path), anonymous: true, onload: response => { if (response.status === 200) { resolve(response) } else { reject(this._formatHttpError(response)) } }, onerror: reject, ...options }) }) } _getTextData(path) { return this._request(path) .then(response => response.responseText) } _getJSONData(path) { return this._request(path, { responseType: 'json' }) .then(response => response.response) } _dataToSearchParams(data) { for (const key in data) { const value = data[key] if (typeof value === 'boolean') { data[key] = value ? 1 : 0 } else if (typeof value === 'undefined' || value === null) { delete data[key] } } return '?' + new URLSearchParams(data).toString() } _getResponse(options) { const result = this.Schemas.GetResponse.safeParse(options) if (!result.success) { throw new Error(this._formatZodError(result.error)) } const results = options.params.map(([schema, param]) => schema.safeParse(param)) const unsuccessfulResult = results.find(result => !result.success) if (unsuccessfulResult) { throw new Error(this._formatZodError(unsuccessfulResult.error)) } const data = results.map(result => result.data) const url = options.getUrl(data) if (options.type === 'json') { return this._getJSONData(url) } else if (options.type === 'text') { return this._getTextData(url) } } get script() { return { getData: id => this._getResponse({ params: [ [this.Schemas.Id, id] ], getUrl: ([id]) => `scripts/${id}.json`, type: 'json' }), getCode: id => this._getResponse({ params: [ [this.Schemas.Id, id] ], getUrl: ([id]) => `https://${this.hostname.replace('api.', '')}/scripts/${id}/code/script.js`, type: 'text' }), getMeta: id => this._getResponse({ params: [ [this.Schemas.Id, id] ], getUrl: ([id]) => `https://${this.hostname.replace('api.', '')}/scripts/${id}/code/script.meta.js`, type: 'text' }), getHistory: id => this._getResponse({ params: [ [this.Schemas.Id, id] ], getUrl: ([id]) => `scripts/${id}/versions.json`, type: 'json' }), getStats: id => this._getResponse({ params: [ [this.Schemas.Id, id] ], getUrl: ([id]) => `scripts/${id}/stats.json`, type: 'json' }), getStatsCsv: id => this._getResponse({ params: [ [this.Schemas.Id, id] ], getUrl: ([id]) => `scripts/${id}/stats.json`, type: 'text' }) } } getScripts(options = {}) { return this._getResponse({ params: [ [this.Schemas.ScriptsQuery, options] ], getUrl: ([options]) => 'scripts.json' + this._dataToSearchParams(options), type: 'json' }) } getScriptsAdvanced(options = {}) { return this._getResponse({ params: [ [this.Schemas.AdvancedScriptsQuery, options] ], getUrl: ([options]) => { options['entry_locales[]'] = options.entry_locales delete options.entry_locales return 'scripts.json' + this._dataToSearchParams(options) }, type: 'json' }) } getScriptsBySite(options = {}) { return this._getResponse({ params: [ [this.Schemas.ScriptsBySiteQuery, options] ], getUrl: ([options]) => { let url = `scripts/by-site/${options.site}.json` delete options.site return url + this._dataToSearchParams(options) }, type: 'json' }) } getSitesPopularity() { return this._getJSONData('/scripts/by-site.json') } getScriptSet(options = {}) { return this._getResponse({ params: [ [this.Schemas.ScriptSetQuery, options] ], getUrl: ([options]) => 'scripts.json' + this._dataToSearchParams(options), type: 'json' }) } getLibraries(options = {}) { return this._getResponse({ params: [ [this.Schemas.LibrariesQuery, options] ], getUrl: ([options]) => 'scripts/libraries.json' + this._dataToSearchParams(options), type: 'json' }) } getUserData(id) { return this._getResponse({ params: [ [this.Schemas.Id, id] ], getUrl: ([id]) => `users/${id}.json`, type: 'json' }) } getUsers(options = {}) { return this._getResponse({ params: [ [this.Schemas.UsersQuery, options] ], getUrl: ([options]) => 'users.json' + this._dataToSearchParams(options), type: 'json' }) } signOut() { return this._request('/users/sign_out') } installUserScript(options) { const result = this.Schemas.Install.safeParse(options) if (!result.success) { throw new Error(this._formatZodError(result.error)) } options = result.data const url = this._getUrl(`scripts/${options.id}/code/userscript.user.${options.type}`) GM.openInTab(url, { active: true }) } } let global = window if (typeof unsafeWindow !== 'undefined') { global = unsafeWindow } global.GreasyFork = GreasyFork }(Zod)