add: 'common' as dependency
This commit is contained in:
19
index.html
19
index.html
@@ -5,30 +5,23 @@
|
||||
</textarea>
|
||||
|
||||
<script type="text/javascript">
|
||||
function main(url, parameters) {
|
||||
let ws = new WebSocket(url);
|
||||
function main(args) {
|
||||
let messages = document.querySelector("#messages");
|
||||
|
||||
let account = new String(parameters['account']);
|
||||
let account = new String(args['account']);
|
||||
let name = account.split('@', 1).pop() ?? 'unknown';
|
||||
|
||||
name = name.charAt(0).toLocaleUpperCase() + name.slice(1);
|
||||
document.title = `${name} (${account})`;
|
||||
document.title += ` (${name})`;
|
||||
|
||||
ws.addEventListener('message', (e) => {
|
||||
object = JSON.parse(e.data);
|
||||
lines = Array.from(object.result);
|
||||
setInterval(async () => {
|
||||
let result = await invoke('sync');
|
||||
let lines = Array.from(result);
|
||||
|
||||
for (let line of lines) {
|
||||
node = document.createTextNode(new String(line).concat('\n'));
|
||||
messages.appendChild(node);
|
||||
}
|
||||
messages.scrollTop = messages.scrollHeight;
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
data = JSON.stringify({ method: "sync", params: null });
|
||||
ws.send(data);
|
||||
}, 1000);
|
||||
}
|
||||
</script>
|
||||
|
||||
144
main.py
144
main.py
@@ -2,12 +2,8 @@ import argparse
|
||||
import openpyxl
|
||||
import logging
|
||||
import base64
|
||||
import json
|
||||
import csv
|
||||
|
||||
import trio
|
||||
import trio_websocket as ws
|
||||
|
||||
from selenium.common.exceptions import StaleElementReferenceException, TimeoutException
|
||||
from selenium.webdriver.chrome.webdriver import WebDriver
|
||||
from selenium.webdriver.chrome.options import Options
|
||||
@@ -18,95 +14,31 @@ from selenium.webdriver.remote.webelement import WebElement
|
||||
from selenium.webdriver.common.bidi.network import Request as NetworkRequest
|
||||
|
||||
from io import BytesIO
|
||||
from enum import Enum
|
||||
from typing import Self, Callable
|
||||
from common import jsonrpc2
|
||||
from typing import Callable
|
||||
from pathlib import Path
|
||||
from urllib3 import PoolManager
|
||||
from threading import Thread
|
||||
from subprocess import Popen
|
||||
|
||||
parser = argparse.ArgumentParser(description="Opportunity Exporter")
|
||||
parser = argparse.ArgumentParser(description="Opportunity Export")
|
||||
parser.add_argument('account', type=str)
|
||||
parser.add_argument('password', type=str)
|
||||
parser.add_argument('-e', '--encoding', type=str, default="utf-8-sig")
|
||||
parser.add_argument('-t', '--timeout', type=int, default=60)
|
||||
parser.add_argument('-r', '--attempts', type=int, default=3)
|
||||
parser.add_argument('-l', '--log-level', choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'])
|
||||
args = parser.parse_args()
|
||||
|
||||
WEBURL = "https://crm.xiaoman.cn/business/export"
|
||||
APIURL = "https://crm.xiaoman.cn/api/opportunityRead/export"
|
||||
|
||||
args = parser.parse_args()
|
||||
handlers = {}
|
||||
connection, server = None, None
|
||||
|
||||
class Request[T]:
|
||||
def __init__(self, method: str, params: T):
|
||||
if not isinstance(method, str): raise TypeError()
|
||||
self.method = method
|
||||
self.params = params
|
||||
|
||||
@classmethod
|
||||
def load(cls, raw: bytes) -> Self:
|
||||
data = json.loads(raw)
|
||||
return cls(**data)
|
||||
|
||||
class Response[T]:
|
||||
def __init__(self, result: T):
|
||||
self.result = result
|
||||
|
||||
def __str__(self):
|
||||
data = { 'result': self.result }
|
||||
return json.dumps(data)
|
||||
|
||||
class Error(Enum):
|
||||
PARSE_ERROR = -200
|
||||
INVALID_REQUEST = -300
|
||||
METHOD_NOT_FOUND = -400
|
||||
INTERNAL_ERROR = -500
|
||||
|
||||
def __str__(self):
|
||||
data = { 'error': self.value }
|
||||
return json.dumps(data)
|
||||
|
||||
class History(logging.Handler):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.records = []
|
||||
|
||||
def emit(self, record):
|
||||
self.records.append(record)
|
||||
|
||||
def truncate(self) -> list:
|
||||
copy = self.records.copy()
|
||||
self.records.clear()
|
||||
return copy
|
||||
|
||||
class ConnectionContext:
|
||||
def __enter__(self):
|
||||
self.healthcheck()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
self.healthcheck()
|
||||
return True
|
||||
|
||||
class Error(Exception):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def healthcheck(cls):
|
||||
if connection.closed is not None:
|
||||
raise cls.Error()
|
||||
|
||||
def main(driver: WebDriver, logger = logging.getLogger('main')):
|
||||
try:
|
||||
http = PoolManager()
|
||||
context = ConnectionContext()
|
||||
|
||||
driver.get(str(Path('index.html').resolve()))
|
||||
endpoint = server.listeners[0]
|
||||
context = jsonrpc2.ConnectionContext()
|
||||
parameters = vars(args)
|
||||
driver.execute_script(f"main(...arguments);", f'ws://{endpoint.address}:{endpoint.port}', parameters)
|
||||
driver.get(str(Path('index.html').resolve()))
|
||||
driver.execute_script(jsonrpc2.prelude(), parameters)
|
||||
except Exception as e:
|
||||
logger.critical('Unable to load starup page', exc_info=e)
|
||||
return 2
|
||||
@@ -214,7 +146,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
|
||||
continue
|
||||
|
||||
try:
|
||||
locate(".mm-modal-content .mm-modal-body input", wait=False).send_keys(parameters.get('password'))
|
||||
locate(".safe-verify-dialog:not([style*='display: none']) .mm-modal-content .mm-modal-body input", wait=False).send_keys(parameters.get('password'))
|
||||
click(".mm-modal-footer button.okki-btn-primary", wait=False)
|
||||
except:
|
||||
pass
|
||||
@@ -288,6 +220,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
|
||||
text = data.decode('ascii')
|
||||
mime = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
href = f"data:{mime};base64,{text}"
|
||||
filename = filename.replace('.csv', '.xlsx')
|
||||
|
||||
driver.switch_to.new_window('tab')
|
||||
driver.execute_script(
|
||||
@@ -297,71 +230,26 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
|
||||
link.href = arguments[1];
|
||||
link.click();
|
||||
""",
|
||||
filename.replace('.csv', '.xlsx'),
|
||||
filename,
|
||||
href
|
||||
)
|
||||
|
||||
try: Popen(['start', (Path.home() / 'Downloads' / filename).as_posix()], shell=True)
|
||||
except Exception as e: logger.warning('Unable to open exported excel file at %s' % file, exc_info=e)
|
||||
|
||||
driver.close()
|
||||
driver.switch_to.window(driver.window_handles[1])
|
||||
logger.info('Done')
|
||||
ready = False
|
||||
|
||||
async def handler(request: ws.WebSocketRequest, logger = logging.getLogger('websocket')):
|
||||
global connection
|
||||
websocket = await request.accept()
|
||||
|
||||
if connection is None:
|
||||
connection = websocket
|
||||
else:
|
||||
await websocket.aclose(code=1000, reason="Non-singular connection prohibited")
|
||||
return
|
||||
|
||||
while True:
|
||||
try:
|
||||
message = await connection.get_message()
|
||||
inbound = Request.load(message)
|
||||
|
||||
handler = handlers[inbound.method]
|
||||
results = handler(inbound.params)
|
||||
response = Response(results)
|
||||
except json.decoder.JSONDecodeError:
|
||||
logger.error('Parse error')
|
||||
response = Error.PARSE_ERROR
|
||||
except TypeError:
|
||||
logger.error('Invalid request')
|
||||
response = Error.INVALID_REQUEST
|
||||
except KeyError:
|
||||
logger.error('Method not found: `%s`', inbound.method)
|
||||
response = Error.METHOD_NOT_FOUND
|
||||
except Exception as e:
|
||||
logger.error('Internal error', exc_info=e)
|
||||
response = Error.INTERNAL_ERROR
|
||||
|
||||
await connection.send_message(str(response))
|
||||
|
||||
async def backend(listen='127.0.0.1', port=0):
|
||||
import _thread as t
|
||||
global server
|
||||
|
||||
listeners = await trio.open_tcp_listeners(port, host=listen)
|
||||
server = ws.WebSocketServer(handler, listeners, max_message_size=125_000_000)
|
||||
await server.run()
|
||||
t.interrupt_main()
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
logging.basicConfig(level=logging.INFO, format="[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s", datefmt="%Y-%m-%d %H:%M")
|
||||
logger = logging.getLogger()
|
||||
formatter = logger.handlers[0].formatter
|
||||
history = History()
|
||||
logger.addHandler(history)
|
||||
logger.info('Initializing...')
|
||||
level = logging.getLevelNamesMapping().get(args.log_level, 'INFO')
|
||||
logger.setLevel(level)
|
||||
|
||||
logger.info('Initializing...')
|
||||
handlers.setdefault('sync', lambda _: list(map(lambda x: formatter.format(x), history.truncate())))
|
||||
thread = Thread(target=lambda: trio.run(backend), daemon=True)
|
||||
thread.start()
|
||||
jsonrpc2.run(logger)
|
||||
|
||||
logger.info('Creating automation instance')
|
||||
opts = Options()
|
||||
|
||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
Reference in New Issue
Block a user