cobalt.py

changeset 73
d67cc4fbc3f1
parent 72
2266d6d73de3
child 92
da291d9426ea
equal deleted inserted replaced
72:2266d6d73de3 73:d67cc4fbc3f1
26 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 26 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
27 THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 ''' 28 '''
29 29
30 import asyncore 30 import asyncore
31 import socket
32 import time
33 import sys 31 import sys
34 import traceback 32 import traceback
35 import re
36 import urllib
37 import urllib2
38 import hgapi
39 import os 33 import os
40 import math
41 import json
42 from datetime import datetime
43 import modulecore as ModuleCore 34 import modulecore as ModuleCore
44 import configfile 35 import configfile as ConfigFile
45 from configfile import Config 36 from configfile import Config
46 import hgpoll as HgPoll 37 import hgpoll as HgPoll
47 import bt as Bt 38 import bt as Bt
39 import irc as Irc
40
41 if __name__ != '__main__':
42 raise ImportError ('cobalt may not be imported as a module')
48 43
49 g_BotActive = False 44 g_BotActive = False
50 45
46 #
47 # Exception handling
48 #
49 def handle_exception(excType, excValue, trace):
50 excepterm (traceback.format_exception(excType, excValue, trace))
51
52 def excepterm (data):
53 for segment in data:
54 for line in segment.splitlines():
55 print line
56 Irc.broadcast (line)
57
58 for client in Irc.all_clients:
59 if len(data) > 0:
60 client.exceptdie()
61 else:
62 client.quit_irc()
63
64 if g_BotActive:
65 restart_self()
66 else:
67 quit()
68
69 def restart_self():
70 os.execl (sys.executable, sys.executable, * sys.argv)
71
51 def main(): 72 def main():
52 ModuleCore.init_data() 73 global g_BotActive
74 sys.excepthook = handle_exception
75 all_clients = []
76 g_BotActive = False
53 77
54 try: 78 try:
55 uid = os.geteuid() 79 uid = os.geteuid()
56 except: 80 except:
57 uid = -1 81 uid = -1
58 82
59 if uid == 0 and raw_input ('Do you seriously want to run cobalt as root? [y/N] ') != 'y': 83 if uid == 0 and raw_input ('Do you seriously want to run cobalt as root? [y/N] ') != 'y':
60 quit() 84 quit()
61 85
62 configfile.init() 86 ConfigFile.init()
63 g_admins = Config.get_value ('admins', default=[]) 87 ModuleCore.init_data()
64 g_mynick = Config.get_value ('nickname', default='cobalt') 88 Bt.init()
89 HgPoll.init()
65 90
66 try: 91 try:
67 autoconnects = Config.get_value ('autoconnect', []) 92 autoconnects = Config.get_value ('autoconnect', [])
68 93
69 if len (autoconnects) == 0: 94 if len (autoconnects) == 0:
71 quit() 96 quit()
72 97
73 for aconn in autoconnects: 98 for aconn in autoconnects:
74 for conndata in Config.get_nodelist ('connections'): 99 for conndata in Config.get_nodelist ('connections'):
75 if conndata.get_value ('name') == aconn: 100 if conndata.get_value ('name') == aconn:
76 irc_client (conndata, 0) 101 Irc.irc_client (conndata, 0)
77 break 102 break
78 else: 103 else:
79 raise ValueError ("unknown autoconnect entry %s" % (aconn)) 104 raise ValueError ("unknown autoconnect entry %s" % (aconn))
80 105
81 g_BotActive = True 106 g_BotActive = True
82 asyncore.loop() 107 asyncore.loop()
83 except KeyboardInterrupt: 108 except KeyboardInterrupt:
84 for client in all_clients: 109 for client in Irc.all_clients:
85 client.keyboardinterrupt() 110 client.keyboardinterrupt()
86 quit() 111 quit()
87 112 except Irc.RestartError as e:
88 # 113 excepterm (e.message)
89 # irc_client flags
90 #
91 CLIF_CONNECTED = (1 << 1)
92
93 #
94 # List of all clients
95 #
96 all_clients = []
97
98 class channel (object):
99 name = ""
100 password = ""
101
102 def __init__ (self, name):
103 self.name = name
104
105 #
106 # Prints a line to log channel(s)
107 #
108 def chanlog (line):
109 for client in all_clients:
110 if not client.flags & CLIF_CONNECTED:
111 continue
112
113 for channel in client.channels:
114 if channel.get_value ('logchannel', default=False):
115 client.write ("PRIVMSG %s :%s" % (channel['name'], line))
116
117 #
118 # Exception handling
119 #
120 def handle_exception(excType, excValue, trace):
121 excepterm (traceback.format_exception(excType, excValue, trace))
122
123 def excepterm(data):
124 for segment in data:
125 for line in segment.splitlines():
126 print line
127 chanlog (line)
128
129 for client in all_clients:
130 if len(data) > 0:
131 client.exceptdie()
132 else:
133 client.quit_irc()
134
135 if g_BotActive:
136 restart_self()
137 else:
138 quit()
139
140 sys.excepthook = handle_exception
141
142 def check_admin (sender, ident, host, command):
143 if not "%s@%s" % (ident, host) in g_admins:
144 raise logical_exception (".%s requires admin access" % command)
145
146 class logical_exception (Exception):
147 def __init__ (self, value):
148 self.value = value
149 def __str__ (self):
150 return self.value
151
152 # from http://www.daniweb.com/software-development/python/code/260268/restart-your-python-program
153 def restart_self():
154 python = sys.executable
155 os.execl (python, python, * sys.argv)
156
157 def plural (a):
158 return '' if a == 1 else 's'
159
160 #
161 # Main IRC client class
162 #
163 class irc_client (asyncore.dispatcher):
164 def __init__ (self, cfg, flags):
165 self.name = cfg.get_value ('name')
166 self.host = cfg.get_value ('address')
167 self.port = cfg.get_value ('port', default=6667)
168 self.password = cfg.get_value ('password', default='')
169 self.channels = cfg.get_nodelist ('channels')
170 self.flags = flags
171 self.send_buffer = []
172 self.umode = cfg.get_value ('umode', default='')
173 self.cfg = cfg
174 self.desired_name = Config.get_value ('nickname', default='cobalt')
175 self.mynick = self.desired_name
176 self.verbose = Config.get_value ('verbose', default=False)
177 self.commandprefix = Config.get_value ('commandprefix', default='.')
178
179 all_clients.append (self)
180 asyncore.dispatcher.__init__ (self)
181 self.create_socket (socket.AF_INET, socket.SOCK_STREAM)
182 self.connect ((self.host, self.port))
183
184 def register_to_irc (self):
185 ident = Config.get_value ('ident', default='cobalt')
186 gecos = Config.get_value ('gecos', default='cobalt')
187 self.write ("PASS %s" % self.password)
188 self.write ("USER %s * * :%s" % (ident, gecos))
189 self.write ("NICK %s" % self.mynick)
190
191 def handle_connect (self):
192 print "Connected to [%s] %s:%d" % (self.name, self.host, self.port)
193 self.register_to_irc()
194
195 def write (self, utfdata):
196 try:
197 self.send_buffer.append ("%s" % utfdata.decode("utf-8","ignore").encode("ascii","ignore"))
198 except UnicodeEncodeError:
199 pass
200
201 def handle_close (self):
202 print "Connection to [%s] %s:%d terminated." % (self.name, self.host, self.port)
203 self.close()
204
205 def handle_write (self):
206 self.send_all_now()
207
208 def readable (self):
209 return True
210
211 def writable (self):
212 return len (self.send_buffer) > 0
213
214 def send_all_now (self):
215 for line in self.send_buffer:
216 if self.verbose:
217 print "[%s] <- %s" % (self.name, line)
218 self.send ("%s\n" % line)
219 self.send_buffer = []
220
221 def handle_read (self):
222 lines = self.recv (4096).splitlines()
223 for utfline in lines:
224 try:
225 line = utfline.decode("utf-8","ignore").encode("ascii","ignore")
226 except UnicodeDecodeError:
227 continue
228
229 if self.verbose:
230 print "[%s] -> %s" % (self.name, line)
231
232 if line.startswith ("PING :"):
233 self.write ("PONG :%s" % line[6:])
234 else:
235 words = line.split(" ")
236 if len(words) >= 2:
237 if words[1] == "001":
238 self.flags |= CLIF_CONNECTED
239
240 for channel in self.channels:
241 self.write ("JOIN %s %s" % (channel.get_value ('name'), channel.get_value ('password', default='')))
242
243 umode = self.cfg.get_value ('umode', '')
244
245 if umode != '':
246 self.write ('MODE %s %s' % (self.mynick, self.cfg.get_value ('umode', '')))
247 elif words[1] == "PRIVMSG":
248 self.handle_privmsg (line)
249 elif words[1] == 'QUIT':
250 rex = re.compile (r'^:([^!]+)!([^@]+)@([^ ]+) QUIT')
251 match = rex.match (line)
252
253 # Try reclaim our nickname if possible
254 if match and match.group(1) == self.desired_name:
255 self.mynick = self.desired_name
256 self.write ("NICK %s" % self.mynick)
257 elif words[1] == "433":
258 #:irc.localhost 433 * cobalt :Nickname is already in use.
259 self.mynick += self.cfg.get_value ('conflictsuffix', default='`')
260 self.write ("NICK " + self.mynick)
261
262 # Check for new issues on the bugtracker
263 bt_checklatest()
264
265 # Check for new commits in the repositories
266 HgPoll.poll()
267
268 def channel_by_name (self, name):
269 for channel in self.channels:
270 if channel.get_value ('name').upper() == args[0].upper():
271 return channel
272 else:
273 raise logical_exception ('unknown channel ' + args[0])
274
275 # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
276 #
277 # Handle a PRIVMSG line from the IRC server
278 #
279 def handle_privmsg (self, line):
280 rex = re.compile (r'^:([^!]+)!([^@]+)@([^ ]+) PRIVMSG ([^ ]+) :(.+)$')
281 match = rex.match (line)
282 if match:
283 sender = match.group (1)
284 user = match.group (2)
285 host = match.group (3)
286 channel = match.group (4)
287 message = match.group (5)
288 replyto = channel if channel != g_mynick else sender
289
290 # Check for tracker url in the message
291 url = Config.get_node ('bt').get_value ('url')
292 http_regex = re.compile (r'.*http(s?)://%s/view\.php\?id=([0-9]+).*' % url)
293 http_match = http_regex.match (line)
294
295 # Check for command.
296 if len(message) >= 2 and message[0] == self.commandprefix and message[1] != self.commandprefix:
297 stuff = message[1:].split(' ')
298 command = stuff[0]
299 args = stuff[1:]
300 try:
301 self.handle_command (sender, user, host, replyto, command, args, message)
302 except logical_exception as e:
303 for line in e.value.split ('\n'):
304 if len(line) > 0:
305 self.privmsg (replyto, "error: %s" % line)
306 elif http_match:
307 self.get_ticket_data (replyto, http_match.group (2), False)
308 else:
309 chanlog ("Recieved bad PRIVMSG: %s" % line)
310
311 def is_admin (self, ident, host):
312 return ("%s@%s" % (ident, host)) in g_admins
313
314 # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
315 #
316 # Process an IRC command
317 #
318 def handle_command (self, sender, ident, host, replyto, command, args, message):
319 kvargs = {'sender': sender, 'ident': ident, 'host': host, 'replyto': replyto, 'cmdname': command, 'message': message}
320
321 try:
322 result = ModuleCore.call_command (self, **kvargs)
323
324 if result:
325 return
326 except ModuleCore.CommandError as e:
327 lines = str (e).split ('\n')
328 self.privmsg (replyto, 'error: %s' % lines[0])
329
330 for line in lines[1:]:
331 self.privmsg (replyto, ' ' + line)
332 return
333 #tried
334
335 if command == 'die':
336 check_admin (sender, ident, host, command)
337 quit()
338 elif command == 'convert':
339 if len(args) != 3 or args[1] != 'as':
340 raise logical_exception ("usage: .%s <value> as <valuetype>" % command)
341
342 value = float (args[0])
343 valuetype = args[2]
344
345 if valuetype in ['radians', 'degrees']:
346 if valuetype == 'radians':
347 radvalue = value
348 degvalue = (value * 180.) / math.pi
349 else:
350 radvalue = (value * math.pi) / 180.
351 degvalue = value
352
353 self.privmsg (replyto, '%s radians, %s degrees (%s)' %(radvalue, degvalue, degvalue % 360.))
354 elif valuetype in ['celsius', 'fahrenheit']:
355 if valuetype == 'celsius':
356 celvalue = value
357 fahrvalue = value * 1.8 + 32
358 else:
359 celvalue = (value - 32) / 1.8
360 fahrvalue = value
361
362 self.privmsg (replyto, '%s degrees celsius, %s degrees fahrenheit' %(celvalue, fahrvalue))
363 else:
364 raise logical_exception ('unknown valuetype, expected one of: degrees, radians (angle conversion), ' +
365 'celsius, fahrenheit (temperature conversion)')
366 elif command == 'urban' or command == 'ud':
367 try:
368 if len(args) < 1:
369 raise logical_exception ('usage: %s <word>' % command)
370
371 url = 'http://api.urbandictionary.com/v0/define?term=%s' % ('%20'.join (args))
372 response = urllib2.urlopen (url).read()
373 data = json.loads (response)
374
375 if 'list' in data and len(data['list']) > 0 and 'word' in data['list'][0] and 'definition' in data['list'][0]:
376 word = data['list'][0]['word']
377 definition = data['list'][0]['definition'].replace ('\r', ' ').replace ('\n', ' ').replace (' ', ' ')
378 up = data['list'][0]['thumbs_up']
379 down = data['list'][0]['thumbs_down']
380 self.privmsg (replyto, "\002%s\002: %s\0033 %d\003 up,\0035 %d\003 down" % (word, definition, up, down))
381 else:
382 self.privmsg (replyto, "couldn't find a definition of \002%s\002" % args[0])
383 except logical_exception as e:
384 raise e
385 except Exception as e:
386 raise logical_exception ('Urban dictionary lookup failed: %s' % `e`)
387 # else:
388 # raise logical_exception ("unknown command `.%s`" % command)
389
390 def handle_error(self):
391 excepterm (traceback.format_exception(sys.exc_type, sys.exc_value, sys.exc_traceback))
392
393 def restart(self):
394 excepterm('')
395
396 def privmsg (self, channel, msg):
397 self.write ("PRIVMSG %s :%s" % (channel, msg))
398
399 def close_connection (self, message):
400 if self.flags & CLIF_CONNECTED:
401 self.write ("QUIT :" + message)
402 self.send_all_now()
403 self.close()
404
405 def quit_irc (self):
406 self.close_connection ('Leaving')
407
408 def exceptdie (self):
409 self.close_connection ('Caught exception')
410
411 def keyboardinterrupt (self):
412 self.close_connection ('KeyboardInterrupt')
413 114
414 if __name__ == '__main__': 115 if __name__ == '__main__':
415 main() 116 main()

mercurial