feat(telegram_bot): added campaign creation and updating, added help command
This commit is contained in:
@@ -18,10 +18,12 @@ class NotFoundError(BaseModel):
|
||||
|
||||
class CampaignTargeting(BaseModel):
|
||||
gender: Literal["MALE", "FEMALE", "ALL"] | None = None
|
||||
age_from: Annotated[NonNegativeInt, Field(strict=True, ls=100)] | None = (
|
||||
age_from: Annotated[NonNegativeInt, Field(strict=False, ls=100)] | None = (
|
||||
None
|
||||
)
|
||||
age_to: Annotated[NonNegativeInt, Field(strict=False, ls=100)] | None = (
|
||||
None
|
||||
)
|
||||
age_to: Annotated[NonNegativeInt, Field(strict=True, ls=100)] | None = None
|
||||
location: str | None = None
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
from aiogram import Router
|
||||
from aiogram.filters import Command
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import Message
|
||||
from aiogram_dialog import DialogManager
|
||||
|
||||
help_router = Router()
|
||||
|
||||
|
||||
@help_router.message(Command("help"))
|
||||
async def help_command(
|
||||
message: Message, dialog_manager: DialogManager, state: FSMContext
|
||||
) -> None:
|
||||
response = (
|
||||
"Commands:\n\n"
|
||||
"/start - Start the bot and authenticate as advertiser\n"
|
||||
"/campaigns - Manage your campaigns\n"
|
||||
"/statistics - See your overall statistics\n"
|
||||
"/logout - Logout of current advertiser account"
|
||||
)
|
||||
await message.answer(response)
|
||||
@@ -19,7 +19,8 @@ async def start_command(
|
||||
await message.answer(
|
||||
"Already authenticated as"
|
||||
f" <code>{state_data['advertiser']['name']}</code> "
|
||||
f"(<code>{state_data['advertiser']['advertiser_id']}</code>)"
|
||||
f"(<code>{state_data['advertiser']['advertiser_id']}</code>)."
|
||||
"Get all commands with /help."
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
@@ -26,8 +26,8 @@ async def stats_command(
|
||||
f"\t• Impressions: {stats.impressions_count}\n"
|
||||
f"\t• Clicks: {stats.clicks_count}\n"
|
||||
f"\t• Conversion: {stats.conversion:.2f}%\n"
|
||||
f"\t• Spent on impressions: ${stats.spent_impressions:.2f}\n"
|
||||
f"\t• Spent on clicks: ${stats.spent_clicks:.2f}\n"
|
||||
f"\t• Spent total: ${stats.spent_total:.2f}"
|
||||
f"\t• Spent on impressions: {stats.spent_impressions:.2f}\n"
|
||||
f"\t• Spent on clicks: {stats.spent_clicks:.2f}\n"
|
||||
f"\t• Spent total: {stats.spent_total:.2f}"
|
||||
)
|
||||
await message.answer(response)
|
||||
|
||||
@@ -6,18 +6,72 @@ from urllib.parse import urlparse
|
||||
|
||||
import httpx
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import CallbackQuery, ContentType
|
||||
from aiogram.types import CallbackQuery, ContentType, Message
|
||||
from aiogram_dialog import Dialog, DialogManager, Window
|
||||
from aiogram_dialog.api.entities import MediaAttachment
|
||||
from aiogram_dialog.widgets.common import Whenable
|
||||
from aiogram_dialog.widgets.input import ManagedTextInput, TextInput
|
||||
from aiogram_dialog.widgets.kbd import Button, ListGroup, ScrollingGroup, Start
|
||||
from aiogram_dialog.widgets.media import DynamicMedia
|
||||
from aiogram_dialog.widgets.text import Const, Format
|
||||
from pydantic import ValidationError
|
||||
|
||||
import config
|
||||
from api.client import AdNovaClient
|
||||
from api.errors import BadRequestError, ForbiddenError
|
||||
from dialogs import utils
|
||||
from states.campaigns import CampaignsDailogState
|
||||
|
||||
campaign_info = (
|
||||
Const('<pre><code class="language-input-format">Title\n'),
|
||||
Const("Text\n"),
|
||||
Const("Impressions limit\n"),
|
||||
Const("Clicks limit\n"),
|
||||
Const("Cost per impression\n"),
|
||||
Const("Cost per click\n"),
|
||||
Const("Start date\n"),
|
||||
Const("End date\n"),
|
||||
Const("Targeting gender (ALL, FEMALE, MALE, None)\n"),
|
||||
Const("Targeting age from (could be None)\n"),
|
||||
Const("Targeting age to (could be None)\n"),
|
||||
Const("Targeting location (could be None)"),
|
||||
Const("</code></pre>"),
|
||||
Const('<pre><code class="language-example">Some title\n'),
|
||||
Const("Some text\n"),
|
||||
Const("15\n"),
|
||||
Const("10\n"),
|
||||
Const("0.4\n"),
|
||||
Const("0.5\n"),
|
||||
Const("100\n"),
|
||||
Const("110\n"),
|
||||
Const("ALL\n"),
|
||||
Const("12\n"),
|
||||
Const("None\n"),
|
||||
Const("Moscow"),
|
||||
Const("</code></pre>"),
|
||||
)
|
||||
|
||||
|
||||
def campaign_has_ad_image(
|
||||
data: dict, widget: Whenable, manager: DialogManager
|
||||
) -> bool:
|
||||
return bool(data["dialog_data"]["campaign"]["ad_image"])
|
||||
|
||||
|
||||
def campaign_has_not_ad_image(
|
||||
data: dict, widget: Whenable, manager: DialogManager
|
||||
) -> bool:
|
||||
return not data["dialog_data"]["campaign"]["ad_image"]
|
||||
|
||||
|
||||
def check_campaign(campaign_data: str) -> None:
|
||||
fields = campaign_data.split("\n\n")
|
||||
|
||||
if len(fields) != 12:
|
||||
raise ValueError
|
||||
|
||||
utils.campaign_from_list(fields)
|
||||
|
||||
|
||||
async def campaigns(**kwargs: dict[Any]) -> dict[str, Any]:
|
||||
state: FSMContext = kwargs["state"]
|
||||
@@ -37,50 +91,10 @@ async def campaigns(**kwargs: dict[Any]) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
async def campaign_detail_on_click(
|
||||
callback: CallbackQuery, button: Button, manager: DialogManager
|
||||
) -> None:
|
||||
manager_data: dict[Any] = await manager.load_data()
|
||||
state: FSMContext = manager_data["middleware_data"]["state"]
|
||||
state_data = await state.get_data()
|
||||
|
||||
advertiser_id = state_data["advertiser_id"]
|
||||
campaign_id = manager.item_id
|
||||
|
||||
if campaign_id == "":
|
||||
return
|
||||
|
||||
async with AdNovaClient() as client:
|
||||
campaign = await client.get_campaign(
|
||||
advertiser_id=advertiser_id,
|
||||
campaign_id=campaign_id,
|
||||
)
|
||||
|
||||
if campaign.ad_image:
|
||||
campaign.ad_image = (
|
||||
f"{config.MINIO_URL}{urlparse(campaign.ad_image).path}"
|
||||
)
|
||||
|
||||
await manager.update({"campaign": campaign.model_dump(mode="json")})
|
||||
await callback.answer()
|
||||
await manager.switch_to(CampaignsDailogState.campaign)
|
||||
|
||||
|
||||
def campaign_has_ad_image(
|
||||
data: dict, widget: Whenable, manager: DialogManager
|
||||
) -> bool:
|
||||
return bool(data["dialog_data"]["campaign"]["ad_image"])
|
||||
|
||||
|
||||
def campaign_has_not_ad_image(
|
||||
data: dict, widget: Whenable, manager: DialogManager
|
||||
) -> bool:
|
||||
return not data["dialog_data"]["campaign"]["ad_image"]
|
||||
|
||||
|
||||
async def campaign_by_id(**kwargs: dict[Any]) -> dict[str, Any]:
|
||||
manager: DialogManager = kwargs["dialog_manager"]
|
||||
manager_data = await manager.load_data()
|
||||
|
||||
ad_image_url = manager_data["dialog_data"]["campaign"]["ad_image"]
|
||||
|
||||
if not ad_image_url:
|
||||
@@ -109,6 +123,146 @@ async def campaign_by_id(**kwargs: dict[Any]) -> dict[str, Any]:
|
||||
return {"ad_image": attachment}
|
||||
|
||||
|
||||
async def campaign_on_error(
|
||||
message: Message,
|
||||
widget: ManagedTextInput,
|
||||
dialog_manager: DialogManager,
|
||||
error: object,
|
||||
) -> None:
|
||||
if isinstance(error, ValidationError):
|
||||
await message.answer(f"Invalid campaign data: {error.json()}")
|
||||
elif isinstance(error, ValueError):
|
||||
await message.answer(f"Invalid campaign data {error!s}")
|
||||
|
||||
|
||||
async def campaign_create_on_success(
|
||||
message: Message,
|
||||
widget: ManagedTextInput,
|
||||
dialog_manager: DialogManager,
|
||||
campaign_data: str,
|
||||
) -> None:
|
||||
state = dialog_manager.middleware_data["state"]
|
||||
state_data = await state.get_data()
|
||||
|
||||
fields = message.text.split("\n\n")
|
||||
|
||||
campaign = utils.campaign_from_list(fields)
|
||||
|
||||
async with AdNovaClient() as client:
|
||||
try:
|
||||
await client.create_campaign(
|
||||
advertiser_id=state_data["advertiser_id"], data=campaign
|
||||
)
|
||||
await dialog_manager.switch_to(CampaignsDailogState.campaigns)
|
||||
except BadRequestError as e:
|
||||
await message.answer(
|
||||
f"Invalid data: {e.model_dump(mode='json')['detail']}"
|
||||
)
|
||||
|
||||
|
||||
async def campaign_edit_on_success(
|
||||
message: Message,
|
||||
widget: ManagedTextInput,
|
||||
dialog_manager: DialogManager,
|
||||
campaign_data: str,
|
||||
) -> None:
|
||||
fields = message.text.split("\n\n")
|
||||
|
||||
new_campaign = utils.campaign_from_list(fields)
|
||||
|
||||
state = dialog_manager.middleware_data["state"]
|
||||
state_data = await state.get_data()
|
||||
|
||||
manager_data: dict[Any] = await dialog_manager.load_data()
|
||||
|
||||
campaign_id = manager_data["dialog_data"]["campaign"]["campaign_id"]
|
||||
|
||||
async with AdNovaClient() as client:
|
||||
try:
|
||||
new_campaign = await client.update_campaign(
|
||||
advertiser_id=state_data["advertiser_id"],
|
||||
campaign_id=campaign_id,
|
||||
data=new_campaign,
|
||||
)
|
||||
await dialog_manager.update(
|
||||
{
|
||||
"campaign": new_campaign.model_dump(mode="json"),
|
||||
}
|
||||
)
|
||||
await dialog_manager.switch_to(CampaignsDailogState.campaign)
|
||||
except BadRequestError as e:
|
||||
await message.answer(
|
||||
f"Invalid data: {e.model_dump(mode='json')['detail']}"
|
||||
)
|
||||
except ForbiddenError as e:
|
||||
await message.answer(
|
||||
"Forbidden changing campaign: "
|
||||
f"{e.model_dump(mode='json')['detail']}"
|
||||
)
|
||||
|
||||
|
||||
async def campaign_detail_on_click(
|
||||
callback: CallbackQuery, button: Button, manager: DialogManager
|
||||
) -> None:
|
||||
manager_data: dict[Any] = await manager.load_data()
|
||||
state: FSMContext = manager_data["middleware_data"]["state"]
|
||||
state_data = await state.get_data()
|
||||
|
||||
advertiser_id = state_data["advertiser_id"]
|
||||
campaign_id = manager.item_id
|
||||
|
||||
if campaign_id == "":
|
||||
return
|
||||
|
||||
async with AdNovaClient() as client:
|
||||
campaign = await client.get_campaign(
|
||||
advertiser_id=advertiser_id,
|
||||
campaign_id=campaign_id,
|
||||
)
|
||||
campaign_statistics = await client.get_campaign_statistics(
|
||||
campaign_id=campaign_id
|
||||
)
|
||||
|
||||
if campaign.ad_image:
|
||||
campaign.ad_image = (
|
||||
f"{config.MINIO_URL}{urlparse(campaign.ad_image).path}"
|
||||
)
|
||||
|
||||
await manager.update(
|
||||
{
|
||||
"campaign": campaign.model_dump(mode="json"),
|
||||
"campaign_statistics": campaign_statistics.model_dump(mode="json"),
|
||||
}
|
||||
)
|
||||
await callback.answer()
|
||||
await manager.switch_to(CampaignsDailogState.campaign)
|
||||
|
||||
|
||||
async def campaign_edit_on_click(
|
||||
callback: CallbackQuery, button: Button, manager: DialogManager
|
||||
) -> None:
|
||||
manager_data: dict[Any] = await manager.load_data()
|
||||
state: FSMContext = manager_data["middleware_data"]["state"]
|
||||
state_data = await state.get_data()
|
||||
|
||||
advertiser_id = state_data["advertiser_id"]
|
||||
campaign_id = manager_data["dialog_data"]["campaign"]["campaign_id"]
|
||||
|
||||
async with AdNovaClient() as client:
|
||||
campaign = await client.get_campaign(
|
||||
advertiser_id=advertiser_id,
|
||||
campaign_id=campaign_id,
|
||||
)
|
||||
|
||||
await manager.update(
|
||||
{
|
||||
"campaign": campaign.model_dump(mode="json"),
|
||||
}
|
||||
)
|
||||
await callback.answer()
|
||||
await manager.switch_to(CampaignsDailogState.campaign_edit)
|
||||
|
||||
|
||||
async def delete_ad_image(
|
||||
callback: CallbackQuery, button: Button, manager: DialogManager
|
||||
) -> None:
|
||||
@@ -150,16 +304,40 @@ async def delete_campaign(
|
||||
await manager.switch_to(CampaignsDailogState.campaigns)
|
||||
|
||||
|
||||
async def back_to_list(
|
||||
async def back_to_campaign(
|
||||
callback: CallbackQuery, button: Button, manager: DialogManager
|
||||
) -> None:
|
||||
await manager.switch_to(CampaignsDailogState.campaigns)
|
||||
manager_data = await manager.load_data()
|
||||
state: FSMContext = manager_data["middleware_data"]["state"]
|
||||
state_data = await state.get_data()
|
||||
|
||||
campaign = manager_data["dialog_data"]["campaign"]
|
||||
campaign_id = campaign["campaign_id"]
|
||||
advertiser_id = state_data["advertiser_id"]
|
||||
|
||||
async with AdNovaClient() as client:
|
||||
campaign = await client.get_campaign(
|
||||
advertiser_id=advertiser_id,
|
||||
campaign_id=campaign_id,
|
||||
)
|
||||
|
||||
await manager.update(
|
||||
{
|
||||
"campaign": campaign.model_dump(mode="json"),
|
||||
}
|
||||
)
|
||||
await callback.answer()
|
||||
await manager.switch_to(CampaignsDailogState.campaign)
|
||||
|
||||
|
||||
campaigns_dialog = Dialog(
|
||||
Window(
|
||||
Const("Campaigns:"),
|
||||
Button(Const("➕ Create"), id="create_campaign"),
|
||||
Start(
|
||||
Const("➕ Create"),
|
||||
id="create_campaign",
|
||||
state=CampaignsDailogState.campaign_create,
|
||||
),
|
||||
ScrollingGroup(
|
||||
ListGroup(
|
||||
Button(
|
||||
@@ -178,6 +356,25 @@ campaigns_dialog = Dialog(
|
||||
state=CampaignsDailogState.campaigns,
|
||||
getter=campaigns,
|
||||
),
|
||||
Window(
|
||||
Const(
|
||||
"Enter campaign info in following format "
|
||||
"(each statement on new line with one enter between each):"
|
||||
),
|
||||
*campaign_info,
|
||||
Start(
|
||||
Const("⬅️ Back to list"),
|
||||
id="back",
|
||||
state=CampaignsDailogState.campaigns,
|
||||
),
|
||||
TextInput(
|
||||
id="campaign",
|
||||
type_factory=check_campaign,
|
||||
on_success=campaign_create_on_success,
|
||||
on_error=campaign_on_error,
|
||||
),
|
||||
state=CampaignsDailogState.campaign_create,
|
||||
),
|
||||
Window(
|
||||
DynamicMedia("ad_image", when=campaign_has_ad_image),
|
||||
Format("• ID: <code>{dialog_data[campaign][campaign_id]}</code>"),
|
||||
@@ -199,12 +396,36 @@ campaigns_dialog = Dialog(
|
||||
Format("\t • Age from: {dialog_data[campaign][targeting][age_from]}"),
|
||||
Format("\t • Age to: {dialog_data[campaign][targeting][age_to]}"),
|
||||
Format("\t • Location: {dialog_data[campaign][targeting][location]}"),
|
||||
Button(Const("📝 Edit campaign"), id="edit_campaign"),
|
||||
Const("\n📊 Statistics:\n"),
|
||||
Format(
|
||||
"Impressions: "
|
||||
"{dialog_data[campaign_statistics][impressions_count]}"
|
||||
),
|
||||
Format("Clicks: {dialog_data[campaign_statistics][clicks_count]}"),
|
||||
Format(
|
||||
"Conversion: "
|
||||
"{dialog_data[campaign_statistics][impressions_count]:.2f}%"
|
||||
),
|
||||
Format(
|
||||
"Spent on impressions: "
|
||||
"{dialog_data[campaign_statistics][impressions_count]:.2f}"
|
||||
),
|
||||
Format(
|
||||
"Spent on clicks: "
|
||||
"{dialog_data[campaign_statistics][spent_clicks]:.2f}"
|
||||
),
|
||||
Format(
|
||||
"Spent total: {dialog_data[campaign_statistics][spent_total]:.2f}"
|
||||
),
|
||||
Button(
|
||||
Const("📝 Edit campaign"),
|
||||
id="edit_campaign",
|
||||
on_click=campaign_edit_on_click,
|
||||
),
|
||||
Start(
|
||||
Const("⬆️ Upload image"),
|
||||
id="upload_ad_image",
|
||||
state=CampaignsDailogState.campaign_upload_ad_image,
|
||||
data=None,
|
||||
when=campaign_has_not_ad_image,
|
||||
),
|
||||
Button(
|
||||
@@ -218,8 +439,31 @@ campaigns_dialog = Dialog(
|
||||
id="delete_ad_image",
|
||||
on_click=delete_campaign,
|
||||
),
|
||||
Button(Const("⬅️ Back to list"), id="back", on_click=back_to_list),
|
||||
Start(
|
||||
Const("⬅️ Back to list"),
|
||||
id="back",
|
||||
state=CampaignsDailogState.campaigns,
|
||||
),
|
||||
state=CampaignsDailogState.campaign,
|
||||
getter=campaign_by_id,
|
||||
),
|
||||
Window(
|
||||
Const(
|
||||
"Enter new campaign info in following format "
|
||||
"(each statement on new line with one enter between each):"
|
||||
),
|
||||
*campaign_info,
|
||||
Button(
|
||||
Const("⬅️ Back to campaign"),
|
||||
id="back_to_campaign",
|
||||
on_click=back_to_campaign,
|
||||
),
|
||||
TextInput(
|
||||
id="campaign_update",
|
||||
type_factory=check_campaign,
|
||||
on_success=campaign_edit_on_success,
|
||||
on_error=campaign_on_error,
|
||||
),
|
||||
state=CampaignsDailogState.campaign_edit,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -45,7 +45,9 @@ async def advertiser_id_on_success(
|
||||
state_data["advertiser_id"] = message.text
|
||||
await state.set_data(state_data)
|
||||
|
||||
await message.answer(f"Successfully authenticated as {message.text}")
|
||||
await message.answer(
|
||||
f"Successfully authenticated as {message.text}. Get help: /help."
|
||||
)
|
||||
await dialog_manager.mark_closed()
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
from api.schemas import CampaignCreateIn, CampaignTargeting
|
||||
|
||||
|
||||
def campaign_from_list(fields: list[str]) -> CampaignCreateIn:
|
||||
return CampaignCreateIn(
|
||||
targeting=CampaignTargeting(
|
||||
gender=None if fields[8] == "None" else fields[8],
|
||||
age_from=None if fields[9] == "None" else fields[9],
|
||||
age_to=None if fields[10] == "None" else fields[10],
|
||||
location=None if fields[11] == "None" else fields[11],
|
||||
),
|
||||
ad_title=fields[0],
|
||||
ad_text=fields[1],
|
||||
impressions_limit=fields[2],
|
||||
clicks_limit=fields[3],
|
||||
cost_per_impression=fields[4],
|
||||
cost_per_click=fields[5],
|
||||
start_date=fields[6],
|
||||
end_date=fields[7],
|
||||
)
|
||||
@@ -8,6 +8,7 @@ from aiogram_dialog import setup_dialogs
|
||||
|
||||
import config
|
||||
from commands.campaigns import campaigns_router
|
||||
from commands.help import help_router
|
||||
from commands.logout import logout_router
|
||||
from commands.start import start_router
|
||||
from commands.stats import statistics_router
|
||||
@@ -43,6 +44,7 @@ async def main() -> None:
|
||||
dp.message.outer_middleware(AuthMiddleware())
|
||||
|
||||
dp.include_routers(
|
||||
help_router,
|
||||
start_router,
|
||||
campaigns_router,
|
||||
statistics_router,
|
||||
|
||||
@@ -100,6 +100,7 @@ ignore = [
|
||||
"FBT001",
|
||||
"FBT002",
|
||||
"N813",
|
||||
"PLR2004",
|
||||
"RUF001",
|
||||
"TC002",
|
||||
]
|
||||
|
||||
@@ -5,4 +5,5 @@ class CampaignsDailogState(StatesGroup):
|
||||
campaigns = State()
|
||||
campaign = State()
|
||||
campaign_upload_ad_image = State()
|
||||
campaign_create = State()
|
||||
campaign_edit = State()
|
||||
|
||||
Reference in New Issue
Block a user