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

ircbot.py 13KB

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