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:
chris48s
2022-12-04 10:53:59 +00:00
committed by GitHub
parent 438677b6f0
commit 53c5cfa94d
9 changed files with 103 additions and 45 deletions

View File

@@ -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'

View File

@@ -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(),

View File

@@ -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`)

View 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)
)
}
}

View 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()
})
})
})

View File

@@ -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),
}
}

View File

@@ -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,

View File

@@ -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}`,

View File

@@ -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}`,