/
bogbot.py
285 lines (241 loc) · 10.9 KB
/
bogbot.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
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import argparse
import os
import sys
import traceback
from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound
import irc.bot
import irc.logging
import irc.strings
import lxml.html
import requests
from db import DatabaseConnection
from model import Hostmask, Nickname, Consumption, Consumable, URL
from spotify_lookup import SpotifyLookup
from twitter_lookup import TwitterLookup
class BogBot(irc.bot.SingleServerIRCBot):
def __init__(self, channel, nickname, realname, server, port=6667):
irc.bot.SingleServerIRCBot.__init__(self, [(server, port)], nickname, realname)
self.channel = channel
self.dbcon = DatabaseConnection()
def on_disconnect(self, c, e):
raise SystemExit()
def on_welcome(self, c, e):
c.join(self.channel)
def on_nicknameinuse(self, c, e):
c.nick(c.get_nickname() + "_")
def on_privmsg(self, c, e):
self.do_command(e, e.arguments[0])
def on_pubmsg(self, srvcon, event):
try:
if event.arguments[0].strip().startswith("!"):
self.do_command(event, event.arguments[0][1:])
else:
self.process_text(event)
except Exception as e:
exc_type, exc_obj, exc_traceback = sys.exc_info()
tb = traceback.format_list(traceback.extract_tb(exc_traceback)[-1:])[-1]
tb = ''.join(tb.splitlines())
msg = "%s: %s %s" % (exc_type, e.message, tb)
if len(msg) > 400:
msg = "%s %s %s" % (msg[:340], "...", msg[-50:])
self.connection.privmsg("jabr", msg)
def process_text(self, event):
message = event.arguments[0]
if "http" in message:
# Add nick/user/host
hostmask_id = self.add_or_update_hostmask(event.source)
start = message.find("http")
end = message.find(" ", start)
if end == -1:
url = message[start:]
else:
url = message[start:end]
if "spotify.com" in url:
spl = SpotifyLookup()
spotify_meta = spl.lookup(url)
if spotify_meta is not None:
self.connection.notice(event.target, spotify_meta)
return
elif "twitter.com" and "status" in url:
twit_lookup = TwitterLookup()
twit_meta = twit_lookup.compose_meta(url)
if twit_meta is not None:
self.connection.notice(event.target, twit_meta)
return
redirect, idn, title = self._get_url_meta(url)
# Log url
if redirect is not None:
self.add_url(redirect, title, hostmask_id, event.target)
else:
self.add_url(url, title, hostmask_id, event.target)
# Output meta
if title is not None:
url_meta = self._compose_url_meta_string(url, redirect,
idn, title)
self.connection.notice(event.target, url_meta)
def _get_url_meta(self, url):
abort, redirect, idn = self._check_headers(url)
if abort:
return None, None, None
doc = self._get_url_content(url)
title = self._get_html_title(doc)
if title is not None and title != "":
return redirect, idn, title
return None, None, None
def _compose_url_meta_string(self, url, redirect, idn, title):
meta = ""
if redirect is not None and idn is False:
meta = "%s )> " % redirect
if title is not None and title != "":
meta = "%s%s" % (meta, title)
return meta
def _get_html_title(self, doc):
"""
Parse the string representation ('document') of the web page.
"""
parsed_doc = lxml.html.fromstring(doc)
title = parsed_doc.find(".//title")
if title is not None:
title_stripped = ''.join(title.text.splitlines())
return title_stripped.strip()
def _check_headers(self, url):
"""
Check size of URL content is within limit. Also check if URL and
response URL are different, and if the response URL indicates
that the original URL is a Internationalized Domain Name (IDN).
"""
response = requests.head(url)
if response.headers is not None:
if "content-type" in response.headers:
if "text/html" not in response.headers['content-type']:
self.connection.privmsg("jabr", "No 'text/html' in headers for %s" % url)
return True, None, None
if "content-length" in response.headers:
# 5.000.000 bytes ~= 5MB
if int(response.headers['content-length']) > 5000000:
self.connection.privmsg("jabr", "Content length too long for %s" % url)
return True, None, None
else:
self.connection.privmsg("jabr", "No response headers for %s" % url)
return True, None, None
if url != response.url:
if response.url.split('://')[1].startswith('xn--'):
return False, response.url, True
return False, response.url, False
return False, None, False
def _get_url_content(self, url):
response = requests.get(url)
if response.text and response.encoding is not None:
return response.text.encode(response.encoding)
def add_or_update_hostmask(self, hostmask_str):
nick, user, host = self.parse_hostmask(hostmask_str)
hostmask_id, nick_present = self.is_nick_in_hostmask(nick, user, host)
if hostmask_id is not None:
if nick_present:
#self.connection.privmsg("jabr", "Nickname, username and hostmask already registered.")
return hostmask_id
else:
self.connection.privmsg("jabr", "Username and hostmask already registered, but not nick. Adding %s" % nick)
return self.dbcon.add_nick(nick, user, host)
else:
self.connection.privmsg("jabr", "Username and hostmask not registered. Adding %s %s %s" % (nick, user, host))
return self.dbcon.add_hostmask(nick, user, host)
def add_consumption(self, hostmask_id, consumable_str, source=None):
with self.dbcon.scoped_db_session() as session:
consumable_qr = session.query(Consumable).\
filter(Consumable.name==consumable_str).all() # One?
if len(consumable_qr) == 0:
self.connection.privmsg("jabr", "Consumable not registered. Registering.")
consumable = Consumable(consumable_str)
session.add(consumable)
elif len(consumable_qr) == 1:
self.connection.privmsg("jabr", "The consumable was found in database.")
consumable = consumable_qr[0]
else:
self.connection.privmsg("jabr","ERROR: Several consumables with same name!")
consumption = Consumption(source, consumable)
hostmask = session.query(Hostmask).get(hostmask_id)
hostmask.consumption.append(consumption)
def add_url(self, url, title, hostmask_id, channel=None):
with self.dbcon.scoped_db_session() as session:
url_qr = session.query(URL).filter(URL.url==url).all() # One?
if len(url_qr) == 0:
url_obj = URL()
#self.connection.privmsg("jabr", type(url_obj.hostmask_id))
url_obj.url = url
url_obj.title = title
url_obj.channel = channel
url_obj.hostmask_id = hostmask_id
session.add(url_obj)
msg = "URL %s added by %d" % (url, hostmask_id)
self.connection.privmsg("jabr", msg)
elif len(url_qr) == 1:
msg = "URL %s already exist" % url
self.connection.privmsg("jabr", msg)
else:
msg = "ERROR: %d instances of URL (%s) in DB" % (len(url_qr), url)
self.connection.privmsg("jabr", msg)
def do_command(self, event, cmd):
self.connection.privmsg("jabr", "%s requested command %s" % (event.source.nick, cmd))
hostmask_id = self.add_or_update_hostmask(event.source)
self.connection.privmsg("jabr", "Hostmask ID: %s" % hostmask_id)
if cmd == "kaffe":
self.add_consumption(hostmask_id, cmd, event.target)
self.connection.privmsg(event.target, "Coffee added!")
elif cmd == "brus":
self.add_consumption(hostmask_id, cmd, event.target)
self.connection.privmsg(event.target, "Brus added!")
elif cmd == "halt" and event.source == "jabr!jorabra@cringer.pludre.net":
self.die()
elif cmd == "stats":
for chname, chobj in self.channels.items():
c.notice(nick, "--- Channel statistics ---")
c.notice(nick, "Channel: " + chname)
users = chobj.users()
users.sort()
c.notice(nick, "Users: " + ", ".join(users))
opers = chobj.opers()
opers.sort()
c.notice(nick, "Opers: " + ", ".join(opers))
voiced = chobj.voiced()
voiced.sort()
c.notice(nick, "Voiced: " + ", ".join(voiced))
def parse_hostmask(self, hostmask):
nick = hostmask.split('!', 1)[0]
user_host = hostmask.split('!', 1)[1].split('@', 1)
user = user_host[0]
host = user_host[1]
return nick, user, host
def is_nick_in_hostmask(self, nick, user, host):
with self.dbcon.scoped_ro_db_session() as session:
try:
hostmask = session.query(Hostmask).\
filter(Hostmask.username==user).\
filter(Hostmask.hostname==host).one()
except MultipleResultsFound, e:
self.connection.privmsg("jabr",
"Multiple hostmasks found for username and hostname. Should not be possible: %s" % e)
except NoResultFound, e:
return None, False
if nick in (nickname.nickname for nickname in hostmask.nickname):
return hostmask.id, True
else:
return hostmask.id, False
def get_args():
parser = argparse.ArgumentParser()
parser.add_argument('server')
parser.add_argument('channel')
parser.add_argument('nickname')
parser.add_argument('realname')
parser.add_argument('-p', '--port', default=6667, type=int)
irc.logging.add_arguments(parser)
return parser.parse_args()
def main():
args = get_args()
irc.logging.setup(args)
bot = BogBot(args.channel, args.nickname, args.realname, args.server, args.port)
bot.start()
if __name__ == "__main__":
main()