/circuits/net/protocols/irc.py

https://bitbucket.org/prologic/circuits/ · Python · 487 lines · 290 code · 94 blank · 103 comment · 25 complexity · 6722d8d9cf2cf1431a498f01a611f3ea MD5 · raw file

  1. # Module: irc
  2. # Date: 04th August 2004
  3. # Author: James Mills <prologic@shortcircuit.net.au>
  4. """Internet Relay Chat Protocol
  5. This module implements the Internet Relay Chat Protocol
  6. or commonly known as IRC.
  7. This module can be used in both server and client
  8. implementations.
  9. """
  10. import re
  11. import time
  12. from circuits.net.sockets import Write
  13. from circuits.core import Event, Component
  14. from .line import LP
  15. ###
  16. ### Supporting Functions
  17. ###
  18. def strip(s, color=False):
  19. """strip(s, color=False) -> str
  20. Strips the : from the start of a string
  21. and optionally also strips all colors if
  22. color is True.
  23. """
  24. if len(s) > 0:
  25. if s[0] == ":":
  26. s = s[1:]
  27. if color:
  28. s = s.replace("\x01", "")
  29. s = s.replace("\x02", "")
  30. return s
  31. def sourceJoin(nick, ident, host):
  32. """sourceJoin(nick, ident, host) -> str
  33. Join a source previously split by sourceSplit
  34. and join it back together inserting the ! and @
  35. appropiately.
  36. """
  37. return "%s!%s@%s" % (nick, ident, host)
  38. def sourceSplit(source):
  39. """sourceSplit(source) -> str, str, str
  40. Split the given source into its parts.
  41. source must be of the form: nick!ident@host
  42. Example:
  43. >>> nick, ident, host, = sourceSplit("Joe!Blogs@localhost")
  44. """
  45. m = re.match(
  46. "(?P<nick>[^!].*)!(?P<ident>.*)@(?P<host>.*)",
  47. source)
  48. if m is not None:
  49. d = m.groupdict()
  50. return d["nick"], d["ident"], d["host"]
  51. else:
  52. return source, None, None
  53. ###
  54. ### IRC Commands
  55. ###
  56. class Command(Event):
  57. """Command (Event)"""
  58. def __init__(self, *args, **kwargs):
  59. super(Command, self).__init__(*args, **kwargs)
  60. self.channel = self.__class__.__name__
  61. class RAW(Command):
  62. """RAW command"""
  63. class PASS(Command):
  64. """PASS command"""
  65. class USER(Command):
  66. """USER command"""
  67. class NICK(Command):
  68. """NICK command"""
  69. class PING(Command):
  70. """PING command"""
  71. class PONG(Command):
  72. """PONG command"""
  73. class QUIT(Command):
  74. """QUIT command"""
  75. class JOIN(Command):
  76. """JOIN command"""
  77. class PART(Command):
  78. """PART command"""
  79. class PRIVMSG(Command):
  80. """PRIVMSG command"""
  81. class NOTICE(Command):
  82. """NOTICE command"""
  83. class CTCP(Command):
  84. """CTCP command"""
  85. class CTCPREPLY(Command):
  86. """CTCPREPLY command"""
  87. class KICK(Command):
  88. """KICK command"""
  89. class TOPIC(Command):
  90. """TOPIC command"""
  91. class MODE(Command):
  92. """MODE command"""
  93. class INVITE(Command):
  94. """INVITE command"""
  95. class NAMES(Command):
  96. """NAMES command"""
  97. ###
  98. ### IRC Responses
  99. ###
  100. class Response(Event):
  101. """Response (Event)"""
  102. def __init__(self, *args, **kwargs):
  103. super(Response, self).__init__(*args, **kwargs)
  104. class Numeric(Response):
  105. """Numeric response"""
  106. class Ping(Response):
  107. """Ping response"""
  108. class Ctcp(Response):
  109. """Ctcp response"""
  110. class Message(Response):
  111. """Message response"""
  112. class Notice(Response):
  113. """Notice response"""
  114. class Join(Response):
  115. """Join response"""
  116. class Part(Response):
  117. """Part response"""
  118. class Quit(Response):
  119. """Quit response"""
  120. class Nick(Response):
  121. """Nick response"""
  122. class Mode(Response):
  123. """Mode response"""
  124. ###
  125. ### Protocol Component(s)
  126. ###
  127. class IRC(Component):
  128. """IRC Protocol Component
  129. Creates a new IRC Component instance that implements the IRC Protocol.
  130. Incoming messages are handled by the "read" Event Handler, parsed and
  131. processed with appropriate Events created and exposed to the rest of
  132. te system to listen to and handle.
  133. @note: This Component must be used in conjunction with a Component that
  134. exposes Read Events on a "read" Channel.
  135. """
  136. def __init__(self, *args, **kwargs):
  137. super(IRC, self).__init__(*args, **kwargs)
  138. LP(**kwargs).register(self)
  139. ###
  140. ### IRC Command Event Handlers
  141. ###
  142. def RAW(self, data):
  143. self.fire(Write("%s\r\n" % data))
  144. def PASS(self, password):
  145. self.fire(RAW("PASS %s" % password))
  146. def USER(self, ident, host, server, name):
  147. self.fire(RAW("USER %s \"%s\" \"%s\" :%s" % (
  148. ident, host, server, name)))
  149. def NICK(self, nick):
  150. self.fire(RAW("NICK %s" % nick))
  151. def PING(self, server):
  152. self.fire(RAW("PING :%s" % server))
  153. def PONG(self, server):
  154. self.fire(RAW("PONG :%s" % server))
  155. def QUIT(self, message="Leaving"):
  156. self.fire(RAW("QUIT :%s" % message))
  157. def JOIN(self, channel, key=None):
  158. if key is None:
  159. self.fire(RAW("JOIN %s" % channel))
  160. else:
  161. self.fire(RAW("JOIN %s %s" % (channel, key)))
  162. def PART(self, channel, message="Leaving"):
  163. self.fire(RAW("PART %s :%s" % (channel, message)))
  164. def PRIVMSG(self, target, message):
  165. self.fire(RAW("PRIVMSG %s :%s" % (target, message)))
  166. def NOTICE(self, target, message):
  167. self.fire(RAW("NOTICE %s :%s" % (target, message)))
  168. def CTCP(self, target, type, message):
  169. self.fire(PRIVMSG(target, "%s %s" % (type, message)))
  170. def CTCPREPLY(self, target, type, message):
  171. self.fire(NOTICE(target, "%s %s" % (type, message)))
  172. def KICK(self, channel, target, message=""):
  173. self.fire(RAW("KICK %s %s :%s" % (channel, target, message)))
  174. def TOPIC(self, channel, topic):
  175. self.fire(RAW("TOPIC %s :%s" % (channel, topic)))
  176. def MODE(self, modes, channel=None):
  177. if channel is None:
  178. self.fire(RAW("MODE :%s" % modes))
  179. else:
  180. self.fire(RAW("MODE %s :%s" % (channel, modes)))
  181. def INVITE(self, target, channel):
  182. self.fire(RAW("INVITE %s %s" % (target, channel)))
  183. def NAMES(self, channel=None):
  184. if channel:
  185. self.fire(RAW("NAMES %s" % channel))
  186. else:
  187. self.fire(RAW("NAMES"))
  188. ###
  189. ### Event Processing
  190. ###
  191. def line(self, line):
  192. """Line Event Handler
  193. Process a line of text and generate the appropiate
  194. event. This must not be overridden by sub-classes,
  195. if it is, this must be explitetly called by the
  196. sub-class. Other Components may however listen to
  197. this event and process custom IRC events.
  198. """
  199. tokens = line.split(" ")
  200. if tokens[0] == "PING":
  201. self.fire(Ping(strip(tokens[1])))
  202. elif re.match("[0-9]+", tokens[1]):
  203. source = strip(tokens[0])
  204. target = tokens[2]
  205. numeric = int(tokens[1])
  206. if tokens[3].startswith(":"):
  207. arg = None
  208. message = strip(" ".join(tokens[3:]))
  209. else:
  210. arg = tokens[3]
  211. message = strip(" ".join(tokens[4:]))
  212. self.fire(Numeric(source, target, numeric, arg, message))
  213. elif tokens[1] == "PRIVMSG":
  214. source = sourceSplit(strip(tokens[0]))
  215. target = tokens[2]
  216. message = strip(" ".join(tokens[3:]))
  217. if message and message[0] == "":
  218. tokens = strip(message, color=True).split(" ")
  219. type = tokens[0]
  220. message = " ".join(tokens[1:])
  221. self.fire(Ctcp(source, target, type, message))
  222. else:
  223. self.fire(Message(source, target, message))
  224. elif tokens[1] == "NOTICE":
  225. source = sourceSplit(strip(tokens[0]))
  226. target = tokens[2]
  227. message = strip(" ".join(tokens[3:]))
  228. self.fire(Notice(source, target, message))
  229. elif tokens[1] == "JOIN":
  230. source = sourceSplit(strip(tokens[0]))
  231. channel = strip(tokens[2])
  232. self.fire(Join(source, channel))
  233. elif tokens[1] == "PART":
  234. source = sourceSplit(strip(tokens[0]))
  235. channel = strip(tokens[2])
  236. message = strip(" ".join(tokens[3:]))
  237. self.fire(Part(source, channel, message))
  238. elif tokens[1] == "QUIT":
  239. source = sourceSplit(strip(tokens[0]))
  240. message = strip(" ".join(tokens[2:]))
  241. self.fire(Quit(source, message))
  242. elif tokens[1] == "NICK":
  243. source = sourceSplit(strip(tokens[0]))
  244. newNick = strip(tokens[2])
  245. self.fire(Nick(source, newNick))
  246. elif tokens[1] == "MODE":
  247. source = sourceSplit(strip(tokens[0]))
  248. target = tokens[2]
  249. modes = strip(" ".join(tokens[3:]))
  250. self.fire(Mode(source, target, modes))
  251. ###
  252. ### Default Events
  253. ###
  254. def ping(self, server):
  255. """Ping Event
  256. This is a default event ro respond to Ping Events
  257. by sending out a Pong in response. Sub-classes
  258. may override this, but be sure to respond to
  259. Ping Events by either explitetly calling this method
  260. or sending your own Pong reponse.
  261. """
  262. self.fire(PONG(server))
  263. ###
  264. ### Errors and Numeric Replies
  265. ###
  266. RPL_WELCOME = 1
  267. RPL_YOURHOST = 2
  268. RPL_TRACELINK = 200
  269. RPL_TRACECONNECTING = 201
  270. RPL_TRACEHANDSHAKE = 202
  271. RPL_TRACEUNKNOWN = 203
  272. RPL_TRACEOPERATOR = 204
  273. RPL_TRACEUSER = 205
  274. RPL_TRACESERVER = 206
  275. RPL_TRACENEWTYPE = 208
  276. RPL_TRACELOG = 261
  277. RPL_STATSLINKINFO = 211
  278. RPL_STATSCOMMANDS = 212
  279. RPL_STATSCLINE = 213
  280. RPL_STATSNLINE = 214
  281. RPL_STATSILINE = 215
  282. RPL_STATSKLINE = 216
  283. RPL_STATSYLINE = 218
  284. RPL_ENDOFSTATS = 219
  285. RPL_STATSLLINE = 241
  286. RPL_STATSUPTIME = 242
  287. RPL_STATSOLINE = 243
  288. RPL_STATSHLINE = 244
  289. RPL_UMODEIS = 221
  290. RPL_LUSERCLIENT = 251
  291. RPL_LUSEROP = 252
  292. RPL_LUSERUNKNOWN = 253
  293. RPL_LUSERCHANNELS = 254
  294. RPL_LUSERME = 255
  295. RPL_ADMINME = 256
  296. RPL_ADMINLOC1 = 257
  297. RPL_ADMINLOC2 = 258
  298. RPL_ADMINEMAIL = 259
  299. RPL_NONE = 300
  300. RPL_USERHOST = 302
  301. RPL_ISON = 303
  302. RPL_AWAY = 301
  303. RPL_UNAWAY = 305
  304. RPL_NOWAWAY = 306
  305. RPL_WHOISUSER = 311
  306. RPL_WHOISSERVER = 312
  307. RPL_WHOISOPERATOR = 313
  308. RPL_WHOISIDLE = 317
  309. RPL_ENDOFWHOIS = 318
  310. RPL_WHOISCHANNELS = 319
  311. RPL_WHOWASUSER = 314
  312. RPL_ENDOFWHOWAS = 369
  313. RPL_LIST = 322
  314. RPL_LISTEND = 323
  315. RPL_CHANNELMODEIS = 324
  316. RPL_NOTOPIC = 331
  317. RPL_TOPIC = 332
  318. RPL_INVITING = 341
  319. RPL_SUMMONING = 342
  320. RPL_VERSION = 351
  321. RPL_WHOREPLY = 352
  322. RPL_ENDOFWHO = 315
  323. RPL_NAMREPLY = 353
  324. RPL_ENDOFNAMES = 366
  325. RPL_LINKS = 364
  326. RPL_ENDOFLINKS = 365
  327. RPL_BANLIST = 367
  328. RPL_ENDOFBANLIST = 368
  329. RPL_INFO = 371
  330. RPL_ENDOFINFO = 374
  331. RPL_MOTDSTART = 375
  332. RPL_MOTD = 372
  333. RPL_ENDOFMOTD = 376
  334. RPL_YOUREOPER = 381
  335. RPL_REHASHING = 382
  336. RPL_TIME = 391
  337. RPL_USERSSTART = 392
  338. RPL_USERS = 393
  339. RPL_ENDOFUSERS = 394
  340. RPL_NOUSERS = 395
  341. ERR_NOSUCHNICK = 401
  342. ERR_NOSUCHSERVER = 402
  343. ERR_NOSUCHCHANNEL = 403
  344. ERR_CANNOTSENDTOCHAN = 404
  345. ERR_TOOMANYCHANNELS = 405
  346. ERR_WASNOSUCHNICK = 406
  347. ERR_TOOMANYTARGETS = 407
  348. ERR_NOORIGIN = 409
  349. ERR_NORECIPIENT = 411
  350. ERR_NOTEXTTOSEND = 412
  351. ERR_NOTOPLEVEL = 413
  352. ERR_WILDTOPLEVEL = 414
  353. ERR_UNKNOWNCOMMAND = 421
  354. ERR_NOMOTD = 422
  355. ERR_NOADMININFO = 423
  356. ERR_FILEERROR = 424
  357. ERR_NONICKNAMEGIVEN = 431
  358. ERR_ERRONEUSNICKNAME = 432
  359. ERR_NICKNAMEINUSE = 433
  360. ERR_NICKCOLLISION = 436
  361. ERR_NOTONCHANNEL = 442
  362. ERR_USERONCHANNEL = 443
  363. ERR_NOLOGIN = 444
  364. ERR_SUMMONDISABLED = 445
  365. ERR_USERSDISABLED = 446
  366. ERR_NOTREGISTERED = 451
  367. ERR_NEEDMOREPARAMS = 461
  368. ERR_ALREADYREGISTRED = 462
  369. ERR_PASSWDMISMATCH = 464
  370. ERR_YOUREBANNEDCREEP = 465
  371. ERR_KEYSET = 467
  372. ERR_CHANNELISFULL = 471
  373. ERR_UNKNOWNMODE = 472
  374. ERR_INVITEONLYCHAN = 473
  375. ERR_BANNEDFROMCHAN = 474
  376. ERR_BADCHANNELKEY = 475
  377. ERR_NOPRIVILEGES = 481
  378. ERR_CHANOPRIVSNEEDED = 482
  379. ERR_CANTKILLSERVER = 483
  380. ERR_NOOPERHOST = 491
  381. ERR_UMODEUNKNOWNFLAG = 501
  382. ERR_USERSDONTMATCH = 502