├── .circleci └── config.yml ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE ├── PULL_REQUEST_TEMPLATE └── stale.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs ├── .gitignore └── header.md ├── endpoints ├── address │ ├── documentation.md │ ├── graphql_schema.js │ ├── index.js │ └── tests │ │ └── integration_test.js ├── aur │ ├── documentation.md │ ├── index.js │ └── tests │ │ └── integration_test.js ├── bus │ ├── documentation.md │ ├── graphql_schema.js │ ├── index.js │ ├── realtime.js │ ├── search.js │ └── tests │ │ └── integration_test.js ├── calendar │ ├── graphql_schema.js │ ├── index.js │ └── tests │ │ └── integration_test.js ├── car │ ├── documentation.md │ ├── graphql_schema.js │ ├── index.js │ └── tests │ │ └── integration_test.js ├── carparks │ ├── index.js │ └── tests │ │ └── integration_test.js ├── cinema │ ├── index.js │ └── tests │ │ └── integration_test.js ├── company │ ├── documentation.md │ ├── index.js │ └── tests │ │ └── integration_test.js ├── concerts │ ├── documentation.md │ ├── index.js │ └── tests │ │ └── integration_test.js ├── currency │ ├── arion.js │ ├── borgun.js │ ├── documentation.md │ ├── index.js │ ├── lb.js │ ├── m5.js │ └── tests │ │ ├── arion.fixture │ │ ├── integration_test.js │ │ └── m5.fixture ├── cyclecounter │ ├── documentation.md │ ├── index.js │ └── tests │ │ └── integration_test.js ├── declension │ ├── index.js │ └── tests │ │ └── integration_test.js ├── earthquake │ ├── documentation.md │ ├── index.js │ └── tests │ │ ├── integration_test.js │ │ └── test.fixture ├── flight │ ├── documentation.md │ ├── index.js │ └── tests │ │ └── integration_test.js ├── frontpage │ └── index.js ├── golf │ ├── index.js │ └── tests │ │ └── integration_test.js ├── hljomaholl │ ├── index.js │ └── tests │ │ └── integration_test.js ├── hospital │ ├── documentation.md │ ├── index.js │ └── tests │ │ ├── integration_test.js │ │ └── test.fixture ├── isbolti │ ├── index.js │ └── tests │ │ ├── integration_test.js │ │ └── test.fixture ├── isnic │ ├── index.js │ └── tests │ │ └── integration_test.js ├── lottery │ ├── documentation.md │ ├── index.js │ └── tests │ │ └── integration_test.js ├── names │ ├── index.js │ └── tests │ │ └── integration_test.js ├── particulates │ ├── documentation.md │ ├── index.js │ └── tests │ │ └── integration_test.js ├── petrol │ ├── documentation.md │ ├── index.js │ └── tests │ │ └── integration_test.js ├── rides │ ├── documentation.md │ ├── index.js │ └── tests │ │ └── integration_test.js ├── sarschool │ ├── documentation.md │ ├── index.js │ └── tests │ │ ├── integration_test.js │ │ └── test.fixture ├── ship │ ├── documentation.md │ ├── index.js │ └── tests │ │ └── integration_test.js ├── sports │ ├── documentation.md │ ├── index.js │ └── tests │ │ └── integration_test.js ├── static_endpoints │ ├── frontpage.js │ ├── help_out.js │ └── phone.js ├── tv │ ├── 365.js │ ├── documentation.md │ ├── index.js │ ├── ruv.js │ ├── skjarinn.js │ └── tests │ │ └── integration_test.js └── weather │ ├── documentation.md │ ├── index.js │ └── tests │ └── integration_test.js ├── graphql └── types │ └── GraphQLDate.js ├── index.js ├── lib ├── cache.js ├── cors.js ├── expressMetrics.js ├── expressMetrics.spec.js ├── redis.js └── test_helpers.js ├── make-docs.js ├── mock-data.json ├── package-lock.json ├── package.json ├── server.js └── test.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:7.10 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/mongo:3.4.4 16 | 17 | working_directory: ~/repo 18 | 19 | steps: 20 | - checkout 21 | 22 | # Download and cache dependencies 23 | - restore_cache: 24 | keys: 25 | - v1-dependencies-{{ checksum "package.json" }} 26 | # fallback to using the latest cache if no exact match is found 27 | - v1-dependencies- 28 | 29 | - run: npm install 30 | 31 | - save_cache: 32 | paths: 33 | - node_modules 34 | key: v1-dependencies-{{ checksum "package.json" }} 35 | 36 | - run: npm run lint 37 | - run: npm run test 38 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Lines starting with '#' are comments. 2 | # Each line is a file pattern followed by one or more owners. 3 | 4 | # These owners will be the default owners for everything in the repo. 5 | * @koddsson @kristjanmik @benediktvaldez @MiniGod 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE: -------------------------------------------------------------------------------- 1 | Describe the issue you are having here. 2 | 3 | ### Steps to reproduce 4 | 5 | Is it a issue with: 6 | - the service running on http://apis.is? 7 | - running the service locally? 8 | - documentation? 9 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE: -------------------------------------------------------------------------------- 1 | Describe this code change as best you can here. Is it a new endpoint, bugfix or 2 | changes to the documentation? 3 | 4 | ### Checklist 5 | 6 | - [ ] Write tests 7 | - [ ] Write documentation 8 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - bug 8 | # Label to use when marking an issue as stale 9 | staleLabel: wontfix 10 | # Comment to post when marking an issue as stale. Set to `false` to disable 11 | markComment: > 12 | This issue has been automatically marked as stale because it has not had 13 | recent activity. It will be closed if no further activity occurs. Thank you 14 | for your contributions. 15 | # Comment to post when closing a stale issue. Set to `false` to disable 16 | closeComment: false 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2012-2017 APIs.is 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [APIs.is](http://apis.is) - Making data pretty since 2012! 2 | 3 | [![Codeship](https://img.shields.io/codeship/7c0ce5a0-9901-0132-893b-365d53813970/master.svg)](https://codeship.com/projects/63542) 4 | [![Greenkeeper badge](https://badges.greenkeeper.io/apis-is/apis.svg)](https://greenkeeper.io/) 5 | 6 | The purpose of [APIs.is](http://apis.is) is to make data readily available to anyone interested. All data that is delivered through APIs.is is JSON formatted and scraped from open public websites. 7 | 8 | The code that is running the service is open source under the [MIT licence](https://en.wikipedia.org/wiki/MIT_License). The platform itself is hosted on a load balanced setup by [Advania](https://www.advania.com/). 9 | 10 | **Don't hesitate to lend a hand - All knowledge and help is much appreciated!** 11 | 12 | ## Maintainers 13 | 14 | [@kristjanmik](https://github.com/kristjanmik/) 15 | 16 | [@benediktvaldez](https://github.com/benediktvaldez/) 17 | 18 | [@koddsson](https://github.com/koddsson/) 19 | 20 | [@MiniGod](https://github.com/minigod/) 21 | 22 | ## Running locally 23 | 24 | To run the project locally, first clone this repository... 25 | ```sh 26 | $ git clone https://github.com/apis-is/apis.git 27 | ``` 28 | 29 | .... install the dependencies and run the project. 30 | 31 | ```sh 32 | $ npm install 33 | [Bunch of output] 34 | $ npm run 35 | ``` 36 | 37 | ## Tests 38 | 39 | To run the tests: 40 | ```sh 41 | $ npm test 42 | ``` 43 | 44 | The tests utilize a man-in-the-middle library called [nock](https://github.com/node-nock/nock) that 45 | intercepts requests that the tests made and respond with data from disk. The data was generated using 46 | the [record feature](https://github.com/node-nock/nock#recording) and saved in [`mock-data.json`](mock-data.json). 47 | 48 | If a endpoints data source has changed and the we need to re-record this data we can simply set the 49 | env variable `RECORD_MOCK_DATA` to a truthy value and run the tests. This will disable nock in the tests 50 | and make requests to each endpoints data source and save that to disk. 51 | 52 | ```sh 53 | RECORD_MOCK_DATA=true npm test 54 | ``` 55 | 56 | Newly added endpoints should mock the endpoints data source using the `nock` API since this initial 57 | data mocking was only made to help migrate to a mocking library. See the [original PR](https://github.com/apis-is/apis/pull/376) 58 | for more info. 59 | 60 | ## Adding a new Endpoint 61 | 62 | ### Step by Step 63 | 64 | 1. View current endpoints for structure and hierarchy. 65 | 2. Add a new folder to the `endpoints/` directory with the name of your endpoint. 66 | 3. The file will be loaded automatically. Remember to require the server. Bare minimum example endpoint: 67 | 68 | ```javascript 69 | const app = require('../../server'); 70 | 71 | app.get('/path', (req,res) => { 72 | //Sends out empty json object 73 | return res.json({}); 74 | }); 75 | ``` 76 | 77 | ### Additional requirements 78 | 79 | Add integration tests to the endpoint by creating a file called `integration_test.js` inside a `tests/` folder within your endpoint directory. For reference, please take a look at one of the integration tests. 80 | 81 | Add documentation for your endpoint to the `gh-pages` branch of this repo. 82 | 83 | ### More servers 84 | 85 | To ensure close to zero downtime, the plan is to start up more workers/servers around the world so that projects relying on this service will not be affected. Want to help out with that? Feel free to contact us by opening a [issue](https://github.com/apis-is/apis/issues/new). 86 | 87 | ### Helpful pointers 88 | 89 | - Endpoints can implement any node module, but try to use the ones that are already included in the project. 90 | - Information on how to handle requests and responses can be found [here](http://expressjs.com/api.html). 91 | - It is much appreciated that endpoints are thoroughly documented and written with care. 92 | - Issues are managed by the [GitHub issue tracker](https://github.com/apis-is/apis/issues). 93 | - Have fun and eat some cake! (preferrably just some plain vanilla cake, but whatever floats your boat) 94 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ -------------------------------------------------------------------------------- /docs/header.md: -------------------------------------------------------------------------------- 1 | 2 | 47 | 48 | -------------------------------------------------------------------------------- /endpoints/address/documentation.md: -------------------------------------------------------------------------------- 1 | # Icelandic Addresses 2 | 3 | Source [Iceland Post](https://postur.is) 4 | 5 | - GET [/address](https://apis.is/address) 6 | 7 | Lookup addresses in Iceland through the Icelandic Post API 8 | 9 | | Parameters | Description | Example | 10 | |--------------------|---------------|--------------------------------------------------------| 11 | | Address (required) | Valid address | [Laugarvegur 1](https://apis.is/address/Laugarvegur 1) | 12 | 13 | --- 14 | -------------------------------------------------------------------------------- /endpoints/address/graphql_schema.js: -------------------------------------------------------------------------------- 1 | const { 2 | GraphQLObjectType, 3 | GraphQLList, 4 | GraphQLString, 5 | } = require('graphql') 6 | 7 | const lookupAddresses = require('.') 8 | 9 | const addressType = new GraphQLObjectType({ 10 | name: 'Address', 11 | fields: { 12 | street: { 13 | type: GraphQLString, 14 | description: 'The name of the street the address belongs to', 15 | }, 16 | house: { 17 | type: GraphQLString, 18 | description: 'The house number the address belongs to', 19 | }, 20 | zip: { 21 | type: GraphQLString, 22 | description: 'The zip code where this address is registered', 23 | }, 24 | city: { 25 | type: GraphQLString, 26 | description: 'The city where this address is registered', 27 | }, 28 | apartment: { 29 | type: GraphQLString, 30 | description: 'A name for an apartment belonging to this address', 31 | }, 32 | letter: { 33 | type: GraphQLString, 34 | }, 35 | }, 36 | }) 37 | 38 | const addressesType = new GraphQLList(addressType) 39 | 40 | module.exports = { 41 | type: addressesType, 42 | args: { 43 | address: { type: GraphQLString }, 44 | }, 45 | resolve: (_, args) => { 46 | const address = args.address.replace(' ', '+') 47 | return lookupAddresses(address) 48 | }, 49 | } 50 | -------------------------------------------------------------------------------- /endpoints/address/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/first */ 2 | const request = require('request') 3 | const h = require('apis-helpers') 4 | const _ = require('lodash') 5 | const app = require('../../server') 6 | 7 | const lookupAddresses = address => new Promise((resolve, reject) => { 8 | request.get({ 9 | headers: { 'User-Agent': h.browser() }, 10 | url: `https://api.postur.is/PosturIs/ws.asmx/GetPostals?address=${address}`, 11 | }, (error, response, body) => { 12 | if (error || response.statusCode !== 200) { 13 | reject(error) 14 | } 15 | 16 | // There is a enclosing () in the response 17 | const data = _.flatten(JSON.parse(body.replace(/[()]/g, ''))) 18 | 19 | const results = _.map(data, elem => ({ 20 | street: elem.Gata, 21 | house: elem.Husnumer, 22 | zip: elem.Postnumer, 23 | city: elem.Sveitafelag, 24 | apartment: elem.Ibud, 25 | letter: elem.Stafur, 26 | })) 27 | resolve(results) 28 | }) 29 | }) 30 | 31 | app.get('/address/:address?', async (req, res) => { 32 | const address = ( 33 | req.query.address || req.params.address || '' 34 | ).replace(' ', '+') 35 | 36 | if (address === '') { 37 | res.status(400).json({ 38 | error: 'Please provide a valid address to lookup', 39 | }) 40 | } 41 | 42 | try { 43 | const results = await lookupAddresses(address) 44 | res.cache().json({ results }) 45 | } catch (error) { 46 | res.status(500).json({ error: 'www.postur.is refuses to respond or give back data' }) 47 | } 48 | }) 49 | 50 | module.exports = lookupAddresses 51 | -------------------------------------------------------------------------------- /endpoints/address/tests/integration_test.js: -------------------------------------------------------------------------------- 1 | const request = require('request') 2 | const helpers = require('../../../lib/test_helpers') 3 | 4 | describe('zip', () => { 5 | it('should return an array of objects containing correct fields', (done) => { 6 | const fieldsToCheckFor = ['street', 'house', 'zip', 'city', 'apartment', 'letter'] 7 | const params = helpers.testRequestParams('/address', { address: 'laugavegur 26' }) 8 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 9 | request.get(params, resultHandler) 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /endpoints/aur/documentation.md: -------------------------------------------------------------------------------- 1 | # Auroracoin to ISK exchange 2 | 3 | Source [ISX.is](https://isx.is) 4 | 5 | - GET [/aur](https://apis.is/aur) 6 | 7 | Current Auroracoin exchange rate and various statistics for the past day. 8 | 9 | --- 10 | -------------------------------------------------------------------------------- /endpoints/aur/index.js: -------------------------------------------------------------------------------- 1 | const request = require('request') 2 | const moment = require('moment') 3 | const app = require('../../server') 4 | 5 | // set cache time to 2 minutes 6 | const cacheTime = 120 7 | 8 | function queryGzipJson(url, callback) { 9 | const headers = { 10 | 'user-agent': 'apis.is', 11 | } 12 | request.get({ 13 | headers, 14 | url, 15 | gzip: true, 16 | json: true, 17 | }, (error, response, data) => { 18 | if (error || response.statusCode !== 200) { 19 | callback(error, response, null) 20 | } 21 | callback(error, response, data) 22 | }) 23 | } 24 | 25 | function queryStats(callback) { 26 | queryGzipJson('https://isx.is/api/stats?currency=isk&market=aur', callback) 27 | } 28 | 29 | function queryHistory(callback) { 30 | // permitted values for timeframe are "1mon", "3mon", "6mon", "1year" and "ytd" 31 | // default is "1mon", "ytd" is from last new years eve to present 32 | queryGzipJson( 33 | 'https://isx.is/api/historical-prices?currency=isk&market=aur&timeframe=1year', 34 | callback 35 | ) 36 | } 37 | 38 | function queryTransactions(callback) { 39 | // set limit to OVER 9000 40 | queryGzipJson( 41 | 'https://isx.is/api/transactions?currency=isk&market=aur&limit=9001', 42 | callback 43 | ) 44 | } 45 | 46 | function queryOrderBook(callback) { 47 | queryGzipJson('https://isx.is/api/order-book?currency=isk&market=aur', callback) 48 | } 49 | 50 | function formatStatsData(data) { 51 | return { 52 | volume_1h: data.stats['1h_volume'], 53 | volume_1h_buy: data.stats['1h_volume_buy'], 54 | volume_1h_sell: data.stats['1h_volume_sell'], 55 | volume_24h: data.stats['24h_volume'], 56 | volume_24h_buy: data.stats['24h_volume_buy'], 57 | volume_24h_sell: data.stats['24h_volume_sell'], 58 | ask: data.stats.ask, 59 | bid: data.stats.bid, 60 | daily_change: data.stats.daily_change, 61 | daily_change_percent: data.stats.daily_change_percent, 62 | global_units: data.stats.global_units, 63 | global_volume: data.stats.global_volume, 64 | last_price: data.stats.last_price, 65 | last_transaction_type: data.stats.last_transaction_type.toLowerCase(), 66 | market_cap: data.stats.market_cap, 67 | max: data.stats.max, 68 | min: data.stats.min, 69 | open: data.stats.open, 70 | timestampApis: moment().format('YYYY-MM-DDTHH:mm:ss'), 71 | } 72 | } 73 | 74 | function formatHistoryData(data) { 75 | const formattedData = { 76 | results: [], 77 | currency: data.currency, 78 | market: data.market, 79 | timestampApis: moment().format('YYYY-MM-DDTHH:mm:ss'), 80 | } 81 | data.data.forEach((result) => { 82 | formattedData.results.push({ 83 | date: result.date, 84 | rate: result.price, 85 | }) 86 | }) 87 | return formattedData 88 | } 89 | 90 | function formatTransactionsData(data) { 91 | const formattedData = [] 92 | data.forEach((result) => { 93 | let transactionType = result.maker_type 94 | if (result.maker_type === 'kaup') { 95 | transactionType = 'buy' 96 | } else if (result.maker_type === 'sala') { 97 | transactionType = 'sell' 98 | } 99 | formattedData.push({ 100 | id: result.id, 101 | amount_isk: result.amount, 102 | amount_aur: result.btc, 103 | rate: result.price, 104 | type: transactionType, 105 | timestamp: moment(result.timestamp * 1000).format('YYYY-MM-DDTHH:mm:ss'), 106 | }) 107 | }) 108 | return formattedData 109 | } 110 | 111 | function formatOrderBookData(data) { 112 | const formattedData = [] 113 | data.forEach((result) => { 114 | formattedData.push({ 115 | rate: result.price, 116 | order_amount_aur: result.order_amount, 117 | order_value_isk: result.order_value, 118 | timestamp: moment(result.timestamp * 1000).format('YYYY-MM-DDTHH:mm'), 119 | }) 120 | }) 121 | return formattedData 122 | } 123 | 124 | function standardErrorResponse(res) { 125 | return res.status(500).json({ 126 | error: 'isx.is refuses to respond or give back data', 127 | }) 128 | } 129 | 130 | app.get('/aur', (req, res) => { 131 | queryStats((error, response, data) => { 132 | if (error || response.statusCode !== 200) { 133 | return standardErrorResponse(res) 134 | } 135 | return res.cache(cacheTime).json(formatStatsData(data)) 136 | }) 137 | }) 138 | 139 | app.get('/aur/history', (req, res) => { 140 | queryHistory((error, response, data) => { 141 | if (error || response.statusCode !== 200) { 142 | return standardErrorResponse(res) 143 | } 144 | return res.cache(cacheTime).json(formatHistoryData(data['historical-prices'])) 145 | }) 146 | }) 147 | 148 | app.get('/aur/transactions', (req, res) => { 149 | queryTransactions((error, response, data) => { 150 | if (error || response.statusCode !== 200) { 151 | return standardErrorResponse(res) 152 | } 153 | return res.cache(cacheTime).json({ 154 | results: formatTransactionsData(data.transactions.data), 155 | timestampApis: moment().format('YYYY-MM-DDTHH:mm:ss'), 156 | }) 157 | }) 158 | }) 159 | 160 | app.get('/aur/order-book', (req, res) => { 161 | queryOrderBook((error, response, data) => { 162 | if (error || response.statusCode !== 200) { 163 | return standardErrorResponse(res) 164 | } 165 | return res.cache(cacheTime).json({ 166 | ask: formatOrderBookData(data['order-book'].ask), 167 | bid: formatOrderBookData(data['order-book'].bid), 168 | timestampApis: moment().format('YYYY-MM-DDTHH:mm:ss'), 169 | }) 170 | }) 171 | }) 172 | -------------------------------------------------------------------------------- /endpoints/aur/tests/integration_test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-bitwise */ 2 | /* eslint-disable import/first */ 3 | /* eslint-disable import/extensions */ 4 | const assert = require('assert') 5 | const request = require('request') 6 | const moment = require('moment') 7 | const helpers = require('../../../lib/test_helpers.js') 8 | 9 | const transactionTypes = ['buy', 'sell'] 10 | 11 | describe('stats', function () { 12 | this.timeout(20000) 13 | const statsFieldsType = { 14 | ask: Number, 15 | bid: Number, 16 | daily_change: Number, 17 | daily_change_percent: Number, 18 | global_units: Number, 19 | global_volume: Number, 20 | last_price: Number, 21 | last_transaction_type: String, 22 | market_cap: Number, 23 | max: Number, 24 | min: Number, 25 | open: Number, 26 | volume_1h: Number, 27 | volume_1h_buy: Number, 28 | volume_1h_sell: Number, 29 | volume_24h: Number, 30 | volume_24h_buy: Number, 31 | volume_24h_sell: Number, 32 | timestampApis: Date, 33 | } 34 | const statsFields = Object.keys(statsFieldsType) 35 | const timestampFormat = 'YYYY-MM-DDTHH:mm:ss' 36 | it('should return object containing specific fields with correct types', (done) => { 37 | const params = helpers.testRequestParams('/aur') 38 | request.get(params, (err, res, body) => { 39 | if (err) throw err 40 | let json 41 | try { 42 | json = JSON.parse(body) 43 | } catch (e) { 44 | throw e 45 | } 46 | helpers.assertPresenceOfFields(statsFields, [json]) 47 | helpers.assertTypesOfFields(statsFieldsType, [json]) 48 | assert( 49 | ~transactionTypes.indexOf(json.last_transaction_type), 50 | `Unexpected transaction type '${json.last_transaction_type}'` 51 | ) 52 | assert( 53 | moment(json.timestampApis, timestampFormat, true).isValid(), 54 | `Unexpected timestamp format, ${json.timestampApis}' does not match ${timestampFormat}` 55 | ) 56 | done() 57 | }) 58 | }) 59 | }) 60 | 61 | describe('history', function () { 62 | this.timeout(20000) 63 | const historyFieldsType = { 64 | date: Date, 65 | rate: Number, 66 | } 67 | const historyFields = Object.keys(historyFieldsType) 68 | const timestampFormat = 'YYYY-MM-DDTHH:mm:ss' 69 | const dateFormat = 'YYYY-MM-DD' 70 | it('should return results array of objects containing correct fields', (done) => { 71 | const params = helpers.testRequestParams('/aur/history') 72 | request.get(params, (err, res, body) => { 73 | if (err) throw err 74 | let json 75 | try { 76 | json = JSON.parse(body) 77 | } catch (e) { 78 | throw e 79 | } 80 | assert(json.results, 'Does not contain a \'results\' field') 81 | assert(json.currency, 'Does not contain a \'currency\' field') 82 | assert(json.market, 'Does not contain a \'market\' field') 83 | helpers.assertPresenceOfFields(historyFields, json.results) 84 | helpers.assertTypesOfFields(historyFieldsType, json.results) 85 | assert( 86 | json.currency.constructor === String, 87 | 'Unexpected currency type, should be String' 88 | ) 89 | assert( 90 | json.market.constructor === String, 91 | 'Unexpected market type, should be String' 92 | ) 93 | assert( 94 | moment(json.timestampApis, timestampFormat, true).isValid(), 95 | `Unexpected timestamp format, '${json.timestampApis}' does not match ${timestampFormat}` 96 | ) 97 | json.results.forEach((result, i) => { 98 | assert( 99 | moment(result.date, dateFormat, true).isValid(), 100 | (`Unexpected date format in result #${i};` + 101 | ` '${result.date}' does not match ${dateFormat}`) 102 | ) 103 | }) 104 | done() 105 | }) 106 | }) 107 | }) 108 | 109 | describe('transactions', function () { 110 | this.timeout(20000) 111 | const transactionsFieldsType = { 112 | id: Number, 113 | amount_isk: Number, 114 | amount_aur: Number, 115 | rate: Number, 116 | type: String, 117 | timestamp: Date, 118 | } 119 | const transactionsFields = Object.keys(transactionsFieldsType) 120 | const timestampFormat = 'YYYY-MM-DDTHH:mm:ss' 121 | it('should return results array of objects containing correct fields', (done) => { 122 | const params = helpers.testRequestParams('/aur/transactions') 123 | request.get(params, (err, res, body) => { 124 | if (err) throw err 125 | let json 126 | try { 127 | json = JSON.parse(body) 128 | } catch (e) { 129 | throw e 130 | } 131 | assert(json.results, 'Does not contain a \'results\' field') 132 | helpers.assertPresenceOfFields(transactionsFields, json.results) 133 | helpers.assertTypesOfFields(transactionsFieldsType, json.results) 134 | json.results.forEach((result, i) => { 135 | assert( 136 | ~transactionTypes.indexOf(result.type), 137 | `Unexpected transaction type '${result.type}' in result #${i}` 138 | ) 139 | assert( 140 | moment(result.timestamp, timestampFormat, true).isValid(), 141 | (`Unexpected timestamp format in result #${i};` + 142 | ` '${result.timestamp}' does not match ${timestampFormat}`) 143 | ) 144 | }) 145 | done() 146 | }) 147 | }) 148 | }) 149 | 150 | describe('order-book', function () { 151 | this.timeout(20000) 152 | const bidAskFieldsType = { 153 | rate: Number, 154 | order_amount_aur: Number, 155 | order_value_isk: Number, 156 | timestamp: Date, 157 | } 158 | const bidAskFields = Object.keys(bidAskFieldsType) 159 | const timestampFormat = 'YYYY-MM-DDTHH:mm' 160 | it('should return bid/ask arrays of objects containing correct fields', (done) => { 161 | const params = helpers.testRequestParams('/aur/order-book') 162 | request.get(params, (err, res, body) => { 163 | if (err) throw err 164 | let json 165 | try { 166 | json = JSON.parse(body) 167 | } catch (e) { 168 | throw e 169 | } 170 | assert(json.ask, 'Does not contain a \'ask\' field') 171 | helpers.assertPresenceOfFields(bidAskFields, json.ask) 172 | helpers.assertTypesOfFields(bidAskFieldsType, json.ask) 173 | json.ask.forEach((result, i) => { 174 | assert( 175 | moment(result.timestamp, timestampFormat, true).isValid(), 176 | (`Unexpected timestamp format in ask result #${i};` + 177 | ` '${result.timestamp}' does not match ${timestampFormat}`) 178 | ) 179 | }) 180 | assert(json.bid, 'Does not contain a \'bid\' field') 181 | helpers.assertPresenceOfFields(bidAskFields, json.bid) 182 | helpers.assertTypesOfFields(bidAskFieldsType, json.bid) 183 | json.bid.forEach((result, i) => { 184 | assert( 185 | moment(result.timestamp, timestampFormat, true).isValid(), 186 | (`Unexpected timestamp format in bid result #${i};` + 187 | ` "${result.timestamp}" does not match ${timestampFormat}`) 188 | ) 189 | }) 190 | done() 191 | }) 192 | }) 193 | }) 194 | -------------------------------------------------------------------------------- /endpoints/bus/documentation.md: -------------------------------------------------------------------------------- 1 | # Icelandic Bus System 2 | 3 | Source [bus.is](https://bus.is) 4 | 5 | - GET [/bus/realtime](https://apis.is/bus/realtime) 6 | 7 | Real-time location of busses. Results are only shown for active busses. 8 | 9 | | Parameters | | 10 | |-------------|---| 11 | | BUSSES | Comma seperated list of numbers. Good to know: If not declared, the endpoint will return location of all available busses. | 12 | 13 | --- 14 | -------------------------------------------------------------------------------- /endpoints/bus/graphql_schema.js: -------------------------------------------------------------------------------- 1 | const { 2 | GraphQLObjectType, 3 | GraphQLList, 4 | GraphQLString, 5 | } = require('graphql') 6 | 7 | const getBusRoutes = require('./realtime') 8 | 9 | const busInfoType = new GraphQLObjectType({ 10 | name: 'BusInfo', 11 | fields: { 12 | unixTime: { type: GraphQLString }, 13 | x: { type: GraphQLString }, 14 | y: { type: GraphQLString }, 15 | from: { type: GraphQLString }, 16 | to: { type: GraphQLString }, 17 | }, 18 | }) 19 | 20 | const busRouteType = new GraphQLObjectType({ 21 | name: 'BusRoutes', 22 | fields: { 23 | busNr: { type: GraphQLString }, 24 | busses: { type: new GraphQLList(busInfoType) }, 25 | }, 26 | }) 27 | 28 | const busRoutesType = new GraphQLList(busRouteType) 29 | 30 | module.exports = { 31 | type: busRoutesType, 32 | args: { 33 | busses: { type: GraphQLString }, 34 | }, 35 | resolve: async (_, args) => { 36 | const data = await getBusRoutes(args) 37 | return data.results 38 | }, 39 | } 40 | -------------------------------------------------------------------------------- /endpoints/bus/index.js: -------------------------------------------------------------------------------- 1 | // const request = require('request') 2 | // const h = require('apis-helpers') 3 | const app = require('../../server') 4 | 5 | app.post('/bus/search', (req, res) => { 6 | return res.status(404).json({ 7 | error: 'This api endpoint has been closed, because Bus.is changed it\'s markup.', 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /endpoints/bus/realtime.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | const h = require('apis-helpers') 4 | const request = require('request') 5 | const app = require('../../server') 6 | const isn2wgs = require('isn2wgs') 7 | 8 | const debug = require('debug')('bus/realtime') 9 | 10 | const getBusRoutes = (data) => new Promise((resolve, reject) => { 11 | request.get('http://straeto.is/bitar/bus/livemap/json.jsp', function (error, response, body) { 12 | if (error || response.statusCode !== 200) { 13 | return reject('The bus api is down or refuses to respond') 14 | } 15 | 16 | var obj 17 | try { 18 | obj = JSON.parse(body) 19 | } catch (error) { 20 | return reject(error) 21 | } 22 | 23 | var activeBusses = [], 24 | requestedBusses = [] 25 | 26 | const routes = obj.routes || [] 27 | 28 | routes.forEach(function (object, key) { 29 | activeBusses.push(object.id) 30 | }) 31 | 32 | if (data.busses) { // Post busses = 1,2,3,4,5 33 | requestedBusses = data.busses.split(',') 34 | 35 | for (var i in requestedBusses) { // Prevent requested to busses that are not available 36 | if (activeBusses.indexOf(requestedBusses[i]) == -1) { 37 | requestedBusses.splice(requestedBusses.indexOf(requestedBusses[i]), 1) 38 | } 39 | } 40 | } else { 41 | // No bus was posted, use all active busses 42 | requestedBusses = activeBusses 43 | } 44 | 45 | var objString = requestedBusses.join('%2C') 46 | 47 | request('http://straeto.is/bitar/bus/livemap/json.jsp?routes=' + objString, function (error, response, body) { 48 | 49 | if (error || response.statusCode !== 200) { 50 | return reject(error) 51 | } 52 | 53 | try { 54 | var data = JSON.parse(body) 55 | } catch (e) { 56 | return reject(e) 57 | } 58 | 59 | var routes = data.routes || [] 60 | 61 | var objRoutes = { 62 | results: [] 63 | } 64 | routes.forEach(function (route, key) { 65 | 66 | var objRoute = { 67 | busNr: route.id || '', // will be undefined if none are active 68 | busses: [] 69 | } 70 | objRoutes.results.push(objRoute) 71 | 72 | if (!route.busses) return // No busses active, eg. after schedule 73 | 74 | route.busses.forEach(function (bus, key) { 75 | 76 | var location = isn2wgs(bus.X, bus.Y), 77 | oneRoute = { 78 | 'unixTime': Date.parse(bus.TIMESTAMPREAL) / 1000, 79 | 'x': location.latitude, 80 | 'y': location.longitude, 81 | 'from': bus.FROMSTOP, 82 | 'to': bus.TOSTOP 83 | } 84 | objRoute.busses.push(oneRoute) 85 | 86 | }) 87 | 88 | }) 89 | return resolve(objRoutes) 90 | }) 91 | }) 92 | }) 93 | 94 | app.get('/bus/realtime', function (req, res) { 95 | var data = req.query 96 | 97 | getBusRoutes(data).then( 98 | (routes) => res.cache(1).json(routes), 99 | () => res.status(500).json({ error:'Something is wrong with the data provided from the data source' }) 100 | ) 101 | }) 102 | 103 | module.exports = getBusRoutes 104 | -------------------------------------------------------------------------------- /endpoints/bus/search.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | var app = require('../../server') 4 | 5 | app.post('/bus/search', function (req, res) { 6 | res.status(404).json({ error: 'This api endpoint has been closed temporarily, because Bus.is changed it\'s markup.' }) 7 | }) // Old 8 | app.get('/bus/search', function (req, res) { 9 | res.status(404).json({ error: 'This api endpoint has been closed temporarily, because Bus.is changed it\'s markup.' }) 10 | }) 11 | -------------------------------------------------------------------------------- /endpoints/bus/tests/integration_test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | var request = require('request'); 4 | var assert = require('assert'); 5 | var helpers = require('../../../lib/test_helpers.js'); 6 | var sinon = require('sinon'); 7 | var getBusRoutes = require('../realtime.js'); 8 | 9 | describe('bus', function() { 10 | var fieldsToCheckFor = ['busNr', 'busses']; 11 | 12 | var customCheck = function(json) { 13 | var busses = json.results[0].busses; 14 | if (busses.length > 0) return; 15 | helpers.assertPresenceOfFields( 16 | ['unixTime', 'x', 'y', 'from', 'to'], 17 | busses 18 | ); 19 | }; 20 | 21 | describe('realtime', function() { 22 | // describe('searching a single bus', function() { 23 | // it('should return an array of objects containing correct fields', function( 24 | // done 25 | // ) { 26 | // var params = helpers.testRequestParams('/bus/realtime?busses=1'); 27 | // var resultHandler = helpers.testRequestHandlerForFields( 28 | // done, 29 | // fieldsToCheckFor, 30 | // customCheck 31 | // ); 32 | // request.get(params, resultHandler); 33 | // }); 34 | // }); 35 | 36 | // describe('searching multiple busses', function () { 37 | // it('should return an array of objects containing correct fields', function (done) { 38 | // var params = helpers.testRequestParams('/bus/realtime?busses=1,5') 39 | // var resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor, customCheck) 40 | // request.get(params, resultHandler) 41 | // }) 42 | // }) 43 | 44 | // describe('searching for a non existant bus', function () { 45 | // it('should return an array of objects containing correct fields', function (done) { 46 | // var params = helpers.testRequestParams('/bus/realtime?busses=999') 47 | // var resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 48 | // request.get(params, resultHandler) 49 | // }) 50 | // }) 51 | 52 | describe('when the data source returns an error', () => { 53 | before(function() { 54 | sinon 55 | .stub(request, 'get') 56 | .yields( 57 | "We don't want no scrapers around these here parts", 58 | null, 59 | null 60 | ); 61 | }); 62 | 63 | after(function() { 64 | request.get.restore(); 65 | }); 66 | 67 | it('should return a appropriate message', done => { 68 | getBusRoutes({}) 69 | .then(data => { 70 | should.fail( 71 | 'Got back data even though the data source returned an error' 72 | ); 73 | }) 74 | .catch(error => done()); 75 | }); 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /endpoints/calendar/graphql_schema.js: -------------------------------------------------------------------------------- 1 | const { 2 | GraphQLObjectType, 3 | GraphQLList, 4 | GraphQLString, 5 | GraphQLBoolean, 6 | } = require('graphql') 7 | const { GraphQLDate } = require('../../graphql/types/GraphQLDate') 8 | 9 | const lookupHolidays = require('.') 10 | 11 | const holiday = new GraphQLObjectType({ 12 | name: 'Holiday', 13 | description: 'An Icelandic holiday', 14 | fields: { 15 | date: { 16 | type: GraphQLDate, 17 | description: 'The datetime in ISO 8601 date format', 18 | }, 19 | description: { 20 | type: GraphQLString, 21 | description: 'The name of the holiday in Icelandic', 22 | }, 23 | holiday: { 24 | type: GraphQLBoolean, 25 | description: 'A boolean value indicating wether this is a holiday or not', 26 | }, 27 | }, 28 | }) 29 | 30 | const holidays = new GraphQLList(holiday) 31 | 32 | module.exports = { 33 | type: holidays, 34 | args: { 35 | year: { type: GraphQLString }, 36 | month: { type: GraphQLString }, 37 | day: { type: GraphQLString }, 38 | }, 39 | resolve: async (_, { year, month, day }) => { 40 | try { 41 | return await lookupHolidays(year, month, day) 42 | } catch (error) { 43 | throw new Error(error) 44 | } 45 | }, 46 | } 47 | -------------------------------------------------------------------------------- /endpoints/calendar/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-promise-reject-errors */ 2 | /* eslint-disable import/first */ 3 | const fridagar = require('fridagar') 4 | const debug = require('debug')('endpoint:calendar') 5 | const { range, isString } = require('lodash') 6 | const app = require('../../server') 7 | 8 | const canBeInt = (intLike) => { 9 | const num = Number.parseInt(intLike, 10) 10 | return !Number.isNaN(num) 11 | } 12 | 13 | const normalizeParams = (year, month, day) => { 14 | // If string parsing failed, reject the promise 15 | if (isString(year) && !canBeInt(year)) return { error: 'Year must be a number' } 16 | if (isString(month) && !canBeInt(month)) return { error: 'Month must be a number' } 17 | if (isString(day) && !canBeInt(day)) return { error: 'Day must be a number' } 18 | 19 | return { year, month, day } 20 | } 21 | 22 | const lookupHolidays = (yearStr, monthStr, dayStr) => new Promise((resolve, reject) => { 23 | const { 24 | year, month, day, error, 25 | } = normalizeParams(yearStr, monthStr, dayStr) 26 | 27 | // Reject promise with relevant error when in error states 28 | if (error) reject({ error }) 29 | if (!year) reject({ error: 'No year was provided' }) 30 | if (!year && !month && !day) reject({ error: 'No parameters were provided' }) 31 | 32 | if (year && !month && !day) { 33 | // Year 34 | const holidays = range(1, 13).reduce((sum, current) => { 35 | debug(`Getting year: ${year}, month: ${current}`) 36 | return sum.concat(fridagar.getHolidays(year, current)) 37 | }, []) 38 | resolve(holidays) 39 | } else if (year && month && !day) { 40 | // Year, Month 41 | resolve(fridagar.getHolidays(year, month)) 42 | } else if (year && month && day) { 43 | // Year, Month, Day 44 | const holiday = fridagar.getHolidays(year, month).find((current) => { 45 | return current.date.toISOString().startsWith(`${year}-${month}-${day}`) 46 | }) 47 | const results = holiday || { 48 | date: new Date(`${year}-${month}-${day}T00:00:00.000Z`), 49 | description: null, 50 | holiday: false, 51 | } 52 | // Wrap the single holiday in an array to keep responses consistent 53 | resolve([results]) 54 | } 55 | }) 56 | 57 | const lookupAndRespondHolidays = async (res, ...args) => { 58 | try { 59 | const results = await lookupHolidays(...args) 60 | res.json({ results }) 61 | } catch (error) { 62 | res.status(400).json(error) 63 | } 64 | } 65 | 66 | app.get('/calendar/', (req, res) => { 67 | return res.json({ 68 | description: 'Returns if a given date range has or is a holiday', 69 | provider: 'https://www.npmjs.com/package/fridagar', 70 | available_endpoints: [ 71 | '/calendar/:year', 72 | '/calendar/:year/:month', 73 | '/calendar/:year/:month/:day', 74 | ], 75 | }) 76 | }) 77 | 78 | app.get('/calendar/:year', (req, res) => { 79 | const { year } = req.params 80 | lookupAndRespondHolidays(res, year) 81 | }) 82 | 83 | app.get('/calendar/:year/:month', (req, res) => { 84 | const { year, month } = req.params 85 | lookupAndRespondHolidays(res, year, month) 86 | }) 87 | 88 | app.get('/calendar/:year/:month/:day', (req, res) => { 89 | const { year, month, day } = req.params 90 | lookupAndRespondHolidays(res, year, month, day) 91 | }) 92 | 93 | module.exports = lookupHolidays 94 | module.exports.normalizeParams = normalizeParams 95 | -------------------------------------------------------------------------------- /endpoints/calendar/tests/integration_test.js: -------------------------------------------------------------------------------- 1 | const request = require('request') 2 | const expect = require('expect') 3 | const helpers = require('../../../lib/test_helpers') 4 | const { normalizeParams } = require('..') 5 | 6 | describe('calendar/:year', () => { 7 | it('should return an array of objects containing correct fields', (done) => { 8 | const fieldsToCheckFor = ['date', 'description', 'holiday'] 9 | const params = helpers.testRequestParams('/calendar/2016') 10 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 11 | request.get(params, resultHandler) 12 | }) 13 | }) 14 | 15 | describe('calendar/:year/month', () => { 16 | it('should return an array of objects containing correct fields', (done) => { 17 | const fieldsToCheckFor = ['date', 'description', 'holiday'] 18 | const params = helpers.testRequestParams('/calendar/2016/06') 19 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 20 | request.get(params, resultHandler) 21 | }) 22 | }) 23 | 24 | describe('calendar/:year/:month/:day', () => { 25 | it('should return an array of objects containing correct fields', (done) => { 26 | const fieldsToCheckFor = ['date', 'description', 'holiday'] 27 | const params = helpers.testRequestParams('/calendar/2016/06/17') 28 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 29 | request.get(params, resultHandler) 30 | }) 31 | }) 32 | 33 | describe('validateParams', () => { 34 | it('should return an error if a parameter could not be parsed as an Int', () => { 35 | expect(normalizeParams('bogusYear', 1, 1)) 36 | .toEqual({ error: 'Year must be a number' }) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /endpoints/car/documentation.md: -------------------------------------------------------------------------------- 1 | # Cars in Iceland 2 | 3 | Source: [The Road Traffic Directorate](http://www.samgongustofa.is/umferd/okutaeki/okutaekjaskra/uppfletting) 4 | 5 | - GET [/car](https://apis.is/car) 6 | 7 | Search the Icelandic vehicle registry. 8 | 9 | | Parameters | Description | Example | 10 | |--------------------|-----------------|-------------------------------------------| 11 | | Number (required) | Registry number | [AA031](https://apis.is/car?number=AA031) | 12 | 13 | --- 14 | -------------------------------------------------------------------------------- /endpoints/car/graphql_schema.js: -------------------------------------------------------------------------------- 1 | const { 2 | GraphQLObjectType, 3 | GraphQLString, 4 | } = require('graphql') 5 | 6 | const lookupCar = require('.') 7 | 8 | const car = new GraphQLObjectType({ 9 | name: 'Car', 10 | description: 'Holds information about a registered Icelandic car', 11 | fields: { 12 | type: { 13 | type: GraphQLString, 14 | description: 'The car brand, model and color ', 15 | }, 16 | subType: { 17 | type: GraphQLString, 18 | description: 'The car model', 19 | }, 20 | color: { 21 | type: GraphQLString, 22 | description: 'The cars color', 23 | }, 24 | registryNumber: { 25 | type: GraphQLString, 26 | description: 'The number on the cars plate', 27 | }, 28 | number: { 29 | type: GraphQLString, 30 | description: 'The original number on the car is registered under', 31 | }, 32 | factoryNumber: { 33 | type: GraphQLString, 34 | description: 'The factory number of the car', 35 | }, 36 | registeredAt: { 37 | type: GraphQLString, 38 | description: 'The date when the car was registered on the format DD.MM.YYYY', 39 | }, 40 | pollution: { 41 | type: GraphQLString, 42 | description: 'The polution rate of the car in g/km', 43 | }, 44 | weight: { 45 | type: GraphQLString, 46 | description: 'The weight of the car in kg', 47 | }, 48 | status: { 49 | type: GraphQLString, 50 | description: 'The status of the car. Example: `Í lagi`', 51 | }, 52 | nextCheck: { 53 | type: GraphQLString, 54 | description: 'The date of this cars next routine check on the format DD.MM.YYYY', 55 | }, 56 | }, 57 | }) 58 | 59 | module.exports = { 60 | type: car, 61 | args: { 62 | carPlate: { 63 | type: GraphQLString, 64 | description: 'The numer on the cars plate', 65 | }, 66 | }, 67 | resolve: async (_, { carPlate }) => { 68 | try { 69 | return await lookupCar(carPlate) 70 | } catch (error) { 71 | throw new Error(error) 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /endpoints/car/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-promise-reject-errors */ 2 | const request = require('request') 3 | const $ = require('cheerio') 4 | const h = require('apis-helpers') 5 | const app = require('../../server') 6 | 7 | const lookupCar = plate => new Promise((resolve, reject) => { 8 | // Encode carPlate so that Icelandic characters will work 9 | const carPlate = encodeURIComponent(plate) 10 | const url = `http://www.samgongustofa.is/umferd/okutaeki/okutaekjaskra/uppfletting?vq=${carPlate}` 11 | 12 | request.get({ 13 | headers: { 'User-Agent': h.browser() }, 14 | url, 15 | }, (error, response, body) => { 16 | if (error || response.statusCode !== 200) { 17 | reject('www.samgongustofa.is refuses to respond or give back data') 18 | } 19 | 20 | const data = $(body) 21 | const fields = [] 22 | 23 | data.find('.vehicleinfo ul li').each(function () { 24 | const val = $(this).find('span').text() 25 | fields.push(val) 26 | }) 27 | 28 | if (fields.length > 0) { 29 | resolve({ 30 | type: fields[0], 31 | subType: fields[0].substring(fields[0].indexOf('-') + 2, fields[0].indexOf('(') - 1), 32 | color: fields[0].substring(fields[0].indexOf('(') + 1, fields[0].indexOf(')')), 33 | registryNumber: fields[1], 34 | number: fields[2], 35 | factoryNumber: fields[3], 36 | registeredAt: fields[4], 37 | pollution: fields[5], 38 | weight: fields[6], 39 | status: fields[7], 40 | nextCheck: fields[8], 41 | }) 42 | } else { 43 | reject(`No car found with the registry number ${plate}`) 44 | } 45 | }) 46 | }) 47 | 48 | app.get('/car', async (req, res) => { 49 | const carPlate = req.query.number || req.query.carPlate || '' 50 | 51 | if (!carPlate) { 52 | return res.status(431).json({ error: 'Please provide a valid carPlate to lookup' }) 53 | } 54 | 55 | try { 56 | const car = await lookupCar(carPlate) 57 | res.cache().json({ results: [car] }) 58 | } catch (error) { 59 | res.status(500).json({ error }) 60 | } 61 | }) 62 | 63 | module.exports = lookupCar 64 | -------------------------------------------------------------------------------- /endpoints/car/tests/integration_test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/extensions */ 2 | const request = require('request') 3 | const helpers = require('../../../lib/test_helpers.js') 4 | 5 | describe('car', () => { 6 | it('should return an array of objects containing correct fields', (done) => { 7 | const fieldsToCheckFor = [ 8 | 'registryNumber', 9 | 'number', 10 | 'factoryNumber', 11 | 'type', 12 | 'subType', 13 | 'color', 14 | 'registeredAt', 15 | 'status', 16 | 'nextCheck', 17 | 'pollution', 18 | 'weight', 19 | ] 20 | const params = helpers.testRequestParams('/car', { carPlate: 'AA031' }) 21 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 22 | request.get(params, resultHandler) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /endpoints/carparks/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-globals */ 2 | /* eslint-disable no-plusplus */ 3 | const request = require('request') 4 | const $ = require('cheerio') 5 | const h = require('apis-helpers') 6 | const app = require('../../server') 7 | 8 | app.get('/carparks', (req, res) => { 9 | const url = 'http://www.bilastaedasjodur.is/' 10 | 11 | request.get({ 12 | headers: { 'User-Agent': h.browser() }, 13 | url, 14 | }, (error, response, body) => { 15 | if (error || response.statusCode !== 200) { 16 | return res.status(500).json({ 17 | error: 'www.bilastaedasjodur.is refuses to respond or give back data', 18 | }) 19 | } 20 | 21 | try { 22 | // FIXME: What is the point of this? It doesn't seem to be used anywhere 23 | // eslint-disable-next-line no-unused-vars 24 | const data = $(body) 25 | } catch (exc) { 26 | return res.status(500).json({ error: 'Could not parse body' }) 27 | } 28 | 29 | const obj = { results: [] } 30 | const hus = $('.hus', $(body)) 31 | 32 | const js = body.match(/LatLng\((.*?)\)/g) 33 | 34 | function parseCoordinates(str) { 35 | try { 36 | const Regexp = /(?:^|\s)LatLng.(.*?)\)(?:\s|$)/g 37 | const match = Regexp.exec(str) 38 | return match[1].split(', ') 39 | } catch (exc) { 40 | return null 41 | } 42 | } 43 | 44 | for (let i = 0; i < hus.length; i++) { 45 | const that = hus[i] 46 | const freeSpaces = parseInt($(that).find('.ib.free h1').text(), 10) 47 | const totalSpaces = parseInt($(that).find('.ib.total h1').text(), 10) 48 | 49 | obj.results.push({ 50 | name: $(that).find('aside h2').text(), 51 | address: $(that).find('h5').text(), 52 | parkingSpaces: { 53 | free: !isNaN(freeSpaces) ? freeSpaces : null, 54 | total: !isNaN(totalSpaces) ? totalSpaces : null, 55 | }, 56 | coordinates: parseCoordinates(js[i]), 57 | }) 58 | } 59 | 60 | return res.cache(180).json(obj) 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /endpoints/carparks/tests/integration_test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/extensions */ 2 | const request = require('request') 3 | const helpers = require('../../../lib/test_helpers.js') 4 | 5 | describe('carparks', () => { 6 | it('should return an array of objects containing correct fields', (done) => { 7 | const fieldsToCheckFor = ['name', 'address', 'parkingSpaces', 'coordinates'] 8 | const params = helpers.testRequestParams('/carparks') 9 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 10 | request.get(params, resultHandler) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /endpoints/cinema/index.js: -------------------------------------------------------------------------------- 1 | const request = require('request') 2 | const cheerio = require('cheerio') 3 | const app = require('../../server') 4 | 5 | /** 6 | * Fetches movies for show today in Icelandic cinemas. 7 | * response - JSON: Movie data within an 'results' array. 8 | */ 9 | app.get('/cinema', (req, res) => { 10 | const url = 'http://kvikmyndir.is/bio/syningatimar/' 11 | 12 | request(url, (error, response, body) => { 13 | if (error) { 14 | return res.status(500).json({ error: `${url} not responding correctly...` }) 15 | } 16 | 17 | let $ 18 | 19 | try { 20 | $ = cheerio.load(body) 21 | } catch (e) { 22 | return res.status(500).json({ error: 'Could not load the body with cherrio.' }) 23 | } 24 | 25 | // Base object to be added to 26 | // and eventually sent as a JSON response. 27 | const obj = { 28 | results: [], 29 | } 30 | 31 | // DOM elements array containing all movies. 32 | const movies = $('.stimar') 33 | 34 | // Loop through movies 35 | movies.each(function () { 36 | // This movie. 37 | const movie = $(this) 38 | 39 | // Showtimes for JSON 40 | const showtimes = [] 41 | 42 | // Find all theaters and loop through them. 43 | const theaters = movie.find('[id^="myndbio"]') 44 | 45 | theaters.each(function () { 46 | // Single theater 47 | const theater = { 48 | theater: $(this).find('#bio a').text().trim(), 49 | schedule: [], 50 | } 51 | 52 | // Loop through each showtime and 53 | // add them to the theater schedule. 54 | $(this).find('.syningartimi_item').each(function () { 55 | theater.schedule.push($(this).text().trim()) 56 | }) 57 | 58 | // Add theater to showtimes array. 59 | showtimes.push(theater) 60 | }) 61 | 62 | const src = movie.find('img').attr('src') 63 | if (src) { 64 | const urls = src.match(/\/images\/poster\/.+\.(jpg|jpeg|png)/ig) || [] 65 | const imgUrl = `http://kvikmyndir.is${urls[0]}` 66 | const realeasedYear = movie 67 | .find('.mynd_titill_artal') 68 | .text() 69 | .replace('/[()]/g', '') 70 | 71 | // Create an object of info 72 | // and add it to the 'results' array. 73 | obj.results.push({ 74 | title: movie.find('.title').remove('.year').html().trim(), 75 | released: realeasedYear, 76 | restricted: null, 77 | imdb: movie.find('.imdbEinkunn').text().trim(), 78 | imdbLink: movie.find('.imdbEinkunn a').attr('href') ? movie.find('.imdbEinkunn a').attr('href').trim() : '', 79 | image: imgUrl, 80 | showtimes, 81 | }) 82 | } 83 | }) 84 | 85 | return res.cache().json(obj) 86 | }) 87 | }) 88 | 89 | /** 90 | * Fetches theaters that are showing movies today. 91 | * response - JSON: Theater data within an 'results' array. 92 | */ 93 | app.get('/cinema/theaters', (req, res) => { 94 | const url = 'http://kvikmyndir.is/bio/syningatimar_bio/' 95 | 96 | request(url, (error, response, body) => { 97 | if (error) return res.status(500).json({ error: `${url} not responding correctly...` }) 98 | 99 | // Cheerio declared and then attemted to load. 100 | let $ 101 | 102 | try { 103 | $ = cheerio.load(body) 104 | } catch (e) { 105 | return res.status(500).json({ error: 'Could not load the body with cherrio.' }) 106 | } 107 | 108 | // Base object to be added to 109 | // and eventually sent as a JSON response. 110 | const obj = { 111 | results: [], 112 | } 113 | 114 | // DOM elements array containing all theaters. 115 | const theaters = $('.stimar') 116 | 117 | // Loop through theaters 118 | theaters.each(function () { 119 | // This theater. 120 | const theater = $(this) 121 | 122 | // List of movies. 123 | const movies = [] 124 | 125 | // Loop through movies. 126 | theater.find('#myndbio_new').each(function () { 127 | // This movie. 128 | const movie = $(this) 129 | 130 | // Time schedule. 131 | const schedule = [] 132 | 133 | // Loop through each showtime on schedule today. 134 | movie.find('#timi_new div').each(function () { 135 | // Add time to the schedule. 136 | schedule.push($(this).find('.syningartimi_item').text().trim()) 137 | }) 138 | 139 | // Append new movie to the list of movies. 140 | movies.push({ 141 | title: movie.find('#bio a').text().trim(), 142 | schedule, 143 | }) 144 | }) 145 | 146 | // Create an object of info 147 | // and add it to the 'results' array. 148 | obj.results.push({ 149 | name: theater.find('#mynd_titill a').text().trim(), 150 | location: theater.find('.mynd_titill_artal').text().trim().replace(/(^\()|(\)$)/g, ''), 151 | image: `http://kvikmyndir.is${theater.find('.mynd_plakat img').attr('src')}`, 152 | movies, 153 | }) 154 | }) 155 | 156 | return res.cache().json(obj) 157 | }) 158 | }) 159 | -------------------------------------------------------------------------------- /endpoints/cinema/tests/integration_test.js: -------------------------------------------------------------------------------- 1 | const request = require('request') 2 | const helpers = require('../../../lib/test_helpers') 3 | 4 | describe('/cinema', () => { 5 | it('should return an array of objects containing correct fields', (done) => { 6 | const fieldsToCheckFor = ['title', 'released', 'restricted', 'imdb', 'imdbLink', 'image', 'showtimes'] 7 | const params = helpers.testRequestParams('/cinema') 8 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 9 | request.get(params, resultHandler) 10 | }) 11 | }) 12 | describe('cinema theaters', () => { }) 13 | -------------------------------------------------------------------------------- /endpoints/company/documentation.md: -------------------------------------------------------------------------------- 1 | # Icelandic companies 2 | 3 | Source: [Directorate of Internal Revenue in Iceland](http://rsk.is/) 4 | 5 | - GET [/company](https://apis.is/company) 6 | 7 | Search the Icelandic company registry 8 | **NB: At least one parameter is required.** 9 | 10 | | Parameters | Description | Example | 11 | |---------------|-----------------------------------------------|-------------------------------------------------| 12 | | Name | Company name | [Blendin](https://apis.is/company?name=blendin) | 13 | | Address | Company's address | | 14 | | SocialNumber | Company's social security number / ID number | | 15 | | VSKNR | Company's VAT-number (VSK in icelandic) || | 16 | 17 | --- 18 | -------------------------------------------------------------------------------- /endpoints/company/index.js: -------------------------------------------------------------------------------- 1 | const request = require('request') 2 | const cheerio = require('cheerio') 3 | const h = require('apis-helpers') 4 | const app = require('../../server') 5 | 6 | app.get('/company', (req, res) => { 7 | const queryString = { 8 | nafn: req.query.name || '', 9 | heimili: req.query.address || '', 10 | kt: req.query.socialnumber || '', 11 | vsknr: req.query.vsknr || '', 12 | } 13 | 14 | request.get({ 15 | headers: { 'User-Agent': h.browser() }, 16 | url: 'http://www.rsk.is/fyrirtaekjaskra/leit', 17 | qs: queryString, 18 | }, (error, response, body) => { 19 | if (error || response.statusCode !== 200) { 20 | return res.status(500).json({ error: 'www.rsk.is refuses to respond or give back data' }) 21 | } 22 | 23 | const obj = { results: [] } 24 | const $ = cheerio.load(body, { decodeEntities: false }) 25 | 26 | if ($('.resultnote').length === 0) { 27 | const tr = $('.boxbody > .nozebra tbody tr').first() 28 | if (tr.length > 0) { 29 | const name = $('.boxbody > h1').html() 30 | const sn = $('.boxbody > h1').html() 31 | 32 | obj.results.push({ 33 | name: name.substring(0, name.indexOf('(') - 1), 34 | sn: sn.substring(sn.length - 11, sn.length - 1), 35 | active: $('p.highlight').text().length === 0 ? 1 : 0, 36 | address: tr.find('td').eq(0).text(), 37 | }) 38 | } 39 | } else { 40 | $('table tr').slice(1).each((i, element) => { 41 | const td = $(element).find('td') 42 | const nameRoot = td.eq(1).text() 43 | const felagAfskrad = '(Félag afskráð)' 44 | 45 | obj.results.push({ 46 | name: nameRoot 47 | .replace('\n', '') 48 | .replace(felagAfskrad, '') 49 | .replace(/^\s\s*/, '') 50 | .replace(/\s\s*$/, ''), 51 | sn: td.eq(0).text(), 52 | active: nameRoot.indexOf(felagAfskrad) > -1 ? 0 : 1, 53 | address: td.eq(2).text(), 54 | }) 55 | }) 56 | } 57 | 58 | return res.cache(86400).json(obj) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /endpoints/company/tests/integration_test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/extensions */ 2 | const request = require('request') 3 | const helpers = require('../../../lib/test_helpers.js') 4 | 5 | describe('company', () => { 6 | const fieldsToCheckFor = ['name', 'sn', 'active', 'address'] 7 | 8 | describe('searching by name', () => { 9 | it('should return an array of objects containing correct fields', (done) => { 10 | const params = helpers.testRequestParams('/company', { name: 'hagar' }) 11 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 12 | request.get(params, resultHandler) 13 | }) 14 | }) 15 | describe('searching by address', () => { 16 | it('should return an array of objects containing correct fields', (done) => { 17 | const params = helpers.testRequestParams('/company', { address: 'Hagasmára' }) 18 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 19 | request.get(params, resultHandler) 20 | }) 21 | }) 22 | describe('search by socialnumber', () => { 23 | it('should return an array of objects containing correct fields', (done) => { 24 | const params = helpers.testRequestParams('/company', { socialnumber: '4307003590' }) 25 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 26 | request.get(params, resultHandler) 27 | }) 28 | }) 29 | describe('search by vsknr', () => { 30 | it('should return an array of objects containing correct fields', (done) => { 31 | const params = helpers.testRequestParams('/company', { vsknr: '78874' }) 32 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 33 | request.get(params, resultHandler) 34 | }) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /endpoints/concerts/documentation.md: -------------------------------------------------------------------------------- 1 | # Concerts in Iceland 2 | 3 | Source: [midi.is](https://midi.is/) 4 | 5 | - GET [/concerts](https://apis.is/concerts) 6 | 7 | Get a list of all the concerts in Iceland sorted by date 8 | 9 | --- 10 | -------------------------------------------------------------------------------- /endpoints/concerts/index.js: -------------------------------------------------------------------------------- 1 | const request = require('request') 2 | const _ = require('lodash') 3 | const app = require('../../server') 4 | 5 | app.get('/concerts', (req, res) => { 6 | const url = 'http://midi.is/Home/LoadMoreEventsByDate?eventType=Concerts&pageNumber=' 7 | const page = req.query.page || 1 8 | 9 | request.get(url + page, (error, response, body) => { 10 | if (error || response.statusCode !== 200) { 11 | return res.status(500).json({ 12 | error: 'Something came up when contacting the midi.is server!', 13 | }) 14 | } 15 | const events = JSON.parse(body) 16 | const filtered = _.map(events, event => ( 17 | _.pick( 18 | event, 19 | 'eventDateName', 20 | 'name', 21 | 'dateOfShow', 22 | 'userGroupName', 23 | 'eventHallName', 24 | 'imageSource' 25 | ) 26 | )) 27 | return res.cache(60).json({ results: filtered }) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /endpoints/concerts/tests/integration_test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/extensions */ 2 | const request = require('request') 3 | const helpers = require('../../../lib/test_helpers.js') 4 | 5 | describe('concerts', () => { 6 | const fieldsToCheckFor = [ 7 | 'eventDateName', 'name', 'dateOfShow', 'userGroupName', 'eventHallName', 8 | 'imageSource', 9 | ] 10 | it('should return an array of objects containing correct fields', (done) => { 11 | const params = helpers.testRequestParams('/concerts') 12 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 13 | request(params, resultHandler) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /endpoints/currency/arion.js: -------------------------------------------------------------------------------- 1 | const request = require('request') 2 | const moment = require('moment') 3 | const h = require('apis-helpers') 4 | const app = require('../../server') 5 | 6 | app.get('/currency/arion/:type?', (req, res) => { 7 | // types: AlmenntGengi,KortaGengi(valitor),SedlaGengi,AirportGengi 8 | const type = req.params.type || 'AlmenntGengi' 9 | let toSend = 'm=GetCurrencies' 10 | 11 | toSend += `&beginDate=${moment().format('YYYY-MM-DD')}` 12 | toSend += `&finalDate=${moment().format('YYYY-MM-DD')}` 13 | toSend += `¤cyType=${type}` 14 | toSend += '¤ciesAvailable=ISK%2CUSD%2CGBP%2CEUR%2CCAD%2CDKK%2CNOK%2CSEK%2CCHF%2CJPY%2CXDR' 15 | request.get({ 16 | headers: { 'content-type': 'application/x-www-form-urlencoded' }, 17 | url: 'https://www.arionbanki.is/Webservice/PortalCurrency.ashx', 18 | body: toSend, 19 | }, (error, response, body) => { 20 | if (error || response.statusCode !== 200) { 21 | return res.status(500).json({ error: 'www.arionbanki.is refuses to respond or give back data' }) 22 | } 23 | 24 | const jsonObject = JSON.parse(body) 25 | const currencies = [] 26 | 27 | jsonObject.forEach((object) => { 28 | const changePer = parseFloat(object.LastValueChange) / parseFloat(object.MidValue) 29 | const currency = { 30 | shortName: object.Ticker, 31 | longName: h.currency[object.Ticker].long, 32 | value: object.MidValue, 33 | askValue: object.AskValue, 34 | bidValue: object.BidValue, 35 | changeCur: object.LastValueChange, 36 | changePer: changePer.toFixed(2), 37 | } 38 | 39 | if (currency.changePer === '-0.00') { 40 | currency.changePer = 0 41 | } 42 | 43 | currencies.push(currency) 44 | }) 45 | 46 | return res.cache(60).json({ results: currencies }) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /endpoints/currency/borgun.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-plusplus */ 2 | /* eslint-disable prefer-destructuring */ 3 | const request = require('request') 4 | const xml2js = require('xml2js') 5 | const app = require('../../server') 6 | 7 | const parseString = xml2js.parseString 8 | 9 | app.get('/currency/borgun', (req, res) => { 10 | request.get( 11 | { url: 'https://www.borgun.is/currency/Default.aspx?function=all' }, 12 | (err, response, xml) => { 13 | if (err || response.statusCode !== 200) { 14 | return res.status(500).json({ error: 'www.borgun.is refuses to respond or give back data' }) 15 | } 16 | 17 | const currencies = [] 18 | parseString(xml, { explicitRoot: false }, (parseError, result) => { 19 | if (parseError || result.Status[0].ResultCode[0] !== '0') { 20 | return res.status(500).json({ error: `Unable to parse Borgun data: ${JSON.stringify(err)}` }) 21 | } 22 | 23 | for (let i = 0; i < result.Rate.length; i++) { 24 | const rate = result.Rate[i] 25 | currencies.push({ 26 | currencyCode: rate.CurrencyCode[0], 27 | currencyDescription: rate.CurrencyDescription[0], 28 | currencyRate: parseFloat(rate.CurrencyRate[0]), 29 | country: rate.Country[0], 30 | countryEnglish: rate.CountryEnglish[0], 31 | countryCode: rate.CountryCode[0], 32 | rateDate: rate.RateDate[0], 33 | }) 34 | } 35 | return res.cache(60).json({ results: currencies }) 36 | }) 37 | } 38 | ) 39 | }) 40 | -------------------------------------------------------------------------------- /endpoints/currency/documentation.md: -------------------------------------------------------------------------------- 1 | # Currency in relation to ISK 2 | 3 | Source: [m5.is](http://m5.is/), [Arion banki](https://arionbanki.is/) and [Landsbankinn](https://landsbankinn.is/) 4 | 5 | - GET [/currency/:source](https://apis.is/currency/:source) 6 | 7 | Get currency data in relation to ISK 8 | 9 | | Parameters | Description | Example | 10 | |--------------------|---------------------|-----------------------| 11 | | :source (required) | Which source to use | [m5](https://apis.is/currency/m5), [arion](https://apis.is/currency/arion) or [lb](https://apis.is/currency/lb) | 12 | 13 | 14 | --- 15 | -------------------------------------------------------------------------------- /endpoints/currency/index.js: -------------------------------------------------------------------------------- 1 | const app = require('../../server') 2 | 3 | app.get('/currency', (req, res) => { 4 | const provider = req.query.provider || 'arion' 5 | const providers = ['m5', 'arion', 'lb', 'borgun'] 6 | 7 | if (providers.indexOf(provider) >= 0) { 8 | return res.redirect(301, `/currency/${provider}`) 9 | } 10 | 11 | return res.status(404).json({ error: 'This provider does not exist', code: 2 }) 12 | }) 13 | -------------------------------------------------------------------------------- /endpoints/currency/lb.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-plusplus */ 2 | /* eslint-disable prefer-destructuring */ 3 | const request = require('request') 4 | const xml2js = require('xml2js') 5 | const app = require('../../server') 6 | 7 | const parseString = xml2js.parseString 8 | 9 | app.get('/currency/lb/:type?', (req, res) => { 10 | // A = Almennt gengi, S = Seðlagengi 11 | const type = req.params.type || 'A' 12 | 13 | request.get({ 14 | url: `https://www.landsbankinn.is/modules/markets/services/XMLGengi.asmx/NyjastaGengiByType?strTegund=${type}`, 15 | }, (err, response, xml) => { 16 | if (err || response.statusCode !== 200) { 17 | return res.status(500).json({ error: 'www.landsbankinn.is refuses to respond or give back data' }) 18 | } 19 | 20 | const currencies = [] 21 | parseString(xml, { explicitRoot: false }, (parseError, result) => { 22 | const arr = result.GjaldmidillRow 23 | for (let i = 0; i < arr.length; i++) { 24 | currencies.push({ 25 | shortName: arr[i].Mynt[0], 26 | longName: arr[i].Heiti[0], 27 | value: parseFloat(arr[i].Midgengi), 28 | askValue: parseFloat(arr[i].Sala), 29 | bidValue: parseFloat(arr[i].Kaup), 30 | changeCur: parseFloat(arr[i].Breyting[0]), 31 | changePer: parseFloat((parseFloat(arr[i].Breyting) / parseFloat(arr[i].Midgengi)).toFixed(2)), 32 | }) 33 | } 34 | return res.cache(60).json({ results: currencies }) 35 | }) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /endpoints/currency/m5.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/first */ 2 | const request = require('request') 3 | const h = require('apis-helpers') 4 | const cheerio = require('cheerio') 5 | const app = require('../../server') 6 | 7 | app.get('/currency/m5', (req, res) => { 8 | // FIXME: Not being used, comment out for now 9 | // const currencyNames = { 10 | // s: ['USD', 'DKK', 'EUR', 'JPY', 'CAD', 'NOK', 'GBP', 'CHF', 'SEK', 'TWI', 'XDR', 'ISK'], 11 | // l: [ 12 | // 'Bandarískur dalur', 13 | // 'Dönsk króna', 14 | // 'Evra', 15 | // 'Japanskt jen', 16 | // 'Kanadískur dalur', 17 | // 'Norsk króna', 18 | // 'Sterlingspund', 19 | // 'Svissneskur franki', 20 | // 'Sænsk króna', 21 | // 'Gengisvísitala', 22 | // 'SDR', 23 | // 'Íslensk króna', 24 | // ], 25 | // } 26 | 27 | request.get({ 28 | headers: { 'User-Agent': h.browser() }, 29 | url: 'http://www.m5.is/?gluggi=gjaldmidlar', 30 | }, (err, response, body) => { 31 | if (err || response.statusCode !== 200) { 32 | return res.status(500).json({ error: 'www.m5.is refuses to respond or give back data' }) 33 | } 34 | 35 | const $ = cheerio.load(body) 36 | const currencies = [] 37 | 38 | $('.table-striped tr').each(function () { 39 | const tds = $(this).find('td') 40 | const name = tds.eq(0).text() 41 | 42 | if (name) { 43 | currencies.push({ 44 | shortName: name, 45 | longName: h.currency[name].long, 46 | value: parseFloat(tds.eq(2).text().replace(',', '.')), 47 | askValue: 0, 48 | bidValue: 0, 49 | changeCur: parseFloat(tds.eq(4).text().replace(',', '.')), 50 | changePer: parseFloat(tds.eq(5).text().replace(/\((.*)%\)/, '$1').replace(',', '.')), 51 | }) 52 | } 53 | }) 54 | 55 | return res.cache(60).json({ results: currencies }) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /endpoints/currency/tests/arion.fixture: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "AskValue": 1, 4 | "BidValue": 1, 5 | "CustomsRate": 0, 6 | "LastValueChange": 0, 7 | "MainTicker": null, 8 | "MidValue": 1, 9 | "Ticker": "ISK", 10 | "Time": "2017-12-28T11:00:25.857", 11 | "Title": null 12 | }, 13 | { 14 | "AskValue": 105.29, 15 | "BidValue": 104.66, 16 | "CustomsRate": 0, 17 | "LastValueChange": -0.094886, 18 | "MainTicker": null, 19 | "MidValue": 104.975, 20 | "Ticker": "USD", 21 | "Time": "2017-12-28T11:00:25.857", 22 | "Title": null 23 | }, 24 | { 25 | "AskValue": 141.53, 26 | "BidValue": 140.69, 27 | "CustomsRate": 0, 28 | "LastValueChange": -0.155203, 29 | "MainTicker": null, 30 | "MidValue": 141.11, 31 | "Ticker": "GBP", 32 | "Time": "2017-12-28T11:00:25.857", 33 | "Title": null 34 | }, 35 | { 36 | "AskValue": 125.63, 37 | "BidValue": 124.87, 38 | "CustomsRate": 0, 39 | "LastValueChange": -0.158945, 40 | "MainTicker": null, 41 | "MidValue": 125.25, 42 | "Ticker": "EUR", 43 | "Time": "2017-12-28T11:00:25.857", 44 | "Title": null 45 | }, 46 | { 47 | "AskValue": 83.472, 48 | "BidValue": 82.973, 49 | "CustomsRate": 0, 50 | "LastValueChange": -0.145943, 51 | "MainTicker": null, 52 | "MidValue": 83.2225, 53 | "Ticker": "CAD", 54 | "Time": "2017-12-28T11:00:25.857", 55 | "Title": null 56 | }, 57 | { 58 | "AskValue": 16.873, 59 | "BidValue": 16.772, 60 | "CustomsRate": 0, 61 | "LastValueChange": -0.177483, 62 | "MainTicker": null, 63 | "MidValue": 16.8225, 64 | "Ticker": "DKK", 65 | "Time": "2017-12-28T11:00:25.857", 66 | "Title": null 67 | }, 68 | { 69 | "AskValue": 12.742, 70 | "BidValue": 12.666, 71 | "CustomsRate": 0, 72 | "LastValueChange": -0.437568, 73 | "MainTicker": null, 74 | "MidValue": 12.704, 75 | "Ticker": "NOK", 76 | "Time": "2017-12-28T11:00:25.857", 77 | "Title": null 78 | }, 79 | { 80 | "AskValue": 12.755, 81 | "BidValue": 12.679, 82 | "CustomsRate": 0, 83 | "LastValueChange": -0.195618, 84 | "MainTicker": null, 85 | "MidValue": 12.717, 86 | "Ticker": "SEK", 87 | "Time": "2017-12-28T11:00:25.857", 88 | "Title": null 89 | }, 90 | { 91 | "AskValue": 107.37, 92 | "BidValue": 106.73, 93 | "CustomsRate": 0, 94 | "LastValueChange": -0.111638, 95 | "MainTicker": null, 96 | "MidValue": 107.05, 97 | "Ticker": "CHF", 98 | "Time": "2017-12-28T11:00:25.857", 99 | "Title": null 100 | }, 101 | { 102 | "AskValue": 0.9327, 103 | "BidValue": 0.9272, 104 | "CustomsRate": 0, 105 | "LastValueChange": -0.224647, 106 | "MainTicker": null, 107 | "MidValue": 0.92995, 108 | "Ticker": "JPY", 109 | "Time": "2017-12-28T11:00:25.857", 110 | "Title": null 111 | }, 112 | { 113 | "AskValue": 149.63, 114 | "BidValue": 148.73, 115 | "CustomsRate": 0, 116 | "LastValueChange": -0.133485, 117 | "MainTicker": null, 118 | "MidValue": 149.18, 119 | "Ticker": "XDR", 120 | "Time": "2017-12-28T11:00:25.857", 121 | "Title": null 122 | } 123 | ] 124 | -------------------------------------------------------------------------------- /endpoints/currency/tests/integration_test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/extensions */ 2 | const fs = require('fs') 3 | const nock = require('nock') 4 | const request = require('request') 5 | const helpers = require('../../../lib/test_helpers.js') 6 | 7 | describe('currency', () => { 8 | // The only thing that changes is the form attribute, so why not just re-use the object 9 | const fieldsToCheckFor = ['shortName', 'longName', 'value', 'askValue', 'bidValue', 'changeCur', 'changePer'] 10 | 11 | describe('searching using provider "m5"', () => { 12 | before(() => { 13 | nock('http://www.m5.is') 14 | .get('/') 15 | .query({ gluggi: 'gjaldmidlar' }) 16 | .reply(200, fs.readFileSync(`${__dirname}/m5.fixture`)) 17 | }) 18 | 19 | it('should return an array of objects containing correct fields', (done) => { 20 | const params = helpers.testRequestParams('/currency/m5') 21 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 22 | request(params, resultHandler) 23 | }) 24 | }) 25 | 26 | describe('searching using provider "arion"', () => { 27 | before(() => { 28 | nock('https://www.arionbanki.is') 29 | .get('/Webservice/PortalCurrency.ashx') 30 | .reply(200, fs.readFileSync(`${__dirname}/arion.fixture`)) 31 | }) 32 | 33 | it('should return an array of objects containing correct fields', (done) => { 34 | const params = helpers.testRequestParams('/currency/arion') 35 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 36 | request(params, resultHandler) 37 | }) 38 | }) 39 | 40 | describe('searching using provider "lb"', () => { 41 | it('should return an array of objects containing correct fields', (done) => { 42 | const params = helpers.testRequestParams('/currency/lb') 43 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 44 | request(params, resultHandler) 45 | }) 46 | }) 47 | 48 | describe('searching using provider "borgun"', () => { 49 | it('should return an array of objects containing correct fields', (done) => { 50 | const params = helpers.testRequestParams('/currency/borgun') 51 | const fields = [ 52 | 'currencyCode', 53 | 'currencyDescription', 54 | 'currencyRate', 55 | 'country', 56 | 'countryEnglish', 57 | 'countryCode', 58 | 'rateDate', 59 | ] 60 | const resultHandler = helpers.testRequestHandlerForFields(done, fields) 61 | request(params, resultHandler) 62 | }) 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /endpoints/currency/tests/m5.fixture: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apis-is/apis/96a23ab30d5b498a0805da06cf87e84d61422c27/endpoints/currency/tests/m5.fixture -------------------------------------------------------------------------------- /endpoints/cyclecounter/documentation.md: -------------------------------------------------------------------------------- 1 | # Bicyclecounter in Reykjavik 2 | 3 | Source: [Bicycle Counter](http://bicyclecounter.dk/) 4 | 5 | - GET [/cyclecounter](https://apis.is/cyclecounter) 6 | 7 | Get current status of bicycle counters in Iceland, currently only one located by Suðurlandsbraut in Reykjavík. 8 | 9 | --- 10 | -------------------------------------------------------------------------------- /endpoints/cyclecounter/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-destructuring */ 2 | const request = require('request') 3 | const xml2js = require('xml2js') 4 | const app = require('../../server') 5 | 6 | const parseString = xml2js.parseString 7 | 8 | app.get('/cyclecounter', (req, res) => { 9 | request.get({ 10 | url: 'http://www.bicyclecounter.dk/BicycleCounter/GetCycleInfo?ran=1379500208853&StationId=235&LaneId=0', 11 | }, (err, response, xml) => { 12 | if (err || response.statusCode !== 200) { 13 | return res.status(500).json({ error: 'www.bicyclecounter.dk refuses to respond or give back data' }) 14 | } 15 | 16 | const cyclecounter = [] 17 | parseString(xml, { explicitRoot: false }, (parseError, result) => { 18 | cyclecounter.push({ 19 | DayCount: result.DayCount[0], 20 | YearCount: result.YearCount[0], 21 | Time: result.Time[0], 22 | Date: result.Date[0], 23 | }) 24 | 25 | return res.cache(5).json({ results: cyclecounter }) 26 | }) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /endpoints/cyclecounter/tests/integration_test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/extensions */ 2 | const request = require('request') 3 | const helpers = require('../../../lib/test_helpers.js') 4 | 5 | describe('cyclecounter', () => { 6 | // The only thing that changes is the form attribute, so why not just re-use the object 7 | const fieldsToCheckFor = ['DayCount', 'YearCount', 'Time', 'Date'] 8 | 9 | it('should return an array of objects containing correct fields', (done) => { 10 | const params = helpers.testRequestParams('/cyclecounter') 11 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 12 | request(params, resultHandler) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /endpoints/declension/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-destructuring */ 2 | /* eslint-disable no-mixed-operators */ 3 | /* eslint-disable import/first */ 4 | const url = require('url') 5 | const request = require('request') 6 | const helper = require('apis-helpers') 7 | const cheerio = require('cheerio') 8 | const _ = require('lodash') 9 | const app = require('../../server') 10 | 11 | const baseUrl = url.parse('http://dev.phpbin.ja.is/ajax_leit.php') 12 | 13 | // return permutation of a given word 14 | function getDeclensions(callback, providedParams) { 15 | const params = Object.assign({}, providedParams) 16 | request.get(params, (err, res, body) => { 17 | if (err || res.statusCode !== 200) { 18 | return res.status(500).json({ 19 | error: 'A request to dev.phpbin.ja.is resulted in a error', 20 | }) 21 | } 22 | 23 | let $ 24 | const sanitisedBody = body.replace(//g, '') 25 | 26 | try { 27 | $ = cheerio.load(sanitisedBody) 28 | } catch (error) { 29 | return res.status(500).json({ 30 | error: 'Parsing the data from dev.phpbin.ja.is resulted in a error', 31 | moreinfo: error, 32 | }) 33 | } 34 | 35 | // Links mean results! 36 | const result = $('a') 37 | 38 | // more than 1 result from request (ex: 'hús') 39 | if (result.length > 1) { 40 | // call recursively again with new url 41 | const id = result[0].attribs.on_click.match(/\d+/)[0] 42 | baseUrl.query = { id } 43 | params.url = url.format(baseUrl) 44 | return getDeclensions(callback, params) 45 | } 46 | 47 | // else just call func to return data 48 | return callback($) 49 | }) 50 | } 51 | 52 | // Creates a sequence of integers, each iteration creates a value and increments that value by 1 53 | // step: specify how often to run the iteration 54 | // increment: how much to increment after each iteration 55 | function generateSequence(start, step, increment) { 56 | // ex: 57 | // input: start: 0, step: 4, increment: 3 58 | // output: [ 0, 1, 4, 5, 8, 9, 12, 13 ] 59 | const results = [] 60 | 61 | _.each(_.range(start, step), (i) => { 62 | const value = (i + increment * i) 63 | 64 | results.push(value, value + 1) 65 | }) 66 | 67 | return results 68 | } 69 | 70 | function parseTable($) { 71 | const type = $('small').contents().text().trim() 72 | 73 | // create a sequence which is the same as the index of 'Eintala' of the td's 74 | // in the HTML table. 75 | const singular = generateSequence(0, 4, 3) 76 | const results = [] 77 | 78 | $('table tr td span').each((i, element) => { 79 | const predicate = ( 80 | element 81 | .parent 82 | .parent 83 | .children 84 | .filter(node => node.name === 'td')[0] 85 | .children[0] 86 | .data 87 | ) 88 | 89 | results.push({ 90 | predicate, 91 | value: element.children[0].data, 92 | category: i in singular ? 'Eintala' : 'Fleirtala', 93 | }) 94 | }) 95 | 96 | return { results, type } 97 | } 98 | 99 | app.get('/declension/:word', (req, res) => { 100 | const word = req.params.word 101 | baseUrl.query = { q: word } 102 | 103 | const params = { 104 | url: url.format(baseUrl), 105 | headers: { 106 | 'User-Agent': helper.browser(), 107 | }, 108 | } 109 | 110 | getDeclensions((body) => { 111 | return res.cache(86400).json(parseTable(body)) 112 | }, params) 113 | }) 114 | -------------------------------------------------------------------------------- /endpoints/declension/tests/integration_test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/extensions */ 2 | const request = require('request') 3 | const helpers = require('../../../lib/test_helpers.js') 4 | 5 | describe('declension', () => { 6 | it('should return an array of objects containing correct fields', (done) => { 7 | const fieldsToCheckFor = ['predicate', 'value', 'category'] 8 | const params = helpers.testRequestParams('/declension/laugavegur') 9 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 10 | request.get(params, resultHandler) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /endpoints/earthquake/documentation.md: -------------------------------------------------------------------------------- 1 | # Earthquakes in Iceland 2 | 3 | Source: [Icelandic Meteorological Office](http://vedur.is) 4 | 5 | - GET [/earthquake/is](https://apis.is/earthquake/is) 6 | 7 | Get earthquake monitoring data for the last 48 hours. 8 | 9 | --- 10 | -------------------------------------------------------------------------------- /endpoints/earthquake/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-prototype-builtins */ 2 | /* eslint-disable no-restricted-syntax */ 3 | /* eslint-disable no-useless-escape */ 4 | /* eslint-disable prefer-destructuring */ 5 | const request = require('request') 6 | const cheerio = require('cheerio') 7 | const helpers = require('apis-helpers') 8 | const app = require('../../server') 9 | 10 | const browser = helpers.browser 11 | 12 | /* 13 | This function only handles the request part and calls callback with 14 | the body 15 | */ 16 | function getEarthquakes(callback, params) { 17 | // eslint-disable-next-line eqeqeq 18 | const reqParams = !params ? { 19 | url: 'http://hraun.vedur.is/ja/skjalftar/skjlisti.html', 20 | headers: { 'User-Agent': browser() }, 21 | // needed for some reason.. defaulting to ISO-8859-1 22 | encoding: 'binary', 23 | } : params 24 | 25 | request(reqParams, (error, res, body) => { 26 | if (error || res.statusCode !== 200) { 27 | return callback(new Error('Could not retrieve the data from the data source')) 28 | } 29 | 30 | return callback(error, body) 31 | }) 32 | } 33 | 34 | /* 35 | * This function traverses the DOM, and looks for a JS variable that is included 36 | * in the source of vedur.is. 37 | * Note that it is synchronous. 38 | */ 39 | function parseJavaScriptVariable(body) { 40 | // Work with empty string if scraping fails. 41 | let res 42 | let jsonString = '' 43 | // Create a cheerio object from response body. 44 | const $ = cheerio.load(body) 45 | 46 | // Find the variable inside one of the