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])
163 if "addquote" == command:
165 elif "quote" == command:
169 def handle_url(url, notice, show_url=False):
171 def mobile_twitter_hack(url):
172 re1 = 'https?://(mobile.twitter.com/)[^/]+(/status/)'
173 re2 = 'https?://mobile.twitter.com/([^/]+)/status/([^\?/]+)'
174 m = re.search(re1, url)
175 if m and m.group(1) == 'mobile.twitter.com/' \
176 and m.group(2) == '/status/':
177 m = re.search(re2, url)
178 url = 'https://twitter.com/' + m.group(1) + '/status/' + m.group(2)
179 handle_url(url, notice, True)
183 r = requests.get(url, timeout=15)
184 except (requests.exceptions.TooManyRedirects,
185 requests.exceptions.ConnectionError,
186 requests.exceptions.InvalidURL,
187 requests.exceptions.InvalidSchema) as error:
188 notice("TROUBLE FOLLOWING URL: " + str(error))
190 if mobile_twitter_hack(url):
192 title = bs4.BeautifulSoup(r.text).title
194 prefix = "PAGE TITLE: "
196 prefix = "PAGE TITLE FOR <" + url + ">: "
197 notice(prefix + title.string.strip())
199 notice("PAGE HAS NO TITLE TAG")
204 def __init__(self, io, username, nickname, channel):
206 self.nickname = nickname
207 self.io.send_line("NICK " + self.nickname)
208 self.io.send_line("USER " + username + " 0 * : ")
209 self.io.send_line("JOIN " + channel)
213 def handle_privmsg(tokens):
215 def handle_input(msg, target):
218 self.io.send_line("NOTICE " + target + " :" + msg)
220 matches = re.findall("(https?://[^\s>]+)", msg)
221 for i in range(len(matches)):
222 handle_url(matches[i], notice)
224 tokens = msg[1:].split()
225 argument = str.join(" ", tokens[1:])
226 handle_command(tokens[0], argument, notice, target)
229 for rune in tokens[0]:
235 for rune in tokens[2]:
241 if receiver != self.nickname:
243 msg = str.join(" ", tokens[3:])[1:]
244 handle_input(msg, target)
247 line = self.io.recv_line()
250 tokens = line.split(" ")
252 if tokens[0] == "PING":
253 self.io.send_line("PONG " + tokens[1])
254 elif tokens[1] == "PRIVMSG":
255 handle_privmsg(tokens)
258 def parse_command_line_arguments():
259 parser = argparse.ArgumentParser()
260 parser.add_argument("-s, --server", action="store", dest="server",
262 help="server or server net to connect to (default: "
264 parser.add_argument("-p, --port", action="store", dest="port", type=int,
265 default=PORT, help="port to connect to (default : "
267 parser.add_argument("-t, --timeout", action="store", dest="timeout",
268 type=int, default=TIMEOUT,
269 help="timeout in seconds after which to attempt " +
270 "reconnect (default: " + str(TIMEOUT) + ")")
271 parser.add_argument("-u, --username", action="store", dest="username",
272 default=USERNAME, help="username to use (default: "
274 parser.add_argument("-n, --nickname", action="store", dest="nickname",
275 default=NICKNAME, help="nickname to use (default: "
277 parser.add_argument("CHANNEL", action="store", help="channel to join")
278 opts, unknown = parser.parse_known_args()
282 opts = parse_command_line_arguments()
285 io = IO(opts.server, opts.port, opts.timeout)
286 session = Session(io, opts.username, opts.nickname, opts.CHANNEL)
288 except ExceptionForRestart: