├── .coveralls.yml ├── .prettierrc ├── .travis.yml ├── .babelrc ├── src ├── log.js ├── metapoints.js ├── config.js └── main.js ├── LICENSE ├── .gitignore ├── package.json ├── test ├── dummydata.json └── main.js └── README.md /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: QCTnmh0xE4BcVBpxVv6vvwfKwf0TqTJp8 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | # .prettierrc 2 | printWidth: 100 3 | semi: false 4 | useTabs: false 5 | tabWidth: 4 6 | singleQuote: true 7 | trailingComma: none 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "lts/*" 4 | install: 5 | - npm install 6 | script: 7 | - npm test 8 | after_success: npm run coverage 9 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "targets": { 7 | "node": "current" 8 | } 9 | } 10 | ] 11 | ], 12 | "plugins": [ 13 | "transform-object-rest-spread" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/log.js: -------------------------------------------------------------------------------- 1 | const LOGLEVEL = process.env.LOGLEVEL || 'none' 2 | const log = { 3 | i: obj => { 4 | if (LOGLEVEL === 'info' || LOGLEVEL === 'error' || LOGLEVEL === 'debug') { 5 | console.log(obj) 6 | } 7 | }, 8 | e: obj => { 9 | if (LOGLEVEL === 'info' || LOGLEVEL === 'error') { 10 | console.error(obj) 11 | } 12 | }, 13 | d: obj => { 14 | if (LOGLEVEL === 'debug') { 15 | console.error(obj) 16 | } 17 | } 18 | } 19 | export default log 20 | -------------------------------------------------------------------------------- /src/metapoints.js: -------------------------------------------------------------------------------- 1 | import log from './log.js' 2 | 3 | export default { 4 | todo: 'make meta endpoints for combining data from multiple endpoints', 5 | async mGetOwnActiveListingsFull(token) { 6 | if (!token) { 7 | log.e("Airbnbapi: Can't get an active listing list without a token") 8 | return null 9 | } 10 | const listings = await this.getOwnActiveListings(token) 11 | const fullListings = await Promise.all( 12 | listings.map(listing => { 13 | return this.getListingInfoHost({ token, id: listing.id }) 14 | }) 15 | ) 16 | return fullListings 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | domain: 'https://api.airbnb.com', 3 | api_key: 'd306zoyjsyarp7ifhu67rjxn52tv0t20', 4 | default_headers: { 5 | 'Content-Type': 'application/json; charset=UTF-8', 6 | 'User-Agent': 7 | 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36' 8 | // 'User-Agent': 'Mozillaz/5.0 (Windows NT 6.1)AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36' 9 | // 'User-Agent': 'TESTING API' 10 | // 'User-Agent': randomUseragent.getRandom() 11 | }, 12 | currency: 'JPY', 13 | proxy: undefined 14 | } 15 | export default config 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-2018 zxol@github.com 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | README_wip.md 2 | test.js 3 | tmp 4 | temp 5 | decompile 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (http://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Typescript v1 declaration files 46 | typings/ 47 | 48 | # Optional npm cache directory 49 | .npm 50 | 51 | # Optional eslint cache 52 | .eslintcache 53 | 54 | # Optional REPL history 55 | .node_repl_history 56 | 57 | # Output of 'npm pack' 58 | *.tgz 59 | 60 | # Yarn Integrity file 61 | .yarn-integrity 62 | 63 | # dotenv environment variables file 64 | .env 65 | 66 | # compiled code 67 | index.js 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "airbnbapijs", 3 | "version": "0.10.5", 4 | "description": "Unofficial airbnb.com REST API wrapper for node.js", 5 | "main": "build/main.js", 6 | "directories": { 7 | "src": "src" 8 | }, 9 | "scripts": { 10 | "build": "npm run prettier && babel src -d build ", 11 | "test": "npm run build && nyc mocha", 12 | "coverage": "nyc report --reporter=text-lcov | coveralls", 13 | "prettier": "prettier --write \"src/**/*.js\"", 14 | "temp": "npm run build; node index.js" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/zxol/airbnbapi.git" 19 | }, 20 | "author": "zxol", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/zxol/airbnbapi/issues" 24 | }, 25 | "homepage": "https://github.com/zxol/airbnbapi#readme", 26 | "dependencies": { 27 | "request": "^2.85.0", 28 | "request-promise": "^4.2.4" 29 | }, 30 | "devDependencies": { 31 | "babel-cli": "^6.26.0", 32 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 33 | "babel-preset-env": "^1.7.0", 34 | "chai": "^4.2.0", 35 | "coveralls": "^3.0.4", 36 | "dotenv": "^8.0.0", 37 | "lodash": "^4.17.11", 38 | "mocha": "^6.1.4", 39 | "nock": "^10.0.6", 40 | "nyc": "^14.1.1", 41 | "prettier": "^1.18.2" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/dummydata.json: -------------------------------------------------------------------------------- 1 | { 2 | "getOwnActiveListings": [ 3 | { 4 | "listing": { 5 | "listing": { 6 | "id": 1, 7 | "city": "Shinagawa-ku", 8 | "thumbnail_url": "https://a0.muscache.com/im/pictures/faked.jpg?aki_policy=small", 9 | "medium_url": "https://a0.muscache.com/im/pictures/faked.jpg?aki_policy=medium", 10 | "user_id": 1, 11 | "picture_url": "https://a0.muscache.com/im/pictures/faked.jpg?aki_policy=large", 12 | "xl_picture_url": "https://a0.muscache.com/im/pictures/faked.jpg?aki_policy=x_large", 13 | "price": 74, 14 | "native_currency": "JPY", 15 | "price_native": 8108, 16 | "price_formatted": "¥ 8108", 17 | "lat": 35.9, 18 | "lng": 139.2, 19 | "country": "Japan", 20 | "name": "First dummy Listing", 21 | "smart_location": "Shinagawa-ku, Japan", 22 | "has_double_blind_reviews": false 23 | } 24 | } 25 | }, 26 | { 27 | "listing": { 28 | "listing": { 29 | 30 | "id": 2, 31 | "city": "Nakano-ku", 32 | "thumbnail_url": "https://a0.muscache.com/im/pictures/faked.jpg?aki_policy=small", 33 | "medium_url": "https://a0.muscache.com/im/pictures/faked.jpg?aki_policy=medium", 34 | "user_id": 1, 35 | "picture_url": "https://a0.muscache.com/im/pictures/faked.jpg?aki_policy=large", 36 | "xl_picture_url": "https://a0.muscache.com/im/pictures/faked.jpg?aki_policy=x_large", 37 | "price": 319, 38 | "native_currency": "JPY", 39 | "price_native": 34953, 40 | "price_formatted": "¥ 34953", 41 | "lat": 35.1, 42 | "lng": 139.1, 43 | "country": "Japan", 44 | "name": "Second dummy Listing", 45 | "smart_location": "Nakano-ku, Japan", 46 | "has_double_blind_reviews": false 47 | } 48 | } 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Breaking changes ahead! 2 | 3 | Version 0.11.0, due to be released sometime after July 1st. Errors will no longer return null values. Instead, errors will be thrown. 4 | 5 | # _Unofficial_ **airbnb.com** REST API wrapper for node.js 6 | 7 | ![](http://eloisecleans.com/blog/wp-content/uploads/2018/02/airbnb-logo-png-logo-black-transparent-airbnb-329-300x300.png) 8 | ![](https://cdn2.iconfinder.com/data/icons/nodejs-1/256/nodejs-256.png) 9 | ![](https://travis-ci.org/zxol/airbnbapi.svg?branch=master) 10 | [![Coverage Status](https://coveralls.io/repos/github/zxol/airbnbapi/badge.svg?branch=master)](https://coveralls.io/github/zxol/airbnbapi?branch=master) 11 | 12 | --- 13 | 14 | Hi there! 👋 This is a javascript library for interacting with airbnb's API. 15 | _Disclaimer: this library is not associated with airbnb and should only be used for educational reasons. It is an interface for a private API used by airbnb's mobile applications._ 16 | This is a pre 1.0 library. Please request endpoints and functionality as repo issues. Collaborators wanted! 17 | 18 | # Essential Info 19 | 20 | - All functions return [**promises**.](https://github.com/wbinnssmith/awesome-promises) 21 | - The returned data format is pre-parsed JSON, i.e. a javascript object. Multiple records will be returned as an array. 22 | - The auth system is a simple crypto token. For the uninitiated, this is like a username and password in one. If you're only using a single account, you can supply a token with `.setDefaultToken()`, otherwise, you will have to supply a token with every function call. 23 | - Yeah, I know, airlock is a massive pain in the posterior. 24 | - Error reporting and data validation is spotty at this stage! 25 | - This library only has one dependency - request-promise. 26 | 27 | # Getting started 👨‍💻 28 | 29 | ## Installing 30 | 31 | ``` 32 | npm install airbnbapijs 33 | ``` 34 | 35 | ## Importing 36 | 37 | ``` 38 | var airbnb = require('airbnbapijs') 39 | ``` 40 | 41 | or es6... 42 | 43 | ``` 44 | import airbnb from 'airbnbapijs' 45 | ``` 46 | 47 | # Reference 📗 48 | 49 | ## Contents 50 | 51 | 1. Authorization 52 | 1. Users 53 | 1. Calendars 54 | 1. Listings 55 | 1. Threads 56 | 1. Reservations 57 | 1. Posting 58 | 1. Configuration 59 | 60 | --- 61 | 62 | ## AUTHORIZATION 63 | 64 | ### testAuth 65 | 66 | Test a token 67 | 68 | ```js 69 | airbnb.testAuth('faketoken3sDdfvtF9if5398j0v5nui') 70 | // returns bool 71 | ``` 72 | 73 | ### newAccessToken 74 | 75 | Request a new token 76 | 77 | ```js 78 | airbnb.newAccessToken({ username: 'foo@bar.com', password: 'hunter2' }) 79 | // returns {token: 'faketoken3sDdfvtF9if5398j0v5nui'} or {error: {error obj}} 80 | ``` 81 | 82 | ### login 83 | 84 | Request a new token (v2 endpoint). Similar to the above function but returns a user info summary with much more information. 85 | 86 | ```js 87 | airbnb.login({ username: 'foo@bar.com', password: 'hunter2' }) 88 | // returns a user info object (includes token) or {error: {error obj}} 89 | ``` 90 | 91 | ### setDefaultToken 92 | 93 | Set the token to use if a token is not supplied for an endpoint function. 94 | 95 | ```js 96 | airbnb.setDefaultToken('faketoken3sDdfvtF9if5398j0v5nui') 97 | ``` 98 | 99 | TODO: support other login methods (facebook, twitter, etc...) 100 | 101 | --- 102 | 103 | ## USERS 104 | 105 | ### getGuestInfo 106 | 107 | Get a user's public facing information 108 | 109 | ```js 110 | airbnb.getGuestInfo(2348485493) 111 | // returns public info about user (JSON) 112 | ``` 113 | 114 | ### getOwnUserInfo 115 | 116 | Obtain user data for the logged in account 117 | 118 | ```js 119 | airbnb.getOwnUserInfo('faketoken3sDdfvtF9if5398j0v5nui') 120 | // returns private info about user (JSON) 121 | ``` 122 | 123 | --- 124 | 125 | ## CALENDARS 126 | 127 | ### getPublicListingCalendar 128 | 129 | Public availability and price data on a listing. `count` is the duration in months. 130 | 131 | ```js 132 | airbnb.getPublicListingCalendar({ 133 | id: 109834757, 134 | month: 1, 135 | year: 2018, 136 | count: 1 137 | }) 138 | // returns array of calendar days, with availability and price 139 | ``` 140 | 141 | ### getCalendar 142 | 143 | Private calendar data regarding your listings. Reservations, cancellations, prices, blocked days. 144 | 145 | ```js 146 | airbnb.getCalendar({ 147 | token: 'faketoken3sDdfvtF9if5398j0v5nui', 148 | id: 109834757, 149 | startDate: '2018-01-01', 150 | endDate: '2018-02-28' 151 | }) 152 | // returns array of calendar days with extended info, for your listings 153 | ``` 154 | 155 | ### setPriceForDay 156 | 157 | Set a price for a day. 158 | 159 | ```js 160 | airbnb.setPriceForDay({ 161 | token: 'faketoken3sDdfvtF9if5398j0v5nui', 162 | id: 109834757, 163 | date: '2018-01-01', 164 | price: 1203 165 | }) 166 | // returns a result of the operation 167 | ``` 168 | 169 | ### setAvailabilityForDay 170 | 171 | Set availability for a day. 172 | 173 | ```js 174 | airbnb.setAvailabilityForDay({ 175 | token: 'faketoken3sDdfvtF9if5398j0v5nui', 176 | id: 109834757, 177 | date: '2018-01-01', 178 | availability: 'available' // or 'blocked'? 179 | }) 180 | // returns a result of the operation 181 | ``` 182 | 183 | --- 184 | 185 | ## LISTINGS 186 | 187 | ### listingSearch 188 | 189 | Airbnb's mighty search bar in JSON form. All arguments are optional. 190 | 191 | ```js 192 | airbnb.listingSearch({ 193 | location: 'Akihabara, Tokyo', 194 | checkin: '2020-01-21', 195 | checkout: '2020-02-10', 196 | offset: 0, 197 | limit: 20, 198 | language: 'en-US', 199 | currency: 'USD', 200 | guests: 6, // Number of guests for price quote 201 | instantBook: true, // only list instant bookable listings. 202 | minBathrooms: 0, 203 | minBedrooms: 2, 204 | minBeds: 6, 205 | minPrice: 0, 206 | maxPrice: 0, 207 | superhost: true, 208 | amenities: [1, 2, 4, 23], // array of IDs. 209 | hostLanguages: [1, 3, 6], // array of IDs. 210 | keywords: 'ocean view,garden,quiet', //comma separated 211 | roomTypes: ['Entire home/apt', 'Private room', 'Shared room'], 212 | neighborhoods: ['westside', 'riverside'], 213 | minPicCount: 4, 214 | sortDirection: 1 // 1 = forward, 0 = reverse 215 | }) 216 | // returns an array of listings 217 | ``` 218 | 219 | ### getListingInfo 220 | 221 | Gets public facing data on any listing. 222 | 223 | ```js 224 | airbnb.getListingInfo(109834757) 225 | // returns public info for any listing (JSON) 226 | ``` 227 | 228 | ### getListingInfoHost 229 | 230 | Gets private data on one of your listings. 231 | 232 | ```js 233 | airbnb.getListingInfoHost({ 234 | token: 'faketoken3sDdfvtF9if5398j0v5nui', 235 | id: 109834757 236 | }) 237 | // returns extended listing info for your listing (JSON) 238 | ``` 239 | 240 | ### getHostSummary 241 | 242 | Gets an object containing a host's active listings, alerts, and upcoming reservations 243 | 244 | ```js 245 | airbnb.getHostSummary('faketoken3sDdfvtF9if5398j0v5nui') 246 | // returns host summary info for your account (JSON) 247 | ``` 248 | 249 | ### getOwnActiveListings 250 | 251 | Gets an array containing a host's active listings 252 | 253 | ```js 254 | airbnb.getOwnActiveListings('faketoken3sDdfvtF9if5398j0v5nui') 255 | // returns listing array for your account (JSON) 256 | ``` 257 | 258 | --- 259 | 260 | ### getOwnListings 261 | 262 | Gets an array containing a host's listings 263 | 264 | ```js 265 | airbnb.getOwnListings({ 266 | token: 'faketoken3sDdfvtF9if5398j0v5nui', 267 | userId: '2344594' 268 | }) 269 | // returns listing array for your account (JSON) 270 | ``` 271 | 272 | --- 273 | 274 | ## THREADS 275 | 276 | ### getThread 277 | 278 | Returns a conversation with a guest or host. This is a legacy endpoint which is somewhat limited in the content (only basic messages are reported in the 'posts' array) 279 | 280 | ```js 281 | airbnb.getThread({ 282 | token: 'faketoken3sDdfvtF9if5398j0v5nui', 283 | id: 909878797 284 | }) 285 | // returns a single thread in the legacy format (JSON) 286 | ``` 287 | 288 | ### getThreads 289 | 290 | A simple list of thread ID's, ordered by latest update. The offset is how many to skip, and the limit is how many to report. 291 | 292 | ```js 293 | airbnb.getThreads({ 294 | token: 'faketoken3sDdfvtF9if5398j0v5nui', 295 | offset: 0, 296 | limit: 20 297 | }) 298 | // returns an array of thread IDS (only the ids, ordered by latest update) (JSON) 299 | ``` 300 | 301 | ### getThreadsFull 302 | 303 | This is the best way to pull thread data. Returns an array of full thread data, ordered by latest update. The `offset` is how many to skip, and the `limit` is how many to report. 304 | 305 | ```js 306 | airbnb.getThreadsFull({ 307 | token: 'faketoken3sDdfvtF9if5398j0v5nui', 308 | offset: 0, 309 | limit: 10 310 | }) 311 | // returns an array of threads in the new format, ordered by latest update (JSON) 312 | ``` 313 | 314 | ### getThreadsBatch 315 | 316 | A batch version of the above. You can grab a collection of threads referenced by thread ID. 317 | 318 | ```js 319 | airbnb.getThreadsBatch({ 320 | token: 'faketoken3sDdfvtF9if5398j0v5nui', 321 | ids: [23049848, 203495875, 398328244] 322 | }) 323 | // returns an array of threads in the new format (JSON) 324 | ``` 325 | 326 | --- 327 | 328 | ## RESERVATIONS 329 | 330 | ### getReservation 331 | 332 | Reservation data for one reservation. 333 | 334 | ```js 335 | airbnb.getReservation({ 336 | token: 'faketoken3sDdfvtF9if5398j0v5nui', 337 | id: 909878797 338 | }) 339 | // returns a single reservation in the mobile app format (JSON) 340 | ``` 341 | 342 | ### getReservations 343 | 344 | Returns a list of reservations in the same format as above, ordered by latest update 345 | 346 | ```js 347 | airbnb.getReservations({ 348 | token: 'faketoken3sDdfvtF9if5398j0v5nui', 349 | offset: 0, 350 | limit: 10 351 | }) 352 | // returns an array of reservations in the mobile app format, ordered by latest update (JSON) 353 | ``` 354 | 355 | ### getReservationsBatch 356 | 357 | Batch call for grabbing a list of reservations by ID. 358 | 359 | ```js 360 | airbnb.getReservationsBatch({ 361 | token: 'faketoken3sDdfvtF9if5398j0v5nui', 362 | ids: [98769876, 98769543, 98756745] 363 | }) 364 | // returns an array of reservations in the new format (JSON) 365 | ``` 366 | 367 | --- 368 | 369 | ## POSTING 370 | 371 | ### sendMessage 372 | 373 | Send a message to a thread. 374 | 375 | ```js 376 | airbnb.sendMessage({ 377 | token: 'faketoken3sDdfvtF9if5398j0v5nui', 378 | id: 2039448789, 379 | message: 'Hi there!' 380 | }) 381 | // returns confirmation 382 | ``` 383 | 384 | ### sendPreApproval 385 | 386 | Send a pre-approval to a guest. 387 | 388 | ```js 389 | airbnb.sendPreApproval({ 390 | token: 'faketoken3sDdfvtF9if5398j0v5nui', 391 | thread_id: 2039448789, 392 | listing_id: 340598483, 393 | message: '' 394 | }) 395 | // returns confirmation 396 | ``` 397 | 398 | ### sendReview 399 | 400 | Send a review to a guest after they have checked out. (`id` is the thread id) 401 | 402 | ```js 403 | airbnb.sendReview({ 404 | token: 'faketoken3sDdfvtF9if5398j0v5nui', 405 | id: 2039448789, 406 | comments: 'They were great guests!', 407 | private_feedback: 'Thank you for staying!', 408 | cleanliness: 5, 409 | communication: 5, 410 | respect_house_rules: 5, 411 | recommend: true 412 | }) 413 | // returns confirmation 414 | ``` 415 | 416 | ### sendSpecialOffer 417 | 418 | Send a special offer to a guest. 419 | 420 | ```js 421 | airbnb.sendSpecialOffer({ 422 | token: 'faketoken3sDdfvtF9if5398j0v5nui', 423 | check_in: '2018-10-13T00:00:00+00:00', 424 | guests: 1, 425 | listing_id: 9876676, 426 | nights: 1, 427 | price: 100000, 428 | thread_id: 98766767, 429 | currency: 'USD' 430 | }) 431 | // returns confirmation 432 | ``` 433 | 434 | ### alterationRequestResponse 435 | 436 | Send a "reservation alteration request response" to a guest 437 | To accept the request, supply the `decision` prop with `true` 438 | To decline the request, supply the `decision` prop with `false` 439 | 440 | ```js 441 | alterationRequestResponse({ 442 | token: 'faketoken3sDdfvtF9if5398j0v5nui', 443 | reservationId: 23049459, 444 | alterationId: 2134094, 445 | decision: true, 446 | currency: 'USD' 447 | }) 448 | // returns alteration object, or an error object. 449 | ``` 450 | 451 | ## CONFIGURATION 452 | 453 | ### setConfig 454 | 455 | Set multiple config variables at once 456 | 457 | ```js 458 | setConfig({ 459 | defaultToken: 'faketoken3sDdfvtF9if5398j0v5nui', 460 | apiKey: '01123581321345589144233377610987', 461 | currency: 'USD', 462 | userAgent: 'Mosaic/0.9', 463 | proxy: 'myproxy.com' 464 | }) 465 | ``` 466 | 467 | ### setDefaultToken 468 | 469 | Set the token to use if a token is not supplied for an endpoint function. 470 | 471 | ```js 472 | airbnb.setDefaultToken('faketoken3sDdfvtF9if5398j0v5nui') 473 | ``` 474 | 475 | ### setApiKey 476 | 477 | Use an api key different from the standard one 478 | 479 | ```js 480 | airbnb.setApiKey('01123581321345589144233377610987') 481 | ``` 482 | 483 | ### setCurrency 484 | 485 | Set the default [currency](https://www.iban.com/currency-codes.html) (the default is JPY, sorry USA) 486 | 487 | ```js 488 | airbnb.setCurrency('USD') 489 | ``` 490 | 491 | ### setUserAgent 492 | 493 | set the user agent string for the requests 494 | 495 | ```js 496 | airbnb.setUserAgent('Mosaic/0.9') 497 | ``` 498 | 499 | ### setProxy 500 | 501 | set a proxy server to run traffic through 502 | 503 | ```js 504 | airbnb.setProxy('myproxy.com') 505 | ``` 506 | -------------------------------------------------------------------------------- /test/main.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | process.env.NODE_ENV = 'test' 3 | let abba = require('../build/main.js') 4 | let log = require('../build/log.js') 5 | let chai = require('chai') 6 | let nock = require('nock') 7 | let _ = require('lodash') 8 | let {assert, should, expect} = chai 9 | 10 | let dummyData = require('./dummydata.json') 11 | 12 | // console.log(JSON.stringify(dummyData, null, 4)) 13 | 14 | const apiBaseUrl = 'https://api.airbnb.com' 15 | const allBut = str => new RegExp('^(?!.*'+str+')') 16 | const nockauth = _ => nock(apiBaseUrl).matchHeader('X-Airbnb-OAuth-Token', 'mockcorrecttoken') 17 | const nockauthl = _ => nockauth().log(console.log) 18 | 19 | describe('airbnbapi', () => { 20 | 21 | describe('#testAuth(token)', () => { 22 | it('should return null if a token is not passed', async () => { 23 | expect(await abba.testAuth()).to.be.null 24 | }) 25 | 26 | // Mock endpoint: invalid token 27 | nock(apiBaseUrl) 28 | .matchHeader('X-Airbnb-OAuth-Token', allBut('mockcorrecttoken')) //anything but regex 29 | .post('/v2/batch', {operations:[]}) 30 | .query(true) 31 | .reply(400) 32 | 33 | it('should return false for incorrect token', async () => { 34 | // console.log(await abba.testAuth('z')) 35 | expect(await abba.testAuth('z')).to.be.false 36 | }) 37 | 38 | // Mock endpoint: valid token 'mockcorrecttoken' 39 | nockauth() 40 | .post('/v2/batch', {operations: []} ) 41 | .query(true) 42 | .reply(200, {operations:[]}) 43 | it('should return true for correct token', async () => { 44 | expect(await abba.testAuth('mockcorrecttoken')).to.be.true 45 | }) 46 | }) 47 | 48 | describe('#newAccessToken({username, password})', () => { 49 | // Mock endpoint: invalid info 50 | it('should return null if no arguments are passed or arguments are missing', async () => { 51 | expect(await abba.newAccessToken()).to.be.null 52 | expect(await abba.newAccessToken({password: 'asdf'})).to.be.null 53 | expect(await abba.newAccessToken({username: 'asdf'})).to.be.null 54 | }) 55 | 56 | nock(apiBaseUrl) 57 | .post('/v1/authorize', { 58 | grant_type: 'password', 59 | username: 'wrong', 60 | password: 'wrong' 61 | }) 62 | .query(true) 63 | .reply(400, {"error": "mock invalid username or password"}) 64 | 65 | it('should return error object if login details are incorrect', async () => { 66 | expect(await abba.newAccessToken({username: 'wrong', password: 'wrong'})).to.have.property('error') 67 | }) 68 | // Mock endpoint: valid info 'mockuser'. 'mockpass' 69 | nock(apiBaseUrl) 70 | .post('/v1/authorize', { 71 | grant_type: 'password', 72 | username: 'mockuser', 73 | password: 'mockpass' 74 | }) 75 | .query(true) 76 | .reply(200, {access_token:'mocktoken'}) 77 | 78 | it('should return a token obejct if the login details are correct', async () => { 79 | expect(await abba.newAccessToken({username: 'mockuser', password: 'mockpass' })).to.have.property('token') 80 | }) 81 | }) 82 | 83 | describe('#login({email, password})', () => { 84 | const testFunc = abba.login.bind(abba) 85 | // Mock endpoint: invalid info 86 | it('should return null if no arguments are passed or arguments are missing', async () => { 87 | expect(await testFunc()).to.be.null 88 | expect(await testFunc({password: 'asdf'})).to.be.null 89 | expect(await testFunc({email: 'asdf'})).to.be.null 90 | }) 91 | 92 | nock(apiBaseUrl) 93 | .post('/v2/logins', { 94 | email: 'wrong', 95 | password: 'wrong' 96 | }) 97 | .query(true) 98 | .reply(400, {"error": "mock invalid username or password"}) 99 | 100 | it('should return error object if login details are incorrect', async () => { 101 | expect(await testFunc({email: 'wrong', password: 'wrong'})).to.have.property('error') 102 | }) 103 | 104 | // Mock endpoint: valid info 'mockuser'. 'mockpass' 105 | nock(apiBaseUrl) 106 | .post('/v2/logins', { 107 | email: 'mockuser', 108 | password: 'mockpass' 109 | }) 110 | .query(true) 111 | .reply(200, { login:{id:'mocktoken'}}) 112 | 113 | it('should return a summary object if the login details are correct', async () => { 114 | expect(await testFunc({email: 'mockuser', password: 'mockpass' })).to.have.property('login') 115 | }) 116 | }) 117 | 118 | describe('#getPublicListingCalendar({id, month, year, count})', () => { 119 | const testFunc = abba.getPublicListingCalendar.bind(abba) 120 | it('should return null if no arguments are passed or arguments are missing', async () => { 121 | expect(await testFunc()).to.be.null 122 | expect(await testFunc({month: '1', year: '2018', count: '1' })).to.be.null 123 | }) 124 | nock(apiBaseUrl) 125 | .get('/v2/calendar_months') 126 | .query({ 127 | currency: "JPY", 128 | key: "d306zoyjsyarp7ifhu67rjxn52tv0t20", 129 | listing_id: "1234", 130 | month: "1", 131 | year: "2018", 132 | count: "1", 133 | _format: "with_conditions" 134 | }) 135 | .reply(200, { public:'calendar'}) 136 | 137 | it('should return a calendar if arguments are correct', async () => { 138 | expect(await testFunc({id: 1234, month: '1', year: '2018', count: '1' })).to.have.property('public') 139 | }) 140 | }) 141 | 142 | describe('#getCalendar({token, id, startDate, endDate})', () => { 143 | const testFunc = abba.getCalendar.bind(abba) 144 | it('should return null if no arguments are passed or arguments are missing', async () => { 145 | expect(await testFunc()).to.be.null 146 | expect(await testFunc({id:1234, startDate:'2017/11/01', endDate:'2017/12/01'})).to.be.null 147 | expect(await testFunc({token: 'mocktoken', startDate:'2017/11/01', endDate:'2017/12/01'})).to.be.null 148 | expect(await testFunc({token: 'mocktoken', id:1234, endDate:'2017/12/01'})).to.be.null 149 | expect(await testFunc({token: 'mocktoken', id:1234, startDate:'2017/11/01'})).to.be.null 150 | }) 151 | nockauth() 152 | .get('/v2/calendar_days') 153 | .query({ 154 | currency: "JPY", 155 | key: "d306zoyjsyarp7ifhu67rjxn52tv0t20", 156 | start_date: '2017-11-01', 157 | listing_id: 1234, 158 | _format: 'host_calendar', 159 | end_date: '2017-12-01' 160 | }) 161 | .reply(200, {calendar_days: []}) 162 | it('should return a array of calendar days if arguments are correct', async () => { 163 | expect(await testFunc({token: 'mockcorrecttoken', id:1234, startDate:'2017-11-01', endDate:'2017-12-01'})).to.be.an('array') 164 | }) 165 | }) 166 | 167 | describe('#setPriceForDay({token, id, date, price, currency})', () => { 168 | const testFunc = abba.setPriceForDay.bind(abba) 169 | nockauth() 170 | .put('/v2/calendars/1234/2017-11-01', { 171 | daily_price: 123, 172 | demand_based_pricing_overridden: true, 173 | availability: 'available' 174 | }) 175 | .query(true) 176 | .reply(200, {response: 'success'}) 177 | it('should return result object for correct arguments', async () => { 178 | expect(await testFunc({token:'mockcorrecttoken', id:1234, date:'2017-11-01', price:123, currency:'USD'})).to.be.an('object') 179 | }) 180 | }) 181 | 182 | describe('#setAvailabilityForDay({token, id, date, availability}))', () => { 183 | const testFunc = abba.setAvailabilityForDay.bind(abba) 184 | nockauth() 185 | .put('/v2/calendars/1234/2017-11-01', { 186 | availability: 'available' 187 | }) 188 | .query(true) 189 | .reply(200, {response: 'success'}) 190 | it('should return result object for correct arguments', async () => { 191 | expect(await testFunc({token:'mockcorrecttoken', id:1234, date:'2017-11-01', availability:'available'})).to.be.an('object') 192 | }) 193 | }) 194 | 195 | describe('#setHouseManual({token, id, manual})', () => { 196 | const testFunc = abba.setHouseManual.bind(abba) 197 | it('should return null if no arguments are passed or arguments are missing', async () => { 198 | expect(await testFunc()).to.be.null 199 | expect(await testFunc({id:1234, manual:'manual'})).to.be.null 200 | expect(await testFunc({token: 'mocktoken', manual:'manual'})).to.be.null 201 | expect(await testFunc({token: 'mocktoken', id:1234})).to.be.null 202 | }) 203 | nockauth() 204 | .post('/v1/listings/1234/update', { 205 | listing: {house_manual: 'manual'} 206 | }) 207 | .query(true) 208 | .reply(200, {response:'ok'}) 209 | it('should return response object', async () => { 210 | expect(await testFunc({token: 'mockcorrecttoken', id:1234, manual:'manual'})).to.be.an('object') 211 | }) 212 | }) 213 | 214 | describe('#getListingInfo(id)', () => { 215 | const testFunc = abba.getListingInfo.bind(abba) 216 | it('should return null if no arguments are passed or arguments are missing', async () => { 217 | expect(await testFunc()).to.be.null 218 | // expect(await testFunc({not_id:1234})).to.be.null 219 | }) 220 | nock(apiBaseUrl) 221 | .get('/v1/listings/1234') 222 | .query(true) 223 | .reply(200, {listing:{}}) 224 | 225 | it('should return a response object if arguments are correct', async () => { 226 | expect(await testFunc(1234)).to.have.property('listing') 227 | }) 228 | }) 229 | 230 | describe('#getListingInfoHost({token, id})', () => { 231 | const testFunc = abba.getListingInfoHost.bind(abba) 232 | it('should return null if no arguments are passed or arguments are missing', async () => { 233 | expect(await testFunc()).to.be.null 234 | expect(await testFunc({id: 1234})).to.be.null 235 | expect(await testFunc({token: 'mockcorrecttoken'})).to.be.null 236 | }) 237 | nockauth() 238 | .get('/v1/listings/1234') 239 | .query(true) 240 | .reply(200, {listing:{}}) 241 | 242 | it('should return a response object if arguments are correct', async () => { 243 | expect(await testFunc({token: 'mockcorrecttoken', id: 1234})).to.have.property('listing') 244 | }) 245 | }) 246 | 247 | describe('#getHostSummary(token)', () => { 248 | const testFunc = abba.getHostSummary.bind(abba) 249 | it('should return null if no arguments are passed or arguments are missing', async () => { 250 | expect(await testFunc()).to.be.null 251 | }) 252 | nockauth() 253 | .get('/v1/account/host_summary') 254 | .query(true) 255 | .reply(200, {active_listings:[{id: 6789}]}) 256 | 257 | it('should return a response object if arguments are correct', async () => { 258 | expect(await testFunc('mockcorrecttoken')).to.have.property('active_listings') 259 | }) 260 | }) 261 | 262 | describe('#getOwnActiveListings(token)', () => { 263 | console.log(JSON.stringify(dummyData.getOwnActiveListings, null, 4)) 264 | const testFunc = abba.getOwnActiveListings.bind(abba) 265 | it('should return null if no arguments are passed or arguments are missing', async () => { 266 | expect(await testFunc()).to.be.null 267 | }) 268 | nockauth() 269 | .get('/v1/account/host_summary') 270 | .query(true) 271 | .reply(200, {active_listings: dummyData.getOwnActiveListings}) 272 | 273 | it('should return a response object if arguments are correct', async () => { 274 | expect(await testFunc('mockcorrecttoken')).to.be.an('array') 275 | }) 276 | }) 277 | 278 | describe('#getThread({token, id, currency})', () => { 279 | const testFunc = abba.getThread.bind(abba) 280 | it('should return null if no arguments are passed or arguments are missing', async () => { 281 | expect(await testFunc()).to.be.null 282 | expect(await testFunc({token:'mockcorrecttoken'})).to.be.null 283 | expect(await testFunc({id:1234})).to.be.null 284 | }) 285 | nockauth() 286 | .get('/v1/threads/1234') 287 | .query(true) 288 | .reply(200, {thread:{id: 1234}}) 289 | 290 | it('should return a thread object if arguments are correct', async () => { 291 | expect(await testFunc({token: 'mockcorrecttoken', id: 1234})).to.have.property('id') 292 | }) 293 | }) 294 | 295 | describe('#getThreadsBatch({token, ids, currency})', () => { 296 | const testFunc = abba.getThreadsBatch.bind(abba) 297 | it('should return null if no arguments are passed or arguments are missing', async () => { 298 | expect(await testFunc()).to.be.null 299 | }) 300 | it('should return null if token or ids is not passed', async () => { 301 | expect(await testFunc({id:1234})).to.be.null 302 | expect(await testFunc({token: 'mocktoken'})).to.be.null 303 | }) 304 | nockauth() 305 | .post('/v2/batch', { 306 | operations: [ 307 | { 308 | method: 'GET', 309 | path: '/threads/987', 310 | query: { 311 | _format: 'for_messaging_sync_with_posts' 312 | } 313 | }, 314 | { 315 | method: 'GET', 316 | path: '/threads/876', 317 | query: { 318 | _format: 'for_messaging_sync_with_posts' 319 | } 320 | }, 321 | ], 322 | _transaction: false 323 | }) 324 | .query(true) 325 | .reply(200, {operations: [{id: 987}, {id: 876}]}) 326 | it('should return type array if arguments are correct', async () => { 327 | expect(await testFunc({token: 'mockcorrecttoken', ids:[987,876]})).to.be.an('array') 328 | }) 329 | }) 330 | 331 | describe('#getThreadsFull({token, offset, limit})', () => { 332 | const testFunc = abba.getThreadsFull.bind(abba) 333 | it('should return null if no arguments are passed or arguments are missing', async () => { 334 | expect(await testFunc()).to.be.null 335 | expect(await testFunc({offset:2})).to.be.null 336 | expect(await testFunc({offset:2, limit:0})).to.be.null 337 | }) 338 | nockauth() 339 | .get('/v2/threads') 340 | .query(q => q._format === 'for_messaging_sync_with_posts') 341 | .reply(200, {threads:[{id: 1234},{id: 2345},{id: 3456},{id: 5687},{id: 6789}]}) 342 | 343 | it('should return a list(array) of threads if arguments are correct', async () => { 344 | expect(await testFunc({token: 'mockcorrecttoken', offset:0, limit:10})).to.be.an('array') 345 | }) 346 | }) 347 | 348 | describe('#getThreads({token, offset, limit})', () => { 349 | const testFunc = abba.getThreads.bind(abba) 350 | it('should return null if no arguments are passed or arguments are missing', async () => { 351 | expect(await testFunc()).to.be.null 352 | expect(await testFunc({offset:2})).to.be.null 353 | expect(await testFunc({offset:2, limit:0})).to.be.null 354 | }) 355 | nockauth() 356 | .get('/v2/threads') 357 | .query(true) 358 | .reply(200, {threads:[{id: 1234},{id: 2345},{id: 3456},{id: 5687},{id: 6789}]}) 359 | 360 | it('should return a list(array) of threads if arguments are correct', async () => { 361 | expect(await testFunc({token: 'mockcorrecttoken', offset:0, limit:10})).to.be.an('array') 362 | }) 363 | }) 364 | 365 | describe('#createThread({token, id, checkin, checkout, guestNum, message})', () => { 366 | const testFunc = abba.createThread.bind(abba) 367 | it('should return null if no arguments are passed or arguments are missing', async () => { 368 | expect(await testFunc()).to.be.null 369 | expect(await testFunc({id:1234, checkIn:'2017-01-01', checkOut:'2017-01-02', message:'asd'})).to.be.null 370 | expect(await testFunc({token: 'mocktoken', checkIn:'2017-01-01', checkOut:'2017-01-02', message:'asd'})).to.be.null 371 | expect(await testFunc({token: 'mocktoken', id:1234, checkOut:'2017-01-02', message:'asd'})).to.be.null 372 | expect(await testFunc({token: 'mocktoken', id:1234, checkIn:'2017-01-01', message:'asd'})).to.be.null 373 | expect(await testFunc({token: 'mocktoken', id:1234, checkIn:'2017-01-01', checkOut:'2017-01-02'})).to.be.null 374 | }) 375 | nockauth() 376 | .post('/v1/threads/create', { 377 | listing_id:1234, 378 | number_of_guests:1, 379 | message:'hello!', 380 | checkin_date:'2017-01-01', 381 | checkout_date:'2017-01-02', 382 | }) 383 | .query(true) 384 | .reply(200, {response:'ok'}) 385 | it('should return response object', async () => { 386 | expect(await testFunc({token: 'mockcorrecttoken', id:1234, checkIn:'2017-01-01', checkOut:'2017-01-02', message:'hello!'})).to.be.an('object') 387 | }) 388 | }) 389 | 390 | describe('getReservations({token, offset, limit}', () => { 391 | const testFunc = abba.getReservations.bind(abba) 392 | it('should return null if no arguments are passed or arguments are missing', async () => { 393 | expect(await testFunc()).to.be.null 394 | expect(await testFunc({offset:2})).to.be.null 395 | expect(await testFunc({offset:2, limit:0})).to.be.null 396 | }) 397 | nockauth() 398 | .get('/v2/reservations') 399 | .query(q => q._format === 'for_mobile_host') 400 | .reply(200, {reservations:[{id: 1234},{id: 2345},{id: 3456},{id: 5687},{id: 6789}]}) 401 | 402 | it('should return a list(array) of threads if arguments are correct', async () => { 403 | expect(await testFunc({token: 'mockcorrecttoken', offset:0, limit:10})).to.be.an('array') 404 | }) 405 | }) 406 | 407 | describe('#getReservationsBatch({ token, ids, currency})', () => { 408 | const testFunc = abba.getThreadsBatch.bind(abba) 409 | it('should return null if no arguments are passed or arguments are missing', async () => { 410 | expect(await testFunc()).to.be.null 411 | }) 412 | it('should return null if token or ids is not passed', async () => { 413 | expect(await testFunc({id:1234})).to.be.null 414 | expect(await testFunc({token: 'mocktoken'})).to.be.null 415 | }) 416 | nockauth() 417 | .post('/v2/batch', { 418 | operations: [ 419 | { 420 | method: 'GET', 421 | path: '/threads/987', 422 | query: { 423 | _format: 'for_messaging_sync_with_posts' 424 | } 425 | }, 426 | { 427 | method: 'GET', 428 | path: '/threads/876', 429 | query: { 430 | _format: 'for_messaging_sync_with_posts' 431 | } 432 | }, 433 | ], 434 | _transaction: false 435 | }) 436 | .query(true) 437 | .reply(200, {operations: [{id: 987}, {id: 876}]}) 438 | it('should return type array if arguments are correct', async () => { 439 | expect(await testFunc({token: 'mockcorrecttoken', ids:[987,876]})).to.be.an('array') 440 | }) 441 | }) 442 | 443 | describe('#getReservation({token, id, currency})', () => { 444 | const testFunc = abba.getReservation.bind(abba) 445 | it('should return null if no arguments are passed or arguments are missing', async () => { 446 | expect(await testFunc()).to.be.null 447 | expect(await testFunc({token:'mockcorrecttoken'})).to.be.null 448 | expect(await testFunc({id:1234})).to.be.null 449 | }) 450 | nockauth() 451 | .get('/v2/reservations/1234') 452 | .query(true) 453 | .reply(200, {reservation:{id: 1234}}) 454 | 455 | it('should return a reservation object if arguments are correct', async () => { 456 | expect(await testFunc({token: 'mockcorrecttoken', id: 1234})).to.have.property('id') 457 | }) 458 | }) 459 | 460 | describe('#sendMessage({ token, id, message })', () => { 461 | const testFunc = abba.sendMessage.bind(abba) 462 | it('should return null if no arguments are passed or arguments are missing', async () => { 463 | expect(await testFunc()).to.be.null 464 | expect(await testFunc({id:1234, message:'hello'})).to.be.null 465 | expect(await testFunc({token: 'mocktoken', message:'hello'})).to.be.null 466 | expect(await testFunc({token: 'mocktoken', id:1234})).to.be.null 467 | }) 468 | nockauth() 469 | .post('/v2/messages', { 470 | thread_id:1234, 471 | message:'hello!' 472 | }) 473 | .query(true) 474 | .reply(200, {response:'ok'}) 475 | it('should return response object', async () => { 476 | expect(await testFunc({token: 'mockcorrecttoken', id:1234, message:'hello!'})).to.be.an('object') 477 | }) 478 | }) 479 | 480 | describe('#sendPreApproval({token, thread_id, listing_id, message})', () => { 481 | const testFunc = abba.sendPreApproval.bind(abba) 482 | it('should return null if no arguments are passed or arguments are missing', async () => { 483 | expect(await testFunc()).to.be.null 484 | expect(await testFunc({thread_id:987, listing_id:1234})).to.be.null 485 | expect(await testFunc({token: 'mocktoken', listing_id:1234})).to.be.null 486 | expect(await testFunc({token: 'mocktoken', thread_id:987})).to.be.null 487 | }) 488 | nockauth() 489 | .post('/v1/threads/987/update', { 490 | listing_id:1234, 491 | message:'', 492 | status:'preapproved' 493 | }) 494 | .query(true) 495 | .reply(200, {response:'ok'}) 496 | it('should return response object', async () => { 497 | expect(await testFunc({token: 'mockcorrecttoken', listing_id:1234, thread_id:987})).to.be.an('object') 498 | }) 499 | }) 500 | 501 | describe('#sendReview({token, id, comments, private_feedback, cleanliness, communication, respect_house_rules, recommend})', () => { 502 | const testFunc = abba.sendReview.bind(abba) 503 | it('should return null if no arguments are passed or arguments are missing', async () => { 504 | expect(await testFunc()).to.be.null 505 | expect(await testFunc({id:456})).to.be.null 506 | expect(await testFunc({token: 'mocktoken'})).to.be.null 507 | }) 508 | nockauth() 509 | .post('/v1/reviews/456/update', { 510 | comments: 'They were great guests!', 511 | private_feedback: 'Thank you for staying!', 512 | cleanliness: 5, 513 | communication: 5, 514 | respect_house_rules: 5, 515 | recommend: true 516 | }) 517 | .query(true) 518 | .reply(200, {response:'ok'}) 519 | it('should return response object', async () => { 520 | expect(await testFunc({token: 'mockcorrecttoken', id:456})).to.be.an('object') 521 | }) 522 | }) 523 | 524 | describe('#sendSpecialOffer({token, startDate, guests, listingId, nights, price, threadId, currency})', () => { 525 | const testFunc = abba.sendSpecialOffer.bind(abba) 526 | it('should return null if no arguments are passed or arguments are missing', async () => { 527 | expect(await testFunc()).to.be.null 528 | // expect(await testFunc({token:'mocktoken', startDate: '2017-01-01', guests:1, listingId:1234, nights:2, price:10000, threadId: 987})).to.be.null 529 | expect(await testFunc({startDate: '2017-01-01', guests:1, listingId:1234, nights:2, price:10000, threadId: 987})).to.be.null 530 | expect(await testFunc({token:'mocktoken', guests:1, listingId:1234, nights:2, price:10000, threadId: 987})).to.be.null 531 | expect(await testFunc({token:'mocktoken', startDate: '2017-01-01', listingId:1234, nights:2, price:10000, threadId: 987})).to.be.null 532 | expect(await testFunc({token:'mocktoken', startDate: '2017-01-01', guests:1, nights:2, price:10000, threadId: 987})).to.be.null 533 | expect(await testFunc({token:'mocktoken', startDate: '2017-01-01', guests:1, listingId:1234, price:10000, threadId: 987})).to.be.null 534 | expect(await testFunc({token:'mocktoken', startDate: '2017-01-01', guests:1, listingId:1234, nights:2, threadId: 987})).to.be.null 535 | expect(await testFunc({token:'mocktoken', startDate: '2017-01-01', guests:1, listingId:1234, nights:2, price:10000})).to.be.null 536 | }) 537 | nockauth() 538 | .post('/v2/special_offers', { 539 | check_in: '2017-01-01', 540 | guests: 1, 541 | listing_id: 1234, 542 | nights: 2, 543 | price: 10000, 544 | thread_id: 987 545 | }) 546 | .query(true) 547 | .reply(200, {response:'ok'}) 548 | it('should return response object', async () => { 549 | expect(await testFunc({token:'mockcorrecttoken', startDate: '2017-01-01', guests:1, listingId:1234, nights:2, price:10000, threadId: 987})).to.be.an('object') 550 | }) 551 | }) 552 | 553 | describe('#getGuestInfo(id)', () => { 554 | const testFunc = abba.getGuestInfo.bind(abba) 555 | it('should return null if no arguments are passed or arguments are missing', async () => { 556 | expect(await testFunc()).to.be.null 557 | }) 558 | nock(apiBaseUrl) 559 | .get('/v2/users/1234') 560 | .query(true) 561 | .reply(200, {user:{id:1234}}) 562 | 563 | it('should return a response object if arguments are correct', async () => { 564 | expect(await testFunc(1234)).to.have.property('id') 565 | }) 566 | }) 567 | 568 | describe('#getOwnUserInfo(token)', () => { 569 | const testFunc = abba.getOwnUserInfo.bind(abba) 570 | it('should return null if no arguments are passed or arguments are missing', async () => { 571 | expect(await testFunc()).to.be.null 572 | expect(await testFunc('wrongtoken')).to.be.null 573 | }) 574 | nockauth() 575 | .get('/v2/users/me') 576 | .query(true) 577 | .reply(200, {user:{id:1234}}) 578 | 579 | it('should return a user info object if arguments are correct', async () => { 580 | expect(await testFunc('mockcorrecttoken')).to.have.property('id') 581 | }) 582 | }) 583 | 584 | describe('#listingSearch({location, offset, limit, language, currency})', () => { 585 | const testFunc = abba.listingSearch.bind(abba) 586 | 587 | it('should return a list of listings', async () => { 588 | nock(apiBaseUrl) 589 | .get('/v2/search_results') 590 | .twice() 591 | .query(true) 592 | .reply(200, {search_results:[{id:123},{id:456},{id:789}], metadata:{foo:'bar'}}) 593 | expect(await testFunc()).to.have.property('search_results') 594 | expect(await testFunc({location: 'New York, United States', offset: 0, limit: 50, language: 'en', currency: 'USD'})).to.have.property('search_results') 595 | }) 596 | }) 597 | 598 | describe('#mGetOwnActiveListings(token)', () => { 599 | const testFunc = abba.mGetOwnActiveListingsFull.bind(abba) 600 | it('should return null if no arguments are passed or arguments are missing', async () => { 601 | expect(await testFunc()).to.be.null 602 | }) 603 | nockauth() 604 | .get('/v1/account/host_summary') 605 | .query(true) 606 | .reply(200, {active_listings: dummyData.getOwnActiveListings}) 607 | 608 | dummyData.getOwnActiveListings.forEach(listing => { 609 | nockauth() 610 | .get(`/v1/listings/${listing.listing.listing.id}`) 611 | .query(true) 612 | .reply(200, {listing:{}}) 613 | }) 614 | 615 | it('should return a response object if arguments are correct', async () => { 616 | expect(await testFunc('mockcorrecttoken')).to.be.an('array') 617 | }) 618 | }) 619 | }) 620 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import requestPromise from 'request-promise' 2 | import log from './log.js' 3 | import config from './config.js' 4 | import metapoints from './metapoints.js' 5 | 6 | class AirApi { 7 | constructor() { 8 | Object.assign(this, metapoints) 9 | this.config = Object.assign({}, config) 10 | } 11 | 12 | // buildOptions() is a factory function to build HTML request data. 13 | // It's used to set config data and common defaults that can be overwritten. 14 | // It makes endpoints less verbose. 15 | buildOptions({ 16 | token, 17 | method = 'GET', 18 | route = '/v2/batch', 19 | uri, 20 | json = true, 21 | headers, 22 | currency = this.config.currency, 23 | format, 24 | qs, 25 | body, 26 | timeout, 27 | proxy 28 | }) { 29 | const out = { 30 | method, 31 | uri: uri || this.config.domain + route, 32 | json, 33 | headers: { 34 | ...this.config.default_headers, 35 | 'X-Airbnb-OAuth-Token': token === 'public' ? '' : this.config.token || token, 36 | ...headers 37 | }, 38 | qs: { 39 | key: this.config.api_key, 40 | currency, 41 | _format: format, 42 | ...qs 43 | }, 44 | body, 45 | timeout, 46 | proxy: this.config.proxy 47 | } 48 | return out 49 | } 50 | 51 | //////////// CONFIG SECTION //////////// 52 | //////////// CONFIG SECTION //////////// 53 | //////////// CONFIG SECTION //////////// 54 | 55 | setConfig({ defaultToken, apiKey, currency, userAgent, proxy }) { 56 | defaultToken && (this.config.token = defaultToken) 57 | apiKey && (this.config.api_key = apiKey) 58 | currency && (this.config.currency = currency) 59 | userAgent && (this.config.default_headers['User-Agent'] = userAgent) 60 | proxy && (this.config.proxy = proxy) 61 | } 62 | 63 | setDefaultToken(token) { 64 | if (token) { 65 | this.config.token = token 66 | } else { 67 | this.config.token = undefined 68 | } 69 | } 70 | 71 | setApiKey(key) { 72 | this.config.api_key = key 73 | } 74 | 75 | setCurrency(currencyString) { 76 | this.config.currency = currencyString 77 | } 78 | 79 | setUserAgent(userAgentString) { 80 | this.config.default_headers['User-Agent'] = userAgentString 81 | } 82 | 83 | setProxy(proxyURL) { 84 | this.config.proxy = proxyURL 85 | } 86 | 87 | //////////// AUTH SECTION //////////// 88 | //////////// AUTH SECTION //////////// 89 | //////////// AUTH SECTION //////////// 90 | 91 | // Ping server to see if the token is good. 92 | async testAuth(token) { 93 | if (!(token || this.config.token)) { 94 | log.i('Airbnbapi: No token included for testAuth() call') 95 | return null 96 | } else { 97 | const options = this.buildOptions({ 98 | method: 'POST', 99 | token, 100 | body: { operations: [] } 101 | }) 102 | let response = await requestPromise(options).catch(e => {}) 103 | return response ? true : false 104 | } 105 | } 106 | 107 | // Grab a new auth token using a 'username and password' login method. 108 | async newAccessToken({ username, password } = {}) { 109 | if (!username) { 110 | log.e("Airbnbapi: Can't apply for a token without a username.") 111 | return null 112 | } else if (!password) { 113 | log.e("Airbnbapi: Can't apply for a token without a password.") 114 | return null 115 | } 116 | const options = this.buildOptions({ 117 | token: 'public', 118 | method: 'POST', 119 | route: '/v1/authorize', 120 | body: { 121 | grant_type: 'password', 122 | username, 123 | password 124 | } 125 | }) 126 | 127 | try { 128 | const response = await requestPromise(options) 129 | if (response && response.access_token) { 130 | log.i( 131 | `Airbnbapi: Successful login for [${username}], auth ID is [${ 132 | response.access_token 133 | }]` 134 | ) 135 | return { token: response.access_token } 136 | } else { 137 | log.e('Airbnbapi: no response from server when fetching token') 138 | return null 139 | } 140 | } catch (e) { 141 | // if(e.response.access_token) { 142 | // log.i('Airbnbapi: Successful login for [ ' + username + ' ], auth ID is [ ' + e.response.access_token + ' ]') 143 | // return { token: e.response.access_token } 144 | // } 145 | // log.i(JSON.stringify(e, null, 4)) 146 | log.e("Airbnbapi: Couldn't get auth token for " + username) 147 | log.e(e.error) 148 | return { error: e.error } 149 | } 150 | } 151 | 152 | async login({ email, password } = {}) { 153 | if (!email) { 154 | log.e("Airbnbapi: Can't login without an email.") 155 | return null 156 | } else if (!password) { 157 | log.e("Airbnbapi: Can't apply for a token without a password.") 158 | return null 159 | } 160 | const options = this.buildOptions({ 161 | token: 'public', 162 | method: 'POST', 163 | route: '/v2/logins', 164 | body: { 165 | email, 166 | password 167 | } 168 | }) 169 | try { 170 | const response = await requestPromise(options) 171 | if (response && response.login) { 172 | log.i( 173 | `Airbnbapi: Successful login for [${email}], auth ID is [${response.login.id}]` 174 | ) 175 | return response 176 | } else { 177 | log.e('Airbnbapi: no response from server when fetching token') 178 | return null 179 | } 180 | } catch (e) { 181 | log.e("Airbnbapi: Couldn't get auth token for " + email) 182 | log.e(e.error) 183 | return { error: e.error } 184 | } 185 | } 186 | 187 | //////////// CALENDAR SECTION //////////// 188 | //////////// CALENDAR SECTION //////////// 189 | //////////// CALENDAR SECTION //////////// 190 | 191 | async getPublicListingCalendar({ id, month = '1', year = '2018', count = '1' } = {}) { 192 | if (!id) { 193 | log.e("Airbnbapi: Can't get public listing calendar without an id") 194 | return null 195 | } 196 | const options = this.buildOptions({ 197 | token: 'public', 198 | route: '/v2/calendar_months', 199 | format: 'with_conditions', 200 | qs: { 201 | listing_id: id, 202 | month, 203 | year, 204 | count 205 | } 206 | }) 207 | try { 208 | const response = await requestPromise(options) 209 | return response 210 | } catch (e) { 211 | log.e("Airbnbapi: Couldn't get public calendar for listing " + id) 212 | log.e(e) 213 | } 214 | } 215 | 216 | async getCalendar({ token, id, startDate, endDate } = {}) { 217 | //log.i(colors.magenta('Airbnbapi: Requesting calendar for [ ' + id + ' ] --')) 218 | if (!(token || this.config.token)) { 219 | log.e("Airbnbapi: Can't get a calendar without a token") 220 | return null 221 | } else if (!id) { 222 | log.e("Airbnbapi: Can't get a calendar without an id") 223 | return null 224 | } else if (!startDate) { 225 | log.e("Airbnbapi: Can't get a calendar without a start date") 226 | return null 227 | } else if (!endDate) { 228 | log.e("Airbnbapi: Can't get a calendar without a end date") 229 | return null 230 | } 231 | 232 | const options = this.buildOptions({ 233 | method: 'GET', 234 | route: '/v2/calendar_days', 235 | token, 236 | qs: { 237 | start_date: startDate, 238 | listing_id: id, 239 | _format: 'host_calendar', 240 | end_date: endDate 241 | }, 242 | timeout: 10000 243 | }) 244 | try { 245 | const response = await requestPromise(options).catch(console.log) 246 | return response.calendar_days 247 | } catch (e) { 248 | log.e("Airbnbapi: Couldn't get calendar for listing " + id) 249 | log.e(e) 250 | } 251 | } 252 | 253 | async setPriceForDay({ token, id, date, price, currency = this.config.currency }) { 254 | // console.log(JSON.stringify(this, null, 4)) 255 | const options = this.buildOptions({ 256 | method: 'PUT', 257 | uri: `https://api.airbnb.com/v2/calendars/${id}/${date}`, 258 | token, 259 | currency, 260 | format: 'host_calendar', 261 | body: { 262 | daily_price: price, 263 | demand_based_pricing_overridden: true, 264 | availability: 'available' 265 | }, 266 | timeout: 10000 267 | }) 268 | try { 269 | const response = await requestPromise(options) 270 | return response 271 | } catch (e) { 272 | log.e("Airbnbapi: Couldn't set price for cal day for listing " + id) 273 | log.e(e) 274 | } 275 | } 276 | async setAvailabilityForDay({ token, id, date, availability }) { 277 | const options = this.buildOptions({ 278 | method: 'PUT', 279 | uri: `https://api.airbnb.com/v2/calendars/${id}/${date}`, 280 | token, 281 | format: 'host_calendar', 282 | body: { 283 | availability: availability 284 | }, 285 | timeout: 10000 286 | }) 287 | try { 288 | const response = await requestPromise(options) 289 | return response 290 | } catch (e) { 291 | log.e("Airbnbapi: Couldn't set availability for cal day for listing " + id) 292 | log.e(e) 293 | } 294 | } 295 | 296 | //////////// LISTING SECTION //////////// 297 | //////////// LISTING SECTION //////////// 298 | //////////// LISTING SECTION //////////// 299 | 300 | async setHouseManual({ token, id, manual } = {}) { 301 | if (!(token || this.config.token)) { 302 | log.e("Airbnbapi: Can't set a house manual without a token") 303 | return null 304 | } else if (!id) { 305 | log.e("Airbnbapi: Can't set a house manual without an id") 306 | return null 307 | } else if (!manual) { 308 | log.e("Airbnbapi: Can't set a house manual without manual text") 309 | return null 310 | } 311 | const options = this.buildOptions({ 312 | method: 'POST', 313 | route: `/v1/listings/${id}/update`, 314 | token, 315 | body: { 316 | listing: { house_manual: manual } 317 | } 318 | }) 319 | try { 320 | const response = await requestPromise(options) 321 | return response 322 | } catch (e) { 323 | log.e("Airbnbapi: Couldn't set house manual for listing " + id) 324 | log.e(e) 325 | } 326 | } 327 | 328 | async getListingInfo(id) { 329 | if (!id) { 330 | log.e("Airbnbapi: Can't get public listing information without an id") 331 | return null 332 | } 333 | const options = this.buildOptions({ 334 | token: 'public', 335 | route: `/v1/listings/${id}` 336 | }) 337 | try { 338 | const response = await requestPromise(options) 339 | return response 340 | } catch (e) { 341 | log.e("Airbnbapi: Couldn't get info for listing " + id) 342 | log.e(e) 343 | } 344 | } 345 | 346 | async getListingInfoHost({ token, id } = {}) { 347 | if (!(token || this.config.token)) { 348 | log.e("Airbnbapi: Can't get a listing without a token") 349 | return null 350 | } else if (!id) { 351 | log.e("Airbnbapi: Can't get a listing without an id") 352 | return null 353 | } 354 | const options = this.buildOptions({ 355 | route: `/v1/listings/${id}`, 356 | token, 357 | format: 'v1_legacy_long_manage_listing' 358 | }) 359 | try { 360 | const response = await requestPromise(options) 361 | return response 362 | } catch (e) { 363 | log.e("Airbnbapi: Couldn't get listing info for id " + id) 364 | log.e(e) 365 | } 366 | } 367 | 368 | async getHostSummary(token) { 369 | if (!(token || this.config.token)) { 370 | log.e("Airbnbapi: Can't get a summary without a token") 371 | return null 372 | } 373 | const options = this.buildOptions({ 374 | route: `/v1/account/host_summary`, 375 | token 376 | }) 377 | try { 378 | const response = await requestPromise(options) 379 | return response 380 | } catch (e) { 381 | log.e("Airbnbapi: Couldn't get a host summary for token " + token) 382 | log.e(e) 383 | } 384 | } 385 | 386 | async getOwnActiveListings(token) { 387 | if (!(token || this.config.token)) { 388 | log.e("Airbnbapi: Can't get an active listing list without a token") 389 | return null 390 | } 391 | const options = this.buildOptions({ 392 | route: `/v1/account/host_summary`, 393 | token 394 | }) 395 | try { 396 | const response = await requestPromise(options) 397 | if (response.active_listings) { 398 | return response.active_listings.map(listing => listing.listing.listing) 399 | } else { 400 | return [] 401 | } 402 | } catch (e) { 403 | log.e("Airbnbapi: Couldn't get an active listing list for token " + token) 404 | log.e(e) 405 | } 406 | } 407 | async getOwnListings({ token, userId }) { 408 | if (!(token || this.config.token)) { 409 | log.e("Airbnbapi: Can't get an listing list without a token") 410 | return null 411 | } 412 | const options = this.buildOptions({ 413 | route: `/v2/listings`, 414 | format: `v1_legacy_long`, 415 | qs: { 416 | user_id: userId 417 | }, 418 | token 419 | }) 420 | try { 421 | const response = await requestPromise(options) 422 | if (response) { 423 | return response.listings 424 | } else { 425 | return [] 426 | } 427 | } catch (e) { 428 | log.e("Airbnbapi: Couldn't get an listing list for token " + token) 429 | log.e(e) 430 | } 431 | } 432 | 433 | //////////// THREADS SECTION //////////// 434 | //////////// THREADS SECTION //////////// 435 | //////////// THREADS SECTION //////////// 436 | 437 | // Gets all the data for one thread 438 | async getThread({ token, id, currency = this.config.currency } = {}) { 439 | if (!(token || this.config.token)) { 440 | log.e("Airbnbapi: Can't get a thread without a token") 441 | return null 442 | } else if (!id) { 443 | log.e("Airbnbapi: Can't get a thread without an id") 444 | return null 445 | } 446 | const options = this.buildOptions({ 447 | route: '/v1/threads/' + id, 448 | token, 449 | qs: { currency } 450 | }) 451 | try { 452 | const response = await requestPromise(options) 453 | return response.thread 454 | } catch (e) { 455 | log.e("Airbnbapi: Couldn't get thread " + id) 456 | log.e(e) 457 | } 458 | } 459 | 460 | async getThreadsBatch({ token, ids, currency = this.config.currency } = {}) { 461 | //log.i(colors.magenta('Airbnbapi: Requesting calendar for [ ' + id + ' ] --')) 462 | if (!(token || this.config.token)) { 463 | log.e("Airbnbapi: Can't get threads without a token") 464 | return null 465 | } else if (!ids) { 466 | log.e("Airbnbapi: Can't get threads without at least one id") 467 | return null 468 | } 469 | 470 | const operations = ids.map(id => ({ 471 | method: 'GET', 472 | path: `/threads/${id}`, 473 | query: { _format: 'for_messaging_sync_with_posts' } 474 | })) 475 | 476 | const options = this.buildOptions({ 477 | method: 'POST', 478 | token, 479 | currency, 480 | body: { 481 | operations, 482 | _transaction: false 483 | }, 484 | timeout: 30000 485 | }) 486 | // log.i(JSON.stringify(options, null, 4)) 487 | 488 | let response = {} 489 | 490 | try { 491 | response = await requestPromise(options).catch(log.e) 492 | return response.operations.map(o => o.response) 493 | // log.i(JSON.stringify(response, null, 4)) 494 | } catch (e) { 495 | log.e("Airbnbapi: Couldn't get threads for threads " + ids) 496 | log.e(e) 497 | } 498 | } 499 | 500 | // Gets a list of thread id's for a host 501 | async getThreadsFull({ token, offset = '0', limit = '2' } = {}) { 502 | if (!(token || this.config.token)) { 503 | log.e("Airbnbapi: Can't get a thread list without a token") 504 | return null 505 | } 506 | const options = this.buildOptions({ 507 | route: '/v2/threads', 508 | token, 509 | format: 'for_messaging_sync_with_posts', 510 | qs: { _offset: offset, _limit: limit } 511 | }) 512 | try { 513 | const response = await requestPromise(options) 514 | if (response.threads) { 515 | return response.threads //.map(item => item.id) 516 | } else return null 517 | } catch (e) { 518 | log.e("Airbnbapi: Couldn't get thread list for token " + token) 519 | log.e(e) 520 | } 521 | } 522 | 523 | // Gets a list of thread id's for a host 524 | async getThreadFull({ token, id } = {}) { 525 | if (!(token || this.config.token)) { 526 | log.e("Airbnbapi: Can't get a thread without a token") 527 | return null 528 | } 529 | if (!id) { 530 | log.e("Airbnbapi: Can't get a thread without an id") 531 | return null 532 | } 533 | 534 | const options = this.buildOptions({ 535 | route: `/v2/threads/${id}`, 536 | token, 537 | format: 'for_messaging_sync_with_posts' 538 | }) 539 | try { 540 | const response = await requestPromise(options) 541 | if (response) { 542 | return response 543 | } else return null 544 | } catch (e) { 545 | log.e("Airbnbapi: Couldn't get thread for id " + id) 546 | log.e(e) 547 | } 548 | } 549 | 550 | // Gets a list of thread id's for a host 551 | async getThreads({ token, offset = '0', limit = '2' } = {}) { 552 | if (!(token || this.config.token)) { 553 | log.e("Airbnbapi: Can't get a thread list without a token") 554 | return null 555 | } 556 | const options = this.buildOptions({ 557 | route: '/v2/threads', 558 | token, 559 | qs: { _offset: offset, _limit: limit } 560 | }) 561 | try { 562 | const response = await requestPromise(options) 563 | if (response.threads) { 564 | return response.threads.map(item => item.id) 565 | } else return null 566 | } catch (e) { 567 | log.e("Airbnbapi: Couldn't get thread list for token " + token) 568 | log.e(e) 569 | } 570 | } 571 | 572 | // Create a new thread 573 | async createThread({ token, id, checkIn, checkOut, guestNum = 1, message } = {}) { 574 | if (!(token || this.config.token)) { 575 | log.e("Airbnbapi: Can't create a thread without a token") 576 | return null 577 | } else if (!id) { 578 | log.e("Airbnbapi: Can't create a thread without an id") 579 | return null 580 | } else if (!checkIn) { 581 | log.e("Airbnbapi: Can't create a thread without a checkin") 582 | return null 583 | } else if (!checkOut) { 584 | log.e("Airbnbapi: Can't create a thread without a checkout") 585 | return null 586 | } else if (!message || message.trim() === '') { 587 | log.e("Airbnbapi: Can't create a thread without a message body") 588 | return null 589 | } 590 | const options = this.buildOptions({ 591 | method: 'POST', 592 | route: '/v1/threads/create', 593 | token, 594 | body: { 595 | listing_id: id, 596 | number_of_guests: guestNum, 597 | message: message.trim(), 598 | checkin_date: checkIn, 599 | checkout_date: checkOut 600 | } 601 | }) 602 | try { 603 | const response = await requestPromise(options) 604 | return response 605 | } catch (e) { 606 | log.e("Airbnbapi: Couldn't send create thread for listing " + id) 607 | log.e(e) 608 | } 609 | } 610 | 611 | //////////// RESERVATIONS SECTION //////////// 612 | //////////// RESERVATIONS SECTION //////////// 613 | //////////// RESERVATIONS SECTION //////////// 614 | 615 | async getReservations({ token, offset = '0', limit = '20' } = {}) { 616 | if (!(token || this.config.token)) { 617 | log.e("Airbnbapi: Can't get a reservation list without a token") 618 | return null 619 | } 620 | const options = this.buildOptions({ 621 | route: '/v2/reservations', 622 | token, 623 | format: 'for_mobile_host', 624 | qs: { _offset: offset, _limit: limit } 625 | }) 626 | try { 627 | const response = await requestPromise(options) 628 | return response.reservations 629 | } catch (e) { 630 | log.e("Airbnbapi: Couldn't get reservation list for token " + token) 631 | log.e(e) 632 | } 633 | } 634 | 635 | async getReservationsBatch({ token, ids, currency = this.config.currency } = {}) { 636 | // TODO change to reservation 637 | //log.i(colors.magenta('Airbnbapi: Requesting calendar for [ ' + id + ' ] --')) 638 | if (!(token || this.config.token)) { 639 | log.e("Airbnbapi: Can't get reservations without a token") 640 | return null 641 | } else if (!ids || !Array.isArray(ids)) { 642 | log.e("Airbnbapi: Can't get reservations without at least one id") 643 | return null 644 | } 645 | const operations = ids.map(id => ({ 646 | method: 'GET', 647 | path: `/reservations/${id}`, 648 | query: { 649 | _format: 'for_mobile_host' 650 | } 651 | })) 652 | 653 | const options = this.buildOptions({ 654 | method: 'POST', 655 | token, 656 | currency, 657 | body: { 658 | operations, 659 | _transaction: false 660 | }, 661 | timeout: 30000 662 | }) 663 | // log.i(JSON.stringify(options, null, 4)) 664 | try { 665 | const response = await requestPromise(options).catch(console.error) 666 | return response.operations.map(o => o.response) 667 | // log.i(JSON.stringify(response, null, 4)) 668 | } catch (e) { 669 | log.e("Airbnbapi: Couldn't get reservations for ids " + ids) 670 | log.e(e) 671 | } 672 | } 673 | 674 | async getReservation({ token, id, currency } = {}) { 675 | if (!(token || this.config.token)) { 676 | log.e("Airbnbapi: Can't get a reservation without a token") 677 | return null 678 | } else if (!id) { 679 | log.e("Airbnbapi: Can't get a reservation without an id") 680 | return null 681 | } 682 | const options = this.buildOptions({ 683 | route: `/v2/reservations/${id}`, 684 | token, 685 | format: 'for_mobile_host', 686 | currency 687 | }) 688 | try { 689 | const response = await requestPromise(options) 690 | return response.reservation 691 | } catch (e) { 692 | log.e("Airbnbapi: Couldn't get reservation for token " + token) 693 | log.e(e) 694 | } 695 | } 696 | 697 | // Send a message to a thread (guest) 698 | async sendMessage({ token, id, message } = {}) { 699 | if (!(token || this.config.token)) { 700 | log.e("Airbnbapi: Can't send a message without a token") 701 | return null 702 | } else if (!id) { 703 | log.e("Airbnbapi: Can't send a message without an id") 704 | return null 705 | } else if (!message || message.trim() === '') { 706 | log.e("Airbnbapi: Can't send a message without a message body") 707 | return null 708 | } 709 | log.i('Airbnbapi: send message for thread: ' + id + ' --') 710 | log.i("'" + message.substring(70) + "'") 711 | const options = this.buildOptions({ 712 | method: 'POST', 713 | route: '/v2/messages', 714 | token, 715 | body: { thread_id: id, message: message.trim() } 716 | }) 717 | try { 718 | const response = await requestPromise(options) 719 | return response 720 | } catch (e) { 721 | log.e("Airbnbapi: Couldn't send message for thread " + id) 722 | log.e(e) 723 | } 724 | } 725 | 726 | // Send pre-approval to an inquiry 727 | // requires a id. id, and optional message 728 | async sendPreApproval({ token, thread_id, listing_id, message = '' } = {}) { 729 | if (!(token || this.config.token)) { 730 | log.e("Airbnbapi: Can't send pre-approval without a token") 731 | return null 732 | } else if (!thread_id) { 733 | log.e("Airbnbapi: Can't send pre-approval without a thread_id") 734 | return null 735 | } else if (!listing_id) { 736 | log.e("Airbnbapi: Can't send pre-approval without a listing_id") 737 | return null 738 | } 739 | const options = this.buildOptions({ 740 | method: 'POST', 741 | route: `/v1/threads/${thread_id}/update`, 742 | token, 743 | body: { 744 | listing_id, 745 | message, 746 | status: 'preapproved' 747 | } 748 | }) 749 | try { 750 | const response = await requestPromise(options) 751 | return response 752 | } catch (e) { 753 | log.e("Airbnbapi: Couldn't send preapproval for thread " + thread_id) 754 | log.e(e) 755 | } 756 | } 757 | 758 | async sendReview({ 759 | token, 760 | id, 761 | comments = 'They were great guests!', 762 | private_feedback = 'Thank you for staying!', 763 | cleanliness = 5, 764 | communication = 5, 765 | respect_house_rules = 5, 766 | recommend = true 767 | } = {}) { 768 | if (!(token || this.config.token)) { 769 | log.e("Airbnbapi: Can't send a review without a token") 770 | return null 771 | } else if (!id) { 772 | log.e("Airbnbapi: Can't send review without an id") 773 | return null 774 | } 775 | const options = this.buildOptions({ 776 | method: 'POST', 777 | route: `/v1/reviews/${id}/update`, 778 | token, 779 | body: { 780 | comments, 781 | private_feedback, 782 | cleanliness, 783 | communication, 784 | respect_house_rules, 785 | recommend 786 | } 787 | }) 788 | try { 789 | const response = await requestPromise(options) 790 | return response 791 | } catch (e) { 792 | log.e("Airbnbapi: Couldn't send a review for thread " + thread_id) 793 | log.e(e) 794 | } 795 | } 796 | 797 | async sendSpecialOffer({ 798 | token, 799 | startDate, //ISO 800 | guests, 801 | listingId, 802 | nights, 803 | price, 804 | threadId, 805 | currency = this.config.currency 806 | } = {}) { 807 | if (!(token || this.config.token)) { 808 | log.e("Airbnbapi: Can't send a special offer without a token") 809 | return null 810 | } else if (!startDate) { 811 | log.e("Airbnbapi: Can't send a special offer without a startDate") 812 | return null 813 | } else if (!guests) { 814 | log.e("Airbnbapi: Can't send a special offer without guests") 815 | return null 816 | } else if (!listingId) { 817 | log.e("Airbnbapi: Can't send a special offer without a listingId") 818 | return null 819 | } else if (!nights) { 820 | log.e("Airbnbapi: Can't send a special offer without nights (staying)") 821 | return null 822 | } else if (!price) { 823 | log.e("Airbnbapi: Can't send a special offer without a price") 824 | return null 825 | } else if (!threadId) { 826 | log.e("Airbnbapi: Can't send a special offer without a threadId") 827 | return null 828 | } 829 | 830 | const options = this.buildOptions({ 831 | method: 'POST', 832 | route: '/v2/special_offers', 833 | token, 834 | currency, 835 | body: { 836 | check_in: startDate, //ISO 837 | guests, 838 | listing_id: listingId, 839 | nights, 840 | price, 841 | thread_id: threadId 842 | } 843 | }) 844 | try { 845 | const response = await requestPromise(options) 846 | return response 847 | } catch (e) { 848 | log.e("Airbnbapi: Couldn't send a review for thread " + thread_id) 849 | log.e(e) 850 | } 851 | } 852 | 853 | async alterationRequestResponse({ 854 | token, 855 | reservationId, 856 | alterationId, 857 | decision, 858 | currency = this.config.currency 859 | }) { 860 | if (!(token || this.config.token)) { 861 | log.e("Airbnbapi: Can't send an alteration request response without a token") 862 | return null 863 | } else if (!reservationId) { 864 | log.e("Airbnbapi: Can't send an alteration request response without a reservationId") 865 | return null 866 | } else if (!alterationId) { 867 | log.e("Airbnbapi: Can't send an alteration request response without an alterationId") 868 | return null 869 | } else if (!decision) { 870 | log.e("Airbnbapi: Can't send an alteration request response without a decision") 871 | return null 872 | } 873 | const options = this.buildOptions({ 874 | method: 'PUT', 875 | uri: `https://api.airbnb.com/v2/reservation_alterations/${alterationId}`, 876 | token, 877 | currency, 878 | format: 'for_mobile_alterations_v3_host', 879 | qs: { reservation_id: reservationId }, 880 | body: { 881 | reservation_id: reservationId, 882 | alteration_id: alterationId, 883 | status: decision ? 1 : 2 884 | }, 885 | timeout: 10000 886 | }) 887 | try { 888 | const response = await requestPromise(options) 889 | return response 890 | } catch (e) { 891 | log.e( 892 | "Airbnbapi: Can't send an alteration request response fro reservation " + 893 | reservationId 894 | ) 895 | log.e(e) 896 | } 897 | } 898 | 899 | async getGuestInfo(id) { 900 | if (!id) { 901 | log.e("Airbnbapi: Can't get guest info without an id") 902 | return null 903 | } 904 | const options = this.buildOptions({ token: 'public', route: `/v2/users/${id}` }) 905 | try { 906 | const response = await requestPromise(options) 907 | return response && response.user ? response.user : undefined 908 | } catch (e) { 909 | log.e("Airbnbapi: Couldn't get guest info with user id " + id) 910 | log.e(e) 911 | } 912 | } 913 | 914 | async getOwnUserInfo(token) { 915 | if (!(token || this.config.token)) { 916 | log.e("Airbnbapi: Can't get user info without a token") 917 | return null 918 | } 919 | const options = this.buildOptions({ route: '/v2/users/me', token }) 920 | try { 921 | const response = await requestPromise(options) 922 | return response && response.user ? response.user : undefined 923 | } catch (e) { 924 | log.e("Airbnbapi: Couldn't get own info with token" + token) 925 | log.e(e) 926 | return null 927 | } 928 | } 929 | 930 | async listingSearch({ 931 | location = 'New York, United States', 932 | checkin, 933 | checkout, 934 | offset = 0, 935 | limit = 20, 936 | language = 'en-US', 937 | currency = this.config.currency, 938 | guests, 939 | instantBook, 940 | minBathrooms, 941 | minBedrooms, 942 | minBeds, 943 | minPrice, 944 | maxPrice, 945 | superhost, 946 | amenities, 947 | hostLanguages, 948 | keywords, 949 | roomTypes, 950 | neighborhoods, 951 | minPicCount, 952 | sortDirection 953 | } = {}) { 954 | const options = this.buildOptions({ 955 | token: 'public', 956 | route: '/v2/search_results', 957 | currency, 958 | qs: { 959 | locale: language, 960 | location, 961 | checkin, 962 | checkout, 963 | _offset: offset, 964 | _limit: limit, 965 | guests, 966 | ib: instantBook, 967 | min_bathrooms: minBathrooms, 968 | min_bedrooms: minBedrooms, 969 | min_beds: minBeds, 970 | price_min: minPrice, 971 | price_max: maxPrice, 972 | superhost, 973 | hosting_amenities: amenities, 974 | languages: hostLanguages, 975 | keywords: keywords, 976 | room_types: roomTypes, 977 | neighborhoods: neighborhoods, 978 | min_num_pic_urls: minPicCount, 979 | sort: sortDirection 980 | } 981 | }) 982 | try { 983 | const response = await requestPromise(options) 984 | return response 985 | } catch (e) { 986 | log.e("Airbnbapi: Couldn't get listings for search of " + location) 987 | log.e(e) 988 | } 989 | } 990 | 991 | async newAccount({ 992 | username, 993 | password, 994 | authenticity_token, 995 | firstname, 996 | lastname, 997 | bdayDay = 1, 998 | bdayMonth = 1, 999 | bdayYear = 1980 1000 | } = {}) { 1001 | if (!username) { 1002 | log.e("Airbnbapi: Can't make a new account without a username") 1003 | return null 1004 | } else if (!password) { 1005 | log.e("Airbnbapi: Can't make a new account without a password") 1006 | return null 1007 | } else if (!authenticity_token) { 1008 | log.e("Airbnbapi: Can't make a new account without an authenticity_token") 1009 | return null 1010 | } else if (!authenticity_token) { 1011 | log.e("Airbnbapi: Can't make a new account without a firstname") 1012 | return null 1013 | } else if (!authenticity_token) { 1014 | log.e("Airbnbapi: Can't make a new account without a lastname") 1015 | return null 1016 | } 1017 | const options = this.buildOptions({ 1018 | method: 'POST', 1019 | uri: 'https://www.airbnb.com/create/', 1020 | form: { 1021 | authenticity_token, 1022 | from: 'email_signup', 1023 | 'user[email]': username, 1024 | 'user[first_name]': firstname, 1025 | 'user[last_name]': lastname, 1026 | 'user[password]': password, 1027 | 'user[birthday_day]': bdayDay, 1028 | 'user[birthday_month]': bdayMonth, 1029 | 'user[birthday_year]': bdayYear, 1030 | 'user_profile_info[receive_promotional_email]': 0 1031 | } 1032 | }) 1033 | try { 1034 | const response = await requestPromise(options) 1035 | return response 1036 | } catch (e) { 1037 | log.e("Airbnbapi: Couldn't make new account for username " + username) 1038 | log.e(e) 1039 | } 1040 | } 1041 | } 1042 | 1043 | const abba = new AirApi() 1044 | module.exports = abba 1045 | --------------------------------------------------------------------------------