1package modes
2
3import (
4 "context"
5 "errors"
6 "fmt"
7
8 "github.com/bnema/zut/packages/agent/modes/telegram"
9)
10
11func (i *Interactive) openTelegramDialog() {
12 items := i.telegramMenuItems()
13 if len(items) == 0 {
14 i.mu.Lock()
15 i.statusErr = "telegram not configured. run `zut telegram-bot setup` first."
16 i.mu.Unlock()
17 i.invalidate()
18 return
19 }
20 i.telegramDialog.Open(items)
21 i.invalidate()
22}
23func (i *Interactive) telegramMenuItems() []telegramItem {
24 cfg, err := telegram.LoadConfig(i.cfg.ZutHome)
25 if err != nil || cfg.BotToken == "" {
26 return nil
27 }
28 var items []telegramItem
29 if i.telegramBridge != nil && i.telegramBridge.Active() {
30 items = append(items, telegramItem{label: "disconnect", action: "disconnect", hint: "stop mirroring"})
31 st := i.telegramBridge.State()
32 hint := "active"
33 if st.Username != "" {
34 hint += " as @" + st.Username
35 }
36 items = append(items, telegramItem{label: "status", action: "status", hint: hint})
37 } else {
38 label := "connect"
39 hint := "start mirroring dms into this session"
40 if cfg.AllowedUserID == 0 {
41 hint = "awaiting pairing (send /start to the bot once connected)"
42 }
43 items = append(items, telegramItem{label: label, action: "connect", hint: hint})
44 items = append(items, telegramItem{label: "status", action: "status", hint: "disconnected"})
45 }
46 return items
47}
48func (i *Interactive) doTelegram(action string) {
49 switch action {
50 case "connect":
51 i.telegramConnect()
52 case "disconnect":
53 i.telegramDisconnect()
54 case "status":
55 i.telegramStatus()
56 default:
57 i.mu.Lock()
58 i.statusErr = "unknown telegram action: " + action + " (use connect, disconnect, or status)"
59 i.mu.Unlock()
60 i.invalidate()
61 }
62}
63func (i *Interactive) telegramConnect() {
64 if i.telegramBridge != nil && i.telegramBridge.Active() {
65 i.mu.Lock()
66 i.statusOK = "telegram already connected"
67 i.statusErr = ""
68 i.mu.Unlock()
69 i.invalidate()
70 return
71 }
72 cfg, err := telegram.LoadConfig(i.cfg.ZutHome)
73 if err != nil {
74 i.mu.Lock()
75 i.statusErr = "telegram: " + err.Error()
76 i.mu.Unlock()
77 i.invalidate()
78 return
79 }
80 if cfg.BotToken == "" {
81 i.mu.Lock()
82 i.statusErr = "telegram: no bot token configured. run `zut telegram-bot setup` first."
83 i.mu.Unlock()
84 i.invalidate()
85 return
86 }
87 // Refuse to start when a background daemon is already polling
88 // the same bot. Two concurrent long-poll consumers race each
89 // update and one always loses, so DMs get half-delivered. The
90 // user can `zut telegram-bot stop` first, then /telegram connect.
91 if pid, alive, _ := telegram.IsRunning(i.cfg.ZutHome); alive && pid > 0 {
92 i.mu.Lock()
93 i.statusErr = fmt.Sprintf("telegram: bot daemon already running (pid %d). stop it with `zut telegram-bot stop` first.", pid)
94 i.mu.Unlock()
95 i.invalidate()
96 return
97 }
98 bridge := &telegram.Bridge{
99 Client: telegram.NewClient(cfg.BotToken),
100 Config: cfg,
101 Save: func(next telegram.Config) error {
102 return telegram.SaveConfig(i.cfg.ZutHome, next)
103 },
104 Host: &telegramHost{iv: i},
105 }
106 i.mu.Lock()
107 i.telegramBridge = bridge
108 i.mu.Unlock()
109 // Strip web_search before the bridge's startup handshake can accept a
110 // Telegram update. ApplyAgentPromptConfig also keeps it stripped from
111 // concurrent refreshes while this bridge pointer is attached.
112 i.applyTelegramTools(true)
113 if err := bridge.Start(i.runCtx); err != nil {
114 i.mu.Lock()
115 i.telegramBridge = nil
116 i.mu.Unlock()
117 refreshErr := i.refreshToolsAfterTelegram()
118 i.mu.Lock()
119 i.statusErr = "telegram connect failed: " + err.Error()
120 if refreshErr != nil {
121 i.statusErr += "; tool refresh: " + refreshErr.Error()
122 }
123 i.mu.Unlock()
124 i.invalidate()
125 return
126 }
127 state := bridge.State()
128 label := "telegram connected"
129 if state.Username != "" {
130 label += " as @" + state.Username
131 }
132 if state.PairedID == 0 {
133 label += " — send /start to the bot from your phone to claim it"
134 }
135 i.mu.Lock()
136 i.statusOK = label
137 i.statusErr = ""
138 i.mu.Unlock()
139 i.invalidate()
140}
141func (i *Interactive) telegramDisconnect() {
142 if i.telegramBridge == nil || !i.telegramBridge.Active() {
143 i.mu.Lock()
144 i.statusOK = "telegram already disconnected"
145 i.statusErr = ""
146 i.mu.Unlock()
147 i.invalidate()
148 return
149 }
150 bridge := i.telegramBridge
151 bridge.Stop()
152 i.mu.Lock()
153 // Clear the pointer before rebuilding the normal registry. This both
154 // allows web_search back into the refresh and marks the bridge inactive
155 // for any concurrent prompt-config commit.
156 i.telegramBridge = nil
157 i.mu.Unlock()
158 refreshErr := i.refreshToolsAfterTelegram()
159 i.mu.Lock()
160 if refreshErr != nil {
161 i.statusOK = ""
162 i.statusErr = "telegram disconnect tool refresh: " + refreshErr.Error()
163 } else {
164 i.statusOK = "telegram disconnected"
165 i.statusErr = ""
166 }
167 i.mu.Unlock()
168 i.invalidate()
169}
170func (i *Interactive) refreshToolsAfterTelegram() error {
171 if i.cfg.RefreshTools == nil {
172 i.stripWebSearchTool()
173 i.applyTelegramTools(false)
174 return errors.New("live tool refresh is unavailable")
175 }
176 if err := i.cfg.RefreshTools(); err != nil {
177 i.stripWebSearchTool()
178 i.applyTelegramTools(false)
179 return err
180 }
181 i.applyTelegramTools(false)
182 return nil
183}
184func (a telegramSenderAdapter) SendImage(ctx context.Context, path, caption string) error {
185 if a.bridge == nil {
186 return fmt.Errorf("telegram bridge is not connected")
187 }
188 return a.bridge.SendImage(ctx, path, caption)
189}
190func (a telegramSenderAdapter) SendDocument(ctx context.Context, path, caption string) error {
191 if a.bridge == nil {
192 return fmt.Errorf("telegram bridge is not connected")
193 }
194 return a.bridge.SendDocument(ctx, path, caption)
195}
196func (a telegramSenderAdapter) Active() bool {
197 return a.bridge != nil && a.bridge.Active()
198}