Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,6 @@ class Config(BaseSettings):
TELEGRAM_API_HASH: str | None = None
TELEGRAM_SESSION_STRING: str | None = None

# @model_validator(mode="after")
# def validate_sentry_non_local(self) -> "Config":
# if self.ENVIRONMENT.is_deployed and not self.SENTRY_DSN:
# raise ValueError("Sentry is not set")

# return self


settings = Config()

Expand Down
10 changes: 3 additions & 7 deletions src/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -697,13 +697,9 @@ def _is_transient_connection_error(exc: BaseException) -> bool:
return _is_stale_connection_error(exc) or _is_concurrent_use_error(exc)


# Shared retry budget for stale / concurrent-use connection errors.
# Three retries (25 ms + 50 ms + 100 ms backoff) give the event loop enough
# time to dispose dirty connections under burst webhook concurrency.
# Under traffic spikes the LIFO pool can hand out several consecutively
# dirty connections; 2 retries proved insufficient (9 Sentry events on
# 2026-04-12). Bumping to 3 retries with wider backoff makes exhaustion
# astronomically unlikely while adding only ~175 ms worst-case latency.
# The LIFO pool can return consecutive stale/concurrent-use connections under load.
# Two retries proved insufficient (9 Sentry events on 2026-04-12); three retries
# allow 25 + 50 + 100 ms of backoff before propagating the failure.
_TRANSIENT_MAX_RETRIES = 3


Expand Down
11 changes: 2 additions & 9 deletions src/flows/crossposting/meme.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,6 @@ def _get_ru_caption_for_crossposting_meme(meme: MemeData, channel: Channel) -> s
cta = random.choice(CTAS)
ref_link = "https://t.me/ffmemesbot?start=sc_{}_{}".format(meme.id, channel.value)

# emoji = get_random_emoji()
# referral_html = f"""{emoji} <i><a href="{ref_link}">Fast Food Memes</a></i>"""
# caption = escape(meme.caption, quote=False) if meme.caption else ""
# text = caption + "\n\n" + referral_html

text = cta + ": " + f"""<a href="{ref_link}">@ffmemesbot</a>"""

return text
Expand Down Expand Up @@ -220,8 +215,7 @@ async def post_meme_to_tgchannelen():
next_meme = MemeData(**meme_data)
logger.info(f"Next meme for TG Channel EN: {next_meme.id}")

# Persist the ranker decision for retro analysis. Failure must NOT propagate
# — Prefect retry would republish the meme.
# Ranker logging is best effort so analytics cannot block posting.
if decision:
try:
await log_ranker_decision(**decision)
Expand Down Expand Up @@ -280,8 +274,7 @@ async def post_meme_to_tgchannelru():
next_meme = MemeData(**meme_data)
logger.info(f"Next meme for TG Channel RU: {next_meme.id}")

# Persist the ranker decision for retro analysis. Failure must NOT propagate
# — Prefect retry would republish the meme.
# Ranker logging is best effort so analytics cannot block posting.
if decision:
try:
await log_ranker_decision(**decision)
Expand Down
2 changes: 0 additions & 2 deletions src/flows/storage/describe_memes.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ async def describe_single_meme(
file_id = meme_row["telegram_file_id"]
existing_ocr = meme_row["ocr_result"] or {}

# Download image from Telegram
try:
image_bytes = await download_meme_content_from_tg(file_id)
except Exception as e:
Expand All @@ -80,7 +79,6 @@ async def describe_single_meme(

image_b64 = base64.b64encode(image_bytes).decode("utf-8")

# Call vision model
try:
result = await call_openrouter_vision(image_b64, log, deadline=deadline, keys=keys)
except Exception as e:
Expand Down
1 change: 0 additions & 1 deletion src/localizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

import yaml

# not sure where to put this const
DEFAULT_LANG = "en"

ALMOST_CIS_LANGUAGES = [
Expand Down
9 changes: 2 additions & 7 deletions src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,16 @@

@asynccontextmanager
async def lifespan(_application: FastAPI) -> AsyncGenerator:
# Startup
tgbot_app.application = tgbot_app.setup_application(settings.ENVIRONMENT.is_deployed)
await tgbot_app.application.initialize()

# flush all redis keys on startup for debug
# await redis.redis_client.flushall()

# if is_webhook: # all gunicorn workers will call this and hit rate limit
# await bot.setup_webhook(bot.application)
# Do not register the webhook here: every Gunicorn worker runs lifespan,
# so startup registration would repeatedly hit Telegram's rate limit.

yield

if settings.ENVIRONMENT.is_testing:
return
# Shutdown
await redis.pool.disconnect()


Expand Down
17 changes: 0 additions & 17 deletions src/storage/etl.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,6 @@ async def insert_parsed_posts_from_telegram(
*,
discover_candidates: bool = True,
) -> None:
# 1. find which memes are already in the database
# 2. update existing memes
# 3. insert new memes

result = await fetch_all(
select(meme_raw_telegram.c.post_id)
.where(meme_raw_telegram.c.meme_source_id == meme_source_id)
Expand Down Expand Up @@ -202,10 +198,6 @@ async def etl_memes_from_raw_telegram_posts(
*,
fresh_only: bool = True,
) -> None:
# get transformed posts
# find ones that are already in the database
# create rows and update rows
#
# Engagement filters before raw posts enter the meme table:
# 1. From each source's latest 10 posts, only promote the top 5 by views.
# Raw rows are still stored, so this threshold can be replayed later.
Expand Down Expand Up @@ -325,15 +317,6 @@ async def etl_memes_from_raw_telegram_posts(
},
)

# find rows which already exist in db by two index columns:
# meme_source_id and raw_meme_id
# so we can update existing rows and create new ones

# join two tables meme_raw_telegram and meme to get
# the meme_source_id and raw_meme_id pairs
# which are not present in memes table.
# That will indicate that we need to create new rows

memes_not_in_memes_table = await fetch_all(
text(
"""
Expand Down
4 changes: 1 addition & 3 deletions src/storage/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ async def _image_hash(meme_id: int, meme_type: MemeType, content: bytes, media:
async def _upload_meme_content_to_tg(
meme_id: int,
meme_type: MemeType,
content: bytes, # ??
content: bytes,
) -> dict[str, Any] | None:
file_id = None
media = None
Expand Down Expand Up @@ -166,8 +166,6 @@ async def upload_meme_content_to_tg(
*,
observability_context: dict[str, Any] | None = None,
) -> dict[str, Any] | None:
# just a wrapper to handle retries

meme_result = None
max_attempts = 3
content_size = len(content)
Expand Down
17 changes: 1 addition & 16 deletions src/storage/watermark.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,8 @@ def draw_text_with_outline(
outline_colour: tuple,
) -> None:
x, y = position
# Draw outline
for adj in range(-1, 2):
for ops in range(-1, 2):
# Avoid the center pixel
if adj != 0 or ops != 0:
draw.text((x + adj, y + ops), text, font=font, fill=outline_colour)
draw.text(position, text, font=font, fill=text_colour)
Expand All @@ -42,39 +40,27 @@ def find_least_detailed_corner(img: Image, text_bbox: tuple, margin: int) -> tup
- A tuple (x, y) representing the top-left coordinate
of the selected corner for text placement.
"""
# Convert the original image to grayscale for intensity analysis
gray_img = img.convert("L")
img_w, img_h = img.size
# Calculate the width and height of the text bounding box
text_width = text_bbox[2] - text_bbox[0]
text_height = text_bbox[3] - text_bbox[1]
# Define dictionary with the coordinates of each corner with some margin applied
corners = {
"top_left": (margin, margin),
"top_right": (img_w - text_width - margin, margin),
"bottom_left": (margin, img_h - text_height - margin),
"bottom_right": (img_w - text_width - margin, img_h - text_height - margin),
}
# Initialize with infinity for comparison
least_detailed_score = float("inf")
# To store the x, y coordinates for watermark
selected_corner = None
selected_wm_colour = None
# Iterate through each corner to find the one with the least detail background
for _, (x, y) in corners.items():
# Crop the image around the current corner
# according to the text bounding box size
sector = gray_img.crop((x, y, x + text_width, y + text_height))
# Calculate mean intensity of the cropped area
mean_intensity = ImageStat.Stat(sector).mean[0]
# Determine how close the mean intensity is to either black (0) or white (255)
# indicates how uniform the background of the selected sector is
# Use mean brightness near black or white as a cheap proxy for a quiet background.
distance_to_extreme = min(abs(0 - mean_intensity), abs(255 - mean_intensity))
# Update if this corner has a lower score (closer to black or white)
if distance_to_extreme < least_detailed_score:
least_detailed_score = distance_to_extreme
selected_corner = (x, y)
# Selecting watermark colour based on mean intensity
selected_wm_colour = (0, 0, 0) if mean_intensity > 178 else (255, 255, 255)

return selected_corner, selected_wm_colour
Expand Down Expand Up @@ -110,7 +96,6 @@ def draw_corner_watermark(
text_position, text_colour = find_least_detailed_corner(base, text_bbox, margin)
outline_colour = (0, 0, 0, 255) if text_colour == (255, 255, 255) else (255, 255, 255, 255)
draw_text_with_outline(d, text_position, text, font, text_colour, outline_colour)
# text opacity
txt.putalpha(txt.getchannel("A").point(lambda x: x * text_opacity))
return Image.alpha_composite(base, txt).convert("RGB")

Expand Down
4 changes: 0 additions & 4 deletions src/tgbot/handlers/admin/forward_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,6 @@ async def handle_forwarded_from_tgchannelru(

await update.message.reply_text(f"Forwarding post to {len(users)}")
await log(f"Forwarding post to {len(users)}", context.bot)

# return await update.message.reply_text(
# f"Can't parse caption: {escape(str(update.message.to_dict()))}"
# )
else:
url = urls[0]
meme_id_str = url.replace("https://t.me/ffmemesbot?start=sc_", "").split("_")[0]
Expand Down
5 changes: 0 additions & 5 deletions src/tgbot/handlers/admin/user_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,5 @@ async def delete_user_data_confirmation_page(

async def delete_user_data(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Deletes all user data we have for testing purposes"""
# user = await get_user_info(update.effective_user.id)
# if user["type"] != UserType.ADMIN:
# return

# TODO: "are you sure" button + callback
await delete_user(update.effective_user.id)
await update.effective_user.send_message("Ciao 👋\n\n --> press /start to try again 👾")
2 changes: 0 additions & 2 deletions src/tgbot/handlers/deep_link.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ async def handle_invited_user(

invitor_user_id = share_link.sharer_user_id

# get invitor user
invitor_user = await get_user_by_id(invitor_user_id)
if not invitor_user:
return # Invitor doesn't exist
Expand All @@ -45,7 +44,6 @@ async def handle_invited_user(
was already invited by #{invited_user["inviter_id"]}"""
)

# set inviter id
await update_user(invited_user["id"], inviter_id=invitor_user_id)

# Don't reward if the inviter cannot receive the bot notification.
Expand Down
5 changes: 0 additions & 5 deletions src/tgbot/handlers/upload/upload_meme.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,6 @@ async def handle_message_with_meme(update: Update, context: ContextTypes.DEFAULT
raise Exception(f"not supported format: {update.message.to_json()}")


# callback: RULES_ACCEPTED_CALLBACK_DATA
async def handle_rules_accepted_callback(
update: Update,
_: ContextTypes.DEFAULT_TYPE,
Expand All @@ -292,7 +291,6 @@ async def handle_rules_accepted_callback(
)


# callback_data = LANG_SETTINGS_END_CALLBACK_DATA
async def handle_meme_upload_lang_other(
update: Update,
_: ContextTypes.DEFAULT_TYPE,
Expand All @@ -304,7 +302,6 @@ async def handle_meme_upload_lang_other(
)


# callback: RULES_ACCEPTED_CALLBACK_DATA
async def handle_meme_upload_lang_selected(
update: Update,
context: ContextTypes.DEFAULT_TYPE,
Expand All @@ -320,8 +317,6 @@ async def handle_meme_upload_lang_selected(

await log(f"""📥 Meme {meme["id"]} uploaded by #{update.effective_user.id}""", context.bot)

# TODO: create a meme object from meme_raw_upload

await _edit_upload_callback_caption(
update.callback_query.message,
localizer.t("upload.submitted", user_info["interface_lang"]),
Expand Down
2 changes: 0 additions & 2 deletions src/tgbot/repo/languages.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ async def add_user_languages(
user_id: int,
language_codes: Sequence[str],
) -> None:
# Prepare a list of dictionaries where each dictionary represents
# the values to be inserted for one row.
values_to_insert = [
{"user_id": user_id, "language_code": language_code} for language_code in language_codes
]
Expand Down
5 changes: 3 additions & 2 deletions src/tgbot/repo/popups.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,12 @@ async def update_user_popup_log(
user_popup_logs.update()
.where(user_popup_logs.c.user_id == user_id)
.where(user_popup_logs.c.popup_id == popup_id)
.where(user_popup_logs.c.reacted_at.is_(None)) # not sure abot that
# Only the first click may claim the reaction.
.where(user_popup_logs.c.reacted_at.is_(None))
.values(reacted_at=datetime.utcnow())
)
res = await execute(update_query)
reaction_is_new = res.rowcount > 0
if not reaction_is_new:
logging.warning(f"User {user_id} already reacted to popup {popup_id}!")
return reaction_is_new # so I can filter double clicks
return reaction_is_new
3 changes: 0 additions & 3 deletions src/tgbot/senders/alerts.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
from telegram import Bot, Message

from src import localizer

# from src.tgbot.senders.keyboards import queue_empty_alert_keyboard
from src.tgbot.user_info import get_user_info


Expand All @@ -11,5 +9,4 @@ async def send_queue_preparing_alert(bot: Bot, user_id: int) -> Message:
return await bot.send_message(
chat_id=user_id,
text=localizer.t("service.out_of_memes", user_info["interface_lang"]),
# reply_markup=queue_empty_alert_keyboard(),
)
3 changes: 0 additions & 3 deletions src/tgbot/senders/keyboards.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@
)
from src.tgbot.sharing import build_meme_share_button

# IDEA: use sometimes another emoji pair like 🤣/🤮

HEART_EMOJI = ["❤️", "♥️", "💙", "💜", "💛", "🧡", "💚", "🩵"]


Expand All @@ -36,7 +34,6 @@ def meme_reaction_keyboard(
):
like = random.choice(HEART_EMOJI)
dislike = "⏬"
# like, dislike = "👍", "👎"

def reaction_callback(reaction_id: int) -> str:
callback_data = MEME_BUTTON_CALLBACK_DATA_PATTERN.format(
Expand Down
Loading