rockneurotiko/ex_gram

How to create many bots

mackdunkan opened this issue · 7 comments

Hello, tell me how to create many bots but with one logic? I have clients and each client has a small question builder for the bot.

I guess you need different bots with different tokens, you can just extract the logic to another module:

defmodule Bot1 do
   use ExGram.Bot, name: :bot1

  defdelegate handle, to: Logic
end

defmodule Bot2 do
  use ExGram.Bot, name: :bot2
  
  defdelegate handle, to: Logic
end

defmodule Logic do
  def handle(command, context) do 
    # whatever
  end
end

# On the supervisor:

children = [
  ExGram, # This will setup the Registry.ExGram
  {Bot1, [method: :polling, token: "TOKEN1"]},
  {Bot2, [method: :polling, token: "TOKEN2"]}
]

In this case, I need to make a module for each bot. what if I have a list of tokens and a name?

Ah, I didn't quite understood the question.

You can start many bots with:

children = [
  ExGram, # This will setup the Registry.ExGram
  {Bot, [id: :bot1, name: :bot1, method: :polling, token: "TOKEN1"]},
  {Bot, [id: :bot2, name: :bot2, method: :polling, token: "TOKEN2"]}
]

The problem is answering the messages, I think that with the current code it would be really difficult to know which token to use when answering. We could try to add the name used when starting to the context, so when building the answers that information can be used.

I wanted to be able to add bots at runtime and not at compile time

You need to take a look to DynamicSupervisor then: https://hexdocs.pm/elixir/1.13/DynamicSupervisor.html

When I try to use DhynamicSupervisor with 2 bot processes I got the error:

defmodule NNBot.BotSupervisor do
  use DynamicSupervisor

  alias NNBot.Bot
  alias NNBot.Nexus

  @spec start_link(any()) :: Supervisor.on_start_child() | :ignore
  def start_link(_init_arg) do
    {:ok, _} = DynamicSupervisor.start_link(__MODULE__, [], name: __MODULE__)

    stories = Nexus.list_active_stories()
    Enum.reduce_while(stories, nil, &start_child_initially/2)
  end
  
  def start_child(story) do
    name = String.to_atom("main_#{story.id}")

    child_spec = {Bot, token: story.token, method: :polling, name: name, id: name}
    res = DynamicSupervisor.start_child(__MODULE__, child_spec)
  end
  
  @impl true
  def init(_init_arg) do
    DynamicSupervisor.init(strategy: :one_for_one])
  end

  defp start_child_initially(story, _acc) do
    res = start_child(story)

    case res do
      {:ok, _child} -> {:cont, res}
      {:error, _error} -> {:halt, res}
    end
  end
** (Mix) Could not start application nn_bot: NNBot.Application.start(:normal, []) returned an error: shutdown: failed to start child: NNBot.BotSupervisor
    ** (EXIT) already started: #PID<0.571.0>

Also, I want to pass the story struct to the context.extra but I can't find the way how to do that 🙁

So, there was some missing parts on the code, I didn't thought on using the same Bot module with different tokens, so what was happening was that the bot_name was clashing (The name setup here: use ExGram.Bot, name: :my_bot)

I just improved and merged #132 with the ability to specify a bot_name on start, and also extra_info to be added to the extra in the context!

Here is a DynamicSupervisor I used to test it:

defmodule MyBot.BotSupervisor do
  use DynamicSupervisor

  alias MyBot.Bot

  @spec start_link(any()) :: Supervisor.on_start_child() | :ignore
  def start_link(_init_arg) do
    DynamicSupervisor.start_link(__MODULE__, [], name: __MODULE__)
  end

  @impl true
  def init(_init_arg) do
    DynamicSupervisor.init(strategy: :one_for_one)
  end

  def start_stories() do
    stories = [
      %{
        id: 1,
        token: "<token>",
        bot_name: :bot1,
        extra_info: %{a: 1}
      },
      %{
        id: 2,
        token: "<token1>",
        bot_name: :bot2,
        extra_info: %{a: 2}
      }
    ]

    Enum.each(stories, &start_child/1)
  end

  def start_child(story) do
    name = String.to_atom("main_#{story.id}")

    child_spec =
      {Bot,
       token: story.token,
       method: :polling,
       name: name,
       id: name,
       bot_name: story.bot_name,
       extra_info: story.extra_info}

    {:ok, _} = DynamicSupervisor.start_child(__MODULE__, child_spec)
  end
end

And in the application:

children = [
      ExGram,
      MyBot.BotSupervisor,
      {Task, &MyBot.BotSupervisor.start_stories/0}
    ]

With this approach you will see that now all bots start, and you have the values used on extra_info in the context.extra field, the only downside is that you can't use the original name anymore, instead you have to use the name in the context. This is only needed when doing specific things or calling ExGram manually, using the DSL the name is already fetched from the context

  def handle({:command, :start, _msg}, context) do
    ExGram.get_me(bot: context.name) # <- if the bot name is needed, use it from the context 
  end