irclib -- Internet Relay Chat (IRC) protocol client library

ircbot.py 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. #! -*- coding: utf-8 -*-
  2. # Copyright (C) 1999-2002 Joel Rosdahl
  3. # Portions Copyring © 2011-2012 Jason R. Coombs
  4. #
  5. # This library is free software; you can redistribute it and/or
  6. # modify it under the terms of the GNU Lesser General Public
  7. # License as published by the Free Software Foundation; either
  8. # version 2.1 of the License, or (at your option) any later version.
  9. #
  10. # This library is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  13. # Lesser General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU Lesser General Public
  16. # License along with this library; if not, write to the Free Software
  17. # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  18. #
  19. # Joel Rosdahl <joel@rosdahl.net>
  20. """
  21. ircbot -- Simple IRC bot library.
  22. This module contains a single-server IRC bot class that can be used to
  23. write simpler bots.
  24. """
  25. import sys
  26. import irclib
  27. from irclib import nm_to_n
  28. class SingleServerIRCBot(irclib.SimpleIRCClient):
  29. """A single-server IRC bot class.
  30. The bot tries to reconnect if it is disconnected.
  31. The bot keeps track of the channels it has joined, the other
  32. clients that are present in the channels and which of those that
  33. have operator or voice modes. The "database" is kept in the
  34. self.channels attribute, which is an IRCDict of Channels.
  35. """
  36. def __init__(self, server_list, nickname, realname, reconnection_interval=60):
  37. """Constructor for SingleServerIRCBot objects.
  38. Arguments:
  39. server_list -- A list of tuples (server, port) that
  40. defines which servers the bot should try to
  41. connect to.
  42. nickname -- The bot's nickname.
  43. realname -- The bot's realname.
  44. reconnection_interval -- How long the bot should wait
  45. before trying to reconnect.
  46. dcc_connections -- A list of initiated/accepted DCC
  47. connections.
  48. """
  49. super(SingleServerIRCBot, self).__init__()
  50. self.channels = IRCDict()
  51. self.server_list = server_list
  52. if not reconnection_interval or reconnection_interval < 0:
  53. reconnection_interval = 2 ** 31
  54. self.reconnection_interval = reconnection_interval
  55. self._nickname = nickname
  56. self._realname = realname
  57. for i in ["disconnect", "join", "kick", "mode",
  58. "namreply", "nick", "part", "quit"]:
  59. self.connection.add_global_handler(i,
  60. getattr(self, "_on_" + i),
  61. -20)
  62. def _connected_checker(self):
  63. """[Internal]"""
  64. if not self.connection.is_connected():
  65. self.connection.execute_delayed(self.reconnection_interval,
  66. self._connected_checker)
  67. self.jump_server()
  68. def _connect(self):
  69. """[Internal]"""
  70. password = None
  71. if len(self.server_list[0]) > 2:
  72. password = self.server_list[0][2]
  73. try:
  74. self.connect(self.server_list[0][0],
  75. self.server_list[0][1],
  76. self._nickname,
  77. password,
  78. ircname=self._realname)
  79. except irclib.ServerConnectionError:
  80. pass
  81. def _on_disconnect(self, c, e):
  82. """[Internal]"""
  83. self.channels = IRCDict()
  84. self.connection.execute_delayed(self.reconnection_interval,
  85. self._connected_checker)
  86. def _on_join(self, c, e):
  87. """[Internal]"""
  88. ch = e.target()
  89. nick = nm_to_n(e.source())
  90. if nick == c.get_nickname():
  91. self.channels[ch] = Channel()
  92. self.channels[ch].add_user(nick)
  93. def _on_kick(self, c, e):
  94. """[Internal]"""
  95. nick = e.arguments()[0]
  96. channel = e.target()
  97. if nick == c.get_nickname():
  98. del self.channels[channel]
  99. else:
  100. self.channels[channel].remove_user(nick)
  101. def _on_mode(self, c, e):
  102. """[Internal]"""
  103. modes = irclib.parse_channel_modes(" ".join(e.arguments()))
  104. t = e.target()
  105. if irclib.is_channel(t):
  106. ch = self.channels[t]
  107. for mode in modes:
  108. if mode[0] == "+":
  109. f = ch.set_mode
  110. else:
  111. f = ch.clear_mode
  112. f(mode[1], mode[2])
  113. else:
  114. # Mode on self... XXX
  115. pass
  116. def _on_namreply(self, c, e):
  117. """[Internal]"""
  118. # e.arguments()[0] == "@" for secret channels,
  119. # "*" for private channels,
  120. # "=" for others (public channels)
  121. # e.arguments()[1] == channel
  122. # e.arguments()[2] == nick list
  123. ch = e.arguments()[1]
  124. for nick in e.arguments()[2].split():
  125. if nick[0] == "@":
  126. nick = nick[1:]
  127. self.channels[ch].set_mode("o", nick)
  128. elif nick[0] == "+":
  129. nick = nick[1:]
  130. self.channels[ch].set_mode("v", nick)
  131. self.channels[ch].add_user(nick)
  132. def _on_nick(self, c, e):
  133. """[Internal]"""
  134. before = nm_to_n(e.source())
  135. after = e.target()
  136. for ch in self.channels.values():
  137. if ch.has_user(before):
  138. ch.change_nick(before, after)
  139. def _on_part(self, c, e):
  140. """[Internal]"""
  141. nick = nm_to_n(e.source())
  142. channel = e.target()
  143. if nick == c.get_nickname():
  144. del self.channels[channel]
  145. else:
  146. self.channels[channel].remove_user(nick)
  147. def _on_quit(self, c, e):
  148. """[Internal]"""
  149. nick = nm_to_n(e.source())
  150. for ch in self.channels.values():
  151. if ch.has_user(nick):
  152. ch.remove_user(nick)
  153. def die(self, msg="Bye, cruel world!"):
  154. """Let the bot die.
  155. Arguments:
  156. msg -- Quit message.
  157. """
  158. self.connection.disconnect(msg)
  159. sys.exit(0)
  160. def disconnect(self, msg="I'll be back!"):
  161. """Disconnect the bot.
  162. The bot will try to reconnect after a while.
  163. Arguments:
  164. msg -- Quit message.
  165. """
  166. self.connection.disconnect(msg)
  167. def get_version(self):
  168. """Returns the bot version.
  169. Used when answering a CTCP VERSION request.
  170. """
  171. return "ircbot.py by Joel Rosdahl <joel@rosdahl.net>"
  172. def jump_server(self, msg="Changing servers"):
  173. """Connect to a new server, possibly disconnecting from the current.
  174. The bot will skip to next server in the server_list each time
  175. jump_server is called.
  176. """
  177. if self.connection.is_connected():
  178. self.connection.disconnect(msg)
  179. self.server_list.append(self.server_list.pop(0))
  180. self._connect()
  181. def on_ctcp(self, c, e):
  182. """Default handler for ctcp events.
  183. Replies to VERSION and PING requests and relays DCC requests
  184. to the on_dccchat method.
  185. """
  186. if e.arguments()[0] == "VERSION":
  187. c.ctcp_reply(nm_to_n(e.source()),
  188. "VERSION " + self.get_version())
  189. elif e.arguments()[0] == "PING":
  190. if len(e.arguments()) > 1:
  191. c.ctcp_reply(nm_to_n(e.source()),
  192. "PING " + e.arguments()[1])
  193. elif e.arguments()[0] == "DCC" and e.arguments()[1].split(" ", 1)[0] == "CHAT":
  194. self.on_dccchat(c, e)
  195. def on_dccchat(self, c, e):
  196. pass
  197. def start(self):
  198. """Start the bot."""
  199. self._connect()
  200. super(SingleServerIRCBot, self).start()
  201. class Channel(object):
  202. """A class for keeping information about an IRC channel.
  203. This class can be improved a lot.
  204. """
  205. def __init__(self):
  206. self.userdict = IRCDict()
  207. self.operdict = IRCDict()
  208. self.voiceddict = IRCDict()
  209. self.modes = {}
  210. def users(self):
  211. """Returns an unsorted list of the channel's users."""
  212. return self.userdict.keys()
  213. def opers(self):
  214. """Returns an unsorted list of the channel's operators."""
  215. return self.operdict.keys()
  216. def voiced(self):
  217. """Returns an unsorted list of the persons that have voice
  218. mode set in the channel."""
  219. return self.voiceddict.keys()
  220. def has_user(self, nick):
  221. """Check whether the channel has a user."""
  222. return nick in self.userdict
  223. def is_oper(self, nick):
  224. """Check whether a user has operator status in the channel."""
  225. return nick in self.operdict
  226. def is_voiced(self, nick):
  227. """Check whether a user has voice mode set in the channel."""
  228. return nick in self.voiceddict
  229. def add_user(self, nick):
  230. self.userdict[nick] = 1
  231. def remove_user(self, nick):
  232. for d in self.userdict, self.operdict, self.voiceddict:
  233. if nick in d:
  234. del d[nick]
  235. def change_nick(self, before, after):
  236. self.userdict[after] = self.userdict.pop(before)
  237. if before in self.operdict:
  238. self.operdict[after] = self.operdict.pop(before)
  239. if before in self.voiceddict:
  240. self.voiceddict[after] = self.voiceddict.pop(before)
  241. def set_userdetails(self, nick, details):
  242. if nick in self.userdict:
  243. self.userdict[nick] = details
  244. def set_mode(self, mode, value=None):
  245. """Set mode on the channel.
  246. Arguments:
  247. mode -- The mode (a single-character string).
  248. value -- Value
  249. """
  250. if mode == "o":
  251. self.operdict[value] = 1
  252. elif mode == "v":
  253. self.voiceddict[value] = 1
  254. else:
  255. self.modes[mode] = value
  256. def clear_mode(self, mode, value=None):
  257. """Clear mode on the channel.
  258. Arguments:
  259. mode -- The mode (a single-character string).
  260. value -- Value
  261. """
  262. try:
  263. if mode == "o":
  264. del self.operdict[value]
  265. elif mode == "v":
  266. del self.voiceddict[value]
  267. else:
  268. del self.modes[mode]
  269. except KeyError:
  270. pass
  271. def has_mode(self, mode):
  272. return mode in self.modes
  273. def is_moderated(self):
  274. return self.has_mode("m")
  275. def is_secret(self):
  276. return self.has_mode("s")
  277. def is_protected(self):
  278. return self.has_mode("p")
  279. def has_topic_lock(self):
  280. return self.has_mode("t")
  281. def is_invite_only(self):
  282. return self.has_mode("i")
  283. def has_allow_external_messages(self):
  284. return self.has_mode("n")
  285. def has_limit(self):
  286. return self.has_mode("l")
  287. def limit(self):
  288. if self.has_limit():
  289. return self.modes["l"]
  290. else:
  291. return None
  292. def has_key(self):
  293. return self.has_mode("k")
  294. # from jaraco.util.dictlib
  295. class KeyTransformingDict(dict):
  296. """
  297. A dict subclass that transforms the keys before they're used.
  298. Subclasses may override the default key_transform to customize behavior.
  299. """
  300. @staticmethod
  301. def key_transform(key):
  302. return key
  303. def __init__(self, *args, **kargs):
  304. super(KeyTransformingDict, self).__init__()
  305. # build a dictionary using the default constructs
  306. d = dict(*args, **kargs)
  307. # build this dictionary using transformed keys.
  308. for item in d.items():
  309. self.__setitem__(*item)
  310. def __setitem__(self, key, val):
  311. key = self.key_transform(key)
  312. super(KeyTransformingDict, self).__setitem__(key, val)
  313. def __getitem__(self, key):
  314. key = self.key_transform(key)
  315. return super(KeyTransformingDict, self).__getitem__(key)
  316. def __contains__(self, key):
  317. key = self.key_transform(key)
  318. return super(KeyTransformingDict, self).__contains__(key)
  319. def __delitem__(self, key):
  320. key = self.key_transform(key)
  321. return super(KeyTransformingDict, self).__delitem__(key)
  322. def setdefault(self, key, *args, **kwargs):
  323. key = self.key_transform(key)
  324. return super(KeyTransformingDict, self).setdefault(key, *args, **kwargs)
  325. def pop(self, key, *args, **kwargs):
  326. key = self.key_transform(key)
  327. return super(KeyTransformingDict, self).pop(key, *args, **kwargs)
  328. class IRCDict(KeyTransformingDict):
  329. """
  330. A dictionary of names whose keys are case-insensitive according to the
  331. IRC RFC rules.
  332. >>> d = IRCDict({'[This]': 'that'}, A='foo')
  333. The dict maintains the original case:
  334. >>> d.keys()
  335. ['A', '[This]']
  336. But the keys can be referenced with a different case
  337. >>> d['a']
  338. 'foo'
  339. >>> d['{this}']
  340. 'that'
  341. >>> d['{THIS}']
  342. 'that'
  343. >>> '{thiS]' in d
  344. True
  345. This should work for operations like delete and pop as well.
  346. >>> d.pop('A')
  347. 'foo'
  348. >>> del d['{This}']
  349. >>> len(d)
  350. 0
  351. """
  352. @staticmethod
  353. def key_transform(key):
  354. if isinstance(key, basestring):
  355. key = irclib.IRCFoldedCase(key)
  356. return key