Skip to content

Commit ddc23cb

Browse files
authored
Add meme share button variants (#301)
* feat: add meme share button variants * fix: address meme share button review * fix: skip already reacted shared memes
1 parent b2a94fd commit ddc23cb

17 files changed

Lines changed: 717 additions & 74 deletions

src/storage/schemas.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class MemeData(CustomModel):
1111
type: MemeType
1212
telegram_file_id: str
1313
caption: str | None
14+
language_code: str | None = None
1415
recommended_by: str | None = None
1516
nlikes: int = 0
1617

src/tgbot/handlers/deep_link.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import re
21
from datetime import datetime
32

43
from telegram import Bot
@@ -13,8 +12,9 @@
1312
get_user_by_id,
1413
update_user,
1514
)
15+
from src.tgbot.sharing import parse_meme_share_deep_link
1616

17-
LINK_UNDER_MEME_PATTERN = r"s_\d+_\d+"
17+
LINK_UNDER_MEME_PATTERN = r"^s_\d+_\d+$"
1818

1919

2020
async def handle_invited_user(
@@ -23,11 +23,11 @@ async def handle_invited_user(
2323
invited_user_name: str,
2424
deep_link: str | None,
2525
):
26-
if not deep_link or not re.match(LINK_UNDER_MEME_PATTERN, deep_link):
26+
share_link = parse_meme_share_deep_link(deep_link)
27+
if share_link is None:
2728
return
2829

29-
_, user_id, _ = deep_link.split("_")
30-
invitor_user_id = int(user_id)
30+
invitor_user_id = share_link.sharer_user_id
3131

3232
# get invitor user
3333
invitor_user = await get_user_by_id(invitor_user_id)
@@ -81,11 +81,11 @@ async def handle_shared_meme_reward(
8181
clicked_user_id: int,
8282
deep_link: str | None,
8383
):
84-
if not deep_link or not re.match(LINK_UNDER_MEME_PATTERN, deep_link):
84+
share_link = parse_meme_share_deep_link(deep_link)
85+
if share_link is None:
8586
return
8687

87-
_, user_id, _ = deep_link.split("_")
88-
invitor_user_id = int(user_id)
88+
invitor_user_id = share_link.sharer_user_id
8989

9090
if clicked_user_id == invitor_user_id:
9191
return # don't reward clicking on your links

src/tgbot/handlers/inline.py

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
from telegram import (
2+
InlineQueryResultCachedGif,
23
InlineQueryResultCachedPhoto,
4+
InlineQueryResultCachedVideo,
35
InlineQueryResultsButton,
46
Update,
57
)
68
from telegram.constants import ParseMode
79
from telegram.ext import ContextTypes
810

9-
from src.config import settings
1011
from src.localizer import t
12+
from src.storage.constants import MemeType
1113
from src.tgbot.constants import (
1214
INLINE_SEARCH_REQUEST_DEEPLINK,
1315
)
@@ -16,8 +18,10 @@
1618
from src.tgbot.service import (
1719
create_inline_chosen_result_log,
1820
create_inline_search_log,
21+
get_shareable_meme_by_id,
1922
search_memes_for_inline_query,
2023
)
24+
from src.tgbot.sharing import get_meme_share_link
2125
from src.tgbot.user_info import get_user_info
2226

2327
MIN_SEARCH_QUERY_LENGTH = 3
@@ -27,8 +31,7 @@
2731

2832

2933
def get_inline_result_ref_link(user_id: int, meme_id: int):
30-
deep_link = f"ir_{user_id}_{meme_id}" # inline result
31-
return f"https://t.me/{settings.TELEGRAM_BOT_USERNAME}?start={deep_link}"
34+
return get_meme_share_link(user_id, meme_id)
3235

3336

3437
def get_inline_result_caption(meme, user_info):
@@ -42,6 +45,62 @@ def get_inline_result_caption(meme, user_info):
4245
return caption
4346

4447

48+
def parse_exact_meme_inline_query(query: str) -> int | None:
49+
if not query.startswith("#"):
50+
return None
51+
52+
meme_id = query[1:]
53+
if not meme_id.isdigit():
54+
return None
55+
56+
return int(meme_id)
57+
58+
59+
def build_inline_meme_result(meme: dict, user_info: dict):
60+
caption = get_inline_result_caption(meme, user_info)
61+
meme_type = MemeType(meme["type"])
62+
if meme_type == MemeType.IMAGE:
63+
return InlineQueryResultCachedPhoto(
64+
id=str(meme["id"]),
65+
photo_file_id=meme["telegram_file_id"],
66+
caption=caption,
67+
parse_mode=ParseMode.HTML,
68+
)
69+
if meme_type == MemeType.VIDEO:
70+
return InlineQueryResultCachedVideo(
71+
id=str(meme["id"]),
72+
video_file_id=meme["telegram_file_id"],
73+
title="Fast Food Memes",
74+
caption=caption,
75+
parse_mode=ParseMode.HTML,
76+
)
77+
if meme_type == MemeType.ANIMATION:
78+
return InlineQueryResultCachedGif(
79+
id=str(meme["id"]),
80+
gif_file_id=meme["telegram_file_id"],
81+
caption=caption,
82+
parse_mode=ParseMode.HTML,
83+
)
84+
return None
85+
86+
87+
async def answer_exact_meme_inline_query(update: Update, user_info: dict, meme_id: int) -> None:
88+
meme = await get_shareable_meme_by_id(meme_id)
89+
result = build_inline_meme_result(meme, user_info) if meme else None
90+
results = [result] if result else []
91+
await update.inline_query.answer(
92+
results,
93+
cache_time=INLINE_SEARCH_RESULT_CACHE_SECONDS,
94+
is_personal=True,
95+
)
96+
97+
await create_inline_search_log(
98+
user_id=update.effective_user.id,
99+
query=update.inline_query.query.strip().lower(),
100+
chat_type=update.inline_query.chat_type,
101+
)
102+
103+
45104
async def search_inline(update: Update, _: ContextTypes.DEFAULT_TYPE):
46105
try:
47106
user_info = await get_user_info(update.effective_user.id)
@@ -56,6 +115,10 @@ async def search_inline(update: Update, _: ContextTypes.DEFAULT_TYPE):
56115

57116
query = update.inline_query.query.strip().lower()
58117

118+
exact_meme_id = parse_exact_meme_inline_query(query)
119+
if exact_meme_id is not None:
120+
return await answer_exact_meme_inline_query(update, user_info, exact_meme_id)
121+
59122
if len(query) == 0:
60123
# TODO: show trending / recommended memes
61124
return await update.inline_query.answer(
@@ -92,17 +155,13 @@ async def search_inline(update: Update, _: ContextTypes.DEFAULT_TYPE):
92155
await update.inline_query.answer([], button=no_results_button)
93156
return
94157

95-
results = [
96-
InlineQueryResultCachedPhoto(
97-
id=str(meme["id"]),
98-
photo_file_id=meme["telegram_file_id"],
99-
caption=get_inline_result_caption(meme, user_info),
100-
parse_mode=ParseMode.HTML,
101-
)
102-
for meme in memes
103-
]
158+
results = [result for meme in memes if (result := build_inline_meme_result(meme, user_info))]
104159

105-
await update.inline_query.answer(results, cache_time=INLINE_SEARCH_RESULT_CACHE_SECONDS)
160+
await update.inline_query.answer(
161+
results,
162+
cache_time=INLINE_SEARCH_RESULT_CACHE_SECONDS,
163+
is_personal=True,
164+
)
106165

107166
await create_inline_search_log(
108167
user_id=update.effective_user.id,

src/tgbot/handlers/reaction.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
update_user_meme_reaction,
1313
)
1414
from src.tgbot.handlers.moderator.invite import maybe_send_moderator_invite
15+
from src.tgbot.handlers.onboarding import onboarding_flow
1516
from src.tgbot.senders.next_message import next_message
17+
from src.tgbot.sharing import MEME_REACTION_CONTEXT_ONBOARD
1618
from src.tgbot.user_info import update_user_info_counters
1719

1820

@@ -25,7 +27,9 @@ def _fire_and_forget(coro):
2527

2628
async def handle_reaction(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
2729
user_id = update.effective_user.id
28-
meme_id, reaction_id = update.callback_query.data[2:].split(":")
30+
callback_parts = update.callback_query.data[2:].split(":")
31+
meme_id, reaction_id = callback_parts[:2]
32+
reaction_context = callback_parts[2] if len(callback_parts) > 2 else None
2933
logging.info(f"🛜 reaction: user_id={user_id}, meme_id={meme_id}, reaction_id={reaction_id}")
3034

3135
# do that in sync since we'll use counters in next_message
@@ -41,6 +45,9 @@ async def handle_reaction(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
4145
)
4246

4347
if reaction_is_new:
48+
if reaction_context == MEME_REACTION_CONTEXT_ONBOARD:
49+
return await onboarding_flow(update, context.bot)
50+
4451
return await next_message(
4552
context.bot,
4653
user_id,

src/tgbot/handlers/start.py

Lines changed: 73 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import asyncio
2-
import re
32

43
from telegram import Bot, Update
54
from telegram.ext import ContextTypes
65

7-
from src.tgbot.handlers.deep_link import (
8-
LINK_UNDER_MEME_PATTERN,
9-
handle_invited_user,
10-
handle_shared_meme_reward,
11-
)
6+
from src.recommendations.meme_queue import clear_meme_queue_for_user
7+
from src.recommendations.service import user_meme_reaction_exists
8+
from src.storage.schemas import MemeData
9+
from src.tgbot.handlers.deep_link import handle_invited_user, handle_shared_meme_reward
1210
from src.tgbot.handlers.language import (
1311
handle_language_settings,
1412
init_user_languages_from_tg_user,
@@ -17,14 +15,21 @@
1715
handle_show_kitchen,
1816
)
1917
from src.tgbot.logs import log
18+
from src.tgbot.senders.meme import send_meme_to_user
2019
from src.tgbot.senders.next_message import next_message
2120
from src.tgbot.service import (
21+
add_user_language,
2222
create_or_update_user,
23+
get_shareable_meme_by_id,
2324
get_tg_user_by_id,
2425
get_user_languages,
2526
log_user_deep_link,
2627
save_tg_user,
2728
)
29+
from src.tgbot.sharing import (
30+
MEME_REACTION_CONTEXT_ONBOARD,
31+
parse_meme_share_deep_link,
32+
)
2833
from src.tgbot.user_info import update_user_info_cache
2934

3035

@@ -42,7 +47,7 @@ async def save_user_data(user_id: int, update: Update, deep_link: str | None) ->
4247
deep_link=deep_link
4348
if tg_user is None
4449
or tg_user["deep_link"] is None
45-
or not re.match(LINK_UNDER_MEME_PATTERN, tg_user["deep_link"])
50+
or parse_meme_share_deep_link(tg_user["deep_link"]) is None
4651
else None,
4752
)
4853

@@ -81,6 +86,47 @@ def _is_blocked_acquisition_channel(deep_link: str | None) -> bool:
8186
return deep_link in BLOCKED_ACQUISITION_CHANNELS or deep_link.startswith("tapps")
8287

8388

89+
async def _add_meme_language_from_share_click(user_id: int, meme_row: dict) -> None:
90+
language_code = meme_row.get("language_code")
91+
if not language_code:
92+
return
93+
94+
user_languages = await get_user_languages(user_id)
95+
if language_code in user_languages:
96+
return
97+
98+
await add_user_language(user_id, language_code)
99+
await clear_meme_queue_for_user(user_id)
100+
await update_user_info_cache(user_id)
101+
102+
103+
async def _send_shared_meme_from_deep_link(
104+
bot: Bot,
105+
user_id: int,
106+
deep_link: str | None,
107+
reaction_context: str | None = None,
108+
) -> bool:
109+
share_link = parse_meme_share_deep_link(deep_link)
110+
if share_link is None:
111+
return False
112+
113+
meme_row = await get_shareable_meme_by_id(share_link.meme_id)
114+
if meme_row is None:
115+
return False
116+
117+
if await user_meme_reaction_exists(user_id, share_link.meme_id):
118+
return False
119+
120+
await _add_meme_language_from_share_click(user_id, meme_row)
121+
await send_meme_to_user(
122+
bot,
123+
user_id,
124+
MemeData(**meme_row),
125+
reaction_context=reaction_context,
126+
)
127+
return True
128+
129+
84130
async def handle_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
85131
# Side effects every /start MUST run, regardless of deep_link branch:
86132
# 1. user_tg + user upsert (save_user_data)
@@ -133,7 +179,12 @@ async def handle_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
133179
return await handle_wrapped(update, context)
134180

135181
if created: # new user:
136-
await handle_language_settings(update, context)
182+
shared_meme_sent = await _send_shared_meme_from_deep_link(
183+
context.bot,
184+
user_id,
185+
deep_link,
186+
reaction_context=MEME_REACTION_CONTEXT_ONBOARD,
187+
)
137188

138189
await handle_invited_user(
139190
context.bot,
@@ -142,6 +193,11 @@ async def handle_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
142193
deep_link,
143194
)
144195

196+
if shared_meme_sent:
197+
return
198+
199+
await handle_language_settings(update, context)
200+
145201
# handle giveaway after onboarding so user_language rows exist
146202
if deep_link and deep_link.startswith("giveaway_"):
147203
from src.tgbot.handlers.treasury.giveaway import handle_giveaway
@@ -154,6 +210,15 @@ async def handle_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
154210

155211
return await handle_giveaway(update, context, deep_link)
156212

213+
shared_meme_sent = await _send_shared_meme_from_deep_link(
214+
context.bot,
215+
user_id,
216+
deep_link,
217+
)
218+
if shared_meme_sent:
219+
await handle_shared_meme_reward(context.bot, user_id, deep_link)
220+
return
221+
157222
await next_message(
158223
context.bot,
159224
user_id,

0 commit comments

Comments
 (0)