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 choice
169 usable_selections = []
170 for i in range(select_length, 0, -1):
171 for selection in selections:
175 if snippet[-j] != selection[-(j+1)]:
179 usable_selections += [selection]
180 if [] != usable_selections:
182 if [] == usable_selections:
183 usable_selections = selections
184 selection = choice(usable_selections)
185 return selection[select_length]
187 hash_string = hashlib.md5(target.encode("utf-8")).hexdigest()
188 markovfeed_name = "markovfeed_" + hash_string
189 if not os.access(markovfeed_name, os.F_OK):
190 notice("NOT ENOUGH TEXT TO MARKOV.")
192 file = open(markovfeed_name, "r")
193 lines = file.readlines()
197 line = line.replace("\n", "").lower()
198 tokens += line.split()
199 if len(tokens) <= select_length:
200 notice("NOT ENOUGH TEXT TO MARKOV.")
204 url_starts = ["http://", "https://", "<http://", "<https://"]
205 for i in range(len(tokens)):
206 for url_start in url_starts:
207 if tokens[i][:len(url_start)] == url_start:
208 length = len(tokens[i])
209 if url_start[0] == "<":
211 length = tokens[i].index(">") + 1
214 urls += [tokens[i][:length]]
215 tokens[i] = url_escape + tokens[i][length:]
217 for i in range(len(tokens) - select_length):
219 for j in range(select_length + 1):
220 token_list += [tokens[i + j]]
221 selections += [token_list]
223 for i in range(select_length):
227 new_end = markov(snippet)
228 for name in session.uses_in_chan:
229 if new_end[:len(name)] == name.lower():
230 new_end = "malkovich" + new_end[len(name):]
232 if len(msg) + len(new_end) > 200:
235 for i in range(select_length - 1):
236 snippet[i] = snippet[i + 1]
237 snippet[select_length - 1] = new_end
239 index = msg.find(url_escape)
242 msg = msg.replace(url_escape, choice(urls), 1)
243 notice(msg + "malkovich.")
245 if "addquote" == command:
247 elif "quote" == command:
249 elif "markov" == command:
253 def handle_url(url, notice, show_url=False):
255 def mobile_twitter_hack(url):
256 re1 = 'https?://(mobile.twitter.com/)[^/]+(/status/)'
257 re2 = 'https?://mobile.twitter.com/([^/]+)/status/([^\?/]+)'
258 m = re.search(re1, url)
259 if m and m.group(1) == 'mobile.twitter.com/' \
260 and m.group(2) == '/status/':
261 m = re.search(re2, url)
262 url = 'https://twitter.com/' + m.group(1) + '/status/' + m.group(2)
263 handle_url(url, notice, True)
267 r = requests.get(url, timeout=15)
268 except (requests.exceptions.TooManyRedirects,
269 requests.exceptions.ConnectionError,
270 requests.exceptions.InvalidURL,
271 requests.exceptions.InvalidSchema) as error:
272 notice("TROUBLE FOLLOWING URL: " + str(error))
274 if mobile_twitter_hack(url):
276 title = bs4.BeautifulSoup(r.text, "html.parser").title
278 prefix = "PAGE TITLE: "
280 prefix = "PAGE TITLE FOR <" + url + ">: "
281 notice(prefix + title.string.strip())
283 notice("PAGE HAS NO TITLE TAG")
288 def __init__(self, io, username, nickname, channel):
290 self.nickname = nickname
291 self.channel = channel
292 self.uses_in_chan = []
293 self.io.send_line("NICK " + self.nickname)
294 self.io.send_line("USER " + username + " 0 * : ")
295 self.io.send_line("JOIN " + self.channel)
299 def handle_privmsg(tokens):
301 def handle_input(msg, target):
304 self.io.send_line("NOTICE " + target + " :" + msg)
306 matches = re.findall("(https?://[^\s>]+)", msg)
307 for i in range(len(matches)):
308 handle_url(matches[i], notice)
310 tokens = msg[1:].split()
311 argument = str.join(" ", tokens[1:])
312 handle_command(tokens[0], argument, notice, target, self)
314 hash_string = hashlib.md5(target.encode("utf-8")).hexdigest()
315 markovfeed_name = "markovfeed_" + hash_string
316 file = open(markovfeed_name, "a")
317 file.write(msg + "\n")
321 for rune in tokens[0]:
327 for rune in tokens[2]:
333 if receiver != self.nickname:
335 msg = str.join(" ", tokens[3:])[1:]
336 handle_input(msg, target)
338 def name_from_join_or_part(tokens):
339 token = tokens[0][1:]
340 index_cut = token.find("@")
341 index_ex = token.find("!")
342 if index_ex > 0 and index_ex < index_cut:
344 return token[:index_cut]
347 line = self.io.recv_line()
350 tokens = line.split(" ")
352 if tokens[0] == "PING":
353 self.io.send_line("PONG " + tokens[1])
354 elif tokens[1] == "PRIVMSG":
355 handle_privmsg(tokens)
356 elif tokens[1] == "353":
358 names[0] = names[0][1:]
359 self.uses_in_chan += names
360 elif tokens[1] == "JOIN":
361 name = name_from_join_or_part(tokens)
362 if name != self.nickname:
363 self.uses_in_chan += [name]
364 elif tokens[1] == "PART":
365 name = name_from_join_or_part(tokens)
366 del(self.uses_in_chan[self.uses_in_chan.index(name)])
368 def parse_command_line_arguments():
369 parser = argparse.ArgumentParser()
370 parser.add_argument("-s, --server", action="store", dest="server",
372 help="server or server net to connect to (default: "
374 parser.add_argument("-p, --port", action="store", dest="port", type=int,
375 default=PORT, help="port to connect to (default : "
377 parser.add_argument("-t, --timeout", action="store", dest="timeout",
378 type=int, default=TIMEOUT,
379 help="timeout in seconds after which to attempt " +
380 "reconnect (default: " + str(TIMEOUT) + ")")
381 parser.add_argument("-u, --username", action="store", dest="username",
382 default=USERNAME, help="username to use (default: "
384 parser.add_argument("-n, --nickname", action="store", dest="nickname",
385 default=NICKNAME, help="nickname to use (default: "
387 parser.add_argument("CHANNEL", action="store", help="channel to join")
388 opts, unknown = parser.parse_known_args()
392 opts = parse_command_line_arguments()
395 io = IO(opts.server, opts.port, opts.timeout)
396 session = Session(io, opts.username, opts.nickname, opts.CHANNEL)
398 except ExceptionForRestart: