A Python robot that edits Wikipedia and interacts with people over IRC https://en.wikipedia.org/wiki/User:EarwigBot
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

260 lines
9.1 KiB

  1. # -*- coding: utf-8 -*-
  2. #
  3. # Copyright (C) 2009-2015 Ben Kurtovic <ben.kurtovic@gmail.com>
  4. #
  5. # Permission is hereby granted, free of charge, to any person obtaining a copy
  6. # of this software and associated documentation files (the "Software"), to deal
  7. # in the Software without restriction, including without limitation the rights
  8. # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9. # copies of the Software, and to permit persons to whom the Software is
  10. # furnished to do so, subject to the following conditions:
  11. #
  12. # The above copyright notice and this permission notice shall be included in
  13. # all copies or substantial portions of the Software.
  14. #
  15. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  16. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  17. # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  18. # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  19. # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  20. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  21. # SOFTWARE.
  22. import socket
  23. from threading import Lock
  24. from time import sleep, time
  25. from earwigbot.exceptions import BrokenSocketError
  26. __all__ = ["IRCConnection"]
  27. class IRCConnection(object):
  28. """Interface with an IRC server."""
  29. def __init__(self, host, port, nick, ident, realname, logger):
  30. self._host = host
  31. self._port = port
  32. self._nick = nick
  33. self._ident = ident
  34. self._realname = realname
  35. self.logger = logger
  36. self._is_running = False
  37. self._send_lock = Lock()
  38. self._last_recv = time()
  39. self._last_send = 0
  40. self._last_ping = 0
  41. def __repr__(self):
  42. """Return the canonical string representation of the IRCConnection."""
  43. res = "IRCConnection(host={0!r}, port={1!r}, nick={2!r}, ident={3!r}, realname={4!r})"
  44. return res.format(self.host, self.port, self.nick, self.ident,
  45. self.realname)
  46. def __str__(self):
  47. """Return a nice string representation of the IRCConnection."""
  48. res = "<IRCConnection {0}!{1} at {2}:{3}>"
  49. return res.format(self.nick, self.ident, self.host, self.port)
  50. def _connect(self):
  51. """Connect to our IRC server."""
  52. self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  53. try:
  54. self._sock.connect((self.host, self.port))
  55. except socket.error:
  56. self.logger.exception("Couldn't connect to IRC server; retrying")
  57. sleep(8)
  58. self._connect()
  59. self._send("NICK {0}".format(self.nick))
  60. self._send("USER {0} {1} * :{2}".format(self.ident, self.host, self.realname))
  61. def _close(self):
  62. """Completely close our connection with the IRC server."""
  63. try:
  64. self._sock.shutdown(socket.SHUT_RDWR) # Shut down connection first
  65. except socket.error:
  66. pass # Ignore if the socket is already down
  67. self._sock.close()
  68. def _get(self, size=4096):
  69. """Receive (i.e. get) data from the server."""
  70. data = self._sock.recv(size)
  71. if not data:
  72. # Socket isn't giving us any data, so it is dead or broken:
  73. raise BrokenSocketError()
  74. return data
  75. def _send(self, msg, hidelog=False):
  76. """Send data to the server."""
  77. with self._send_lock:
  78. time_since_last = time() - self._last_send
  79. if time_since_last < 0.75:
  80. sleep(0.75 - time_since_last)
  81. try:
  82. self._sock.sendall(msg + "\r\n")
  83. except socket.error:
  84. self._is_running = False
  85. else:
  86. if not hidelog:
  87. self.logger.debug(msg)
  88. self._last_send = time()
  89. def _split(self, msgs, maxlen, maxsplits=3):
  90. """Split a large message into multiple messages smaller than maxlen."""
  91. words = msgs.split(" ")
  92. splits = 0
  93. while words and splits < maxsplits:
  94. splits += 1
  95. if len(words[0]) > maxlen:
  96. word = words.pop(0)
  97. yield word[:maxlen]
  98. words.insert(0, word[maxlen:])
  99. else:
  100. msg = []
  101. while words and len(" ".join(msg + [words[0]])) <= maxlen:
  102. msg.append(words.pop(0))
  103. yield " ".join(msg)
  104. def _quit(self, msg=None):
  105. """Issue a quit message to the server. Doesn't close the connection."""
  106. if msg:
  107. self._send("QUIT :{0}".format(msg))
  108. else:
  109. self._send("QUIT")
  110. def _process_defaults(self, line):
  111. """Default process hooks for lines received on IRC."""
  112. self._last_recv = time()
  113. if line[0] == "PING": # If we are pinged, pong back
  114. self.pong(line[1][1:])
  115. def _process_message(self, line):
  116. """To be overridden in subclasses."""
  117. raise NotImplementedError()
  118. @property
  119. def host(self):
  120. """The hostname of the IRC server, like ``"irc.freenode.net"``."""
  121. return self._host
  122. @property
  123. def port(self):
  124. """The port of the IRC server, like ``6667``."""
  125. return self._port
  126. @property
  127. def nick(self):
  128. """Our nickname on the server, like ``"EarwigBot"``."""
  129. return self._nick
  130. @property
  131. def ident(self):
  132. """Our ident on the server, like ``"earwig"``.
  133. See http://en.wikipedia.org/wiki/Ident.
  134. """
  135. return self._ident
  136. @property
  137. def realname(self):
  138. """Our realname (gecos field) on the server."""
  139. return self._realname
  140. def say(self, target, msg, hidelog=False):
  141. """Send a private message to a target on the server."""
  142. for msg in self._split(msg, 400):
  143. msg = "PRIVMSG {0} :{1}".format(target, msg)
  144. self._send(msg, hidelog)
  145. def reply(self, data, msg, hidelog=False):
  146. """Send a private message as a reply to a user on the server."""
  147. if data.is_private:
  148. self.say(data.chan, msg, hidelog)
  149. else:
  150. msg = "\x02{0}\x0F: {1}".format(data.nick, msg)
  151. self.say(data.chan, msg, hidelog)
  152. def action(self, target, msg, hidelog=False):
  153. """Send a private message to a target on the server as an action."""
  154. msg = "\x01ACTION {0}\x01".format(msg)
  155. self.say(target, msg, hidelog)
  156. def notice(self, target, msg, hidelog=False):
  157. """Send a notice to a target on the server."""
  158. for msg in self._split(msg, 400):
  159. msg = "NOTICE {0} :{1}".format(target, msg)
  160. self._send(msg, hidelog)
  161. def join(self, chan, hidelog=False):
  162. """Join a channel on the server."""
  163. msg = "JOIN {0}".format(chan)
  164. self._send(msg, hidelog)
  165. def part(self, chan, msg=None, hidelog=False):
  166. """Part from a channel on the server, optionally using an message."""
  167. if msg:
  168. self._send("PART {0} :{1}".format(chan, msg), hidelog)
  169. else:
  170. self._send("PART {0}".format(chan), hidelog)
  171. def mode(self, target, level, msg, hidelog=False):
  172. """Send a mode message to the server."""
  173. msg = "MODE {0} {1} {2}".format(target, level, msg)
  174. self._send(msg, hidelog)
  175. def ping(self, target, hidelog=False):
  176. """Ping another entity on the server."""
  177. msg = "PING {0}".format(target)
  178. self._send(msg, hidelog)
  179. def pong(self, target, hidelog=False):
  180. """Pong another entity on the server."""
  181. msg = "PONG {0}".format(target)
  182. self._send(msg, hidelog)
  183. def loop(self):
  184. """Main loop for the IRC connection."""
  185. self._is_running = True
  186. read_buffer = ""
  187. while 1:
  188. try:
  189. read_buffer += self._get()
  190. except BrokenSocketError:
  191. self._is_running = False
  192. break
  193. lines = read_buffer.split("\n")
  194. read_buffer = lines.pop()
  195. for line in lines:
  196. line = line.strip().split()
  197. self._process_defaults(line)
  198. self._process_message(line)
  199. if self.is_stopped():
  200. break
  201. self._close()
  202. def keep_alive(self):
  203. """Ensure that we stay connected, stopping if the connection breaks."""
  204. now = time()
  205. if now - self._last_recv > 120:
  206. if self._last_ping < self._last_recv:
  207. log = "Last message was received over 120 seconds ago. Pinging."
  208. self.logger.debug(log)
  209. self.ping(self.host)
  210. self._last_ping = now
  211. elif now - self._last_ping > 60:
  212. self.logger.debug("No ping response in 60 seconds. Stopping.")
  213. self.stop()
  214. def stop(self, msg=None):
  215. """Request the IRC connection to close at earliest convenience."""
  216. if self._is_running:
  217. self._quit(msg)
  218. self._is_running = False
  219. def is_stopped(self):
  220. """Return whether the IRC connection has been (or is to be) closed."""
  221. return not self._is_running