A Python robot that edits Wikipedia and interacts with people over IRC https://en.wikipedia.org/wiki/User:EarwigBot
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

12 лет назад
12 лет назад
12 лет назад
12 лет назад
12 лет назад
11 лет назад
12 лет назад
12 лет назад
12 лет назад
12 лет назад
12 лет назад
12 лет назад
12 лет назад
12 лет назад
12 лет назад
12 лет назад
12 лет назад
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  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:
  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. self._myhost = "." * 63 # default: longest possible hostname
  42. def __repr__(self):
  43. """Return the canonical string representation of the IRCConnection."""
  44. res = "IRCConnection(host={0!r}, port={1!r}, nick={2!r}, ident={3!r}, realname={4!r})"
  45. return res.format(self.host, self.port, self.nick, self.ident,
  46. self.realname)
  47. def __str__(self):
  48. """Return a nice string representation of the IRCConnection."""
  49. res = "<IRCConnection {0}!{1} at {2}:{3}>"
  50. return res.format(self.nick, self.ident, self.host, self.port)
  51. def _connect(self):
  52. """Connect to our IRC server."""
  53. self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  54. try:
  55. self._sock.connect((self.host, self.port))
  56. except socket.error:
  57. self.logger.exception("Couldn't connect to IRC server; retrying")
  58. sleep(8)
  59. self._connect()
  60. self._send("NICK {0}".format(self.nick))
  61. self._send("USER {0} {1} * :{2}".format(self.ident, self.host, self.realname))
  62. def _close(self):
  63. """Completely close our connection with the IRC server."""
  64. try:
  65. self._sock.shutdown(socket.SHUT_RDWR) # Shut down connection first
  66. except socket.error:
  67. pass # Ignore if the socket is already down
  68. self._sock.close()
  69. def _get(self, size=4096):
  70. """Receive (i.e. get) data from the server."""
  71. data = self._sock.recv(size)
  72. if not data:
  73. # Socket isn't giving us any data, so it is dead or broken:
  74. raise BrokenSocketError()
  75. return data.decode(errors="ignore")
  76. def _send(self, msg, hidelog=False):
  77. """Send data to the server."""
  78. with self._send_lock:
  79. time_since_last = time() - self._last_send
  80. if time_since_last < 0.75:
  81. sleep(0.75 - time_since_last)
  82. try:
  83. self._sock.sendall(msg.encode() + b"\r\n")
  84. except socket.error:
  85. self._is_running = False
  86. else:
  87. if not hidelog:
  88. self.logger.debug(msg)
  89. self._last_send = time()
  90. def _get_maxlen(self, extra):
  91. """Return our best guess of the maximum length of a standard message.
  92. This applies mainly to PRIVMSGs and NOTICEs.
  93. """
  94. base_max = 512
  95. userhost = len(self.nick) + len(self.ident) + len(self._myhost) + 2
  96. padding = 4 # "\r\n" at end, ":" at beginning, and " " after userhost
  97. return base_max - userhost - padding - extra
  98. def _split(self, msgs, extralen, maxsplits=3):
  99. """Split a large message into multiple messages."""
  100. maxlen = self._get_maxlen(extralen)
  101. words = msgs.split(" ")
  102. splits = 0
  103. while words and splits < maxsplits:
  104. splits += 1
  105. if len(words[0]) > maxlen:
  106. word = words.pop(0)
  107. yield word[:maxlen]
  108. words.insert(0, word[maxlen:])
  109. else:
  110. msg = []
  111. while words and len(" ".join(msg + [words[0]])) <= maxlen:
  112. msg.append(words.pop(0))
  113. yield " ".join(msg)
  114. def _quit(self, msg=None):
  115. """Issue a quit message to the server. Doesn't close the connection."""
  116. if msg:
  117. self._send("QUIT :{0}".format(msg))
  118. else:
  119. self._send("QUIT")
  120. def _process_defaults(self, line):
  121. """Default process hooks for lines received on IRC."""
  122. self._last_recv = time()
  123. if line[0] == "PING": # If we are pinged, pong back
  124. self.pong(line[1][1:])
  125. elif line[1] == "001": # Update nickname on startup
  126. if line[2] != self.nick:
  127. self.logger.warn("Nickname changed from {0} to {1}".format(
  128. self.nick, line[2]))
  129. self._nick = line[2]
  130. elif line[1] == "376": # After sign-on, get our userhost
  131. self._send("WHOIS {0}".format(self.nick))
  132. elif line[1] == "311": # Receiving WHOIS result
  133. if line[2] == self.nick:
  134. self._ident = line[4]
  135. self._myhost = line[5]
  136. elif line[1] == "396": # Hostname change
  137. self._myhost = line[3]
  138. def _process_message(self, line):
  139. """To be overridden in subclasses."""
  140. raise NotImplementedError()
  141. @property
  142. def host(self):
  143. """The hostname of the IRC server, like ``"irc.libera.chat"``."""
  144. return self._host
  145. @property
  146. def port(self):
  147. """The port of the IRC server, like ``6667``."""
  148. return self._port
  149. @property
  150. def nick(self):
  151. """Our nickname on the server, like ``"EarwigBot"``."""
  152. return self._nick
  153. @property
  154. def ident(self):
  155. """Our ident on the server, like ``"earwig"``.
  156. See https://en.wikipedia.org/wiki/Ident_protocol.
  157. """
  158. return self._ident
  159. @property
  160. def realname(self):
  161. """Our realname (gecos field) on the server."""
  162. return self._realname
  163. def say(self, target, msg, hidelog=False):
  164. """Send a private message to a target on the server."""
  165. for msg in self._split(msg, len(target) + 10):
  166. msg = "PRIVMSG {0} :{1}".format(target, msg)
  167. self._send(msg, hidelog)
  168. def reply(self, data, msg, hidelog=False):
  169. """Send a private message as a reply to a user on the server."""
  170. if data.is_private:
  171. self.say(data.chan, msg, hidelog)
  172. else:
  173. msg = "\x02{0}\x0F: {1}".format(data.reply_nick, msg)
  174. self.say(data.chan, msg, hidelog)
  175. def action(self, target, msg, hidelog=False):
  176. """Send a private message to a target on the server as an action."""
  177. msg = "\x01ACTION {0}\x01".format(msg)
  178. self.say(target, msg, hidelog)
  179. def notice(self, target, msg, hidelog=False):
  180. """Send a notice to a target on the server."""
  181. for msg in self._split(msg, len(target) + 9):
  182. msg = "NOTICE {0} :{1}".format(target, msg)
  183. self._send(msg, hidelog)
  184. def join(self, chan, hidelog=False):
  185. """Join a channel on the server."""
  186. msg = "JOIN {0}".format(chan)
  187. self._send(msg, hidelog)
  188. def part(self, chan, msg=None, hidelog=False):
  189. """Part from a channel on the server, optionally using an message."""
  190. if msg:
  191. self._send("PART {0} :{1}".format(chan, msg), hidelog)
  192. else:
  193. self._send("PART {0}".format(chan), hidelog)
  194. def mode(self, target, level, msg, hidelog=False):
  195. """Send a mode message to the server."""
  196. msg = "MODE {0} {1} {2}".format(target, level, msg)
  197. self._send(msg, hidelog)
  198. def ping(self, target, hidelog=False):
  199. """Ping another entity on the server."""
  200. msg = "PING {0}".format(target)
  201. self._send(msg, hidelog)
  202. def pong(self, target, hidelog=False):
  203. """Pong another entity on the server."""
  204. msg = "PONG {0}".format(target)
  205. self._send(msg, hidelog)
  206. def loop(self):
  207. """Main loop for the IRC connection."""
  208. self._is_running = True
  209. read_buffer = ""
  210. while 1:
  211. try:
  212. read_buffer += self._get()
  213. except BrokenSocketError:
  214. self._is_running = False
  215. break
  216. lines = read_buffer.split("\n")
  217. read_buffer = lines.pop()
  218. for line in lines:
  219. line = line.strip().split()
  220. self._process_defaults(line)
  221. self._process_message(line)
  222. if self.is_stopped():
  223. break
  224. self._close()
  225. def keep_alive(self):
  226. """Ensure that we stay connected, stopping if the connection breaks."""
  227. now = time()
  228. if now - self._last_recv > 120:
  229. if self._last_ping < self._last_recv:
  230. log = "Last message was received over 120 seconds ago. Pinging."
  231. self.logger.debug(log)
  232. self.ping(self.host)
  233. self._last_ping = now
  234. elif now - self._last_ping > 60:
  235. self.logger.debug("No ping response in 60 seconds. Stopping.")
  236. self.stop()
  237. def stop(self, msg=None):
  238. """Request the IRC connection to close at earliest convenience."""
  239. if self._is_running:
  240. self._quit(msg)
  241. self._is_running = False
  242. def is_stopped(self):
  243. """Return whether the IRC connection has been (or is to be) closed."""
  244. return not self._is_running