text: """
  Communication commands:
  
  - channel
  - page
  - irc/rss/grapevine linking
  
  """
  
  from django.conf import settings
  from evennia.comms.models import Msg
  from evennia.accounts.models import AccountDB
  from evennia.accounts import bots
  from evennia.locks.lockhandler import LockException
  from evennia.comms.comms import DefaultChannel
  from evennia.utils import create, logger, utils
  from evennia.utils.logger import tail_log_file
  from evennia.utils.utils import class_from_module, strip_unsafe_input
  from evennia.utils.evmenu import ask_yes_no
  
  COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
  CHANNEL_DEFAULT_TYPECLASS = class_from_module(
      settings.BASE_CHANNEL_TYPECLASS, fallback=settings.FALLBACK_CHANNEL_TYPECLASS)
  
  
  # limit symbol import for API
  __all__ = (
      "CmdChannel",
      "CmdObjectChannel",
  
      "CmdPage",
  
      "CmdIRC2Chan",
      "CmdIRCStatus",
      "CmdRSS2Chan",
      "CmdGrapevine2Chan",
  )
  _DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
  
  # helper functions to make it easier to override the main CmdChannel
  # command and to keep the legacy addcom etc commands around.
  
  
  class CmdChannel(COMMAND_DEFAULT_CLASS):
      """
      Use and manage in-game channels.
  
      Usage:
        channel channelname <msg>
        channel channel name = <msg>
        channel        (show all subscription)
        channel/all    (show available channels)
        channel/alias channelname = alias[;alias...]
        channel/unalias alias
        channel/who channelname
        channel/history channelname [= index]
        channel/sub channelname [= alias[;alias...]]
        channel/unsub channelname[,channelname, ...]
        channel/mute channelname[,channelname,...]
        channel/unmute channelname[,channelname,...]
  
        channel/create channelname[;alias;alias[:typeclass]] [= description]
        channel/destroy channelname [= reason]
        channel/desc channelname = description
        channel/lock channelname = lockstring
        channel/unlock channelname = lockstring
        channel/ban channelname   (list bans)
        channel/ban[/quiet] channelname[, channelname, ...] = subscribername [: reason]
        channel/unban[/quiet] channelname[, channelname, ...] = subscribername
        channel/boot[/quiet] channelname[,channelname,...] = subscribername [: reason]
  
      # subtopics
  
      ## sending
  
      Usage: channel channelname msg
             channel channel name = msg  (with space in channel name)
  
      This sends a message to the channel. Note that you will rarely use this
      command like this; instead you can use the alias
  
          channelname <msg>
          channelalias <msg>
  
      For example
  
          public Hello World
          pub Hello World
  
      (this shortcut doesn't work for aliases containing spaces)
  
      See channel/alias for help on setting channel aliases.
  
      ## alias and unalias
  
      Usage: channel/alias channel = alias[;alias[;alias...]]
             channel/unalias alias
             channel    - this will list your subs and aliases to each channel
  
      Set one or more personal aliases for referencing a channel. For example:
  
          channel/alias warrior's guild = warrior;wguild;warchannel;warrior guild
  
      You can now send to the channel using all of these:
  
          warrior's guild Hello
          warrior Hello
          wguild Hello
          warchannel Hello
  
      Note that this will not work if the alias has a space in it. So the
      'warrior guild' alias must be used with the `channel` command:
  
          channel warrior guild = Hello
  
      Channel-aliases can be removed one at a time, using the '/unalias' switch.
  
      ## who
  
      Usage: channel/who channelname
  
      List the channel's subscribers. Shows who are currently offline or are
      muting the channel. Subscribers who are 'muting' will not see messages sent
      to the channel (use channel/mute to mute a channel).
  
      ## history
  
      Usage: channel/history channel [= index]
  
      This will display the last |c20|n lines of channel history. By supplying an
      index number, you will step that many lines back before viewing those 20 lines.
  
      For example:
  
          channel/history public = 35
  
      will go back 35 lines and show the previous 20 lines from that point (so
      lines -35 to -55).
  
      ## sub and unsub
  
      Usage: channel/sub channel [=alias[;alias;...]]
             channel/unsub channel
  
      This subscribes you to a channel and optionally assigns personal shortcuts
      for you to use to send to that channel (see aliases). When you unsub, all
      your personal aliases will also be removed.
  
      ## mute and unmute
  
      Usage: channel/mute channelname
             channel/unmute channelname
  
      Muting silences all output from the channel without actually
      un-subscribing. Other channel members will see that you are muted in the /who
      list. Sending a message to the channel will automatically unmute you.
  
      ## create and destroy
  
      Usage: channel/create channelname[;alias;alias[:typeclass]] [= description]
             channel/destroy channelname [= reason]
  
      Creates a new channel (or destroys one you control). You will automatically
      join the channel you create and everyone will be kicked and loose all aliases
      to a destroyed channel.
  
      ## lock and unlock
  
      Usage: channel/lock channelname = lockstring
             channel/unlock channelname = lockstring
  
      Note: this is an admin command.
  
      A lockstring is on the form locktype:lockfunc(). Channels understand three
      locktypes:
          listen - who may listen or join the channel.
          send - who may send messages to the channel
          control - who controls the channel. This is usually the one creating
              the channel.
  
      Common lockfuncs are all() and perm(). To make a channel everyone can
      listen to but only builders can talk on, use this:
  
          listen:all()
          send: perm(Builders)
  
      ## boot and ban
  
      Usage:
             channel/boot[/quiet] channelname[,channelname,...] = subscribername [: reason]
             channel/ban channelname[, channelname, ...] = subscribername [: reason]
             channel/unban channelname[, channelname, ...] = subscribername
             channel/unban channelname
             channel/ban channelname    (list bans)
  
      Booting will kick a named subscriber from channel(s) temporarily. The
      'reason' will be passed to the booted user. Unless the /quiet switch is
      used, the channel will also be informed of the action. A booted user is
      still able to re-connect, but they'll have to set up their aliases again.
  
      Banning will blacklist a user from (re)joining the provided channels. It
      will then proceed to boot them from those channels if they were connected.
      The 'reason' and `/quiet` works the same as for booting.
  
      Example:
          boot mychannel1 = EvilUser : Kicking you to cool down a bit.
          ban mychannel1,mychannel2= EvilUser : Was banned for spamming.
  
      """
      key = "@channel"
      aliases = ["@chan", "@channels"]
      help_category = "Comms"
      # these cmd: lock controls access to the channel command itself
      # the admin: lock controls access to /boot/ban/unban switches
      # the manage: lock  controls access to /create/destroy/desc/lock/unlock switches
      locks = "cmd:not pperm(channel_banned);admin:all();manage:all();changelocks:perm(Admin)"
      switch_options = (
          "list", "all", "history", "sub", "unsub", "mute", "unmute", "alias", "unalias",
          "create", "destroy", "desc", "lock", "unlock", "boot", "ban", "unban", "who",)
      # disable this in child command classes if wanting on-character channels
      account_caller = True
  
      def search_channel(self, channelname, exact=False, handle_errors=True):
          """
          Helper function for searching for a single channel with some error
          handling.
  
          Args:
              channelname (str): Name, alias #dbref or partial name/alias to search
                  for.
              exact (bool, optional): If an exact or fuzzy-match of the name should be done.
                  Note that even for a fuzzy match, an exactly given, unique channel name
                  will always be returned.
              handle_errors (bool): If true, use `self.msg` to report errors if
                  there are non/multiple matches. If so, the return will always be
                  a single match or None.
          Returns:
              object, list or None: If `handle_errors` is `True`, this is either a found Channel
                  or `None`. Otherwise it's a list  of zero, one or more channels found.
          Notes:
              The 'listen' and 'control' accesses are checked before returning.
  
          """
          caller = self.caller
          # first see if this is a personal alias
          channelname = caller.nicks.get(key=channelname, category="channel") or channelname
  
          # always try the exact match first.
          channels = CHANNEL_DEFAULT_TYPECLASS.objects.channel_search(channelname, exact=True)
  
          if not channels and not exact:
              # try fuzzy matching as well
              channels = CHANNEL_DEFAULT_TYPECLASS.objects.channel_search(channelname, exact=exact)
  
          # check permissions
          channels = [channel for channel in channels
                      if channel.access(caller, 'listen') or channel.access(caller, 'control')]
  
          if handle_errors:
              if not channels:
                  self.msg(f"No channel found matching '{channelname}' "
                           "(could also be due to missing access).")
                  return None
              elif len(channels) > 1:
                  self.msg("Multiple possible channel matches/alias for "
                           "'{channelname}':\n" + ", ".join(chan.key for chan in channels))
                  return None
              return channels[0]
          else:
              if not channels:
                  return []
              elif len(channels) > 1:
                  return list(channels)
              return [channels[0]]
  
      def msg_channel(self, channel, message, **kwargs):
          """
          Send a message to a given channel. This will check the 'send'
          permission on the channel.
  
          Args:
              channel (Channel): The channel to send to.
              message (str): The message to send.
              **kwargs: Unused by default. These kwargs will be passed into
                  all channel messaging hooks for custom overriding.
  
          """
          if not channel.access(self.caller, "send"):
              caller.msg(f"You are not allowed to send messages to channel {channel}")
              return
  
          # avoid unsafe tokens in message
          message = strip_unsafe_input(message, self.session)
  
          channel.msg(message, senders=self.caller, **kwargs)
  
      def get_channel_history(self, channel, start_index=0):
          """
          View a channel's history.
  
          Args:
              channel (Channel): The channel to access.
              message (str): The message to send.
              **kwargs: Unused by default. These kwargs will be passed into
                  all channel messaging hooks for custom overriding.
  
          """
          caller = self.caller
          log_file = channel.get_log_filename()
  
          def send_msg(lines):
              return self.msg(
                  "".join(line.split("[-]", 1)[1] if "[-]" in line else line for line in lines)
              )
          # asynchronously tail the log file
          tail_log_file(log_file, start_index, 20, callback=send_msg)
  
      def sub_to_channel(self, channel):
          """
          Subscribe to a channel. Note that all permissions should
          be checked before this step.
  
          Args:
              channel (Channel): The channel to access.
  
          Returns:
              bool, str: True, None if connection failed. If False,
                  the second part is an error string.
  
          """
          caller = self.caller
  
          if channel.has_connection(caller):
              return False, f"Already listening to channel {channel.key}."
  
          # this sets up aliases in post_join_channel by default
          result = channel.connect(caller)
  
          return result, "" if result else f"Were not allowed to subscribe to channel {channel.key}"
  
      def unsub_from_channel(self, channel, **kwargs):
          """
          Un-Subscribe to a channel. Note that all permissions should
          be checked before this step.
  
          Args:
              channel (Channel): The channel to unsub from.
              **kwargs: Passed on to nick removal.
  
          Returns:
              bool, str: True, None if un-connection succeeded. If False,
                  the second part is an error string.
  
          """
          caller = self.caller
  
          if not channel.has_connection(caller):
              return False, f"Not listening to channel {channel.key}."
  
          # this will also clean aliases
          result = channel.disconnect(caller)
  
          return result, "" if result else f"Could not unsubscribe from channel {channel.key}"
  
      def add_alias(self, channel, alias, **kwargs):
          """
          Add a new alias (nick) for the user to use with this channel.
  
          Args:
              channel (Channel): The channel to alias.
              alias (str): The personal alias to use for this channel.
              **kwargs: If given, passed into nicks.add.
  
          Note:
              We add two nicks - one is a plain `alias -> channel.key` that
              we need to be able to reference this channel easily. The other
              is a templated nick to easily be able to send messages to the
              channel without needing to give the full `channel` command. The
              structure of this nick is given by `self.channel_msg_pattern`
              and `self.channel_msg_nick_replacement`. By default it maps
              `alias <msg> -> channel <channelname> = <msg>`, so that you can
              for example just write `pub Hello` to send a message.
  
              The alias created is `alias $1 -> channel channel = $1`, to allow
              for sending to channel using the main channel command.
  
          """
          channel.add_user_channel_alias(self.caller, alias, **kwargs)
  
      def remove_alias(self, alias, **kwargs):
          """
          Remove an alias from a channel.
  
          Args:
              alias (str, optional): The alias to remove.
                  The channel will be reverse-determined from the
                  alias, if it exists.
  
          Returns:
              bool, str: True, None if removal succeeded. If False,
                  the second part is an error string.
              **kwargs: If given, passed into nicks.get/add.
  
          Note:
              This will remove two nicks - the plain channel alias and the templated
              nick used for easily sending messages to the channel.
  
          """
          if self.caller.nicks.has(alias, category="channel", **kwargs):
              DefaultChannel.remove_user_channel_alias(self.caller, alias)
              return True, ""
          return False, "No such alias was defined."
  
      def get_channel_aliases(self, channel):
          """
          Get a user's aliases for a given channel. The user is retrieved
          through self.caller.
  
          Args:
              channel (Channel): The channel to act on.
  
          Returns:
              list: A list of zero, one or more alias-strings.
  
          """
          chan_key = channel.key.lower()
          nicktuples = self.caller.nicks.get(category="channel", return_tuple=True, return_list=True)
          if nicktuples:
              return [tup[2] for tup in nicktuples if tup[3].lower() == chan_key]
          return []
  
      def mute_channel(self, channel):
          """
          Temporarily mute a channel.
  
          Args:
              channel (Channel): The channel to alias.
  
          Returns:
              bool, str: True, None if muting successful. If False,
                  the second part is an error string.
          """
          if channel.mute(self.caller):
              return True, ""
          return False, f"Channel {channel.key} was already muted."
  
      def unmute_channel(self, channel):
          """
          Unmute a channel.
  
          Args:
              channel (Channel): The channel to alias.
  
          Returns:
              bool, str: True, None if unmuting successful. If False,
                  the second part is an error string.
  
          """
          if channel.unmute(self.caller):
              return True, ""
          return False, f"Channel {channel.key} was already unmuted."
  
      def create_channel(self, name, description, typeclass=None, aliases=None):
          """
          Create a new channel. Its name must not previously exist
          (users can alias as needed). Will also connect to the
          new channel.
  
          Args:
              name (str): The new channel name/key.
              description (str): This is used in listings.
              aliases (list): A list of strings - alternative aliases for the channel
                  (not to be confused with per-user aliases; these are available for
                  everyone).
  
          Returns:
              channel, str: new_channel, "" if creation successful. If False,
                  the second part is an error string.
  
          """
          caller = self.caller
          if typeclass:
              typeclass = class_from_module(typeclass)
          else:
              typeclass = CHANNEL_DEFAULT_TYPECLASS
  
          if typeclass.objects.channel_search(name, exact=True):
              return False, f"Channel {name} already exists."
  
          # set up the new channel
          lockstring = "send:all();listen:all();control:id(%s)" % caller.id
  
          new_chan = create.create_channel(
              name, aliases=aliases, desc=description, locks=lockstring, typeclass=typeclass)
          self.sub_to_channel(new_chan)
          return new_chan, ""
  
      def destroy_channel(self, channel, message=None):
          """
          Destroy an existing channel. Access should be checked before
          calling this function.
  
          Args:
              channel (Channel): The channel to alias.
              message (str, optional): Final message to send onto the channel
                  before destroying it. If not given, a default message is
                  used. Set to the empty string for no message.
  
          if typeclass:
             pass
  
          """
          caller = self.caller
  
          channel_key = channel.key
          if message is None:
              message = (f"|rChannel {channel_key} is being destroyed. "
                         "Make sure to clean any channel aliases.|n")
          if message:
              channel.msg(message, senders=caller, bypass_mute=True)
          channel.delete()
          logger.log_sec(
              "Channel {} was deleted by {}".format(channel_key, caller)
          )
  
      def set_lock(self, channel, lockstring):
          """
          Set a lockstring on a channel. Permissions must have been
          checked before this call.
  
          Args:
              channel (Channel): The channel to operate on.
              lockstring (str): A lockstring on the form 'type:lockfunc();...'
  
          Returns:
              bool, str: True, None if setting lock was successful. If False,
                  the second part is an error string.
  
          """
          try:
              channel.locks.add(lockstring)
          except LockException as err:
              return False, err
          return True, ""
  
      def unset_lock(self, channel, lockstring):
          """
          Remove locks in a lockstring on a channel. Permissions must have been
          checked before this call.
  
          Args:
              channel (Channel): The channel to operate on.
              lockstring (str): A lockstring on the form 'type:lockfunc();...'
  
          Returns:
              bool, str: True, None if setting lock was successful. If False,
                  the second part is an error string.
  
          """
          try:
              channel.locks.remove(lockstring)
          except LockException as err:
              return False, err
          return True, ""
  
      def set_desc(self, channel, description):
          """
          Set a channel description. This is shown in listings etc.
  
          Args:
              caller (Object or Account): The entity performing the action.
              channel (Channel): The channel to operate on.
              description (str): A short description of the channel.
  
          Returns:
              bool, str: True, None if setting lock was successful. If False,
                  the second part is an error string.
  
          """
          channel.db.desc = description
  
      def boot_user(self, channel, target, quiet=False, reason=""):
          """
          Boot a user from a channel, with optional reason. This will
          also remove all their aliases for this channel.
  
          Args:
              channel (Channel): The channel to operate on.
              target (Object or Account): The entity to boot.
              quiet (bool, optional): Whether or not to announce to channel.
              reason (str, optional): A reason for the boot.
  
          Returns:
              bool, str: True, None if setting lock was successful. If False,
                  the second part is an error string.
  
          """
          if not channel.subscriptions.has(target):
              return False, f"{target} is not connected to channel {channel.key}."
          # find all of target's nicks linked to this channel and delete them
          for nick in [
              nick
              for nick in target.nicks.get(category="channel") or []
              if nick.value[3].lower() == channel.key
          ]:
              nick.delete()
          channel.disconnect(target)
          reason = f" Reason: {reason}" if reason else ""
          target.msg(f"You were booted from channel {channel.key} by {self.caller.key}.{reason}")
          if not quiet:
              channel.msg(f"{target.key} was booted from channel by {self.caller.key}.{reason}")
  
          logger.log_sec(f"Channel Boot: {target} (Channel: {channel}, "
                         f"Reason: {reason.strip()}, Caller: {self.caller}")
          return True, ""
  
      def ban_user(self, channel, target, quiet=False, reason=""):
          """
          Ban a user from a channel, by locking them out. This will also
          boot them, if they are currently connected.
  
          Args:
              channel (Channel): The channel to operate on.
              target (Object or Account): The entity to ban
              quiet (bool, optional): Whether or not to announce to channel.
              reason (str, optional): A reason for the ban
  
          Returns:
              bool, str: True, None if banning was successful. If False,
                  the second part is an error string.
  
          """
          self.boot_user(channel, target, quiet=quiet, reason=reason)
          if channel.ban(target):
              return True, ""
          return False, f"{target} is already banned from this channel."
  
      def unban_user(self, channel, target):
          """
          Un-Ban a user from a channel. This will not reconnect them
          to the channel, just allow them to connect again (assuming
          they have the suitable 'listen' lock like everyone else).
  
          Args:
              channel (Channel): The channel to operate on.
              target (Object or Account): The entity to unban
  
          Returns:
              bool, str: True, None if unbanning was successful. If False,
                  the second part is an error string.
  
          """
          if channel.unban(target):
              return True, ""
          return False, f"{target} was not previously banned from this channel."
  
      def channel_list_bans(self, channel):
          """
          Show a channel's bans.
  
          Args:
              channel (Channel): The channel to operate on.
  
          Returns:
              list: A list of strings, each the name of a banned user.
  
          """
          return [banned.key for banned in channel.banlist]
  
      def channel_list_who(self, channel):
          """
          Show a list of online people is subscribing to a channel. This will check
          the 'control' permission of `caller` to determine if only online users
          should be returned or everyone.
  
          Args:
              channel (Channel): The channel to operate on.
  
          Returns:
              list: A list of prepared strings, with name + markers for if they are
                  muted or offline.
  
          """
          caller = self.caller
          mute_list = list(channel.mutelist)
          online_list = channel.subscriptions.online()
          if channel.access(caller, 'control'):
              # for those with channel control, show also offline users
              all_subs = list(channel.subscriptions.all())
          else:
              # for others, only show online users
              all_subs = online_list
  
          who_list = []
          for subscriber in all_subs:
              name = subscriber.get_display_name(caller)
              conditions = ("muting" if subscriber in mute_list else "",
                            "offline" if subscriber not in online_list else "")
              conditions = [cond for cond in conditions if cond]
              cond_text = "(" + ", ".join(conditions) + ")" if conditions else ""
              who_list.append(f"{name}{cond_text}")
  
          return who_list
  
      def list_channels(self, channelcls=CHANNEL_DEFAULT_TYPECLASS):
          """
          Return a available channels.
  
          Args:
              channelcls (Channel, optional): The channel-class to query on. Defaults
                  to the default channel class from settings.
  
          Returns:
              tuple: A tuple `(subbed_chans, available_chans)` with the channels
                  currently subscribed to, and those we have 'listen' access to but
                  don't actually sub to yet.
  
          """
          caller = self.caller
          subscribed_channels = list(channelcls.objects.get_subscriptions(caller))
          unsubscribed_available_channels = [
              chan
              for chan in channelcls.objects.get_all_channels()
              if chan not in subscribed_channels and chan.access(caller, "listen")
          ]
          return subscribed_channels, unsubscribed_available_channels
  
      def display_subbed_channels(self, subscribed):
          """
          Display channels subscribed to.
  
          Args:
              subscribed (list): List of subscribed channels
  
          Returns:
              EvTable: Table to display.
  
          """
          comtable = self.styled_table(
              "id",
              "channel",
              "my aliases",
              "locks",
              "description",
              align="l",
              maxwidth=_DEFAULT_WIDTH
          )
          for chan in subscribed:
  
              locks = "-"
              chanid = "-"
              if chan.access(self.caller, "control"):
                  locks = chan.locks
                  chanid = chan.id
  
              my_aliases = ", ".join(self.get_channel_aliases(chan))
              comtable.add_row(
                  *(
                    chanid,
                    "{key}{aliases}".format(
                        key=chan.key,
                        aliases=";"+ ";".join(chan.aliases.all()) if chan.aliases.all() else ""
                    ),
                    my_aliases,
                    locks,
                    chan.db.desc
                  )
              )
          return comtable
  
      def display_all_channels(self, subscribed, available):
          """
          Display all available channels
  
          Args:
              subscribed (list): List of subscribed channels
          Returns:
              EvTable: Table to display.
  
          """
          caller = self.caller
  
          comtable = self.styled_table(
              "sub",
              "channel",
              "aliases",
              "my aliases",
              "description",
              maxwidth=_DEFAULT_WIDTH,
          )
          channels = subscribed + available
  
          for chan in channels:
              if chan not in subscribed:
                  substatus = "|rNo|n"
              elif caller in chan.mutelist:
                  substatus = "|rMuting|n"
              else:
                  substatus = "|gYes|n"
              my_aliases = ", ".join(self.get_channel_aliases(chan))
              comtable.add_row(
                  *(substatus,
                    chan.key,
                    ",".join(chan.aliases.all()) if chan.aliases.all() else "",
                    my_aliases,
                    chan.db.desc))
          comtable.reformat_column(0, width=8)
  
          return comtable
  
      def func(self):
          """
          Main functionality of command.
          """
          # from evennia import set_trace;set_trace()
  
          caller = self.caller
          switches = self.switches
          channel_names = [name for name in self.lhslist if name]
  
          #from evennia import set_trace;set_trace()
  
          if 'all' in switches:
              # show all available channels
              subscribed, available = self.list_channels()
              table = self.display_all_channels(subscribed, available)
  
              self.msg(
                  "\n|wAvailable channels|n (use no argument to "
                  f"only show your subscriptions)\n{table}")
              return
  
          if not channel_names:
              # empty arg show only subscribed channels
              subscribed, _ = self.list_channels()
              table = self.display_subbed_channels(subscribed)
  
              self.msg("\n|wChannel subscriptions|n "
                       f"(use |w/all|n to see all available):\n{table}")
              return
  
          if not self.switches and not self.args:
              self.msg("Usage[/switches]: channel [= message]")
              return
  
          if 'create' in switches:
              # create a new channel
  
              if not self.access(caller, "manage"):
                  self.msg("You don't have access to use channel/create.")
                  return
  
              config = self.lhs
              if not config:
                  self.msg("To create: channel/create name[;aliases][:typeclass] [= description]")
                  return
              name, *typeclass = config.rsplit(":", 1)
              typeclass = typeclass[0] if typeclass else None
              name, *aliases = name.rsplit(";")
              description = self.rhs or ""
              chan, err = self.create_channel(name, description, typeclass=typeclass, aliases=aliases)
              if chan:
                  self.msg(f"Created (and joined) new channel '{chan.key}'.")
              else:
                  self.msg(err)
              return
  
          if 'unalias' in switches:
              # remove a personal alias (no channel needed)
              alias = self.args.strip()
              if not alias:
                  self.msg("Specify the alias to remove as channel/unalias <alias>")
                  return
              success, err = self.remove_alias(alias)
              if success:
                  self.msg(f"Removed your channel alias '{alias}'.")
              else:
                  self.msg(err)
              return
  
          possible_lhs_message = ""
          if not self.rhs and self.args and " " in self.args:
              # since we want to support messaging with `channel name text` (for
              # channels without a space in their name), we need to check if the
              # first 'channel name' is in fact 'channelname text'
              no_rhs_channel_name = self.args.split(" ", 1)[0]
              possible_lhs_message = self.args[len(no_rhs_channel_name):]
              if possible_lhs_message.strip() == '=':
                  possible_lhs_message = ""
              channel_names.append(no_rhs_channel_name)
  
  
          channels = []
          errors = []
          for channel_name in channel_names:
              # find a channel by fuzzy-matching. This also checks
              # 'listen/control' perms.
              found_channels = self.search_channel(channel_name, exact=False, handle_errors=False)
              if not found_channels:
                  errors.append(f"No channel found matching '{channel_name}' "
                                "(could also be due to missing access).")
              elif len(found_channels) > 1:
                  errors.append("Multiple possible channel matches/alias for "
                                "'{channel_name}':\n" + ", ".join(chan.key for chan in found_channels))
              else:
                  channels.append(found_channels[0])
  
          if not channels:
              self.msg('\n'.join(errors))
              return
  
          # we have at least one channel at this point
          channel = channels[0]
  
          if not switches:
              if self.rhs:
                  # send message to channel
                  self.msg_channel(channel, self.rhs.strip())
              elif channel and possible_lhs_message:
                  # called on the form channelname message without =
                  self.msg_channel(channel, possible_lhs_message.strip())
              else:
                  # inspect a given channel
                  subscribed, available = self.list_channels()
                  if channel in subscribed:
                      table = self.display_subbed_channels([channel])
                      header = f"Channel |w{channel.key}|n"
                      self.msg(f"{header}\n(use |w{channel.key} <msg>|n (or a channel-alias) "
                               f"to chat and the 'channel' command "
                               f"to customize)\n{table}")
                  elif channel in available:
                      table = self.display_all_channels([], [channel])
                      self.msg(
                          "\n|wNot subscribed to this channel|n (use /list to "
                          f"show all subscriptions)\n{table}")
              return
  
          if 'history' in switches or 'hist' in switches:
              # view channel history
  
              index = self.rhs or 0
              try:
                  index = max(0, int(index))
              except ValueError:
                  self.msg("The history index (describing how many lines to go back) "
                           "must be an integer >= 0.")
                  return
              self.get_channel_history(channel, start_index=index)
              return
  
          if 'sub' in switches:
              # subscribe to a channel
              aliases = []
              if self.rhs:
                  aliases = set(alias.strip().lower() for alias in self.rhs.split(";"))
              success, err = self.sub_to_channel(channel)
              if success:
                  for alias in aliases:
                      self.add_alias(channel, alias)
                  alias_txt = ', '.join(aliases)
                  alias_txt = f" using alias(es) {alias_txt}" if aliases else ''
                  self.msg("You are now subscribed "
                           f"to the channel {channel.key}{alias_txt}. Use /alias to "
                           "add additional aliases for referring to the channel.")
              else:
                  self.msg(err)
              return
  
          if 'unsub' in switches:
              # un-subscribe from a channel
              success, err = self.unsub_from_channel(channel)
              if success:
                  self.msg(f"You un-subscribed from channel {channel.key}. "
                           "All aliases were cleared.")
              else:
                  self.msg(err)
              return
  
          if 'alias' in switches:
              # create a new personal alias for a channel
              alias = self.rhs
              if not alias:
                  self.msg("Specify the alias as channel/alias channelname = alias")
                  return
              self.add_alias(channel, alias)
              self.msg(f"Added/updated your alias '{alias}' for channel {channel.key}.")
              return
  
          if 'mute' in switches:
              # mute a given channel
              success, err = self.mute_channel(channel)
              if success:
                  self.msg(f"Muted channel {channel.key}.")
              else:
                  self.msg(err)
              return
  
          if 'unmute' in switches:
              # unmute a given channel
              success, err = self.unmute_channel(channel)
              if success:
                  self.msg(f"Un-muted channel {channel.key}.")
              else:
                  self.msg(err)
              return
  
          if 'destroy' in switches or 'delete' in switches:
              # destroy a channel we control
  
              if not self.access(caller, "manage"):
                  self.msg("You don't have access to use channel/destroy.")
                  return
  
              if not channel.access(caller, "control"):
                  self.msg("You can only delete channels you control.")
                  return
  
              reason = self.rhs or None
  
              def _perform_delete(caller, *args, **kwargs):
                  self.destroy_channel(channel, message=reason)
                  self.msg(f"Channel {channel.key} was successfully deleted.")
  
              ask_yes_no(
                  caller,
                  prompt=f"Are you sure you want to delete channel '{channel.key}' "
                  "(make sure name is correct!)?\nThis will disconnect and "
                  "remove all users' aliases. {options}?",
                  yes_action=_perform_delete,
                  no_action="Aborted.",
                  default="N"
              )
  
          if 'desc' in switches:
              # set channel description
  
              if not self.access(caller, "manage"):
                  self.msg("You don't have access to use channel/desc.")
                  return
  
              if not channel.access(caller, "control"):
                  self.msg("You can only change description of channels you control.")
                  return
  
              desc = self.rhs.strip()
  
              if not desc:
                  self.msg("Usage: /desc channel = description")
                  return
  
              self.set_desc(channel, desc)
              self.msg("Updated channel description.")
  
          if 'lock' in switches:
              # add a lockstring to channel
  
              if not self.access(caller, "changelocks"):
                  self.msg("You don't have access to use channel/lock.")
                  return
  
              if not channel.access(caller, "control"):
                  self.msg("You need 'control'-access to change locks on this channel.")
                  return
  
              lockstring = self.rhs.strip()
  
              if not lockstring:
                  self.msg("Usage: channel/lock channelname = lockstring")
                  return
  
              success, err = self.set_lock(channel, self.rhs)
              if success:
                  self.msg("Added/updated lock on channel.")
              else:
                  self.msg(f"Could not add/update lock: {err}")
              return
  
          if 'unlock' in switches:
              # remove/update lockstring from channel
  
              if not self.access(caller, "changelocks"):
                  self.msg("You don't have access to use channel/unlock.")
                  return
  
              if not channel.access(caller, "control"):
                  self.msg("You need 'control'-access to change locks on this channel.")
                  return
  
              lockstring = self.rhs.strip()
  
              if not lockstring:
                  self.msg("Usage: channel/unlock channelname = lockstring")
                  return
  
              success, err = self.unset_lock(channel, self.rhs)
              if success:
                  self.msg("Removed lock from channel.")
              else:
                  self.msg(f"Could not remove lock: {err}")
              return
  
          if 'boot' in switches:
              # boot a user from channel(s)
  
              if not self.access(caller, "admin"):
                  self.msg("You don't have access to use channel/boot.")
                  return
  
              if not self.rhs:
                  self.msg("Usage: channel/boot channel[,channel,...] = username [:reason]")
                  return
  
              target_str, *reason = self.rhs.rsplit(":", 1)
              reason = reason[0].strip() if reason else ""
  
              for chan in channels:
  
                  if not chan.access(caller, "control"):
                      self.msg(f"You need 'control'-access to boot a user from {chan.key}.")
                      return
  
                  # the target must be a member of all given channels
                  target = caller.search(target_str, candidates=chan.subscriptions.all())
                  if not target:
                      self.msg(f"Cannot boot '{target_str}' - not in channel {chan.key}.")
                      return
  
              def _boot_user(caller, *args, **kwargs):
                  for chan in channels:
                      success, err = self.boot_user(chan, target, quiet=False, reason=reason)
                      if success:
                          self.msg(f"Booted {target.key} from channel {chan.key}.")
                      else:
                          self.msg(f"Cannot boot {target.key} from channel {chan.key}: {err}")
  
              channames = ", ".join(chan.key for chan in channels)
              reasonwarn = (". Also note that your reason will be echoed to the channel"
                            if reason else '')
              ask_yes_no(
                  caller,
                  prompt=f"Are you sure you want to boot user {target.key} from "
                  f"channel(s) {channames} (make sure name/channels are correct{reasonwarn}). "
                  "{options}?",
                  yes_action=_boot_user,
                  no_action="Aborted.",
                  default="Y"
              )
              return
  
          if 'ban' in switches:
              # ban a user from channel(s)
  
              if not self.access(caller, "admin"):
                  self.msg("You don't have access to use channel/ban.")
                  return
  
              if not self.rhs:
                  # view bans for channels
  
                  if not channel.access(caller, "control"):
                      self.msg(f"You need 'control'-access to view bans on channel {channel.key}")
                      return
  
                  bans = ["Channel bans "
                          "(to ban, use channel/ban channel[,channel,...] = username [:reason]"]
                  bans.extend(self.channel_list_bans(channel))
                  self.msg("\n".join(bans))
                  return
  
              target_str, *reason = self.rhs.rsplit(":", 1)
              reason = reason[0].strip() if reason else ""
  
              for chan in channels:
                  # the target must be a member of all given channels
                  if not chan.access(caller, "control"):
                      self.msg(f"You don't have access to ban users on channel {chan.key}")
                      return
  
                  target = caller.search(target_str, candidates=chan.subscriptions.all())
  
                  if not target:
                      self.msg(f"Cannot ban '{target_str}' - not in channel {chan.key}.")
                      return
  
              def _ban_user(caller, *args, **kwargs):
                  for chan in channels:
                      success, err = self.ban_user(chan, target, quiet=False, reason=reason)
                      if success:
                          self.msg(f"Banned {target.key} from channel {chan.key}.")
                      else:
                          self.msg(f"Cannot boot {target.key} from channel {chan.key}: {err}")
  
              channames = ", ".join(chan.key for chan in channels)
              reasonwarn = (". Also note that your reason will be echoed to the channel"
                            if reason else '')
              ask_yes_no(
                  caller,
                  f"Are you sure you want to ban user {target.key} from "
                  f"channel(s) {channames} (make sure name/channels are correct{reasonwarn}) "
                  "{options}?",
                  _ban_user,
                  "Aborted.",
              )
              return
  
          if 'unban' in switches:
              # unban a previously banned user from channel
  
              if not self.access(caller, "admin"):
                  self.msg("You don't have access to use channel/unban.")
                  return
  
              target_str = self.rhs.strip()
  
              if not target_str:
                  self.msg("Usage: channel[,channel,...] = user")
                  return
  
              banlists = []
              for chan in channels:
                  # the target must be a member of all given channels
                  if not chan.access(caller, "control"):
                      self.msg(f"You don't have access to unban users on channel {chan.key}")
                      return
                  banlists.extend(chan.banlist)
  
              target = caller.search(target_str, candidates=banlists)
              if not target:
                  self.msg("Could not find a banned user '{target_str}' in given channel(s).")
                  return
  
              for chan in channels:
                  success, err = self.unban_user(channel, target)
                  if success:
                      self.msg(f"Un-banned {target_str} from channel {chan.key}")
                  else:
                      self.msg(err)
              return
  
          if "who" in switches:
              # view who's a member of a channel
  
              who_list = [f"Subscribed to {channel.key}:"]
              who_list.extend(self.channel_list_who(channel))
              self.msg("\n".join(who_list))
              return
  
  
  # a channel-command parent for use with Characters/Objects.
  class CmdObjectChannel(CmdChannel):
      account_caller = False
  
  
  class CmdPage(COMMAND_DEFAULT_CLASS):
      """
      send a private message to another account
  
      Usage:
        page <account> <message>
        page[/switches] [<account>,<account>,... = <message>]
        tell        ''
        page <number>
  
      Switches:
        last - shows who you last messaged
        list - show your last <number> of tells/pages (default)
  
      Send a message to target user (if online). If no argument is given, you
      will get a list of your latest messages. The equal sign is needed for
      multiple targets or if sending to target with space in the name.
  
      """
  
      key = "page"
      aliases = ["tell"]
      switch_options = ("last", "list")
      locks = "cmd:not pperm(page_banned)"
      help_category = "Comms"
  
      # this is used by the COMMAND_DEFAULT_CLASS parent
      account_caller = True
  
      def func(self):
          """Implement function using the Msg methods"""
  
          # Since account_caller is set above, this will be an Account.
          caller = self.caller
  
          # get the messages we've sent (not to channels)
          pages_we_sent = Msg.objects.get_messages_by_sender(caller)
          # get last messages we've got
          pages_we_got = Msg.objects.get_messages_by_receiver(caller)
          targets, message, number = [], None, None
  
          if "last" in self.switches:
              if pages_we_sent:
                  recv = ",".join(obj.key for obj in pages_we_sent[-1].receivers)
                  self.msg("You last paged |c%s|n:%s" % (recv, pages_we_sent[-1].message))
                  return
              else:
                  self.msg("You haven't paged anyone yet.")
                  return
  
          if self.args:
              if self.rhs:
                  for target in self.lhslist:
                      target_obj = self.caller.search(target)
                      if not target_obj:
                          return
                      targets.append(target_obj)
                  message = self.rhs.strip()
              else:
                  target, *message = self.args.split(" ", 4)
                  if target and target.isnumeric():
                      # a number to specify a historic page
                      number = int(target)
                  elif target:
                      target_obj = self.caller.search(target, quiet=True)
                      if target_obj:
                          # a proper target
                          targets = [target_obj[0]]
                          message = message[0].strip()
                      else:
                          # a message with a space in it - put it back together
                          message = target + " " + (message[0] if message else "")
                  else:
                      # a single-word message
                      message = message[0].strip()
  
          pages = list(pages_we_sent) + list(pages_we_got)
          pages = sorted(pages, key=lambda page: page.date_created)
  
          if message:
              # send a message
              if not targets:
                  # no target given - send to last person we paged
                  if pages_we_sent:
                      targets = pages_we_sent[-1].receivers
                  else:
                      self.msg("Who do you want page?")
                      return
  
              header = "|wAccount|n |c%s|n |wpages:|n" % caller.key
              if message.startswith(":"):
                  message = "%s %s" % (caller.key, message.strip(":").strip())
  
              # create the persistent message object
              create.create_message(caller, message, receivers=targets)
  
              # tell the accounts they got a message.
              received = []
              rstrings = []
              for target in targets:
                  if not target.access(caller, "msg"):
                      rstrings.append(f"You are not allowed to page {target}.")
                      continue
                  target.msg(f"{header} {message}")
                  if hasattr(target, "sessions") and not target.sessions.count():
                      received.append(f"|C{target.name}|n")
                      rstrings.append(
                          f"{received[-1]} is offline. They will see your message "
                          "if they list their pages later."
                      )
                  else:
                      received.append(f"|c{target.name}|n")
              if rstrings:
                  self.msg("\n".join(rstrings))
              self.msg("You paged %s with: '%s'." % (", ".join(received), message))
              return
  
          else:
              # no message to send
              if number is not None and len(pages) > number:
                  lastpages = pages[-number:]
              else:
                  lastpages = pages
              to_template = "|w{date}{clr} {sender}|nto{clr}{receiver}|n:> {message}"
              from_template = "|w{date}{clr} {receiver}|nfrom{clr}{sender}|n:< {message}"
              listing = []
              prev_selfsend = False
              for page in lastpages:
                  multi_send = len(page.senders) > 1
                  multi_recv = len(page.receivers) > 1
                  sending = self.caller in page.senders
                  # self-messages all look like sends, so we assume they always
                  # come in close pairs and treat the second of the pair as the recv.
                  selfsend = sending and self.caller in page.receivers
                  if selfsend:
                      if prev_selfsend:
                          # this is actually a receive of a self-message
                          sending = False
                          prev_selfsend = False
                      else:
                          prev_selfsend = True
  
                  clr = "|c" if sending else "|g"
  
                  sender = f"|n,{clr}".join(obj.key for obj in page.senders)
                  receiver = f"|n,{clr}".join([obj.name for obj in page.receivers])
                  if sending:
                      template = to_template
                      sender = f"{sender} " if multi_send else ""
                      receiver = f" {receiver}" if multi_recv else f" {receiver}"
                  else:
                      template = from_template
                      receiver = f"{receiver} " if multi_recv else ""
                      sender = f" {sender} " if multi_send else f" {sender}"
  
                  listing.append(
                      template.format(
                          date=utils.datetime_format(page.date_created),
                          clr=clr,
                          sender=sender,
                          receiver=receiver,
                          message=page.message,
                      )
  
                  )
              lastpages = "\n ".join(listing)
  
              if lastpages:
                  string = "Your latest pages:\n %s" % lastpages
              else:
                  string = "You haven't paged anyone yet."
              self.msg(string)
              return
  
  
  def _list_bots(cmd):
      """
      Helper function to produce a list of all IRC bots.
  
      Args:
          cmd (Command): Instance of the Bot command.
      Returns:
          bots (str): A table of bots or an error message.
  
      """
      ircbots = [
          bot for bot in AccountDB.objects.filter(db_is_bot=True, username__startswith="ircbot-")
      ]
      if ircbots:
          table = cmd.styled_table(
              "|w#dbref|n",
              "|wbotname|n",
              "|wev-channel|n",
              "|wirc-channel|n",
              "|wSSL|n",
              maxwidth=_DEFAULT_WIDTH,
          )
          for ircbot in ircbots:
              ircinfo = "%s (%s:%s)" % (
                  ircbot.db.irc_channel,
                  ircbot.db.irc_network,
                  ircbot.db.irc_port,
              )
              table.add_row(
                  "#%i" % ircbot.id,
                  ircbot.db.irc_botname,
                  ircbot.db.ev_channel,
                  ircinfo,
                  ircbot.db.irc_ssl,
              )
          return table
      else:
          return "No irc bots found."
  
  class CmdIRC2Chan(COMMAND_DEFAULT_CLASS):
      """
      Link an evennia channel to an external IRC channel
  
      Usage:
        irc2chan[/switches] <evennia_channel> = <ircnetwork> <port> <#irchannel> <botname>[:typeclass]
        irc2chan/delete botname|#dbid
  
      Switches:
        /delete     - this will delete the bot and remove the irc connection
                      to the channel. Requires the botname or #dbid as input.
        /remove     - alias to /delete
        /disconnect - alias to /delete
        /list       - show all irc<->evennia mappings
        /ssl        - use an SSL-encrypted connection
  
      Example:
        irc2chan myircchan = irc.dalnet.net 6667 #mychannel evennia-bot
        irc2chan public = irc.freenode.net 6667 #evgaming #evbot:accounts.mybot.MyBot
  
      This creates an IRC bot that connects to a given IRC network and
      channel. If a custom typeclass path is given, this will be used
      instead of the default bot class.
      The bot will relay everything said in the evennia channel to the
      IRC channel and vice versa. The bot will automatically connect at
      server start, so this command need only be given once. The
      /disconnect switch will permanently delete the bot. To only
      temporarily deactivate it, use the  |wservices|n command instead.
      Provide an optional bot class path to use a custom bot.
      """
  
      key = "irc2chan"
      switch_options = ("delete", "remove", "disconnect", "list", "ssl")
      locks = "cmd:serversetting(IRC_ENABLED) and pperm(Developer)"
      help_category = "Comms"
  
      def func(self):
          """Setup the irc-channel mapping"""
  
          if not settings.IRC_ENABLED:
              string = """IRC is not enabled. You need to activate it in game/settings.py."""
              self.msg(string)
              return
  
          if "list" in self.switches:
              # show all connections
              self.msg(_list_bots(self))
              return
  
          if "disconnect" in self.switches or "remove" in self.switches or "delete" in self.switches:
              botname = "ircbot-%s" % self.lhs
              matches = AccountDB.objects.filter(db_is_bot=True, username=botname)
              dbref = utils.dbref(self.lhs)
              if not matches and dbref:
                  # try dbref match
                  matches = AccountDB.objects.filter(db_is_bot=True, id=dbref)
              if matches:
                  matches[0].delete()
                  self.msg("IRC connection destroyed.")
              else:
                  self.msg("IRC connection/bot could not be removed, does it exist?")
              return
  
          if not self.args or not self.rhs:
              string = (
                  "Usage: irc2chan[/switches] <evennia_channel> ="
                  " <ircnetwork> <port> <#irchannel> <botname>[:typeclass]"
              )
              self.msg(string)
              return
  
          channel = self.lhs
          self.rhs = self.rhs.replace("#", " ")  # to avoid Python comment issues
          try:
              irc_network, irc_port, irc_channel, irc_botname = [
                  part.strip() for part in self.rhs.split(None, 4)
              ]
              irc_channel = "#%s" % irc_channel
          except Exception:
              string = "IRC bot definition '%s' is not valid." % self.rhs
              self.msg(string)
              return
  
          botclass = None
          if ":" in irc_botname:
              irc_botname, botclass = [part.strip() for part in irc_botname.split(":", 2)]
          botname = "ircbot-%s" % irc_botname
          # If path given, use custom bot otherwise use default.
          botclass = botclass if botclass else bots.IRCBot
          irc_ssl = "ssl" in self.switches
  
          # create a new bot
          bot = AccountDB.objects.filter(username__iexact=botname)
          if bot:
              # re-use an existing bot
              bot = bot[0]
              if not bot.is_bot:
                  self.msg("Account '%s' already exists and is not a bot." % botname)
                  return
          else:
              try:
                  bot = create.create_account(botname, None, None, typeclass=botclass)
              except Exception as err:
                  self.msg("|rError, could not create the bot:|n '%s'." % err)
                  return
          bot.start(
              ev_channel=channel,
              irc_botname=irc_botname,
              irc_channel=irc_channel,
              irc_network=irc_network,
              irc_port=irc_port,
              irc_ssl=irc_ssl,
          )
          self.msg("Connection created. Starting IRC bot.")
  
  
  class CmdIRCStatus(COMMAND_DEFAULT_CLASS):
      """
      Check and reboot IRC bot.
  
      Usage:
          ircstatus [#dbref ping | nicklist | reconnect]
  
      If not given arguments, will return a list of all bots (like
      irc2chan/list). The 'ping' argument will ping the IRC network to
      see if the connection is still responsive. The 'nicklist' argument
      (aliases are 'who' and 'users') will return a list of users on the
      remote IRC channel.  Finally, 'reconnect' will force the client to
      disconnect and reconnect again. This may be a last resort if the
      client has silently lost connection (this may happen if the remote
      network experience network issues). During the reconnection
      messages sent to either channel will be lost.
  
      """
  
      key = "ircstatus"
      locks = "cmd:serversetting(IRC_ENABLED) and perm(ircstatus) or perm(Builder))"
      help_category = "Comms"
  
      def func(self):
          """Handles the functioning of the command."""
  
          if not self.args:
              self.msg(_list_bots(self))
              return
          # should always be on the form botname option
          args = self.args.split()
          if len(args) != 2:
              self.msg("Usage: ircstatus [#dbref ping||nicklist||reconnect]")
              return
          botname, option = args
          if option not in ("ping", "users", "reconnect", "nicklist", "who"):
              self.msg("Not a valid option.")
              return
          matches = None
          if utils.dbref(botname):
              matches = AccountDB.objects.filter(db_is_bot=True, id=utils.dbref(botname))
          if not matches:
              self.msg(
                  "No matching IRC-bot found. Use ircstatus without arguments to list active bots."
              )
              return
          ircbot = matches[0]
          channel = ircbot.db.irc_channel
          network = ircbot.db.irc_network
          port = ircbot.db.irc_port
          chtext = "IRC bot '%s' on channel %s (%s:%s)" % (
              ircbot.db.irc_botname,
              channel,
              network,
              port,
          )
          if option == "ping":
              # check connection by sending outself a ping through the server.
              self.caller.msg("Pinging through %s." % chtext)
              ircbot.ping(self.caller)
          elif option in ("users", "nicklist", "who"):
              # retrieve user list. The bot must handles the echo since it's
              # an asynchronous call.
              self.caller.msg("Requesting nicklist from %s (%s:%s)." % (channel, network, port))
              ircbot.get_nicklist(self.caller)
          elif self.caller.locks.check_lockstring(
              self.caller, "dummy:perm(ircstatus) or perm(Developer)"
          ):
              # reboot the client
              self.caller.msg("Forcing a disconnect + reconnect of %s." % chtext)
              ircbot.reconnect()
          else:
              self.caller.msg("You don't have permission to force-reload the IRC bot.")
  
  
  # RSS connection
  class CmdRSS2Chan(COMMAND_DEFAULT_CLASS):
      """
      link an evennia channel to an external RSS feed
  
      Usage:
        rss2chan[/switches] <evennia_channel> = <rss_url>
  
      Switches:
        /disconnect - this will stop the feed and remove the connection to the
                      channel.
        /remove     -                                 "
        /list       - show all rss->evennia mappings
  
      Example:
        rss2chan rsschan = http://code.google.com/feeds/p/evennia/updates/basic
  
      This creates an RSS reader  that connects to a given RSS feed url. Updates
      will be echoed as a title and news link to the given channel. The rate of
      updating is set with the RSS_UPDATE_INTERVAL variable in settings (default
      is every 10 minutes).
  
      When disconnecting you need to supply both the channel and url again so as
      to identify the connection uniquely.
      """
  
      key = "rss2chan"
      switch_options = ("disconnect", "remove", "list")
      locks = "cmd:serversetting(RSS_ENABLED) and pperm(Developer)"
      help_category = "Comms"
  
      def func(self):
          """Setup the rss-channel mapping"""
  
          # checking we have all we need
          if not settings.RSS_ENABLED:
              string = """RSS is not enabled. You need to activate it in game/settings.py."""
              self.msg(string)
              return
          try:
              import feedparser
  
              assert feedparser  # to avoid checker error of not being used
          except ImportError:
              string = (
                  "RSS requires python-feedparser (https://pypi.python.org/pypi/feedparser)."
                  " Install before continuing."
              )
              self.msg(string)
              return
  
          if "list" in self.switches:
              # show all connections
              rssbots = [
                  bot
                  for bot in AccountDB.objects.filter(db_is_bot=True, username__startswith="rssbot-")
              ]
              if rssbots:
                  table = self.styled_table(
                      "|wdbid|n",
                      "|wupdate rate|n",
                      "|wev-channel",
                      "|wRSS feed URL|n",
                      border="cells",
                      maxwidth=_DEFAULT_WIDTH,
                  )
                  for rssbot in rssbots:
                      table.add_row(
                          rssbot.id, rssbot.db.rss_rate, rssbot.db.ev_channel, rssbot.db.rss_url
                      )
                  self.msg(table)
              else:
                  self.msg("No rss bots found.")
              return
  
          if "disconnect" in self.switches or "remove" in self.switches or "delete" in self.switches:
              botname = "rssbot-%s" % self.lhs
              matches = AccountDB.objects.filter(db_is_bot=True, db_key=botname)
              if not matches:
                  # try dbref match
                  matches = AccountDB.objects.filter(db_is_bot=True, id=self.args.lstrip("#"))
              if matches:
                  matches[0].delete()
                  self.msg("RSS connection destroyed.")
              else:
                  self.msg("RSS connection/bot could not be removed, does it exist?")
              return
  
          if not self.args or not self.rhs:
              string = "Usage: rss2chan[/switches] <evennia_channel> = <rss url>"
              self.msg(string)
              return
          channel = self.lhs
          url = self.rhs
  
          botname = "rssbot-%s" % url
          bot = AccountDB.objects.filter(username__iexact=botname)
          if bot:
              # re-use existing bot
              bot = bot[0]
              if not bot.is_bot:
                  self.msg("Account '%s' already exists and is not a bot." % botname)
                  return
          else:
              # create a new bot
              bot = create.create_account(botname, None, None, typeclass=bots.RSSBot)
          bot.start(ev_channel=channel, rss_url=url, rss_rate=10)
          self.msg("RSS reporter created. Fetching RSS.")
  
  
  class CmdGrapevine2Chan(COMMAND_DEFAULT_CLASS):
      """
      Link an Evennia channel to an exteral Grapevine channel
  
      Usage:
        grapevine2chan[/switches] <evennia_channel> = <grapevine_channel>
        grapevine2chan/disconnect <connection #id>
  
      Switches:
          /list     - (or no switch): show existing grapevine <-> Evennia
                      mappings and available grapevine chans
          /remove   - alias to disconnect
          /delete   - alias to disconnect
  
      Example:
          grapevine2chan mygrapevine = gossip
  
      This creates a link between an in-game Evennia channel and an external
      Grapevine channel. The game must be registered with the Grapevine network
      (register at https://grapevine.haus) and the GRAPEVINE_* auth information
      must be added to game settings.
      """
  
      key = "grapevine2chan"
      switch_options = ("disconnect", "remove", "delete", "list")
      locks = "cmd:serversetting(GRAPEVINE_ENABLED) and pperm(Developer)"
      help_category = "Comms"
  
      def func(self):
          """Setup the Grapevine channel mapping"""
  
          if not settings.GRAPEVINE_ENABLED:
              self.msg("Set GRAPEVINE_ENABLED=True in settings to enable.")
              return
  
          if "list" in self.switches:
              # show all connections
              gwbots = [
                  bot
                  for bot in AccountDB.objects.filter(
                      db_is_bot=True, username__startswith="grapevinebot-"
                  )
              ]
              if gwbots:
                  table = self.styled_table(
                      "|wdbid|n",
                      "|wev-channel",
                      "|wgw-channel|n",
                      border="cells",
                      maxwidth=_DEFAULT_WIDTH,
                  )
                  for gwbot in gwbots:
                      table.add_row(gwbot.id, gwbot.db.ev_channel, gwbot.db.grapevine_channel)
                  self.msg(table)
              else:
                  self.msg("No grapevine bots found.")
              return
  
          if "disconnect" in self.switches or "remove" in self.switches or "delete" in self.switches:
              botname = "grapevinebot-%s" % self.lhs
              matches = AccountDB.objects.filter(db_is_bot=True, db_key=botname)
  
              if not matches:
                  # try dbref match
                  matches = AccountDB.objects.filter(db_is_bot=True, id=self.args.lstrip("#"))
              if matches:
                  matches[0].delete()
                  self.msg("Grapevine connection destroyed.")
              else:
                  self.msg("Grapevine connection/bot could not be removed, does it exist?")
              return
  
          if not self.args or not self.rhs:
              string = "Usage: grapevine2chan[/switches] <evennia_channel> = <grapevine_channel>"
              self.msg(string)
              return
  
          channel = self.lhs
          grapevine_channel = self.rhs
  
          botname = "grapewinebot-%s-%s" % (channel, grapevine_channel)
          bot = AccountDB.objects.filter(username__iexact=botname)
          if bot:
              # re-use existing bot
              bot = bot[0]
              if not bot.is_bot:
                  self.msg("Account '%s' already exists and is not a bot." % botname)
                  return
              else:
                  self.msg("Reusing bot '%s' (%s)" % (botname, bot.dbref))
          else:
              # create a new bot
              bot = create.create_account(botname, None, None, typeclass=bots.GrapevineBot)
  
          bot.start(ev_channel=channel, grapevine_channel=grapevine_channel)
          self.msg(f"Grapevine connection created {channel} <-> {grapevine_channel}.")
  