/
Chatbot.py
287 lines (260 loc) · 11.5 KB
/
Chatbot.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
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
import getpass
import re
import logging.handlers
import sys
import time
from ChatExchange.chatexchange.client import Client
from ChatExchange.chatexchange.browser import LoginError
from ChatExchange.chatexchange.events import MessagePosted, MessageEdited
from ChatExchange.chatexchange.messages import Message
from fixedfont import fixed_font_to_normal, is_fixed_font
from Config import Config
import ModuleManifest
from Module import MetaModule
from ConsoleCommandHandler import ConsoleCommandHandler
import SaveIO
from SaveIO import DuplicateDirectoryException
class Chatbot:
def __init__(self):
self.room = None
self.client = None
self.privileged_users = []
self.owners = []
self.owner_name = ""
self.chatbot_name = ""
self.enabled = True
self.suspended_until = -1
self.running = True
self.site = ""
self.owner_ids = []
self.privileged_user_ids = []
self.save_subdirs = ['main']
self.modules = MetaModule(ModuleManifest.module_file_names, self, 'all')
try:
SaveIO.set_subdirs(self.save_subdirs)
except DuplicateDirectoryException as e:
if "-q" not in sys.argv:
print("[Chatbot] WARNING: there are modules with the same save directory: " + str(e))
SaveIO.create_if_not_exists(SaveIO.data_dir)
del self.save_subdirs
duplicates = self.get_duplicate_commands()
if duplicates and "-q" not in sys.argv:
print('[Chatbot] WARNING: there are commands with the same name: ' + str(duplicates))
def main(self, config_data, additional_general_config):
if "owners" in Config.General:
self.owners = Config.General["owners"]
else:
sys.exit("[Chatbot] FATAL: no owners found. Please update Config.py.")
if "privileged_users" in config_data:
self.privileged_users = config_data["privileged_users"]
if "github" in Config.General:
self.github = Config.General["github"]
else:
self.github = "https://github.com/ProgramFOX/SE-Chatbot"
if "owner_name" in Config.General:
self.owner_name = Config.General["owner_name"]
else:
sys.exit("[Chatbot] FATAL: no owner name found. Please update Config.py.")
if "chatbot_name" in Config.General:
self.chatbot_name = Config.General["chatbot_name"]
else:
sys.exit("[Chatbot] FATAL: no chatbot name found. Please update Config.py.")
# self.setup_logging() # if you want to have logging, un-comment this line
if "site" in config_data:
self.site = config_data["site"]
print("Site: %s" % self.site)
else:
self.site = input("Site: ")
for o in self.owners:
if self.site in o:
self.owner_ids.append(o[self.site])
if len(self.owner_ids) < 1:
sys.exit("[Chatbot] FATAL: no owners found for this site: %s." % self.site)
for p in self.privileged_users:
if self.site in p:
self.privileged_user_ids.append(p[self.site])
if "room" in config_data:
room_number = config_data["room"]
print("Room number: %i" % room_number)
else:
room_number = int(input("Room number: "))
if "prefix" in config_data:
self.prefix = config_data["prefix"]
else:
self.prefix = '>>'
print("Prefix: %s" % self.prefix)
if "email" in Config.General:
email = Config.General["email"]
elif "email" in additional_general_config:
email = additional_general_config["email"]
else:
email = input("Email address: ")
self.client = Client(self.site)
# Setting the timeout down to 5 fixes random SSL errors when terminating.
# The bot's timeout on exit is 5; the request timeout is 30 by default. Requests overrun the
# bot timeout, get force-closed, and cause errors.
self.client._br.request_timeout = 5.0
try:
if "password" in Config.General: # I would not recommend to store the password in Config.py
password = Config.General["password"]
self.client.login(email, password)
elif "password" in additional_general_config:
password = additional_general_config["password"]
self.client.login(email, password)
else:
for attempts in range(3):
try:
password = getpass.getpass("Password: ")
self.client.login(email, password)
break
except LoginError:
if attempts < 2:
print("Incorrect password.")
else:
raise
except LoginError:
sys.exit("[Chatbot] FATAL: Incorrect password, shutting down.")
self.room = self.client.get_room(room_number)
self.room.join()
if "message" not in additional_general_config:
bot_message = "Bot started."
else:
bot_message = additional_general_config["message"]
if bot_message is not None:
self.room.send_message(bot_message)
on_loads = self.modules.get_on_load_methods()
for on_load in on_loads:
on_load(self)
self.room.watch_socket(self.on_event)
while self.running:
inputted = input("<< ")
if inputted.strip() == "":
continue
if inputted.startswith("$") and len(inputted) > 2:
command_in = inputted[2:]
cmd_handler = ConsoleCommandHandler(self, inputted[1] == "+", self.prefix + command_in)
event_mock = type('MockEvent', (), {})()
user_mock = type('', (), {})()
user_mock.id = -1
event_mock.user = user_mock
event_mock.message = cmd_handler
command_out = self.command(command_in, cmd_handler, event_mock, 0)
if command_out is not False and command_out is not None:
cmd_handler.reply(command_out)
else:
self.room.send_message(inputted)
def get_duplicate_commands(self):
checked_cmds = []
dupe_cmds = []
all_cmds = self.modules.list_commands()
for command in all_cmds:
if command.name not in checked_cmds:
checked_cmds.append(command.name)
else:
if command.name not in dupe_cmds:
dupe_cmds.append(command.name)
return dupe_cmds
def setup_logging(self): # logging method taken from ChatExchange/examples/chat.py
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
logger.setLevel(logging.DEBUG)
# In addition to the basic stderr logging configured globally
# above, we'll use a log file for chatexchange.client.
wrapper_logger = logging.getLogger('chatexchange.client')
wrapper_handler = logging.handlers.TimedRotatingFileHandler(
filename='client.log',
when='midnight', delay=True, utc=True, backupCount=7,
)
wrapper_handler.setFormatter(logging.Formatter(
"%(asctime)s: %(levelname)s: %(threadName)s: %(message)s"
))
wrapper_logger.addHandler(wrapper_handler)
def check_existence_and_chars(self, cmd_name, content):
cmd_list = self.modules.list_commands()
allowed = -1
disallowed = -1
for cmd in cmd_list:
if cmd.name == cmd_name or (cmd.aliases is not None and cmd_name in cmd.aliases):
allowed = cmd.allowed_chars
disallowed = cmd.disallowed_chars
break
if allowed == -1:
return False, False
for c in content:
if disallowed is not None and c in disallowed:
return True, False
if allowed is not None and c not in allowed:
return True, False
return True, True
def requires_special_arg_parsing(self, cmd_name):
cmd_list = self.modules.list_commands()
for cmd in cmd_list:
if cmd.name == cmd_name:
return cmd.special_arg_parsing is not None
return False
def do_special_arg_parsing(self, cmd_name, full_cmd):
cmd_list = self.modules.list_commands()
for cmd in cmd_list:
if cmd.name == cmd_name and cmd.special_arg_parsing is not None:
return cmd.special_arg_parsing(full_cmd)
return False
def on_event(self, event, client):
if ((not self.enabled or self.suspended_until > time.time()) and event.user.id not in self.owner_ids) \
or not self.running:
return
watchers = self.modules.get_event_watchers()
for w in watchers:
w(event, client, self)
if not (isinstance(event, MessagePosted) or isinstance(event, MessageEdited) or str(type(event)).find('Chatbot.MockEvent') > -1):
return
if event.user.id == self.client.get_me().id:
return
if isinstance(event, MessageEdited):
message = Message(event.message.id, client)
else:
message = event.message
content_source = message.content_source
content = content_source
fixed_font = is_fixed_font(content)
if fixed_font:
fixed_font = True
content = fixed_font_to_normal(content)
content = re.sub(r"^%s\s+" % self.prefix, self.prefix, content)
content = re.sub("(^[^ \r\n]+)(\r?\n)", r"\1 ", content)
if not fixed_font:
stripped_content = re.sub(r"\s+", " ", content)
stripped_content = stripped_content.strip()
else:
stripped_content = content
parts = stripped_content.split(" ")
if not parts[0].startswith(self.prefix):
return
cmd_args = stripped_content[len(self.prefix):]
if self.requires_special_arg_parsing(cmd_args.split(" ")[0]):
cmd_args = content[len(self.prefix):]
output = self.command(cmd_args, message, event, 0)
if output is not False and output is not None:
output_with_reply = ":%i %s" % (message.id, output)
if len(output_with_reply) > 500 and "\n" not in output_with_reply:
message.reply("Output would be longer than 500 characters (the limit for single-line messages), so only the first 500 characters are posted now.")
message.reply(output[:500 - (len(message.id) + 2)])
else:
message.reply(output, False)
def command(self, cmd, msg, event, start):
cmd_args = cmd.split(' ')
cmd_name = cmd_args[0].lower()
args = cmd_args[1:]
exists, allowed = self.check_existence_and_chars(cmd_name, ' '.join(args))
if not exists:
return "Command not found."
if not allowed:
return "Command contains invalid characters."
if self.requires_special_arg_parsing(cmd_name):
args = self.do_special_arg_parsing(cmd_name, cmd)
if args is False:
return "Argument parsing failed."
return self.modules.command(cmd_name, args, msg, event)
def bot_stopping(self):
on_stops = self.modules.get_on_stop_methods()
for on_stop in on_stops:
on_stop(self)