Skip to content

feat(go): complete Go rewrite with ent, fx, echo, bubbletea, gotd/td#563

Open
luoling8192 wants to merge 12 commits intomainfrom
claude/golang-rewrite-tui-tgbot-TgNDY
Open

feat(go): complete Go rewrite with ent, fx, echo, bubbletea, gotd/td#563
luoling8192 wants to merge 12 commits intomainfrom
claude/golang-rewrite-tui-tgbot-TgNDY

Conversation

@luoling8192
Copy link
Copy Markdown
Collaborator

Full Go rewrite of the Telegram message search codebase preserving
the core takeout and embedding search features.

Stack:

  • entgo/ent v0.14.4 for ORM (auto-migration + generated client)
  • go.uber.org/fx v1.24.0 for dependency injection
  • labstack/echo v4 for HTTP + bot webhook
  • gotd/td v0.115.0 for MTProto (confined to tgclient package)
  • go-telegram/bot v1.14.0 for Telegram Bot API
  • charmbracelet/bubbletea v1.3.4 for TUI
  • pgx/v5 + pgvector for raw SQL and vector search
  • go-openai for embedding API (OpenAI-compatible)
  • viper + zap for config and structured logging

Architecture:

  • tgclient.API interface wraps all gotd/td types, preventing leakage
  • sync, tui, bot packages only depend on the interface
  • Vector columns managed via raw SQL outside ent schema
  • Webhook and long-polling modes supported for bot
  • fx.Module per package for clean wiring in cmd/server and cmd/tui

Binaries:

  • cmd/server: Echo HTTP server + Telegram bot
  • cmd/tui: interactive terminal for auth, sync, and search

https://claude.ai/code/session_01A57Vi2yjqv9vRbEdA5NrBN

Full Go rewrite of the Telegram message search codebase preserving
the core takeout and embedding search features.

Stack:
- entgo/ent v0.14.4 for ORM (auto-migration + generated client)
- go.uber.org/fx v1.24.0 for dependency injection
- labstack/echo v4 for HTTP + bot webhook
- gotd/td v0.115.0 for MTProto (confined to tgclient package)
- go-telegram/bot v1.14.0 for Telegram Bot API
- charmbracelet/bubbletea v1.3.4 for TUI
- pgx/v5 + pgvector for raw SQL and vector search
- go-openai for embedding API (OpenAI-compatible)
- viper + zap for config and structured logging

Architecture:
- tgclient.API interface wraps all gotd/td types, preventing leakage
- sync, tui, bot packages only depend on the interface
- Vector columns managed via raw SQL outside ent schema
- Webhook and long-polling modes supported for bot
- fx.Module per package for clean wiring in cmd/server and cmd/tui

Binaries:
- cmd/server: Echo HTTP server + Telegram bot
- cmd/tui: interactive terminal for auth, sync, and search

https://claude.ai/code/session_01A57Vi2yjqv9vRbEdA5NrBN
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @luoling8192, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request represents a significant architectural shift, replacing the existing Telegram message search system with a robust and high-concurrency Go implementation. It introduces a modular design, separating concerns into distinct services for configuration, database interaction, embedding generation, search logic, Telegram client communication, and user interfaces. The new system is designed for scalability and maintainability, offering both a web-based bot interface and a terminal-based interactive tool.

Highlights

  • Complete Go Rewrite: The entire Telegram message search codebase has been rewritten in Go, leveraging modern frameworks and libraries.
  • New Technology Stack: The rewrite utilizes entgo/ent for ORM, go.uber.org/fx for dependency injection, labstack/echo for the HTTP server, charmbracelet/bubbletea for the TUI, gotd/td for MTProto, and pgvector for vector search.
  • Dual Application Modes: The system now provides two main binaries: a cmd/server for HTTP and bot webhook functionality, and a cmd/tui for an interactive terminal user interface for authentication, syncing, and searching.
  • Semantic Search Integration: The new architecture supports semantic (AI-powered) search using OpenAI-compatible embedding APIs and PostgreSQL with the pgvector extension, alongside traditional keyword search.
  • Database Schema and Migration: A new database schema is defined using Ent ORM for accounts, chats, messages, and users, with custom raw SQL for pgvector column management and migration.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • go/.golangci.yml
    • Added a new golangci-lint configuration file for code quality checks.
  • go/Makefile
    • Added a new Makefile to streamline common development tasks like building, generating code, testing, and linting.
  • go/README.md
    • Added a comprehensive README detailing the new Go rewrite's architecture, tech stack, quick start guide, and configuration reference.
  • go/cmd/server/main.go
    • Added the main entry point for the server binary, setting up the Echo HTTP server and Telegram bot with Fx dependency injection.
  • go/cmd/tui/main.go
    • Added the main entry point for the tui binary, providing an interactive terminal user interface for authentication, sync, and search.
  • go/ent/account.go
    • Added generated Ent ORM code for the Account entity, including fields for phone, session data, settings, MTProto update counters, and timestamps.
  • go/ent/account/account.go
    • Added generated Ent ORM code defining the Account schema constants and default values.
  • go/ent/account/where.go
    • Added generated Ent ORM code for Account query predicates.
  • go/ent/account_create.go
    • Added generated Ent ORM code for creating Account entities.
  • go/ent/account_delete.go
    • Added generated Ent ORM code for deleting Account entities.
  • go/ent/account_query.go
    • Added generated Ent ORM code for querying Account entities.
  • go/ent/account_update.go
    • Added generated Ent ORM code for updating Account entities.
  • go/ent/chat.go
    • Added generated Ent ORM code for the Chat entity, including fields for platform, chat ID, name, type, username, and timestamps.
  • go/ent/chat/chat.go
    • Added generated Ent ORM code defining the Chat schema constants and default values.
  • go/ent/chat/where.go
    • Added generated Ent ORM code for Chat query predicates.
  • go/ent/chat_create.go
    • Added generated Ent ORM code for creating Chat entities.
  • go/ent/chat_delete.go
    • Added generated Ent ORM code for deleting Chat entities.
  • go/ent/chat_query.go
    • Added generated Ent ORM code for querying Chat entities.
  • go/ent/chat_update.go
    • Added generated Ent ORM code for updating Chat entities.
  • go/ent/client.go
    • Added generated Ent ORM client for interacting with all defined entities.
  • go/ent/ent.go
    • Added generated Ent ORM core types and utilities.
  • go/ent/enttest/enttest.go
    • Added generated Ent ORM testing utilities.
  • go/ent/generate.go
    • Added Ent ORM code generation directive.
  • go/ent/hook/hook.go
    • Added generated Ent ORM hook utilities.
  • go/ent/message.go
    • Added generated Ent ORM code for the Message entity, including content, sender info, timestamps, and embedding model details.
  • go/ent/message/message.go
    • Added generated Ent ORM code defining the Message schema constants and default values.
  • go/ent/message/where.go
    • Added generated Ent ORM code for Message query predicates.
  • go/ent/message_create.go
    • Added generated Ent ORM code for creating Message entities.
  • go/ent/message_delete.go
    • Added generated Ent ORM code for deleting Message entities.
  • go/ent/message_query.go
    • Added generated Ent ORM code for querying Message entities.
  • go/ent/message_update.go
    • Added generated Ent ORM code for updating Message entities.
  • go/ent/migrate/migrate.go
    • Added generated Ent ORM migration utilities.
  • go/ent/migrate/schema.go
    • Added generated Ent ORM database schema definition for all entities and their relationships.
  • go/ent/predicate/predicate.go
    • Added generated Ent ORM predicate types.
  • go/ent/runtime.go
    • Added generated Ent ORM runtime initialization for schema descriptors.
  • go/ent/runtime/runtime.go
    • Added generated Ent ORM runtime constants.
  • go/ent/schema/account.go
    • Added the Ent ORM schema definition for the Account entity, including its fields and edges.
  • go/ent/schema/chat.go
    • Added the Ent ORM schema definition for the Chat entity, including its fields, edges, and indexes.
  • go/ent/schema/message.go
    • Added the Ent ORM schema definition for the Message entity, including its fields, edges, and indexes.
  • go/ent/schema/user.go
    • Added the Ent ORM schema definition for the User entity, including its fields and indexes.
  • go/ent/tx.go
    • Added generated Ent ORM transaction management utilities.
  • go/ent/user.go
    • Added generated Ent ORM code for the User entity, including fields for Telegram ID, username, names, and bot status.
  • go/ent/user/user.go
    • Added generated Ent ORM code defining the User schema constants and default values.
  • go/ent/user/where.go
    • Added generated Ent ORM code for User query predicates.
  • go/ent/user_create.go
    • Added generated Ent ORM code for creating User entities.
  • go/ent/user_delete.go
    • Added generated Ent ORM code for deleting User entities.
  • go/ent/user_query.go
    • Added generated Ent ORM code for querying User entities.
  • go/ent/user_update.go
    • Added generated Ent ORM code for updating User entities.
  • go/go.mod
    • Added Go module definition and listed all project dependencies.
  • go/go.sum
    • Added Go module checksums for dependency integrity.
  • go/internal/bot/handler/handler.go
    • Added Telegram bot command handlers for /start, /help, /search, and /sync commands, including markdown formatting and flag parsing.
  • go/internal/bot/module.go
    • Added the bot module, wiring the Telegram Bot API client and registering command handlers with the Echo server or long-polling.
  • go/internal/config/config.go
    • Added configuration loading and management using Viper, supporting environment variables, config files, and default values.
  • go/internal/db/db.go
    • Added database connection setup for Ent ORM and pgxpool, including schema migration and pgvector extension management.
  • go/internal/embed/service.go
    • Added an embedding service for interacting with OpenAI-compatible APIs to generate message embeddings.
  • go/internal/search/service.go
    • Added a search service that combines vector cosine-similarity search with keyword fallback for message retrieval.
  • go/internal/server/server.go
    • Added the Echo HTTP server setup, including request logging, recovery middleware, and a health endpoint.
  • go/internal/sync/takeout.go
    • Added the Telegram message history synchronization service, handling message fetching, upserting, and embedding.
  • go/internal/tgclient/client.go
    • Added a wrapper for the gotd/td MTProto client, abstracting Telegram API interactions and managing session persistence.
  • go/internal/tui/auth.go
    • Added the TUI authentication flow for Telegram accounts, guiding users through phone number, code, and 2FA input.
  • go/internal/tui/model.go
    • Added the core TUI model, managing tab navigation and delegating updates to sub-models.
  • go/internal/tui/search.go
    • Added the TUI search view, allowing users to input queries and display search results interactively.
  • go/internal/tui/sync.go
    • Added the TUI sync view, providing interactive controls and progress updates for Telegram message synchronization.
  • go/tgsearch.example.yaml
    • Added an example configuration file with detailed comments for database, Telegram, bot, embedding, and server settings.
Activity
  • The pull request was created by luoling8192.
  • It introduces a full Go rewrite of the Telegram message search codebase.
  • The changes include new files for project configuration, database schema, application logic, and user interfaces.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This is an impressive and comprehensive rewrite of the application in Go. The architecture is well-structured, leveraging modern libraries like fx for dependency injection, ent for ORM, and echo for the web server. The code is generally clean and demonstrates good practices. My review focuses on a few areas for improvement, including configuration, error handling, graceful shutdown, and some incomplete functionality that could lead to bugs. Overall, this is a very strong foundation for the project.

Comment thread go/internal/bot/module.go
Comment on lines +80 to +89
func register(lc fx.Lifecycle, b *Bot, e *echo.Echo) {
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
if b.cfg.WebhookURL != "" {
return b.startWebhook(ctx, e)
}
return b.startPolling(ctx)
},
})
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The DeleteWebhook method is defined but never called. When the server shuts down, the webhook registration on Telegram's side will persist. This can lead to Telegram sending updates to a dead endpoint, resulting in failed delivery notifications and potential rate-limiting. You should add an OnStop hook to the register function's lifecycle to call b.DeleteWebhook(ctx) for a clean shutdown.

func register(lc fx.Lifecycle, b *Bot, e *echo.Echo) {
	lc.Append(fx.Hook{
		OnStart: func(ctx context.Context) error {
			if b.cfg.WebhookURL != "" {
				return b.startWebhook(ctx, e)
			}
			return b.startPolling(ctx)
		},
		OnStop: func(ctx context.Context) error {
			if b.cfg.WebhookURL != "" {
				b.log.Info("deleting webhook")
				return b.DeleteWebhook(ctx)
			}
			return nil
		},
	})
}

Comment thread go/pkg/tgclient/client.go
Comment on lines +230 to +244
// GetHistory fetches up to limit messages for the given chat, offset by offsetID.
func (c *Client) GetHistory(ctx context.Context, chatID string, offsetID, limit int) ([]TGMessage, error) {
// NOTE: A full implementation resolves chatID to the correct InputPeer type
// and access hash from the DB entity cache. This stub returns an empty result
// until the entity cache is wired up.
_ = chatID

result, err := c.api.MessagesGetHistory(ctx, &tg.MessagesGetHistoryRequest{
Peer: &tg.InputPeerEmpty{},
OffsetID: offsetID,
Limit: limit,
})
if err != nil {
return nil, fmt.Errorf("tgclient: get history: %w", err)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The GetHistory function appears to be a stub that uses &tg.InputPeerEmpty{}. This will prevent it from fetching history for any specific chat, as a concrete InputPeer is required. The implementation needs to resolve the chatID to a proper tg.InputPeer with its corresponding access hash, likely by looking it up from the database where chat information is stored. Without this, the message sync feature will not work correctly.

// Spawn a goroutine to keep draining the channel and sending messages.
go func() {
for p := range ch {
_ = p // NOTE: In a real implementation send via a tea.Program.Send().
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The goroutine created to drain the progress channel from the sync service is not correctly sending messages back to the Bubble Tea runtime. The line _ = p discards the progress updates. To fix this, you need a way to send tea.Msg to the program, for example by having the command return a function that receives the tea.Program instance to call its Send method. As it is, only the first progress message is displayed, and the rest are lost, making the progress view non-functional.

Comment thread go/.golangci.yml Outdated
Comment on lines +75 to +76
ignorePackageGlobs:
- 'github.com/groupultra/telegram-search/*'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The wrapcheck linter is enabled, but its checks are then disabled for the entire project via ignorePackageGlobs. This configuration makes the linter ineffective. If the goal is to enforce error wrapping, this ignore rule should be removed. If you're planning to adopt it incrementally, consider using //nolint:wrapcheck comments on a case-by-case basis instead of a global ignore.

    # ignorePackageGlobs:
    #   - "github.com/groupultra/telegram-search/*"

Comment thread go/cmd/tui/main.go
log.Error("TUI exited with error", zap.Error(err))
fmt.Fprintln(os.Stderr, "error:", err)
}
os.Exit(0)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Calling os.Exit(0) here causes an immediate, forceful termination of the process. This bypasses the fx application's graceful shutdown procedure, meaning OnStop hooks won't run. For a cleaner shutdown that allows for resource cleanup, you should inject fx.Shutdowner into runTUI and call shutdowner.Shutdown() when the TUI exits.

Text: text,
}); err != nil {
// Log but don't propagate; send failures are non-fatal.
_ = err
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The comment on line 193 states "Log but don't propagate", but the error is currently being discarded. While not propagating the error is reasonable here, it should be logged to help with debugging issues where the bot might be failing to send messages.

Suggested change
_ = err
log.Warn("failed to send message", zap.Int64("chat_id", chatID), zap.Error(err))

ChatID: chatID,
Text: text,
ParseMode: models.ParseModeMarkdown,
}); err != nil {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The comment on line 203 states "Log but don't propagate", but the error is currently being discarded. While not propagating the error is reasonable here, it should be logged to help with debugging issues where the bot might be failing to send messages.

log.Warn("failed to send markdown message", zap.Int64("chat_id", chatID), zap.Error(err))

if len(filter.ChatIDs) == 0 {
return ""
}
return "AND c.chat_id = ANY($5)"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The SQL fragment AND c.chat_id = ANY($5) uses a hardcoded parameter index $5. This is brittle and can easily break if you add or remove parameters in the calling queries (vectorSearch and keywordSearch). Consider using named arguments with pgx (pgx.NamedArgs) or at least dynamically determining the parameter index to make this more robust.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 95f8e70e7e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread go/pkg/tgclient/client.go
log: log.Named("tgclient"),
ready: make(chan struct{}),
}
_ = lc
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Wire tgclient.Run into fx lifecycle

The constructor drops fx.Lifecycle (_ = lc) and there is no caller for (*Client).Run in the current Go app wiring, but c.api is initialized only inside Run. That means MTProto methods used by sync (GetDialogs/GetHistory) can execute with a nil API client after auth state is marked ready, causing runtime failures instead of a working sync flow.

Useful? React with 👍 / 👎.

Comment thread go/internal/bot/handler/handler.go Outdated
// NOTE: In a full implementation, resolve the accountID from the
// Telegram user ID via the DB (accounts table lookup).
// For now we use the user ID as a placeholder.
accountID := fmt.Sprintf("%d", userID)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Resolve bot account ID to UUID before search

The handler uses Telegram numeric userID as accountID, but the search SQL casts AccountID to UUID (owner_account_id = $1::uuid in internal/search/service.go). For normal bot users this causes invalid input syntax for type uuid, so /search requests fail consistently until this is mapped to a real account UUID.

Useful? React with 👍 / 👎.

err := s.pool.QueryRow(ctx, `
INSERT INTO chats (platform, chat_id, chat_name, owner_account_id)
VALUES ('telegram', $1, $2,
(SELECT id FROM accounts WHERE phone = $3 LIMIT 1))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Look up sync owner by account UUID, not phone

This query resolves owner_account_id via accounts.phone even though the function argument is named accountID and sync/search flows treat account identity as an ID. If callers pass a UUID, the subquery returns NULL and inserts fail on owner_account_id, so sync cannot persist chats/messages for UUID-based account references.

Useful? React with 👍 / 👎.

Comment on lines +160 to +163
go func() {
for p := range ch {
_ = p // NOTE: In a real implementation send via a tea.Program.Send().
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Forward all sync progress messages to the TUI

After returning the first progress event, the code drains the remaining channel messages and discards them (_ = p) instead of sending them back into Bubble Tea. As a result, the UI misses subsequent progress and completion/error signals, and the sync tab can remain stuck in a running state even when the background job has already finished.

Useful? React with 👍 / 👎.

Migrate all application-level logging from go.uber.org/zap to the
standard library log/slog (Go 1.21+), reducing one external dependency.

Changes:
- cmd/server: newLogger() returns *slog.Logger with JSON handler
- cmd/tui: newDevLogger() returns *slog.Logger with text handler at WARN
- fx wiring: fxevent.ZapLogger → fxevent.SlogLogger
- All internal packages: *zap.Logger → *slog.Logger, log.Named("x") → log.With("component", "x")
- tgclient: keep a minimal internal zap.Logger only for gotd/td's
  telegram.Options.Logger (which requires *zap.Logger); documented with
  a NOTICE comment. All other tgclient logging uses slog.

go.mod: remove go.uber.org/zap from the direct-deps block; it remains
as an indirect dep pulled in by go.uber.org/fx/fxevent (ZapLogger) and
gotd/td internals.

https://claude.ai/code/session_01A57Vi2yjqv9vRbEdA5NrBN
Co-authored-by: Neko <neko@ayaka.moe>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants