Add [CurseForge] badges (#9252)
* add curseforge downloads badge * Add more [CurseForge] badges Adds the following badges: - /curseforge/dt/:projectId (downloads) - /curseforge/game-versions/:projectId (game versions) - /curseforge/v/:projectId (version) The following secret: - CURSEFORGE_API_KEY (yml: private.curseforge_api_key) * Remove default logo from badges * Linter fixes * Rename `errorMessages` to `httpErrors` * Remove namedLogo from ModrinthGameVersions badge * Remove namedLogo from ModrinthVersion badge * Remove namedLogo from ModrinthFollowers badge --------- Co-authored-by: Minecraftschurli <minecraftschurli@gmail.com> Co-authored-by: Pierre-Yves Bigourdan <10694593+PyvesB@users.noreply.github.com>
This commit is contained in:
@@ -77,6 +77,7 @@ private:
|
||||
bitbucket_password: 'BITBUCKET_PASS'
|
||||
bitbucket_server_username: 'BITBUCKET_SERVER_USER'
|
||||
bitbucket_server_password: 'BITBUCKET_SERVER_PASS'
|
||||
curseforge_api_key: 'CURSEFORGE_API_KEY'
|
||||
discord_bot_token: 'DISCORD_BOT_TOKEN'
|
||||
drone_token: 'DRONE_TOKEN'
|
||||
gh_client_id: 'GH_CLIENT_ID'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
private:
|
||||
# These are the keys which are set on the production servers.
|
||||
curseforge_api_key: ...
|
||||
discord_bot_token: ...
|
||||
gh_client_id: ...
|
||||
gh_client_secret: ...
|
||||
|
||||
@@ -4,6 +4,7 @@ private:
|
||||
# The possible values are documented in `doc/server-secrets.md`. Note that
|
||||
# you can also set these values through environment variables, which may be
|
||||
# preferable for self hosting.
|
||||
curseforge_api_key: '...'
|
||||
gh_token: '...'
|
||||
gitlab_token: '...'
|
||||
obs_user: '...'
|
||||
|
||||
@@ -153,6 +153,11 @@ class AuthHelper {
|
||||
: undefined
|
||||
}
|
||||
|
||||
_apiKeyHeader(apiKeyHeader) {
|
||||
const { _pass: pass } = this
|
||||
return this.isConfigured ? { [apiKeyHeader]: pass } : undefined
|
||||
}
|
||||
|
||||
static _mergeHeaders(requestParams, headers) {
|
||||
const {
|
||||
options: { headers: existingHeaders, ...restOptions } = {},
|
||||
@@ -170,6 +175,12 @@ class AuthHelper {
|
||||
}
|
||||
}
|
||||
|
||||
withApiKeyHeader(requestParams, header = 'x-api-key') {
|
||||
return this._withAnyAuth(requestParams, requestParams =>
|
||||
this.constructor._mergeHeaders(requestParams, this._apiKeyHeader(header)),
|
||||
)
|
||||
}
|
||||
|
||||
withBearerAuthHeader(
|
||||
requestParams,
|
||||
bearerKey = 'Bearer', // lgtm [js/hardcoded-credentials]
|
||||
|
||||
@@ -162,6 +162,7 @@ const publicConfigSchema = Joi.object({
|
||||
|
||||
const privateConfigSchema = Joi.object({
|
||||
azure_devops_token: Joi.string(),
|
||||
curseforge_api_key: Joi.string(),
|
||||
discord_bot_token: Joi.string(),
|
||||
drone_token: Joi.string(),
|
||||
gh_client_id: Joi.string(),
|
||||
|
||||
@@ -97,6 +97,19 @@ self-hosted Shields installation access to private repositories hosted on bitbuc
|
||||
Bitbucket badges use basic auth. Provide a username and password to give your
|
||||
self-hosted Shields installation access to a private Bitbucket Server instance.
|
||||
|
||||
### CurseForge
|
||||
|
||||
- `CURSEFORGE_API_KEY` (yml: `private.curseforge_api_key`)
|
||||
|
||||
A CurseForge API key is required to use the [CurseForge API][cf api]. To obtain
|
||||
an API key, [signup to CurseForge Console][cf signup] with a Google account and
|
||||
create an organization, then go to the [API keys page][cf api key] and copy the
|
||||
generated API key.
|
||||
|
||||
[cf api]: https://docs.curseforge.com
|
||||
[cf signup]: https://console.curseforge.com/#/signup
|
||||
[cf api key]: https://console.curseforge.com/#/api-keys
|
||||
|
||||
### Discord
|
||||
|
||||
Using a token for Dicsord is optional but will allow higher API rates.
|
||||
|
||||
61
services/curseforge/curseforge-base.js
Normal file
61
services/curseforge/curseforge-base.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import Joi from 'joi'
|
||||
import { BaseJsonService } from '../index.js'
|
||||
import { nonNegativeInteger } from '../validators.js'
|
||||
|
||||
const schema = Joi.object({
|
||||
data: Joi.object({
|
||||
downloadCount: nonNegativeInteger,
|
||||
latestFiles: Joi.array()
|
||||
.items({
|
||||
displayName: Joi.string().required(),
|
||||
gameVersions: Joi.array().items(Joi.string().required()).required(),
|
||||
})
|
||||
.required(),
|
||||
}).required(),
|
||||
}).required()
|
||||
|
||||
const documentation = `
|
||||
<p>
|
||||
The CurseForge badge requires the <code>Project ID</code> in order access the
|
||||
<a href="https://docs.curseforge.com/#get-mod" target="_blank">CurseForge API</a>.
|
||||
</p>
|
||||
<p>
|
||||
The <code>Project ID</code> is different from the URL slug and can be found in the 'About Project' section of your
|
||||
CurseForge mod page.
|
||||
</p>
|
||||
<img src="https://github.com/badges/shields/assets/1098773/0d45b5fa-2cde-415d-8152-b84c535a1535"
|
||||
alt="The Project ID in the 'About Projection' section on CurseForge." />
|
||||
`
|
||||
|
||||
export default class BaseCurseForgeService extends BaseJsonService {
|
||||
static auth = {
|
||||
passKey: 'curseforge_api_key',
|
||||
authorizedOrigins: ['https://api.curseforge.com'],
|
||||
isRequired: true,
|
||||
}
|
||||
|
||||
async fetchMod({ projectId }) {
|
||||
// Documentation: https://docs.curseforge.com/#get-mod
|
||||
const response = await this._requestJson(
|
||||
this.authHelper.withApiKeyHeader({
|
||||
schema,
|
||||
url: `https://api.curseforge.com/v1/mods/${projectId}`,
|
||||
httpErrors: {
|
||||
403: 'invalid API key',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
const latestFiles = response.data.latestFiles
|
||||
const latestFile =
|
||||
latestFiles.length > 0 ? latestFiles[latestFiles.length - 1] : {}
|
||||
|
||||
return {
|
||||
downloads: response.data.downloadCount,
|
||||
version: latestFile?.displayName || 'N/A',
|
||||
gameVersions: latestFile?.gameVersions || ['N/A'],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { BaseCurseForgeService, documentation }
|
||||
29
services/curseforge/curseforge-downloads.service.js
Normal file
29
services/curseforge/curseforge-downloads.service.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { renderDownloadsBadge } from '../downloads.js'
|
||||
import BaseCurseForgeService, { documentation } from './curseforge-base.js'
|
||||
|
||||
export default class CurseForgeDownloads extends BaseCurseForgeService {
|
||||
static category = 'downloads'
|
||||
|
||||
static route = {
|
||||
base: 'curseforge/dt',
|
||||
pattern: ':projectId',
|
||||
}
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'CurseForge Downloads',
|
||||
namedParams: {
|
||||
projectId: '238222',
|
||||
},
|
||||
staticPreview: renderDownloadsBadge({ downloads: 234000000 }),
|
||||
documentation,
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = { label: 'downloads' }
|
||||
|
||||
async handle({ projectId }) {
|
||||
const { downloads } = await this.fetchMod({ projectId })
|
||||
return renderDownloadsBadge({ downloads })
|
||||
}
|
||||
}
|
||||
22
services/curseforge/curseforge-downloads.tester.js
Normal file
22
services/curseforge/curseforge-downloads.tester.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { createServiceTester } from '../tester.js'
|
||||
import { isMetric } from '../test-validators.js'
|
||||
import { noToken } from '../test-helpers.js'
|
||||
import CurseForgeDownloads from './curseforge-downloads.service.js'
|
||||
|
||||
export const t = await createServiceTester()
|
||||
const noApiKey = noToken(CurseForgeDownloads)
|
||||
|
||||
t.create('Downloads')
|
||||
.skipWhen(noApiKey)
|
||||
.get('/238222.json')
|
||||
.expectBadge({ label: 'downloads', message: isMetric })
|
||||
|
||||
t.create('Downloads (empty)')
|
||||
.skipWhen(noApiKey)
|
||||
.get('/872620.json')
|
||||
.expectBadge({ label: 'downloads', message: '0' })
|
||||
|
||||
t.create('Downloads (not found)')
|
||||
.skipWhen(noApiKey)
|
||||
.get('/invalid-project-id.json')
|
||||
.expectBadge({ label: 'downloads', message: 'not found', color: 'red' })
|
||||
36
services/curseforge/curseforge-game-versions.service.js
Normal file
36
services/curseforge/curseforge-game-versions.service.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import BaseCurseForgeService, { documentation } from './curseforge-base.js'
|
||||
|
||||
export default class CurseForgeGameVersions extends BaseCurseForgeService {
|
||||
static category = 'platform-support'
|
||||
|
||||
static route = {
|
||||
base: 'curseforge/game-versions',
|
||||
pattern: ':projectId',
|
||||
}
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'CurseForge Game Versions',
|
||||
namedParams: {
|
||||
projectId: '238222',
|
||||
},
|
||||
staticPreview: this.render({ versions: ['1.20.0', '1.19.4'] }),
|
||||
documentation,
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = { label: 'game versions' }
|
||||
|
||||
static render({ versions }) {
|
||||
return {
|
||||
message: versions.join(' | '),
|
||||
color: 'blue',
|
||||
}
|
||||
}
|
||||
|
||||
async handle({ projectId }) {
|
||||
const { gameVersions } = await this.fetchMod({ projectId })
|
||||
const versions = gameVersions
|
||||
return this.constructor.render({ versions })
|
||||
}
|
||||
}
|
||||
22
services/curseforge/curseforge-game-versions.tester.js
Normal file
22
services/curseforge/curseforge-game-versions.tester.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { createServiceTester } from '../tester.js'
|
||||
import { withRegex } from '../test-validators.js'
|
||||
import { noToken } from '../test-helpers.js'
|
||||
import CurseForgeGameVersions from './curseforge-game-versions.service.js'
|
||||
|
||||
export const t = await createServiceTester()
|
||||
const noApiKey = noToken(CurseForgeGameVersions)
|
||||
|
||||
t.create('Game Versions')
|
||||
.skipWhen(noApiKey)
|
||||
.get('/238222.json')
|
||||
.expectBadge({ label: 'game versions', message: withRegex(/.+( \| )?/) })
|
||||
|
||||
t.create('Game Versions (empty)')
|
||||
.skipWhen(noApiKey)
|
||||
.get('/872620.json')
|
||||
.expectBadge({ label: 'game versions', message: 'N/A', color: 'blue' })
|
||||
|
||||
t.create('Game Versions (not found)')
|
||||
.skipWhen(noApiKey)
|
||||
.get('/invalid-project-id.json')
|
||||
.expectBadge({ label: 'game versions', message: 'not found', color: 'red' })
|
||||
31
services/curseforge/curseforge-version.service.js
Normal file
31
services/curseforge/curseforge-version.service.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { renderVersionBadge } from '../version.js'
|
||||
import BaseCurseForgeService, { documentation } from './curseforge-base.js'
|
||||
|
||||
export default class CurseForgeVersion extends BaseCurseForgeService {
|
||||
static category = 'version'
|
||||
|
||||
static route = {
|
||||
base: 'curseforge/v',
|
||||
pattern: ':projectId',
|
||||
}
|
||||
|
||||
static examples = [
|
||||
{
|
||||
title: 'CurseForge Version',
|
||||
namedParams: {
|
||||
projectId: '238222',
|
||||
},
|
||||
staticPreview: renderVersionBadge({
|
||||
version: 'jei-1.20-forge-14.0.0.4.jar',
|
||||
}),
|
||||
documentation,
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = { label: 'version' }
|
||||
|
||||
async handle({ projectId }) {
|
||||
const { version } = await this.fetchMod({ projectId })
|
||||
return renderVersionBadge({ version })
|
||||
}
|
||||
}
|
||||
22
services/curseforge/curseforge-version.tester.js
Normal file
22
services/curseforge/curseforge-version.tester.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { createServiceTester } from '../tester.js'
|
||||
import { withRegex } from '../test-validators.js'
|
||||
import { noToken } from '../test-helpers.js'
|
||||
import CurseForgeVersion from './curseforge-version.service.js'
|
||||
|
||||
export const t = await createServiceTester()
|
||||
const noApiKey = noToken(CurseForgeVersion)
|
||||
|
||||
t.create('Version')
|
||||
.skipWhen(noApiKey)
|
||||
.get('/238222.json')
|
||||
.expectBadge({ label: 'version', message: withRegex(/.+/) })
|
||||
|
||||
t.create('Version (empty)')
|
||||
.skipWhen(noApiKey)
|
||||
.get('/872620.json')
|
||||
.expectBadge({ label: 'version', message: 'N/A', color: 'blue' })
|
||||
|
||||
t.create('Version (not found)')
|
||||
.skipWhen(noApiKey)
|
||||
.get('/invalid-project-id.json')
|
||||
.expectBadge({ label: 'version', message: 'not found', color: 'red' })
|
||||
Reference in New Issue
Block a user