|
1 #!/usr/bin/env python |
|
2 ''' |
|
3 Copyright 2014 Santeri Piippo |
|
4 All rights reserved. |
|
5 |
|
6 Redistribution and use in source and binary forms, with or without |
|
7 modification, are permitted provided that the following conditions |
|
8 are met: |
|
9 |
|
10 1. Redistributions of source code must retain the above copyright |
|
11 notice, this list of conditions and the following disclaimer. |
|
12 2. Redistributions in binary form must reproduce the above copyright |
|
13 notice, this list of conditions and the following disclaimer in the |
|
14 documentation and/or other materials provided with the distribution. |
|
15 3. The name of the author may not be used to endorse or promote products |
|
16 derived from this software without specific prior written permission. |
|
17 |
|
18 THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR |
|
19 IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
|
20 OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. |
|
21 IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, |
|
22 INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
|
23 NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
|
24 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
|
25 THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
|
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. |
|
28 ''' |
|
29 |
|
30 import asyncore |
|
31 import socket |
|
32 import time |
|
33 import sys |
|
34 import traceback |
|
35 import re |
|
36 import json |
|
37 from suds.xsd.doctor import Import |
|
38 from suds.xsd.doctor import ImportDoctor |
|
39 from suds.client import Client |
|
40 |
|
41 try: |
|
42 with open ('cobalt.json', 'r') as fp: |
|
43 g_config = json.loads (fp.read()) |
|
44 except IOError as e: |
|
45 print 'couldn\'t open cobalt.json: %s' % e |
|
46 quit() |
|
47 |
|
48 g_admins = g_config['admins'] |
|
49 g_mynick = g_config['nickname'] |
|
50 |
|
51 # |
|
52 # SOAP stuff |
|
53 # |
|
54 suds_import = Import ('http://schemas.xmlsoap.org/soap/encoding/', \ |
|
55 'http://schemas.xmlsoap.org/soap/encoding/') |
|
56 suds_client = Client ('https://zandronum.com/tracker/api/soap/mantisconnect.php?wsdl', \ |
|
57 plugins=[ImportDoctor (suds_import)]) |
|
58 |
|
59 # |
|
60 # irc_client flags |
|
61 # |
|
62 CLIF_CONTROL = (1 << 0) |
|
63 CLIF_CONNECTED = (1 << 1) |
|
64 |
|
65 # |
|
66 # List of all clients |
|
67 # |
|
68 g_clients = [] |
|
69 |
|
70 class channel (object): |
|
71 name = "" |
|
72 password = "" |
|
73 |
|
74 def __init__ (self, name): |
|
75 self.name = name |
|
76 |
|
77 # |
|
78 # Prints a line to control channel(s) |
|
79 # |
|
80 def control (line): |
|
81 for client in g_clients: |
|
82 if client.flags & (CLIF_CONTROL|CLIF_CONNECTED) == (CLIF_CONTROL|CLIF_CONNECTED): |
|
83 client.write ("PRIVMSG %s :%s" % (client.channels[0]['name'], line)) |
|
84 |
|
85 # |
|
86 # Exception handling |
|
87 # |
|
88 def handle_exception(excType, excValue, trace): |
|
89 excepterm (traceback.format_exception(excType, excValue, trace)) |
|
90 |
|
91 def excepterm(data): |
|
92 for segment in data: |
|
93 for line in segment.splitlines(): |
|
94 print line |
|
95 control (line) |
|
96 for client in g_clients: |
|
97 client.exceptdie() |
|
98 quit() |
|
99 |
|
100 sys.excepthook = handle_exception |
|
101 |
|
102 def check_admin (sender, ident, host): |
|
103 if not "%s!%s@%s" % (sender, user, host) in g_admins: |
|
104 raise ".%s requires admin access" % command |
|
105 |
|
106 class irc_client (asyncore.dispatcher): |
|
107 def __init__ (self, cfg, flags): |
|
108 self.name = cfg['name'] |
|
109 self.host = cfg['address'] |
|
110 self.port = cfg['port'] |
|
111 self.password = cfg['password'] if 'password' in cfg else '' |
|
112 self.channels = cfg['channels'] |
|
113 self.flags = flags |
|
114 self.send_buffer = list() |
|
115 self.umode = cfg['umode'] if 'umode' in cfg else '' |
|
116 self.cfg = cfg |
|
117 self.mynick = '' |
|
118 g_clients.append (self) |
|
119 asyncore.dispatcher.__init__ (self) |
|
120 self.create_socket (socket.AF_INET, socket.SOCK_STREAM) |
|
121 self.connect ((self.host, self.port)) |
|
122 |
|
123 def handle_connect (self): |
|
124 nick = self.cfg['nickname'] if 'nickname' in self.cfg else g_config['nickname'] |
|
125 ident = self.cfg['ident'] if 'ident' in self.cfg else g_config['ident'] |
|
126 gecos = self.cfg['gecos'] if 'gecos' in self.cfg else g_config['gecos'] |
|
127 self.mynick = nick |
|
128 print "Connected to [%s] %s:%d" % (self.name, self.host, self.port) |
|
129 if 'password' in self.cfg: |
|
130 self.write ("PASS %s" % self.cfg['password']) |
|
131 self.write ("USER %s * * :%s" % (ident, gecos)) |
|
132 self.write ("NICK %s" % nick) |
|
133 |
|
134 def write (self, data): |
|
135 self.send_buffer.append ("%s" % data) |
|
136 |
|
137 def handle_close (self): |
|
138 print "Connection to [%s] %s:%d terminated." % (self.name, self.host, self.port) |
|
139 self.close() |
|
140 |
|
141 def handle_write (self): |
|
142 self.send_all_now() |
|
143 |
|
144 def readable (self): |
|
145 return True |
|
146 |
|
147 def writable (self): |
|
148 return len (self.send_buffer) > 0 |
|
149 |
|
150 def send_all_now (self): |
|
151 for line in self.send_buffer: |
|
152 print "[%s] <- %s" % (self.name, line) |
|
153 self.send ("%s\n" % line) |
|
154 self.send_buffer = [] |
|
155 |
|
156 def handle_read (self): |
|
157 lines = self.recv (4096).splitlines() |
|
158 for line in lines: |
|
159 print "[%s] -> %s" % (self.name, line) |
|
160 |
|
161 if line.startswith ("PING :"): |
|
162 self.write ("PONG :%s" % line[6:]) |
|
163 continue |
|
164 |
|
165 words = line.split(" ") |
|
166 if len(words) >= 2 and words[1] == "001": |
|
167 self.flags |= CLIF_CONNECTED |
|
168 |
|
169 for channel in self.cfg['channels']: |
|
170 self.write ("JOIN %s %s" % (channel['name'], channel['password'] if 'password' in channel else '')) |
|
171 |
|
172 if 'umode' in self.cfg: |
|
173 self.write ('MODE %s %s' % (self.mynick, self.cfg['umode'])) |
|
174 |
|
175 if len(words) >= 2 and words[1] == "PRIVMSG": |
|
176 self.handle_privmsg (line) |
|
177 |
|
178 def handle_privmsg (self, line): |
|
179 rex = re.compile (r'^:([^!]+)!([^@]+)@([^ ]+) PRIVMSG ([^ ]+) :(.+)$') |
|
180 match = rex.match (line) |
|
181 if match: |
|
182 sender = match.group (1) |
|
183 user = match.group (2) |
|
184 host = match.group (3) |
|
185 channel = match.group (4) |
|
186 message = match.group (5) |
|
187 replyto = channel if channel != g_mynick else sender |
|
188 |
|
189 # Check for tracker url in the message |
|
190 http_regex = re.compile (r'.*http(s?)://%s/view\.php\?id=([0-9]+).*' % g_config['trackerurl']) |
|
191 http_match = http_regex.match (line) |
|
192 |
|
193 # Check for command. |
|
194 if len(message) >= 2 and message[0] == '.' and message[1] != '.': |
|
195 stuff = message[1:].split(' ') |
|
196 command = stuff[0] |
|
197 args = stuff[1:] |
|
198 try: |
|
199 handle_command (sender, user, host, replyto, command, args) |
|
200 except str as msg: |
|
201 privmsg (replyto, "error: %s" % msg) |
|
202 elif http_match: |
|
203 self.get_ticket_data (replyto, http_match.group (2), False) |
|
204 else: |
|
205 control ("Recieved bad PRIVMSG: %s" % line) |
|
206 |
|
207 def get_ticket_data (self, replyto, ticket, withlink): |
|
208 data = {} |
|
209 try: |
|
210 data = suds_client.service.mc_issue_get (g_config['trackeruser'], g_config ['trackerpassword'], ticket) |
|
211 except Exception, e: |
|
212 self.privmsg (replyto, "Failed to get info for issue %s: %s" % (ticket, `e`)) |
|
213 |
|
214 if data: |
|
215 self.privmsg (replyto, "Issue %s: %s: Reporter: %s, assigned to: %s, status: %s (%s)" % \ |
|
216 (ticket, \ |
|
217 data.summary, \ |
|
218 data.reporter.name, \ |
|
219 data.handler.name if hasattr (data, 'handler') else "nobody", \ |
|
220 data.status.name, \ |
|
221 data.resolution.name)) |
|
222 |
|
223 if withlink: |
|
224 self.privmsg (replyto, "Read all about it here: https://%s/view.php?id=%s" % (g_config['trackerurl'], ticket)) |
|
225 |
|
226 def handle_command (self, sender, user, host, replyto, command, args): |
|
227 if command == "raw": |
|
228 check_admin (sender, ident, host) |
|
229 self.write (" ".join (args)) |
|
230 elif command == "msg": |
|
231 check_admin (sender, ident, host) |
|
232 if len(args) < 2: |
|
233 raise "usage: .%s <target> <message...>" % command |
|
234 self.privmsg (args[0], " ".join (args[1:])) |
|
235 elif command == "ticket": |
|
236 if len(args) != 1: |
|
237 raise "usage: .%s <ticket>" % command |
|
238 self.get_ticket_data (replyto, args[0], True) |
|
239 else: |
|
240 self.privmsg (replyto, "unknown command `.%s`" % command) |
|
241 |
|
242 def handle_error(self): |
|
243 excepterm (traceback.format_exception(sys.exc_type, sys.exc_value, sys.exc_traceback)) |
|
244 |
|
245 def privmsg (self, channel, msg): |
|
246 self.write ("PRIVMSG %s :%s" % (channel, msg)) |
|
247 |
|
248 def exceptdie (self): |
|
249 if self.flags & CLIF_CONNECTED: |
|
250 self.write ("QUIT :Caught exception") |
|
251 self.send_all_now() |
|
252 self.close() |
|
253 |
|
254 def keyboardinterrupt (self): |
|
255 if self.flags & CLIF_CONNECTED: |
|
256 self.write ("QUIT :KeyboardInterrupt") |
|
257 self.send_all_now() |
|
258 self.close() |
|
259 |
|
260 try: |
|
261 for conndata in g_config['connections']: |
|
262 irc_client (conndata, CLIF_CONTROL) |
|
263 asyncore.loop() |
|
264 except KeyboardInterrupt: |
|
265 for client in g_clients: |
|
266 client.keyboardinterrupt() |
|
267 quit() |