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):
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 hash_string = hashlib.md5(target.encode("utf-8")).hexdigest()
187 markovfeed_name = "markovfeed_" + hash_string
188 if not os.access(markovfeed_name, os.F_OK):
189 notice("NOT ENOUGH TEXT TO MARKOV.")
191 file = open(markovfeed_name, "r")
192 lines = file.readlines()
196 line = line.replace("\n", "")
197 tokens += line.split()
198 if len(tokens) <= select_length:
199 notice("NOT ENOUGH TEXT TO MARKOV.")
201 for i in range(len(tokens) - select_length):
203 for j in range(select_length + 1):
204 token_list += [tokens[i + j]]
205 selections += [token_list]
207 for i in range(select_length):
211 new_end = markov(snippet)
212 if len(msg) + len(new_end) > 200:
215 for i in range(select_length - 1):
216 snippet[i] = snippet[i + 1]
217 snippet[select_length - 1] = new_end
218 notice(msg.lower() + "malkovich.")
220 if "addquote" == command:
222 elif "quote" == command:
224 elif "markov" == command:
228 def handle_url(url, notice, show_url=False):
230 def mobile_twitter_hack(url):
231 re1 = 'https?://(mobile.twitter.com/)[^/]+(/status/)'
232 re2 = 'https?://mobile.twitter.com/([^/]+)/status/([^\?/]+)'
233 m = re.search(re1, url)
234 if m and m.group(1) == 'mobile.twitter.com/' \
235 and m.group(2) == '/status/':
236 m = re.search(re2, url)
237 url = 'https://twitter.com/' + m.group(1) + '/status/' + m.group(2)
238 handle_url(url, notice, True)
242 r = requests.get(url, timeout=15)
243 except (requests.exceptions.TooManyRedirects,
244 requests.exceptions.ConnectionError,
245 requests.exceptions.InvalidURL,
246 requests.exceptions.InvalidSchema) as error:
247 notice("TROUBLE FOLLOWING URL: " + str(error))
249 if mobile_twitter_hack(url):
251 title = bs4.BeautifulSoup(r.text, "html.parser").title
253 prefix = "PAGE TITLE: "
255 prefix = "PAGE TITLE FOR <" + url + ">: "
256 notice(prefix + title.string.strip())
258 notice("PAGE HAS NO TITLE TAG")
263 def __init__(self, io, username, nickname, channel):
265 self.nickname = nickname
266 self.io.send_line("NICK " + self.nickname)
267 self.io.send_line("USER " + username + " 0 * : ")
268 self.io.send_line("JOIN " + channel)
272 def handle_privmsg(tokens):
274 def handle_input(msg, target):
277 self.io.send_line("NOTICE " + target + " :" + msg)
279 matches = re.findall("(https?://[^\s>]+)", msg)
280 for i in range(len(matches)):
281 handle_url(matches[i], notice)
283 tokens = msg[1:].split()
284 argument = str.join(" ", tokens[1:])
285 handle_command(tokens[0], argument, notice, target)
287 hash_string = hashlib.md5(target.encode("utf-8")).hexdigest()
288 markovfeed_name = "markovfeed_" + hash_string
289 file = open(markovfeed_name, "a")
290 file.write(msg + "\n")
294 for rune in tokens[0]:
300 for rune in tokens[2]:
306 if receiver != self.nickname:
308 msg = str.join(" ", tokens[3:])[1:]
309 handle_input(msg, target)
312 line = self.io.recv_line()
315 tokens = line.split(" ")
317 if tokens[0] == "PING":
318 self.io.send_line("PONG " + tokens[1])
319 elif tokens[1] == "PRIVMSG":
320 handle_privmsg(tokens)
323 def parse_command_line_arguments():
324 parser = argparse.ArgumentParser()
325 parser.add_argument("-s, --server", action="store", dest="server",
327 help="server or server net to connect to (default: "
329 parser.add_argument("-p, --port", action="store", dest="port", type=int,
330 default=PORT, help="port to connect to (default : "
332 parser.add_argument("-t, --timeout", action="store", dest="timeout",
333 type=int, default=TIMEOUT,
334 help="timeout in seconds after which to attempt " +
335 "reconnect (default: " + str(TIMEOUT) + ")")
336 parser.add_argument("-u, --username", action="store", dest="username",
337 default=USERNAME, help="username to use (default: "
339 parser.add_argument("-n, --nickname", action="store", dest="nickname",
340 default=NICKNAME, help="nickname to use (default: "
342 parser.add_argument("CHANNEL", action="store", help="channel to join")
343 opts, unknown = parser.parse_known_args()
347 opts = parse_command_line_arguments()
350 io = IO(opts.server, opts.port, opts.timeout)
351 session = Session(io, opts.username, opts.nickname, opts.CHANNEL)
353 except ExceptionForRestart: