cronokirby/alchemy

[feature | breaking] Support for handling many bots at once

Opened this issue · 2 comments

Right now, alchemy has a global architecture intended to support a single bot account. But there are many cases where a user would want to load and interface with many bots at a time, all in the same application instance.

Prerequisites

  • In any case where named processes are used, replace it with a composite key using the client.
  • For every stateful API method that alchemy provides, accept a client reference.

Problems

  • Right now, both commands and events are implemented with __using__ and the developer issuing a use in their startup logic. This is inherently a global operation, so we would have to replace the method of registering these callbacks.

Cogs

  • Current usage
defmodule MyCommands do
  use Alchemy.Cogs

  Cogs.def hello(name) do
    Cogs.say "Hello " <> name
  end

  Cogs.def hello do
    :noop
  end
end

defmodule MyBot do
  use Application

  def start(_type, _args) do
    run = Client.start("token")
    use MyCommands
    run
  end
end
  • Suggested usage (also moving away from a magical Cogs)
defmodule MyCommands do
  def hello(message, [name], _context = %{client: client}) do
    Alchemy.Client.reply(client, message, "Hello " <> name)
  end

  def hello(_message, _args, _context) do
    :noop
  end

  def register_commands(client) do
    Alchemy.Commands.add_command(client, "hello", &Commands.hello/3)
  end
end

defmodule MyBot do
  use Application

  alias Alchemy.Client

  def start(_type, _args) do
    with {:ok, client} <- Client.start("token") do
      # if we need custom keys in context
      Alchemy.Commands.put_context(client, :some_key, :some_value)
      MyCommands.register_commands(client)
      {:ok, client}
    end
  end
end

Events

  • Current usage
defmodule MyEvents do
  use Alchemy.Events

  Events.on_message(:on_message)
  def on_message(message = %{channel_id: channel_id, content: content}) do
    Alchemy.Client.send_message(channel_id, "Received: " <> content)
  end
end

defmodule MyBot do
  use Application

  def start(_type, _args) do
    run = Client.start("token")
    use MyEvents
    run
  end
end
  • Suggested usage (also with proposed events usage from #71)
defmodule MyEvents do
  def on_message(_event = %MessageCreateEvent{channel_id: channel_id, message: message}, _context = %{client: client}) do
    %{content: content} = message
    Alchemy.Client.send_message(client, channel_id, "Received: " <> content)
  end

  def register_handlers(client) do
    Alchemy.Events.add_handler(client, :message_create, &on_message/2)
  end
end

defmodule MyBot do
  use Application

  def start(_type, _args) do
    with {:ok, client} <- Client.start("token") do
      # if we need custom keys in context
      Alchemy.Events.put_context(client, :some_key, :some_value)
      MyEvents.register_handlers(client)
      {:ok, client}
    end
  end
end

These changes would make more sense to make after #71 and Cogs is changed. It's not clear whether or not Cache would be affected by this change since we may want Clients to be able to share a Cache. With three bots, Alchemy would be receiving the same events three times (if the bots were in the same servers), which I think is an argument to make caches client-specific.

Yeah, this is probably the most longstanding flaw in the library, and something I've always wondered how to address in a clean way.

I remember kind of realizing that passing around the current client was pretty much unavoidable, or at the very least, you need some kind of way of knowing which API key to be using at any time, since you've got multiple floating around.

Maybe this redesign would be a candidate for a 1.0?

But yeah, this seems like a good idea, and a bit cleaner too.

I think you can design the Cache with this in mind, and have much problems. All requests would flow through the same infrastructure, just tagged with the API key, so you could use a universal cache for requests.

For guild events in theory you can share a cache, although one issue might be that one bot could see things in the cache that it would otherwise not have permission to see if it asked for them globally. This would give non-deterministic results at that point.

One solution to that problem is to simply separate things out into multiple caches, then you would never have problems of that sort.

I think that's one big thing that would be need to modified in the cache, as I think if you just used the current cache with multiple bots you could get into weird states where a bot can sometimes see things it's not supposed to have access to. But you could easily just dynamically spin up multiple caches, and route to the correct one based on which bot you're dealing with.