-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
17 changed files
with
6,221 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# http://editorconfig.org | ||
root = true | ||
|
||
[*] | ||
indent_style = space | ||
indent_size = 2 | ||
end_of_line = lf | ||
charset = utf-8 | ||
trim_trailing_whitespace = true | ||
insert_final_newline = true | ||
|
||
# Use 4 spaces for the Python files | ||
[*.py] | ||
indent_size = 4 | ||
max_line_length = 80 | ||
|
||
# The JSON files contain newlines inconsistently | ||
[*.json] | ||
insert_final_newline = ignore | ||
|
||
# Minified JavaScript files shouldn't be changed | ||
[**.min.js] | ||
indent_style = ignore | ||
insert_final_newline = ignore | ||
|
||
# Makefiles always use tabs for indentation | ||
[Makefile] | ||
indent_style = tab | ||
|
||
# Batch files use tabs for indentation | ||
[*.bat] | ||
indent_style = tab | ||
|
||
[*.md] | ||
trim_trailing_whitespace = false | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import { NotLoggedInError } from './core/errors'; | ||
import { Client } from './client'; | ||
import { Auth } from './core/auth'; | ||
import { Gateway } from './core/gateway'; | ||
import commander from 'commander'; | ||
import packageJson from '../package.json'; | ||
import * as constants from './core/constants'; | ||
|
||
import * as fs from 'fs'; | ||
import * as readline from 'readline'; | ||
import * as path from 'path'; | ||
|
||
const input = (question: string) => new Promise<string>((resolve) => { | ||
const rl = readline.createInterface(process.stdin, process.stdout); | ||
rl.question(question, (answer) => resolve(answer)); | ||
}); | ||
|
||
const options = { | ||
country: constants.DEFAULT_COUNTRY, | ||
language: constants.DEFAULT_LANGUAGE, | ||
statePath: 'wideq-state.json', | ||
}; | ||
|
||
const program = new commander.Command('WideQJS'); | ||
program | ||
.version(packageJson.version) | ||
.option('-C, --country <type>', 'Country code for account', constants.DEFAULT_COUNTRY) | ||
.on('option:country', (value) => options.country = value) | ||
.option('-l, --language <type>', 'Language code for account', constants.DEFAULT_LANGUAGE) | ||
.on('option:language', (value) => options.language = value) | ||
.option('-S, --state-path <type>', 'State file path', 'wideq-state.json') | ||
.on('option:statePath', (value) => options.statePath = value); | ||
|
||
program | ||
.command('auth') | ||
.description('Authenticate') | ||
.action(async () => { | ||
const { country, language, statePath } = options; | ||
|
||
const client = await init(country, language, statePath); | ||
|
||
saveState(statePath, client); | ||
}); | ||
|
||
program | ||
.command('ls', { isDefault: true }) | ||
.description('List devices') | ||
.action(async function (...args) { | ||
const { country, language, statePath } = options; | ||
const client = await init(country, language, statePath); | ||
|
||
for (const device of client.devices) { | ||
console.info(String(device)); | ||
} | ||
|
||
saveState(statePath, client); | ||
}); | ||
|
||
async function authenticate(gateway: Gateway) { | ||
const loginUrl = gateway.oauthUrl; | ||
|
||
console.info('Log in here:', loginUrl); | ||
const callbackUrl = await input('Then paste the URL where the browser is redirected: '); | ||
|
||
return Auth.fromUrl(gateway, callbackUrl); | ||
} | ||
|
||
async function init(country: string, language: string, stateFilePath?: string) { | ||
let state: any = {}; | ||
|
||
if (stateFilePath) { | ||
if (fs.existsSync(stateFilePath)) { | ||
try { | ||
state = JSON.parse(fs.readFileSync(stateFilePath).toString()); | ||
} catch { } | ||
} | ||
} | ||
|
||
const client = Client.loadFromState({ | ||
country, | ||
language, | ||
|
||
...state, | ||
}); | ||
|
||
if (!client.gateway) { | ||
client.gateway = await Gateway.discover(country, language); | ||
} | ||
|
||
if (!client.auth) { | ||
client.auth = await authenticate(client.gateway); | ||
} | ||
|
||
if (!client.session && client.auth) { | ||
({ | ||
session: client.session, | ||
items: client.devices, | ||
} = await client.auth.startSession()); | ||
} | ||
|
||
await client.updateDevices(); | ||
|
||
return client; | ||
} | ||
|
||
function saveState(stateFilePath: string, client: Client) { | ||
fs.writeFileSync(stateFilePath, JSON.stringify(client.toStateObject())); | ||
} | ||
|
||
program.parse(process.argv); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
import { Auth } from './core/auth'; | ||
import { Gateway } from './core/gateway'; | ||
import { Session } from './core/session'; | ||
import * as constants from './core/constants'; | ||
import { Device } from './core/device'; | ||
import { DeviceInfo } from './core/device-info'; | ||
import { ModelInfo } from './core/model-info'; | ||
|
||
export class Client { | ||
public devices: any[] = []; | ||
public modelInfo: { [key: string]: any } = {}; | ||
|
||
public constructor( | ||
public gateway: Gateway, | ||
public auth: Auth, | ||
public session: Session | null, | ||
public country = constants.DEFAULT_COUNTRY, | ||
public language = constants.DEFAULT_LANGUAGE, | ||
) { | ||
} | ||
|
||
public static async loadFromToken(refreshToken: string, country: string = constants.DEFAULT_COUNTRY, language: string = constants.DEFAULT_LANGUAGE) { | ||
const gateway = await Gateway.discover(country, language); | ||
const auth = new Auth(gateway, null, refreshToken); | ||
|
||
const client = new Client(gateway, auth, null, country, language); | ||
await client.refresh(); | ||
|
||
return client; | ||
} | ||
|
||
public static loadFromState(state: { | ||
[key in 'gateway' | 'auth' | 'session' | 'modelInfo' | 'country' | 'language']: any; | ||
}) { | ||
let gateway: Gateway; | ||
let auth: Auth; | ||
let session: Session; | ||
let modelInfo: Client['modelInfo'] = {}; | ||
let country: string = constants.DEFAULT_COUNTRY; | ||
let language: string = constants.DEFAULT_LANGUAGE; | ||
|
||
for (const key of Object.keys(state)) { | ||
switch (key) { | ||
case 'gateway': { | ||
const data = state.gateway; | ||
gateway = new Gateway( | ||
data.authBase, | ||
data.apiRoot, | ||
data.oauthRoot, | ||
data.country || constants.DEFAULT_COUNTRY, | ||
data.language || constants.DEFAULT_LANGUAGE, | ||
); | ||
} break; | ||
|
||
case 'auth': { | ||
const data = state.auth; | ||
auth = new Auth( | ||
gateway!, | ||
data.accessToken, | ||
data.refreshToken, | ||
); | ||
} break; | ||
|
||
case 'session': | ||
session = new Session(auth!, state.session); | ||
break; | ||
|
||
case 'modelInfo': | ||
modelInfo = state.modelInfo; | ||
break; | ||
|
||
case 'country': | ||
country = state.country; | ||
break; | ||
|
||
case 'language': | ||
language = state.language; | ||
break; | ||
} | ||
} | ||
|
||
const client = new Client( | ||
gateway!, | ||
auth!, | ||
session!, | ||
country, | ||
language, | ||
); | ||
client.modelInfo = modelInfo; | ||
|
||
return client; | ||
} | ||
|
||
public toStateObject() { | ||
return { | ||
modelInfo: this.modelInfo, | ||
|
||
gateway: !this.gateway | ||
? undefined | ||
: { | ||
authBase: this.gateway.authBase, | ||
apiRoot: this.gateway.apiRoot, | ||
oauthRoot: this.gateway.oauthRoot, | ||
country: this.gateway.country, | ||
language: this.gateway.language, | ||
}, | ||
|
||
auth: !this.auth | ||
? undefined | ||
: { | ||
accessToken: this.auth.accessToken, | ||
refreshToken: this.auth.refreshToken, | ||
}, | ||
|
||
session: !this.session | ||
? undefined | ||
: this.session.sessionId, | ||
|
||
country: this.country, | ||
language: this.language, | ||
}; | ||
} | ||
|
||
public async updateDevices() { | ||
const devices: any[] = await this.session!.getDevices(); | ||
const deviceInfos = devices.map(device => new DeviceInfo(device)); | ||
|
||
this.devices = deviceInfos; | ||
} | ||
|
||
public async getDevice(deviceId: string) { | ||
return this.devices.find(({ id }) => id === deviceId); | ||
} | ||
|
||
public async refresh() { | ||
this.auth = await this.auth.refresh(); | ||
|
||
({ | ||
session: this.session, | ||
items: this.devices, | ||
} = await this.auth.startSession()); | ||
} | ||
|
||
public async getModelInfo(device: DeviceInfo) { | ||
const url = device.modelInfoUrl; | ||
if (!(url in this.modelInfo)) { | ||
this.modelInfo[url] = await device.loadModelInfo(); | ||
} | ||
|
||
return new ModelInfo(this.modelInfo[url]); | ||
} | ||
} |
Oops, something went wrong.