Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 45 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,14 @@ The application includes an automatic dialog summarization system to optimize AI
- **`Bodhi.Release`** (`lib/bodhi/release.ex`) - Release tasks including:
- `backfill_summaries/1` - Migration tool for historical data (line ~26)
- Supports dry-run, date ranges, and chat filtering
- **`Bodhi.TgWebhookHandler`** (`lib/bodhi/tg_webhook_handler.ex`) - Updated at line ~130
- **`Bodhi.TgUpdateHandler`** (`lib/bodhi/tg_update_handler.ex`) - Updated at line ~130
- Uses `get_chat_context_for_ai/2` instead of `get_chat_messages/1`

**Context Assembly:**
- `Bodhi.Chats.get_chat_context_for_ai/2` assembles context from:
- Summaries for messages older than 7 days (configurable)
- Full messages from last 7 days
- Used by `TgWebhookHandler.get_answer/2` instead of `get_chat_messages/1`
- Used by `TgUpdateHandler.get_answer/2` instead of `get_chat_messages/1`
- Gracefully falls back to recent messages when no summaries exist

**Configuration:**
Expand All @@ -73,6 +73,49 @@ config :bodhi, Oban,
- Summaries are idempotent - safe to regenerate
- Worker processes chats sequentially to respect rate limits
- Backfill tool supports dry-run mode for cost estimation

#### Telegram Bot Handlers

Update delivery is split across three modules so the same
business logic runs under both polling (dev/test) and
webhook (production) transports:

- **`Bodhi.TgUpdateHandler`** (`lib/bodhi/tg_update_handler.ex`) -
Shared update handling logic: parses the Telegram update,
routes commands, calls the LLM, and sends replies. Both
handlers below delegate to `on_update/1` here.
- **`Bodhi.TgPollingHandler`** (`lib/bodhi/tg_polling_handler.ex`) -
`Telegex.Polling.GenHandler` used in dev/test. Long-polls
Telegram for updates and forwards each to
`Bodhi.TgUpdateHandler.on_update/1`.
- **`Bodhi.TgHookHandler`** (`lib/bodhi/tg_hook_handler.ex`) -
GenServer used in production. On `init/1` it registers the
webhook URL (derived from `BodhiWeb.Endpoint.url/0`) via
`Bodhi.Telegram.delete_webhook/0` and
`Bodhi.Telegram.set_webhook/2`, and stops with
`{:webhook_setup_failed, reason}` if either call fails.
Updates arrive over HTTP via
`BodhiWeb.TelegramWebhookController`
(`POST /api/telegram/webhook`), authenticated by
`BodhiWeb.Plugs.TelegramWebhookAuth` comparing the
`X-Telegram-Bot-Api-Secret-Token` header against the
configured secret.

**Configuration:**
```elixir
# config/config.exs / config/test.exs / config/runtime.exs
config :bodhi, :tg_mode, :polling # :polling | :webhook | :disabled

# Only read when :tg_mode is :webhook (production)
config :bodhi, Bodhi.TgHookHandler, secret_token: "..."
```
- `Bodhi.Application` starts `Bodhi.TgPollingHandler`,
`Bodhi.TgHookHandler`, or neither based on `:tg_mode`
(see `tg_handler/0` in `lib/bodhi/application.ex`).
- Webhook registration goes through `Bodhi.Telegram` (the
same `Bodhi.Behaviours.TelegramClient` indirection used
by every other Telegram call), so it can be mocked with
`Bodhi.TelegramMock` in tests.
- Summary messages use `user_id: -1` as a special marker

## Project guidelines
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,20 @@ Edit `lib/bodhi/open_router.ex` and modify the `@default_model` attribute:

See all available models at: https://openrouter.ai/models

## Telegram Bot Configuration

In dev/test, the bot receives updates by polling Telegram
and no extra configuration is required. In production
(`MIX_ENV=prod`), it switches to webhook mode and requires:

- **Environment Variable:** `TG_WEBHOOK_SECRET` (required)
- Secret token Telegram must echo back on the
`X-Telegram-Bot-Api-Secret-Token` header of every
webhook request; requests without a matching token are
rejected with `401`.
- Set in `.envrc`: `export TG_WEBHOOK_SECRET=your_random_secret_here`
- The app raises on boot in production if this is unset.

## Database Configuration

By default, `dev` and `test` connect to the Postgres instance defined in
Expand Down
1 change: 1 addition & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -97,5 +97,6 @@ config :bodhi, Bodhi.Cache,

config :bodhi, :telegram_client, Bodhi.Telegram.TelegexAdapter
config :bodhi, :llm_provider, Bodhi.OpenRouter
config :bodhi, :tg_mode, :polling

import_config "#{config_env()}.exs"
8 changes: 8 additions & 0 deletions config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ if config_env() in [:dev, :test] do
end

if config_env() == :prod do
config :bodhi, :tg_mode, :webhook

secret_token =
System.get_env("TG_WEBHOOK_SECRET") ||
raise "environment variable TG_WEBHOOK_SECRET is missing"

config :bodhi, Bodhi.TgHookHandler, secret_token: secret_token

database_url =
System.get_env("DATABASE_URL") ||
raise """
Expand Down
1 change: 1 addition & 0 deletions config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,4 @@ config :bodhi, Bodhi.Cache,

config :bodhi, :telegram_client, Bodhi.TelegramMock
config :bodhi, :llm_provider, Bodhi.LLMMock
config :bodhi, :tg_mode, :disabled
2 changes: 1 addition & 1 deletion docs/SUMMARIZATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ If issues arise:

2. Revert to old behavior:
```elixir
# In TgWebhookHandler
# In TgUpdateHandler
defp get_answer(%_{chat_id: chat_id}, _) do
messages = Bodhi.Chats.get_chat_messages(chat_id) # Old way
{:ok, _answer} = Bodhi.AI.ask_llm(messages)
Expand Down
12 changes: 11 additions & 1 deletion lib/bodhi/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ defmodule Bodhi.Application do
{Phoenix.PubSub, name: Bodhi.PubSub},
# Start the Endpoint (http/https)
BodhiWeb.Endpoint,
Bodhi.TgWebhookHandler,
tg_handler(),
{Finch,
name: LLM,
pools: %{
Expand All @@ -29,12 +29,22 @@ defmodule Bodhi.Application do
{Oban, Application.fetch_env!(:bodhi, Oban)}
]

children = Enum.reject(children, &is_nil/1)

# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Bodhi.Supervisor]
Supervisor.start_link(children, opts)
end

defp tg_handler do
case Application.get_env(:bodhi, :tg_mode, :polling) do
:webhook -> Bodhi.TgHookHandler
:polling -> Bodhi.TgPollingHandler
:disabled -> nil
end
end

# Tell Phoenix to update the endpoint configuration
# whenever the application is updated.
@spec config_change(
Expand Down
12 changes: 12 additions & 0 deletions lib/bodhi/behaviours/telegram_client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,16 @@ defmodule Bodhi.Behaviours.TelegramClient do
chat_id :: integer(),
action :: String.t()
) :: {:ok, boolean()} | {:error, Telegex.Type.error()}

@doc """
Removes the currently configured webhook.
"""
@callback delete_webhook() ::
{:ok, boolean()} | {:error, Telegex.Type.error()}

@doc """
Registers a webhook URL to receive updates.
"""
@callback set_webhook(url :: String.t(), opts :: Keyword.t()) ::
{:ok, boolean()} | {:error, Telegex.Type.error()}
end
2 changes: 1 addition & 1 deletion lib/bodhi/periodic_messages.ex
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,6 @@ defmodule Bodhi.PeriodicMessages do
prompt_id: prompt_id
})

Bodhi.TgWebhookHandler.send_message(chat_id, text)
Bodhi.TgUpdateHandler.send_message(chat_id, text)
end
end
16 changes: 16 additions & 0 deletions lib/bodhi/telegram.ex
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,22 @@ defmodule Bodhi.Telegram do
impl().send_chat_action(chat_id, action)
end

@doc """
Removes the currently configured webhook.
"""
@impl true
def delete_webhook do
impl().delete_webhook()
end

@doc """
Registers a webhook URL to receive updates.
"""
@impl true
def set_webhook(url, opts \\ []) do
impl().set_webhook(url, opts)
end

defp impl do
Application.get_env(:bodhi, :telegram_client, Bodhi.Telegram.TelegexAdapter)
end
Expand Down
10 changes: 10 additions & 0 deletions lib/bodhi/telegram/telegex_adapter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,14 @@ defmodule Bodhi.Telegram.TelegexAdapter do
def send_chat_action(chat_id, action) do
Telegex.send_chat_action(chat_id, action)
end

@impl true
def delete_webhook do
Telegex.delete_webhook()
end

@impl true
def set_webhook(url, opts) do
Telegex.set_webhook(url, opts)
end
end
49 changes: 49 additions & 0 deletions lib/bodhi/tg_hook_handler.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
defmodule Bodhi.TgHookHandler do
@moduledoc """
Registers the Telegram webhook on application start.

The webhook URL is derived from `BodhiWeb.Endpoint.url/0`
combined with the Telegram webhook route. The secret
token is read from application config. The actual HTTP
handling is done by
`BodhiWeb.TelegramWebhookController`.
"""
use GenServer

require Logger

@webhook_path "/api/telegram/webhook"

@spec start_link(keyword()) :: GenServer.on_start()
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end

@impl true
@spec init(keyword()) ::
{:ok, map()} | {:stop, {:webhook_setup_failed, term()}}
def init(_opts) do
config =
Application.get_env(:bodhi, __MODULE__, [])

webhook_url = BodhiWeb.Endpoint.url() <> @webhook_path

with {:ok, true} <- Bodhi.Telegram.delete_webhook(),
{:ok, true} <-
Bodhi.Telegram.set_webhook(webhook_url,
secret_token: config[:secret_token]
) do
Logger.info("Telegram webhook registered: #{webhook_url}")

{:ok, %{}}
else
error ->
Logger.error(
"Failed to configure Telegram webhook: " <>
"#{inspect(error)}"
)

{:stop, {:webhook_setup_failed, error}}
end
end
end
19 changes: 19 additions & 0 deletions lib/bodhi/tg_polling_handler.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
defmodule Bodhi.TgPollingHandler do
@moduledoc """
Telegram polling handler for dev/test environments.
Delegates update processing to `Bodhi.TgUpdateHandler`.
"""
use Telegex.Polling.GenHandler

@impl true
@spec on_boot :: Telegex.Polling.Config.t()
def on_boot do
%Telegex.Polling.Config{}
end

@impl true
@spec on_update(Telegex.Type.Update.t()) :: :ok
def on_update(update) do
Bodhi.TgUpdateHandler.on_update(update)
end
end
Loading
Loading