Skip to content

Commit

Permalink
Merge pull request #114 from Studio-Yandex-Practicum/feature/wednesda…
Browse files Browse the repository at this point in the history
…y_messages

Feature/wednesday messages
  • Loading branch information
LyapkovAleksey authored Dec 4, 2023
2 parents 835cd94 + b554252 commit 634e8e7
Show file tree
Hide file tree
Showing 10 changed files with 279 additions and 8 deletions.
94 changes: 92 additions & 2 deletions src/bot/plugins/week_routine.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import re
from typing import Any

from apscheduler.schedulers.asyncio import AsyncIOScheduler
from dependency_injector.wiring import Provide, inject
Expand All @@ -8,6 +9,7 @@
from src.bot.schemas import Actions, Attachment, Context, Integration
from src.bot.services.matching import MatchingService
from src.bot.services.notify_service import NotifyService
from src.core.db.models import MatchReviewAnswerEnum
from src.depends import Container
from src.endpoints import Endpoints

Expand All @@ -17,6 +19,8 @@
DAY_OF_WEEK_FRIDAY = "fri"
SUNDAY_TIME_SENDING_MESSAGE = 11
DAY_OF_WEEK_SUNDAY = "sun"
WEDNESDAY_TIME_SENDING_MESSAGE = 11
DAY_OF_WEEK_WEDNESDAY = "wed"


class WeekRoutine(Plugin):
Expand Down Expand Up @@ -65,14 +69,15 @@ def on_start(
matching_service: MatchingService = Provide[Container.matching_service],
scheduler: AsyncIOScheduler = Provide[Container.scheduler],
) -> None:
attachments = self.direct_friday_message()
friday_attachments = self.direct_friday_message()
wednesday_attachments = self.direct_wednesday_message()

scheduler.add_job(
notify_service.notify_all_users,
"cron",
day_of_week=DAY_OF_WEEK_FRIDAY,
hour=FRIDAY_TIME_SENDING_MESSAGE,
kwargs=dict(plugin=self, attachments=attachments, title="Еженедельный пятничный опрос"),
kwargs=dict(plugin=self, attachments=friday_attachments, title="Еженедельный пятничный опрос"),
)
scheduler.add_job(
matching_service.run_matching,
Expand All @@ -87,6 +92,13 @@ def on_start(
hour=MONDAY_TIME_SENDING_MESSAGE,
kwargs=dict(plugin=self),
)
scheduler.add_job(
notify_service.match_review_notifications,
"cron",
day_of_week=DAY_OF_WEEK_WEDNESDAY,
hour=WEDNESDAY_TIME_SENDING_MESSAGE,
kwargs=dict(plugin=self, attachments=wednesday_attachments),
)
scheduler.start()

@listen_to("/stop_jobs", re.IGNORECASE)
Expand Down Expand Up @@ -122,3 +134,81 @@ async def no(self, event: ActionEvent) -> None:
"update": {"message": "На следующей неделе отправлю новое предложение.", "props": {}},
},
)

@inject
def direct_wednesday_message(self, endpoints: Endpoints = Provide[Container.endpoints]) -> Attachment:
action_yes = Actions(
id="yes",
name="Да",
type="button",
integration=Integration(url=endpoints.answer_yes, context=Context(action="yes")),
)

action_no = Actions(
id="No",
name="Нет",
type="button",
integration=Integration(url=endpoints.answer_no, context=Context(action="no")),
)

every_wednesday_message = Attachment(text="Удалось ли вам встретиться?", actions=[action_yes, action_no])
return every_wednesday_message

@listen_webhook("match_review_answer_yes")
async def answer_yes(
self,
event: ActionEvent,
) -> None:
await self._save_user_answer(event.user_id, MatchReviewAnswerEnum.IS_COMPLETE)
self.driver.respond_to_web(
event,
{
"update": {
"message": "Поделитесь итогами вашей встречи в канале "
'"Coffe на этой неделе", отправьте фото и '
"краткие эмоции, чтобы мотивировать других "
"поучаствовать в Random Coffee!",
"props": {},
},
},
)

@listen_webhook("match_review_answer_no")
async def answer_no(
self,
event: ActionEvent,
) -> None:
await self._save_user_answer(event.user_id, MatchReviewAnswerEnum.IS_NOT_COMPLETE)
user_nickname = await self._get_pair_nickname(event.user_id)
self.driver.respond_to_web(
event,
{
"update": {
"message": f"Неделя скоро закончится, не забудь "
f"познакомиться с новым человеком и провести "
f"время за классным разговором, напиши "
f"{user_nickname} точно ждёт вашей встречи",
"props": {},
},
},
)

@inject
async def _save_user_answer(
self, user_id: str, answer: str, notify_service: NotifyService = Provide[Container.week_routine_service,]
) -> None:
await notify_service.set_match_review_answer(user_id, answer)

@inject
async def _get_pair_nickname(
self, user_id: str, matching_service: MatchingService = Provide[Container.matching_service,]
) -> Any:
return await matching_service.get_match_pair_nickname(user_id)

@listen_to("/wednesday_message", re.IGNORECASE)
@inject
async def test_wednesday_message(
self, message: str, notify_service: NotifyService = Provide[Container.week_routine_service,]
) -> None:
attachments = self.direct_wednesday_message()
await notify_service.match_review_notifications(plugin=self, attachments=attachments)
12 changes: 11 additions & 1 deletion src/bot/services/matching.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Sequence
from typing import Any, Sequence

import structlog

Expand Down Expand Up @@ -38,3 +38,13 @@ async def run_closing_meetings(self) -> Sequence[UsersMatch]:
user.status = StatusEnum.NOT_INVOLVED
await self._user_repository.update(user.id, user)
return await self._match_repository.closing_meetings()

async def get_match_pair_nickname(self, user_id: str) -> Any:
"""Возвращает никнейм второго пользователя
по user_id первого пользователя"""
match = await self._match_repository.get_by_user_id(user_id)
if user_id == match.object_user_one.user_id:
user = match.object_user_two
elif user_id == match.object_user_two.user_id:
user = match.object_user_one
return user.username
29 changes: 28 additions & 1 deletion src/bot/services/notify_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,23 @@

from src.bot.schemas import Attachment
from src.core.db.models import MatchStatusEnum, User
from src.core.db.repository.match_review import MatchReviewRepository
from src.core.db.repository.user import UserRepository
from src.core.db.repository.usersmatch import UsersMatchRepository

logger = structlog.get_logger()


class NotifyService:
def __init__(self, user_repository: UserRepository, match_repository: UsersMatchRepository) -> None:
def __init__(
self,
user_repository: UserRepository,
match_repository: UsersMatchRepository,
match_review_repository: MatchReviewRepository,
) -> None:
self._user_repository = user_repository
self._match_repository = match_repository
self._match_review_repository = match_review_repository

async def notify_all_users(
self, plugin: Plugin, attachments: Attachment, title: str = "Еженедельный опрос"
Expand Down Expand Up @@ -47,3 +54,23 @@ async def meeting_notifications(self, plugin: Plugin) -> None:
)
except InvalidOrMissingParameters as error:
logger.error(str(error))

async def set_match_review_answer(self, user_id: str, answer: str) -> None:
match = await self._match_repository.get_by_user_id(user_id)
await self._match_review_repository.set_match_review_answer(match, user_id, answer)

async def match_review_notifications(
self,
plugin: Plugin,
attachments: Attachment,
title: str = "Опрос по результатам встречи",
) -> None:
for match in await self._match_repository.get_by_status(status=MatchStatusEnum.ONGOING):
pair: list[User] = [match.object_user_one, match.object_user_two]
for user_one, user_two in zip(pair, pair[::-1]):
try:
plugin.driver.direct_message(
receiver_id=user_one.user_id, message=title, props={"attachments": [attachments.model_dump()]}
)
except InvalidOrMissingParameters as error:
logger.error(str(error))
47 changes: 47 additions & 0 deletions src/core/db/migrations/versions/abb6775f7512_add_match_review.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""add match_review
Revision ID: abb6775f7512
Revises: 6d584f53f8cf
Create Date: 2023-12-04 20:54:36.381591
"""
import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "abb6775f7512"
down_revision = "6d584f53f8cf"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"match_review",
sa.Column("usersmatch_id", sa.Integer(), nullable=False),
sa.Column("user_id", sa.Integer(), nullable=False),
sa.Column(
"user_answer", sa.Enum("IS_COMPLETE", "IS_NOT_COMPLETE", name="matchreviewanswerenum"), nullable=True
),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created_at", sa.Date(), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
sa.Column("updated_at", sa.Date(), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
sa.ForeignKeyConstraint(
["user_id"],
["users.id"],
),
sa.ForeignKeyConstraint(
["usersmatch_id"],
["usersmatch.id"],
),
sa.PrimaryKeyConstraint("id"),
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("match_review")
op.execute("drop type matchreviewanswerenum")
# ### end Alembic commands ###
17 changes: 16 additions & 1 deletion src/core/db/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from datetime import date
from enum import StrEnum
from typing import Optional

from sqlalchemy import ForeignKey, String, func
from sqlalchemy import ForeignKey, Integer, String, func
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship

SERVER_DEFAULT_TIME = func.current_timestamp()
Expand All @@ -18,6 +19,11 @@ class MatchStatusEnum(StrEnum):
CLOSED = "CLOSED"


class MatchReviewAnswerEnum(StrEnum):
IS_COMPLETE = "IS_COMPLETE"
IS_NOT_COMPLETE = "IS_NOT_COMPLETE"


class Base(DeclarativeBase):
"""Base class for models"""

Expand Down Expand Up @@ -58,3 +64,12 @@ class UsersMatch(Base):

object_user_one = relationship("User", foreign_keys=[matched_user_one], backref="matches_as_user_one")
object_user_two = relationship("User", foreign_keys=[matched_user_two], backref="matches_as_user_two")
match_review: Mapped["MatchReview"] = relationship("MatchReview", backref="usersmatch")


class MatchReview(Base):
__tablename__ = "match_review"

usersmatch_id: Mapped[int] = mapped_column(Integer(), ForeignKey("usersmatch.id"), nullable=False)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False)
user_answer: Mapped[Optional[MatchReviewAnswerEnum]] = mapped_column(nullable=True)
31 changes: 31 additions & 0 deletions src/core/db/repository/match_review.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from sqlalchemy import select

from src.core.db.models import MatchReview, UsersMatch
from src.core.db.repository.base import AbstractRepository
from src.core.exceptions.exceptions import ObjectAlreadyExistsError


class MatchReviewRepository(AbstractRepository[MatchReview]):
_model = MatchReview

async def set_match_review_answer(self, match: UsersMatch, user_id: str, answer: str) -> MatchReview:
if user_id == match.object_user_one.user_id:
user = match.object_user_one
elif user_id == match.object_user_two.user_id:
user = match.object_user_two
if await self.get_match_review_if_already_exists(usersmatch_id=match.id, user_id=user.id):
raise ObjectAlreadyExistsError(self._model) # type: ignore[arg-type]
return await self.create(MatchReview(usersmatch_id=match.id, user_id=user.id, user_answer=answer))

async def get_match_review_if_already_exists(
self,
usersmatch_id: int,
user_id: int,
) -> MatchReview | None:
async with self._sessionmaker() as session:
match_review = await session.scalar(
select(self._model).where(
(self._model.usersmatch_id == usersmatch_id) & (self._model.user_id == user_id)
)
)
return match_review
26 changes: 24 additions & 2 deletions src/core/db/repository/usersmatch.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from typing import Sequence

from sqlalchemy import select, update
from sqlalchemy.orm import selectinload
from sqlalchemy.orm import aliased, selectinload

from src.core.db.models import MatchStatusEnum, User, UsersMatch
from src.core.db.repository.base import AbstractRepository
from src.core.exceptions.exceptions import ObjectAlreadyExistsError
from src.core.exceptions.exceptions import MatchNotFoundError, ObjectAlreadyExistsError, TooManyMatchesError


class UsersMatchRepository(AbstractRepository[UsersMatch]):
Expand Down Expand Up @@ -51,3 +51,25 @@ async def get_by_status(self, status: str | MatchStatusEnum) -> Sequence[UsersMa
.where(self._model.status == status)
)
return meetings.all()

async def get_by_user_id(self, user_id: str) -> UsersMatch:
"""Получает текущую встречу по user_id участника"""
async with self._sessionmaker() as session:
matched_user_one = aliased(User)
matched_user_two = aliased(User)
result = await session.scalars(
select(self._model)
.options(selectinload(self._model.object_user_one), selectinload(self._model.object_user_two))
.join(matched_user_one, self._model.object_user_one)
.join(matched_user_two, self._model.object_user_two)
.where(
(self._model.status == MatchStatusEnum.ONGOING)
& ((matched_user_one.user_id == user_id) | (matched_user_two.user_id == user_id))
)
)
matches = result.all()
if not matches:
raise MatchNotFoundError(user_id)
elif len(matches) > 1:
raise TooManyMatchesError(user_id)
return matches[0]
16 changes: 16 additions & 0 deletions src/core/exceptions/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,19 @@ def __init__(self, obj: DatabaseModel) -> None:

def __str__(self) -> str:
return self.detail


class MatchNotFoundError(Exception):
def __init__(self, user_id: str) -> None:
self.detail = f"Встреча пользователя с user_id {user_id} не найдена"

def __str__(self) -> str:
return self.detail


class TooManyMatchesError(Exception):
def __init__(self, user_id: str) -> None:
self.detail = f"У пользователя с user_id {user_id} " f"найдено несколько встреч на этой неделе"

def __str__(self) -> str:
return self.detail
Loading

0 comments on commit 634e8e7

Please sign in to comment.