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.

211 rindas
7.3 KiB

  1. # Copyright (C) 2009-2016 Ben Kurtovic <ben.kurtovic@gmail.com>
  2. #
  3. # Permission is hereby granted, free of charge, to any person obtaining a copy
  4. # of this software and associated documentation files (the "Software"), to deal
  5. # in the Software without restriction, including without limitation the rights
  6. # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  7. # copies of the Software, and to permit persons to whom the Software is
  8. # furnished to do so, subject to the following conditions:
  9. #
  10. # The above copyright notice and this permission notice shall be included in
  11. # all copies or substantial portions of the Software.
  12. #
  13. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  14. # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  15. # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  16. # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  17. # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  18. # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  19. # SOFTWARE.
  20. import re
  21. import socket
  22. from collections import namedtuple
  23. from socket import AF_INET, AF_INET6
  24. from earwigbot.commands import Command
  25. _IP = namedtuple("_IP", ["family", "ip", "size"])
  26. _Range = namedtuple("_Range", ["family", "range", "low", "high", "size", "addresses"])
  27. class CIDR(Command):
  28. """Calculates the smallest CIDR range that encompasses a list of IP
  29. addresses. Used to make range blocks."""
  30. name = "cidr"
  31. commands = [
  32. "cidr",
  33. "range",
  34. "rangeblock",
  35. "rangecalc",
  36. "blockcalc",
  37. "iprange",
  38. "cdir",
  39. ]
  40. # https://www.mediawiki.org/wiki/Manual:$wgBlockCIDRLimit
  41. LIMIT_IPv4 = 16
  42. LIMIT_IPv6 = 19
  43. def process(self, data):
  44. if not data.args:
  45. msg = (
  46. "Specify a list of IP addresses to calculate a CIDR range "
  47. "for. For example, \x0306!{0} 192.168.0.3 192.168.0.15 "
  48. "192.168.1.4\x0f or \x0306!{0} 2500:1:2:3:: "
  49. "2500:1:2:3:dead:beef::\x0f."
  50. )
  51. self.reply(data, msg.format(data.command))
  52. return
  53. try:
  54. ips = [self._parse_ip(arg) for arg in data.args]
  55. except ValueError as exc:
  56. self.reply(data, f"Can't parse IP address: \x0302{exc}\x0f")
  57. return
  58. if any(ip.family == AF_INET for ip in ips) and any(
  59. ip.family == AF_INET6 for ip in ips
  60. ):
  61. msg = "Can't calculate a range for both IPv4 and IPv6 addresses."
  62. self.reply(data, msg)
  63. return
  64. cidr = self._calculate_range(ips[0].family, ips)
  65. descr = self._describe(cidr.family, cidr.size)
  66. msg = (
  67. "Smallest CIDR range is \x02{0}\x0f, covering {1} from "
  68. "\x0305{2}\x0f to \x0305{3}\x0f{4}."
  69. )
  70. self.reply(
  71. data,
  72. msg.format(
  73. cidr.range,
  74. cidr.addresses,
  75. cidr.low,
  76. cidr.high,
  77. f" (\x0304{descr}\x0f)" if descr else "",
  78. ),
  79. )
  80. def _parse_ip(self, arg):
  81. """Converts an argument into an IP address object."""
  82. arg = self._parse_arg(arg)
  83. oldarg = arg
  84. size = None
  85. if "/" in arg:
  86. arg, size = arg.split("/", 1)
  87. try:
  88. size = int(size, 10)
  89. except ValueError:
  90. raise ValueError(oldarg)
  91. if size < 0 or size > 128:
  92. raise ValueError(oldarg)
  93. try:
  94. ip = _IP(AF_INET, socket.inet_pton(AF_INET, arg), size)
  95. except OSError:
  96. try:
  97. return _IP(AF_INET6, socket.inet_pton(AF_INET6, arg), size)
  98. except OSError:
  99. raise ValueError(oldarg)
  100. else:
  101. if size and size > 32:
  102. raise ValueError(oldarg)
  103. return ip
  104. def _parse_arg(self, arg):
  105. """Converts an argument into an IP address string."""
  106. if "[[" in arg and "]]" in arg:
  107. regex = r"\[\[\s*(?:User(?:\stalk)?:)?(.*?)(?:\|.*?)?\s*\]\]"
  108. match = re.search(regex, arg, re.I)
  109. if not match:
  110. raise ValueError(arg)
  111. arg = match.group(1)
  112. if re.match(r"https?://", arg):
  113. if "target=" in arg:
  114. regex = r"target=(.*?)(?:&|$)"
  115. elif "page=" in arg:
  116. regex = r"page=(?:User(?:(?:\s|_)talk)?(?::|%3A))?(.*?)(?:&|$)"
  117. elif re.search(r"Special(:|%3A)Contributions/", arg, re.I):
  118. regex = r"Special(?:\:|%3A)Contributions/(.*?)(?:\&|\?|$)"
  119. elif re.search(r"User((\s|_)talk)?(:|%3A)", arg, re.I):
  120. regex = r"User(?:(?:\s|_)talk)?(?:\:|%3A)(.*?)(?:\&|\?|$)"
  121. else:
  122. raise ValueError(arg)
  123. match = re.search(regex, arg, re.I)
  124. if not match:
  125. raise ValueError(arg)
  126. arg = match.group(1)
  127. return arg
  128. def _calculate_range(self, family, ips):
  129. """Calculate the smallest CIDR range encompassing a list of IPs."""
  130. bin_ips = [
  131. "".join(bin(ord(octet))[2:].zfill(8) for octet in ip.ip) for ip in ips
  132. ]
  133. for i, ip in enumerate(ips):
  134. if ip.size is not None:
  135. suffix = "X" * (len(bin_ips[i]) - ip.size)
  136. bin_ips[i] = bin_ips[i][: ip.size] + suffix
  137. size = len(bin_ips[0])
  138. for i in range(len(bin_ips[0])):
  139. if any(ip[i] == "X" for ip in bin_ips) or (
  140. any(ip[i] == "0" for ip in bin_ips)
  141. and any(ip[i] == "1" for ip in bin_ips)
  142. ):
  143. size = i
  144. break
  145. bin_low = bin_ips[0][:size].ljust(len(bin_ips[0]), "0")
  146. bin_high = bin_ips[0][:size].ljust(len(bin_ips[0]), "1")
  147. low = self._format_bin(family, bin_low)
  148. high = self._format_bin(family, bin_high)
  149. return _Range(
  150. family,
  151. low + "/" + str(size),
  152. low,
  153. high,
  154. size,
  155. self._format_count(2 ** (len(bin_ips[0]) - size)),
  156. )
  157. @staticmethod
  158. def _format_bin(family, binary):
  159. """Convert an IP's binary representation to presentation format."""
  160. return socket.inet_ntop(
  161. family,
  162. b"".join(
  163. chr(int(binary[i : i + 8], 2)).encode()
  164. for i in range(0, len(binary), 8)
  165. ),
  166. )
  167. @staticmethod
  168. def _format_count(count):
  169. """Nicely format a number of addresses affected by a range block."""
  170. if count == 1:
  171. return "1 address"
  172. if count > 2**32:
  173. base = f"{count:.2E} addresses"
  174. if count == 2**64:
  175. return base + " (1 /64 subnet)"
  176. if count > 2**96:
  177. return base + f" ({count >> 64:.2E} /64 subnets)"
  178. if count > 2**63:
  179. return base + f" ({count >> 64:,} /64 subnets)"
  180. return base
  181. return f"{count:,} addresses"
  182. def _describe(self, family, size):
  183. """Return an optional English description of a range."""
  184. if (family == AF_INET and size < self.LIMIT_IPv4) or (
  185. family == AF_INET6 and size < self.LIMIT_IPv6
  186. ):
  187. return "too large to block"