diff --git a/README.md b/README.md
deleted file mode 120000
index c3e5058..0000000
--- a/README.md
+++ /dev/null
@@ -1 +0,0 @@
-backend/README.md
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..da7b3d7
--- /dev/null
+++ b/README.md
@@ -0,0 +1,30 @@
+[](https://github.com/mbrav/signup_api/actions/workflows/fastapi.yml)
+[](https://opensource.org/licenses/BSD-3-Clause)
+[](https://wakatime.com/badge/user/54ad05ce-f39b-4fa3-9f2a-6fe4b1c53ba4/project/218dc651-c58d-4dfb-baeb-1f70c7bdf2c1)
+
+## FastAPI signup_api
+
+An 100% asynchronous Fast API service for signups and Telegram integration.
+
+### Intent
+
+As of now, this project is mainly an **architecture design** experimental ground with an abstract end goal in mind, rather than an actual functioning app and therefore would be most useful if used as a starting template example. The project uses [FastAPI](https://fastapi.tiangolo.com/) as a base framework with the following stack:
+
+- Integration with [SQLAlchemy's](https://www.sqlalchemy.org/) new ORM statement paradigm to be implemented in [v2.0](https://docs.sqlalchemy.org/en/20/changelog/migration_20.html);
+- Asynchronous PostgreSQL databse via [asyncpg](https://github.com/MagicStack/asyncpg), one of the fastest and high performant Database Client Libraries for python/asyncio;
+- Integration with Telegram library [aiogram](https://github.com/aiogram/aiogram) using its upcoming [v3.0 version](https://docs.aiogram.dev/en/dev-3.x/) with webhooks as an integration method with FastAPI;
+- A token authorization system using the [argon2 password hashing algorithm](https://github.com/P-H-C/phc-winner-argon2), the password-hashing function that won the [Password Hashing Competition (PHC)](https://www.password-hashing.net/);
+- Asynchronous task scheduling using [apscheduler](https://github.com/agronholm/apscheduler);
+- Designed to run efficently as possbile on a device such as the Raspberry Pi;
+- Asynchronous pytests using [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) and [httpx](https://www.python-httpx.org/) libraries instead of the synchronous requests library;
+- Vue.js 3.2 basic frontend with potential future experimentation with [vite](https://vitejs.dev/) and [vuetify](https://github.com/vuetifyjs/vuetify) framework.
+
+### Run FastAPI backend in Docker
+
+With docker-compose installed, do:
+
+```bash
+docker-compose up
+```
+
+Go to [0.0.0.0:8000/docs](http://0.0.0.0:8000/docs) for SwaggerUI
diff --git a/backend/app/middleware.py b/backend/app/middleware.py
index 3a89d88..af890aa 100644
--- a/backend/app/middleware.py
+++ b/backend/app/middleware.py
@@ -11,7 +11,7 @@
class ProcessTimeMiddleware(BaseHTTPMiddleware):
"""Add request process time to response headers with logger"""
- time_warning = 0.2
+ timeout_warning = 0.2
async def dispatch(self, request, call_next):
start_time = time.time()
@@ -25,11 +25,14 @@ async def dispatch(self, request, call_next):
f'client {request.client.host} port {request.client.port} ' \
f'time {process_time:.5f}s'
- slow_warning = process_time > self.time_warning
+ slow_warning = process_time > self.timeout_warning
if response.status_code < 203 and not slow_warning:
- logger.info(log_message)
- elif response.status_code < 500 or slow_warning:
- logger.warning(log_message)
+ logger.debug(log_message)
+ elif response.status_code < 500:
+ if slow_warning:
+ logger.warning(log_message)
+ else:
+ logger.info(log_message)
else:
logger.error(log_message)
diff --git a/backend/app/models/events.py b/backend/app/models/events.py
index ec828e6..028066c 100644
--- a/backend/app/models/events.py
+++ b/backend/app/models/events.py
@@ -10,13 +10,13 @@
class Event(BaseModel):
"""Event class"""
- name = Column(String(100), nullable=False)
+ name = Column(String(128), nullable=False)
description = Column(Text, default='')
start = Column(DateTime, nullable=False)
end = Column(DateTime, nullable=True)
google_modified = Column(DateTime, nullable=True)
- google_id = Column(String(30), nullable=True)
+ google_id = Column(String(128), nullable=True)
signups = relationship('Signup', back_populates='event')
@@ -39,15 +39,15 @@ def __init__(self,
async def get_current(
self,
db_session: AsyncSession,
- days_ago: int = 0,
+ hours_ago: int = 0,
limit: int = None,
offset: int = 0
):
- """Get events newer than days_ago
+ """Get events newer than hours_ago
Args:
db_session (AsyncSession): Current db session
- days_ago (int, optional): Ignore events before n days ago.
+ hours_ago (int, optional): Ignore events before n hours ago.
Defaults to 0.
limit (int, optional): limit result. Defaults to 0.
offset (int, optional): offset result. Defaults to 0.
@@ -57,7 +57,7 @@ async def get_current(
"""
db_query = select(self).where(
- self.start > datetime.utcnow() - timedelta(days=days_ago+1)
+ self.start > datetime.utcnow() - timedelta(hours=hours_ago)
).order_by(
self.start.asc())
@@ -70,11 +70,11 @@ async def get_current(
async def get_current_count(
self,
db_session: AsyncSession,
- days_ago: int = 0,
+ hours_ago: int = 0,
) -> int:
- """Return count only of events newer than days_ago"""
+ """Return count only of events newer than hours_ago"""
db_query = select([func.count()]).select_from(self).where(
- self.start > datetime.utcnow() - timedelta(days=days_ago))
+ self.start > datetime.utcnow() - timedelta(hours=hours_ago))
result = await db_session.execute(db_query)
return result.scalar()
diff --git a/backend/app/models/signups.py b/backend/app/models/signups.py
index 2dcc9a3..8676e71 100644
--- a/backend/app/models/signups.py
+++ b/backend/app/models/signups.py
@@ -29,6 +29,7 @@ def __init__(self,
notification: bool = True):
self.user_id = user_id
self.event_id = event_id
+ self.cancelled = cancelled
self.notification = notification
@classmethod
@@ -36,16 +37,16 @@ async def by_user(
self,
db_session: AsyncSession,
user_id: int,
- days_ago: Optional[Union[int, None]] = 0,
+ hours_ago: Optional[Union[int, None]] = 0,
limit: int = None,
offset: int = 0
):
- """Get signups of a user newer than days_ago
+ """Get signups of a user newer than hours_ago
Args:
db_session (AsyncSession): Current db session
user_id (int): User id
- days_ago (Union[int, None], optional): Ignore events before n days ago.
+ hours_ago (Union[int, None], optuser_idional): Ignore events before n hours ago.
Show all events if None. Defaults to 0.
limit (int, optional): limit result. Defaults to None.
offset (int, optional): offset result. Defaults to 0.
@@ -59,10 +60,10 @@ async def by_user(
joinedload(self.event)).filter(
self.user_id == user_id)
- if days_ago is not None:
+ if hours_ago is not None:
db_query = db_query.where(
Event.start > datetime.utcnow() -
- timedelta(days=days_ago))
+ timedelta(hours=hours_ago))
if limit:
db_query = db_query.limit(limit).offset(offset)
diff --git a/backend/app/models/tasks.py b/backend/app/models/tasks.py
index 731f7f9..3263178 100644
--- a/backend/app/models/tasks.py
+++ b/backend/app/models/tasks.py
@@ -13,10 +13,10 @@
class Task(BaseModel):
"""Task class"""
- name = Column(String(40), nullable=False)
+ name = Column(String(64), nullable=False)
kwargs = Column(PickleType, nullable=False)
result = Column(Text, nullable=True)
- status = Column(String(20), nullable=False)
+ status = Column(String(16), nullable=False)
delay_seconds = Column(Integer, nullable=False)
planned_for = Column(DateTime, nullable=True)
diff --git a/backend/app/models/users.py b/backend/app/models/users.py
index f39068d..42d03ae 100644
--- a/backend/app/models/users.py
+++ b/backend/app/models/users.py
@@ -7,13 +7,13 @@
class User(BaseModel):
"""User class"""
- username = Column(String(20), nullable=False)
- hashed_password = Column(String(130), nullable=True)
+ username = Column(String(16), nullable=False)
+ hashed_password = Column(String(128), nullable=True)
tg_id = Column(BigInteger, nullable=True)
- email = Column(String(30), nullable=True)
- first_name = Column(String(30), nullable=True)
- last_name = Column(String(30), nullable=True)
+ email = Column(String(32), nullable=True)
+ first_name = Column(String(32), nullable=True)
+ last_name = Column(String(32), nullable=True)
is_active = Column(Boolean(), default=True)
is_admin = Column(Boolean(), default=False)
diff --git a/backend/app/services/scheduler/google_cal.py b/backend/app/services/scheduler/google_cal.py
index dec6818..14c8f4f 100644
--- a/backend/app/services/scheduler/google_cal.py
+++ b/backend/app/services/scheduler/google_cal.py
@@ -1,12 +1,12 @@
import logging
from datetime import datetime, timedelta, timezone
-from typing import Dict, List, Optional
+from typing import List, Optional
+import httpx
from app import db
from app.models import Event
from app.schemas import EventCalIn
-from httpx import AsyncClient
-from pydantic import BaseModel
+from pydantic import BaseModel, validator
from sqlalchemy.ext.asyncio import AsyncSession
logger = logging.getLogger(__name__)
@@ -23,6 +23,18 @@ class EventListParams(BaseModel):
timeMin: Optional[str]
# timeMax: Optional[datetime]
+ @validator('singleEvents')
+ def singleEvents_valid(cls, val):
+ error = 'singleEvents must be "true" or false'
+ assert val in ('true', 'false'), error
+ return val
+
+ @validator('sanitizeHtml')
+ def sanitizeHtml_valid(cls, val):
+ error = 'sanitizeHtml must be "true" or false'
+ assert val in ('true', 'false'), error
+ return val
+
def sanitize_keys(d: dict) -> dict:
"""Sanitize dict unnecessary dict keys"""
@@ -57,9 +69,9 @@ def __init__(
self,
api_key: str,
cal_id: str,
- days_ago: int = 0):
+ hours_ago: int = 6):
- self.days_ago = days_ago
+ self.hours_ago = hours_ago
self._set_time()
self.params = EventListParams(
key=api_key, calendarId=cal_id, timeMin=self.iso)
@@ -69,30 +81,41 @@ def __init__(
def _set_time(self):
"""Setup event fetching time"""
- yesterday = datetime.utcnow() - timedelta(days=self.days_ago)
- self.iso = yesterday.astimezone().isoformat()
+ date_before = datetime.utcnow() - timedelta(hours=self.hours_ago)
+ self.iso = date_before.astimezone().isoformat()
async def fetch_events(self, sanitize: bool = False) -> List:
"""Fetch events from Google Calendar API"""
- async with AsyncClient() as client:
- response = await client.get(self.cal_url, params=self.params.dict())
- response_json = response.json()
- self.events = response_json.get('items', [])
-
- elapsed = response.elapsed.total_seconds()
- slow_warning = elapsed > 0.7
- log_message = f'Response {response.status_code}, ' \
- f'time {elapsed:.4f}s, ' \
- f'events {len(self.events)}, ' \
- f'yesterday {self.iso}, '
-
- if response.status_code == 200 and not slow_warning:
- logger.debug(log_message)
- elif slow_warning:
- logger.warning(log_message)
- else:
- logger.error(log_message)
+ async with httpx.AsyncClient() as client:
+ response = None
+ try:
+ response = await client.get(self.cal_url, params=self.params.dict())
+ except httpx.ConnectError as errc:
+ logger.error("Error Connecting:", errc)
+ except httpx.ConnectTimeout as errt:
+ logger.error("Timeout Error:", errt)
+ except httpx.RequestError as err:
+ logger.error("OOps: Something Else", err)
+ except httpx.HTTPError as errh:
+ logger.error("Http Error:", errh)
+
+ response_json = response.json()
+ self.events = response_json.get('items', [])
+
+ elapsed = response.elapsed.total_seconds()
+ slow_warning = elapsed > 0.7
+ log_message = f'Response {response.status_code}, ' \
+ f'time {elapsed:.4f}s, ' \
+ f'events {len(self.events)}, ' \
+ f'date_before {self.iso}, '
+
+ if response.status_code == 200 and not slow_warning:
+ logger.debug(log_message)
+ elif slow_warning:
+ logger.warning(log_message)
+ else:
+ logger.error(log_message)
async def update_events(self):
"""Update events in db or create new ones"""
@@ -137,9 +160,12 @@ async def _update_events_db(
google_modified=google_modified)
new_object = Event(**new_event.dict())
- await new_object.save(db_session)
- logger.debug(
- f'Added #{google_id} - {name} to db ')
+ try:
+ logger.debug(f'Adding #{google_id} - {name} to db ')
+ await new_object.save(db_session)
+ except Exception as e:
+ logger.error(
+ f'{e}, Failed to add #{google_id} - {name} to db ')
continue
event = event_db_dict[google_id]
diff --git a/backend/app/services/telegram/callbacks.py b/backend/app/services/telegram/callbacks.py
index bbd7ab0..2ce00d7 100644
--- a/backend/app/services/telegram/callbacks.py
+++ b/backend/app/services/telegram/callbacks.py
@@ -1,6 +1,6 @@
from enum import Enum
-from aiogram.dispatcher.filters.callback_data import CallbackData
+from aiogram.filters.callback_data import CallbackData
class PageNav(str, Enum):
diff --git a/backend/app/services/telegram/commands.py b/backend/app/services/telegram/commands.py
index 050f0bc..f722cdc 100644
--- a/backend/app/services/telegram/commands.py
+++ b/backend/app/services/telegram/commands.py
@@ -9,18 +9,18 @@ async def set_bot_commands(bot: Bot):
commands = [
BotCommand(
command='start',
- description=texts.command1_detail),
+ description=texts.ru.command1_detail),
BotCommand(
command='help',
- description=texts.command2_detail),
+ description=texts.ru.command2_detail),
BotCommand(
command='register',
- description=texts.command3_detail),
+ description=texts.ru.command3_detail),
BotCommand(
command='events',
- description=texts.command4_detail),
+ description=texts.ru.command4_detail),
BotCommand(
command='me',
- description=texts.command5_detail)]
+ description=texts.ru.command5_detail)]
await bot.set_my_commands(
commands=commands, scope=BotCommandScopeAllPrivateChats())
diff --git a/backend/app/services/telegram/handlers/handlers.py b/backend/app/services/telegram/handlers/handlers.py
index 45e82e8..d79a360 100644
--- a/backend/app/services/telegram/handlers/handlers.py
+++ b/backend/app/services/telegram/handlers/handlers.py
@@ -4,7 +4,7 @@
from typing import Union
from aiogram import types
-from aiogram.dispatcher.fsm.context import FSMContext
+from aiogram.fsm.context import FSMContext
from app import models, schemas
from app.config import settings
from app.db import Session
@@ -44,7 +44,7 @@ async def get_valid_state(call: types.CallbackQuery, state: FSMContext) -> FSMCo
seconds = 5
while seconds > 0:
await call.message.edit_text(
- texts.inline_expired_destroy.format(seconds=seconds),
+ texts.ru.inline_expired_destroy.format(seconds=seconds),
reply_markup=None)
await asyncio.sleep(1)
seconds -= 1
@@ -75,11 +75,12 @@ async def _user_get_or_create(
if registration:
if user:
- return await message.reply(
- texts.register_already.format(
+ await message.reply(
+ texts.ru.register_already.format(
username=message.from_user.username))
+ return user
await bot.send_message(
- message.from_user.id, texts.register_create)
+ message.from_user.id, texts.ru.register_create)
username = str(message.from_user.id) if not (
message.from_user.username) else message.from_user.username
@@ -94,7 +95,7 @@ async def _user_get_or_create(
created_user = await new_user.save(db_session)
await bot.send_message(
message.from_user.id,
- texts.register_success.format(
+ texts.ru.register_success.format(
username=created_user.username))
await bot.send_message(
settings.TELEGRAM_ADMIN,
@@ -102,16 +103,17 @@ async def _user_get_or_create(
except Exception as ex:
await bot.send_message(
message.from_user.id,
- texts.register_fail)
+ texts.ru.register_fail)
await bot.send_message(
settings.TELEGRAM_ADMIN,
f'Error creating account\n {repr(ex)}')
- return
+ return created_user
if not user:
- return await bot.send_message(
+ await bot.send_message(
message.from_user.id,
- texts.register_not)
+ texts.ru.register_not)
+ return None
return user
@@ -157,7 +159,7 @@ async def events_page(page_current: int = 1, limit: int = 10) -> Page:
elements_total = await models.Event.get_current_count(db_session)
pages_total = (elements_total // limit) + 1
- text = texts.events_page_body.format(
+ text = texts.ru.events_page_body.format(
page_current=page_current,
pages_total=pages_total,
elements_total=elements_total)
@@ -165,7 +167,7 @@ async def events_page(page_current: int = 1, limit: int = 10) -> Page:
event_ids = []
for index, event in enumerate(db_events):
event_ids.append(event.id)
- text += texts.events_page_detail.format(
+ text += texts.ru.events_page_detail.format(
index=emoji_num[index+1],
name=event.name,
start=time_text(event.start),
@@ -190,11 +192,11 @@ async def user_signup_page(call: types.CallbackQuery) -> str:
db_signups = await models.Signup.by_user(db_session, user.id)
elements_total = len(db_signups)
- text = texts.my_signups.format(
+ text = texts.ru.my_signups.format(
signup_count=elements_total)
for index, signup in enumerate(db_signups):
- text += texts.signups_page_detail.format(
+ text += texts.ru.signups_page_detail.format(
index=emoji_num[index+1],
name=signup.event.name,
start=time_text(signup.event.start),
@@ -202,7 +204,7 @@ async def user_signup_page(call: types.CallbackQuery) -> str:
return text
-async def _get_or_create_signup(call: types.CallbackQuery, event_id: int) -> Union[bool, models.Signup]:
+async def _get_or_create_signup(call: types.CallbackQuery, event_id: int) -> Union[None, models.Signup]:
"""Get or create new signup
Args:
@@ -210,25 +212,29 @@ async def _get_or_create_signup(call: types.CallbackQuery, event_id: int) -> Uni
event_id (int): Id of the event in the db table
Returns:
- Union[bool, models.Signup]: False or model
+ Union[None, models.Signup]: None or model
"""
async with Session() as db_session:
try:
user = await models.User.get(
db_session, tg_id=call.from_user.id, raise_404=False)
- except Exception:
- await call.message.edit_text(
- texts.register_not,
- reply_markup=None)
- return False
- try:
event = await models.Event.get(db_session, id=event_id)
+ if not user:
+ await call.message.edit_text(
+ texts.ru.register_not,
+ reply_markup=None)
+ return None
+ if not event:
+ raise Exception(f'Event with id {event_id} not found')
except Exception:
await call.message.edit_text(
- texts.inline_fail,
+ texts.ru.inline_fail,
reply_markup=None)
- return False
+ await bot.send_message(
+ settings.TELEGRAM_ADMIN,
+ f'Event with id {event_id} not found')
+ return None
get_signup = await models.Signup.get_list(
db_session=db_session,
diff --git a/backend/app/services/telegram/handlers/inlines.py b/backend/app/services/telegram/handlers/inlines.py
index 5f9ad41..c22a751 100644
--- a/backend/app/services/telegram/handlers/inlines.py
+++ b/backend/app/services/telegram/handlers/inlines.py
@@ -1,5 +1,5 @@
from aiogram import F, types
-from aiogram.dispatcher.fsm.context import FSMContext
+from aiogram.fsm.context import FSMContext
from .. import texts
from ..callbacks import Action, EventCallback, MeCallback, PageNav
@@ -34,7 +34,7 @@ async def inline_events_list(call: types.CallbackQuery, state: FSMContext):
if not page:
return await bot.send_message(
call.from_user.id,
- texts.inline_fail)
+ texts.ru.inline_fail)
await state.update_data(page_current=page_current)
await state.update_data(elements_ids=page.elements_ids)
@@ -60,33 +60,39 @@ async def inline_event_detail(
if not event:
return await bot.send_message(
call.from_user.id,
- texts.inline_fail)
+ texts.ru.inline_fail)
signup = None
if action is Action.signup:
signup = await signup_create(call, event_id)
- await bot.send_message(
- call.from_user.id,
- texts.signup_success.format(
- name=event.name,
- start=time_text(event.end),
- end=time_text(event.start, time_only=True)))
+ if signup:
+ await bot.send_message(
+ call.from_user.id,
+ texts.ru.signup_success.format(
+ name=event.name,
+ start=time_text(event.end),
+ end=time_text(event.start, time_only=True)))
+ else:
+ return
if action is Action.signup_cancel:
signup = await signup_cancel(call, event_id)
- await bot.send_message(
- call.from_user.id,
- texts.signup_cancel.format(
- name=event.name,
- start=time_text(event.end),
- end=time_text(event.start, time_only=True)))
+ if signup:
+ await bot.send_message(
+ call.from_user.id,
+ texts.ru.signup_cancel.format(
+ name=event.name,
+ start=time_text(event.end),
+ end=time_text(event.start, time_only=True)))
+ else:
+ return
selected = True if signup and action is not Action.signup_cancel else False
reply_markup = signup_detail_nav(
id=event_id, selected=selected)
await call.message.edit_text(
- texts.event_detail.format(
+ texts.ru.event_detail.format(
name=event.name,
start=time_text(event.end),
end=time_text(event.start, time_only=True)),
@@ -108,7 +114,7 @@ async def inline_me(
# Temp place holder
if action in (Action.account, Action.settings):
user_info = call.from_user
- text = texts.help_text_extra.format(
+ text = texts.ru.help_text_extra.format(
first_name=user_info.first_name,
last_name=user_info.first_name,
username=user_info.username,
@@ -118,7 +124,7 @@ async def inline_me(
if action is Action.back:
user = await user_profile(call)
signup_count = await user_signup_count(user.id)
- text = texts.my_account.format(signup_count=signup_count)
+ text = texts.ru.my_account.format(signup_count=signup_count)
await call.message.edit_text(
text,
diff --git a/backend/app/services/telegram/handlers/main.py b/backend/app/services/telegram/handlers/main.py
index 468120a..a7badac 100644
--- a/backend/app/services/telegram/handlers/main.py
+++ b/backend/app/services/telegram/handlers/main.py
@@ -1,6 +1,6 @@
from aiogram import F, types
-from aiogram.dispatcher.filters import Command
-from aiogram.dispatcher.fsm.context import FSMContext
+from aiogram.filters import Command
+from aiogram.fsm.context import FSMContext
from app.config import settings
from .. import texts
@@ -11,7 +11,7 @@
from .states import BotState
-@dp.message(Command(commands=['start']), state='*')
+@dp.message(Command(commands=['start']))
@dp.message(F.text.in_({'start', 'begin'}))
async def start(message: types.Message, state: FSMContext):
await state.set_state(None)
@@ -19,23 +19,23 @@ async def start(message: types.Message, state: FSMContext):
message.from_user.id,
sticker=(texts.lotus_sheep))
await message.reply(
- texts.start_text.format(version=settings.VERSION))
+ texts.ru.start_text.format(version=settings.VERSION))
-@dp.message(Command(commands=['register']), state='*')
+@dp.message(Command(commands=['register']))
@dp.message(F.text.in_({'register', 'login'}))
async def register(message: types.Message, state: FSMContext):
await state.set_state(None)
await user_registration(message)
-@dp.message(Command(commands=['help']), state='*')
+@dp.message(Command(commands=['help']))
@dp.message(F.text.in_({'help', 'fuck'}))
async def help(message: types.Message, state: FSMContext):
user_info = message.from_user
await state.set_state(None)
await message.reply(
- texts.help_text_extra.format(
+ texts.ru.help_text_extra.format(
first_name=user_info.first_name,
last_name=user_info.first_name,
username=user_info.username,
@@ -43,7 +43,7 @@ async def help(message: types.Message, state: FSMContext):
id=user_info.id))
-@dp.message(Command(commands=['events']), state='*')
+@dp.message(Command(commands=['events']))
@dp.message(F.text.in_({'events', 'calendar'}))
async def events(message: types.Message, state: FSMContext):
page = await events_page()
@@ -58,7 +58,7 @@ async def events(message: types.Message, state: FSMContext):
ids=page.elements_ids))
-@dp.message(Command(commands=['me']), state='*')
+@dp.message(Command(commands=['me']))
@dp.message(F.text.in_({'me', 'my'}))
async def me(message: types.Message, state: FSMContext):
@@ -69,5 +69,5 @@ async def me(message: types.Message, state: FSMContext):
await bot.send_message(
message.chat.id,
- texts.my_account.format(signup_count=signup_count),
+ texts.ru.my_account.format(signup_count=signup_count),
reply_markup=me_keyboard(action=None))
diff --git a/backend/app/services/telegram/handlers/states.py b/backend/app/services/telegram/handlers/states.py
index 011344b..0b84478 100644
--- a/backend/app/services/telegram/handlers/states.py
+++ b/backend/app/services/telegram/handlers/states.py
@@ -2,7 +2,7 @@
from typing import Any, Optional
-from aiogram.dispatcher.fsm.state import State, StatesGroup
+from aiogram.fsm.state import State, StatesGroup
from pydantic import BaseModel
diff --git a/backend/app/services/telegram/keyboards.py b/backend/app/services/telegram/keyboards.py
index 29c3571..4c2e154 100644
--- a/backend/app/services/telegram/keyboards.py
+++ b/backend/app/services/telegram/keyboards.py
@@ -21,13 +21,13 @@
pagination_nav = [
InlineKeyboardButton(
- text=texts.inline_signup_button_1,
+ text=texts.ru.inline_signup_button_1,
callback_data=EventCallback(page_nav=PageNav.left).pack()),
InlineKeyboardButton(
- text=texts.inline_signup_button_2,
+ text=texts.ru.inline_signup_button_2,
callback_data=EventCallback(page_nav=PageNav.center).pack()),
InlineKeyboardButton(
- text=texts.inline_signup_button_3,
+ text=texts.ru.inline_signup_button_3,
callback_data=EventCallback(page_nav=PageNav.right).pack())
]
@@ -37,22 +37,22 @@ def me_keyboard(action: Action):
if action and action != Action.back:
keys.append(InlineKeyboardButton(
- text=texts.inline_signup_action_1,
+ text=texts.ru.inline_signup_action_1,
callback_data=MeCallback(
action=Action.back).pack()))
if action != Action.signups:
keys.append(InlineKeyboardButton(
- text=texts.inline_me_button_1,
+ text=texts.ru.inline_me_button_1,
callback_data=MeCallback(
action=Action.signups).pack()))
if action != Action.account:
keys.append(InlineKeyboardButton(
- text=texts.inline_me_button_2,
+ text=texts.ru.inline_me_button_2,
callback_data=MeCallback(
action=Action.account).pack()))
if action != Action.settings:
keys.append(InlineKeyboardButton(
- text=texts.inline_me_button_3,
+ text=texts.ru.inline_me_button_3,
callback_data=MeCallback(
action=Action.settings).pack()))
@@ -78,21 +78,21 @@ def signup_detail_nav(
keys = [
InlineKeyboardButton(
- text=texts.inline_signup_action_1,
+ text=texts.ru.inline_signup_action_1,
callback_data=EventCallback(page_nav=PageNav.center).pack())
]
if selected is True:
keys.append(
InlineKeyboardButton(
- text=texts.inline_signup_action_3,
+ text=texts.ru.inline_signup_action_3,
callback_data=EventCallback(
action=Action.signup_cancel,
option_id=id).pack()))
else:
keys.append(
InlineKeyboardButton(
- text=texts.inline_signup_action_2,
+ text=texts.ru.inline_signup_action_2,
callback_data=EventCallback(
action=Action.signup,
option_id=id).pack()))
@@ -100,14 +100,14 @@ def signup_detail_nav(
if notification is True:
keys.append(
InlineKeyboardButton(
- text=texts.inline_notify_on,
+ text=texts.ru.inline_notify_on,
callback_data=EventCallback(
action=Action.notify_toggle,
option_id=id).pack()))
elif notification is False:
keys.append(
InlineKeyboardButton(
- text=texts.inline_notify_off,
+ text=texts.ru.inline_notify_off,
callback_data=EventCallback(
action=Action.notify_toggle,
option_id=id).pack()))
diff --git a/backend/app/services/telegram/loader.py b/backend/app/services/telegram/loader.py
index efc5d07..80be863 100644
--- a/backend/app/services/telegram/loader.py
+++ b/backend/app/services/telegram/loader.py
@@ -1,9 +1,9 @@
from aiogram import Bot, Dispatcher
+from aiogram.fsm.storage.memory import MemoryStorage
from app.config import settings
bot = Bot(
token=settings.TELEGRAM_TOKEN.get_secret_value(),
- parse_mode='HTML'
-)
-
-dp = Dispatcher()
+ parse_mode='HTML')
+storage = MemoryStorage()
+dp = Dispatcher(storage=storage)
diff --git a/backend/app/services/telegram/routes.py b/backend/app/services/telegram/routes.py
index a773b00..01aa778 100644
--- a/backend/app/services/telegram/routes.py
+++ b/backend/app/services/telegram/routes.py
@@ -18,7 +18,7 @@ async def feed_update(update: Dict[str, Any]) -> None:
if settings.WEBHOOK_USE:
await dp.feed_webhook_update(bot, telegram_update)
else:
- await dp.feed_update(bot, telegram_update)
+ await dp.feed_raw_update(bot, update)
@router.post(path='')
@@ -33,11 +33,11 @@ async def on_startup() -> None:
Bot.set_current(bot)
await set_bot_commands(bot)
if settings.WEBHOOK_USE:
- current_webhook = await bot.get_webhook_info()
- if current_webhook.url != settings.WEBHOOK_PATH:
- await bot.set_webhook(
- url=settings.WEBHOOK_URL,
- certificate=settings.SSL_PUBLIC)
+ await bot.set_webhook(url=settings.WEBHOOK_URL)
+ # current_webhook = await bot.get_webhook_info()
+ # if current_webhook.url != settings.WEBHOOK_PATH:
+ # await bot.set_webhook(
+ # url=settings.WEBHOOK_URL)
else:
bot.get_updates()
await bot.send_message(settings.TELEGRAM_ADMIN, '🤖🟢Signup Bot Startup')
diff --git a/backend/app/services/telegram/texts/__init__.py b/backend/app/services/telegram/texts/__init__.py
index 14afea0..5692a41 100644
--- a/backend/app/services/telegram/texts/__init__.py
+++ b/backend/app/services/telegram/texts/__init__.py
@@ -1,4 +1,3 @@
-from .keyboards import *
-from .main import *
+from . import en, ru
from .stickers import *
diff --git a/backend/app/services/telegram/texts/en/__init__.py b/backend/app/services/telegram/texts/en/__init__.py
new file mode 100644
index 0000000..26a1f8b
--- /dev/null
+++ b/backend/app/services/telegram/texts/en/__init__.py
@@ -0,0 +1,3 @@
+
+from .keyboards import *
+from .main import *
diff --git a/backend/app/services/telegram/texts/keyboards.py b/backend/app/services/telegram/texts/en/keyboards.py
similarity index 100%
rename from backend/app/services/telegram/texts/keyboards.py
rename to backend/app/services/telegram/texts/en/keyboards.py
diff --git a/backend/app/services/telegram/texts/main.py b/backend/app/services/telegram/texts/en/main.py
similarity index 99%
rename from backend/app/services/telegram/texts/main.py
rename to backend/app/services/telegram/texts/en/main.py
index 8fec88a..90472eb 100644
--- a/backend/app/services/telegram/texts/main.py
+++ b/backend/app/services/telegram/texts/en/main.py
@@ -25,7 +25,6 @@
start_text = """
🤖Hi!
This is signup_api v{version}!
-Powered by aiogram
""" + help_text
my_account = """
@@ -66,6 +65,7 @@
inline_expired_destroy = inline_expired + """
🗑️The following message will self-destruct in {seconds} seconds.
+Please wait
"""
events_page_body = """
diff --git a/backend/app/services/telegram/texts/ru/__init__.py b/backend/app/services/telegram/texts/ru/__init__.py
new file mode 100644
index 0000000..26a1f8b
--- /dev/null
+++ b/backend/app/services/telegram/texts/ru/__init__.py
@@ -0,0 +1,3 @@
+
+from .keyboards import *
+from .main import *
diff --git a/backend/app/services/telegram/texts/ru/keyboards.py b/backend/app/services/telegram/texts/ru/keyboards.py
new file mode 100644
index 0000000..dcdcf0d
--- /dev/null
+++ b/backend/app/services/telegram/texts/ru/keyboards.py
@@ -0,0 +1,14 @@
+inline_me_button_1 = '📝Мои записи'
+inline_me_button_2 = '👤Мой профиль'
+inline_me_button_3 = '⚙️Мои настройки'
+
+inline_signup_button_1 = '⬅️'
+inline_signup_button_2 = 'Первая страница'
+inline_signup_button_3 = '➡️'
+
+inline_signup_action_1 = '🔙 Назад'
+inline_signup_action_2 = '📝 Записаться'
+inline_signup_action_3 = '❌ Отмена'
+
+inline_notify_on = '🔔вкл.'
+inline_notify_off = '🔕выкл.'
diff --git a/backend/app/services/telegram/texts/ru/main.py b/backend/app/services/telegram/texts/ru/main.py
new file mode 100644
index 0000000..8093a42
--- /dev/null
+++ b/backend/app/services/telegram/texts/ru/main.py
@@ -0,0 +1,111 @@
+command1_detail = 'Начать бот'
+command2_detail = 'Получить помощь'
+command3_detail = 'Регистрация'
+command4_detail = 'Список занятий'
+command5_detail = 'Моя учётная запись'
+
+help_text = f"""
+Команды:
+/start - {command1_detail}
+/register - {command2_detail}
+/events - {command3_detail}
+/me - {command4_detail}
+/help - {command5_detail}
+"""
+
+help_text_extra = help_text + """
+Ваша информация:
+🔸Имя: {first_name}
+🔸Фамилия: {last_name}
+🔸Никнейм: {username}
+🔸Код языка: {lang}
+🔸id: {id}
+"""
+
+start_text = """
+🤖Привет!
+Это signup_api v{version}!
+""" + help_text
+
+my_account = """
+👤Моя учётная запись
+📝Кол-во: {signup_count}
+"""
+
+my_signups = """
+👤Мои записи
+📝Кол-во: {signup_count}
+"""
+
+register_create = '🤖Создаю новую учётную запись ...'
+
+register_not = """
+🤖Вы не зарегистрированные ...
+🔧Пожалуйста, пройдите регистрацию: /register
+"""
+
+register_success = 'Привет {username}, вы теперь зарегистрированы!'
+
+register_already = 'Привет {username}, вы уже зарегистрированы!'
+
+register_fail = """
+🤖Произошёл сбой при создании учётной записи ...
+🔧Свяжитесь с поддержкой
+"""
+
+inline_fail = """
+🤖Просим прощения, бот где-то поломался, но не сильно ...
+🔧Начните заново: /start
+"""
+
+inline_expired = """
+🤖Просим прощения, но данное собщение болье не действительно ...
+🔧Начните заново: /start
+"""
+
+inline_expired_destroy = inline_expired + """
+🗑️Данное сообщение удалится через {seconds} секунд.
+Подождите
+"""
+
+events_page_body = """
+Страница: {page_current} из {pages_total}
+Занятий: {elements_total}
+"""
+
+events_page_detail = """
+{index} - {name}
+{start} - {end}
+"""
+
+signups_page_detail = """
+{index} - {name}
+{start} - {end}
+"""
+
+signup_success = """
+Успешная запись!
+
+📝{name}
+{start} - {end}
+
+Вы будете уведомлены до начала занятий
+Нажмите /me чтобы просмотреть все ваши записи
+"""
+
+signup_cancel = """
+Запись удалена:
+
+❌{name}
+{start} - {end}
+"""
+
+event_detail = """
+{name}
+{start} - {end}
+"""
+
+signup_detail = """
+{name}
+{start} - {end}
+"""
diff --git a/backend/pyproject.toml b/backend/pyproject.toml
index 44a340f..60ceb5c 100644
--- a/backend/pyproject.toml
+++ b/backend/pyproject.toml
@@ -6,19 +6,19 @@ authors = ["mbrav "]
[tool.poetry.dependencies]
python = "^3.8"
-fastapi = "^0.78.0"
-fastapi-pagination = "^0.9"
+fastapi = "^0.85"
+fastapi-pagination = "^0.10"
pydantic = {extras = ["email", "dotenv"], version = "^1.9"}
-uvicorn = {extras = ["standard"], version = "^0.17"}
+uvicorn = {extras = ["standard"], version = "^0.18"}
SQLAlchemy = "^1.4"
-asyncpg = "*"
+asyncpg = "^0.26"
python-jose = {extras = ["cryptography"], version = "^3.3"}
passlib = {extras = ["argon2"], version = "^1.7"}
-python-multipart = "*"
-httpx = {extras = ["http2"], version = "^0.22"}
+python-multipart = "0.0.5"
+httpx = {extras = ["http2"], version = "^0.23"}
APScheduler = "^3.9"
-transliterate = "*"
-aiogram = "^3.0.0b3"
+transliterate = "^1.10"
+aiogram = "^3.0.0b5"
[tool.poetry.dev-dependencies]