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.

151 lines
6.0 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. base = super(Frontend, self)
  41. base.__init__(cf["host"], cf["port"], cf["nick"], cf["ident"],
  42. cf["realname"], bot.logger.getChild("frontend"))
  43. self._auth_wait = False
  44. self._channels = set()
  45. self._connect()
  46. def __repr__(self):
  47. """Return the canonical string representation of the Frontend."""
  48. res = "Frontend(host={0!r}, port={1!r}, nick={2!r}, ident={3!r}, realname={4!r}, bot={5!r})"
  49. return res.format(self.host, self.port, self.nick, self.ident,
  50. self.realname, self.bot)
  51. def __str__(self):
  52. """Return a nice string representation of the Frontend."""
  53. res = "<Frontend {0}!{1} at {2}:{3}>"
  54. return res.format(self.nick, self.ident, self.host, self.port)
  55. def _join_channels(self):
  56. """Join all startup channels."""
  57. permdb = self.bot.config.irc["permissions"]
  58. try:
  59. # Try channels previously joined:
  60. chans = permdb.get_attr("meta:frontend", "channels").split(",")
  61. except KeyError:
  62. # Channels specified in the config file:
  63. chans = self.bot.config.irc["frontend"]["channels"]
  64. for chan in chans:
  65. self.join(chan)
  66. def _save_channels(self):
  67. """Save the channel list persistently."""
  68. permdb = self.bot.config.irc["permissions"]
  69. permdb.set_attr("meta:frontend", "channels", ",".join(sorted(self._channels)))
  70. def _add_channel(self, chan):
  71. """Add a channel to the list of our channels."""
  72. self._channels.add(chan)
  73. self._save_channels()
  74. def _remove_channel(self, chan):
  75. """Remove a channel from the list of our channels."""
  76. self._channels.discard(chan)
  77. self._save_channels()
  78. def _process_message(self, line):
  79. """Process a single message from IRC."""
  80. if line[1] == "JOIN":
  81. data = Data(self.nick, line, msgtype="JOIN")
  82. if data.nick == self.nick:
  83. self._add_channel(data.chan)
  84. self.bot.commands.call("join", data)
  85. elif line[1] == "PART":
  86. data = Data(self.nick, line, msgtype="PART")
  87. if data.nick == self.nick:
  88. self._remove_channel(data.chan)
  89. self.bot.commands.call("part", data)
  90. elif line[1] == "PRIVMSG":
  91. data = Data(self.nick, line, msgtype="PRIVMSG")
  92. if data.is_private:
  93. self.bot.commands.call("msg_private", data)
  94. else:
  95. self.bot.commands.call("msg_public", data)
  96. self.bot.commands.call("msg", data)
  97. elif line[1] == "NOTICE":
  98. data = Data(self.nick, line, msgtype="NOTICE")
  99. if self._auth_wait and data.nick == self.NICK_SERVICES:
  100. if data.msg.startswith("This nickname is registered."):
  101. return
  102. self._auth_wait = False
  103. sleep(2) # Wait for hostname change to propagate
  104. self._join_channels()
  105. elif line[1] == "KICK":
  106. if line[3] == self.nick:
  107. self._remove_channel(line[2])
  108. elif line[1] == "376": # On successful connection to the server
  109. # If we're supposed to auth to NickServ, do that:
  110. try:
  111. username = self.bot.config.irc["frontend"]["nickservUsername"]
  112. password = self.bot.config.irc["frontend"]["nickservPassword"]
  113. except KeyError:
  114. self._join_channels()
  115. else:
  116. self.logger.debug("Identifying with services")
  117. msg = "IDENTIFY {0} {1}".format(username, password)
  118. self.say(self.NICK_SERVICES, msg, hidelog=True)
  119. self._auth_wait = True
  120. elif line[1] == "401": # No such nickname
  121. if self._auth_wait and line[3] == self.NICK_SERVICES:
  122. # Services is down, or something...?
  123. self._auth_wait = False
  124. self._join_channels()
  125. @property
  126. def channels(self):
  127. """A set containing all channels the bot is in."""
  128. return self._channels