-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathassistant.py
executable file
·257 lines (212 loc) · 9.69 KB
/
assistant.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
#!/usr/bin/python
# -*- coding: latin-1 -*-
import datetime
import logging
import os
import sys
import time
import traceback
import urllib
import _thread
from PIL import Image
import requests
from io import BytesIO
import schedule
import telepot
from telepot.namedtuple import ReplyKeyboardMarkup, KeyboardButton
import lib
from lib.interaction import Interaction
from lib.db import Database
from lib import config
from responders import console, telegram
from behaviours import *
try:
logging.basicConfig(filename=os.path.dirname(os.path.realpath(__file__)) + '/files/assistant_debug.log', level=logging.DEBUG)
except OSError as ex:
pass
class Assistant(object):
"""The Bot Object that handles interactions and passes them to the behaviours for processing
Extends Telegram Bot
Attributes:
logging: logging object
behaviours: an object containing lists of behaviours, keyed by execution order
dir: path to application directory
files: path to application files directory
config: config key: value pairs
admin: telegram ID of admin account
"""
def __init__(self, *args, **kwargs):
""" Initialise attributes and register all behaviours """
self.logging = logging
self.__log('Starting Assistant')
self.db = Database()
self.mode = kwargs.get('mode', 'console')
self.behaviours = {}
self.dir = os.path.dirname(os.path.realpath(__file__))
self.files = self.dir + '/files'
self.config = config.Config()
self.responder = None
self.admin = self.config.get_or_request('Admin')
self.register_behaviours()
self.register_responders()
schedule.clear()
schedule.every(5).minutes.do(self.idle)
def register_responders(self):
if self.mode == 'telegram':
print('loading telegram')
self.responder = telegram.Telegram(config=self.config, files=self.files, logging=logging)
else:
print('loading console')
self.responder = console.Console(config=self.config, files=self.files, logging=logging)
def register_behaviours(self):
""" Instantiate and create reference to all behaviours as observers """
dir_path = self.dir + '/behaviours'
for path, subdirs, files in os.walk(dir_path):
for name in files:
if name.endswith('.py') and '__' not in name and name != 'behaviour.py' and 'test_' not in name:
m = name.split('.')[0]
instance = getattr(globals()[m], m.title())(db=self.db, config=self.config, dir=self.dir,
logging=logging, assistant=self) # Get instance of class
# Add to behaviours list in order of execution
if instance.execution_order not in self.behaviours:
self.behaviours[instance.execution_order] = []
self.behaviours[instance.execution_order].append(instance)
def listen(self, **kwargs): # pragma: no cover
""" Handle messages via telegram and run scheduled tasks """
self.responder.admin_message('Hello!')
self.responder.message_loop(self.handle)
self.__log('Listening ...')
# Keep the program running.
while 1:
if self.mode != 'telegram':
self.responder.message_loop(self.handle) # @todo handle in the same way as telegram, with threading
schedule.run_pending()
# self.__idle_behaviours()
time.sleep(1)
def handle(self, msg):
""" Handle messages from users (must be public for telegram) """
text = self.responder.get_text(msg)
if text == '':
return
self.__log(self.__datetime() + ': Message received: ' + text)
act = Interaction(user=[msg['chat']['id']],
command={'text': text.strip()},
config=self.config,
msg=msg, logging=logging)
if 'unittest' in sys.argv[0]:
self.__interact(act)
else:
_thread.start_new_thread(self.__interact, (act, ))
def idle(self):
""" Call idle method for each behaviour """
_thread.start_new_thread(self.__interact, (Interaction(user=self.config.get('Users').split(','),
config=self.config,
method='idle',
logging=logging), ))
def __interact(self, act):
""" Send interaction to behaviours, in order of execution.
Stop when response returned if act.finish == True
"""
if act.method != 'idle':
self.__log('Received command: ' + act.command['text'])
try:
# Try observers first
for ex_order in self.behaviours:
for behaviour in self.behaviours[ex_order]:
r = getattr(behaviour, act.method)(act) # call method specified in interaction object
if r is not None:
act.respond(r, act.user)
if len(act.response) > 0 and act.finish:
break
except Exception as e:
template = "An exception of type {0} occurred with the message '{1}'. Arguments:\n{2!r}"
message = template.format(type(e).__name__, str(e), e.args)
if (len(sys.argv) == 2 and sys.argv[1] != 'discover'): # pragma: no cover
print(traceback.print_tb(e.__traceback__))
self.__log(message)
act.respond(str(message))
if len(act.response) == 0 and act.method != 'idle':
self.__log('No match')
act.respond("I'm sorry I don't know what to say")
# Handle response(s)
if len(act.response) > 0:
if act.msg and 'voice' in act.msg and False: # disable TTS for now
# Respond with voice if audio input received
responses = act.get_response_str()
for r in responses:
lib.speech.speak(self, r['msg'])
if self.mode == 'telegram':
self.responder.sendAudio(r['user'], open(self.files + '/speech/output.mp3'))
else:
self.responder.sendAudio(r['user'], self.files + '/speech/output.mp3')
else:
# Standard text response via telegram
self.__message(act)
# Handle chained commands
for r in act.response:
if 'command' in r:
print("I think theres another command: " + str(r))
print(r)
if type(r['command']['user']) != list:
r['command']['user'] = [r['command']['user']]
for usr in r['command']['user']:
new_cmd = {'text': r['command']['text'], 'chat': {'id': usr}}
self.handle(new_cmd)
def __message(self, act):
""" Parse interaction object and convert to user friendly response message """
responses = act.get_response_str()
for r in responses:
msg = r['text']
if msg != '':
self.__log(msg)
if r['user']:
for u in r['user']:
self.__log('sending to' + str(u))
self.responder.sendMessage(u, msg, None, True)
keyboard = act.get_response_keyboard()
if keyboard is not None:
for u in act.user:
self.responder.sendMessage(u, 'Keyboard Received', reply_markup=keyboard)
# @todo modify to handle user in response object
files = act.get_response_files()
try:
if len(files) > 0:
self.__log('found file in response')
for f in files:
if f['file'] == 'photo':
self.__log('it is a photo')
if 'http' in f['path']:
path = self.files + "/temp.jpg"
urllib.request.urlretrieve(f['path'], path)
f['path'] = path
photo = open(f['path'], 'rb')
for u in act.user:
self.__log('sending photo to' + str(u))
self.responder.sendPhoto(u, photo, f['caption'])
os.remove(f['path'])
if f['file'] == 'video':
self.__log('it is a video')
video = open(f['path'], 'rb')
for u in act.user:
self.__log('sending video to' + str(u))
self.responder.sendVideo(u, video)
os.remove(f['path'])
if f['file'] == 'file':
self.__log('it is a file')
doc = open(f['path'], 'rb')
for u in act.user:
self.__log('sending document to' + str(u))
self.responder.sendDocument(u, doc)
except Exception as e:
self.__log('There was a problem with a file: ' + str(e))
if r['user']:
for u in r['user']:
self.responder.sendMessage(u, 'There was a problem with a file: ' + str(e), None, True)
@staticmethod
def __log(text):
""" Output and log text """
logging.info(Assistant.__datetime() + text)
@staticmethod
def __datetime():
""" Return readable datetime """
return datetime.datetime.now().strftime('%d-%m-%y %I:%M %p - ')