├── docs ├── .gitignore └── header.md ├── index.js ├── .gitignore ├── endpoints ├── currency │ ├── tests │ │ ├── m5.fixture │ │ ├── integration_test.js │ │ └── arion.fixture │ ├── index.js │ ├── documentation.md │ ├── lb.js │ ├── borgun.js │ ├── arion.js │ └── m5.js ├── isbolti │ ├── tests │ │ ├── test.fixture │ │ └── integration_test.js │ └── index.js ├── earthquake │ ├── tests │ │ ├── test.fixture │ │ └── integration_test.js │ ├── documentation.md │ └── index.js ├── sarschool │ ├── tests │ │ ├── test.fixture │ │ └── integration_test.js │ ├── documentation.md │ └── index.js ├── concerts │ ├── documentation.md │ ├── tests │ │ └── integration_test.js │ └── index.js ├── aur │ ├── documentation.md │ ├── index.js │ └── tests │ │ └── integration_test.js ├── lottery │ ├── documentation.md │ ├── tests │ │ └── integration_test.js │ └── index.js ├── particulates │ ├── documentation.md │ ├── tests │ │ └── integration_test.js │ └── index.js ├── petrol │ ├── documentation.md │ ├── tests │ │ └── integration_test.js │ └── index.js ├── hospital │ ├── documentation.md │ ├── tests │ │ └── integration_test.js │ └── index.js ├── cyclecounter │ ├── documentation.md │ ├── tests │ │ └── integration_test.js │ └── index.js ├── bus │ ├── index.js │ ├── documentation.md │ ├── search.js │ ├── graphql_schema.js │ ├── tests │ │ └── integration_test.js │ └── realtime.js ├── frontpage │ └── index.js ├── rides │ ├── documentation.md │ ├── tests │ │ └── integration_test.js │ └── index.js ├── static_endpoints │ ├── frontpage.js │ ├── help_out.js │ └── phone.js ├── sports │ ├── documentation.md │ ├── tests │ │ └── integration_test.js │ └── index.js ├── address │ ├── documentation.md │ ├── tests │ │ └── integration_test.js │ ├── graphql_schema.js │ └── index.js ├── car │ ├── documentation.md │ ├── tests │ │ └── integration_test.js │ ├── graphql_schema.js │ └── index.js ├── carparks │ ├── tests │ │ └── integration_test.js │ └── index.js ├── declension │ ├── tests │ │ └── integration_test.js │ └── index.js ├── cinema │ ├── tests │ │ └── integration_test.js │ └── index.js ├── tv │ ├── documentation.md │ ├── index.js │ ├── skjarinn.js │ ├── ruv.js │ ├── 365.js │ └── tests │ │ └── integration_test.js ├── flight │ ├── documentation.md │ ├── tests │ │ └── integration_test.js │ └── index.js ├── hljomaholl │ ├── tests │ │ └── integration_test.js │ └── index.js ├── ship │ ├── documentation.md │ ├── tests │ │ └── integration_test.js │ └── index.js ├── isnic │ ├── tests │ │ └── integration_test.js │ └── index.js ├── company │ ├── documentation.md │ ├── tests │ │ └── integration_test.js │ └── index.js ├── golf │ ├── tests │ │ └── integration_test.js │ └── index.js ├── calendar │ ├── graphql_schema.js │ ├── tests │ │ └── integration_test.js │ └── index.js ├── weather │ ├── documentation.md │ ├── tests │ │ └── integration_test.js │ └── index.js └── names │ ├── index.js │ └── tests │ └── integration_test.js ├── .github ├── PULL_REQUEST_TEMPLATE ├── ISSUE_TEMPLATE ├── CODEOWNERS └── stale.yml ├── lib ├── expressMetrics.js ├── redis.js ├── cors.js ├── expressMetrics.spec.js ├── cache.js └── test_helpers.js ├── make-docs.js ├── test.js ├── .circleci └── config.yml ├── LICENSE ├── graphql └── types │ └── GraphQLDate.js ├── package.json ├── server.js └── README.md /docs/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./server.js') 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log -------------------------------------------------------------------------------- /endpoints/currency/tests/m5.fixture: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apis-is/apis/HEAD/endpoints/currency/tests/m5.fixture -------------------------------------------------------------------------------- /endpoints/isbolti/tests/test.fixture: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apis-is/apis/HEAD/endpoints/isbolti/tests/test.fixture -------------------------------------------------------------------------------- /endpoints/earthquake/tests/test.fixture: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apis-is/apis/HEAD/endpoints/earthquake/tests/test.fixture -------------------------------------------------------------------------------- /endpoints/sarschool/tests/test.fixture: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apis-is/apis/HEAD/endpoints/sarschool/tests/test.fixture -------------------------------------------------------------------------------- /.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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /endpoints/lottery/documentation.md: -------------------------------------------------------------------------------- 1 | # Icelandic lottery 2 | 3 | Source: [The Icelandic Government Lottery](https://games.lotto.is/) 4 | 5 | - GET [/lottery](https://apis.is/lottery) 6 | 7 | Get the most recent numbers for the Icelandic lottery. 8 | 9 | --- 10 | -------------------------------------------------------------------------------- /endpoints/particulates/documentation.md: -------------------------------------------------------------------------------- 1 | # Particulates in Reykjavik 2 | 3 | Source: [Reykjavik City](http://reykjavik.is/) 4 | 5 | - GET [/particulates](https://apis.is/particulates) 6 | 7 | Get current status of particulates in Reykjavik City 8 | 9 | --- 10 | -------------------------------------------------------------------------------- /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/petrol/documentation.md: -------------------------------------------------------------------------------- 1 | # Petrol stations in Iceland 2 | 3 | Source: [github.com/gasvaktin](https://github.com/gasvaktin/gasvaktin) 4 | 5 | - GET [/petrol](https://apis.is/petrol) 6 | 7 | Lookup locations and gas prices for petrol stations in Iceland. 8 | 9 | --- 10 | -------------------------------------------------------------------------------- /lib/expressMetrics.js: -------------------------------------------------------------------------------- 1 | const redis = require('./redis') 2 | 3 | module.exports = function () { 4 | return function (req, res, next) { 5 | if (req.path !== '/metrics') { 6 | redis.hincrby('metrics', `${req.path}.${req.method}.callCount`, 1) 7 | } 8 | next() 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /endpoints/hospital/documentation.md: -------------------------------------------------------------------------------- 1 | # Status of Icelandic hospitals 2 | 3 | Source: [The National University Hospital of Iceland](http://landspitali.is) 4 | 5 | - GET [/hospital](https://apis.is/hospital) 6 | 7 | Get the current status of the National University Hospital of Iceland. 8 | 9 | --- 10 | -------------------------------------------------------------------------------- /endpoints/sarschool/documentation.md: -------------------------------------------------------------------------------- 1 | # Landsbjörg SAR School course schedule 2 | 3 | Source: [SAR School](http://skoli.landsbjorg.is/) 4 | 5 | - GET [/sarschool](https://apis.is/sarschool) 6 | 7 | Get a list of all courses scheduled by the Search and Rescue school for training of SAR Squad members. 8 | 9 | --- 10 | -------------------------------------------------------------------------------- /lib/redis.js: -------------------------------------------------------------------------------- 1 | const Redis = require('redis') 2 | const debug = require('debug')('redis') 3 | 4 | const redis = Redis.createClient(process.env.REDIS_URL) 5 | 6 | redis.on('connect', () => { 7 | debug('connected') 8 | }) 9 | 10 | redis.on('error', (error) => { 11 | debug(error) 12 | }) 13 | 14 | module.exports = redis 15 | -------------------------------------------------------------------------------- /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/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/frontpage/index.js: -------------------------------------------------------------------------------- 1 | const app = require('../../server') 2 | 3 | app.get('/', (req, res) => { 4 | res.redirect(301, 'http://docs.apis.is') 5 | }) 6 | 7 | app.post('/', (req, res) => { 8 | res.json({ 9 | info: 'Velkominn á apis.is! Kíktu á docs.apis.is í vafranum þínum fyrir frekari upplýsingar!', 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /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/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/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/rides/documentation.md: -------------------------------------------------------------------------------- 1 | # Carpooling in Iceland 2 | 3 | Source: [samferda.net](http://samferda.net) 4 | 5 | - GET [/rides/samferda-drivers/](https://apis.is/rides/samferda-drivers/) 6 | 7 | Get a list of drivers requesting passengers, sorted by departure date. 8 | 9 | - GET [/rides/samferda-passengers/](https://apis.is/rides/samferda-passengers/) 10 | 11 | Get a list of passengers requesting rides, sorted by preferred departure date 12 | 13 | --- 14 | -------------------------------------------------------------------------------- /endpoints/static_endpoints/frontpage.js: -------------------------------------------------------------------------------- 1 | const app = require('../../server') 2 | 3 | app.get('/', (req, res) => { 4 | res.redirect(301, 'http://docs.apis.is') 5 | }) 6 | 7 | app.post('/', (req, res) => { 8 | res.json({ 9 | info: { 10 | english: 'Hey there! Check out docs.apis.is in your browser for mor info', 11 | icelandic: 'Velkominn á apis.is! Kíktu á docs.apis.is í vafranum þínum fyrir frekari upplýsingar!', 12 | }, 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /endpoints/sports/documentation.md: -------------------------------------------------------------------------------- 1 | # Sport events in Iceland 2 | 3 | Source: [KSÍ](http://ksi.is) and [HSÍ](http://hsi.is) 4 | 5 | - GET [/sports/:sport](https://apis.is/sports/:sport) 6 | 7 | Get events for Icelandic football and/or handball 8 | 9 | | Parameters | Description | Example | 10 | |-------------------|---------------------------|---------| 11 | | :sport (required) | Which sport events to get | [football](https://apis.is/sports/football), [handball](https://apis.is/sports/handball) | 12 | 13 | --- 14 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/static_endpoints/help_out.js: -------------------------------------------------------------------------------- 1 | 2 | function helpOut(req, res, next) { 3 | res.header('Access-Control-Allow-Origin', '*') 4 | res.header('Access-Control-Allow-Headers', 'X-Requested-With') 5 | res.status(200).json({ message: 'Send us mail: apis@apis.is ,thanks for your interest!' }) 6 | next() 7 | } 8 | 9 | exports.setup = function (server) { 10 | server.post({ path: '/help_out', version: '1.0.0' }, helpOut) 11 | server.get({ path: '/help_out', version: '1.0.0' }, helpOut) 12 | server.post({ path: '/help', version: '1.0.0' }, helpOut) 13 | server.get({ path: '/help', version: '1.0.0' }, helpOut) 14 | } 15 | -------------------------------------------------------------------------------- /make-docs.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const globby = require('globby') 3 | const marked = require('marked'); 4 | 5 | (async () => { 6 | const paths = await globby(['./docs/*.md', './endpoints/**/*.md', '!node_modules/**']) 7 | 8 | let content = '' 9 | paths.forEach((path) => { 10 | content += fs.readFileSync(path, 'utf8') 11 | }) 12 | const html = marked(content, { escapeMarkdown: false }) 13 | 14 | if (!fs.existsSync('./docs/dist/')) { 15 | fs.mkdirSync('./docs/dist/') 16 | } 17 | fs.writeFileSync('./docs/dist/index.html', html, 'utf8') 18 | })().catch((error) => { 19 | console.error(error) 20 | }) 21 | -------------------------------------------------------------------------------- /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/tv/documentation.md: -------------------------------------------------------------------------------- 1 | # Icelandic television schedules 2 | 3 | Source: [RÚV](http://ruv.is), [Stöð 2](http://stod2.is) and [SkjárEinn](http://skjarinn.is/einn) 4 | 5 | - GET [/tv/:channel](https://apis.is/tv/:channel) 6 | 7 | Get schedules for Icelandic tv stations. 8 | 9 | | Parameters | Description | Example | 10 | |------------|---------------------------------|------------------------------------------------------------------| 11 | | :channel | Which channel's schedule to get | [ruv](https://apis.is/tv/ruv), [stod2](https://apis.is/tv/stod2) | 12 | 13 | --- 14 | -------------------------------------------------------------------------------- /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/static_endpoints/phone.js: -------------------------------------------------------------------------------- 1 | function phone(req, res, next) { 2 | res.header('Access-Control-Allow-Origin', '*') 3 | res.header('Access-Control-Allow-Headers', 'X-Requested-With') 4 | res.status(410).json({ 5 | error: 'This api endpoint has been closed and it will not be available in the foreseeable future.', 6 | }) 7 | next() 8 | } 9 | 10 | exports.setup = function (server) { 11 | server.post({ path: '/phone', version: '1.0.0' }, phone) 12 | server.get({ path: '/phone', version: '1.0.0' }, phone) 13 | server.post({ path: '/phone', version: '2.0.0' }, phone) 14 | server.get({ path: '/phone', version: '2.0.0' }, phone) 15 | } 16 | -------------------------------------------------------------------------------- /endpoints/flight/documentation.md: -------------------------------------------------------------------------------- 1 | # International flights in Iceland 2 | 3 | Source: [Keflavik Airport](https://kefairport.is/) 4 | 5 | - GET [/flight](https://apis.is/flight) 6 | 7 | Get a list of all international flights departing and arriving at Keflavik Airport today. 8 | 9 | | Parameters | Description | Example | 10 | |---------------------|------------------------------------|----------------------------| 11 | | Language (required) | The language to get the results in | 'en' or 'is' | 12 | | Type (required) | The type of flights to fetch | 'departures' or 'arrivals' | 13 | 14 | --- 15 | -------------------------------------------------------------------------------- /lib/cors.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | return (req, res, next) => { 3 | const allowedHeaders = [ 4 | 'X-Requested-With', 5 | 'Accept', 6 | 'Origin', 7 | 'Referer', 8 | 'User-Agent', 9 | 'Content-Type', 10 | 'Authorization', 11 | 'X-Mindflash-SessionID', 12 | ] 13 | 14 | res.header('Access-Control-Allow-Origin', '*') 15 | res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE') 16 | res.header('Access-Control-Allow-Headers', allowedHeaders.join(', ')) 17 | 18 | if (req.method !== 'OPTIONS') { 19 | return next() 20 | } 21 | 22 | return res.sendStatus(200) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /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/flight/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('flight', () => { 6 | it('should return an array of objects containing correct fields', (done) => { 7 | const fieldsToCheckFor = ['date', 'flightNumber', 'airline', 'to', 'plannedArrival', 'realArrival', 'status'] 8 | const params = helpers.testRequestParams('/flight', { 9 | language: 'en', 10 | type: 'departures', 11 | }) 12 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 13 | request.get(params, resultHandler) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /endpoints/particulates/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('particulates', () => { 6 | // The only thing that changes is the form attribute, so why not just re-use the object 7 | const fieldsToCheckFor = ['PM10nuna', 'PM10medaltal', 'Counter', 'Dags', 'nanariuppl'] 8 | 9 | it.skip('should return an array of objects containing correct fields', (done) => { 10 | const params = helpers.testRequestParams('/particulates') 11 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 12 | request(params, resultHandler) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /endpoints/petrol/tests/integration_test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/extensions */ 2 | const request = require('request') 3 | const helpers = require('../../../lib/test_helpers.js') 4 | 5 | const fieldsToCheckFor = [ 6 | 'bensin95', 7 | 'bensin95_discount', 8 | 'company', 9 | 'diesel', 10 | 'diesel_discount', 11 | 'geo', 12 | 'key', 13 | 'name', 14 | ] 15 | 16 | describe('get-petrol-stations', function () { 17 | this.timeout(20000) 18 | it('should return an array of objects containing correct fields', (done) => { 19 | const params = helpers.testRequestParams('/petrol') 20 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 21 | request.get(params, resultHandler) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /endpoints/hljomaholl/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('hljomaholl', () => { 6 | // The only thing that changes is the form attribute, so why not just re-use the object 7 | const fieldsToCheckFor = [ 8 | 'date', 'time', 'image', 'title', 'description', 'location', 'buyTicketURL', 'moreInfoURL', 9 | ] 10 | 11 | it('should return an array of items with correct fields', (done) => { 12 | const params = helpers.testRequestParams('/hljomaholl') 13 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor, null, true) 14 | request(params, resultHandler) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /endpoints/ship/documentation.md: -------------------------------------------------------------------------------- 1 | # Icelandic Ship Registry 2 | 3 | Source: [The Icelandic Transport Authority](https://www.samgongustofa.is/siglingar/skrar-og-utgafa/skipaskra/uppfletting/) 4 | 5 | - GET [/ship](https://apis.is/ship) 6 | 7 | Search the Icelandic ship registry 8 | 9 | | Parameters | Description | Example | 10 | |-------------------|---------------------------------------------------------------|---------------| 11 | | Search (required) | Ship name, regional code or the registy's registration number | [engey](https://apis.is/ship?search=engey), [RE-100](https://apis.is/ship?search=RE-100) | 12 | 13 | --- 14 | -------------------------------------------------------------------------------- /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/isbolti/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('isboltinn', () => { 8 | it('Should return an array of objects with fixed amount of fields.', (done) => { 9 | before(() => { 10 | nock('http://fotbolti.net') 11 | .get('/isboltinn.php') 12 | .reply(200, fs.readFileSync(`${__dirname}/test.fixture`)) 13 | }) 14 | const fields = [ 15 | 'place', 'team', 'gamesPlayed', 16 | 'gamesWon', 'gamesDraw', 'gamesLost', 17 | 'goals', 'goalDifference', 'points', 18 | ] 19 | 20 | const params = helpers.testRequestParams('/isbolti', {}) 21 | const resultHandler = helpers.testRequestHandlerForFields(done, fields) 22 | request.get(params, resultHandler) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /endpoints/isnic/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('isnic', () => { 6 | it('should return an array of objects containing correct fields', (done) => { 7 | const fieldsToCheckFor = [ 8 | 'domain', 9 | 'registrantname', 10 | 'address', 11 | 'city', 12 | 'postalCode', 13 | 'country', 14 | 'phone', 15 | 'email', 16 | 'registered', 17 | 'expires', 18 | 'lastChange', 19 | ] 20 | const params = { 21 | url: 'http://localhost:3101/isnic', 22 | method: 'GET', 23 | qs: { domain: 'apis.is' }, 24 | headers: ['Content-Type: application/json'], 25 | } 26 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 27 | request.get(params, resultHandler) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/header.md: -------------------------------------------------------------------------------- 1 | 2 | 47 | 48 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const nock = require('nock') 3 | 4 | const mockDataFilename = './mock-data.json' 5 | 6 | before(() => { 7 | if (process.env.RECORD_MOCK_DATA) { 8 | nock.recorder.rec({ 9 | output_objects: true, 10 | dont_print: true, 11 | }) 12 | } else { 13 | nock.load(mockDataFilename) 14 | } 15 | }) 16 | 17 | after(() => { 18 | if (process.env.RECORD_MOCK_DATA) { 19 | const nockCallObjects = nock.recorder.play() 20 | const noLocalhost = nockCallObjects.filter((o) => { 21 | return ![ 22 | 'http://localhost:3101', 23 | 'http://www.m5.is:80', 24 | 'http://hraun.vedur.is:80', 25 | 'http://www.vedur.is:80', 26 | 'http://www.landspitali.is:80', 27 | 'http://fotbolti.net:80', 28 | 'http://skoli.landsbjorg.is:80', 29 | 'http://www.worldfengur.com:80', 30 | ].includes(o.scope) 31 | }) 32 | fs.writeFileSync(mockDataFilename, JSON.stringify(noLocalhost, null, 2)) 33 | } 34 | }) 35 | -------------------------------------------------------------------------------- /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/sarschool/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('sarschool', () => { 8 | before(() => { 9 | nock('http://skoli.landsbjorg.is') 10 | .get('/Open/Seminars.aspx?') 11 | .reply(200, fs.readFileSync(`${__dirname}/test.fixture`)) 12 | }) 13 | 14 | // The only thing that changes is the form attribute, so why not just re-use the object 15 | const fieldsToCheckFor = [ 16 | 'id', 'name', 'time_start', 'time_end', 'sar_members_only', 'host', 17 | 'location', 'price_regular', 'price_members', 'link', 18 | ] 19 | 20 | it('should return an array of objects containing correct fields', (done) => { 21 | const params = helpers.testRequestParams('/sarschool') 22 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 23 | request(params, resultHandler) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /lib/expressMetrics.spec.js: -------------------------------------------------------------------------------- 1 | const sinon = require('sinon') 2 | const expect = require('expect') 3 | const redis = require('./redis') 4 | const metricsMiddleware = require('./expressMetrics') 5 | 6 | describe('Metrics Middleware', () => { 7 | let redisSpy = null 8 | 9 | beforeEach(() => { 10 | redisSpy = sinon.spy(redis, 'hincrby') 11 | }) 12 | 13 | afterEach(() => { 14 | redis.hincrby.restore() 15 | }) 16 | 17 | it('should record how often a endpoint is called', () => { 18 | // Call the middleware directly threee times 19 | metricsMiddleware()({ path: '/car?carPlate=foo' }, null, () => {}) 20 | metricsMiddleware()({ path: '/car?carPlate=bar' }, null, () => {}) 21 | metricsMiddleware()({ path: '/car?carPlate=baz' }, null, () => {}) 22 | 23 | expect(redisSpy.callCount).toBe(3) 24 | }) 25 | 26 | it('should not record how often a endpoint if it\'s /metrics', () => { 27 | metricsMiddleware()({ path: '/metrics' }, null, () => {}) 28 | 29 | expect(redisSpy.callCount).toBe(0) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /endpoints/particulates/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('/particulates', (req, res) => { 9 | request.get({ 10 | url: 'http://www.loft.rvk.is/xml/Xsvifryk.xml', 11 | }, (err, response, xml) => { 12 | if (err || response.statusCode !== 200) { 13 | return res.status(500).json({ 14 | error: 'www.loft.rvk.is refuses to respond or give back data', 15 | }) 16 | } 17 | 18 | const particulates = [] 19 | parseString(xml, { explicitRoot: false }, (parseError, result) => { 20 | particulates.push({ 21 | PM10nuna: result.PM10nuna[0], 22 | PM10medaltal: result.PM10medaltal[0], 23 | Counter: result.Counter[0], 24 | Dags: result.Dags[0], 25 | nanariuppl: result.nanariuppl[0], 26 | }) 27 | 28 | return res.json({ results: particulates }) 29 | }) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /endpoints/rides/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('rides root', () => { 6 | it('should return info and endpoints', (done) => { 7 | const fieldsToCheckFor = ['info', 'endpoints'] 8 | const params = helpers.testRequestParams('/rides/') 9 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 10 | request.get(params, resultHandler) 11 | }) 12 | }); 13 | 14 | ['samferda-drivers', 'samferda-passengers'].forEach((api) => { 15 | const apiName = `rides - ${api}` 16 | describe(apiName, () => { 17 | const fieldsToCheckFor = ['link', 'from', 'to', 'date', 'time'] 18 | it('should return an array of objects containing correct fields', (done) => { 19 | const params = helpers.testRequestParams(`/rides/${api}`) 20 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 21 | request.get(params, resultHandler) 22 | }) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /endpoints/hospital/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('hospital', () => { 8 | before(() => { 9 | nock('http://www.landspitali.is/') 10 | .get('/') 11 | .reply(200, fs.readFileSync(`${__dirname}/test.fixture`)) 12 | }) 13 | // The only thing that changes is the form attribute, so why not just re-use the object 14 | const fieldsToCheckFor = [ 15 | 'birthNumbers', 'surgeries', 'dischargedNumbers', 'hospitalizedNumbers', 'atwork', 'patients-child', 16 | 'patients-er', 'patients-walk', 'patients-icu', 'donors', 'patients-skilun', 'patients-heart2', 17 | ] 18 | 19 | it('should return an array of objects containing correct fields', (done) => { 20 | const params = helpers.testRequestParams('/hospital') 21 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 22 | request(params, resultHandler) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /endpoints/golf/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('golf', () => { 6 | // The only thing that changes is the form attribute, so why not just re-use the object 7 | const fieldsToCheckFor = ['abbreviation', 'club', 'location'] 8 | 9 | it('should return an array of objects containing correct fields', (done) => { 10 | const params = helpers.testRequestParams('/golf/clubs') 11 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 12 | request(params, resultHandler) 13 | }) 14 | }) 15 | 16 | describe('golf', () => { 17 | // The only thing that changes is the form attribute, so why not just re-use the object 18 | const fieldsToCheckFor = ['time', 'name', 'club', 'handicap'] 19 | 20 | it('should return an array of objects containing correct fields', (done) => { 21 | const params = helpers.testRequestParams('/golf/teetimes', { club: 61 }) 22 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 23 | request(params, resultHandler) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /endpoints/hospital/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/first */ 2 | const request = require('request') 3 | const cheerio = require('cheerio') 4 | const _ = require('lodash') 5 | const app = require('../../server') 6 | 7 | app.get('/hospital', (req, res) => { 8 | request.get( 9 | { url: 'http://www.landspitali.is/' }, 10 | (err, response, body) => { 11 | if (err || response.statusCode !== 200) { 12 | return res.status(500).json({ 13 | error: 'www.landspitali.is refuses to respond or give back data', 14 | }) 15 | } 16 | 17 | let $ 18 | try { 19 | $ = cheerio.load(body) 20 | } catch (e) { 21 | return res.status(500).json({ 22 | error: 'An error occured when parsing the data from landspitali.is', 23 | }) 24 | } 25 | 26 | const data = {} 27 | _.each( 28 | $('.activityNumbers.activityNumbersNew').children('div'), 29 | (elem) => { 30 | data[elem.attribs.class] = parseInt($(elem).children().eq(1).html(), 10) 31 | } 32 | ) 33 | // Cache for a hour. 34 | return res.cache(3600).json({ results: [data] }) 35 | } 36 | ) 37 | }) 38 | -------------------------------------------------------------------------------- /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/tv/index.js: -------------------------------------------------------------------------------- 1 | const app = require('../../server') 2 | 3 | /* Root TV */ 4 | app.get('/tv', (req, res) => { 5 | return res.json({ 6 | results: [ 7 | { 8 | info: 'This is an api for Icelandic tv channel schedule\'s', 9 | endpoints: { 10 | ruv: '/tv/ruv/', 11 | ruvithrottir: '/tv/ruvithrottir/', 12 | stod2: '/tv/stod2/', 13 | stod2sport: '/tv/stod2sport', 14 | stod2sport2: '/tv/stod2sport2', 15 | stod2gull: '/tv/stod2gull', 16 | stod2bio: '/tv/stod2bio', 17 | stod3: '/tv/stod3', 18 | skjar1: '/tv/skjar1', 19 | }, 20 | channels: [ 21 | { name: 'Rúv', endpoint: '/tv/ruv/' }, 22 | { name: 'Rúv íþróttir', endpoint: '/tv/ruvithrottir/' }, 23 | { name: 'Stöð 2', endpoint: '/tv/stod2/' }, 24 | { name: 'Stöð 2 Sport', endpoint: '/tv/stod2sport' }, 25 | { name: 'Stöð 2 Sport 2', endpoint: '/tv/stod2sport2' }, 26 | { name: 'Stöð 2 Gull', endpoint: '/tv/stod2gull' }, 27 | { name: 'Stöð 2 Bíó', endpoint: '/tv/stod2bio' }, 28 | { name: 'Stöð 2', endpoint: '/tv/stod3' }, 29 | { name: 'Skjár 1', endpoint: '/tv/skjar1' }, 30 | ], 31 | }, 32 | ], 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /graphql/types/GraphQLDate.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | /* eslint-disable no-restricted-globals */ 3 | const { GraphQLScalarType } = require('graphql') 4 | const { GraphQLError } = require('graphql/error') 5 | const { Kind } = require('graphql/language') 6 | 7 | const coerceDate = (value) => { 8 | if (!(value instanceof Date)) { 9 | throw new Error('[Field error]: value is not an instance of Date') 10 | } 11 | if (isNaN(value.getTime())) { 12 | throw new Error('[Field error]: value is an invalid Date') 13 | } 14 | return value.toJSON() 15 | } 16 | 17 | exports.GraphQLDate = new GraphQLScalarType({ 18 | name: 'DateTime', 19 | serialize: coerceDate, 20 | parseValue: coerceDate, 21 | parseLiteral(ast) { 22 | if (ast.kind !== Kind.STRING) { 23 | throw new GraphQLError(`[Query error]: Can only parse strings to dates but got a: ${ast.kind}`, [ast]) 24 | } 25 | const result = new Date(ast.value) 26 | if (isNaN(result.getTime())) { 27 | throw new GraphQLError('[Query error]: Invalid date', [ast]) 28 | } 29 | if (ast.value !== result.toJSON()) { 30 | throw new GraphQLError('[Query error]: Invalid date format, only accepts: YYYY-MM-DDTHH:MM:SS.SSSZ', [ast]) 31 | } 32 | return result 33 | }, 34 | }) 35 | -------------------------------------------------------------------------------- /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/ship/tests/integration_test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/extensions */ 2 | const assert = require('assert') 3 | const request = require('request') 4 | const helpers = require('../../../lib/test_helpers.js') 5 | 6 | describe('ship', () => { 7 | it('should return an array of objects containing correct fields', (done) => { 8 | const fieldsToCheckFor = [ 9 | 'name', 10 | 'type', 11 | 'registrationNumber', 12 | 'regionalCode', 13 | 'homePort', 14 | 'registrationStatus', 15 | 'grossRegisterTonnage', 16 | 'grossTonnage', 17 | 'length', 18 | 'buildYear', 19 | 'buildYard', 20 | 'owners', 21 | ] 22 | const params = helpers.testRequestParams('/ship', { search: 'helga maría' }) 23 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 24 | request.get(params, resultHandler) 25 | }) 26 | it('should return a 404 when a ship is not found', (done) => { 27 | const params = helpers.testRequestParams('/ship', { search: 'koddsson' }) 28 | request.get(params, (error, response, body) => { 29 | if (error) { 30 | return done(error) 31 | } 32 | const json = JSON.parse(body) 33 | assert.equal(json.error, 'No ship found with the query koddsson') 34 | done() 35 | }) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /lib/cache.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-destructuring */ 2 | // TODO: Find a way to enable no-param-reassign 3 | /* eslint-disable no-param-reassign */ 4 | const debug = require('debug')('cache') 5 | const redis = require('./redis') 6 | 7 | module.exports = function () { 8 | return function (req, res, next) { 9 | if (process.env.NODE_ENV !== 'production' || !redis.connected) { 10 | res.cache = function () { 11 | return this 12 | } 13 | return next() 14 | } 15 | 16 | const end = res.end 17 | const write = res.write 18 | const chunkCache = [] 19 | const key = req.originalUrl 20 | 21 | redis.get(key, (error, reply) => { 22 | if (error) { 23 | debug('Error in caching layer:', error) 24 | return next() 25 | } 26 | 27 | if (reply) { 28 | debug('cache HIT for %s', key) 29 | res.type('json') 30 | res.send(reply) 31 | } else { 32 | debug('cache MISS for %s', key) 33 | res.cache = function (timeout = 21600) { 34 | debug('will cache response of %s for %d seconds', key, timeout) 35 | 36 | res.write = function (chunk, encoding) { 37 | chunkCache.push(chunk) 38 | write.call(res, chunk, encoding) 39 | } 40 | 41 | res.end = function (chunk, encoding) { 42 | if (chunk) { 43 | this.write(chunk, encoding) 44 | } 45 | 46 | redis.setex(key, timeout, chunkCache.join('')) 47 | end.call(res) 48 | } 49 | return this 50 | } 51 | return next() 52 | } 53 | }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /endpoints/isbolti/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/first */ 2 | const request = require('request') 3 | const cheerio = require('cheerio') 4 | const iconv = require('iconv-lite') 5 | const app = require('../../server') 6 | 7 | // returns the stats table of Icelandic football teams. 8 | // data retrieved from fotbolti.net. 9 | app.get('/isbolti', (req, res) => { 10 | const stats = { results: [] } 11 | 12 | request({ 13 | url: 'http://fotbolti.net/isboltinn.php', 14 | encoding: null, 15 | }, (error, response, html) => { 16 | let $ 17 | if (error || response.statusCode !== 200) { 18 | return res.status(500).json({ error: 'Could not retreive data from fotbolti.net' }) 19 | } 20 | const body = iconv.decode(html, 'iso-8859-1') 21 | 22 | try { 23 | $ = cheerio.load(body) 24 | } catch (err) { 25 | return res.status(500).json({ error: 'Could not parse body' }) 26 | } 27 | 28 | $('table tr').each(function (key) { 29 | if (key !== 0) { 30 | const temp = { 31 | place: $(this).children('td').slice(0).html(), 32 | team: $(this).children('td').slice(1).html(), 33 | gamesPlayed: $(this).children('td').slice(2).html(), 34 | gamesWon: $(this).children('td').slice(3).html(), 35 | gamesDraw: $(this).children('td').slice(4).html(), 36 | gamesLost: $(this).children('td').slice(5).html(), 37 | goals: $(this).children('td').slice(6).html(), 38 | goalDifference: $(this).children('td').slice(7).html(), 39 | points: $(this).children('td').slice(8).html(), 40 | } 41 | stats.results.push(temp) 42 | } 43 | }) 44 | return res.send(stats) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /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/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/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/earthquake/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('earthquake/is', () => { 8 | before(() => { 9 | nock('http://hraun.vedur.is') 10 | .get('/ja/skjalftar/skjlisti.html') 11 | .reply(200, fs.readFileSync(`${__dirname}/test.fixture`)) 12 | }) 13 | // Which fields we expect and of which type they should be 14 | const fieldsToCheckFor = { 15 | timestamp: Date, 16 | latitude: Number, 17 | longitude: Number, 18 | depth: Number, 19 | size: Number, 20 | quality: Number, 21 | humanReadableLocation: String, 22 | } 23 | 24 | it('should return an array of objects containing correct fields', (done) => { 25 | const params = helpers.testRequestParams('/earthquake/is') 26 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 27 | request(params, resultHandler) 28 | }) 29 | }) 30 | 31 | describe('earthquake/is/sec', () => { 32 | before(() => { 33 | nock('http://hraun.vedur.is') 34 | .get('/ja/skjalftar/skjlisti.html') 35 | .reply(200, fs.readFileSync(`${__dirname}/test.fixture`)) 36 | }) 37 | 38 | // Which fields we expect and of which type they should be 39 | const fieldsToCheckFor = { 40 | timestamp: Date, 41 | latitude: Number, 42 | longitude: Number, 43 | depth: Number, 44 | size: Number, 45 | quality: Number, 46 | humanReadableLocation: String, 47 | } 48 | 49 | it('should return an array of objects containing correct fields', (done) => { 50 | const params = helpers.testRequestParams('/earthquake/is') 51 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 52 | request(params, resultHandler) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /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/sports/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.skip('handball', () => { 6 | it('should return an array of objects containing correct fields', (done) => { 7 | const fieldsToCheckFor = ['Date', 'Time', 'Tournament', 'Venue', 'Teams'] 8 | const params = helpers.testRequestParams('/sports/handball', { language: 'en' }) 9 | 10 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 11 | request.get(params, resultHandler) 12 | }) 13 | }) 14 | 15 | describe.skip('football', () => { 16 | it('should return an array of objects containing correct fields', (done) => { 17 | const fieldsToCheckFor = ['counter', 'date', 'time', 'tournament', 'location', 'homeTeam', 'awayTeam'] 18 | const params = helpers.testRequestParams('/sports/football', { language: 'en' }) 19 | 20 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 21 | request.get(params, resultHandler) 22 | }) 23 | }) 24 | 25 | function testFootballLeague(leagueParams) { 26 | describe(`football/${leagueParams}`, () => { 27 | it('should return an array of objects containing correct fields', (done) => { 28 | const fieldsToCheckFor = ['counter', 'date', 'time', 'teams', 'location', 'scores'] 29 | const params = helpers.testRequestParams(`/sports/football/${leagueParams}`, { language: 'en' }) 30 | 31 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 32 | request.get(params, resultHandler) 33 | }) 34 | }) 35 | } 36 | 37 | testFootballLeague('male-leagues/borgun') 38 | testFootballLeague('male-leagues/pepsi') 39 | testFootballLeague('male-leagues/1st') 40 | testFootballLeague('male-leagues/2nd') 41 | testFootballLeague('male-leagues/3rd') 42 | 43 | testFootballLeague('female-leagues/borgun') 44 | testFootballLeague('female-leagues/pepsi') 45 | -------------------------------------------------------------------------------- /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/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/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/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/hljomaholl/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-plusplus */ 2 | const request = require('request') 3 | const cheerio = require('cheerio') 4 | const app = require('../../server') 5 | 6 | app.get('/hljomaholl', (req, res) => { 7 | const url = 'http://www.hljomaholl.is/vidburdir' 8 | 9 | request.get(url, (error, response, body) => { 10 | if (error || response.statusCode !== 200) { 11 | return res.status(500).json({ 12 | error: 'www.hljomaholl.is refuses to respond or give back data', 13 | }) 14 | } 15 | 16 | let $ 17 | 18 | try { 19 | $ = cheerio.load(body) 20 | } catch (err) { 21 | return res.status(500).json({ 22 | error: 'Could not parse body', 23 | }) 24 | } 25 | 26 | const obj = { results: [] } 27 | const fields = [ 28 | 'date', 29 | 'time', 30 | 'image', 31 | 'title', 32 | 'description', 33 | 'location', 34 | 'buyTicketURL', 35 | 'moreInfoURL', 36 | ] 37 | 38 | try { 39 | $('.main-content-body ul').find('li').each(() => { 40 | const event = {} 41 | let counter = 0 42 | $('.time', this).find('time').each((key) => { 43 | if (counter === 2) { 44 | return false 45 | } 46 | 47 | const val = $(this).text() 48 | event[fields[key]] = val 49 | counter++ 50 | }) 51 | event[fields[2]] = $('img', this).attr('src') 52 | event[fields[3]] = $('.time h1', this).text().trim() 53 | event[fields[4]] = $('p', this).text().trim() 54 | event[fields[5]] = $('.time h2', this).text().trim() 55 | event[fields[6]] = $('.btn-wrapper', this) 56 | .find('.btn-green').attr('href') || false 57 | 58 | const href = $('.btn-wrapper', this) 59 | .find('.btn-blue') 60 | .attr('href') 61 | event[fields[7]] = `http://www.hljomaholl.is${href}` 62 | 63 | obj.results.push(event) 64 | }) 65 | } catch (err) { 66 | return res.status(500).json({ 67 | error: 'Could not parse event data', 68 | }) 69 | } 70 | 71 | return res.json(obj) 72 | }) 73 | }) 74 | -------------------------------------------------------------------------------- /endpoints/isnic/index.js: -------------------------------------------------------------------------------- 1 | const request = require('request') 2 | const $ = require('cheerio') 3 | const h = require('apis-helpers') 4 | const app = require('../../server') 5 | 6 | app.get('/isnic', (req, res) => { 7 | const domainName = req.query.domain || '' 8 | 9 | if (!domainName) { 10 | return res.status(431).json({ error: 'Please provide a valid domainName to lookup' }) 11 | } 12 | 13 | const url = `https://www.isnic.is/en/whois/search?yt1972183=%C3%81fram&type=all&query=${domainName}` 14 | 15 | request.get({ 16 | headers: { 'User-Agent': h.browser() }, 17 | url, 18 | }, (error, response, body) => { 19 | if (error || response.statusCode !== 200) { 20 | return res.status(500).json({ 21 | error: 'www.isnic.is refuses to respond or give back data', 22 | }) 23 | } 24 | 25 | const data = $(body) 26 | 27 | const obj = { 28 | results: [], 29 | } 30 | 31 | const fields = [] 32 | 33 | data.find('.miniWhois .row').each(function () { 34 | const label = $(this).find('label').text().trim() 35 | const val = $(this).text().replace(label, '').trim() 36 | 37 | fields.push({ val, label }) 38 | }) 39 | 40 | if (fields.length > 0) { 41 | obj.results.push({ 42 | domain: (fields.find(x => x.label === 'Domain:') || { val: '' }).val, 43 | registrantname: (fields.find(x => x.label === 'Registrant name:') || { val: '' }).val, 44 | address: (fields.find(x => x.label === 'Address:') || { val: '' }).val, 45 | city: (fields.find(x => x.label === 'City/Municipality:') || { val: '' }).val, 46 | postalCode: (fields.find(x => x.label === 'Postal code:') || { val: '' }).val, 47 | country: (fields.find(x => x.label === 'Country:') || { val: '' }).val, 48 | phone: (fields.find(x => x.label === 'Phone:') || { val: '' }).val, 49 | email: (fields.find(x => x.label === 'E-mail:') || { val: '' }).val, 50 | registered: (fields.find(x => x.label === 'Registered:') || { val: '' }).val, 51 | expires: (fields.find(x => x.label === 'Expires:') || { val: '' }).val, 52 | lastChange: (fields.find(x => x.label === 'Last change:') || { val: '' }).val, 53 | }) 54 | } 55 | 56 | return res.cache(86400).json(obj) 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /endpoints/lottery/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.skip('lottery', () => { 6 | describe('lotto', () => { 7 | // The only thing that changes is the form attribute, so why not just re-use the object 8 | const fieldsToCheckFor = ['date', 'lotto', 'joker', 'prize', 'link'] 9 | 10 | it('should return an array of objects containing correct fields', (done) => { 11 | const params = helpers.testRequestParams('/lottery') 12 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 13 | request(params, resultHandler) 14 | }) 15 | }) 16 | 17 | describe('lotto', () => { 18 | // The only thing that changes is the form attribute, so why not just re-use the object 19 | const fieldsToCheckFor = ['date', 'lotto', 'joker', 'prize', 'link'] 20 | 21 | it('should return an array of objects containing correct fields', (done) => { 22 | const params = helpers.testRequestParams('/lottery/lotto') 23 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 24 | request(params, resultHandler) 25 | }) 26 | }) 27 | 28 | describe('vikingalotto', () => { 29 | // The only thing that changes is the form attribute, so why not just re-use the object 30 | const fieldsToCheckFor = ['date', 'lotto', 'joker', 'prize', 'link'] 31 | 32 | it('should return an array of objects containing correct fields', (done) => { 33 | const params = helpers.testRequestParams('/lottery/vikingalotto') 34 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 35 | request(params, resultHandler) 36 | }) 37 | }) 38 | 39 | describe('eurojackpot', () => { 40 | // The only thing that changes is the form attribute, so why not just re-use the object 41 | const fieldsToCheckFor = ['date', 'lotto', 'joker', 'prize', 'link'] 42 | 43 | it('should return an array of objects containing correct fields', (done) => { 44 | const params = helpers.testRequestParams('/lottery/eurojackpot') 45 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 46 | request(params, resultHandler) 47 | }) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /endpoints/tv/skjarinn.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-plusplus */ 2 | /* eslint-disable prefer-destructuring */ 3 | const request = require('request') 4 | const moment = require('moment') 5 | const xml2js = require('xml2js') 6 | const h = require('apis-helpers') 7 | const app = require('../../server') 8 | 9 | const parseString = xml2js.parseString 10 | 11 | /* Parse feed from Skjarinn */ 12 | const parseSkjar1 = function (callback, data) { 13 | parseString(data, (err, result, title) => { 14 | if (err) throw new Error(`Parsing of XML failed. Title ${title}`) 15 | 16 | const schedule = [] 17 | 18 | for (let i = 0; i < result.schedule.service[0].event.length; ++i) { 19 | const event = result.schedule.service[0].event[i] 20 | if (moment().add('d', 1).startOf('day').hour(6) > moment(event.$['start-time'])) { 21 | schedule.push({ 22 | title: event.title[0], 23 | originalTitle: event['original-title'][0], 24 | duration: event.$.duration, 25 | description: event.description[0], 26 | shortDescription: event['short-description'][0], 27 | live: event.live[0] === 'yes', 28 | premier: event.rerun[0] === 'yes', 29 | startTime: event.$['start-time'], 30 | aspectRatio: event['aspect-ratio'][0].size[0], 31 | series: { 32 | episode: event.episode[0].$.number, 33 | series: event.episode[0].$['number-of-episodes'], 34 | }, 35 | }) 36 | } 37 | } 38 | return callback(schedule) 39 | }) 40 | } 41 | 42 | /* Skjar 1 */ 43 | app.get('/tv/:var(skjar1|skjareinn)', (req, res) => { 44 | res.status(503).json({ error: 'Source page has changed. Scraping needs to be re-implemented' }) 45 | return 46 | /* eslint-disable */ 47 | const url = 'http://www.skjarinn.is/einn/dagskrarupplysingar/?channel_id=7&output_format=xml' 48 | 49 | request.get({ 50 | headers: { 'User-Agent': h.browser() }, 51 | url: url 52 | }, function (error, response, body) { 53 | if (error || response.statusCode !== 200) { 54 | return res.status(504).json({ error:'skjarinn.is is not responding with the right data' }) 55 | } 56 | 57 | parseSkjar1(function (data) { 58 | res.cache(1800).json(200, { 59 | results: data 60 | }) 61 | }, body) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /endpoints/lottery/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/first */ 2 | const request = require('request') 3 | const cheerio = require('cheerio') 4 | const app = require('../../server') 5 | 6 | const parseList = function (body) { 7 | let $ 8 | try { 9 | $ = cheerio.load(body) 10 | } catch (error) { 11 | throw new Error('Could not parse body') 12 | } 13 | 14 | const results = [] 15 | 16 | const tr = $('table').eq(1).find('tr') 17 | 18 | tr.each(function (i) { 19 | if (i === 0) return 20 | const td = $(this).find('td') 21 | 22 | results.push({ 23 | date: td.eq(0).text().trim(), 24 | lotto: td.eq(1).html().match(/\d{1,2}/g).join(' ') 25 | .replace(/(\d{1,2})$/, '($1)'), 26 | joker: td.eq(2).text().trim(), 27 | prize: td.eq(3).text().trim(), 28 | link: `http://lotto.is${td.eq(4).find('a').attr('href').trim()}`, 29 | }) 30 | }) 31 | 32 | return results 33 | } 34 | 35 | const getLottery = function (callback, providedUrl) { 36 | const url = providedUrl || 'https://igvefur.lotto.is/lottoleikir/urslit/lotto/' 37 | 38 | const params = { url } 39 | 40 | request(params, (error, res, body) => { 41 | if (error) return callback(error) 42 | 43 | if (res.statusCode !== 200) { 44 | return callback(new Error(`HTTP error from endpoint, status code ${res.statusCode}`)) 45 | } 46 | 47 | return callback(null, body) 48 | }) 49 | } 50 | 51 | const getLotto = (req, res, next) => { 52 | getLottery((err, body) => { 53 | if (err) { 54 | return next(502) 55 | } 56 | return res.cache(3600).json({ 57 | results: parseList(body), 58 | }) 59 | }) 60 | } 61 | 62 | app.get('/lottery', getLotto) 63 | app.get('/lottery/lotto', getLotto) 64 | 65 | app.get('/lottery/vikingalotto', (req, res, next) => { 66 | getLottery((err, body) => { 67 | if (err) { 68 | return next(502) 69 | } 70 | return res.cache(3600).json({ 71 | results: parseList(body), 72 | }) 73 | }, 'https://games.lotto.is/lottoleikir/urslit/vikingalotto/') 74 | }) 75 | app.get('/lottery/eurojackpot', (req, res, next) => { 76 | getLottery((err, body) => { 77 | if (err) { 78 | return next(502) 79 | } 80 | return res.cache(3600).json({ 81 | results: parseList(body), 82 | }) 83 | }, 'https://games.lotto.is/lottoleikir/urslit/eurojackpot/') 84 | }) 85 | -------------------------------------------------------------------------------- /endpoints/rides/index.js: -------------------------------------------------------------------------------- 1 | const request = require('request') 2 | const cheerio = require('cheerio') 3 | const app = require('../../server') 4 | 5 | /* Root Rides */ 6 | app.get('/rides', (req, res) => { 7 | return res.json({ 8 | results: [ 9 | { 10 | info: 'This is an api for requests for rides/passengers in Iceland', 11 | endpoints: { 12 | 'samferda-drivers': '/rides/samferda-drivers/', 13 | 'samferda-passengers': '/rides/samferda-passengers/', 14 | }, 15 | }, 16 | ], 17 | }) 18 | }) 19 | 20 | const scrapeSamferdaFor = (requesting, res) => { 21 | const url = 'http://www.samferda.net/' 22 | 23 | request(url, (error, response, body) => { 24 | if (error) { 25 | return res.status(500).json({ error: `${url} not responding correctly...` }) 26 | } 27 | 28 | // Cheerio declared and then attemted to load. 29 | let $ 30 | try { 31 | $ = cheerio.load(body) 32 | } catch (e) { 33 | return res.status(500).json({ error: 'Could not load the body with cherrio.' }) 34 | } 35 | 36 | // Base object to be added to 37 | // and eventually sent as a JSON response. 38 | const results = [] 39 | 40 | $('table:has(thead) > tbody > tr[bgcolor]').each(function () { 41 | const cols = $(this).children() 42 | const rowType = cols.eq(1).text().trim() 43 | 44 | if (rowType === requesting) { 45 | results.push({ 46 | link: cols.eq(0).find('a').attr('href'), 47 | from: cols.eq(2).text().trim(), 48 | to: cols.eq(3).text().trim(), 49 | // convert to YYYY-MM-DD for easier sorting 50 | date: cols.eq(4).text().trim().split('.') 51 | .reverse() 52 | .join('-'), 53 | time: cols.eq(5).text().trim(), 54 | }) 55 | } 56 | }) 57 | 58 | return res.cache(1800).json({ results }) 59 | }) 60 | } 61 | 62 | /** 63 | * Fetches list of Passenger requests on Samferda.net. 64 | * response - JSON: Passenger requests within an 'results' array. 65 | */ 66 | app.get('/rides/samferda-drivers', (req, res) => { 67 | scrapeSamferdaFor('Passengers', res) 68 | }) 69 | /** 70 | * Fetches list of Ride requests on Samferda.net. 71 | * response - JSON: Ride requests within an 'results' array. 72 | */ 73 | app.get('/rides/samferda-passengers', (req, res) => { 74 | scrapeSamferdaFor('Ride', res) 75 | }) 76 | -------------------------------------------------------------------------------- /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/tv/ruv.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-plusplus */ 2 | /* eslint-disable no-prototype-builtins */ 3 | /* eslint-disable prefer-destructuring */ 4 | const request = require('request') 5 | const moment = require('moment') 6 | const xml2js = require('xml2js') 7 | const h = require('apis-helpers') 8 | const app = require('../../server') 9 | 10 | const parseString = xml2js.parseString 11 | 12 | /* Parse feeds from RUV */ 13 | const parseFeed = function (callback, data) { 14 | parseString(data, (err, result, title) => { 15 | if (err) return callback(new Error(`Parsing of XML failed. Title ${title}`)) 16 | 17 | const schedule = [] 18 | 19 | if (result.schedule.error || !result.schedule.service[0].hasOwnProperty('event')) { 20 | return (callback(schedule)) 21 | } 22 | 23 | for (let i = 0; i < result.schedule.service[0].event.length; ++i) { 24 | const event = result.schedule.service[0].event[i] 25 | schedule.push({ 26 | title: event.title[0], 27 | originalTitle: event['original-title'][0], 28 | duration: event.$.duration, 29 | description: event.description[0], 30 | shortDescription: event['short-description'][0], 31 | live: event.live[0] === 'yes', 32 | premier: event.rerun[0] === 'yes', 33 | startTime: event.$['start-time'], 34 | aspectRatio: event['aspect-ratio'][0].size[0], 35 | series: { 36 | episode: event.episode[0].$.number, 37 | series: event.episode[0].$['number-of-episodes'], 38 | }, 39 | }) 40 | } 41 | return callback(null, schedule) 42 | }) 43 | } 44 | 45 | const getFeed = function (url, callback) { 46 | request.get({ 47 | headers: { 'User-Agent': h.browser() }, 48 | url, 49 | }, (error, response, body) => { 50 | if (error) return callback(new Error(`${url} did not respond`)) 51 | 52 | parseFeed(callback, body) 53 | }) 54 | } 55 | 56 | const serve = function (url, res, next) { 57 | getFeed(url, (err, data) => { 58 | if (err) { 59 | console.error(err) 60 | return next(502) 61 | } 62 | 63 | res.cache(1800).json({ results: data }) 64 | }) 65 | } 66 | 67 | /* RUV */ 68 | app.get('/tv/ruv', (req, res, next) => { 69 | let url = 'http://muninn.ruv.is/files/xml/ruv/' 70 | 71 | if (req.params.date) { 72 | if (moment(req.params.date).isValid()) { 73 | const date = moment(req.params.date) 74 | // Example : http://muninn.ruv.is/files/xml/ruv/2013-06-11/ 75 | url += date.format('YYYY-MM-DD') 76 | } 77 | } 78 | 79 | serve(url, res, next) 80 | }) 81 | 82 | /* RUV Ithrottir */ 83 | app.get('/tv/ruvithrottir', (req, res, next) => { 84 | const url = 'http://muninn.ruv.is/files/xml/ruvithrottir/' 85 | serve(url, res, next) 86 | }) 87 | -------------------------------------------------------------------------------- /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/golf/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-destructuring */ 2 | const { parse: parseUrl } = require('url') 3 | const request = require('request') 4 | const cheerio = require('cheerio') 5 | const h = require('apis-helpers') 6 | const _ = require('lodash') 7 | const app = require('../../server') 8 | 9 | app.get('/golf/teetimes', (req, res) => { 10 | const clubId = req.query.club 11 | if (!clubId) { 12 | return res.status(500).json({ 13 | error: 'Please provide a valid club id to lookup', 14 | }) 15 | } 16 | 17 | request.get({ 18 | // http://stackoverflow.com/a/20091919 19 | rejectUnauthorized: false, 20 | headers: { 'User-Agent': h.browser() }, 21 | url: `http://mitt.golf.is/pages/rastimar/rastimayfirlit/?club=${clubId}`, 22 | }, (err, response, html) => { 23 | if (err || response.statusCode !== 200) { 24 | return res.status(500).json({ 25 | error: 'mitt.golf.is refuses to respond or give back data', 26 | }) 27 | } 28 | const $ = cheerio.load(html) 29 | const rows = $('table.teeTimeTable tbody').children() 30 | let time = '' 31 | return res.cache().json({ 32 | results: _.map(rows, (row) => { 33 | const $row = $(row) 34 | time = $row.children('td.time').html() === null ? time : $row.children('td.time').html() 35 | return { 36 | time, 37 | name: $($row.children('td.name')).html(), 38 | club: $($row.children('td.club')).html(), 39 | handicap: $($row.children('td.handicap')).html(), 40 | } 41 | }), 42 | }) 43 | }) 44 | }) 45 | 46 | app.get('/golf/clubs', (req, res) => { 47 | request.get({ 48 | // http://stackoverflow.com/a/20091919 49 | rejectUnauthorized: false, 50 | headers: { 'User-Agent': h.browser() }, 51 | url: 'http://mitt.golf.is/pages/rastimar/', 52 | }, (err, response, html) => { 53 | if (err || response.statusCode !== 200) { 54 | return res.status(500).json({ 55 | error: 'mitt.golf.is refuses to respond or give back data', 56 | }) 57 | } 58 | 59 | const $ = cheerio.load(html) 60 | // Skip the first element. 61 | const rows = $('table.golfTable tr').slice(2) 62 | return res.cache(3600).json({ 63 | results: _.map(rows, (row) => { 64 | const $row = $(row) 65 | const url = ( 66 | $row.children('td.club').children('a').attr('href') 67 | ) 68 | 69 | const query = parseUrl(url, true).query 70 | 71 | return { 72 | abbreviation: $row.children('td.abbreviation').html(), 73 | club: { 74 | id: query.club, 75 | name: $row.children('td.club').children('a').html(), 76 | }, 77 | location: $row.children('td.location').html(), 78 | } 79 | }), 80 | }) 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /endpoints/flight/index.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('/flight', (req, res) => { 8 | const data = req.query 9 | let url = '' 10 | let $ 11 | 12 | if (!data.type) data.type = '' 13 | if (!data.language) data.language = '' 14 | 15 | if (data.type === 'departures' && data.language === 'is') { 16 | url = 'http://www.kefairport.is/Flugaaetlun/Brottfarir/' 17 | } else if (data.type === 'departures' && data.language === 'en') { 18 | url = 'http://www.kefairport.is/English/Timetables/Departures/' 19 | } else if (data.type === 'arrivals' && data.language === 'is') { 20 | url = 'http://www.kefairport.is/Flugaaetlun/Komur/' 21 | } else if (data.type === 'arrivals' && data.language === 'en') { 22 | url = 'http://www.kefairport.is/English/Timetables/Arrivals/' 23 | } else { 24 | url = 'http://www.kefairport.is/English/Timetables/Arrivals/' 25 | } 26 | 27 | request.get({ 28 | headers: { 'User-Agent': h.browser() }, 29 | url, 30 | }, (error, response, body) => { 31 | if (error || response.statusCode !== 200) { 32 | return res.status(500).json({ error: 'www.kefairport.is refuses to respond or give back data' }) 33 | } 34 | 35 | try { 36 | $ = cheerio.load(body) 37 | } catch (err) { 38 | return res.status(500).json({ error: 'Could not parse body' }) 39 | } 40 | 41 | const obj = { results: [] } 42 | 43 | $('table tr').each(function (key) { 44 | if (key !== 0) { 45 | let flight = {} 46 | if (data.type === 'departures') { 47 | flight = { 48 | date: $(this).children('td').slice(0).html(), 49 | flightNumber: $(this).children('td').slice(1).html(), 50 | airline: $(this).children('td').slice(2).html(), 51 | to: $(this).children('td').slice(3).html(), 52 | plannedArrival: $(this).children('td').slice(4).html(), 53 | realArrival: $(this).children('td').slice(5).html(), 54 | status: $(this).children('td').slice(6).html(), 55 | } 56 | } else { 57 | flight = { 58 | date: $(this).children('td').slice(0).html(), 59 | flightNumber: $(this).children('td').slice(1).html(), 60 | airline: $(this).children('td').slice(2).html(), 61 | from: $(this).children('td').slice(3).html(), 62 | plannedArrival: $(this).children('td').slice(4).html(), 63 | realArrival: $(this).children('td').slice(5).html(), 64 | status: $(this).children('td').slice(6).html(), 65 | } 66 | } 67 | 68 | obj.results.push(flight) 69 | } 70 | }) 71 | 72 | return res.cache(3600).json(obj) 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /endpoints/tv/365.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-plusplus */ 2 | /* eslint-disable prefer-destructuring */ 3 | const request = require('request') 4 | const xml2js = require('xml2js') 5 | const h = require('apis-helpers') 6 | const app = require('../../server') 7 | 8 | const parseString = xml2js.parseString 9 | 10 | /* Parse feeds from 365 midlar */ 11 | const parseFeed = function (callback, data) { 12 | parseString(data, (err, result, title) => { 13 | if (err) return callback(new Error(`Parsing of XML failed. Title ${title}`)) 14 | 15 | const schedule = [] 16 | 17 | for (let i = 0; i < result.schedule.event.length; ++i) { 18 | const event = result.schedule.event[i] 19 | 20 | schedule.push({ 21 | title: event.title[0], 22 | originalTitle: event.org_title[0], 23 | duration: event.$.duration, 24 | description: event.description[0], 25 | live: event.live[0].$.value === 'true', 26 | premier: event.premier[0].$.value === 'true', 27 | startTime: event.$.starttime, 28 | aspectRatio: event.aspectratio[0].$.value, 29 | series: { 30 | episode: event.series ? event.series[0].$.episode : '', 31 | series: event.series ? event.series[0].$.series : '', 32 | }, 33 | }) 34 | } 35 | return callback(null, schedule) 36 | }) 37 | } 38 | 39 | const getFeed = function (url, callback) { 40 | request.get({ 41 | headers: { 'User-Agent': h.browser() }, 42 | url, 43 | }, (error, response, body) => { 44 | if (error) return callback(new Error(`${url} did not respond`)) 45 | 46 | parseFeed(callback, body) 47 | }) 48 | } 49 | 50 | const serve = function (url, res, next) { 51 | getFeed(url, (err, data) => { 52 | if (err) { 53 | console.error(err) 54 | return next(502) 55 | } 56 | 57 | res.cache(1800).json({ 58 | results: data, 59 | }) 60 | }) 61 | } 62 | 63 | /* Stod 2 */ 64 | app.get('/tv/stod2', (req, res, next) => { 65 | const url = 'http://stod2.is/XML--dagskrar-feed/XML-Stod-2-dagurinn' 66 | serve(url, res, next) 67 | }) 68 | 69 | /* Stod 2 Sport */ 70 | app.get('/tv/stod2sport', (req, res, next) => { 71 | const url = 'http://www.stod2.is/XML--dagskrar-feed/XML-Stod-2-Sport-dagurinn' 72 | serve(url, res, next) 73 | }) 74 | 75 | /* Stod 2 Sport 2 */ 76 | app.get('/tv/stod2sport2', (req, res, next) => { 77 | const url = 'http://www.stod2.is/XML--dagskrar-feed/XML-Stod-2-Sport-2-dagurinn' 78 | serve(url, res, next) 79 | }) 80 | 81 | /* Stod 3 */ 82 | app.get('/tv/stod3', (req, res, next) => { 83 | const url = 'http://www.stod2.is/XML--dagskrar-feed/XML-Stod-3-dagurinn' 84 | serve(url, res, next) 85 | }) 86 | 87 | /* Stod 2 Bio */ 88 | app.get('/tv/stod2bio', (req, res, next) => { 89 | const url = 'http://www.stod2.is/XML--dagskrar-feed/XML-Stod-2-Bio-dagurinn' 90 | serve(url, res, next) 91 | }) 92 | -------------------------------------------------------------------------------- /lib/test_helpers.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-destructuring */ 2 | /* eslint-disable no-prototype-builtins */ 3 | /* eslint-disable no-restricted-syntax */ 4 | const assert = require('assert') 5 | 6 | function assertResults(json, canBeEmpty) { 7 | assert(json.results, 'Does not contain a \'results\' field') 8 | if (!canBeEmpty) { 9 | assert(json.results.length > 0, 'Results are empty') 10 | } 11 | } 12 | 13 | function assertPresenceOfFields(fields, arr) { 14 | arr.forEach((result, i) => { 15 | fields.forEach((field) => { 16 | const fieldExists = result[field] !== undefined 17 | assert(fieldExists, `Missing field ${field} in result #${i}`) 18 | }) 19 | }) 20 | } 21 | 22 | /* 23 | Asserts that all fields provided are of the expected type (Date is a bit 24 | messy, since it will be a string until we actually try to parse it as a Date). 25 | */ 26 | function assertTypesOfFields(fields, arr) { 27 | arr.forEach((result) => { 28 | for (const key in fields) { 29 | if (fields.hasOwnProperty(key)) { 30 | const type = fields[key] 31 | const value = result[key] 32 | let constructor = value.constructor 33 | if (type === Date && !Number.isNaN(Date.parse(value))) { 34 | constructor = Date 35 | } 36 | assert( 37 | constructor === type, 38 | `Field ${key} should be ${type.name}, but is ${constructor.name}` 39 | ) 40 | } 41 | } 42 | }) 43 | } 44 | 45 | // always returns the same fields, so we'll just reuse this function for both cases 46 | // (I may be going a bit overboard on this) 47 | exports.testRequestHandlerForFields = (done, fieldsToCheckFor, customCallback, canBeEmpty) => { 48 | return (err, res, body) => { 49 | if (err) throw err 50 | let json 51 | try { 52 | json = JSON.parse(body) 53 | } catch (e) { 54 | throw e 55 | } 56 | 57 | // Check for the presence of the results property 58 | assertResults(json, canBeEmpty) 59 | 60 | const fieldsIsObject = fieldsToCheckFor.constructor === Object 61 | const fields = fieldsIsObject ? Object.keys(fieldsToCheckFor) : fieldsToCheckFor 62 | 63 | if (!canBeEmpty) { 64 | // Check for the presence of all expected fields 65 | assertPresenceOfFields(fields, json.results) 66 | } 67 | 68 | if (fieldsIsObject) { 69 | assertTypesOfFields(fieldsToCheckFor, json.results) 70 | } 71 | 72 | if (customCallback) { 73 | customCallback(json) 74 | } 75 | 76 | done() 77 | } 78 | } 79 | // Generate http request params for a particular endpoint 80 | exports.testRequestParams = (path, form) => { 81 | return { 82 | url: `http://localhost:3101${path}`, 83 | method: 'GET', 84 | qs: form, 85 | headers: ['Content-Type: application/json'], 86 | } 87 | } 88 | exports.assertPresenceOfFields = assertPresenceOfFields 89 | exports.assertTypesOfFields = assertTypesOfFields 90 | -------------------------------------------------------------------------------- /endpoints/weather/documentation.md: -------------------------------------------------------------------------------- 1 | # Icelandic Weather 2 | 3 | Source: [Icelandic Meteorological Office](http://vedur.is) 4 | 5 | - GET [/weather/:source/:lang](https://apis.is/weather/:source/:lang) 6 | 7 | Get weather information for Iceland. 8 | Under 'Descriptions' you will find a list that will help you understand the output of the following endpoints. 9 | 10 | | Parameters | Description | Example | 11 | |------------------|--------------------------------------|------------------------------------------------| 12 | | :type (required) | Type of information to get | [forecasts](https://apis.is/weather/forecasts) | 13 | | :lang | Language of output, defaults to 'is' | [en](https://apis.is/weather/forecasts/en) | 14 | 15 | - GET [/weather/forecasts/:lang](https://apis.is/weather/forecasts/:lang) 16 | 17 | | Parameters | Description | Example | 18 | |---------------------|--------------------------------------|------------------------------------------------| 19 | | :lang | Language of output, defaults to 'is' | [en](https://apis.is/weather/forecasts/en) | 20 | | stations (required) | List of station numbers seperated by commas(,) or semicolons(;). See links below for more information. [Weather stations (icelandic)](http://www.vedur.is/vedur/stodvar) & [Weather stations (english)](http://en.vedur.is/weather/stations/) | | 21 | 22 | - GET [/weather/observations/:lang](https://apis.is/weather/observations/:lang) 23 | 24 | | Parameters | Description | Example | 25 | |---------------------|--------------------------------------|------------------------------------------------| 26 | | :lang | Language of output, defaults to 'is' | [en](https://apis.is/weather/forecasts/en) | 27 | | stations (required) | List of station numbers seperated by commas(,) or semicolons(;). See links below for more information. [Weather stations (icelandic)](http://www.vedur.is/vedur/stodvar) & [Weather stations (english)](http://en.vedur.is/weather/stations/) | | 28 | | time | 1h (default) = Fetch data from automatic weather stations that are updated on the hour. 3h = Only fetch mixed data from manned and automatic weather stations that is updated every 3 hours. | | 29 | | anytime | 0 (default) = an error will be returned if current data is not available. 1 = last available numbers will be displayed, regardless of date. | | 30 | 31 | - GET [/weather/texts](https://apis.is/weather/texts) 32 | 33 | | Parameters | Description | 34 | |------------------|--------------------------------------| 35 | | types (required) | List of types seperated by commas(,) or semicolons(;). See 'Valid types' below for full list of valid type numbers and what they stand for. | 36 | 37 | --- 38 | -------------------------------------------------------------------------------- /endpoints/petrol/index.js: -------------------------------------------------------------------------------- 1 | const request = require('request') 2 | const $ = require('cheerio') 3 | const h = require('apis-helpers') 4 | const app = require('../../server') 5 | 6 | function queryData(callback) { 7 | const url = 'https://raw.githubusercontent.com/gasvaktin/gasvaktin/master/vaktin/gas.min.json' 8 | const headers = { 9 | Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 10 | 'Accept-Language': 'en-US,en;q=0.8,is;q=0.6', 11 | 'Cache-Control': 'max-age=0', 12 | 'Content-Type': 'application/x-www-form-urlencoded', 13 | 'User-Agent': h.browser(), 14 | } 15 | request.get({ 16 | headers, 17 | url, 18 | }, (error, response, body) => { 19 | if (error || response.statusCode !== 200) { 20 | callback(error, response, '') 21 | } 22 | callback(error, response, body) 23 | }) 24 | } 25 | 26 | function queryTimestamps(callback) { 27 | const url = 'https://gist.github.com/gasvaktin' 28 | const headers = { 29 | Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 30 | 'Accept-Language': 'en-US,en;q=0.8,is;q=0.6', 31 | 'Cache-Control': 'no-cache', 32 | Connection: 'keep-alive', 33 | Host: 'gist.github.com', 34 | Pragma: 'no-cache', 35 | 'Upgrade-Insecure-Requests': 1, 36 | 'User-Agent': h.browser(), 37 | } 38 | request.get({ 39 | headers, 40 | url, 41 | }, (error, response, body) => { 42 | if (error || response.statusCode !== 200) { 43 | callback(error, response, '') 44 | } 45 | callback(error, response, body) 46 | }) 47 | } 48 | 49 | function parseTimestamps(htmlText) { 50 | const data = $(htmlText) 51 | return { 52 | lastPriceChanges: data.find('#file-prices_changed_timestamp-LC1').text().slice(0, 23), 53 | lastPriceCheck: data.find('#file-prices_lookup_timestamp-LC1').text().slice(0, 23), 54 | } 55 | } 56 | 57 | app.get('/petrol', (req, res) => { 58 | const timestampApis = (new Date().toISOString().slice(0, 23)) 59 | const nineMinutes = 900 60 | queryData((error1, response1, jsonText) => { 61 | if (error1 || response1.statusCode !== 200) { 62 | return res.status(500).json({ 63 | error: 'raw.githubusercontent.com refuses to respond or give back data', 64 | }) 65 | } 66 | const results = JSON.parse(jsonText).stations 67 | queryTimestamps((error2, response2, htmlText) => { 68 | if (error2 || response2.statusCode !== 200) { 69 | return res.cache(nineMinutes).json({ 70 | results, 71 | timestampApis, 72 | timestampPriceChanges: null, 73 | timestampPriceCheck: null, 74 | }) 75 | } 76 | const timestamps = parseTimestamps(htmlText) 77 | return res.cache(nineMinutes).json({ 78 | results, 79 | timestampApis, 80 | timestampPriceChanges: timestamps.lastPriceChanges, 81 | timestampPriceCheck: timestamps.lastPriceCheck, 82 | }) 83 | }) 84 | }) 85 | }) 86 | -------------------------------------------------------------------------------- /endpoints/sarschool/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/first */ 2 | const request = require('request') 3 | const cheerio = require('cheerio') 4 | const app = require('../../server') 5 | 6 | function pad(n) { 7 | return n < 10 ? `0${n}` : n 8 | } 9 | 10 | const getRequest = (callback, providedUrl) => { 11 | const url = providedUrl || 'http://skoli.landsbjorg.is/Open/Seminars.aspx?' 12 | 13 | const params = { url } 14 | 15 | request(params, (error, res, body) => { 16 | if (error) throw new Error(error) 17 | 18 | if (res.statusCode !== 200) { 19 | throw new Error(`HTTP error from endpoint, status code ${res.statusCode}`) 20 | } 21 | 22 | return callback(body) 23 | }) 24 | } 25 | 26 | const parseList = (body) => { 27 | let $ 28 | try { 29 | $ = cheerio.load(body) 30 | } catch (error) { 31 | throw new Error('Could not parse body') 32 | } 33 | 34 | const results = [] 35 | 36 | const tr = $('.rgMasterTable').find('tbody').find('tr') 37 | 38 | tr.each(() => { 39 | const td = $(this).find('td') 40 | 41 | // Change start time from d.m.YYYY to YYYY-mm-dd 42 | const startDate = td.eq(6).text().trim() 43 | let startDateFinal 44 | let sdSplit 45 | let sd 46 | if (startDate === '') { 47 | startDateFinal = 'n/a' 48 | } else { 49 | sdSplit = startDate.split('.') 50 | sd = new Date(sdSplit[2], sdSplit[1], sdSplit[0]) 51 | startDateFinal = `${sd.getFullYear()}-${pad(sd.getMonth())}-${pad(sd.getDate())}` 52 | } 53 | 54 | // Change end time from d.m.YYYY to YYYY-mm-dd 55 | const endDate = td.eq(7).text().trim() 56 | let endDateFinal 57 | let edSplit 58 | let ed 59 | if (endDate === '') { 60 | endDateFinal = 'n/a' 61 | } else { 62 | edSplit = endDate.split('.') 63 | ed = new Date(edSplit[2], edSplit[1], edSplit[0]) 64 | endDateFinal = `${ed.getFullYear()}-${pad(ed.getMonth())}-${pad(ed.getDate())}` 65 | } 66 | 67 | results.push({ 68 | id: (td.eq(3).text().trim() === '' ? '' : parseFloat(td.eq(3).text().trim())), 69 | name: (td.eq(4).text().trim() === '' ? '' : td.eq(4).text().trim()), 70 | time_start: startDateFinal, 71 | time_end: endDateFinal, 72 | sar_members_only: (td.eq(0).find('img').length > 0 ? 1 : 0), 73 | host: (td.eq(5).find('input').attr('checked') === 'checked' ? 'Squad' : 'Other'), 74 | location: (td.eq(8).text().trim() === '' ? '' : td.eq(8).text().trim()), 75 | price_regular: (td.eq(9).text().trim() === '' ? '' : parseFloat(td.eq(9).text().trim().replace('.', ''))), 76 | price_members: (td.eq(10).text().trim() === '' ? '' : parseFloat(td.eq(10).text().trim().replace('.', ''))), 77 | link: `http://skoli.landsbjorg.is/Open/Course.aspx?Id=${td.eq(3).text().trim()}`, 78 | }) 79 | }) 80 | 81 | return results 82 | } 83 | 84 | app.get('/sarschool', (req, res) => { 85 | getRequest((body) => { 86 | return res.cache().json({ 87 | results: parseList(body), 88 | }) 89 | }) 90 | }) 91 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apis.is", 3 | "version": "0.3.0", 4 | "main": "server.js", 5 | "author": { 6 | "name": "Kristján Ingi Mikaelsson", 7 | "email": "kristjanmik@gmail.com" 8 | }, 9 | "engines": { 10 | "node": ">=8.6.0" 11 | }, 12 | "xo": { 13 | "envs": [ 14 | "node", 15 | "mocha" 16 | ], 17 | "semicolon": false, 18 | "space": true, 19 | "rules": { 20 | "object-curly-spacing": "off", 21 | "arrow-parens": "off", 22 | "comma-dangle": "off", 23 | "camelcase": "off", 24 | "curly": "off", 25 | "capitalized-comments": "off", 26 | "unicorn/catch-error-name": "off", 27 | "unicorn/filename-case": "off", 28 | "brace-style": "off", 29 | "max-params": "off", 30 | "no-implicit-coercion": "off", 31 | "max-nested-callbacks": "off", 32 | "handle-callback-err": "off", 33 | "no-warning-comments": "off", 34 | "no-negated-condition": "off", 35 | "unicorn/prefer-type-error": "off", 36 | "unicorn/no-abusive-eslint-disable": "off" 37 | } 38 | }, 39 | "description": "A json wrapper around publicly open data in Iceland", 40 | "licence": "MIT", 41 | "repository": { 42 | "type": "git", 43 | "url": "https://github.com/kristjanmik/apis.git" 44 | }, 45 | "contributors": [ 46 | { 47 | "name": "Arnór Heiðar Sigurðsson", 48 | "email": "arnorhs@gmail.com" 49 | }, 50 | { 51 | "name": "Ragnar Þór Valgeirsson (rthor)", 52 | "email": "ragnar.valgeirsson@gmail.com", 53 | "url": "http://rthor.is" 54 | }, 55 | { 56 | "name": "Baldur Már Helgason", 57 | "email": "baldur.helgason@gmail.com" 58 | }, 59 | { 60 | "name": "Benedikt D Valdez (benediktvaldez)", 61 | "email": "benediktvaldez@gmail.com", 62 | "url": "http://bdvs.info" 63 | }, 64 | { 65 | "name": "Kristján Oddsson (koddsson)", 66 | "email": "koddsson@gmail.com", 67 | "url": "https://koddsson.com" 68 | }, 69 | { 70 | "name": "Alexander Baldvin", 71 | "email": "alexanderbaldvin@gmail.com" 72 | } 73 | ], 74 | "dependencies": { 75 | "apis-helpers": "0.0.1", 76 | "cheerio": "0.22.0", 77 | "debug": "3.1.0", 78 | "expect": "22.4.3", 79 | "express": "^4.16.3", 80 | "file": "~0.2.2", 81 | "fridagar": "3.0.0", 82 | "globby": "8.0.1", 83 | "graphql": "0.13.2", 84 | "iconv-lite": "0.4.21", 85 | "isn2wgs": "0.0.2", 86 | "lodash": "^4.17.5", 87 | "marked": "github:koddsson/marked#escape-markdown-option", 88 | "mocha": "5.1.1", 89 | "moment": "2.22.1", 90 | "nock": "^9.2.3", 91 | "raven": "2.4.2", 92 | "redis": "^2.8.0", 93 | "request": "2.85.0", 94 | "scraper": "0.0.9", 95 | "statuses": "^1.4.0", 96 | "xml2js": "0.4.19", 97 | "xregexp": "4.1.1" 98 | }, 99 | "scripts": { 100 | "dev": "nodemon server.js", 101 | "start": "DEBUG=server node server.js", 102 | "test": "NODE_ENV=test mocha --exit --timeout 10000 --reporter=dot ./lib/*.spec.js ./test.js ./endpoints/*/tests/integration_test.js", 103 | "coveralls": "exit 0", 104 | "lint": "xo" 105 | }, 106 | "devDependencies": { 107 | "nodemon": "1.17.3", 108 | "sinon": "5.0.3", 109 | "xo": "^0.21.0" 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-globals */ 2 | /* eslint-disable import/no-dynamic-require */ 3 | /* eslint-disable global-require */ 4 | /* eslint-disable prefer-destructuring */ 5 | /* eslint-disable import/first */ 6 | /* eslint-disable new-cap */ 7 | 8 | const { EventEmitter: EE } = require('events') 9 | const express = require('express') 10 | 11 | const fileModule = require('file') 12 | const statuses = require('statuses') 13 | const debug = require('debug')('server') 14 | const Raven = require('raven') 15 | const metricsMiddleware = require('./lib/expressMetrics') 16 | 17 | const cache = require('./lib/cache') 18 | const cors = require('./lib/cors') 19 | 20 | const app = express() 21 | 22 | // Set up error tracking with Sentry 23 | const SENTRY_URL = process.env.SENTRY_URL 24 | const redis = require('./lib/redis') 25 | 26 | if (SENTRY_URL !== undefined && SENTRY_URL !== '') { 27 | Raven.config(SENTRY_URL).install() 28 | 29 | // The request handler must be the first middleware on the app 30 | app.use(Raven.requestHandler()) 31 | 32 | // The error handler must be before any other error middleware 33 | app.use(Raven.errorHandler()) 34 | } 35 | 36 | if (process.env.NODE_ENV !== 'test') { 37 | app.use(metricsMiddleware()) 38 | app.get('/metrics', (req, res) => { 39 | redis.HGETALL('metrics', (error, metrics) => { 40 | res.json(metrics) 41 | }) 42 | }) 43 | } 44 | 45 | module.exports = app 46 | 47 | /** 48 | * Set the spacing to 0 for shorter output 49 | */ 50 | app.set('json spaces', 0) 51 | 52 | /** 53 | * Create an event listener for app 54 | */ 55 | EE.call(app) 56 | 57 | /** 58 | * Cross-origin resource sharing 59 | */ 60 | app.use(cors()) 61 | 62 | /** 63 | * Caching layer 64 | */ 65 | app.use(cache()) 66 | 67 | /** 68 | * Set up endpoints 69 | */ 70 | fileModule.walkSync('./endpoints', (dirPath, dirs, endpoints) => { 71 | function requireEndpoint(endpoint) { 72 | if (endpoint.includes('.js') && endpoint !== 'graphql_schema.js') { 73 | try { 74 | require(`./${dirPath}/${endpoint}`) 75 | } catch (e) { 76 | console.error('Error loading file', e) 77 | } 78 | } 79 | } 80 | 81 | if (endpoints && dirPath.indexOf('test') < 0) { 82 | endpoints.forEach(requireEndpoint) 83 | } 84 | }) 85 | 86 | app.use((error, req, res, next) => { 87 | let code = 500 88 | let message = 'Unknown error' 89 | 90 | if (res.headersSent) { 91 | console.error('Headers already sent') 92 | return next() 93 | } 94 | 95 | if (typeof error === 'number') { 96 | code = error 97 | message = statuses[code] || message 98 | } else if ( 99 | typeof error === 'object' && 100 | error.message && 101 | error.message.length === 3 && 102 | !isNaN(error.message)) { 103 | code = error.message 104 | message = statuses[code] || message 105 | } else { 106 | // Other errors that might have been swallowed 107 | debug(error.stack) 108 | message = error.message 109 | } 110 | 111 | code = parseInt(code, 10) 112 | 113 | res.status(code).json({ error: message }) 114 | }) 115 | 116 | /** 117 | * Start the server 118 | */ 119 | const port = 120 | process.env.PORT || (process.env.NODE_ENV === 'test' ? 3101 : 3100) 121 | app.listen(port, () => { 122 | app.emit('ready') 123 | }) 124 | 125 | app.on('ready', () => { 126 | debug('Server running at port:', port) 127 | }) 128 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /endpoints/tv/tests/integration_test.js: -------------------------------------------------------------------------------- 1 | const request = require('request') 2 | const helpers = require('../../../lib/test_helpers') 3 | 4 | describe('tv root', () => { 5 | it('should return info', (done) => { 6 | const fieldsToCheckFor = ['info'] 7 | const params = helpers.testRequestParams('/tv/') 8 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 9 | request.get(params, resultHandler) 10 | }) 11 | }) 12 | 13 | describe.skip('tv - skjar1', () => { 14 | it('should return an array of objects containing correct fields', (done) => { 15 | const fieldsToCheckFor = ['series', 'title', 'originalTitle', 'description', 'live', 'premier'] 16 | const params = helpers.testRequestParams('/tv/skjar1') 17 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 18 | request.get(params, resultHandler) 19 | }) 20 | }) 21 | 22 | describe('tv - stod2 bio', () => { 23 | it('should return an array of objects containing correct fields', (done) => { 24 | const fieldsToCheckFor = ['series', 'title', 'originalTitle', 'description', 'live', 'premier'] 25 | const params = helpers.testRequestParams('/tv/stod2bio') 26 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 27 | request.get(params, resultHandler) 28 | }) 29 | }) 30 | 31 | describe('tv - stod3', () => { 32 | it('should return an array of objects containing correct fields', (done) => { 33 | const fieldsToCheckFor = ['series', 'title', 'originalTitle', 'description', 'live', 'premier'] 34 | const params = helpers.testRequestParams('/tv/stod3') 35 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 36 | request.get(params, resultHandler) 37 | }) 38 | }) 39 | 40 | describe.skip('tv - stod2 sport2', () => { 41 | it('should return an array of objects containing correct fields', (done) => { 42 | const fieldsToCheckFor = ['series', 'title', 'originalTitle', 'description', 'live', 'premier'] 43 | const params = helpers.testRequestParams('/tv/stod2sport2') 44 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 45 | request.get(params, resultHandler) 46 | }) 47 | }) 48 | 49 | describe('tv - stod2 sport', () => { 50 | it('should return an array of objects containing correct fields', (done) => { 51 | const fieldsToCheckFor = ['series', 'title', 'originalTitle', 'description', 'live', 'premier'] 52 | const params = helpers.testRequestParams('/tv/stod2sport') 53 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 54 | request.get(params, resultHandler) 55 | }) 56 | }) 57 | 58 | describe('tv - stod2', () => { 59 | it('should return an array of objects containing correct fields', (done) => { 60 | const fieldsToCheckFor = ['series', 'title', 'originalTitle', 'description', 'live', 'premier'] 61 | const params = helpers.testRequestParams('/tv/stod2') 62 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 63 | request.get(params, resultHandler) 64 | }) 65 | }) 66 | 67 | describe.skip('tv - ruv ithrottir', function () { 68 | it('should return an array of objects containing correct fields', (done) => { 69 | this.timeout(20000) 70 | const fieldsToCheckFor = ['series', 'title', 'originalTitle', 'description', 'live', 'premier'] 71 | const params = helpers.testRequestParams('/tv/ruvithrottir') 72 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor, undefined, true) 73 | request.get(params, resultHandler) 74 | }) 75 | }) 76 | 77 | describe.skip('tv - ruv', function () { 78 | it('should return an array of objects containing correct fields', (done) => { 79 | this.timeout(20000) 80 | const fieldsToCheckFor = ['series', 'title', 'originalTitle', 'description', 'live', 'premier'] 81 | const params = helpers.testRequestParams('/tv/ruv') 82 | const resultHandler = helpers.testRequestHandlerForFields(done, fieldsToCheckFor) 83 | request.get(params, resultHandler) 84 | }) 85 | }) 86 | -------------------------------------------------------------------------------- /endpoints/names/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/first */ 2 | /* 3 | About: An API for all names that are allowed in Iceland. Names are devided into 4 | male names, female names and middle names 5 | Author: Hjörtur Líndal Stefánsson 6 | Email: hjorturls@gmail.com 7 | Created: August 2014 8 | */ 9 | 10 | const request = require('request') 11 | const h = require('apis-helpers') 12 | const cheerio = require('cheerio') 13 | const app = require('../../server') 14 | 15 | /* Handles the request for a specific request URL */ 16 | function handleRequest(providedUrl, req, res) { 17 | let $ 18 | // Check for the filter parameter 19 | const filter = req.params.filter || req.query.filter || req.query.search || '' 20 | 21 | // Add name filtering if it is requested 22 | let url = providedUrl || '' 23 | if (filter !== '') { 24 | url += `&Nafn=${filter}` 25 | } 26 | 27 | request.get({ 28 | headers: { 'User-Agent': h.browser() }, 29 | url, 30 | }, (error, response, body) => { 31 | if (error || response.statusCode !== 200) { 32 | return res.status(500).json({ error: 'www.island.is refuses to respond or give back data' }) 33 | } 34 | 35 | try { 36 | $ = cheerio.load(body) 37 | } catch (exc) { 38 | return res.status(500).json({ error: 'Could not parse body' }) 39 | } 40 | 41 | const obj = { results: [] } 42 | 43 | // Clear data regarding the acceptance date of the name (not needed) 44 | $('.dir li i').each(() => { 45 | $(this).remove() 46 | }) 47 | 48 | // Loop through all the names in the list and add them to our array 49 | $('.dir li').each(() => { 50 | const name = $(this).text() 51 | obj.results.push(name.trim()) 52 | }) 53 | 54 | // Return the results as JSON and cache for 24 hours 55 | return res.cache(86400).json(obj) 56 | }) 57 | } 58 | 59 | /* Root names handler - only returns a list of resources */ 60 | app.get('/names', (req, res) => { 61 | return res.json({ 62 | results: [ 63 | { 64 | // eslint-disable-next-line max-len 65 | info: 'This is an api that lists all allowed Icelandic names. A search parameter can be used with each endpoint', 66 | endpoints: { 67 | males: '/names/males/', 68 | females: '/names/females/', 69 | middlenames: '/names/middlenames/', 70 | }, 71 | }, 72 | ], 73 | }) 74 | }) 75 | 76 | /* Get all legal names for males */ 77 | app.get('/names/males/:filter?', (req, res) => { 78 | const url = 'https://www.island.is/mannanofn/leit-ad-nafni/?Stafrof=&Drengir=on&Samthykkt=yes' 79 | return handleRequest(url, req, res) 80 | }) 81 | 82 | /* Get all legal names for females */ 83 | app.get('/names/females/:filter?', (req, res) => { 84 | const url = 'https://www.island.is/mannanofn/leit-ad-nafni/?Stafrof=&Stulkur=on&Samthykkt=yes' 85 | return handleRequest(url, req, res) 86 | }) 87 | 88 | /* Get all legal middle names */ 89 | app.get('/names/middlenames/:filter?', (req, res) => { 90 | const url = 'https://www.island.is/mannanofn/leit-ad-nafni/?Stafrof=&Millinofn=on&Samthykkt=yes' 91 | return handleRequest(url, req, res) 92 | }) 93 | 94 | /* Get all rejected names for males */ 95 | app.get('/names/rejected/males/:filter?', (req, res) => { 96 | const url = 'https://www.island.is/mannanofn/leit-ad-nafni/?Stafrof=&Drengir=on&Samthykkt=no' 97 | return handleRequest(url, req, res) 98 | }) 99 | 100 | /* Get all rejected names for females */ 101 | app.get('/names/rejected/females/:filter?', (req, res) => { 102 | const url = 'https://www.island.is/mannanofn/leit-ad-nafni/?Stafrof=&Stulkur=on&Samthykkt=no' 103 | return handleRequest(url, req, res) 104 | }) 105 | 106 | /* Get all rejected middle names */ 107 | app.get('/names/rejected/middlenames/:filter?', (req, res) => { 108 | const url = 'https://www.island.is/mannanofn/leit-ad-nafni/?Stafrof=&Millinofn=on&Samthykkt=no' 109 | return handleRequest(url, req, res) 110 | }) 111 | 112 | /* Get all rejected names */ 113 | app.get('/names/rejected/:filter?', (req, res) => { 114 | const url = 'https://www.island.is/mannanofn/leit-ad-nafni/?Stafrof=&Stulkur=on&Drengir=on&Millinofn=on&Samthykkt=no' 115 | return handleRequest(url, req, res) 116 | }) 117 | -------------------------------------------------------------------------------- /endpoints/ship/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | /* eslint-disable no-restricted-globals */ 3 | /* eslint-disable prefer-promise-reject-errors */ 4 | const request = require('request') 5 | const $ = require('cheerio') 6 | const h = require('apis-helpers') 7 | const app = require('../../server') 8 | 9 | const parseIsFloat = str => parseFloat(str.replace('.', '').replace(',', '.')) 10 | 11 | const lookupShip = searchStr => new Promise((resolve, reject) => { 12 | // Encode searchString so that Icelandic characters will work 13 | const searchString = encodeURIComponent(searchStr) 14 | const url = `http://www.samgongustofa.is/siglingar/skrar-og-utgafa/skipaskra/uppfletting?sq=${searchString}` 15 | 16 | request.get({ 17 | headers: { 'User-Agent': h.browser() }, 18 | url, 19 | }, (error, response, body) => { 20 | if (error || response.statusCode !== 200) { 21 | reject('www.samgongustofa.is refuses to respond or give back data') 22 | } 23 | 24 | // Translations from: https:// www.samgongustofa.is/media/siglingar/skip/Vefskipaskra-2012.pdf 25 | const typeDict = { 26 | BJÖRGUNARSKIP: 'LIFEBOAT', 27 | DRÁTTARSKIP: 'TUGBOAT', 28 | 'DÝPK. OG SANDSKIP': 'DREDGERSET', 29 | DÝPKUNARSKIP: 'DREDGER', 30 | 'EFTIRLITS‐ OG BJÖRGUNARSKIP': 'PATROL AND LIFEBOAT', 31 | FARÞEGASKIP: 'PASSENGERSHIP', 32 | 'FISKI,FARÞEGASKIP': 'FISHING,PASSENGERSHIP', 33 | FISKISKIP: 'FISHING VESSEL', 34 | FLOTBRYGGJA: 'PONTOON BRIDGE', 35 | FLOTKVÍ: 'FLOATING DOCK', 36 | 'FLUTNINGA/BRUNNSKIP': 'CARGO VESSEL LIVE FISH CARRIER', 37 | FRÍSTUNDAFISKISKIP: '', 38 | 'HAFNSÖGU/DRÁTTARSKIP': 'PILOT‐ AND TUGBOAT', 39 | HVALVEIÐISKIP: 'FISH.V.WHALE CATCHER', 40 | LÓÐSSKIP: 'PILOT BOAT', 41 | 'NÓTAVEIÐI/SKUTTOGARI': 'FISH.V.PURSE STEINERS/ST', 42 | OLÍUSKIP: 'OIL TANKER', 43 | PRAMMI: 'BARGE', 44 | RANNSÓKNARSKIP: 'RESEARCH VESSEL', 45 | SAFNSKIP: 'MUSEUM SHIP', 46 | SEGLSKIP: 'SAILBOAT', 47 | SJÓMÆLINGASKIP: '', 48 | SKEMMTISKIP: 'PLEASURE CRAFT', 49 | SKÓLASKIP: 'TRAINING VESSEL', 50 | SKUTTOGARI: 'FISH.V.STERN TRAWLER', 51 | VARÐSKIP: 'INSPECTION SHIP', 52 | VINNUSKIP: 'WORKBOAT', 53 | VÍKINGASKIP: 'VIKING SHIP', 54 | VÖRUFLUTNINGASKIP: 'DRY CARGO SHIP', 55 | ÞANGSKURÐARPRAMMI: 'BARGE', 56 | } 57 | 58 | const data = $(body) 59 | const fieldList = [] 60 | data.find('.vehicleinfo ul').each((index, element) => { 61 | const fields = [] 62 | $(element).find('li').each((i, el) => { 63 | // i === 10 is info about ship owners 64 | if (i !== 10) { 65 | const val = $(el).find('span').text() 66 | fields.push(val) 67 | } else { 68 | // We'll parse the owners' field separately 69 | const owners = [] 70 | $(el).children('span').each(function () { 71 | const info = $(this).text().split(/\s{2,}/g) 72 | const owner = { 73 | name: info[1], 74 | socialnumber: info[2].replace('(kt. ', '').replace('-', '').replace(')', ''), 75 | share: parseIsFloat(info[3].replace(' eign', '')) / 100, 76 | } 77 | owners.push(owner) 78 | }) 79 | fields.push(owners) 80 | } 81 | }) 82 | if (fields.length > 0) { 83 | fieldList.push(fields) 84 | } 85 | }) 86 | 87 | if (fieldList.length > 0 && fieldList[0].length > 0) { 88 | resolve(fieldList.map((fields) => { 89 | const type = typeDict[fields[1]] ? typeDict[fields[1]] : fields[1] 90 | const registrationStatus = fields[5] === 'Skráð' ? 'Listed' : 'Unlisted' 91 | return { 92 | name: fields[0], 93 | type, 94 | registrationNumber: fields[2], 95 | regionalCode: fields[3].replace(/(\(.*\))/g, '').match(/(\S.*)/g).join(' '), 96 | homePort: fields[4], 97 | registrationStatus, 98 | grossRegisterTonnage: parseIsFloat(fields[6]), 99 | grossTonnage: parseIsFloat(fields[7]), 100 | length: parseIsFloat(fields[8]), 101 | buildYear: parseInt(fields[9].split('af')[0], 10), 102 | buildYard: fields[9].split('af')[1].match(/(\S.*)/g).join(' '), 103 | owners: fields[10], 104 | } 105 | })) 106 | } else { 107 | reject(`No ship found with the query ${searchStr}`) 108 | } 109 | }) 110 | }) 111 | 112 | app.get('/ship', async (req, res) => { 113 | const search = req.query.search || '' 114 | 115 | if (!search) { 116 | return res.status(431).json({ error: 'Please provide a valid search string to lookup' }) 117 | } 118 | 119 | try { 120 | const ships = await lookupShip(search) 121 | res.cache().json({ results: ships }) 122 | } catch (error) { 123 | res.status(500).json({ error }) 124 | } 125 | }) 126 | 127 | module.exports = lookupShip 128 | -------------------------------------------------------------------------------- /endpoints/names/tests/integration_test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/extensions */ 2 | /* 3 | About: Integration tests for the Names API 4 | Author: Hjörtur Líndal Stefánsson 5 | Email: hjorturls@gmail.com 6 | Created: August 2014 7 | */ 8 | 9 | const assert = require('assert') 10 | const request = require('request') 11 | const helpers = require('../../../lib/test_helpers.js') 12 | 13 | /* Asserts the results */ 14 | function assertResults(params, beEmpty) { 15 | const shouldBeEmpty = beEmpty || false 16 | request.get(params, (err, res, body) => { 17 | const json = JSON.parse(body) 18 | 19 | assert(json.results && typeof json.results.length !== 'undefined', 'Does not contain a \'results\' field') 20 | if (!shouldBeEmpty) { 21 | assert(json.results.length > 0, 'Results are empty') 22 | } else { 23 | assert(json.results.length <= 0, 'Results are NOT empty') 24 | } 25 | }) 26 | } 27 | 28 | /* Test the unfiltered list of names */ 29 | function testUnfiltered(url) { 30 | it('should return an array of strings', (done) => { 31 | const params = helpers.testRequestParams(url, {}) 32 | 33 | assertResults(params) 34 | done() 35 | }) 36 | } 37 | 38 | /* Test filtering with an invalid string */ 39 | function testFilteredEmpty(url) { 40 | it('should return an array of strings', (done) => { 41 | const params = helpers.testRequestParams(url, { search: '234asdf' }) 42 | 43 | assertResults(params, true) 44 | done() 45 | }) 46 | } 47 | 48 | /* Test filtering the results with a valid filter */ 49 | function testFiltered(url) { 50 | it('should return an array of strings', (done) => { 51 | const params = helpers.testRequestParams(url, { search: 'an' }) 52 | 53 | assertResults(params) 54 | done() 55 | }) 56 | } 57 | 58 | describe('names', () => { 59 | /* Test the /names/males endpoint with filtering */ 60 | describe('males-filtered', () => { 61 | testFiltered('/names/males') 62 | }) 63 | 64 | /* Test the /names/males endpoint with invalid filtering */ 65 | describe('males-filtered-invalid', () => { 66 | testFilteredEmpty('/names/males') 67 | }) 68 | 69 | /* Test the /names/males endpoint without filtering */ 70 | describe('males', () => { 71 | testUnfiltered('/names/males') 72 | }) 73 | 74 | /* Test the /names/females endpoint with filtering */ 75 | describe('females-filtered', () => { 76 | testFiltered('/names/females') 77 | }) 78 | 79 | /* Test the /names/females endpoint with invalid filtering */ 80 | describe('females-filtered-invalid', () => { 81 | testFilteredEmpty('/names/males') 82 | }) 83 | 84 | /* Test the /names/females endpoint without filtering */ 85 | describe('females', () => { 86 | testUnfiltered('/names/females') 87 | }) 88 | 89 | /* Test the /names/middlenames endpoint with filtering */ 90 | describe('middlenames-filtered', () => { 91 | testFiltered('/names/middlenames') 92 | }) 93 | 94 | /* Test the /names/middlenames endpoint with invalid filtering */ 95 | describe('middlenames-filtered-invalid', () => { 96 | testFilteredEmpty('/names/middlenames') 97 | }) 98 | 99 | /* Test the /names/middlenames endpoint without filtering */ 100 | describe('middlenames', () => { 101 | testUnfiltered('/names/middlenames') 102 | }) 103 | 104 | /* Test the /names/rejected/males endpoint with filtering */ 105 | describe('rejected-males-filtered', () => { 106 | testFiltered('/names/rejected/males') 107 | }) 108 | 109 | /* Test the /names/rejected/males endpoint with invalid filtering */ 110 | describe('rejected-males-filtered-invalid', () => { 111 | testFilteredEmpty('/names/rejected/males') 112 | }) 113 | 114 | /* Test the /names/rejected/males endpoint without filtering */ 115 | describe('rejected-males', () => { 116 | testUnfiltered('/names/rejected/males') 117 | }) 118 | 119 | /* Test the /names/rejected/females endpoint with filtering */ 120 | describe('rejected-females-filtered', () => { 121 | testFiltered('/names/rejected/females') 122 | }) 123 | 124 | /* Test the /names/rejected/females endpoint with invalid filtering */ 125 | describe('rejected-females-filtered-invalid', () => { 126 | testFilteredEmpty('/names/rejected/females') 127 | }) 128 | 129 | /* Test the /names/rejected/females endpoint without filtering */ 130 | describe('rejected-females', () => { 131 | testUnfiltered('/names/rejected/females') 132 | }) 133 | 134 | /* Test the /names/rejected/middlenames endpoint with filtering */ 135 | describe('rejected-middlenames-filtered', () => { 136 | testFiltered('/names/rejected/middlenames') 137 | }) 138 | 139 | /* Test the /names/rejected/middlenames endpoint with invalid filtering */ 140 | describe('rejected-middlenames-filtered-invalid', () => { 141 | testFilteredEmpty('/names/rejected/middlenames') 142 | }) 143 | 144 | /* Test the /names/rejected/middlenames endpoint without filtering */ 145 | describe('rejected-middlenames', () => { 146 | testUnfiltered('/names/rejected/middlenames') 147 | }) 148 | }) 149 | -------------------------------------------------------------------------------- /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/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/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