first commit

This commit is contained in:
s.golasch
2023-08-01 12:47:58 +02:00
commit b1439a55bb
65 changed files with 3085 additions and 0 deletions

35
lib/cli.js Normal file
View File

@@ -0,0 +1,35 @@
'use strict'
var meow = require('meow')
var help = '' +
'Authenticates against the Amazon Echo Web App API.\n' +
'Spins up a webserver (and optionally a telnet server) to access your Echo devices for remote control.\n\n' +
'Usage:\n' +
' $ alexis [options]\n\n' +
'Example:\n' +
' $ alexis --http-port=5000 --email=my@mail.com --pass=MyHopefullySuperSecretAmazonPasswort\n\n' +
'Options:\n' +
' --email Your Echo account e-mail\n' +
' --pass Your Echo account password\n' +
' --http-port [optional] Port the HTTP Server should be running on\n' +
' --telnet-port [optional] Port the Telnet Server should be running on\n' +
' --interval [optional] The fetch interval for querying the Echo API (default 10 sec.)\n' +
' --no-colors [optional] Disables all colors in the terminal outoput\n' +
' --quiet [optional] Disables all terminal output\n'
var args = {
string: [
'http-port',
'telnet-port',
'email',
'pass',
'interval',
'no-colors',
'quiet',
]
}
module.exports = function (runner) {
var cli = meow(help, args)
runner(cli.flags, cli.showHelp)
}

View File

@@ -0,0 +1,21 @@
'use strict'
const all = (store, params, cb) => {
let state = store.getState()
cb({
todos: state.todos,
//shopping_list: state.shopping_list.items,
//timeline: state.timeline.items,
//network: state.network.network,
//devices: state.devices.devices.devices,
//household: state.household.household,
//notifications: state.notifications.notifications.notifications,
//user_data: state.user_data.userData.authentication,
//wakewords: state.wakewords.wakewords,
})
}
module.exports = {
'/': all,
'/all': all,
}

View File

@@ -0,0 +1,5 @@
'use strict'
module.exports = {
RECEIVE_DEVICES: 'RECEIVE_DEVICES',
}

View File

@@ -0,0 +1,12 @@
'use strict'
const {RECEIVE_DEVICES} = require('./action_constants')
module.exports = {
receivedDevices: devices => {
return {
type: RECEIVE_DEVICES,
devices,
}
}
}

View File

@@ -0,0 +1,20 @@
'use strict'
const {receivedDevices} = require('./actions')
module.exports = {
// urls that should be called for the remote actions
urls: {
fetch: 'https://layla.amazon.de/api/devices/device',
},
// called after an remote http call to the device API has been successfully executed
postHooks: {
// called after the devices data has been fetched
fetch: (store, data) => {
return new Promise((resolve, reject) => {
store.dispatch(receivedDevices(data))
resolve(data)
})
}
}
}

View File

@@ -0,0 +1,13 @@
'use strict'
const {RECEIVE_DEVICES} = require('./action_constants')
module.exports = (state = {}, action) => {
switch (action.type) {
case RECEIVE_DEVICES:
return Object.assign({}, state, {
devices: action.devices
})
default:
return state
}
}

View File

@@ -0,0 +1,7 @@
'use strict'
module.exports = {
'/devices': (store, params, cb) => {
cb(store.getState().devices.devices.devices)
}
}

View File

@@ -0,0 +1,5 @@
'use strict'
module.exports = {
RECEIVE_HOUSEHOLD: 'RECEIVE_HOUSEHOLD',
}

View File

@@ -0,0 +1,12 @@
'use strict'
const {RECEIVE_HOUSEHOLD} = require('./action_constants')
module.exports = {
receivedHousehold: household => {
return {
type: RECEIVE_HOUSEHOLD,
household,
}
}
}

View File

@@ -0,0 +1,20 @@
'use strict'
const {receivedHousehold} = require('./actions')
module.exports = {
// urls that should be called for the remote actions
urls: {
fetch: 'https://layla.amazon.de/api/household',
},
// called after an remote http call to the household API has been successfully executed
postHooks: {
// called after the household items have been fetched
fetch: (store, data) => {
return new Promise((resolve, reject) => {
store.dispatch(receivedHousehold(data))
resolve(data)
})
}
}
}

View File

@@ -0,0 +1,13 @@
'use strict'
const {RECEIVE_HOUSEHOLD} = require('./action_constants')
module.exports = (state = {}, action) => {
switch (action.type) {
case RECEIVE_HOUSEHOLD:
return Object.assign({}, state, {
household: action.household
})
default:
return state
}
}

View File

@@ -0,0 +1,7 @@
'use strict'
module.exports = {
'/household': (store, params, cb) => {
res.send(store.getState().household.household)
}
}

View File

@@ -0,0 +1,5 @@
'use strict'
module.exports = {
RECEIVE_NETWORK: 'RECEIVE_NETWORK',
}

View File

@@ -0,0 +1,12 @@
'use strict'
const {RECEIVE_NETWORK} = require('./action_constants')
module.exports = {
receivedNetwork: network => {
return {
type: RECEIVE_NETWORK,
network,
}
}
}

View File

@@ -0,0 +1,21 @@
'use strict'
const {receivedNetwork} = require('./actions')
module.exports = {
// urls that should be called for the remote actions
urls: {
fetch: 'https://layla.amazon.de/api/phoenix',
discover: 'https://layla.amazon.de/api/phoenix/discovery',
},
// called after an remote http call to the network API has been successfully executed
postHooks: {
// called after the network data has been fetched
fetch: (store, data) => {
return new Promise((resolve, reject) => {
store.dispatch(receivedNetwork(JSON.parse(data.networkDetail)))
resolve(data)
})
}
}
}

View File

@@ -0,0 +1,13 @@
'use strict'
const {RECEIVE_NETWORK} = require('./action_constants')
module.exports = (state = {}, action) => {
switch (action.type) {
case RECEIVE_NETWORK:
return Object.assign({}, state, {
network: action.network
})
default:
return state
}
}

View File

@@ -0,0 +1,7 @@
'use strict'
module.exports = {
'/network': (store, params, cb) => {
res.send(store.getState().network.network)
}
}

View File

@@ -0,0 +1,5 @@
'use strict'
module.exports = {
RECEIVE_NOTIFICATIONS: 'RECEIVE_NOTIFICATIONS',
}

View File

@@ -0,0 +1,12 @@
'use strict'
const {RECEIVE_NOTIFICATIONS} = require('./action_constants')
module.exports = {
receivedNotifications: notifications => {
return {
type: RECEIVE_NOTIFICATIONS,
notifications,
}
}
}

View File

@@ -0,0 +1,20 @@
'use strict'
const {receivedNotifications} = require('./actions')
module.exports = {
// urls that should be called for the remote actions
urls: {
fetch: 'https://layla.amazon.de/api/notifications',
},
// called after an remote http call to the notifications API has been successfully executed
postHooks: {
// called after the notifications have been fetched
fetch:(store, data) => {
return new Promise((resolve, reject) => {
store.dispatch(receivedNotifications(data))
resolve(data)
})
}
}
}

View File

@@ -0,0 +1,13 @@
'use strict'
const {RECEIVE_NOTIFICATIONS} = require('./action_constants')
module.exports = (state = {}, action) => {
switch (action.type) {
case RECEIVE_NOTIFICATIONS:
return Object.assign({}, state, {
notifications: action.notifications
})
default:
return state
}
}

View File

@@ -0,0 +1,7 @@
'use strict'
module.exports = {
'/notifications': (store, params, cb) => {
cb(store.getState().notifications.notifications.notifications)
}
}

View File

@@ -0,0 +1,5 @@
'use strict'
module.exports = {
RECEIVE_SHOPPING_LIST: 'RECEIVE_SHOPPING_LIST',
}

View File

@@ -0,0 +1,12 @@
'use strict'
const {RECEIVE_SHOPPING_LIST} = require('./action_constants')
module.exports = {
receivedShoppingList: todos => {
return {
type: RECEIVE_SHOPPING_LIST,
items: todos,
}
}
}

View File

@@ -0,0 +1,20 @@
'use strict'
const {receivedShoppingList} = require('./actions')
module.exports = {
// urls that should be called for the remote actions
urls: {
fetch: 'https://layla.amazon.de/api/todos?type=SHOPPING_ITEM&size=100',
},
// called after an remote http call to the shopping_list API has been successfully executed
postHooks: {
// called after the shopping items have been fetched
fetch:(store, data) => {
return new Promise((resolve, reject) => {
store.dispatch(receivedShoppingList(data.values))
resolve(data)
})
}
}
}

View File

@@ -0,0 +1,13 @@
'use strict'
const {RECEIVE_SHOPPING_LIST} = require('./action_constants')
module.exports = (state = {}, action) => {
switch (action.type) {
case RECEIVE_SHOPPING_LIST:
return Object.assign({}, state, {
items: action.items
})
default:
return state
}
}

View File

@@ -0,0 +1,7 @@
'use strict'
module.exports = {
'/shopping_list': (store, params, cb) => {
cb(store.getState().shopping_list.items)
}
}

View File

@@ -0,0 +1,5 @@
'use strict'
module.exports = {
RECEIVE_TIMELINE: 'RECEIVE_TIMELINE',
}

View File

@@ -0,0 +1,12 @@
'use strict'
const {RECEIVE_TIMELINE} = require('./action_constants')
module.exports = {
receivedTimeline: timeline => {
return {
type: RECEIVE_TIMELINE,
items: timeline,
}
}
}

View File

@@ -0,0 +1,19 @@
'use strict'
const {receivedTimeline} = require('./actions')
module.exports = {
// urls that should be called for the remote actions
urls: {
fetch: 'https://layla.amazon.de/api/cards?limit=50',
},
postHooks: {
// called after the timeline has been fetched
fetch: (store, data) => {
return new Promise((resolve, reject) => {
store.dispatch(receivedTimeline(data.cards))
resolve(data)
})
},
}
}

View File

@@ -0,0 +1,13 @@
'use strict'
const {RECEIVE_TIMELINE} = require('./action_constants')
module.exports = (state = {}, action) => {
switch (action.type) {
case RECEIVE_TIMELINE:
return Object.assign({}, state, {
items: action.items
})
default:
return state
}
}

View File

@@ -0,0 +1,7 @@
'use strict'
module.exports = {
'/timeline': (store, params, cb) => {
cb(store.getState().timeline.items)
}
}

View File

@@ -0,0 +1,19 @@
'use strict'
const RECEIVE_TODOS = 'RECEIVE_TODOS'
const FETCH_TODOS = 'FETCH_TODOS'
const ADD_TODO = 'ADD_TODO'
const ADDED_TODO = 'ADDED_TODO'
const UPDATE_TODO = 'UPDATE_TODO'
const DELETE_TODO = 'DELETE_TODO'
const COMPLETE_TODO = 'COMPLETE_TODO'
module.exports = {
RECEIVE_TODOS,
FETCH_TODOS,
ADD_TODO,
ADDED_TODO,
UPDATE_TODO,
DELETE_TODO,
COMPLETE_TODO,
}

View File

@@ -0,0 +1,61 @@
'use strict'
const {FETCH_TODOS, RECEIVED_TODOS, ADD_TODO, ADDED_TODO, UPDATE_TODO, DELETE_TODO, COMPLETE_TODO} = require('./action_constants')
const fetchTodos = todos => {
return {
type: FETCH_TODOS,
items: todos,
}
}
const receivedTodos = todos => {
return {
type: RECEIVED_TODOS,
items: todos,
}
}
const addTodo = todo => {
return {
type: ADD_TODO,
item: todo,
}
}
const addedTodo = todo => {
return {
type: ADDED_TODO,
item: todo,
}
}
const updateTodo = todo => {
return {
type: UPDATE_TODO,
item: todo,
}
}
const deleteTodo = todo => {
return {
type: DELETE_TODO,
item: todo,
}
}
const completeTodo = todo => {
return {
type: COMPLETE_TODO,
item: todo,
}
}
module.exports = {
receivedTodos,
addTodo,
addedTodo,
updateTodo,
deleteTodo,
completeTodo,
}

View File

@@ -0,0 +1,61 @@
'use strict'
const {receivedTodos, addTodo, addedTodo} = require('./actions')
module.exports = {
// urls that should be called for the remote actions
urls: {
fetch: 'https://layla.amazon.de/api/todos?type=TASK&size=100',
add: 'https://layla.amazon.de/api/todos',
complete: 'https://layla.amazon.de/api/todos/{ID}',
delete: 'https://layla.amazon.de/api/todos/{ID}',
update: 'https://layla.amazon.de/api/todos/{ID}',
},
// called before an remote http call to the todos API gets executed
preHooks: {
fetch: async options => [{method: 'GET', url: options.url}],
// adds a new TODO
add: async options => {
let todoTemplate = {
text: options.text,
type: 'TASK',
itemId: null,
lastLocalUpdatedDate: null,
lastUpdatedDate: null,
createdDate: Date.now(),
utteranceId: null,
nbestItems: null,
complete: false,
version: null,
deleted: false,
reminderTime: null
}
// dispatch data about the item to be added
store.dispatch(addTodo(todoTemplate))
// build request options
// TODO: Add error handling
return [{method: 'POST', url: options.url, body: JSON.stringify(todoTemplate)}]
}
},
// called after an remote http call to the todos API has been successfully executed
postHooks: {
// called after the todo list has been fetched
fetch: async (store, raw_data) => {
let data = []
try {
data = JSON.parse(raw_data)
store.dispatch(receivedTodos(data.values))
} catch (error) {
// TODO: Add error handling
}
return data
},
// called after the new todo has been created
add: async (store, raw_data) => {
// TODO: Add error handling
const data = JSON.parse(raw_data)
store.dispatch(addedTodo(data))
return data
}
}
}

View File

@@ -0,0 +1,42 @@
'use strict'
const {RECEIVE_TODOS, RECEIVED_TODOS, ADD_TODO, ADDED_TODO} = require('./action_constants')
const INITIAL_STATE = {toBeAdded: [], items: [], size: 0, lastUpdated: null, fetching: false, error: null}
// create an array of todos that are still queued, but not yet added
const removeToBeAdded = (current, itemToBeRemoved) => {
if (current.length === 1) return []
return current.map(item => {
if (item.text !== itemToBeRemoved.text) return item
})
}
module.exports = (state = INITIAL_STATE, action) => {
switch (action.type) {
// received all todos
case RECEIVED_TODOS:
return Object.assign({}, state, {
items: action.items,
size: action.items.length,
lastUpdated: Date.now(),
})
// trying to add a todo (request to amazon not yet made)
case ADD_TODO:
return Object.assign({}, state, {
toBeAdded: state.toBeAdded.push(action.item),
lastUpdated: Date.now(),
})
// todo has been added successfully
case ADDED_TODO:
// remove the item from the toBeadded array
const toBeAdded = removeToBeAdded(state.toBeAdded, action.item)
state.items.push(action.item)
return Object.assign({}, state, {
items: state.items,
size: state.size + 1,
toBeAdded: toBeAdded,
lastUpdated: Date.now(),
})
default:
return state
}
}

View File

@@ -0,0 +1,31 @@
'use strict'
const {receivedTodos, addTodo} = require('./actions')
module.exports = {
// returns all todos
'/todos': (store, params, cb) => cb(store.getState().todos),
// adds a todo & returns all todos
'/todos/add': (store, params, cb, Proxy) => Proxy.act('todo', 'add', params, result => {
console.log('result', result)
cb(result)
}),
// deletes a todo & returns all todos
'/todos/delete': (store, params, cb, Proxy) => Proxy.act('todo', 'delete', params, async result => {
const data = await Proxy.fetch('todo')
store.dispatch(receivedTodos(data.todo.values))
cb(data.todo.values)
}),
// completes a todo & returns all todos
'/todos/complete': (store, params, cb, Proxy) => Proxy.act('todo', 'complete', params, async result => {
const data = await Proxy.fetch('todo')
store.dispatch(receivedTodos(data.todo.values))
cb(data.todo.values)
}),
// updates a todo & returns all todos
'/todos/update': (store, params, cb, Proxy) => Proxy.act('todo', 'update', params, async result => {
const data = await Proxy.fetch('todo')
store.dispatch(receivedTodos(data.todo.values))
cb(data.todo.values)
}),
}

View File

@@ -0,0 +1,5 @@
'use strict'
module.exports = {
RECEIVE_USER_DATA: 'RECEIVE_USER_DATA',
}

View File

@@ -0,0 +1,12 @@
'use strict'
const {RECEIVE_USER_DATA} = require('./action_constants')
module.exports = {
receivedUserData: userData => {
return {
type: RECEIVE_USER_DATA,
userData,
}
}
}

View File

@@ -0,0 +1,20 @@
'use strict'
const {receivedUserData} = require('./actions')
module.exports = {
// urls that should be called for the remote actions
urls: {
fetch: 'https://layla.amazon.de/api/bootstrap?version=1.24.2445.0',
},
// called after an remote http call to the user data API has been successfully executed
postHooks: {
// called after the user data has been fetched
fetch: (store, data) => {
return new Promise((resolve, reject) => {
store.dispatch(receivedUserData(data))
resolve(data)
})
}
}
}

View File

@@ -0,0 +1,13 @@
'use strict'
const {RECEIVE_USER_DATA} = require('./action_constants')
module.exports = (state = {}, action) => {
switch (action.type) {
case RECEIVE_USER_DATA:
return Object.assign({}, state, {
userData: action.userData
})
default:
return state
}
}

View File

@@ -0,0 +1,7 @@
'use strict'
module.exports = {
'/user_data': (store, params, cb) => {
cb(store.getState().user_data.userData)
}
}

View File

@@ -0,0 +1,5 @@
'use strict'
module.exports = {
RECEIVE_WAKEWORDS: 'RECEIVE_WAKEWORDS',
}

View File

@@ -0,0 +1,12 @@
'use strict'
const {RECEIVE_WAKEWORDS} = require('./action_constants')
module.exports = {
receivedWakewords: wakewords => {
return {
type: RECEIVE_WAKEWORDS,
wakewords,
}
}
}

View File

@@ -0,0 +1,20 @@
'use strict'
const {receivedWakewords} = require('./actions')
module.exports = {
// urls that should be called for the remote actions
urls: {
fetch: 'https://layla.amazon.de/api/wake-word',
},
// called after an remote http call to the user data API has been successfully executed
postHooks: {
// called after the user data has been fetched
fetch: (store, data) => {
return new Promise((resolve, reject) => {
store.dispatch(receivedWakewords(data.wakeWords))
resolve(data)
})
}
}
}

View File

@@ -0,0 +1,13 @@
'use strict'
const {RECEIVE_WAKEWORDS} = require('./action_constants')
module.exports = (state = {}, action) => {
switch (action.type) {
case RECEIVE_WAKEWORDS:
return Object.assign({}, state, {
wakewords: action.wakewords
})
default:
return state
}
}

View File

@@ -0,0 +1,7 @@
'use strict'
module.exports = {
'/wakewords': (store, params, cb) => {
cb(store.getState().wakewords.wakewords)
}
}

69
lib/cookies.js Normal file
View File

@@ -0,0 +1,69 @@
'use strict'
const fs = require('fs')
const path = require('path')
const request =require('request')
const Headers = require('./headers')
// files & endpoints
const RAW_COOKIE_FILE = 'raw_jar.txt'
const JSON_COOKIE_FILE = 'cookies.json'
const TEST_ENDPOINT = 'https://layla.amazon.de/api/devices/device'
// Returns the full raw cookies path
const getRawCookiePath = data_dir => path.join(data_dir, RAW_COOKIE_FILE)
// Checks if the JSON cookie file exists
const getJsonCookiePath = data_dir => path.join(data_dir, JSON_COOKIE_FILE)
// Checks if the raw cookie file exists
const checkRawCookiesExist = data_dir => fs.existsSync(getRawCookiePath(data_dir))
// Checks if the JSON cookie file exists
const checkJsonCookiesExist = data_dir => fs.existsSync(getJsonCookiePath(data_dir))
// Checks if the raw cookie file exists
const deleteRawCookies = data_dir => fs.removeSync(getRawCookiePath(data_dir))
// Checks if the JSON cookie file exists
const deleteJsonCookies = data_dir => fs.removeSync(getJsonCookiePath(data_dir))
// Returns the cookies as a header compatible string
const transformCookiesForHeader = json_cookies => {
let string_cookies = ''
json_cookies.forEach(cookie => {
Object.keys(cookie).forEach(key => {
if (key == 'name') return
string_cookies += key == 'value' ? cookie.name + '=' + cookie.value : key + '=' + cookie[key]
string_cookies += ';'
})
})
return string_cookies
}
// Validate the cookie
const validateCookie = (data_dir) => {
const json_cookies = require(path.join(data_dir, JSON_COOKIE_FILE))
const string_cookies = transformCookiesForHeader(json_cookies)
const headers = Headers.getFetchHeaders(string_cookies)
return new Promise((resolve, reject) => {
request.get({headers, url: TEST_ENDPOINT}, (error, response, body) => {
try {
let contents = JSON.parse(body)
if (contents.devices) {
resolve([true, contents])
} else {
reject([false, null])
}
} catch (e) {
reject([false, body])
}
})
})
}
module.exports = {
getRawCookiePath,
getJsonCookiePath,
checkRawCookiesExist,
checkJsonCookiesExist,
deleteRawCookies,
deleteJsonCookies,
validateCookie,
transformCookiesForHeader,
RAW_COOKIE_FILE,
JSON_COOKIE_FILE,
}

28
lib/headers.js Normal file
View File

@@ -0,0 +1,28 @@
'use strict'
const ua = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/603.2.4 (KHTML, like Gecko) Version/10.1.1 Safari/603.2.4'
const base_headers = {
'Pragma': 'no-cache',
'Accept-Encoding': 'identity',
'Accept-Language': 'de',
'User-Agent': ua,
'Accept': 'application/json, text/javascript, */*; q=0.01',
'Cache-Control': 'no-cache',
'X-Requested-With': 'XMLHttpRequest',
'Connection': 'keep-alive',
'Referer': 'https://layla.amazon.de/spa/index.html',
}
const getActionHeaders = (json_cookies, cookies) => {
var csrf = '252916842'
return Object.assign({}, getFetchHeaders(cookies + 'csrf=' + csrf + ';'), {csrf})
}
const getFetchHeaders = (cookies) => Object.assign({}, base_headers, {'Cookie': cookies})
module.exports = {
ua,
getActionHeaders,
getFetchHeaders,
}

218
lib/init.js Normal file
View File

@@ -0,0 +1,218 @@
'use strict'
const os = require('os')
const fs = require('fs')
const path = require('path')
const homedir = require('homedir')
const Store = require('./store')
const phantomloader = require('./phantomloader')
const webserver = require('./webserver')
const Cookies = require('./cookies')
const Headers = require('./headers')
const Login = require('./login')
const proxy = require('./proxy')
// file & directory names
const DATA_DIR = '.alexis'
const PHANTOMFILE = os.platform() === 'win32' ? 'phantomjs.exe' : 'phantomjs'
// computes the data directory
const getDataDir = () => path.join(homedir(), DATA_DIR)
// checks if the data directory exists
const checkDataDirExists = () => fs.existsSync(getDataDir())
// creates the data directory
const createDataDir = () => fs.mkdirSync(getDataDir())
// checks if the PhantomJS binary exists
const checkPhantomExist = () => fs.existsSync(path.join(getDataDir(), PHANTOMFILE))
// checks if the given options are valid
const validateOptions = (options, verbose) => {
let missing = [];
['httpPort', 'email', 'pass'].forEach(option => {
if (!options[option]) missing.push(option)
})
if (verbose && missing.length > 0) return missing.join(', ')
if (missing.length > 0) return false
return true
}
// validate options
const optionsAreValid = (options = {}, invalidOptionsCb) => {
if (!options.interval) options.interval = 10
options.interval = options.interval * 1000
if(!validateOptions(options)) {
invalidOptionsCb()
return false
}
return true
}
// checks if data dir exists and creates it if not
const doCreateUserdataDirectory = (data_dir, logger) => {
if (!checkDataDirExists()) {
logger.log('SETUP', `Creating data directory: ${data_dir}`)
createDataDir()
}
}
// checks if Phantom is installed, downloads it if not
const mountPhantom = async (data_dir, logger) => {
if (!checkPhantomExist()) {
phantom_mounted = false
const phantom_path_name = path.join(data_dir, PHANTOMFILE)
logger.log('SETUP', 'Downloading Phantom')
return await Phantom.download(phantom_path_name, data_dir)
}
return true
}
// Initializing flow
const run = async (logger, options, invalidOptionsCb) => {
// helper variable to determine if cookies exist
let cookies_exist = false
// helper variable to determine if cookies are valid
let cookies_valid = false
// helper variable to determine if login did work
let login_success = false
// init store
let store = null
// device data store
let device_data = null
// init PhantomJS downlaoder
const Phantom = phantomloader(logger)
// cache data dir location
const data_dir = getDataDir()
// validate options
if (!optionsAreValid(options, invalidOptionsCb)) return false
// set up redux store
logger.log('SETUP', 'Creating in-memory store')
store = Store()
// check if data dir exists, else create
doCreateUserdataDirectory(data_dir, logger)
// checks if Phantom is installed, downloads it if not
const phantom_mounted = await mountPhantom(data_dir, logger)
// check if phantom is mounted
if (phantom_mounted !== true) {
logger.log('ERROR', 'Error with mounting Phantom')
return false
}
// everythings okay
logger.log('SETUP', 'Dependency check successful')
// cookie check
if (Cookies.checkJsonCookiesExist(data_dir) && Cookies.checkJsonCookiesExist(data_dir)) {
logger.log('SETUP', 'Cookies exist, trying to reuse them')
// validate existing cookies
try {
cookies_valid = await Cookies.validateCookie(data_dir)
device_data = cookies_valid[1]
cookies_valid = cookies_valid[0]
} catch (e) {
cookies_valid = false
}
// set flasg & log cookie state
if (cookies_valid) {
cookies_exist = true
login_success = true
logger.log('SETUP', 'Cookies valid, connection could be established')
} else {
cookies_exist = false
logger.log('SETUP', 'Cookies invalid, connection could not be established')
}
}
// create a new set of cookies by logging in
if (!cookies_exist) {
logger.log('SETUP', 'No cookies found or invalid, logging in to create a fresh pair')
try {
login_success = await Login.fetchInitialCookies(
Cookies.getJsonCookiePath(data_dir),
Cookies.getRawCookiePath(data_dir),
Headers.ua,
options.email,
options.pass,
path.join(data_dir, PHANTOMFILE),
)
} catch (error) {
console.log(error)
if (error === 'captcha') {
logger.log('ERROR', 'Login failed, we got a captcha notice. Please change your public IP and try again.')
} else {
logger.log('ERROR', 'Login failed, please check your credentials')
}
login_success = false
return false
}
// Login should´ve been successful, lets validate
logger.log('SETUP', 'Login successful')
// cookie check
if (Cookies.checkJsonCookiesExist(getDataDir()) && Cookies.checkJsonCookiesExist(getDataDir())) {
// validate existing cookies
try {
cookies_valid = await Cookies.validateCookie(getDataDir())
device_data = cookies_valid[1]
cookies_valid = cookies_valid[0]
} catch (e) {
cookies_valid = false
}
// set flasg & log cookie state
if (cookies_valid) {
logger.log('SETUP', 'Cookies valid, connection could be established')
} else {
logger.log('SETUP', 'Cookies invalid, connection could not be established')
return false
}
}
}
// set up proxy dialer
const Proxy = await proxy(logger, data_dir, store)
// initial fetch of data
logger.log('SETUP', 'Fetching initial data')
const initial_data = await Proxy.fetchAll()
logger.log('SETUP', 'Fetch finished')
// start webserver
logger.log('SETUP', 'Starting webserver')
try {
await webserver(logger, options.httpPort, store, Proxy)
} catch (error) {
logger.log('ERROR', `Problem starting webserver: ${error}`)
return false
}
// set up scheduler to periodically fetch data
logger.log('SETUP', `Setting up scheduler with interval: ${options.interval}ms`)
// display data about the echo devices we found
logger.log('ECHO', `Found ${device_data.devices.length} Device(s) connected with this account`)
device_data.devices.forEach(device => {
logger.log('ECHO', `---------------------------------`)
logger.log('ECHO', `Name: ${device.accountName}`)
logger.log('ECHO', `Id: ${device.deviceAccountId}`)
logger.log('ECHO', `Type: ${device.deviceFamily}`)
logger.log('ECHO', `Active: ${device.online}`)
logger.log('ECHO', `Mac Address: ${device.macAddress}`)
})
logger.log('ECHO', `---------------------------------`)
// setup done, system is ready to use
logger.log('SETUP', 'Setup finished. Ready to use. Have Fun!')
return true
}
module.exports = (logger) => {
return {
getDataDir,
checkDataDirExists,
createDataDir,
checkPhantomExist,
run: run.bind(this, logger)
}
}

48
lib/logger.js Normal file
View File

@@ -0,0 +1,48 @@
'use strict'
var chalk = require('chalk')
// colorize keywords
var keywordMap = {
SETUP: chalk.black.bgGreen.bold,
PHANTOMJS: chalk.black.bgMagenta.bold,
ROUTES: chalk.white.bgBlue.bold,
WEBSERVER: chalk.black.bgCyan.bold,
ERROR: chalk.black.bgRed.bold,
ECHO: chalk.black.bgYellowBright.bold,
}
// output formatted datetime before each log entry
var formatConsoleDate = () => {
var date = new Date()
var hour = date.getHours()
var minutes = date.getMinutes()
var seconds = date.getSeconds()
var milliseconds = date.getMilliseconds()
return ((hour < 10) ? '0' + hour: hour) + ':' +
((minutes < 10) ? '0' + minutes: minutes) + ':' +
((seconds < 10) ? '0' + seconds: seconds) + '.' +
('00' + milliseconds).slice(-3)
}
// log with colour
var logWithColour = function(item, text) {
console.log(
chalk.black.bgWhite(formatConsoleDate()),
keywordMap[item]('[' + item + ']'),
text)
}
// log without colour
var logWithoutColour = function(item, text) {
console.log(formatConsoleDate(), '[' + item + ']', text)
}
// export the log function
module.exports = {
setOption: function (use_colour, quiet) {
if (use_colour === false) this.log = logWithoutColour
if (quiet === true) this.log = function () {}
},
log: logWithColour
}

32
lib/login.js Normal file
View File

@@ -0,0 +1,32 @@
'use strict'
const fs = require('fs')
const Horseman = require('node-horseman')
const LOGIN_PAGE_URL = 'https://www.amazon.de/ap/signin?showRmrMe=1&openid.return_to=https%3A%2F%2Flayla.amazon.de&openid.identity=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.assoc_handle=amzn_dp_project_dee_de&openid.mode=checkid_setup&openid.claimed_id=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.ns=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0&'
const fetchInitialCookies = (jsonCookiesFile, cookiesFile, useragent, user, password, phantomPath) => {
const horseman = new Horseman({cookiesFile, phantomPath})
return new Promise((resolve, reject) => {
horseman
.userAgent(useragent)
.open(LOGIN_PAGE_URL)
.text('#auth-fpp-link-bottom')
.type('#ap_email', user)
.type('#ap_password', password)
.click('[name="rememberMe"]')
.click('#signInSubmit')
.waitForNextPage()
.plainText()
.then((text) => {
if (text.search('Anmelden') !== -1) return reject(false)
})
.cookies()
.then(cookies => resolve(fs.writeFileSync(jsonCookiesFile, JSON.stringify(cookies))))
.close()
})
}
module.exports = {
fetchInitialCookies,
}

177
lib/phantomloader.js Normal file
View File

@@ -0,0 +1,177 @@
'use strict'
const os = require('os')
const url = require('url')
const path = require('path')
const cp = require('child_process')
const fs = require('fs-extra')
const request = require('request')
const extractZip = require('extract-zip')
const PHANTOM_VERSION = '2.1.1'
const DEFAULT_CDN = 'https://github.com/Medium/phantomjs/releases/download/v2.1.1'
const ARM_URL = 'https://github.com/fg2it/phantomjs-on-raspberry/raw/master/rpi-1-2-3/wheezy-jessie/v2.1.1/phantomjs'
const PLATFORM = os.platform()
const ARCH = os.arch()
const getDownloadUrl = () => {
let downloadUrl = DEFAULT_CDN + '/phantomjs-' + PHANTOM_VERSION + '-'
if (PLATFORM === 'linux' && ARCH === 'x64') {
downloadUrl += 'linux-x86_64.tar.bz2'
} else if (PLATFORM === 'linux' && ARCH == 'ia32') {
downloadUrl += 'linux-i686.tar.bz2'
} else if (PLATFORM === 'linux' && ARCH.search('arm') !== -1) {
downloadUrl = ARM_URL
} else if (PLATFORM === 'darwin') {
downloadUrl += 'macosx.zip'
} else if (PLATFORM === 'win32') {
downloadUrl += 'windows.zip'
} else {
return null
}
return downloadUrl
}
const downloadPhantomjs = (logger, downloadUrl, data_dir) => {
return new Promise((resolve, reject) => {
const file_name = downloadUrl.split('/').pop()
const downloaded_file = path.join(data_dir, file_name)
// actually downloads the file
const startDownload = (url, dest, cb) => {
const file = fs.createWriteStream(dest)
// download file
request(url)
.pipe(file)
.on('error', cb)
file.on('close', () => {
// close file & invoke callback
file.close()
cb()
})
}
startDownload(downloadUrl, downloaded_file, err => {
if (err) return reject(err)
resolve(downloaded_file)
})
})
}
const extractDownload = (logger, filePath) => {
return new Promise((resolve, reject) => {
var extractedPath = filePath + '-extract-' + Date.now()
var options = {cwd: extractedPath}
fs.mkdirsSync(extractedPath, '0777')
// Make double sure we have 0777 permissions; some operating systems
// default umask does not allow write by default.
fs.chmodSync(extractedPath, '0777')
if (filePath.substr(-4) === '.zip') {
logger.log('PHANTOMJS','Extracting zip contents')
extractZip(path.resolve(filePath), {dir: extractedPath}, err => {
if (err) {
reject(err)
} else {
resolve(extractedPath)
}
})
} else {
logger.log('PHANTOMJS', 'Extracting tar contents (via spawned process)')
cp.execFile('tar', ['jxf', path.resolve(filePath)], options, err => {
if (err) {
reject(err)
} else {
resolve(extractedPath)
}
})
}
})
}
const copyIntoPlace = (logger, extractedPath, archiveFile, targetPath) => {
return new Promise((resolve, reject) => {
logger.log('PHANTOMJS', `Removing ${archiveFile}`)
fs.removeSync(archiveFile)
const folders = fs.readdirSync(extractedPath)
const bin_dir = path.join(extractedPath, folders[0], 'bin')
const files = fs.readdirSync(path.join(extractedPath, folders[0], 'bin'))
let moved = false
for (let i = 0; i < files.length; i++) {
let file = path.join(bin_dir, files[i])
let filename = files[i]
let target = targetPath + path.sep + filename
logger.log('PHANTOMJS', `Copying extracted binary ${file} -> ${target}`)
fs.moveSync(file, target)
logger.log('PHANTOMJS', `Removing temporary download folder ${extractedPath}`)
fs.removeSync(extractedPath)
moved = target
}
if (moved !== false) {
resolve(moved)
} else {
reject()
}
})
}
const download = async (logger, phantom_bin, data_dir) => {
const download_url = getDownloadUrl()
let downloaded_file = ''
let extracted_folder = ''
let moved_file = ''
// error out if we couldn't find a suitable platform/arch combination
if (download_url === null) {
logger.log('ERROR',
`Unexpected platform or architecture: ${PLATFORM} / ${ARCH}\n` +
'It seems there is no binary available for your platform/architecture\n' +
`Try to install PhantomJS on your own & copy/link the executable to "${data_dir}"`)
return false
}
logger.log('PHANTOMJS', `Downloading from "${download_url}" to "${data_dir}/"`)
// download phantomjs
try {
downloaded_file = await downloadPhantomjs(logger, download_url, data_dir)
logger.log('PHANTOMJS', 'Download finished')
} catch (e) {
logger.log('ERROR', 'Error downloading PhantomJS - ' + e)
return false
}
// extract phantomjs (if needed)
// if it´s ARM, we´re done now
if (ARCH.search('arm') !== -1) return true
logger.log('PHANTOMJS', `Extracting PhantomJS binary: ${downloaded_file}`)
try {
extracted_folder = await extractDownload(logger, downloaded_file)
} catch (e) {
logger.log('ERROR', 'Error extracting PhantomJS - ' + e)
return false
}
logger.log('PHANTOMJS', 'Successfully extracted archive: "${extracted_folder}"')
// move Phantom executable
logger.log('PHANTOMJS', 'Moving executable to top level')
try {
moved_file = await copyIntoPlace(logger, extracted_folder, downloaded_file, data_dir)
} catch (e) {
logger.log('PHANTOMJS', 'Could not find extracted file')
return false
}
// make phantomjs executable
try {
fs.chmodSync(moved_file, '755')
} catch (e) {
logger.log('PHANTOMJS', 'Possible error with rights of downloaded Phantom binary')
}
return true
}
module.exports = (logger) => {
return {
download: download.bind(this, logger)
}
}

93
lib/proxy.js Normal file
View File

@@ -0,0 +1,93 @@
'use strict'
const path = require('path')
const klaw = require('klaw')
const request = require('request')
const through2 = require('through2')
const Cookies = require('./cookies')
const Headers = require('./headers')
// check if a resource is available
const checkForResource = (fetchers, resource) => !fetchers[resource]
// check if a ressources subressource has a registered preHook
const checkForPostHooks = (fetchers, resource, subressource) => fetchers[resource].postHooks && fetchers[resource].postHooks[subressource]
// check if a ressources subressource has a registered preHook
const checkForPreHooks = (fetchers, resource, subressource) => fetchers[resource].preHooks && fetchers[resource].preHooks[subressource]
// resource not found
const resourceNotFound = (reject, resource) => reject(`Resource not found: ${resource}`)
// helper stream transform to exclude all non proxy files from components
const excludeNonComponentProxies = through2.obj(function (item, enc, next){
if (path.basename(item.path) === path.basename(__filename) && item.path !== __filename) this.push(item)
next()
})
// register all component proxies
const registerProxies = () => {
return new Promise((resolve, reject) => {
let items = {}
klaw(__dirname)
.pipe(excludeNonComponentProxies)
.on('data', item => items[path.dirname(item.path).split(path.sep).pop()] = require(item.path))
.on('error', reject)
.on('end', resolve.bind(null, items))
})
}
// get array of requests that should be made
const getActions = async (fetchers, resource, subressource, params) => {
if (checkForPreHooks(fetchers, resource, subressource)) {
return await fetchers[resource].preHooks[subressource](Object.assign({}, params, {url: fetchers[resource].urls[subressource]}))
} else {
return []
}
}
// called when the resquest is done
const requestDone = async (resolve, reject, fetchers, store, resource, subressource, err, response, body) => {
if (err) return reject(err)
try {
if (checkForPostHooks(fetchers, resource, subressource)) body = await fetchers[resource].postHooks[subressource](store, body)
resolve({[resource]: body})
} catch (error) {
reject(error)
}
}
// fetch one ressource
const fetch = async (logger, headers, fetchers, store, resource, subressource = 'fetch', params = {}) => {
if (checkForResource(fetchers, resource)) return resourceNotFound(reject, resource)
const actions = await getActions(fetchers, resource, subressource)
const requests = actions.map(action => new Promise((resolve, reject) => request(Object.assign({}, {headers}, action), requestDone.bind(null, resolve, reject, fetchers, store, resource, subressource))))
const data = await Promise.all(requests)
return data
}
// fetch all registered resources
const fetchAll = (logger, headers, fetchers, store) => Promise.all(Object.keys(fetchers).map(resource => fetch(logger, headers, fetchers, store, resource)))
// fetch resources periodically
const fetchInterval = (logger, headers, fetchers, store, interval = 10000) => setInterval(() => resolveInterval(fetchAll(logger, headers, fetchers)), interval)
// fire actions
const act = async (logger, headers, fetchers, store, resource, subresource, params, cb) => {
if (checkForResource(fetchers, resource) || !checkForPreHooks(fetchers, resource, subresource)) return resourceNotFound(reject, resource)
const actions = await fetchers[resource].preHooks[subresource](Object.assign({}, params, {url: fetchers[resource].urls[subresource]}))
const response = () => actions.map(action => {
return new Promise((resolve, reject) => request(Object.assign({}, {headers}, action), requestDone.bind(null, resolve, reject, fetchers, store, resource, subresource)))
})
const res = await Promise.all(response())
console.log('res', res)
cb(res)
}
module.exports = async (logger, data_dir, store, interval) => {
const json_cookies = require(path.join(data_dir, Cookies.JSON_COOKIE_FILE))
const cookies = Cookies.transformCookiesForHeader(json_cookies)
const headers = Headers.getFetchHeaders(cookies)
const action_headers = Headers.getActionHeaders(json_cookies, cookies)
const fetchers = await registerProxies()
return new Promise((resolve, reject) => {
resolve({
act: act.bind(this, logger, action_headers, fetchers, store),
fetch: fetch.bind(this, logger, headers, fetchers, store),
fetchAll: fetchAll.bind(this, logger, headers, fetchers, store),
fetchInterval: fetchInterval.bind(this, logger, headers, fetchers, store, interval),
})
})
}

28
lib/reducer.js Normal file
View File

@@ -0,0 +1,28 @@
'use strict'
const {combineReducers} = require('redux')
// load sub reducers from components
const todos = require('./components/todo/reducer')
const shopping_list = require('./components/shopping_list/reducer')
const timeline = require('./components/timeline/reducer')
const network = require('./components/network/reducer')
const devices = require('./components/devices/reducer')
const household = require('./components/household/reducer')
const notifications = require('./components/notifications/reducer')
const user_data = require('./components/user_data/reducer')
const wakewords = require('./components/wakewords/reducer')
module.exports = () => {
return combineReducers({
todos,
shopping_list,
timeline,
network,
devices,
household,
notifications,
user_data,
wakewords,
})
}

12
lib/store.js Normal file
View File

@@ -0,0 +1,12 @@
'use strict'
const {createStore, applyMiddleware} = require('redux')
const rootReducer = require('./reducer')
// local shadowed store
let store = null
module.exports = () => {
if (store === null) store = createStore(rootReducer(), {})
return store
}

43
lib/telnetserver.js Normal file
View File

@@ -0,0 +1,43 @@
'use strict'
const net = require('net')
let server = null
let sockets = []
// cleans the input of carriage return, newline
const cleanInput = data => data.toString().replace(/(\r\n|\n|\r)/gm, '')
// executed when data is received from a socket
const receiveData = (socket, data) => {
const cleanData = cleanInput(data)
if (cleanData === "@quit") {
socket.end('Goodbye!\n')
} else {
for (let i = 0; i < sockets.length; i++) {
if (sockets[i] !== socket) {
sockets[i].write(data)
}
}
}
}
// executed when a socket ends
const closeSocket = socket => {
const i = sockets.indexOf(socket)
if (i != -1) sockets.splice(i, 1)
}
// callback method executed when a new TCP socket is opened
const newSocket = socket => {
sockets.push(socket)
socket.write('Welcome to the Telnet server!\n')
socket.on('data', data => receiveData(socket, data))
socket.on('end', () => closeSocket(socket))
}
// create a new server
module.exports = (port) => {
if (server === null) server = net.createServer(newSocket).listen(port)
return server
}

55
lib/webserver.js Normal file
View File

@@ -0,0 +1,55 @@
'use strict'
const express = require('express')
const app = express()
// routes
const all = require('./components/all/route')
const todos = require('./components/todo/route')
const shopping_list = require('./components/shopping_list/route')
const timeline = require('./components/timeline/route')
const network = require('./components/network/route')
const devices = require('./components/devices/route')
const household = require('./components/household/route')
const notifications = require('./components/notifications/route')
const user_data = require('./components/user_data/route')
const wakewords = require('./components/wakewords/route')
module.exports = (logger, port, store, Proxy) => {
return new Promise((resolve, reject) => {
// collect routes
const components = [
all,
todos,
shopping_list,
timeline,
network,
devices,
household,
notifications,
user_data,
wakewords,
]
// register routes
components.forEach(component => {
Object.keys(component).forEach(route => {
logger.log('ROUTES', `(webserver) Register route ${route}`)
app.get(route, (req, res) => {
component[route](store, req.query, (data) => {
res.send(data)
}, Proxy)
})
})
})
// listen on errors
app.on('error', reject)
// start webserver
app.listen(port, () => {
logger.log('WEBSERVER', `Webserver started: http://localhost:${port}/`)
resolve(true)
})
})
}