Add badges for [Keybase] (#2969)

* Add Keybase PGP badge

* Return 'not found' if the key is not present

* Change the default colour

* Add more constraints to the schema

* Render 64-bit fingerprints

* Add example

* Add a 'hex()' constraint to the fingerprint

* Improve error handling

* Add class 'KeybaseProfile'

* Add unit tests for Keybase PGP

* Add Keybase BTC

* Add unit tests for Keybase BTC

* Add Keybase ZEC

* Add unit tests for Keybase ZEC

* Add Keybase XLM

* Add unit tests for Keybase XLM

* Validate the BTC address using a regex

Regex taken from
https://mokagio.github.io/tech-journal/2014/11/21/regex-bitcoin.html.

* Exclude 'not found' from addresses' value in unit tests

* Remove useless keywords

* Add the link to the Keybase API documentation

* Move the colour into 'defaultBadgeData'

* Remove the HTTP method

'GET' is already the default one.

* Improve the error handling for Keybase BTC

* Add more constraints to the Keybase BTC schema

* Update one unit test for Keybase BTC

* Fix the error handling for Keybase BTC

* Add more unit tests for Keybase BTC

* Improve the error handling for Keybase ZEC

* Improve the error handling for Keybase PGP

* Improve the error handling for Keybase XLM

* Display a real username value in the examples

* Include the status code in the schemas

* Move the category to the base class

The same category is used by all badges.

* Add function 'transform' to the base class

The function 'transform' is used to encapsulate the error handling logic
as it is the same in each service.
This commit is contained in:
Skyper
2019-02-15 17:33:06 +00:00
committed by Caleb Cartwright
parent 90f8ad5b73
commit 6bfa9b1b41
9 changed files with 576 additions and 0 deletions

View File

@@ -0,0 +1,94 @@
'use strict'
const KeybaseProfile = require('./keybase-profile')
const Joi = require('joi')
const { nonNegativeInteger } = require('../validators')
const bitcoinAddressSchema = Joi.object({
status: Joi.object({
code: nonNegativeInteger.required(),
}).required(),
them: Joi.array()
.items(
Joi.object({
cryptocurrency_addresses: Joi.object({
bitcoin: Joi.array().items(
Joi.object({
address: Joi.string().required(),
}).required()
),
})
.required()
.allow(null),
})
.required()
.allow(null)
)
.min(0)
.max(1),
}).required()
module.exports = class KeybaseBTC extends KeybaseProfile {
static get apiVersion() {
return '1.0'
}
static get route() {
return {
base: 'keybase/btc',
pattern: ':username',
}
}
static get defaultBadgeData() {
return {
label: 'btc',
color: 'informational',
}
}
async handle({ username }) {
const options = {
form: {
usernames: username,
fields: 'cryptocurrency_addresses',
},
}
const data = await this.fetch({
schema: bitcoinAddressSchema,
options,
})
const { user } = this.transform({ data })
const bitcoinAddresses = user.cryptocurrency_addresses.bitcoin
if (bitcoinAddresses == null || bitcoinAddresses.length === 0) {
return {
message: 'no bitcoin addresses found',
color: 'inactive',
}
}
return this.constructor.render({ address: bitcoinAddresses[0].address })
}
static render({ address }) {
return {
message: address,
}
}
static get examples() {
return [
{
title: 'Keybase BTC',
namedParams: { username: 'skyplabs' },
staticPreview: this.render({
address: '12ufRLmbEmgjsdGzhUUFY4pcfiQZyRPV9J',
}),
keywords: ['bitcoin'],
},
]
}
}

View File

@@ -0,0 +1,42 @@
'use strict'
const Joi = require('joi')
const { withRegex } = require('../test-validators')
const t = (module.exports = require('../tester').createServiceTester())
t.create('existing bitcoin address')
.get('/skyplabs.json')
.expectJSONTypes(
Joi.object({
name: 'btc',
value: withRegex(/^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$/),
})
)
t.create('unknown username')
.get('/skyplabsssssss.json')
.expectJSONTypes(
Joi.object({
name: 'btc',
value: 'profile not found',
})
)
t.create('invalid username')
.get('/s.json')
.expectJSONTypes(
Joi.object({
name: 'btc',
value: 'invalid username',
})
)
t.create('missing bitcoin address')
.get('/test.json')
.expectJSONTypes(
Joi.object({
name: 'btc',
value: 'no bitcoin addresses found',
})
)

View File

@@ -0,0 +1,89 @@
'use strict'
const KeybaseProfile = require('./keybase-profile')
const Joi = require('joi')
const { nonNegativeInteger } = require('../validators')
const keyFingerprintSchema = Joi.object({
status: Joi.object({
code: nonNegativeInteger.required(),
}).required(),
them: Joi.array()
.items(
Joi.object({
public_keys: {
primary: {
key_fingerprint: Joi.string()
.hex()
.required(),
},
},
})
.required()
.allow(null)
)
.min(0)
.max(1),
}).required()
module.exports = class KeybasePGP extends KeybaseProfile {
static get apiVersion() {
return '1.0'
}
static get route() {
return {
base: 'keybase/pgp',
pattern: ':username',
}
}
static get defaultBadgeData() {
return {
label: 'pgp',
color: 'informational',
}
}
async handle({ username }) {
const options = {
form: {
usernames: username,
fields: 'public_keys',
},
}
const data = await this.fetch({
schema: keyFingerprintSchema,
options,
})
const { user } = this.transform({ data })
const primaryKey = user.public_keys.primary
if (primaryKey == null) {
return {
message: 'no key fingerprint found',
color: 'inactive',
}
}
return this.constructor.render({ fingerprint: primaryKey.key_fingerprint })
}
static render({ fingerprint }) {
return {
message: fingerprint.slice(-16).toUpperCase(),
}
}
static get examples() {
return [
{
title: 'Keybase PGP',
namedParams: { username: 'skyplabs' },
staticPreview: this.render({ fingerprint: '1863145FD39EE07E' }),
},
]
}
}

View File

@@ -0,0 +1,43 @@
'use strict'
const Joi = require('joi')
const t = (module.exports = require('../tester').createServiceTester())
t.create('existing key fingerprint')
.get('/skyplabs.json')
.expectJSONTypes(
Joi.object({
name: 'pgp',
value: Joi.string()
.hex()
.length(16),
})
)
t.create('unknown username')
.get('/skyplabsssssss.json')
.expectJSONTypes(
Joi.object({
name: 'pgp',
value: 'profile not found',
})
)
t.create('invalid username')
.get('/s.json')
.expectJSONTypes(
Joi.object({
name: 'pgp',
value: 'invalid username',
})
)
t.create('missing key fingerprint')
.get('/skyp.json')
.expectJSONTypes(
Joi.object({
name: 'pgp',
value: 'no key fingerprint found',
})
)

View File

@@ -0,0 +1,38 @@
'use strict'
const { BaseJsonService } = require('..')
const { NotFound } = require('..')
module.exports = class KeybaseProfile extends BaseJsonService {
static get apiVersion() {
throw new Error(`apiVersion() is not implemented for ${this.name}`)
}
static get category() {
return 'social'
}
async fetch({ schema, options }) {
const apiVersion = this.constructor.apiVersion
// See https://keybase.io/docs/api/1.0/call/user/lookup.
const url = `https://keybase.io/_/api/${apiVersion}/user/lookup.json`
return this._requestJson({
url,
schema,
options,
})
}
transform({ data }) {
if (data.status.code !== 0) {
throw new NotFound({ prettyMessage: 'invalid username' })
}
if (data.them.length === 0 || !data.them[0]) {
throw new NotFound({ prettyMessage: 'profile not found' })
}
return { user: data.them[0] }
}
}

View File

@@ -0,0 +1,92 @@
'use strict'
const KeybaseProfile = require('./keybase-profile')
const Joi = require('joi')
const { nonNegativeInteger } = require('../validators')
const stellarAddressSchema = Joi.object({
status: Joi.object({
code: nonNegativeInteger.required(),
}).required(),
them: Joi.array()
.items(
Joi.object({
stellar: Joi.object({
primary: Joi.object({
account_id: Joi.string(),
})
.required()
.allow(null),
}).required(),
})
.required()
.allow(null)
)
.min(0)
.max(1),
}).required()
module.exports = class KeybaseXLM extends KeybaseProfile {
static get apiVersion() {
return '1.0'
}
static get route() {
return {
base: 'keybase/xlm',
pattern: ':username',
}
}
static get defaultBadgeData() {
return {
label: 'xlm',
color: 'informational',
}
}
async handle({ username }) {
const options = {
form: {
usernames: username,
fields: 'stellar',
},
}
const data = await this.fetch({
schema: stellarAddressSchema,
options,
})
const { user } = this.transform({ data })
const accountId = user.stellar.primary.account_id
if (accountId == null) {
return {
message: 'no stellar address found',
color: 'inactive',
}
}
return this.constructor.render({ address: accountId })
}
static render({ address }) {
return {
message: address,
}
}
static get examples() {
return [
{
title: 'Keybase XLM',
namedParams: { username: 'skyplabs' },
staticPreview: this.render({
address: 'GCGH37DYONEBPGAZGCHJEZZF3J2Q3EFYZBQBE6UJL5QKTULCMEA6MXLA',
}),
keywords: ['stellar'],
},
]
}
}

View File

@@ -0,0 +1,42 @@
'use strict'
const Joi = require('joi')
const { withRegex } = require('../test-validators')
const t = (module.exports = require('../tester').createServiceTester())
t.create('existing stellar address')
.get('/skyplabs.json')
.expectJSONTypes(
Joi.object({
name: 'xlm',
value: withRegex(/^(?!not found$)/),
})
)
t.create('unknown username')
.get('/skyplabsssssss.json')
.expectJSONTypes(
Joi.object({
name: 'xlm',
value: 'profile not found',
})
)
t.create('invalid username')
.get('/s.json')
.expectJSONTypes(
Joi.object({
name: 'xlm',
value: 'invalid username',
})
)
t.create('missing stellar address')
.get('/test.json')
.expectJSONTypes(
Joi.object({
name: 'xlm',
value: 'no stellar address found',
})
)

View File

@@ -0,0 +1,94 @@
'use strict'
const KeybaseProfile = require('./keybase-profile')
const Joi = require('joi')
const { nonNegativeInteger } = require('../validators')
const zcachAddressSchema = Joi.object({
status: Joi.object({
code: nonNegativeInteger.required(),
}).required(),
them: Joi.array()
.items(
Joi.object({
cryptocurrency_addresses: Joi.object({
zcash: Joi.array().items(
Joi.object({
address: Joi.string().required(),
}).required()
),
})
.required()
.allow(null),
})
.required()
.allow(null)
)
.min(0)
.max(1),
}).required()
module.exports = class KeybaseZEC extends KeybaseProfile {
static get apiVersion() {
return '1.0'
}
static get route() {
return {
base: 'keybase/zec',
pattern: ':username',
}
}
static get defaultBadgeData() {
return {
label: 'zec',
color: 'informational',
}
}
async handle({ username }) {
const options = {
form: {
usernames: username,
fields: 'cryptocurrency_addresses',
},
}
const data = await this.fetch({
schema: zcachAddressSchema,
options,
})
const { user } = this.transform({ data })
const zcashAddresses = user.cryptocurrency_addresses.zcash
if (zcashAddresses == null || zcashAddresses.length === 0) {
return {
message: 'no zcash addresses found',
color: 'inactive',
}
}
return this.constructor.render({ address: zcashAddresses[0].address })
}
static render({ address }) {
return {
message: address,
}
}
static get examples() {
return [
{
title: 'Keybase ZEC',
namedParams: { username: 'skyplabs' },
staticPreview: this.render({
address: 't1RJDxpBcsgqAotqhepkhLFMv2XpMfvnf1y',
}),
keywords: ['zcash'],
},
]
}
}

View File

@@ -0,0 +1,42 @@
'use strict'
const Joi = require('joi')
const { withRegex } = require('../test-validators')
const t = (module.exports = require('../tester').createServiceTester())
t.create('existing zcash address')
.get('/skyplabs.json')
.expectJSONTypes(
Joi.object({
name: 'zec',
value: withRegex(/^(?!not found$)/),
})
)
t.create('unknown username')
.get('/skyplabsssssss.json')
.expectJSONTypes(
Joi.object({
name: 'zec',
value: 'profile not found',
})
)
t.create('invalid username')
.get('/s.json')
.expectJSONTypes(
Joi.object({
name: 'zec',
value: 'invalid username',
})
)
t.create('missing zcash address')
.get('/test.json')
.expectJSONTypes(
Joi.object({
name: 'zec',
value: 'no zcash addresses found',
})
)