Add GPL license headers
[people/stefanha/gpxebot.git] / gpxebot.py
1 #!/usr/bin/env python
2 # Copyright (C) 2008 Stefan Hajnoczi <stefanha@gmail.com>.
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License as
6 # published by the Free Software Foundation; either version 2 of the
7 # License, or any later version.
8 #
9 # This program is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12 # General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17
18 import select
19 import socket
20 import sys
21 import time
22 import os
23
24 import config
25 import cmds
26 import utils
27
28 NO_ARGS = -1
29
30 handlers = {}
31
32 def autojoin():
33     del handlers['376']
34
35     # Receive '+' or '-' indicator with every message showing whether the
36     # sender is identified to nick services.  Only identified users may be bot
37     # operators.
38     cmd('CAP REQ :identify-msg')
39
40     return do_response(cmds.readrc_command())
41
42 def ping(_, arg):
43     cmd('PONG %s' % arg)
44     return False
45
46 def do_response(response):
47     # it should be in the format (command, args) 
48     # or [(command, args), (command, args)...]
49     # args is a tuple
50     if not response:
51         return False
52     if isinstance(response, list):
53         return any(do_response(r) for r in response)
54     (command, args) = response
55     if command == 'PRIVMSG':
56         pmsg(*args)
57     elif command == 'CMD':
58         cmd(*args)
59     return command == 'RESTART'
60
61 def privmsg(_, target, msg):
62     # Establish identify of sender from CAPAB IDENTIFY-MSG info
63     is_identified = msg.startswith('+')
64     if msg.startswith('-') or msg.startswith('+'):
65         msg = msg[1:]
66     who = cmds.Who(mask, is_identified)
67
68     utils.do_log(cmds.logs, target, who.nick, msg)
69
70     words = msg.split()
71     if not words:
72         return False
73     if target.startswith('#'):
74         replyto = target
75         if not config.NICK in words[0]:
76             return False
77         words.pop(0)
78     elif target == config.NICK:
79         replyto = who.nick
80
81     return do_response(cmds.do_command(who, target, replyto, words))
82
83 def add_handler(command, handler, nargs):
84     handlers[command] = (handler, nargs)
85
86 def cmd(msg):
87     utils.dbg('WRITE ' + msg)
88     sock.sendall('%s\r\n' % msg)
89
90 def pmsg(target, msg):
91     if target:
92         utils.do_log(cmds.logs, target, config.NICK, msg)
93         cmd('PRIVMSG %s :%s' % (target, msg))
94
95 def dispatch(args):
96     command = args[0]
97     if command in handlers:
98         h = handlers[command]
99         if h[1] == NO_ARGS:
100             return h[0]()
101         elif len(args) == h[1]:
102             return h[0](*args)
103     return False
104
105 def parse(line):
106     if line[0] == ':':
107         mask, line = line[1:].split(None, 1)
108     else:
109         mask = None
110
111     args = []
112     while line and line[0] != ':':
113         fields = line.split(None, 1)
114         if len(fields) == 1:
115             fields.append(None)
116         arg, line = fields
117         args.append(arg)
118     if line:
119         args.append(line[1:])
120
121     return mask, args
122
123 add_handler('376', autojoin, NO_ARGS)
124 add_handler('PING', ping, 2)
125 add_handler('PRIVMSG', privmsg, 3)
126
127 try:
128     if len(sys.argv) == 3 and sys.argv[1] == '--socket-fd':
129         # Restart with existing IRC session
130         fd = int(sys.argv[2])
131         sock = socket.fromfd(fd, socket.AF_INET, socket.SOCK_STREAM)
132         os.close(fd)
133         autojoin()
134     else:
135         # Connect to server and start new IRC session
136         sock = socket.socket()
137         sock.connect((config.HOST, config.PORT))
138         cmd('NICK %s' % config.NICK)
139         cmd('USER %s none none :%s' % (config.IDENT, config.REALNAME))
140 except Exception, e:
141     # Log top-level exceptions and restart
142     try:
143         sock.close()
144     except Exception:
145         pass
146     sys.stderr.write(str(e) + '\n')
147     time.sleep(10) # prevent spinning on repeated failure
148     os.execl(sys.argv[0], sys.argv[0])
149
150 # Try to create command FIFO
151 try:
152     os.mkfifo(config.FIFO)
153 except OSError:
154     pass
155
156 # Opening a fifo for reading with no writers normally blocks, but opening
157 # read/write will not block on Linux.
158 cmdfifo = open(config.FIFO, 'r+')
159
160 restart = False
161 sockbuf = ''
162 try:
163     while not restart:
164         rlist, _, xlist = select.select([sock, cmdfifo], [], [sock])
165
166         if sock in rlist or sock in xlist:
167             r = sock.recv(4096)
168             if not r:
169                 break
170             sockbuf += r
171
172             while '\r\n' in sockbuf:
173                 line, sockbuf = sockbuf.split('\r\n', 1)
174                 if not line:
175                     continue
176                 utils.dbg('READ ' + line)
177                 mask, args = parse(line)
178                 restart = dispatch(args)
179
180         if cmdfifo in rlist:
181             words = cmdfifo.readline().strip().split()
182             restart = do_response(cmds.do_command(cmds.Who(config.NICK, True), config.NICK, config.NICK, words)) or restart
183 except Exception, e:
184     # Log top-level exceptions and restart
185     cmdfifo.close()
186     sock.close()
187     sys.stderr.write(str(e) + '\n')
188     time.sleep(10) # prevent spinning on repeated failure
189     os.execl(sys.argv[0], sys.argv[0])
190
191 cmdfifo.close()
192
193 # Restart with existing IRC session
194 fd = sock.fileno()
195 os.execl(sys.argv[0], sys.argv[0], '--socket-fd', str(fd))