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.

150 lines
5.9 KiB

  1. # -*- coding: utf-8 -*-
  2. #
  3. # Copyright (C) 2009-2021 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. from time import sleep
  23. from earwigbot.irc import IRCConnection, Data
  24. __all__ = ["Frontend"]
  25. class Frontend(IRCConnection):
  26. """
  27. **EarwigBot: IRC Frontend Component**
  28. The IRC frontend runs on a normal IRC server and expects users to interact
  29. with it and give it commands. Commands are stored as "command classes",
  30. subclasses of :py:class:`~earwigbot.commands.Command`. All command classes
  31. are automatically imported by :py:meth:`commands.load()
  32. <earwigbot.managers._ResourceManager.load>` if they are in
  33. :py:mod:`earwigbot.commands` or the bot's custom command directory
  34. (explained in the :doc:`documentation </customizing>`).
  35. """
  36. NICK_SERVICES = "NickServ"
  37. def __init__(self, bot):
  38. self.bot = bot
  39. cf = bot.config.irc["frontend"]
  40. super().__init__(cf["host"], cf["port"], cf["nick"], cf["ident"],
  41. cf["realname"], bot.logger.getChild("frontend"))
  42. self._auth_wait = False
  43. self._channels = set()
  44. self._connect()
  45. def __repr__(self):
  46. """Return the canonical string representation of the Frontend."""
  47. res = "Frontend(host={0!r}, port={1!r}, nick={2!r}, ident={3!r}, realname={4!r}, bot={5!r})"
  48. return res.format(self.host, self.port, self.nick, self.ident,
  49. self.realname, self.bot)
  50. def __str__(self):
  51. """Return a nice string representation of the Frontend."""
  52. res = "<Frontend {0}!{1} at {2}:{3}>"
  53. return res.format(self.nick, self.ident, self.host, self.port)
  54. def _join_channels(self):
  55. """Join all startup channels."""
  56. permdb = self.bot.config.irc["permissions"]
  57. try:
  58. # Try channels previously joined:
  59. chans = permdb.get_attr("meta:frontend", "channels").split(",")
  60. except KeyError:
  61. # Channels specified in the config file:
  62. chans = self.bot.config.irc["frontend"]["channels"]
  63. for chan in chans:
  64. self.join(chan)
  65. def _save_channels(self):
  66. """Save the channel list persistently."""
  67. permdb = self.bot.config.irc["permissions"]
  68. permdb.set_attr("meta:frontend", "channels", ",".join(sorted(self._channels)))
  69. def _add_channel(self, chan):
  70. """Add a channel to the list of our channels."""
  71. self._channels.add(chan)
  72. self._save_channels()
  73. def _remove_channel(self, chan):
  74. """Remove a channel from the list of our channels."""
  75. self._channels.discard(chan)
  76. self._save_channels()
  77. def _process_message(self, line):
  78. """Process a single message from IRC."""
  79. if line[1] == "JOIN":
  80. data = Data(self.nick, line, msgtype="JOIN")
  81. if data.nick == self.nick:
  82. self._add_channel(data.chan)
  83. self.bot.commands.call("join", data)
  84. elif line[1] == "PART":
  85. data = Data(self.nick, line, msgtype="PART")
  86. if data.nick == self.nick:
  87. self._remove_channel(data.chan)
  88. self.bot.commands.call("part", data)
  89. elif line[1] == "PRIVMSG":
  90. data = Data(self.nick, line, msgtype="PRIVMSG")
  91. if data.is_private:
  92. self.bot.commands.call("msg_private", data)
  93. else:
  94. self.bot.commands.call("msg_public", data)
  95. self.bot.commands.call("msg", data)
  96. elif line[1] == "NOTICE":
  97. data = Data(self.nick, line, msgtype="NOTICE")
  98. if self._auth_wait and data.nick == self.NICK_SERVICES:
  99. if data.msg.startswith("This nickname is registered."):
  100. return
  101. self._auth_wait = False
  102. sleep(2) # Wait for hostname change to propagate
  103. self._join_channels()
  104. elif line[1] == "KICK":
  105. if line[3] == self.nick:
  106. self._remove_channel(line[2])
  107. elif line[1] == "376": # On successful connection to the server
  108. # If we're supposed to auth to NickServ, do that:
  109. try:
  110. username = self.bot.config.irc["frontend"]["nickservUsername"]
  111. password = self.bot.config.irc["frontend"]["nickservPassword"]
  112. except KeyError:
  113. self._join_channels()
  114. else:
  115. self.logger.debug("Identifying with services")
  116. msg = "IDENTIFY {0} {1}".format(username, password)
  117. self.say(self.NICK_SERVICES, msg, hidelog=True)
  118. self._auth_wait = True
  119. elif line[1] == "401": # No such nickname
  120. if self._auth_wait and line[3] == self.NICK_SERVICES:
  121. # Services is down, or something...?
  122. self._auth_wait = False
  123. self._join_channels()
  124. @property
  125. def channels(self):
  126. """A set containing all channels the bot is in."""
  127. return self._channels