Skip to content

Commit 88c6337

Browse files
authored
feat: add channel audience giveaway (#295)
1 parent f76c0e5 commit 88c6337

3 files changed

Lines changed: 101 additions & 7 deletions

File tree

src/tgbot/handlers/treasury/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class TrxType(str, Enum):
3030
BOT_REPLY_PAYMENT = "bot_reply_payment"
3131

3232
GIVEAWAY = "giveaway"
33+
CHANNEL_AUDIENCE_GIVEAWAY = "channel_audience_giveaway"
3334

3435

3536
TREASURY_USER_ID = 1123681771
@@ -52,6 +53,7 @@ class TrxType(str, Enum):
5253
TrxType.ACTIVE_IN_CHAT: 5,
5354
TrxType.BOT_REPLY_PAYMENT: -1,
5455
TrxType.GIVEAWAY: 77,
56+
TrxType.CHANNEL_AUDIENCE_GIVEAWAY: 10,
5557
}
5658

5759
# TODO: localize
@@ -73,4 +75,5 @@ class TrxType(str, Enum):
7375
TrxType.ACTIVE_IN_CHAT: "being active in chat",
7476
TrxType.BOT_REPLY_PAYMENT: "chatting_with_bot",
7577
TrxType.GIVEAWAY: "channel giveaway",
78+
TrxType.CHANNEL_AUDIENCE_GIVEAWAY: "channel audience giveaway",
7679
}

src/tgbot/handlers/treasury/giveaway.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
Only whitelisted campaign IDs are accepted. Users craft arbitrary
88
giveaway_* links otherwise and mint unlimited burgers.
99
10-
Each giveaway campaign is identified by its deep link string.
10+
Each giveaway campaign is identified by its deep link string and transaction type.
1111
A user can only claim each giveaway once (enforced by pay_if_not_paid).
1212
"""
1313

@@ -19,10 +19,11 @@
1919
from src.tgbot.handlers.treasury.payments import pay_if_not_paid
2020
from src.tgbot.senders.next_message import next_message
2121

22-
# Whitelist of active giveaway campaign IDs.
22+
# Whitelist of active giveaway campaign IDs mapped to their readout transaction type.
2323
# Add new campaigns here before posting the deep link to the channel.
24-
ACTIVE_GIVEAWAY_CAMPAIGNS = {
25-
"giveaway_77", # launch giveaway: 77 burgers to first clickers
24+
ACTIVE_GIVEAWAY_CAMPAIGNS: dict[str, TrxType] = {
25+
"giveaway_77": TrxType.GIVEAWAY, # launch giveaway: 77 burgers to first clickers
26+
"giveaway_channel_audience_2026_05_25": TrxType.CHANNEL_AUDIENCE_GIVEAWAY,
2627
}
2728

2829

@@ -33,7 +34,8 @@ async def handle_giveaway(
3334
) -> None:
3435
user_id = update.effective_user.id
3536

36-
if deep_link not in ACTIVE_GIVEAWAY_CAMPAIGNS:
37+
trx_type = ACTIVE_GIVEAWAY_CAMPAIGNS.get(deep_link)
38+
if trx_type is None:
3739
# Unknown campaign — silently continue to meme feed
3840
await next_message(
3941
context.bot,
@@ -43,10 +45,10 @@ async def handle_giveaway(
4345
)
4446
return
4547

46-
amount = PAYOUTS[TrxType.GIVEAWAY]
48+
amount = PAYOUTS[trx_type]
4749

4850
# deep_link is the external_id — ensures one claim per campaign per user
49-
balance = await pay_if_not_paid(user_id, TrxType.GIVEAWAY, deep_link)
51+
balance = await pay_if_not_paid(user_id, trx_type, deep_link)
5052

5153
if balance is not None:
5254
await context.bot.send_message(

tests/tgbot/test_giveaway.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from types import SimpleNamespace
2+
from unittest.mock import AsyncMock, patch
3+
4+
import pytest
5+
6+
from src.tgbot.handlers.treasury.constants import TrxType
7+
from src.tgbot.handlers.treasury.giveaway import handle_giveaway
8+
9+
10+
def _make_update(user_id: int = 90203):
11+
return SimpleNamespace(effective_user=SimpleNamespace(id=user_id))
12+
13+
14+
def _make_context():
15+
return SimpleNamespace(bot=AsyncMock())
16+
17+
18+
@pytest.mark.asyncio
19+
async def test_channel_audience_giveaway_uses_tagged_ten_burger_reward():
20+
update = _make_update()
21+
context = _make_context()
22+
deep_link = "giveaway_channel_audience_2026_05_25"
23+
24+
with (
25+
patch(
26+
"src.tgbot.handlers.treasury.giveaway.pay_if_not_paid",
27+
new_callable=AsyncMock,
28+
return_value=10,
29+
) as pay,
30+
patch(
31+
"src.tgbot.handlers.treasury.giveaway.next_message",
32+
new_callable=AsyncMock,
33+
) as next_message,
34+
):
35+
await handle_giveaway(update, context, deep_link)
36+
37+
pay.assert_awaited_once_with(
38+
update.effective_user.id,
39+
TrxType.CHANNEL_AUDIENCE_GIVEAWAY,
40+
deep_link,
41+
)
42+
context.bot.send_message.assert_awaited_once()
43+
assert "+<b>10</b>" in context.bot.send_message.await_args.kwargs["text"]
44+
next_message.assert_awaited_once()
45+
46+
47+
@pytest.mark.asyncio
48+
async def test_legacy_giveaway_keeps_legacy_payout_type():
49+
update = _make_update()
50+
context = _make_context()
51+
52+
with (
53+
patch(
54+
"src.tgbot.handlers.treasury.giveaway.pay_if_not_paid",
55+
new_callable=AsyncMock,
56+
return_value=77,
57+
) as pay,
58+
patch("src.tgbot.handlers.treasury.giveaway.next_message", new_callable=AsyncMock),
59+
):
60+
await handle_giveaway(update, context, "giveaway_77")
61+
62+
pay.assert_awaited_once_with(
63+
update.effective_user.id,
64+
TrxType.GIVEAWAY,
65+
"giveaway_77",
66+
)
67+
assert "+<b>77</b>" in context.bot.send_message.await_args.kwargs["text"]
68+
69+
70+
@pytest.mark.asyncio
71+
async def test_unknown_giveaway_does_not_pay():
72+
update = _make_update()
73+
context = _make_context()
74+
75+
with (
76+
patch(
77+
"src.tgbot.handlers.treasury.giveaway.pay_if_not_paid",
78+
new_callable=AsyncMock,
79+
) as pay,
80+
patch(
81+
"src.tgbot.handlers.treasury.giveaway.next_message",
82+
new_callable=AsyncMock,
83+
) as next_message,
84+
):
85+
await handle_giveaway(update, context, "giveaway_fake")
86+
87+
pay.assert_not_awaited()
88+
context.bot.send_message.assert_not_awaited()
89+
next_message.assert_awaited_once()

0 commit comments

Comments
 (0)