Skip to content

Commit 2a632a1

Browse files
committed
feat(autobuy): add configurable quota threshold for auto-buy
Introduce `AutoBuyThreshold` to session data. Implement UI in Telegram bot and CLI to set and display the threshold. Update auto-buy monitor logic to trigger purchase when internet quota falls below the threshold. Add `ParseQuotaToMB` utility for quota string conversion. Extend MCP `start_auto_buy` tool with `threshold_mb` parameter. This allows users to proactively manage internet quota, ensuring a new package is purchased before complete depletion, improving connectivity.
1 parent 6017f24 commit 2a632a1

9 files changed

Lines changed: 254 additions & 32 deletions

File tree

bot/autobuy.go

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/PaulSonOfLars/gotgbot/v2"
1212

1313
"telkomsel-bot/telkomsel"
14+
"telkomsel-bot/util"
1415
)
1516

1617
func (h *Handler) cbShowAutoMonitor(b *gotgbot.Bot, chatID, msgID, userID int64) {
@@ -21,9 +22,13 @@ func (h *Handler) cbShowAutoMonitor(b *gotgbot.Bot, chatID, msgID, userID int64)
2122

2223
if session.AutoBuyActive {
2324
kb := kbAutoRunning()
25+
threshStr := fmt.Sprintf("< %d MB", session.AutoBuyThreshold)
26+
if session.AutoBuyThreshold == 0 {
27+
threshStr = "Habis (0 MB)"
28+
}
2429
h.editMsg(b, chatID, msgID, fmt.Sprintf(
25-
"🤖 *Auto-Buy Sedang Aktif!*\n\n⏱ Interval: *%d menit*\n📦 Paket: *%s*\n💳 Bayar: *Pulsa*\n\nMonitor berjalan di background...",
26-
session.AutoBuyInterval, session.AutoBuyPackage,
30+
"🤖 *Auto-Buy Sedang Aktif!*\n\n⏱ Interval: *%d menit*\n📉 Batas Kuota: *%s*\n📦 Paket: *%s*\n💳 Bayar: *Pulsa*\n\nMonitor berjalan di background...",
31+
session.AutoBuyInterval, threshStr, session.AutoBuyPackage,
2732
), &kb)
2833
return
2934
}
@@ -41,8 +46,26 @@ func (h *Handler) cbSetAutoInterval(b *gotgbot.Bot, chatID, msgID, userID int64,
4146
session.AutoBuyInterval = minutes
4247
h.sessions.Set(userID, session)
4348

49+
kb := kbAutoThreshold()
50+
h.editMsg(b, chatID, msgID, fmt.Sprintf("✅ Interval: *%d menit*\n\n📉 Pilih batas minimum kuota untuk auto-buy:", minutes), &kb)
51+
}
52+
53+
func (h *Handler) cbSetAutoThreshold(b *gotgbot.Bot, chatID, msgID, userID int64, threshold int) {
54+
session, ok := h.checkSession(b, chatID, msgID, userID)
55+
if !ok {
56+
return
57+
}
58+
59+
session.AutoBuyThreshold = threshold
60+
h.sessions.Set(userID, session)
61+
62+
threshStr := fmt.Sprintf("< %d MB", threshold)
63+
if threshold == 0 {
64+
threshStr = "Habis (0 MB)"
65+
}
66+
4467
kb := kbAutoPackage()
45-
h.editMsg(b, chatID, msgID, fmt.Sprintf("✅ Interval: *%d menit*\n\n📦 Pilih paket untuk auto-buy:", minutes), &kb)
68+
h.editMsg(b, chatID, msgID, fmt.Sprintf("✅ Interval: *%d menit*\n📉 Batas Kuota: *%s*\n\n📦 Pilih paket untuk auto-buy:", session.AutoBuyInterval, threshStr), &kb)
4669
}
4770

4871
func (h *Handler) cbSetAutoPackage(b *gotgbot.Bot, chatID, msgID, userID int64, pkg string) {
@@ -54,8 +77,13 @@ func (h *Handler) cbSetAutoPackage(b *gotgbot.Bot, chatID, msgID, userID int64,
5477
session.AutoBuyPackage = pkg
5578
h.sessions.Set(userID, session)
5679

80+
threshStr := fmt.Sprintf("< %d MB", session.AutoBuyThreshold)
81+
if session.AutoBuyThreshold == 0 {
82+
threshStr = "Habis (0 MB)"
83+
}
84+
5785
kb := kbAutoPay()
58-
h.editMsg(b, chatID, msgID, fmt.Sprintf("✅ Interval: *%d menit*\n📦 Paket: *%s*\n\n💳 Pembayaran via:", session.AutoBuyInterval, pkg), &kb)
86+
h.editMsg(b, chatID, msgID, fmt.Sprintf("✅ Interval: *%d menit*\n📉 Batas Kuota: *%s*\n📦 Paket: *%s*\n\n💳 Pembayaran via:", session.AutoBuyInterval, threshStr, pkg), &kb)
5987
}
6088

6189
func (h *Handler) cbStartAutoBuy(b *gotgbot.Bot, chatID, msgID, userID int64) {
@@ -81,10 +109,15 @@ func (h *Handler) cbStartAutoBuy(b *gotgbot.Bot, chatID, msgID, userID int64) {
81109
h.autoStops[userID] = cancel
82110
h.autoStopsMu.Unlock()
83111

112+
threshStr := fmt.Sprintf("< %d MB", session.AutoBuyThreshold)
113+
if session.AutoBuyThreshold == 0 {
114+
threshStr = "Habis (0 MB)"
115+
}
116+
84117
kb := kbAutoRunning()
85118
h.editMsg(b, chatID, msgID, fmt.Sprintf(
86-
"🤖 *Auto-Buy Aktif!*\n\n⏱ Interval: *%d menit*\n📦 Paket: *%s*\n💳 Bayar: *Pulsa*\n\nMonitor berjalan di background...",
87-
session.AutoBuyInterval, session.AutoBuyPackage,
119+
"🤖 *Auto-Buy Aktif!*\n\n⏱ Interval: *%d menit*\n📉 Batas Kuota: *%s*\n📦 Paket: *%s*\n💳 Bayar: *Pulsa*\n\nMonitor berjalan di background...",
120+
session.AutoBuyInterval, threshStr, session.AutoBuyPackage,
88121
), &kb)
89122

90123
go h.runAutoBuyMonitor(autCtx, b, chatID, userID)
@@ -157,10 +190,26 @@ func (h *Handler) runAutoBuyMonitor(ctx context.Context, b *gotgbot.Bot, chatID,
157190
}
158191

159192
needsBuy := false
160-
for _, group := range quota.Groups {
161-
if strings.EqualFold(group.Class, "Internet") && len(group.Items) == 0 {
193+
if session.AutoBuyThreshold == 0 {
194+
for _, group := range quota.Groups {
195+
if strings.EqualFold(group.Class, "Internet") && len(group.Items) == 0 {
196+
needsBuy = true
197+
break
198+
}
199+
}
200+
} else {
201+
var totalInternetQuota float64
202+
hasInternetGroup := false
203+
for _, group := range quota.Groups {
204+
if strings.EqualFold(group.Class, "Internet") {
205+
hasInternetGroup = true
206+
for _, item := range group.Items {
207+
totalInternetQuota += util.ParseQuotaToMB(item.Remaining)
208+
}
209+
}
210+
}
211+
if !hasInternetGroup || totalInternetQuota <= float64(session.AutoBuyThreshold) {
162212
needsBuy = true
163-
break
164213
}
165214
}
166215

bot/handler.go

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,23 @@ func (h *Handler) handleCallback(b *gotgbot.Bot, ctx *ext.Context) error {
174174
h.sessions.Set(userID, session)
175175
}
176176

177+
case data == "auto_thresh_0":
178+
h.cbSetAutoThreshold(b, chatID, msgID, userID, 0)
179+
180+
case data == "auto_thresh_100":
181+
h.cbSetAutoThreshold(b, chatID, msgID, userID, 100)
182+
183+
case data == "auto_thresh_200":
184+
h.cbSetAutoThreshold(b, chatID, msgID, userID, 200)
185+
186+
case data == "auto_thresh_custom":
187+
h.editMsg(b, chatID, msgID, "⌨️ Kirim batas minimum kuota dalam MB.\n\nContoh: `500`", nil)
188+
session := h.sessions.Get(userID)
189+
if session != nil {
190+
session.State = model.StateAwaitingAutoThreshold
191+
h.sessions.Set(userID, session)
192+
}
193+
177194
case data == "auto_pkg_ilmupedia":
178195
h.cbSetAutoPackage(b, chatID, msgID, userID, "ilmupedia")
179196

@@ -243,8 +260,31 @@ func (h *Handler) handleMessage(b *gotgbot.Bot, ctx *ext.Context) error {
243260
session.AutoBuyInterval = minutes
244261
h.sessions.Set(userID, session)
245262

263+
kb := kbAutoThreshold()
264+
_, err := ctx.EffectiveMessage.Reply(b, fmt.Sprintf("✅ Interval: *%d menit*\n\n📉 Pilih batas minimum kuota untuk auto-buy:", minutes), &gotgbot.SendMessageOpts{
265+
ParseMode: "Markdown",
266+
ReplyMarkup: kb,
267+
})
268+
return err
269+
}
270+
271+
if session != nil && session.State == model.StateAwaitingAutoThreshold {
272+
session.State = model.StateIdle
273+
mb, parseErr := strconv.Atoi(text)
274+
if parseErr != nil || mb < 0 {
275+
_, err := ctx.EffectiveMessage.Reply(b, "❌ Masukkan angka yang valid (dalam MB).\n\nContoh: `500`", &gotgbot.SendMessageOpts{ParseMode: "Markdown"})
276+
return err
277+
}
278+
session.AutoBuyThreshold = mb
279+
h.sessions.Set(userID, session)
280+
281+
threshStr := fmt.Sprintf("< %d MB", mb)
282+
if mb == 0 {
283+
threshStr = "Habis (0 MB)"
284+
}
285+
246286
kb := kbAutoPackage()
247-
_, err := ctx.EffectiveMessage.Reply(b, fmt.Sprintf("✅ Interval: *%d menit*\n\n📦 Pilih paket untuk auto-buy:", minutes), &gotgbot.SendMessageOpts{
287+
_, err := ctx.EffectiveMessage.Reply(b, fmt.Sprintf("✅ Interval: *%d menit*\n📉 Batas Kuota: *%s*\n\n📦 Pilih paket untuk auto-buy:", session.AutoBuyInterval, threshStr), &gotgbot.SendMessageOpts{
248288
ParseMode: "Markdown",
249289
ReplyMarkup: kb,
250290
})
@@ -256,8 +296,13 @@ func (h *Handler) handleMessage(b *gotgbot.Bot, ctx *ext.Context) error {
256296
session.AutoBuyPackage = text
257297
h.sessions.Set(userID, session)
258298

299+
threshStr := fmt.Sprintf("< %d MB", session.AutoBuyThreshold)
300+
if session.AutoBuyThreshold == 0 {
301+
threshStr = "Habis (0 MB)"
302+
}
303+
259304
kb := kbAutoPay()
260-
_, err := ctx.EffectiveMessage.Reply(b, fmt.Sprintf("✅ Interval: *%d menit*\n📦 Paket: *%s*\n\n💳 Pembayaran via:", session.AutoBuyInterval, text), &gotgbot.SendMessageOpts{
305+
_, err := ctx.EffectiveMessage.Reply(b, fmt.Sprintf("✅ Interval: *%d menit*\n📉 Batas Kuota: *%s*\n📦 Paket: *%s*\n\n💳 Pembayaran via:", session.AutoBuyInterval, threshStr, text), &gotgbot.SendMessageOpts{
261306
ParseMode: "Markdown",
262307
ReplyMarkup: kb,
263308
})

bot/keyboards.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,22 @@ func kbAutoMonitor() gotgbot.InlineKeyboardMarkup {
101101
}
102102
}
103103

104+
func kbAutoThreshold() gotgbot.InlineKeyboardMarkup {
105+
return gotgbot.InlineKeyboardMarkup{
106+
InlineKeyboard: [][]gotgbot.InlineKeyboardButton{
107+
{
108+
{Text: "Habis (0 MB)", CallbackData: "auto_thresh_0"},
109+
{Text: "📉 < 100 MB", CallbackData: "auto_thresh_100"},
110+
},
111+
{
112+
{Text: "📉 < 200 MB", CallbackData: "auto_thresh_200"},
113+
{Text: "⌨️ Custom", CallbackData: "auto_thresh_custom"},
114+
},
115+
{{Text: "🔙 Kembali", CallbackData: "auto_buy"}},
116+
},
117+
}
118+
}
119+
104120
func kbAutoPackage() gotgbot.InlineKeyboardMarkup {
105121
return gotgbot.InlineKeyboardMarkup{
106122
InlineKeyboard: [][]gotgbot.InlineKeyboardButton{

cli/menus.go

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ func (m tuiModel) updateScheduleMenu(msg tea.Msg) (tea.Model, tea.Cmd) {
257257
if m.loggedInUser != nil && m.loggedInUser.AutoBuyActive {
258258
status = "Aktif"
259259
}
260-
items := []string{"Status: " + status, "Ubah Jadwal", "Ubah Offer ID", "Ubah Payment", "Kembali"}
260+
items := []string{"Status: " + status, "Ubah Jadwal", "Ubah Threshold", "Ubah Offer ID", "Ubah Payment", "Kembali"}
261261
if key, ok := msg.(tea.KeyMsg); ok {
262262
switch key.String() {
263263
case "up", "k":
@@ -286,14 +286,19 @@ func (m tuiModel) updateScheduleMenu(msg tea.Msg) (tea.Model, tea.Cmd) {
286286
m.input.Placeholder = "Misal: 10, 30, 60 (menit)"
287287
m.input.Focus()
288288
case 2:
289+
m.screen = screenScheduleThreshold
290+
m.input.SetValue("")
291+
m.input.Placeholder = "Misal: 0 (Habis), 100, 200 (MB)"
292+
m.input.Focus()
293+
case 3:
289294
m.screen = screenScheduleOfferID
290295
m.input.SetValue("")
291296
m.input.Placeholder = "Offer ID dari web..."
292297
m.input.Focus()
293-
case 3:
298+
case 4:
294299
m.screen = screenSchedulePayment
295300
m.cursor = 0
296-
case 4:
301+
case 5:
297302
m.screen = screenMenu
298303
m.cursor = 0
299304
}
@@ -302,6 +307,27 @@ func (m tuiModel) updateScheduleMenu(msg tea.Msg) (tea.Model, tea.Cmd) {
302307
return m, nil
303308
}
304309

310+
func (m tuiModel) updateScheduleThreshold(msg tea.Msg) (tea.Model, tea.Cmd) {
311+
if key, ok := msg.(tea.KeyMsg); ok {
312+
if key.String() == "enter" {
313+
m.schThreshold = m.input.Value()
314+
if m.loggedInUser != nil {
315+
var i int
316+
fmt.Sscanf(m.schThreshold, "%d", &i)
317+
m.loggedInUser.AutoBuyThreshold = i
318+
m.sessions.Set(m.loggedInID, m.loggedInUser)
319+
m.message = "✓ Threshold diupdate"
320+
}
321+
m.screen = screenScheduleMenu
322+
m.cursor = 0
323+
return m, nil
324+
}
325+
}
326+
var cmd tea.Cmd
327+
m.input, cmd = m.input.Update(msg)
328+
return m, cmd
329+
}
330+
305331
func (m tuiModel) updateScheduleInterval(msg tea.Msg) (tea.Model, tea.Cmd) {
306332
if key, ok := msg.(tea.KeyMsg); ok {
307333
if key.String() == "enter" {
@@ -490,6 +516,10 @@ func (m tuiModel) viewScheduleMenu() string {
490516
if m.loggedInUser.AutoBuyInterval == 0 {
491517
interval = "(belum diset)"
492518
}
519+
thresholdStr := fmt.Sprintf("< %d MB", m.loggedInUser.AutoBuyThreshold)
520+
if m.loggedInUser.AutoBuyThreshold == 0 {
521+
thresholdStr = "0 MB (Habis)"
522+
}
493523
offer := m.loggedInUser.AutoBuyPackage
494524
if offer == "" {
495525
offer = "(belum diset)"
@@ -498,7 +528,7 @@ func (m tuiModel) viewScheduleMenu() string {
498528
if pay == "" {
499529
pay = "(belum diset)"
500530
}
501-
s := fmt.Sprintf("Interval : %s\nOffer ID : %s\nPayment : %s", interval, offer, pay)
531+
s := fmt.Sprintf("Interval : %s\nThreshold: %s\nOffer ID : %s\nPayment : %s", interval, thresholdStr, offer, pay)
502532
b.WriteString(boxStyle.Render(s))
503533
b.WriteString("\n\n")
504534
}
@@ -507,7 +537,7 @@ func (m tuiModel) viewScheduleMenu() string {
507537
if m.loggedInUser != nil && m.loggedInUser.AutoBuyActive {
508538
status = "Aktif"
509539
}
510-
items := []string{"Status: " + status, "Ubah Jadwal", "Ubah Offer ID", "Ubah Payment", "🔙 Kembali"}
540+
items := []string{"Status: " + status, "Ubah Jadwal", "Ubah Threshold", "Ubah Offer ID", "Ubah Payment", "🔙 Kembali"}
511541
for i, item := range items {
512542
if i == m.cursor {
513543
b.WriteString(selectedStyle.Render("▸ " + item))

cli/model.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const (
2727
screenPaymentPoll
2828
screenScheduleMenu
2929
screenScheduleInterval
30+
screenScheduleThreshold
3031
screenScheduleOfferID
3132
screenSchedulePayment
3233
screenError
@@ -96,6 +97,7 @@ type tuiModel struct {
9697
qrString string
9798

9899
schInterval string
100+
schThreshold string
99101
schOfferID string
100102
schPayment string
101103
}

cli/update.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ func (m tuiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
6767
existing := m.sessions.Get(m.loggedInID)
6868
if existing != nil {
6969
msg.session.AutoBuyInterval = existing.AutoBuyInterval
70+
msg.session.AutoBuyThreshold = existing.AutoBuyThreshold
7071
msg.session.AutoBuyPackage = existing.AutoBuyPackage
7172
msg.session.AutoBuyPayment = existing.AutoBuyPayment
7273
msg.session.AutoBuyActive = existing.AutoBuyActive
@@ -177,6 +178,8 @@ func (m tuiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
177178
return m.updateScheduleMenu(msg)
178179
case screenScheduleInterval:
179180
return m.updateScheduleInterval(msg)
181+
case screenScheduleThreshold:
182+
return m.updateScheduleThreshold(msg)
180183
case screenScheduleOfferID:
181184
return m.updateScheduleOfferID(msg)
182185
case screenSchedulePayment:

0 commit comments

Comments
 (0)