Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Search #52

Merged
merged 2 commits into from
Mar 7, 2024
Merged
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
64 changes: 42 additions & 22 deletions alembic/versions/2024-03-02_popup_logs_meme_source_stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,39 +10,59 @@


# revision identifiers, used by Alembic.
revision = '594149282af3'
down_revision = '4b8328f00221'
revision = "594149282af3"
down_revision = "4b8328f00221"
branch_labels = None
depends_on = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('user_popup_logs',
sa.Column('user_id', sa.BigInteger(), nullable=False),
sa.Column('popup_id', sa.String(), nullable=False),
sa.Column('sent_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
sa.Column('reacted_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], name=op.f('user_popup_logs_user_id_fkey'), ondelete='CASCADE'),
sa.PrimaryKeyConstraint('user_id', 'popup_id', name=op.f('user_popup_logs_pkey'))
op.create_table(
"user_popup_logs",
sa.Column("user_id", sa.BigInteger(), nullable=False),
sa.Column("popup_id", sa.String(), nullable=False),
sa.Column(
"sent_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False
),
sa.Column("reacted_at", sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(
["user_id"],
["user.id"],
name=op.f("user_popup_logs_user_id_fkey"),
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint(
"user_id", "popup_id", name=op.f("user_popup_logs_pkey")
),
)
op.create_table('meme_source_stats',
sa.Column('meme_source_id', sa.Integer(), nullable=False),
sa.Column('nlikes', sa.Integer(), server_default='0', nullable=False),
sa.Column('ndislikes', sa.Integer(), server_default='0', nullable=False),
sa.Column('nmemes_sent_events', sa.Integer(), server_default='0', nullable=False),
sa.Column('nmemes_parsed', sa.Integer(), server_default='0', nullable=False),
sa.Column('nmemes_sent', sa.Integer(), server_default='0', nullable=False),
sa.Column('latest_meme_age', sa.Integer(), server_default='0', nullable=False),
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['meme_source_id'], ['meme_source.id'], name=op.f('meme_source_stats_meme_source_id_fkey'), ondelete='CASCADE'),
sa.PrimaryKeyConstraint('meme_source_id', name=op.f('meme_source_stats_pkey'))
op.create_table(
"meme_source_stats",
sa.Column("meme_source_id", sa.Integer(), nullable=False),
sa.Column("nlikes", sa.Integer(), server_default="0", nullable=False),
sa.Column("ndislikes", sa.Integer(), server_default="0", nullable=False),
sa.Column(
"nmemes_sent_events", sa.Integer(), server_default="0", nullable=False
),
sa.Column("nmemes_parsed", sa.Integer(), server_default="0", nullable=False),
sa.Column("nmemes_sent", sa.Integer(), server_default="0", nullable=False),
sa.Column("latest_meme_age", sa.Integer(), server_default="0", nullable=False),
sa.Column(
"updated_at", sa.DateTime(), server_default=sa.text("now()"), nullable=False
),
sa.ForeignKeyConstraint(
["meme_source_id"],
["meme_source.id"],
name=op.f("meme_source_stats_meme_source_id_fkey"),
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("meme_source_id", name=op.f("meme_source_stats_pkey")),
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('meme_source_stats')
op.drop_table('user_popup_logs')
op.drop_table("meme_source_stats")
op.drop_table("user_popup_logs")
# ### end Alembic commands ###
24 changes: 24 additions & 0 deletions alembic/versions/2024-03-07_extension_pg_pg_trgm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""extension_pg_pg_trgm

Revision ID: b780738c821f
Revises: 594149282af3
Create Date: 2024-03-07 19:11:32.863862

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = "b780738c821f"
down_revision = "594149282af3"
branch_labels = None
depends_on = None


def upgrade() -> None:
op.execute("CREATE EXTENSION IF NOT EXISTS pg_trgm")


def downgrade() -> None:
pass
46 changes: 46 additions & 0 deletions alembic/versions/2024-03-07_inlinesearch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""inlinesearch

Revision ID: f58d305bf511
Revises: b780738c821f
Create Date: 2024-03-07 20:09:12.025031

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = 'f58d305bf511'
down_revision = 'b780738c821f'
branch_labels = None
depends_on = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('inline_search_chosen_result_logs',
sa.Column('id', sa.Integer(), sa.Identity(always=False), nullable=False),
sa.Column('result_id', sa.String(), nullable=False),
sa.Column('user_id', sa.BigInteger(), nullable=False),
sa.Column('query', sa.String(), nullable=False),
sa.Column('chosen_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], name=op.f('inline_search_chosen_result_logs_user_id_fkey'), ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id', name=op.f('inline_search_chosen_result_logs_pkey'))
)
op.create_table('inline_search_logs',
sa.Column('id', sa.Integer(), sa.Identity(always=False), nullable=False),
sa.Column('user_id', sa.BigInteger(), nullable=False),
sa.Column('query', sa.String(), nullable=False),
sa.Column('chat_type', sa.String(), nullable=True),
sa.Column('searched_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], name=op.f('inline_search_logs_user_id_fkey'), ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id', name=op.f('inline_search_logs_pkey'))
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('inline_search_logs')
op.drop_table('inline_search_chosen_result_logs')
# ### end Alembic commands ###
20 changes: 20 additions & 0 deletions src/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,26 @@
Column("reacted_at", DateTime),
)

inline_search_logs = Table(
"inline_search_logs",
metadata,
Column("id", Integer, Identity(), primary_key=True),
Column("user_id", ForeignKey("user.id", ondelete="CASCADE"), nullable=False),
Column("query", String, nullable=False),
Column("chat_type", String),
Column("searched_at", DateTime, server_default=func.now(), nullable=False),
)

inline_search_chosen_result_logs = Table(
"inline_search_chosen_result_logs",
metadata,
Column("id", Integer, Identity(), primary_key=True),
Column("result_id", String, nullable=False),
Column("user_id", ForeignKey("user.id", ondelete="CASCADE"), nullable=False),
Column("query", String, nullable=False),
Column("chosen_at", DateTime, server_default=func.now(), nullable=False),
)


async def fetch_one(select_query: Select | Insert | Update) -> dict[str, Any] | None:
async with engine.begin() as conn:
Expand Down
9 changes: 9 additions & 0 deletions src/tgbot/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
ChatBoostHandler,
ChatMemberHandler,
CommandHandler,
InlineQueryHandler,
ChosenInlineResultHandler,
MessageHandler,
filters,
)
Expand All @@ -24,6 +26,7 @@
block,
broken,
error,
inline,
popup,
reaction,
start,
Expand Down Expand Up @@ -151,6 +154,12 @@ def add_handlers(application: Application) -> None:
)
)

# inline search
application.add_handler(InlineQueryHandler(inline.search_inline))
application.add_handler(
ChosenInlineResultHandler(inline.handle_chosen_inline_result)
)

application.add_error_handler(error.send_stacktrace_to_tg_chat, block=False)

############## admin
Expand Down
3 changes: 3 additions & 0 deletions src/tgbot/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,6 @@ def is_positive(self) -> bool:

TELEGRAM_CHANNEL_RU_CHAT_ID = -1001152876229
TELEGRAM_CHANNEL_RU_LINK = "https://t.me/fastfoodmemes"

# if a user tries the inline search but not used the bot yet
INLINE_SEARCH_REQUEST_DEEPLINK = "inline_search_request"
12 changes: 12 additions & 0 deletions src/tgbot/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class BaseFFMemesException(Exception):
BASE_DETAIL = "Base FastFoodMemes Exception"

def __init__(self, *args) -> None:
super().__init__(*args if args else self.BASE_DETAIL)


class UserNotFound(BaseFFMemesException):
DETAIL = "No user info found in database."

def __init__(self, user_id: int) -> None:
super().__init__(f"Can't get_user_info({user_id}). Probably no data in db.")
129 changes: 129 additions & 0 deletions src/tgbot/handlers/inline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
from html import escape as escape_html

from telegram import (
InlineKeyboardButton,
InlineKeyboardMarkup,
InlineQueryResultCachedPhoto,
InlineQueryResultsButton,
Update,
)
from telegram.ext import ContextTypes
from telegram.constants import ParseMode

from src.localizer import t
from src.tgbot.constants import (
INLINE_SEARCH_REQUEST_DEEPLINK,
)
from src.tgbot.exceptions import UserNotFound
from src.tgbot.handlers.language import (
get_active_language_from_user_languages,
get_user_languages_from_language_code_and_full_name,
)
from src.tgbot.service import (
search_memes_for_inline_query,
create_inline_search_log,
create_inline_chosen_result_log,
)
from src.tgbot.user_info import get_user_info
from src.tgbot.senders.utils import get_random_emoji
from src.config import settings

MIN_SEARCH_QUERY_LENGTH = 3
MAX_SEARCH_QUERY_LENGTH = 128
INLINE_SEARCH_RESULT_CACHE_SECONDS = 60 * 60 * 12 # 12 hours


def get_inline_result_ref_link(user_id: int, meme_id: int):
deep_link = f"ir_{user_id}_{meme_id}" # inline result
return f"https://t.me/{settings.TELEGRAM_BOT_USERNAME}?start={deep_link}"


def get_inline_result_caption(meme, user_info):
# caption = escape_html(meme["caption"]) if meme["caption"] else ""
caption = ""

ref_link = get_inline_result_ref_link(user_info["id"], meme["id"])
emoji = get_random_emoji()
caption += f"""{emoji} <a href="{ref_link}">Fast Food Memes</a>"""

return caption


async def search_inline(update: Update, _: ContextTypes.DEFAULT_TYPE):
try:
user_info = await get_user_info(update.effective_user.id)
except UserNotFound:
# user doesn't exist. Tell them to start up the bot
button = InlineQueryResultsButton(
text=t("inline.you_need_to_register", update.effective_user.language_code),
start_parameter=INLINE_SEARCH_REQUEST_DEEPLINK,
)
await update.inline_query.answer([], button=button, cache_time=0)
return

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

if len(query) == 0:
# TODO: show trending / recommended memes
return await update.inline_query.answer(
[],
button=InlineQueryResultsButton(
text=t("inline.enter_your_query", user_info["interface_lang"]),
start_parameter=INLINE_SEARCH_REQUEST_DEEPLINK,
),
)
elif len(query) < MIN_SEARCH_QUERY_LENGTH:
return await update.inline_query.answer(
[],
button=InlineQueryResultsButton(
text=t("inline.search_query_too_short", user_info["interface_lang"]),
start_parameter=INLINE_SEARCH_REQUEST_DEEPLINK,
),
)
if len(query) >= MAX_SEARCH_QUERY_LENGTH:
return await update.inline_query.answer(
[],
button=InlineQueryResultsButton(
text=t("inline.search_query_too_long", user_info["interface_lang"]),
start_parameter=INLINE_SEARCH_REQUEST_DEEPLINK,
),
)

memes = await search_memes_for_inline_query(query, limit=10)

if len(memes) == 0:
no_results_button = InlineQueryResultsButton(
text=t("inline.no_results", user_info["interface_lang"]),
start_parameter=INLINE_SEARCH_REQUEST_DEEPLINK,
)
await update.inline_query.answer([], button=no_results_button)
return

results = [
InlineQueryResultCachedPhoto(
id=str(meme["id"]),
photo_file_id=meme["telegram_file_id"],
caption=get_inline_result_caption(meme, user_info),
parse_mode=ParseMode.HTML,
)
for meme in memes
]

await update.inline_query.answer(
results, cache_time=INLINE_SEARCH_RESULT_CACHE_SECONDS
)

await create_inline_search_log(
user_id=update.effective_user.id,
query=query,
chat_type=update.inline_query.chat_type,
)


async def handle_chosen_inline_result(update: Update, _: ContextTypes.DEFAULT_TYPE):
chosen_inline_result = update.chosen_inline_result
await create_inline_chosen_result_log(
user_id=chosen_inline_result.from_user.id,
result_id=chosen_inline_result.result_id,
query=update.chosen_inline_result.query,
)
Loading
Loading