allow passing key to [stackexchange] (#8539)
* refactoring groundwork * add stackapps_api_key setting * add test for stackexchange auth * clarify docs Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
This commit is contained in:
@@ -98,6 +98,7 @@ private:
|
||||
sl_insight_userUuid: 'SL_INSIGHT_USER_UUID'
|
||||
sl_insight_apiToken: 'SL_INSIGHT_API_TOKEN'
|
||||
sonarqube_token: 'SONARQUBE_TOKEN'
|
||||
stackapps_api_key: 'STACKAPPS_API_KEY'
|
||||
teamcity_user: 'TEAMCITY_USER'
|
||||
teamcity_pass: 'TEAMCITY_PASS'
|
||||
twitch_client_id: 'TWITCH_CLIENT_ID'
|
||||
|
||||
@@ -186,6 +186,7 @@ const privateConfigSchema = Joi.object({
|
||||
sl_insight_userUuid: Joi.string(),
|
||||
sl_insight_apiToken: Joi.string(),
|
||||
sonarqube_token: Joi.string(),
|
||||
stackapps_api_key: Joi.string(),
|
||||
teamcity_user: Joi.string(),
|
||||
teamcity_pass: Joi.string(),
|
||||
twitch_client_id: Joi.string(),
|
||||
|
||||
@@ -244,6 +244,17 @@ Create an account, sign in and obtain a uuid and token from your
|
||||
to give your self-hosted Shields installation access to a
|
||||
private SonarQube instance or private project on a public instance.
|
||||
|
||||
### StackApps (for StackExchange and StackOverflow)
|
||||
|
||||
- `STACKAPPS_API_KEY`: (yml: `private.stackapps_api_key`)
|
||||
|
||||
Anonymous requests to the stackexchange API are limited to 300 calls per day.
|
||||
To increase your quota to 10,000 calls per day, create an account at
|
||||
[StackApps](https://stackapps.com/) and
|
||||
[register an OAuth app](https://stackapps.com/apps/oauth/register). Having registered
|
||||
an OAuth app, you'll be granted a key which can be used to increase your request quota.
|
||||
It is not necessary to performa full OAuth Flow to gain an access token.
|
||||
|
||||
### TeamCity
|
||||
|
||||
- `TEAMCITY_ORIGINS` (yml: `public.services.teamcity.authorizedOrigins`)
|
||||
|
||||
37
services/stackexchange/stackexchange-base.js
Normal file
37
services/stackexchange/stackexchange-base.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { BaseJsonService } from '../index.js'
|
||||
import { metric } from '../text-formatters.js'
|
||||
import { floorCount as floorCountColor } from '../color-formatters.js'
|
||||
|
||||
export function renderQuestionsBadge({
|
||||
suffix,
|
||||
stackexchangesite,
|
||||
query,
|
||||
numValue,
|
||||
}) {
|
||||
const label = `${stackexchangesite} ${query} questions`
|
||||
return {
|
||||
label,
|
||||
message: `${metric(numValue)}${suffix}`,
|
||||
color: floorCountColor(numValue, 1000, 10000, 20000),
|
||||
}
|
||||
}
|
||||
|
||||
export class StackExchangeBase extends BaseJsonService {
|
||||
static category = 'chat'
|
||||
|
||||
static auth = {
|
||||
passKey: 'stackapps_api_key',
|
||||
authorizedOrigins: ['https://api.stackexchange.com'],
|
||||
isRequired: false,
|
||||
}
|
||||
|
||||
static defaultBadgeData = {
|
||||
label: 'stackoverflow',
|
||||
}
|
||||
|
||||
async fetch(params) {
|
||||
return this._requestJson(
|
||||
this.authHelper.withQueryStringAuth({ passKey: 'key' }, params)
|
||||
)
|
||||
}
|
||||
}
|
||||
38
services/stackexchange/stackexchange-base.spec.js
Normal file
38
services/stackexchange/stackexchange-base.spec.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import Joi from 'joi'
|
||||
import { expect } from 'chai'
|
||||
import nock from 'nock'
|
||||
import { cleanUpNockAfterEach, defaultContext } from '../test-helpers.js'
|
||||
import { StackExchangeBase } from './stackexchange-base.js'
|
||||
|
||||
class DummyStackExchangeService extends StackExchangeBase {
|
||||
static route = { base: 'fake-base' }
|
||||
|
||||
async handle() {
|
||||
const data = await this.fetch({
|
||||
schema: Joi.any(),
|
||||
url: 'https://api.stackexchange.com/2.2/tags/python/info',
|
||||
})
|
||||
return { message: data.message }
|
||||
}
|
||||
}
|
||||
|
||||
describe('StackExchangeBase', function () {
|
||||
describe('auth', function () {
|
||||
cleanUpNockAfterEach()
|
||||
|
||||
const config = { private: { stackapps_api_key: 'fake-key' } }
|
||||
|
||||
it('sends the auth information as configured', async function () {
|
||||
const scope = nock('https://api.stackexchange.com')
|
||||
.get('/2.2/tags/python/info')
|
||||
.query({ key: 'fake-key' })
|
||||
.reply(200, { message: 'fake message' })
|
||||
|
||||
expect(
|
||||
await DummyStackExchangeService.invoke(defaultContext, config, {})
|
||||
).to.deep.equal({ message: 'fake message' })
|
||||
|
||||
scope.done()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,16 +0,0 @@
|
||||
import { metric } from '../text-formatters.js'
|
||||
import { floorCount as floorCountColor } from '../color-formatters.js'
|
||||
|
||||
export default function renderQuestionsBadge({
|
||||
suffix,
|
||||
stackexchangesite,
|
||||
query,
|
||||
numValue,
|
||||
}) {
|
||||
const label = `${stackexchangesite} ${query} questions`
|
||||
return {
|
||||
label,
|
||||
message: `${metric(numValue)}${suffix}`,
|
||||
color: floorCountColor(numValue, 1000, 10000, 20000),
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
import dayjs from 'dayjs'
|
||||
import Joi from 'joi'
|
||||
import { nonNegativeInteger } from '../validators.js'
|
||||
import { BaseJsonService } from '../index.js'
|
||||
import renderQuestionsBadge from './stackexchange-helpers.js'
|
||||
import {
|
||||
renderQuestionsBadge,
|
||||
StackExchangeBase,
|
||||
} from './stackexchange-base.js'
|
||||
|
||||
const tagSchema = Joi.object({
|
||||
total: nonNegativeInteger,
|
||||
}).required()
|
||||
|
||||
export default class StackExchangeMonthlyQuestions extends BaseJsonService {
|
||||
static category = 'chat'
|
||||
|
||||
export default class StackExchangeMonthlyQuestions extends StackExchangeBase {
|
||||
static route = {
|
||||
base: 'stackexchange',
|
||||
pattern: ':stackexchangesite/qm/:query',
|
||||
@@ -29,10 +29,6 @@ export default class StackExchangeMonthlyQuestions extends BaseJsonService {
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = {
|
||||
label: 'stackoverflow',
|
||||
}
|
||||
|
||||
static render(props) {
|
||||
return renderQuestionsBadge({
|
||||
suffix: '/month',
|
||||
@@ -51,7 +47,7 @@ export default class StackExchangeMonthlyQuestions extends BaseJsonService {
|
||||
.endOf('month')
|
||||
.unix()
|
||||
|
||||
const parsedData = await this._requestJson({
|
||||
const parsedData = await this.fetch({
|
||||
schema: tagSchema,
|
||||
options: {
|
||||
decompress: true,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Joi from 'joi'
|
||||
import { metric } from '../text-formatters.js'
|
||||
import { floorCount as floorCountColor } from '../color-formatters.js'
|
||||
import { BaseJsonService } from '../index.js'
|
||||
import { StackExchangeBase } from './stackexchange-base.js'
|
||||
|
||||
const reputationSchema = Joi.object({
|
||||
items: Joi.array()
|
||||
@@ -14,9 +14,7 @@ const reputationSchema = Joi.object({
|
||||
.required(),
|
||||
}).required()
|
||||
|
||||
export default class StackExchangeReputation extends BaseJsonService {
|
||||
static category = 'chat'
|
||||
|
||||
export default class StackExchangeReputation extends StackExchangeBase {
|
||||
static route = {
|
||||
base: 'stackexchange',
|
||||
pattern: ':stackexchangesite/r/:query',
|
||||
@@ -34,10 +32,6 @@ export default class StackExchangeReputation extends BaseJsonService {
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = {
|
||||
label: 'stackoverflow',
|
||||
}
|
||||
|
||||
static render({ stackexchangesite, numValue }) {
|
||||
const label = `${stackexchangesite} reputation`
|
||||
|
||||
@@ -51,7 +45,7 @@ export default class StackExchangeReputation extends BaseJsonService {
|
||||
async handle({ stackexchangesite, query }) {
|
||||
const path = `users/${query}`
|
||||
|
||||
const parsedData = await this._requestJson({
|
||||
const parsedData = await this.fetch({
|
||||
schema: reputationSchema,
|
||||
options: { decompress: true, searchParams: { site: stackexchangesite } },
|
||||
url: `https://api.stackexchange.com/2.2/${path}`,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import Joi from 'joi'
|
||||
import { BaseJsonService } from '../index.js'
|
||||
import renderQuestionsBadge from './stackexchange-helpers.js'
|
||||
import {
|
||||
renderQuestionsBadge,
|
||||
StackExchangeBase,
|
||||
} from './stackexchange-base.js'
|
||||
|
||||
const tagSchema = Joi.object({
|
||||
items: Joi.array()
|
||||
@@ -13,9 +15,7 @@ const tagSchema = Joi.object({
|
||||
.required(),
|
||||
}).required()
|
||||
|
||||
export default class StackExchangeQuestions extends BaseJsonService {
|
||||
static category = 'chat'
|
||||
|
||||
export default class StackExchangeQuestions extends StackExchangeBase {
|
||||
static route = {
|
||||
base: 'stackexchange',
|
||||
pattern: ':stackexchangesite/t/:query',
|
||||
@@ -34,10 +34,6 @@ export default class StackExchangeQuestions extends BaseJsonService {
|
||||
},
|
||||
]
|
||||
|
||||
static defaultBadgeData = {
|
||||
label: 'stackoverflow',
|
||||
}
|
||||
|
||||
static render(props) {
|
||||
return renderQuestionsBadge({
|
||||
suffix: '',
|
||||
@@ -48,7 +44,7 @@ export default class StackExchangeQuestions extends BaseJsonService {
|
||||
async handle({ stackexchangesite, query }) {
|
||||
const path = `tags/${query}/info`
|
||||
|
||||
const parsedData = await this._requestJson({
|
||||
const parsedData = await this.fetch({
|
||||
schema: tagSchema,
|
||||
options: { decompress: true, searchParams: { site: stackexchangesite } },
|
||||
url: `https://api.stackexchange.com/2.2/${path}`,
|
||||
|
||||
Reference in New Issue
Block a user