A Python robot that edits Wikipedia and interacts with people over IRC https://en.wikipedia.org/wiki/User:EarwigBot
Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

229 rindas
7.9 KiB

  1. # -*- coding: utf-8 -*-
  2. #
  3. # Copyright (C) 2009-2012 Ben Kurtovic <ben.kurtovic@verizon.net>
  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):
  30. self._host = host
  31. self._port = port
  32. self._nick = nick
  33. self._ident = ident
  34. self._realname = realname
  35. self._is_running = False
  36. self._send_lock = Lock()
  37. self._last_recv = time()
  38. self._last_ping = 0
  39. def __repr__(self):
  40. """Return the canonical string representation of the IRCConnection."""
  41. res = "IRCConnection(host={0!r}, port={1!r}, nick={2!r}, ident={3!r}, realname={4!r})"
  42. return res.format(self.host, self.port, self.nick, self.ident,
  43. self.realname)
  44. def __str__(self):
  45. """Return a nice string representation of the IRCConnection."""
  46. res = "<IRCConnection {0}!{1} at {2}:{3}>"
  47. return res.format(self.nick, self.ident, self.host, self.port)
  48. def _connect(self):
  49. """Connect to our IRC server."""
  50. self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  51. try:
  52. self._sock.connect((self.host, self.port))
  53. except socket.error:
  54. self.logger.exception("Couldn't connect to IRC server; retrying")
  55. sleep(8)
  56. self._connect()
  57. self._send("NICK {0}".format(self.nick))
  58. self._send("USER {0} {1} * :{2}".format(self.ident, self.host, self.realname))
  59. def _close(self):
  60. """Completely close our connection with the IRC server."""
  61. try:
  62. self._sock.shutdown(socket.SHUT_RDWR) # Shut down connection first
  63. except socket.error:
  64. pass # Ignore if the socket is already down
  65. self._sock.close()
  66. def _get(self, size=4096):
  67. """Receive (i.e. get) data from the server."""
  68. data = self._sock.recv(size)
  69. if not data:
  70. # Socket isn't giving us any data, so it is dead or broken:
  71. raise BrokenSocketError()
  72. return data
  73. def _send(self, msg, hidelog=False):
  74. """Send data to the server."""
  75. with self._send_lock:
  76. try:
  77. self._sock.sendall(msg + "\r\n")
  78. except socket.error:
  79. self._is_running = False
  80. else:
  81. if not hidelog:
  82. self.logger.debug(msg)
  83. def _quit(self, msg=None):
  84. """Issue a quit message to the server. Doesn't close the connection."""
  85. if msg:
  86. self._send("QUIT :{0}".format(msg))
  87. else:
  88. self._send("QUIT")
  89. def _process_defaults(self, line):
  90. """Default process hooks for lines received on IRC."""
  91. self._last_recv = time()
  92. if line[0] == "PING": # If we are pinged, pong back
  93. self.pong(line[1][1:])
  94. @property
  95. def host(self):
  96. """The hostname of the IRC server, like ``"irc.freenode.net"``."""
  97. return self._host
  98. @property
  99. def port(self):
  100. """The port of the IRC server, like ``6667``."""
  101. return self._port
  102. @property
  103. def nick(self):
  104. """Our nickname on the server, like ``"EarwigBot"``."""
  105. return self._nick
  106. @property
  107. def ident(self):
  108. """Our ident on the server, like ``"earwig"``.
  109. See http://en.wikipedia.org/wiki/Ident.
  110. """
  111. return self._ident
  112. @property
  113. def realname(self):
  114. """Our realname (gecos field) on the server."""
  115. return self._realname
  116. def say(self, target, msg, hidelog=False):
  117. """Send a private message to a target on the server."""
  118. msg = "PRIVMSG {0} :{1}".format(target, msg)
  119. self._send(msg, hidelog)
  120. def reply(self, data, msg, hidelog=False):
  121. """Send a private message as a reply to a user on the server."""
  122. msg = "\x02{0}\x0f: {1}".format(data.nick, msg)
  123. self.say(data.chan, msg, hidelog)
  124. def action(self, target, msg, hidelog=False):
  125. """Send a private message to a target on the server as an action."""
  126. msg = "\x01ACTION {0}\x01".format(msg)
  127. self.say(target, msg, hidelog)
  128. def notice(self, target, msg, hidelog=False):
  129. """Send a notice to a target on the server."""
  130. msg = "NOTICE {0} :{1}".format(target, msg)
  131. self._send(msg, hidelog)
  132. def join(self, chan, hidelog=False):
  133. """Join a channel on the server."""
  134. msg = "JOIN {0}".format(chan)
  135. self._send(msg, hidelog)
  136. def part(self, chan, msg=None, hidelog=False):
  137. """Part from a channel on the server, optionally using an message."""
  138. if msg:
  139. self._send("PART {0} :{1}".format(chan, msg), hidelog)
  140. else:
  141. self._send("PART {0}".format(chan), hidelog)
  142. def mode(self, target, level, msg, hidelog=False):
  143. """Send a mode message to the server."""
  144. msg = "MODE {0} {1} {2}".format(target, level, msg)
  145. self._send(msg, hidelog)
  146. def ping(self, target, hidelog=False):
  147. """Ping another entity on the server."""
  148. msg = "PING {0}".format(target)
  149. self._send(msg, hidelog)
  150. def pong(self, target, hidelog=False):
  151. """Pong another entity on the server."""
  152. msg = "PONG {0}".format(target)
  153. self._send(msg, hidelog)
  154. def loop(self):
  155. """Main loop for the IRC connection."""
  156. self._is_running = True
  157. read_buffer = ""
  158. while 1:
  159. try:
  160. read_buffer += self._get()
  161. except BrokenSocketError:
  162. self._is_running = False
  163. break
  164. lines = read_buffer.split("\n")
  165. read_buffer = lines.pop()
  166. for line in lines:
  167. line = line.strip().split()
  168. self._process_defaults(line)
  169. self._process_message(line)
  170. if self.is_stopped():
  171. break
  172. self._close()
  173. def keep_alive(self):
  174. """Ensure that we stay connected, stopping if the connection breaks."""
  175. now = time()
  176. if now - self._last_recv > 120:
  177. if self._last_ping < self._last_recv:
  178. log = "Last message was received over 120 seconds ago. Pinging."
  179. self.logger.debug(log)
  180. self.ping(self.host)
  181. self._last_ping = now
  182. elif now - self._last_ping > 60:
  183. self.logger.debug("No ping response in 60 seconds. Stopping.")
  184. self.stop()
  185. def stop(self, msg=None):
  186. """Request the IRC connection to close at earliest convenience."""
  187. if self._is_running:
  188. self._quit(msg)
  189. self._is_running = False
  190. def is_stopped(self):
  191. """Return whether the IRC connection has been (or is to be) closed."""
  192. return not self._is_running