Skip to content

Commit

Permalink
[OpenCollective] update opencollective to api v2 (#9346)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
xxchan and repo-ranger[bot] authored Jul 8, 2023
1 parent 1251ada commit e02c8aa
Show file tree
Hide file tree
Showing 8 changed files with 162 additions and 238 deletions.
6 changes: 5 additions & 1 deletion services/opencollective/opencollective-all.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
36 changes: 2 additions & 34 deletions services/opencollective/opencollective-all.tester.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,17 @@ 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({
label: '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',
})
8 changes: 5 additions & 3 deletions services/opencollective/opencollective-backers.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
78 changes: 2 additions & 76 deletions services/opencollective/opencollective-backers.tester.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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',
})
109 changes: 57 additions & 52 deletions services/opencollective/opencollective-base.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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
}
}
86 changes: 84 additions & 2 deletions services/opencollective/opencollective-by-tier.service.js
Original file line number Diff line number Diff line change
@@ -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 = `<h3>How to get the tierId</h3>
<p>According to <a target="_blank" href="https://developer.opencollective.com/#/api/collectives?id=get-members-per-tier">open collectives documentation</a>, 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)</p>`

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 = [
Expand Down
Loading

0 comments on commit e02c8aa

Please sign in to comment.