2 |
404
3 |
4 | <% if (locals.flash && flash.warning) { %>
5 |
6 | <%= flash.warning %>
7 |
8 | ×
9 |
10 |
11 | <% } %>
12 |
13 | <% if (locals.noExist){ %>
14 |
<%= locals.noExist %>
15 | <% } %>
16 |
17 | Unable to find
<%= req.url %>
18 | <% if (locals.flash && flash.err_message) { %>
19 |
<%= flash.err_message %>
20 | <% } %>
21 |
22 | <% if (locals.flash && flash.err) { %>
23 |
<%= JSON.stringify(flash.err) %>
24 | <% } %>
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/.github/workflows/nodejs.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Node.js CI
5 |
6 | on:
7 | push:
8 | branches: [ master ]
9 | pull_request:
10 | branches: [ master ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | strategy:
18 | matrix:
19 | node-version: [24.x]
20 |
21 | steps:
22 | - uses: actions/checkout@v2
23 | - name: Use Node.js ${{ matrix.node-version }}
24 | uses: actions/setup-node@v1
25 | with:
26 | node-version: ${{ matrix.node-version }}
27 | - run: npm ci
28 | - run: npm run build --if-present
29 | - run: npm run lint
30 | env:
31 | CI: true
32 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "App",
3 | "icons": [
4 | {
5 | "src": "\/android-icon-36x36.png",
6 | "sizes": "36x36",
7 | "type": "image\/png",
8 | "density": "0.75"
9 | },
10 | {
11 | "src": "\/android-icon-48x48.png",
12 | "sizes": "48x48",
13 | "type": "image\/png",
14 | "density": "1.0"
15 | },
16 | {
17 | "src": "\/android-icon-72x72.png",
18 | "sizes": "72x72",
19 | "type": "image\/png",
20 | "density": "1.5"
21 | },
22 | {
23 | "src": "\/android-icon-96x96.png",
24 | "sizes": "96x96",
25 | "type": "image\/png",
26 | "density": "2.0"
27 | },
28 | {
29 | "src": "\/android-icon-144x144.png",
30 | "sizes": "144x144",
31 | "type": "image\/png",
32 | "density": "3.0"
33 | },
34 | {
35 | "src": "\/android-icon-192x192.png",
36 | "sizes": "192x192",
37 | "type": "image\/png",
38 | "density": "4.0"
39 | }
40 | ]
41 | }
--------------------------------------------------------------------------------
/models/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const fs = require('fs')
4 | const path = require('path')
5 | const Sequelize = require('sequelize')
6 | const basename = path.basename(__filename)
7 | const db = {}
8 |
9 | const sequelize = new Sequelize(process.env.POSTGRESQL_SERVER || 'postgres://localhost:5432/spr')
10 |
11 | fs
12 | .readdirSync(__dirname)
13 | .filter(file => {
14 | return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js')
15 | })
16 | .forEach(file => {
17 | // const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes)
18 | const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes)
19 | db[model.name] = model
20 | })
21 |
22 | Object.keys(db).forEach(modelName => {
23 | if (db[modelName].associate) {
24 | db[modelName].associate(db)
25 | }
26 | })
27 |
28 | db.sequelize = sequelize
29 | db.Sequelize = Sequelize
30 |
31 | module.exports = db
32 |
--------------------------------------------------------------------------------
/views/userAllPackage.ejs:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
Add a Swift package
11 | <% if (flash && flash.error) { %>
12 |
13 | <%= flash.error %>
14 |
15 | ×
16 |
17 |
18 | <% } %>
19 |
28 |
29 |
Only public repositories can be successfully added to the Swift Package Registry . Swift package will not update automatically unless the GitHub App is installed on the repository.
30 |
31 |
32 |
--------------------------------------------------------------------------------
/lib/search.js:
--------------------------------------------------------------------------------
1 | module.exports = function (router) {
2 | router.get('/search', async function (req, res) {
3 | var wildcard = '%%'
4 | var term = ''
5 | var orStatement = []
6 | var andStatement = []
7 |
8 | if (Array.isArray(req.query.term)) {
9 | wildcard = ''
10 | req.query.term.forEach(function (term) {
11 | wildcard = wildcard + '%' + term
12 | })
13 | wildcard = wildcard + '%'
14 | term = req.query.term[0]
15 | } else {
16 | term = req.query.term || ''
17 | wildcard = '%' + term + '%'
18 | }
19 |
20 | if (wildcard !== '' && wildcard !== '%%') {
21 | orStatement.push({
22 | info: {
23 | full_name: { [router.db.Sequelize.Op.iLike]: wildcard }
24 | }
25 | })
26 |
27 | orStatement.push({
28 | 'info.description': { [router.db.Sequelize.Op.iLike]: wildcard }
29 | })
30 |
31 | orStatement.push({
32 | 'description.name': { [router.db.Sequelize.Op.iLike]: wildcard }
33 | })
34 | }
35 |
36 | if (Array.isArray(req.query.topic)) {
37 | andStatement.push({
38 | topics: { [router.db.Sequelize.Op.contains]: req.query.topic }
39 | })
40 | } else {
41 | const topic = req.query.topic || ''
42 | if (topic !== '') {
43 | orStatement.push({
44 | topics: { [router.db.Sequelize.Op.contains]: [topic] }
45 | })
46 | }
47 | }
48 |
49 | var where = {
50 | processing: false
51 | }
52 |
53 | if (orStatement && orStatement.length > 0) {
54 | where[router.db.Sequelize.Op.or] = orStatement
55 | }
56 |
57 | if (andStatement && andStatement.length > 0) {
58 | where[router.db.Sequelize.Op.and] = andStatement
59 | }
60 |
61 | console.log(where)
62 |
63 | router.db.Package.findAll({
64 | where: where,
65 | order: [[router.db.sequelize.cast(router.db.sequelize.json('info.stargazers_count'), 'int'), 'DESC'], [router.db.sequelize.json('info.name'), 'DESC']]
66 | }).then(function (packages) {
67 | res.render('search', { packages: packages, term: term, title: term + ' Search' })
68 | })
69 | })
70 | }
71 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "swift-package-registry",
3 | "version": "1.1.0",
4 | "private": true,
5 | "description": "A registry of Swift packages",
6 | "author": "twodayslate ",
7 | "license": "ISC",
8 | "repository": "https://github.com/twodayslate/swift-package-registry.git",
9 | "homepage": "https://github.com/twodayslate/swift-package-registry",
10 | "bugs": "https://github.com/twodayslate/swift-package-registry/issues",
11 | "keywords": [
12 | "probot",
13 | "github",
14 | "probot-app"
15 | ],
16 | "scripts": {
17 | "dev": "nodemon",
18 | "start": "probot run ./index.js",
19 | "lint": "standard --fix",
20 | "test": "jest && standard",
21 | "test:watch": "jest --watch --notify --notifyMode=change --coverage",
22 | "postinstall": "patch-package"
23 | },
24 | "dependencies": {
25 | "@actions/core": "1.11.1",
26 | "@octokit/rest": "^19.0.7",
27 | "absolutify": "^0.1.0",
28 | "apicache": "^1.6.3",
29 | "async": "3.2.6",
30 | "axios": "1.9.0",
31 | "body-parser": "^1.20.2",
32 | "connect-flash": "^0.1.1",
33 | "dateformat": "^4.6.3",
34 | "dockerode": "^3.3.4",
35 | "ejs": "3.1.10",
36 | "express": "^4.18.2",
37 | "express-partials": "^0.3.0",
38 | "express-session": "1.18.1",
39 | "html-to-text": "^9.0.4",
40 | "patch-package": "^6.5.1",
41 | "pg": "8.16.0",
42 | "pg-hstore": "^2.3.4",
43 | "probot": "^12.3.0",
44 | "require-context": "^1.1.0",
45 | "semver": "7.7.2",
46 | "sequelize": "6.37.7",
47 | "uuid": "^9.0.0"
48 | },
49 | "devDependencies": {
50 | "https-localhost": "^4.7.0",
51 | "jest": "29.7.0",
52 | "nock": "^12.0.0",
53 | "nodemon": "^2.0.4",
54 | "smee-client": "^1.2.2",
55 | "standard": "^14.3.4"
56 | },
57 | "engines": {
58 | "node": ">= 8.3.0"
59 | },
60 | "standard": {
61 | "env": [
62 | "jest"
63 | ],
64 | "ignore": [
65 | "/action/index.js"
66 | ]
67 | },
68 | "nodemonConfig": {
69 | "exec": "npm start",
70 | "watch": [
71 | ".env",
72 | "."
73 | ]
74 | },
75 | "jest": {
76 | "testEnvironment": "node"
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | [fork]: /fork
4 | [pr]: /compare
5 | [style]: https://standardjs.com/
6 | [code-of-conduct]: CODE_OF_CONDUCT.md
7 |
8 | Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great.
9 |
10 | Please note that this project is released with a [Contributor Code of Conduct][code-of-conduct]. By participating in this project you agree to abide by its terms.
11 |
12 | ## Issues and PRs
13 |
14 | If you have suggestions for how this project could be improved, or want to report a bug, open an issue! We'd love all and any contributions. If you have questions, too, we'd love to hear them.
15 |
16 | We'd also love PRs. If you're thinking of a large PR, we advise opening up an issue first to talk about it, though! Look at the links below if you're not sure how to open a PR.
17 |
18 | ## Submitting a pull request
19 |
20 | 1. [Fork][fork] and clone the repository.
21 | 1. Configure and install the dependencies: `npm ci`.
22 | 1. Make sure the tests pass on your machine: `npm test`, note: these tests also apply the linter, so there's no need to lint separately.
23 | 1. Create a new branch: `git checkout -b my-branch-name`.
24 | 1. Make your change, add tests, and make sure the tests still pass.
25 | 1. Push to your fork and [submit a pull request][pr].
26 | 1. Pat your self on the back and wait for your pull request to be reviewed and merged.
27 |
28 | Here are a few things you can do that will increase the likelihood of your pull request being accepted:
29 |
30 | - Follow the [style guide][style] which is using standard. Any linting errors should be shown when running `npm test`.
31 | - Write and update tests.
32 | - Keep your changes as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests.
33 | - Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
34 |
35 | Work in Progress pull requests are also welcome to get feedback early on, or if there is something blocked you.
36 |
37 | ## Resources
38 |
39 | - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)
40 | - [Using Pull Requests](https://help.github.com/articles/about-pull-requests/)
41 | - [GitHub Help](https://help.github.com)
42 |
--------------------------------------------------------------------------------
/lib/package.js:
--------------------------------------------------------------------------------
1 | const { parsePackageRequest } = require('./process')
2 |
3 | module.exports = function (router) {
4 | router.get('/:owner/:repo.json', async function (req, res) {
5 | var myPackage = await router.db.Package.findOne({
6 | where: {
7 | 'info.name': req.params.repo,
8 | 'info.owner.login': req.params.owner
9 | }
10 | })
11 | res.json(myPackage)
12 | })
13 |
14 | router.get('/:owner/:repo', async function (req, res) {
15 | const fullName = req.params.owner + '/' + req.params.repo
16 | req.octokitWithPersonalAccessToken.repos.get({ owner: req.params.owner, repo: req.params.repo }).then(async function (repository) {
17 | if (!repository || !repository.data) {
18 | res.status(404)
19 | req.flash('warning', 'This is not a GitHub repository!')
20 | res.render('404')
21 | return
22 | }
23 |
24 | if (repository.data.full_name !== fullName) {
25 | // this repository was renamed so go to the real one
26 | res.redirect('/' + repository.data.full_name)
27 | return
28 | }
29 |
30 | try {
31 | const [_package, created] = await router.db.Package.findOrCreate({
32 | where: {
33 | github_id: repository.data.id
34 | },
35 | defaults: {
36 | github_id: repository.data.id,
37 | info: repository.data
38 | }
39 | })
40 | if (created) {
41 | parsePackageRequest(req, _package)
42 | } else {
43 | _package.info = repository.data
44 | _package.save()
45 | }
46 |
47 | var title = repository.data.name
48 | if (_package.description) {
49 | title = _package.description.name
50 | }
51 |
52 | res.render('package', { repository: repository.data, package: _package, title: title })
53 | } catch (err) {
54 | res.flash(500)
55 | req.flash('err_message', err.message)
56 | req.flash('err', err)
57 | res.render('500')
58 | }
59 | }).catch(function (err) {
60 | if (err.status === 404) {
61 | res.status(404)
62 | req.flash('warning', 'This is not a GitHub repository!')
63 | res.render('404')
64 | return
65 | }
66 | res.status(500)
67 | req.flash('err_message', err.message)
68 | req.flash('err', err)
69 | res.render('500')
70 | })
71 | })
72 | }
73 |
--------------------------------------------------------------------------------
/lib/add.js:
--------------------------------------------------------------------------------
1 | const { parsePackageRequest } = require('./process')
2 |
3 | var async = require('async')
4 |
5 | var addQueue = async.queue(async function (task, callback) {
6 | console.log('processing package in queue', task.package.info.full_name)
7 | await parsePackageRequest(task.req, task.package)
8 | callback()
9 | }, parseInt(process.env.PROCESS_QUEUE_SIZE || '', 10))
10 |
11 | module.exports = function (router) {
12 | router.get('/add', async function (req, res) {
13 | res.render('add', { title: 'Add a Swift Package' })
14 | })
15 |
16 | router.post('/add', async function (req, res) {
17 | var parsedUrl
18 |
19 | var onInvalidURL = function () {
20 | req.flash('error', 'Invalid GitHub URL')
21 | res.status(400)
22 | res.redirect('/add')
23 | }
24 |
25 | try {
26 | parsedUrl = new URL(req.body.url)
27 | } catch (err) {
28 | onInvalidURL()
29 | return
30 | }
31 |
32 | if (parsedUrl.hostname === 'github.com' || parsedUrl.hostname === 'swiftpackageregistry.com' || parsedUrl.hostname === 'swiftpkg.dev') {
33 | const fullName = parsedUrl.pathname.trim().replace('.git', '').replace(/^\/|\/$/g, '') // https://stackoverflow.com/a/3840645/193772
34 | if (fullName.split('/').length !== 2) {
35 | onInvalidURL()
36 | return
37 | }
38 |
39 | const owner = fullName.split('/')[0]
40 | const repo = fullName.split('/')[1]
41 |
42 | const repository = await req.octokitWithPersonalAccessToken.repos.get({ owner, repo })
43 |
44 | if (repository.private) {
45 | onInvalidURL()
46 | return
47 | }
48 | const [_package, created] = await router.db.Package.findOrCreate({
49 | where: { github_id: repository.data.id },
50 | defaults: {
51 | github_id: repository.data.id,
52 | info: repository.data
53 | }
54 | })
55 |
56 | if (created) {
57 | console.log('Newly created package', _package.info.full_name)
58 | // only parse newly created packages
59 | addQueue.push({ req: req, package: _package })
60 | } else {
61 | _package.info = repository.data
62 | _package.save()
63 |
64 | console.log('adding already created package', _package.info.full_name)
65 |
66 | // todo: only process if there is an update
67 | addQueue.push({ req: req, package: _package })
68 | }
69 |
70 | res.redirect(_package.info.full_name)
71 | } else {
72 | onInvalidURL()
73 | }
74 | })
75 | }
76 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Swift Package Registry
2 |
3 | [](https://github.com/twodayslate/swift-package-registry/actions?query=workflow%3A%22Node.js+CI%22)
4 |
5 | The [Swift Package Registry](https://swiftpackageregistry.com/) is a collection of Swift Packages. Since the [GitHub Swift Package Registry](https://github.blog/2019-06-03-github-package-registry-will-support-swift-packages/) is not currently released and the [IBM Swift Package Catalog](https://developer.ibm.com/swift/2016/02/22/introducing-swift-package-catalog/) is has been killed, the [Swift Package Registry](https://swiftpackageregistry.com/) fills the current gap for easier searching and discoverability of Swift Packages.
6 |
7 | ## Technical
8 |
9 | This [GitHub App](https://developer.github.com/apps/about-apps/) is a [Node.js](https://nodejs.org/en/) application built with [Probot](https://github.com/probot/probot). It parses all Swift packages using [Docker](https://github.com/apocas/dockerode). You can help contribue to this project by visiting the [GitHub page](https://github.com/twodayslate/swift-package-registry/). Issues and pull requests are welcomed!
10 |
11 | Only public repositories are supported. When adding packages manually, a personal access token is used to fetch information about the package.
12 |
13 | Docker is used to help validate packages. Just parsing Package.swift is not always enough. It is also an additional check for Swift version compatability.
14 |
15 | You can access the [Swift Package Registry](https://swiftpackageregistry.com/) via [swiftpackageregistry.com](https://swiftpackageregistry.com/) (main site), [swift-packages.com](https://swift-packages.com), [swiftpkg.dev](https://swiftpkg.dev), [SVVlFT.com](https://svvlft.com), and [swiftdependencies.com](https://swiftdependencies.com).
16 |
17 | ## Development
18 |
19 | ### Setup
20 |
21 | #### Install dependencies
22 | ```sh
23 | npm install
24 | ```
25 |
26 | Setup .env . You can use .env.exmaple as a base.
27 |
28 | #### Run the bot
29 | ```
30 | npm start
31 | ```
32 |
33 | *GitHub Action*
34 |
35 | ```sh
36 | ncc build -o action action.js
37 | ```
38 |
39 | For locally testing Swift Package Collections you will need to be using TLS/SSL. This can be achieved with the `https-localhost` npm package and manually modifying probot to use that over it's regular express web server.
40 |
41 | ### Contributing
42 |
43 | If you have suggestions for how Swift Package Registry could be improved, or want to report a bug, open an issue! We'd love all and any contributions.
44 |
45 | For more, check out the [Contributing Guide](https://github.com/twodayslate/swift-package-registry/blob/master/CONTRIBUTING.md).
46 |
--------------------------------------------------------------------------------
/views/account.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 | <% if (locals.needsInstall) { %>
4 |
Install GitHub App
5 |
In order to have your packages automatically updated, you will need to install the GitHub application and enable it your desired repository.
6 |
7 |
Install GitHub App
8 | <% } else { %>
9 |
13 |
14 | <% if (repos.total_count > 0) { %>
15 |
Enabled Repositories
16 |
17 |
18 |
19 | Repository
20 | Description
21 |
22 |
23 |
24 | <% repos.repositories.forEach(function(repo) { %>
25 |
26 | <%= repo.name %>
27 |
28 | <%= repo.description %>
29 |
30 | <% }) %>
31 |
32 |
33 |
34 | <% if (repos.total_count > per_page) { %>
35 |
36 |
60 |
61 | <% } %>
62 | <% } else { %>
63 | No enabled repositories
64 | <% } %>
65 | <% } %>
66 |
67 |
--------------------------------------------------------------------------------
/views/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 | <% if(locals.flash) { %>
4 | <% if(flash.error) { %>
5 |
6 | <%= flash.error.message || flash.error %>
7 |
8 | ×
9 |
10 |
11 | <% } %>
12 | <% } %>
13 |
14 |
23 |
24 |
25 |
26 |
Popular Swift Packages
27 |
53 |
54 |
55 |
56 |
Recently Added
57 |
71 |
72 |
Recently Released
73 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/views/mod.ejs:
--------------------------------------------------------------------------------
1 |
2 |
Processing Swift Packages
3 |
4 |
5 |
6 | ID
7 | Repository
8 | Description
9 |
10 | <% if (locals.user && user.isAdmin) { %>
11 |
12 | <% } %>
13 |
14 |
15 |
16 | <% processing_packages.forEach(function(package){ %>
17 |
18 | <%= package.id %>
19 | <% if(package.info) { %><%= package.info.full_name %> <% } %>
20 |
21 | <% if(package.info) { %><%= package.info.description %><% } %>
22 |
23 |
32 |
33 | <% if (locals.user && user.isAdmin) { %>
34 |
35 | <% } %>
36 |
37 | <% }); %>
38 |
39 |
40 |
41 |
All Swift Packages
42 |
43 |
44 |
45 | ID
46 | Repository
47 | Description
48 | Error
49 |
50 | <% if (locals.user && user.isAdmin) { %>
51 |
52 | <% } %>
53 |
54 |
55 |
56 | <% all_packages.forEach(function(package){ %>
57 |
58 | <%= package.id %>
59 | <% if(package.info) { %><%= package.info.full_name %> <% } else { %><%= package.full_name %><% } %>
60 | <% if(package.info) { %><%= package.info.description %><% } %>
61 | <%= package.error %>
62 |
63 |
72 |
73 | <% if (locals.user && user.isAdmin) { %>
74 |
75 | <% } %>
76 |
77 | <% }); %>
78 |
79 |
80 |
--------------------------------------------------------------------------------
/lib/docker.js:
--------------------------------------------------------------------------------
1 | function runCommand (container, cmd, workDir, callback) {
2 | workDir = workDir || '/usr/src/twodayslate'
3 | container.exec({ Cmd: cmd, AttachStdin: true, AttachStdout: true, AttachStderr: true, WorkingDir: workDir }, function (err, exec) {
4 | if (err) { console.log(err); return callback(err) }
5 | exec.start({}, function (err, stream) {
6 | if (err) { console.log(err); return callback(err) }
7 | var nextDataType = null
8 | var nextDataLength = null
9 | var buffer = Buffer.from('')
10 | var finished = false
11 |
12 | var stdoutContent = ''
13 | var stderrContent = ''
14 |
15 | function processData (data) {
16 | if (data) {
17 | buffer = Buffer.concat([buffer, data])
18 | }
19 | if (!nextDataType) {
20 | if (buffer.length >= 8) {
21 | var header = bufferSlice(8)
22 | nextDataType = header.readUInt8(0)
23 | nextDataLength = header.readUInt32BE(4)
24 | // It's possible we got a "data" that contains multiple messages
25 | // Process the next one
26 | processData()
27 | }
28 | } else {
29 | if (buffer.length >= nextDataLength) {
30 | var content = bufferSlice(nextDataLength)
31 | if (nextDataType === 1) {
32 | stdoutContent += content
33 | // process.stdout.write(content);
34 | } else {
35 | stderrContent += content
36 | // process.stderr.write(content);
37 | }
38 | nextDataType = null
39 | // It's possible we got a "data" that contains multiple messages
40 | // Process the next one
41 | processData()
42 | }
43 | }
44 | }
45 |
46 | function bufferSlice (end) {
47 | var out = buffer.slice(0, end)
48 | buffer = Buffer.from(buffer.slice(end, buffer.length))
49 | return out
50 | }
51 |
52 | function didClose () {
53 | if (!finished) {
54 | exec.inspect(function (err, data) {
55 | callback(err, stdoutContent, stderrContent, data)
56 | })
57 | }
58 | finished = true
59 | }
60 | function onStreamError (err) {
61 | console.log('did get an error')
62 | finished = true
63 | stream.removeListener('data', processData)
64 | stream.removeListener('error', onStreamError)
65 | stream.removeListener('close', didClose)
66 | stream.removeListener('end', didClose)
67 | callback(err, stdoutContent, stderrContent)
68 | }
69 |
70 | stream.on('data', processData)
71 | stream.on('close', didClose)
72 | stream.on('end', didClose)
73 | stream.on('error', onStreamError)
74 | })
75 | })
76 | }
77 |
78 | module.exports = {
79 | runCommand: runCommand,
80 | asyncRunCommand: async function (container, cmd, workDir) {
81 | return new Promise(function (resolve, reject) {
82 | runCommand(container, cmd, workDir, function (err, stdout, stderr, data) {
83 | if (err) {
84 | reject(err, stdout, stderr)
85 | } else {
86 | resolve({ stdout, stderr, data })
87 | }
88 | })
89 | })
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | education, socio-economic status, nationality, personal appearance, race,
10 | religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at zac@gorak.us. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 |
--------------------------------------------------------------------------------
/lib/oauth.js:
--------------------------------------------------------------------------------
1 | // https://jasonet.co/posts/probot-with-ui/
2 | const axios = require('axios')
3 | const querystring = require('querystring')
4 | const { Octokit } = require('@octokit/rest')
5 |
6 | const uuid = require('uuid')
7 |
8 | module.exports = function (router) {
9 | router.use(async (req, res, next) => {
10 | if (req.session.token) {
11 | req.oauth = new Octokit({
12 | auth: req.session.token,
13 | throttle: {
14 | onAbuseLimit: function (retryAfter, options) { console.log('onAbuseLimit reached') },
15 | onRateLimit: function (retryAfter, options) { console.log('onRateLimit reached') }
16 | }
17 | })
18 |
19 | await req.oauth.auth()
20 |
21 | const { data } = await req.oauth.users.getAuthenticated()
22 |
23 | if (!req.session.login) {
24 | req.session.login = data.login
25 | }
26 |
27 | if (!req.session.github_id) {
28 | req.session.github_id = data.id
29 | }
30 |
31 | router.db.User.findOrCreate({
32 | where: {
33 | github_id: req.session.github_id
34 | },
35 | defaults: {
36 | github_id: req.session.github_id,
37 | uuid: uuid.v5(req.session.github_id.toString(), process.env.UUID_NAMESPACE),
38 | accessToken: req.session.token,
39 | github_login: req.session.login,
40 | github_json: data
41 | }
42 | }).then(function (users, created) {
43 | req.user = users[0]
44 | req.user.github_json = data
45 | req.user.accessToken = req.session.token
46 | req.user.save()
47 | res.locals.user = req.user
48 | next()
49 | })
50 | } else {
51 | next()
52 | }
53 | })
54 |
55 | router.get('/auth/github/login', (req, res) => {
56 | const protocol = req.headers['x-forwarded-proto'] || req.protocol
57 | const host = req.headers['x-forwarded-host'] || req.get('host')
58 |
59 | const params = querystring.stringify({
60 | client_id: process.env.GITHUB_CLIENT_ID,
61 | redirect_uri: `${protocol}://${host}/auth/github/callback`
62 | })
63 | const url = `https://github.com/login/oauth/authorize?${params}`
64 | res.redirect(url)
65 | })
66 |
67 | router.get('/auth/github/callback', async (req, res) => {
68 | // complete OAuth dance
69 | try {
70 | const response = await axios.post('https://github.com/login/oauth/access_token', {
71 | client_id: process.env.GITHUB_CLIENT_ID,
72 | client_secret: process.env.GITHUB_CLIENT_SECRET,
73 | code: req.query.code,
74 | state: req.query.state
75 | }, {
76 | headers: {
77 | Accept: 'application/json'
78 | }
79 | })
80 |
81 | req.session.token = response.data.access_token
82 | if (response.data.error_description) {
83 | req.flash('error', response.data.error_description)
84 | }
85 | res.redirect(req.session.redirect || '/')
86 | } catch (error) {
87 | console.error('GitHub OAuth error:', error)
88 | res.status(500)
89 | req.flash('Invalid GitHub code')
90 | res.redirect(req.session.redirect || '/')
91 | }
92 | })
93 |
94 | router.get('/login', function (req, res) {
95 | if (req.user) {
96 | res.redirect('/')
97 | return
98 | }
99 | res.render('login')
100 | })
101 |
102 | router.get('/logout', function (req, res) {
103 | // https://stackoverflow.com/a/60382823/193772
104 | req.session.destroy(() => {
105 | res.redirect('/') // will always fire after session is destroyed
106 | })
107 | })
108 | }
109 |
--------------------------------------------------------------------------------
/lib/api/collection.js:
--------------------------------------------------------------------------------
1 | const { convert } = require('html-to-text')
2 | const semver = require('semver')
3 | const dateFormat = require('dateformat')
4 |
5 | module.exports = function (router) {
6 | function mapPackages (packages) {
7 | return packages.map(myPackage => {
8 | // will use 0.0.0 if can't parse semversion
9 | var version = '0.0.0'
10 | if (myPackage.latest_release) {
11 | const newVer = semver.clean(myPackage.latest_release.tag_name.toLowerCase().replace('version', ''))
12 | if (newVer) {
13 | version = newVer
14 | }
15 | }
16 |
17 | var ans = {
18 | url: myPackage.info.git_url.replace('git://', 'https://'),
19 | versions: [
20 | {
21 | version: version,
22 | summary: convert(myPackage.latest_release.body),
23 | manifests: {
24 | [myPackage.description.tools_version]: {
25 | toolsVersion: myPackage.description.tools_version,
26 | packageName: myPackage.description.name,
27 | targets: myPackage.description.targets,
28 | products: myPackage.description.products
29 | }
30 | },
31 | defaultToolsVersion: myPackage.description.tools_version,
32 | createdAt: dateFormat(myPackage.latest_release.created_at, 'isoDateTime')
33 | }
34 | ]
35 | }
36 | if (myPackage.info.description) {
37 | ans.summary = myPackage.info.description
38 | }
39 | return ans
40 | })
41 | }
42 |
43 | router.get('/collection.json', router.apicache('12 hours'), async function (req, res) {
44 | const packages = await router.db.Package.findAll({
45 | where: {
46 | processing: false,
47 | latest_release: {
48 | [router.db.Sequelize.Op.not]: null
49 | },
50 | error: null,
51 | description: {
52 | [router.db.Sequelize.Op.not]: null
53 | },
54 | 'description.tools_version': {
55 | [router.db.Sequelize.Op.not]: null
56 | }
57 | },
58 | order: [['info.stargazers_count', 'DESC']],
59 | limit: 1000
60 | })
61 |
62 | req.robot.log('packages:')
63 | req.robot.log(packages)
64 | // format https://github.com/apple/swift-package-manager/blob/main/Sources/PackageCollectionsModel/Formats/v1.md
65 | const ans = JSON.stringify({
66 | overview: 'A collection of the top 1000 Swift packages on the Swift Package Registry',
67 | formatVersion: '1.0',
68 | name: 'Top 1000 Packages from the Swift Package Registry',
69 | generatedBy: { name: 'The Swift Package Registry' },
70 | keywords: ['swift package registry'],
71 | packages: mapPackages(packages),
72 | // 2020-10-22T06:03:52Z format
73 | generatedAt: dateFormat((new Date()).toISOString(), 'isoDateTime')
74 | })
75 | res.setHeader('Content-Length', Buffer.byteLength(ans))
76 | res.setHeader('Content-Type', 'application/json')
77 | res.send(ans)
78 | })
79 |
80 | router.get('/:owner/collection.json', router.apicache('6 hours'), async function (req, res) {
81 | const packages = await router.db.Package.findAll({
82 | where: {
83 | processing: false,
84 | latest_release: {
85 | [router.db.Sequelize.Op.not]: null
86 | },
87 | error: null,
88 | description: {
89 | [router.db.Sequelize.Op.not]: null
90 | },
91 | 'info.owner.login': req.params.owner,
92 | 'description.tools_version': {
93 | [router.db.Sequelize.Op.not]: null
94 | }
95 | }
96 | })
97 |
98 | req.robot.log('packages:')
99 | req.robot.log(packages)
100 | // format https://github.com/apple/swift-package-manager/blob/main/Sources/PackageCollectionsModel/Formats/v1.md
101 | const ans = JSON.stringify({
102 | overview: 'A collection of ' + req.params.owner + "'s Swift packages on the Swift Package Registry",
103 | formatVersion: '1.0',
104 | name: req.params.owner + "'s packages from the Swift Package Registry",
105 | generatedBy: { name: 'The Swift Package Registry' },
106 | keywords: ['swift package registry', req.params.owner],
107 | packages: mapPackages(packages),
108 | // 2020-10-22T06:03:52Z format
109 | generatedAt: dateFormat((new Date()).toISOString(), 'isoDateTime')
110 | })
111 | res.setHeader('Content-Length', Buffer.byteLength(ans))
112 | res.setHeader('Content-Type', 'application/json')
113 | res.send(ans)
114 | })
115 | }
116 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
2 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
3 |
4 | # User-specific stuff
5 | .idea/**/workspace.xml
6 | .idea/**/tasks.xml
7 | .idea/**/usage.statistics.xml
8 | .idea/**/dictionaries
9 | .idea/**/shelf
10 |
11 | # Generated files
12 | .idea/**/contentModel.xml
13 |
14 | # Sensitive or high-churn files
15 | .idea/**/dataSources/
16 | .idea/**/dataSources.ids
17 | .idea/**/dataSources.local.xml
18 | .idea/**/sqlDataSources.xml
19 | .idea/**/dynamic.xml
20 | .idea/**/uiDesigner.xml
21 | .idea/**/dbnavigator.xml
22 |
23 | # Gradle
24 | .idea/**/gradle.xml
25 | .idea/**/libraries
26 |
27 | # Gradle and Maven with auto-import
28 | # When using Gradle or Maven with auto-import, you should exclude module files,
29 | # since they will be recreated, and may cause churn. Uncomment if using
30 | # auto-import.
31 | # .idea/artifacts
32 | # .idea/compiler.xml
33 | # .idea/jarRepositories.xml
34 | # .idea/modules.xml
35 | # .idea/*.iml
36 | # .idea/modules
37 | # *.iml
38 | # *.ipr
39 |
40 | # CMake
41 | cmake-build-*/
42 |
43 | # Mongo Explorer plugin
44 | .idea/**/mongoSettings.xml
45 |
46 | # File-based project format
47 | *.iws
48 |
49 | # IntelliJ
50 | out/
51 |
52 | # mpeltonen/sbt-idea plugin
53 | .idea_modules/
54 |
55 | # JIRA plugin
56 | atlassian-ide-plugin.xml
57 |
58 | # Cursive Clojure plugin
59 | .idea/replstate.xml
60 |
61 | # Crashlytics plugin (for Android Studio and IntelliJ)
62 | com_crashlytics_export_strings.xml
63 | crashlytics.properties
64 | crashlytics-build.properties
65 | fabric.properties
66 |
67 | # Editor-based Rest Client
68 | .idea/httpRequests
69 |
70 | # Android studio 3.1+ serialized cache file
71 | .idea/caches/build_file_checksums.ser
72 |
73 |
74 | node_modules
75 | npm-debug.log
76 | *.pem
77 | !mock-cert.pem
78 | .env
79 | package-lock.json
80 | coverage
81 |
82 |
83 | # General
84 | .DS_Store
85 | .AppleDouble
86 | .LSOverride
87 |
88 | # Icon must end with two \r
89 | Icon
90 |
91 |
92 | # Thumbnails
93 | ._*
94 |
95 | # Files that might appear in the root of a volume
96 | .DocumentRevisions-V100
97 | .fseventsd
98 | .Spotlight-V100
99 | .TemporaryItems
100 | .Trashes
101 | .VolumeIcon.icns
102 | .com.apple.timemachine.donotpresent
103 |
104 | # Directories potentially created on remote AFP share
105 | .AppleDB
106 | .AppleDesktop
107 | Network Trash Folder
108 | Temporary Items
109 | .apdisk
110 |
111 |
112 | # Logs
113 | logs
114 | *.log
115 | npm-debug.log*
116 | yarn-debug.log*
117 | yarn-error.log*
118 | lerna-debug.log*
119 |
120 | # Diagnostic reports (https://nodejs.org/api/report.html)
121 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
122 |
123 | # Runtime data
124 | pids
125 | *.pid
126 | *.seed
127 | *.pid.lock
128 |
129 | # Directory for instrumented libs generated by jscoverage/JSCover
130 | lib-cov
131 |
132 | # Coverage directory used by tools like istanbul
133 | coverage
134 | *.lcov
135 |
136 | # nyc test coverage
137 | .nyc_output
138 |
139 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
140 | .grunt
141 |
142 | # Bower dependency directory (https://bower.io/)
143 | bower_components
144 |
145 | # node-waf configuration
146 | .lock-wscript
147 |
148 | # Compiled binary addons (https://nodejs.org/api/addons.html)
149 | build/Release
150 |
151 | # Dependency directories
152 | node_modules/
153 | jspm_packages/
154 |
155 | # Snowpack dependency directory (https://snowpack.dev/)
156 | web_modules/
157 |
158 | # TypeScript cache
159 | *.tsbuildinfo
160 |
161 | # Optional npm cache directory
162 | .npm
163 |
164 | # Optional eslint cache
165 | .eslintcache
166 |
167 | # Microbundle cache
168 | .rpt2_cache/
169 | .rts2_cache_cjs/
170 | .rts2_cache_es/
171 | .rts2_cache_umd/
172 |
173 | # Optional REPL history
174 | .node_repl_history
175 |
176 | # Output of 'npm pack'
177 | *.tgz
178 |
179 | # Yarn Integrity file
180 | .yarn-integrity
181 |
182 | # dotenv environment variables file
183 | .env
184 | .env.test
185 |
186 | # parcel-bundler cache (https://parceljs.org/)
187 | .cache
188 | .parcel-cache
189 |
190 | # Next.js build output
191 | .next
192 |
193 | # Nuxt.js build / generate output
194 | .nuxt
195 | dist
196 |
197 | # Gatsby files
198 | .cache/
199 | # Comment in the public line in if your project uses Gatsby and not Next.js
200 | # https://nextjs.org/blog/next-9-1#public-directory-support
201 | # public
202 |
203 | # vuepress build output
204 | .vuepress/dist
205 |
206 | # Serverless directories
207 | .serverless/
208 |
209 | # FuseBox cache
210 | .fusebox/
211 |
212 | # DynamoDB Local files
213 | .dynamodb/
214 |
215 | # TernJS port file
216 | .tern-port
217 |
218 | # Stores VSCode versions used for testing VSCode extensions
219 | .vscode-test
220 |
221 | # yarn v2
222 |
223 | .yarn/cache
224 | .yarn/unplugged
225 | .yarn/build-state.yml
226 | .pnp.*
227 |
--------------------------------------------------------------------------------
/app.yml:
--------------------------------------------------------------------------------
1 | # This is a GitHub App Manifest. These settings will be used by default when
2 | # initially configuring your GitHub App.
3 | #
4 | # NOTE: changing this file will not update your GitHub App settings.
5 | # You must visit github.com/settings/apps/your-app-name to edit them.
6 | #
7 | # Read more about configuring your GitHub App:
8 | # https://probot.github.io/docs/development/#configuring-a-github-app
9 | #
10 | # Read more about GitHub App Manifests:
11 | # https://developer.github.com/apps/building-github-apps/creating-github-apps-from-a-manifest/
12 |
13 | # The list of events the GitHub App subscribes to.
14 | # Uncomment the event names below to enable them.
15 | default_events:
16 | - check_run
17 | - check_suite
18 | - commit_comment
19 | - create
20 | - delete
21 | - deployment
22 | - deployment_status
23 | - fork
24 | - gollum
25 | - issue_comment
26 | - issues
27 | - label
28 | - milestone
29 | - member
30 | - membership
31 | - org_block
32 | - organization
33 | - page_build
34 | - project
35 | - project_card
36 | - project_column
37 | - public
38 | - pull_request
39 | - pull_request_review
40 | - pull_request_review_comment
41 | - push
42 | - release
43 | - repository
44 | - repository_import
45 | - status
46 | - team
47 | - team_add
48 | - watch
49 | - star
50 |
51 | # The set of permissions needed by the GitHub App. The format of the object uses
52 | # the permission name for the key (for example, issues) and the access type for
53 | # the value (for example, write).
54 | # Valid values are `read`, `write`, and `none`
55 | default_permissions:
56 | # Repository creation, deletion, settings, teams, and collaborators.
57 | # https://developer.github.com/v3/apps/permissions/#permission-on-administration
58 | administration: read
59 |
60 | # Checks on code.
61 | # https://developer.github.com/v3/apps/permissions/#permission-on-checks
62 | # checks: read
63 |
64 | # Repository contents, commits, branches, downloads, releases, and merges.
65 | # https://developer.github.com/v3/apps/permissions/#permission-on-contents
66 | contents: read
67 |
68 | # Deployments and deployment statuses.
69 | # https://developer.github.com/v3/apps/permissions/#permission-on-deployments
70 | # deployments: read
71 |
72 | # Issues and related comments, assignees, labels, and milestones.
73 | # https://developer.github.com/v3/apps/permissions/#permission-on-issues
74 | issues: write
75 |
76 | # Search repositories, list collaborators, and access repository metadata.
77 | # https://developer.github.com/v3/apps/permissions/#metadata-permissions
78 | metadata: read
79 |
80 | # Retrieve Pages statuses, configuration, and builds, as well as create new builds.
81 | # https://developer.github.com/v3/apps/permissions/#permission-on-pages
82 | # pages: read
83 |
84 | # Pull requests and related comments, assignees, labels, milestones, and merges.
85 | # https://developer.github.com/v3/apps/permissions/#permission-on-pull-requests
86 | # pull_requests: read
87 |
88 | # Manage the post-receive hooks for a repository.
89 | # https://developer.github.com/v3/apps/permissions/#permission-on-repository-hooks
90 | # repository_hooks: read
91 |
92 | # Manage repository projects, columns, and cards.
93 | # https://developer.github.com/v3/apps/permissions/#permission-on-repository-projects
94 | # repository_projects: read
95 |
96 | # Retrieve security vulnerability alerts.
97 | # https://developer.github.com/v4/object/repositoryvulnerabilityalert/
98 | # vulnerability_alerts: read
99 |
100 | # Commit statuses.
101 | # https://developer.github.com/v3/apps/permissions/#permission-on-statuses
102 | # statuses: read
103 |
104 | # Organization members and teams.
105 | # https://developer.github.com/v3/apps/permissions/#permission-on-members
106 | # members: read
107 |
108 | # View and manage users blocked by the organization.
109 | # https://developer.github.com/v3/apps/permissions/#permission-on-organization-user-blocking
110 | # organization_user_blocking: read
111 |
112 | # Manage organization projects, columns, and cards.
113 | # https://developer.github.com/v3/apps/permissions/#permission-on-organization-projects
114 | # organization_projects: read
115 |
116 | # Manage team discussions and related comments.
117 | # https://developer.github.com/v3/apps/permissions/#permission-on-team-discussions
118 | # team_discussions: read
119 |
120 | # Manage the post-receive hooks for an organization.
121 | # https://developer.github.com/v3/apps/permissions/#permission-on-organization-hooks
122 | # organization_hooks: read
123 |
124 | # Get notified of, and update, content references.
125 | # https://developer.github.com/v3/apps/permissions/
126 | # organization_administration: read
127 |
128 |
129 | # The name of the GitHub App. Defaults to the name specified in package.json
130 | # name: My Probot App
131 |
132 | # The homepage of your GitHub App.
133 | # url: https://example.com/
134 |
135 | # A description of the GitHub App.
136 | # description: A description of my awesome app
137 |
138 | # Set to true when your GitHub App is available to the public or false when it is only accessible to the owner of the app.
139 | # Default: true
140 | # public: false
141 |
--------------------------------------------------------------------------------
/views/layout.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | <% if (locals.title) { %>
8 | <%= title %> - <%= whoami.name %>
9 | <% } else { %>
10 | <%= whoami.name %>
11 | <% } %>
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | Swift Package Registry
49 |
50 |
51 |
52 |
53 |
57 |
58 | <% if (!locals.user) { %>
59 | Log In
60 | <% } else { %>
61 | <% if(user.isMod) { %>
62 | Mod
63 | <% } %>
64 | Log Out
65 | <% } %>
66 |
67 |
68 |
69 |
70 | <%- body %>
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
<%= packageCount %> successfully parsed packages
79 |
Copyright © 2020 All rights reserved.
80 |
Swift and the Swift logo are trademarks of Apple Inc.
81 |
82 |
89 |
90 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
129 |
130 |
131 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const session = require('express-session')
3 | const partials = require('express-partials')
4 | const oauth = require('./lib/oauth')
5 | const bodyParser = require('body-parser')
6 | const { Octokit } = require('@octokit/rest')
7 | const { parsePackageContext } = require('./lib/process')
8 | const path = require('path')
9 | const apicache = require('apicache')
10 |
11 | /**
12 | * This is the main entrypoint to your Probot app
13 | * @param {import('probot').Application} app
14 | */
15 | module.exports = async (app, options = {}) => {
16 | // Create a router even if getRouter isn't provided (for tests)
17 | const router = options.getRouter ? options.getRouter('/') : express.Router()
18 |
19 | // Your code here
20 | app.log('Yay, the app was loaded!')
21 |
22 | const db = require('./models')
23 | try {
24 | await db.sequelize.authenticate()
25 | app.log('Connection has been established successfully.')
26 | } catch (error) {
27 | app.log('Unable to connect to the database:', error)
28 | console.log(error)
29 | }
30 | app.log('authenticated')
31 | await db.sequelize.sync({ alter: true })
32 | app.log('synced')
33 |
34 | if (process.env.REPROCESS_ALL === 'True') {
35 | app.log('Reporecessing all packages')
36 | db.Package.update(
37 | {
38 | processing: true
39 | },
40 | {
41 | where: {}
42 | }
43 | )
44 | .then(function (rows) {
45 | console.log('modified', rows)
46 | })
47 | .catch(function (err) {
48 | console.log('caught an error', err)
49 | })
50 | }
51 |
52 | const expressApp = express()
53 | expressApp.use(express.static('public'))
54 | expressApp.db = db
55 | expressApp.set('views', path.join(__dirname, 'views'))
56 | expressApp.set('view engine', 'ejs')
57 | expressApp.use(
58 | session({ secret: 'keyboard cat', resave: false, saveUninitialized: true })
59 | )
60 | expressApp.use(partials())
61 | expressApp.use(require('connect-flash')())
62 | expressApp.use(bodyParser.urlencoded({ extended: true }))
63 | expressApp.use(bodyParser.json())
64 | expressApp.apicache = apicache.middleware
65 |
66 | expressApp.use(async (req, res, next) => {
67 | console.log('setting robot', req.url)
68 | req.robot = app
69 |
70 | const personalAccess = new Octokit({
71 | auth: process.env.GITHUB_PERSONAL_ACCESS_TOKEN
72 | })
73 | req.octokitWithPersonalAccessToken = personalAccess
74 |
75 | const octokit = await app.auth()
76 | req.octokit = octokit
77 | const { data } = await octokit.apps.getAuthenticated()
78 |
79 | req.whoami = data
80 | res.locals.whoami = data
81 | res.locals.req = req
82 | res.locals.flash = req.flash()
83 |
84 | res.locals.packageCount = await db.Package.count({
85 | where: { processing: false, error: null }
86 | })
87 |
88 | next()
89 | })
90 |
91 | oauth(expressApp)
92 | require('./lib/index')(expressApp)
93 | require('./lib/about')(expressApp)
94 | require('./lib/add')(expressApp)
95 | require('./lib/all')(expressApp)
96 | require('./lib/api/collection')(expressApp)
97 | require('./lib/package')(expressApp)
98 | require('./lib/search')(expressApp)
99 | require('./lib/mod')(expressApp)
100 | require('./lib/delete')(expressApp)
101 | require('./lib/api/package')(expressApp)
102 | require('./lib/api/all')(expressApp)
103 | require('./lib/api/username')(expressApp)
104 |
105 | expressApp.get('/whoami', async function (req, res) {
106 | const octokit = await app.auth()
107 | const { data } = await octokit.apps.getAuthenticated()
108 | const response = data
109 | response.process = { version: process.version }
110 | const { version } = require('./package.json')
111 | response.version = version
112 | res.json(response)
113 | })
114 |
115 | expressApp.get('/mod/cache/performance', async function (req, res) {
116 | if (!req.user || !req.user.isMod) {
117 | res.redirect('/')
118 | return
119 | }
120 | res.json(apicache.getPerformance())
121 | })
122 |
123 | expressApp.get('/mod/cache/index', async function (req, res) {
124 | if (!req.user || !req.user.isMod) {
125 | res.redirect('/')
126 | return
127 | }
128 | res.json(res.json(apicache.getIndex()))
129 | })
130 |
131 | require('./lib/userAllPackage')(expressApp)
132 |
133 | router.use(expressApp)
134 |
135 | var removeIsInstall = function (repo) {
136 | db.Package.findOne({
137 | where: {
138 | github_id: repo.id
139 | }
140 | })
141 | .then(function (_package) {
142 | _package.is_installed = false
143 | _package.save()
144 | })
145 | .catch(() => {})
146 | }
147 |
148 | var addRepo = async function (context, repo) {
149 | if (repo.private) {
150 | return
151 | }
152 | const owner = repo.full_name.split('/')[0]
153 | const name = repo.full_name.split('/')[1]
154 |
155 | var latestRelease
156 | try {
157 | var { data: lr } = await context.github.repos.getLatestRelease({
158 | owner,
159 | repo: name
160 | })
161 | latestRelease = lr
162 | } catch (err) {}
163 |
164 | var ref = 'master'
165 | if (latestRelease) {
166 | ref = latestRelease.tag_name
167 | }
168 |
169 | var pkg
170 | try {
171 | var { data } = await context.github.repos.getContents({
172 | owner,
173 | repo: name,
174 | path: 'Package.swift',
175 | ref
176 | })
177 | pkg = data
178 | } catch (err) {
179 | return
180 | }
181 |
182 | if (pkg) {
183 | const { data: info } = await context.github.repos.get({
184 | owner,
185 | repo: name
186 | })
187 |
188 | if (!info) {
189 | return
190 | }
191 |
192 | const [_package] = await db.Package.findOrCreate({
193 | where: { github_id: repo.id },
194 | defaults: {
195 | github_id: repo.id,
196 | info: info
197 | }
198 | })
199 |
200 | _package.info = info
201 | _package.is_installed = true
202 | _package.save()
203 |
204 | parsePackageContext(context, _package)
205 | }
206 | }
207 |
208 | app.on('installation', async (context) => {
209 | const payload = context.payload
210 | app.log('An installation was ', payload.action)
211 |
212 | // console.log(context)
213 |
214 | const jwt = app.app.getSignedJsonWebToken()
215 | context.log('jwt', jwt)
216 |
217 | if (payload.action === 'created') {
218 | payload.repositories.forEach(async function (repo) {
219 | addRepo(context, repo)
220 | })
221 | } else if (payload.action === 'deleted') {
222 | payload.repositories.forEach(async function (repo) {
223 | removeIsInstall(repo)
224 | })
225 | }
226 | })
227 |
228 | app.on('installation_repositories', async (context) => {
229 | const payload = context.payload
230 | if (!payload) {
231 | return
232 | }
233 | if (payload.repositories_added) {
234 | payload.repositories_added.forEach(function (repo) {
235 | addRepo(context, repo)
236 | })
237 | }
238 | if (payload.repositories_removed) {
239 | payload.repositories_removed.forEach(function (repo) {
240 | removeIsInstall(repo)
241 | })
242 | }
243 | })
244 |
245 | app.on(['release', 'public', 'created'], async (context) => {
246 | const payload = context.payload
247 | console.log(context.payload)
248 | if (!payload) {
249 | return
250 | }
251 |
252 | if (!payload.repository) {
253 | return
254 | }
255 |
256 | await addRepo(context, payload.repository)
257 | })
258 |
259 | var updateRepoInfo = async function (context, repo) {
260 | const owner = repo.full_name.split('/')[0]
261 | const name = repo.full_name.split('/')[1]
262 |
263 | const { data: info } = await context.github.repos.get({
264 | owner,
265 | repo: name
266 | })
267 |
268 | if (!info) {
269 | return
270 | }
271 |
272 | const _package = await db.Package.findOne({
273 | where: { github_id: repo.id }
274 | })
275 | if (_package) {
276 | _package.info = info
277 | _package.save()
278 | }
279 | }
280 |
281 | app.on(['star', 'repository'], async (context) => {
282 | const payload = context.payload
283 |
284 | if (!payload) {
285 | return
286 | }
287 |
288 | if (!payload.repository) {
289 | return
290 | }
291 |
292 | if (payload.repository.private) {
293 | return
294 | }
295 | const repo = payload.repository
296 |
297 | updateRepoInfo(context, repo)
298 | })
299 | }
300 |
--------------------------------------------------------------------------------
/lib/process.js:
--------------------------------------------------------------------------------
1 | var { asyncRunCommand } = require('./docker')
2 | var Docker = require('dockerode')
3 | var docker = new Docker()
4 | const { Octokit } = require('@octokit/rest')
5 | const apicache = require('apicache')
6 | const absolutify = require('absolutify')
7 |
8 | const dockerTags = ['swift:latest', 'swift:5.9', 'swift:5.6', 'swift:5.4', 'swift:5.2', 'swift:5.0', 'swift:4.2']
9 |
10 | var robotPackage = async function (_package, callback) {
11 | console.log('robotPackage', _package)
12 |
13 | const personalAccess = new Octokit({
14 | auth: process.env.GITHUB_PERSONAL_ACCESS_TOKEN
15 | })
16 |
17 | if (!_package.info) {
18 | var owner
19 | var repo
20 | if (_package.full_name) {
21 | owner = _package.full_name.split('/')[0]
22 | repo = _package.full_name.split('/')[1]
23 | }
24 | const repository = await personalAccess.repos.get({ owner: owner, repo: repo })
25 | _package.info = repository.data
26 | await _package.save()
27 | console.log('going to parse', _package.full_name, 'again')
28 | return await robotPackage(_package, callback)
29 | }
30 |
31 | try {
32 | console.log('trying for', _package.info.name)
33 | const releases = await personalAccess.repos.listReleases({ owner: _package.info.owner.login, repo: _package.info.name })
34 | var release
35 | var refs = [_package.info.default_branch]
36 |
37 | releases.data.some(function (ele) {
38 | if (!ele.draft && !ele.prerelease) {
39 | release = ele
40 | return true
41 | }
42 | })
43 |
44 | if (release) {
45 | refs.unshift(release.tag_name)
46 | if (release.body) {
47 | const readmemd = await personalAccess.markdown.render({ text: release.body, mode: 'gfm', context: _package.info.full_name })
48 | release.body = readmemd.data
49 | }
50 | _package.latest_release = release
51 | await _package.save()
52 | }
53 |
54 | try {
55 | const readme = await personalAccess.request('GET /repos/:owner/:repo/readme', {
56 | owner: _package.info.owner.login,
57 | repo: _package.info.name,
58 | ref: refs[0],
59 | headers: {
60 | accept: 'application/vnd.github.v3.html'
61 | }
62 | })
63 | if (readme) {
64 | _package.readme = absolutify(readme.data, function (url, attrName) {
65 | // images need to point to the raw asset while everything else can point
66 | // to the blob
67 | if (attrName !== 'src') {
68 | return _package.info.html_url + '/blob/' + refs[0] + url
69 | }
70 | return _package.info.html_url + '/raw/' + refs[0] + url
71 | })
72 | await _package.save()
73 | }
74 | } catch (err) {
75 | // package does not have a readme
76 | }
77 |
78 | const topics = await personalAccess.repos.getAllTopics({ owner: _package.info.owner.login, repo: _package.info.name })
79 | _package.topics = topics.data.names
80 | await _package.save()
81 |
82 | var cb = async function (err, swiftDump, toolsVersion, description, dependencies) {
83 | if (swiftDump) { _package.dump = swiftDump }
84 | if (toolsVersion) { _package.tools_version = toolsVersion }
85 | if (description) { _package.description = description }
86 | if (dependencies) { _package.dependencies = dependencies }
87 | _package.processing = false
88 | if (err) {
89 | _package.error = err.message || err
90 | } else {
91 | _package.error = null
92 | }
93 |
94 | await _package.save()
95 | return _package
96 | }
97 |
98 | var lastErr
99 | var resultData = {}
100 |
101 | console.log('going to now process package from robotPackage')
102 | for (var index = 0; index < dockerTags.length; index++) {
103 | try {
104 | resultData = await processSwiftPackage(dockerTags[index], refs[0], _package.info.git_url.replace('git://', 'https://'))
105 | return await cb(null, resultData.swiftDump, resultData.toolsVersion, resultData.description, resultData.dependencies)
106 | } catch (err) {
107 | lastErr = err
108 | }
109 | }
110 |
111 | return await cb(lastErr, resultData.swiftDump, resultData.toolsVersion, resultData.description, resultData.dependencies)
112 | } catch (err) {
113 | console.log('robotPackage: got an error in robotPackage', err)
114 | throw err
115 | }
116 | }
117 |
118 | var processSwiftPackage = async function (dockerTag, branch, cloneUrl) {
119 | console.log('processing swift package', dockerTag, branch, cloneUrl)
120 | var _container
121 |
122 | var swiftDump
123 | var toolsVersion
124 | var description
125 | var dependencies
126 |
127 | const baseDir = '/usr/src/'
128 | const packageDir = '/usr/src/package'
129 |
130 | async function cleanup (err) {
131 | if (_container) {
132 | try {
133 | await new Promise((resolve) => {
134 | _container.stop(() => {
135 | _container.remove(() => {
136 | resolve()
137 | })
138 | })
139 | })
140 | } catch (cleanupErr) {
141 | console.log('Error during cleanup:', cleanupErr)
142 | }
143 | }
144 |
145 | if (err) {
146 | console.log('done with error', err)
147 | throw err
148 | } else {
149 | console.log('done with package processing')
150 | return { swiftDump, toolsVersion, description, dependencies }
151 | }
152 | }
153 |
154 | async function runCommandAndCheck (cmd, dir, acceptAnyways) {
155 | const { stdout, stderr, data } = await asyncRunCommand(_container, cmd, dir)
156 | const inspectData = data
157 | console.log('got stdout, stderr', stdout, stderr, inspectData)
158 |
159 | if (inspectData.ExitCode !== 0) {
160 | if (inspectData.ProcessConfig.entrypoint === 'timeout' && inspectData.ExitCode === 124) {
161 | console.log('Got a timeout!', cmd, stdout, stderr, inspectData)
162 | if (!acceptAnyways) {
163 | throw new Error((stderr || stdout) + '\nTimeout exceeded')
164 | }
165 | }
166 |
167 | console.log('exit code not zero when running', cmd, stdout, stderr, inspectData)
168 | if (!acceptAnyways) {
169 | throw new Error(stderr || stdout)
170 | }
171 | }
172 | return { stdout, stderr, data }
173 | }
174 |
175 | try {
176 | await docker.pull(dockerTag)
177 | const container = await docker.createContainer({ Image: dockerTag, Cmd: ['/bin/bash'], AutoRemove: true, Tty: true, platform: 'linux/amd64' })
178 | await container.start()
179 | _container = container
180 | await runCommandAndCheck(['git', 'clone', cloneUrl, '--recurse-submodules', '--branch', branch, '--single-branch', 'package'], baseDir)
181 |
182 | const toolsResult = await runCommandAndCheck(['swift', 'package', 'tools-version'], packageDir)
183 | toolsVersion = toolsResult.stdout.trim()
184 |
185 | await runCommandAndCheck(['timeout', '--signal=ABRT', '1800', 'swift', 'package', 'resolve'], packageDir, true)
186 |
187 | const describeResult = await runCommandAndCheck(['swift', 'package', 'describe', '--type=json'], packageDir, true)
188 | try {
189 | description = JSON.parse(describeResult.stdout)
190 | } catch (err) {
191 | // ignore JSON parse errors
192 | }
193 |
194 | const dependenciesResult = await runCommandAndCheck(['timeout', '--signal=ABRT', '1800', 'swift', 'package', 'show-dependencies', '--format=json'], packageDir)
195 | dependencies = JSON.parse(dependenciesResult.stdout.substr(dependenciesResult.stdout.indexOf('{')))
196 |
197 | const dumpResult = await runCommandAndCheck(['swift', 'package', 'dump-package'], packageDir)
198 | swiftDump = JSON.parse(dumpResult.stdout)
199 | } catch (err) {
200 | return await cleanup(err)
201 | }
202 |
203 | // clear the cache so we get the new package
204 | apicache.clear()
205 | return await cleanup(null)
206 | }
207 |
208 | module.exports = {
209 | parsePackageRequest: async function (req, _package) { // this is models.Package
210 | console.log('parsing package request')
211 | return robotPackage(_package, function (err) {
212 | if (err) {
213 | console.log('Got an error processing', _package.info.full_name, err)
214 | }
215 | })
216 | },
217 |
218 | parsePackageContext: async function (context, _package) {
219 | return robotPackage(_package, function (err) {
220 | if (err) {
221 | context.log('Got an error processing', _package.info.full_name, err)
222 | }
223 | })
224 | },
225 |
226 | parsePackageRobot: async function (robot, _package) {
227 | return robotPackage(_package, function (err) {
228 | if (err) {
229 | robot.log('Got an error processing', _package.info.full_name, err)
230 | }
231 | })
232 | }
233 | }
234 |
--------------------------------------------------------------------------------
/views/package.ejs:
--------------------------------------------------------------------------------
1 |
2 | <% if (!package || package.processing || !package.info) { %>
3 |
4 |
5 | Loading...
6 |
7 |
Package is still processing. This should only take a few minutes.
8 |
9 |
10 |
23 | <% } else { %>
24 | <% if (package.info && package.info.archived) { %>
25 |
26 |
Warning: Package Archived
27 |
This package is archived and has a low chance of being updated.
28 |
29 | <% } %>
30 | <% if (package.error) { %>
31 |
32 |
Error processing
33 |
<%= package.error %>
34 | This package can be updated automatically by having the owner add the
GitHub App to this repository and creating a new release.
35 |
36 | <% } %>
37 | <% if (package.info && package.info.fork) { %>
38 |
46 | <% } %>
47 |
48 |
49 |
50 |
51 |
52 | <% if (package.description) { %>
53 | <%= package.description.name %>
54 | <% } else { %>
55 | <%= package.info.name %>
56 | <% } %>
57 |
58 |
59 | <% var release = package.info.default_branch %>
60 | <% if (package.latest_release) { release = package.latest_release.tag_name.replace('v','')} %>
61 |
62 | <%= release %>
63 |
64 |
65 |
68 |
69 |
70 |
<%= package.info.description %>
71 |
72 | <% if(package.topics) { %>
73 |
74 | <% package.topics.forEach(function(tag) { %>
75 |
<%= tag %>
76 | <% }) %>
77 |
78 | <% } %>
79 |
80 |
81 | <%= package.info.full_name %>
82 |
83 |
84 |
108 |
109 |
110 |
111 |
112 | <% if(package.latest_release) { %>
113 |
114 |
115 |
116 |
117 |
<%= package.latest_release.published_at %>
118 |
119 | <%- package.latest_release.body %>
120 |
121 |
122 |
123 | <% } %>
124 |
125 |
126 |
127 |
128 |
129 | <%- package.readme || package.readme_html || package.readme_raw %>
130 |
131 |
132 |
133 |
134 |
Description
135 |
136 |
137 | Swift Tools <%= package.tools_version %>
138 |
139 |
140 |
View Raw Output
141 |
142 |
143 |
144 |
145 |
151 |
152 |
<%= JSON.stringify(package.description, null, 2) %>
153 |
154 |
155 |
156 |
157 |
158 |
View Package
159 |
160 |
View More Packages from this Author
161 |
162 |
163 |
164 |
165 |
171 |
172 |
<%= JSON.stringify(package.dump, null, 2) %>
173 |
174 |
175 |
176 |
177 |
178 |
Dependencies
179 |
180 |
181 | <% if (package.dependencies && package.dependencies.dependencies && package.dependencies.dependencies.length > 0) { %>
182 |
183 | <% package.dependencies.dependencies.forEach(function(dep) { %>
184 | <% let depurl = new URL(dep.url); %>
185 | <% if (depurl.hostname == "github.com") { %>
186 | <% let deppath = depurl.pathname.replace('.git', ''); %>
187 | <%= dep.name %> <%= dep.version %>
188 | <% } else { %>
189 | <%= dep.name %><%= dep.version %>
190 | <% } %>
191 | <% }) %>
192 | <% } else { %>
193 | None
194 | <% } %>
195 |
196 |
197 |
198 |
View Raw Output
199 |
200 |
201 |
202 |
203 |
204 |
210 |
211 |
<%= JSON.stringify(package.dependencies, null, 2) %>
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
Last updated: <%= package.updatedAt %>
221 | <% } %>
222 |
--------------------------------------------------------------------------------
/public/css/bootstrap-social.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Social Buttons for Bootstrap
3 | *
4 | * Copyright 2013-2016 Panayiotis Lipiridis
5 | * Licensed under the MIT License
6 | *
7 | * https://github.com/lipis/bootstrap-social
8 | */
9 |
10 | .btn-social{position:relative;padding-left:44px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.btn-social>:first-child{position:absolute;left:0;top:0;bottom:0;width:32px;line-height:34px;font-size:1.6em;text-align:center;border-right:1px solid rgba(0,0,0,0.2)}
11 | .btn-social.btn-lg{padding-left:61px}.btn-social.btn-lg>:first-child{line-height:45px;width:45px;font-size:1.8em}
12 | .btn-social.btn-sm{padding-left:38px}.btn-social.btn-sm>:first-child{line-height:28px;width:28px;font-size:1.4em}
13 | .btn-social.btn-xs{padding-left:30px}.btn-social.btn-xs>:first-child{line-height:20px;width:20px;font-size:1.2em}
14 | .btn-social-icon{position:relative;padding-left:44px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;height:34px;width:34px;padding:0}.btn-social-icon>:first-child{position:absolute;left:0;top:0;bottom:0;width:32px;line-height:34px;font-size:1.6em;text-align:center;border-right:1px solid rgba(0,0,0,0.2)}
15 | .btn-social-icon.btn-lg{padding-left:61px}.btn-social-icon.btn-lg>:first-child{line-height:45px;width:45px;font-size:1.8em}
16 | .btn-social-icon.btn-sm{padding-left:38px}.btn-social-icon.btn-sm>:first-child{line-height:28px;width:28px;font-size:1.4em}
17 | .btn-social-icon.btn-xs{padding-left:30px}.btn-social-icon.btn-xs>:first-child{line-height:20px;width:20px;font-size:1.2em}
18 | .btn-social-icon>:first-child{border:none;text-align:center;width:100% !important}
19 | .btn-social-icon.btn-lg{height:45px;width:45px;padding-left:0;padding-right:0}
20 | .btn-social-icon.btn-sm{height:30px;width:30px;padding-left:0;padding-right:0}
21 | .btn-social-icon.btn-xs{height:22px;width:22px;padding-left:0;padding-right:0}
22 | .btn-adn{color:#fff;background-color:#d87a68;border-color:rgba(0,0,0,0.2)}.btn-adn:focus,.btn-adn.focus{color:#fff;background-color:#ce563f;border-color:rgba(0,0,0,0.2)}
23 | .btn-adn:hover{color:#fff;background-color:#ce563f;border-color:rgba(0,0,0,0.2)}
24 | .btn-adn:active,.btn-adn.active,.open>.dropdown-toggle.btn-adn{color:#fff;background-color:#ce563f;border-color:rgba(0,0,0,0.2)}.btn-adn:active:hover,.btn-adn.active:hover,.open>.dropdown-toggle.btn-adn:hover,.btn-adn:active:focus,.btn-adn.active:focus,.open>.dropdown-toggle.btn-adn:focus,.btn-adn:active.focus,.btn-adn.active.focus,.open>.dropdown-toggle.btn-adn.focus{color:#fff;background-color:#b94630;border-color:rgba(0,0,0,0.2)}
25 | .btn-adn:active,.btn-adn.active,.open>.dropdown-toggle.btn-adn{background-image:none}
26 | .btn-adn.disabled:hover,.btn-adn[disabled]:hover,fieldset[disabled] .btn-adn:hover,.btn-adn.disabled:focus,.btn-adn[disabled]:focus,fieldset[disabled] .btn-adn:focus,.btn-adn.disabled.focus,.btn-adn[disabled].focus,fieldset[disabled] .btn-adn.focus{background-color:#d87a68;border-color:rgba(0,0,0,0.2)}
27 | .btn-adn .badge{color:#d87a68;background-color:#fff}
28 | .btn-bitbucket{color:#fff;background-color:#205081;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:focus,.btn-bitbucket.focus{color:#fff;background-color:#163758;border-color:rgba(0,0,0,0.2)}
29 | .btn-bitbucket:hover{color:#fff;background-color:#163758;border-color:rgba(0,0,0,0.2)}
30 | .btn-bitbucket:active,.btn-bitbucket.active,.open>.dropdown-toggle.btn-bitbucket{color:#fff;background-color:#163758;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:active:hover,.btn-bitbucket.active:hover,.open>.dropdown-toggle.btn-bitbucket:hover,.btn-bitbucket:active:focus,.btn-bitbucket.active:focus,.open>.dropdown-toggle.btn-bitbucket:focus,.btn-bitbucket:active.focus,.btn-bitbucket.active.focus,.open>.dropdown-toggle.btn-bitbucket.focus{color:#fff;background-color:#0f253c;border-color:rgba(0,0,0,0.2)}
31 | .btn-bitbucket:active,.btn-bitbucket.active,.open>.dropdown-toggle.btn-bitbucket{background-image:none}
32 | .btn-bitbucket.disabled:hover,.btn-bitbucket[disabled]:hover,fieldset[disabled] .btn-bitbucket:hover,.btn-bitbucket.disabled:focus,.btn-bitbucket[disabled]:focus,fieldset[disabled] .btn-bitbucket:focus,.btn-bitbucket.disabled.focus,.btn-bitbucket[disabled].focus,fieldset[disabled] .btn-bitbucket.focus{background-color:#205081;border-color:rgba(0,0,0,0.2)}
33 | .btn-bitbucket .badge{color:#205081;background-color:#fff}
34 | .btn-dropbox{color:#fff;background-color:#1087dd;border-color:rgba(0,0,0,0.2)}.btn-dropbox:focus,.btn-dropbox.focus{color:#fff;background-color:#0d6aad;border-color:rgba(0,0,0,0.2)}
35 | .btn-dropbox:hover{color:#fff;background-color:#0d6aad;border-color:rgba(0,0,0,0.2)}
36 | .btn-dropbox:active,.btn-dropbox.active,.open>.dropdown-toggle.btn-dropbox{color:#fff;background-color:#0d6aad;border-color:rgba(0,0,0,0.2)}.btn-dropbox:active:hover,.btn-dropbox.active:hover,.open>.dropdown-toggle.btn-dropbox:hover,.btn-dropbox:active:focus,.btn-dropbox.active:focus,.open>.dropdown-toggle.btn-dropbox:focus,.btn-dropbox:active.focus,.btn-dropbox.active.focus,.open>.dropdown-toggle.btn-dropbox.focus{color:#fff;background-color:#0a568c;border-color:rgba(0,0,0,0.2)}
37 | .btn-dropbox:active,.btn-dropbox.active,.open>.dropdown-toggle.btn-dropbox{background-image:none}
38 | .btn-dropbox.disabled:hover,.btn-dropbox[disabled]:hover,fieldset[disabled] .btn-dropbox:hover,.btn-dropbox.disabled:focus,.btn-dropbox[disabled]:focus,fieldset[disabled] .btn-dropbox:focus,.btn-dropbox.disabled.focus,.btn-dropbox[disabled].focus,fieldset[disabled] .btn-dropbox.focus{background-color:#1087dd;border-color:rgba(0,0,0,0.2)}
39 | .btn-dropbox .badge{color:#1087dd;background-color:#fff}
40 | .btn-facebook{color:#fff;background-color:#3b5998;border-color:rgba(0,0,0,0.2)}.btn-facebook:focus,.btn-facebook.focus{color:#fff;background-color:#2d4373;border-color:rgba(0,0,0,0.2)}
41 | .btn-facebook:hover{color:#fff;background-color:#2d4373;border-color:rgba(0,0,0,0.2)}
42 | .btn-facebook:active,.btn-facebook.active,.open>.dropdown-toggle.btn-facebook{color:#fff;background-color:#2d4373;border-color:rgba(0,0,0,0.2)}.btn-facebook:active:hover,.btn-facebook.active:hover,.open>.dropdown-toggle.btn-facebook:hover,.btn-facebook:active:focus,.btn-facebook.active:focus,.open>.dropdown-toggle.btn-facebook:focus,.btn-facebook:active.focus,.btn-facebook.active.focus,.open>.dropdown-toggle.btn-facebook.focus{color:#fff;background-color:#23345a;border-color:rgba(0,0,0,0.2)}
43 | .btn-facebook:active,.btn-facebook.active,.open>.dropdown-toggle.btn-facebook{background-image:none}
44 | .btn-facebook.disabled:hover,.btn-facebook[disabled]:hover,fieldset[disabled] .btn-facebook:hover,.btn-facebook.disabled:focus,.btn-facebook[disabled]:focus,fieldset[disabled] .btn-facebook:focus,.btn-facebook.disabled.focus,.btn-facebook[disabled].focus,fieldset[disabled] .btn-facebook.focus{background-color:#3b5998;border-color:rgba(0,0,0,0.2)}
45 | .btn-facebook .badge{color:#3b5998;background-color:#fff}
46 | .btn-flickr{color:#fff;background-color:#ff0084;border-color:rgba(0,0,0,0.2)}.btn-flickr:focus,.btn-flickr.focus{color:#fff;background-color:#cc006a;border-color:rgba(0,0,0,0.2)}
47 | .btn-flickr:hover{color:#fff;background-color:#cc006a;border-color:rgba(0,0,0,0.2)}
48 | .btn-flickr:active,.btn-flickr.active,.open>.dropdown-toggle.btn-flickr{color:#fff;background-color:#cc006a;border-color:rgba(0,0,0,0.2)}.btn-flickr:active:hover,.btn-flickr.active:hover,.open>.dropdown-toggle.btn-flickr:hover,.btn-flickr:active:focus,.btn-flickr.active:focus,.open>.dropdown-toggle.btn-flickr:focus,.btn-flickr:active.focus,.btn-flickr.active.focus,.open>.dropdown-toggle.btn-flickr.focus{color:#fff;background-color:#a80057;border-color:rgba(0,0,0,0.2)}
49 | .btn-flickr:active,.btn-flickr.active,.open>.dropdown-toggle.btn-flickr{background-image:none}
50 | .btn-flickr.disabled:hover,.btn-flickr[disabled]:hover,fieldset[disabled] .btn-flickr:hover,.btn-flickr.disabled:focus,.btn-flickr[disabled]:focus,fieldset[disabled] .btn-flickr:focus,.btn-flickr.disabled.focus,.btn-flickr[disabled].focus,fieldset[disabled] .btn-flickr.focus{background-color:#ff0084;border-color:rgba(0,0,0,0.2)}
51 | .btn-flickr .badge{color:#ff0084;background-color:#fff}
52 | .btn-foursquare{color:#fff;background-color:#f94877;border-color:rgba(0,0,0,0.2)}.btn-foursquare:focus,.btn-foursquare.focus{color:#fff;background-color:#f71752;border-color:rgba(0,0,0,0.2)}
53 | .btn-foursquare:hover{color:#fff;background-color:#f71752;border-color:rgba(0,0,0,0.2)}
54 | .btn-foursquare:active,.btn-foursquare.active,.open>.dropdown-toggle.btn-foursquare{color:#fff;background-color:#f71752;border-color:rgba(0,0,0,0.2)}.btn-foursquare:active:hover,.btn-foursquare.active:hover,.open>.dropdown-toggle.btn-foursquare:hover,.btn-foursquare:active:focus,.btn-foursquare.active:focus,.open>.dropdown-toggle.btn-foursquare:focus,.btn-foursquare:active.focus,.btn-foursquare.active.focus,.open>.dropdown-toggle.btn-foursquare.focus{color:#fff;background-color:#e30742;border-color:rgba(0,0,0,0.2)}
55 | .btn-foursquare:active,.btn-foursquare.active,.open>.dropdown-toggle.btn-foursquare{background-image:none}
56 | .btn-foursquare.disabled:hover,.btn-foursquare[disabled]:hover,fieldset[disabled] .btn-foursquare:hover,.btn-foursquare.disabled:focus,.btn-foursquare[disabled]:focus,fieldset[disabled] .btn-foursquare:focus,.btn-foursquare.disabled.focus,.btn-foursquare[disabled].focus,fieldset[disabled] .btn-foursquare.focus{background-color:#f94877;border-color:rgba(0,0,0,0.2)}
57 | .btn-foursquare .badge{color:#f94877;background-color:#fff}
58 | .btn-github{color:#fff;background-color:#444;border-color:rgba(0,0,0,0.2)}.btn-github:focus,.btn-github.focus{color:#fff;background-color:#2b2b2b;border-color:rgba(0,0,0,0.2)}
59 | .btn-github:hover{color:#fff;background-color:#2b2b2b;border-color:rgba(0,0,0,0.2)}
60 | .btn-github:active,.btn-github.active,.open>.dropdown-toggle.btn-github{color:#fff;background-color:#2b2b2b;border-color:rgba(0,0,0,0.2)}.btn-github:active:hover,.btn-github.active:hover,.open>.dropdown-toggle.btn-github:hover,.btn-github:active:focus,.btn-github.active:focus,.open>.dropdown-toggle.btn-github:focus,.btn-github:active.focus,.btn-github.active.focus,.open>.dropdown-toggle.btn-github.focus{color:#fff;background-color:#191919;border-color:rgba(0,0,0,0.2)}
61 | .btn-github:active,.btn-github.active,.open>.dropdown-toggle.btn-github{background-image:none}
62 | .btn-github.disabled:hover,.btn-github[disabled]:hover,fieldset[disabled] .btn-github:hover,.btn-github.disabled:focus,.btn-github[disabled]:focus,fieldset[disabled] .btn-github:focus,.btn-github.disabled.focus,.btn-github[disabled].focus,fieldset[disabled] .btn-github.focus{background-color:#444;border-color:rgba(0,0,0,0.2)}
63 | .btn-github .badge{color:#444;background-color:#fff}
64 | .btn-google{color:#fff;background-color:#dd4b39;border-color:rgba(0,0,0,0.2)}.btn-google:focus,.btn-google.focus{color:#fff;background-color:#c23321;border-color:rgba(0,0,0,0.2)}
65 | .btn-google:hover{color:#fff;background-color:#c23321;border-color:rgba(0,0,0,0.2)}
66 | .btn-google:active,.btn-google.active,.open>.dropdown-toggle.btn-google{color:#fff;background-color:#c23321;border-color:rgba(0,0,0,0.2)}.btn-google:active:hover,.btn-google.active:hover,.open>.dropdown-toggle.btn-google:hover,.btn-google:active:focus,.btn-google.active:focus,.open>.dropdown-toggle.btn-google:focus,.btn-google:active.focus,.btn-google.active.focus,.open>.dropdown-toggle.btn-google.focus{color:#fff;background-color:#a32b1c;border-color:rgba(0,0,0,0.2)}
67 | .btn-google:active,.btn-google.active,.open>.dropdown-toggle.btn-google{background-image:none}
68 | .btn-google.disabled:hover,.btn-google[disabled]:hover,fieldset[disabled] .btn-google:hover,.btn-google.disabled:focus,.btn-google[disabled]:focus,fieldset[disabled] .btn-google:focus,.btn-google.disabled.focus,.btn-google[disabled].focus,fieldset[disabled] .btn-google.focus{background-color:#dd4b39;border-color:rgba(0,0,0,0.2)}
69 | .btn-google .badge{color:#dd4b39;background-color:#fff}
70 | .btn-instagram{color:#fff;background-color:#3f729b;border-color:rgba(0,0,0,0.2)}.btn-instagram:focus,.btn-instagram.focus{color:#fff;background-color:#305777;border-color:rgba(0,0,0,0.2)}
71 | .btn-instagram:hover{color:#fff;background-color:#305777;border-color:rgba(0,0,0,0.2)}
72 | .btn-instagram:active,.btn-instagram.active,.open>.dropdown-toggle.btn-instagram{color:#fff;background-color:#305777;border-color:rgba(0,0,0,0.2)}.btn-instagram:active:hover,.btn-instagram.active:hover,.open>.dropdown-toggle.btn-instagram:hover,.btn-instagram:active:focus,.btn-instagram.active:focus,.open>.dropdown-toggle.btn-instagram:focus,.btn-instagram:active.focus,.btn-instagram.active.focus,.open>.dropdown-toggle.btn-instagram.focus{color:#fff;background-color:#26455d;border-color:rgba(0,0,0,0.2)}
73 | .btn-instagram:active,.btn-instagram.active,.open>.dropdown-toggle.btn-instagram{background-image:none}
74 | .btn-instagram.disabled:hover,.btn-instagram[disabled]:hover,fieldset[disabled] .btn-instagram:hover,.btn-instagram.disabled:focus,.btn-instagram[disabled]:focus,fieldset[disabled] .btn-instagram:focus,.btn-instagram.disabled.focus,.btn-instagram[disabled].focus,fieldset[disabled] .btn-instagram.focus{background-color:#3f729b;border-color:rgba(0,0,0,0.2)}
75 | .btn-instagram .badge{color:#3f729b;background-color:#fff}
76 | .btn-linkedin{color:#fff;background-color:#007bb6;border-color:rgba(0,0,0,0.2)}.btn-linkedin:focus,.btn-linkedin.focus{color:#fff;background-color:#005983;border-color:rgba(0,0,0,0.2)}
77 | .btn-linkedin:hover{color:#fff;background-color:#005983;border-color:rgba(0,0,0,0.2)}
78 | .btn-linkedin:active,.btn-linkedin.active,.open>.dropdown-toggle.btn-linkedin{color:#fff;background-color:#005983;border-color:rgba(0,0,0,0.2)}.btn-linkedin:active:hover,.btn-linkedin.active:hover,.open>.dropdown-toggle.btn-linkedin:hover,.btn-linkedin:active:focus,.btn-linkedin.active:focus,.open>.dropdown-toggle.btn-linkedin:focus,.btn-linkedin:active.focus,.btn-linkedin.active.focus,.open>.dropdown-toggle.btn-linkedin.focus{color:#fff;background-color:#00405f;border-color:rgba(0,0,0,0.2)}
79 | .btn-linkedin:active,.btn-linkedin.active,.open>.dropdown-toggle.btn-linkedin{background-image:none}
80 | .btn-linkedin.disabled:hover,.btn-linkedin[disabled]:hover,fieldset[disabled] .btn-linkedin:hover,.btn-linkedin.disabled:focus,.btn-linkedin[disabled]:focus,fieldset[disabled] .btn-linkedin:focus,.btn-linkedin.disabled.focus,.btn-linkedin[disabled].focus,fieldset[disabled] .btn-linkedin.focus{background-color:#007bb6;border-color:rgba(0,0,0,0.2)}
81 | .btn-linkedin .badge{color:#007bb6;background-color:#fff}
82 | .btn-microsoft{color:#fff;background-color:#2672ec;border-color:rgba(0,0,0,0.2)}.btn-microsoft:focus,.btn-microsoft.focus{color:#fff;background-color:#125acd;border-color:rgba(0,0,0,0.2)}
83 | .btn-microsoft:hover{color:#fff;background-color:#125acd;border-color:rgba(0,0,0,0.2)}
84 | .btn-microsoft:active,.btn-microsoft.active,.open>.dropdown-toggle.btn-microsoft{color:#fff;background-color:#125acd;border-color:rgba(0,0,0,0.2)}.btn-microsoft:active:hover,.btn-microsoft.active:hover,.open>.dropdown-toggle.btn-microsoft:hover,.btn-microsoft:active:focus,.btn-microsoft.active:focus,.open>.dropdown-toggle.btn-microsoft:focus,.btn-microsoft:active.focus,.btn-microsoft.active.focus,.open>.dropdown-toggle.btn-microsoft.focus{color:#fff;background-color:#0f4bac;border-color:rgba(0,0,0,0.2)}
85 | .btn-microsoft:active,.btn-microsoft.active,.open>.dropdown-toggle.btn-microsoft{background-image:none}
86 | .btn-microsoft.disabled:hover,.btn-microsoft[disabled]:hover,fieldset[disabled] .btn-microsoft:hover,.btn-microsoft.disabled:focus,.btn-microsoft[disabled]:focus,fieldset[disabled] .btn-microsoft:focus,.btn-microsoft.disabled.focus,.btn-microsoft[disabled].focus,fieldset[disabled] .btn-microsoft.focus{background-color:#2672ec;border-color:rgba(0,0,0,0.2)}
87 | .btn-microsoft .badge{color:#2672ec;background-color:#fff}
88 | .btn-odnoklassniki{color:#fff;background-color:#f4731c;border-color:rgba(0,0,0,0.2)}.btn-odnoklassniki:focus,.btn-odnoklassniki.focus{color:#fff;background-color:#d35b0a;border-color:rgba(0,0,0,0.2)}
89 | .btn-odnoklassniki:hover{color:#fff;background-color:#d35b0a;border-color:rgba(0,0,0,0.2)}
90 | .btn-odnoklassniki:active,.btn-odnoklassniki.active,.open>.dropdown-toggle.btn-odnoklassniki{color:#fff;background-color:#d35b0a;border-color:rgba(0,0,0,0.2)}.btn-odnoklassniki:active:hover,.btn-odnoklassniki.active:hover,.open>.dropdown-toggle.btn-odnoklassniki:hover,.btn-odnoklassniki:active:focus,.btn-odnoklassniki.active:focus,.open>.dropdown-toggle.btn-odnoklassniki:focus,.btn-odnoklassniki:active.focus,.btn-odnoklassniki.active.focus,.open>.dropdown-toggle.btn-odnoklassniki.focus{color:#fff;background-color:#b14c09;border-color:rgba(0,0,0,0.2)}
91 | .btn-odnoklassniki:active,.btn-odnoklassniki.active,.open>.dropdown-toggle.btn-odnoklassniki{background-image:none}
92 | .btn-odnoklassniki.disabled:hover,.btn-odnoklassniki[disabled]:hover,fieldset[disabled] .btn-odnoklassniki:hover,.btn-odnoklassniki.disabled:focus,.btn-odnoklassniki[disabled]:focus,fieldset[disabled] .btn-odnoklassniki:focus,.btn-odnoklassniki.disabled.focus,.btn-odnoklassniki[disabled].focus,fieldset[disabled] .btn-odnoklassniki.focus{background-color:#f4731c;border-color:rgba(0,0,0,0.2)}
93 | .btn-odnoklassniki .badge{color:#f4731c;background-color:#fff}
94 | .btn-openid{color:#fff;background-color:#f7931e;border-color:rgba(0,0,0,0.2)}.btn-openid:focus,.btn-openid.focus{color:#fff;background-color:#da7908;border-color:rgba(0,0,0,0.2)}
95 | .btn-openid:hover{color:#fff;background-color:#da7908;border-color:rgba(0,0,0,0.2)}
96 | .btn-openid:active,.btn-openid.active,.open>.dropdown-toggle.btn-openid{color:#fff;background-color:#da7908;border-color:rgba(0,0,0,0.2)}.btn-openid:active:hover,.btn-openid.active:hover,.open>.dropdown-toggle.btn-openid:hover,.btn-openid:active:focus,.btn-openid.active:focus,.open>.dropdown-toggle.btn-openid:focus,.btn-openid:active.focus,.btn-openid.active.focus,.open>.dropdown-toggle.btn-openid.focus{color:#fff;background-color:#b86607;border-color:rgba(0,0,0,0.2)}
97 | .btn-openid:active,.btn-openid.active,.open>.dropdown-toggle.btn-openid{background-image:none}
98 | .btn-openid.disabled:hover,.btn-openid[disabled]:hover,fieldset[disabled] .btn-openid:hover,.btn-openid.disabled:focus,.btn-openid[disabled]:focus,fieldset[disabled] .btn-openid:focus,.btn-openid.disabled.focus,.btn-openid[disabled].focus,fieldset[disabled] .btn-openid.focus{background-color:#f7931e;border-color:rgba(0,0,0,0.2)}
99 | .btn-openid .badge{color:#f7931e;background-color:#fff}
100 | .btn-pinterest{color:#fff;background-color:#cb2027;border-color:rgba(0,0,0,0.2)}.btn-pinterest:focus,.btn-pinterest.focus{color:#fff;background-color:#9f191f;border-color:rgba(0,0,0,0.2)}
101 | .btn-pinterest:hover{color:#fff;background-color:#9f191f;border-color:rgba(0,0,0,0.2)}
102 | .btn-pinterest:active,.btn-pinterest.active,.open>.dropdown-toggle.btn-pinterest{color:#fff;background-color:#9f191f;border-color:rgba(0,0,0,0.2)}.btn-pinterest:active:hover,.btn-pinterest.active:hover,.open>.dropdown-toggle.btn-pinterest:hover,.btn-pinterest:active:focus,.btn-pinterest.active:focus,.open>.dropdown-toggle.btn-pinterest:focus,.btn-pinterest:active.focus,.btn-pinterest.active.focus,.open>.dropdown-toggle.btn-pinterest.focus{color:#fff;background-color:#801419;border-color:rgba(0,0,0,0.2)}
103 | .btn-pinterest:active,.btn-pinterest.active,.open>.dropdown-toggle.btn-pinterest{background-image:none}
104 | .btn-pinterest.disabled:hover,.btn-pinterest[disabled]:hover,fieldset[disabled] .btn-pinterest:hover,.btn-pinterest.disabled:focus,.btn-pinterest[disabled]:focus,fieldset[disabled] .btn-pinterest:focus,.btn-pinterest.disabled.focus,.btn-pinterest[disabled].focus,fieldset[disabled] .btn-pinterest.focus{background-color:#cb2027;border-color:rgba(0,0,0,0.2)}
105 | .btn-pinterest .badge{color:#cb2027;background-color:#fff}
106 | .btn-reddit{color:#000;background-color:#eff7ff;border-color:rgba(0,0,0,0.2)}.btn-reddit:focus,.btn-reddit.focus{color:#000;background-color:#bcddff;border-color:rgba(0,0,0,0.2)}
107 | .btn-reddit:hover{color:#000;background-color:#bcddff;border-color:rgba(0,0,0,0.2)}
108 | .btn-reddit:active,.btn-reddit.active,.open>.dropdown-toggle.btn-reddit{color:#000;background-color:#bcddff;border-color:rgba(0,0,0,0.2)}.btn-reddit:active:hover,.btn-reddit.active:hover,.open>.dropdown-toggle.btn-reddit:hover,.btn-reddit:active:focus,.btn-reddit.active:focus,.open>.dropdown-toggle.btn-reddit:focus,.btn-reddit:active.focus,.btn-reddit.active.focus,.open>.dropdown-toggle.btn-reddit.focus{color:#000;background-color:#98ccff;border-color:rgba(0,0,0,0.2)}
109 | .btn-reddit:active,.btn-reddit.active,.open>.dropdown-toggle.btn-reddit{background-image:none}
110 | .btn-reddit.disabled:hover,.btn-reddit[disabled]:hover,fieldset[disabled] .btn-reddit:hover,.btn-reddit.disabled:focus,.btn-reddit[disabled]:focus,fieldset[disabled] .btn-reddit:focus,.btn-reddit.disabled.focus,.btn-reddit[disabled].focus,fieldset[disabled] .btn-reddit.focus{background-color:#eff7ff;border-color:rgba(0,0,0,0.2)}
111 | .btn-reddit .badge{color:#eff7ff;background-color:#000}
112 | .btn-soundcloud{color:#fff;background-color:#f50;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:focus,.btn-soundcloud.focus{color:#fff;background-color:#c40;border-color:rgba(0,0,0,0.2)}
113 | .btn-soundcloud:hover{color:#fff;background-color:#c40;border-color:rgba(0,0,0,0.2)}
114 | .btn-soundcloud:active,.btn-soundcloud.active,.open>.dropdown-toggle.btn-soundcloud{color:#fff;background-color:#c40;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:active:hover,.btn-soundcloud.active:hover,.open>.dropdown-toggle.btn-soundcloud:hover,.btn-soundcloud:active:focus,.btn-soundcloud.active:focus,.open>.dropdown-toggle.btn-soundcloud:focus,.btn-soundcloud:active.focus,.btn-soundcloud.active.focus,.open>.dropdown-toggle.btn-soundcloud.focus{color:#fff;background-color:#a83800;border-color:rgba(0,0,0,0.2)}
115 | .btn-soundcloud:active,.btn-soundcloud.active,.open>.dropdown-toggle.btn-soundcloud{background-image:none}
116 | .btn-soundcloud.disabled:hover,.btn-soundcloud[disabled]:hover,fieldset[disabled] .btn-soundcloud:hover,.btn-soundcloud.disabled:focus,.btn-soundcloud[disabled]:focus,fieldset[disabled] .btn-soundcloud:focus,.btn-soundcloud.disabled.focus,.btn-soundcloud[disabled].focus,fieldset[disabled] .btn-soundcloud.focus{background-color:#f50;border-color:rgba(0,0,0,0.2)}
117 | .btn-soundcloud .badge{color:#f50;background-color:#fff}
118 | .btn-tumblr{color:#fff;background-color:#2c4762;border-color:rgba(0,0,0,0.2)}.btn-tumblr:focus,.btn-tumblr.focus{color:#fff;background-color:#1c2d3f;border-color:rgba(0,0,0,0.2)}
119 | .btn-tumblr:hover{color:#fff;background-color:#1c2d3f;border-color:rgba(0,0,0,0.2)}
120 | .btn-tumblr:active,.btn-tumblr.active,.open>.dropdown-toggle.btn-tumblr{color:#fff;background-color:#1c2d3f;border-color:rgba(0,0,0,0.2)}.btn-tumblr:active:hover,.btn-tumblr.active:hover,.open>.dropdown-toggle.btn-tumblr:hover,.btn-tumblr:active:focus,.btn-tumblr.active:focus,.open>.dropdown-toggle.btn-tumblr:focus,.btn-tumblr:active.focus,.btn-tumblr.active.focus,.open>.dropdown-toggle.btn-tumblr.focus{color:#fff;background-color:#111c26;border-color:rgba(0,0,0,0.2)}
121 | .btn-tumblr:active,.btn-tumblr.active,.open>.dropdown-toggle.btn-tumblr{background-image:none}
122 | .btn-tumblr.disabled:hover,.btn-tumblr[disabled]:hover,fieldset[disabled] .btn-tumblr:hover,.btn-tumblr.disabled:focus,.btn-tumblr[disabled]:focus,fieldset[disabled] .btn-tumblr:focus,.btn-tumblr.disabled.focus,.btn-tumblr[disabled].focus,fieldset[disabled] .btn-tumblr.focus{background-color:#2c4762;border-color:rgba(0,0,0,0.2)}
123 | .btn-tumblr .badge{color:#2c4762;background-color:#fff}
124 | .btn-twitter{color:#fff;background-color:#55acee;border-color:rgba(0,0,0,0.2)}.btn-twitter:focus,.btn-twitter.focus{color:#fff;background-color:#2795e9;border-color:rgba(0,0,0,0.2)}
125 | .btn-twitter:hover{color:#fff;background-color:#2795e9;border-color:rgba(0,0,0,0.2)}
126 | .btn-twitter:active,.btn-twitter.active,.open>.dropdown-toggle.btn-twitter{color:#fff;background-color:#2795e9;border-color:rgba(0,0,0,0.2)}.btn-twitter:active:hover,.btn-twitter.active:hover,.open>.dropdown-toggle.btn-twitter:hover,.btn-twitter:active:focus,.btn-twitter.active:focus,.open>.dropdown-toggle.btn-twitter:focus,.btn-twitter:active.focus,.btn-twitter.active.focus,.open>.dropdown-toggle.btn-twitter.focus{color:#fff;background-color:#1583d7;border-color:rgba(0,0,0,0.2)}
127 | .btn-twitter:active,.btn-twitter.active,.open>.dropdown-toggle.btn-twitter{background-image:none}
128 | .btn-twitter.disabled:hover,.btn-twitter[disabled]:hover,fieldset[disabled] .btn-twitter:hover,.btn-twitter.disabled:focus,.btn-twitter[disabled]:focus,fieldset[disabled] .btn-twitter:focus,.btn-twitter.disabled.focus,.btn-twitter[disabled].focus,fieldset[disabled] .btn-twitter.focus{background-color:#55acee;border-color:rgba(0,0,0,0.2)}
129 | .btn-twitter .badge{color:#55acee;background-color:#fff}
130 | .btn-vimeo{color:#fff;background-color:#1ab7ea;border-color:rgba(0,0,0,0.2)}.btn-vimeo:focus,.btn-vimeo.focus{color:#fff;background-color:#1295bf;border-color:rgba(0,0,0,0.2)}
131 | .btn-vimeo:hover{color:#fff;background-color:#1295bf;border-color:rgba(0,0,0,0.2)}
132 | .btn-vimeo:active,.btn-vimeo.active,.open>.dropdown-toggle.btn-vimeo{color:#fff;background-color:#1295bf;border-color:rgba(0,0,0,0.2)}.btn-vimeo:active:hover,.btn-vimeo.active:hover,.open>.dropdown-toggle.btn-vimeo:hover,.btn-vimeo:active:focus,.btn-vimeo.active:focus,.open>.dropdown-toggle.btn-vimeo:focus,.btn-vimeo:active.focus,.btn-vimeo.active.focus,.open>.dropdown-toggle.btn-vimeo.focus{color:#fff;background-color:#0f7b9f;border-color:rgba(0,0,0,0.2)}
133 | .btn-vimeo:active,.btn-vimeo.active,.open>.dropdown-toggle.btn-vimeo{background-image:none}
134 | .btn-vimeo.disabled:hover,.btn-vimeo[disabled]:hover,fieldset[disabled] .btn-vimeo:hover,.btn-vimeo.disabled:focus,.btn-vimeo[disabled]:focus,fieldset[disabled] .btn-vimeo:focus,.btn-vimeo.disabled.focus,.btn-vimeo[disabled].focus,fieldset[disabled] .btn-vimeo.focus{background-color:#1ab7ea;border-color:rgba(0,0,0,0.2)}
135 | .btn-vimeo .badge{color:#1ab7ea;background-color:#fff}
136 | .btn-vk{color:#fff;background-color:#587ea3;border-color:rgba(0,0,0,0.2)}.btn-vk:focus,.btn-vk.focus{color:#fff;background-color:#466482;border-color:rgba(0,0,0,0.2)}
137 | .btn-vk:hover{color:#fff;background-color:#466482;border-color:rgba(0,0,0,0.2)}
138 | .btn-vk:active,.btn-vk.active,.open>.dropdown-toggle.btn-vk{color:#fff;background-color:#466482;border-color:rgba(0,0,0,0.2)}.btn-vk:active:hover,.btn-vk.active:hover,.open>.dropdown-toggle.btn-vk:hover,.btn-vk:active:focus,.btn-vk.active:focus,.open>.dropdown-toggle.btn-vk:focus,.btn-vk:active.focus,.btn-vk.active.focus,.open>.dropdown-toggle.btn-vk.focus{color:#fff;background-color:#3a526b;border-color:rgba(0,0,0,0.2)}
139 | .btn-vk:active,.btn-vk.active,.open>.dropdown-toggle.btn-vk{background-image:none}
140 | .btn-vk.disabled:hover,.btn-vk[disabled]:hover,fieldset[disabled] .btn-vk:hover,.btn-vk.disabled:focus,.btn-vk[disabled]:focus,fieldset[disabled] .btn-vk:focus,.btn-vk.disabled.focus,.btn-vk[disabled].focus,fieldset[disabled] .btn-vk.focus{background-color:#587ea3;border-color:rgba(0,0,0,0.2)}
141 | .btn-vk .badge{color:#587ea3;background-color:#fff}
142 | .btn-yahoo{color:#fff;background-color:#720e9e;border-color:rgba(0,0,0,0.2)}.btn-yahoo:focus,.btn-yahoo.focus{color:#fff;background-color:#500a6f;border-color:rgba(0,0,0,0.2)}
143 | .btn-yahoo:hover{color:#fff;background-color:#500a6f;border-color:rgba(0,0,0,0.2)}
144 | .btn-yahoo:active,.btn-yahoo.active,.open>.dropdown-toggle.btn-yahoo{color:#fff;background-color:#500a6f;border-color:rgba(0,0,0,0.2)}.btn-yahoo:active:hover,.btn-yahoo.active:hover,.open>.dropdown-toggle.btn-yahoo:hover,.btn-yahoo:active:focus,.btn-yahoo.active:focus,.open>.dropdown-toggle.btn-yahoo:focus,.btn-yahoo:active.focus,.btn-yahoo.active.focus,.open>.dropdown-toggle.btn-yahoo.focus{color:#fff;background-color:#39074e;border-color:rgba(0,0,0,0.2)}
145 | .btn-yahoo:active,.btn-yahoo.active,.open>.dropdown-toggle.btn-yahoo{background-image:none}
146 | .btn-yahoo.disabled:hover,.btn-yahoo[disabled]:hover,fieldset[disabled] .btn-yahoo:hover,.btn-yahoo.disabled:focus,.btn-yahoo[disabled]:focus,fieldset[disabled] .btn-yahoo:focus,.btn-yahoo.disabled.focus,.btn-yahoo[disabled].focus,fieldset[disabled] .btn-yahoo.focus{background-color:#720e9e;border-color:rgba(0,0,0,0.2)}
147 | .btn-yahoo .badge{color:#720e9e;background-color:#fff}
148 |
--------------------------------------------------------------------------------