├── .github └── robot.jpg ├── .gitignore ├── README.md ├── context └── __init__.py ├── jarviscalendar └── __init__.py ├── jarvismailer ├── __init__.py └── signature.txt ├── jarvismeetingnotes └── __init__.py ├── jarvismeetingscheduler ├── __init__.py └── meeting-proposal-email.txt ├── jarvisweather └── __init__.py ├── main.py └── nlphelpers └── __init__.py /.github/robot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lorey/totally-not-jarvis/c6f77278cec8519e44779bb5981929b8f002e9a2/.github/robot.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | config.py 2 | 3 | __pycache__ 4 | .idea -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Not a picture of Jarvis](.github/robot.jpg) 2 | 3 | # Totally not Jarvis - a personal assistant bot 4 | 5 | This is a personal assistant bot built with [Telegram](https://github.com/python-telegram-bot/python-telegram-bot). I've attempted to lay the foundations for a framework that allows you to switch intents by issuing new commands while not forgetting about old intents. An example: 6 | 7 | ``` 8 | Jarvis: Karl, what are you planning to do today? 9 | Karl: I have no idea. How is the weather? 10 | Jarvis: The weather is fine. 11 | Jarvis: So what are you planning to do today? 12 | Karl: I'm going to the beach. 13 | Jarvis: Oh, a very good idea. 14 | ``` 15 | 16 | Note: Most other bots would forget about their initial question (plans for the day) if you issue a new command. But not mine :) 17 | 18 | Current skills: 19 | - Ask me to take memos during meetings and remind me later (based on iCAL calendar entries) 20 | - Send emails that propose dates for a meeting 21 | - Check the weather (based on [OpenWeatherMap](https://openweathermap.org/api)) 22 | - Check the next calendar entry (iCal integration that works with Google Calendar) 23 | 24 | Features: 25 | - Context classes that encapsulate dialogs, e.g. checking the weather. 26 | - no hard dependency on Telegram, so I can use a voice interface like Alexa or Google Home later 27 | 28 | Skills I'd love to implement: 29 | - smart scheduling: automatically derive possible timeslots when scheduling a meeting or use something like doodle 30 | - get recommendations to keep in touch with people you have not contacted for a while (hubspot integration) 31 | - send meeting reminders via email or sms (based on calendar and hubspot contacts) 32 | - log current position on demand 33 | - save reminders 34 | - let others talk to the bot and play with it 35 | - send contacts your current phone number, etc. 36 | - delay tasks (e.g. meeting memos: delay so that the dialog reappears after 10 minutes) 37 | - email meeting memos to participants 38 | - create drafts of emails via smtp 39 | 40 | 41 | ## Example of meeting time proposals 42 | The command `/schedule` triggers the creation of an meeting proposal that is sent to all participants after you have answered all necessary questions. It looks like this: 43 | 44 | > Hello, 45 | > 46 | > this is Jarvis, personal assistant of Karl Lorey. For your appointment ('Learning more about Jarvis, my personal 47 | > assistant') I propose a time slot of 60 minutes on one of the following dates: 48 | > - Tomorrow 49 | > - Friday 50 | > 51 | > Currently, I just send the proposals, so please hit 'reply all' to get Karl Lorey back into the loop. 52 | > __________ 53 | > Jarvis 54 | > Personal Assistant of Karl Lorey 55 | > 56 | > You can find my code at https://github.com/lorey/totally-not-jarvis 57 | 58 | 59 | ## The config file: config.py 60 | 61 | The config file should look like this (not included for obvious reasons): 62 | 63 | ```python 64 | # names 65 | BOT_NAME = 'Jarvis' 66 | OWNER_NAME = 'Karl Lorey' 67 | POSITION = {'lat': 49.01, 'lon': 8.4} 68 | 69 | # telegram 70 | TELEGRAM_TOKEN = '' 71 | TELEGRAM_CHAT_ID = '' 72 | 73 | # weather 74 | OPENWEATHERMAP_API_KEY = '' 75 | 76 | # calendar (name: url) 77 | ICAL_URLS = { 78 | "private": "https://calendar.google.com/calendar/ical/exampleexampleexample/basic.ics", 79 | "work": "https://calendar.google.com/calendar/ical/justanexample/public/basic.ics", 80 | } 81 | 82 | # email 83 | EMAIL_ADDRESS_JARVIS = 'jarvis@mail.com' 84 | EMAIL_ADDRESS_CC = 'your@mail.com' 85 | EMAIL_SMTP_HOST = 'www.server.com' 86 | EMAIL_SMTP_PORT = 465 87 | EMAIL_IMAP_USER = 'jarvis' 88 | EMAIL_IMAP_PASSWORD = 'captainwho?' 89 | 90 | 91 | ``` 92 | 93 | 94 | ## Implement own functionality 95 | 96 | To implement own skills, you just have to extend the base class and make sure a command puts the context on top of the contexts stack. We'll start with a `RepeatContext` that just repeats your input: 97 | 98 | ```python 99 | class RepeatContext(BaseContext): 100 | def is_done(self): 101 | return True # executed only once 102 | 103 | def process(self, bot, update): 104 | bot.send_message(chat_id=update.message.chat_id, text=update.message.text.replace('/repeat ', '') 105 | ``` 106 | 107 | The base class only has two methods that you need to implement: 108 | - `process` receives all user input while the context is active (`is_done() == False`). You can basically do whatever you want as long as you want. Messages can be accessed with `update.message.text`. 109 | - `is_done` ist self-explanatory. If true, the context will be removed and the context before that will start again. 110 | 111 | Afterwards, you just have to check for the command and open the context: 112 | 113 | ```python 114 | class DefaultContext(BaseContext): 115 | # ... 116 | 117 | def process(self, bot, update): 118 | if update.message is not None and update.message.text.startswith('/repeat '): 119 | # starts the repeat context 120 | context = RepeatContext() # crate context 121 | self.jarvis.contexts.append(context) # put it on the stack 122 | context.process(bot, update) # start by processing for the first time 123 | else: 124 | bot.send_message(chat_id=update.message.chat_id, text="I don't understand") 125 | ``` 126 | 127 | # Also check my other HubSpot-related projects 128 | 129 | If you came here for the HubSpot integration, make sure to check out my other projects: 130 | 131 | - [awesome-hubspot: Tools and libraries for HubSpot](https://github.com/lorey/awesome-hubspot) 132 | - [hubspot-contact-import: A HubSpot import tool for vcards and Xing](https://github.com/lorey/hubspot-contact-import) 133 | - [hubspot-reporting: tool to generate diagrams from your HubSpot data](https://github.com/lorey/hubspot-reporting) 134 | -------------------------------------------------------------------------------- /context/__init__.py: -------------------------------------------------------------------------------- 1 | import config 2 | 3 | 4 | class BaseContext(object): 5 | name = None 6 | 7 | def is_done(self): 8 | raise Exception('implementation missing') 9 | 10 | def process(self, bot, update): 11 | """ 12 | Processes a given input (update) from the user. 13 | """ 14 | raise Exception('implementation missing') 15 | 16 | def start(self, bot): 17 | """ 18 | This is how an external trigger starts a context. As such, there is no input. 19 | """ 20 | raise Exception('implementation missing') 21 | 22 | 23 | class StateContext(BaseContext): 24 | current_context = None 25 | 26 | def __init__(self): 27 | self.current_context = self.decide_next_context() 28 | 29 | def process(self, bot, update): 30 | # pass input and process 31 | self.current_context.process(bot, update) 32 | 33 | # check if done and load next state 34 | if self.current_context.is_done(): 35 | self.current_context = self.decide_next_context(self.current_context) 36 | 37 | # start new context 38 | if self.current_context is not None: 39 | self.current_context.start(bot) 40 | 41 | def is_done(self): 42 | return self.current_context is None 43 | 44 | def decide_next_context(self, last_context=None): 45 | """ 46 | Decide what context to open next. 47 | :param last_context: the previous context 48 | :return: the next context object to process 49 | """ 50 | raise Exception('implementation missing') 51 | 52 | 53 | class QuestionAnswerContext(BaseContext): 54 | """ 55 | Ask a question and receive the user's answer. 56 | """ 57 | 58 | question = None 59 | answer = None 60 | 61 | def __init__(self, question): 62 | self.question = question 63 | 64 | def process(self, bot, update): 65 | self.answer = update.message.text 66 | 67 | def start(self, bot): 68 | bot.send_message(chat_id=config.TELEGRAM_CHAT_ID, text=self.question) 69 | 70 | def is_done(self): 71 | return self.answer is not None 72 | -------------------------------------------------------------------------------- /jarviscalendar/__init__.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import requests 3 | from icalendar import Calendar 4 | from icalendar import Event 5 | 6 | import config 7 | 8 | 9 | def get_events(): 10 | urls = config.ICAL_URLS 11 | events = [] 12 | for name, url in urls.items(): 13 | response = requests.get(url) 14 | cal = Calendar.from_ical(response.content.decode('utf-8')) 15 | calendar_events = [IcalEvent(comp) for comp in cal.subcomponents if type(comp) == Event] 16 | events.extend(calendar_events) 17 | return events 18 | 19 | 20 | def get_next_event(): 21 | next_event = None 22 | 23 | events = [e for e in get_events() if not e.is_full_day()] 24 | for event in events: 25 | if next_event is None or next_event.get_start() > event.get_start() > datetime.datetime.now(tz=datetime.timezone.utc): 26 | next_event = event 27 | 28 | return next_event 29 | 30 | 31 | class IcalEvent(object): 32 | ical_event = None 33 | 34 | def __init__(self, ical): 35 | self.ical_event = ical 36 | 37 | def is_full_day(self): 38 | return type(self.ical_event['dtstart'].dt) == datetime.date 39 | 40 | def get_start(self): 41 | return self.ical_event['dtstart'].dt 42 | 43 | def get_end(self): 44 | return self.ical_event['dtend'].dt 45 | 46 | def get_summary(self): 47 | return self.ical_event['summary'] 48 | 49 | def get_description(self): 50 | return self.ical_event['description'] 51 | 52 | def get_attendee_emails(self): 53 | if 'attendee' in self.ical_event: 54 | return [str(attendee).replace('mailto:', '') for attendee in self.ical_event['attendee']] 55 | return [] 56 | -------------------------------------------------------------------------------- /jarvismailer/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import smtplib 3 | from email.mime.multipart import MIMEMultipart 4 | from email.mime.text import MIMEText 5 | 6 | import telegram 7 | 8 | import config 9 | from context import BaseContext 10 | 11 | SIGNATURE_FILE = 'signature.txt' 12 | 13 | 14 | class Email(object): 15 | subject = None 16 | message = None 17 | to = [] 18 | 19 | 20 | class MailerContext(BaseContext): 21 | email = None 22 | is_done_ = False 23 | 24 | def __init__(self, email: Email): 25 | self.email = email 26 | 27 | def is_done(self): 28 | return self.is_done_ 29 | 30 | def start(self, bot): 31 | bot.send_message(chat_id=config.TELEGRAM_CHAT_ID, text='I\'m about to send the following email:') 32 | bot.send_message(chat_id=config.TELEGRAM_CHAT_ID, text=self.email.__dict__) 33 | 34 | keyboard = [['Yes', 'No']] 35 | reply_markup = telegram.ReplyKeyboardMarkup(keyboard) 36 | bot.send_message(chat_id=config.TELEGRAM_CHAT_ID, text='Would you like to send this email?', 37 | reply_markup=reply_markup) 38 | 39 | def process(self, bot, update): 40 | if update.message.text == 'Yes': 41 | print('Trying to send a mail') 42 | print(self.email.__dict__) 43 | send_email(self.email) 44 | bot.send_message(chat_id=update.message.chat_id, text='Okay, I have sent the mail successfully.', 45 | reply_markup=telegram.ReplyKeyboardRemove()) 46 | else: 47 | bot.send_message(chat_id=update.message.chat_id, text='Okay, I will not send a mail.', 48 | reply_markup=telegram.ReplyKeyboardRemove()) 49 | 50 | self.is_done_ = True 51 | 52 | 53 | def send_email(email): 54 | from_address = config.EMAIL_ADDRESS_JARVIS 55 | to_addresses = email.to 56 | cc_addresses = [config.EMAIL_ADDRESS_CC] 57 | 58 | # compose message 59 | msg = MIMEMultipart() 60 | 61 | msg['From'] = from_address 62 | msg['To'] = ','.join(to_addresses) 63 | msg['Cc'] = ','.join(cc_addresses) 64 | msg['Subject'] = email.subject 65 | 66 | message_content = '\n'.join([email.message, get_signature()]) 67 | mime_text = MIMEText(message_content, 'plain') 68 | msg.attach(mime_text) 69 | 70 | with smtplib.SMTP_SSL(config.EMAIL_SMTP_HOST, config.EMAIL_SMTP_PORT) as s: 71 | # todo works for me but not for everyone 72 | s.ehlo() 73 | s.login(config.EMAIL_IMAP_USER, config.EMAIL_IMAP_PASSWORD) 74 | 75 | s.send_message(msg) 76 | 77 | 78 | def get_signature(): 79 | dir = os.path.dirname(__file__) 80 | filename = os.path.join(dir, SIGNATURE_FILE) 81 | with open(filename, 'r') as file: 82 | signature = file.read() 83 | return signature % {'bot': config.BOT_NAME, 'owner': config.OWNER_NAME} -------------------------------------------------------------------------------- /jarvismailer/signature.txt: -------------------------------------------------------------------------------- 1 | __________ 2 | %(bot)s 3 | Personal Assistant of %(owner)s 4 | 5 | You can find my code at https://github.com/lorey/totally-not-jarvis -------------------------------------------------------------------------------- /jarvismeetingnotes/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import telegram 4 | 5 | import config 6 | from context import BaseContext 7 | from main import Jarvis 8 | 9 | 10 | class MeetingNotesContext(BaseContext): 11 | jarvis = None 12 | event = None 13 | 14 | state = None 15 | is_done_ = False 16 | 17 | def __init__(self, event, jarvis: Jarvis): 18 | self.event = event 19 | self.jarvis = jarvis 20 | 21 | def start(self, bot): 22 | self.state = 'asking' 23 | self.ask_if_desired(bot) 24 | 25 | def process(self, bot, update): 26 | if self.state is None: 27 | # start was not used. try to start nonetheless but issue an error 28 | self.start(bot) 29 | logging.error('start was not used, incoming message is ignored.') 30 | return 31 | 32 | if self.state == 'asking': 33 | self.process_asking(bot, update) 34 | elif self.state == 'memo': 35 | self.process_memo(bot, update) 36 | else: 37 | raise RuntimeError('unknown state: %s' % self.state) 38 | 39 | def process_memo(self, bot, update): 40 | notes = update.message.text 41 | bot.send_message(chat_id=update.message.chat_id, text='Your message has been saved. I will remind you later.') 42 | print('Notes for %s: %s' % (self.event.get_summary(), notes)) 43 | 44 | # add reminder 45 | reminder_text = 'Your notes for %s:\n' % self.event.get_summary() 46 | reminder_text += notes 47 | self.jarvis.add_reminder(reminder_text, 60) 48 | 49 | self.is_done_ = True 50 | 51 | def process_asking(self, bot, update): 52 | if update.message.text == 'Yes': 53 | bot.send_message(chat_id=config.TELEGRAM_CHAT_ID, text='Starting to record notes for this meeting:', 54 | reply_markup=telegram.ReplyKeyboardRemove()) 55 | self.state = 'memo' 56 | elif update.message.text in ['No', 'Later']: 57 | bot.send_message(chat_id=config.TELEGRAM_CHAT_ID, text='Okay.', reply_markup=telegram.ReplyKeyboardRemove()) 58 | self.is_done_ = True 59 | else: 60 | # didn't understand answer, ask again 61 | self.ask_if_desired(bot) 62 | 63 | def ask_if_desired(self, bot): 64 | keyboard = [['Yes', 'No'], ['Later']] 65 | reply_markup = telegram.ReplyKeyboardMarkup(keyboard) 66 | bot.send_message(chat_id=config.TELEGRAM_CHAT_ID, text='Would you like to take notes?', 67 | reply_markup=reply_markup) 68 | 69 | def is_done(self): 70 | return self.is_done_ 71 | -------------------------------------------------------------------------------- /jarvismeetingscheduler/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from time import sleep 3 | 4 | import config 5 | import jarvismailer 6 | import nlphelpers 7 | from context import BaseContext, StateContext, QuestionAnswerContext 8 | 9 | 10 | class MeetingProposal(object): 11 | who = [] 12 | when = None 13 | why = None 14 | length = None 15 | 16 | def get_message(self): 17 | # load template 18 | dir = os.path.dirname(__file__) 19 | filename = os.path.join(dir, 'meeting-proposal-email.txt') 20 | with open(filename, 'r') as file: 21 | template = file.read() 22 | 23 | # fill template 24 | data = { 25 | 'why': self.why, 26 | 'length': self.length, 27 | 'list_of_dates': '\n'.join(['- %s' % when for when in self.when]), 28 | 'owner': config.OWNER_NAME, 29 | 'bot': config.BOT_NAME, 30 | } 31 | message = template % data 32 | 33 | return message 34 | 35 | def __str__(self): 36 | return 'MeetingProposal: ' + str(self.__dict__) 37 | 38 | 39 | class ScheduleMeetingContext(StateContext): 40 | jarvis = None 41 | 42 | meeting_proposal = None 43 | 44 | entities = { 45 | 'participants': { 46 | 'question': 'Who should attend the meeting? (list of emails of participants)' 47 | }, 48 | 'proposed_time': { 49 | 'question': 'When should the meeting take place?' 50 | }, 51 | 'occasion': { 52 | 'question': 'What\'s the occasion?' 53 | }, 54 | 'length': { 55 | 'question': 'How much time should participants block? (in minutes)' 56 | }, 57 | } 58 | processed_entities = [] 59 | current_entity = None 60 | 61 | def __init__(self, jarvis): 62 | super().__init__() 63 | self.jarvis = jarvis 64 | self.meeting_proposal = MeetingProposal() 65 | 66 | def start(self, bot): 67 | bot.send_message(chat_id=config.TELEGRAM_CHAT_ID, text='Okay, let\'s schedule a meeting.') 68 | self.current_context.start(bot) 69 | 70 | def decide_next_context(self, last_context=None): 71 | # process last context 72 | if last_context is not None: 73 | self.process_finished_context(last_context) 74 | self.processed_entities.append(last_context.name) 75 | 76 | # instantiate next context 77 | next_context = None 78 | 79 | next_entity_key = self.get_next_entity() 80 | if next_entity_key is not None: 81 | question = self.entities[next_entity_key]['question'] 82 | 83 | next_context = QuestionAnswerContext(question) 84 | next_context.name = next_entity_key 85 | elif 'email' not in self.processed_entities: 86 | # mail has not been sent. 87 | next_context = self.create_email_context() 88 | next_context.name = 'email' 89 | 90 | return next_context 91 | 92 | def create_email_context(self): 93 | email = jarvismailer.Email() 94 | email.subject = self.meeting_proposal.why 95 | email.message = self.meeting_proposal.get_message() 96 | email.to = self.meeting_proposal.who 97 | next_context = jarvismailer.MailerContext(email) 98 | return next_context 99 | 100 | def get_next_entity(self): 101 | unprocessed_entities = self.get_unprocessed_entities() 102 | return next(iter(unprocessed_entities), None) 103 | 104 | def get_unprocessed_entities(self): 105 | return [entity for entity in self.entities if entity not in self.processed_entities] 106 | 107 | def process_finished_context(self, context): 108 | if context.name == 'participants': 109 | participants = nlphelpers.extract_list(context.answer) 110 | self.meeting_proposal.who = participants 111 | elif context.name == 'proposed_time': 112 | self.meeting_proposal.when = nlphelpers.extract_list(context.answer) 113 | elif context.name == 'occasion': 114 | self.meeting_proposal.why = context.answer 115 | elif context.name == 'length': 116 | self.meeting_proposal.length = int(context.answer) 117 | -------------------------------------------------------------------------------- /jarvismeetingscheduler/meeting-proposal-email.txt: -------------------------------------------------------------------------------- 1 | Hello, 2 | 3 | this is %(bot)s, personal assistant of %(owner)s. For your appointment ('%(why)s') I propose a time slot of %(length)d minutes on one of the following dates: 4 | %(list_of_dates)s 5 | 6 | Currently, I just send the proposals, so please hit 'reply all' to get %(owner)s back into the loop. -------------------------------------------------------------------------------- /jarvisweather/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import datetime 4 | import requests 5 | 6 | import config 7 | from context import BaseContext 8 | 9 | 10 | class WeatherContext(BaseContext): 11 | def process(self, bot, update): 12 | message = self.fetch_weather() 13 | bot.send_message(chat_id=update.message.chat_id, text=message) 14 | 15 | def fetch_weather(self): 16 | url_template = 'http://api.openweathermap.org/data/2.5/forecast?lat=%f&lon=%f&APPID=%s' 17 | url = url_template % (config.POSITION['lat'], config.POSITION['lon'], config.OPENWEATHERMAP_API_KEY) 18 | response = requests.get(url) 19 | json_plain = response.content.decode('utf-8') 20 | data = json.loads(json_plain) 21 | # print(json.dumps(data, indent=2)) 22 | 23 | msg = 'The weather forecast for %s:\n' % data['city']['name'] 24 | for forecast in data['list'][0:8]: 25 | time = datetime.datetime.fromtimestamp(forecast['dt']).strftime('%H:%M') 26 | description = forecast['weather'][0]['description'] 27 | temperature_celsius = forecast['main']['temp'] - 273.15 28 | humidity = forecast['main']['humidity'] 29 | 30 | msg += '%s: %s (%d°C, %d%%)\n' % (time, description, temperature_celsius, humidity) 31 | 32 | return msg 33 | 34 | def is_done(self): 35 | return True -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import logging 3 | 4 | from telegram.ext import CommandHandler 5 | from telegram.ext import Filters 6 | from telegram.ext import MessageHandler 7 | from telegram.ext import Updater 8 | 9 | import config 10 | import jarviscalendar 11 | import jarvismeetingnotes 12 | import jarvismeetingscheduler 13 | import jarvisweather 14 | from context import BaseContext 15 | 16 | EVENT_TRIGGER_INTERVAL_IN_SECONDS = 60 * 5 17 | 18 | 19 | class Jarvis(object): 20 | name = 'Jarvis' 21 | contexts = [] 22 | default_context = None 23 | 24 | updater = None 25 | 26 | def __init__(self, updater: Updater): 27 | self.default_context = DefaultContext(self) 28 | self.updater = updater 29 | 30 | def hello(self, bot, update): 31 | bot.send_message(chat_id=update.message.chat_id, text="I'm totally not %s! Talk to me nonetheless." % self.name) 32 | 33 | def receive(self, bot, update): 34 | # decide what to do with update 35 | print('received: %s' % update.message.text) 36 | 37 | # get context 38 | if len(self.contexts) > 0: 39 | applicable_context = self.contexts[-1] 40 | else: 41 | # use default context 42 | applicable_context = self.default_context 43 | 44 | applicable_context.process(bot, update) 45 | 46 | # remove completed contexts 47 | for context in self.contexts: 48 | if context.is_done(): 49 | self.contexts.remove(context) 50 | 51 | def events(self, bot, job): 52 | # avoid interrupting running context 53 | if len(self.contexts) > 0: 54 | return 55 | 56 | next_event = jarviscalendar.get_next_event() 57 | time_to_event = next_event.get_start() - datetime.datetime.now(tz=datetime.timezone.utc) 58 | if time_to_event.total_seconds() < EVENT_TRIGGER_INTERVAL_IN_SECONDS: 59 | # send event message 60 | msg = 'Next event: %s\n' % next_event.get_summary() 61 | msg += next_event.get_description() 62 | bot.send_message(chat_id=config.TELEGRAM_CHAT_ID, text=msg) 63 | 64 | # start notes context 65 | notes_context = jarvismeetingnotes.MeetingNotesContext(next_event, self) 66 | notes_context.start(bot) 67 | self.contexts.append(notes_context) 68 | 69 | def add_reminder(self, text, after_minutes): 70 | when = datetime.datetime.now() + datetime.timedelta(minutes=after_minutes) 71 | self.updater.job_queue.run_once(self.send_reminder_callback, when, context=text) 72 | 73 | def send_reminder_callback(self, bot, job): 74 | bot.send_message(chat_id=config.TELEGRAM_CHAT_ID, text=job.context) 75 | 76 | 77 | class DefaultContext(BaseContext): 78 | jarvis = None 79 | 80 | def __init__(self, jarvis): 81 | self.jarvis = jarvis 82 | 83 | def process(self, bot, update): 84 | if update.message is not None and update.message.text.startswith('/weather'): 85 | context = jarvisweather.WeatherContext() 86 | self.jarvis.contexts.append(context) 87 | context.start(bot) 88 | elif update.message is not None and update.message.text.startswith('/schedule'): 89 | context = jarvismeetingscheduler.ScheduleMeetingContext(self.jarvis) 90 | self.jarvis.contexts.append(context) 91 | context.start(bot) 92 | else: 93 | bot.send_message(chat_id=update.message.chat_id, text="I don't understand") 94 | 95 | 96 | def main(): 97 | updater = Updater(token=config.TELEGRAM_TOKEN) 98 | print(updater.bot.get_me()) 99 | 100 | logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO) 101 | 102 | jarvis = Jarvis(updater) 103 | 104 | interval = datetime.timedelta(seconds=EVENT_TRIGGER_INTERVAL_IN_SECONDS) 105 | updater.job_queue.run_repeating(jarvis.events, interval, first=0) 106 | 107 | handlers = [ 108 | CommandHandler('start', jarvis.hello), 109 | MessageHandler(Filters.all, jarvis.receive) 110 | ] 111 | 112 | dispatcher = updater.dispatcher 113 | for handler in handlers: 114 | dispatcher.add_handler(handler) 115 | 116 | updater.start_polling() 117 | updater.idle() 118 | 119 | 120 | if __name__ == '__main__': 121 | main() 122 | -------------------------------------------------------------------------------- /nlphelpers/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def extract_list(text): 5 | return text.replace(', and', ', ').replace('and ', ', ').split(', ') 6 | 7 | def extract_emails(text): 8 | # extract emails 9 | email_regex = re.compile('[\w\.-]+@[\w\.-]+') 10 | emails = email_regex.findall(text) 11 | return emails --------------------------------------------------------------------------------