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
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
|
import time
from twisted.python import log
from twisted.internet.protocol import ReconnectingClientFactory
from twisted.protocols.basic import LineOnlyReceiver
DEBUG = False
class IRCBadMessage(ValueError):
"""
Raised when a malformed or invalid IRC message is encountered.
"""
class IRCBadModes(IRCBadMessage):
"""
Raised when a malformed modes string is encountered.
"""
class EmptyTarget(IRCBadMessage):
"""
Raised when a target (nick / channel / etc.) is empty.
"""
def parsemsg(s):
"""
Parses a raw IRC message from the server and returns the components.
Empty messages are treated as errors and raise L{IRCBadMessage}.
@param s: Raw IRC message
@type s: C{str}
@return: The prefix, command and additional arguments
@rtype: C{(str, str, list)}
"""
prefix = ''
trailing = None
if not s:
raise IRCBadMessage('Empty line.')
if s.startswith(':'):
prefix, s = s[1:].split(' ', 1)
if ' :' in s:
s, trailing = s.split(' :', 1)
args = s.split()
if trailing:
args.append(trailing)
command = args.pop(0)
return prefix, command, args
def parseModes(modes, params, paramModes=('', '')):
"""
Parse an IRC mode string.
The mode string is parsed into two lists of mode changes (added and
removed), with each mode change represented as C{(mode, param)} where mode
is the mode character, and param is the parameter passed for that mode, or
C{None} if no parameter is required.
@param modes: The modes string
@type modes: C{str}
@param params: The parameters specified along with the modes
@type params: C{list}
@returns: (added, removed) modes
@rtype: C{([(mode, param)], [(mode, param]))}
"""
if len(modes) == 0:
raise IRCBadModes('Empty mode string')
if modes[0] not in '+-':
raise IRCBadModes('Malformed modes string: %r' % modes)
changes = ([], [])
direction = None
count = -1
for ch in modes:
if ch in '+-':
if count == 0:
raise IRCBadModes('Empty mode sequence: %r' % modes)
direction = '+-'.index(ch)
count = 0
else:
param = None
if ch in paramModes[direction]:
try:
param = params.pop(0)
except IndexError:
raise IRCBadModes('Not enough parameters: %r' % ch)
changes[direction].append((ch, param))
count += 1
if len(params) > 0:
raise IRCBadModes('Too many parameters: %r %r' % (modes, params))
if count == 0:
raise IRCBadModes('Empty mode sequence: %r' % modes)
return changes
class IRCServices(LineOnlyReceiver):
serviceModes = ''
def connectionMade(self):
self.params = self.services.params
self.name = self.params['name']
self.startHandshake()
def startHandshake(self):
raise NotImplementedError()
def sendLine(self, line):
if DEBUG:
print '>>>', line
self.transport.write(line + '\r\n')
def sendMessage(self, command, *params, **kwargs):
prefix = kwargs.get('prefix')
line = ' '.join([command] + [str(param) for param in params])
if prefix is not None:
line = ':%s %s' % (prefix, line)
self.sendLine(line)
if len(params) > 15:
log.msg('Message has %d parameters (RFC allows 15):\n%s' % (len(params), line))
def lineReceived(self, line):
if DEBUG:
print '<<<', line
if line == '':
return
try:
prefix, command, params = parsemsg(line)
except IRCBadMessage, e:
log.msg('Invalid protocol line: %r' % (line,))
log.err()
self.fatalError(str(e))
else:
self.handleCommand(command, prefix, params)
def handleCommand(self, command, prefix, params):
method = getattr(self, 'irc_%s' % (command,), None)
try:
def handle():
if method is not None:
method(prefix, *params)
else:
self.irc_unknown(prefix, command, params)
self.services.store.transact(handle)
except IRCBadMessage, e:
raise
except:
log.msg('%s|%s - %s' % (prefix, command, params))
log.err()
### handlers
def irc_unknown(self, prefix, command, params):
log.msg('%s|%s - %s' % (prefix, command, params))
def irc_PING(self, source, target):
self.sendMessage('PONG', target, prefix=self.name)
def irc_QUIT(self, source, msg):
self.services.event('quit', source, msg)
def irc_PRIVMSG(self, prefix, nick, msg):
self.services.event('privmsg', prefix, nick, msg)
def irc_NOTICE(self, prefix, nick, msg):
self.services.event('notice', prefix, nick, msg)
### commands
def msg(self, source, nick, msg):
self.sendMessage('PRIVMSG', nick, ':%s' % (msg,), prefix=source)
def notice(self, source, nick, msg):
self.sendMessage('NOTICE', nick, ':%s' % (msg,), prefix=source)
def kill(self, nick, reason='', source=None):
if source is None:
source = self.name
self.sendMessage('KILL', nick, '%s (%s)' % (source, reason), prefix=source)
def fatalError(self, reason):
self.sendMessage('ERROR', ':' + reason)
self.transport.loseConnection()
def newServiceNick(self, nick, name, modes):
return self.newNick(nick, nick.lower(), 'services.', name, ''.join(set(modes + self.serviceModes)))
class InspircdServices(IRCServices):
serviceModes = 'd'
userParamModes = ('', '')
channelParamModes = ('abefghjkloqvIL', 'abehoqvI')
channelPrefixes = '#'
def startHandshake(self):
self.sendMessage('SERVER', self.params['name'], self.params['password'], '0', ':%(desc)s' % self.params)
def getTargetKind(self, target):
"""
Determine what kind of entity a target is.
Currently the only possibilities are channel and user.
"""
if len(target) == 0:
raise EmptyTarget(target)
return dict.fromkeys(self.channelPrefixes, 'channel').get(target[0], 'user')
def irc_SERVER(self, source, name, password, hopcount, description):
hopcount = int(hopcount)
if hopcount == 0:
# authentication phase
if source != '':
raise IRCBadMessage(source)
# XXX: actually check auth
self.sendMessage('BURST', '%ld' % time.time())
self.sendMessage('VERSION', ':Ares X')
self.services.event('connected', self)
self.sendMessage('ENDBURST')
else:
self.services.event('newserver', name, description)
def irc_NICK(self, source, *params):
if len(params) == 1:
oldnick = source.decode('ascii')
newnick = params[0].decode('ascii')
self.services.event('moderemoved', 'INTERNAL', oldnick, 'user', ('r', None))
self.services.event('nickchange', oldnick, newnick)
else:
paramNames = 'timestamp nick hostname fakehost username modes ip realname'.split()
user = dict(zip(paramNames, params))
user['nick'] = user['nick'].decode('ascii')
user['modes'] = user['modes'].lstrip('+')
user['timestamp'] = int(user['timestamp'])
user['server'] = source
user['servicestamp'] = 0
self.services.createUser(user)
def irc_OPERTYPE(self, nick, opertype):
self.services.event('opertype', nick.decode('ascii'), opertype)
def irc_BURST(self, prefix, stamp):
pass
def irc_ENDBURST(self, prefix):
pass
def irc_METADATA(self, source, target, key, value):
pass
def irc_CAPAB(self, source, *params):
pass
def irc_VERSION(self, source, version):
pass
def irc_IDLE(self, source, dest):
self.services.event('idle', source, dest)
def irc_TIME(self, source, target, nick):
if target == self.params['name']:
self.sendMessage('TIME', source, nick, '%ld' % time.time(), prefix=target)
def irc_FMODE(self, source, target, timestamp, modes, *params):
self.irc_MODE(source, target, modes, *params)
def irc_MODE(self, source, target, modes, *params):
targetKind = self.getTargetKind(target)
paramModes = {'user': self.userParamModes,
'channel': self.channelParamModes}[targetKind]
if not modes.startswith('+') and not modes.startswith('-'):
modes = '+' + modes
added, removed = parseModes(modes, list(params), paramModes)
for mode in added:
self.services.event('modeadded', source, target, targetKind, mode)
for mode in removed:
self.services.event('moderemoved', source, target, targetKind, mode)
def irc_FJOIN(self, source, channel, timestamp, users):
for user in users.split():
prefixes, nick = user.split(',', 1)
self.services.event('joined', channel, nick)
# XXX: translate the prefixes into channel mode changes
def irc_JOIN(self, source, channels, timestamp):
for channel in channels.split(','):
self.services.event('joined', channel, source)
def irc_PART(self, source, channel, message=''):
self.services.event('parted', channel, source, message)
def newNick(self, nick, user, host, name, modes, server=None):
if server is None:
server = self.params['name']
self.sendMessage('NICK', '%ld' % time.time(), nick.encode('ascii'), host, host, user, '+' + modes, '0.0.0.0', ':%s' % name)
class ServicesFactory(ReconnectingClientFactory):
"""
A simple client factory for services.
The protocol factory is passed in, to ease selection of the relevant
ircd-specific protocol. We inherit from ReconnectingClientFactory to ensure
services tries to stay connected at all times.
"""
maxDelay = 5 * 60
def __init__(self, servicesFactory, protocolFactory):
self.servicesFactory = servicesFactory
self.protocolFactory = protocolFactory
def buildProtocol(self, addr):
"""
Create a protocol instance.
We give the protocol a services attribute, because the factory doesn't
have any functionality the protocol needs to care about.
"""
p = self.protocolFactory()
p.services = self.servicesFactory()
return p
|