Compare commits

..

14 Commits

Author SHA1 Message Date
3208850b6f Supervisor de trabajadores 2020-11-02 00:59:11 -03:00
7545495ff9 Mensaje entre trabajadores 2020-11-02 00:58:57 -03:00
626f836f8b Manejo de personas 2020-11-02 00:58:41 -03:00
3c5d3d5c52 Trabajadores de acciones 2020-11-02 00:58:23 -03:00
7fc616ab3a Trabajadores de emails 2020-11-02 00:58:02 -03:00
702f9e94ad Manejo de configuraciones 2020-11-02 00:57:23 -03:00
510df82520 Configuraciones 2020-11-02 00:57:09 -03:00
658fdbb552 Logging 2020-11-02 00:56:55 -03:00
84d2c80504 Ignore de cache 2020-11-02 00:56:14 -03:00
f355d5100f Entrada de emails 2020-11-02 00:56:01 -03:00
78cbb5e786 Punto de partida del programa 2020-11-02 00:55:06 -03:00
7bdaeae37a Ignore Logs y Data 2020-11-02 00:54:23 -03:00
b877f26ca6 Pipfile 2020-11-02 00:53:06 -03:00
6dccec26b1 Todo 2020-11-02 00:52:49 -03:00
19 changed files with 833 additions and 1 deletions

9
.gitignore vendored
View File

@ -1,5 +1,12 @@
# PyCharm
/.idea/
__pycache__/
# PipEnv
Pipfile.lock
Pipfile.lock
# Logs
/logs/
# Data
/data/

14
Pipfile Normal file
View File

@ -0,0 +1,14 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
[packages]
keyboard = "*"
beautifulsoup4 = "*"
pytz = "*"
[requires]
python_version = "3.9"

25
TODO.md Normal file
View File

@ -0,0 +1,25 @@
# TODO
## 1. Main modulo
1. Workers para
1. Revisar Email
1. Revisar WhatsApp
1. Procesar Texto
## 2. Modulo de Revision de Email
Para revisar si hay mails nuevos, validando que sea de las personas registradas.
Si no está registrada se avisa a administrador para saber que hacer.
Limpieza de Inbox y Spam.
## 3. Modulo de WhatsApp
Respuestas a mensajes.
## 4. Modulo de Recordatorio
Crear recordatorios y mandar correo o WhatsApp en el momento del recordatorio.
## 5. Modulo de Registro de Eventos
Ir llevando un registro de eventos.
### Otros
+ [ ] Establecer lenguage de interpretacion para los comandos
+ [ ] Crear AI para interpretacion de comandos en lenguage comun

105
common/helper/logger.py Normal file
View File

@ -0,0 +1,105 @@
import os
import datetime
import locale
from threading import Thread
import queue
class Logger:
def __init__(self, log_folder, timezone):
self.folder = log_folder
self.tz = timezone
self.messages = []
def load_last(self):
files = [f for f in os.listdir(self.folder) if os.path.isfile(os.path.join(self.folder, f))]
today = datetime.datetime.now(tz=self.tz)
if len(files) == 0:
self.messages = []
return
last = files[-1]
self.messages = []
with open(os.path.join(self.folder, last), 'r') as f:
self.messages.append(f.readline())
def get_filename(self):
files = [f for f in os.listdir(self.folder) if os.path.isfile(os.path.join(self.folder, f)) and 'dairy' in f]
today = datetime.datetime.now(tz=self.tz)
if len(files) == 0:
return os.path.join(self.folder, 'diary-{0}.log'.format(today.strftime('%Y-%m-%d')))
last = files[-1]
return os.path.join(self.folder, last)
def start_new(self):
today = datetime.datetime.now(tz=self.tz)
filename = os.path.join(self.folder, 'diary-{0}.log'.format(today.strftime('%Y-%m-%d')))
with open(filename, 'w') as f:
pass
def start_log(self):
today = datetime.datetime.now(tz=self.tz)
locale.setlocale(locale.LC_TIME, 'es_ES')
msg = 'Inicio un nuevo dia siendo las {0} del {1}'.format(today.strftime('%H:%M:%S'),
today.strftime('%d de %B de %Y'))
self.log(msg)
def stop_log(self):
now = datetime.datetime.now(tz=self.tz)
locale.setlocale(locale.LC_TIME, 'es_ES')
msg = 'Siendo las {0}, termino mis registros por hoy'.format(now.strftime('%H:%M:%S'))
self.log(msg)
msg = '--------'
self.log(msg)
def log_action(self, action):
today = datetime.datetime.now(tz=self.tz)
locale.setlocale(locale.LC_TIME, 'es_ES')
msg = 'A las {0} del {1}, he realizado {2}'.format(today.strftime('%H:%M:%S'),
today.strftime('%d de %B de %Y'), action)
self.log(msg)
def log_not_action(self, action):
today = datetime.datetime.now(tz=self.tz)
locale.setlocale(locale.LC_TIME, 'es_ES')
msg = 'A las {0} del {1}, no he podido realizar {2}'.format(today.strftime('%H:%M:%S'),
today.strftime('%d de %B de %Y'), action)
self.log(msg)
def log(self, message):
line = message.rstrip('.') + '.'
self.messages.append(line)
if len(self.messages) > 1000:
self.start_new()
self.load_last()
with open(self.get_filename(), 'a') as f:
f.write(line + "\n")
class Worker(Thread):
def __init__(self, params, configs):
super().__init__()
self.event = params['events']['stop']
self.queue = params['queues']['log']
self.wait = configs.get('supervisor.wait')
self.logger = Logger(params['folders']['log'], configs.get('timezone'))
self.logging = params['logging']
def run(self):
self.logging.log('Starting', caller=type(self))
while not self.event.is_set():
self.logging.log('Looping status {0}'.format(not self.event.is_set()), caller=type(self))
try:
message = self.queue.get(timeout=self.wait)
self.logging.log('Logger received message', caller=type(self))
if 'is_start' in message and message['is_start']:
self.logger.start_log()
continue
if 'not' in message and message['not']:
self.logger.log_not_action(message['action'])
continue
self.logger.log_action(message['action'])
except queue.Empty:
pass
self.logger.stop_log()
self.logging.log('Exiting', caller=type(self))
return

40
common/helper/logging.py Normal file
View File

@ -0,0 +1,40 @@
import os
import datetime
class Logging:
def __init__(self, timezone, folder=None):
self.tz = timezone
self.folder = folder
self.filename = self.get_filename()
self.log('------')
def get_filename(self):
files = [f for f in os.listdir(self.folder) if os.path.isfile(os.path.join(self.folder, f)) and 'logging' in f]
if len(files) > 0:
filename = files[-1]
if os.path.getsize(os.path.join(self.folder, filename)) < 1024 * 1024:
return filename
today = datetime.datetime.now(tz=self.tz)
filename = 'logging-{0}.log'.format(today.strftime('%Y-%m-%d'))
base_name = filename
n = 1
while os.path.isfile(os.path.join(self.folder, filename)):
filename = '-'.join([
base_name,
str(n)
])
n += 1
return filename
def log(self, message, caller=None):
now = datetime.datetime.now(tz=self.tz)
if caller is None:
msg = '[{0}] {1}.'.format(now.strftime('%Y-%m-%d %H:%M:%S'), message.strip('.'))
else:
msg = '[{0}] ({2}) {1}.'.format(now.strftime('%Y-%m-%d %H:%M:%S'), message.strip('.'), caller)
print(msg)
if self.folder is not None:
filename = os.path.join(self.folder, self.filename)
with open(filename, 'a') as f:
f.write(msg + "\n")

7
config/email.json Normal file
View File

@ -0,0 +1,7 @@
{
"imap_server": "imap.yandex.com",
"port": 993,
"username": "secretary@incoviba.cl",
"password": "quzshqzyfcnydevp",
"ssl": true
}

3
config/supervisor.json Normal file
View File

@ -0,0 +1,3 @@
{
"wait": 15
}

72
entry/email/inbox.py Normal file
View File

@ -0,0 +1,72 @@
import argparse
import imaplib
import email
# Email class that fetches emails from server by uid and can parse body for secretary, can also delete email by uid
class Email:
def __init__(self, uid):
self.uid = uid
self.message = ''
def get(self, imap):
status, raw_data = imap.uid('fetch', self.uid, '(RFC822)')
if status != 'OK':
raise Exception('Could not recover message {0}'.format(self.uid))
self.message = email.message_from_bytes(raw_data[0][1])
def delete(self, imap):
status, result = imap.uid('STORE', self.uid, '+FLAGS', '(\\Deleted)')
if status != 'OK':
raise Exception('Could not flag message {0}'.format(self.uid))
def connect(imap_url, port, username, password, ssl=False):
if ssl:
server = imaplib.IMAP4_SSL(imap_url, port=port)
else:
server = imaplib.IMAP4(imap_url, port=port)
server.login(username, password)
return server
def check_inbox(server):
status, msg = server.select('INBOX')
if status != 'OK':
return None
status, ids = server.uid('search', None, 'All')
if status != 'OK':
return None
ids = ids[0].decode().split()
emails = []
for mid in ids:
em = Email(mid)
em.get(server)
emails.append(em)
return emails
def main(args):
print('Connecting')
imap_server = connect(imap_url=args.url, port=args.port, username=args.username, password=args.password,
ssl=args.ssl)
print('Checking emails')
emails = check_inbox(imap_server)
print(len(emails))
[print(e) for e in emails]
imap_server.close()
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('-u', '--url')
parser.add_argument('-p', '--port')
parser.add_argument('-un', '--username')
parser.add_argument('-up', '--password')
parser.add_argument('--ssl')
_args = parser.parse_args()
main(_args)

53
entry/main.py Normal file
View File

@ -0,0 +1,53 @@
import argparse
import sys
import os
import pytz
from common.helper.logging import Logging
from setup.config import load_config
from src.supervisor import Supervisor
from src.bosses import Bosses
def main(args):
configs = load_config(args.config_folder)
configs.set('timezone', pytz.timezone('America/Santiago'))
params = {
'folders': {
'config': args.config_folder,
'log': args.log_folder,
'data': args.data_folder
},
'bosses': Bosses(args.data_folder),
'logging': Logging(configs.get('timezone'), args.log_folder)
}
params['logging'].log('Starting', caller='main')
supervisor = Supervisor(configs=configs, params=params)
supervisor.register_worker('common.helper.logger', 'Worker')
supervisor.add_queue('log')
supervisor.register_worker('src.email', 'RevisorEmailWorker')
supervisor.register_worker('src.email', 'EmailInterpreter')
supervisor.add_queue('emails')
supervisor.add_queue('questions')
supervisor.register_worker('src.action.questions', 'QuestionWorker')
supervisor.register_worker('src.action.reminder', 'RemindRegisterWorker')
supervisor.register_worker('src.action.reminder', 'ReminderWorker')
supervisor.add_queue('reminders')
supervisor.add_lock('reminders')
supervisor.start()
supervisor.join()
params['logging'].log('Waiting for Supervisor', caller='main')
# sys.exit()
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('-c', '--config_folder', default=os.path.join(os.path.realpath('../'), 'config'))
parser.add_argument('-d', '--data_folder', default=os.path.join(os.path.realpath('..'), 'data'))
parser.add_argument('-l', '--log_folder', default=os.path.join(os.path.realpath('..'), 'logs'))
_args = parser.parse_args()
main(_args)

70
setup/config.py Normal file
View File

@ -0,0 +1,70 @@
import os
import json
class ConfigFile:
def __init__(self, name):
self.name = name
self._data = []
def load(self, folder):
filename = os.path.join(folder, self.name + '.json')
with open(filename, 'r') as f:
self._data = json.load(f)
def get(self, name):
if '.' not in name and name in self._data:
return self._data[name]
subs = "']['".join(name.split('.'))
substr = "self._data['" + subs + "']"
print(substr)
class Config:
def __init__(self, folder):
self.folder = folder
self._configs = {}
def load(self):
files = [f for f in os.listdir(self.folder) if os.path.isfile(os.path.join(self.folder, f)) and '.json' in f]
configs = {}
for f in files:
name = f.split('.')[0]
filename = os.path.join(self.folder, f)
with open(filename, 'r') as fh:
configs[name] = json.load(fh)
self._configs = configs
def get(self, name):
if '.' not in name and name in self._configs:
return self._configs[name]
subs = "']['".join(name.split('.'))
substr = "self._configs['" + subs + "']"
try:
sub = eval(substr)
return sub
except:
return None
def get_all(self, name_list):
output = {}
for name in name_list:
output[name] = self.get(name)
return output
def set(self, name, value):
if '.' not in name:
self._configs[name] = value
return
subs = "']['".join(name.split('.'))
substr = "self._configs['" + subs + "'] = {0}".format(value)
try:
eval(substr)
except:
pass
def load_config(folder):
config = Config(folder)
config.load()
return config

44
src/action/questions.py Normal file
View File

@ -0,0 +1,44 @@
import os
from threading import Thread
import queue
import json
class QuestionWorker(Thread):
def __init__(self, params, configs):
super(QuestionWorker, self).__init__()
self.queue = params['queues']['questions']
self.logging = params['logging']
self.logger = params['queues']['log']
self.event = params['events']['stop']
self.wait = configs.get('supervisor.wait')
self.folder = params['folders']['data']
def question(self, text):
filename = os.path.join(self.folder, 'questions.json')
with open(filename, 'w+', encoding='utf8') as f:
try:
data = json.load(f)
except json.decoder.JSONDecodeError:
data = []
if text in data:
return
data.append(text)
json.dump(data, f, ensure_ascii=False, indent=4)
self.logging.log(text, type(self))
def run(self):
self.logging.log('Starting', type(self))
self.logger.put({'action': type(self)})
while not self.event.is_set():
try:
question = self.queue.get(timeout=self.wait)
if question.type == 'email':
self.question(
'¿Que hago con este correo de {0} y el texto dice {1} en la fecha {2}?'.
format(question.sender, question.text, question.datetime.strftime('%d-%m-%Y %H:%M:%S'))
)
except queue.Empty:
pass
self.logging.log('Exiting', type(self))
return

112
src/action/reminder.py Normal file
View File

@ -0,0 +1,112 @@
import datetime
import queue
from threading import Thread
import os
import json
import time
class Reminder:
def __init__(self, timezone):
self.datetime = 0
self.message = ''
self.recursive = False
self.last = 0
self.repeats = 0
self.tz = timezone
def should_remind(self):
now = datetime.datetime.now(tz=self.tz)
event = self.get_remind_time()
dif = event - now
if dif.days * 24 * 60 * 60 + dif.seconds == 0:
return True
return False
def get_remind_time(self):
if self.recursive:
event = self.last + self.datetime
return event
return self.datetime
def remind(self):
# Do remind
if self.recursive:
self.last = datetime.datetime.now(tz=self.tz)
self.repeats -= 1
class RemindRegisterWorker(Thread):
def __init__(self, params, configs):
super().__init__()
self.event = params['events']['stop']
self.queue = params['queues']['reminders']
self.lock = params['locks']['reminders']
self.folder = params['folders']['data']
self.logging = params['logging']
self.wait = configs.get('supervisor.wait')
def add_reminder(self, reminder):
self.lock.acquire()
filename = os.path.join(self.folder, 'reminders.json')
with open(filename, 'w+') as f:
reminders = json.load(f)
reminders.append(reminder)
json.dump(reminders, f)
self.lock.release()
def run(self):
self.logging.log('Starting', caller=type(self))
while not self.event.is_set():
self.logging.log('Looping status {0}'.format(not self.event.is_set()),
caller=type(self))
try:
reminder = self.queue.get(timeout=self.wait)
self.add_reminder(reminder)
except queue.Empty:
pass
self.logging.log('Exiting', caller=type(self))
return
class ReminderWorker(Thread):
def __init__(self, params, configs):
super().__init__()
self.event = params['events']['stop']
self.folder = params['folders']['data']
self.lock = params['locks']['reminders']
self.logging = params['logging']
self.wait = configs.get('supervisor.wait')
self.tz = configs.get('timezone')
def check_reminders(self):
self.lock.acquire()
filename = os.path.join(self.folder, 'reminders.json')
if not os.path.isfile(filename):
self.lock.release()
return
with open(filename, 'w+') as f:
reminders = json.load(f)
new_reminders = []
for r in reminders:
reminder = Reminder(self.tz)
for k, val in r.items():
setattr(reminder, k, val)
if reminder.should_remind():
reminder.remind()
if reminder.recursive and reminder.repeats > 0:
new_reminders.append(reminder)
else:
new_reminders.append(reminder)
json.dump(new_reminders, f)
self.lock.release()
def run(self):
self.logging.log('Starting', caller=type(self))
while not self.event.is_set():
self.logging.log('Looping status {0}'.format(not self.event.is_set()),
caller=type(self))
self.check_reminders()
time.sleep(self.wait)
self.logging.log('Exiting', caller=type(self))
return

40
src/address_book.py Normal file
View File

@ -0,0 +1,40 @@
import os
import json
class Contact:
def __init__(self):
self.names = ''
self.last_name = ''
self.maiden_name = ''
self.emails = []
self.phones = []
def full_name(self):
return ' '.join([self.names, self.last_name, self.maiden_name])
class AddressBook:
def __init__(self, data_folder):
filename = os.path.join(data_folder, 'address_book.json')
with open(filename, 'r') as f:
data = json.load(f)
self.contacts = []
for c in data:
contact = Contact()
for k, val in c.items():
setattr(contact, k, val)
self.contacts.append(contact)
def find(self, name):
for c in self.contacts:
if c.full_name() == name or c.names == name or c.last_name == name:
return c
return None
def find_id(self, name):
for (k, c) in enumerate(self.contacts):
if c.full_name() == name or c.names == name or c.last_name == name:
return k
return -1

37
src/bosses.py Normal file
View File

@ -0,0 +1,37 @@
import os
import json
from src.address_book import AddressBook
class Boss:
def __init__(self):
self.full_name = ''
self.aliases = []
self.contact = None
class Bosses:
def __init__(self, data_folder):
filename = os.path.join(data_folder, 'bosses.json')
with open(filename, 'r') as f:
data = json.load(f)
self.bosses = []
addrs = AddressBook(data_folder)
for b in data:
boss = Boss()
for k, val in b.items():
setattr(boss, k, val)
boss.contact = addrs.find(boss.full_name)
self.bosses.append(boss)
def is_boss(self, name):
for boss in self.bosses:
if boss.full_name in name:
return True
for m in boss.contact.emails:
if m in name:
return True
for a in boss.aliases:
if a in name:
return True
return False

2
src/email/__init__.py Normal file
View File

@ -0,0 +1,2 @@
from .email_interpreter import EmailInterpreter
from .revisor_worker import RevisorEmailWorker

View File

@ -0,0 +1,10 @@
from threading import Thread
class EmailInterpreter(Thread):
def __init__(self, configs, params):
super().__init__()
self.logging = params['logging']
def run(self):
self.logging.log('Starting', type(self))

View File

@ -0,0 +1,90 @@
from threading import Thread
import re
import time
import email.utils
from bs4 import BeautifulSoup
from entry.email.inbox import connect, check_inbox
from src.text.message import Message
class RevisorEmailWorker(Thread):
def __init__(self, configs, params):
super().__init__()
self._url = configs.get('email.imap_server')
self._port = configs.get('email.port')
self._username = configs.get('email.username')
self._password = configs.get('email.password')
self._ssl = configs.get('email.ssl')
self.queue = params['queues']['emails']
self.questions = params['queues']['questions']
self.event = params['events']['stop']
self._wait = configs.get('supervisor.wait')
self._bosses = params['bosses']
self._logger = params['queues']['log']
self.logging = params['logging']
self.revisados = []
def revisado(self, uid):
if not self.check_revisado(uid):
self.revisados.append(uid)
def check_revisado(self, uid):
if uid in self.revisados:
return True
return False
def run(self):
self.logging.log('Starting', type(self))
self._logger.put({'action': 'Inicio jornada trabajador Revisor Email'})
while not self.event.is_set():
self.logging.log('Looping status {0}'.format(not self.event.is_set()), type(self))
self.logging.log('Connecting to Email Server', type(self))
imap = connect(imap_url=self._url, port=self._port, username=self._username, password=self._password,
ssl=self._ssl)
self.logging.log('Getting emails', type(self))
emails = check_inbox(imap)
if emails is not None:
c = 0
p = 0
for em in emails:
if self.check_revisado(em.uid):
continue
sender = em.message['from']
text = ' '.join([em.message['subject'] + '.'] + self.build_message(em.message))
msg = Message('email', text=text, original=em, sender=sender,
datetime=email.utils.parsedate_to_datetime(em.message['Date']))
if not self._bosses.is_boss(sender):
self.logging.log('Sender {0} is not a boss'.format(sender), type(self))
self.revisado(em.uid)
self.questions.put(msg)
p += 1
continue
self.queue.put(msg)
self.revisado(em.uid)
c += 1
self.logging.log('{0} emails checked'.format(c), type(self))
if c > 0:
self._logger.put({'action': 'Revise {0} nuevos correos'.format(c)})
self.logging.log('{0} emails pending'.format(p), type(self))
if p > 0:
self._logger.put({'action': 'Tengo dudas en {0} correos'.format(p)})
imap.close()
time.sleep(self._wait)
self.logging.log('Exiting', type(self))
return
def build_message(self, email_part):
output = []
if email_part.is_multipart():
for part in email_part.get_payload():
output.append(self.build_message(part))
else:
html = email_part.get_payload(decode=True)
bs = BeautifulSoup(html, 'html.parser')
if bs.body:
html = bs.body.get_text()
else:
html = bs.get_text()
html = re.sub(' +', ' ', re.sub("\n+", ' ', html)).strip(' ')
output.append(html)
return output

93
src/supervisor.py Normal file
View File

@ -0,0 +1,93 @@
from threading import Thread, Event, Lock
from queue import Queue
import keyboard
import time
import importlib
class Supervisor(Thread):
def __init__(self, configs, params):
super().__init__()
self._configs = configs
self.params = params
self._workers = []
self._status = []
self._registry = {}
self.add_event('stop')
def add_worker(self, worker):
self._workers.append(worker)
def start_workers(self):
[w.start() for w in self._workers]
[self._status.append(True) for w in self._workers]
def check_workers(self):
stopped = 0
for (k, w) in enumerate(self._workers):
if not self._status[k]:
stopped += 1
continue
if not w.is_alive():
self.params['logging'].log('Worker {0} stopped'.format(type(w)))
self.params['queues']['log'].put({'not': True, 'action': type(w)})
stopped += 1
self._status[k] = False
if stopped == len(self._workers):
return False
return True
def join_workers(self):
[w.join(self._configs.get('supervisor.wait')) for w in self._workers]
def add_queue(self, name):
if 'queues' not in self.params or self.params['queues'] is None:
self.params['queues'] = {}
self.params['queues'][name] = Queue()
def add_event(self, name):
if 'events' not in self.params or self.params['events'] is None:
self.params['events'] = {}
self.params['events'][name] = Event()
def add_lock(self, name):
if 'locks' not in self.params or self.params['locks'] is None:
self.params['locks'] = {}
self.params['locks'][name] = Lock()
def register_worker(self, module, name):
if module not in self._registry:
self._registry[module] = []
self._registry[module].append(name)
def run(self):
for (module_name, ws) in self._registry.items():
module = importlib.import_module(module_name)
for w in ws:
worker = getattr(module, w)
self.add_worker(worker(configs=self._configs, params=self.params))
self.add_worker(Thread(target=exit_thread, args=(self.params['events']['stop'], self.params['logging'])))
self.params['queues']['log'].put({'is_start': True})
self.start_workers()
while not self.params['events']['stop'].is_set():
self.params['logging'].log('Looping main status {0}'.format(not self.params['events']['stop'].is_set()),
caller=type(self))
if not self.check_workers():
break
self.params['logging'].log('Waiting {0} secs'.format(self._configs.get('supervisor.wait')),
caller=type(self))
time.sleep(self._configs.get('supervisor.wait'))
self.params['logging'].log('Exiting', caller=type(self))
self.join_workers()
self.params['logging'].log('Waiting for Workers', caller=type(self))
return
def exit_thread(event, logging):
logging.log('Starting exit thread', caller='exit_thread')
keyboard.wait('Esc')
logging.log('Escape pressed', caller='exit_thread')
event.set()
logging.log('Exit signal sent', caller='exit_thread')

8
src/text/message.py Normal file
View File

@ -0,0 +1,8 @@
class Message:
def __init__(self, mtype, sender, datetime, text, original):
self.type = mtype
self.sender = sender
self.datetime = datetime
self.text = text
self.checked = False
self.original = original