Tue, 13 May 2014 23:29:37 +0300
- fixed up exception handling, no longer connects to every possible connection, rather uses the autoconnect config entry
0 | 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 | ||
1
29c7e9d13a30
- fixed up exception handling, no longer connects to every possible connection, rather uses the autoconnect config entry
Santeri Piippo <crimsondusk64@gmail.com>
parents:
0
diff
changeset
|
102 | def check_admin (sender, ident, host, command): |
29c7e9d13a30
- fixed up exception handling, no longer connects to every possible connection, rather uses the autoconnect config entry
Santeri Piippo <crimsondusk64@gmail.com>
parents:
0
diff
changeset
|
103 | if not "%s!%s@%s" % (sender, ident, host) in g_admins: |
29c7e9d13a30
- fixed up exception handling, no longer connects to every possible connection, rather uses the autoconnect config entry
Santeri Piippo <crimsondusk64@gmail.com>
parents:
0
diff
changeset
|
104 | raise logical_exception (".%s requires admin access" % command) |
0 | 105 | |
1
29c7e9d13a30
- fixed up exception handling, no longer connects to every possible connection, rather uses the autoconnect config entry
Santeri Piippo <crimsondusk64@gmail.com>
parents:
0
diff
changeset
|
106 | class logical_exception (Exception): |
29c7e9d13a30
- fixed up exception handling, no longer connects to every possible connection, rather uses the autoconnect config entry
Santeri Piippo <crimsondusk64@gmail.com>
parents:
0
diff
changeset
|
107 | def __init__ (self, value): |
29c7e9d13a30
- fixed up exception handling, no longer connects to every possible connection, rather uses the autoconnect config entry
Santeri Piippo <crimsondusk64@gmail.com>
parents:
0
diff
changeset
|
108 | self.value = value |
29c7e9d13a30
- fixed up exception handling, no longer connects to every possible connection, rather uses the autoconnect config entry
Santeri Piippo <crimsondusk64@gmail.com>
parents:
0
diff
changeset
|
109 | def __str__ (self): |
29c7e9d13a30
- fixed up exception handling, no longer connects to every possible connection, rather uses the autoconnect config entry
Santeri Piippo <crimsondusk64@gmail.com>
parents:
0
diff
changeset
|
110 | return self.value |
29c7e9d13a30
- fixed up exception handling, no longer connects to every possible connection, rather uses the autoconnect config entry
Santeri Piippo <crimsondusk64@gmail.com>
parents:
0
diff
changeset
|
111 | |
29c7e9d13a30
- fixed up exception handling, no longer connects to every possible connection, rather uses the autoconnect config entry
Santeri Piippo <crimsondusk64@gmail.com>
parents:
0
diff
changeset
|
112 | # |
29c7e9d13a30
- fixed up exception handling, no longer connects to every possible connection, rather uses the autoconnect config entry
Santeri Piippo <crimsondusk64@gmail.com>
parents:
0
diff
changeset
|
113 | # Main IRC client class |
29c7e9d13a30
- fixed up exception handling, no longer connects to every possible connection, rather uses the autoconnect config entry
Santeri Piippo <crimsondusk64@gmail.com>
parents:
0
diff
changeset
|
114 | # |
0 | 115 | class irc_client (asyncore.dispatcher): |
116 | def __init__ (self, cfg, flags): | |
117 | self.name = cfg['name'] | |
118 | self.host = cfg['address'] | |
119 | self.port = cfg['port'] | |
120 | self.password = cfg['password'] if 'password' in cfg else '' | |
121 | self.channels = cfg['channels'] | |
122 | self.flags = flags | |
123 | self.send_buffer = list() | |
124 | self.umode = cfg['umode'] if 'umode' in cfg else '' | |
125 | self.cfg = cfg | |
126 | self.mynick = '' | |
127 | g_clients.append (self) | |
128 | asyncore.dispatcher.__init__ (self) | |
129 | self.create_socket (socket.AF_INET, socket.SOCK_STREAM) | |
130 | self.connect ((self.host, self.port)) | |
131 | ||
132 | def handle_connect (self): | |
133 | nick = self.cfg['nickname'] if 'nickname' in self.cfg else g_config['nickname'] | |
134 | ident = self.cfg['ident'] if 'ident' in self.cfg else g_config['ident'] | |
135 | gecos = self.cfg['gecos'] if 'gecos' in self.cfg else g_config['gecos'] | |
136 | self.mynick = nick | |
137 | print "Connected to [%s] %s:%d" % (self.name, self.host, self.port) | |
138 | if 'password' in self.cfg: | |
139 | self.write ("PASS %s" % self.cfg['password']) | |
140 | self.write ("USER %s * * :%s" % (ident, gecos)) | |
141 | self.write ("NICK %s" % nick) | |
142 | ||
143 | def write (self, data): | |
144 | self.send_buffer.append ("%s" % data) | |
145 | ||
146 | def handle_close (self): | |
147 | print "Connection to [%s] %s:%d terminated." % (self.name, self.host, self.port) | |
148 | self.close() | |
149 | ||
150 | def handle_write (self): | |
151 | self.send_all_now() | |
152 | ||
153 | def readable (self): | |
154 | return True | |
155 | ||
156 | def writable (self): | |
157 | return len (self.send_buffer) > 0 | |
158 | ||
159 | def send_all_now (self): | |
160 | for line in self.send_buffer: | |
161 | print "[%s] <- %s" % (self.name, line) | |
162 | self.send ("%s\n" % line) | |
163 | self.send_buffer = [] | |
164 | ||
165 | def handle_read (self): | |
166 | lines = self.recv (4096).splitlines() | |
167 | for line in lines: | |
168 | print "[%s] -> %s" % (self.name, line) | |
169 | ||
170 | if line.startswith ("PING :"): | |
171 | self.write ("PONG :%s" % line[6:]) | |
172 | continue | |
173 | ||
174 | words = line.split(" ") | |
175 | if len(words) >= 2 and words[1] == "001": | |
176 | self.flags |= CLIF_CONNECTED | |
177 | ||
178 | for channel in self.cfg['channels']: | |
179 | self.write ("JOIN %s %s" % (channel['name'], channel['password'] if 'password' in channel else '')) | |
180 | ||
181 | if 'umode' in self.cfg: | |
182 | self.write ('MODE %s %s' % (self.mynick, self.cfg['umode'])) | |
183 | ||
184 | if len(words) >= 2 and words[1] == "PRIVMSG": | |
185 | self.handle_privmsg (line) | |
186 | ||
187 | def handle_privmsg (self, line): | |
188 | rex = re.compile (r'^:([^!]+)!([^@]+)@([^ ]+) PRIVMSG ([^ ]+) :(.+)$') | |
189 | match = rex.match (line) | |
190 | if match: | |
191 | sender = match.group (1) | |
192 | user = match.group (2) | |
193 | host = match.group (3) | |
194 | channel = match.group (4) | |
195 | message = match.group (5) | |
196 | replyto = channel if channel != g_mynick else sender | |
197 | ||
198 | # Check for tracker url in the message | |
199 | http_regex = re.compile (r'.*http(s?)://%s/view\.php\?id=([0-9]+).*' % g_config['trackerurl']) | |
200 | http_match = http_regex.match (line) | |
201 | ||
202 | # Check for command. | |
203 | if len(message) >= 2 and message[0] == '.' and message[1] != '.': | |
204 | stuff = message[1:].split(' ') | |
205 | command = stuff[0] | |
206 | args = stuff[1:] | |
207 | try: | |
1
29c7e9d13a30
- fixed up exception handling, no longer connects to every possible connection, rather uses the autoconnect config entry
Santeri Piippo <crimsondusk64@gmail.com>
parents:
0
diff
changeset
|
208 | self.handle_command (sender, user, host, replyto, command, args) |
29c7e9d13a30
- fixed up exception handling, no longer connects to every possible connection, rather uses the autoconnect config entry
Santeri Piippo <crimsondusk64@gmail.com>
parents:
0
diff
changeset
|
209 | except logical_exception as msg: |
29c7e9d13a30
- fixed up exception handling, no longer connects to every possible connection, rather uses the autoconnect config entry
Santeri Piippo <crimsondusk64@gmail.com>
parents:
0
diff
changeset
|
210 | self.privmsg (replyto, "error: %s" % msg) |
0 | 211 | elif http_match: |
212 | self.get_ticket_data (replyto, http_match.group (2), False) | |
213 | else: | |
214 | control ("Recieved bad PRIVMSG: %s" % line) | |
215 | ||
216 | def get_ticket_data (self, replyto, ticket, withlink): | |
217 | data = {} | |
218 | try: | |
219 | data = suds_client.service.mc_issue_get (g_config['trackeruser'], g_config ['trackerpassword'], ticket) | |
220 | except Exception, e: | |
221 | self.privmsg (replyto, "Failed to get info for issue %s: %s" % (ticket, `e`)) | |
222 | ||
223 | if data: | |
224 | self.privmsg (replyto, "Issue %s: %s: Reporter: %s, assigned to: %s, status: %s (%s)" % \ | |
225 | (ticket, \ | |
226 | data.summary, \ | |
227 | data.reporter.name, \ | |
228 | data.handler.name if hasattr (data, 'handler') else "nobody", \ | |
229 | data.status.name, \ | |
230 | data.resolution.name)) | |
231 | ||
232 | if withlink: | |
233 | self.privmsg (replyto, "Read all about it here: https://%s/view.php?id=%s" % (g_config['trackerurl'], ticket)) | |
234 | ||
1
29c7e9d13a30
- fixed up exception handling, no longer connects to every possible connection, rather uses the autoconnect config entry
Santeri Piippo <crimsondusk64@gmail.com>
parents:
0
diff
changeset
|
235 | def handle_command (self, sender, ident, host, replyto, command, args): |
0 | 236 | if command == "raw": |
1
29c7e9d13a30
- fixed up exception handling, no longer connects to every possible connection, rather uses the autoconnect config entry
Santeri Piippo <crimsondusk64@gmail.com>
parents:
0
diff
changeset
|
237 | check_admin (sender, ident, host, command) |
0 | 238 | self.write (" ".join (args)) |
239 | elif command == "msg": | |
1
29c7e9d13a30
- fixed up exception handling, no longer connects to every possible connection, rather uses the autoconnect config entry
Santeri Piippo <crimsondusk64@gmail.com>
parents:
0
diff
changeset
|
240 | check_admin (sender, ident, host, command) |
0 | 241 | if len(args) < 2: |
1
29c7e9d13a30
- fixed up exception handling, no longer connects to every possible connection, rather uses the autoconnect config entry
Santeri Piippo <crimsondusk64@gmail.com>
parents:
0
diff
changeset
|
242 | raise logical_exception ("usage: .%s <target> <message...>" % command) |
0 | 243 | self.privmsg (args[0], " ".join (args[1:])) |
244 | elif command == "ticket": | |
245 | if len(args) != 1: | |
1
29c7e9d13a30
- fixed up exception handling, no longer connects to every possible connection, rather uses the autoconnect config entry
Santeri Piippo <crimsondusk64@gmail.com>
parents:
0
diff
changeset
|
246 | raise logical_exception ("usage: .%s <ticket>" % command) |
0 | 247 | self.get_ticket_data (replyto, args[0], True) |
248 | else: | |
1
29c7e9d13a30
- fixed up exception handling, no longer connects to every possible connection, rather uses the autoconnect config entry
Santeri Piippo <crimsondusk64@gmail.com>
parents:
0
diff
changeset
|
249 | raise logical_exception ("unknown command `.%s`" % command) |
0 | 250 | |
251 | def handle_error(self): | |
252 | excepterm (traceback.format_exception(sys.exc_type, sys.exc_value, sys.exc_traceback)) | |
253 | ||
254 | def privmsg (self, channel, msg): | |
255 | self.write ("PRIVMSG %s :%s" % (channel, msg)) | |
256 | ||
257 | def exceptdie (self): | |
258 | if self.flags & CLIF_CONNECTED: | |
259 | self.write ("QUIT :Caught exception") | |
260 | self.send_all_now() | |
261 | self.close() | |
262 | ||
263 | def keyboardinterrupt (self): | |
264 | if self.flags & CLIF_CONNECTED: | |
265 | self.write ("QUIT :KeyboardInterrupt") | |
266 | self.send_all_now() | |
267 | self.close() | |
268 | ||
1
29c7e9d13a30
- fixed up exception handling, no longer connects to every possible connection, rather uses the autoconnect config entry
Santeri Piippo <crimsondusk64@gmail.com>
parents:
0
diff
changeset
|
269 | # |
29c7e9d13a30
- fixed up exception handling, no longer connects to every possible connection, rather uses the autoconnect config entry
Santeri Piippo <crimsondusk64@gmail.com>
parents:
0
diff
changeset
|
270 | # Main procedure: |
29c7e9d13a30
- fixed up exception handling, no longer connects to every possible connection, rather uses the autoconnect config entry
Santeri Piippo <crimsondusk64@gmail.com>
parents:
0
diff
changeset
|
271 | # |
0 | 272 | try: |
273 | for conndata in g_config['connections']: | |
1
29c7e9d13a30
- fixed up exception handling, no longer connects to every possible connection, rather uses the autoconnect config entry
Santeri Piippo <crimsondusk64@gmail.com>
parents:
0
diff
changeset
|
274 | if conndata['name'] in g_config['autoconnect']: |
29c7e9d13a30
- fixed up exception handling, no longer connects to every possible connection, rather uses the autoconnect config entry
Santeri Piippo <crimsondusk64@gmail.com>
parents:
0
diff
changeset
|
275 | irc_client (conndata, CLIF_CONTROL if conndata['control'] else 0) |
0 | 276 | asyncore.loop() |
277 | except KeyboardInterrupt: | |
278 | for client in g_clients: | |
279 | client.keyboardinterrupt() | |
280 | quit() |