mirror of
https://github.com/tiennm99/openai-status-bot.git
synced 2026-10-13 18:16:01 +00:00
feat(bot): register telegram command menu
This commit is contained in:
1 parent
aa425ec98d
commit
956b2a98b4
8 files changed
+98
-1
No files matched your search
@@ -11,6 +11,7 @@ Telegram bot that watches [OpenAI Status](https://status.openai.com/) every minu
|
||||
- Uses Redis for subscribers, delivery state, component checkpoints, and seen incident update versions
|
||||
- Supports Telegram supergroup topics via `message_thread_id`
|
||||
- Clears an existing Telegram webhook before long polling, for migration from webhook deployments
|
||||
- Registers the Telegram command menu on startup
|
||||
- Includes Docker Compose for local Redis + bot runtime
|
||||
|
||||
## Bot Commands
|
||||
|
||||
@@ -45,6 +45,9 @@ func main() {
|
||||
logger.Error("delete telegram webhook", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := telegramClient.SetMyCommands(ctx, bot.MenuCommands()); err != nil {
|
||||
logger.Warn("set telegram bot commands", "error", err)
|
||||
}
|
||||
|
||||
botUsername := ""
|
||||
if me, err := telegramClient.GetMe(ctx); err != nil {
|
||||
|
||||
@@ -38,7 +38,7 @@ Subscriber set members are `chatID` or `chatID:threadID`. Missing settings defau
|
||||
|
||||
## Runtime
|
||||
|
||||
The service uses Telegram `getUpdates`, so it does not need a public webhook URL. On startup it calls `deleteWebhook` before long polling, which allows migration from a webhook-based deployment using the same bot token. Redis is configured with `REDIS_URL`. Docker Compose starts Redis and the bot.
|
||||
The service uses Telegram `getUpdates`, so it does not need a public webhook URL. On startup it calls `deleteWebhook` before long polling, registers the Telegram command menu, and then starts polling. Redis is configured with `REDIS_URL`. Docker Compose starts Redis and the bot.
|
||||
|
||||
## Failure Behavior
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package bot
|
||||
|
||||
import "github.com/tiennm99/openai-status-bot/internal/telegram"
|
||||
|
||||
func MenuCommands() []telegram.BotCommand {
|
||||
return []telegram.BotCommand{
|
||||
{Command: "start", Description: "Subscribe this chat or topic"},
|
||||
{Command: "stop", Description: "Unsubscribe this chat or topic"},
|
||||
{Command: "status", Description: "Show current OpenAI status"},
|
||||
{Command: "components", Description: "Show all component statuses"},
|
||||
{Command: "subscribe", Description: "Set notification preferences"},
|
||||
{Command: "history", Description: "Show recent incidents"},
|
||||
{Command: "uptime", Description: "Show component health overview"},
|
||||
{Command: "info", Description: "Show chat and subscription details"},
|
||||
{Command: "help", Description: "Show command help"},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package bot
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestMenuCommandsMatchSupportedCommands(t *testing.T) {
|
||||
commands := MenuCommands()
|
||||
got := make([]string, 0, len(commands))
|
||||
for _, command := range commands {
|
||||
if command.Command == "" {
|
||||
t.Fatal("command is empty")
|
||||
}
|
||||
if command.Command[0] == '/' {
|
||||
t.Fatalf("command %q must not include leading slash", command.Command)
|
||||
}
|
||||
if command.Description == "" {
|
||||
t.Fatalf("description for %q is empty", command.Command)
|
||||
}
|
||||
got = append(got, command.Command)
|
||||
}
|
||||
|
||||
want := []string{"start", "stop", "status", "components", "subscribe", "history", "uptime", "info", "help"}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("commands = %v, want %v", got, want)
|
||||
}
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Fatalf("commands = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,12 @@ func (c *Client) GetMe(ctx context.Context) (User, error) {
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (c *Client) SetMyCommands(ctx context.Context, commands []BotCommand) error {
|
||||
payload := map[string]any{"commands": commands}
|
||||
var result json.RawMessage
|
||||
return c.postJSON(ctx, "/setMyCommands", payload, &result, c.requestTimeout)
|
||||
}
|
||||
|
||||
func (c *Client) GetUpdates(ctx context.Context, offset int64, timeoutSeconds int) ([]Update, error) {
|
||||
payload := map[string]any{
|
||||
"offset": offset,
|
||||
|
||||
@@ -59,6 +59,41 @@ func TestSendTextUsesHTMLAndDisablesPreview(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetMyCommandsRegistersCommands(t *testing.T) {
|
||||
var gotPath string
|
||||
var payload struct {
|
||||
Commands []BotCommand `json:"commands"`
|
||||
}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotPath = r.URL.Path
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
t.Fatalf("decode payload: %v", err)
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"ok":true,"result":true}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := &Client{baseURL: server.URL, httpClient: server.Client()}
|
||||
commands := []BotCommand{
|
||||
{Command: "start", Description: "Subscribe"},
|
||||
{Command: "status", Description: "Show status"},
|
||||
}
|
||||
if err := client.SetMyCommands(context.Background(), commands); err != nil {
|
||||
t.Fatalf("SetMyCommands returned error: %v", err)
|
||||
}
|
||||
if gotPath != "/setMyCommands" {
|
||||
t.Fatalf("path = %s", gotPath)
|
||||
}
|
||||
if len(payload.Commands) != len(commands) {
|
||||
t.Fatalf("commands = %v, want %v", payload.Commands, commands)
|
||||
}
|
||||
for i := range commands {
|
||||
if payload.Commands[i] != commands[i] {
|
||||
t.Fatalf("commands[%d] = %+v, want %+v", i, payload.Commands[i], commands[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsTerminalSendError(t *testing.T) {
|
||||
terminal := &APIError{StatusCode: 200, ErrorCode: 403, Description: "Forbidden: bot was blocked by the user"}
|
||||
if !IsTerminalSendError(terminal) {
|
||||
|
||||
@@ -24,3 +24,8 @@ type User struct {
|
||||
FirstName string `json:"first_name"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
type BotCommand struct {
|
||||
Command string `json:"command"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
Reference in new issue
Block a user