From bedba47d777cb3b27467fa8b020ac4c5a0105fdd Mon Sep 17 00:00:00 2001 From: Paul Melnikow <github@paulmelnikow.com> Date: Mon, 27 Aug 2018 13:29:54 -0400 Subject: [PATCH] Move legacy services from server.js into services/ (#1958) This builds on the work of #1931 by moving the legacy services into `services/`. --- lib/github-provider.js | 124 - lib/teamcity-badge-helpers.js | 51 - lib/vscode-badge-helpers.js | 41 - server.js | 6830 +---------------- services/amo/amo.service.js | 93 + services/ansible/ansible.service.js | 54 + services/aur/aur.service.js | 67 + services/base.js | 2 +- services/base.spec.js | 5 +- services/beerpay/beerpay.service.js | 42 + services/bintray/bintray.service.js | 53 + .../bitbucket/bitbucket-issues.service.js | 51 + .../bitbucket/bitbucket-pipelines.service.js | 77 + .../bitbucket-pull-request.service.js | 51 + services/bithound/bithound.service.js | 18 + services/bitrise/bitrise.service.js | 54 + services/bountysource/bountysource.service.js | 45 + services/bower/bower-license.service.js | 45 + services/bower/bower-version.service.js | 58 + services/bugzilla/bugzilla.service.js | 76 + services/buildkite/buildkite.service.js | 49 + services/bundlephobia/bundlephobia.service.js | 72 + services/cauditor/cauditor.service.js | 18 + .../chrome-web-store.service.js | 98 + services/cocoapods/cocoapods-apps.service.js | 49 + .../cocoapods/cocoapods-downloads.service.js | 53 + .../cocoapods/cocoapods-metrics.service.js | 41 + services/cocoapods/cocoapods.service.js | 63 + services/codacy/codacy-coverage.service.js | 49 + services/codacy/codacy-grade.service.js | 63 + services/codeclimate/codeclimate.service.js | 137 + services/codecov/codecov.service.js | 54 + services/codeship/codeship.service.js | 75 + services/codetally/codetally.service.js | 38 + services/conda/conda.service.js | 90 + services/cookbook/cookbook.service.js | 43 + services/coveralls/coveralls.service.js | 59 + .../coverity/coverity-on-demand.service.js | 59 + services/coverity/coverity-scan.service.js | 45 + services/cpan/cpan.service.js | 45 + services/cran/cran.service.js | 62 + services/crates/crates.service.js | 95 + services/ctan/ctan.service.js | 62 + services/david/david.service.js | 79 + services/dependabot/dependabot.service.js | 47 + services/depfu/depfu.service.js | 34 + services/discord/discord.service.js | 49 + services/discourse/discourse.service.js | 88 + services/dockbit/dockbit.service.js | 57 + services/docker/docker.service.js | 168 + services/dotnetstatus/dotnetstatus.service.js | 18 + services/dub/dub-download.service.js | 72 + services/dub/dub-license-version.service.js | 57 + .../eclipse-marketplace.service.js | 93 + services/elm-package/elm-package.service.js | 38 + services/gemnasium/gemnasium.service.js | 64 + .../github/github-commit-activity.service.js | 69 + .../github/github-commit-status.service.js | 66 + .../github/github-commits-since.service.js | 68 + .../github/github-contributors.service.js | 56 + services/github/github-downloads.service.js | 116 + services/github/github-followers.service.js | 42 + services/github/github-forks.service.js | 49 + {lib => services/github}/github-helpers.js | 4 +- .../github}/github-helpers.spec.js | 0 .../github/github-issue-detail.service.js | 116 + services/github/github-issues.service.js | 76 + services/github/github-languages.service.js | 84 + services/github/github-last-commit.service.js | 51 + services/github/github-license.service.js | 57 + .../github/github-manifest-version.service.js | 79 + .../github-pull-request-status.service.js | 85 + .../github/github-release-date.service.js | 61 + services/github/github-release.service.js | 52 + services/github/github-repo-size.service.js | 44 + services/github/github-search.service.js | 45 + services/github/github-size.service.js | 59 + services/github/github-stars.service.js | 48 + services/github/github-tag.service.js | 49 + services/github/github-watchers.service.js | 47 + services/gitter/gitter.service.js | 21 + services/gratipay/gratipay.service.js | 21 + services/hackage/hackage-deps.service.js | 52 + services/hackage/hackage-version.service.js | 45 + services/hexpm/hexpm.service.js | 82 + services/homebrew/homebrew.service.js | 44 + services/imagelayers/imagelayers.service.js | 58 + services/issuestats/issuestats.service.js | 77 + services/itunes/itunes.service.js | 46 + services/jenkins/jenkins-build.service.js | 71 + services/jenkins/jenkins-coverage.service.js | 80 + services/jenkins/jenkins-plugin.service.js | 51 + services/jenkins/jenkins-tests.service.js | 83 + services/jetbrains/jetbrains.service.js | 78 + services/jira/jira-issue.service.js | 74 + services/jira/jira-sprint.service.js | 74 + services/jitpack/jitpack.service.js | 54 + services/legacy-service.js | 13 +- services/lgtm/lgtm-alerts.service.js | 47 + services/lgtm/lgtm-grade.service.js | 75 + services/liberapay/liberapay.service.js | 83 + .../librariesio-dependencies.service.js | 73 + services/libscore/libscore.service.js | 43 + .../luarocks}/luarocks-version.js | 0 .../luarocks}/luarocks-version.spec.js | 0 services/luarocks/luarocks.service.js | 80 + services/magnumci/magnumci.service.js | 18 + services/maintenance/maintenance.service.js | 41 + .../maven-central/maven-central.service.js | 60 + .../maven-metadata/maven-metadata.service.js | 52 + services/microbadger/microbadger.service.js | 90 + {lib => services/nexus}/nexus-version.js | 0 services/nexus/nexus.service.js | 114 + services/nsp/nsp.service.js | 135 + .../nuget/nuget.service.js | 50 +- services/osstracker/osstracker.service.js | 57 + .../packagecontrol/packagecontrol.service.js | 81 + .../packagist/packagist-downloads.service.js | 57 + .../packagist/packagist-license.service.js | 60 + .../packagist-php-version.service.js | 43 + .../packagist/packagist-version.service.js | 103 + services/php-eye/php-eye-hhvm.service.js | 74 + .../php-eye/php-eye-php-version.service.js | 89 + services/pub/pub.service.js | 42 + .../puppetforge-modules.service.js | 86 + .../puppetforge/puppetforge-users.service.js | 51 + services/readthedocs/readthedocs.service.js | 49 + services/redmine/redmine.service.js | 55 + services/requires/requires.service.js | 53 + services/scrutinizer/scrutinizer.service.js | 87 + services/sensiolabs/sensiolabs.service.js | 85 + services/snap-ci/snap-ci.service.js | 17 + services/sonarqube/sonarqube.service.js | 163 + services/sourceforge/sourceforge.service.js | 77 + services/sourcegraph/sourcegraph.service.js | 35 + .../stackexchange/stackexchange.service.js | 66 + services/swagger/swagger.service.js | 67 + services/teamcity/teamcity-build.service.js | 91 + .../teamcity/teamcity-coverage.service.js | 63 + services/travis/travis-build.service.js | 63 + services/travis/travis-php-version.service.js | 85 + services/twitter/twitter.service.js | 83 + .../vaadin-directory.service.js | 105 + services/versioneye/versioneye.service.js | 18 + .../vscode-marketplace.service.js | 118 + services/vso/vso.service.js | 50 + services/waffle/waffle.service.js | 59 + services/website/website.service.js | 53 + services/wheelmap/wheelmap.service.js | 40 + .../wordpress/wordpress-plugin.service.js | 188 + services/wordpress/wordpress-theme.service.js | 90 + 151 files changed, 9196 insertions(+), 6935 deletions(-) delete mode 100644 lib/github-provider.js delete mode 100644 lib/teamcity-badge-helpers.js delete mode 100644 lib/vscode-badge-helpers.js create mode 100644 services/amo/amo.service.js create mode 100644 services/ansible/ansible.service.js create mode 100644 services/aur/aur.service.js create mode 100644 services/beerpay/beerpay.service.js create mode 100644 services/bintray/bintray.service.js create mode 100644 services/bitbucket/bitbucket-issues.service.js create mode 100644 services/bitbucket/bitbucket-pipelines.service.js create mode 100644 services/bitbucket/bitbucket-pull-request.service.js create mode 100644 services/bithound/bithound.service.js create mode 100644 services/bitrise/bitrise.service.js create mode 100644 services/bountysource/bountysource.service.js create mode 100644 services/bower/bower-license.service.js create mode 100644 services/bower/bower-version.service.js create mode 100644 services/bugzilla/bugzilla.service.js create mode 100644 services/buildkite/buildkite.service.js create mode 100644 services/bundlephobia/bundlephobia.service.js create mode 100644 services/cauditor/cauditor.service.js create mode 100644 services/chrome-web-store/chrome-web-store.service.js create mode 100644 services/cocoapods/cocoapods-apps.service.js create mode 100644 services/cocoapods/cocoapods-downloads.service.js create mode 100644 services/cocoapods/cocoapods-metrics.service.js create mode 100644 services/cocoapods/cocoapods.service.js create mode 100644 services/codacy/codacy-coverage.service.js create mode 100644 services/codacy/codacy-grade.service.js create mode 100644 services/codeclimate/codeclimate.service.js create mode 100644 services/codecov/codecov.service.js create mode 100644 services/codeship/codeship.service.js create mode 100644 services/codetally/codetally.service.js create mode 100644 services/conda/conda.service.js create mode 100644 services/cookbook/cookbook.service.js create mode 100644 services/coveralls/coveralls.service.js create mode 100644 services/coverity/coverity-on-demand.service.js create mode 100644 services/coverity/coverity-scan.service.js create mode 100644 services/cpan/cpan.service.js create mode 100644 services/cran/cran.service.js create mode 100644 services/crates/crates.service.js create mode 100644 services/ctan/ctan.service.js create mode 100644 services/david/david.service.js create mode 100644 services/dependabot/dependabot.service.js create mode 100644 services/depfu/depfu.service.js create mode 100644 services/discord/discord.service.js create mode 100644 services/discourse/discourse.service.js create mode 100644 services/dockbit/dockbit.service.js create mode 100644 services/docker/docker.service.js create mode 100644 services/dotnetstatus/dotnetstatus.service.js create mode 100644 services/dub/dub-download.service.js create mode 100644 services/dub/dub-license-version.service.js create mode 100644 services/eclipse-marketplace/eclipse-marketplace.service.js create mode 100644 services/elm-package/elm-package.service.js create mode 100644 services/gemnasium/gemnasium.service.js create mode 100644 services/github/github-commit-activity.service.js create mode 100644 services/github/github-commit-status.service.js create mode 100644 services/github/github-commits-since.service.js create mode 100644 services/github/github-contributors.service.js create mode 100644 services/github/github-downloads.service.js create mode 100644 services/github/github-followers.service.js create mode 100644 services/github/github-forks.service.js rename {lib => services/github}/github-helpers.js (88%) rename {lib => services/github}/github-helpers.spec.js (100%) create mode 100644 services/github/github-issue-detail.service.js create mode 100644 services/github/github-issues.service.js create mode 100644 services/github/github-languages.service.js create mode 100644 services/github/github-last-commit.service.js create mode 100644 services/github/github-license.service.js create mode 100644 services/github/github-manifest-version.service.js create mode 100644 services/github/github-pull-request-status.service.js create mode 100644 services/github/github-release-date.service.js create mode 100644 services/github/github-release.service.js create mode 100644 services/github/github-repo-size.service.js create mode 100644 services/github/github-search.service.js create mode 100644 services/github/github-size.service.js create mode 100644 services/github/github-stars.service.js create mode 100644 services/github/github-tag.service.js create mode 100644 services/github/github-watchers.service.js create mode 100644 services/gitter/gitter.service.js create mode 100644 services/gratipay/gratipay.service.js create mode 100644 services/hackage/hackage-deps.service.js create mode 100644 services/hackage/hackage-version.service.js create mode 100644 services/hexpm/hexpm.service.js create mode 100644 services/homebrew/homebrew.service.js create mode 100644 services/imagelayers/imagelayers.service.js create mode 100644 services/issuestats/issuestats.service.js create mode 100644 services/itunes/itunes.service.js create mode 100644 services/jenkins/jenkins-build.service.js create mode 100644 services/jenkins/jenkins-coverage.service.js create mode 100644 services/jenkins/jenkins-plugin.service.js create mode 100644 services/jenkins/jenkins-tests.service.js create mode 100644 services/jetbrains/jetbrains.service.js create mode 100644 services/jira/jira-issue.service.js create mode 100644 services/jira/jira-sprint.service.js create mode 100644 services/jitpack/jitpack.service.js create mode 100644 services/lgtm/lgtm-alerts.service.js create mode 100644 services/lgtm/lgtm-grade.service.js create mode 100644 services/liberapay/liberapay.service.js create mode 100644 services/librariesio/librariesio-dependencies.service.js create mode 100644 services/libscore/libscore.service.js rename {lib => services/luarocks}/luarocks-version.js (100%) rename {lib => services/luarocks}/luarocks-version.spec.js (100%) create mode 100644 services/luarocks/luarocks.service.js create mode 100644 services/magnumci/magnumci.service.js create mode 100644 services/maintenance/maintenance.service.js create mode 100644 services/maven-central/maven-central.service.js create mode 100644 services/maven-metadata/maven-metadata.service.js create mode 100644 services/microbadger/microbadger.service.js rename {lib => services/nexus}/nexus-version.js (100%) create mode 100644 services/nexus/nexus.service.js create mode 100644 services/nsp/nsp.service.js rename lib/nuget-provider.js => services/nuget/nuget.service.js (87%) create mode 100644 services/osstracker/osstracker.service.js create mode 100644 services/packagecontrol/packagecontrol.service.js create mode 100644 services/packagist/packagist-downloads.service.js create mode 100644 services/packagist/packagist-license.service.js create mode 100644 services/packagist/packagist-php-version.service.js create mode 100644 services/packagist/packagist-version.service.js create mode 100644 services/php-eye/php-eye-hhvm.service.js create mode 100644 services/php-eye/php-eye-php-version.service.js create mode 100644 services/pub/pub.service.js create mode 100644 services/puppetforge/puppetforge-modules.service.js create mode 100644 services/puppetforge/puppetforge-users.service.js create mode 100644 services/readthedocs/readthedocs.service.js create mode 100644 services/redmine/redmine.service.js create mode 100644 services/requires/requires.service.js create mode 100644 services/scrutinizer/scrutinizer.service.js create mode 100644 services/sensiolabs/sensiolabs.service.js create mode 100644 services/snap-ci/snap-ci.service.js create mode 100644 services/sonarqube/sonarqube.service.js create mode 100644 services/sourceforge/sourceforge.service.js create mode 100644 services/sourcegraph/sourcegraph.service.js create mode 100644 services/stackexchange/stackexchange.service.js create mode 100644 services/swagger/swagger.service.js create mode 100644 services/teamcity/teamcity-build.service.js create mode 100644 services/teamcity/teamcity-coverage.service.js create mode 100644 services/travis/travis-build.service.js create mode 100644 services/travis/travis-php-version.service.js create mode 100644 services/twitter/twitter.service.js create mode 100644 services/vaadin-directory/vaadin-directory.service.js create mode 100644 services/versioneye/versioneye.service.js create mode 100644 services/vscode-marketplace/vscode-marketplace.service.js create mode 100644 services/vso/vso.service.js create mode 100644 services/waffle/waffle.service.js create mode 100644 services/website/website.service.js create mode 100644 services/wheelmap/wheelmap.service.js create mode 100644 services/wordpress/wordpress-plugin.service.js create mode 100644 services/wordpress/wordpress-theme.service.js diff --git a/lib/github-provider.js b/lib/github-provider.js deleted file mode 100644 index 28b106c0ed..0000000000 --- a/lib/github-provider.js +++ /dev/null @@ -1,124 +0,0 @@ -'use strict' -const moment = require('moment') - -const { - makeBadgeData: getBadgeData, - makeLabel: getLabel, - makeLogo: getLogo, -} = require('./badge-data') -const { formatDate } = require('./text-formatters') - -const { age } = require('./color-formatters') - -// GitHub commits since integration. -function mapGithubCommitsSince({ camp, cache }, githubApiProvider) { - camp.route( - /^\/github\/commits-since\/([^/]+)\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, - cache((data, match, sendBadge, request) => { - const user = match[1] // eg, SubtitleEdit - const repo = match[2] // eg, subtitleedit - const version = match[3] // eg, 3.4.7 or latest - const format = match[4] - const badgeData = getBadgeData('commits since ' + version, data) - - function setCommitsSinceBadge(user, repo, version) { - const apiUrl = `/repos/${user}/${repo}/compare/${version}...master` - if (badgeData.template === 'social') { - badgeData.logo = getLogo('github', data) - } - githubApiProvider.request(request, apiUrl, {}, (err, res, buffer) => { - if (err != null) { - badgeData.text[1] = 'inaccessible' - sendBadge(format, badgeData) - return - } - - try { - const result = JSON.parse(buffer) - badgeData.text[1] = result.ahead_by - badgeData.colorscheme = 'blue' - badgeData.text[0] = getLabel('commits since ' + version, data) - sendBadge(format, badgeData) - } catch (e) { - badgeData.text[1] = 'invalid' - sendBadge(format, badgeData) - } - }) - } - - if (version === 'latest') { - const url = `/repos/${user}/${repo}/releases/latest` - githubApiProvider.request(request, url, {}, (err, res, buffer) => { - if (err != null) { - badgeData.text[1] = 'inaccessible' - sendBadge(format, badgeData) - return - } - try { - const data = JSON.parse(buffer) - setCommitsSinceBadge(user, repo, data.tag_name) - } catch (e) { - badgeData.text[1] = 'invalid' - sendBadge(format, badgeData) - } - }) - } else { - setCommitsSinceBadge(user, repo, version) - } - }) - ) -} - -//Github Release & Pre-Release Date Integration release-date-pre (?:\/(all))? -function mapGithubReleaseDate({ camp, cache }, githubApiProvider) { - camp.route( - /^\/github\/(release-date|release-date-pre)\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, - cache((data, match, sendBadge, request) => { - const releaseType = match[1] // eg, release-date-pre / release-date - const user = match[2] // eg, microsoft - const repo = match[3] // eg, vscode - const format = match[4] - let apiUrl = `/repos/${user}/${repo}/releases` - if (releaseType === 'release-date') { - apiUrl += '/latest' - } - const badgeData = getBadgeData('release date', data) - if (badgeData.template === 'social') { - badgeData.logo = getLogo('github', data) - } - githubApiProvider.request(request, apiUrl, {}, (err, res, buffer) => { - if (err != null) { - badgeData.text[1] = 'inaccessible' - sendBadge(format, badgeData) - return - } - - //github return 404 if repo not found or no release - if (res.statusCode === 404) { - badgeData.text[1] = 'no releases or repo not found' - sendBadge(format, badgeData) - return - } - - try { - let data = JSON.parse(buffer) - if (releaseType === 'release-date-pre') { - data = data[0] - } - const releaseDate = moment(data.created_at) - badgeData.text[1] = formatDate(releaseDate) - badgeData.colorscheme = age(releaseDate) - sendBadge(format, badgeData) - } catch (e) { - badgeData.text[1] = 'invalid' - sendBadge(format, badgeData) - } - }) - }) - ) -} - -module.exports = { - mapGithubCommitsSince, - mapGithubReleaseDate, -} diff --git a/lib/teamcity-badge-helpers.js b/lib/teamcity-badge-helpers.js deleted file mode 100644 index 24ace473fe..0000000000 --- a/lib/teamcity-badge-helpers.js +++ /dev/null @@ -1,51 +0,0 @@ -'use strict' - -const { makeBadgeData: getBadgeData } = require('./badge-data') - -function teamcityBadge( - url, - buildId, - advanced, - format, - data, - sendBadge, - request -) { - const apiUrl = url + '/app/rest/builds/buildType:(id:' + buildId + ')?guest=1' - const badgeData = getBadgeData('build', data) - request( - apiUrl, - { headers: { Accept: 'application/json' } }, - (err, res, buffer) => { - if (err != null) { - badgeData.text[1] = 'inaccessible' - sendBadge(format, badgeData) - return - } - try { - const data = JSON.parse(buffer) - if (advanced) - badgeData.text[1] = ( - data.statusText || - data.status || - '' - ).toLowerCase() - else badgeData.text[1] = (data.status || '').toLowerCase() - if (data.status === 'SUCCESS') { - badgeData.colorscheme = 'brightgreen' - badgeData.text[1] = 'passing' - } else { - badgeData.colorscheme = 'red' - } - sendBadge(format, badgeData) - } catch (e) { - badgeData.text[1] = 'invalid' - sendBadge(format, badgeData) - } - } - ) -} - -module.exports = { - teamcityBadge, -} diff --git a/lib/vscode-badge-helpers.js b/lib/vscode-badge-helpers.js deleted file mode 100644 index 3353cbbf53..0000000000 --- a/lib/vscode-badge-helpers.js +++ /dev/null @@ -1,41 +0,0 @@ -'use strict' - -//To generate API request Options for VS Code marketplace -function getVscodeApiReqOptions(packageName) { - return { - method: 'POST', - url: - 'https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery/', - headers: { - accept: 'application/json;api-version=3.0-preview.1', - 'content-type': 'application/json', - }, - body: { - filters: [ - { - criteria: [{ filterType: 7, value: packageName }], - }, - ], - flags: 914, - }, - json: true, - } -} - -//To extract Statistics (Install/Rating/RatingCount) from respose object for vscode marketplace -function getVscodeStatistic(data, statisticName) { - const statistics = data.results[0].extensions[0].statistics - try { - const statistic = statistics.find( - x => x.statisticName.toLowerCase() === statisticName.toLowerCase() - ) - return statistic.value - } catch (err) { - return 0 //In case required statistic is not found means ZERO. - } -} - -module.exports = { - getVscodeApiReqOptions, - getVscodeStatistic, -} diff --git a/server.js b/server.js index 1660e051f0..bb5f6c1350 100644 --- a/server.js +++ b/server.js @@ -1,14 +1,9 @@ 'use strict'; -const countBy = require('lodash.countby'); const dom = require('xmldom').DOMParser; const jp = require('jsonpath'); -const moment = require('moment'); const path = require('path'); -const prettyBytes = require('pretty-bytes'); const queryString = require('query-string'); -const semver = require('semver'); -const xml2js = require('xml2js'); const xpath = require('xpath'); const yaml = require('js-yaml'); const Raven = require('raven'); @@ -18,7 +13,6 @@ Raven.config(process.env.SENTRY_DSN || serverSecrets.sentry_dsn).install(); Raven.disableConsoleAlerts(); const { loadServiceClasses } = require('./services'); -const { isDeprecated, getDeprecatedBadge } = require('./lib/deprecation-helpers'); const { checkErrorResponse } = require('./lib/error-helper'); const analytics = require('./lib/analytics'); const config = require('./lib/server-config'); @@ -28,42 +22,9 @@ const log = require('./lib/log'); const { makeMakeBadgeFn } = require('./lib/make-badge'); const { QuickTextMeasurer } = require('./lib/text-measurer'); const suggest = require('./lib/suggest'); -const { licenseToColor } = require('./lib/licenses'); -const { latest: latestVersion } = require('./lib/version'); -const { - compare: phpVersionCompare, - latest: phpLatestVersion, - isStable: phpStableVersion, - minorVersion: phpMinorVersion, - versionReduction: phpVersionReduction, - getPhpReleases, -} = require('./lib/php-version'); -const { - parseVersion: luarocksParseVersion, - compareVersionLists: luarocksCompareVersionLists, -} = require('./lib/luarocks-version'); -const { - currencyFromCode, - metric, - starRating, - omitv, - addv: versionText, - maybePluralize, - formatDate, -} = require('./lib/text-formatters'); -const { - coveragePercentage: coveragePercentageColor, - downloadCount: downloadCountColor, - floorCount: floorCountColor, - letterScore: letterScoreColor, - version: versionColor, - age: ageColor, - colorScale, -} = require('./lib/color-formatters'); const { makeColorB, makeLabel: getLabel, - makeLogo: getLogo, makeBadgeData: getBadgeData, setBadgeColor, } = require('./lib/badge-data'); @@ -71,40 +32,9 @@ const { makeHandleRequestFn, clearRequestCache, } = require('./lib/request-handler'); -const { - regularUpdate, - clearRegularUpdateCache, -} = require('./lib/regular-update'); +const { clearRegularUpdateCache } = require('./lib/regular-update'); const { makeSend } = require('./lib/result-sender'); -const { fetchFromSvg } = require('./lib/svg-badge-parser'); -const { - escapeFormat, - escapeFormatSlashes, -} = require('./lib/path-helpers'); -const { - isSnapshotVersion: isNexusSnapshotVersion, -} = require('./lib/nexus-version'); -const { - teamcityBadge, -} = require('./lib/teamcity-badge-helpers'); -const { - mapNugetFeedv2, - mapNugetFeed, -} = require('./lib/nuget-provider'); -const { - getVscodeApiReqOptions, - getVscodeStatistic, -} = require('./lib/vscode-badge-helpers'); -const { - stateColor: githubStateColor, - checkStateColor: githubCheckStateColor, - commentsColor: githubCommentsColor, - checkErrorResponse: githubCheckErrorResponse, -} = require('./lib/github-helpers'); -const { - mapGithubCommitsSince, - mapGithubReleaseDate, -} = require('./lib/github-provider'); +const { escapeFormat } = require('./lib/path-helpers'); const serverStartTime = new Date((new Date()).toGMTString()); @@ -185,6213 +115,179 @@ camp.notfound(/.*/, function(query, match, end, request) { loadServiceClasses().forEach( serviceClass => serviceClass.register( - camp, - cache, + { camp, handleRequest: cache, githubApiProvider }, { handleInternalErrors: config.handleInternalErrors })); -// JIRA issue integration -camp.route(/^\/jira\/issue\/(http(?:s)?)\/(.+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, -cache(function (data, match, sendBadge, request) { - var protocol = match[1]; // eg, https - var host = match[2]; // eg, issues.apache.org/jira - var issueKey = match[3]; // eg, KAFKA-2896 - var format = match[4]; - - var options = { - method: 'GET', - json: true, - uri: protocol + '://' + host + '/rest/api/2/issue/' + - encodeURIComponent(issueKey), - }; - if (serverSecrets && serverSecrets.jira_username) { - options.auth = { - user: serverSecrets.jira_username, - pass: serverSecrets.jira_password, - }; - } - - // map JIRA color names to closest shields color schemes - var colorMap = { - 'medium-gray': 'lightgrey', - 'green': 'green', - 'yellow': 'yellow', - 'brown': 'orange', - 'warm-red': 'red', - 'blue-gray': 'blue', - }; - - var badgeData = getBadgeData(issueKey, data); - request(options, function (err, res, json) { - if (err !== null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - var jiraIssue = json; - if (jiraIssue.fields && jiraIssue.fields.status) { - if (jiraIssue.fields.status.name) { - badgeData.text[1] = jiraIssue.fields.status.name; // e.g. "In Development" - } - if (jiraIssue.fields.status.statusCategory) { - badgeData.colorscheme = colorMap[jiraIssue.fields.status.statusCategory.colorName] || 'lightgrey'; - } - } else { - badgeData.text[1] = 'invalid'; - } - sendBadge(format, badgeData); - } catch (e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// JIRA agile sprint completion integration -camp.route(/^\/jira\/sprint\/(http(?:s)?)\/(.+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, -cache(function (data, match, sendBadge, request) { - var protocol = match[1]; // eg, https - var host = match[2]; // eg, jira.spring.io - var sprintId = match[3]; // eg, 94 - var format = match[4]; // eg, png - - var options = { - method: 'GET', - json: true, - uri: protocol + '://' + host + '/rest/api/2/search?jql=sprint='+sprintId+'%20AND%20type%20IN%20(Bug,Improvement,Story,"Technical%20task")&fields=resolution&maxResults=500', - }; - if (serverSecrets && serverSecrets.jira_username) { - options.auth = { - user: serverSecrets.jira_username, - pass: serverSecrets.jira_password, - }; - } - - var badgeData = getBadgeData('completion', data); - request(options, function (err, res, json) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - if (json && json.total >= 0) { - var issuesDone = json.issues.filter(function (el) { - if (el.fields.resolution != null) { - return el.fields.resolution.name !== "Unresolved"; - } - }).length; - badgeData.text[1] = Math.round(issuesDone * 100 / json.total) + "%"; - switch(issuesDone) { - case 0: - badgeData.colorscheme = 'red'; - break; - case json.total: - badgeData.colorscheme = 'brightgreen'; - break; - default: - badgeData.colorscheme = 'orange'; - } - } else { - badgeData.text[1] = 'invalid'; - } - sendBadge(format, badgeData); - } catch (e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// PHP version from .travis.yml -camp.route(/^\/travis(?:-ci)?\/php-v\/([^/]+\/[^/]+)(?:\/([^/]+))?\.(svg|png|gif|jpg|json)$/, +// CircleCI build integration. +// https://circleci.com/api/v1/project/BrightFlair/PHP.Gt?circle-token=0a5143728784b263d9f0238b8d595522689b3af2&limit=1&filter=completed +camp.route(/^\/circleci\/(?:token\/(\w+))?[+/]?project\/(?:(github|bitbucket)\/)?([^/]+\/[^/]+)(?:\/(.*))?\.(svg|png|gif|jpg|json)$/, cache(function(data, match, sendBadge, request) { - const userRepo = match[1]; // eg, espadrine/sc - const version = match[2] || 'master'; - const format = match[3]; - const options = { - method: 'GET', - uri: `https://api.travis-ci.org/repos/${userRepo}/branches/${version}`, - }; - const badgeData = getBadgeData('PHP', data); - getPhpReleases(githubApiProvider, (err, phpReleases) => { - if (err != null) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - return; - } - request(options, (err, res, buffer) => { - if (err !== null) { - log.error(`Travis CI error: ${err.stack}`); - if (res) { - log.error('' + res); - } - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - return; - } + const token = match[1]; + const type = match[2] || 'github'; // github OR bitbucket + const userRepo = match[3]; // eg, `RedSparr0w/node-csgo-parser`. + const branch = match[4]; + const format = match[5]; - try { - const data = JSON.parse(buffer); - let travisVersions = []; + // Base API URL + let apiUrl = 'https://circleci.com/api/v1.1/project/' + type + '/' + userRepo; - // from php - if (typeof data.branch.config.php !== 'undefined') { - travisVersions = travisVersions.concat(data.branch.config.php.map((v) => v.toString())); - } - // from matrix - if (typeof data.branch.config.matrix.include !== 'undefined') { - travisVersions = travisVersions.concat(data.branch.config.matrix.include.map((v) => v.php.toString())); - } + // Query Params + var queryParams = {}; + queryParams['limit'] = 1; + queryParams['filter'] = 'completed'; - const hasHhvm = travisVersions.find((v) => v.startsWith('hhvm')); - const versions = travisVersions.map((v) => phpMinorVersion(v)).filter((v) => v.indexOf('.') !== -1); - let reduction = phpVersionReduction(versions, phpReleases); + // Custom Banch if present + if (branch != null) { + apiUrl += "/tree/" + branch; + } - if (hasHhvm) { - reduction += reduction ? ', ' : ''; - reduction += 'HHVM'; - } + // Append Token to Query Params if present + if (token) { + queryParams['circle-token'] = token; + } - if (reduction) { - badgeData.colorscheme = 'blue'; - badgeData.text[1] = reduction; - } else { - badgeData.text[1] = 'invalid'; - } - } catch(e) { - badgeData.text[1] = 'invalid'; - } - sendBadge(format, badgeData); - }); - }); -})); + // Apprend query params to API URL + apiUrl += '?' + queryString.stringify(queryParams); -// Travis integration (.org and .com) -camp.route(/^\/travis(-ci)?\/(?:(com)\/)?([^/]+\/[^/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - const travisDomain = match[2] || 'org'; // (com | org) org by default - const userRepo = match[3]; // eg, espadrine/sc - const branch = match[4]; - const format = match[5]; - const options = { - method: 'HEAD', - uri: `https://api.travis-ci.${travisDomain}/${userRepo}.svg`, - }; - if (branch != null) { - options.uri += `?branch=${branch}`; - } const badgeData = getBadgeData('build', data); - request(options, function(err, res) { - if (err != null) { - log.error('Travis error: data:' + JSON.stringify(data) + - '\nStack: ' + err.stack); - if (res) { log.error(''+res); } - } - if (checkErrorResponse(badgeData, err, res)) { + request(apiUrl, { json:true }, function(err, res, data) { + if (checkErrorResponse(badgeData, err, res, { 404: 'project not found' })) { sendBadge(format, badgeData); return; } try { - const state = res.headers['content-disposition'] - .match(/filename="(.+)\.svg"/)[1]; - badgeData.text[1] = state; - if (state === 'passing') { - badgeData.colorscheme = 'brightgreen'; - } else if (state === 'failing') { - badgeData.colorscheme = 'red'; - } else { - badgeData.text[1] = state; - } - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// NetflixOSS metadata integration -camp.route(/^\/osslifecycle?\/([^/]+\/[^/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, - cache(function(data, match, sendBadge, request) { - var orgOrUserAndRepo = match[1]; - var branch = match[2]; - var format = match[3]; - var url = 'https://raw.githubusercontent.com/' + orgOrUserAndRepo; - if (branch != null) { - url += "/" + branch + "/OSSMETADATA"; - } - else { - url += "/master/OSSMETADATA"; - } - var options = { - method: 'GET', - uri: url, - }; - var badgeData = getBadgeData('OSS Lifecycle', data); - request(options, function(err, res, body) { - if (err != null) { - log.error('NetflixOSS error: ' + err.stack); - if (res) { log.error(''+res); } - badgeData.text[1] = 'invalid'; + if (data.message !== undefined){ + badgeData.text[1] = data.message; sendBadge(format, badgeData); return; } - try { - var matchStatus = body.match(/osslifecycle=([a-z]+)/im); - if (matchStatus === null) { - badgeData.text[1] = 'inaccessible'; + + let passCount = 0; + let status; + for (let i=0; i<data.length; i++) { + status = data[i].status; + if (['success', 'fixed'].includes(status)) { + passCount++; + } else if (status === 'failed') { + badgeData.colorscheme = 'red'; + badgeData.text[1] = 'failed'; + sendBadge(format, badgeData); + return; + } else if (['no_tests', 'scheduled', 'not_run'].includes(status)) { + badgeData.colorscheme = 'yellow'; + badgeData.text[1] = status.replace('_', ' '); sendBadge(format, badgeData); return; } else { - badgeData.text[1] = matchStatus[1]; + badgeData.text[1] = status.replace('_', ' '); sendBadge(format, badgeData); return; } - } catch(e) { - log(e); - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); } - }); -})); - -// Rust download and version integration -camp.route(/^\/crates\/(d|v|dv|l)\/([A-Za-z0-9_-]+)(?:\/([0-9.]+))?\.(svg|png|gif|jpg|json)$/, -cache(function (data, match, sendBadge, request) { - var mode = match[1]; // d - downloads (total or for version), v - (latest) version, dv - downloads (for latest version) - var crate = match[2]; // crate name, e.g. rustc-serialize - var version = match[3]; // crate version in semver format, optional, e.g. 0.1.2 - var format = match[4]; - var modes = { - 'd': { - name: 'downloads', - version: true, - process: function (data, badgeData) { - var downloads = data.crate? data.crate.downloads: data.version.downloads; - version = data.version && data.version.num; - badgeData.text[1] = metric(downloads) + (version? ' version ' + version: ''); - badgeData.colorscheme = downloadCountColor(downloads); - }, - }, - 'dv': { - name: 'downloads', - version: true, - process: function (data, badgeData) { - var downloads = data.version? data.version.downloads: data.versions[0].downloads; - version = data.version && data.version.num; - badgeData.text[1] = metric(downloads) + (version? ' version ' + version: ' latest version'); - badgeData.colorscheme = downloadCountColor(downloads); - }, - }, - 'v': { - name: 'crates.io', - version: true, - process: function (data, badgeData) { - version = data.version? data.version.num: data.crate.max_version; - badgeData.text[1] = versionText(version); - badgeData.colorscheme = versionColor(version); - }, - }, - 'l': { - name: 'license', - version: false, - process: function (data, badgeData) { - badgeData.text[1] = data.versions[0].license; - badgeData.colorscheme = 'blue'; - }, - }, - }; - var behavior = modes[mode]; - var apiUrl = 'https://crates.io/api/v1/crates/' + crate; - if (version != null && behavior.version) { - apiUrl += '/' + version; - } - var badgeData = getBadgeData(behavior.name, data); - request(apiUrl, { headers: { 'Accept': 'application/json' } }, function (err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - var data = JSON.parse(buffer); - behavior.process(data, badgeData); + if (passCount === data.length) { + badgeData.colorscheme = 'brightgreen'; + badgeData.text[1] = 'passing'; + } sendBadge(format, badgeData); - - } catch (e) { + } catch(e) { badgeData.text[1] = 'invalid'; sendBadge(format, badgeData); } }); })); -// Old url for CodeBetter TeamCity instance. -camp.route(/^\/teamcity\/codebetter\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var buildType = match[1]; // eg, `bt428`. - var format = match[2]; - teamcityBadge('http://teamcity.codebetter.com', buildType, false, format, data, sendBadge, request); -})); +// User defined sources - JSON response +camp.route(/^\/badge\/dynamic\/(json|xml|yaml)\.(svg|png|gif|jpg|json)$/, +cache({ + queryParams: ['uri', 'url', 'query', 'prefix', 'suffix'], + handler: function(query, match, sendBadge, request) { + var type = match[1]; + var format = match[2]; + var prefix = query.prefix || ''; + var suffix = query.suffix || ''; + var pathExpression = query.query; + var requestOptions = {}; -// Generic TeamCity instance -camp.route(/^\/teamcity\/(http|https)\/(.*)\/(s|e)\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var scheme = match[1]; - var serverUrl = match[2]; - var advanced = (match[3] == 'e'); - var buildType = match[4]; // eg, `bt428`. - var format = match[5]; - teamcityBadge(scheme + '://' + serverUrl, buildType, advanced, format, data, sendBadge, request); -})); + var badgeData = getBadgeData('custom badge', query); -// TeamCity CodeBetter code coverage -camp.route(/^\/teamcity\/coverage\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var buildType = match[1]; // eg, `bt428`. - var format = match[2]; - var apiUrl = 'http://teamcity.codebetter.com/app/rest/builds/buildType:(id:' + buildType + ')/statistics?guest=1'; - var badgeData = getBadgeData('coverage', data); - request(apiUrl, { headers: { 'Accept': 'application/json' } }, function(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; + if (!query.uri && !query.url || !query.query){ + setBadgeColor(badgeData, 'red'); + badgeData.text[1] = !query.query ? 'no query specified' : 'no url specified'; sendBadge(format, badgeData); return; } - try { - var data = JSON.parse(buffer); - var covered; - var total; - - data.property.forEach(function(property) { - if (property.name === 'CodeCoverageAbsSCovered') { - covered = property.value; - } else if (property.name === 'CodeCoverageAbsSTotal') { - total = property.value; - } - }); - if (covered === undefined || total === undefined) { - badgeData.text[1] = 'malformed'; - sendBadge(format, badgeData); - return; - } - - var percentage = covered / total * 100; - badgeData.text[1] = percentage.toFixed(0) + '%'; - badgeData.colorscheme = coveragePercentageColor(percentage); - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; + try { + var url = encodeURI(decodeURIComponent(query.url || query.uri)); + } catch(e){ + setBadgeColor(badgeData, 'red'); + badgeData.text[1] = 'malformed url'; sendBadge(format, badgeData); + return; } - }); -})); - -// SonarQube code coverage -camp.route(/^\/sonar\/?([0-9.]+)?\/(http|https)\/(.*)\/(.*)\/(.*)\.(svg|png|gif|jpg|json)$/, - cache(function(data, match, sendBadge, request) { - var version = parseFloat(match[1]); - var scheme = match[2]; - var serverUrl = match[3]; - var buildType = match[4]; - var metricName = match[5]; - var format = match[6]; - var sonarMetricName = metricName; - if (metricName === 'tech_debt') { - //special condition for backwards compatibility - sonarMetricName = 'sqale_debt_ratio'; - } - - const useLegacyApi = !!version && version < 5.4; - - var uri = useLegacyApi ? - scheme + '://' + serverUrl + '/api/resources?resource=' + buildType + '&depth=0&metrics=' + encodeURIComponent(sonarMetricName) + '&includetrends=true': - scheme + '://' + serverUrl + '/api/measures/component?componentKey=' + buildType + '&metricKeys=' + encodeURIComponent(sonarMetricName); - - var options = { - uri, - headers: { - Accept: 'application/json', - }, - }; - if (serverSecrets && serverSecrets.sonarqube_token) { - options.auth = { - user: serverSecrets.sonarqube_token, + switch (type) { + case 'json': + requestOptions = { + headers: { + Accept: 'application/json', + }, + json: true, }; - } - - var badgeData = getBadgeData(metricName.replace(/_/g, ' '), data); + break; + case 'xml': + requestOptions = { + headers: { + Accept: 'application/xml, text/xml', + }, + }; + break; + case 'yaml': + requestOptions = { + headers: { + Accept: 'text/x-yaml, text/yaml, application/x-yaml, application/yaml, text/plain', + }, + }; + break; + } - request(options, function(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); + request(url, requestOptions, (err, res, data) => { + try { + if (checkErrorResponse(badgeData, err, res, { 404: 'resource not found' })) { return; } - try { - var data = JSON.parse(buffer); - - var value = parseInt(useLegacyApi ? data[0].msr[0].val : data.component.measures[0].value); - - if (value === undefined) { - badgeData.text[1] = 'unknown'; - sendBadge(format, badgeData); - return; - } - - if (metricName.indexOf('coverage') !== -1) { - badgeData.text[1] = value.toFixed(0) + '%'; - badgeData.colorscheme = coveragePercentageColor(value); - } else if (/^\w+_violations$/.test(metricName)) { - badgeData.text[1] = value; - badgeData.colorscheme = 'brightgreen'; - if (value > 0) { - if (metricName === 'blocker_violations') { - badgeData.colorscheme = 'red'; - } else if (metricName === 'critical_violations') { - badgeData.colorscheme = 'orange'; - } else if (metricName === 'major_violations') { - badgeData.colorscheme = 'yellow'; - } else if (metricName === 'minor_violations') { - badgeData.colorscheme = 'yellowgreen'; - } else if (metricName === 'info_violations') { - badgeData.colorscheme = 'green'; - } - } - } else if (metricName === 'fortify-security-rating') { - badgeData.text[1] = value + '/5'; + badgeData.colorscheme = 'brightgreen'; - if (value === 0) { - badgeData.colorscheme = 'red'; - } else if (value === 1) { - badgeData.colorscheme = 'orange'; - } else if (value === 2) { - badgeData.colorscheme = 'yellow'; - } else if (value === 3) { - badgeData.colorscheme = 'yellowgreen'; - } else if (value === 4) { - badgeData.colorscheme = 'green'; - } else if (value === 5) { - badgeData.colorscheme = 'brightgreen'; - } else { - badgeData.colorscheme = 'lightgrey'; + let innerText = []; + switch (type){ + case 'json': + data = (typeof data == 'object' ? data : JSON.parse(data)); + data = jp.query(data, pathExpression); + if (!data.length) { + throw 'no result'; } - } else if (metricName === 'sqale_debt_ratio' || metricName === 'tech_debt' || metricName === 'public_documented_api_density') { - // colors are based on sonarqube default rating grid and display colors - // [0,0.1) ==> A (green) - // [0.1,0.2) ==> B (yellowgreen) - // [0.2,0.5) ==> C (yellow) - // [0.5,1) ==> D (orange) - // [1,) ==> E (red) - var colorValue = value; - if (metricName === 'public_documented_api_density'){ - //Some metrics higher % is better - colorValue = 100 - value; + innerText = data; + break; + case 'xml': + data = new dom().parseFromString(data); + data = xpath.select(pathExpression, data); + if (!data.length) { + throw 'no result'; } - badgeData.text[1] = value + '%'; - if (colorValue >= 100) { - badgeData.colorscheme = 'red'; - } else if (colorValue >= 50) { - badgeData.colorscheme = 'orange'; - } else if (colorValue >= 20) { - badgeData.colorscheme = 'yellow'; - } else if (colorValue >= 10) { - badgeData.colorscheme = 'yellowgreen'; - } else if (colorValue >= 0) { - badgeData.colorscheme = 'brightgreen'; - } else { - badgeData.colorscheme = 'lightgrey'; + data.forEach((i,v)=>{ + innerText.push(pathExpression.indexOf('@') + 1 ? i.value : i.firstChild.data); + }); + break; + case 'yaml': + data = yaml.safeLoad(data); + data = jp.query(data, pathExpression); + if (!data.length) { + throw 'no result'; } - } else { - badgeData.text[1] = metric(value); - badgeData.colorscheme = 'brightgreen'; - } - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); - })); - -// Coverity integration -camp.route(/^\/coverity\/scan\/(.+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var projectId = match[1]; // eg, `3997` - var format = match[2]; - var url = 'https://scan.coverity.com/projects/' + projectId + '/badge.json'; - var badgeData = getBadgeData('coverity', data); - request(url, function(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - var data = JSON.parse(buffer); - badgeData.text[1] = data.message; - - if (data.message === 'passed') { - badgeData.colorscheme = 'brightgreen'; - badgeData.text[1] = 'passing'; - } else if (/^passed .* new defects$/.test(data.message)) { - badgeData.colorscheme = 'yellow'; - } else if (data.message === 'pending') { - badgeData.colorscheme = 'orange'; - } else if (data.message === 'failed') { - badgeData.colorscheme = 'red'; - } - sendBadge(format, badgeData); - - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// Coverity Code Advisor On Demand integration -camp.route(/^\/coverity\/ondemand\/(.+)\/(.+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var badgeType = match[1]; // One of the strings "streams" or "jobs" - var badgeTypeId = match[2]; // streamId or jobId - var format = match[3]; - - var badgeData = getBadgeData('coverity', data); - if ((badgeType == 'jobs' && badgeTypeId == 'JOB') || - (badgeType == 'streams' && badgeTypeId == 'STREAM')) { - // Request is for a static demo badge - badgeData.text[1] = 'clean'; - badgeData.colorscheme = 'green'; - sendBadge(format, badgeData); - return; - } else { - // - // Request is for a real badge; send request to Coverity On Demand API - // server to get the badge - // - // Example URLs for requests sent to Coverity On Demand are: - // - // https://api.ondemand.coverity.com/streams/44b25sjc9l3ntc2ngfi29tngro/badge - // https://api.ondemand.coverity.com/jobs/p4tmm8031t4i971r0im4s7lckk/badge - // - - var url = 'https://api.ondemand.coverity.com/' + - badgeType + '/' + badgeTypeId + '/badge'; - request(url, function(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - var data = JSON.parse(buffer); - sendBadge(format, data); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); - } -})); - -// LGTM alerts integration -camp.route(/^\/lgtm\/alerts\/(.+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - const projectId = match[1]; // eg, `g/apache/cloudstack` - const format = match[2]; - const url = 'https://lgtm.com/api/v0.1/project/' + projectId + '/details'; - const badgeData = getBadgeData('lgtm', data); - request(url, function(err, res, buffer) { - if (checkErrorResponse(badgeData, err, res, { 404: 'project not found' })) { - sendBadge(format, badgeData); - return; - } - try { - const data = JSON.parse(buffer); - if (!('alerts' in data)) - throw new Error("Invalid data"); - badgeData.text[1] = metric(data.alerts) + (data.alerts === 1 ? ' alert' : ' alerts'); - - if (data.alerts === 0) { - badgeData.colorscheme = 'brightgreen'; - } else { - badgeData.colorscheme = 'yellow'; - } - sendBadge(format, badgeData); - - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// LGTM grades integration -camp.route(/^\/lgtm\/grade\/([^/]+)\/(.+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - const language = match[1]; // eg, `java` - const projectId = match[2]; // eg, `g/apache/cloudstack` - const format = match[3]; - const url = 'https://lgtm.com/api/v0.1/project/' + projectId + '/details'; - const languageLabel = (() => { - switch(language) { - case 'cpp': - return 'c/c++'; - case 'csharp': - return 'c#'; - // Javascript analysis on LGTM also includes TypeScript - case 'javascript': - return 'js/ts'; - default: - return language; - } - })(); - const badgeData = getBadgeData('code quality: ' + languageLabel, data); - request(url, function(err, res, buffer) { - if (checkErrorResponse(badgeData, err, res, { 404: 'project not found' })) { - sendBadge(format, badgeData); - return; - } - try { - const data = JSON.parse(buffer); - if (!('languages' in data)) - throw new Error("Invalid data"); - for (const languageData of data.languages) { - if (languageData.lang === language && 'grade' in languageData) { - // Pretty label for the language - badgeData.text[1] = languageData.grade; - // Pick colour based on grade - if (languageData.grade === 'A+') { - badgeData.colorscheme = 'brightgreen'; - } else if (languageData.grade === 'A') { - badgeData.colorscheme = 'green'; - } else if (languageData.grade === 'B') { - badgeData.colorscheme = 'yellowgreen'; - } else if (languageData.grade === 'C') { - badgeData.colorscheme = 'yellow'; - } else if (languageData.grade === 'D') { - badgeData.colorscheme = 'orange'; - } else { - badgeData.colorscheme = 'red'; - } - sendBadge(format, badgeData); - return; - } - } - badgeData.text[1] = 'no data for language'; - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// Gratipay integration. -camp.route(/^\/(?:gittip|gratipay(\/user|\/team|\/project)?)\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(queryParams, match, sendBadge, request) { - const format = match[3]; - const badgeData = getDeprecatedBadge('gratipay', queryParams); - if (badgeData.template === 'social') { - badgeData.logo = getLogo('gratipay', queryParams); - } - sendBadge(format, badgeData); -})); - -// Liberapay integration. -camp.route(/^\/liberapay\/(receives|gives|patrons|goal)\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var type = match[1]; // e.g., 'gives' - var entity = match[2]; // e.g., 'Changaco' - var format = match[3]; - var apiUrl = 'https://liberapay.com/' + entity + '/public.json'; - // Lock down type - const label = { - 'receives': 'receives', - 'gives': 'gives', - 'patrons': 'patrons', - 'goal': 'goal progress', - }[type]; - const badgeData = getBadgeData(label, data); - if (badgeData.template === 'social') { - badgeData.logo = getLogo('liberapay', data); - } - request(apiUrl, function dealWithData(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - var data = JSON.parse(buffer); - var value; - var currency; - switch(type) { - case 'receives': - if (data.receiving) { - value = data.receiving.amount; - currency = data.receiving.currency; - badgeData.text[1] = `${metric(value)} ${currency}/week`; - } - break; - case 'gives': - if (data.giving) { - value = data.giving.amount; - currency = data.giving.currency; - badgeData.text[1] = `${metric(value)} ${currency}/week`; - } - break; - case 'patrons': - value = data.npatrons; - badgeData.text[1] = metric(value); - break; - case 'goal': - if (data.goal) { - value = Math.round(data.receiving.amount/data.goal.amount*100); - badgeData.text[1] = `${value}%`; - } - break; - } - if (value != null) { - badgeData.colorscheme = colorScale([0, 10, 100])(value); - sendBadge(format, badgeData); - } else { - badgeData.text[1] = 'anonymous'; - badgeData.colorscheme = 'blue'; - sendBadge(format, badgeData); - } - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// Libscore integration. -camp.route(/^\/libscore\/s\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var library = match[1]; // eg, `jQuery`. - var format = match[2]; - var apiUrl = 'http://api.libscore.com/v1/libraries/' + library; - var badgeData = getBadgeData('libscore', data); - request(apiUrl, function dealWithData(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - var data = JSON.parse(buffer); - if (data.count.length === 0) { - /* Note the 'not found' response from libscore is: - status code = 200, - body = {"github":"","meta":{},"count":[],"sites":[]} - */ - badgeData.text[1] = 'not found'; - sendBadge(format, badgeData); - return; - } - badgeData.text[1] = metric(+data.count[data.count.length-1]); - badgeData.colorscheme = 'blue'; - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// Codetally integration. -camp.route(/^\/codetally\/(.*)\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var owner = match[1]; // eg, triggerman722. - var repo = match[2]; // eg, colorstrap - var format = match[3]; - var apiUrl = 'http://www.codetally.com/formattedshield/' + owner + '/' + repo; - var badgeData = getBadgeData('codetally', data); - request(apiUrl, function dealWithData(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - var data = JSON.parse(buffer); - badgeData.text[1] = " " + data.currency_sign + data.amount + " " + data.multiplier; - badgeData.colorscheme = null; - badgeData.colorB = '#2E8B57'; - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - - -// Bountysource integration. -camp.route(/^\/bountysource\/team\/([^/]+)\/activity\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - const team = match[1]; // eg, `mozilla-core`. - const format = match[2]; - const url = 'https://api.bountysource.com/teams/' + team; - const options = { - headers: { 'Accept': 'application/vnd.bountysource+json; version=2' } }; - let badgeData = getBadgeData('bounties', data); - request(url, options, function dealWithData(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - if (res.statusCode !== 200) { - throw Error('Bad response.'); - } - const parsedData = JSON.parse(buffer); - const activity = parsedData.activity_total; - badgeData.colorscheme = 'brightgreen'; - badgeData.text[1] = activity; - sendBadge(format, badgeData); - } catch(e) { - if (res.statusCode === 404) { - badgeData.text[1] = 'not found'; - } else { - badgeData.text[1] = 'invalid'; - } - sendBadge(format, badgeData); - } - }); -})); - -// HHVM integration. -camp.route(/^\/hhvm\/([^/]+\/[^/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - const user = match[1]; // eg, `symfony/symfony`. - let branch = match[2] - ? omitv(match[2]) - : 'dev-master'; - const format = match[3]; - const apiUrl = 'https://php-eye.com/api/v1/package/'+user+'.json'; - let badgeData = getBadgeData('hhvm', data); - if (branch === 'master') { - branch = 'dev-master'; - } - request(apiUrl, function dealWithData(err, res, buffer) { - if (checkErrorResponse(badgeData, err, res, { 404: 'repo not found' })) { - sendBadge(format, badgeData); - return; - } - try { - let data = JSON.parse(buffer); - let verInfo = {}; - if (!data.versions) { - throw Error('Unexpected response.'); - } - badgeData.text[1] = 'branch not found'; - for (let i = 0, count = data.versions.length; i < count; i++) { - verInfo = data.versions[i]; - if (verInfo.name === branch) { - if (!verInfo.travis.runtime_status) { - throw Error('Unexpected response.'); - } - switch (verInfo.travis.runtime_status.hhvm) { - case 3: - // tested` - badgeData.colorscheme = 'brightgreen'; - badgeData.text[1] = 'tested'; - break; - case 2: - // allowed failure - badgeData.colorscheme = 'yellow'; - badgeData.text[1] = 'partially tested'; - break; - case 1: - // not tested - badgeData.colorscheme = 'red'; - badgeData.text[1] = 'not tested'; - break; - case 0: - // unknown/no config file - badgeData.text[1] = 'maybe untested'; - break; - } - break; - } - } - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// SensioLabs. -camp.route(/^\/sensiolabs\/i\/([^/]+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var projectUuid = match[1]; - var format = match[2]; - var options = { - method: 'GET', - uri: 'https://insight.sensiolabs.com/api/projects/' + projectUuid, - headers: { - Accept: 'application/vnd.com.sensiolabs.insight+xml', - }, - }; - - if (serverSecrets && serverSecrets.sl_insight_userUuid) { - options.auth = { - user: serverSecrets.sl_insight_userUuid, - pass: serverSecrets.sl_insight_apiToken, - }; - } - - var badgeData = getBadgeData('check', data); - - request(options, function(err, res, body) { - if (err != null || res.statusCode !== 200) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - - var matchStatus = body.match(/<status><!\[CDATA\[([a-z]+)\]\]><\/status>/im); - var matchGrade = body.match(/<grade><!\[CDATA\[([a-z]+)\]\]><\/grade>/im); - - if (matchStatus === null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } else if (matchStatus[1] !== 'finished') { - badgeData.text[1] = 'pending'; - sendBadge(format, badgeData); - return; - } else if (matchGrade === null) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - return; - } - - if (matchGrade[1] === 'platinum') { - badgeData.text[1] = 'platinum'; - badgeData.colorscheme = 'brightgreen'; - } else if (matchGrade[1] === 'gold') { - badgeData.text[1] = 'gold'; - badgeData.colorscheme = 'yellow'; - } else if (matchGrade[1] === 'silver') { - badgeData.text[1] = 'silver'; - badgeData.colorscheme = 'lightgrey'; - } else if (matchGrade[1] === 'bronze') { - badgeData.text[1] = 'bronze'; - badgeData.colorscheme = 'orange'; - } else if (matchGrade[1] === 'none') { - badgeData.text[1] = 'no medal'; - badgeData.colorscheme = 'red'; - } else { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - return; - } - - sendBadge(format, badgeData); - return; - }); -})); - -// Packagist integration. -camp.route(/^\/packagist\/(dm|dd|dt)\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var info = match[1]; // either `dm` or dt`. - var userRepo = match[2]; // eg, `doctrine/orm`. - var format = match[3]; - var apiUrl = 'https://packagist.org/packages/' + userRepo + '.json'; - var badgeData = getBadgeData('downloads', data); - if (userRepo.substr(-14) === '/:package_name') { - badgeData.text[1] = 'invalid'; - return sendBadge(format, badgeData); - } - request(apiUrl, function(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - var data = JSON.parse(buffer); - var downloads; - switch (info.charAt(1)) { - case 'm': - downloads = data.package.downloads.monthly; - badgeData.text[1] = metric(downloads) + '/month'; - break; - case 'd': - downloads = data.package.downloads.daily; - badgeData.text[1] = metric(downloads) + '/day'; - break; - case 't': - downloads = data.package.downloads.total; - badgeData.text[1] = metric(downloads); - break; - } - badgeData.colorscheme = downloadCountColor(downloads); - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// Packagist version integration. -camp.route(/^\/packagist\/(v|vpre)\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var info = match[1]; // either `v` or `vpre`. - var userRepo = match[2]; // eg, `doctrine/orm`. - var format = match[3]; - var apiUrl = 'https://packagist.org/packages/' + userRepo + '.json'; - var badgeData = getBadgeData('packagist', data); - if (userRepo.substr(-14) === '/:package_name') { - badgeData.text[1] = 'invalid'; - return sendBadge(format, badgeData); - } - request(apiUrl, function(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - var data = JSON.parse(buffer); - - var versionsData = data.package.versions; - var versions = Object.keys(versionsData); - - // Map aliases (eg, dev-master). - var aliasesMap = {}; - versions.forEach(function(version) { - var versionData = versionsData[version]; - if (versionData.extra && versionData.extra['branch-alias'] && - versionData.extra['branch-alias'][version]) { - // eg, version is 'dev-master', mapped to '2.0.x-dev'. - var validVersion = versionData.extra['branch-alias'][version]; - if (aliasesMap[validVersion] === undefined || - phpVersionCompare(aliasesMap[validVersion], validVersion) < 0) { - versions.push(validVersion); - aliasesMap[validVersion] = version; - } - } - }); - versions = versions.filter(function(version) { - return !(/^dev-/.test(version)); - }); - - var badgeText = null; - var badgeColor = null; - - switch (info) { - case 'v': - var stableVersions = versions.filter(phpStableVersion); - var stableVersion = phpLatestVersion(stableVersions); - if (!stableVersion) { - stableVersion = phpLatestVersion(versions); - } - //if (!!aliasesMap[stableVersion]) { - // stableVersion = aliasesMap[stableVersion]; - //} - badgeText = versionText(stableVersion); - badgeColor = versionColor(stableVersion); - break; - case 'vpre': - var unstableVersion = phpLatestVersion(versions); - //if (!!aliasesMap[unstableVersion]) { - // unstableVersion = aliasesMap[unstableVersion]; - //} - badgeText = versionText(unstableVersion); - badgeColor = 'orange'; - break; - } - - if (badgeText !== null) { - badgeData.text[1] = badgeText; - badgeData.colorscheme = badgeColor; - } - - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// Packagist license integration. -camp.route(/^\/packagist\/l\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var userRepo = match[1]; - var format = match[2]; - var apiUrl = 'https://packagist.org/packages/' + userRepo + '.json'; - var badgeData = getBadgeData('license', data); - if (userRepo.substr(-14) === '/:package_name') { - badgeData.text[1] = 'invalid'; - return sendBadge(format, badgeData); - } - request(apiUrl, function(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - var data = JSON.parse(buffer); - // Note: if you change the latest version detection algorithm here, - // change it above (for the actual version badge). - var version; - var unstable = function(ver) { return /dev/.test(ver); }; - // Grab the latest stable version, or an unstable - for (var versionName in data.package.versions) { - var current = data.package.versions[versionName]; - - if (version !== undefined) { - if (unstable(version.version) && !unstable(current.version)) { - version = current; - } else if (version.version_normalized < current.version_normalized) { - version = current; - } - } else { - version = current; - } - } - badgeData.text[1] = version.license[0]; - badgeData.colorscheme = 'blue'; - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// Package Control integration. -camp.route(/^\/packagecontrol\/(dm|dw|dd|dt)\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var info = match[1]; // either `dm`, `dw`, `dd` or dt`. - var userRepo = match[2]; // eg, `Package%20Control`. - var format = match[3]; - var apiUrl = 'https://packagecontrol.io/packages/' + userRepo + '.json'; - var badgeData = getBadgeData('downloads', data); - request(apiUrl, function(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - var data = JSON.parse(buffer); - var downloads = 0; - var platforms; - switch (info.charAt(1)) { - case 'm': - // daily downloads are separated by Operating System - platforms = data.installs.daily.data; - platforms.forEach(function(platform) { - // loop through the first 30 days or 1 month - for (var i = 0; i < 30; i++) { - // add the downloads for that day for that platform - downloads += platform.totals[i]; - } - }); - badgeData.text[1] = metric(downloads) + '/month'; - break; - case 'w': - // daily downloads are separated by Operating System - platforms = data.installs.daily.data; - platforms.forEach(function(platform) { - // loop through the first 7 days or 1 week - for (var i = 0; i < 7; i++) { - // add the downloads for that day for that platform - downloads += platform.totals[i]; - } - }); - badgeData.text[1] = metric(downloads) + '/week'; - break; - case 'd': - // daily downloads are separated by Operating System - platforms = data.installs.daily.data; - platforms.forEach(function(platform) { - // use the downloads from yesterday - downloads += platform.totals[1]; - }); - badgeData.text[1] = metric(downloads) + '/day'; - break; - case 't': - // all-time downloads are already compiled - downloads = data.installs.total; - badgeData.text[1] = metric(downloads); - break; - } - badgeData.colorscheme = downloadCountColor(downloads); - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// Anaconda Cloud / conda package manager integration -camp.route(/^\/conda\/([dvp]n?)\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, -cache(function(queryData, match, sendBadge, request) { - const mode = match[1]; - const channel = match[2]; - const pkgname = match[3]; - const format = match[4]; - const url = 'https://api.anaconda.org/package/' + channel + '/' + pkgname; - const labels = { - 'd': 'downloads', - 'p': 'platform', - 'v': channel, - }; - const modes = { - // downloads - 'd' - 'd': function(data, badgeData) { - const downloads = data.files.reduce((total, file) => total + file.ndownloads, 0); - badgeData.text[1] = metric(downloads); - badgeData.colorscheme = downloadCountColor(downloads); - }, - // latest version 'v' - 'v': function(data, badgeData) { - const version = data.latest_version; - badgeData.text[1] = versionText(version); - badgeData.colorscheme = versionColor(version); - }, - // platform 'p' - 'p': function(data, badgeData) { - const platforms = data.conda_platforms.join(' | '); - badgeData.text[1] = platforms; - }, - }; - const variants = { - // default use `conda|{channelname}` as label - '': function(queryData, badgeData) { - badgeData.text[0] = getLabel(`conda|${badgeData.text[0]}`, queryData); - }, - // skip `conda|` prefix - 'n': function(queryData, badgeData) { - }, - }; - - const update = modes[mode.charAt(0)]; - const variant = variants[mode.charAt(1)]; - - var badgeData = getBadgeData(labels[mode.charAt(0)], queryData); - request(url, function(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - variant(queryData, badgeData); - sendBadge(format, badgeData); - return; - } - try { - var data = JSON.parse(buffer); - update(data, badgeData); - variant(queryData, badgeData); - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - variant(queryData, badgeData); - sendBadge(format, badgeData); - } - }); -})); - -// Bintray version integration -camp.route(/^\/bintray\/v\/(.+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var path = match[1]; // :subject/:repo/:package (e.g. asciidoctor/maven/asciidoctorj) - var format = match[2]; - - var options = { - method: 'GET', - uri: 'https://bintray.com/api/v1/packages/' + path + '/versions/_latest', - headers: { - Accept: 'application/json', - }, - }; - - if (serverSecrets && serverSecrets.bintray_user) { - options.auth = { - user: serverSecrets.bintray_user, - pass: serverSecrets.bintray_apikey, - }; - } - - var badgeData = getBadgeData('bintray', data); - request(options, function(err, res, buffer) { - if (err !== null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - var data = JSON.parse(buffer); - badgeData.text[1] = versionText(data.name); - badgeData.colorscheme = versionColor(data.name); - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// iTunes App Store version -camp.route(/^\/itunes\/v\/(.+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var bundleId = match[1]; // eg, `324684580` - var format = match[2]; - var apiUrl = 'https://itunes.apple.com/lookup?id=' + bundleId; - var badgeData = getBadgeData('itunes app store', data); - request(apiUrl, function(err, res, buffer) { - if (err !== null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - var data = JSON.parse(buffer); - if (data.resultCount === 0) { - /* Note the 'not found' response from iTunes is: - status code = 200, - body = { "resultCount":0, "results": [] } - */ - badgeData.text[1] = 'not found'; - sendBadge(format, badgeData); - return; - } - var version = data.results[0].version; - badgeData.text[1] = versionText(version); - badgeData.colorscheme = versionColor(version); - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// LuaRocks version integration. -camp.route(/^\/luarocks\/v\/([^/]+)\/([^/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - const user = match[1]; // eg, `leafo`. - const moduleName = match[2]; // eg, `lapis`. - const format = match[4]; - const apiUrl = 'https://luarocks.org/manifests/' + user + '/manifest.json'; - const badgeData = getBadgeData('luarocks', data); - let version = match[3]; // you can explicitly specify a version - request(apiUrl, function(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - let versions; - try { - const moduleInfo = JSON.parse(buffer).repository[moduleName]; - versions = Object.keys(moduleInfo); - if (version && versions.indexOf(version) === -1) { - throw new Error('unknown version'); - } - } catch (e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - return; - } - if (!version) { - if (versions.length === 1) { - version = omitv(versions[0]); - } else { - let latestVersionString, latestVersionList; - versions.forEach(function(versionString) { - versionString = omitv(versionString); // remove leading 'v' - let versionList = luarocksParseVersion(versionString); - if ( - !latestVersionList || // first iteration - luarocksCompareVersionLists(versionList, latestVersionList) > 0 - ) { - latestVersionString = versionString; - latestVersionList = versionList; - } - }); - version = latestVersionString; - } - } - let color; - switch (version.slice(0, 3).toLowerCase()) { - case 'dev': - color = 'yellow'; - break; - case 'scm': - case 'cvs': - color = 'orange'; - break; - default: - color = 'brightgreen'; - } - badgeData.text[1] = versionText(version); - badgeData.colorscheme = color; - sendBadge(format, badgeData); - }); -})); - -// Dart's pub version integration. -camp.route(/^\/pub\/v(pre)?\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - const includePre = Boolean(match[1]); - const userRepo = match[2]; // eg, "box2d" - const format = match[3]; - const apiUrl = 'https://pub.dartlang.org/packages/' + userRepo + '.json'; - let badgeData = getBadgeData('pub', data); - request(apiUrl, function(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - var data = JSON.parse(buffer); - // Grab the latest stable version, or an unstable - var versions = data.versions; - var version = latestVersion(versions, { pre: includePre }); - badgeData.text[1] = versionText(version); - badgeData.colorscheme = versionColor(version); - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// Hex.pm integration. -camp.route(/^\/hexpm\/([^/]+)\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(queryParams, match, sendBadge, request) { - const info = match[1]; - const repo = match[2]; // eg, `httpotion`. - const format = match[3]; - const apiUrl = 'https://hex.pm/api/packages/' + repo; - const badgeData = getBadgeData('hex', queryParams); - request(apiUrl, function(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - const data = JSON.parse(buffer); - if (info.charAt(0) === 'd') { - badgeData.text[0] = getLabel('downloads', queryParams); - var downloads; - switch (info.charAt(1)) { - case 'w': - downloads = data.downloads.week; - badgeData.text[1] = metric(downloads) + '/week'; - break; - case 'd': - downloads = data.downloads.day; - badgeData.text[1] = metric(downloads) + '/day'; - break; - case 't': - downloads = data.downloads.all; - badgeData.text[1] = metric(downloads); - break; - } - badgeData.colorscheme = downloadCountColor(downloads); - sendBadge(format, badgeData); - } else if (info === 'v') { - const version = data.releases[0].version; - badgeData.text[1] = versionText(version); - badgeData.colorscheme = versionColor(version); - sendBadge(format, badgeData); - } else if (info == 'l') { - const license = (data.meta.licenses || []).join(', '); - badgeData.text[0] = getLabel(maybePluralize('license', data.meta.licenses), queryParams); - if (license == '') { - badgeData.text[1] = 'Unknown'; - } else { - badgeData.text[1] = license; - badgeData.colorscheme = 'blue'; - } - sendBadge(format, badgeData); - } - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// Coveralls integration. -camp.route(/^\/coveralls\/(?:(bitbucket|github)\/)?([^/]+\/[^/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var repoService = match[1] ? match[1] : 'github'; - var userRepo = match[2]; // eg, `jekyll/jekyll`. - var branch = match[3]; - var format = match[4]; - var apiUrl = { - url: `https://coveralls.io/repos/${repoService}/${userRepo}/badge.svg`, - followRedirect: false, - method: 'HEAD', - }; - if (branch) { - apiUrl.url += '?branch=' + branch; - } - var badgeData = getBadgeData('coverage', data); - request(apiUrl, function(err, res) { - if (err != null) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - return; - } - // We should get a 302. Look inside the Location header. - var buffer = res.headers.location; - if (!buffer) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - return; - } - try { - var score = buffer.split('_')[1].split('.')[0]; - var percentage = parseInt(score); - if (percentage !== percentage) { - // It is NaN, treat it as unknown. - badgeData.text[1] = 'unknown'; - sendBadge(format, badgeData); - return; - } - badgeData.text[1] = score + '%'; - badgeData.colorscheme = coveragePercentageColor(percentage); - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'malformed'; - sendBadge(format, badgeData); - } - }); -})); - -// Codecov integration. -camp.route(/^\/codecov\/c\/(?:token\/(\w+))?[+/]?([^/]+\/[^/]+\/[^/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var token = match[1]; - var userRepo = match[2]; // eg, `github/codecov/example-python`. - var branch = match[3]; - var format = match[4]; - let apiUrl; - if (branch) { - apiUrl = `https://codecov.io/${userRepo}/branch/${branch}/graphs/badge.txt`; - } else { - apiUrl = `https://codecov.io/${userRepo}/graphs/badge.txt`; - } - if (token) { - apiUrl += '?' + queryString.stringify({ token }); - } - var badgeData = getBadgeData('coverage', data); - request(apiUrl, function(err, res, body) { - if (err != null) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - return; - } - try { - // Body: range(0, 100) or "unknown" - var coverage = body.trim(); - // Is `coverage` NaN when converted to number? - if (+coverage !== +coverage) { - badgeData.text[1] = 'unknown'; - sendBadge(format, badgeData); - return; - } - badgeData.text[1] = coverage + '%'; - badgeData.colorscheme = coveragePercentageColor(coverage); - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'malformed'; - sendBadge(format, badgeData); - } - }); -})); - -// Code Climate integration. -camp.route(/^\/codeclimate(\/(c|coverage|maintainability|issues|tech-debt)(-letter|-percentage)?)?\/(.+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - let type; - if (match[2] === 'c' || !match[2]) { - // Top-level and /coverage URLs equivalent to /c, still supported for backwards compatibility. See #1387. - type = 'coverage'; - } else if (match[2] === 'tech-debt') { - type = 'technical debt'; - } else { - type = match[2]; - } - // For maintainability, default is letter, alternative is percentage. For coverage, default is percentage, alternative is letter. - const isAlternativeFormat = match[3]; - const userRepo = match[4]; // eg, `twbs/bootstrap`. - const format = match[5]; - request({ - method: 'GET', - uri: `https://api.codeclimate.com/v1/repos?github_slug=${userRepo}`, - json: true, - }, function (err, res, body) { - const badgeData = getBadgeData(type, data); - if (err != null) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - return; - } - - try { - if (!body.data || body.data.length === 0) { - badgeData.text[1] = 'not found'; - sendBadge(format, badgeData); - return; - } - - const branchData = type === 'coverage' - ? body.data[0].relationships.latest_default_branch_test_report.data - : body.data[0].relationships.latest_default_branch_snapshot.data; - if (branchData == null) { - badgeData.text[1] = 'unknown'; - sendBadge(format, badgeData); - return; - } - - const url = `https://api.codeclimate.com/v1/repos/${body.data[0].id}/${type === 'coverage' ? 'test_reports' : 'snapshots'}/${branchData.id}`; - request(url, function(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - return; - } - - try { - const parsedData = JSON.parse(buffer); - if (type === 'coverage' && isAlternativeFormat) { - const score = parsedData.data.attributes.rating.letter; - badgeData.text[1] = score; - badgeData.colorscheme = letterScoreColor(score); - } else if (type === 'coverage') { - const percentage = parseFloat(parsedData.data.attributes.covered_percent); - badgeData.text[1] = percentage.toFixed(0) + '%'; - badgeData.colorscheme = coveragePercentageColor(percentage); - } else if (type === 'issues') { - const count = parsedData.data.meta.issues_count; - badgeData.text[1] = count; - badgeData.colorscheme = colorScale([1, 5, 10, 20], ['brightgreen', 'green', 'yellowgreen', 'yellow', 'red'])(count); - } else if (type === 'technical debt') { - const percentage = parseFloat(parsedData.data.attributes.ratings[0].measure.value); - badgeData.text[1] = percentage.toFixed(0) + '%'; - badgeData.colorscheme = colorScale([5, 10, 20, 50], ['brightgreen', 'green', 'yellowgreen', 'yellow', 'red'])(percentage); - } else if (type === 'maintainability' && isAlternativeFormat) { - // maintainability = 100 - technical debt - const percentage = 100 - parseFloat(parsedData.data.attributes.ratings[0].measure.value); - badgeData.text[1] = percentage.toFixed(0) + '%'; - badgeData.colorscheme = colorScale([50, 80, 90, 95], ['red', 'yellow', 'yellowgreen', 'green', 'brightgreen'])(percentage); - } else if (type === 'maintainability') { - const score = parsedData.data.attributes.ratings[0].letter; - badgeData.text[1] = score; - badgeData.colorscheme = letterScoreColor(score); - } - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// Scrutinizer coverage integration. -camp.route(/^\/scrutinizer(?:\/(build|coverage))?\/([^/]+\/[^/]+\/[^/]+|gp\/[^/])(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - const type = match[1] ? match[1] : 'code quality'; - const repo = match[2]; // eg, g/phpmyadmin/phpmyadmin - let branch = match[3]; - const format = match[4]; - const apiUrl = `https://scrutinizer-ci.com/api/repositories/${repo}`; - const badgeData = getBadgeData(type, data); - request(apiUrl, {}, function(err, res, buffer) { - if (checkErrorResponse(badgeData, err, res, { 404: 'project or branch not found' })) { - sendBadge(format, badgeData); - return; - } - try { - const parsedData = JSON.parse(buffer); - // Which branch are we dealing with? - if (branch === undefined) { - branch = parsedData.default_branch; - } - if (type === 'coverage') { - const percentage = parsedData.applications[branch].index._embedded - .project.metric_values['scrutinizer.test_coverage'] * 100; - if (isNaN(percentage)) { - badgeData.text[1] = 'unknown'; - badgeData.colorscheme = 'gray'; - } else { - badgeData.text[1] = percentage.toFixed(0) + '%'; - badgeData.colorscheme = coveragePercentageColor(percentage); - } - } else if (type === 'build') { - const status = parsedData.applications[branch].build_status.status; - badgeData.text[1] = status; - if (status === 'passed') { - badgeData.colorscheme = 'brightgreen'; - badgeData.text[1] = 'passing'; - } else if (status === 'failed' || status === 'error') { - badgeData.colorscheme = 'red'; - } else if (status === 'pending') { - badgeData.colorscheme = 'orange'; - } else if (status === 'unknown') { - badgeData.colorscheme = 'gray'; - } - } else { - let score = parsedData.applications[branch].index._embedded - .project.metric_values['scrutinizer.quality']; - score = Math.round(score * 100) / 100; - badgeData.text[1] = score; - if (score > 9) { - badgeData.colorscheme = 'brightgreen'; - } else if (score > 7) { - badgeData.colorscheme = 'green'; - } else if (score > 5) { - badgeData.colorscheme = 'yellow'; - } else if (score > 4) { - badgeData.colorscheme = 'orange'; - } else { - badgeData.colorscheme = 'red'; - } - } - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// David integration -camp.route(/^\/david\/(dev\/|optional\/|peer\/)?(.+?)\.(svg|png|gif|jpg|json)$/, -cache({ - queryParams: ['path'], - handler: function(data, match, sendBadge, request) { - var dev = match[1]; - if (dev != null) { dev = dev.slice(0, -1); } // 'dev', 'optional' or 'peer'. - // eg, `expressjs/express`, `webcomponents/generator-element`. - var userRepo = match[2]; - var format = match[3]; - var options = 'https://david-dm.org/' + userRepo + '/' - + (dev ? (dev + '-') : '') + 'info.json'; - if (data.path) { - // path can be used to specify the package.json location, useful for monorepos - options += '?path=' + data.path; - } - var badgeData = getBadgeData( (dev? (dev+'D') :'d') + 'ependencies', data); - request(options, function(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } else if (res.statusCode === 500) { - /* note: - david returns a 500 response for 'not found' - e.g: https://david-dm.org/foo/barbaz/info.json - not a 404 so we can't handle 'not found' cleanly - because this might also be some other error. - */ - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - return; - } - try { - var data = JSON.parse(buffer); - var status = data.status; - if (status === 'insecure') { - badgeData.colorscheme = 'red'; - status = 'insecure'; - } else if (status === 'notsouptodate') { - badgeData.colorscheme = 'yellow'; - status = 'up to date'; - } else if (status === 'outofdate') { - badgeData.colorscheme = 'red'; - status = 'out of date'; - } else if (status === 'uptodate') { - badgeData.colorscheme = 'brightgreen'; - status = 'up to date'; - } else if (status === 'none') { - badgeData.colorscheme = 'brightgreen'; - } - badgeData.text[1] = status; - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - return; - } - }); - }, -})); - -// dotnet-status integration - deprecated as of April 2018. -camp.route(/^\/dotnetstatus\/(.+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - const format = match[2]; - const badgeData = getDeprecatedBadge('dotnet status', data); - sendBadge(format, badgeData); -})); - -// Gemnasium integration -camp.route(/^\/gemnasium\/(.+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var userRepo = match[1]; // eg, `jekyll/jekyll`. - var format = match[2]; - - if (isDeprecated('gemnasium', serverStartTime)) { - const badgeData = getDeprecatedBadge('gemnasium', data); - sendBadge(format, badgeData); - return; - } - - var options = 'https://gemnasium.com/' + userRepo + '.svg'; - var badgeData = getBadgeData('dependencies', data); - request(options, function(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - var nameMatch = buffer.match(/(devD|d)ependencies/)[0]; - var statusMatch = buffer.match(/'14'>(.+)<\/text>\s*<\/g>/)[1]; - badgeData.text[0] = getLabel(nameMatch, data); - badgeData.text[1] = statusMatch; - if (statusMatch === 'up-to-date') { - badgeData.text[1] = 'up to date'; - badgeData.colorscheme = 'brightgreen'; - } else if (statusMatch === 'out-of-date') { - badgeData.text[1] = 'out of date'; - badgeData.colorscheme = 'yellow'; - } else if (statusMatch === 'update!') { - badgeData.colorscheme = 'red'; - } else if (statusMatch === 'none') { - badgeData.colorscheme = 'brightgreen'; - } else { - badgeData.text[1] = 'undefined'; - } - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - return; - } - }); -})); - -// Depfu integration -camp.route(/^\/depfu\/(.+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var userRepo = match[1]; // eg, `jekyll/jekyll`. - var format = match[2]; - var url = 'https://depfu.com/github/shields/' + userRepo; - var badgeData = getBadgeData('dependencies', data); - request(url, function(err, res) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - var data = JSON.parse(res['body']); - badgeData.text[1] = data['text']; - badgeData.colorscheme = data['colorscheme']; - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// VersionEye integration - deprecated as of August 2018. -camp.route(/^\/versioneye\/d\/(.+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - const format = match[2]; - const badgeData = getDeprecatedBadge('versioneye', data); - sendBadge(format, badgeData); -})); - -// Codacy integration -camp.route(/^\/codacy\/(?:grade\/)?(?!coverage\/)([^/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var projectId = match[1]; // eg. e27821fb6289410b8f58338c7e0bc686 - var branch = match[2]; - var format = match[3]; - - var queryParams = {}; - if (branch) { - queryParams.branch = branch; - } - var query = queryString.stringify(queryParams); - var url = 'https://api.codacy.com/project/badge/grade/' + projectId + '?' + query; - var badgeData = getBadgeData('code quality', data); - fetchFromSvg(request, url, function(err, res) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - badgeData.text[1] = res; - if (res === 'A') { - badgeData.colorscheme = 'brightgreen'; - } else if (res === 'B') { - badgeData.colorscheme = 'green'; - } else if (res === 'C') { - badgeData.colorscheme = 'yellowgreen'; - } else if (res === 'D') { - badgeData.colorscheme = 'yellow'; - } else if (res === 'E') { - badgeData.colorscheme = 'orange'; - } else if (res === 'F') { - badgeData.colorscheme = 'red'; - } else if (res === 'X') { - badgeData.text[1] = 'invalid'; - badgeData.colorscheme = 'lightgrey'; - } else { - badgeData.colorscheme = 'red'; - } - sendBadge(format, badgeData); - - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// Discourse integration -camp.route(/^\/discourse\/(http(?:s)?)\/(.*)\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - const scheme = match[1]; // eg, https - const host = match[2]; // eg, meta.discourse.org - const stat = match[3]; // eg, user_count - const format = match[4]; - const url = scheme + '://' + host + '/site/statistics.json'; - - const options = { - method: 'GET', - uri: url, - headers: { - 'Accept': 'application/json', - }, - }; - - var badgeData = getBadgeData('discourse', data); - request(options, function(err, res) { - if (err != null) { - if (res) { - console.error('' + res); - } - - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - - if (res.statusCode !== 200) { - badgeData.text[1] = 'inaccessible'; - badgeData.colorscheme = 'red'; - sendBadge(format, badgeData); - return; - } - - badgeData.colorscheme = 'brightgreen'; - - try { - var data = JSON.parse(res['body']); - let statCount; - - switch (stat) { - case 'topics': - statCount = data.topic_count; - badgeData.text[1] = metric(statCount) + ' topics'; - break; - case 'posts': - statCount = data.post_count; - badgeData.text[1] = metric(statCount) + ' posts'; - break; - case 'users': - statCount = data.user_count; - badgeData.text[1] = metric(statCount) + ' users'; - break; - case 'likes': - statCount = data.like_count; - badgeData.text[1] = metric(statCount) + ' likes'; - break; - case 'status': - badgeData.text[1] = 'online'; - break; - default: - badgeData.text[1] = 'invalid'; - badgeData.colorscheme = 'yellow'; - break; - } - - sendBadge(format, badgeData); - } catch(e) { - console.error('' + e.stack); - badgeData.colorscheme = 'yellow'; - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); - -})); - -// ReadTheDocs build -camp.route(/^\/readthedocs\/([^/]+)(?:\/(.+))?.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var project = match[1]; - var version = match[2]; - var format = match[3]; - var badgeData = getBadgeData('docs', data); - var url = 'https://readthedocs.org/projects/' + encodeURIComponent(project) + '/badge/'; - if (version != null) { - url += '?version=' + encodeURIComponent(version); - } - fetchFromSvg(request, url, function(err, res) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - badgeData.text[1] = res; - if (res === 'passing') { - badgeData.colorscheme = 'brightgreen'; - } else if (res === 'failing') { - badgeData.colorscheme = 'red'; - } else if (res === 'unknown') { - badgeData.colorscheme = 'yellow'; - } else { - badgeData.colorscheme = 'red'; - } - sendBadge(format, badgeData); - - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -camp.route(/^\/codacy\/coverage\/(?!grade\/)([^/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var projectId = match[1]; // eg. e27821fb6289410b8f58338c7e0bc686 - var branch = match[2]; - var format = match[3]; - - var queryParams = {}; - if (branch) { - queryParams.branch = branch; - } - var query = queryString.stringify(queryParams); - var url = 'https://api.codacy.com/project/badge/coverage/' + projectId + '?' + query; - var badgeData = getBadgeData('coverage', data); - fetchFromSvg(request, url, function(err, res) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - badgeData.text[1] = res; - badgeData.colorscheme = coveragePercentageColor(parseInt(res)); - sendBadge(format, badgeData); - - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// Hackage version integration. -camp.route(/^\/hackage\/v\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var repo = match[1]; // eg, `lens`. - var format = match[2]; - var apiUrl = 'https://hackage.haskell.org/package/' + repo + '/' + repo + '.cabal'; - var badgeData = getBadgeData('hackage', data); - request(apiUrl, function(err, res, buffer) { - if (checkErrorResponse(badgeData, err, res)) { - sendBadge(format, badgeData); - return; - } - - try { - var lines = buffer.split("\n"); - var versionLines = lines.filter(function(e) { - return (/^version:/i).test(e) === true; - }); - // We don't have to check length of versionLines, because if we throw, - // we'll render the 'invalid' badge below, which is the correct thing - // to do. - var version = versionLines[0].split(/:/)[1].trim(); - badgeData.text[1] = versionText(version); - badgeData.colorscheme = versionColor(version); - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// Hackage dependencies version integration. -camp.route(/^\/hackage-deps\/v\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - const repo = match[1]; // eg, `lens`. - const format = match[2]; - const reverseUrl = 'http://packdeps.haskellers.com/licenses/' + repo; - const feedUrl = 'http://packdeps.haskellers.com/feed/' + repo; - const badgeData = getBadgeData('dependencies', data); - - // first call /reverse to check if the package exists - // this will throw a 404 if it doesn't - request(reverseUrl, function(err, res, buffer) { - if (checkErrorResponse(badgeData, err, res)) { - sendBadge(format, badgeData); - return; - } - - // if the package exists, then query /feed to check the dependencies - request(feedUrl, function(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - - try { - const outdatedStr = "Outdated dependencies for " + repo + " "; - if (buffer.indexOf(outdatedStr) >= 0) { - badgeData.text[1] = 'outdated'; - badgeData.colorscheme = 'orange'; - } else { - badgeData.text[1] = 'up to date'; - badgeData.colorscheme = 'brightgreen'; - } - } catch(e) { - badgeData.text[1] = 'invalid'; - } - sendBadge(format, badgeData); - }); - - }); - -})); - -// Elm package version integration. -camp.route(/^\/elm-package\/v\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - const urlPrefix = 'http://package.elm-lang.org/packages'; - const [, user, repo, format] = match; - const apiUrl = `${urlPrefix}/${user}/${repo}/latest/elm-package.json`; - const badgeData = getBadgeData('elm-package', data); - request(apiUrl, (err, res, buffer) => { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - const data = JSON.parse(buffer); - if (data && typeof data.version === 'string') { - badgeData.text[1] = versionText(data.version); - badgeData.colorscheme = versionColor(data.version); - } - sendBadge(format, badgeData); - } catch (e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -}) -); - -// CocoaPods version integration. -camp.route(/^\/cocoapods\/(v|p|l)\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var type = match[1]; - var spec = match[2]; // eg, AFNetworking - var format = match[3]; - var apiUrl = 'https://trunk.cocoapods.org/api/v1/pods/' + spec + '/specs/latest'; - const typeToLabel = { 'v' : 'pod', 'p': 'platform', 'l': 'license' }; - const badgeData = getBadgeData(typeToLabel[type], data); - badgeData.colorscheme = null; - request(apiUrl, function(err, res, buffer) { - if (checkErrorResponse(badgeData, err, res)) { - sendBadge(format, badgeData); - return; - } - try { - var parsedData = JSON.parse(buffer); - var version = parsedData.version; - var license; - if (typeof parsedData.license === 'string') { - license = parsedData.license; - } else { license = parsedData.license.type; } - - var platforms = Object.keys(parsedData.platforms || { - 'ios' : '5.0', - 'osx' : '10.7', - }).join(' | '); - if (type === 'v') { - badgeData.text[1] = versionText(version); - badgeData.colorscheme = versionColor(version); - } else if (type === 'p') { - badgeData.text[1] = platforms; - badgeData.colorB = '#989898'; - } else if (type === 'l') { - badgeData.text[1] = license; - badgeData.colorB = '#373737'; - } - - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// CocoaPods metrics -camp.route(/^\/cocoapods\/metrics\/doc-percent\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var spec = match[1]; // eg, AFNetworking - var format = match[2]; - var apiUrl = 'https://metrics.cocoapods.org/api/v1/pods/' + spec; - var badgeData = getBadgeData('docs', data); - request(apiUrl, function(err, res, buffer) { - if (checkErrorResponse(badgeData, err, res)) { - sendBadge(format, badgeData); - return; - } - try { - var parsedData = JSON.parse(buffer); - var percentage = parsedData.cocoadocs.doc_percent; - if (percentage == null) { - percentage = 0; - } - badgeData.colorscheme = coveragePercentageColor(percentage); - badgeData.text[1] = percentage + '%'; - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// Cocoapods Downloads integration. -camp.route(/^\/cocoapods\/(dm|dw|dt)\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var info = match[1]; // One of these: "dm", "dw", "dt" - var spec = match[2]; // eg, AFNetworking - var format = match[3]; - var apiUrl = 'https://metrics.cocoapods.org/api/v1/pods/' + spec; - var badgeData = getBadgeData('downloads', data); - request(apiUrl, function(err, res, buffer) { - if (checkErrorResponse(badgeData, err, res)) { - sendBadge(format, badgeData); - return; - } - try { - var data = JSON.parse(buffer); - var downloads = 0; - switch (info.charAt(1)) { - case 'm': - downloads = data.stats.download_month; - badgeData.text[1] = metric(downloads) + '/month'; - break; - case 'w': - downloads = data.stats.download_week; - badgeData.text[1] = metric(downloads) + '/week'; - break; - case 't': - downloads = data.stats.download_total; - badgeData.text[1] = metric(downloads); - break; - } - badgeData.colorscheme = downloadCountColor(downloads); - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// CocoaPods Apps Integration -camp.route(/^\/cocoapods\/(aw|at)\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var info = match[1]; // One of these: "aw", "at" - var spec = match[2]; // eg, AFNetworking - var format = match[3]; - var apiUrl = 'https://metrics.cocoapods.org/api/v1/pods/' + spec; - var badgeData = getBadgeData('apps', data); - request(apiUrl, function(err, res, buffer) { - if (checkErrorResponse(badgeData, err, res)) { - sendBadge(format, badgeData); - return; - } - try { - var data = JSON.parse(buffer); - var apps = 0; - switch (info.charAt(1)) { - case 'w': - apps = data.stats.app_week; - badgeData.text[1] = metric(apps) + '/week'; - break; - case 't': - apps = data.stats.app_total; - badgeData.text[1] = metric(apps); - break; - } - badgeData.colorscheme = downloadCountColor(apps); - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -camp.route(/^\/sourcegraph\/rrc\/([\s\S]+)\.(svg|png|gif|jpg|json)$/, -cache(function (data, match, sendBadge, request) { - var repo = match[1]; - var format = match[2]; - var apiUrl = "https://sourcegraph.com/.api/repos/" + repo + "/-/shield"; - var badgeData = getBadgeData('used by', data); - request(apiUrl, function(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - badgeData.colorscheme = 'brightgreen'; - var data = JSON.parse(buffer); - badgeData.text[1] = data.value; - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// GitHub tag integration. -camp.route(/^\/github\/tag(-?pre)?\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - const includePre = Boolean(match[1]); - const user = match[2]; // eg, expressjs/express - const repo = match[3]; - const format = match[4]; - const apiUrl = `/repos/${user}/${repo}/tags`; - let badgeData = getBadgeData('tag', data); - if (badgeData.template === 'social') { - badgeData.logo = getLogo('github', data); - } - githubApiProvider.request(request, apiUrl, {}, (err, res, buffer) => { - if (githubCheckErrorResponse(badgeData, err, res)) { - sendBadge(format, badgeData); - return; - } - try { - var data = JSON.parse(buffer); - var versions = data.map(function(e) { return e.name; }); - var tag = latestVersion(versions, { pre: includePre }); - badgeData.text[1] = versionText(tag); - badgeData.colorscheme = versionColor(tag); - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'none'; - sendBadge(format, badgeData); - } - }); -})); - -// GitHub package and manifest version integration. -camp.route(/^\/github\/(package|manifest)-json\/([^/]+)\/([^/]+)\/([^/]+)\/?([^/]+)?\.(svg|png|gif|jpg|json)$/, -cache(function(query_data, match, sendBadge, request) { - var type = match[1]; - var info = match[2]; - var user = match[3]; - var repo = match[4]; - var branch = match[5] || 'master'; - var format = match[6]; - var apiUrl = 'https://raw.githubusercontent.com/' + user + '/' + repo + '/' + branch + '/' + type + '.json'; - var badgeData = getBadgeData(type, query_data); - request(apiUrl, function(err, res, buffer) { - if (githubCheckErrorResponse(badgeData, err, res)) { - sendBadge(format, badgeData); - return; - } - try { - var json_data = JSON.parse(buffer); - switch(info) { - case 'v': - case 'version': - var version = json_data.version; - badgeData.text[1] = versionText(version); - badgeData.colorscheme = versionColor(version); - break; - case 'n': - info = 'name'; - // falls through - default: - var value = typeof json_data[info] != 'undefined' && typeof json_data[info] != 'object' ? json_data[info] : Array.isArray(json_data[info]) ? json_data[info].join(", ") : 'invalid data'; - badgeData.text[0] = getLabel(`${type} ${info}`, query_data); - badgeData.text[1] = value; - badgeData.colorscheme = value != 'invalid data' ? 'blue' : 'lightgrey'; - break; - } - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid data'; - sendBadge(format, badgeData); - } - }); -})); - -// GitHub contributors integration. -camp.route(/^\/github\/contributors(-anon)?\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var isAnon = match[1]; - var user = match[2]; - var repo = match[3]; - var format = match[4]; - const apiUrl = `/repos/${user}/${repo}/contributors?page=1&per_page=1&anon=${!!isAnon}`; - var badgeData = getBadgeData('contributors', data); - if (badgeData.template === 'social') { - badgeData.logo = getLogo('github', data); - } - githubApiProvider.request(request, apiUrl, {}, (err, res, buffer) => { - if (githubCheckErrorResponse(badgeData, err, res)) { - sendBadge(format, badgeData); - return; - } - try { - var contributors; - - if (res.headers['link'] && res.headers['link'].indexOf('rel="last"') !== -1) { - contributors = res.headers['link'].match(/[?&]page=(\d+)[^>]+>; rel="last"/)[1]; - } else { - contributors = JSON.parse(buffer).length; - } - - badgeData.text[1] = metric(+contributors); - badgeData.colorscheme = 'blue'; - } catch(e) { - badgeData.text[1] = 'inaccessible'; - } - sendBadge(format, badgeData); - }); -})); - -// GitHub release integration -camp.route(/^\/github\/release\/([^/]+\/[^/]+)(?:\/(all))?\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var userRepo = match[1]; // eg, qubyte/rubidium - var allReleases = match[2]; - var format = match[3]; - let apiUrl = `/repos/${userRepo}/releases`; - if (allReleases === undefined) { - apiUrl = apiUrl + '/latest'; - } - var badgeData = getBadgeData('release', data); - if (badgeData.template === 'social') { - badgeData.logo = getLogo('github', data); - } - githubApiProvider.request(request, apiUrl, {}, (err, res, buffer) => { - if (githubCheckErrorResponse(badgeData, err, res)) { - sendBadge(format, badgeData); - return; - } - try { - var data = JSON.parse(buffer); - if (allReleases === 'all') { - data = data[0]; - } - var version = data.tag_name; - var prerelease = data.prerelease; - badgeData.text[1] = versionText(version); - badgeData.colorscheme = prerelease ? 'orange' : 'blue'; - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'none'; - sendBadge(format, badgeData); - } - }); -})); - -// GitHub release & pre-release date integration. -mapGithubReleaseDate({ camp, cache }, githubApiProvider); - -// GitHub commits since integration. -mapGithubCommitsSince({ camp, cache }, githubApiProvider); - -// GitHub release-download-count and pre-release-download-count integration. -camp.route(/^\/github\/(downloads|downloads-pre)\/([^/]+)\/([^/]+)(\/.+)?\/([^/]+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - const type = match[1]; // downloads or downloads-pre - var user = match[2]; // eg, qubyte/rubidium - var repo = match[3]; - - var tag = match[4]; // eg, v0.190.0, latest, null if querying all releases - var asset_name = match[5].toLowerCase(); // eg. total, atom-amd64.deb, atom.x86_64.rpm - var format = match[6]; - - if (tag) { tag = tag.slice(1); } - - var total = true; - if (tag) { - total = false; - } - - let apiUrl = `/repos/${user}/${repo}/releases`; - if (!total) { - var release_path = tag === 'latest' ? (type === 'downloads' ? 'latest' : '') : 'tags/' + tag; - if (release_path) { - apiUrl = apiUrl + '/' + release_path; - } - } - var badgeData = getBadgeData('downloads', data); - if (badgeData.template === 'social') { - badgeData.logo = getLogo('github', data); - } - githubApiProvider.request(request, apiUrl, {}, (err, res, buffer) => { - if (githubCheckErrorResponse(badgeData, err, res, 'repo or release not found')) { - sendBadge(format, badgeData); - return; - } - try { - var data = JSON.parse(buffer); - if (type === 'downloads-pre' && tag === 'latest') { - data = data[0]; - } - var downloads = 0; - - const labelWords = []; - if (total) { - data.forEach(function (tagData) { - tagData.assets.forEach(function (asset) { - if (asset_name === 'total' || asset_name === asset.name.toLowerCase()) { - downloads += asset.download_count; - } - }); - }); - - labelWords.push('total'); - if (asset_name !== 'total') { - labelWords.push(`[${asset_name}]`); - } - } else { - data.assets.forEach(function (asset) { - if (asset_name === 'total' || asset_name === asset.name.toLowerCase()) { - downloads += asset.download_count; - } - }); - - if (tag !== 'latest') { - labelWords.push(tag); - } - if (asset_name !== 'total') { - labelWords.push(`[${asset_name}]`); - } - } - labelWords.unshift(metric(downloads)); - badgeData.text[1] = labelWords.join(' '); - badgeData.colorscheme = 'brightgreen'; - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'none'; - sendBadge(format, badgeData); - } - }); -})); - -// GitHub issues integration. -camp.route(/^\/github\/issues(-pr)?(-closed)?(-raw)?\/(?!detail)([^/]+)\/([^/]+)\/?(.+)?\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var isPR = !!match[1]; - var isClosed = !!match[2]; - var isRaw = !!match[3]; - var user = match[4]; // eg, badges - var repo = match[5]; // eg, shields - var ghLabel = match[6]; // eg, website - var format = match[7]; - var query = {}; - var hasLabel = (ghLabel !== undefined); - - query.q = 'repo:' + user + '/' + repo + - (isPR? ' is:pr': ' is:issue') + - (isClosed? ' is:closed': ' is:open') + - (hasLabel? ` label:"${ghLabel}"` : ''); - - var classText = isClosed? 'closed': 'open'; - var leftClassText = isRaw? classText + ' ': ''; - var rightClassText = !isRaw? ' ' + classText: ''; - const isGhLabelMultiWord = hasLabel && ghLabel.includes(' '); - var labelText = hasLabel? (isGhLabelMultiWord? `"${ghLabel}"`: ghLabel) + ' ': ''; - var targetText = isPR? 'pull requests': 'issues'; - var badgeData = getBadgeData(leftClassText + labelText + targetText, data); - if (badgeData.template === 'social') { - badgeData.logo = getLogo('github', data); - } - githubApiProvider.request(request, '/search/issues', query, (err, res, buffer) => { - if (githubCheckErrorResponse(badgeData, err, res)) { - sendBadge(format, badgeData); - return; - } - try { - var data = JSON.parse(buffer); - var issues = data.total_count; - badgeData.text[1] = metric(issues) + rightClassText; - badgeData.colorscheme = (issues > 0)? 'yellow': 'brightgreen'; - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// GitHub issue detail integration. -camp.route(/^\/github\/(?:issues|pulls)\/detail\/(s|title|u|label|comments|age|last-update)\/([^/]+)\/([^/]+)\/(\d+)\.(svg|png|gif|jpg|json)$/, -cache((queryParams, match, sendBadge, request) => { - const [, which, owner, repo, number, format] = match; - const uri = `/repos/${owner}/${repo}/issues/${number}`; - const badgeData = getBadgeData(`issue/pull request ${number}`, queryParams); - if (badgeData.template === 'social') { - badgeData.logo = getLogo('github', queryParams); - } - githubApiProvider.request(request, uri, {}, (err, res, buffer) => { - if (githubCheckErrorResponse(badgeData, err, res, 'issue, pull request or repo not found')) { - sendBadge(format, badgeData); - return; - } - try { - const parsedData = JSON.parse(buffer); - const isPR = 'pull_request' in parsedData; - const noun = isPR ? 'pull request' : 'issue'; - badgeData.text[0] = getLabel(`${noun} ${parsedData.number}`, queryParams); - switch (which) { - case 's': { - const state = badgeData.text[1] = parsedData.state; - badgeData.colorscheme = null; - badgeData.colorB = makeColorB(githubStateColor(state), queryParams); - break; - } - case 'title': - badgeData.text[1] = parsedData.title; - break; - case 'u': - badgeData.text[0] = getLabel('author', queryParams); - badgeData.text[1] = parsedData.user.login; - break; - case 'label': - badgeData.text[0] = getLabel('label', queryParams); - badgeData.text[1] = parsedData.labels.map(i => i.name).join(' | '); - if (parsedData.labels.length === 1) { - badgeData.colorscheme = null; - badgeData.colorB = makeColorB(parsedData.labels[0].color, queryParams); - } - break; - case 'comments': { - badgeData.text[0] = getLabel('comments', queryParams); - const comments = badgeData.text[1] = parsedData.comments; - badgeData.colorscheme = null; - badgeData.colorB = makeColorB(githubCommentsColor(comments), queryParams); - break; - } - case 'age': - case 'last-update': { - const label = which === 'age' ? 'created' : 'updated'; - const date = which === 'age' ? parsedData.created_at : parsedData.updated_at; - badgeData.text[0] = getLabel(label, queryParams); - badgeData.text[1] = formatDate(date); - badgeData.colorscheme = ageColor(Date.parse(date)); - break; - } - default: - throw Error('Unreachable due to regex'); - } - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// GitHub pull request build status integration. -camp.route(/^\/github\/status\/(s|contexts)\/pulls\/([^/]+)\/([^/]+)\/(\d+)\.(svg|png|gif|jpg|json)$/, -cache((queryParams, match, sendBadge, request) => { - const [, which, owner, repo, number, format] = match; - const issueUri = `/repos/${owner}/${repo}/pulls/${number}`; - const badgeData = getBadgeData('checks', queryParams); - if (badgeData.template === 'social') { - badgeData.logo = getLogo('github', queryParams); - } - githubApiProvider.request(request, issueUri, {}, (err, res, buffer) => { - if (githubCheckErrorResponse(badgeData, err, res, 'pull request or repo not found')) { - sendBadge(format, badgeData); - return; - } - try { - const parsedData = JSON.parse(buffer); - const ref = parsedData.head.sha; - const statusUri = `/repos/${owner}/${repo}/commits/${ref}/status`; - githubApiProvider.request(request, statusUri, {}, (err, res, buffer) => { - try { - const parsedData = JSON.parse(buffer); - const state = badgeData.text[1] = parsedData.state; - badgeData.colorscheme = null; - badgeData.colorB = makeColorB(githubCheckStateColor(state), queryParams); - switch(which) { - case 's': - badgeData.text[1] = state; - break; - case 'contexts': { - const counts = countBy(parsedData.statuses, 'state'); - badgeData.text[1] = Object.keys(counts).map(k => `${counts[k]} ${k}`).join(', '); - break; - } - default: - throw Error('Unreachable due to regex'); - } - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// GitHub forks integration. -camp.route(/^\/github\/forks\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var user = match[1]; // eg, qubyte/rubidium - var repo = match[2]; - var format = match[3]; - const apiUrl = `/repos/${user}/${repo}`; - var badgeData = getBadgeData('forks', data); - if (badgeData.template === 'social') { - badgeData.logo = getLogo('github', data); - badgeData.links = [ - 'https://github.com/' + user + '/' + repo + '/fork', - 'https://github.com/' + user + '/' + repo + '/network', - ]; - } - githubApiProvider.request(request, apiUrl, {}, function(err, res, buffer) { - if (githubCheckErrorResponse(badgeData, err, res)) { - sendBadge(format, badgeData); - return; - } - try { - var data = JSON.parse(buffer); - var forks = data.forks_count; - badgeData.text[1] = forks; - badgeData.colorscheme = null; - badgeData.colorB = '#4183C4'; - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// GitHub stars integration. -camp.route(/^\/github\/stars\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var user = match[1]; // eg, qubyte/rubidium - var repo = match[2]; - var format = match[3]; - const apiUrl = `/repos/${user}/${repo}`; - var badgeData = getBadgeData('stars', data); - if (badgeData.template === 'social') { - badgeData.logo = getLogo('github', data); - badgeData.links = [ - 'https://github.com/' + user + '/' + repo, - 'https://github.com/' + user + '/' + repo + '/stargazers', - ]; - } - githubApiProvider.request(request, apiUrl, {}, (err, res, buffer) => { - if (githubCheckErrorResponse(badgeData, err, res)) { - sendBadge(format, badgeData); - return; - } - try { - badgeData.text[1] = metric(JSON.parse(buffer).stargazers_count); - badgeData.colorscheme = null; - badgeData.colorB = '#4183C4'; - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// GitHub watchers integration. -camp.route(/^\/github\/watchers\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var user = match[1]; // eg, qubyte/rubidium - var repo = match[2]; - var format = match[3]; - const apiUrl = `/repos/${user}/${repo}`; - var badgeData = getBadgeData('watchers', data); - if (badgeData.template === 'social') { - badgeData.logo = getLogo('github', data); - badgeData.links = [ - 'https://github.com/' + user + '/' + repo, - 'https://github.com/' + user + '/' + repo + '/watchers', - ]; - } - githubApiProvider.request(request, apiUrl, {}, (err, res, buffer) => { - if (githubCheckErrorResponse(badgeData, err, res)) { - sendBadge(format, badgeData); - return; - } - try { - badgeData.text[1] = JSON.parse(buffer).subscribers_count; - badgeData.colorscheme = null; - badgeData.colorB = '#4183C4'; - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// GitHub user followers integration. -camp.route(/^\/github\/followers\/([^/]+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var user = match[1]; // eg, qubyte - var format = match[2]; - const apiUrl = `/users/${user}`; - var badgeData = getBadgeData('followers', data); - if (badgeData.template === 'social') { - badgeData.logo = getLogo('github', data); - } - githubApiProvider.request(request, apiUrl, {}, (err, res, buffer) => { - if (githubCheckErrorResponse(badgeData, err, res, 'user not found')) { - sendBadge(format, badgeData); - return; - } - try { - badgeData.text[1] = JSON.parse(buffer).followers; - badgeData.colorscheme = null; - badgeData.colorB = '#4183C4'; - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// GitHub license integration. -camp.route(/^\/github\/license\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var user = match[1]; // eg, mashape - var repo = match[2]; // eg, apistatus - var format = match[3]; - const apiUrl = `/repos/${user}/${repo}`; - var badgeData = getBadgeData('license', data); - if (badgeData.template === 'social') { - badgeData.logo = getLogo('github', data); - } - githubApiProvider.request(request, apiUrl, {}, (err, res, buffer) => { - if (githubCheckErrorResponse(badgeData, err, res, 'repo not found', { 403: 'access denied' })) { - sendBadge(format, badgeData); - return; - } - try { - var body = JSON.parse(buffer); - const license = body.license; - if (license != null) { - badgeData.text[1] = license.spdx_id || 'unknown'; - setBadgeColor(badgeData, licenseToColor(license.spdx_id)); - sendBadge(format, badgeData); - } else { - badgeData.text[1] = 'missing'; - badgeData.colorscheme = 'red'; - sendBadge(format, badgeData); - } - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// GitHub file size. -camp.route(/^\/github\/size\/([^/]+)\/([^/]+)\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var user = match[1]; // eg, mashape - var repo = match[2]; // eg, apistatus - var path = match[3]; - var format = match[4]; - const apiUrl = `/repos/${user}/${repo}/contents/${path}`; - - var badgeData = getBadgeData('size', data); - if (badgeData.template === 'social') { - badgeData.logo = getLogo('github', data); - } - - githubApiProvider.request(request, apiUrl, {}, (err, res, buffer) => { - if (githubCheckErrorResponse(badgeData, err, res, 'repo or file not found')) { - sendBadge(format, badgeData); - return; - } - try { - var body = JSON.parse(buffer); - if (body && Number.isInteger(body.size)) { - badgeData.text[1] = prettyBytes(body.size); - badgeData.colorscheme = 'green'; - sendBadge(format, badgeData); - } else { - badgeData.text[1] = 'not a regular file'; - sendBadge(format, badgeData); - } - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// GitHub search hit counter. -camp.route(/^\/github\/search\/([^/]+)\/([^/]+)\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var user = match[1]; - var repo = match[2]; - var search = match[3]; - var format = match[4]; - var query = { q: search + ' repo:' + user + '/' + repo }; - var badgeData = getBadgeData(search + ' counter', data); - githubApiProvider.request(request, '/search/code', query, function(err, res, buffer) { - if (githubCheckErrorResponse(badgeData, err, res)) { - sendBadge(format, badgeData); - return; - } - try { - var body = JSON.parse(buffer); - badgeData.text[1] = metric(body.total_count); - badgeData.colorscheme = 'blue'; - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// GitHub commit statistics integration. -camp.route(/^\/github\/commit-activity\/(y|4w|w)\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - const interval = match[1]; - const user = match[2]; - const repo = match[3]; - const format = match[4]; - const apiUrl = `/repos/${user}/${repo}/stats/commit_activity`; - const badgeData = getBadgeData('commit activity', data); - if (badgeData.template === 'social') { - badgeData.logo = getLogo('github', data); - badgeData.links = [`https://github.com/${user}/${repo}`]; - } - githubApiProvider.request(request, apiUrl, {}, (err, res, buffer) => { - if (githubCheckErrorResponse(badgeData, err, res)) { - sendBadge(format, badgeData); - return; - } - try { - const parsedData = JSON.parse(buffer); - let value; - let intervalLabel; - switch (interval) { - case 'y': - value = parsedData.reduce((sum, weekInfo) => sum + weekInfo.total, 0); - intervalLabel = '/year'; - break; - case '4w': - value = parsedData.slice(-4).reduce((sum, weekInfo) => sum + weekInfo.total, 0); - intervalLabel = '/4 weeks'; - break; - case 'w': - value = parsedData.slice(-2)[0].total; - intervalLabel = '/week'; - break; - default: - throw Error('Unhandled case'); - } - badgeData.text[1] = `${metric(value)}${intervalLabel}`; - badgeData.colorscheme = 'blue'; - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// GitHub last commit integration. -camp.route(/^\/github\/last-commit\/([^/]+)\/([^/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - const user = match[1]; // eg, mashape - const repo = match[2]; // eg, apistatus - const branch = match[3]; - const format = match[4]; - let apiUrl = `/repos/${user}/${repo}/commits`; - if (branch) { - apiUrl += `?sha=${branch}`; - } - const badgeData = getBadgeData('last commit', data); - if (badgeData.template === 'social') { - badgeData.logo = getLogo('github', data); - badgeData.links = [`https://github.com/${user}/${repo}`]; - } - githubApiProvider.request(request, apiUrl, {}, (err, res, buffer) => { - if (githubCheckErrorResponse(badgeData, err, res)) { - sendBadge(format, badgeData); - return; - } - try { - const parsedData = JSON.parse(buffer); - const commitDate = parsedData[0].commit.author.date; - badgeData.text[1] = formatDate(commitDate); - badgeData.colorscheme = ageColor(Date.parse(commitDate)); - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// GitHub languages integration. -camp.route(/^\/github\/languages\/(top|count|code-size)\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var type = match[1]; - var user = match[2]; - var repo = match[3]; - var format = match[4]; - const apiUrl = `/repos/${user}/${repo}/languages`; - var badgeData = getBadgeData('languages', data); - if (badgeData.template === 'social') { - badgeData.logo = getLogo('github', data); - } - githubApiProvider.request(request, apiUrl, {}, (err, res, buffer) => { - if (githubCheckErrorResponse(badgeData, err, res)) { - sendBadge(format, badgeData); - return; - } - try { - const parsedData = JSON.parse(buffer); - var sumBytes = 0; - switch(type) { - case 'top': - var topLanguage = 'language'; - var maxBytes = 0; - for (const language of Object.keys(parsedData)) { - const bytes = parseInt(parsedData[language]); - if (bytes >= maxBytes) { - maxBytes = bytes; - topLanguage = language; - } - sumBytes += bytes; - } - badgeData.text[0] = getLabel(topLanguage, data); - if (sumBytes === 0) { // eg, empty repo, only .md files, etc. - badgeData.text[1] = 'none'; - badgeData.colorscheme = 'blue'; - } else { - badgeData.text[1] = (maxBytes / sumBytes * 100).toFixed(1) + '%'; // eg, 9.1% - } - break; - case 'count': - badgeData.text[0] = getLabel('languages', data); - badgeData.text[1] = Object.keys(parsedData).length; - badgeData.colorscheme = 'blue'; - break; - case 'code-size': - for (const language of Object.keys(parsedData)) { - sumBytes += parseInt(parsedData[language]); - } - badgeData.text[0] = getLabel('code size', data); - badgeData.text[1] = prettyBytes(sumBytes); - badgeData.colorscheme = 'blue'; - break; - default: - throw Error('Unreachable due to regex'); - } - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -//GitHub repository size integration. -camp.route(/^\/github\/repo-size\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var user = match[1]; - var repo = match[2]; - var format = match[3]; - const apiUrl = `/repos/${user}/${repo}`; - var badgeData = getBadgeData('repo size', data); - if (badgeData.template === 'social') { - badgeData.logo = getLogo('github', data); - } - githubApiProvider.request(request, apiUrl, {}, (err, res, buffer) => { - if (githubCheckErrorResponse(badgeData, err, res)) { - sendBadge(format, badgeData); - return; - } - try { - const parsedData = JSON.parse(buffer); - badgeData.text[1] = prettyBytes(parseInt(parsedData.size) * 1024); - badgeData.colorscheme = 'blue'; - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// GitHub commit status integration. -camp.route(/^\/github\/commit-status\/([^/]+)\/([^/]+)\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - const [, user, repo, branch, commit, format] = match; - const apiUrl = `/repos/${user}/${repo}/compare/${branch}...${commit}`; - const badgeData = getBadgeData('commit status', data); - githubApiProvider.request(request, apiUrl, {}, function(err, res, buffer) { - if (githubCheckErrorResponse(badgeData, err, res, 'commit or branch not found')) { - if (res && res.statusCode === 404) { - try { - if (JSON.parse(buffer).message.startsWith('No common ancestor between')) { - badgeData.text[1] = 'no common ancestor'; - badgeData.colorscheme = 'lightgrey'; - } - } catch(e) { - badgeData.text[1] = 'invalid'; - badgeData.colorscheme = 'lightgrey'; - } - } - sendBadge(format, badgeData); - return; - } - try { - const parsedData = JSON.parse(buffer); - const isInBranch = parsedData.status === 'identical' || parsedData.status === 'behind'; - if (isInBranch) { - badgeData.text[1] = `in ${branch}`; - badgeData.colorscheme = 'brightgreen'; - } else { - // status: ahead or diverged - badgeData.text[1] = `not in ${branch}`; - badgeData.colorscheme = 'yellow'; - } - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// Bitbucket issues integration. -camp.route(/^\/bitbucket\/issues(-raw)?\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var isRaw = !!match[1]; - var user = match[2]; // eg, atlassian - var repo = match[3]; // eg, python-bitbucket - var format = match[4]; - var apiUrl = 'https://bitbucket.org/api/1.0/repositories/' + user + '/' + repo - + '/issues/?limit=0&status=new&status=open'; - - var badgeData = getBadgeData('issues', data); - request(apiUrl, function(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - if (res.statusCode !== 200) { - throw Error('Failed to count issues.'); - } - var data = JSON.parse(buffer); - var issues = data.count; - badgeData.text[1] = metric(issues) + (isRaw? '': ' open'); - badgeData.colorscheme = issues ? 'yellow' : 'brightgreen'; - sendBadge(format, badgeData); - } catch(e) { - if (res.statusCode === 404) { - badgeData.text[1] = 'not found'; - } else { - badgeData.text[1] = 'invalid'; - } - sendBadge(format, badgeData); - } - }); -})); - -// Bitbucket pull requests integration. -camp.route(/^\/bitbucket\/pr(-raw)?\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var isRaw = !!match[1]; - var user = match[2]; // eg, atlassian - var repo = match[3]; // eg, python-bitbucket - var format = match[4]; - var apiUrl = 'https://bitbucket.org/api/2.0/repositories/' - + encodeURI(user) + '/' + encodeURI(repo) - + '/pullrequests/?limit=0&state=OPEN'; - - var badgeData = getBadgeData('pull requests', data); - request(apiUrl, function(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - if (res.statusCode !== 200) { - throw Error('Failed to count pull requests.'); - } - var data = JSON.parse(buffer); - var pullrequests = data.size; - badgeData.text[1] = metric(pullrequests) + (isRaw? '': ' open'); - badgeData.colorscheme = (pullrequests > 0)? 'yellow': 'brightgreen'; - sendBadge(format, badgeData); - } catch(e) { - if (res.statusCode === 404) { - badgeData.text[1] = 'not found'; - } else { - badgeData.text[1] = 'invalid'; - } - sendBadge(format, badgeData); - } - }); -})); - -// Bitbucket Pipelines integration. -camp.route(/^\/bitbucket\/pipelines\/([^/]+)\/([^/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - const user = match[1]; // eg, atlassian - const repo = match[2]; // eg, adf-builder-javascript - const branch = match[3] || 'master'; // eg, development - const format = match[4]; - const apiUrl = 'https://api.bitbucket.org/2.0/repositories/' - + encodeURIComponent(user) + '/' + encodeURIComponent(repo) - + '/pipelines/?fields=values.state&page=1&pagelen=2&sort=-created_on' - + '&target.ref_type=BRANCH&target.ref_name=' + encodeURIComponent(branch); - - const badgeData = getBadgeData('build', data); - - request(apiUrl, function(err, res, buffer) { - if (checkErrorResponse(badgeData, err, res)) { - sendBadge(format, badgeData); - return; - } - try { - const data = JSON.parse(buffer); - if (!data.values) { - throw Error('Unexpected response'); - } - const values = data.values.filter(value => value.state && value.state.name === 'COMPLETED'); - if (values.length > 0) { - switch (values[0].state.result.name) { - case 'SUCCESSFUL': - badgeData.text[1] = 'passing'; - badgeData.colorscheme = 'brightgreen'; - break; - case 'FAILED': - badgeData.text[1] = 'failing'; - badgeData.colorscheme = 'red'; - break; - case 'ERROR': - badgeData.text[1] = 'error'; - badgeData.colorscheme = 'red'; - break; - case 'STOPPED': - badgeData.text[1] = 'stopped'; - badgeData.colorscheme = 'yellow'; - break; - case 'EXPIRED': - badgeData.text[1] = 'expired'; - badgeData.colorscheme = 'yellow'; - break; - default: - badgeData.text[1] = 'unknown'; - } - } else { - badgeData.text[1] = 'never built'; - } - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// Chef cookbook integration. -camp.route(/^\/cookbook\/v\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var cookbook = match[1]; // eg, chef-sugar - var format = match[2]; - var apiUrl = 'https://supermarket.getchef.com/api/v1/cookbooks/' + cookbook + '/versions/latest'; - var badgeData = getBadgeData('cookbook', data); - - request(apiUrl, function(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - - try { - var data = JSON.parse(buffer); - var version = data.version; - badgeData.text[1] = versionText(version); - badgeData.colorscheme = versionColor(version); - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// ReSharper -mapNugetFeedv2({ camp, cache }, 'resharper', 0, function(match) { - return { - site: 'resharper', - feed: 'https://resharper-plugins.jetbrains.com/api/v2', - }; -}); - -// Chocolatey -mapNugetFeedv2({ camp, cache }, 'chocolatey', 0, function(match) { - return { - site: 'chocolatey', - feed: 'https://www.chocolatey.org/api/v2', - }; -}); - -// PowerShell Gallery -mapNugetFeedv2({ camp, cache }, 'powershellgallery', 0, function(match) { - return { - site: 'powershellgallery', - feed: 'https://www.powershellgallery.com/api/v2', - }; -}); - -// NuGet -mapNugetFeed({ camp, cache }, 'nuget', 0, function(match) { - return { - site: 'nuget', - feed: 'https://api.nuget.org/v3', - }; -}); - -// MyGet -mapNugetFeed({ camp, cache }, '(.+\\.)?myget\\/(.*)', 2, function(match) { - var tenant = match[1] || 'www.'; // eg. dotnet - var feed = match[2]; - return { - site: feed, - feed: 'https://' + tenant + 'myget.org/F/' + feed + '/api/v3', - }; -}); - -// Puppet Forge modules -camp.route(/^\/puppetforge\/([^/]+)\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var info = match[1]; // either `v`, `dt`, `e` or `f` - var user = match[2]; - var module = match[3]; - var format = match[4]; - var options = { - json: true, - uri: 'https://forgeapi.puppetlabs.com/v3/modules/' + user + '-' + module, - }; - var badgeData = getBadgeData('puppetforge', data); - request(options, function dealWithData(err, res, json) { - if (err != null || (json.length !== undefined && json.length === 0)) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - if (info === 'v') { - if (json.current_release) { - var version = json.current_release.version; - badgeData.text[1] = versionText(version); - badgeData.colorscheme = versionColor(version); - } else { - badgeData.text[1] = 'none'; - badgeData.colorscheme = 'lightgrey'; - } - } else if (info === 'dt') { - var total = json.downloads; - badgeData.colorscheme = downloadCountColor(total); - badgeData.text[0] = getLabel('downloads', data); - badgeData.text[1] = metric(total); - } else if (info === 'e') { - var endorsement = json.endorsement; - if (endorsement === 'approved') { - badgeData.colorscheme = 'green'; - } else if (endorsement === 'supported') { - badgeData.colorscheme = 'brightgreen'; - } else { - badgeData.colorscheme = 'red'; - } - badgeData.text[0] = getLabel('endorsement', data); - if (endorsement != null) { - badgeData.text[1] = endorsement; - } else { - badgeData.text[1] = 'none'; - } - } else if (info === 'f') { - var feedback = json.feedback_score; - badgeData.text[0] = getLabel('score', data); - if (feedback != null) { - badgeData.text[1] = feedback + '%'; - badgeData.colorscheme = coveragePercentageColor(feedback); - } else { - badgeData.text[1] = 'unknown'; - badgeData.colorscheme = 'lightgrey'; - } - } - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// Puppet Forge users -camp.route(/^\/puppetforge\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var info = match[1]; // either `rc` or `mc` - var user = match[2]; - var format = match[3]; - var options = { - json: true, - uri: 'https://forgeapi.puppetlabs.com/v3/users/' + user, - }; - var badgeData = getBadgeData('puppetforge', data); - request(options, function dealWithData(err, res, json) { - if (err != null || (json.length !== undefined && json.length === 0)) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - if (info === 'rc') { - var releases = json.release_count; - badgeData.colorscheme = floorCountColor(releases, 10, 50, 100); - badgeData.text[0] = getLabel('releases', data); - badgeData.text[1] = metric(releases); - } else if (info === 'mc') { - var modules = json.module_count; - badgeData.colorscheme = floorCountColor(modules, 5, 10, 50); - badgeData.text[0] = getLabel('modules', data); - badgeData.text[1] = metric(modules); - } - sendBadge(format, badgeData); - - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// Jenkins build status integration -camp.route(/^\/jenkins(?:-ci)?\/s\/(http(?:s)?)\/([^/]+)\/(.+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var scheme = match[1]; // http(s) - var host = match[2]; // example.org:8080 - var job = match[3]; // folder/job - var format = match[4]; - var options = { - json: true, - uri: scheme + '://' + host + '/job/' + job + '/api/json?tree=color', - }; - if (job.indexOf('/') > -1 ) { - options.uri = scheme + '://' + host + '/' + job + '/api/json?tree=color'; - } - - if (serverSecrets && serverSecrets.jenkins_user) { - options.auth = { - user: serverSecrets.jenkins_user, - pass: serverSecrets.jenkins_pass, - }; - } - - var badgeData = getBadgeData('build', data); - request(options, function(err, res, json) { - if (err !== null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - - try { - if (json.color === 'blue' || json.color === 'green') { - badgeData.colorscheme = 'brightgreen'; - badgeData.text[1] = 'passing'; - } else if (json.color === 'red') { - badgeData.colorscheme = 'red'; - badgeData.text[1] = 'failing'; - } else if (json.color === 'yellow') { - badgeData.colorscheme = 'yellow'; - badgeData.text[1] = 'unstable'; - } else if (json.color === 'grey' || json.color === 'disabled' - || json.color === 'aborted' || json.color === 'notbuilt') { - badgeData.colorscheme = 'lightgrey'; - badgeData.text[1] = 'not built'; - } else { - badgeData.colorscheme = 'lightgrey'; - badgeData.text[1] = 'building'; - } - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// Jenkins tests integration -camp.route(/^\/jenkins(?:-ci)?\/t\/(http(?:s)?)\/([^/]+)\/(.+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var scheme = match[1]; // http(s) - var host = match[2]; // example.org:8080 - var job = match[3]; // folder/job - var format = match[4]; - var options = { - json: true, - uri: scheme + '://' + host + '/job/' + job - + '/lastBuild/api/json?tree=' + encodeURIComponent('actions[failCount,skipCount,totalCount]'), - }; - if (job.indexOf('/') > -1 ) { - options.uri = scheme + '://' + host + '/' + job - + '/lastBuild/api/json?tree=' + encodeURIComponent('actions[failCount,skipCount,totalCount]'); - } - - if (serverSecrets && serverSecrets.jenkins_user) { - options.auth = { - user: serverSecrets.jenkins_user, - pass: serverSecrets.jenkins_pass, - }; - } - - var badgeData = getBadgeData('tests', data); - request(options, function(err, res, json) { - if (err !== null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - - try { - var testsObject = json.actions.filter(function (obj) { - return obj.hasOwnProperty('failCount'); - })[0]; - if (testsObject === undefined) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - var successfulTests = testsObject.totalCount - - (testsObject.failCount + testsObject.skipCount); - var percent = successfulTests / testsObject.totalCount; - badgeData.text[1] = successfulTests + ' / ' + testsObject.totalCount; - if (percent === 1) { - badgeData.colorscheme = 'brightgreen'; - } else if (percent === 0) { - badgeData.colorscheme = 'red'; - } else { - badgeData.colorscheme = 'yellow'; - } - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// Jenkins coverage integration (cobertura + jacoco) -camp.route(/^\/jenkins(?:-ci)?\/(c|j)\/(http(?:s)?)\/([^/]+)\/(.+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - const type = match[1]; // c - cobertura | j - jacoco - const scheme = match[2]; // http(s) - const host = match[3]; // example.org:8080 - const job = match[4]; // folder/job - const format = match[5]; - const options = { - json: true, - uri: `${scheme}://${host}/job/${job}/`, - }; - - if (job.indexOf('/') > -1 ) { - options.uri = `${scheme}://${host}/${job}/`; - } - - switch (type) { - case 'c': - options.uri += 'lastBuild/cobertura/api/json?tree=results[elements[name,denominator,numerator,ratio]]'; - break; - case 'j': - options.uri += 'lastBuild/jacoco/api/json?tree=instructionCoverage[covered,missed,percentage,total]'; - break; - } - - if (serverSecrets && serverSecrets.jenkins_user) { - options.auth = { - user: serverSecrets.jenkins_user, - pass: serverSecrets.jenkins_pass, - }; - } - - const badgeData = getBadgeData('coverage', data); - request(options, function(err, res, json) { - if (checkErrorResponse(badgeData, err, res)) { - sendBadge(format, badgeData); - return; - } - - try { - const coverageObject = json.instructionCoverage; - if (coverageObject === undefined) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - const coverage = coverageObject.percentage; - if (isNaN(coverage)) { - badgeData.text[1] = 'unknown'; - sendBadge(format, badgeData); - return; - } - badgeData.text[1] = coverage.toFixed(0) + '%'; - badgeData.colorscheme = coveragePercentageColor(coverage); - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// Jenkins Plugins version integration -camp.route(/^\/jenkins\/plugin\/v\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var pluginId = match[1]; // e.g. blueocean - var format = match[2]; - var badgeData = getBadgeData('plugin', data); - regularUpdate({ - url: 'https://updates.jenkins-ci.org/current/update-center.actual.json', - intervalMillis: 4 * 3600 * 1000, - scraper: json => Object.keys(json.plugins).reduce((previous, current) => { - previous[current] = json.plugins[current].version; - return previous; - }, {}), - }, (err, versions) => { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - var version = versions[pluginId]; - if (version === undefined) { - throw Error('Plugin not found!'); - } - badgeData.text[1] = versionText(version); - badgeData.colorscheme = versionColor(version); - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'not found'; - sendBadge(format, badgeData); - } - }); -})); - -// Ansible integration -camp.route(/^\/ansible\/role\/(?:(d)\/)?(\d+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - const type = match[1]; // eg d or nothing - const roleId = match[2]; // eg 3078 - const format = match[3]; - const options = { - json: true, - uri: 'https://galaxy.ansible.com/api/v1/roles/' + roleId + '/', - }; - const badgeData = getBadgeData('role', data); - request(options, function(err, res, json) { - if (res && (res.statusCode === 404 || json === undefined || json.state === null)) { - badgeData.text[1] = 'not found'; - sendBadge(format, badgeData); - return; - } - try { - if (type === 'd') { - badgeData.text[0] = getLabel('role downloads', data); - badgeData.text[1] = metric(json.download_count); - badgeData.colorscheme = 'blue'; - } else { - badgeData.text[1] = json.summary_fields.namespace.name + '.' + json.name; - badgeData.colorscheme = 'blue'; - } - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'errored'; - sendBadge(format, badgeData); - } - }); -})); - -// Codeship.io integration -camp.route(/^\/codeship\/([^/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var projectId = match[1]; // eg, `ab123456-00c0-0123-42de-6f98765g4h32`. - var format = match[3]; - var branch = match[2]; - var options = { - method: 'GET', - uri: 'https://codeship.com/projects/' + projectId + '/status' + (branch != null ? '?branch=' + branch : ''), - }; - var badgeData = getBadgeData('build', data); - request(options, function(err, res) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - var statusMatch = res.headers['content-disposition'] - .match(/filename="status_(.+)\./); - if (!statusMatch) { - badgeData.text[1] = 'unknown'; - sendBadge(format, badgeData); - return; - } - - switch (statusMatch[1]) { - case 'success': - badgeData.text[1] = 'passing'; - badgeData.colorscheme = 'brightgreen'; - break; - case 'projectnotfound': - badgeData.text[1] = 'not found'; - break; - case 'branchnotfound': - badgeData.text[1] = 'branch not found'; - break; - case 'testing': - case 'waiting': - case 'initiated': - badgeData.text[1] = 'pending'; - break; - case 'error': - case 'infrastructure_failure': - badgeData.text[1] = 'failing'; - badgeData.colorscheme = 'red'; - break; - case 'stopped': - case 'ignored': - case 'blocked': - badgeData.text[1] = 'not built'; - break; - } - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// Magnum CI integration - deprecated as of July 2018 -camp.route(/^\/magnumci\/ci\/([^/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - const format = match[3]; - const badgeData = getDeprecatedBadge('magnum ci', data); - sendBadge(format, badgeData); -})); - -// Maven-Central artifact version integration -// (based on repo1.maven.org rather than search.maven.org because of #846) -camp.route(/^\/maven-central\/v\/([^/]*)\/([^/]*)(?:\/([^/]*))?\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var groupId = match[1]; // eg, `com.google.inject` - var artifactId = match[2]; // eg, `guice` - var versionPrefix = match[3] || ''; // eg, `1.` - var format = match[4] || 'gif'; // eg, `svg` - var metadataUrl = 'http://repo1.maven.org/maven2' - + '/' + encodeURIComponent(groupId).replace(/\./g, '/') - + '/' + encodeURIComponent(artifactId) - + '/maven-metadata.xml'; - var badgeData = getBadgeData('maven-central', data); - request(metadataUrl, { headers: { 'Accept': 'text/xml' } }, function(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - xml2js.parseString(buffer.toString(), function (err, data) { - if (err != null) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - return; - } - try { - var versions = data.metadata.versioning[0].versions[0].version.reverse(); - var version = versions.find(function(version){ - return version.indexOf(versionPrefix) === 0; - }); - badgeData.text[1] = versionText(version); - badgeData.colorscheme = versionColor(version); - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); - }); -})); - -// standalone sonatype nexus installation -// API pattern: -// /nexus/(r|s|<repo-name>)/(http|https)/<nexus.host>[:port][/<entry-path>]/<group>/<artifact>[:k1=v1[:k2=v2[...]]].<format> -// for /nexus/[rs]/... pattern, use the search api of the nexus server, and -// for /nexus/<repo-name>/... pattern, use the resolve api of the nexus server. -camp.route(/^\/nexus\/(r|s|[^/]+)\/(https?)\/((?:[^/]+)(?:\/[^/]+)?)\/([^/]+)\/([^/:]+)(:.+)?\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var repo = match[1]; // r | s | repo-name - var scheme = match[2]; // http | https - var host = match[3]; // eg, `nexus.example.com` - var groupId = encodeURIComponent(match[4]); // eg, `com.google.inject` - var artifactId = encodeURIComponent(match[5]); // eg, `guice` - var queryOpt = (match[6] || '').replace(/:/g, '&'); // eg, `&p=pom&c=doc` - var format = match[7]; - - var badgeData = getBadgeData('nexus', data); - - var apiUrl = scheme + '://' + host - + ((repo == 'r' || repo == 's') - ? ('/service/local/lucene/search?g=' + groupId + '&a=' + artifactId + queryOpt) - : ('/service/local/artifact/maven/resolve?r=' + repo + '&g=' + groupId + '&a=' + artifactId + '&v=LATEST' + queryOpt)); - - request(apiUrl, { headers: { 'Accept': 'application/json' } }, function(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } else if (res && (res.statusCode === 404)) { - badgeData.text[1] = 'no-artifact'; - sendBadge(format, badgeData); - return; - } - try { - var parsed = JSON.parse(buffer); - var version = '0'; - switch (repo) { - case 'r': - if (parsed.data.length === 0) { - badgeData.text[1] = 'no-artifact'; - sendBadge(format, badgeData); - return; - } - version = parsed.data[0].latestRelease; - break; - case 's': - if (parsed.data.length === 0) { - badgeData.text[1] = 'no-artifact'; - sendBadge(format, badgeData); - return; - } - // only want to match 1.2.3-SNAPSHOT style versions, which may not always be in - // 'latestSnapshot' so check 'version' as well before continuing to next entry - parsed.data.every(function(artifact) { - if (isNexusSnapshotVersion(artifact.latestSnapshot)) { - version = artifact.latestSnapshot; - return; - } - if (isNexusSnapshotVersion(artifact.version)) { - version = artifact.version; - return; - } - return true; - }); - break; - default: - version = parsed.data.baseVersion || parsed.data.version; - break; - } - if (version !== '0') { - badgeData.text[1] = versionText(version); - badgeData.colorscheme = versionColor(version); - } else { - badgeData.text[1] = 'undefined'; - badgeData.colorscheme = 'orange'; - } - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// Bower version integration. -camp.route(/^\/bower\/(v|vpre)\/(.*)\.(svg|png|gif|jpg|json)$/, -cache((data, match, sendBadge, request) => { - const reqType = match[1]; - const repo = match[2]; // eg, `bootstrap`. - const format = match[3]; - const badgeData = getBadgeData('bower', data); - - // API doc: https://libraries.io/api#project - const options = { - method: 'GET', - json: true, - uri: `https://libraries.io/api/bower/${repo}`, - }; - if (serverSecrets && serverSecrets.libraries_io_api_key) { - options.qs = { - api_key: serverSecrets.libraries_io_api_key, - }; - } - request(options, (err, res, data) => { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - if(res.statusCode !== 200) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - return; - } - try { - //if reqType is `v`, then stable release number, if `vpre` then latest release - const version = reqType == 'v' ? data.latest_stable_release.name : data.latest_release_number; - badgeData.text[1] = versionText(version); - badgeData.colorscheme = versionColor(version); - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'no releases'; - sendBadge(format, badgeData); - } - }); -})); - -// Bower license integration. -camp.route(/^\/bower\/l\/(.*)\.(svg|png|gif|jpg|json)$/, -cache((data, match, sendBadge, request) => { - const repo = match[1]; // eg, `bootstrap`. - const format = match[2]; - const badgeData = getBadgeData('bower', data); - // API doc: https://libraries.io/api#project - const options = { - method: 'GET', - json: true, - uri: `https://libraries.io/api/bower/${repo}`, - }; - if (serverSecrets && serverSecrets.libraries_io_api_key) { - options.qs = { - api_key: serverSecrets.libraries_io_api_key, - }; - } - request(options, (err, res, data) => { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - const license = data.normalized_licenses[0]; - badgeData.text[1] = license; - badgeData.colorscheme = 'blue'; - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// Wheelmap integration. -camp.route(/^\/wheelmap\/a\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var nodeId = match[1]; // eg, `2323004600`. - var format = match[2]; - var options = { - method: 'GET', - json: true, - uri: 'http://wheelmap.org/nodes/' + nodeId + '.json', - }; - var badgeData = getBadgeData('wheelmap', data); - request(options, function(err, res, json) { - try { - var accessibility = json.node.wheelchair; - badgeData.text[1] = accessibility; - if (accessibility === 'yes') { - badgeData.colorscheme = 'brightgreen'; - } else if (accessibility === 'limited') { - badgeData.colorscheme = 'yellow'; - } else if (accessibility === 'no') { - badgeData.colorscheme = 'red'; - } - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'void'; - sendBadge(format, badgeData); - } - }); -})); - -// wordpress plugin version integration. -// example: https://img.shields.io/wordpress/plugin/v/akismet.svg for https://wordpress.org/plugins/akismet -camp.route(/^\/wordpress\/plugin\/v\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var plugin = match[1]; // eg, `akismet`. - var format = match[2]; - var apiUrl = 'http://api.wordpress.org/plugins/info/1.0/' + plugin + '.json'; - var badgeData = getBadgeData('plugin', data); - request(apiUrl, function(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - var data = JSON.parse(buffer); - var version = data.version; - badgeData.text[1] = versionText(version); - badgeData.colorscheme = versionColor(version); - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// wordpress plugin downloads integration. -// example: https://img.shields.io/wordpress/plugin/dt/akismet.svg for https://wordpress.org/plugins/akismet -camp.route(/^\/wordpress\/plugin\/dt\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var plugin = match[1]; // eg, `akismet`. - var format = match[2]; - var apiUrl = 'http://api.wordpress.org/plugins/info/1.0/' + plugin + '.json'; - var badgeData = getBadgeData('downloads', data); - request(apiUrl, function(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - var total = JSON.parse(buffer).downloaded; - badgeData.text[1] = metric(total); - if (total === 0) { - badgeData.colorscheme = 'red'; - } else if (total < 100) { - badgeData.colorscheme = 'yellow'; - } else if (total < 1000) { - badgeData.colorscheme = 'yellowgreen'; - } else if (total < 10000) { - badgeData.colorscheme = 'green'; - } else { - badgeData.colorscheme = 'brightgreen'; - } - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// wordpress plugin rating integration. -// example: https://img.shields.io/wordpress/plugin/r/akismet.svg for https://wordpress.org/plugins/akismet -camp.route(/^\/wordpress\/plugin\/r\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var plugin = match[1]; // eg, `akismet`. - var format = match[2]; - var apiUrl = 'http://api.wordpress.org/plugins/info/1.0/' + plugin + '.json'; - var badgeData = getBadgeData('rating', data); - request(apiUrl, function(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - var rating = parseInt(JSON.parse(buffer).rating); - rating = rating / 100 * 5; - badgeData.text[1] = starRating(rating); - if (rating === 0) { - badgeData.colorscheme = 'red'; - } else if (rating < 2) { - badgeData.colorscheme = 'yellow'; - } else if (rating < 3) { - badgeData.colorscheme = 'yellowgreen'; - } else if (rating < 4) { - badgeData.colorscheme = 'green'; - } else { - badgeData.colorscheme = 'brightgreen'; - } - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// wordpress version support integration. -// example: https://img.shields.io/wordpress/v/akismet.svg for https://wordpress.org/plugins/akismet -camp.route(/^\/wordpress\/v\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var plugin = match[1]; // eg, `akismet`. - var format = match[2]; - var apiUrl = 'http://api.wordpress.org/plugins/info/1.0/' + plugin + '.json'; - var badgeData = getBadgeData('wordpress', data); - request(apiUrl, function(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - var data = JSON.parse(buffer); - if (data.tested) { - var testedVersion = data.tested.replace(/[^0-9.]/g,''); - badgeData.text[1] = testedVersion + ' tested'; - var coreUrl = 'https://api.wordpress.org/core/version-check/1.7/'; - request(coreUrl, function(err, res, response) { - try { - var versions = JSON.parse(response).offers.map(function(v) { - return v.version; - }); - if (err != null) { sendBadge(format, badgeData); return; } - var svTestedVersion = testedVersion.split('.').length == 2 ? testedVersion += '.0' : testedVersion; - var svVersion = versions[0].split('.').length == 2 ? versions[0] += '.0' : versions[0]; - if (testedVersion == versions[0] || semver.gtr(svTestedVersion, svVersion)) { - badgeData.colorscheme = 'brightgreen'; - } else if (versions.indexOf(testedVersion) != -1) { - badgeData.colorscheme = 'orange'; - } else { - badgeData.colorscheme = 'yellow'; - } - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); - } else { - sendBadge(format, badgeData); - } - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// wordpress theme rating integration. -// example: https://img.shields.io/wordpress/theme/r/hestia.svg for https://wordpress.org/themes/hestia -camp.route(/^\/wordpress\/theme\/r\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var queryParams = { - 'action': 'theme_information', - 'request[slug]': match[1], // eg, `hestia`. - }; - var format = match[2]; - var apiUrl = 'https://api.wordpress.org/themes/info/1.1/?' + queryString.stringify(queryParams); - var badgeData = getBadgeData('rating', data); - request(apiUrl, function(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - var rating = parseInt(JSON.parse(buffer).rating); - rating = rating / 100 * 5; - badgeData.text[1] = starRating(rating); - if (rating === 0) { - badgeData.colorscheme = 'red'; - } else if (rating < 2) { - badgeData.colorscheme = 'yellow'; - } else if (rating < 3) { - badgeData.colorscheme = 'yellowgreen'; - } else if (rating < 4) { - badgeData.colorscheme = 'green'; - } else { - badgeData.colorscheme = 'brightgreen'; - } - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// wordpress theme download integration. -// example: https://img.shields.io/wordpress/theme/dt/hestia.svg for https://wordpress.org/themes/hestia -camp.route(/^\/wordpress\/theme\/dt\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var queryParams = { - 'action': 'theme_information', - 'request[slug]': match[1], // eg, `hestia`. - }; - var format = match[2]; - var apiUrl = 'https://api.wordpress.org/themes/info/1.1/?' + queryString.stringify(queryParams); - var badgeData = getBadgeData('downloads', data); - request(apiUrl, function(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - var downloads = JSON.parse(buffer).downloaded; - badgeData.text[1] = metric(downloads); - badgeData.colorscheme = downloadCountColor(downloads); - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// SourceForge integration. -camp.route(/^\/sourceforge\/(dt|dm|dw|dd)\/([^/]*)\/?(.*).(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - const info = match[1]; // eg, 'dm' - const project = match[2]; // eg, 'sevenzip`. - const folder = match[3]; - const format = match[4]; - let apiUrl = 'http://sourceforge.net/projects/' + project + '/files/' + folder + '/stats/json'; - const badgeData = getBadgeData('sourceforge', data); - let time_period, start_date; - badgeData.text[0] = getLabel('downloads', data); - // get yesterday since today is incomplete - const end_date = moment().subtract(24, 'hours'); - switch (info.charAt(1)) { - case 'm': - start_date = moment(end_date).subtract(30, 'days'); - time_period = '/month'; - break; - case 'w': - start_date = moment(end_date).subtract(6, 'days'); // 6, since date range is inclusive - time_period = '/week'; - break; - case 'd': - start_date = end_date; - time_period = '/day'; - break; - case 't': - start_date = moment(0); - time_period = ''; - break; - } - apiUrl += '?start_date=' + start_date.format("YYYY-MM-DD") + '&end_date=' + end_date.format("YYYY-MM-DD"); - request(apiUrl, function(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - const data = JSON.parse(buffer); - const downloads = data.total; - badgeData.text[1] = metric(downloads) + time_period; - badgeData.colorscheme = downloadCountColor(downloads); - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// Requires.io status integration -camp.route(/^\/requires\/([^/]+\/[^/]+\/[^/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var userRepo = match[1]; // eg, `github/celery/celery`. - var branch = match[2]; - var format = match[3]; - var uri = 'https://requires.io/api/v1/status/' + userRepo; - if (branch != null) { - uri += '?branch=' + branch; - } - var options = { - method: 'GET', - uri: uri, - }; - var badgeData = getBadgeData('requirements', data); - request(options, function(err, res, buffer) { - if (checkErrorResponse(badgeData, err, res)) { - sendBadge(format, badgeData); - return; - } - try { - const json = JSON.parse(buffer); - if (json.status === 'up-to-date') { - badgeData.text[1] = 'up to date'; - badgeData.colorscheme = 'brightgreen'; - } else if (json.status === 'outdated') { - badgeData.text[1] = 'outdated'; - badgeData.colorscheme = 'yellow'; - } else if (json.status === 'insecure') { - badgeData.text[1] = 'insecure'; - badgeData.colorscheme = 'red'; - } else { - badgeData.text[1] = 'unknown'; - badgeData.colorscheme = 'lightgrey'; - } - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - return; - } - }); -})); - -//vscode-marketplace download/version/rating integration -camp.route(/^\/vscode-marketplace\/(d|v|r|stars)\/(.*)\.(svg|png|gif|jpg|json)$/, - cache(function (data, match, sendBadge, request) { - let reqType = match[1]; // eg, d/v/r - let repo = match[2]; // eg, `ritwickdey.LiveServer`. - let format = match[3]; - - let badgeData = getBadgeData('vscode-marketplace', data); //temporary name - let options = getVscodeApiReqOptions(repo); - - request(options, function (err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - - try { - switch (reqType) { - case 'd': - badgeData.text[0] = getLabel('downloads', data); - var count = getVscodeStatistic(buffer, 'install'); - badgeData.text[1] = metric(count); - badgeData.colorscheme = downloadCountColor(count); - break; - case 'r': - badgeData.text[0] = getLabel('rating', data); - var rate = getVscodeStatistic(buffer, 'averagerating').toFixed(2); - var totalrate = getVscodeStatistic(buffer, 'ratingcount'); - badgeData.text[1] = rate + '/5 (' + totalrate + ')'; - badgeData.colorscheme = floorCountColor(rate, 2, 3, 4); - break; - case 'stars': - badgeData.text[0] = getLabel('rating', data); - var rating = getVscodeStatistic(buffer, 'averagerating').toFixed(2); - badgeData.text[1] = starRating(rating); - badgeData.colorscheme = floorCountColor(rating, 2, 3, 4); - break; - case 'v': - badgeData.text[0] = getLabel('visual studio marketplace', data); - var version = buffer.results[0].extensions[0].versions[0].version; - badgeData.text[1] = versionText(version); - badgeData.colorscheme = versionColor(version); - break; - } - sendBadge(format, badgeData); - } catch (e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - - }); - }) -); - -// Eclipse Marketplace integration. -camp.route(/^\/eclipse-marketplace\/(dt|dm|v|favorites|last-update)\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var type = match[1]; - var project = match[2]; - var format = match[3]; - var apiUrl = 'https://marketplace.eclipse.org/content/' + project + '/api/p'; - var badgeData = getBadgeData('eclipse marketplace', data); - request(apiUrl, function(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - xml2js.parseString(buffer.toString(), function (parseErr, parsedData) { - if (parseErr != null) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - return; - } - try { - const projectNode = parsedData.marketplace.node[0]; - switch (type) { - case 'dt': - badgeData.text[0] = getLabel('downloads', data); - var downloads = parseInt(projectNode.installstotal[0]); - badgeData.text[1] = metric(downloads); - badgeData.colorscheme = downloadCountColor(downloads); - break; - case 'dm': - badgeData.text[0] = getLabel('downloads', data); - var monthlydownloads = parseInt(projectNode.installsrecent[0]); - badgeData.text[1] = metric(monthlydownloads) + '/month'; - badgeData.colorscheme = downloadCountColor(monthlydownloads); - break; - case 'v': - badgeData.text[1] = versionText(projectNode.version[0]); - badgeData.colorscheme = versionColor(projectNode.version[0]); - break; - case 'favorites': - badgeData.text[0] = getLabel('favorites', data); - badgeData.text[1] = parseInt(projectNode.favorited[0]); - badgeData.colorscheme = 'brightgreen'; - break; - case 'last-update': - var date = 1000 * parseInt(projectNode.changed[0]); - badgeData.text[0] = getLabel('updated', data); - badgeData.text[1] = formatDate(date); - badgeData.colorscheme = ageColor(Date.parse(date)); - break; - default: - throw Error('Unreachable due to regex'); - } - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); - }); -})); - -camp.route(/^\/dockbit\/([A-Za-z0-9-_]+)\/([A-Za-z0-9-_]+)\.(svg|png|gif|jpg|json)$/, -cache({ - queryParams: ['token'], - handler: (data, match, sendBadge, request) => { - const org = match[1]; - const pipeline = match[2]; - const format = match[3]; - - const token = data.token; - const badgeData = getBadgeData('deploy', data); - const apiUrl = `https://dockbit.com/${org}/${pipeline}/status/${token}`; - - var dockbitStates = { - success: '#72BC37', - failure: '#F55C51', - error: '#F55C51', - working: '#FCBC41', - pending: '#CFD0D7', - rejected: '#CFD0D7', - }; - - request(apiUrl, { json: true }, function(err, res, data) { - try { - if (res && (res.statusCode === 404 || data.state === null)) { - badgeData.text[1] = 'not found'; - sendBadge(format, badgeData); - return; - } - - if (!res || err !== null || res.statusCode !== 200) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - - badgeData.text[1] = data.state; - badgeData.colorB = dockbitStates[data.state]; - - sendBadge(format, badgeData); - } - catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); - }, -})); - -camp.route(/^\/bitrise\/([^/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, -cache({ - queryParams: ['token'], - handler: (data, match, sendBadge, request) => { - const appId = match[1]; - const branch = match[2]; - const format = match[3]; - const token = data.token; - const badgeData = getBadgeData('bitrise', data); - let apiUrl = 'https://app.bitrise.io/app/' + appId + '/status.json?token=' + token; - if (typeof branch !== 'undefined') { - apiUrl += '&branch=' + branch; - } - - const statusColorScheme = { - success: 'brightgreen', - error: 'red', - unknown: 'lightgrey', - }; - - request(apiUrl, { json: true }, function(err, res, data) { - try { - if (!res || err !== null || res.statusCode !== 200) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - - badgeData.text[1] = data.status; - badgeData.colorscheme = statusColorScheme[data.status]; - - sendBadge(format, badgeData); - } - catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); - }, -})); - -// CircleCI build integration. -// https://circleci.com/api/v1/project/BrightFlair/PHP.Gt?circle-token=0a5143728784b263d9f0238b8d595522689b3af2&limit=1&filter=completed -camp.route(/^\/circleci\/(?:token\/(\w+))?[+/]?project\/(?:(github|bitbucket)\/)?([^/]+\/[^/]+)(?:\/(.*))?\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - const token = match[1]; - const type = match[2] || 'github'; // github OR bitbucket - const userRepo = match[3]; // eg, `RedSparr0w/node-csgo-parser`. - const branch = match[4]; - const format = match[5]; - - // Base API URL - let apiUrl = 'https://circleci.com/api/v1.1/project/' + type + '/' + userRepo; - - // Query Params - var queryParams = {}; - queryParams['limit'] = 1; - queryParams['filter'] = 'completed'; - - // Custom Banch if present - if (branch != null) { - apiUrl += "/tree/" + branch; - } - - // Append Token to Query Params if present - if (token) { - queryParams['circle-token'] = token; - } - - // Apprend query params to API URL - apiUrl += '?' + queryString.stringify(queryParams); - - const badgeData = getBadgeData('build', data); - request(apiUrl, { json:true }, function(err, res, data) { - if (checkErrorResponse(badgeData, err, res, { 404: 'project not found' })) { - sendBadge(format, badgeData); - return; - } - try { - if (data.message !== undefined){ - badgeData.text[1] = data.message; - sendBadge(format, badgeData); - return; - } - - let passCount = 0; - let status; - for (let i=0; i<data.length; i++) { - status = data[i].status; - if (['success', 'fixed'].includes(status)) { - passCount++; - } else if (status === 'failed') { - badgeData.colorscheme = 'red'; - badgeData.text[1] = 'failed'; - sendBadge(format, badgeData); - return; - } else if (['no_tests', 'scheduled', 'not_run'].includes(status)) { - badgeData.colorscheme = 'yellow'; - badgeData.text[1] = status.replace('_', ' '); - sendBadge(format, badgeData); - return; - } else { - badgeData.text[1] = status.replace('_', ' '); - sendBadge(format, badgeData); - return; - } - } - - if (passCount === data.length) { - badgeData.colorscheme = 'brightgreen'; - badgeData.text[1] = 'passing'; - } - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// CPAN integration. -camp.route(/^\/cpan\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var info = match[1]; // either `v` or `l` - var pkg = match[2]; // eg, Config-Augeas - var format = match[3]; - var badgeData = getBadgeData('cpan', data); - var url = 'https://fastapi.metacpan.org/v1/release/'+pkg; - request(url, function(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - var data = JSON.parse(buffer); - - if (info === 'v') { - var version = data.version; - badgeData.text[1] = versionText(version); - badgeData.colorscheme = versionColor(version); - } else if (info === 'l') { - var license = data.license[0]; - badgeData.text[1] = license; - badgeData.colorscheme = 'blue'; - } - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// CRAN/METACRAN integration. -camp.route(/^\/cran\/([vl])\/([^/]+)\.(svg|png|gif|jpg|json)$/, -cache(function(queryParams, match, sendBadge, request) { - var info = match[1]; // either `v` or `l` - var pkg = match[2]; // eg, devtools - var format = match[3]; - var url = 'http://crandb.r-pkg.org/' + pkg; - var badgeData = getBadgeData('cran', queryParams); - request(url, function (err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - if (res.statusCode === 404) { - badgeData.text[1] = 'not found'; - sendBadge(format, badgeData); - return; - } - try { - var data = JSON.parse(buffer); - - if (info === 'v') { - var version = data.Version; - badgeData.text[1] = versionText(version); - badgeData.colorscheme = versionColor(version); - sendBadge(format, badgeData); - } else if (info === 'l') { - badgeData.text[0] = getLabel('license', queryParams); - var license = data.License; - if (license) { - badgeData.text[1] = license; - badgeData.colorscheme = 'blue'; - } else { - badgeData.text[1] = 'unknown'; - } - sendBadge(format, badgeData); - } else { - throw Error('Unreachable due to regex'); - } - } catch (e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - - -// CTAN integration. -camp.route(/^\/ctan\/([vl])\/([^/]+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var info = match[1]; // either `v` or `l` - var pkg = match[2]; // eg, tex - var format = match[3]; - var url = 'http://www.ctan.org/json/pkg/' + pkg; - var badgeData = getBadgeData('ctan', data); - request(url, function (err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - if (res.statusCode === 404) { - badgeData.text[1] = 'not found'; - sendBadge(format, badgeData); - return; - } - try { - var parsedData = JSON.parse(buffer); - - if (info === 'v') { - var version = parsedData.version.number; - badgeData.text[1] = versionText(version); - badgeData.colorscheme = versionColor(version); - sendBadge(format, badgeData); - } else if (info === 'l') { - badgeData.text[0] = getLabel('license', data); - var license = parsedData.license; - if (Array.isArray(license) && license.length > 0) { - // API returns licenses inconsistently ordered, so fix the order. - badgeData.text[1] = license.sort().join(','); - badgeData.colorscheme = 'blue'; - } else { - badgeData.text[1] = 'unknown'; - } - sendBadge(format, badgeData); - } else { - throw Error('Unreachable due to regex'); - } - } catch (e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - });} -)); - -// DUB download integration -camp.route(/^\/dub\/(dd|dw|dm|dt)\/([^/]+)(?:\/([^/]+))?\.(svg|png|gif|jpg|json)$/, -cache(function (data, match, sendBadge, request) { - var info = match[1]; // downloads (dd - daily, dw - weekly, dm - monthly, dt - total) - var pkg = match[2]; // package name, e.g. vibe-d - var version = match[3]; // version (1.2.3 or latest) - var format = match[4]; - var apiUrl = 'https://code.dlang.org/api/packages/'+pkg; - if (version) { - apiUrl += '/' + version; - } - apiUrl += '/stats'; - var badgeData = getBadgeData('dub', data); - request(apiUrl, function(err, res, buffer) { - if (checkErrorResponse(badgeData, err, res)) { - sendBadge(format, badgeData); - return; - } - try { - var parsedData = JSON.parse(buffer); - if (info.charAt(0) === 'd') { - badgeData.text[0] = getLabel('downloads', data); - var downloads; - switch (info.charAt(1)) { - case 'm': - downloads = parsedData.downloads.monthly; - badgeData.text[1] = metric(downloads) + '/month'; - break; - case 'w': - downloads = parsedData.downloads.weekly; - badgeData.text[1] = metric(downloads) + '/week'; - break; - case 'd': - downloads = parsedData.downloads.daily; - badgeData.text[1] = metric(downloads) + '/day'; - break; - case 't': - downloads = parsedData.downloads.total; - badgeData.text[1] = metric(downloads); - break; - } - if (version) { - badgeData.text[1] += ' ' + versionText(version); - } - badgeData.colorscheme = downloadCountColor(downloads); - sendBadge(format, badgeData); - } - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// DUB license and version integration -camp.route(/^\/dub\/(v|l)\/([^/]+)\.(svg|png|gif|jpg|json)$/, -cache(function (data, match, sendBadge, request) { - var info = match[1]; // (v - version, l - license) - var pkg = match[2]; // package name, e.g. vibe-d - var format = match[3]; - var apiUrl = 'https://code.dlang.org/api/packages/' + pkg; - if (info === 'v') { - apiUrl += '/latest'; - } else if (info === 'l') { - apiUrl += '/latest/info'; - } - var badgeData = getBadgeData('dub', data); - request(apiUrl, function(err, res, buffer) { - if (checkErrorResponse(badgeData, err, res)) { - sendBadge(format, badgeData); - return; - } - try { - var parsedData = JSON.parse(buffer); - if (info === 'v') { - badgeData.text[1] = versionText(parsedData); - badgeData.colorscheme = versionColor(parsedData); - sendBadge(format, badgeData); - } else if (info == 'l') { - var license = parsedData.info.license; - badgeData.text[0] = getLabel('license', data); - if (license == null) { - badgeData.text[1] = 'Unknown'; - } else { - badgeData.text[1] = license; - badgeData.colorscheme = 'blue'; - } - sendBadge(format, badgeData); - } - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// Docker Hub stars integration. -camp.route(/^\/docker\/stars\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var user = match[1]; // eg, mashape - var repo = match[2]; // eg, kong - var format = match[3]; - if (user === '_') { - user = 'library'; - } - var path = user + '/' + repo; - var url = 'https://hub.docker.com/v2/repositories/' + path + '/stars/count/'; - var badgeData = getBadgeData('docker stars', data); - request(url, function(err, res, buffer) { - if (checkErrorResponse(badgeData, err, res, { 404: 'repo not found' })) { - sendBadge(format, badgeData); - return; - } - try { - const stars = parseInt(buffer, 10); - if (Number.isNaN(stars)) { - throw Error('Unexpected response.'); - } - badgeData.text[1] = metric(stars); - setBadgeColor(badgeData, data.colorB || '066da5'); - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// Docker Hub pulls integration. -camp.route(/^\/docker\/pulls\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var user = match[1]; // eg, mashape - var repo = match[2]; // eg, kong - var format = match[3]; - if (user === '_') { - user = 'library'; - } - var path = user + '/' + repo; - var url = 'https://hub.docker.com/v2/repositories/' + path; - var badgeData = getBadgeData('docker pulls', data); - request(url, function(err, res, buffer) { - if (checkErrorResponse(badgeData, err, res, { 404: 'repo not found' })) { - sendBadge(format, badgeData); - return; - } - try { - var parseData = JSON.parse(buffer); - var pulls = parseData.pull_count; - badgeData.text[1] = metric(pulls); - setBadgeColor(badgeData, data.colorB || '066da5'); - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - - -// Buildkite integration. -camp.route(/^\/buildkite\/([^/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var identifier = match[1]; // eg, 3826789cf8890b426057e6fe1c4e683bdf04fa24d498885489 - var branch = match[2] || 'master'; // Defaults to master if not specified - var format = match[3]; - - var url = 'https://badge.buildkite.com/' + identifier + '.json?branch=' + branch; - var badgeData = getBadgeData('build', data); - - request(url, function(err, res, buffer) { - if (checkErrorResponse(badgeData, err, res)) { - sendBadge(format, badgeData); - return; - } - - try { - var data = JSON.parse(buffer); - var status = data.status; - if (status === 'passing') { - badgeData.text[1] = 'passing'; - badgeData.colorscheme = 'green'; - } else if (status === 'failing') { - badgeData.text[1] = 'failing'; - badgeData.colorscheme = 'red'; - } else { - badgeData.text[1] = 'unknown'; - badgeData.colorscheme = 'lightgray'; - } - - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// Docker Hub automated integration. -camp.route(/^\/docker\/automated\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var user = match[1]; // eg, jrottenberg - var repo = match[2]; // eg, ffmpeg - var format = match[3]; - if (user === '_') { - user = 'library'; - } - var path = user + '/' + repo; - var url = 'https://registry.hub.docker.com/v2/repositories/' + path; - var badgeData = getBadgeData('docker build', data); - request(url, function(err, res, buffer) { - if (checkErrorResponse(badgeData, err, res, { 404: 'repo not found' })) { - sendBadge(format, badgeData); - return; - } - try { - var parsedData = JSON.parse(buffer); - var is_automated = parsedData.is_automated; - if (is_automated) { - badgeData.text[1] = 'automated'; - setBadgeColor(badgeData, data.colorB || '066da5'); - } else { - badgeData.text[1] = 'manual'; - badgeData.colorscheme = 'yellow'; - } - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// Docker Hub automated integration, most recent build's status (passed, pending, failed) -camp.route(/^\/docker\/build\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var user = match[1]; // eg, jrottenberg - var repo = match[2]; // eg, ffmpeg - var format = match[3]; - if (user === '_') { - user = 'library'; - } - var path = user + '/' + repo; - var url = 'https://registry.hub.docker.com/v2/repositories/' + path + '/buildhistory'; - var badgeData = getBadgeData('docker build', data); - request(url, function(err, res, buffer) { - if (checkErrorResponse(badgeData, err, res, { 404: 'repo not found' })) { - sendBadge(format, badgeData); - return; - } - try { - var parsedData = JSON.parse(buffer); - var most_recent_status = parsedData.results[0].status; - if (most_recent_status == 10) { - badgeData.text[1] = 'passing'; - badgeData.colorscheme = 'brightgreen'; - } else if (most_recent_status < 0) { - badgeData.text[1] = 'failing'; - badgeData.colorscheme = 'red'; - } else { - badgeData.text[1] = 'building'; - setBadgeColor(badgeData, data.colorB || '066da5'); - } - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// Twitter integration. -camp.route(/^\/twitter\/url\/([^/]+)\/(.+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var scheme = match[1]; // eg, https - var path = match[2]; // eg, shields.io - var format = match[3]; - var page = encodeURIComponent(scheme + '://' + path); - // The URL API died: #568. - //var url = 'http://cdn.api.twitter.com/1/urls/count.json?url=' + page; - var badgeData = getBadgeData('tweet', data); - if (badgeData.template === 'social') { - badgeData.logo = getLogo('twitter', data); - badgeData.links = [ - 'https://twitter.com/intent/tweet?text=Wow:&url=' + page, - 'https://twitter.com/search?q=' + page, - ]; - } - badgeData.text[1] = ''; - badgeData.colorscheme = null; - badgeData.colorB = data.colorB || '#55ACEE'; - sendBadge(format, badgeData); -})); - -// Twitter follow badge. -camp.route(/^\/twitter\/follow\/@?([^/]+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var user = match[1]; // eg, shields_io - var format = match[2]; - var options = { - url: 'http://cdn.syndication.twimg.com/widgets/followbutton/info.json?screen_names=' + user, - }; - var badgeData = getBadgeData('Follow @' + user, data); - - badgeData.colorscheme = null; - badgeData.colorB = '#55ACEE'; - if (badgeData.template === 'social') { - badgeData.logo = getLogo('twitter', data); - } - badgeData.links = [ - 'https://twitter.com/intent/follow?screen_name=' + user, - 'https://twitter.com/' + user + '/followers', - ]; - badgeData.text[1] = ''; - request(options, function(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - // The data is formatted as an array. - var data = JSON.parse(buffer)[0]; - if (data === undefined){ - badgeData.text[1] = 'invalid user'; - } else if (data.followers_count != null){// data.followers_count could be zero… don't just check if falsey. - badgeData.text[1] = metric(data.followers_count); - } - } catch(e) { - badgeData.text[1] = 'invalid'; - } - sendBadge(format, badgeData); - }); -})); - -// Snap CI build integration - no longer available. -camp.route(/^\/snap(-ci?)\/([^/]+\/[^/]+)(?:\/(.+))\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - const format = match[4]; - const badgeData = getDeprecatedBadge('snap CI', data); - sendBadge(format, badgeData); -})); - -// Visual Studio Team Services build integration. -camp.route(/^\/vso\/build\/([^/]+)\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var name = match[1]; // User name - var project = match[2]; // Project ID, e.g. 953a34b9-5966-4923-a48a-c41874cfb5f5 - var build = match[3]; // Build definition ID, e.g. 1 - var format = match[4]; - var url = 'https://' + name + '.visualstudio.com/DefaultCollection/_apis/public/build/definitions/' + project + '/' + build + '/badge'; - var badgeData = getBadgeData('build', data); - fetchFromSvg(request, url, function(err, res) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - badgeData.text[1] = res.toLowerCase(); - if (res === 'succeeded') { - badgeData.colorscheme = 'brightgreen'; - badgeData.text[1] = 'passing'; - } else if (res === 'failed') { - badgeData.colorscheme = 'red'; - badgeData.text[1] = 'failing'; - } - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// ImageLayers.io integration. -camp.route(/^\/imagelayers\/(image-size|layers)\/([^/]+)\/([^/]+)\/([^/]*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var type = match[1]; - var user = match[2]; - var repo = match[3]; - var tag = match[4]; - var format = match[5]; - if (user === '_') { - user = 'library'; - } - var path = user + '/' + repo; - var badgeData = getBadgeData(type, data); - var options = { - method: 'POST', - json: true, - body: { - "repos": [{ "name": path, "tag": tag }], - }, - uri: 'https://imagelayers.io/registry/analyze', - }; - request(options, function(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - if (type == 'image-size') { - var size = metric(buffer[0].repo.size) + "B"; - badgeData.text[0] = getLabel('image size', data); - badgeData.text[1] = size; - } else if (type == 'layers') { - badgeData.text[1] = buffer[0].repo.count; - } - badgeData.colorscheme = null; - badgeData.colorB = '#007ec6'; - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// MicroBadger integration. -camp.route(/^\/microbadger\/(image-size|layers)\/([^/]+)\/([^/]+)\/?([^/]*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - const type = match[1]; - let user = match[2]; - const repo = match[3]; - const tag = match[4]; - const format = match[5]; - if (user === '_') { - user = 'library'; - } - const url = `https://api.microbadger.com/v1/images/${user}/${repo}`; - - let badgeData = getBadgeData(type, data); - if (type === 'image-size') { - badgeData.text[0] = getLabel('image size', data); - } - - const options = { - method: 'GET', - uri: url, - headers: { - 'Accept': 'application/json', - }, - }; - request(options, function(err, res, buffer) { - if (res && res.statusCode === 404) { - badgeData.text[1] = 'not found'; - sendBadge(format, badgeData); - return; - } - - if (err != null || !res || res.statusCode !== 200) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - - try { - const parsedData = JSON.parse(buffer); - let image; - - if (tag) { - image = parsedData.Versions && parsedData.Versions.find(v => v.Tags.some(t => t.tag === tag)); - if (!image) { - badgeData.text[1] = 'not found'; - sendBadge(format, badgeData); - return; - } - } else { - image = parsedData; - } - - if (type === 'image-size') { - const downloadSize = image.DownloadSize; - if (downloadSize === undefined) { - badgeData.text[1] = 'unknown'; - sendBadge(format, badgeData); - return; - } - badgeData.text[1] = prettyBytes(parseInt(downloadSize)); - } else if (type === 'layers') { - badgeData.text[1] = image.LayerCount; - } - badgeData.colorscheme = null; - badgeData.colorB = '#007ec6'; - sendBadge(format, badgeData); - } catch(e) { - badgeData.colorscheme = 'red'; - badgeData.text[1] = 'error'; - sendBadge(format, badgeData); - } - }); -})); - -// Gitter room integration. -camp.route(/^\/gitter\/room\/([^/]+\/[^/]+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - // match[1] is the repo, which is not used. - var format = match[2]; - - var badgeData = getBadgeData('chat', data); - badgeData.text[1] = 'on gitter'; - badgeData.colorscheme = 'brightgreen'; - sendBadge(format, badgeData); -})); - -// homebrew integration -camp.route(/^\/homebrew\/v\/([^/]+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var pkg = match[1]; // eg. cake - var format = match[2]; - var apiUrl = 'https://formulae.brew.sh/api/formula/' + pkg + '.json'; - - var badgeData = getBadgeData('homebrew', data); - request(apiUrl, { headers: { 'Accept': 'application/json' } }, function(err, res, buffer) { - if (checkErrorResponse(badgeData, err, res)) { - sendBadge(format, badgeData); - return; - } - try { - var data = JSON.parse(buffer); - var version = data.versions.stable; - - badgeData.text[1] = versionText(version); - badgeData.colorscheme = versionColor(version); - - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// StackExchange integration. -camp.route(/^\/stackexchange\/([^/]+)\/([^/])\/([^/]+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var site = match[1]; // eg, stackoverflow - var info = match[2]; // either `r` - var item = match[3]; // eg, 232250 - var format = match[4]; - var path; - if (info === 'r') { - path = 'users/' + item; - } else if (info === 't') { - path = 'tags/' + item + '/info'; - } - var options = { - method: 'GET', - uri: 'https://api.stackexchange.com/2.2/' + path + '?site=' + site, - gzip: true, - }; - var badgeData = getBadgeData(site, data); - request(options, function (err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - try { - var parsedData = JSON.parse(buffer.toString()); - - // IP rate limiting - if (parsedData.error_name === 'throttle_violation') { - return; // Hope for the best in the cache. - } - - if (info === 'r') { - var reputation = parsedData.items[0].reputation; - badgeData.text[0] = getLabel(site + ' reputation', data); - badgeData.text[1] = metric(reputation); - badgeData.colorscheme = floorCountColor(1000, 10000, 20000); - } else if (info === 't') { - var count = parsedData.items[0].count; - badgeData.text[0] = getLabel(`${site} ${item} questions`, data); - badgeData.text[1] = metric(count); - badgeData.colorscheme = floorCountColor(1000, 10000, 20000); - } - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - });} -)); - -// beerpay.io integration. -// e.g. JSON response: https://beerpay.io/api/v1/beerpay/projects/beerpay.io -// e.g. SVG badge: https://beerpay.io/beerpay/beerpay.io/badge.svg?style=flat-square -camp.route(/^\/beerpay\/(.*)\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var user = match[1]; // eg, beerpay - var project = match[2]; // eg, beerpay.io - var format = match[3]; - - var apiUrl = 'https://beerpay.io/api/v1/' + user + '/projects/' + project; - var badgeData = getBadgeData('beerpay', data); - - request(apiUrl, function (err, res, buffer) { - if (err) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - - try { - var data = JSON.parse(buffer); - badgeData.text[1] = '$' + (data.total_amount || 0); - badgeData.colorscheme = 'red'; - sendBadge(format, badgeData); - } catch (e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// Maintenance integration. -camp.route(/^\/maintenance\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var status = match[1]; // eg, yes - var year = +match[2]; // eg, 2016 - var format = match[3]; - var badgeData = getBadgeData('maintained', data); - try { - var now = new Date(); - var cy = now.getUTCFullYear(); // current year. - var m = now.getUTCMonth(); // month. - if (status == 'no'){ - badgeData.text[1] = 'no! (as of ' + year + ')'; - badgeData.colorscheme = 'red'; - } else if (cy <= year) { - badgeData.text[1] = status; - badgeData.colorscheme = 'brightgreen'; - } else if ((cy === year + 1) && (m < 3)) { - badgeData.text[1] = 'stale (as of ' + cy + ')'; - } else { - badgeData.text[1] = 'no! (as of ' + year + ')'; - badgeData.colorscheme = 'red'; - } - sendBadge(format, badgeData); - } catch(e) { - log.error(e.stack); - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } -})); - -// bitHound integration - deprecated as of July 2018 -camp.route(/^\/bithound\/(code\/|dependencies\/|devDependencies\/)?(.+?)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - const format = match[3]; - const badgeData = getDeprecatedBadge('bithound', data); - sendBadge(format, badgeData); -})); - -// Waffle.io integration -camp.route(/^\/waffle\/label\/([^/]+)\/([^/]+)\/?([^/]+)?\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - const user = match[1]; // eg, evancohen - const repo = match[2]; // eg, smart-mirror - const ghLabel = match[3] || 'ready'; // eg, in%20progress - const format = match[4]; - const apiUrl = `https://api.waffle.io/${user}/${repo}/columns?with=count`; - const badgeData = getBadgeData('waffle', data); - - request(apiUrl, function(err, res, buffer) { - try { - if (checkErrorResponse(badgeData, err, res)) { - sendBadge(format, badgeData); - return; - } - const cols = JSON.parse(buffer); - if (cols.length === 0) { - badgeData.text[1] = 'absent'; - sendBadge(format, badgeData); - return; - } - let count = 0; - let color = '78bdf2'; - for (let i = 0; i < cols.length; i++) { - if (('label' in cols[i]) && (cols[i].label !== null)) { - if (cols[i].label.name === ghLabel) { - count = cols[i].count; - color = cols[i].label.color; - break; - } - } - } - badgeData.text[0] = getLabel(ghLabel, data); - badgeData.text[1] = '' + count; - badgeData.colorscheme = null; - badgeData.colorB = makeColorB(color, data); - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// Arch user repository (AUR) integration. -camp.route(/^\/aur\/(version|votes|license)\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var info = match[1]; - var pkg = match[2]; - var format = match[3]; - var apiUrl = 'https://aur.archlinux.org/rpc.php?type=info&arg=' + pkg; - var badgeData = getBadgeData('AUR', data); - request(apiUrl, function(err, res, buffer) { - if (checkErrorResponse(badgeData, err, res)) { - sendBadge(format, badgeData); - return; - } - try { - const parsedBuffer = JSON.parse(buffer); - const parsedData = parsedBuffer.results; - if (parsedBuffer.resultcount === 0) { - /* Note the 'not found' response from Arch Linux is: - status code = 200, - body = {"version":1,"type":"info","resultcount":0,"results":[]} - */ - badgeData.text[1] = 'not found'; - sendBadge(format, badgeData); - return; - } - - if (info === 'version') { - badgeData.text[1] = versionText(parsedData.Version); - if (parsedData.OutOfDate === null) { - badgeData.colorscheme = 'blue'; - } else { - badgeData.colorscheme = 'orange'; - } - } else if (info === 'votes') { - var votes = parsedData.NumVotes; - badgeData.text[0] = getLabel('votes', data); - badgeData.text[1] = votes; - badgeData.colorscheme = floorCountColor(votes, 2, 20, 60); - } else if (info === 'license') { - var license = parsedData.License; - badgeData.text[0] = getLabel('license', data); - badgeData.text[1] = license; - badgeData.colorscheme = 'blue'; - } - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// Chrome web store integration -camp.route(/^\/chrome-web-store\/(v|d|users|price|rating|stars|rating-count)\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var type = match[1]; - var storeId = match[2]; // eg, nimelepbpejjlbmoobocpfnjhihnpked - var format = match[3]; - var badgeData = getBadgeData('chrome web store', data); - var url = 'https://chrome.google.com/webstore/detail/' + storeId + '?hl=en&gl=US'; - var chromeWebStore = require('chrome-web-store-item-property'); - request(url, function(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - chromeWebStore.convert(buffer) - .then(function (value) { - var rating; - switch (type) { - case 'v': - badgeData.text[1] = versionText(value.version); - badgeData.colorscheme = versionColor(value.version); - break; - case 'd': - case 'users': - var downloads = value.interactionCount.UserDownloads; - badgeData.text[0] = getLabel('users', data); - badgeData.text[1] = metric(downloads); - badgeData.colorscheme = downloadCountColor(downloads); - break; - case 'price': - badgeData.text[0] = getLabel('price', data); - badgeData.text[1] = currencyFromCode(value.priceCurrency) + value.price; - badgeData.colorscheme = 'brightgreen'; - break; - case 'rating': - rating = Math.round(value.ratingValue * 100) / 100; - badgeData.text[0] = getLabel('rating', data); - badgeData.text[1] = rating + '/5'; - badgeData.colorscheme = floorCountColor(rating, 2, 3, 4); - break; - case 'stars': - rating = parseFloat(value.ratingValue); - badgeData.text[0] = getLabel('rating', data); - badgeData.text[1] = starRating(rating); - badgeData.colorscheme = floorCountColor(rating, 2, 3, 4); - break; - case 'rating-count': - var ratingCount = value.ratingCount; - badgeData.text[0] = getLabel('rating count', data); - badgeData.text[1] = metric(ratingCount) + ' total'; - badgeData.colorscheme = floorCountColor(ratingCount, 5, 50, 500); - break; - } - sendBadge(format, badgeData); - }).catch(function (err) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - }); - }); -})); - -// Cauditor integration - Badge deprectiated as of March 2018 -camp.route(/^\/cauditor\/(mi|ccn|npath|hi|i|ca|ce|dit)\/([^/]+)\/([^/]+)\/(.+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - const format = match[5]; - const badgeData = getDeprecatedBadge('cauditor', data); - sendBadge(format, badgeData); -})); - -// Mozilla addons integration -camp.route(/^\/amo\/(v|d|rating|stars|users)\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(query_data, match, sendBadge, request) { - var type = match[1]; - var addonId = match[2]; - var format = match[3]; - var badgeData = getBadgeData('mozilla add-on', query_data); - var url = 'https://services.addons.mozilla.org/api/1.5/addon/' + addonId; - - request(url, function(err, res, buffer) { - if (err) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - - xml2js.parseString(buffer.toString(), function (err, data) { - if (err) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - return; - } - - try { - var rating; - switch (type) { - case 'v': - var version = data.addon.version[0]; - badgeData.text[1] = versionText(version); - badgeData.colorscheme = versionColor(version); - break; - case 'd': - var downloads = parseInt(data.addon.total_downloads[0], 10); - badgeData.text[0] = getLabel('downloads', query_data); - badgeData.text[1] = metric(downloads); - badgeData.colorscheme = downloadCountColor(downloads); - break; - case 'rating': - rating = parseInt(data.addon.rating, 10); - badgeData.text[0] = getLabel('rating', query_data); - badgeData.text[1] = rating + '/5'; - badgeData.colorscheme = floorCountColor(rating, 2, 3, 4); - break; - case 'stars': - rating = parseInt(data.addon.rating, 10); - badgeData.text[0] = getLabel('stars', query_data); - badgeData.text[1] = starRating(rating); - badgeData.colorscheme = floorCountColor(rating, 2, 3, 4); - break; - case 'users': - var dailyUsers = parseInt(data.addon.daily_users[0], 10); - badgeData.text[0] = getLabel('users', query_data); - badgeData.text[1] = metric(dailyUsers); - badgeData.colorscheme = 'brightgreen'; - break; - } - - sendBadge(format, badgeData); - } catch (err) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); - }); -})); - -// jitPack version integration. -camp.route(/^\/jitpack\/v\/([^/]*)\/([^/]*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var groupId = 'com.github.' + match[1]; // github user - var artifactId = match[2]; // the project's name - var format = match[3]; // "svg" - var name = 'JitPack'; - - var pkg = groupId + '/' + artifactId + '/latest'; - var apiUrl = 'https://jitpack.io/api/builds/' + pkg ; - - var badgeData = getBadgeData(name, data); - - request(apiUrl, function(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - if (res.statusCode === 404) { - badgeData.text[1] = 'not found'; - sendBadge(format, badgeData); - return; - } - try { - var data = JSON.parse(buffer); - var status = data['status']; - var color = versionColor(data['version']); - var version = versionText(data['version']); - if(status !== 'ok'){ - color = 'red'; - version = 'unknown'; - } - badgeData.text[1] = version; - badgeData.colorscheme = color; - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// Test if a webpage is online -camp.route(/^\/website(-(([^-/]|--|\/\/)+)-(([^-/]|--|\/\/)+)(-(([^-/]|--|\/\/)+)-(([^-/]|--|\/\/)+))?)?\/([^/]+)\/(.+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var onlineMessage = escapeFormatSlashes(match[2] != null ? match[2] : "online"); - var offlineMessage = escapeFormatSlashes(match[4] != null ? match[4] : "offline"); - var onlineColor = escapeFormatSlashes(match[7] != null ? match[7] : "brightgreen"); - var offlineColor = escapeFormatSlashes(match[9] != null ? match[9] : "red"); - var userProtocol = match[11]; - var userURI = match[12]; - var format = match[13]; - var withProtocolURI = userProtocol + "://" + userURI; - var options = { - method: 'HEAD', - uri: withProtocolURI, - }; - var badgeData = getBadgeData("website", data); - badgeData.colorscheme = undefined; - request(options, function(err, res) { - // We consider all HTTP status codes below 310 as success. - if (err != null || res.statusCode >= 310) { - badgeData.text[1] = offlineMessage; - setBadgeColor(badgeData, offlineColor); - sendBadge(format, badgeData); - return; - } else { - badgeData.text[1] = onlineMessage; - setBadgeColor(badgeData, onlineColor); - sendBadge(format, badgeData); - return; - } - }); -})); - -// Issue Stats integration. -camp.route(/^\/issuestats\/([^/]+)(\/long)?\/([^/]+)\/(.+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var type = match[1]; // e.g. `i` for Issue or `p` for PR - var longForm = !!match[2]; - var host = match[3]; // e.g. `github` - var userRepo = match[4]; // e.g. `ruby/rails` - var format = match[5]; - - var badgeData = getBadgeData('Issue Stats', data); - - // Maps type name from URL to JSON property name prefix for badge data - var typeToPropPrefix = { - i: 'issue', - p: 'pr', - }; - var typePropPrefix = typeToPropPrefix[type]; - if (typePropPrefix === undefined) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - return; - } - - var url = 'http://issuestats.com/' + host + '/' + userRepo; - var qs = { format: 'json' }; - if (!longForm) { - qs.concise = true; - } - var options = { - method: 'GET', - url: url, - qs: qs, - gzip: true, - json: true, - }; - request(options, function(err, res, json) { - if (err != null || res.statusCode >= 500) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - return; - } - - if (res.statusCode >= 400 || !json || typeof json !== 'object') { - badgeData.text[1] = 'not found'; - sendBadge(format, badgeData); - return; - } - - try { - var label = json[typePropPrefix + '_badge_preamble']; - var value = json[typePropPrefix + '_badge_words']; - var color = json[typePropPrefix + '_badge_color']; - - if (label != null) badgeData.text[0] = getLabel(label, data); - badgeData.text[1] = value || 'invalid'; - if (color != null) badgeData.colorscheme = color; - - sendBadge(format, badgeData); - } catch (e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// Libraries.io integration. -camp.route(/^\/librariesio\/(github|release)\/([\w\-_]+\/[\w\-_]+)\/?([\w\-_.]+)?\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - const resource = match[1]; - const project = match[2]; - const version = match[3]; - const format = match[4]; - - let uri; - switch (resource) { - case 'github': { - uri = 'https://libraries.io/api/github/' + project + '/dependencies'; - break; - } - case 'release': { - const v = version || 'latest'; - uri = 'https://libraries.io/api/' + project + '/' + v + '/dependencies'; - break; - } - } - - const options = { method: 'GET', json: true, uri: uri }; - const badgeData = getBadgeData('dependencies', data); - - request(options, function(err, res, json) { - if (checkErrorResponse(badgeData, err, res, { 404: 'not available' })) { - sendBadge(format, badgeData); - return; - } - - try { - const deprecated = json.dependencies.filter(function(dep) { - return dep.deprecated; - }); - - const outofdate = json.dependencies.filter(function(dep) { - return dep.outdated; - }); - - // Deprecated dependencies are really bad - if (deprecated.length > 0) { - badgeData.colorscheme = 'red'; - badgeData.text[1] = deprecated.length + ' deprecated'; - return sendBadge(format, badgeData); - } - - // Out of date dependencies are pretty bad - if (outofdate.length > 0) { - badgeData.colorscheme = 'orange'; - badgeData.text[1] = outofdate.length + ' out of date'; - return sendBadge(format, badgeData); - } - - // Up to date dependencies are good! - badgeData.colorscheme = 'brightgreen'; - badgeData.text[1] = 'up to date'; - return sendBadge(format, badgeData); - } catch (e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// JetBrains Plugins repository integration -camp.route(/^\/jetbrains\/plugin\/(d|v)\/([^/]+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var pluginId = match[2]; - var type = match[1]; - var format = match[3]; - var leftText = type === 'v' ? 'jetbrains plugin' : 'downloads'; - var badgeData = getBadgeData(leftText, data); - var url = 'https://plugins.jetbrains.com/plugins/list?pluginId=' + pluginId; - - request(url, function(err, res, buffer) { - if (err || res.statusCode !== 200) { - badgeData.text[1] = 'inaccessible'; - return sendBadge(format, badgeData); - } - - xml2js.parseString(buffer.toString(), function (err, data) { - if (err) { - badgeData.text[1] = 'invalid'; - return sendBadge(format, badgeData); - } - - try { - var plugin = data["plugin-repository"].category; - if (!plugin) { - badgeData.text[1] = 'not found'; - return sendBadge(format, badgeData); - } - switch (type) { - case 'd': - var downloads = parseInt(data["plugin-repository"].category[0]["idea-plugin"][0]["$"].downloads, 10); - if (isNaN(downloads)) { - badgeData.text[1] = 'invalid'; - return sendBadge(format, badgeData); - } - badgeData.text[1] = metric(downloads); - badgeData.colorscheme = downloadCountColor(downloads); - return sendBadge(format, badgeData); - case 'v': - var version = data['plugin-repository'].category[0]["idea-plugin"][0].version[0]; - badgeData.text[1] = versionText(version); - badgeData.colorscheme = versionColor(version); - return sendBadge(format, badgeData); - } - } catch (err) { - badgeData.text[1] = 'invalid'; - return sendBadge(format, badgeData); - } - }); - }); -})); - -// Swagger Validator integration. -camp.route(/^\/swagger\/(valid)\/(2\.0)\/(https?)\/(.+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - // match[1] is not used // e.g. `valid` for validate - // match[2] is reserved for future use // e.g. `2.0` for OpenAPI 2.0 - var scheme = match[3]; // e.g. `https` - var swaggerUrl = match[4]; // e.g. `api.example.com/swagger.yaml` - var format = match[5]; - - var badgeData = getBadgeData('swagger', data); - - var urlParam = encodeURIComponent(scheme + '://' + swaggerUrl); - var url = 'http://online.swagger.io/validator/debug?url=' + urlParam; - var options = { - method: 'GET', - url: url, - gzip: true, - json: true, - }; - request(options, function(err, res, json) { - try { - if (err != null || res.statusCode >= 500 || typeof json !== 'object') { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - - var messages = json.schemaValidationMessages; - if (messages == null || messages.length === 0) { - badgeData.colorscheme = 'brightgreen'; - badgeData.text[1] = 'valid'; - } else { - badgeData.colorscheme = 'red'; - - var firstMessage = messages[0]; - if (messages.length === 1 && - firstMessage.level === 'error' && - /^Can't read from/.test(firstMessage.message)) { - badgeData.text[1] = 'not found'; - } else { - badgeData.text[1] = 'invalid'; - } - } - sendBadge(format, badgeData); - } catch (e) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - } - }); -})); - -// Discord integration -camp.route(/^\/discord\/([^/]+)\.(svg|png|gif|jpg|json)$/, -cache((data, match, sendBadge, request) => { - const serverID = match[1]; - const format = match[2]; - const apiUrl = `https://discordapp.com/api/guilds/${serverID}/widget.json`; - - request(apiUrl, (err, res, buffer) => { - const badgeData = getBadgeData('chat', data); - if (res && res.statusCode === 404) { - badgeData.text[1] = 'invalid server'; - sendBadge(format, badgeData); - return; - } - if (err != null || !res || res.statusCode !== 200) { - badgeData.text[1] = 'inaccessible'; - if (res && res.headers['content-type'] === 'application/json') { - try { - const data = JSON.parse(buffer); - if (data && typeof data.message === 'string') { - badgeData.text[1] = data.message.toLowerCase(); - } - } catch(e) { - } - } - sendBadge(format, badgeData); - return; - } - try { - const data = JSON.parse(buffer); - const members = Array.isArray(data.members) ? data.members : []; - badgeData.text[1] = members.length + ' online'; - badgeData.colorscheme = 'brightgreen'; - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); -})); - -// Maven metadata versioning integration. -camp.route(/^\/maven-metadata\/v\/(https?)\/(.+\.xml)\.(svg|png|gif|jpg|json)$/, - cache(function (data, match, sendBadge, request) { - const [, scheme, hostAndPath, format] = match; - const metadataUri = `${scheme}://${hostAndPath}`; - request(metadataUri, (error, response, body) => { - const badge = getBadgeData('maven', data); - if (!error && response.statusCode >= 200 && response.statusCode < 300) { - try { - xml2js.parseString(body, (err, result) => { - if (err) { - badge.text[1] = 'error'; - badge.colorscheme = 'red'; - sendBadge(format, badge); - } else { - const version = result.metadata.versioning[0].versions[0].version.slice(-1)[0]; - badge.text[1] = versionText(version); - badge.colorscheme = versionColor(version); - sendBadge(format, badge); - } - }); - } catch (e) { - badge.text[1] = 'error'; - badge.colorscheme = 'red'; - sendBadge(format, badge); - } - } else { - badge.text[1] = 'error'; - badge.colorscheme = 'red'; - sendBadge(format, badge); - } - }); -})); - -// User defined sources - JSON response -camp.route(/^\/badge\/dynamic\/(json|xml|yaml)\.(svg|png|gif|jpg|json)$/, -cache({ - queryParams: ['uri', 'url', 'query', 'prefix', 'suffix'], - handler: function(query, match, sendBadge, request) { - var type = match[1]; - var format = match[2]; - var prefix = query.prefix || ''; - var suffix = query.suffix || ''; - var pathExpression = query.query; - var requestOptions = {}; - - var badgeData = getBadgeData('custom badge', query); - - if (!query.uri && !query.url || !query.query){ - setBadgeColor(badgeData, 'red'); - badgeData.text[1] = !query.query ? 'no query specified' : 'no url specified'; - sendBadge(format, badgeData); - return; - } - - try { - var url = encodeURI(decodeURIComponent(query.url || query.uri)); - } catch(e){ - setBadgeColor(badgeData, 'red'); - badgeData.text[1] = 'malformed url'; - sendBadge(format, badgeData); - return; - } - - switch (type) { - case 'json': - requestOptions = { - headers: { - Accept: 'application/json', - }, - json: true, - }; - break; - case 'xml': - requestOptions = { - headers: { - Accept: 'application/xml, text/xml', - }, - }; - break; - case 'yaml': - requestOptions = { - headers: { - Accept: 'text/x-yaml, text/yaml, application/x-yaml, application/yaml, text/plain', - }, - }; - break; - } - - request(url, requestOptions, (err, res, data) => { - try { - if (checkErrorResponse(badgeData, err, res, { 404: 'resource not found' })) { - return; - } - - badgeData.colorscheme = 'brightgreen'; - - let innerText = []; - switch (type){ - case 'json': - data = (typeof data == 'object' ? data : JSON.parse(data)); - data = jp.query(data, pathExpression); - if (!data.length) { - throw 'no result'; - } - innerText = data; - break; - case 'xml': - data = new dom().parseFromString(data); - data = xpath.select(pathExpression, data); - if (!data.length) { - throw 'no result'; - } - data.forEach((i,v)=>{ - innerText.push(pathExpression.indexOf('@') + 1 ? i.value : i.firstChild.data); - }); - break; - case 'yaml': - data = yaml.safeLoad(data); - data = jp.query(data, pathExpression); - if (!data.length) { - throw 'no result'; - } - innerText = data; - break; + innerText = data; + break; } badgeData.text[1] = (prefix || '') + innerText.join(', ') + (suffix || ''); } catch (e) { @@ -6404,482 +300,6 @@ cache({ }, })); -// nsp for npm packages -camp.route(/^\/nsp\/npm\/(?:@([^/]+)?\/)?([^/]+)?(?:\/([^/]+)?)?\.(svg|png|gif|jpg|json)?$/, cache((data, match, sendBadge, request) => { - // A: /nsp/npm/:package.:format - // B: /nsp/npm/:package/:version.:format - // C: /nsp/npm/@:scope/:package.:format - // D: /nsp/npm/@:scope/:package/:version.:format - const badgeData = getBadgeData('nsp', data); - const capturedScopeWithoutAtSign = match[1]; - const capturedPackageName = match[2]; - const capturedVersion = match[3]; - const capturedFormat= match[4]; - - function getNspResults (scopeWithoutAtSign = null, packageName = '', packageVersion = '') { - const nspRequestOptions = { - method: 'POST', - body: { - package: { - name: null, - version: packageVersion, - }, - }, - json: true, - }; - - if (typeof scopeWithoutAtSign === 'string') { - nspRequestOptions.body.package.name = `@${scopeWithoutAtSign}/${packageName}`; - } else { - nspRequestOptions.body.package.name = packageName; - } - - request('https://api.nodesecurity.io/check', nspRequestOptions, (error, response, body) => { - if (error !== null || typeof body !== 'object' || body === null) { - badgeData.text[1] = 'invalid'; - badgeData.colorscheme = 'red'; - } else if (body.length !== 0) { - badgeData.text[1] = `${body.length} vulnerabilities`; - badgeData.colorscheme = 'red'; - } else { - badgeData.text[1] = 'no known vulnerabilities'; - badgeData.colorscheme = 'brightgreen'; - } - - sendBadge(capturedFormat, badgeData); - }); - } - - function getNpmVersionThenNspResults (scopeWithoutAtSign = null, packageName = '') { - // nsp doesn't properly detect the package version in POST requests so this function gets it for us - // https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#getpackageversion - const npmRequestOptions = { - headers: { - Accept: '*/*', - }, - json: true, - }; - let npmURL = null; - - if (typeof scopeWithoutAtSign === 'string') { - // Using 'latest' would save bandwidth, but it is currently not supported for scoped packages - npmURL = `http://registry.npmjs.org/@${scopeWithoutAtSign}%2F${packageName}`; - } else { - npmURL = `http://registry.npmjs.org/${packageName}/latest`; - } - - request(npmURL, npmRequestOptions, (error, response, body) => { - if (response !== null && response.statusCode === 404) { - // NOTE: in POST requests nsp does not distinguish between - // 'package not found' and 'no known vulnerabilities'. - // To keep consistency in the use case where a version is provided - // (which skips `getNpmVersionThenNspResults()` altogether) we'll say - // 'no known vulnerabilities' since it is technically true in both cases - badgeData.text[1] = 'no known vulnerabilities'; - - sendBadge(capturedFormat, badgeData); - } else if (error !== null || typeof body !== 'object' || body === null) { - badgeData.text[1] = 'invalid'; - badgeData.colorscheme = 'red'; - - sendBadge(capturedFormat, badgeData); - } else if (typeof body.version === 'string') { - getNspResults(scopeWithoutAtSign, packageName, body.version); - } else if (typeof body['dist-tags'] === 'object') { - getNspResults(scopeWithoutAtSign, packageName, body['dist-tags'].latest); - } else { - badgeData.text[1] = 'invalid'; - badgeData.colorscheme = 'red'; - - sendBadge(capturedFormat, badgeData); - } - }); - } - - if (typeof capturedVersion === 'string') { - getNspResults(capturedScopeWithoutAtSign, capturedPackageName, capturedVersion); - } else { - getNpmVersionThenNspResults(capturedScopeWithoutAtSign, capturedPackageName); - } -})); - -// bundle size for npm packages -camp.route(/^\/bundlephobia\/(min|minzip)\/(?:@([^/]+)?\/)?([^/]+)?(?:\/([^/]+)?)?\.(svg|png|gif|jpg|json)?$/, - cache((data, match, sendBadge, request) => { - // A: /bundlephobia/(min|minzip)/:package.:format - // B: /bundlephobia/(min|minzip)/:package/:version.:format - // C: /bundlephobia/(min|minzip)/@:scope/:package.:format - // D: /bundlephobia/(min|minzip)/@:scope/:package/:version.:format - const resultType = match[1]; - const scope = match[2]; - const packageName = match[3]; - const packageVersion = match[4]; - const format = match[5]; - const showMin = resultType === 'min'; - - const badgeData = getBadgeData(showMin ? 'minified size' : 'minzipped size', data); - - let packageString = typeof scope === 'string' ? - `@${scope}/${packageName}` : packageName; - - if(packageVersion) { - packageString += `@${packageVersion}`; - } - - const requestOptions = { - url: 'https://bundlephobia.com/api/size', - qs: { - package: packageString, - }, - json: true, - }; - - /** - * `ErrorCode` => `error code` - * @param {string} code - * @returns {string} - */ - const formatErrorCode = (code) => - code.replace(/([A-Z])/g, ' $1').trim().toLowerCase(); - - request(requestOptions, (error, response, body) => { - if(typeof body !== 'object' || body === null) { - badgeData.text[1] = 'error'; - badgeData.colorscheme = 'red'; - } else if (error !== null || body.error) { - badgeData.text[1] = 'code' in body.error ? - formatErrorCode(body.error.code) : 'error'; - badgeData.colorscheme = 'red'; - } else { - badgeData.text[1] = prettyBytes(showMin ? body.size : body.gzip); - badgeData.colorscheme = 'blue'; - } - sendBadge(format, badgeData); - }); -})); - -// Redmine plugin rating. -camp.route(/^\/redmine\/plugin\/(rating|stars)\/(.*)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - var type = match[1]; - var plugin = match[2]; - var format = match[3]; - var options = { - method: 'GET', - uri: 'https://www.redmine.org/plugins/' + plugin + '.xml', - }; - - var badgeData = getBadgeData(type, data); - request(options, function(err, res, buffer) { - if (err != null) { - badgeData.text[1] = 'inaccessible'; - sendBadge(format, badgeData); - return; - } - - xml2js.parseString(buffer.toString(), function (err, data) { - try { - var rating = data['redmine-plugin']['ratings-average'][0]._; - badgeData.colorscheme = floorCountColor(rating, 2, 3, 4); - - switch (type) { - case 'rating': - badgeData.text[1] = rating + '/5.0'; - break; - case 'stars': - badgeData.text[1] = starRating(Math.round(rating)); - break; - } - - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); - }); -})); - -// PHP version from Packagist -camp.route(/^\/packagist\/php-v\/([^/]+\/[^/]+)(?:\/([^/]+))?\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - const userRepo = match[1]; // eg, espadrine/sc - const version = match[2] ? match[2] : 'dev-master'; - const format = match[3]; - const options = { - method: 'GET', - uri: 'https://packagist.org/p/' + userRepo + '.json', - }; - const badgeData = getBadgeData('PHP', data); - request(options, function(err, res, buffer) { - if (err !== null) { - log.error('Packagist error: ' + err.stack); - if (res) { - log.error('' + res); - } - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - return; - } - - try { - const data = JSON.parse(buffer); - badgeData.text[1] = data.packages[userRepo][version].require.php; - badgeData.colorscheme = 'blue'; - } catch(e) { - badgeData.text[1] = 'invalid'; - } - sendBadge(format, badgeData); - }); -})); - - -// PHP version from PHP-Eye -camp.route(/^\/php-eye\/([^/]+\/[^/]+)(?:\/([^/]+))?\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - const userRepo = match[1]; // eg, espadrine/sc - const version = match[2] || 'dev-master'; - const format = match[3]; - const options = { - method: 'GET', - uri: 'https://php-eye.com/api/v1/package/' + userRepo + '.json', - }; - const badgeData = getBadgeData('PHP tested', data); - getPhpReleases(githubApiProvider, (err, phpReleases) => { - if (err != null) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - return; - } - request(options, function(err, res, buffer) { - if (err !== null) { - log.error('PHP-Eye error: ' + err.stack); - if (res) { - log.error('' + res); - } - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - return; - } - - try { - const data = JSON.parse(buffer); - const travis = data.versions.filter((release) => release.name === version)[0].travis; - - if (!travis.config_exists) { - badgeData.colorscheme = 'red'; - badgeData.text[1] = 'not tested'; - sendBadge(format, badgeData); - return; - } - - let versions = []; - for (const index in travis.runtime_status) { - if (travis.runtime_status[index] === 3 && index.match(/^php\d\d$/) !== null) { - versions.push(index.replace(/^php(\d)(\d)$/, '$1.$2')); - } - } - - let reduction = phpVersionReduction(versions, phpReleases); - - if (travis.runtime_status.hhvm === 3) { - reduction += reduction ? ', ' : ''; - reduction += 'HHVM'; - } - - if (reduction) { - badgeData.colorscheme = 'brightgreen'; - badgeData.text[1] = reduction; - } else if (!versions.length) { - badgeData.colorscheme = 'red'; - badgeData.text[1] = 'not tested'; - } else { - badgeData.text[1] = 'invalid'; - } - } catch(e) { - badgeData.text[1] = 'invalid'; - } - sendBadge(format, badgeData); - }); - }); -})); - -// Vaadin Directory Integration -camp.route(/^\/vaadin-directory\/(star|stars|status|rating|rc|rating-count|v|version|rd|release-date)\/(.*).(svg|png|gif|jpg|json)$/, cache(function (data, match, sendBadge, request) { - var type = match[1]; // Field required - var urlIdentifier = match[2]; // Name of repository - var format = match[3]; // Format - // API URL which contains also authentication info - var apiUrl = 'https://vaadin.com/vaadincom/directory-service/components/search/findByUrlIdentifier?projection=summary&urlIdentifier=' + urlIdentifier; - - // Set left-side text to 'Vaadin-Directory' by default - var badgeData = getBadgeData("Vaadin Directory", data); - request(apiUrl, function(err, res, buffer) { - if (checkErrorResponse(badgeData, err, res)) { - sendBadge(format, badgeData); - return; - } - - try { - var data = JSON.parse(buffer); - // Round the rating to 1 points decimal - var rating = ( Math.round(data.averageRating * 10) / 10 ).toFixed(1); - var ratingCount = data.ratingCount; - var lv = data.latestAvailableRelease.name.toLowerCase(); - var ld = data.latestAvailableRelease.publicationDate; - switch (type) { - // Since the first deploy was with `star`, I put the case there - // for safety pre-caution - case 'star': - case 'stars': // Stars - badgeData.text[0] = getLabel('stars', data); - badgeData.text[1] = starRating(rating); - badgeData.colorscheme = floorCountColor(rating, 2, 3, 4); - break; - case 'status': // Status of the component - var isPublished = data.status.toLowerCase(); - if (isPublished === 'published') { - badgeData.text[1] = "published"; - badgeData.colorB = '#00b4f0'; - } else { - badgeData.text[1] = "unpublished"; - } - break; - case 'rating': // rating - badgeData.text[0] = getLabel('rating', data); - if (!isNaN(rating)) { - badgeData.text[1] = rating + '/5'; - badgeData.colorscheme = floorCountColor(rating, 2, 3, 4); - } - break; - case 'rc': // rating count - case 'rating-count': - badgeData.text[0] = getLabel('rating count', data); - if (ratingCount && ratingCount != 0) { - badgeData.text[1] = metric(data.ratingCount) + ' total'; - badgeData.colorscheme = floorCountColor(data.ratingCount, 5, 50, 500); - } - break; - case 'v': // latest version - case 'version': - badgeData.text[0] = getLabel('latest ver', data); - badgeData.text[1] = lv; - badgeData.colorB = '#00b4f0'; - break; - case 'rd': - case 'release-date': // The release date of the latest version - badgeData.text[0] = getLabel('latest release date', data); - badgeData.text[1] = formatDate(ld); - badgeData.colorscheme = ageColor(ld); - break; - } - sendBadge(format, badgeData); - } catch (e) { - badgeData.text[1] = 'invalid'; - sendBadge(format, badgeData); - } - }); - -})); - -// Bugzilla bug integration -camp.route(/^\/bugzilla\/(\d+)\.(svg|png|gif|jpg|json)$/, -cache(function (data, match, sendBadge, request) { - var bugNumber = match[1]; // eg, 1436739 - var format = match[2]; - var options = { - method: 'GET', - json: true, - uri: 'https://bugzilla.mozilla.org/rest/bug/' + bugNumber, - }; - var badgeData = getBadgeData('bug ' + bugNumber, data); - request(options, function (err, res, json) { - if (checkErrorResponse(badgeData, err, res)) { - sendBadge(format, badgeData); - return; - } - try { - var bug = json.bugs[0]; - - switch (bug.status) { - case 'UNCONFIRMED': - badgeData.text[1] = 'unconfirmed'; - badgeData.colorscheme = 'blue'; - break; - case 'NEW': - badgeData.text[1] = 'new'; - badgeData.colorscheme = 'blue'; - break; - case 'ASSIGNED': - badgeData.text[1] = 'assigned'; - badgeData.colorscheme = 'green'; - break; - case 'RESOLVED': - if (bug.resolution === 'FIXED') { - badgeData.text[1] = 'fixed'; - badgeData.colorscheme = 'brightgreen'; - } else if (bug.resolution === 'INVALID') { - badgeData.text[1] = 'invalid'; - badgeData.colorscheme = 'yellow'; - } else if (bug.resolution === 'WONTFIX') { - badgeData.text[1] = 'won\'t fix'; - badgeData.colorscheme = 'orange'; - } else if (bug.resolution === 'DUPLICATE') { - badgeData.text[1] = 'duplicate'; - badgeData.colorscheme = 'lightgrey'; - } else if (bug.resolution === 'WORKSFORME') { - badgeData.text[1] = 'works for me'; - badgeData.colorscheme = 'yellowgreen'; - } else if (bug.resolution === 'INCOMPLETE') { - badgeData.text[1] = 'incomplete'; - badgeData.colorscheme = 'red'; - } else { - badgeData.text[1] = 'unknown'; - } - break; - default: - badgeData.text[1] = 'unknown'; - } - sendBadge(format, badgeData); - } catch (e) { - badgeData.text[1] = 'unknown'; - sendBadge(format, badgeData); - } - }); -})); - -// Dependabot SemVer compatibility integration -camp.route(/^\/dependabot\/semver\/([^/]+)\/(.+)\.(svg|png|gif|jpg|json)$/, -cache(function(data, match, sendBadge, request) { - const packageManager = match[1]; - const dependencyName = match[2]; - const format = match[3]; - const options = { - method: 'GET', - headers: { 'Accept': 'application/json' }, - uri: `https://api.dependabot.com/badges/compatibility_score?package-manager=${packageManager}&dependency-name=${dependencyName}&version-scheme=semver`, - }; - const badgeData = getBadgeData('semver stability', data); - badgeData.links = [`https://dependabot.com/compatibility-score.html?package-manager=${packageManager}&dependency-name=${dependencyName}&version-scheme=semver`]; - badgeData.logo = getLogo('dependabot', data); - request(options, function(err, res) { - if (checkErrorResponse(badgeData, err, res)) { - sendBadge(format, badgeData); - return; - } - try { - const dependabotData = JSON.parse(res['body']); - badgeData.text[1] = dependabotData.status; - badgeData.colorscheme = dependabotData.colour; - sendBadge(format, badgeData); - } catch(e) { - badgeData.text[1] = 'invalid'; - badgeData.colorscheme = 'red'; - sendBadge(format, badgeData); - } - }); -})); - // Any badge. camp.route(/^\/(:|badge\/)(([^-]|--)*?)-?(([^-]|--)*)-(([^-]|--)+)\.(svg|png|gif|jpg)$/, function(data, match, end, ask) { diff --git a/services/amo/amo.service.js b/services/amo/amo.service.js new file mode 100644 index 0000000000..6d932f53bd --- /dev/null +++ b/services/amo/amo.service.js @@ -0,0 +1,93 @@ +'use strict' + +const xml2js = require('xml2js') +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLabel: getLabel, +} = require('../../lib/badge-data') +const { + metric, + starRating, + addv: versionText, +} = require('../../lib/text-formatters') +const { + version: versionColor, + downloadCount: downloadCountColor, + floorCount: floorCountColor, +} = require('../../lib/color-formatters') + +module.exports = class Amo extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/amo\/(v|d|rating|stars|users)\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((queryData, match, sendBadge, request) => { + const type = match[1] + const addonId = match[2] + const format = match[3] + const badgeData = getBadgeData('mozilla add-on', queryData) + const url = + 'https://services.addons.mozilla.org/api/1.5/addon/' + addonId + + request(url, (err, res, buffer) => { + if (err) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + + xml2js.parseString(buffer.toString(), (err, data) => { + if (err) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + return + } + + try { + let rating + switch (type) { + case 'v': { + const version = data.addon.version[0] + badgeData.text[1] = versionText(version) + badgeData.colorscheme = versionColor(version) + break + } + case 'd': { + const downloads = parseInt(data.addon.total_downloads[0], 10) + badgeData.text[0] = getLabel('downloads', queryData) + badgeData.text[1] = metric(downloads) + badgeData.colorscheme = downloadCountColor(downloads) + break + } + case 'rating': + rating = parseInt(data.addon.rating, 10) + badgeData.text[0] = getLabel('rating', queryData) + badgeData.text[1] = rating + '/5' + badgeData.colorscheme = floorCountColor(rating, 2, 3, 4) + break + case 'stars': + rating = parseInt(data.addon.rating, 10) + badgeData.text[0] = getLabel('stars', queryData) + badgeData.text[1] = starRating(rating) + badgeData.colorscheme = floorCountColor(rating, 2, 3, 4) + break + case 'users': { + const dailyUsers = parseInt(data.addon.daily_users[0], 10) + badgeData.text[0] = getLabel('users', queryData) + badgeData.text[1] = metric(dailyUsers) + badgeData.colorscheme = 'brightgreen' + break + } + } + + sendBadge(format, badgeData) + } catch (err) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + }) + ) + } +} diff --git a/services/ansible/ansible.service.js b/services/ansible/ansible.service.js new file mode 100644 index 0000000000..026999b73a --- /dev/null +++ b/services/ansible/ansible.service.js @@ -0,0 +1,54 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLabel: getLabel, +} = require('../../lib/badge-data') +const { metric } = require('../../lib/text-formatters') + +module.exports = class Ansible extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/ansible\/role\/(?:(d)\/)?(\d+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const type = match[1] // eg d or nothing + const roleId = match[2] // eg 3078 + const format = match[3] + const options = { + json: true, + uri: 'https://galaxy.ansible.com/api/v1/roles/' + roleId + '/', + } + const badgeData = getBadgeData('role', data) + // eslint-disable-next-line handle-callback-err + request(options, (err, res, json) => { + if ( + res && + (res.statusCode === 404 || + json === undefined || + json.state === null) + ) { + badgeData.text[1] = 'not found' + sendBadge(format, badgeData) + return + } + try { + if (type === 'd') { + badgeData.text[0] = getLabel('role downloads', data) + badgeData.text[1] = metric(json.download_count) + badgeData.colorscheme = 'blue' + } else { + badgeData.text[1] = + json.summary_fields.namespace.name + '.' + json.name + badgeData.colorscheme = 'blue' + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'errored' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/aur/aur.service.js b/services/aur/aur.service.js new file mode 100644 index 0000000000..7c083ca333 --- /dev/null +++ b/services/aur/aur.service.js @@ -0,0 +1,67 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLabel: getLabel, +} = require('../../lib/badge-data') +const { checkErrorResponse } = require('../../lib/error-helper') +const { floorCount: floorCountColor } = require('../../lib/color-formatters') +const { addv: versionText } = require('../../lib/text-formatters') + +// For the Arch user repository (AUR). +module.exports = class Aur extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/aur\/(version|votes|license)\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const info = match[1] + const pkg = match[2] + const format = match[3] + const apiUrl = 'https://aur.archlinux.org/rpc.php?type=info&arg=' + pkg + const badgeData = getBadgeData('AUR', data) + request(apiUrl, (err, res, buffer) => { + if (checkErrorResponse(badgeData, err, res)) { + sendBadge(format, badgeData) + return + } + try { + const parsedBuffer = JSON.parse(buffer) + const parsedData = parsedBuffer.results + if (parsedBuffer.resultcount === 0) { + // Note the 'not found' response from Arch Linux is: + // status code = 200, + // body = {"version":1,"type":"info","resultcount":0,"results":[]} + badgeData.text[1] = 'not found' + sendBadge(format, badgeData) + return + } + + if (info === 'version') { + badgeData.text[1] = versionText(parsedData.Version) + if (parsedData.OutOfDate === null) { + badgeData.colorscheme = 'blue' + } else { + badgeData.colorscheme = 'orange' + } + } else if (info === 'votes') { + const votes = parsedData.NumVotes + badgeData.text[0] = getLabel('votes', data) + badgeData.text[1] = votes + badgeData.colorscheme = floorCountColor(votes, 2, 20, 60) + } else if (info === 'license') { + const license = parsedData.License + badgeData.text[0] = getLabel('license', data) + badgeData.text[1] = license + badgeData.colorscheme = 'blue' + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/base.js b/services/base.js index 07d79be2b3..09400390c8 100644 --- a/services/base.js +++ b/services/base.js @@ -269,7 +269,7 @@ class BaseService { return badgeData } - static register(camp, handleRequest, serviceConfig) { + static register({ camp, handleRequest, githubApiProvider }, serviceConfig) { const ServiceClass = this // In a static context, "this" is the class. camp.route( diff --git a/services/base.spec.js b/services/base.spec.js index dd70add96c..1b7a4525eb 100644 --- a/services/base.spec.js +++ b/services/base.spec.js @@ -285,7 +285,10 @@ describe('BaseService', function() { route: sinon.spy(), } mockHandleRequest = sinon.spy() - DummyService.register(mockCamp, mockHandleRequest, defaultConfig) + DummyService.register( + { camp: mockCamp, handleRequest: mockHandleRequest }, + defaultConfig + ) }) it('registers the service', function() { diff --git a/services/beerpay/beerpay.service.js b/services/beerpay/beerpay.service.js new file mode 100644 index 0000000000..94953568a7 --- /dev/null +++ b/services/beerpay/beerpay.service.js @@ -0,0 +1,42 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') + +// beerpay.io integration. +// e.g. JSON response: https://beerpay.io/api/v1/beerpay/projects/beerpay.io +// e.g. SVG badge: https://beerpay.io/beerpay/beerpay.io/badge.svg?style=flat-square +module.exports = class Beerpay extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/beerpay\/(.*)\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const user = match[1] // eg, beerpay + const project = match[2] // eg, beerpay.io + const format = match[3] + + const apiUrl = + 'https://beerpay.io/api/v1/' + user + '/projects/' + project + const badgeData = getBadgeData('beerpay', data) + + request(apiUrl, (err, res, buffer) => { + if (err) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + + try { + const data = JSON.parse(buffer) + badgeData.text[1] = '$' + (data.total_amount || 0) + badgeData.colorscheme = 'red' + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/bintray/bintray.service.js b/services/bintray/bintray.service.js new file mode 100644 index 0000000000..73af93cebd --- /dev/null +++ b/services/bintray/bintray.service.js @@ -0,0 +1,53 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { addv: versionText } = require('../../lib/text-formatters') +const { version: versionColor } = require('../../lib/color-formatters') +const serverSecrets = require('../../lib/server-secrets') + +module.exports = class Bintray extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/bintray\/v\/(.+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const path = match[1] // :subject/:repo/:package (e.g. asciidoctor/maven/asciidoctorj) + const format = match[2] + + const options = { + method: 'GET', + uri: + 'https://bintray.com/api/v1/packages/' + path + '/versions/_latest', + headers: { + Accept: 'application/json', + }, + } + + if (serverSecrets && serverSecrets.bintray_user) { + options.auth = { + user: serverSecrets.bintray_user, + pass: serverSecrets.bintray_apikey, + } + } + + const badgeData = getBadgeData('bintray', data) + request(options, (err, res, buffer) => { + if (err !== null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(buffer) + badgeData.text[1] = versionText(data.name) + badgeData.colorscheme = versionColor(data.name) + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/bitbucket/bitbucket-issues.service.js b/services/bitbucket/bitbucket-issues.service.js new file mode 100644 index 0000000000..eb67caa18d --- /dev/null +++ b/services/bitbucket/bitbucket-issues.service.js @@ -0,0 +1,51 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { metric } = require('../../lib/text-formatters') + +module.exports = class BitbucketIssues extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/bitbucket\/issues(-raw)?\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const isRaw = !!match[1] + const user = match[2] // eg, atlassian + const repo = match[3] // eg, python-bitbucket + const format = match[4] + const apiUrl = + 'https://bitbucket.org/api/1.0/repositories/' + + user + + '/' + + repo + + '/issues/?limit=0&status=new&status=open' + + const badgeData = getBadgeData('issues', data) + request(apiUrl, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + if (res.statusCode !== 200) { + throw Error('Failed to count issues.') + } + const data = JSON.parse(buffer) + const issues = data.count + badgeData.text[1] = metric(issues) + (isRaw ? '' : ' open') + badgeData.colorscheme = issues ? 'yellow' : 'brightgreen' + sendBadge(format, badgeData) + } catch (e) { + if (res.statusCode === 404) { + badgeData.text[1] = 'not found' + } else { + badgeData.text[1] = 'invalid' + } + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/bitbucket/bitbucket-pipelines.service.js b/services/bitbucket/bitbucket-pipelines.service.js new file mode 100644 index 0000000000..c5dab665a2 --- /dev/null +++ b/services/bitbucket/bitbucket-pipelines.service.js @@ -0,0 +1,77 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { checkErrorResponse } = require('../../lib/error-helper') + +module.exports = class BitbucketPipelines extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/bitbucket\/pipelines\/([^/]+)\/([^/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const user = match[1] // eg, atlassian + const repo = match[2] // eg, adf-builder-javascript + const branch = match[3] || 'master' // eg, development + const format = match[4] + const apiUrl = + 'https://api.bitbucket.org/2.0/repositories/' + + encodeURIComponent(user) + + '/' + + encodeURIComponent(repo) + + '/pipelines/?fields=values.state&page=1&pagelen=2&sort=-created_on' + + '&target.ref_type=BRANCH&target.ref_name=' + + encodeURIComponent(branch) + + const badgeData = getBadgeData('build', data) + + request(apiUrl, (err, res, buffer) => { + if (checkErrorResponse(badgeData, err, res)) { + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(buffer) + if (!data.values) { + throw Error('Unexpected response') + } + const values = data.values.filter( + value => value.state && value.state.name === 'COMPLETED' + ) + if (values.length > 0) { + switch (values[0].state.result.name) { + case 'SUCCESSFUL': + badgeData.text[1] = 'passing' + badgeData.colorscheme = 'brightgreen' + break + case 'FAILED': + badgeData.text[1] = 'failing' + badgeData.colorscheme = 'red' + break + case 'ERROR': + badgeData.text[1] = 'error' + badgeData.colorscheme = 'red' + break + case 'STOPPED': + badgeData.text[1] = 'stopped' + badgeData.colorscheme = 'yellow' + break + case 'EXPIRED': + badgeData.text[1] = 'expired' + badgeData.colorscheme = 'yellow' + break + default: + badgeData.text[1] = 'unknown' + } + } else { + badgeData.text[1] = 'never built' + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/bitbucket/bitbucket-pull-request.service.js b/services/bitbucket/bitbucket-pull-request.service.js new file mode 100644 index 0000000000..97b61e761e --- /dev/null +++ b/services/bitbucket/bitbucket-pull-request.service.js @@ -0,0 +1,51 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { metric } = require('../../lib/text-formatters') + +module.exports = class BitbucketPullRequest extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/bitbucket\/pr(-raw)?\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const isRaw = !!match[1] + const user = match[2] // eg, atlassian + const repo = match[3] // eg, python-bitbucket + const format = match[4] + const apiUrl = + 'https://bitbucket.org/api/2.0/repositories/' + + encodeURI(user) + + '/' + + encodeURI(repo) + + '/pullrequests/?limit=0&state=OPEN' + + const badgeData = getBadgeData('pull requests', data) + request(apiUrl, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + if (res.statusCode !== 200) { + throw Error('Failed to count pull requests.') + } + const data = JSON.parse(buffer) + const pullrequests = data.size + badgeData.text[1] = metric(pullrequests) + (isRaw ? '' : ' open') + badgeData.colorscheme = pullrequests > 0 ? 'yellow' : 'brightgreen' + sendBadge(format, badgeData) + } catch (e) { + if (res.statusCode === 404) { + badgeData.text[1] = 'not found' + } else { + badgeData.text[1] = 'invalid' + } + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/bithound/bithound.service.js b/services/bithound/bithound.service.js new file mode 100644 index 0000000000..8b27bed3d1 --- /dev/null +++ b/services/bithound/bithound.service.js @@ -0,0 +1,18 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { getDeprecatedBadge } = require('../../lib/deprecation-helpers') + +// bitHound integration - deprecated as of July 2018 +module.exports = class Bithound extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/bithound\/(code\/|dependencies\/|devDependencies\/)?(.+?)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const format = match[3] + const badgeData = getDeprecatedBadge('bithound', data) + sendBadge(format, badgeData) + }) + ) + } +} diff --git a/services/bitrise/bitrise.service.js b/services/bitrise/bitrise.service.js new file mode 100644 index 0000000000..13de7b5e8b --- /dev/null +++ b/services/bitrise/bitrise.service.js @@ -0,0 +1,54 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') + +module.exports = class Bitrise extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/bitrise\/([^/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, + cache({ + queryParams: ['token'], + handler: (data, match, sendBadge, request) => { + const appId = match[1] + const branch = match[2] + const format = match[3] + const token = data.token + const badgeData = getBadgeData('bitrise', data) + let apiUrl = + 'https://app.bitrise.io/app/' + + appId + + '/status.json?token=' + + token + if (typeof branch !== 'undefined') { + apiUrl += '&branch=' + branch + } + + const statusColorScheme = { + success: 'brightgreen', + error: 'red', + unknown: 'lightgrey', + } + + request(apiUrl, { json: true }, (err, res, data) => { + try { + if (!res || err !== null || res.statusCode !== 200) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + + badgeData.text[1] = data.status + badgeData.colorscheme = statusColorScheme[data.status] + + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }, + }) + ) + } +} diff --git a/services/bountysource/bountysource.service.js b/services/bountysource/bountysource.service.js new file mode 100644 index 0000000000..60e0259f4f --- /dev/null +++ b/services/bountysource/bountysource.service.js @@ -0,0 +1,45 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') + +module.exports = class Bountysource extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/bountysource\/team\/([^/]+)\/activity\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const team = match[1] // eg, `mozilla-core`. + const format = match[2] + const url = 'https://api.bountysource.com/teams/' + team + const options = { + headers: { Accept: 'application/vnd.bountysource+json; version=2' }, + } + const badgeData = getBadgeData('bounties', data) + request(url, options, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + if (res.statusCode !== 200) { + throw Error('Bad response.') + } + const parsedData = JSON.parse(buffer) + const activity = parsedData.activity_total + badgeData.colorscheme = 'brightgreen' + badgeData.text[1] = activity + sendBadge(format, badgeData) + } catch (e) { + if (res.statusCode === 404) { + badgeData.text[1] = 'not found' + } else { + badgeData.text[1] = 'invalid' + } + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/bower/bower-license.service.js b/services/bower/bower-license.service.js new file mode 100644 index 0000000000..f0052abce4 --- /dev/null +++ b/services/bower/bower-license.service.js @@ -0,0 +1,45 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const serverSecrets = require('../../lib/server-secrets') + +module.exports = class BowerLicense extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/bower\/l\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const repo = match[1] // eg, `bootstrap`. + const format = match[2] + const badgeData = getBadgeData('bower', data) + // API doc: https://libraries.io/api#project + const options = { + method: 'GET', + json: true, + uri: `https://libraries.io/api/bower/${repo}`, + } + if (serverSecrets && serverSecrets.libraries_io_api_key) { + options.qs = { + api_key: serverSecrets.libraries_io_api_key, + } + } + request(options, (err, res, data) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + const license = data.normalized_licenses[0] + badgeData.text[1] = license + badgeData.colorscheme = 'blue' + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/bower/bower-version.service.js b/services/bower/bower-version.service.js new file mode 100644 index 0000000000..103ab4c83e --- /dev/null +++ b/services/bower/bower-version.service.js @@ -0,0 +1,58 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { addv: versionText } = require('../../lib/text-formatters') +const { version: versionColor } = require('../../lib/color-formatters') +const serverSecrets = require('../../lib/server-secrets') + +module.exports = class BowerVersion extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/bower\/(v|vpre)\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const reqType = match[1] + const repo = match[2] // eg, `bootstrap`. + const format = match[3] + const badgeData = getBadgeData('bower', data) + + // API doc: https://libraries.io/api#project + const options = { + method: 'GET', + json: true, + uri: `https://libraries.io/api/bower/${repo}`, + } + if (serverSecrets && serverSecrets.libraries_io_api_key) { + options.qs = { + api_key: serverSecrets.libraries_io_api_key, + } + } + request(options, (err, res, data) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + if (res.statusCode !== 200) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + return + } + try { + //if reqType is `v`, then stable release number, if `vpre` then latest release + const version = + reqType === 'v' + ? data.latest_stable_release.name + : data.latest_release_number + badgeData.text[1] = versionText(version) + badgeData.colorscheme = versionColor(version) + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'no releases' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/bugzilla/bugzilla.service.js b/services/bugzilla/bugzilla.service.js new file mode 100644 index 0000000000..ff5c3b015c --- /dev/null +++ b/services/bugzilla/bugzilla.service.js @@ -0,0 +1,76 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { checkErrorResponse } = require('../../lib/error-helper') + +module.exports = class Bugzilla extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/bugzilla\/(\d+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const bugNumber = match[1] // eg, 1436739 + const format = match[2] + const options = { + method: 'GET', + json: true, + uri: 'https://bugzilla.mozilla.org/rest/bug/' + bugNumber, + } + const badgeData = getBadgeData('bug ' + bugNumber, data) + request(options, (err, res, json) => { + if (checkErrorResponse(badgeData, err, res)) { + sendBadge(format, badgeData) + return + } + try { + const bug = json.bugs[0] + + switch (bug.status) { + case 'UNCONFIRMED': + badgeData.text[1] = 'unconfirmed' + badgeData.colorscheme = 'blue' + break + case 'NEW': + badgeData.text[1] = 'new' + badgeData.colorscheme = 'blue' + break + case 'ASSIGNED': + badgeData.text[1] = 'assigned' + badgeData.colorscheme = 'green' + break + case 'RESOLVED': + if (bug.resolution === 'FIXED') { + badgeData.text[1] = 'fixed' + badgeData.colorscheme = 'brightgreen' + } else if (bug.resolution === 'INVALID') { + badgeData.text[1] = 'invalid' + badgeData.colorscheme = 'yellow' + } else if (bug.resolution === 'WONTFIX') { + badgeData.text[1] = "won't fix" + badgeData.colorscheme = 'orange' + } else if (bug.resolution === 'DUPLICATE') { + badgeData.text[1] = 'duplicate' + badgeData.colorscheme = 'lightgrey' + } else if (bug.resolution === 'WORKSFORME') { + badgeData.text[1] = 'works for me' + badgeData.colorscheme = 'yellowgreen' + } else if (bug.resolution === 'INCOMPLETE') { + badgeData.text[1] = 'incomplete' + badgeData.colorscheme = 'red' + } else { + badgeData.text[1] = 'unknown' + } + break + default: + badgeData.text[1] = 'unknown' + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'unknown' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/buildkite/buildkite.service.js b/services/buildkite/buildkite.service.js new file mode 100644 index 0000000000..ee17baaca0 --- /dev/null +++ b/services/buildkite/buildkite.service.js @@ -0,0 +1,49 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { checkErrorResponse } = require('../../lib/error-helper') + +module.exports = class Buildkite extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/buildkite\/([^/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const identifier = match[1] // eg, 3826789cf8890b426057e6fe1c4e683bdf04fa24d498885489 + const branch = match[2] || 'master' // Defaults to master if not specified + const format = match[3] + + const url = + 'https://badge.buildkite.com/' + identifier + '.json?branch=' + branch + const badgeData = getBadgeData('build', data) + + request(url, (err, res, buffer) => { + if (checkErrorResponse(badgeData, err, res)) { + sendBadge(format, badgeData) + return + } + + try { + const data = JSON.parse(buffer) + const status = data.status + if (status === 'passing') { + badgeData.text[1] = 'passing' + badgeData.colorscheme = 'green' + } else if (status === 'failing') { + badgeData.text[1] = 'failing' + badgeData.colorscheme = 'red' + } else { + badgeData.text[1] = 'unknown' + badgeData.colorscheme = 'lightgray' + } + + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/bundlephobia/bundlephobia.service.js b/services/bundlephobia/bundlephobia.service.js new file mode 100644 index 0000000000..c8f231dfef --- /dev/null +++ b/services/bundlephobia/bundlephobia.service.js @@ -0,0 +1,72 @@ +'use strict' + +const prettyBytes = require('pretty-bytes') +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') + +// Bundle size for npm packages. +module.exports = class Bundlephobia extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/bundlephobia\/(min|minzip)\/(?:@([^/]+)?\/)?([^/]+)?(?:\/([^/]+)?)?\.(svg|png|gif|jpg|json)?$/, + cache((data, match, sendBadge, request) => { + // A: /bundlephobia/(min|minzip)/:package.:format + // B: /bundlephobia/(min|minzip)/:package/:version.:format + // C: /bundlephobia/(min|minzip)/@:scope/:package.:format + // D: /bundlephobia/(min|minzip)/@:scope/:package/:version.:format + const resultType = match[1] + const scope = match[2] + const packageName = match[3] + const packageVersion = match[4] + const format = match[5] + const showMin = resultType === 'min' + + const badgeData = getBadgeData( + showMin ? 'minified size' : 'minzipped size', + data + ) + + let packageString = + typeof scope === 'string' ? `@${scope}/${packageName}` : packageName + + if (packageVersion) { + packageString += `@${packageVersion}` + } + + const requestOptions = { + url: 'https://bundlephobia.com/api/size', + qs: { + package: packageString, + }, + json: true, + } + + /** + * `ErrorCode` => `error code` + * @param {string} code + * @returns {string} + */ + const formatErrorCode = code => + code + .replace(/([A-Z])/g, ' $1') + .trim() + .toLowerCase() + + request(requestOptions, (error, response, body) => { + if (typeof body !== 'object' || body === null) { + badgeData.text[1] = 'error' + badgeData.colorscheme = 'red' + } else if (error !== null || body.error) { + badgeData.text[1] = + 'code' in body.error ? formatErrorCode(body.error.code) : 'error' + badgeData.colorscheme = 'red' + } else { + badgeData.text[1] = prettyBytes(showMin ? body.size : body.gzip) + badgeData.colorscheme = 'blue' + } + sendBadge(format, badgeData) + }) + }) + ) + } +} diff --git a/services/cauditor/cauditor.service.js b/services/cauditor/cauditor.service.js new file mode 100644 index 0000000000..11e6f8d5fb --- /dev/null +++ b/services/cauditor/cauditor.service.js @@ -0,0 +1,18 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { getDeprecatedBadge } = require('../../lib/deprecation-helpers') + +// Cauditor integration - Badge deprectiated as of March 2018 +module.exports = class Cauditor extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/cauditor\/(mi|ccn|npath|hi|i|ca|ce|dit)\/([^/]+)\/([^/]+)\/(.+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const format = match[5] + const badgeData = getDeprecatedBadge('cauditor', data) + sendBadge(format, badgeData) + }) + ) + } +} diff --git a/services/chrome-web-store/chrome-web-store.service.js b/services/chrome-web-store/chrome-web-store.service.js new file mode 100644 index 0000000000..97f380cfdd --- /dev/null +++ b/services/chrome-web-store/chrome-web-store.service.js @@ -0,0 +1,98 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLabel: getLabel, +} = require('../../lib/badge-data') +const { metric, starRating } = require('../../lib/text-formatters') +const { + downloadCount: downloadCountColor, + floorCount: floorCountColor, + version: versionColor, +} = require('../../lib/color-formatters') +const { + addv: versionText, + currencyFromCode, +} = require('../../lib/text-formatters') + +module.exports = class ChromeWebStore extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/chrome-web-store\/(v|d|users|price|rating|stars|rating-count)\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const type = match[1] + const storeId = match[2] // eg, nimelepbpejjlbmoobocpfnjhihnpked + const format = match[3] + const badgeData = getBadgeData('chrome web store', data) + const url = + 'https://chrome.google.com/webstore/detail/' + + storeId + + '?hl=en&gl=US' + const chromeWebStore = require('chrome-web-store-item-property') + request(url, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + chromeWebStore + .convert(buffer) + .then(value => { + let rating + switch (type) { + case 'v': + badgeData.text[1] = versionText(value.version) + badgeData.colorscheme = versionColor(value.version) + break + case 'd': + case 'users': { + const downloads = value.interactionCount.UserDownloads + badgeData.text[0] = getLabel('users', data) + badgeData.text[1] = metric(downloads) + badgeData.colorscheme = downloadCountColor(downloads) + break + } + case 'price': + badgeData.text[0] = getLabel('price', data) + badgeData.text[1] = + currencyFromCode(value.priceCurrency) + value.price + badgeData.colorscheme = 'brightgreen' + break + case 'rating': + rating = Math.round(value.ratingValue * 100) / 100 + badgeData.text[0] = getLabel('rating', data) + badgeData.text[1] = rating + '/5' + badgeData.colorscheme = floorCountColor(rating, 2, 3, 4) + break + case 'stars': + rating = parseFloat(value.ratingValue) + badgeData.text[0] = getLabel('rating', data) + badgeData.text[1] = starRating(rating) + badgeData.colorscheme = floorCountColor(rating, 2, 3, 4) + break + case 'rating-count': { + const ratingCount = value.ratingCount + badgeData.text[0] = getLabel('rating count', data) + badgeData.text[1] = metric(ratingCount) + ' total' + badgeData.colorscheme = floorCountColor( + ratingCount, + 5, + 50, + 500 + ) + break + } + } + sendBadge(format, badgeData) + }) + // eslint-disable-next-line handle-callback-err + .catch(err => { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + }) + }) + }) + ) + } +} diff --git a/services/cocoapods/cocoapods-apps.service.js b/services/cocoapods/cocoapods-apps.service.js new file mode 100644 index 0000000000..53b78da40d --- /dev/null +++ b/services/cocoapods/cocoapods-apps.service.js @@ -0,0 +1,49 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { checkErrorResponse } = require('../../lib/error-helper') +const { metric } = require('../../lib/text-formatters') +const { + downloadCount: downloadCountColor, +} = require('../../lib/color-formatters') + +module.exports = class CocoapodsApps extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/cocoapods\/(aw|at)\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const info = match[1] // One of these: "aw", "at" + const spec = match[2] // eg, AFNetworking + const format = match[3] + const apiUrl = 'https://metrics.cocoapods.org/api/v1/pods/' + spec + const badgeData = getBadgeData('apps', data) + request(apiUrl, (err, res, buffer) => { + if (checkErrorResponse(badgeData, err, res)) { + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(buffer) + let apps = 0 + switch (info.charAt(1)) { + case 'w': + apps = data.stats.app_week + badgeData.text[1] = metric(apps) + '/week' + break + case 't': + apps = data.stats.app_total + badgeData.text[1] = metric(apps) + break + } + badgeData.colorscheme = downloadCountColor(apps) + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/cocoapods/cocoapods-downloads.service.js b/services/cocoapods/cocoapods-downloads.service.js new file mode 100644 index 0000000000..6bd59e52dd --- /dev/null +++ b/services/cocoapods/cocoapods-downloads.service.js @@ -0,0 +1,53 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { checkErrorResponse } = require('../../lib/error-helper') +const { metric } = require('../../lib/text-formatters') +const { + downloadCount: downloadCountColor, +} = require('../../lib/color-formatters') + +module.exports = class CocoapodsDownloads extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/cocoapods\/(dm|dw|dt)\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const info = match[1] // One of these: "dm", "dw", "dt" + const spec = match[2] // eg, AFNetworking + const format = match[3] + const apiUrl = 'https://metrics.cocoapods.org/api/v1/pods/' + spec + const badgeData = getBadgeData('downloads', data) + request(apiUrl, (err, res, buffer) => { + if (checkErrorResponse(badgeData, err, res)) { + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(buffer) + let downloads = 0 + switch (info.charAt(1)) { + case 'm': + downloads = data.stats.download_month + badgeData.text[1] = metric(downloads) + '/month' + break + case 'w': + downloads = data.stats.download_week + badgeData.text[1] = metric(downloads) + '/week' + break + case 't': + downloads = data.stats.download_total + badgeData.text[1] = metric(downloads) + break + } + badgeData.colorscheme = downloadCountColor(downloads) + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/cocoapods/cocoapods-metrics.service.js b/services/cocoapods/cocoapods-metrics.service.js new file mode 100644 index 0000000000..3d1a77b450 --- /dev/null +++ b/services/cocoapods/cocoapods-metrics.service.js @@ -0,0 +1,41 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { checkErrorResponse } = require('../../lib/error-helper') +const { + coveragePercentage: coveragePercentageColor, +} = require('../../lib/color-formatters') + +module.exports = class CocoapodsMetrics extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/cocoapods\/metrics\/doc-percent\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const spec = match[1] // eg, AFNetworking + const format = match[2] + const apiUrl = 'https://metrics.cocoapods.org/api/v1/pods/' + spec + const badgeData = getBadgeData('docs', data) + request(apiUrl, (err, res, buffer) => { + if (checkErrorResponse(badgeData, err, res)) { + sendBadge(format, badgeData) + return + } + try { + const parsedData = JSON.parse(buffer) + let percentage = parsedData.cocoadocs.doc_percent + if (percentage == null) { + percentage = 0 + } + badgeData.colorscheme = coveragePercentageColor(percentage) + badgeData.text[1] = percentage + '%' + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/cocoapods/cocoapods.service.js b/services/cocoapods/cocoapods.service.js new file mode 100644 index 0000000000..c0601ef834 --- /dev/null +++ b/services/cocoapods/cocoapods.service.js @@ -0,0 +1,63 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { checkErrorResponse } = require('../../lib/error-helper') +const { addv: versionText } = require('../../lib/text-formatters') +const { version: versionColor } = require('../../lib/color-formatters') + +module.exports = class CocoapodsVersionPlatformLicense extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/cocoapods\/(v|p|l)\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const type = match[1] + const spec = match[2] // eg, AFNetworking + const format = match[3] + const apiUrl = + 'https://trunk.cocoapods.org/api/v1/pods/' + spec + '/specs/latest' + const typeToLabel = { v: 'pod', p: 'platform', l: 'license' } + const badgeData = getBadgeData(typeToLabel[type], data) + badgeData.colorscheme = null + request(apiUrl, (err, res, buffer) => { + if (checkErrorResponse(badgeData, err, res)) { + sendBadge(format, badgeData) + return + } + try { + const parsedData = JSON.parse(buffer) + const version = parsedData.version + let license + if (typeof parsedData.license === 'string') { + license = parsedData.license + } else { + license = parsedData.license.type + } + + const platforms = Object.keys( + parsedData.platforms || { + ios: '5.0', + osx: '10.7', + } + ).join(' | ') + if (type === 'v') { + badgeData.text[1] = versionText(version) + badgeData.colorscheme = versionColor(version) + } else if (type === 'p') { + badgeData.text[1] = platforms + badgeData.colorB = '#989898' + } else if (type === 'l') { + badgeData.text[1] = license + badgeData.colorB = '#373737' + } + + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/codacy/codacy-coverage.service.js b/services/codacy/codacy-coverage.service.js new file mode 100644 index 0000000000..381df73248 --- /dev/null +++ b/services/codacy/codacy-coverage.service.js @@ -0,0 +1,49 @@ +'use strict' + +const queryString = require('query-string') +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { fetchFromSvg } = require('../../lib/svg-badge-parser') +const { + coveragePercentage: coveragePercentageColor, +} = require('../../lib/color-formatters') + +module.exports = class CodacyCoverage extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/codacy\/coverage\/(?!grade\/)([^/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const projectId = match[1] // eg. e27821fb6289410b8f58338c7e0bc686 + const branch = match[2] + const format = match[3] + + const queryParams = {} + if (branch) { + queryParams.branch = branch + } + const query = queryString.stringify(queryParams) + const url = + 'https://api.codacy.com/project/badge/coverage/' + + projectId + + '?' + + query + const badgeData = getBadgeData('coverage', data) + fetchFromSvg(request, url, (err, res) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + badgeData.text[1] = res + badgeData.colorscheme = coveragePercentageColor(parseInt(res)) + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/codacy/codacy-grade.service.js b/services/codacy/codacy-grade.service.js new file mode 100644 index 0000000000..8d884007e4 --- /dev/null +++ b/services/codacy/codacy-grade.service.js @@ -0,0 +1,63 @@ +'use strict' + +const queryString = require('query-string') +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { fetchFromSvg } = require('../../lib/svg-badge-parser') + +module.exports = class CodacyGrade extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/codacy\/(?:grade\/)?(?!coverage\/)([^/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const projectId = match[1] // eg. e27821fb6289410b8f58338c7e0bc686 + const branch = match[2] + const format = match[3] + + const queryParams = {} + if (branch) { + queryParams.branch = branch + } + const query = queryString.stringify(queryParams) + const url = + 'https://api.codacy.com/project/badge/grade/' + + projectId + + '?' + + query + const badgeData = getBadgeData('code quality', data) + fetchFromSvg(request, url, (err, res) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + badgeData.text[1] = res + if (res === 'A') { + badgeData.colorscheme = 'brightgreen' + } else if (res === 'B') { + badgeData.colorscheme = 'green' + } else if (res === 'C') { + badgeData.colorscheme = 'yellowgreen' + } else if (res === 'D') { + badgeData.colorscheme = 'yellow' + } else if (res === 'E') { + badgeData.colorscheme = 'orange' + } else if (res === 'F') { + badgeData.colorscheme = 'red' + } else if (res === 'X') { + badgeData.text[1] = 'invalid' + badgeData.colorscheme = 'lightgrey' + } else { + badgeData.colorscheme = 'red' + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/codeclimate/codeclimate.service.js b/services/codeclimate/codeclimate.service.js new file mode 100644 index 0000000000..fbe6cce2a4 --- /dev/null +++ b/services/codeclimate/codeclimate.service.js @@ -0,0 +1,137 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { + coveragePercentage: coveragePercentageColor, + letterScore: letterScoreColor, + colorScale, +} = require('../../lib/color-formatters') + +module.exports = class Codeclimate extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/codeclimate(\/(c|coverage|maintainability|issues|tech-debt)(-letter|-percentage)?)?\/(.+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + let type + if (match[2] === 'c' || !match[2]) { + // Top-level and /coverage URLs equivalent to /c, still supported for backwards compatibility. See #1387. + type = 'coverage' + } else if (match[2] === 'tech-debt') { + type = 'technical debt' + } else { + type = match[2] + } + // For maintainability, default is letter, alternative is percentage. For coverage, default is percentage, alternative is letter. + const isAlternativeFormat = match[3] + const userRepo = match[4] // eg, `twbs/bootstrap`. + const format = match[5] + request( + { + method: 'GET', + uri: `https://api.codeclimate.com/v1/repos?github_slug=${userRepo}`, + json: true, + }, + (err, res, body) => { + const badgeData = getBadgeData(type, data) + if (err != null) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + return + } + + try { + if (!body.data || body.data.length === 0) { + badgeData.text[1] = 'not found' + sendBadge(format, badgeData) + return + } + + const branchData = + type === 'coverage' + ? body.data[0].relationships.latest_default_branch_test_report + .data + : body.data[0].relationships.latest_default_branch_snapshot + .data + if (branchData == null) { + badgeData.text[1] = 'unknown' + sendBadge(format, badgeData) + return + } + + const url = `https://api.codeclimate.com/v1/repos/${ + body.data[0].id + }/${type === 'coverage' ? 'test_reports' : 'snapshots'}/${ + branchData.id + }` + request(url, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + return + } + + try { + const parsedData = JSON.parse(buffer) + if (type === 'coverage' && isAlternativeFormat) { + const score = parsedData.data.attributes.rating.letter + badgeData.text[1] = score + badgeData.colorscheme = letterScoreColor(score) + } else if (type === 'coverage') { + const percentage = parseFloat( + parsedData.data.attributes.covered_percent + ) + badgeData.text[1] = percentage.toFixed(0) + '%' + badgeData.colorscheme = coveragePercentageColor(percentage) + } else if (type === 'issues') { + const count = parsedData.data.meta.issues_count + badgeData.text[1] = count + badgeData.colorscheme = colorScale( + [1, 5, 10, 20], + ['brightgreen', 'green', 'yellowgreen', 'yellow', 'red'] + )(count) + } else if (type === 'technical debt') { + const percentage = parseFloat( + parsedData.data.attributes.ratings[0].measure.value + ) + badgeData.text[1] = percentage.toFixed(0) + '%' + badgeData.colorscheme = colorScale( + [5, 10, 20, 50], + ['brightgreen', 'green', 'yellowgreen', 'yellow', 'red'] + )(percentage) + } else if ( + type === 'maintainability' && + isAlternativeFormat + ) { + // maintainability = 100 - technical debt + const percentage = + 100 - + parseFloat( + parsedData.data.attributes.ratings[0].measure.value + ) + badgeData.text[1] = percentage.toFixed(0) + '%' + badgeData.colorscheme = colorScale( + [50, 80, 90, 95], + ['red', 'yellow', 'yellowgreen', 'green', 'brightgreen'] + )(percentage) + } else if (type === 'maintainability') { + const score = parsedData.data.attributes.ratings[0].letter + badgeData.text[1] = score + badgeData.colorscheme = letterScoreColor(score) + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + } + ) + }) + ) + } +} diff --git a/services/codecov/codecov.service.js b/services/codecov/codecov.service.js new file mode 100644 index 0000000000..ec3702eae5 --- /dev/null +++ b/services/codecov/codecov.service.js @@ -0,0 +1,54 @@ +'use strict' + +const queryString = require('query-string') +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { + coveragePercentage: coveragePercentageColor, +} = require('../../lib/color-formatters') + +module.exports = class Codecov extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/codecov\/c\/(?:token\/(\w+))?[+/]?([^/]+\/[^/]+\/[^/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const token = match[1] + const userRepo = match[2] // eg, `github/codecov/example-python`. + const branch = match[3] + const format = match[4] + let apiUrl + if (branch) { + apiUrl = `https://codecov.io/${userRepo}/branch/${branch}/graphs/badge.txt` + } else { + apiUrl = `https://codecov.io/${userRepo}/graphs/badge.txt` + } + if (token) { + apiUrl += '?' + queryString.stringify({ token }) + } + const badgeData = getBadgeData('coverage', data) + request(apiUrl, (err, res, body) => { + if (err != null) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + return + } + try { + // Body: range(0, 100) or "unknown" + const coverage = body.trim() + if (Number.isNaN(+coverage)) { + badgeData.text[1] = 'unknown' + sendBadge(format, badgeData) + return + } + badgeData.text[1] = coverage + '%' + badgeData.colorscheme = coveragePercentageColor(coverage) + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'malformed' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/codeship/codeship.service.js b/services/codeship/codeship.service.js new file mode 100644 index 0000000000..17a9cd725b --- /dev/null +++ b/services/codeship/codeship.service.js @@ -0,0 +1,75 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') + +module.exports = class Codeship extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/codeship\/([^/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const projectId = match[1] // eg, `ab123456-00c0-0123-42de-6f98765g4h32`. + const format = match[3] + const branch = match[2] + const options = { + method: 'GET', + uri: + 'https://codeship.com/projects/' + + projectId + + '/status' + + (branch != null ? '?branch=' + branch : ''), + } + const badgeData = getBadgeData('build', data) + request(options, (err, res) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + const statusMatch = res.headers['content-disposition'].match( + /filename="status_(.+)\./ + ) + if (!statusMatch) { + badgeData.text[1] = 'unknown' + sendBadge(format, badgeData) + return + } + + switch (statusMatch[1]) { + case 'success': + badgeData.text[1] = 'passing' + badgeData.colorscheme = 'brightgreen' + break + case 'projectnotfound': + badgeData.text[1] = 'not found' + break + case 'branchnotfound': + badgeData.text[1] = 'branch not found' + break + case 'testing': + case 'waiting': + case 'initiated': + badgeData.text[1] = 'pending' + break + case 'error': + case 'infrastructure_failure': + badgeData.text[1] = 'failing' + badgeData.colorscheme = 'red' + break + case 'stopped': + case 'ignored': + case 'blocked': + badgeData.text[1] = 'not built' + break + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/codetally/codetally.service.js b/services/codetally/codetally.service.js new file mode 100644 index 0000000000..e152b87a7b --- /dev/null +++ b/services/codetally/codetally.service.js @@ -0,0 +1,38 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') + +module.exports = class Codetally extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/codetally\/(.*)\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const owner = match[1] // eg, triggerman722. + const repo = match[2] // eg, colorstrap + const format = match[3] + const apiUrl = + 'http://www.codetally.com/formattedshield/' + owner + '/' + repo + const badgeData = getBadgeData('codetally', data) + request(apiUrl, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(buffer) + badgeData.text[1] = + ' ' + data.currency_sign + data.amount + ' ' + data.multiplier + badgeData.colorscheme = null + badgeData.colorB = '#2E8B57' + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/conda/conda.service.js b/services/conda/conda.service.js new file mode 100644 index 0000000000..0d70c838af --- /dev/null +++ b/services/conda/conda.service.js @@ -0,0 +1,90 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLabel: getLabel, +} = require('../../lib/badge-data') +const { metric, addv: versionText } = require('../../lib/text-formatters') +const { + downloadCount: downloadCountColor, + version: versionColor, +} = require('../../lib/color-formatters') + +// For Anaconda Cloud / conda package manager. +module.exports = class Conda extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/conda\/([dvp]n?)\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, + cache((queryData, match, sendBadge, request) => { + const mode = match[1] + const channel = match[2] + const pkgname = match[3] + const format = match[4] + const url = + 'https://api.anaconda.org/package/' + channel + '/' + pkgname + const labels = { + d: 'downloads', + p: 'platform', + v: channel, + } + const modes = { + // downloads - 'd' + d: function(data, badgeData) { + const downloads = data.files.reduce( + (total, file) => total + file.ndownloads, + 0 + ) + badgeData.text[1] = metric(downloads) + badgeData.colorscheme = downloadCountColor(downloads) + }, + // latest version 'v' + v: function(data, badgeData) { + const version = data.latest_version + badgeData.text[1] = versionText(version) + badgeData.colorscheme = versionColor(version) + }, + // platform 'p' + p: function(data, badgeData) { + const platforms = data.conda_platforms.join(' | ') + badgeData.text[1] = platforms + }, + } + const variants = { + // default use `conda|{channelname}` as label + '': function(queryData, badgeData) { + badgeData.text[0] = getLabel( + `conda|${badgeData.text[0]}`, + queryData + ) + }, + // skip `conda|` prefix + n: function(queryData, badgeData) {}, + } + + const update = modes[mode.charAt(0)] + const variant = variants[mode.charAt(1)] + + const badgeData = getBadgeData(labels[mode.charAt(0)], queryData) + request(url, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + variant(queryData, badgeData) + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(buffer) + update(data, badgeData) + variant(queryData, badgeData) + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + variant(queryData, badgeData) + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/cookbook/cookbook.service.js b/services/cookbook/cookbook.service.js new file mode 100644 index 0000000000..00376b817b --- /dev/null +++ b/services/cookbook/cookbook.service.js @@ -0,0 +1,43 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { addv: versionText } = require('../../lib/text-formatters') +const { version: versionColor } = require('../../lib/color-formatters') + +// For Chef cookbook. +module.exports = class Cookbook extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/cookbook\/v\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const cookbook = match[1] // eg, chef-sugar + const format = match[2] + const apiUrl = + 'https://supermarket.getchef.com/api/v1/cookbooks/' + + cookbook + + '/versions/latest' + const badgeData = getBadgeData('cookbook', data) + + request(apiUrl, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + + try { + const data = JSON.parse(buffer) + const version = data.version + badgeData.text[1] = versionText(version) + badgeData.colorscheme = versionColor(version) + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/coveralls/coveralls.service.js b/services/coveralls/coveralls.service.js new file mode 100644 index 0000000000..1351650c01 --- /dev/null +++ b/services/coveralls/coveralls.service.js @@ -0,0 +1,59 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { + coveragePercentage: coveragePercentageColor, +} = require('../../lib/color-formatters') + +module.exports = class Coveralls extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/coveralls\/(?:(bitbucket|github)\/)?([^/]+\/[^/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const repoService = match[1] ? match[1] : 'github' + const userRepo = match[2] // eg, `jekyll/jekyll`. + const branch = match[3] + const format = match[4] + const apiUrl = { + url: `https://coveralls.io/repos/${repoService}/${userRepo}/badge.svg`, + followRedirect: false, + method: 'HEAD', + } + if (branch) { + apiUrl.url += '?branch=' + branch + } + const badgeData = getBadgeData('coverage', data) + request(apiUrl, (err, res) => { + if (err != null) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + return + } + // We should get a 302. Look inside the Location header. + const buffer = res.headers.location + if (!buffer) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + return + } + try { + const score = buffer.split('_')[1].split('.')[0] + const percentage = parseInt(score) + if (Number.isNaN(percentage)) { + badgeData.text[1] = 'unknown' + sendBadge(format, badgeData) + return + } + badgeData.text[1] = score + '%' + badgeData.colorscheme = coveragePercentageColor(percentage) + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'malformed' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/coverity/coverity-on-demand.service.js b/services/coverity/coverity-on-demand.service.js new file mode 100644 index 0000000000..4a576cf296 --- /dev/null +++ b/services/coverity/coverity-on-demand.service.js @@ -0,0 +1,59 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') + +// For Coverity Code Advisor On Demand. +module.exports = class CoverityOnDemand extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/coverity\/ondemand\/(.+)\/(.+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const badgeType = match[1] // One of the strings "streams" or "jobs" + const badgeTypeId = match[2] // streamId or jobId + const format = match[3] + + const badgeData = getBadgeData('coverity', data) + if ( + (badgeType === 'jobs' && badgeTypeId === 'JOB') || + (badgeType === 'streams' && badgeTypeId === 'STREAM') + ) { + // Request is for a static demo badge + badgeData.text[1] = 'clean' + badgeData.colorscheme = 'green' + sendBadge(format, badgeData) + } else { + // + // Request is for a real badge; send request to Coverity On Demand API + // server to get the badge + // + // Example URLs for requests sent to Coverity On Demand are: + // + // https://api.ondemand.coverity.com/streams/44b25sjc9l3ntc2ngfi29tngro/badge + // https://api.ondemand.coverity.com/jobs/p4tmm8031t4i971r0im4s7lckk/badge + // + const url = + 'https://api.ondemand.coverity.com/' + + badgeType + + '/' + + badgeTypeId + + '/badge' + request(url, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(buffer) + sendBadge(format, data) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + } + }) + ) + } +} diff --git a/services/coverity/coverity-scan.service.js b/services/coverity/coverity-scan.service.js new file mode 100644 index 0000000000..55c24c5f1b --- /dev/null +++ b/services/coverity/coverity-scan.service.js @@ -0,0 +1,45 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') + +module.exports = class CoverityScan extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/coverity\/scan\/(.+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const projectId = match[1] // eg, `3997` + const format = match[2] + const url = + 'https://scan.coverity.com/projects/' + projectId + '/badge.json' + const badgeData = getBadgeData('coverity', data) + request(url, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(buffer) + badgeData.text[1] = data.message + + if (data.message === 'passed') { + badgeData.colorscheme = 'brightgreen' + badgeData.text[1] = 'passing' + } else if (/^passed .* new defects$/.test(data.message)) { + badgeData.colorscheme = 'yellow' + } else if (data.message === 'pending') { + badgeData.colorscheme = 'orange' + } else if (data.message === 'failed') { + badgeData.colorscheme = 'red' + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/cpan/cpan.service.js b/services/cpan/cpan.service.js new file mode 100644 index 0000000000..0a36c0c4bd --- /dev/null +++ b/services/cpan/cpan.service.js @@ -0,0 +1,45 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { addv: versionText } = require('../../lib/text-formatters') +const { version: versionColor } = require('../../lib/color-formatters') + +module.exports = class Cpan extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/cpan\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const info = match[1] // either `v` or `l` + const pkg = match[2] // eg, Config-Augeas + const format = match[3] + const badgeData = getBadgeData('cpan', data) + const url = 'https://fastapi.metacpan.org/v1/release/' + pkg + request(url, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(buffer) + + if (info === 'v') { + const version = data.version + badgeData.text[1] = versionText(version) + badgeData.colorscheme = versionColor(version) + } else if (info === 'l') { + const license = data.license[0] + badgeData.text[1] = license + badgeData.colorscheme = 'blue' + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/cran/cran.service.js b/services/cran/cran.service.js new file mode 100644 index 0000000000..609f7e0fef --- /dev/null +++ b/services/cran/cran.service.js @@ -0,0 +1,62 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLabel: getLabel, +} = require('../../lib/badge-data') +const { addv: versionText } = require('../../lib/text-formatters') +const { version: versionColor } = require('../../lib/color-formatters') + +// For CRAN/METACRAN. +module.exports = class Cran extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/cran\/([vl])\/([^/]+)\.(svg|png|gif|jpg|json)$/, + cache((queryParams, match, sendBadge, request) => { + const info = match[1] // either `v` or `l` + const pkg = match[2] // eg, devtools + const format = match[3] + const url = 'http://crandb.r-pkg.org/' + pkg + const badgeData = getBadgeData('cran', queryParams) + request(url, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + if (res.statusCode === 404) { + badgeData.text[1] = 'not found' + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(buffer) + + if (info === 'v') { + const version = data.Version + badgeData.text[1] = versionText(version) + badgeData.colorscheme = versionColor(version) + sendBadge(format, badgeData) + } else if (info === 'l') { + badgeData.text[0] = getLabel('license', queryParams) + const license = data.License + if (license) { + badgeData.text[1] = license + badgeData.colorscheme = 'blue' + } else { + badgeData.text[1] = 'unknown' + } + sendBadge(format, badgeData) + } else { + throw Error('Unreachable due to regex') + } + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/crates/crates.service.js b/services/crates/crates.service.js new file mode 100644 index 0000000000..4b4406b9b9 --- /dev/null +++ b/services/crates/crates.service.js @@ -0,0 +1,95 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { + downloadCount: downloadCountColor, + version: versionColor, +} = require('../../lib/color-formatters') +const { metric, addv: versionText } = require('../../lib/text-formatters') + +module.exports = class Crates extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/crates\/(d|v|dv|l)\/([A-Za-z0-9_-]+)(?:\/([0-9.]+))?\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const mode = match[1] // d - downloads (total or for version), v - (latest) version, dv - downloads (for latest version) + const crate = match[2] // crate name, e.g. rustc-serialize + let version = match[3] // crate version in semver format, optional, e.g. 0.1.2 + const format = match[4] + const modes = { + d: { + name: 'downloads', + version: true, + process: function(data, badgeData) { + const downloads = data.crate + ? data.crate.downloads + : data.version.downloads + version = data.version && data.version.num + badgeData.text[1] = + metric(downloads) + (version ? ' version ' + version : '') + badgeData.colorscheme = downloadCountColor(downloads) + }, + }, + dv: { + name: 'downloads', + version: true, + process: function(data, badgeData) { + const downloads = data.version + ? data.version.downloads + : data.versions[0].downloads + version = data.version && data.version.num + badgeData.text[1] = + metric(downloads) + + (version ? ' version ' + version : ' latest version') + badgeData.colorscheme = downloadCountColor(downloads) + }, + }, + v: { + name: 'crates.io', + version: true, + process: function(data, badgeData) { + version = data.version ? data.version.num : data.crate.max_version + badgeData.text[1] = versionText(version) + badgeData.colorscheme = versionColor(version) + }, + }, + l: { + name: 'license', + version: false, + process: function(data, badgeData) { + badgeData.text[1] = data.versions[0].license + badgeData.colorscheme = 'blue' + }, + }, + } + const behavior = modes[mode] + let apiUrl = 'https://crates.io/api/v1/crates/' + crate + if (version != null && behavior.version) { + apiUrl += '/' + version + } + + const badgeData = getBadgeData(behavior.name, data) + request( + apiUrl, + { headers: { Accept: 'application/json' } }, + (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(buffer) + behavior.process(data, badgeData) + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + } + ) + }) + ) + } +} diff --git a/services/ctan/ctan.service.js b/services/ctan/ctan.service.js new file mode 100644 index 0000000000..ded7d3f545 --- /dev/null +++ b/services/ctan/ctan.service.js @@ -0,0 +1,62 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLabel: getLabel, +} = require('../../lib/badge-data') +const { addv: versionText } = require('../../lib/text-formatters') +const { version: versionColor } = require('../../lib/color-formatters') + +module.exports = class Ctan extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/ctan\/([vl])\/([^/]+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const info = match[1] // either `v` or `l` + const pkg = match[2] // eg, tex + const format = match[3] + const url = 'http://www.ctan.org/json/pkg/' + pkg + const badgeData = getBadgeData('ctan', data) + request(url, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + if (res.statusCode === 404) { + badgeData.text[1] = 'not found' + sendBadge(format, badgeData) + return + } + try { + const parsedData = JSON.parse(buffer) + + if (info === 'v') { + const version = parsedData.version.number + badgeData.text[1] = versionText(version) + badgeData.colorscheme = versionColor(version) + sendBadge(format, badgeData) + } else if (info === 'l') { + badgeData.text[0] = getLabel('license', data) + const license = parsedData.license + if (Array.isArray(license) && license.length > 0) { + // API returns licenses inconsistently ordered, so fix the order. + badgeData.text[1] = license.sort().join(',') + badgeData.colorscheme = 'blue' + } else { + badgeData.text[1] = 'unknown' + } + sendBadge(format, badgeData) + } else { + throw Error('Unreachable due to regex') + } + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/david/david.service.js b/services/david/david.service.js new file mode 100644 index 0000000000..8ab229919e --- /dev/null +++ b/services/david/david.service.js @@ -0,0 +1,79 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') + +module.exports = class David extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/david\/(dev\/|optional\/|peer\/)?(.+?)\.(svg|png|gif|jpg|json)$/, + cache({ + queryParams: ['path'], + handler: function(data, match, sendBadge, request) { + let dev = match[1] + if (dev != null) { + dev = dev.slice(0, -1) + } // 'dev', 'optional' or 'peer'. + // eg, `expressjs/express`, `webcomponents/generator-element`. + const userRepo = match[2] + const format = match[3] + let options = + 'https://david-dm.org/' + + userRepo + + '/' + + (dev ? dev + '-' : '') + + 'info.json' + if (data.path) { + // path can be used to specify the package.json location, useful for monorepos + options += '?path=' + data.path + } + const badgeData = getBadgeData( + (dev ? dev + 'D' : 'd') + 'ependencies', + data + ) + request(options, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } else if (res.statusCode === 500) { + /* note: + david returns a 500 response for 'not found' + e.g: https://david-dm.org/foo/barbaz/info.json + not a 404 so we can't handle 'not found' cleanly + because this might also be some other error. + */ + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(buffer) + let status = data.status + if (status === 'insecure') { + badgeData.colorscheme = 'red' + status = 'insecure' + } else if (status === 'notsouptodate') { + badgeData.colorscheme = 'yellow' + status = 'up to date' + } else if (status === 'outofdate') { + badgeData.colorscheme = 'red' + status = 'out of date' + } else if (status === 'uptodate') { + badgeData.colorscheme = 'brightgreen' + status = 'up to date' + } else if (status === 'none') { + badgeData.colorscheme = 'brightgreen' + } + badgeData.text[1] = status + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }, + }) + ) + } +} diff --git a/services/dependabot/dependabot.service.js b/services/dependabot/dependabot.service.js new file mode 100644 index 0000000000..39b4a8ca69 --- /dev/null +++ b/services/dependabot/dependabot.service.js @@ -0,0 +1,47 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLogo: getLogo, +} = require('../../lib/badge-data') +const { checkErrorResponse } = require('../../lib/error-helper') + +module.exports = class DependabotSemverCompatibility extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/dependabot\/semver\/([^/]+)\/(.+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const packageManager = match[1] + const dependencyName = match[2] + const format = match[3] + const options = { + method: 'GET', + headers: { Accept: 'application/json' }, + uri: `https://api.dependabot.com/badges/compatibility_score?package-manager=${packageManager}&dependency-name=${dependencyName}&version-scheme=semver`, + } + const badgeData = getBadgeData('semver stability', data) + badgeData.links = [ + `https://dependabot.com/compatibility-score.html?package-manager=${packageManager}&dependency-name=${dependencyName}&version-scheme=semver`, + ] + badgeData.logo = getLogo('dependabot', data) + request(options, (err, res) => { + if (checkErrorResponse(badgeData, err, res)) { + sendBadge(format, badgeData) + return + } + try { + const dependabotData = JSON.parse(res['body']) + badgeData.text[1] = dependabotData.status + badgeData.colorscheme = dependabotData.colour + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + badgeData.colorscheme = 'red' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/depfu/depfu.service.js b/services/depfu/depfu.service.js new file mode 100644 index 0000000000..faaa87607f --- /dev/null +++ b/services/depfu/depfu.service.js @@ -0,0 +1,34 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') + +module.exports = class Depfu extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/depfu\/(.+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const userRepo = match[1] // eg, `jekyll/jekyll`. + const format = match[2] + const url = 'https://depfu.com/github/shields/' + userRepo + const badgeData = getBadgeData('dependencies', data) + request(url, (err, res) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(res['body']) + badgeData.text[1] = data['text'] + badgeData.colorscheme = data['colorscheme'] + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/discord/discord.service.js b/services/discord/discord.service.js new file mode 100644 index 0000000000..0e0e0c65dc --- /dev/null +++ b/services/discord/discord.service.js @@ -0,0 +1,49 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') + +module.exports = class Discord extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/discord\/([^/]+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const serverID = match[1] + const format = match[2] + const apiUrl = `https://discordapp.com/api/guilds/${serverID}/widget.json` + + request(apiUrl, (err, res, buffer) => { + const badgeData = getBadgeData('chat', data) + if (res && res.statusCode === 404) { + badgeData.text[1] = 'invalid server' + sendBadge(format, badgeData) + return + } + if (err != null || !res || res.statusCode !== 200) { + badgeData.text[1] = 'inaccessible' + if (res && res.headers['content-type'] === 'application/json') { + try { + const data = JSON.parse(buffer) + if (data && typeof data.message === 'string') { + badgeData.text[1] = data.message.toLowerCase() + } + } catch (e) {} + } + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(buffer) + const members = Array.isArray(data.members) ? data.members : [] + badgeData.text[1] = members.length + ' online' + badgeData.colorscheme = 'brightgreen' + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/discourse/discourse.service.js b/services/discourse/discourse.service.js new file mode 100644 index 0000000000..c03d2eac47 --- /dev/null +++ b/services/discourse/discourse.service.js @@ -0,0 +1,88 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { metric } = require('../../lib/text-formatters') + +module.exports = class Discourse extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/discourse\/(http(?:s)?)\/(.*)\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const scheme = match[1] // eg, https + const host = match[2] // eg, meta.discourse.org + const stat = match[3] // eg, user_count + const format = match[4] + const url = scheme + '://' + host + '/site/statistics.json' + + const options = { + method: 'GET', + uri: url, + headers: { + Accept: 'application/json', + }, + } + + const badgeData = getBadgeData('discourse', data) + request(options, (err, res) => { + if (err != null) { + if (res) { + console.error('' + res) + } + + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + + if (res.statusCode !== 200) { + badgeData.text[1] = 'inaccessible' + badgeData.colorscheme = 'red' + sendBadge(format, badgeData) + return + } + + badgeData.colorscheme = 'brightgreen' + + try { + const data = JSON.parse(res['body']) + let statCount + + switch (stat) { + case 'topics': + statCount = data.topic_count + badgeData.text[1] = metric(statCount) + ' topics' + break + case 'posts': + statCount = data.post_count + badgeData.text[1] = metric(statCount) + ' posts' + break + case 'users': + statCount = data.user_count + badgeData.text[1] = metric(statCount) + ' users' + break + case 'likes': + statCount = data.like_count + badgeData.text[1] = metric(statCount) + ' likes' + break + case 'status': + badgeData.text[1] = 'online' + break + default: + badgeData.text[1] = 'invalid' + badgeData.colorscheme = 'yellow' + break + } + + sendBadge(format, badgeData) + } catch (e) { + console.error('' + e.stack) + badgeData.colorscheme = 'yellow' + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/dockbit/dockbit.service.js b/services/dockbit/dockbit.service.js new file mode 100644 index 0000000000..0895236a60 --- /dev/null +++ b/services/dockbit/dockbit.service.js @@ -0,0 +1,57 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') + +module.exports = class Dockbit extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/dockbit\/([A-Za-z0-9-_]+)\/([A-Za-z0-9-_]+)\.(svg|png|gif|jpg|json)$/, + cache({ + queryParams: ['token'], + handler: (data, match, sendBadge, request) => { + const org = match[1] + const pipeline = match[2] + const format = match[3] + + const token = data.token + const badgeData = getBadgeData('deploy', data) + const apiUrl = `https://dockbit.com/${org}/${pipeline}/status/${token}` + + const dockbitStates = { + success: '#72BC37', + failure: '#F55C51', + error: '#F55C51', + working: '#FCBC41', + pending: '#CFD0D7', + rejected: '#CFD0D7', + } + + request(apiUrl, { json: true }, (err, res, data) => { + try { + if (res && (res.statusCode === 404 || data.state === null)) { + badgeData.text[1] = 'not found' + sendBadge(format, badgeData) + return + } + + if (!res || err !== null || res.statusCode !== 200) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + + badgeData.text[1] = data.state + badgeData.colorB = dockbitStates[data.state] + + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }, + }) + ) + } +} diff --git a/services/docker/docker.service.js b/services/docker/docker.service.js new file mode 100644 index 0000000000..2e1033e7c1 --- /dev/null +++ b/services/docker/docker.service.js @@ -0,0 +1,168 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + setBadgeColor, +} = require('../../lib/badge-data') +const { checkErrorResponse } = require('../../lib/error-helper') +const { metric } = require('../../lib/text-formatters') + +module.exports = class Docker extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + // Docker Hub stars integration. + camp.route( + /^\/docker\/stars\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + let user = match[1] // eg, mashape + const repo = match[2] // eg, kong + const format = match[3] + if (user === '_') { + user = 'library' + } + const path = user + '/' + repo + const url = + 'https://hub.docker.com/v2/repositories/' + path + '/stars/count/' + const badgeData = getBadgeData('docker stars', data) + request(url, (err, res, buffer) => { + if ( + checkErrorResponse(badgeData, err, res, { 404: 'repo not found' }) + ) { + sendBadge(format, badgeData) + return + } + try { + const stars = parseInt(buffer, 10) + if (Number.isNaN(stars)) { + throw Error('Unexpected response.') + } + badgeData.text[1] = metric(stars) + setBadgeColor(badgeData, data.colorB || '066da5') + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + + // Docker Hub pulls integration. + camp.route( + /^\/docker\/pulls\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + let user = match[1] // eg, mashape + const repo = match[2] // eg, kong + const format = match[3] + if (user === '_') { + user = 'library' + } + const path = user + '/' + repo + const url = 'https://hub.docker.com/v2/repositories/' + path + const badgeData = getBadgeData('docker pulls', data) + request(url, (err, res, buffer) => { + if ( + checkErrorResponse(badgeData, err, res, { 404: 'repo not found' }) + ) { + sendBadge(format, badgeData) + return + } + try { + const parseData = JSON.parse(buffer) + const pulls = parseData.pull_count + badgeData.text[1] = metric(pulls) + setBadgeColor(badgeData, data.colorB || '066da5') + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + + // Docker Hub automated integration, most recent build's status (passed, pending, failed) + camp.route( + /^\/docker\/build\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + let user = match[1] // eg, jrottenberg + const repo = match[2] // eg, ffmpeg + const format = match[3] + if (user === '_') { + user = 'library' + } + const path = user + '/' + repo + const url = + 'https://registry.hub.docker.com/v2/repositories/' + + path + + '/buildhistory' + const badgeData = getBadgeData('docker build', data) + request(url, (err, res, buffer) => { + if ( + checkErrorResponse(badgeData, err, res, { 404: 'repo not found' }) + ) { + sendBadge(format, badgeData) + return + } + try { + const parsedData = JSON.parse(buffer) + const mostRecentStatus = parsedData.results[0].status + if (mostRecentStatus === 10) { + badgeData.text[1] = 'passing' + badgeData.colorscheme = 'brightgreen' + } else if (mostRecentStatus < 0) { + badgeData.text[1] = 'failing' + badgeData.colorscheme = 'red' + } else { + badgeData.text[1] = 'building' + setBadgeColor(badgeData, data.colorB || '066da5') + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + + // Docker Hub automated integration. + camp.route( + /^\/docker\/automated\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + let user = match[1] // eg, jrottenberg + const repo = match[2] // eg, ffmpeg + const format = match[3] + if (user === '_') { + user = 'library' + } + const path = user + '/' + repo + const url = 'https://registry.hub.docker.com/v2/repositories/' + path + const badgeData = getBadgeData('docker build', data) + request(url, (err, res, buffer) => { + if ( + checkErrorResponse(badgeData, err, res, { 404: 'repo not found' }) + ) { + sendBadge(format, badgeData) + return + } + try { + const parsedData = JSON.parse(buffer) + const isAutomated = parsedData.is_automated + if (isAutomated) { + badgeData.text[1] = 'automated' + setBadgeColor(badgeData, data.colorB || '066da5') + } else { + badgeData.text[1] = 'manual' + badgeData.colorscheme = 'yellow' + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/dotnetstatus/dotnetstatus.service.js b/services/dotnetstatus/dotnetstatus.service.js new file mode 100644 index 0000000000..d1d6ab3f87 --- /dev/null +++ b/services/dotnetstatus/dotnetstatus.service.js @@ -0,0 +1,18 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { getDeprecatedBadge } = require('../../lib/deprecation-helpers') + +// dotnet-status integration - deprecated as of April 2018. +module.exports = class DotnetStatus extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/dotnetstatus\/(.+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const format = match[2] + const badgeData = getDeprecatedBadge('dotnet status', data) + sendBadge(format, badgeData) + }) + ) + } +} diff --git a/services/dub/dub-download.service.js b/services/dub/dub-download.service.js new file mode 100644 index 0000000000..f315551ccf --- /dev/null +++ b/services/dub/dub-download.service.js @@ -0,0 +1,72 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLabel: getLabel, +} = require('../../lib/badge-data') +const { checkErrorResponse } = require('../../lib/error-helper') +const { metric } = require('../../lib/text-formatters') +const { addv: versionText } = require('../../lib/text-formatters') +const { + downloadCount: downloadCountColor, +} = require('../../lib/color-formatters') + +module.exports = class DubDownload extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/dub\/(dd|dw|dm|dt)\/([^/]+)(?:\/([^/]+))?\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const info = match[1] // downloads (dd - daily, dw - weekly, dm - monthly, dt - total) + const pkg = match[2] // package name, e.g. vibe-d + const version = match[3] // version (1.2.3 or latest) + const format = match[4] + let apiUrl = 'https://code.dlang.org/api/packages/' + pkg + if (version) { + apiUrl += '/' + version + } + apiUrl += '/stats' + const badgeData = getBadgeData('dub', data) + request(apiUrl, (err, res, buffer) => { + if (checkErrorResponse(badgeData, err, res)) { + sendBadge(format, badgeData) + return + } + try { + const parsedData = JSON.parse(buffer) + if (info.charAt(0) === 'd') { + badgeData.text[0] = getLabel('downloads', data) + let downloads + switch (info.charAt(1)) { + case 'm': + downloads = parsedData.downloads.monthly + badgeData.text[1] = metric(downloads) + '/month' + break + case 'w': + downloads = parsedData.downloads.weekly + badgeData.text[1] = metric(downloads) + '/week' + break + case 'd': + downloads = parsedData.downloads.daily + badgeData.text[1] = metric(downloads) + '/day' + break + case 't': + downloads = parsedData.downloads.total + badgeData.text[1] = metric(downloads) + break + } + if (version) { + badgeData.text[1] += ' ' + versionText(version) + } + badgeData.colorscheme = downloadCountColor(downloads) + sendBadge(format, badgeData) + } + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/dub/dub-license-version.service.js b/services/dub/dub-license-version.service.js new file mode 100644 index 0000000000..568c2a3432 --- /dev/null +++ b/services/dub/dub-license-version.service.js @@ -0,0 +1,57 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLabel: getLabel, +} = require('../../lib/badge-data') +const { checkErrorResponse } = require('../../lib/error-helper') +const { addv: versionText } = require('../../lib/text-formatters') +const { version: versionColor } = require('../../lib/color-formatters') + +module.exports = class DubLicenseVersion extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/dub\/(v|l)\/([^/]+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const info = match[1] // (v - version, l - license) + const pkg = match[2] // package name, e.g. vibe-d + const format = match[3] + let apiUrl = 'https://code.dlang.org/api/packages/' + pkg + if (info === 'v') { + apiUrl += '/latest' + } else if (info === 'l') { + apiUrl += '/latest/info' + } + const badgeData = getBadgeData('dub', data) + request(apiUrl, (err, res, buffer) => { + if (checkErrorResponse(badgeData, err, res)) { + sendBadge(format, badgeData) + return + } + try { + const parsedData = JSON.parse(buffer) + if (info === 'v') { + badgeData.text[1] = versionText(parsedData) + badgeData.colorscheme = versionColor(parsedData) + sendBadge(format, badgeData) + } else if (info === 'l') { + const license = parsedData.info.license + badgeData.text[0] = getLabel('license', data) + if (license == null) { + badgeData.text[1] = 'Unknown' + } else { + badgeData.text[1] = license + badgeData.colorscheme = 'blue' + } + sendBadge(format, badgeData) + } + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/eclipse-marketplace/eclipse-marketplace.service.js b/services/eclipse-marketplace/eclipse-marketplace.service.js new file mode 100644 index 0000000000..2ef89754fe --- /dev/null +++ b/services/eclipse-marketplace/eclipse-marketplace.service.js @@ -0,0 +1,93 @@ +'use strict' + +const xml2js = require('xml2js') +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLabel: getLabel, +} = require('../../lib/badge-data') +const { + metric, + addv: versionText, + formatDate, +} = require('../../lib/text-formatters') +const { + age: ageColor, + downloadCount: downloadCountColor, + version: versionColor, +} = require('../../lib/color-formatters') + +module.exports = class EclipseMarketplace extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/eclipse-marketplace\/(dt|dm|v|favorites|last-update)\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const type = match[1] + const project = match[2] + const format = match[3] + const apiUrl = + 'https://marketplace.eclipse.org/content/' + project + '/api/p' + const badgeData = getBadgeData('eclipse marketplace', data) + request(apiUrl, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + xml2js.parseString(buffer.toString(), (parseErr, parsedData) => { + if (parseErr != null) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + return + } + try { + const projectNode = parsedData.marketplace.node[0] + switch (type) { + case 'dt': { + badgeData.text[0] = getLabel('downloads', data) + const downloads = parseInt(projectNode.installstotal[0]) + badgeData.text[1] = metric(downloads) + badgeData.colorscheme = downloadCountColor(downloads) + break + } + case 'dm': { + badgeData.text[0] = getLabel('downloads', data) + const monthlydownloads = parseInt( + projectNode.installsrecent[0] + ) + badgeData.text[1] = metric(monthlydownloads) + '/month' + badgeData.colorscheme = downloadCountColor(monthlydownloads) + break + } + case 'v': { + badgeData.text[1] = versionText(projectNode.version[0]) + badgeData.colorscheme = versionColor(projectNode.version[0]) + break + } + case 'favorites': { + badgeData.text[0] = getLabel('favorites', data) + badgeData.text[1] = parseInt(projectNode.favorited[0]) + badgeData.colorscheme = 'brightgreen' + break + } + case 'last-update': { + const date = 1000 * parseInt(projectNode.changed[0]) + badgeData.text[0] = getLabel('updated', data) + badgeData.text[1] = formatDate(date) + badgeData.colorscheme = ageColor(Date.parse(date)) + break + } + default: + throw Error('Unreachable due to regex') + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + }) + ) + } +} diff --git a/services/elm-package/elm-package.service.js b/services/elm-package/elm-package.service.js new file mode 100644 index 0000000000..2f64a35bd4 --- /dev/null +++ b/services/elm-package/elm-package.service.js @@ -0,0 +1,38 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { addv: versionText } = require('../../lib/text-formatters') +const { version: versionColor } = require('../../lib/color-formatters') + +module.exports = class ElmPackage extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/elm-package\/v\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const urlPrefix = 'http://package.elm-lang.org/packages' + const [, user, repo, format] = match + const apiUrl = `${urlPrefix}/${user}/${repo}/latest/elm-package.json` + const badgeData = getBadgeData('elm-package', data) + request(apiUrl, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(buffer) + if (data && typeof data.version === 'string') { + badgeData.text[1] = versionText(data.version) + badgeData.colorscheme = versionColor(data.version) + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/gemnasium/gemnasium.service.js b/services/gemnasium/gemnasium.service.js new file mode 100644 index 0000000000..b5f9b22fb8 --- /dev/null +++ b/services/gemnasium/gemnasium.service.js @@ -0,0 +1,64 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLabel: getLabel, +} = require('../../lib/badge-data') +const { + isDeprecated, + getDeprecatedBadge, +} = require('../../lib/deprecation-helpers') + +const serverStartTime = new Date(new Date().toGMTString()) + +module.exports = class Gemnasium extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/gemnasium\/(.+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const userRepo = match[1] // eg, `jekyll/jekyll`. + const format = match[2] + + if (isDeprecated('gemnasium', serverStartTime)) { + const badgeData = getDeprecatedBadge('gemnasium', data) + sendBadge(format, badgeData) + return + } + + const options = 'https://gemnasium.com/' + userRepo + '.svg' + const badgeData = getBadgeData('dependencies', data) + request(options, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + const nameMatch = buffer.match(/(devD|d)ependencies/)[0] + const statusMatch = buffer.match(/'14'>(.+)<\/text>\s*<\/g>/)[1] + badgeData.text[0] = getLabel(nameMatch, data) + badgeData.text[1] = statusMatch + if (statusMatch === 'up-to-date') { + badgeData.text[1] = 'up to date' + badgeData.colorscheme = 'brightgreen' + } else if (statusMatch === 'out-of-date') { + badgeData.text[1] = 'out of date' + badgeData.colorscheme = 'yellow' + } else if (statusMatch === 'update!') { + badgeData.colorscheme = 'red' + } else if (statusMatch === 'none') { + badgeData.colorscheme = 'brightgreen' + } else { + badgeData.text[1] = 'undefined' + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/github/github-commit-activity.service.js b/services/github/github-commit-activity.service.js new file mode 100644 index 0000000000..df0dcda677 --- /dev/null +++ b/services/github/github-commit-activity.service.js @@ -0,0 +1,69 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLogo: getLogo, +} = require('../../lib/badge-data') +const { metric } = require('../../lib/text-formatters') +const { + checkErrorResponse: githubCheckErrorResponse, +} = require('./github-helpers') + +module.exports = class GithubCommitActivity extends LegacyService { + static registerLegacyRouteHandler({ camp, cache, githubApiProvider }) { + camp.route( + /^\/github\/commit-activity\/(y|4w|w)\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const interval = match[1] + const user = match[2] + const repo = match[3] + const format = match[4] + const apiUrl = `/repos/${user}/${repo}/stats/commit_activity` + const badgeData = getBadgeData('commit activity', data) + if (badgeData.template === 'social') { + badgeData.logo = getLogo('github', data) + badgeData.links = [`https://github.com/${user}/${repo}`] + } + githubApiProvider.request(request, apiUrl, {}, (err, res, buffer) => { + if (githubCheckErrorResponse(badgeData, err, res)) { + sendBadge(format, badgeData) + return + } + try { + const parsedData = JSON.parse(buffer) + let value + let intervalLabel + switch (interval) { + case 'y': + value = parsedData.reduce( + (sum, weekInfo) => sum + weekInfo.total, + 0 + ) + intervalLabel = '/year' + break + case '4w': + value = parsedData + .slice(-4) + .reduce((sum, weekInfo) => sum + weekInfo.total, 0) + intervalLabel = '/4 weeks' + break + case 'w': + value = parsedData.slice(-2)[0].total + intervalLabel = '/week' + break + default: + throw Error('Unhandled case') + } + badgeData.text[1] = `${metric(value)}${intervalLabel}` + badgeData.colorscheme = 'blue' + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/github/github-commit-status.service.js b/services/github/github-commit-status.service.js new file mode 100644 index 0000000000..7565196495 --- /dev/null +++ b/services/github/github-commit-status.service.js @@ -0,0 +1,66 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { + checkErrorResponse: githubCheckErrorResponse, +} = require('./github-helpers') + +module.exports = class GithubCommitStatus extends LegacyService { + static registerLegacyRouteHandler({ camp, cache, githubApiProvider }) { + camp.route( + /^\/github\/commit-status\/([^/]+)\/([^/]+)\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const [, user, repo, branch, commit, format] = match + const apiUrl = `/repos/${user}/${repo}/compare/${branch}...${commit}` + const badgeData = getBadgeData('commit status', data) + githubApiProvider.request(request, apiUrl, {}, (err, res, buffer) => { + if ( + githubCheckErrorResponse( + badgeData, + err, + res, + 'commit or branch not found' + ) + ) { + if (res && res.statusCode === 404) { + try { + if ( + JSON.parse(buffer).message.startsWith( + 'No common ancestor between' + ) + ) { + badgeData.text[1] = 'no common ancestor' + badgeData.colorscheme = 'lightgrey' + } + } catch (e) { + badgeData.text[1] = 'invalid' + badgeData.colorscheme = 'lightgrey' + } + } + sendBadge(format, badgeData) + return + } + try { + const parsedData = JSON.parse(buffer) + const isInBranch = + parsedData.status === 'identical' || + parsedData.status === 'behind' + if (isInBranch) { + badgeData.text[1] = `in ${branch}` + badgeData.colorscheme = 'brightgreen' + } else { + // status: ahead or diverged + badgeData.text[1] = `not in ${branch}` + badgeData.colorscheme = 'yellow' + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/github/github-commits-since.service.js b/services/github/github-commits-since.service.js new file mode 100644 index 0000000000..ee05d10482 --- /dev/null +++ b/services/github/github-commits-since.service.js @@ -0,0 +1,68 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLabel: getLabel, + makeLogo: getLogo, +} = require('../../lib/badge-data') + +module.exports = class GithubCommitsSince extends LegacyService { + static registerLegacyRouteHandler({ camp, cache, githubApiProvider }) { + camp.route( + /^\/github\/commits-since\/([^/]+)\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const user = match[1] // eg, SubtitleEdit + const repo = match[2] // eg, subtitleedit + const version = match[3] // eg, 3.4.7 or latest + const format = match[4] + const badgeData = getBadgeData('commits since ' + version, data) + + function setCommitsSinceBadge(user, repo, version) { + const apiUrl = `/repos/${user}/${repo}/compare/${version}...master` + if (badgeData.template === 'social') { + badgeData.logo = getLogo('github', data) + } + githubApiProvider.request(request, apiUrl, {}, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + + try { + const result = JSON.parse(buffer) + badgeData.text[1] = result.ahead_by + badgeData.colorscheme = 'blue' + badgeData.text[0] = getLabel('commits since ' + version, data) + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + } + + if (version === 'latest') { + const url = `/repos/${user}/${repo}/releases/latest` + githubApiProvider.request(request, url, {}, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(buffer) + setCommitsSinceBadge(user, repo, data.tag_name) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + } else { + setCommitsSinceBadge(user, repo, version) + } + }) + ) + } +} diff --git a/services/github/github-contributors.service.js b/services/github/github-contributors.service.js new file mode 100644 index 0000000000..4fb9687b6b --- /dev/null +++ b/services/github/github-contributors.service.js @@ -0,0 +1,56 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLogo: getLogo, +} = require('../../lib/badge-data') +const { metric } = require('../../lib/text-formatters') +const { + checkErrorResponse: githubCheckErrorResponse, +} = require('./github-helpers') + +module.exports = class GithubContributors extends LegacyService { + static registerLegacyRouteHandler({ camp, cache, githubApiProvider }) { + camp.route( + /^\/github\/contributors(-anon)?\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const isAnon = match[1] + const user = match[2] + const repo = match[3] + const format = match[4] + const apiUrl = `/repos/${user}/${repo}/contributors?page=1&per_page=1&anon=${!!isAnon}` + const badgeData = getBadgeData('contributors', data) + if (badgeData.template === 'social') { + badgeData.logo = getLogo('github', data) + } + githubApiProvider.request(request, apiUrl, {}, (err, res, buffer) => { + if (githubCheckErrorResponse(badgeData, err, res)) { + sendBadge(format, badgeData) + return + } + try { + let contributors + + if ( + res.headers['link'] && + res.headers['link'].indexOf('rel="last"') !== -1 + ) { + contributors = res.headers['link'].match( + /[?&]page=(\d+)[^>]+>; rel="last"/ + )[1] + } else { + contributors = JSON.parse(buffer).length + } + + badgeData.text[1] = metric(+contributors) + badgeData.colorscheme = 'blue' + } catch (e) { + badgeData.text[1] = 'inaccessible' + } + sendBadge(format, badgeData) + }) + }) + ) + } +} diff --git a/services/github/github-downloads.service.js b/services/github/github-downloads.service.js new file mode 100644 index 0000000000..213ea50d2d --- /dev/null +++ b/services/github/github-downloads.service.js @@ -0,0 +1,116 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLogo: getLogo, +} = require('../../lib/badge-data') +const { metric } = require('../../lib/text-formatters') +const { + checkErrorResponse: githubCheckErrorResponse, +} = require('./github-helpers') + +module.exports = class GithubDownloads extends LegacyService { + static registerLegacyRouteHandler({ camp, cache, githubApiProvider }) { + camp.route( + /^\/github\/(downloads|downloads-pre)\/([^/]+)\/([^/]+)(\/.+)?\/([^/]+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const type = match[1] // downloads or downloads-pre + const user = match[2] // eg, qubyte/rubidium + const repo = match[3] + + let tag = match[4] // eg, v0.190.0, latest, null if querying all releases + const assetName = match[5].toLowerCase() // eg. total, atom-amd64.deb, atom.x86_64.rpm + const format = match[6] + + if (tag) { + tag = tag.slice(1) + } + + let total = true + if (tag) { + total = false + } + + let apiUrl = `/repos/${user}/${repo}/releases` + if (!total) { + const releasePath = + tag === 'latest' + ? type === 'downloads' + ? 'latest' + : '' + : 'tags/' + tag + if (releasePath) { + apiUrl = apiUrl + '/' + releasePath + } + } + const badgeData = getBadgeData('downloads', data) + if (badgeData.template === 'social') { + badgeData.logo = getLogo('github', data) + } + githubApiProvider.request(request, apiUrl, {}, (err, res, buffer) => { + if ( + githubCheckErrorResponse( + badgeData, + err, + res, + 'repo or release not found' + ) + ) { + sendBadge(format, badgeData) + return + } + try { + let data = JSON.parse(buffer) + if (type === 'downloads-pre' && tag === 'latest') { + data = data[0] + } + let downloads = 0 + + const labelWords = [] + if (total) { + data.forEach(tagData => { + tagData.assets.forEach(asset => { + if ( + assetName === 'total' || + assetName === asset.name.toLowerCase() + ) { + downloads += asset.download_count + } + }) + }) + + labelWords.push('total') + if (assetName !== 'total') { + labelWords.push(`[${assetName}]`) + } + } else { + data.assets.forEach(asset => { + if ( + assetName === 'total' || + assetName === asset.name.toLowerCase() + ) { + downloads += asset.download_count + } + }) + + if (tag !== 'latest') { + labelWords.push(tag) + } + if (assetName !== 'total') { + labelWords.push(`[${assetName}]`) + } + } + labelWords.unshift(metric(downloads)) + badgeData.text[1] = labelWords.join(' ') + badgeData.colorscheme = 'brightgreen' + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'none' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/github/github-followers.service.js b/services/github/github-followers.service.js new file mode 100644 index 0000000000..7703a3f331 --- /dev/null +++ b/services/github/github-followers.service.js @@ -0,0 +1,42 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLogo: getLogo, +} = require('../../lib/badge-data') +const { + checkErrorResponse: githubCheckErrorResponse, +} = require('./github-helpers') + +module.exports = class GithubFollowers extends LegacyService { + static registerLegacyRouteHandler({ camp, cache, githubApiProvider }) { + camp.route( + /^\/github\/followers\/([^/]+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const user = match[1] // eg, qubyte + const format = match[2] + const apiUrl = `/users/${user}` + const badgeData = getBadgeData('followers', data) + if (badgeData.template === 'social') { + badgeData.logo = getLogo('github', data) + } + githubApiProvider.request(request, apiUrl, {}, (err, res, buffer) => { + if (githubCheckErrorResponse(badgeData, err, res, 'user not found')) { + sendBadge(format, badgeData) + return + } + try { + badgeData.text[1] = JSON.parse(buffer).followers + badgeData.colorscheme = null + badgeData.colorB = '#4183C4' + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/github/github-forks.service.js b/services/github/github-forks.service.js new file mode 100644 index 0000000000..3359779872 --- /dev/null +++ b/services/github/github-forks.service.js @@ -0,0 +1,49 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLogo: getLogo, +} = require('../../lib/badge-data') +const { + checkErrorResponse: githubCheckErrorResponse, +} = require('./github-helpers') + +module.exports = class GithubForks extends LegacyService { + static registerLegacyRouteHandler({ camp, cache, githubApiProvider }) { + camp.route( + /^\/github\/forks\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const user = match[1] // eg, qubyte/rubidium + const repo = match[2] + const format = match[3] + const apiUrl = `/repos/${user}/${repo}` + const badgeData = getBadgeData('forks', data) + if (badgeData.template === 'social') { + badgeData.logo = getLogo('github', data) + badgeData.links = [ + 'https://github.com/' + user + '/' + repo + '/fork', + 'https://github.com/' + user + '/' + repo + '/network', + ] + } + githubApiProvider.request(request, apiUrl, {}, (err, res, buffer) => { + if (githubCheckErrorResponse(badgeData, err, res)) { + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(buffer) + const forks = data.forks_count + badgeData.text[1] = forks + badgeData.colorscheme = null + badgeData.colorB = '#4183C4' + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/lib/github-helpers.js b/services/github/github-helpers.js similarity index 88% rename from lib/github-helpers.js rename to services/github/github-helpers.js index a698a2d68c..f7dd512cfd 100644 --- a/lib/github-helpers.js +++ b/services/github/github-helpers.js @@ -1,9 +1,9 @@ 'use strict' -const { colorScale } = require('./color-formatters') +const { colorScale } = require('../../lib/color-formatters') const { checkErrorResponse: standardCheckErrorResponse, -} = require('./error-helper') +} = require('../../lib/error-helper') function stateColor(s) { return { open: '2cbe4e', closed: 'cb2431', merged: '6f42c1' }[s] diff --git a/lib/github-helpers.spec.js b/services/github/github-helpers.spec.js similarity index 100% rename from lib/github-helpers.spec.js rename to services/github/github-helpers.spec.js diff --git a/services/github/github-issue-detail.service.js b/services/github/github-issue-detail.service.js new file mode 100644 index 0000000000..9dcf6399cc --- /dev/null +++ b/services/github/github-issue-detail.service.js @@ -0,0 +1,116 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { + makeColorB, + makeLabel: getLabel, + makeBadgeData: getBadgeData, + makeLogo: getLogo, +} = require('../../lib/badge-data') +const { formatDate } = require('../../lib/text-formatters') +const { age: ageColor } = require('../../lib/color-formatters') +const { + stateColor: githubStateColor, + commentsColor: githubCommentsColor, + checkErrorResponse: githubCheckErrorResponse, +} = require('./github-helpers') + +module.exports = class GithubIssueDetail extends LegacyService { + static registerLegacyRouteHandler({ camp, cache, githubApiProvider }) { + camp.route( + /^\/github\/(?:issues|pulls)\/detail\/(s|title|u|label|comments|age|last-update)\/([^/]+)\/([^/]+)\/(\d+)\.(svg|png|gif|jpg|json)$/, + cache((queryParams, match, sendBadge, request) => { + const [, which, owner, repo, number, format] = match + const uri = `/repos/${owner}/${repo}/issues/${number}` + const badgeData = getBadgeData( + `issue/pull request ${number}`, + queryParams + ) + if (badgeData.template === 'social') { + badgeData.logo = getLogo('github', queryParams) + } + githubApiProvider.request(request, uri, {}, (err, res, buffer) => { + if ( + githubCheckErrorResponse( + badgeData, + err, + res, + 'issue, pull request or repo not found' + ) + ) { + sendBadge(format, badgeData) + return + } + try { + const parsedData = JSON.parse(buffer) + const isPR = 'pull_request' in parsedData + const noun = isPR ? 'pull request' : 'issue' + badgeData.text[0] = getLabel( + `${noun} ${parsedData.number}`, + queryParams + ) + switch (which) { + case 's': { + const state = (badgeData.text[1] = parsedData.state) + badgeData.colorscheme = null + badgeData.colorB = makeColorB( + githubStateColor(state), + queryParams + ) + break + } + case 'title': + badgeData.text[1] = parsedData.title + break + case 'u': + badgeData.text[0] = getLabel('author', queryParams) + badgeData.text[1] = parsedData.user.login + break + case 'label': + badgeData.text[0] = getLabel('label', queryParams) + badgeData.text[1] = parsedData.labels + .map(i => i.name) + .join(' | ') + if (parsedData.labels.length === 1) { + badgeData.colorscheme = null + badgeData.colorB = makeColorB( + parsedData.labels[0].color, + queryParams + ) + } + break + case 'comments': { + badgeData.text[0] = getLabel('comments', queryParams) + const comments = (badgeData.text[1] = parsedData.comments) + badgeData.colorscheme = null + badgeData.colorB = makeColorB( + githubCommentsColor(comments), + queryParams + ) + break + } + case 'age': + case 'last-update': { + const label = which === 'age' ? 'created' : 'updated' + const date = + which === 'age' + ? parsedData.created_at + : parsedData.updated_at + badgeData.text[0] = getLabel(label, queryParams) + badgeData.text[1] = formatDate(date) + badgeData.colorscheme = ageColor(Date.parse(date)) + break + } + default: + throw Error('Unreachable due to regex') + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/github/github-issues.service.js b/services/github/github-issues.service.js new file mode 100644 index 0000000000..62f247aae0 --- /dev/null +++ b/services/github/github-issues.service.js @@ -0,0 +1,76 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLogo: getLogo, +} = require('../../lib/badge-data') +const { metric } = require('../../lib/text-formatters') +const { + checkErrorResponse: githubCheckErrorResponse, +} = require('./github-helpers') + +module.exports = class GithubIssues extends LegacyService { + static registerLegacyRouteHandler({ camp, cache, githubApiProvider }) { + camp.route( + /^\/github\/issues(-pr)?(-closed)?(-raw)?\/(?!detail)([^/]+)\/([^/]+)\/?(.+)?\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const isPR = !!match[1] + const isClosed = !!match[2] + const isRaw = !!match[3] + const user = match[4] // eg, badges + const repo = match[5] // eg, shields + const ghLabel = match[6] // eg, website + const format = match[7] + const query = {} + const hasLabel = ghLabel !== undefined + + query.q = + 'repo:' + + user + + '/' + + repo + + (isPR ? ' is:pr' : ' is:issue') + + (isClosed ? ' is:closed' : ' is:open') + + (hasLabel ? ` label:"${ghLabel}"` : '') + + const classText = isClosed ? 'closed' : 'open' + const leftClassText = isRaw ? classText + ' ' : '' + const rightClassText = !isRaw ? ' ' + classText : '' + const isGhLabelMultiWord = hasLabel && ghLabel.includes(' ') + const labelText = hasLabel + ? (isGhLabelMultiWord ? `"${ghLabel}"` : ghLabel) + ' ' + : '' + const targetText = isPR ? 'pull requests' : 'issues' + const badgeData = getBadgeData( + leftClassText + labelText + targetText, + data + ) + if (badgeData.template === 'social') { + badgeData.logo = getLogo('github', data) + } + githubApiProvider.request( + request, + '/search/issues', + query, + (err, res, buffer) => { + if (githubCheckErrorResponse(badgeData, err, res)) { + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(buffer) + const issues = data.total_count + badgeData.text[1] = metric(issues) + rightClassText + badgeData.colorscheme = issues > 0 ? 'yellow' : 'brightgreen' + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + } + ) + }) + ) + } +} diff --git a/services/github/github-languages.service.js b/services/github/github-languages.service.js new file mode 100644 index 0000000000..2f1725cbd5 --- /dev/null +++ b/services/github/github-languages.service.js @@ -0,0 +1,84 @@ +'use strict' + +const prettyBytes = require('pretty-bytes') +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLogo: getLogo, + makeLabel: getLabel, +} = require('../../lib/badge-data') +const { + checkErrorResponse: githubCheckErrorResponse, +} = require('./github-helpers') + +module.exports = class GithubLanguages extends LegacyService { + static registerLegacyRouteHandler({ camp, cache, githubApiProvider }) { + camp.route( + /^\/github\/languages\/(top|count|code-size)\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const type = match[1] + const user = match[2] + const repo = match[3] + const format = match[4] + const apiUrl = `/repos/${user}/${repo}/languages` + const badgeData = getBadgeData('languages', data) + if (badgeData.template === 'social') { + badgeData.logo = getLogo('github', data) + } + githubApiProvider.request(request, apiUrl, {}, (err, res, buffer) => { + if (githubCheckErrorResponse(badgeData, err, res)) { + sendBadge(format, badgeData) + return + } + try { + const parsedData = JSON.parse(buffer) + let sumBytes = 0 + switch (type) { + case 'top': { + let topLanguage = 'language' + let maxBytes = 0 + for (const language of Object.keys(parsedData)) { + const bytes = parseInt(parsedData[language]) + if (bytes >= maxBytes) { + maxBytes = bytes + topLanguage = language + } + sumBytes += bytes + } + badgeData.text[0] = getLabel(topLanguage, data) + if (sumBytes === 0) { + // eg, empty repo, only .md files, etc. + badgeData.text[1] = 'none' + badgeData.colorscheme = 'blue' + } else { + badgeData.text[1] = + ((maxBytes / sumBytes) * 100).toFixed(1) + '%' // eg, 9.1% + } + break + } + case 'count': + badgeData.text[0] = getLabel('languages', data) + badgeData.text[1] = Object.keys(parsedData).length + badgeData.colorscheme = 'blue' + break + case 'code-size': + for (const language of Object.keys(parsedData)) { + sumBytes += parseInt(parsedData[language]) + } + badgeData.text[0] = getLabel('code size', data) + badgeData.text[1] = prettyBytes(sumBytes) + badgeData.colorscheme = 'blue' + break + default: + throw Error('Unreachable due to regex') + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/github/github-last-commit.service.js b/services/github/github-last-commit.service.js new file mode 100644 index 0000000000..041029116d --- /dev/null +++ b/services/github/github-last-commit.service.js @@ -0,0 +1,51 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLogo: getLogo, +} = require('../../lib/badge-data') +const { formatDate } = require('../../lib/text-formatters') +const { age: ageColor } = require('../../lib/color-formatters') +const { + checkErrorResponse: githubCheckErrorResponse, +} = require('./github-helpers') + +module.exports = class GithubLastCommit extends LegacyService { + static registerLegacyRouteHandler({ camp, cache, githubApiProvider }) { + camp.route( + /^\/github\/last-commit\/([^/]+)\/([^/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const user = match[1] // eg, mashape + const repo = match[2] // eg, apistatus + const branch = match[3] + const format = match[4] + let apiUrl = `/repos/${user}/${repo}/commits` + if (branch) { + apiUrl += `?sha=${branch}` + } + const badgeData = getBadgeData('last commit', data) + if (badgeData.template === 'social') { + badgeData.logo = getLogo('github', data) + badgeData.links = [`https://github.com/${user}/${repo}`] + } + githubApiProvider.request(request, apiUrl, {}, (err, res, buffer) => { + if (githubCheckErrorResponse(badgeData, err, res)) { + sendBadge(format, badgeData) + return + } + try { + const parsedData = JSON.parse(buffer) + const commitDate = parsedData[0].commit.author.date + badgeData.text[1] = formatDate(commitDate) + badgeData.colorscheme = ageColor(Date.parse(commitDate)) + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/github/github-license.service.js b/services/github/github-license.service.js new file mode 100644 index 0000000000..1900af1a1f --- /dev/null +++ b/services/github/github-license.service.js @@ -0,0 +1,57 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLogo: getLogo, + setBadgeColor, +} = require('../../lib/badge-data') +const { licenseToColor } = require('../../lib/licenses') +const { + checkErrorResponse: githubCheckErrorResponse, +} = require('./github-helpers') + +module.exports = class GithubLicense extends LegacyService { + static registerLegacyRouteHandler({ camp, cache, githubApiProvider }) { + // GitHub license integration. + camp.route( + /^\/github\/license\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const user = match[1] // eg, mashape + const repo = match[2] // eg, apistatus + const format = match[3] + const apiUrl = `/repos/${user}/${repo}` + const badgeData = getBadgeData('license', data) + if (badgeData.template === 'social') { + badgeData.logo = getLogo('github', data) + } + githubApiProvider.request(request, apiUrl, {}, (err, res, buffer) => { + if ( + githubCheckErrorResponse(badgeData, err, res, 'repo not found', { + 403: 'access denied', + }) + ) { + sendBadge(format, badgeData) + return + } + try { + const body = JSON.parse(buffer) + const license = body.license + if (license != null) { + badgeData.text[1] = license.spdx_id || 'unknown' + setBadgeColor(badgeData, licenseToColor(license.spdx_id)) + sendBadge(format, badgeData) + } else { + badgeData.text[1] = 'missing' + badgeData.colorscheme = 'red' + sendBadge(format, badgeData) + } + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/github/github-manifest-version.service.js b/services/github/github-manifest-version.service.js new file mode 100644 index 0000000000..1f7ea17ffd --- /dev/null +++ b/services/github/github-manifest-version.service.js @@ -0,0 +1,79 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLabel: getLabel, +} = require('../../lib/badge-data') +const { addv: versionText } = require('../../lib/text-formatters') +const { version: versionColor } = require('../../lib/color-formatters') +const { + checkErrorResponse: githubCheckErrorResponse, +} = require('./github-helpers') + +// For GitHub package and manifest version. +module.exports = class GithubManifestVersion extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/github\/(package|manifest)-json\/([^/]+)\/([^/]+)\/([^/]+)\/?([^/]+)?\.(svg|png|gif|jpg|json)$/, + cache((queryData, match, sendBadge, request) => { + const type = match[1] + let info = match[2] + const user = match[3] + const repo = match[4] + const branch = match[5] || 'master' + const format = match[6] + const apiUrl = + 'https://raw.githubusercontent.com/' + + user + + '/' + + repo + + '/' + + branch + + '/' + + type + + '.json' + const badgeData = getBadgeData(type, queryData) + request(apiUrl, (err, res, buffer) => { + if (githubCheckErrorResponse(badgeData, err, res)) { + sendBadge(format, badgeData) + return + } + try { + const jsonData = JSON.parse(buffer) + switch (info) { + case 'v': + case 'version': { + const version = jsonData.version + badgeData.text[1] = versionText(version) + badgeData.colorscheme = versionColor(version) + break + } + case 'n': + info = 'name' + // falls through + default: { + const value = + typeof jsonData[info] !== 'undefined' && + typeof jsonData[info] !== 'object' + ? jsonData[info] + : Array.isArray(jsonData[info]) + ? jsonData[info].join(', ') + : 'invalid data' + badgeData.text[0] = getLabel(`${type} ${info}`, queryData) + badgeData.text[1] = value + badgeData.colorscheme = + value !== 'invalid data' ? 'blue' : 'lightgrey' + break + } + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid data' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/github/github-pull-request-status.service.js b/services/github/github-pull-request-status.service.js new file mode 100644 index 0000000000..b15d7528c6 --- /dev/null +++ b/services/github/github-pull-request-status.service.js @@ -0,0 +1,85 @@ +'use strict' + +const countBy = require('lodash.countby') +const LegacyService = require('../legacy-service') +const { + makeColorB, + makeBadgeData: getBadgeData, + makeLogo: getLogo, +} = require('../../lib/badge-data') +const { + checkStateColor: githubCheckStateColor, + checkErrorResponse: githubCheckErrorResponse, +} = require('./github-helpers') + +module.exports = class GithubPullRequestStatus extends LegacyService { + static registerLegacyRouteHandler({ camp, cache, githubApiProvider }) { + camp.route( + /^\/github\/status\/(s|contexts)\/pulls\/([^/]+)\/([^/]+)\/(\d+)\.(svg|png|gif|jpg|json)$/, + cache((queryParams, match, sendBadge, request) => { + const [, which, owner, repo, number, format] = match + const issueUri = `/repos/${owner}/${repo}/pulls/${number}` + const badgeData = getBadgeData('checks', queryParams) + if (badgeData.template === 'social') { + badgeData.logo = getLogo('github', queryParams) + } + githubApiProvider.request(request, issueUri, {}, (err, res, buffer) => { + if ( + githubCheckErrorResponse( + badgeData, + err, + res, + 'pull request or repo not found' + ) + ) { + sendBadge(format, badgeData) + return + } + try { + const parsedData = JSON.parse(buffer) + const ref = parsedData.head.sha + const statusUri = `/repos/${owner}/${repo}/commits/${ref}/status` + githubApiProvider.request( + request, + statusUri, + {}, + // eslint-disable-next-line handle-callback-err + (err, res, buffer) => { + try { + const parsedData = JSON.parse(buffer) + const state = (badgeData.text[1] = parsedData.state) + badgeData.colorscheme = null + badgeData.colorB = makeColorB( + githubCheckStateColor(state), + queryParams + ) + switch (which) { + case 's': + badgeData.text[1] = state + break + case 'contexts': { + const counts = countBy(parsedData.statuses, 'state') + badgeData.text[1] = Object.keys(counts) + .map(k => `${counts[k]} ${k}`) + .join(', ') + break + } + default: + throw Error('Unreachable due to regex') + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + } + ) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/github/github-release-date.service.js b/services/github/github-release-date.service.js new file mode 100644 index 0000000000..9956d636cd --- /dev/null +++ b/services/github/github-release-date.service.js @@ -0,0 +1,61 @@ +'use strict' + +const moment = require('moment') +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLogo: getLogo, +} = require('../../lib/badge-data') +const { formatDate } = require('../../lib/text-formatters') +const { age } = require('../../lib/color-formatters') + +// For Github Release & Pre-Release Date release-date-pre (?:\/(all))? +module.exports = class GithubReleaseDate extends LegacyService { + static registerLegacyRouteHandler({ camp, cache, githubApiProvider }) { + camp.route( + /^\/github\/(release-date|release-date-pre)\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const releaseType = match[1] // eg, release-date-pre / release-date + const user = match[2] // eg, microsoft + const repo = match[3] // eg, vscode + const format = match[4] + let apiUrl = `/repos/${user}/${repo}/releases` + if (releaseType === 'release-date') { + apiUrl += '/latest' + } + const badgeData = getBadgeData('release date', data) + if (badgeData.template === 'social') { + badgeData.logo = getLogo('github', data) + } + githubApiProvider.request(request, apiUrl, {}, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + + //github return 404 if repo not found or no release + if (res.statusCode === 404) { + badgeData.text[1] = 'no releases or repo not found' + sendBadge(format, badgeData) + return + } + + try { + let data = JSON.parse(buffer) + if (releaseType === 'release-date-pre') { + data = data[0] + } + const releaseDate = moment(data.created_at) + badgeData.text[1] = formatDate(releaseDate) + badgeData.colorscheme = age(releaseDate) + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/github/github-release.service.js b/services/github/github-release.service.js new file mode 100644 index 0000000000..9234366f41 --- /dev/null +++ b/services/github/github-release.service.js @@ -0,0 +1,52 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLogo: getLogo, +} = require('../../lib/badge-data') +const { addv: versionText } = require('../../lib/text-formatters') +const { + checkErrorResponse: githubCheckErrorResponse, +} = require('./github-helpers') + +module.exports = class GithubRelease extends LegacyService { + static registerLegacyRouteHandler({ camp, cache, githubApiProvider }) { + camp.route( + /^\/github\/release\/([^/]+\/[^/]+)(?:\/(all))?\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const userRepo = match[1] // eg, qubyte/rubidium + const allReleases = match[2] + const format = match[3] + let apiUrl = `/repos/${userRepo}/releases` + if (allReleases === undefined) { + apiUrl = apiUrl + '/latest' + } + const badgeData = getBadgeData('release', data) + if (badgeData.template === 'social') { + badgeData.logo = getLogo('github', data) + } + githubApiProvider.request(request, apiUrl, {}, (err, res, buffer) => { + if (githubCheckErrorResponse(badgeData, err, res)) { + sendBadge(format, badgeData) + return + } + try { + let data = JSON.parse(buffer) + if (allReleases === 'all') { + data = data[0] + } + const version = data.tag_name + const prerelease = data.prerelease + badgeData.text[1] = versionText(version) + badgeData.colorscheme = prerelease ? 'orange' : 'blue' + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'none' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/github/github-repo-size.service.js b/services/github/github-repo-size.service.js new file mode 100644 index 0000000000..64e8ef2001 --- /dev/null +++ b/services/github/github-repo-size.service.js @@ -0,0 +1,44 @@ +'use strict' + +const prettyBytes = require('pretty-bytes') +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLogo: getLogo, +} = require('../../lib/badge-data') +const { + checkErrorResponse: githubCheckErrorResponse, +} = require('./github-helpers') + +module.exports = class GithubRepoSize extends LegacyService { + static registerLegacyRouteHandler({ camp, cache, githubApiProvider }) { + camp.route( + /^\/github\/repo-size\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const user = match[1] + const repo = match[2] + const format = match[3] + const apiUrl = `/repos/${user}/${repo}` + const badgeData = getBadgeData('repo size', data) + if (badgeData.template === 'social') { + badgeData.logo = getLogo('github', data) + } + githubApiProvider.request(request, apiUrl, {}, (err, res, buffer) => { + if (githubCheckErrorResponse(badgeData, err, res)) { + sendBadge(format, badgeData) + return + } + try { + const parsedData = JSON.parse(buffer) + badgeData.text[1] = prettyBytes(parseInt(parsedData.size) * 1024) + badgeData.colorscheme = 'blue' + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/github/github-search.service.js b/services/github/github-search.service.js new file mode 100644 index 0000000000..f6b8d416ba --- /dev/null +++ b/services/github/github-search.service.js @@ -0,0 +1,45 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { metric } = require('../../lib/text-formatters') +const { + checkErrorResponse: githubCheckErrorResponse, +} = require('./github-helpers') + +module.exports = class GithubSearch extends LegacyService { + static registerLegacyRouteHandler({ camp, cache, githubApiProvider }) { + // GitHub search hit counter. + camp.route( + /^\/github\/search\/([^/]+)\/([^/]+)\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const user = match[1] + const repo = match[2] + const search = match[3] + const format = match[4] + const query = { q: search + ' repo:' + user + '/' + repo } + const badgeData = getBadgeData(search + ' counter', data) + githubApiProvider.request( + request, + '/search/code', + query, + (err, res, buffer) => { + if (githubCheckErrorResponse(badgeData, err, res)) { + sendBadge(format, badgeData) + return + } + try { + const body = JSON.parse(buffer) + badgeData.text[1] = metric(body.total_count) + badgeData.colorscheme = 'blue' + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + } + ) + }) + ) + } +} diff --git a/services/github/github-size.service.js b/services/github/github-size.service.js new file mode 100644 index 0000000000..3c73266547 --- /dev/null +++ b/services/github/github-size.service.js @@ -0,0 +1,59 @@ +'use strict' + +const prettyBytes = require('pretty-bytes') +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLogo: getLogo, +} = require('../../lib/badge-data') +const { + checkErrorResponse: githubCheckErrorResponse, +} = require('./github-helpers') + +module.exports = class GithubSize extends LegacyService { + static registerLegacyRouteHandler({ camp, cache, githubApiProvider }) { + camp.route( + /^\/github\/size\/([^/]+)\/([^/]+)\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const user = match[1] // eg, mashape + const repo = match[2] // eg, apistatus + const path = match[3] + const format = match[4] + const apiUrl = `/repos/${user}/${repo}/contents/${path}` + + const badgeData = getBadgeData('size', data) + if (badgeData.template === 'social') { + badgeData.logo = getLogo('github', data) + } + + githubApiProvider.request(request, apiUrl, {}, (err, res, buffer) => { + if ( + githubCheckErrorResponse( + badgeData, + err, + res, + 'repo or file not found' + ) + ) { + sendBadge(format, badgeData) + return + } + try { + const body = JSON.parse(buffer) + if (body && Number.isInteger(body.size)) { + badgeData.text[1] = prettyBytes(body.size) + badgeData.colorscheme = 'green' + sendBadge(format, badgeData) + } else { + badgeData.text[1] = 'not a regular file' + sendBadge(format, badgeData) + } + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/github/github-stars.service.js b/services/github/github-stars.service.js new file mode 100644 index 0000000000..1ea78c1d04 --- /dev/null +++ b/services/github/github-stars.service.js @@ -0,0 +1,48 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLogo: getLogo, +} = require('../../lib/badge-data') +const { metric } = require('../../lib/text-formatters') +const { + checkErrorResponse: githubCheckErrorResponse, +} = require('./github-helpers') + +module.exports = class GithubStars extends LegacyService { + static registerLegacyRouteHandler({ camp, cache, githubApiProvider }) { + camp.route( + /^\/github\/stars\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const user = match[1] // eg, qubyte/rubidium + const repo = match[2] + const format = match[3] + const apiUrl = `/repos/${user}/${repo}` + const badgeData = getBadgeData('stars', data) + if (badgeData.template === 'social') { + badgeData.logo = getLogo('github', data) + badgeData.links = [ + 'https://github.com/' + user + '/' + repo, + 'https://github.com/' + user + '/' + repo + '/stargazers', + ] + } + githubApiProvider.request(request, apiUrl, {}, (err, res, buffer) => { + if (githubCheckErrorResponse(badgeData, err, res)) { + sendBadge(format, badgeData) + return + } + try { + badgeData.text[1] = metric(JSON.parse(buffer).stargazers_count) + badgeData.colorscheme = null + badgeData.colorB = '#4183C4' + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/github/github-tag.service.js b/services/github/github-tag.service.js new file mode 100644 index 0000000000..cc0dd15a12 --- /dev/null +++ b/services/github/github-tag.service.js @@ -0,0 +1,49 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLogo: getLogo, +} = require('../../lib/badge-data') +const { addv: versionText } = require('../../lib/text-formatters') +const { version: versionColor } = require('../../lib/color-formatters') +const { latest: latestVersion } = require('../../lib/version') +const { + checkErrorResponse: githubCheckErrorResponse, +} = require('./github-helpers') + +module.exports = class GithubTag extends LegacyService { + static registerLegacyRouteHandler({ camp, cache, githubApiProvider }) { + camp.route( + /^\/github\/tag(-?pre)?\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const includePre = Boolean(match[1]) + const user = match[2] // eg, expressjs/express + const repo = match[3] + const format = match[4] + const apiUrl = `/repos/${user}/${repo}/tags` + const badgeData = getBadgeData('tag', data) + if (badgeData.template === 'social') { + badgeData.logo = getLogo('github', data) + } + githubApiProvider.request(request, apiUrl, {}, (err, res, buffer) => { + if (githubCheckErrorResponse(badgeData, err, res)) { + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(buffer) + const versions = data.map(e => e.name) + const tag = latestVersion(versions, { pre: includePre }) + badgeData.text[1] = versionText(tag) + badgeData.colorscheme = versionColor(tag) + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'none' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/github/github-watchers.service.js b/services/github/github-watchers.service.js new file mode 100644 index 0000000000..f2f832d48d --- /dev/null +++ b/services/github/github-watchers.service.js @@ -0,0 +1,47 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLogo: getLogo, +} = require('../../lib/badge-data') +const { + checkErrorResponse: githubCheckErrorResponse, +} = require('./github-helpers') + +module.exports = class GithubWatchers extends LegacyService { + static registerLegacyRouteHandler({ camp, cache, githubApiProvider }) { + camp.route( + /^\/github\/watchers\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const user = match[1] // eg, qubyte/rubidium + const repo = match[2] + const format = match[3] + const apiUrl = `/repos/${user}/${repo}` + const badgeData = getBadgeData('watchers', data) + if (badgeData.template === 'social') { + badgeData.logo = getLogo('github', data) + badgeData.links = [ + 'https://github.com/' + user + '/' + repo, + 'https://github.com/' + user + '/' + repo + '/watchers', + ] + } + githubApiProvider.request(request, apiUrl, {}, (err, res, buffer) => { + if (githubCheckErrorResponse(badgeData, err, res)) { + sendBadge(format, badgeData) + return + } + try { + badgeData.text[1] = JSON.parse(buffer).subscribers_count + badgeData.colorscheme = null + badgeData.colorB = '#4183C4' + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/gitter/gitter.service.js b/services/gitter/gitter.service.js new file mode 100644 index 0000000000..5d474dde06 --- /dev/null +++ b/services/gitter/gitter.service.js @@ -0,0 +1,21 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') + +module.exports = class Gitter extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/gitter\/room\/([^/]+\/[^/]+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + // match[1] is the repo, which is not used. + const format = match[2] + + const badgeData = getBadgeData('chat', data) + badgeData.text[1] = 'on gitter' + badgeData.colorscheme = 'brightgreen' + sendBadge(format, badgeData) + }) + ) + } +} diff --git a/services/gratipay/gratipay.service.js b/services/gratipay/gratipay.service.js new file mode 100644 index 0000000000..559bb02eee --- /dev/null +++ b/services/gratipay/gratipay.service.js @@ -0,0 +1,21 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { getDeprecatedBadge } = require('../../lib/deprecation-helpers') +const { makeLogo: getLogo } = require('../../lib/badge-data') + +module.exports = class Gratipay extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/(?:gittip|gratipay(\/user|\/team|\/project)?)\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((queryParams, match, sendBadge, request) => { + const format = match[3] + const badgeData = getDeprecatedBadge('gratipay', queryParams) + if (badgeData.template === 'social') { + badgeData.logo = getLogo('gratipay', queryParams) + } + sendBadge(format, badgeData) + }) + ) + } +} diff --git a/services/hackage/hackage-deps.service.js b/services/hackage/hackage-deps.service.js new file mode 100644 index 0000000000..83d996b2c0 --- /dev/null +++ b/services/hackage/hackage-deps.service.js @@ -0,0 +1,52 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { checkErrorResponse } = require('../../lib/error-helper') + +module.exports = class HackageDeps extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/hackage-deps\/v\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const repo = match[1] // eg, `lens`. + const format = match[2] + const reverseUrl = 'http://packdeps.haskellers.com/licenses/' + repo + const feedUrl = 'http://packdeps.haskellers.com/feed/' + repo + const badgeData = getBadgeData('dependencies', data) + + // first call /reverse to check if the package exists + // this will throw a 404 if it doesn't + request(reverseUrl, (err, res, buffer) => { + if (checkErrorResponse(badgeData, err, res)) { + sendBadge(format, badgeData) + return + } + + // if the package exists, then query /feed to check the dependencies + request(feedUrl, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + + try { + const outdatedStr = 'Outdated dependencies for ' + repo + ' ' + if (buffer.indexOf(outdatedStr) >= 0) { + badgeData.text[1] = 'outdated' + badgeData.colorscheme = 'orange' + } else { + badgeData.text[1] = 'up to date' + badgeData.colorscheme = 'brightgreen' + } + } catch (e) { + badgeData.text[1] = 'invalid' + } + sendBadge(format, badgeData) + }) + }) + }) + ) + } +} diff --git a/services/hackage/hackage-version.service.js b/services/hackage/hackage-version.service.js new file mode 100644 index 0000000000..3597549737 --- /dev/null +++ b/services/hackage/hackage-version.service.js @@ -0,0 +1,45 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { checkErrorResponse } = require('../../lib/error-helper') +const { addv: versionText } = require('../../lib/text-formatters') +const { version: versionColor } = require('../../lib/color-formatters') + +module.exports = class HackageVersion extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/hackage\/v\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const repo = match[1] // eg, `lens`. + const format = match[2] + const apiUrl = + 'https://hackage.haskell.org/package/' + repo + '/' + repo + '.cabal' + const badgeData = getBadgeData('hackage', data) + request(apiUrl, (err, res, buffer) => { + if (checkErrorResponse(badgeData, err, res)) { + sendBadge(format, badgeData) + return + } + + try { + const lines = buffer.split('\n') + const versionLines = lines.filter( + e => /^version:/i.test(e) === true + ) + // We don't have to check length of versionLines, because if we throw, + // we'll render the 'invalid' badge below, which is the correct thing + // to do. + const version = versionLines[0].split(/:/)[1].trim() + badgeData.text[1] = versionText(version) + badgeData.colorscheme = versionColor(version) + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/hexpm/hexpm.service.js b/services/hexpm/hexpm.service.js new file mode 100644 index 0000000000..c6d9175368 --- /dev/null +++ b/services/hexpm/hexpm.service.js @@ -0,0 +1,82 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLabel: getLabel, +} = require('../../lib/badge-data') +const { + metric, + addv: versionText, + maybePluralize, +} = require('../../lib/text-formatters') +const { + downloadCount: downloadCountColor, + version: versionColor, +} = require('../../lib/color-formatters') + +module.exports = class Hexpm extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/hexpm\/([^/]+)\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((queryParams, match, sendBadge, request) => { + const info = match[1] + const repo = match[2] // eg, `httpotion`. + const format = match[3] + const apiUrl = 'https://hex.pm/api/packages/' + repo + const badgeData = getBadgeData('hex', queryParams) + request(apiUrl, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(buffer) + if (info.charAt(0) === 'd') { + badgeData.text[0] = getLabel('downloads', queryParams) + let downloads + switch (info.charAt(1)) { + case 'w': + downloads = data.downloads.week + badgeData.text[1] = metric(downloads) + '/week' + break + case 'd': + downloads = data.downloads.day + badgeData.text[1] = metric(downloads) + '/day' + break + case 't': + downloads = data.downloads.all + badgeData.text[1] = metric(downloads) + break + } + badgeData.colorscheme = downloadCountColor(downloads) + sendBadge(format, badgeData) + } else if (info === 'v') { + const version = data.releases[0].version + badgeData.text[1] = versionText(version) + badgeData.colorscheme = versionColor(version) + sendBadge(format, badgeData) + } else if (info === 'l') { + const license = (data.meta.licenses || []).join(', ') + badgeData.text[0] = getLabel( + maybePluralize('license', data.meta.licenses), + queryParams + ) + if (license === '') { + badgeData.text[1] = 'Unknown' + } else { + badgeData.text[1] = license + badgeData.colorscheme = 'blue' + } + sendBadge(format, badgeData) + } + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/homebrew/homebrew.service.js b/services/homebrew/homebrew.service.js new file mode 100644 index 0000000000..b52cd52b09 --- /dev/null +++ b/services/homebrew/homebrew.service.js @@ -0,0 +1,44 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { checkErrorResponse } = require('../../lib/error-helper') +const { addv: versionText } = require('../../lib/text-formatters') +const { version: versionColor } = require('../../lib/color-formatters') + +module.exports = class Homebrew extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/homebrew\/v\/([^/]+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const pkg = match[1] // eg. cake + const format = match[2] + const apiUrl = 'https://formulae.brew.sh/api/formula/' + pkg + '.json' + + const badgeData = getBadgeData('homebrew', data) + request( + apiUrl, + { headers: { Accept: 'application/json' } }, + (err, res, buffer) => { + if (checkErrorResponse(badgeData, err, res)) { + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(buffer) + const version = data.versions.stable + + badgeData.text[1] = versionText(version) + badgeData.colorscheme = versionColor(version) + + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + } + ) + }) + ) + } +} diff --git a/services/imagelayers/imagelayers.service.js b/services/imagelayers/imagelayers.service.js new file mode 100644 index 0000000000..f3a71a1b26 --- /dev/null +++ b/services/imagelayers/imagelayers.service.js @@ -0,0 +1,58 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLabel: getLabel, +} = require('../../lib/badge-data') +const { metric } = require('../../lib/text-formatters') + +module.exports = class Imagelayers extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/imagelayers\/(image-size|layers)\/([^/]+)\/([^/]+)\/([^/]*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const type = match[1] + let user = match[2] + const repo = match[3] + const tag = match[4] + const format = match[5] + if (user === '_') { + user = 'library' + } + const path = user + '/' + repo + const badgeData = getBadgeData(type, data) + const options = { + method: 'POST', + json: true, + body: { + repos: [{ name: path, tag: tag }], + }, + uri: 'https://imagelayers.io/registry/analyze', + } + request(options, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + if (type === 'image-size') { + const size = metric(buffer[0].repo.size) + 'B' + badgeData.text[0] = getLabel('image size', data) + badgeData.text[1] = size + } else if (type === 'layers') { + badgeData.text[1] = buffer[0].repo.count + } + badgeData.colorscheme = null + badgeData.colorB = '#007ec6' + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/issuestats/issuestats.service.js b/services/issuestats/issuestats.service.js new file mode 100644 index 0000000000..298795b2d8 --- /dev/null +++ b/services/issuestats/issuestats.service.js @@ -0,0 +1,77 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLabel: getLabel, +} = require('../../lib/badge-data') + +module.exports = class IssueStats extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/issuestats\/([^/]+)(\/long)?\/([^/]+)\/(.+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const type = match[1] // e.g. `i` for Issue or `p` for PR + const longForm = !!match[2] + const host = match[3] // e.g. `github` + const userRepo = match[4] // e.g. `ruby/rails` + const format = match[5] + + const badgeData = getBadgeData('Issue Stats', data) + + // Maps type name from URL to JSON property name prefix for badge data + const typeToPropPrefix = { + i: 'issue', + p: 'pr', + } + const typePropPrefix = typeToPropPrefix[type] + if (typePropPrefix === undefined) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + return + } + + const url = 'http://issuestats.com/' + host + '/' + userRepo + const qs = { format: 'json' } + if (!longForm) { + qs.concise = true + } + const options = { + method: 'GET', + url: url, + qs: qs, + gzip: true, + json: true, + } + request(options, (err, res, json) => { + if (err != null || res.statusCode >= 500) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + return + } + + if (res.statusCode >= 400 || !json || typeof json !== 'object') { + badgeData.text[1] = 'not found' + sendBadge(format, badgeData) + return + } + + try { + const label = json[typePropPrefix + '_badge_preamble'] + const value = json[typePropPrefix + '_badge_words'] + const color = json[typePropPrefix + '_badge_color'] + + if (label != null) badgeData.text[0] = getLabel(label, data) + badgeData.text[1] = value || 'invalid' + if (color != null) badgeData.colorscheme = color + + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/itunes/itunes.service.js b/services/itunes/itunes.service.js new file mode 100644 index 0000000000..27506add73 --- /dev/null +++ b/services/itunes/itunes.service.js @@ -0,0 +1,46 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { addv: versionText } = require('../../lib/text-formatters') +const { version: versionColor } = require('../../lib/color-formatters') + +module.exports = class Itunes extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/itunes\/v\/(.+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const bundleId = match[1] // eg, `324684580` + const format = match[2] + const apiUrl = 'https://itunes.apple.com/lookup?id=' + bundleId + const badgeData = getBadgeData('itunes app store', data) + request(apiUrl, (err, res, buffer) => { + if (err !== null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(buffer) + if (data.resultCount === 0) { + /* Note the 'not found' response from iTunes is: + status code = 200, + body = { "resultCount":0, "results": [] } + */ + badgeData.text[1] = 'not found' + sendBadge(format, badgeData) + return + } + const version = data.results[0].version + badgeData.text[1] = versionText(version) + badgeData.colorscheme = versionColor(version) + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/jenkins/jenkins-build.service.js b/services/jenkins/jenkins-build.service.js new file mode 100644 index 0000000000..eb4e02ba9a --- /dev/null +++ b/services/jenkins/jenkins-build.service.js @@ -0,0 +1,71 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const serverSecrets = require('../../lib/server-secrets') + +module.exports = class JenkinsBuild extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/jenkins(?:-ci)?\/s\/(http(?:s)?)\/([^/]+)\/(.+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const scheme = match[1] // http(s) + const host = match[2] // example.org:8080 + const job = match[3] // folder/job + const format = match[4] + const options = { + json: true, + uri: scheme + '://' + host + '/job/' + job + '/api/json?tree=color', + } + if (job.indexOf('/') > -1) { + options.uri = + scheme + '://' + host + '/' + job + '/api/json?tree=color' + } + + if (serverSecrets && serverSecrets.jenkins_user) { + options.auth = { + user: serverSecrets.jenkins_user, + pass: serverSecrets.jenkins_pass, + } + } + + const badgeData = getBadgeData('build', data) + request(options, (err, res, json) => { + if (err !== null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + + try { + if (json.color === 'blue' || json.color === 'green') { + badgeData.colorscheme = 'brightgreen' + badgeData.text[1] = 'passing' + } else if (json.color === 'red') { + badgeData.colorscheme = 'red' + badgeData.text[1] = 'failing' + } else if (json.color === 'yellow') { + badgeData.colorscheme = 'yellow' + badgeData.text[1] = 'unstable' + } else if ( + json.color === 'grey' || + json.color === 'disabled' || + json.color === 'aborted' || + json.color === 'notbuilt' + ) { + badgeData.colorscheme = 'lightgrey' + badgeData.text[1] = 'not built' + } else { + badgeData.colorscheme = 'lightgrey' + badgeData.text[1] = 'building' + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/jenkins/jenkins-coverage.service.js b/services/jenkins/jenkins-coverage.service.js new file mode 100644 index 0000000000..cce8e4e710 --- /dev/null +++ b/services/jenkins/jenkins-coverage.service.js @@ -0,0 +1,80 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const serverSecrets = require('../../lib/server-secrets') +const { checkErrorResponse } = require('../../lib/error-helper') +const { + coveragePercentage: coveragePercentageColor, +} = require('../../lib/color-formatters') + +// For Jenkins coverage (cobertura + jacoco). +module.exports = class JenkinsCoverage extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/jenkins(?:-ci)?\/(c|j)\/(http(?:s)?)\/([^/]+)\/(.+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const type = match[1] // c - cobertura | j - jacoco + const scheme = match[2] // http(s) + const host = match[3] // example.org:8080 + const job = match[4] // folder/job + const format = match[5] + const options = { + json: true, + uri: `${scheme}://${host}/job/${job}/`, + } + + if (job.indexOf('/') > -1) { + options.uri = `${scheme}://${host}/${job}/` + } + + switch (type) { + case 'c': + options.uri += + 'lastBuild/cobertura/api/json?tree=results[elements[name,denominator,numerator,ratio]]' + break + case 'j': + options.uri += + 'lastBuild/jacoco/api/json?tree=instructionCoverage[covered,missed,percentage,total]' + break + } + + if (serverSecrets && serverSecrets.jenkins_user) { + options.auth = { + user: serverSecrets.jenkins_user, + pass: serverSecrets.jenkins_pass, + } + } + + const badgeData = getBadgeData('coverage', data) + request(options, (err, res, json) => { + if (checkErrorResponse(badgeData, err, res)) { + sendBadge(format, badgeData) + return + } + + try { + const coverageObject = json.instructionCoverage + if (coverageObject === undefined) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + const coverage = coverageObject.percentage + if (isNaN(coverage)) { + badgeData.text[1] = 'unknown' + sendBadge(format, badgeData) + return + } + badgeData.text[1] = coverage.toFixed(0) + '%' + badgeData.colorscheme = coveragePercentageColor(coverage) + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/jenkins/jenkins-plugin.service.js b/services/jenkins/jenkins-plugin.service.js new file mode 100644 index 0000000000..a234c31461 --- /dev/null +++ b/services/jenkins/jenkins-plugin.service.js @@ -0,0 +1,51 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { regularUpdate } = require('../../lib/regular-update') +const { addv: versionText } = require('../../lib/text-formatters') +const { version: versionColor } = require('../../lib/color-formatters') + +module.exports = class JenkinsPlugin extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/jenkins\/plugin\/v\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const pluginId = match[1] // e.g. blueocean + const format = match[2] + const badgeData = getBadgeData('plugin', data) + regularUpdate( + { + url: + 'https://updates.jenkins-ci.org/current/update-center.actual.json', + intervalMillis: 4 * 3600 * 1000, + scraper: json => + Object.keys(json.plugins).reduce((previous, current) => { + previous[current] = json.plugins[current].version + return previous + }, {}), + }, + (err, versions) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + const version = versions[pluginId] + if (version === undefined) { + throw Error('Plugin not found!') + } + badgeData.text[1] = versionText(version) + badgeData.colorscheme = versionColor(version) + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'not found' + sendBadge(format, badgeData) + } + } + ) + }) + ) + } +} diff --git a/services/jenkins/jenkins-tests.service.js b/services/jenkins/jenkins-tests.service.js new file mode 100644 index 0000000000..89355334ed --- /dev/null +++ b/services/jenkins/jenkins-tests.service.js @@ -0,0 +1,83 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const serverSecrets = require('../../lib/server-secrets') + +module.exports = class JenkinsTests extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/jenkins(?:-ci)?\/t\/(http(?:s)?)\/([^/]+)\/(.+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const scheme = match[1] // http(s) + const host = match[2] // example.org:8080 + const job = match[3] // folder/job + const format = match[4] + const options = { + json: true, + uri: + scheme + + '://' + + host + + '/job/' + + job + + '/lastBuild/api/json?tree=' + + encodeURIComponent('actions[failCount,skipCount,totalCount]'), + } + if (job.indexOf('/') > -1) { + options.uri = + scheme + + '://' + + host + + '/' + + job + + '/lastBuild/api/json?tree=' + + encodeURIComponent('actions[failCount,skipCount,totalCount]') + } + + if (serverSecrets && serverSecrets.jenkins_user) { + options.auth = { + user: serverSecrets.jenkins_user, + pass: serverSecrets.jenkins_pass, + } + } + + const badgeData = getBadgeData('tests', data) + request(options, (err, res, json) => { + if (err !== null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + + try { + const testsObject = json.actions.filter(obj => + obj.hasOwnProperty('failCount') + )[0] + if (testsObject === undefined) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + const successfulTests = + testsObject.totalCount - + (testsObject.failCount + testsObject.skipCount) + const percent = successfulTests / testsObject.totalCount + badgeData.text[1] = successfulTests + ' / ' + testsObject.totalCount + if (percent === 1) { + badgeData.colorscheme = 'brightgreen' + } else if (percent === 0) { + badgeData.colorscheme = 'red' + } else { + badgeData.colorscheme = 'yellow' + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/jetbrains/jetbrains.service.js b/services/jetbrains/jetbrains.service.js new file mode 100644 index 0000000000..6104db35a1 --- /dev/null +++ b/services/jetbrains/jetbrains.service.js @@ -0,0 +1,78 @@ +'use strict' + +const xml2js = require('xml2js') +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { metric } = require('../../lib/text-formatters') +const { + downloadCount: downloadCountColor, +} = require('../../lib/color-formatters') +const { addv: versionText } = require('../../lib/text-formatters') +const { version: versionColor } = require('../../lib/color-formatters') + +// JetBrains Plugins repository integration +module.exports = class JetBrains extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/jetbrains\/plugin\/(d|v)\/([^/]+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const pluginId = match[2] + const type = match[1] + const format = match[3] + const leftText = type === 'v' ? 'jetbrains plugin' : 'downloads' + const badgeData = getBadgeData(leftText, data) + const url = + 'https://plugins.jetbrains.com/plugins/list?pluginId=' + pluginId + + request(url, (err, res, buffer) => { + if (err || res.statusCode !== 200) { + badgeData.text[1] = 'inaccessible' + return sendBadge(format, badgeData) + } + + xml2js.parseString(buffer.toString(), (err, data) => { + if (err) { + badgeData.text[1] = 'invalid' + return sendBadge(format, badgeData) + } + + try { + const plugin = data['plugin-repository'].category + if (!plugin) { + badgeData.text[1] = 'not found' + return sendBadge(format, badgeData) + } + switch (type) { + case 'd': { + const downloads = parseInt( + data['plugin-repository'].category[0]['idea-plugin'][0]['$'] + .downloads, + 10 + ) + if (isNaN(downloads)) { + badgeData.text[1] = 'invalid' + return sendBadge(format, badgeData) + } + badgeData.text[1] = metric(downloads) + badgeData.colorscheme = downloadCountColor(downloads) + return sendBadge(format, badgeData) + } + case 'v': { + const version = + data['plugin-repository'].category[0]['idea-plugin'][0] + .version[0] + badgeData.text[1] = versionText(version) + badgeData.colorscheme = versionColor(version) + return sendBadge(format, badgeData) + } + } + } catch (err) { + badgeData.text[1] = 'invalid' + return sendBadge(format, badgeData) + } + }) + }) + }) + ) + } +} diff --git a/services/jira/jira-issue.service.js b/services/jira/jira-issue.service.js new file mode 100644 index 0000000000..c6606f33f9 --- /dev/null +++ b/services/jira/jira-issue.service.js @@ -0,0 +1,74 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const serverSecrets = require('../../lib/server-secrets') + +module.exports = class JiraIssue extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/jira\/issue\/(http(?:s)?)\/(.+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const protocol = match[1] // eg, https + const host = match[2] // eg, issues.apache.org/jira + const issueKey = match[3] // eg, KAFKA-2896 + const format = match[4] + + const options = { + method: 'GET', + json: true, + uri: + protocol + + '://' + + host + + '/rest/api/2/issue/' + + encodeURIComponent(issueKey), + } + if (serverSecrets && serverSecrets.jira_username) { + options.auth = { + user: serverSecrets.jira_username, + pass: serverSecrets.jira_password, + } + } + + // map JIRA color names to closest shields color schemes + const colorMap = { + 'medium-gray': 'lightgrey', + green: 'green', + yellow: 'yellow', + brown: 'orange', + 'warm-red': 'red', + 'blue-gray': 'blue', + } + + const badgeData = getBadgeData(issueKey, data) + request(options, (err, res, json) => { + if (err !== null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + const jiraIssue = json + if (jiraIssue.fields && jiraIssue.fields.status) { + if (jiraIssue.fields.status.name) { + badgeData.text[1] = jiraIssue.fields.status.name // e.g. "In Development" + } + if (jiraIssue.fields.status.statusCategory) { + badgeData.colorscheme = + colorMap[jiraIssue.fields.status.statusCategory.colorName] || + 'lightgrey' + } + } else { + badgeData.text[1] = 'invalid' + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/jira/jira-sprint.service.js b/services/jira/jira-sprint.service.js new file mode 100644 index 0000000000..239537b44a --- /dev/null +++ b/services/jira/jira-sprint.service.js @@ -0,0 +1,74 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const serverSecrets = require('../../lib/server-secrets') + +// JIRA agile sprint completion. +module.exports = class JiraSprint extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/jira\/sprint\/(http(?:s)?)\/(.+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const protocol = match[1] // eg, https + const host = match[2] // eg, jira.spring.io + const sprintId = match[3] // eg, 94 + const format = match[4] // eg, png + + const options = { + method: 'GET', + json: true, + uri: + protocol + + '://' + + host + + '/rest/api/2/search?jql=sprint=' + + sprintId + + '%20AND%20type%20IN%20(Bug,Improvement,Story,"Technical%20task")&fields=resolution&maxResults=500', + } + if (serverSecrets && serverSecrets.jira_username) { + options.auth = { + user: serverSecrets.jira_username, + pass: serverSecrets.jira_password, + } + } + + const badgeData = getBadgeData('completion', data) + request(options, (err, res, json) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + if (json && json.total >= 0) { + const issuesDone = json.issues.filter(el => { + if (el.fields.resolution != null) { + return el.fields.resolution.name !== 'Unresolved' + } + }).length + badgeData.text[1] = + Math.round((issuesDone * 100) / json.total) + '%' + switch (issuesDone) { + case 0: + badgeData.colorscheme = 'red' + break + case json.total: + badgeData.colorscheme = 'brightgreen' + break + default: + badgeData.colorscheme = 'orange' + } + } else { + badgeData.text[1] = 'invalid' + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/jitpack/jitpack.service.js b/services/jitpack/jitpack.service.js new file mode 100644 index 0000000000..5f04fa3d0b --- /dev/null +++ b/services/jitpack/jitpack.service.js @@ -0,0 +1,54 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { addv: versionText } = require('../../lib/text-formatters') +const { version: versionColor } = require('../../lib/color-formatters') + +module.exports = class Jitpack extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/jitpack\/v\/([^/]*)\/([^/]*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const groupId = 'com.github.' + match[1] // github user + const artifactId = match[2] // the project's name + const format = match[3] // "svg" + const name = 'JitPack' + + const pkg = groupId + '/' + artifactId + '/latest' + const apiUrl = 'https://jitpack.io/api/builds/' + pkg + + const badgeData = getBadgeData(name, data) + + request(apiUrl, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + if (res.statusCode === 404) { + badgeData.text[1] = 'not found' + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(buffer) + const status = data['status'] + let color = versionColor(data['version']) + let version = versionText(data['version']) + if (status !== 'ok') { + color = 'red' + version = 'unknown' + } + badgeData.text[1] = version + badgeData.colorscheme = color + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/legacy-service.js b/services/legacy-service.js index c131707182..9cc4be4902 100644 --- a/services/legacy-service.js +++ b/services/legacy-service.js @@ -4,12 +4,19 @@ const BaseService = require('./base') // registerFn: ({ camp, cache }) => { camp.route(/.../, cache(...)) } class LegacyService extends BaseService { - static registerLegacyRouteHandler({ camp, cache }) { + static registerLegacyRouteHandler({ camp, cache, githubApiProvider }) { throw Error('registerLegacyRouteHandler() not implemented') } - static register(camp, handleRequest, serviceConfig) { - this.registerLegacyRouteHandler({ camp, cache: handleRequest }) + static register( + { camp, handleRequest: cache, githubApiProvider }, + serviceConfig + ) { + this.registerLegacyRouteHandler({ + camp, + cache, + githubApiProvider, + }) } } diff --git a/services/lgtm/lgtm-alerts.service.js b/services/lgtm/lgtm-alerts.service.js new file mode 100644 index 0000000000..e1a9cd35b2 --- /dev/null +++ b/services/lgtm/lgtm-alerts.service.js @@ -0,0 +1,47 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { checkErrorResponse } = require('../../lib/error-helper') +const { metric } = require('../../lib/text-formatters') + +module.exports = class LgtmAlerts extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/lgtm\/alerts\/(.+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const projectId = match[1] // eg, `g/apache/cloudstack` + const format = match[2] + const url = + 'https://lgtm.com/api/v0.1/project/' + projectId + '/details' + const badgeData = getBadgeData('lgtm', data) + request(url, (err, res, buffer) => { + if ( + checkErrorResponse(badgeData, err, res, { + 404: 'project not found', + }) + ) { + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(buffer) + if (!('alerts' in data)) throw new Error('Invalid data') + badgeData.text[1] = + metric(data.alerts) + (data.alerts === 1 ? ' alert' : ' alerts') + + if (data.alerts === 0) { + badgeData.colorscheme = 'brightgreen' + } else { + badgeData.colorscheme = 'yellow' + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/lgtm/lgtm-grade.service.js b/services/lgtm/lgtm-grade.service.js new file mode 100644 index 0000000000..b69af9045c --- /dev/null +++ b/services/lgtm/lgtm-grade.service.js @@ -0,0 +1,75 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { checkErrorResponse } = require('../../lib/error-helper') + +module.exports = class LgtmGrade extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/lgtm\/grade\/([^/]+)\/(.+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const language = match[1] // eg, `java` + const projectId = match[2] // eg, `g/apache/cloudstack` + const format = match[3] + const url = + 'https://lgtm.com/api/v0.1/project/' + projectId + '/details' + const languageLabel = (() => { + switch (language) { + case 'cpp': + return 'c/c++' + case 'csharp': + return 'c#' + // Javascript analysis on LGTM also includes TypeScript + case 'javascript': + return 'js/ts' + default: + return language + } + })() + const badgeData = getBadgeData('code quality: ' + languageLabel, data) + request(url, (err, res, buffer) => { + if ( + checkErrorResponse(badgeData, err, res, { + 404: 'project not found', + }) + ) { + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(buffer) + if (!('languages' in data)) throw new Error('Invalid data') + for (const languageData of data.languages) { + if (languageData.lang === language && 'grade' in languageData) { + // Pretty label for the language + badgeData.text[1] = languageData.grade + // Pick colour based on grade + if (languageData.grade === 'A+') { + badgeData.colorscheme = 'brightgreen' + } else if (languageData.grade === 'A') { + badgeData.colorscheme = 'green' + } else if (languageData.grade === 'B') { + badgeData.colorscheme = 'yellowgreen' + } else if (languageData.grade === 'C') { + badgeData.colorscheme = 'yellow' + } else if (languageData.grade === 'D') { + badgeData.colorscheme = 'orange' + } else { + badgeData.colorscheme = 'red' + } + sendBadge(format, badgeData) + return + } + } + badgeData.text[1] = 'no data for language' + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/liberapay/liberapay.service.js b/services/liberapay/liberapay.service.js new file mode 100644 index 0000000000..89d54915f2 --- /dev/null +++ b/services/liberapay/liberapay.service.js @@ -0,0 +1,83 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { metric } = require('../../lib/text-formatters') +const { makeLogo: getLogo } = require('../../lib/badge-data') +const { colorScale } = require('../../lib/color-formatters') + +module.exports = class Liberapay extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/liberapay\/(receives|gives|patrons|goal)\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const type = match[1] // e.g., 'gives' + const entity = match[2] // e.g., 'Changaco' + const format = match[3] + const apiUrl = 'https://liberapay.com/' + entity + '/public.json' + // Lock down type + const label = { + receives: 'receives', + gives: 'gives', + patrons: 'patrons', + goal: 'goal progress', + }[type] + const badgeData = getBadgeData(label, data) + if (badgeData.template === 'social') { + badgeData.logo = getLogo('liberapay', data) + } + request(apiUrl, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(buffer) + let value + let currency + switch (type) { + case 'receives': + if (data.receiving) { + value = data.receiving.amount + currency = data.receiving.currency + badgeData.text[1] = `${metric(value)} ${currency}/week` + } + break + case 'gives': + if (data.giving) { + value = data.giving.amount + currency = data.giving.currency + badgeData.text[1] = `${metric(value)} ${currency}/week` + } + break + case 'patrons': + value = data.npatrons + badgeData.text[1] = metric(value) + break + case 'goal': + if (data.goal) { + value = Math.round( + (data.receiving.amount / data.goal.amount) * 100 + ) + badgeData.text[1] = `${value}%` + } + break + } + if (value != null) { + badgeData.colorscheme = colorScale([0, 10, 100])(value) + sendBadge(format, badgeData) + } else { + badgeData.text[1] = 'anonymous' + badgeData.colorscheme = 'blue' + sendBadge(format, badgeData) + } + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/librariesio/librariesio-dependencies.service.js b/services/librariesio/librariesio-dependencies.service.js new file mode 100644 index 0000000000..6ad16d75dc --- /dev/null +++ b/services/librariesio/librariesio-dependencies.service.js @@ -0,0 +1,73 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { checkErrorResponse } = require('../../lib/error-helper') + +module.exports = class LibrariesioDependencies extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/librariesio\/(github|release)\/([\w\-_]+\/[\w\-_]+)\/?([\w\-_.]+)?\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const resource = match[1] + const project = match[2] + const version = match[3] + const format = match[4] + + let uri + switch (resource) { + case 'github': { + uri = 'https://libraries.io/api/github/' + project + '/dependencies' + break + } + case 'release': { + const v = version || 'latest' + uri = + 'https://libraries.io/api/' + project + '/' + v + '/dependencies' + break + } + } + + const options = { method: 'GET', json: true, uri: uri } + const badgeData = getBadgeData('dependencies', data) + + request(options, (err, res, json) => { + if ( + checkErrorResponse(badgeData, err, res, { 404: 'not available' }) + ) { + sendBadge(format, badgeData) + return + } + + try { + const deprecated = json.dependencies.filter(dep => dep.deprecated) + + const outofdate = json.dependencies.filter(dep => dep.outdated) + + // Deprecated dependencies are really bad + if (deprecated.length > 0) { + badgeData.colorscheme = 'red' + badgeData.text[1] = deprecated.length + ' deprecated' + return sendBadge(format, badgeData) + } + + // Out of date dependencies are pretty bad + if (outofdate.length > 0) { + badgeData.colorscheme = 'orange' + badgeData.text[1] = outofdate.length + ' out of date' + return sendBadge(format, badgeData) + } + + // Up to date dependencies are good! + badgeData.colorscheme = 'brightgreen' + badgeData.text[1] = 'up to date' + return sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/libscore/libscore.service.js b/services/libscore/libscore.service.js new file mode 100644 index 0000000000..13497db3b1 --- /dev/null +++ b/services/libscore/libscore.service.js @@ -0,0 +1,43 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { metric } = require('../../lib/text-formatters') + +module.exports = class Libscore extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/libscore\/s\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const library = match[1] // eg, `jQuery`. + const format = match[2] + const apiUrl = 'http://api.libscore.com/v1/libraries/' + library + const badgeData = getBadgeData('libscore', data) + request(apiUrl, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(buffer) + if (data.count.length === 0) { + // Note the 'not found' response from libscore is: + // status code = 200, + // body = {"github":"","meta":{},"count":[],"sites":[]} + badgeData.text[1] = 'not found' + sendBadge(format, badgeData) + return + } + badgeData.text[1] = metric(+data.count[data.count.length - 1]) + badgeData.colorscheme = 'blue' + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/lib/luarocks-version.js b/services/luarocks/luarocks-version.js similarity index 100% rename from lib/luarocks-version.js rename to services/luarocks/luarocks-version.js diff --git a/lib/luarocks-version.spec.js b/services/luarocks/luarocks-version.spec.js similarity index 100% rename from lib/luarocks-version.spec.js rename to services/luarocks/luarocks-version.spec.js diff --git a/services/luarocks/luarocks.service.js b/services/luarocks/luarocks.service.js new file mode 100644 index 0000000000..91d125dee7 --- /dev/null +++ b/services/luarocks/luarocks.service.js @@ -0,0 +1,80 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { omitv, addv: versionText } = require('../../lib/text-formatters') +const { + parseVersion: luarocksParseVersion, + compareVersionLists: luarocksCompareVersionLists, +} = require('./luarocks-version') + +module.exports = class Luarocks extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/luarocks\/v\/([^/]+)\/([^/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const user = match[1] // eg, `leafo`. + const moduleName = match[2] // eg, `lapis`. + const format = match[4] + const apiUrl = + 'https://luarocks.org/manifests/' + user + '/manifest.json' + const badgeData = getBadgeData('luarocks', data) + let version = match[3] // you can explicitly specify a version + request(apiUrl, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + let versions + try { + const moduleInfo = JSON.parse(buffer).repository[moduleName] + versions = Object.keys(moduleInfo) + if (version && versions.indexOf(version) === -1) { + throw new Error('unknown version') + } + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + return + } + if (!version) { + if (versions.length === 1) { + version = omitv(versions[0]) + } else { + let latestVersionString, latestVersionList + versions.forEach(versionString => { + versionString = omitv(versionString) // remove leading 'v' + const versionList = luarocksParseVersion(versionString) + if ( + !latestVersionList || // first iteration + luarocksCompareVersionLists(versionList, latestVersionList) > + 0 + ) { + latestVersionString = versionString + latestVersionList = versionList + } + }) + version = latestVersionString + } + } + let color + switch (version.slice(0, 3).toLowerCase()) { + case 'dev': + color = 'yellow' + break + case 'scm': + case 'cvs': + color = 'orange' + break + default: + color = 'brightgreen' + } + badgeData.text[1] = versionText(version) + badgeData.colorscheme = color + sendBadge(format, badgeData) + }) + }) + ) + } +} diff --git a/services/magnumci/magnumci.service.js b/services/magnumci/magnumci.service.js new file mode 100644 index 0000000000..e1c39eb467 --- /dev/null +++ b/services/magnumci/magnumci.service.js @@ -0,0 +1,18 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { getDeprecatedBadge } = require('../../lib/deprecation-helpers') + +// Magnum CI integration - deprecated as of July 2018 +module.exports = class MagnumCi extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/magnumci\/ci\/([^/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const format = match[3] + const badgeData = getDeprecatedBadge('magnum ci', data) + sendBadge(format, badgeData) + }) + ) + } +} diff --git a/services/maintenance/maintenance.service.js b/services/maintenance/maintenance.service.js new file mode 100644 index 0000000000..a7eb6f6498 --- /dev/null +++ b/services/maintenance/maintenance.service.js @@ -0,0 +1,41 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const log = require('../../lib/log') + +module.exports = class Maintenance extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/maintenance\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const status = match[1] // eg, yes + const year = +match[2] // eg, 2016 + const format = match[3] + const badgeData = getBadgeData('maintained', data) + try { + const now = new Date() + const cy = now.getUTCFullYear() // current year. + const m = now.getUTCMonth() // month. + if (status === 'no') { + badgeData.text[1] = 'no! (as of ' + year + ')' + badgeData.colorscheme = 'red' + } else if (cy <= year) { + badgeData.text[1] = status + badgeData.colorscheme = 'brightgreen' + } else if (cy === year + 1 && m < 3) { + badgeData.text[1] = 'stale (as of ' + cy + ')' + } else { + badgeData.text[1] = 'no! (as of ' + year + ')' + badgeData.colorscheme = 'red' + } + sendBadge(format, badgeData) + } catch (e) { + log.error(e.stack) + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + ) + } +} diff --git a/services/maven-central/maven-central.service.js b/services/maven-central/maven-central.service.js new file mode 100644 index 0000000000..f739f88273 --- /dev/null +++ b/services/maven-central/maven-central.service.js @@ -0,0 +1,60 @@ +'use strict' + +const xml2js = require('xml2js') +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { addv: versionText } = require('../../lib/text-formatters') +const { version: versionColor } = require('../../lib/color-formatters') + +// Based on repo1.maven.org rather than search.maven.org because of #846. +module.exports = class MavenCentral extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/maven-central\/v\/([^/]*)\/([^/]*)(?:\/([^/]*))?\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const groupId = match[1] // eg, `com.google.inject` + const artifactId = match[2] // eg, `guice` + const versionPrefix = match[3] || '' // eg, `1.` + const format = match[4] || 'gif' // eg, `svg` + const metadataUrl = + 'http://repo1.maven.org/maven2' + + '/' + + encodeURIComponent(groupId).replace(/\./g, '/') + + '/' + + encodeURIComponent(artifactId) + + '/maven-metadata.xml' + const badgeData = getBadgeData('maven-central', data) + request( + metadataUrl, + { headers: { Accept: 'text/xml' } }, + (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + xml2js.parseString(buffer.toString(), (err, data) => { + if (err != null) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + return + } + try { + const versions = data.metadata.versioning[0].versions[0].version.reverse() + const version = versions.find( + version => version.indexOf(versionPrefix) === 0 + ) + badgeData.text[1] = versionText(version) + badgeData.colorscheme = versionColor(version) + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + } + ) + }) + ) + } +} diff --git a/services/maven-metadata/maven-metadata.service.js b/services/maven-metadata/maven-metadata.service.js new file mode 100644 index 0000000000..e7f3f8cd86 --- /dev/null +++ b/services/maven-metadata/maven-metadata.service.js @@ -0,0 +1,52 @@ +'use strict' + +const xml2js = require('xml2js') +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { addv: versionText } = require('../../lib/text-formatters') +const { version: versionColor } = require('../../lib/color-formatters') + +module.exports = class MavenMetadata extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/maven-metadata\/v\/(https?)\/(.+\.xml)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const [, scheme, hostAndPath, format] = match + const metadataUri = `${scheme}://${hostAndPath}` + request(metadataUri, (error, response, body) => { + const badge = getBadgeData('maven', data) + if ( + !error && + response.statusCode >= 200 && + response.statusCode < 300 + ) { + try { + xml2js.parseString(body, (err, result) => { + if (err) { + badge.text[1] = 'error' + badge.colorscheme = 'red' + sendBadge(format, badge) + } else { + const version = result.metadata.versioning[0].versions[0].version.slice( + -1 + )[0] + badge.text[1] = versionText(version) + badge.colorscheme = versionColor(version) + sendBadge(format, badge) + } + }) + } catch (e) { + badge.text[1] = 'error' + badge.colorscheme = 'red' + sendBadge(format, badge) + } + } else { + badge.text[1] = 'error' + badge.colorscheme = 'red' + sendBadge(format, badge) + } + }) + }) + ) + } +} diff --git a/services/microbadger/microbadger.service.js b/services/microbadger/microbadger.service.js new file mode 100644 index 0000000000..9089671cce --- /dev/null +++ b/services/microbadger/microbadger.service.js @@ -0,0 +1,90 @@ +'use strict' + +const prettyBytes = require('pretty-bytes') +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLabel: getLabel, +} = require('../../lib/badge-data') + +module.exports = class MicroBadger extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/microbadger\/(image-size|layers)\/([^/]+)\/([^/]+)\/?([^/]*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const type = match[1] + let user = match[2] + const repo = match[3] + const tag = match[4] + const format = match[5] + if (user === '_') { + user = 'library' + } + const url = `https://api.microbadger.com/v1/images/${user}/${repo}` + + const badgeData = getBadgeData(type, data) + if (type === 'image-size') { + badgeData.text[0] = getLabel('image size', data) + } + + const options = { + method: 'GET', + uri: url, + headers: { + Accept: 'application/json', + }, + } + request(options, (err, res, buffer) => { + if (res && res.statusCode === 404) { + badgeData.text[1] = 'not found' + sendBadge(format, badgeData) + return + } + + if (err != null || !res || res.statusCode !== 200) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + + try { + const parsedData = JSON.parse(buffer) + let image + + if (tag) { + image = + parsedData.Versions && + parsedData.Versions.find(v => v.Tags.some(t => t.tag === tag)) + if (!image) { + badgeData.text[1] = 'not found' + sendBadge(format, badgeData) + return + } + } else { + image = parsedData + } + + if (type === 'image-size') { + const downloadSize = image.DownloadSize + if (downloadSize === undefined) { + badgeData.text[1] = 'unknown' + sendBadge(format, badgeData) + return + } + badgeData.text[1] = prettyBytes(parseInt(downloadSize)) + } else if (type === 'layers') { + badgeData.text[1] = image.LayerCount + } + badgeData.colorscheme = null + badgeData.colorB = '#007ec6' + sendBadge(format, badgeData) + } catch (e) { + badgeData.colorscheme = 'red' + badgeData.text[1] = 'error' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/lib/nexus-version.js b/services/nexus/nexus-version.js similarity index 100% rename from lib/nexus-version.js rename to services/nexus/nexus-version.js diff --git a/services/nexus/nexus.service.js b/services/nexus/nexus.service.js new file mode 100644 index 0000000000..0f0a9bc546 --- /dev/null +++ b/services/nexus/nexus.service.js @@ -0,0 +1,114 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { isSnapshotVersion: isNexusSnapshotVersion } = require('./nexus-version') +const { addv: versionText } = require('../../lib/text-formatters') +const { version: versionColor } = require('../../lib/color-formatters') + +module.exports = class Nexus extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + // standalone sonatype nexus installation + // API pattern: + // /nexus/(r|s|<repo-name>)/(http|https)/<nexus.host>[:port][/<entry-path>]/<group>/<artifact>[:k1=v1[:k2=v2[...]]].<format> + // for /nexus/[rs]/... pattern, use the search api of the nexus server, and + // for /nexus/<repo-name>/... pattern, use the resolve api of the nexus server. + camp.route( + /^\/nexus\/(r|s|[^/]+)\/(https?)\/((?:[^/]+)(?:\/[^/]+)?)\/([^/]+)\/([^/:]+)(:.+)?\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const repo = match[1] // r | s | repo-name + const scheme = match[2] // http | https + const host = match[3] // eg, `nexus.example.com` + const groupId = encodeURIComponent(match[4]) // eg, `com.google.inject` + const artifactId = encodeURIComponent(match[5]) // eg, `guice` + const queryOpt = (match[6] || '').replace(/:/g, '&') // eg, `&p=pom&c=doc` + const format = match[7] + + const badgeData = getBadgeData('nexus', data) + + const apiUrl = + scheme + + '://' + + host + + (repo === 'r' || repo === 's' + ? '/service/local/lucene/search?g=' + + groupId + + '&a=' + + artifactId + + queryOpt + : '/service/local/artifact/maven/resolve?r=' + + repo + + '&g=' + + groupId + + '&a=' + + artifactId + + '&v=LATEST' + + queryOpt) + + request( + apiUrl, + { headers: { Accept: 'application/json' } }, + (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } else if (res && res.statusCode === 404) { + badgeData.text[1] = 'no-artifact' + sendBadge(format, badgeData) + return + } + try { + const parsed = JSON.parse(buffer) + let version = '0' + switch (repo) { + case 'r': + if (parsed.data.length === 0) { + badgeData.text[1] = 'no-artifact' + sendBadge(format, badgeData) + return + } + version = parsed.data[0].latestRelease + break + case 's': + if (parsed.data.length === 0) { + badgeData.text[1] = 'no-artifact' + sendBadge(format, badgeData) + return + } + // only want to match 1.2.3-SNAPSHOT style versions, which may not always be in + // 'latestSnapshot' so check 'version' as well before continuing to next entry + parsed.data.every(artifact => { + if (isNexusSnapshotVersion(artifact.latestSnapshot)) { + version = artifact.latestSnapshot + return + } + if (isNexusSnapshotVersion(artifact.version)) { + version = artifact.version + return + } + return true + }) + break + default: + version = parsed.data.baseVersion || parsed.data.version + break + } + if (version !== '0') { + badgeData.text[1] = versionText(version) + badgeData.colorscheme = versionColor(version) + } else { + badgeData.text[1] = 'undefined' + badgeData.colorscheme = 'orange' + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + } + ) + }) + ) + } +} diff --git a/services/nsp/nsp.service.js b/services/nsp/nsp.service.js new file mode 100644 index 0000000000..8cd732180c --- /dev/null +++ b/services/nsp/nsp.service.js @@ -0,0 +1,135 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') + +module.exports = class Nsp extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/nsp\/npm\/(?:@([^/]+)?\/)?([^/]+)?(?:\/([^/]+)?)?\.(svg|png|gif|jpg|json)?$/, + cache((data, match, sendBadge, request) => { + // A: /nsp/npm/:package.:format + // B: /nsp/npm/:package/:version.:format + // C: /nsp/npm/@:scope/:package.:format + // D: /nsp/npm/@:scope/:package/:version.:format + const badgeData = getBadgeData('nsp', data) + const capturedScopeWithoutAtSign = match[1] + const capturedPackageName = match[2] + const capturedVersion = match[3] + const capturedFormat = match[4] + + function getNspResults( + scopeWithoutAtSign = null, + packageName = '', + packageVersion = '' + ) { + const nspRequestOptions = { + method: 'POST', + body: { + package: { + name: null, + version: packageVersion, + }, + }, + json: true, + } + + if (typeof scopeWithoutAtSign === 'string') { + nspRequestOptions.body.package.name = `@${scopeWithoutAtSign}/${packageName}` + } else { + nspRequestOptions.body.package.name = packageName + } + + request( + 'https://api.nodesecurity.io/check', + nspRequestOptions, + (error, response, body) => { + if (error !== null || typeof body !== 'object' || body === null) { + badgeData.text[1] = 'invalid' + badgeData.colorscheme = 'red' + } else if (body.length !== 0) { + badgeData.text[1] = `${body.length} vulnerabilities` + badgeData.colorscheme = 'red' + } else { + badgeData.text[1] = 'no known vulnerabilities' + badgeData.colorscheme = 'brightgreen' + } + + sendBadge(capturedFormat, badgeData) + } + ) + } + + function getNpmVersionThenNspResults( + scopeWithoutAtSign = null, + packageName = '' + ) { + // nsp doesn't properly detect the package version in POST requests so this function gets it for us + // https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#getpackageversion + const npmRequestOptions = { + headers: { + Accept: '*/*', + }, + json: true, + } + let npmURL = null + + if (typeof scopeWithoutAtSign === 'string') { + // Using 'latest' would save bandwidth, but it is currently not supported for scoped packages + npmURL = `http://registry.npmjs.org/@${scopeWithoutAtSign}%2F${packageName}` + } else { + npmURL = `http://registry.npmjs.org/${packageName}/latest` + } + + request(npmURL, npmRequestOptions, (error, response, body) => { + if (response !== null && response.statusCode === 404) { + // NOTE: in POST requests nsp does not distinguish between + // 'package not found' and 'no known vulnerabilities'. + // To keep consistency in the use case where a version is provided + // (which skips `getNpmVersionThenNspResults()` altogether) we'll say + // 'no known vulnerabilities' since it is technically true in both cases + badgeData.text[1] = 'no known vulnerabilities' + + sendBadge(capturedFormat, badgeData) + } else if ( + error !== null || + typeof body !== 'object' || + body === null + ) { + badgeData.text[1] = 'invalid' + badgeData.colorscheme = 'red' + + sendBadge(capturedFormat, badgeData) + } else if (typeof body.version === 'string') { + getNspResults(scopeWithoutAtSign, packageName, body.version) + } else if (typeof body['dist-tags'] === 'object') { + getNspResults( + scopeWithoutAtSign, + packageName, + body['dist-tags'].latest + ) + } else { + badgeData.text[1] = 'invalid' + badgeData.colorscheme = 'red' + + sendBadge(capturedFormat, badgeData) + } + }) + } + + if (typeof capturedVersion === 'string') { + getNspResults( + capturedScopeWithoutAtSign, + capturedPackageName, + capturedVersion + ) + } else { + getNpmVersionThenNspResults( + capturedScopeWithoutAtSign, + capturedPackageName + ) + } + }) + ) + } +} diff --git a/lib/nuget-provider.js b/services/nuget/nuget.service.js similarity index 87% rename from lib/nuget-provider.js rename to services/nuget/nuget.service.js index dd16bede54..e9d15aea4c 100644 --- a/lib/nuget-provider.js +++ b/services/nuget/nuget.service.js @@ -1,9 +1,12 @@ 'use strict' -const { downloadCount: downloadCountColor } = require('./color-formatters') -const { makeBadgeData: getBadgeData } = require('./badge-data') -const { metric } = require('./text-formatters') -const { regularUpdate } = require('./regular-update') +const LegacyService = require('../legacy-service') +const { + downloadCount: downloadCountColor, +} = require('../../lib/color-formatters') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { metric } = require('../../lib/text-formatters') +const { regularUpdate } = require('../../lib/regular-update') function mapNugetFeedv2({ camp, cache }, pattern, offset, getInfo) { const vRegex = new RegExp( @@ -323,7 +326,40 @@ function mapNugetFeed({ camp, cache }, pattern, offset, getInfo) { ) } -module.exports = { - mapNugetFeedv2, - mapNugetFeed, +module.exports = class Nuget extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + // ReSharper + mapNugetFeedv2({ camp, cache }, 'resharper', 0, match => ({ + site: 'resharper', + feed: 'https://resharper-plugins.jetbrains.com/api/v2', + })) + + // Chocolatey + mapNugetFeedv2({ camp, cache }, 'chocolatey', 0, match => ({ + site: 'chocolatey', + feed: 'https://www.chocolatey.org/api/v2', + })) + + // PowerShell Gallery + mapNugetFeedv2({ camp, cache }, 'powershellgallery', 0, match => ({ + site: 'powershellgallery', + feed: 'https://www.powershellgallery.com/api/v2', + })) + + // NuGet + mapNugetFeed({ camp, cache }, 'nuget', 0, match => ({ + site: 'nuget', + feed: 'https://api.nuget.org/v3', + })) + + // MyGet + mapNugetFeed({ camp, cache }, '(.+\\.)?myget\\/(.*)', 2, match => { + const tenant = match[1] || 'www.' // eg. dotnet + const feed = match[2] + return { + site: feed, + feed: 'https://' + tenant + 'myget.org/F/' + feed + '/api/v3', + } + }) + } } diff --git a/services/osstracker/osstracker.service.js b/services/osstracker/osstracker.service.js new file mode 100644 index 0000000000..9b57ae7fb1 --- /dev/null +++ b/services/osstracker/osstracker.service.js @@ -0,0 +1,57 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const log = require('../../lib/log') + +// For NetflixOSS metadata: https://github.com/Netflix/osstracker +module.exports = class OssTracker extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/osslifecycle?\/([^/]+\/[^/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const orgOrUserAndRepo = match[1] + const branch = match[2] + const format = match[3] + let url = 'https://raw.githubusercontent.com/' + orgOrUserAndRepo + if (branch != null) { + url += '/' + branch + '/OSSMETADATA' + } else { + url += '/master/OSSMETADATA' + } + const options = { + method: 'GET', + uri: url, + } + const badgeData = getBadgeData('OSS Lifecycle', data) + request(options, (err, res, body) => { + if (err != null) { + log.error('NetflixOSS error: ' + err.stack) + if (res) { + log.error('' + res) + } + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + return + } + try { + const matchStatus = body.match(/osslifecycle=([a-z]+)/im) + if (matchStatus === null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } else { + badgeData.text[1] = matchStatus[1] + sendBadge(format, badgeData) + return + } + } catch (e) { + log(e) + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/packagecontrol/packagecontrol.service.js b/services/packagecontrol/packagecontrol.service.js new file mode 100644 index 0000000000..cf86ca7412 --- /dev/null +++ b/services/packagecontrol/packagecontrol.service.js @@ -0,0 +1,81 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { metric } = require('../../lib/text-formatters') +const { + downloadCount: downloadCountColor, +} = require('../../lib/color-formatters') + +module.exports = class PackageControl extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/packagecontrol\/(dm|dw|dd|dt)\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const info = match[1] // either `dm`, `dw`, `dd` or dt`. + const userRepo = match[2] // eg, `Package%20Control`. + const format = match[3] + const apiUrl = + 'https://packagecontrol.io/packages/' + userRepo + '.json' + const badgeData = getBadgeData('downloads', data) + request(apiUrl, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(buffer) + let downloads = 0 + let platforms + switch (info.charAt(1)) { + case 'm': + // daily downloads are separated by Operating System + platforms = data.installs.daily.data + platforms.forEach(platform => { + // loop through the first 30 days or 1 month + for (let i = 0; i < 30; i++) { + // add the downloads for that day for that platform + downloads += platform.totals[i] + } + }) + badgeData.text[1] = metric(downloads) + '/month' + break + case 'w': + // daily downloads are separated by Operating System + platforms = data.installs.daily.data + platforms.forEach(platform => { + // loop through the first 7 days or 1 week + for (let i = 0; i < 7; i++) { + // add the downloads for that day for that platform + downloads += platform.totals[i] + } + }) + badgeData.text[1] = metric(downloads) + '/week' + break + case 'd': + // daily downloads are separated by Operating System + platforms = data.installs.daily.data + platforms.forEach(platform => { + // use the downloads from yesterday + downloads += platform.totals[1] + }) + badgeData.text[1] = metric(downloads) + '/day' + break + case 't': + // all-time downloads are already compiled + downloads = data.installs.total + badgeData.text[1] = metric(downloads) + break + } + badgeData.colorscheme = downloadCountColor(downloads) + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/packagist/packagist-downloads.service.js b/services/packagist/packagist-downloads.service.js new file mode 100644 index 0000000000..c70ebb5c87 --- /dev/null +++ b/services/packagist/packagist-downloads.service.js @@ -0,0 +1,57 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { metric } = require('../../lib/text-formatters') +const { + downloadCount: downloadCountColor, +} = require('../../lib/color-formatters') + +module.exports = class PackagistDownloads extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/packagist\/(dm|dd|dt)\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const info = match[1] // either `dm` or dt`. + const userRepo = match[2] // eg, `doctrine/orm`. + const format = match[3] + const apiUrl = 'https://packagist.org/packages/' + userRepo + '.json' + const badgeData = getBadgeData('downloads', data) + if (userRepo.substr(-14) === '/:package_name') { + badgeData.text[1] = 'invalid' + return sendBadge(format, badgeData) + } + request(apiUrl, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(buffer) + let downloads + switch (info.charAt(1)) { + case 'm': + downloads = data.package.downloads.monthly + badgeData.text[1] = metric(downloads) + '/month' + break + case 'd': + downloads = data.package.downloads.daily + badgeData.text[1] = metric(downloads) + '/day' + break + case 't': + downloads = data.package.downloads.total + badgeData.text[1] = metric(downloads) + break + } + badgeData.colorscheme = downloadCountColor(downloads) + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/packagist/packagist-license.service.js b/services/packagist/packagist-license.service.js new file mode 100644 index 0000000000..373859f861 --- /dev/null +++ b/services/packagist/packagist-license.service.js @@ -0,0 +1,60 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') + +module.exports = class PackagistLicense extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/packagist\/l\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const userRepo = match[1] + const format = match[2] + const apiUrl = 'https://packagist.org/packages/' + userRepo + '.json' + const badgeData = getBadgeData('license', data) + if (userRepo.substr(-14) === '/:package_name') { + badgeData.text[1] = 'invalid' + return sendBadge(format, badgeData) + } + request(apiUrl, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(buffer) + // Note: if you change the latest version detection algorithm here, + // change it above (for the actual version badge). + let version + const unstable = function(ver) { + return /dev/.test(ver) + } + // Grab the latest stable version, or an unstable + for (const versionName in data.package.versions) { + const current = data.package.versions[versionName] + + if (version !== undefined) { + if (unstable(version.version) && !unstable(current.version)) { + version = current + } else if ( + version.version_normalized < current.version_normalized + ) { + version = current + } + } else { + version = current + } + } + badgeData.text[1] = version.license[0] + badgeData.colorscheme = 'blue' + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/packagist/packagist-php-version.service.js b/services/packagist/packagist-php-version.service.js new file mode 100644 index 0000000000..471814e36e --- /dev/null +++ b/services/packagist/packagist-php-version.service.js @@ -0,0 +1,43 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const log = require('../../lib/log') + +module.exports = class PackagistPhpVersion extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/packagist\/php-v\/([^/]+\/[^/]+)(?:\/([^/]+))?\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const userRepo = match[1] // eg, espadrine/sc + const version = match[2] ? match[2] : 'dev-master' + const format = match[3] + const options = { + method: 'GET', + uri: 'https://packagist.org/p/' + userRepo + '.json', + } + const badgeData = getBadgeData('PHP', data) + request(options, (err, res, buffer) => { + if (err !== null) { + log.error('Packagist error: ' + err.stack) + if (res) { + log.error('' + res) + } + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + return + } + + try { + const data = JSON.parse(buffer) + badgeData.text[1] = data.packages[userRepo][version].require.php + badgeData.colorscheme = 'blue' + } catch (e) { + badgeData.text[1] = 'invalid' + } + sendBadge(format, badgeData) + }) + }) + ) + } +} diff --git a/services/packagist/packagist-version.service.js b/services/packagist/packagist-version.service.js new file mode 100644 index 0000000000..6ca7f8591a --- /dev/null +++ b/services/packagist/packagist-version.service.js @@ -0,0 +1,103 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { addv: versionText } = require('../../lib/text-formatters') +const { version: versionColor } = require('../../lib/color-formatters') +const { + compare: phpVersionCompare, + latest: phpLatestVersion, + isStable: phpStableVersion, +} = require('../../lib/php-version') + +module.exports = class PackagistVersion extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/packagist\/(v|vpre)\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const info = match[1] // either `v` or `vpre`. + const userRepo = match[2] // eg, `doctrine/orm`. + const format = match[3] + const apiUrl = 'https://packagist.org/packages/' + userRepo + '.json' + const badgeData = getBadgeData('packagist', data) + if (userRepo.substr(-14) === '/:package_name') { + badgeData.text[1] = 'invalid' + return sendBadge(format, badgeData) + } + request(apiUrl, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(buffer) + + const versionsData = data.package.versions + let versions = Object.keys(versionsData) + + // Map aliases (eg, dev-master). + const aliasesMap = {} + versions.forEach(version => { + const versionData = versionsData[version] + if ( + versionData.extra && + versionData.extra['branch-alias'] && + versionData.extra['branch-alias'][version] + ) { + // eg, version is 'dev-master', mapped to '2.0.x-dev'. + const validVersion = versionData.extra['branch-alias'][version] + if ( + aliasesMap[validVersion] === undefined || + phpVersionCompare(aliasesMap[validVersion], validVersion) < 0 + ) { + versions.push(validVersion) + aliasesMap[validVersion] = version + } + } + }) + versions = versions.filter(version => !/^dev-/.test(version)) + + let badgeText = null + let badgeColor = null + + switch (info) { + case 'v': { + const stableVersions = versions.filter(phpStableVersion) + let stableVersion = phpLatestVersion(stableVersions) + if (!stableVersion) { + stableVersion = phpLatestVersion(versions) + } + //if (!!aliasesMap[stableVersion]) { + // stableVersion = aliasesMap[stableVersion]; + //} + badgeText = versionText(stableVersion) + badgeColor = versionColor(stableVersion) + break + } + case 'vpre': { + const unstableVersion = phpLatestVersion(versions) + //if (!!aliasesMap[unstableVersion]) { + // unstableVersion = aliasesMap[unstableVersion]; + //} + badgeText = versionText(unstableVersion) + badgeColor = 'orange' + break + } + } + + if (badgeText !== null) { + badgeData.text[1] = badgeText + badgeData.colorscheme = badgeColor + } + + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/php-eye/php-eye-hhvm.service.js b/services/php-eye/php-eye-hhvm.service.js new file mode 100644 index 0000000000..522f3314d7 --- /dev/null +++ b/services/php-eye/php-eye-hhvm.service.js @@ -0,0 +1,74 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { checkErrorResponse } = require('../../lib/error-helper') +const { omitv } = require('../../lib/text-formatters') + +module.exports = class PhpeyeHhvm extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/hhvm\/([^/]+\/[^/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const user = match[1] // eg, `symfony/symfony`. + let branch = match[2] ? omitv(match[2]) : 'dev-master' + const format = match[3] + const apiUrl = 'https://php-eye.com/api/v1/package/' + user + '.json' + const badgeData = getBadgeData('hhvm', data) + if (branch === 'master') { + branch = 'dev-master' + } + request(apiUrl, (err, res, buffer) => { + if ( + checkErrorResponse(badgeData, err, res, { 404: 'repo not found' }) + ) { + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(buffer) + let verInfo = {} + if (!data.versions) { + throw Error('Unexpected response.') + } + badgeData.text[1] = 'branch not found' + for (let i = 0, count = data.versions.length; i < count; i++) { + verInfo = data.versions[i] + if (verInfo.name === branch) { + if (!verInfo.travis.runtime_status) { + throw Error('Unexpected response.') + } + switch (verInfo.travis.runtime_status.hhvm) { + case 3: + // tested` + badgeData.colorscheme = 'brightgreen' + badgeData.text[1] = 'tested' + break + case 2: + // allowed failure + badgeData.colorscheme = 'yellow' + badgeData.text[1] = 'partially tested' + break + case 1: + // not tested + badgeData.colorscheme = 'red' + badgeData.text[1] = 'not tested' + break + case 0: + // unknown/no config file + badgeData.text[1] = 'maybe untested' + break + } + break + } + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/php-eye/php-eye-php-version.service.js b/services/php-eye/php-eye-php-version.service.js new file mode 100644 index 0000000000..10fcdb8203 --- /dev/null +++ b/services/php-eye/php-eye-php-version.service.js @@ -0,0 +1,89 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { + versionReduction: phpVersionReduction, + getPhpReleases, +} = require('../../lib/php-version') +const log = require('../../lib/log') + +module.exports = class PhpEyePhpVersion extends LegacyService { + static registerLegacyRouteHandler({ camp, cache, githubApiProvider }) { + camp.route( + /^\/php-eye\/([^/]+\/[^/]+)(?:\/([^/]+))?\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const userRepo = match[1] // eg, espadrine/sc + const version = match[2] || 'dev-master' + const format = match[3] + const options = { + method: 'GET', + uri: 'https://php-eye.com/api/v1/package/' + userRepo + '.json', + } + const badgeData = getBadgeData('PHP tested', data) + getPhpReleases(githubApiProvider, (err, phpReleases) => { + if (err != null) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + return + } + request(options, (err, res, buffer) => { + if (err !== null) { + log.error('PHP-Eye error: ' + err.stack) + if (res) { + log.error('' + res) + } + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + return + } + + try { + const data = JSON.parse(buffer) + const travis = data.versions.filter( + release => release.name === version + )[0].travis + + if (!travis.config_exists) { + badgeData.colorscheme = 'red' + badgeData.text[1] = 'not tested' + sendBadge(format, badgeData) + return + } + + const versions = [] + for (const index in travis.runtime_status) { + if ( + travis.runtime_status[index] === 3 && + index.match(/^php\d\d$/) !== null + ) { + versions.push(index.replace(/^php(\d)(\d)$/, '$1.$2')) + } + } + + let reduction = phpVersionReduction(versions, phpReleases) + + if (travis.runtime_status.hhvm === 3) { + reduction += reduction ? ', ' : '' + reduction += 'HHVM' + } + + if (reduction) { + badgeData.colorscheme = 'brightgreen' + badgeData.text[1] = reduction + } else if (!versions.length) { + badgeData.colorscheme = 'red' + badgeData.text[1] = 'not tested' + } else { + badgeData.text[1] = 'invalid' + } + } catch (e) { + badgeData.text[1] = 'invalid' + } + sendBadge(format, badgeData) + }) + }) + }) + ) + } +} diff --git a/services/pub/pub.service.js b/services/pub/pub.service.js new file mode 100644 index 0000000000..efd8528c5c --- /dev/null +++ b/services/pub/pub.service.js @@ -0,0 +1,42 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { addv: versionText } = require('../../lib/text-formatters') +const { version: versionColor } = require('../../lib/color-formatters') +const { latest: latestVersion } = require('../../lib/version') + +// For Dart's pub. +module.exports = class Pub extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/pub\/v(pre)?\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const includePre = Boolean(match[1]) + const userRepo = match[2] // eg, "box2d" + const format = match[3] + const apiUrl = 'https://pub.dartlang.org/packages/' + userRepo + '.json' + const badgeData = getBadgeData('pub', data) + request(apiUrl, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(buffer) + // Grab the latest stable version, or an unstable + const versions = data.versions + const version = latestVersion(versions, { pre: includePre }) + badgeData.text[1] = versionText(version) + badgeData.colorscheme = versionColor(version) + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/puppetforge/puppetforge-modules.service.js b/services/puppetforge/puppetforge-modules.service.js new file mode 100644 index 0000000000..2601c1f318 --- /dev/null +++ b/services/puppetforge/puppetforge-modules.service.js @@ -0,0 +1,86 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLabel: getLabel, +} = require('../../lib/badge-data') +const { metric, addv: versionText } = require('../../lib/text-formatters') +const { + version: versionColor, + coveragePercentage: coveragePercentageColor, + downloadCount: downloadCountColor, +} = require('../../lib/color-formatters') + +module.exports = class PuppetforgeModules extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/puppetforge\/([^/]+)\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const info = match[1] // either `v`, `dt`, `e` or `f` + const user = match[2] + const module = match[3] + const format = match[4] + const options = { + json: true, + uri: + 'https://forgeapi.puppetlabs.com/v3/modules/' + user + '-' + module, + } + const badgeData = getBadgeData('puppetforge', data) + request(options, (err, res, json) => { + if (err != null || (json.length !== undefined && json.length === 0)) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + if (info === 'v') { + if (json.current_release) { + const version = json.current_release.version + badgeData.text[1] = versionText(version) + badgeData.colorscheme = versionColor(version) + } else { + badgeData.text[1] = 'none' + badgeData.colorscheme = 'lightgrey' + } + } else if (info === 'dt') { + const total = json.downloads + badgeData.colorscheme = downloadCountColor(total) + badgeData.text[0] = getLabel('downloads', data) + badgeData.text[1] = metric(total) + } else if (info === 'e') { + const endorsement = json.endorsement + if (endorsement === 'approved') { + badgeData.colorscheme = 'green' + } else if (endorsement === 'supported') { + badgeData.colorscheme = 'brightgreen' + } else { + badgeData.colorscheme = 'red' + } + badgeData.text[0] = getLabel('endorsement', data) + if (endorsement != null) { + badgeData.text[1] = endorsement + } else { + badgeData.text[1] = 'none' + } + } else if (info === 'f') { + const feedback = json.feedback_score + badgeData.text[0] = getLabel('score', data) + if (feedback != null) { + badgeData.text[1] = feedback + '%' + badgeData.colorscheme = coveragePercentageColor(feedback) + } else { + badgeData.text[1] = 'unknown' + badgeData.colorscheme = 'lightgrey' + } + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/puppetforge/puppetforge-users.service.js b/services/puppetforge/puppetforge-users.service.js new file mode 100644 index 0000000000..cab8abf23f --- /dev/null +++ b/services/puppetforge/puppetforge-users.service.js @@ -0,0 +1,51 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLabel: getLabel, +} = require('../../lib/badge-data') +const { metric } = require('../../lib/text-formatters') +const { floorCount: floorCountColor } = require('../../lib/color-formatters') + +module.exports = class PuppetforgeUsers extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/puppetforge\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const info = match[1] // either `rc` or `mc` + const user = match[2] + const format = match[3] + const options = { + json: true, + uri: 'https://forgeapi.puppetlabs.com/v3/users/' + user, + } + const badgeData = getBadgeData('puppetforge', data) + request(options, (err, res, json) => { + if (err != null || (json.length !== undefined && json.length === 0)) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + if (info === 'rc') { + const releases = json.release_count + badgeData.colorscheme = floorCountColor(releases, 10, 50, 100) + badgeData.text[0] = getLabel('releases', data) + badgeData.text[1] = metric(releases) + } else if (info === 'mc') { + const modules = json.module_count + badgeData.colorscheme = floorCountColor(modules, 5, 10, 50) + badgeData.text[0] = getLabel('modules', data) + badgeData.text[1] = metric(modules) + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/readthedocs/readthedocs.service.js b/services/readthedocs/readthedocs.service.js new file mode 100644 index 0000000000..9da36fa8b5 --- /dev/null +++ b/services/readthedocs/readthedocs.service.js @@ -0,0 +1,49 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { fetchFromSvg } = require('../../lib/svg-badge-parser') + +module.exports = class ReadTheDocs extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/readthedocs\/([^/]+)(?:\/(.+))?.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const project = match[1] + const version = match[2] + const format = match[3] + const badgeData = getBadgeData('docs', data) + let url = + 'https://readthedocs.org/projects/' + + encodeURIComponent(project) + + '/badge/' + if (version != null) { + url += '?version=' + encodeURIComponent(version) + } + fetchFromSvg(request, url, (err, res) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + badgeData.text[1] = res + if (res === 'passing') { + badgeData.colorscheme = 'brightgreen' + } else if (res === 'failing') { + badgeData.colorscheme = 'red' + } else if (res === 'unknown') { + badgeData.colorscheme = 'yellow' + } else { + badgeData.colorscheme = 'red' + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/redmine/redmine.service.js b/services/redmine/redmine.service.js new file mode 100644 index 0000000000..67024b77a1 --- /dev/null +++ b/services/redmine/redmine.service.js @@ -0,0 +1,55 @@ +'use strict' + +const xml2js = require('xml2js') +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { starRating } = require('../../lib/text-formatters') +const { floorCount: floorCountColor } = require('../../lib/color-formatters') + +module.exports = class Redmine extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/redmine\/plugin\/(rating|stars)\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const type = match[1] + const plugin = match[2] + const format = match[3] + const options = { + method: 'GET', + uri: 'https://www.redmine.org/plugins/' + plugin + '.xml', + } + + const badgeData = getBadgeData(type, data) + request(options, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + + // eslint-disable-next-line handle-callback-err + xml2js.parseString(buffer.toString(), (err, data) => { + try { + const rating = data['redmine-plugin']['ratings-average'][0]._ + badgeData.colorscheme = floorCountColor(rating, 2, 3, 4) + + switch (type) { + case 'rating': + badgeData.text[1] = rating + '/5.0' + break + case 'stars': + badgeData.text[1] = starRating(Math.round(rating)) + break + } + + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + }) + ) + } +} diff --git a/services/requires/requires.service.js b/services/requires/requires.service.js new file mode 100644 index 0000000000..0d23f3e911 --- /dev/null +++ b/services/requires/requires.service.js @@ -0,0 +1,53 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { checkErrorResponse } = require('../../lib/error-helper') + +module.exports = class RequiresIo extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/requires\/([^/]+\/[^/]+\/[^/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const userRepo = match[1] // eg, `github/celery/celery`. + const branch = match[2] + const format = match[3] + let uri = 'https://requires.io/api/v1/status/' + userRepo + if (branch != null) { + uri += '?branch=' + branch + } + const options = { + method: 'GET', + uri: uri, + } + const badgeData = getBadgeData('requirements', data) + request(options, (err, res, buffer) => { + if (checkErrorResponse(badgeData, err, res)) { + sendBadge(format, badgeData) + return + } + try { + const json = JSON.parse(buffer) + if (json.status === 'up-to-date') { + badgeData.text[1] = 'up to date' + badgeData.colorscheme = 'brightgreen' + } else if (json.status === 'outdated') { + badgeData.text[1] = 'outdated' + badgeData.colorscheme = 'yellow' + } else if (json.status === 'insecure') { + badgeData.text[1] = 'insecure' + badgeData.colorscheme = 'red' + } else { + badgeData.text[1] = 'unknown' + badgeData.colorscheme = 'lightgrey' + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/scrutinizer/scrutinizer.service.js b/services/scrutinizer/scrutinizer.service.js new file mode 100644 index 0000000000..249298cc5f --- /dev/null +++ b/services/scrutinizer/scrutinizer.service.js @@ -0,0 +1,87 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { checkErrorResponse } = require('../../lib/error-helper') +const { + coveragePercentage: coveragePercentageColor, +} = require('../../lib/color-formatters') + +module.exports = class Scrutinizer extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/scrutinizer(?:\/(build|coverage))?\/([^/]+\/[^/]+\/[^/]+|gp\/[^/])(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const type = match[1] ? match[1] : 'code quality' + const repo = match[2] // eg, g/phpmyadmin/phpmyadmin + let branch = match[3] + const format = match[4] + const apiUrl = `https://scrutinizer-ci.com/api/repositories/${repo}` + const badgeData = getBadgeData(type, data) + request(apiUrl, {}, (err, res, buffer) => { + if ( + checkErrorResponse(badgeData, err, res, { + 404: 'project or branch not found', + }) + ) { + sendBadge(format, badgeData) + return + } + try { + const parsedData = JSON.parse(buffer) + // Which branch are we dealing with? + if (branch === undefined) { + branch = parsedData.default_branch + } + if (type === 'coverage') { + const percentage = + parsedData.applications[branch].index._embedded.project + .metric_values['scrutinizer.test_coverage'] * 100 + if (isNaN(percentage)) { + badgeData.text[1] = 'unknown' + badgeData.colorscheme = 'gray' + } else { + badgeData.text[1] = percentage.toFixed(0) + '%' + badgeData.colorscheme = coveragePercentageColor(percentage) + } + } else if (type === 'build') { + const status = parsedData.applications[branch].build_status.status + badgeData.text[1] = status + if (status === 'passed') { + badgeData.colorscheme = 'brightgreen' + badgeData.text[1] = 'passing' + } else if (status === 'failed' || status === 'error') { + badgeData.colorscheme = 'red' + } else if (status === 'pending') { + badgeData.colorscheme = 'orange' + } else if (status === 'unknown') { + badgeData.colorscheme = 'gray' + } + } else { + let score = + parsedData.applications[branch].index._embedded.project + .metric_values['scrutinizer.quality'] + score = Math.round(score * 100) / 100 + badgeData.text[1] = score + if (score > 9) { + badgeData.colorscheme = 'brightgreen' + } else if (score > 7) { + badgeData.colorscheme = 'green' + } else if (score > 5) { + badgeData.colorscheme = 'yellow' + } else if (score > 4) { + badgeData.colorscheme = 'orange' + } else { + badgeData.colorscheme = 'red' + } + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/sensiolabs/sensiolabs.service.js b/services/sensiolabs/sensiolabs.service.js new file mode 100644 index 0000000000..6abcd8ca55 --- /dev/null +++ b/services/sensiolabs/sensiolabs.service.js @@ -0,0 +1,85 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const serverSecrets = require('../../lib/server-secrets') + +module.exports = class Sensiolabs extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/sensiolabs\/i\/([^/]+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const projectUuid = match[1] + const format = match[2] + const options = { + method: 'GET', + uri: 'https://insight.sensiolabs.com/api/projects/' + projectUuid, + headers: { + Accept: 'application/vnd.com.sensiolabs.insight+xml', + }, + } + + if (serverSecrets && serverSecrets.sl_insight_userUuid) { + options.auth = { + user: serverSecrets.sl_insight_userUuid, + pass: serverSecrets.sl_insight_apiToken, + } + } + + const badgeData = getBadgeData('check', data) + + request(options, (err, res, body) => { + if (err != null || res.statusCode !== 200) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + + const matchStatus = body.match( + /<status><!\[CDATA\[([a-z]+)\]\]><\/status>/im + ) + const matchGrade = body.match( + /<grade><!\[CDATA\[([a-z]+)\]\]><\/grade>/im + ) + + if (matchStatus === null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } else if (matchStatus[1] !== 'finished') { + badgeData.text[1] = 'pending' + sendBadge(format, badgeData) + return + } else if (matchGrade === null) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + return + } + + if (matchGrade[1] === 'platinum') { + badgeData.text[1] = 'platinum' + badgeData.colorscheme = 'brightgreen' + } else if (matchGrade[1] === 'gold') { + badgeData.text[1] = 'gold' + badgeData.colorscheme = 'yellow' + } else if (matchGrade[1] === 'silver') { + badgeData.text[1] = 'silver' + badgeData.colorscheme = 'lightgrey' + } else if (matchGrade[1] === 'bronze') { + badgeData.text[1] = 'bronze' + badgeData.colorscheme = 'orange' + } else if (matchGrade[1] === 'none') { + badgeData.text[1] = 'no medal' + badgeData.colorscheme = 'red' + } else { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + return + } + + sendBadge(format, badgeData) + }) + }) + ) + } +} diff --git a/services/snap-ci/snap-ci.service.js b/services/snap-ci/snap-ci.service.js new file mode 100644 index 0000000000..6f3076c69c --- /dev/null +++ b/services/snap-ci/snap-ci.service.js @@ -0,0 +1,17 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { getDeprecatedBadge } = require('../../lib/deprecation-helpers') + +module.exports = class SnapCi extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/snap(-ci?)\/([^/]+\/[^/]+)(?:\/(.+))\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const format = match[4] + const badgeData = getDeprecatedBadge('snap CI', data) + sendBadge(format, badgeData) + }) + ) + } +} diff --git a/services/sonarqube/sonarqube.service.js b/services/sonarqube/sonarqube.service.js new file mode 100644 index 0000000000..5036cb2adb --- /dev/null +++ b/services/sonarqube/sonarqube.service.js @@ -0,0 +1,163 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const serverSecrets = require('../../lib/server-secrets') +const { metric } = require('../../lib/text-formatters') +const { + coveragePercentage: coveragePercentageColor, +} = require('../../lib/color-formatters') + +module.exports = class Sonarqube extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/sonar\/?([0-9.]+)?\/(http|https)\/(.*)\/(.*)\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const version = parseFloat(match[1]) + const scheme = match[2] + const serverUrl = match[3] + const buildType = match[4] + const metricName = match[5] + const format = match[6] + + let sonarMetricName = metricName + if (metricName === 'tech_debt') { + //special condition for backwards compatibility + sonarMetricName = 'sqale_debt_ratio' + } + + const useLegacyApi = !!version && version < 5.4 + + const uri = useLegacyApi + ? scheme + + '://' + + serverUrl + + '/api/resources?resource=' + + buildType + + '&depth=0&metrics=' + + encodeURIComponent(sonarMetricName) + + '&includetrends=true' + : scheme + + '://' + + serverUrl + + '/api/measures/component?componentKey=' + + buildType + + '&metricKeys=' + + encodeURIComponent(sonarMetricName) + + const options = { + uri, + headers: { + Accept: 'application/json', + }, + } + if (serverSecrets && serverSecrets.sonarqube_token) { + options.auth = { + user: serverSecrets.sonarqube_token, + } + } + + const badgeData = getBadgeData(metricName.replace(/_/g, ' '), data) + + request(options, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(buffer) + + const value = parseInt( + useLegacyApi + ? data[0].msr[0].val + : data.component.measures[0].value + ) + + if (value === undefined) { + badgeData.text[1] = 'unknown' + sendBadge(format, badgeData) + return + } + + if (metricName.indexOf('coverage') !== -1) { + badgeData.text[1] = value.toFixed(0) + '%' + badgeData.colorscheme = coveragePercentageColor(value) + } else if (/^\w+_violations$/.test(metricName)) { + badgeData.text[1] = value + badgeData.colorscheme = 'brightgreen' + if (value > 0) { + if (metricName === 'blocker_violations') { + badgeData.colorscheme = 'red' + } else if (metricName === 'critical_violations') { + badgeData.colorscheme = 'orange' + } else if (metricName === 'major_violations') { + badgeData.colorscheme = 'yellow' + } else if (metricName === 'minor_violations') { + badgeData.colorscheme = 'yellowgreen' + } else if (metricName === 'info_violations') { + badgeData.colorscheme = 'green' + } + } + } else if (metricName === 'fortify-security-rating') { + badgeData.text[1] = value + '/5' + + if (value === 0) { + badgeData.colorscheme = 'red' + } else if (value === 1) { + badgeData.colorscheme = 'orange' + } else if (value === 2) { + badgeData.colorscheme = 'yellow' + } else if (value === 3) { + badgeData.colorscheme = 'yellowgreen' + } else if (value === 4) { + badgeData.colorscheme = 'green' + } else if (value === 5) { + badgeData.colorscheme = 'brightgreen' + } else { + badgeData.colorscheme = 'lightgrey' + } + } else if ( + metricName === 'sqale_debt_ratio' || + metricName === 'tech_debt' || + metricName === 'public_documented_api_density' + ) { + // colors are based on sonarqube default rating grid and display colors + // [0,0.1) ==> A (green) + // [0.1,0.2) ==> B (yellowgreen) + // [0.2,0.5) ==> C (yellow) + // [0.5,1) ==> D (orange) + // [1,) ==> E (red) + let colorValue = value + if (metricName === 'public_documented_api_density') { + //Some metrics higher % is better + colorValue = 100 - value + } + badgeData.text[1] = value + '%' + if (colorValue >= 100) { + badgeData.colorscheme = 'red' + } else if (colorValue >= 50) { + badgeData.colorscheme = 'orange' + } else if (colorValue >= 20) { + badgeData.colorscheme = 'yellow' + } else if (colorValue >= 10) { + badgeData.colorscheme = 'yellowgreen' + } else if (colorValue >= 0) { + badgeData.colorscheme = 'brightgreen' + } else { + badgeData.colorscheme = 'lightgrey' + } + } else { + badgeData.text[1] = metric(value) + badgeData.colorscheme = 'brightgreen' + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/sourceforge/sourceforge.service.js b/services/sourceforge/sourceforge.service.js new file mode 100644 index 0000000000..b671db432e --- /dev/null +++ b/services/sourceforge/sourceforge.service.js @@ -0,0 +1,77 @@ +'use strict' + +const moment = require('moment') +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLabel: getLabel, +} = require('../../lib/badge-data') +const { metric } = require('../../lib/text-formatters') +const { + downloadCount: downloadCountColor, +} = require('../../lib/color-formatters') + +module.exports = class Sourceforge extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/sourceforge\/(dt|dm|dw|dd)\/([^/]*)\/?(.*).(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const info = match[1] // eg, 'dm' + const project = match[2] // eg, 'sevenzip`. + const folder = match[3] + const format = match[4] + let apiUrl = + 'http://sourceforge.net/projects/' + + project + + '/files/' + + folder + + '/stats/json' + const badgeData = getBadgeData('sourceforge', data) + let timePeriod, startDate + badgeData.text[0] = getLabel('downloads', data) + // get yesterday since today is incomplete + const endDate = moment().subtract(24, 'hours') + switch (info.charAt(1)) { + case 'm': + startDate = moment(endDate).subtract(30, 'days') + timePeriod = '/month' + break + case 'w': + startDate = moment(endDate).subtract(6, 'days') // 6, since date range is inclusive + timePeriod = '/week' + break + case 'd': + startDate = endDate + timePeriod = '/day' + break + case 't': + startDate = moment(0) + timePeriod = '' + break + } + apiUrl += + '?start_date=' + + startDate.format('YYYY-MM-DD') + + '&end_date=' + + endDate.format('YYYY-MM-DD') + request(apiUrl, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(buffer) + const downloads = data.total + badgeData.text[1] = metric(downloads) + timePeriod + badgeData.colorscheme = downloadCountColor(downloads) + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/sourcegraph/sourcegraph.service.js b/services/sourcegraph/sourcegraph.service.js new file mode 100644 index 0000000000..bebc12a3f7 --- /dev/null +++ b/services/sourcegraph/sourcegraph.service.js @@ -0,0 +1,35 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') + +module.exports = class Sourcegraph extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/sourcegraph\/rrc\/([\s\S]+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const repo = match[1] + const format = match[2] + const apiUrl = + 'https://sourcegraph.com/.api/repos/' + repo + '/-/shield' + const badgeData = getBadgeData('used by', data) + request(apiUrl, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + badgeData.colorscheme = 'brightgreen' + const data = JSON.parse(buffer) + badgeData.text[1] = data.value + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/stackexchange/stackexchange.service.js b/services/stackexchange/stackexchange.service.js new file mode 100644 index 0000000000..b7b8dace01 --- /dev/null +++ b/services/stackexchange/stackexchange.service.js @@ -0,0 +1,66 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLabel: getLabel, +} = require('../../lib/badge-data') +const { metric } = require('../../lib/text-formatters') +const { floorCount: floorCountColor } = require('../../lib/color-formatters') + +module.exports = class StackExchange extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/stackexchange\/([^/]+)\/([^/])\/([^/]+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const site = match[1] // eg, stackoverflow + const info = match[2] // either `r` + const item = match[3] // eg, 232250 + const format = match[4] + let path + if (info === 'r') { + path = 'users/' + item + } else if (info === 't') { + path = 'tags/' + item + '/info' + } + const options = { + method: 'GET', + uri: 'https://api.stackexchange.com/2.2/' + path + '?site=' + site, + gzip: true, + } + const badgeData = getBadgeData(site, data) + request(options, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + const parsedData = JSON.parse(buffer.toString()) + + // IP rate limiting + if (parsedData.error_name === 'throttle_violation') { + return // Hope for the best in the cache. + } + + if (info === 'r') { + const reputation = parsedData.items[0].reputation + badgeData.text[0] = getLabel(site + ' reputation', data) + badgeData.text[1] = metric(reputation) + badgeData.colorscheme = floorCountColor(1000, 10000, 20000) + } else if (info === 't') { + const count = parsedData.items[0].count + badgeData.text[0] = getLabel(`${site} ${item} questions`, data) + badgeData.text[1] = metric(count) + badgeData.colorscheme = floorCountColor(1000, 10000, 20000) + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/swagger/swagger.service.js b/services/swagger/swagger.service.js new file mode 100644 index 0000000000..753c0fa8f1 --- /dev/null +++ b/services/swagger/swagger.service.js @@ -0,0 +1,67 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') + +// For a Swagger Validator. +module.exports = class Swagger extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/swagger\/(valid)\/(2\.0)\/(https?)\/(.+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + // match[1] is not used // e.g. `valid` for validate + // match[2] is reserved for future use // e.g. `2.0` for OpenAPI 2.0 + const scheme = match[3] // e.g. `https` + const swaggerUrl = match[4] // e.g. `api.example.com/swagger.yaml` + const format = match[5] + + const badgeData = getBadgeData('swagger', data) + + const urlParam = encodeURIComponent(scheme + '://' + swaggerUrl) + const url = 'http://online.swagger.io/validator/debug?url=' + urlParam + const options = { + method: 'GET', + url: url, + gzip: true, + json: true, + } + request(options, (err, res, json) => { + try { + if ( + err != null || + res.statusCode >= 500 || + typeof json !== 'object' + ) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + + const messages = json.schemaValidationMessages + if (messages == null || messages.length === 0) { + badgeData.colorscheme = 'brightgreen' + badgeData.text[1] = 'valid' + } else { + badgeData.colorscheme = 'red' + + const firstMessage = messages[0] + if ( + messages.length === 1 && + firstMessage.level === 'error' && + /^Can't read from/.test(firstMessage.message) + ) { + badgeData.text[1] = 'not found' + } else { + badgeData.text[1] = 'invalid' + } + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/teamcity/teamcity-build.service.js b/services/teamcity/teamcity-build.service.js new file mode 100644 index 0000000000..74608b9939 --- /dev/null +++ b/services/teamcity/teamcity-build.service.js @@ -0,0 +1,91 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') + +function teamcityBadge( + url, + buildId, + advanced, + format, + data, + sendBadge, + request +) { + const apiUrl = url + '/app/rest/builds/buildType:(id:' + buildId + ')?guest=1' + const badgeData = getBadgeData('build', data) + request( + apiUrl, + { headers: { Accept: 'application/json' } }, + (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(buffer) + if (advanced) + badgeData.text[1] = ( + data.statusText || + data.status || + '' + ).toLowerCase() + else badgeData.text[1] = (data.status || '').toLowerCase() + if (data.status === 'SUCCESS') { + badgeData.colorscheme = 'brightgreen' + badgeData.text[1] = 'passing' + } else { + badgeData.colorscheme = 'red' + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + } + ) +} + +module.exports = class TeamcityBuild extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + // Old url for CodeBetter TeamCity instance. + camp.route( + /^\/teamcity\/codebetter\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const buildType = match[1] // eg, `bt428`. + const format = match[2] + teamcityBadge( + 'http://teamcity.codebetter.com', + buildType, + false, + format, + data, + sendBadge, + request + ) + }) + ) + + // Generic TeamCity instance + camp.route( + /^\/teamcity\/(http|https)\/(.*)\/(s|e)\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const scheme = match[1] + const serverUrl = match[2] + const advanced = match[3] === 'e' + const buildType = match[4] // eg, `bt428`. + const format = match[5] + teamcityBadge( + scheme + '://' + serverUrl, + buildType, + advanced, + format, + data, + sendBadge, + request + ) + }) + ) + } +} diff --git a/services/teamcity/teamcity-coverage.service.js b/services/teamcity/teamcity-coverage.service.js new file mode 100644 index 0000000000..b8511b368f --- /dev/null +++ b/services/teamcity/teamcity-coverage.service.js @@ -0,0 +1,63 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { + coveragePercentage: coveragePercentageColor, +} = require('../../lib/color-formatters') + +// TeamCity CodeBetter code coverage. +module.exports = class TeamcityCoverage extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/teamcity\/coverage\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const buildType = match[1] // eg, `bt428`. + const format = match[2] + const apiUrl = + 'http://teamcity.codebetter.com/app/rest/builds/buildType:(id:' + + buildType + + ')/statistics?guest=1' + const badgeData = getBadgeData('coverage', data) + request( + apiUrl, + { headers: { Accept: 'application/json' } }, + (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(buffer) + let covered + let total + + data.property.forEach(property => { + if (property.name === 'CodeCoverageAbsSCovered') { + covered = property.value + } else if (property.name === 'CodeCoverageAbsSTotal') { + total = property.value + } + }) + + if (covered === undefined || total === undefined) { + badgeData.text[1] = 'malformed' + sendBadge(format, badgeData) + return + } + + const percentage = (covered / total) * 100 + badgeData.text[1] = percentage.toFixed(0) + '%' + badgeData.colorscheme = coveragePercentageColor(percentage) + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + } + ) + }) + ) + } +} diff --git a/services/travis/travis-build.service.js b/services/travis/travis-build.service.js new file mode 100644 index 0000000000..d3c43af9d2 --- /dev/null +++ b/services/travis/travis-build.service.js @@ -0,0 +1,63 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { checkErrorResponse } = require('../../lib/error-helper') +const log = require('../../lib/log') + +// Handle .org and .com. +module.exports = class TravisBuild extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/travis(-ci)?\/(?:(com)\/)?(?!php-v)([^/]+\/[^/]+)(?:\/(.+))?\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const travisDomain = match[2] || 'org' // (com | org) org by default + const userRepo = match[3] // eg, espadrine/sc + const branch = match[4] + const format = match[5] + const options = { + method: 'HEAD', + uri: `https://api.travis-ci.${travisDomain}/${userRepo}.svg`, + } + if (branch != null) { + options.uri += `?branch=${branch}` + } + const badgeData = getBadgeData('build', data) + request(options, (err, res) => { + if (err != null) { + log.error( + 'Travis error: data:' + + JSON.stringify(data) + + '\nStack: ' + + err.stack + ) + if (res) { + log.error('' + res) + } + } + if (checkErrorResponse(badgeData, err, res)) { + sendBadge(format, badgeData) + return + } + try { + const state = res.headers['content-disposition'].match( + /filename="(.+)\.svg"/ + )[1] + badgeData.text[1] = state + if (state === 'passing') { + badgeData.colorscheme = 'brightgreen' + } else if (state === 'failing') { + badgeData.colorscheme = 'red' + } else { + badgeData.text[1] = state + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/travis/travis-php-version.service.js b/services/travis/travis-php-version.service.js new file mode 100644 index 0000000000..07eeb62907 --- /dev/null +++ b/services/travis/travis-php-version.service.js @@ -0,0 +1,85 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const log = require('../../lib/log') +const { + minorVersion: phpMinorVersion, + versionReduction: phpVersionReduction, + getPhpReleases, +} = require('../../lib/php-version') + +module.exports = class TravisPhpVersion extends LegacyService { + static registerLegacyRouteHandler({ camp, cache, githubApiProvider }) { + camp.route( + /^\/travis(?:-ci)?\/php-v\/([^/]+\/[^/]+)(?:\/([^/]+))?\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const userRepo = match[1] // eg, espadrine/sc + const version = match[2] || 'master' + const format = match[3] + const options = { + method: 'GET', + uri: `https://api.travis-ci.org/repos/${userRepo}/branches/${version}`, + } + const badgeData = getBadgeData('PHP', data) + getPhpReleases(githubApiProvider, (err, phpReleases) => { + if (err != null) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + return + } + request(options, (err, res, buffer) => { + if (err !== null) { + log.error(`Travis CI error: ${err.stack}`) + if (res) { + log.error('' + res) + } + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + return + } + + try { + const data = JSON.parse(buffer) + let travisVersions = [] + + // from php + if (typeof data.branch.config.php !== 'undefined') { + travisVersions = travisVersions.concat( + data.branch.config.php.map(v => v.toString()) + ) + } + // from matrix + if (typeof data.branch.config.matrix.include !== 'undefined') { + travisVersions = travisVersions.concat( + data.branch.config.matrix.include.map(v => v.php.toString()) + ) + } + + const hasHhvm = travisVersions.find(v => v.startsWith('hhvm')) + const versions = travisVersions + .map(v => phpMinorVersion(v)) + .filter(v => v.indexOf('.') !== -1) + let reduction = phpVersionReduction(versions, phpReleases) + + if (hasHhvm) { + reduction += reduction ? ', ' : '' + reduction += 'HHVM' + } + + if (reduction) { + badgeData.colorscheme = 'blue' + badgeData.text[1] = reduction + } else { + badgeData.text[1] = 'invalid' + } + } catch (e) { + badgeData.text[1] = 'invalid' + } + sendBadge(format, badgeData) + }) + }) + }) + ) + } +} diff --git a/services/twitter/twitter.service.js b/services/twitter/twitter.service.js new file mode 100644 index 0000000000..2be5497804 --- /dev/null +++ b/services/twitter/twitter.service.js @@ -0,0 +1,83 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLogo: getLogo, +} = require('../../lib/badge-data') +const { metric } = require('../../lib/text-formatters') + +module.exports = class Twitter extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + // Twitter integration. + camp.route( + /^\/twitter\/url\/([^/]+)\/(.+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const scheme = match[1] // eg, https + const path = match[2] // eg, shields.io + const format = match[3] + const page = encodeURIComponent(scheme + '://' + path) + // The URL API died: #568. + //var url = 'http://cdn.api.twitter.com/1/urls/count.json?url=' + page; + const badgeData = getBadgeData('tweet', data) + if (badgeData.template === 'social') { + badgeData.logo = getLogo('twitter', data) + badgeData.links = [ + 'https://twitter.com/intent/tweet?text=Wow:&url=' + page, + 'https://twitter.com/search?q=' + page, + ] + } + badgeData.text[1] = '' + badgeData.colorscheme = null + badgeData.colorB = data.colorB || '#55ACEE' + sendBadge(format, badgeData) + }) + ) + + // Twitter follow badge. + camp.route( + /^\/twitter\/follow\/@?([^/]+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const user = match[1] // eg, shields_io + const format = match[2] + const options = { + url: + 'http://cdn.syndication.twimg.com/widgets/followbutton/info.json?screen_names=' + + user, + } + const badgeData = getBadgeData('Follow @' + user, data) + + badgeData.colorscheme = null + badgeData.colorB = '#55ACEE' + if (badgeData.template === 'social') { + badgeData.logo = getLogo('twitter', data) + } + badgeData.links = [ + 'https://twitter.com/intent/follow?screen_name=' + user, + 'https://twitter.com/' + user + '/followers', + ] + badgeData.text[1] = '' + request(options, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + // The data is formatted as an array. + const data = JSON.parse(buffer)[0] + if (data === undefined) { + badgeData.text[1] = 'invalid user' + } else if (data.followers_count != null) { + // data.followers_count could be zero… don't just check if falsey. + badgeData.text[1] = metric(data.followers_count) + } + } catch (e) { + badgeData.text[1] = 'invalid' + } + sendBadge(format, badgeData) + }) + }) + ) + } +} diff --git a/services/vaadin-directory/vaadin-directory.service.js b/services/vaadin-directory/vaadin-directory.service.js new file mode 100644 index 0000000000..8a02b1369b --- /dev/null +++ b/services/vaadin-directory/vaadin-directory.service.js @@ -0,0 +1,105 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLabel: getLabel, +} = require('../../lib/badge-data') +const { checkErrorResponse } = require('../../lib/error-helper') +const { metric, starRating, formatDate } = require('../../lib/text-formatters') +const { + floorCount: floorCountColor, + age: ageColor, +} = require('../../lib/color-formatters') + +module.exports = class VaadinDirectory extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/vaadin-directory\/(star|stars|status|rating|rc|rating-count|v|version|rd|release-date)\/(.*).(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const type = match[1] // Field required + const urlIdentifier = match[2] // Name of repository + const format = match[3] // Format + // API URL which contains also authentication info + const apiUrl = + 'https://vaadin.com/vaadincom/directory-service/components/search/findByUrlIdentifier?projection=summary&urlIdentifier=' + + urlIdentifier + + // Set left-side text to 'Vaadin-Directory' by default + const badgeData = getBadgeData('Vaadin Directory', data) + request(apiUrl, (err, res, buffer) => { + if (checkErrorResponse(badgeData, err, res)) { + sendBadge(format, badgeData) + return + } + + try { + const data = JSON.parse(buffer) + // Round the rating to 1 points decimal + const rating = (Math.round(data.averageRating * 10) / 10).toFixed(1) + const ratingCount = data.ratingCount + const lv = data.latestAvailableRelease.name.toLowerCase() + const ld = data.latestAvailableRelease.publicationDate + switch (type) { + // Since the first deploy was with `star`, I put the case there + // for safety pre-caution + case 'star': + case 'stars': // Stars + badgeData.text[0] = getLabel('stars', data) + badgeData.text[1] = starRating(rating) + badgeData.colorscheme = floorCountColor(rating, 2, 3, 4) + break + case 'status': { + // Status of the component + const isPublished = data.status.toLowerCase() + if (isPublished === 'published') { + badgeData.text[1] = 'published' + badgeData.colorB = '#00b4f0' + } else { + badgeData.text[1] = 'unpublished' + } + break + } + case 'rating': // rating + badgeData.text[0] = getLabel('rating', data) + if (!isNaN(rating)) { + badgeData.text[1] = rating + '/5' + badgeData.colorscheme = floorCountColor(rating, 2, 3, 4) + } + break + case 'rc': // rating count + case 'rating-count': + badgeData.text[0] = getLabel('rating count', data) + if (ratingCount && ratingCount !== 0) { + badgeData.text[1] = metric(data.ratingCount) + ' total' + badgeData.colorscheme = floorCountColor( + data.ratingCount, + 5, + 50, + 500 + ) + } + break + case 'v': // latest version + case 'version': + badgeData.text[0] = getLabel('latest ver', data) + badgeData.text[1] = lv + badgeData.colorB = '#00b4f0' + break + case 'rd': + case 'release-date': // The release date of the latest version + badgeData.text[0] = getLabel('latest release date', data) + badgeData.text[1] = formatDate(ld) + badgeData.colorscheme = ageColor(ld) + break + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/versioneye/versioneye.service.js b/services/versioneye/versioneye.service.js new file mode 100644 index 0000000000..9524027dcd --- /dev/null +++ b/services/versioneye/versioneye.service.js @@ -0,0 +1,18 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { getDeprecatedBadge } = require('../../lib/deprecation-helpers') + +// VersionEye integration - deprecated as of August 2018. +module.exports = class VersionEye extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/versioneye\/d\/(.+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const format = match[2] + const badgeData = getDeprecatedBadge('versioneye', data) + sendBadge(format, badgeData) + }) + ) + } +} diff --git a/services/vscode-marketplace/vscode-marketplace.service.js b/services/vscode-marketplace/vscode-marketplace.service.js new file mode 100644 index 0000000000..31ad511001 --- /dev/null +++ b/services/vscode-marketplace/vscode-marketplace.service.js @@ -0,0 +1,118 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLabel: getLabel, +} = require('../../lib/badge-data') +const { metric, starRating } = require('../../lib/text-formatters') +const { + floorCount: floorCountColor, + downloadCount: downloadCountColor, +} = require('../../lib/color-formatters') +const { addv: versionText } = require('../../lib/text-formatters') +const { version: versionColor } = require('../../lib/color-formatters') + +//To generate API request Options for VS Code marketplace +function getVscodeApiReqOptions(packageName) { + return { + method: 'POST', + url: + 'https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery/', + headers: { + accept: 'application/json;api-version=3.0-preview.1', + 'content-type': 'application/json', + }, + body: { + filters: [ + { + criteria: [{ filterType: 7, value: packageName }], + }, + ], + flags: 914, + }, + json: true, + } +} + +//To extract Statistics (Install/Rating/RatingCount) from respose object for vscode marketplace +function getVscodeStatistic(data, statisticName) { + const statistics = data.results[0].extensions[0].statistics + try { + const statistic = statistics.find( + x => x.statisticName.toLowerCase() === statisticName.toLowerCase() + ) + return statistic.value + } catch (err) { + return 0 //In case required statistic is not found means ZERO. + } +} + +module.exports = class VscodeMarketplace extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/vscode-marketplace\/(d|v|r|stars)\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const reqType = match[1] // eg, d/v/r + const repo = match[2] // eg, `ritwickdey.LiveServer`. + const format = match[3] + + const badgeData = getBadgeData('vscode-marketplace', data) //temporary name + const options = getVscodeApiReqOptions(repo) + + request(options, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + + try { + switch (reqType) { + case 'd': { + badgeData.text[0] = getLabel('downloads', data) + const count = getVscodeStatistic(buffer, 'install') + badgeData.text[1] = metric(count) + badgeData.colorscheme = downloadCountColor(count) + break + } + case 'r': { + badgeData.text[0] = getLabel('rating', data) + const rate = getVscodeStatistic( + buffer, + 'averagerating' + ).toFixed(2) + const totalrate = getVscodeStatistic(buffer, 'ratingcount') + badgeData.text[1] = rate + '/5 (' + totalrate + ')' + badgeData.colorscheme = floorCountColor(rate, 2, 3, 4) + break + } + case 'stars': { + badgeData.text[0] = getLabel('rating', data) + const rating = getVscodeStatistic( + buffer, + 'averagerating' + ).toFixed(2) + badgeData.text[1] = starRating(rating) + badgeData.colorscheme = floorCountColor(rating, 2, 3, 4) + break + } + case 'v': { + badgeData.text[0] = getLabel('visual studio marketplace', data) + const version = + buffer.results[0].extensions[0].versions[0].version + badgeData.text[1] = versionText(version) + badgeData.colorscheme = versionColor(version) + break + } + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/vso/vso.service.js b/services/vso/vso.service.js new file mode 100644 index 0000000000..93a07d27e0 --- /dev/null +++ b/services/vso/vso.service.js @@ -0,0 +1,50 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { fetchFromSvg } = require('../../lib/svg-badge-parser') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') + +// For Visual Studio Team Services build. +module.exports = class Vso extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/vso\/build\/([^/]+)\/([^/]+)\/([^/]+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const name = match[1] // User name + const project = match[2] // Project ID, e.g. 953a34b9-5966-4923-a48a-c41874cfb5f5 + const build = match[3] // Build definition ID, e.g. 1 + const format = match[4] + const url = + 'https://' + + name + + '.visualstudio.com/DefaultCollection/_apis/public/build/definitions/' + + project + + '/' + + build + + '/badge' + const badgeData = getBadgeData('build', data) + fetchFromSvg(request, url, (err, res) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + badgeData.text[1] = res.toLowerCase() + if (res === 'succeeded') { + badgeData.colorscheme = 'brightgreen' + badgeData.text[1] = 'passing' + } else if (res === 'failed') { + badgeData.colorscheme = 'red' + badgeData.text[1] = 'failing' + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/waffle/waffle.service.js b/services/waffle/waffle.service.js new file mode 100644 index 0000000000..0a9fab8f24 --- /dev/null +++ b/services/waffle/waffle.service.js @@ -0,0 +1,59 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + makeLabel: getLabel, + makeColorB, +} = require('../../lib/badge-data') +const { checkErrorResponse } = require('../../lib/error-helper') + +module.exports = class Waffle extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/waffle\/label\/([^/]+)\/([^/]+)\/?([^/]+)?\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const user = match[1] // eg, evancohen + const repo = match[2] // eg, smart-mirror + const ghLabel = match[3] || 'ready' // eg, in%20progress + const format = match[4] + const apiUrl = `https://api.waffle.io/${user}/${repo}/columns?with=count` + const badgeData = getBadgeData('waffle', data) + + request(apiUrl, (err, res, buffer) => { + try { + if (checkErrorResponse(badgeData, err, res)) { + sendBadge(format, badgeData) + return + } + const cols = JSON.parse(buffer) + if (cols.length === 0) { + badgeData.text[1] = 'absent' + sendBadge(format, badgeData) + return + } + let count = 0 + let color = '78bdf2' + for (let i = 0; i < cols.length; i++) { + if ('label' in cols[i] && cols[i].label !== null) { + if (cols[i].label.name === ghLabel) { + count = cols[i].count + color = cols[i].label.color + break + } + } + } + badgeData.text[0] = getLabel(ghLabel, data) + badgeData.text[1] = '' + count + badgeData.colorscheme = null + badgeData.colorB = makeColorB(color, data) + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/website/website.service.js b/services/website/website.service.js new file mode 100644 index 0000000000..b5e66c833e --- /dev/null +++ b/services/website/website.service.js @@ -0,0 +1,53 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { + makeBadgeData: getBadgeData, + setBadgeColor, +} = require('../../lib/badge-data') +const { escapeFormatSlashes } = require('../../lib/path-helpers') + +// Test if a webpage is online. +module.exports = class Website extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/website(-(([^-/]|--|\/\/)+)-(([^-/]|--|\/\/)+)(-(([^-/]|--|\/\/)+)-(([^-/]|--|\/\/)+))?)?\/([^/]+)\/(.+)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const onlineMessage = escapeFormatSlashes( + match[2] != null ? match[2] : 'online' + ) + const offlineMessage = escapeFormatSlashes( + match[4] != null ? match[4] : 'offline' + ) + const onlineColor = escapeFormatSlashes( + match[7] != null ? match[7] : 'brightgreen' + ) + const offlineColor = escapeFormatSlashes( + match[9] != null ? match[9] : 'red' + ) + const userProtocol = match[11] + const userURI = match[12] + const format = match[13] + const withProtocolURI = userProtocol + '://' + userURI + const options = { + method: 'HEAD', + uri: withProtocolURI, + } + const badgeData = getBadgeData('website', data) + badgeData.colorscheme = undefined + request(options, (err, res) => { + // We consider all HTTP status codes below 310 as success. + if (err != null || res.statusCode >= 310) { + badgeData.text[1] = offlineMessage + setBadgeColor(badgeData, offlineColor) + sendBadge(format, badgeData) + } else { + badgeData.text[1] = onlineMessage + setBadgeColor(badgeData, onlineColor) + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/wheelmap/wheelmap.service.js b/services/wheelmap/wheelmap.service.js new file mode 100644 index 0000000000..0ae93667d5 --- /dev/null +++ b/services/wheelmap/wheelmap.service.js @@ -0,0 +1,40 @@ +'use strict' + +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') + +module.exports = class Wheelmap extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + camp.route( + /^\/wheelmap\/a\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const nodeId = match[1] // eg, `2323004600`. + const format = match[2] + const options = { + method: 'GET', + json: true, + uri: 'http://wheelmap.org/nodes/' + nodeId + '.json', + } + const badgeData = getBadgeData('wheelmap', data) + // eslint-disable-next-line handle-callback-err + request(options, (err, res, json) => { + try { + const accessibility = json.node.wheelchair + badgeData.text[1] = accessibility + if (accessibility === 'yes') { + badgeData.colorscheme = 'brightgreen' + } else if (accessibility === 'limited') { + badgeData.colorscheme = 'yellow' + } else if (accessibility === 'no') { + badgeData.colorscheme = 'red' + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'void' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/wordpress/wordpress-plugin.service.js b/services/wordpress/wordpress-plugin.service.js new file mode 100644 index 0000000000..d5f47f471a --- /dev/null +++ b/services/wordpress/wordpress-plugin.service.js @@ -0,0 +1,188 @@ +'use strict' + +const semver = require('semver') +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { metric, starRating } = require('../../lib/text-formatters') +const { addv: versionText } = require('../../lib/text-formatters') +const { version: versionColor } = require('../../lib/color-formatters') + +module.exports = class WordpressPlugin extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + // wordpress plugin version integration. + // example: https://img.shields.io/wordpress/plugin/v/akismet.svg for https://wordpress.org/plugins/akismet + camp.route( + /^\/wordpress\/plugin\/v\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const plugin = match[1] // eg, `akismet`. + const format = match[2] + const apiUrl = + 'http://api.wordpress.org/plugins/info/1.0/' + plugin + '.json' + const badgeData = getBadgeData('plugin', data) + request(apiUrl, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(buffer) + const version = data.version + badgeData.text[1] = versionText(version) + badgeData.colorscheme = versionColor(version) + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + + // wordpress plugin downloads integration. + // example: https://img.shields.io/wordpress/plugin/dt/akismet.svg for https://wordpress.org/plugins/akismet + camp.route( + /^\/wordpress\/plugin\/dt\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const plugin = match[1] // eg, `akismet`. + const format = match[2] + const apiUrl = + 'http://api.wordpress.org/plugins/info/1.0/' + plugin + '.json' + const badgeData = getBadgeData('downloads', data) + request(apiUrl, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + const total = JSON.parse(buffer).downloaded + badgeData.text[1] = metric(total) + if (total === 0) { + badgeData.colorscheme = 'red' + } else if (total < 100) { + badgeData.colorscheme = 'yellow' + } else if (total < 1000) { + badgeData.colorscheme = 'yellowgreen' + } else if (total < 10000) { + badgeData.colorscheme = 'green' + } else { + badgeData.colorscheme = 'brightgreen' + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + + // wordpress plugin rating integration. + // example: https://img.shields.io/wordpress/plugin/r/akismet.svg for https://wordpress.org/plugins/akismet + camp.route( + /^\/wordpress\/plugin\/r\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const plugin = match[1] // eg, `akismet`. + const format = match[2] + const apiUrl = + 'http://api.wordpress.org/plugins/info/1.0/' + plugin + '.json' + const badgeData = getBadgeData('rating', data) + request(apiUrl, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + let rating = parseInt(JSON.parse(buffer).rating) + rating = (rating / 100) * 5 + badgeData.text[1] = starRating(rating) + if (rating === 0) { + badgeData.colorscheme = 'red' + } else if (rating < 2) { + badgeData.colorscheme = 'yellow' + } else if (rating < 3) { + badgeData.colorscheme = 'yellowgreen' + } else if (rating < 4) { + badgeData.colorscheme = 'green' + } else { + badgeData.colorscheme = 'brightgreen' + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + + // wordpress version support integration. + // example: https://img.shields.io/wordpress/v/akismet.svg for https://wordpress.org/plugins/akismet + camp.route( + /^\/wordpress\/v\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const plugin = match[1] // eg, `akismet`. + const format = match[2] + const apiUrl = + 'http://api.wordpress.org/plugins/info/1.0/' + plugin + '.json' + const badgeData = getBadgeData('wordpress', data) + request(apiUrl, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + const data = JSON.parse(buffer) + if (data.tested) { + let testedVersion = data.tested.replace(/[^0-9.]/g, '') + badgeData.text[1] = testedVersion + ' tested' + const coreUrl = + 'https://api.wordpress.org/core/version-check/1.7/' + request(coreUrl, (err, res, response) => { + try { + const versions = JSON.parse(response).offers.map( + v => v.version + ) + if (err !== null) { + sendBadge(format, badgeData) + return + } + const svTestedVersion = + testedVersion.split('.').length === 2 + ? (testedVersion += '.0') + : testedVersion + const svVersion = + versions[0].split('.').length === 2 + ? (versions[0] += '.0') + : versions[0] + if ( + testedVersion === versions[0] || + semver.gtr(svTestedVersion, svVersion) + ) { + badgeData.colorscheme = 'brightgreen' + } else if (versions.indexOf(testedVersion) !== -1) { + badgeData.colorscheme = 'orange' + } else { + badgeData.colorscheme = 'yellow' + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + } else { + sendBadge(format, badgeData) + } + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} diff --git a/services/wordpress/wordpress-theme.service.js b/services/wordpress/wordpress-theme.service.js new file mode 100644 index 0000000000..878cb25ad4 --- /dev/null +++ b/services/wordpress/wordpress-theme.service.js @@ -0,0 +1,90 @@ +'use strict' + +const queryString = require('query-string') +const LegacyService = require('../legacy-service') +const { makeBadgeData: getBadgeData } = require('../../lib/badge-data') +const { metric, starRating } = require('../../lib/text-formatters') +const { + downloadCount: downloadCountColor, +} = require('../../lib/color-formatters') + +module.exports = class WordpressTheme extends LegacyService { + static registerLegacyRouteHandler({ camp, cache }) { + // wordpress theme rating integration. + // example: https://img.shields.io/wordpress/theme/r/hestia.svg for https://wordpress.org/themes/hestia + camp.route( + /^\/wordpress\/theme\/r\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const queryParams = { + action: 'theme_information', + 'request[slug]': match[1], // eg, `hestia`. + } + const format = match[2] + const apiUrl = + 'https://api.wordpress.org/themes/info/1.1/?' + + queryString.stringify(queryParams) + const badgeData = getBadgeData('rating', data) + request(apiUrl, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + let rating = parseInt(JSON.parse(buffer).rating) + rating = (rating / 100) * 5 + badgeData.text[1] = starRating(rating) + if (rating === 0) { + badgeData.colorscheme = 'red' + } else if (rating < 2) { + badgeData.colorscheme = 'yellow' + } else if (rating < 3) { + badgeData.colorscheme = 'yellowgreen' + } else if (rating < 4) { + badgeData.colorscheme = 'green' + } else { + badgeData.colorscheme = 'brightgreen' + } + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + + // wordpress theme download integration. + // example: https://img.shields.io/wordpress/theme/dt/hestia.svg for https://wordpress.org/themes/hestia + camp.route( + /^\/wordpress\/theme\/dt\/(.*)\.(svg|png|gif|jpg|json)$/, + cache((data, match, sendBadge, request) => { + const queryParams = { + action: 'theme_information', + 'request[slug]': match[1], // eg, `hestia`. + } + const format = match[2] + const apiUrl = + 'https://api.wordpress.org/themes/info/1.1/?' + + queryString.stringify(queryParams) + const badgeData = getBadgeData('downloads', data) + request(apiUrl, (err, res, buffer) => { + if (err != null) { + badgeData.text[1] = 'inaccessible' + sendBadge(format, badgeData) + return + } + try { + const downloads = JSON.parse(buffer).downloaded + badgeData.text[1] = metric(downloads) + badgeData.colorscheme = downloadCountColor(downloads) + sendBadge(format, badgeData) + } catch (e) { + badgeData.text[1] = 'invalid' + sendBadge(format, badgeData) + } + }) + }) + ) + } +} -- GitLab