From e02c8aa88434a5a8ae9eef8235738a62ef33cdb1 Mon Sep 17 00:00:00 2001 From: xxchan Date: Sat, 8 Jul 2023 12:18:02 +0200 Subject: [PATCH] [OpenCollective] update opencollective to api v2 (#9346) * update opencollective to api v2 * fix tests * fix: do not filter by accountType for opencollective/all * remove 404 * remove required in schema * cnt -> count * keep by-tier code as-is --------- Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com> --- .../opencollective-all.service.js | 6 +- .../opencollective-all.tester.js | 36 +----- .../opencollective-backers.service.js | 8 +- .../opencollective-backers.tester.js | 78 +------------ .../opencollective/opencollective-base.js | 109 +++++++++--------- .../opencollective-by-tier.service.js | 86 +++++++++++++- .../opencollective-sponsors.service.js | 7 +- .../opencollective-sponsors.tester.js | 70 +---------- 8 files changed, 162 insertions(+), 238 deletions(-) diff --git a/services/opencollective/opencollective-all.service.js b/services/opencollective/opencollective-all.service.js index dac3229f97791..b7784bf36b4db 100644 --- a/services/opencollective/opencollective-all.service.js +++ b/services/opencollective/opencollective-all.service.js @@ -17,7 +17,11 @@ export default class OpencollectiveAll extends OpencollectiveBase { } async handle({ collective }) { - const { backersCount } = await this.fetchCollectiveInfo(collective) + const data = await this.fetchCollectiveInfo({ + collective, + accountType: [], + }) + const backersCount = this.getCount(data) return this.constructor.render(backersCount) } } diff --git a/services/opencollective/opencollective-all.tester.js b/services/opencollective/opencollective-all.tester.js index d827313c509ad..61f2ef3e2cabb 100644 --- a/services/opencollective/opencollective-all.tester.js +++ b/services/opencollective/opencollective-all.tester.js @@ -2,25 +2,6 @@ import { nonNegativeInteger } from '../validators.js' import { createServiceTester } from '../tester.js' export const t = await createServiceTester() -t.create('renders correctly') - .get('/shields.json') - .intercept(nock => - nock('https://opencollective.com/').get('/shields.json').reply(200, { - slug: 'shields', - currency: 'USD', - image: - 'https://opencollective-production.s3-us-west-1.amazonaws.com/44dcbb90-1ee9-11e8-a4c3-7bb1885c0b6e.png', - balance: 105494, - yearlyIncome: 157371, - backersCount: 35, - contributorsCount: 276, - }) - ) - .expectBadge({ - label: 'backers and sponsors', - message: '35', - color: 'brightgreen', - }) t.create('gets amount of backers and sponsors') .get('/shields.json') .expectBadge({ @@ -28,23 +9,10 @@ t.create('gets amount of backers and sponsors') message: nonNegativeInteger, }) -t.create('renders not found correctly') - .get('/nonexistent-collective.json') - .intercept(nock => - nock('https://opencollective.com/') - .get('/nonexistent-collective.json') - .reply(404, 'Not found') - ) - .expectBadge({ - label: 'backers and sponsors', - message: 'collective not found', - color: 'red', - }) - t.create('handles not found correctly') .get('/nonexistent-collective.json') .expectBadge({ label: 'backers and sponsors', - message: 'collective not found', - color: 'red', + message: 'No collective found with slug nonexistent-collective', + color: 'lightgrey', }) diff --git a/services/opencollective/opencollective-backers.service.js b/services/opencollective/opencollective-backers.service.js index 3f2b7da4d7541..8afdd56784e39 100644 --- a/services/opencollective/opencollective-backers.service.js +++ b/services/opencollective/opencollective-backers.service.js @@ -17,10 +17,12 @@ export default class OpencollectiveBackers extends OpencollectiveBase { } async handle({ collective }) { - const { backersCount } = await this.fetchCollectiveBackersCount( + const data = await this.fetchCollectiveInfo({ collective, - { userType: 'users' } - ) + accountType: ['INDIVIDUAL'], + }) + const backersCount = this.getCount(data) + return this.constructor.render(backersCount) } } diff --git a/services/opencollective/opencollective-backers.tester.js b/services/opencollective/opencollective-backers.tester.js index 7163f9a5da4ce..29855ae57d07c 100644 --- a/services/opencollective/opencollective-backers.tester.js +++ b/services/opencollective/opencollective-backers.tester.js @@ -2,80 +2,6 @@ import { nonNegativeInteger } from '../validators.js' import { createServiceTester } from '../tester.js' export const t = await createServiceTester() -t.create('renders correctly') - .get('/shields.json') - .intercept(nock => - nock('https://opencollective.com/') - .get('/shields/members/users.json') - .reply(200, [ - { MemberId: 8685, type: 'USER', role: 'ADMIN' }, - { MemberId: 8686, type: 'USER', role: 'ADMIN' }, - { MemberId: 8682, type: 'USER', role: 'ADMIN' }, - { MemberId: 10305, type: 'USER', role: 'BACKER', tier: 'backer' }, - { MemberId: 10396, type: 'USER', role: 'BACKER', tier: 'backer' }, - { MemberId: 10733, type: 'USER', role: 'BACKER' }, - { MemberId: 8684, type: 'USER', role: 'ADMIN' }, - { MemberId: 10741, type: 'USER', role: 'BACKER' }, - { - MemberId: 10756, - type: 'USER', - role: 'BACKER', - tier: 'monthly backer', - }, - { MemberId: 11578, type: 'USER', role: 'CONTRIBUTOR' }, - { MemberId: 13459, type: 'USER', role: 'CONTRIBUTOR' }, - { - MemberId: 13507, - type: 'USER', - role: 'BACKER', - tier: 'monthly backer', - }, - { MemberId: 13512, type: 'USER', role: 'BACKER' }, - { MemberId: 13513, type: 'USER', role: 'FUNDRAISER' }, - { MemberId: 13984, type: 'USER', role: 'BACKER', tier: 'backer' }, - { MemberId: 14916, type: 'USER', role: 'BACKER' }, - { - MemberId: 16326, - type: 'USER', - role: 'BACKER', - tier: 'monthly backer', - }, - { MemberId: 18252, type: 'USER', role: 'BACKER', tier: 'backer' }, - { MemberId: 17631, type: 'USER', role: 'BACKER', tier: 'backer' }, - { - MemberId: 16420, - type: 'USER', - role: 'BACKER', - tier: 'monthly backer', - }, - { MemberId: 17186, type: 'USER', role: 'BACKER', tier: 'backer' }, - { MemberId: 18791, type: 'USER', role: 'BACKER', tier: 'backer' }, - { - MemberId: 19279, - type: 'USER', - role: 'BACKER', - tier: 'monthly backer', - }, - { MemberId: 19863, type: 'USER', role: 'BACKER', tier: 'backer' }, - { MemberId: 21451, type: 'USER', role: 'BACKER', tier: 'backer' }, - { MemberId: 22718, type: 'USER', role: 'BACKER' }, - { MemberId: 23561, type: 'USER', role: 'BACKER', tier: 'backer' }, - { MemberId: 25092, type: 'USER', role: 'CONTRIBUTOR' }, - { MemberId: 24473, type: 'USER', role: 'BACKER', tier: 'backer' }, - { MemberId: 25439, type: 'USER', role: 'BACKER', tier: 'backer' }, - { MemberId: 24483, type: 'USER', role: 'BACKER', tier: 'backer' }, - { MemberId: 25090, type: 'USER', role: 'CONTRIBUTOR' }, - { MemberId: 26404, type: 'USER', role: 'BACKER', tier: 'backer' }, - { MemberId: 27026, type: 'USER', role: 'BACKER', tier: 'backer' }, - { MemberId: 27132, type: 'USER', role: 'CONTRIBUTOR' }, - ]) - ) - .expectBadge({ - label: 'backers', - message: '25', - color: 'brightgreen', - }) - t.create('gets amount of backers').get('/shields.json').expectBadge({ label: 'backers', message: nonNegativeInteger, @@ -85,6 +11,6 @@ t.create('handles not found correctly') .get('/nonexistent-collective.json') .expectBadge({ label: 'backers', - message: 'collective not found', - color: 'red', + message: 'No collective found with slug nonexistent-collective', + color: 'lightgrey', }) diff --git a/services/opencollective/opencollective-base.js b/services/opencollective/opencollective-base.js index 203cfabf03274..2b4cc50340506 100644 --- a/services/opencollective/opencollective-base.js +++ b/services/opencollective/opencollective-base.js @@ -1,26 +1,30 @@ +import gql from 'graphql-tag' import Joi from 'joi' +import { BaseGraphqlService } from '../index.js' import { nonNegativeInteger } from '../validators.js' -import { BaseJsonService } from '../index.js' import { metric } from '../text-formatters.js' -// https://developer.opencollective.com/#/api/collectives?id=get-info -const collectiveDetailsSchema = Joi.object().keys({ - slug: Joi.string().required(), - backersCount: nonNegativeInteger, -}) +const schema = Joi.object({ + data: Joi.object({ + account: Joi.object({ + name: Joi.string(), + slug: Joi.string(), + members: Joi.object({ + totalCount: nonNegativeInteger, + nodes: Joi.array().items( + Joi.object({ + tier: Joi.object({ + legacyId: Joi.number(), + name: Joi.string(), + }).allow(null), + }) + ), + }).required(), + }).required(), + }).required(), +}).required() -// https://developer.opencollective.com/#/api/collectives?id=get-members -function buildMembersArraySchema({ userType, tierRequired }) { - const keys = { - MemberId: Joi.number().required(), - type: userType || Joi.string().required(), - role: Joi.string().required(), - } - if (tierRequired) keys.tier = Joi.string().required() - return Joi.array().items(Joi.object().keys(keys)) -} - -export default class OpencollectiveBase extends BaseJsonService { +export default class OpencollectiveBase extends BaseGraphqlService { static category = 'funding' static buildRoute(base, withTierId) { @@ -38,45 +42,46 @@ export default class OpencollectiveBase extends BaseJsonService { } } - async fetchCollectiveInfo(collective) { - return this._requestJson({ - schema: collectiveDetailsSchema, - // https://developer.opencollective.com/#/api/collectives?id=get-info - url: `https://opencollective.com/${collective}.json`, - httpErrors: { - 404: 'collective not found', + async fetchCollectiveInfo({ collective, accountType }) { + return this._requestGraphql({ + schema, + url: 'https://api.opencollective.com/graphql/v2', + query: gql` + query account($slug: String, $accountType: [AccountType]) { + account(slug: $slug) { + name + slug + members(accountType: $accountType, role: BACKER) { + totalCount + nodes { + tier { + legacyId + name + } + } + } + } + } + `, + variables: { + slug: collective, + accountType, + }, + options: { + headers: { 'content-type': 'application/json' }, }, }) } - async fetchCollectiveBackersCount(collective, { userType, tierId }) { - const schema = buildMembersArraySchema({ - userType: - userType === 'users' - ? 'USER' - : userType === 'organizations' - ? 'ORGANIZATION' - : undefined, - tierRequired: tierId, - }) - const members = await this._requestJson({ - schema, - // https://developer.opencollective.com/#/api/collectives?id=get-members - // https://developer.opencollective.com/#/api/collectives?id=get-members-per-tier - url: `https://opencollective.com/${collective}/members/${ - userType || 'all' - }.json${tierId ? `?TierId=${tierId}` : ''}`, - httpErrors: { - 404: 'collective not found', + getCount(data) { + const { + data: { + account: { + members: { totalCount }, + }, }, - }) + } = data - const result = { - backersCount: members.filter(member => member.role === 'BACKER').length, - } - // Find the title of the tier - if (tierId && members.length > 0) - result.tier = members.map(member => member.tier)[0] - return result + return totalCount } } diff --git a/services/opencollective/opencollective-by-tier.service.js b/services/opencollective/opencollective-by-tier.service.js index 98acc383f509d..673597df41d2f 100644 --- a/services/opencollective/opencollective-by-tier.service.js +++ b/services/opencollective/opencollective-by-tier.service.js @@ -1,9 +1,91 @@ -import OpencollectiveBase from './opencollective-base.js' +import Joi from 'joi' +import { nonNegativeInteger } from '../validators.js' +import { BaseJsonService } from '../index.js' +import { metric } from '../text-formatters.js' const documentation = `

How to get the tierId

According to open collectives documentation, you can find the tierId by looking at the URL after clicking on a Tier Card on the collective page. (e.g. tierId for https://opencollective.com/shields/order/2988 is 2988)

` -export default class OpencollectiveByTier extends OpencollectiveBase { +// https://developer.opencollective.com/#/api/collectives?id=get-info +const collectiveDetailsSchema = Joi.object().keys({ + slug: Joi.string().required(), + backersCount: nonNegativeInteger, +}) + +// https://developer.opencollective.com/#/api/collectives?id=get-members +function buildMembersArraySchema({ userType, tierRequired }) { + const keys = { + MemberId: Joi.number().required(), + type: userType || Joi.string().required(), + role: Joi.string().required(), + } + if (tierRequired) keys.tier = Joi.string().required() + return Joi.array().items(Joi.object().keys(keys)) +} + +class OpencollectiveBaseJson extends BaseJsonService { + static category = 'funding' + + static buildRoute(base, withTierId) { + return { + base: `opencollective${base ? `/${base}` : ''}`, + pattern: `:collective${withTierId ? '/:tierId' : ''}`, + } + } + + static render(backersCount, label) { + return { + label, + message: metric(backersCount), + color: backersCount > 0 ? 'brightgreen' : 'lightgrey', + } + } + + async fetchCollectiveInfo(collective) { + return this._requestJson({ + schema: collectiveDetailsSchema, + // https://developer.opencollective.com/#/api/collectives?id=get-info + url: `https://opencollective.com/${collective}.json`, + httpErrors: { + 404: 'collective not found', + }, + }) + } + + async fetchCollectiveBackersCount(collective, { userType, tierId }) { + const schema = buildMembersArraySchema({ + userType: + userType === 'users' + ? 'USER' + : userType === 'organizations' + ? 'ORGANIZATION' + : undefined, + tierRequired: tierId, + }) + const members = await this._requestJson({ + schema, + // https://developer.opencollective.com/#/api/collectives?id=get-members + // https://developer.opencollective.com/#/api/collectives?id=get-members-per-tier + url: `https://opencollective.com/${collective}/members/${ + userType || 'all' + }.json${tierId ? `?TierId=${tierId}` : ''}`, + httpErrors: { + 404: 'collective not found', + }, + }) + + const result = { + backersCount: members.filter(member => member.role === 'BACKER').length, + } + // Find the title of the tier + if (tierId && members.length > 0) + result.tier = members.map(member => member.tier)[0] + return result + } +} + +// TODO: 1. pagination is needed. 2. use new graphql api instead of legacy rest api +export default class OpencollectiveByTier extends OpencollectiveBaseJson { static route = this.buildRoute('tier', true) static examples = [ diff --git a/services/opencollective/opencollective-sponsors.service.js b/services/opencollective/opencollective-sponsors.service.js index 0f41df6f03657..c6015ce40db5e 100644 --- a/services/opencollective/opencollective-sponsors.service.js +++ b/services/opencollective/opencollective-sponsors.service.js @@ -17,10 +17,11 @@ export default class OpencollectiveSponsors extends OpencollectiveBase { } async handle({ collective }) { - const { backersCount } = await this.fetchCollectiveBackersCount( + const data = await this.fetchCollectiveInfo({ collective, - { userType: 'organizations' } - ) + accountType: ['ORGANIZATION'], + }) + const backersCount = this.getCount(data) return this.constructor.render(backersCount) } } diff --git a/services/opencollective/opencollective-sponsors.tester.js b/services/opencollective/opencollective-sponsors.tester.js index 300af584c42ce..d563744a49ae3 100644 --- a/services/opencollective/opencollective-sponsors.tester.js +++ b/services/opencollective/opencollective-sponsors.tester.js @@ -2,80 +2,16 @@ import { nonNegativeInteger } from '../validators.js' import { createServiceTester } from '../tester.js' export const t = await createServiceTester() -t.create('renders correctly') - .get('/shields.json') - .intercept(nock => - nock('https://opencollective.com/') - .get('/shields/members/organizations.json') - .reply(200, [ - { MemberId: 8683, type: 'ORGANIZATION', role: 'HOST' }, - { - MemberId: 13484, - type: 'ORGANIZATION', - role: 'BACKER', - tier: 'backer', - }, - { MemberId: 13508, type: 'ORGANIZATION', role: 'FUNDRAISER' }, - { MemberId: 15987, type: 'ORGANIZATION', role: 'BACKER' }, - { - MemberId: 16561, - type: 'ORGANIZATION', - role: 'BACKER', - tier: 'sponsor', - }, - { - MemberId: 16469, - type: 'ORGANIZATION', - role: 'BACKER', - tier: 'sponsor', - }, - { - MemberId: 18162, - type: 'ORGANIZATION', - role: 'BACKER', - tier: 'sponsor', - }, - { - MemberId: 21023, - type: 'ORGANIZATION', - role: 'BACKER', - tier: 'sponsor', - }, - { - MemberId: 21482, - type: 'ORGANIZATION', - role: 'BACKER', - tier: 'monthly backer', - }, - { - MemberId: 26367, - type: 'ORGANIZATION', - role: 'BACKER', - tier: 'monthly backer', - }, - { MemberId: 27531, type: 'ORGANIZATION', role: 'BACKER' }, - { - MemberId: 29443, - type: 'ORGANIZATION', - role: 'BACKER', - tier: 'monthly backer', - }, - ]) - ) - .expectBadge({ - label: 'sponsors', - message: '10', - color: 'brightgreen', - }) t.create('gets amount of sponsors').get('/shields.json').expectBadge({ label: 'sponsors', message: nonNegativeInteger, + color: 'brightgreen', }) t.create('handles not found correctly') .get('/nonexistent-collective.json') .expectBadge({ label: 'sponsors', - message: 'collective not found', - color: 'red', + message: 'No collective found with slug nonexistent-collective', + color: 'lightgrey', })