16 # Defaults, may be overwritten by command line arguments.
17 SERVER = "irc.freenode.net"
20 USERNAME = "plomlombot"
24 class ExceptionForRestart(Exception):
30 def __init__(self, server, port, timeout):
31 self.timeout = timeout
32 self.socket = socket.socket()
33 self.socket.connect((server, port))
34 self.socket.setblocking(0)
37 self.last_pong = time.time()
38 self.servername = self.recv_line(send_ping=False).split(" ")[0][1:]
40 def _pingtest(self, send_ping=True):
41 if self.last_pong + self.timeout < time.time():
42 print("SERVER NOT ANSWERING")
43 raise ExceptionForRestart
45 self.send_line("PING " + self.servername)
47 def send_line(self, msg):
48 msg = msg.replace("\r", " ")
49 msg = msg.replace("\n", " ")
50 if len(msg.encode("utf-8")) > 510:
51 print("NOT SENT LINE TO SERVER (too long): " + msg)
52 print("LINE TO SERVER: "
53 + str(datetime.datetime.now()) + ": " + msg)
57 while total_sent_len < msg_len:
58 sent_len = self.socket.send(bytes(msg[total_sent_len:], "UTF-8"))
60 print("SOCKET CONNECTION BROKEN")
61 raise ExceptionForRestart
62 total_sent_len += sent_len
64 def _recv_line_wrapped(self, send_ping=True):
65 if len(self.line_buffer) > 0:
66 return self.line_buffer.pop(0)
68 ready = select.select([self.socket], [], [], int(self.timeout / 2))
70 self._pingtest(send_ping)
72 self.last_pong = time.time()
73 received_bytes = self.socket.recv(1024)
75 received_runes = received_bytes.decode("UTF-8")
76 except UnicodeDecodeError:
77 received_runes = received_bytes.decode("latin1")
78 if len(received_runes) == 0:
79 print("SOCKET CONNECTION BROKEN")
80 raise ExceptionForRestart
81 self.rune_buffer += received_runes
82 lines_split = str.split(self.rune_buffer, "\r\n")
83 self.line_buffer += lines_split[:-1]
84 self.rune_buffer = lines_split[-1]
85 if len(self.line_buffer) > 0:
86 return self.line_buffer.pop(0)
88 def recv_line(self, send_ping=True):
89 line = self._recv_line_wrapped(send_ping)
91 print("LINE FROM SERVER " + str(datetime.datetime.now()) + ": " +
96 def handle_command(command, argument, notice, target, session):
97 hash_string = hashlib.md5(target.encode("utf-8")).hexdigest()
98 quotesfile_name = "quotes_" + hash_string
101 if not os.access(quotesfile_name, os.F_OK):
102 quotesfile = open(quotesfile_name, "w")
103 quotesfile.write("QUOTES FOR " + target + ":\n")
105 quotesfile = open(quotesfile_name, "a")
106 quotesfile.write(argument + "\n")
108 quotesfile = open(quotesfile_name, "r")
109 lines = quotesfile.readlines()
111 notice("ADDED QUOTE #" + str(len(lines) - 1))
116 notice("SYNTAX: !quote [int] OR !quote search QUERY")
117 notice("QUERY may be a boolean grouping of quoted or unquoted " +
118 "search terms, examples:")
119 notice("!quote search foo")
120 notice("!quote search foo AND (bar OR NOT baz)")
121 notice("!quote search \"foo\\\"bar\" AND ('NOT\"' AND \"'foo'\"" +
127 tokens = argument.split(" ")
128 if (len(tokens) > 1 and tokens[0] != "search") or \
129 (len(tokens) == 1 and
130 (tokens[0] == "search" or not tokens[0].isdigit())):
133 if not os.access(quotesfile_name, os.F_OK):
134 notice("NO QUOTES AVAILABLE")
136 quotesfile = open(quotesfile_name, "r")
137 lines = quotesfile.readlines()
142 if i == 0 or i > len(lines):
143 notice("THERE'S NO QUOTE OF THAT INDEX")
146 elif len(tokens) > 1:
147 query = str.join(" ", tokens[1:])
149 results = plomsearch.search(query, lines)
150 except plomsearch.LogicParserError as err:
151 notice("FAILED QUERY PARSING: " + str(err))
153 if len(results) == 0:
154 notice("NO QUOTES MATCHING QUERY")
156 for result in results:
157 notice("QUOTE #" + str(result[0] + 1) + " : " + result[1])
160 i = random.randrange(len(lines))
161 notice("QUOTE #" + str(i + 1) + ": " + lines[i])
164 from random import shuffle
169 usable_selections = []
170 for i in range(select_length, 0, -1):
171 for selection in selections:
174 if snippet[j] != selection[j]:
178 usable_selections += [selection]
179 if [] != usable_selections:
181 if [] == usable_selections:
182 usable_selections = selections
183 shuffle(usable_selections)
184 return usable_selections[0][select_length]
186 def purge_present_users(tokens):
187 for name in session.uses_in_chan:
190 del(tokens[tokens.index(name)])
195 hash_string = hashlib.md5(target.encode("utf-8")).hexdigest()
196 markovfeed_name = "markovfeed_" + hash_string
197 if not os.access(markovfeed_name, os.F_OK):
198 notice("NOT ENOUGH TEXT TO MARKOV.")
200 file = open(markovfeed_name, "r")
201 lines = file.readlines()
205 line = line.replace("\n", "")
206 tokens += line.split()
207 tokens = purge_present_users(tokens)
208 if len(tokens) <= select_length:
209 notice("NOT ENOUGH TEXT TO MARKOV.")
211 for i in range(len(tokens) - select_length):
213 for j in range(select_length + 1):
214 token_list += [tokens[i + j]]
215 selections += [token_list]
217 for i in range(select_length):
221 new_end = markov(snippet)
222 if len(msg) + len(new_end) > 200:
225 for i in range(select_length - 1):
226 snippet[i] = snippet[i + 1]
227 snippet[select_length - 1] = new_end
228 notice(msg.lower() + "malkovich.")
230 if "addquote" == command:
232 elif "quote" == command:
234 elif "markov" == command:
238 def handle_url(url, notice, show_url=False):
240 def mobile_twitter_hack(url):
241 re1 = 'https?://(mobile.twitter.com/)[^/]+(/status/)'
242 re2 = 'https?://mobile.twitter.com/([^/]+)/status/([^\?/]+)'
243 m = re.search(re1, url)
244 if m and m.group(1) == 'mobile.twitter.com/' \
245 and m.group(2) == '/status/':
246 m = re.search(re2, url)
247 url = 'https://twitter.com/' + m.group(1) + '/status/' + m.group(2)
248 handle_url(url, notice, True)
252 r = requests.get(url, timeout=15)
253 except (requests.exceptions.TooManyRedirects,
254 requests.exceptions.ConnectionError,
255 requests.exceptions.InvalidURL,
256 requests.exceptions.InvalidSchema) as error:
257 notice("TROUBLE FOLLOWING URL: " + str(error))
259 if mobile_twitter_hack(url):
261 title = bs4.BeautifulSoup(r.text, "html.parser").title
263 prefix = "PAGE TITLE: "
265 prefix = "PAGE TITLE FOR <" + url + ">: "
266 notice(prefix + title.string.strip())
268 notice("PAGE HAS NO TITLE TAG")
273 def __init__(self, io, username, nickname, channel):
275 self.nickname = nickname
276 self.channel = channel
277 self.uses_in_chan = []
278 self.io.send_line("NICK " + self.nickname)
279 self.io.send_line("USER " + username + " 0 * : ")
280 self.io.send_line("JOIN " + self.channel)
284 def handle_privmsg(tokens):
286 def handle_input(msg, target):
289 self.io.send_line("NOTICE " + target + " :" + msg)
291 matches = re.findall("(https?://[^\s>]+)", msg)
292 for i in range(len(matches)):
293 handle_url(matches[i], notice)
295 tokens = msg[1:].split()
296 argument = str.join(" ", tokens[1:])
297 handle_command(tokens[0], argument, notice, target, self)
299 hash_string = hashlib.md5(target.encode("utf-8")).hexdigest()
300 markovfeed_name = "markovfeed_" + hash_string
301 file = open(markovfeed_name, "a")
302 file.write(msg + "\n")
306 for rune in tokens[0]:
312 for rune in tokens[2]:
318 if receiver != self.nickname:
320 msg = str.join(" ", tokens[3:])[1:]
321 handle_input(msg, target)
323 def name_from_join_or_part(tokens):
324 token = tokens[0][1:]
325 index_cut = token.find("@")
326 index_ex = token.find("!")
327 if index_ex > 0 and index_ex < index_cut:
329 return token[:index_cut]
332 line = self.io.recv_line()
335 tokens = line.split(" ")
337 if tokens[0] == "PING":
338 self.io.send_line("PONG " + tokens[1])
339 elif tokens[1] == "PRIVMSG":
340 handle_privmsg(tokens)
341 elif tokens[1] == "353":
343 names[0] = names[0][1:]
344 self.uses_in_chan += names
345 elif tokens[1] == "JOIN":
346 name = name_from_join_or_part(tokens)
347 if name != self.nickname:
348 self.uses_in_chan += [name]
349 elif tokens[1] == "PART":
350 name = name_from_join_or_part(tokens)
351 del(self.uses_in_chan[self.uses_in_chan.index(name)])
353 def parse_command_line_arguments():
354 parser = argparse.ArgumentParser()
355 parser.add_argument("-s, --server", action="store", dest="server",
357 help="server or server net to connect to (default: "
359 parser.add_argument("-p, --port", action="store", dest="port", type=int,
360 default=PORT, help="port to connect to (default : "
362 parser.add_argument("-t, --timeout", action="store", dest="timeout",
363 type=int, default=TIMEOUT,
364 help="timeout in seconds after which to attempt " +
365 "reconnect (default: " + str(TIMEOUT) + ")")
366 parser.add_argument("-u, --username", action="store", dest="username",
367 default=USERNAME, help="username to use (default: "
369 parser.add_argument("-n, --nickname", action="store", dest="nickname",
370 default=NICKNAME, help="nickname to use (default: "
372 parser.add_argument("CHANNEL", action="store", help="channel to join")
373 opts, unknown = parser.parse_known_args()
377 opts = parse_command_line_arguments()
380 io = IO(opts.server, opts.port, opts.timeout)
381 session = Session(io, opts.username, opts.nickname, opts.CHANNEL)
383 except ExceptionForRestart: