Skip to content

Commit ca31e80

Browse files
committed
refactor(otp): update WaitForOTP to accept context for cancellation and timeout
feat(bot): add auto OTP handling for Telegram bot using webhook feat(cli): implement auto OTP handling for CLI using webhook fix(cli): display "Auto OTP received" message when OTP is received via webhook fix(otp): ensure correct waiter channel is deleted in WaitForOTP to prevent race conditions
1 parent 0de3d11 commit ca31e80

6 files changed

Lines changed: 70 additions & 16 deletions

File tree

bot/login.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,29 @@ func (h *Handler) handlePhoneInput(b *gotgbot.Bot, ctx *ext.Context, userID int6
3939
otpCallback := func() (string, error) {
4040
_, _ = ctx.EffectiveMessage.Reply(b, "📲 OTP dikirim ke HP kamu.\n\n🔢 *Kirim kode OTP:*", &gotgbot.SendMessageOpts{ParseMode: "Markdown"})
4141

42+
if h.otpListener != nil {
43+
waitCtx, cancel := context.WithTimeout(apiCtx, 3*time.Minute)
44+
defer cancel() // LANGSUNG mati kalau manual input yang jalan/menang
45+
46+
webhookChan := make(chan string, 1)
47+
go func() {
48+
otp, err := h.otpListener.WaitForOTP(waitCtx, local)
49+
if err == nil && otp != "" {
50+
webhookChan <- otp
51+
}
52+
}()
53+
54+
select {
55+
case otp := <-otpChan:
56+
return otp, nil
57+
case otp := <-webhookChan:
58+
_, _ = ctx.EffectiveMessage.Reply(b, "🤖 *Auto OTP diterima dari SMS Forwarder!* Memverifikasi...", &gotgbot.SendMessageOpts{ParseMode: "Markdown"})
59+
return otp, nil
60+
case <-time.After(2 * time.Minute):
61+
return "", fmt.Errorf("OTP timeout (2 menit)")
62+
}
63+
}
64+
4265
select {
4366
case otp := <-otpChan:
4467
return otp, nil

cli/commands.go

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -77,20 +77,37 @@ func (m *tuiModel) doLogin(localPhone string) tea.Cmd {
7777
return func() tea.Msg {
7878
ctx := context.Background()
7979
otpCallback := func() (string, error) {
80+
if programRef != nil {
81+
programRef.Send(otpRequestMsg{})
82+
}
83+
8084
if m.otpListener != nil {
81-
if programRef != nil {
82-
programRef.Send(otpRequestMsg{})
83-
}
84-
otp, err := m.otpListener.WaitForOTP(localPhone, 3*time.Minute)
85-
if err != nil {
86-
return "", fmt.Errorf("auto OTP: %w", err)
85+
waitCtx, cancel := context.WithTimeout(ctx, 3*time.Minute)
86+
defer cancel() // LANGSUNG mati kalau manual input yang jalan/menang
87+
88+
webhookChan := make(chan string, 1)
89+
go func() {
90+
otp, err := m.otpListener.WaitForOTP(waitCtx, localPhone)
91+
if err == nil && otp != "" {
92+
webhookChan <- otp
93+
}
94+
}()
95+
96+
select {
97+
case otp := <-otpChan:
98+
if otp == "" {
99+
return "", fmt.Errorf("OTP kosong")
100+
}
101+
return otp, nil
102+
case otp := <-webhookChan:
103+
if programRef != nil {
104+
programRef.Send(autoOtpMsg{})
105+
}
106+
return otp, nil
87107
}
88-
return otp, nil
89108
}
90109

91-
if programRef != nil {
92-
programRef.Send(otpRequestMsg{})
93-
}
110+
// Manual OTP: wait from user input
94111
otp := <-otpChan
95112
if otp == "" {
96113
return "", fmt.Errorf("OTP kosong")

cli/menus.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@ func (m tuiModel) updateLogin(msg tea.Msg) (tea.Model, tea.Cmd) {
105105
}
106106

107107
func (m tuiModel) updateOTP(msg tea.Msg) (tea.Model, tea.Cmd) {
108+
if _, ok := msg.(autoOtpMsg); ok {
109+
m.screen = screenLoading
110+
m.loading = "🤖 Auto OTP diterima, memverifikasi..."
111+
return m, nil
112+
}
113+
108114
if key, ok := msg.(tea.KeyMsg); ok {
109115
if key.String() == "enter" {
110116
otp := m.input.Value()

cli/model.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ type buyMsg struct {
5555
err error
5656
}
5757
type otpRequestMsg struct{}
58+
type autoOtpMsg struct{}
5859
type paymentPollMsg struct {
5960
status *telkomsel.PaymentStatus
6061
err error

otp/listener.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func (l *Listener) Stop() error {
7373
return l.server.Shutdown(ctx)
7474
}
7575

76-
func (l *Listener) WaitForOTP(phone string, timeout time.Duration) (string, error) {
76+
func (l *Listener) WaitForOTP(ctx context.Context, phone string) (string, error) {
7777
normalized := normalizePhone(phone)
7878
ch := make(chan string, 1)
7979

@@ -83,18 +83,21 @@ func (l *Listener) WaitForOTP(phone string, timeout time.Duration) (string, erro
8383

8484
defer func() {
8585
l.mu.Lock()
86-
delete(l.waiters, normalized)
86+
// Only delete if it hasn't been overwritten by a newer request
87+
if l.waiters[normalized] == ch {
88+
delete(l.waiters, normalized)
89+
}
8790
l.mu.Unlock()
8891
}()
8992

90-
log.Printf("[OTP] Waiting for OTP for phone %s (timeout: %v)", normalized, timeout)
93+
log.Printf("[OTP] Waiting for OTP for phone %s", normalized)
9194

9295
select {
9396
case otp := <-ch:
9497
log.Printf("[OTP] Received OTP for phone %s: %s", normalized, otp)
9598
return otp, nil
96-
case <-time.After(timeout):
97-
return "", fmt.Errorf("OTP timeout after %v — no SMS received from webhook", timeout)
99+
case <-ctx.Done():
100+
return "", ctx.Err()
98101
}
99102
}
100103

otp/relogin.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,11 @@ func AutoRelogin(ctx context.Context, auth *telkomsel.Auth, listener *Listener,
4747
}
4848

4949
log.Printf("[AutoRelogin] Waiting for OTP from webhook (timeout: %v)...", OTPWaitTimeout)
50-
otp, err := listener.WaitForOTP(phone, OTPWaitTimeout)
50+
51+
waitCtx, cancel := context.WithTimeout(ctx, OTPWaitTimeout)
52+
defer cancel()
53+
54+
otp, err := listener.WaitForOTP(waitCtx, phone)
5155
if err != nil {
5256
return nil, fmt.Errorf("auto re-login wait OTP: %w", err)
5357
}

0 commit comments

Comments
 (0)