diff --git a/config/custom-environment-variables.yml b/config/custom-environment-variables.yml index 5ab6258194..9b760f285e 100644 --- a/config/custom-environment-variables.yml +++ b/config/custom-environment-variables.yml @@ -64,8 +64,6 @@ public: fetchLimit: 'FETCH_LIMIT' - shieldsProductionHerokuHacks: 'SHIELDS_PRODUCTION_HEROKU_HACKS' - private: azure_devops_token: 'AZURE_DEVOPS_TOKEN' bintray_user: 'BINTRAY_USER' @@ -74,6 +72,7 @@ private: bitbucket_password: 'BITBUCKET_PASS' bitbucket_server_username: 'BITBUCKET_SERVER_USER' bitbucket_server_password: 'BITBUCKET_SERVER_PASS' + discord_bot_token: 'DISCORD_BOT_TOKEN' drone_token: 'DRONE_TOKEN' gh_client_id: 'GH_CLIENT_ID' gh_client_secret: 'GH_CLIENT_SECRET' diff --git a/config/default.yml b/config/default.yml index e27a3cbc6d..e782ca0857 100644 --- a/config/default.yml +++ b/config/default.yml @@ -36,6 +36,4 @@ public: fetchLimit: '10MB' - shieldsProductionHerokuHacks: false - private: {} diff --git a/config/local-shields-io-production.template.yml b/config/local-shields-io-production.template.yml index e2b25630de..1c0594234d 100644 --- a/config/local-shields-io-production.template.yml +++ b/config/local-shields-io-production.template.yml @@ -1,5 +1,6 @@ private: # These are the keys which are set on the production servers. + discord_bot_token: ... gh_client_id: ... gh_client_secret: ... redis_url: ... diff --git a/core/base-service/auth-helper.js b/core/base-service/auth-helper.js index 6dedf1ba9e..48b1601ef1 100644 --- a/core/base-service/auth-helper.js +++ b/core/base-service/auth-helper.js @@ -146,9 +146,11 @@ class AuthHelper { ) } - get _bearerAuthHeader() { + _bearerAuthHeader(bearerKey) { const { _pass: pass } = this - return this.isConfigured ? { Authorization: `Bearer ${pass}` } : undefined + return this.isConfigured + ? { Authorization: `${bearerKey} ${pass}` } + : undefined } static _mergeHeaders(requestParams, headers) { @@ -168,9 +170,15 @@ class AuthHelper { } } - withBearerAuthHeader(requestParams) { + withBearerAuthHeader( + requestParams, + bearerKey = 'Bearer' // lgtm [js/hardcoded-credentials] + ) { return this._withAnyAuth(requestParams, requestParams => - this.constructor._mergeHeaders(requestParams, this._bearerAuthHeader) + this.constructor._mergeHeaders( + requestParams, + this._bearerAuthHeader(bearerKey) + ) ) } diff --git a/core/server/server.js b/core/server/server.js index 4fab514436..9ccecb2b7c 100644 --- a/core/server/server.js +++ b/core/server/server.js @@ -148,13 +148,13 @@ const publicConfigSchema = Joi.object({ rateLimit: Joi.boolean().required(), handleInternalErrors: Joi.boolean().required(), fetchLimit: Joi.string().regex(/^[0-9]+(b|kb|mb|gb|tb)$/i), - shieldsProductionHerokuHacks: Joi.boolean(), }).required() const privateConfigSchema = Joi.object({ azure_devops_token: Joi.string(), bintray_user: Joi.string(), bintray_apikey: Joi.string(), + discord_bot_token: Joi.string(), drone_token: Joi.string(), gh_client_id: Joi.string(), gh_client_secret: Joi.string(), @@ -392,8 +392,6 @@ class Server { rasterUrl: config.public.rasterUrl, private: config.private, public: config.public, - shieldsProductionHerokuHacks: - config.public.shieldsProductionHerokuHacks, } ) ) diff --git a/doc/production-hosting.md b/doc/production-hosting.md index 92a316e29e..94f7b399b6 100644 --- a/doc/production-hosting.md +++ b/doc/production-hosting.md @@ -34,6 +34,7 @@ Production hosting is managed by the Shields ops team: | Cloudflare (CDN) | Access management | @espadrine | | Cloudflare (CDN) | Admin access | @calebcartwright, @chris48s, @espadrine, @paulmelnikow, @PyvesB | | Twitch | OAuth app | @PyvesB | +| Discord | OAuth app | @PyvesB | | YouTube | Account owner | @PyvesB | | OpenStreetMap (for Wheelmap) | Account owner | @paulmelnikow | | DNS | Account owner | @olivierlacan | diff --git a/doc/server-secrets.md b/doc/server-secrets.md index 55463eaa35..28c4fbbef6 100644 --- a/doc/server-secrets.md +++ b/doc/server-secrets.md @@ -105,6 +105,15 @@ 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. +### Discord + +Using a token for Dicsord is optional but will allow higher API rates. + +- `DISCORD_BOT_TOKEN` (yml: `discord_bot_token`) + +Register an application in the [Discord developer console](https://discord.com/developers). +To obtain a token, simply create a bot for your application. + ### Drone - `DRONE_ORIGINS` (yml: `public.services.drone.authorizedOrigins`) diff --git a/services/discord/discord.service.js b/services/discord/discord.service.js index fba862037f..b3edfa88bd 100644 --- a/services/discord/discord.service.js +++ b/services/discord/discord.service.js @@ -4,15 +4,10 @@ const Joi = require('@hapi/joi') const { nonNegativeInteger } = require('../validators') const { BaseJsonService } = require('..') -const discordSchema = Joi.object({ +const schema = Joi.object({ presence_count: nonNegativeInteger, }).required() -const proxySchema = Joi.object({ - message: Joi.string().required(), - color: Joi.string().required(), -}).required() - const documentation = `

The Discord badge requires the SERVER ID in order access the Discord JSON API. @@ -41,6 +36,14 @@ module.exports = class Discord extends BaseJsonService { } } + static get auth() { + return { + passKey: 'discord_bot_token', + authorizedOrigins: ['https://discord.com'], + isRequired: false, + } + } + static get examples() { return [ { @@ -67,36 +70,24 @@ module.exports = class Discord extends BaseJsonService { } } - constructor(context, config) { - super(context, config) - this._shieldsProductionHerokuHacks = config.shieldsProductionHerokuHacks - } - async fetch({ serverId }) { - const url = `https://discord.com/api/guilds/${serverId}/widget.json` - return this._requestJson({ - url, - schema: discordSchema, - errorMessages: { - 404: 'invalid server', - 403: 'widget disabled', - }, - }) - } - - async fetchOvhProxy({ serverId }) { - return this._requestJson({ - url: `https://legacy-img.shields.io/discord/${serverId}.json`, - schema: proxySchema, - }) + const url = `https://discord.com/api/v6/guilds/${serverId}/widget.json` + return this._requestJson( + this.authHelper.withBearerAuthHeader( + { + url, + schema, + errorMessages: { + 404: 'invalid server', + 403: 'widget disabled', + }, + }, + 'Bot' + ) + ) } async handle({ serverId }) { - if (this._shieldsProductionHerokuHacks) { - const { message, color } = await this.fetchOvhProxy({ serverId }) - return { message, color } - } - const data = await this.fetch({ serverId }) return this.constructor.render({ members: data.presence_count }) } diff --git a/services/discord/discord.spec.js b/services/discord/discord.spec.js new file mode 100644 index 0000000000..926ac9859d --- /dev/null +++ b/services/discord/discord.spec.js @@ -0,0 +1,40 @@ +'use strict' + +const { expect } = require('chai') +const nock = require('nock') +const { cleanUpNockAfterEach, defaultContext } = require('../test-helpers') +const Discord = require('./discord.service') + +describe('Discord', function () { + cleanUpNockAfterEach() + + it('sends the auth information as configured', async function () { + const pass = 'password' + const config = { + private: { + discord_bot_token: pass, + }, + } + + const scope = nock(`https://discord.com`, { + // This ensures that the expected credential is actually being sent with the HTTP request. + // Without this the request wouldn't match and the test would fail. + reqheaders: { Authorization: `Bot password` }, + }) + .get(`/api/v6/guilds/12345/widget.json`) + .reply(200, { + presence_count: 125, + }) + + expect( + await Discord.invoke(defaultContext, config, { + serverId: '12345', + }) + ).to.deep.equal({ + message: '125 online', + color: 'brightgreen', + }) + + scope.done() + }) +}) diff --git a/services/discord/discord.tester.js b/services/discord/discord.tester.js index b033ff1fc4..1d4c660d58 100644 --- a/services/discord/discord.tester.js +++ b/services/discord/discord.tester.js @@ -19,7 +19,7 @@ t.create('widget disabled') .get('/12345.json') .intercept(nock => nock('https://discord.com/') - .get('/api/guilds/12345/widget.json') + .get('/api/v6/guilds/12345/widget.json') .reply(403, { code: 50004, message: 'Widget Disabled', @@ -31,7 +31,7 @@ t.create('server error') .get('/12345.json') .intercept(nock => nock('https://discord.com/') - .get('/api/guilds/12345/widget.json') + .get('/api/v6/guilds/12345/widget.json') .reply(500, 'Something broke') ) .expectBadge({ label: 'chat', message: 'inaccessible' })