update: bump 'common' version to 0.1.20
This commit is contained in:
+13
-20
@@ -191,15 +191,14 @@ import { default as $, LogRecord, Rpc2 } from '/';
|
|||||||
let major = 0;
|
let major = 0;
|
||||||
let minor = 0;
|
let minor = 0;
|
||||||
let busy = false;
|
let busy = false;
|
||||||
|
let save = false;
|
||||||
let status = null;
|
let status = null;
|
||||||
let locale = null;
|
let locale = null;
|
||||||
let unique = null;
|
let unique = null;
|
||||||
let writable = null;
|
let filehandle = null;
|
||||||
let filereader = null;
|
|
||||||
let subcategories = [];
|
let subcategories = [];
|
||||||
|
|
||||||
window.addEventListener('beforeunload', (e) => {
|
window.addEventListener('beforeunload', (e) => {
|
||||||
writable?.abort();
|
|
||||||
Rpc2.notify('exit');
|
Rpc2.notify('exit');
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.returnValue = '';
|
e.returnValue = '';
|
||||||
@@ -214,7 +213,7 @@ document.title += ` (${name})`;
|
|||||||
async function handlePrimaryButtonClick() {
|
async function handlePrimaryButtonClick() {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'IDLE':
|
case 'IDLE':
|
||||||
let PickerOptions = {
|
[filehandle] = await showOpenFilePicker({
|
||||||
types: [
|
types: [
|
||||||
{
|
{
|
||||||
description: "Excel Spreadsheet",
|
description: "Excel Spreadsheet",
|
||||||
@@ -223,17 +222,11 @@ async function handlePrimaryButtonClick() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
});
|
||||||
let [handle] = await showOpenFilePicker(PickerOptions);
|
let chain = filehandle.getFile().then(f => Promise.all([f, f.arrayBuffer()]));
|
||||||
filereader = () => handle.getFile().then(file => new Promise((resolve, reject) => {
|
let [file, buffer] = await chain.catch(e => alert('(ERROR) ' + new String(e ?? 'Failed to load file')));
|
||||||
let reader = new FileReader();
|
|
||||||
reader.onload = () => resolve([file, reader.result]);
|
|
||||||
reader.onerror = () => reject(reader.error);
|
|
||||||
reader.readAsArrayBuffer(file);
|
|
||||||
}));
|
|
||||||
|
|
||||||
let [file, buffer] = await filereader().catch(e => alert('(ERROR) ' + new String(e ?? 'Unable to read data')));
|
save = await handle.requestPermission({ mode: 'readwrite' }) === 'granted';
|
||||||
writable = await handle.createWritable().catch(() => null);
|
|
||||||
major = await Rpc2.invoke('loads', { params: [file.name, new Uint8Array(buffer).toBase64()] });
|
major = await Rpc2.invoke('loads', { params: [file.name, new Uint8Array(buffer).toBase64()] });
|
||||||
$.get('#fileLabel').innerText = file.name;
|
$.get('#fileLabel').innerText = file.name;
|
||||||
$.get('#fromLabel').innerText = parameters.account;
|
$.get('#fromLabel').innerText = parameters.account;
|
||||||
@@ -241,7 +234,7 @@ async function handlePrimaryButtonClick() {
|
|||||||
break;
|
break;
|
||||||
case 'READY':
|
case 'READY':
|
||||||
let locale = $('#locale').value;
|
let locale = $('#locale').value;
|
||||||
let options = { locale, subcategories, mapping: new Object(), writable: new Boolean(writable), start: 2, limit: null };
|
let options = { locale, subcategories, save, mapping: new Object(), start: 2, limit: null };
|
||||||
|
|
||||||
for (let element of $.all("input[type='number'].params")) {
|
for (let element of $.all("input[type='number'].params")) {
|
||||||
parameters[element.id] = element.disabled ? null : element.valueAsNumber;
|
parameters[element.id] = element.disabled ? null : element.valueAsNumber;
|
||||||
@@ -422,7 +415,6 @@ while (await new Promise(o => setTimeout(o, 1000, true))) {
|
|||||||
case 'IDLE':
|
case 'IDLE':
|
||||||
$('#send > span.text').innerText = 'Open';
|
$('#send > span.text').innerText = 'Open';
|
||||||
$('#send').classList.remove('pulse');
|
$('#send').classList.remove('pulse');
|
||||||
await writable?.abort().catch(() => void 0).finally(() => writable = null);
|
|
||||||
break;
|
break;
|
||||||
case 'READY':
|
case 'READY':
|
||||||
$('#send > span.text').innerText = 'Send';
|
$('#send > span.text').innerText = 'Send';
|
||||||
@@ -431,17 +423,18 @@ while (await new Promise(o => setTimeout(o, 1000, true))) {
|
|||||||
$('#remainingLabel').innerText = '';
|
$('#remainingLabel').innerText = '';
|
||||||
break;
|
break;
|
||||||
case 'CLOSING':
|
case 'CLOSING':
|
||||||
if (writable) try {
|
if (save) try {
|
||||||
$.all('#actions > button').forEach(e => e.disabled = true);
|
$.all('#actions > button').forEach(e => e.disabled = true);
|
||||||
let data = await filereader().then(([_, b]) => Rpc2.invoke('merge', { params: [new Uint8Array(b).toBase64()] }));
|
let writable = await filehandle.createWritable({ mode: 'exclusive' });
|
||||||
|
let data = await filehandle.getFile().arrayBuffer().then(b => Rpc2.invoke('merge', { params: [new Uint8Array(b).toBase64()] }));
|
||||||
await writable.write(Uint8Array.fromBase64(data));
|
await writable.write(Uint8Array.fromBase64(data));
|
||||||
await writable.close();
|
await writable.close();
|
||||||
await Rpc2.notify('close');
|
await Rpc2.notify('close');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (Error.isError(e) && e.name === 'NoModificationAllowedError') continue;
|
||||||
alert('(ERROR) ' + new String(e ?? 'File operation failed'));
|
alert('(ERROR) ' + new String(e ?? 'File operation failed'));
|
||||||
await writable.abort().catch(() => void 0);
|
|
||||||
}
|
}
|
||||||
writable = null;
|
save = false;
|
||||||
continue;
|
continue;
|
||||||
case 'RUNNING':
|
case 'RUNNING':
|
||||||
$('#send > span.text').innerText = 'Pause';
|
$('#send > span.text').innerText = 'Pause';
|
||||||
|
|||||||
@@ -43,15 +43,15 @@ WEBMAIL = "https://email.ionos.fr/appsuite/#app=io.ox/mail&mailto=%s"
|
|||||||
def main(driver: Chrome, logger = logging.getLogger('main')):
|
def main(driver: Chrome, logger = logging.getLogger('main')):
|
||||||
timer = Timer()
|
timer = Timer()
|
||||||
parameters = vars(args)
|
parameters = vars(args)
|
||||||
sp = ServiceProvider.default()
|
sp = ServiceProvider(ServiceProvider.Options(application='Mailer'))
|
||||||
|
|
||||||
class Status(Enum):
|
class Status(Enum):
|
||||||
IDLE = 0
|
IDLE = 0
|
||||||
READY = 1
|
READY = 1
|
||||||
RUNNING = 2
|
RUNNING = 2
|
||||||
STANDBY = 3
|
STANDBY = 3
|
||||||
CLOSING = 4
|
CLOSING = 4
|
||||||
|
|
||||||
class ColumnMapping:
|
class ColumnMapping:
|
||||||
def __init__(self, email=None, recipient=None, code=None, region=None, sent=None, variables=None):
|
def __init__(self, email=None, recipient=None, code=None, region=None, sent=None, variables=None):
|
||||||
self.email = email or '邮箱'
|
self.email = email or '邮箱'
|
||||||
@@ -60,7 +60,7 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
|
|||||||
self.region = region or '国家地区'
|
self.region = region or '国家地区'
|
||||||
self.sent = sent or '已发送'
|
self.sent = sent or '已发送'
|
||||||
self.variables = variables or '变量值'
|
self.variables = variables or '变量值'
|
||||||
|
|
||||||
status = Status.IDLE
|
status = Status.IDLE
|
||||||
options = dict()
|
options = dict()
|
||||||
headers = list()
|
headers = list()
|
||||||
@@ -69,7 +69,7 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
|
|||||||
filename: str = None
|
filename: str = None
|
||||||
column: ColumnMapping = None
|
column: ColumnMapping = None
|
||||||
limit = 0
|
limit = 0
|
||||||
|
|
||||||
def begin(opts: dict, args: dict):
|
def begin(opts: dict, args: dict):
|
||||||
nonlocal status
|
nonlocal status
|
||||||
if status != Status.READY: raise ValueError(status)
|
if status != Status.READY: raise ValueError(status)
|
||||||
@@ -78,32 +78,32 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
|
|||||||
parameters.update(args)
|
parameters.update(args)
|
||||||
timer.clear()
|
timer.clear()
|
||||||
timer.start()
|
timer.start()
|
||||||
|
|
||||||
def pause():
|
def pause():
|
||||||
nonlocal status
|
nonlocal status
|
||||||
status = Status.STANDBY
|
status = Status.STANDBY
|
||||||
timer.pause()
|
timer.pause()
|
||||||
|
|
||||||
def resume():
|
def resume():
|
||||||
nonlocal status
|
nonlocal status
|
||||||
status = Status.RUNNING
|
status = Status.RUNNING
|
||||||
driver.switch_to.window(driver.current_window_handle)
|
driver.switch_to.window(driver.current_window_handle)
|
||||||
timer.start()
|
timer.start()
|
||||||
|
|
||||||
def unique(header: str):
|
def unique(header: str):
|
||||||
result = dict()
|
result = dict()
|
||||||
col = headers.index(header) + 1
|
col = headers.index(header) + 1
|
||||||
|
|
||||||
for row in range(options.get('start'), limit + 1):
|
for row in range(options.get('start'), limit + 1):
|
||||||
key = str(wb.active.cell(row, col).value)
|
key = str(wb.active.cell(row, col).value)
|
||||||
result[key] = result.get(key, 0) + 1
|
result[key] = result.get(key, 0) + 1
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
sp.add(begin, pause, resume, unique)
|
sp.add(begin, pause, resume, unique)
|
||||||
sp.set('status', lambda: status.name)
|
sp.set('status', lambda: status.name)
|
||||||
sp.set('uptime', lambda: timer.delta())
|
sp.set('uptime', lambda: timer.delta())
|
||||||
|
|
||||||
try:
|
try:
|
||||||
mapping = ColumnMapping(**{
|
mapping = ColumnMapping(**{
|
||||||
k.lower().strip(): v.strip() for k, v in map(lambda o: str.split(o, '=', 2), parameters['column'] or list())
|
k.lower().strip(): v.strip() for k, v in map(lambda o: str.split(o, '=', 2), parameters['column'] or list())
|
||||||
@@ -111,7 +111,7 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.critical('Unable to load column mappings', exc_info=e)
|
logger.critical('Unable to load column mappings', exc_info=e)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
class Locale:
|
class Locale:
|
||||||
def __init__(self, name, timezone, default, morning=None, afternoon=None, evening=None, keywords=None):
|
def __init__(self, name, timezone, default, morning=None, afternoon=None, evening=None, keywords=None):
|
||||||
self.name: str = name
|
self.name: str = name
|
||||||
@@ -121,7 +121,7 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
|
|||||||
self.afternoon: str = afternoon
|
self.afternoon: str = afternoon
|
||||||
self.evening: str = evening
|
self.evening: str = evening
|
||||||
self.keywords: list[str] = keywords
|
self.keywords: list[str] = keywords
|
||||||
|
|
||||||
locales = [
|
locales = [
|
||||||
Locale("en", "Europe/London", "Hello" , "Good morning", "Good afternoon" , "Good evening", []),
|
Locale("en", "Europe/London", "Hello" , "Good morning", "Good afternoon" , "Good evening", []),
|
||||||
Locale("fr", "Europe/Paris" , "Bonjour", None , None , "Bonsoir" , ['法国', '比利时', '留尼汪']),
|
Locale("fr", "Europe/Paris" , "Bonjour", None , None , "Bonsoir" , ['法国', '比利时', '留尼汪']),
|
||||||
@@ -130,29 +130,29 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
|
|||||||
Locale("es", "Europe/Madrid", "Hola" , "Buenos días" , "Buenas tardes" , None , ['西班牙']),
|
Locale("es", "Europe/Madrid", "Hola" , "Buenos días" , "Buenas tardes" , None , ['西班牙']),
|
||||||
Locale("pt", "Europe/Lisbon", "Olá" , "Bom dia" , "Boa tarde" , None , ['葡萄牙']),
|
Locale("pt", "Europe/Lisbon", "Olá" , "Bom dia" , "Boa tarde" , None , ['葡萄牙']),
|
||||||
]
|
]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sp.set('context', lambda: { 'locales': list(map(vars, locales)), 'mapping': vars(mapping), 'parameters': parameters })
|
sp.set('context', lambda: { 'locales': list(map(vars, locales)), 'mapping': vars(mapping), 'parameters': parameters })
|
||||||
driver.get(sp.run())
|
driver.get(sp.run())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.critical('Unable to load starup page', exc_info=e)
|
logger.critical('Unable to load starup page', exc_info=e)
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
def contains_non_latin_alphabet(string: str):
|
def contains_non_latin_alphabet(string: str):
|
||||||
from unicodedata import category, name
|
from unicodedata import category, name
|
||||||
for char in string:
|
for char in string:
|
||||||
if char.isdigit() or (category(char).startswith('L') and not name(char, '').startswith('LATIN')):
|
if char.isdigit() or (category(char).startswith('L') and not name(char, '').startswith('LATIN')):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def ready(driver: Chrome):
|
def ready(driver: Chrome):
|
||||||
busy = driver.find_element(By.CSS_SELECTOR, ".io-ox-busy")
|
busy = driver.find_element(By.CSS_SELECTOR, ".io-ox-busy")
|
||||||
return busy
|
return busy
|
||||||
|
|
||||||
def catch(predicate: Callable[[Chrome], WebElement]):
|
def catch(predicate: Callable[[Chrome], WebElement]):
|
||||||
wait = WebDriverWait(driver, timeout=parameters['interval'])
|
wait = WebDriverWait(driver, timeout=parameters['interval'])
|
||||||
return wait.until(predicate, 'Timeout')
|
return wait.until(predicate, 'Timeout')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
driver.switch_to.new_window('tab')
|
driver.switch_to.new_window('tab')
|
||||||
driver.set_page_load_timeout(parameters['timeout'])
|
driver.set_page_load_timeout(parameters['timeout'])
|
||||||
@@ -160,33 +160,33 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
|
|||||||
except TimeoutException:
|
except TimeoutException:
|
||||||
logger.warning('Timeout')
|
logger.warning('Timeout')
|
||||||
driver.execute_script("window.stop();")
|
driver.execute_script("window.stop();")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
setup(driver, parameters)
|
setup(driver, parameters)
|
||||||
click("#selectAll")
|
click("#selectAll")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug('Can I haz cheez burger? :3', exc_info=e)
|
logger.debug('Can I haz cheez burger? :3', exc_info=e)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logger.info('Logging in as %s', parameters['account'])
|
logger.info('Logging in as %s', parameters['account'])
|
||||||
username = locate("#username")
|
username = locate("#username")
|
||||||
username.send_keys(parameters['account'])
|
username.send_keys(parameters['account'])
|
||||||
click("#button--with-loader")
|
click("#button--with-loader")
|
||||||
|
|
||||||
password = locate("#password")
|
password = locate("#password")
|
||||||
password.send_keys(parameters['password'])
|
password.send_keys(parameters['password'])
|
||||||
click("#button--with-loader")
|
click("#button--with-loader")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.critical('Error while logging in to %s', LOGIN, exc_info=e)
|
logger.critical('Error while logging in to %s', LOGIN, exc_info=e)
|
||||||
return 3
|
return 3
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
loader = locate("#background-loader", wait=False)
|
loader = locate("#background-loader", wait=False)
|
||||||
if not loader.is_displayed(): break
|
if not loader.is_displayed(): break
|
||||||
except:
|
except:
|
||||||
sleep(1)
|
sleep(1)
|
||||||
|
|
||||||
def get_subject():
|
def get_subject():
|
||||||
try:
|
try:
|
||||||
element = locate("h1.subject", wait=False)
|
element = locate("h1.subject", wait=False)
|
||||||
@@ -194,7 +194,7 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
|
|||||||
return subject
|
return subject
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_address():
|
def get_address():
|
||||||
try:
|
try:
|
||||||
element = locate("header div.from", wait=False)
|
element = locate("header div.from", wait=False)
|
||||||
@@ -202,11 +202,11 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
|
|||||||
return address
|
return address
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def cell(row: int, header: str):
|
def cell(row: int, header: str):
|
||||||
result = wb.active.cell(row, headers.index(header) + 1)
|
result = wb.active.cell(row, headers.index(header) + 1)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
try:
|
try:
|
||||||
click("li[data-id='default0/Brouillons']", condition=EC.presence_of_element_located)
|
click("li[data-id='default0/Brouillons']", condition=EC.presence_of_element_located)
|
||||||
click("button[data-id='default0/Brouillons']", condition=EC.presence_of_element_located)
|
click("button[data-id='default0/Brouillons']", condition=EC.presence_of_element_located)
|
||||||
@@ -216,74 +216,74 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
|
|||||||
logger.warning("Could not open drafts; this may cause issues", exc_info=e)
|
logger.warning("Could not open drafts; this may cause issues", exc_info=e)
|
||||||
finally:
|
finally:
|
||||||
driver.switch_to.window(driver.window_handles[0])
|
driver.switch_to.window(driver.window_handles[0])
|
||||||
|
|
||||||
class Wait(Action):
|
class Wait(Action):
|
||||||
@classmethod
|
@classmethod
|
||||||
def prepare(cls):
|
def prepare(cls):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def perform(cls):
|
def perform(cls):
|
||||||
if status == Status.RUNNING: return False
|
if status == Status.RUNNING: return False
|
||||||
sleep(0.2); return True
|
sleep(0.2); return True
|
||||||
|
|
||||||
class Timeout(Action):
|
class Timeout(Action):
|
||||||
timer = Timer()
|
clock = Timer()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def prepare(cls):
|
def prepare(cls):
|
||||||
cls.timer.clear()
|
cls.clock.clear()
|
||||||
cls.timer.start()
|
cls.clock.start()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def perform(cls):
|
def perform(cls):
|
||||||
if cls.timer.delta() > parameters['timeout']: raise cls
|
if cls.clock.delta() > parameters['timeout']: raise cls
|
||||||
sleep(0.2); return True
|
sleep(0.2); return True
|
||||||
|
|
||||||
class Acknowledge(Action):
|
class Acknowledge(Action):
|
||||||
@classmethod
|
@classmethod
|
||||||
def prepare(cls):
|
def prepare(cls):
|
||||||
return status == Status.RUNNING
|
return status == Status.RUNNING
|
||||||
|
|
||||||
class Cancel(Action):
|
class Cancel(Action):
|
||||||
@classmethod
|
@classmethod
|
||||||
def prepare(cls):
|
def prepare(cls):
|
||||||
nonlocal status
|
nonlocal status
|
||||||
status = Status.RUNNING
|
status = Status.RUNNING
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def perform(cls):
|
def perform(cls):
|
||||||
driver.switch_to.window(driver.window_handles[0])
|
driver.switch_to.window(driver.window_handles[0])
|
||||||
raise cls
|
raise cls
|
||||||
|
|
||||||
class Close(Action):
|
class Close(Action):
|
||||||
@classmethod
|
@classmethod
|
||||||
def prepare(cls):
|
def prepare(cls):
|
||||||
return status == Status.CLOSING
|
return status == Status.CLOSING
|
||||||
|
|
||||||
class Skip(Action):
|
class Skip(Action):
|
||||||
@classmethod
|
@classmethod
|
||||||
def prepare(cls):
|
def prepare(cls):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def perform(cls):
|
def perform(cls):
|
||||||
driver.switch_to.window(driver.current_window_handle)
|
driver.switch_to.window(driver.current_window_handle)
|
||||||
raise cls
|
raise cls
|
||||||
|
|
||||||
flow = ActionFlow()
|
flow = ActionFlow()
|
||||||
flow.stage(Wait, Acknowledge, Timeout, Cancel, Close, Skip)
|
flow.stage(Wait, Acknowledge, Timeout, Cancel, Close, Skip)
|
||||||
sp.add(pairs=[ (k.lower(), v) for k, v in flow ])
|
sp.add(pairs=[ (k.lower(), v) for k, v in flow ])
|
||||||
sp.set('actions', lambda: flow.capabilities())
|
sp.set('actions', lambda: flow.capabilities())
|
||||||
|
|
||||||
def loads(name: str, b64: str):
|
def loads(name: str, b64: str):
|
||||||
from base64 import b64decode
|
from base64 import b64decode
|
||||||
from openpyxl import load_workbook
|
from openpyxl import load_workbook
|
||||||
nonlocal status, filename, wb, limit
|
nonlocal status, filename, wb, limit
|
||||||
if status != Status.IDLE: raise ValueError(status)
|
if status != Status.IDLE: raise ValueError(status)
|
||||||
|
|
||||||
data = b64decode(b64)
|
data = b64decode(b64)
|
||||||
logger.info('Received %s byte(s) total', len(data))
|
logger.info('Received %s byte(s) total', len(data))
|
||||||
logger.info('Loading...')
|
logger.info('Loading...')
|
||||||
@@ -292,56 +292,56 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
|
|||||||
wb = load_workbook(buffer)
|
wb = load_workbook(buffer)
|
||||||
limit = wb.active.max_row
|
limit = wb.active.max_row
|
||||||
headers.clear()
|
headers.clear()
|
||||||
|
|
||||||
for col in range(1, wb.active.max_column + 1):
|
for col in range(1, wb.active.max_column + 1):
|
||||||
value = str(wb.active.cell(1, col).value)
|
value = str(wb.active.cell(1, col).value)
|
||||||
headers.append(value)
|
headers.append(value)
|
||||||
|
|
||||||
logger.info('Done')
|
logger.info('Done')
|
||||||
flow.allow(Cancel)
|
flow.allow(Cancel)
|
||||||
status = Status.READY
|
status = Status.READY
|
||||||
return limit
|
return limit
|
||||||
|
|
||||||
def merge(b64: str):
|
def merge(b64: str):
|
||||||
from base64 import b64decode, b64encode
|
from base64 import b64decode, b64encode
|
||||||
from openpyxl import load_workbook
|
from openpyxl import load_workbook
|
||||||
if status != Status.CLOSING: raise ValueError(status)
|
if status != Status.CLOSING: raise ValueError(status)
|
||||||
|
|
||||||
data = b64decode(b64)
|
data = b64decode(b64)
|
||||||
index = headers.index(column.sent)
|
index = headers.index(column.sent)
|
||||||
buffer = BytesIO(data)
|
buffer = BytesIO(data)
|
||||||
target = load_workbook(buffer)
|
target = load_workbook(buffer)
|
||||||
|
|
||||||
for row in range(options.get('start'), limit + 1):
|
for row in range(options.get('start'), limit + 1):
|
||||||
c1 = wb.active.cell(row, index + 1)
|
c1 = wb.active.cell(row, index + 1)
|
||||||
c2 = target.active.cell(row, index + 1)
|
c2 = target.active.cell(row, index + 1)
|
||||||
if not (c1.value and str(c1.value).strip()):
|
if not (c1.value and str(c1.value).strip()):
|
||||||
if (c2.value and str(c2.value).strip()):
|
if (c2.value and str(c2.value).strip()):
|
||||||
c1.value = c2.value
|
c1.value = c2.value
|
||||||
|
|
||||||
io = BytesIO()
|
io = BytesIO()
|
||||||
wb.save(io)
|
wb.save(io)
|
||||||
result = b64encode(io.getbuffer()).decode('ascii')
|
result = b64encode(io.getbuffer()).decode('ascii')
|
||||||
return result
|
return result
|
||||||
|
|
||||||
sp.add(loads, merge)
|
sp.add(loads, merge)
|
||||||
sp.set('progress', lambda: progress)
|
sp.set('progress', lambda: progress)
|
||||||
|
|
||||||
def save():
|
def save():
|
||||||
try:
|
try:
|
||||||
file = Path(parameters['directory']).joinpath(filename or f'Mailer-{datetime.now().strftime('%Y%m%d-%H%M%S-%f')}.xlsx').resolve()
|
file = Path(parameters['directory']).joinpath(datetime.now().strftime('Mailer-%Y%m%d-%H%M%S-%f.xlsx')).resolve()
|
||||||
logger.info('Saving document at %s', str(file))
|
logger.info('Saving document at %s', str(file))
|
||||||
wb.save(file)
|
wb.save(file)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error('Error while writing data', exc_info=e)
|
logger.error('Error while writing data', exc_info=e)
|
||||||
|
|
||||||
exit = sp.pop('exit')
|
exit = sp.pop('exit')
|
||||||
sp.set('exit', lambda: (save() if options.get('writable') and status.value >= 2 else None, exit()))
|
sp.set('exit', lambda: (save() if options.get('save') and status.value >= 2 else None, exit()))
|
||||||
|
|
||||||
class MismatchedEmailSubject(Exception): pass
|
class MismatchedEmailSubject(Exception): pass
|
||||||
class MismatchedEmailAddress(Exception): pass
|
class MismatchedEmailAddress(Exception): pass
|
||||||
class Next(Exception): pass
|
class Next(Exception): pass
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
flow.react(Wait)
|
flow.react(Wait)
|
||||||
@@ -349,22 +349,22 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
|
|||||||
driver.switch_to.window(driver.window_handles[1])
|
driver.switch_to.window(driver.window_handles[1])
|
||||||
subject = get_subject()
|
subject = get_subject()
|
||||||
column = ColumnMapping(**options.get('mapping'))
|
column = ColumnMapping(**options.get('mapping'))
|
||||||
|
|
||||||
if column.email not in headers or limit < 2:
|
if column.email not in headers or limit < 2:
|
||||||
logger.error("Column '%s' is not found or does not contain valid data", column.email)
|
logger.error("Column '%s' is not found or does not contain valid data", column.email)
|
||||||
raise Cancel()
|
raise Cancel()
|
||||||
|
|
||||||
if column.sent not in headers:
|
if column.sent not in headers:
|
||||||
wb.active.cell(1, wb.active.max_column + 1).value = column.sent
|
wb.active.cell(1, wb.active.max_column + 1).value = column.sent
|
||||||
headers.append(column.sent)
|
headers.append(column.sent)
|
||||||
|
|
||||||
logger.info('Read %s line(s) total', limit)
|
logger.info('Read %s line(s) total', limit)
|
||||||
logger.info('Subject: %s', subject)
|
logger.info('Subject: %s', subject)
|
||||||
logger.info('From: %s', parameters['account'])
|
logger.info('From: %s', parameters['account'])
|
||||||
locale: Locale = next(filter(lambda o: o.name == options.get('locale'), locales))
|
locale: Locale = next(filter(lambda o: o.name == options.get('locale'), locales))
|
||||||
logger.info('Locale: %s', locale.name.upper())
|
logger.info('Locale: %s', locale.name.upper())
|
||||||
logger.info('Timezone: %s', locale.timezone)
|
logger.info('Timezone: %s', locale.timezone)
|
||||||
|
|
||||||
progress['done'] = 0
|
progress['done'] = 0
|
||||||
progress['skip'] = 0
|
progress['skip'] = 0
|
||||||
progress['next'] = 0
|
progress['next'] = 0
|
||||||
@@ -378,12 +378,12 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
|
|||||||
status = Status.IDLE
|
status = Status.IDLE
|
||||||
logger.error('Error while loading data', exc_info=e)
|
logger.error('Error while loading data', exc_info=e)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
index = 2
|
index = 2
|
||||||
attempts = 0
|
attempts = 0
|
||||||
tz = ZoneInfo(locale.timezone)
|
tz = ZoneInfo(locale.timezone)
|
||||||
occurrence = dict()
|
occurrence = dict()
|
||||||
|
|
||||||
while index <= limit:
|
while index <= limit:
|
||||||
try:
|
try:
|
||||||
flow.react(Wait)
|
flow.react(Wait)
|
||||||
@@ -391,16 +391,16 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
|
|||||||
try: code = str(cell(index, column.code).value)
|
try: code = str(cell(index, column.code).value)
|
||||||
except ValueError: code = None
|
except ValueError: code = None
|
||||||
occurrence.setdefault(code, 0)
|
occurrence.setdefault(code, 0)
|
||||||
|
|
||||||
if (target := cell(index, column.email).value) is None or not (email := str(target).strip()):
|
if (target := cell(index, column.email).value) is None or not (email := str(target).strip()):
|
||||||
logger.warning("[%d/%d] Not an email address; skipping", index-1, limit-1)
|
logger.warning("[%d/%d] Not an email address; skipping", index-1, limit-1)
|
||||||
raise Skip()
|
raise Skip()
|
||||||
|
|
||||||
if attempts > parameters['attempts']:
|
if attempts > parameters['attempts']:
|
||||||
cell(index, column.sent).value = '❌'
|
cell(index, column.sent).value = '❌'
|
||||||
logger.warning("[%d/%d] Exhausted all allowed attempts; skipping", index-1, limit-1)
|
logger.warning("[%d/%d] Exhausted all allowed attempts; skipping", index-1, limit-1)
|
||||||
raise Skip()
|
raise Skip()
|
||||||
|
|
||||||
if options.get('slice') and (start := options.get('start')) and (end := options.get('limit')):
|
if options.get('slice') and (start := options.get('start')) and (end := options.get('limit')):
|
||||||
if index < start or index >= start + end:
|
if index < start or index >= start + end:
|
||||||
logger.info("[%d/%d] Not planned; skipping '%s'", index-1, limit-1, email)
|
logger.info("[%d/%d] Not planned; skipping '%s'", index-1, limit-1, email)
|
||||||
@@ -408,22 +408,20 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
|
|||||||
if (items := options.get('subcategories')) and (item := str(cell(index, column.region).value)) not in items:
|
if (items := options.get('subcategories')) and (item := str(cell(index, column.region).value)) not in items:
|
||||||
logger.info("[%d/%d] Value '%s' not enlisted; skipping '%s'", index-1, limit-1, item, email)
|
logger.info("[%d/%d] Value '%s' not enlisted; skipping '%s'", index-1, limit-1, item, email)
|
||||||
raise Next()
|
raise Next()
|
||||||
|
|
||||||
if (sent := cell(index, column.sent).value) is not None and str(sent).strip():
|
if (sent := cell(index, column.sent).value) is not None and str(sent).strip():
|
||||||
logger.info("[%d/%d] Already visited; skipping '%s'", index-1, limit-1, email)
|
logger.info("[%d/%d] Already visited; skipping '%s'", index-1, limit-1, email)
|
||||||
occurrence[code] += 1
|
occurrence[code] += 1
|
||||||
raise Skip()
|
raise Skip()
|
||||||
|
|
||||||
match options.get('occurrence'):
|
if (caps := options.get('occurrence')) is not None and caps > 0:
|
||||||
case o if o is None:
|
if code is None:
|
||||||
pass
|
|
||||||
case o if o > 0 and code is None:
|
|
||||||
logger.error("Column '%s' cannot be used for spam control", column.code)
|
logger.error("Column '%s' cannot be used for spam control", column.code)
|
||||||
raise Cancel()
|
raise Cancel()
|
||||||
case o if o > 0 and occurrence[code] >= o:
|
if occurrence[code] >= caps:
|
||||||
logger.info("[%d/%d] No Spam! Skipping '%s'", index-1, limit-1, email)
|
logger.info("[%d/%d] No Spam! Skipping '%s'", index-1, limit-1, email)
|
||||||
raise Skip()
|
raise Skip()
|
||||||
|
|
||||||
while mails := driver.find_elements(By.CSS_SELECTOR, "div.io-ox-mail-compose-window"):
|
while mails := driver.find_elements(By.CSS_SELECTOR, "div.io-ox-mail-compose-window"):
|
||||||
try:
|
try:
|
||||||
click(mails[0].find_element(By.CSS_SELECTOR, "button[data-action='close']"), wait=False)
|
click(mails[0].find_element(By.CSS_SELECTOR, "button[data-action='close']"), wait=False)
|
||||||
@@ -431,39 +429,39 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug('Unable to close off email compose windows', exc_info=e)
|
logger.debug('Unable to close off email compose windows', exc_info=e)
|
||||||
break
|
break
|
||||||
|
|
||||||
flow.allow(Skip, Cancel)
|
flow.allow(Skip, Cancel)
|
||||||
flow.react(Wait)
|
flow.react(Wait)
|
||||||
progress['email'] = str(email)
|
progress['email'] = str(email)
|
||||||
logger.info('[%d/%d] Sending to %s', index-1, limit-1, email)
|
logger.info('[%d/%d] Sending to %s', index-1, limit-1, email)
|
||||||
clean = True
|
clean = True
|
||||||
error = None
|
error = None
|
||||||
|
|
||||||
if (target := get_address()) != parameters['account']:
|
if (target := get_address()) != parameters['account']:
|
||||||
error = MismatchedEmailAddress(target)
|
error = MismatchedEmailAddress(target)
|
||||||
|
|
||||||
if (target := get_subject()) != subject:
|
if (target := get_subject()) != subject:
|
||||||
error = MismatchedEmailSubject(target)
|
error = MismatchedEmailSubject(target)
|
||||||
|
|
||||||
if error is not None and not options.get('force'):
|
if error is not None and not options.get('force'):
|
||||||
raise error
|
raise error
|
||||||
|
|
||||||
flow.react(Wait)
|
flow.react(Wait)
|
||||||
click("ul.classic-toolbar button[aria-label='Edit copy']")
|
click("ul.classic-toolbar button[aria-label='Edit copy']")
|
||||||
until(ready)
|
until(ready)
|
||||||
locate("div.io-ox-mail-compose-window iframe", condition=EC.frame_to_be_available_and_switch_to_it)
|
locate("div.io-ox-mail-compose-window iframe", condition=EC.frame_to_be_available_and_switch_to_it)
|
||||||
|
|
||||||
if options.get('greet') and not (clean := False):
|
if options.get('greet') and not (clean := False):
|
||||||
match datetime.now(tz).hour:
|
match datetime.now(tz).hour:
|
||||||
case h if 6 <= h < 12: hello = locale.morning
|
case h if 6 <= h < 12: hello = locale.morning
|
||||||
case h if 12 <= h < 18: hello = locale.afternoon
|
case h if 12 <= h < 18: hello = locale.afternoon
|
||||||
case h if 18 <= h < 21: hello = locale.evening
|
case h if 18 <= h < 21: hello = locale.evening
|
||||||
case _: hello = None
|
case _: hello = None
|
||||||
|
|
||||||
iframe = driver.switch_to.active_element
|
iframe = driver.switch_to.active_element
|
||||||
action = ActionChains(driver)
|
action = ActionChains(driver)
|
||||||
hello = hello or locale.default
|
hello = hello or locale.default
|
||||||
|
|
||||||
if column.recipient in headers:
|
if column.recipient in headers:
|
||||||
if name := str(cell(index, column.recipient).value).strip():
|
if name := str(cell(index, column.recipient).value).strip():
|
||||||
if not contains_non_latin_alphabet(name):
|
if not contains_non_latin_alphabet(name):
|
||||||
@@ -472,29 +470,29 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
|
|||||||
parts.capitalize(force=True)
|
parts.capitalize(force=True)
|
||||||
short = len(parts.first) < 3 or (len(parts.first) < 5 and parts.first.endswith('.'))
|
short = len(parts.first) < 3 or (len(parts.first) < 5 and parts.first.endswith('.'))
|
||||||
hello = ' '.join(filter(None, [hello, parts.title, parts.first, (parts.middle or parts.last) if short else None]))
|
hello = ' '.join(filter(None, [hello, parts.title, parts.first, (parts.middle or parts.last) if short else None]))
|
||||||
|
|
||||||
hello += ','
|
hello += ','
|
||||||
action.send_keys(hello).perform()
|
action.send_keys(hello).perform()
|
||||||
|
|
||||||
if elements := iframe.find_elements(By.XPATH, f'//*[contains(text(), "{hello}")]'):
|
if elements := iframe.find_elements(By.XPATH, f'//*[contains(text(), "{hello}")]'):
|
||||||
target = elements[0]
|
target = elements[0]
|
||||||
clean = target.text == hello
|
clean = target.text == hello
|
||||||
|
|
||||||
driver.switch_to.default_content()
|
driver.switch_to.default_content()
|
||||||
flow.react(Wait)
|
flow.react(Wait)
|
||||||
click("div.io-ox-mail-compose-window div[data-extension-id='to'] > div.mail-input")
|
click("div.io-ox-mail-compose-window div[data-extension-id='to'] > div.mail-input")
|
||||||
box = locate("div.io-ox-mail-compose-window div[data-extension-id='to'] > div.mail-input input.token-input.tt-input[tabindex='0']")
|
box = locate("div.io-ox-mail-compose-window div[data-extension-id='to'] > div.mail-input input.token-input.tt-input[tabindex='0']")
|
||||||
box.send_keys(str(email) + Keys.ENTER)
|
box.send_keys(str(email) + Keys.ENTER)
|
||||||
|
|
||||||
if column.variables in headers and (v := cell(index, column.variables).value) is not None:
|
if column.variables in headers and (v := cell(index, column.variables).value) is not None:
|
||||||
clean = False
|
clean = False
|
||||||
target = locate("div.io-ox-mail-compose-window div[data-extension-id='subject'] input")
|
target = locate("div.io-ox-mail-compose-window div[data-extension-id='subject'] input")
|
||||||
template = target.get_attribute('value')
|
template = target.get_attribute('value')
|
||||||
length = len(template)
|
length = len(template)
|
||||||
|
|
||||||
for i, value in enumerate(str(v).split(',')):
|
for i, value in enumerate(str(v).split(',')):
|
||||||
template = template.replace('$$%s' % i, value.strip())
|
template = template.replace('$$%s' % i, value.strip())
|
||||||
|
|
||||||
target.send_keys(Keys.BACKSPACE * length)
|
target.send_keys(Keys.BACKSPACE * length)
|
||||||
target.send_keys(template)
|
target.send_keys(template)
|
||||||
clean = target.get_attribute('value') == template
|
clean = target.get_attribute('value') == template
|
||||||
@@ -502,15 +500,15 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
|
|||||||
flow.react(Wait)
|
flow.react(Wait)
|
||||||
box = locate("div.io-ox-mail-compose-window .mail-input .tokenfield .token")
|
box = locate("div.io-ox-mail-compose-window .mail-input .tokenfield .token")
|
||||||
recipient = box.get_attribute('innerText').strip()
|
recipient = box.get_attribute('innerText').strip()
|
||||||
|
|
||||||
if recipient != str(email):
|
if recipient != str(email):
|
||||||
logger.warning('Malformed email address detected; retrying (%d)...', attempts)
|
logger.warning('Malformed email address detected; retrying (%d)...', attempts)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not clean:
|
if not clean:
|
||||||
logger.warning('Malformed email content detected; retrying (%d)...', attempts)
|
logger.warning('Malformed email content detected; retrying (%d)...', attempts)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
flow.react(Wait)
|
flow.react(Wait)
|
||||||
click("div.io-ox-mail-compose-window button[data-action='send']")
|
click("div.io-ox-mail-compose-window button[data-action='send']")
|
||||||
except Cancel:
|
except Cancel:
|
||||||
@@ -528,7 +526,7 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
|
|||||||
logger.error('Unexptected error', exc_info=e)
|
logger.error('Unexptected error', exc_info=e)
|
||||||
status = Status.STANDBY
|
status = Status.STANDBY
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
alert = catch(lambda x: x.find_element(By.CSS_SELECTOR, "div.io-ox-alert.io-ox-alert-error"))
|
alert = catch(lambda x: x.find_element(By.CSS_SELECTOR, "div.io-ox-alert.io-ox-alert-error"))
|
||||||
message = alert.text.replace('\n', ' ')
|
message = alert.text.replace('\n', ' ')
|
||||||
@@ -544,7 +542,7 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
|
|||||||
logger.error('Unexptected error', exc_info=e)
|
logger.error('Unexptected error', exc_info=e)
|
||||||
status = Status.STANDBY
|
status = Status.STANDBY
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logger.info('Waiting for client to acknowledge...')
|
logger.info('Waiting for client to acknowledge...')
|
||||||
flow.deter(Cancel, Skip, Wait)
|
flow.deter(Cancel, Skip, Wait)
|
||||||
@@ -555,8 +553,8 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error('RPC communication failed', exc_info=e)
|
logger.error('RPC communication failed', exc_info=e)
|
||||||
sp.pop('exit')()
|
sp.pop('exit')()
|
||||||
|
|
||||||
if options.get('writable'):
|
if options.get('save'):
|
||||||
try:
|
try:
|
||||||
logger.info('Saving document...')
|
logger.info('Saving document...')
|
||||||
status = Status.CLOSING
|
status = Status.CLOSING
|
||||||
@@ -567,7 +565,7 @@ def main(driver: Chrome, logger = logging.getLogger('main')):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning('RPC communication failed', exc_info=e)
|
logger.warning('RPC communication failed', exc_info=e)
|
||||||
save()
|
save()
|
||||||
|
|
||||||
flow.deter(Acknowledge, Timeout, Close)
|
flow.deter(Acknowledge, Timeout, Close)
|
||||||
driver.switch_to.window(driver.window_handles[0])
|
driver.switch_to.window(driver.window_handles[0])
|
||||||
status = Status.IDLE
|
status = Status.IDLE
|
||||||
@@ -592,7 +590,7 @@ if __name__ == '__main__':
|
|||||||
"profile.default_content_setting_values.notifications": 2,
|
"profile.default_content_setting_values.notifications": 2,
|
||||||
"download.default_directory": args.directory,
|
"download.default_directory": args.directory,
|
||||||
})
|
})
|
||||||
|
|
||||||
with keep.presenting():
|
with keep.presenting():
|
||||||
driver = Chrome(options=opts)
|
driver = Chrome(options=opts)
|
||||||
status = main(driver)
|
status = main(driver)
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user