Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
0966ebf
feat: custom email messages
niqzart Dec 14, 2025
a4b0b3e
feat: custom notification adaptation for telegram & email
niqzart Dec 14, 2025
c09b810
Merge [4129] Custom Notifications
niqzart Dec 17, 2025
43b7e33
devops-fix: band-aid fix for github's horrible breaking change
niqzart Feb 1, 2026
12affe2
feat: classroom events rst
ByrDen Oct 24, 2025
f127d5e
Merge Classroom Events
niqzart Feb 1, 2026
3258001
feat: switch sentry provider from glitchtip to self-hosted bugsink
niqzart Feb 8, 2026
77d35d8
fix: update vacancies link in supbot to sovlium
niqzart Feb 11, 2026
a83d0f3
feat: subscription service: promocode mub interface
sipmine Mar 8, 2026
3fabcb9
Merge [4108] Promocodes MUB
niqzart Mar 8, 2026
c2a5db1
feat: allow tutor to manage room metadata
niqzart Feb 1, 2026
3e02c33
fix: update lazy-fixtures for better lfc support
niqzart Feb 1, 2026
6a96c6a
feat: participant metadata management
niqzart Feb 1, 2026
ee753da
Conference and participant metadata (#102)
niqzart Mar 14, 2026
a1e462c
fix: use aiofiles for saving files
niqzart Feb 16, 2026
d52b8b1
feat: allow more image formats and add conversion & resizing for avatars
niqzart Feb 16, 2026
ee36264
feat: allow more image formats & add conversion in storage-2
niqzart Feb 16, 2026
e594d51
fix: include image-related asserts in testing for avatars & storage
niqzart Feb 16, 2026
b991637
Merge [4132] Image Conversion
niqzart Mar 15, 2026
01768a2
fix: install a plugin for avif support
niqzart Apr 4, 2026
3ff1aa2
Merge: Fix AVIF image support (#104)
niqzart Apr 25, 2026
251b22e
fix: enable ounce proxy
niqzart Apr 10, 2026
a5376b3
Merge: Ounce service (#105)
niqzart Apr 25, 2026
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
4 changes: 2 additions & 2 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ extend-ignore =
VNE003 WPS115

# # weird
PIE803 C101 FNE007 FNE008 N812 ANN101 ANN102 WPS110 WPS111 WPS114 WPS338 WPS407 WPS414 WPS440 VNE001 VNE002 CM001
PIE803 C101 FNE007 FNE008 N812 ANN101 ANN102 PT004 WPS110 WPS111 WPS114 WPS338 WPS407 WPS414 WPS440 VNE001 VNE002 CM001
# too many
WPS200 WPS201 WPS202 WPS203 WPS204 WPS210 WPS211 WPS212 WPS213 WPS214 WPS217 WPS218 WPS221 WPS224 WPS230 WPS231 WPS234 WPS235 WPS238
# "vague" imports
WPS347

# # broken
PIE798 WPS226 WPS354 WPS432 WPS473 WPS507 FNE004
PIE786 PIE798 WPS226 WPS354 WPS432 WPS473 WPS507 FNE004
# fails to understand `raise NotImplementedError` and overloading
U100
# fails to understand enums
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/development.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ jobs:
&& contains(github.event.pull_request.labels.*.name, 'ci:deployable')

runs-on: ubuntu-latest
environment: ${{ needs.namer.outputs.branch == 'staging' && github.actor == github.triggering_actor && 'staging' || 'manual-staging' }}
environment: manual-staging

env:
pull_image: ${{ secrets.DOCKER_USERNAME }}/${{ secrets.DOCKER_USERNAME }}:${{ needs.namer.outputs.tag }}
Expand Down
46 changes: 46 additions & 0 deletions alembic/versions/055_classroom_events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""classroom_events

Revision ID: 055
Revises: 054
Create Date: 2025-11-21 00:41:26.686639

"""

from typing import Sequence, Union

import sqlalchemy as sa

from alembic import op

# revision identifiers, used by Alembic.
revision: str = "055"
down_revision: Union[str, None] = "054"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None

EventKind = sa.Enum("CLASSROOM", name="eventkind")


def upgrade() -> None:
op.execute("TRUNCATE TABLE xi_back_2.scheduler_events")
EventKind.create(bind=op.get_bind())
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"scheduler_events",
sa.Column("kind", EventKind, nullable=False),
schema="xi_back_2",
)
op.add_column(
"scheduler_events",
sa.Column("classroom_id", sa.Integer(), nullable=True),
schema="xi_back_2",
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("scheduler_events", "classroom_id", schema="xi_back_2")
op.drop_column("scheduler_events", "kind", schema="xi_back_2")
# ### end Alembic commands ###
EventKind.drop(bind=op.get_bind())
54 changes: 54 additions & 0 deletions alembic/versions/056_promocodes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""promocodes

Revision ID: 056
Revises: 055
Create Date: 2026-01-14 13:05:54.325494

"""

from typing import Sequence, Union

import sqlalchemy as sa

from alembic import op

# revision identifiers, used by Alembic.
revision: str = "056"
down_revision: Union[str, None] = "055"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"promocodes",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("title", sa.String(length=100), nullable=False),
sa.Column("code", sa.String(length=10), nullable=False),
sa.Column("valid_from", sa.DateTime(timezone=True), nullable=True),
sa.Column("valid_until", sa.DateTime(timezone=True), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
sa.PrimaryKeyConstraint("id", name=op.f("pk_promocodes")),
schema="xi_back_2",
)
op.create_index(
op.f("ix_xi_back_2_promocodes_code"),
"promocodes",
["code"],
unique=True,
schema="xi_back_2",
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(
op.f("ix_xi_back_2_promocodes_code"),
table_name="promocodes",
schema="xi_back_2",
)
op.drop_table("promocodes", schema="xi_back_2")
# ### end Alembic commands ###
15 changes: 14 additions & 1 deletion app/common/aiogram_ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from typing import Any

from aiogram import Bot, Dispatcher, Router
from aiogram.client.session.aiohttp import AiohttpSession
from aiogram.client.telegram import TelegramAPIServer
from aiogram.filters import CommandObject
from aiogram.fsm.storage.redis import RedisStorage
from aiogram.methods import GetUpdates
Expand Down Expand Up @@ -101,7 +103,18 @@ async def maybe_initialize_from_config(
return

await self.initialize(
bot=Bot(bot_settings.token),
bot=Bot(
bot_settings.token,
session=(
None
if settings.telegram_server_base_url is None
else AiohttpSession(
api=TelegramAPIServer.from_base(
settings.telegram_server_base_url
)
)
),
),
dispatcher=Dispatcher(
storage=None if redis_dsn is None else RedisStorage.from_url(redis_dsn),
**dispatcher_kwargs,
Expand Down
8 changes: 7 additions & 1 deletion app/common/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ def is_testing_mode(self) -> bool:

cookie_domain: str = "localhost"
frontend_app_base_url: str = "https://app.sovlium.ru"
frontend_vacancies_base_url: str = "https://vacancy.sovlium.ru/vacancy"

password_reset_keys: FernetSettings = FernetSettings(encryption_ttl=60 * 60)
email_confirmation_keys: FernetSettings = FernetSettings(
Expand Down Expand Up @@ -191,6 +192,7 @@ def redis_supbot_dsn(self) -> str:

supbot: SupbotSettings | None = None
notifications_bot: TelegramBotSettings | None = None
telegram_server_base_url: str | None = None
telegram_webhook_base_url: str | None = None

sentry_dsn: str | None = None
Expand All @@ -208,9 +210,13 @@ def redis_supbot_dsn(self) -> str:
FaststreamIntegration(),
# other integrations are automatic
],
before_breadcrumb=before_breadcrumb,
traces_sample_rate=0,
profiles_sample_rate=0,
before_breadcrumb=before_breadcrumb,
send_default_pii=True,
max_request_body_size="always",
send_client_reports=False,
auto_session_tracking=False,
)


Expand Down
26 changes: 26 additions & 0 deletions app/common/filetype_ext.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from typing import Final

import filetype # type: ignore[import-untyped]
from filetype.types import image # type: ignore[import-untyped]

FILE_HEADER_SIZE: Final[int] = 8192

SUPPORTED_IMAGE_FORMATS: list[filetype.Type] = [
image.Avif(),
image.Bmp(),
image.Gif(),
image.Ico(),
image.Jpeg(),
image.Jpx(),
image.Png(),
image.Tiff(),
image.Webp(),
]


def match_filetype(obj: bytes, matchers: list[filetype.Type]) -> filetype.Type | None:
return filetype.match(obj, matchers)


def match_image_filetype(obj: bytes) -> filetype.Type | None:
return match_filetype(obj, SUPPORTED_IMAGE_FORMATS)
42 changes: 39 additions & 3 deletions app/common/livekit_ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
CreateRoomRequest,
ListParticipantsRequest,
ListRoomsRequest,
UpdateParticipantRequest,
UpdateRoomMetadataRequest,
)


Expand Down Expand Up @@ -50,23 +52,57 @@ def api(self) -> LiveKitAPI:
def room(self) -> RoomService:
return self.api.room

def generate_access_token(self, identity: str, name: str, room_name: str) -> str:
def generate_access_token(
self,
room_name: str,
identity: str,
name: str,
metadata: str = "",
) -> str:
return (
AccessToken(self.api_key, self.api_secret)
.with_identity(identity=identity)
.with_name(name=name)
.with_grants(VideoGrants(room_join=True, room=room_name))
.with_metadata(metadata=metadata)
).to_jwt()

async def list_rooms(self, room_names: list[str]) -> Iterator[Room]:
response = await self.room.list_rooms(ListRoomsRequest(names=room_names))
return (room for room in response.rooms)

async def find_or_create_room(self, room_name: str) -> Room:
return await self.room.create_room(CreateRoomRequest(name=room_name))
async def find_or_create_room(self, room_name: str, metadata: str) -> Room:
return await self.room.create_room(
CreateRoomRequest(
name=room_name,
metadata=metadata,
)
)

async def update_room_metadata(self, room_name: str, metadata: str) -> Room:
return await self.room.update_room_metadata(
UpdateRoomMetadataRequest(
room=room_name,
metadata=metadata,
)
)

async def list_room_participants(self, room_name: str) -> Iterator[ParticipantInfo]:
response = await self.room.list_participants(
ListParticipantsRequest(room=room_name)
)
return (participant for participant in response.participants)

async def update_participant_metadata(
self,
room_name: str,
identity: str,
metadata: str,
) -> ParticipantInfo:
return await self.room.update_participant(
UpdateParticipantRequest(
room=room_name,
identity=identity,
metadata=metadata,
)
)
16 changes: 15 additions & 1 deletion app/common/schemas/notifications_sch.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ class NotificationKind(StrEnum):
RECIPIENT_INVOICE_CREATED_V1 = auto()
STUDENT_RECIPIENT_INVOICE_PAYMENT_CONFIRMED_V1 = auto()

CUSTOM_V1 = auto()


class InvitationAcceptanceNotificationPayloadSchema(BaseModel):
kind: Literal[
Expand Down Expand Up @@ -49,11 +51,23 @@ class RecipientInvoiceNotificationPayloadSchema(BaseModel):
recipient_invoice_id: int


class CustomNotificationPayloadSchema(BaseModel):
kind: Literal[NotificationKind.CUSTOM_V1]

theme: str
pre_header: str
header: str
content: str
button_text: str
button_link: str


AnyNotificationPayloadSchema = Annotated[
InvitationAcceptanceNotificationPayloadSchema
| EnrollmentNotificationPayloadSchema
| ClassroomNotificationPayloadSchema
| RecipientInvoiceNotificationPayloadSchema,
| RecipientInvoiceNotificationPayloadSchema
| CustomNotificationPayloadSchema,
Field(discriminator="kind"),
]

Expand Down
16 changes: 15 additions & 1 deletion app/common/schemas/pochta_sch.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@


class EmailMessageKind(StrEnum):
CUSTOM_V1 = auto()

EMAIL_CONFIRMATION_V2 = auto()
EMAIL_CHANGE_V2 = auto()
PASSWORD_RESET_V2 = auto()
Expand All @@ -21,6 +23,17 @@ class EmailMessageKind(StrEnum):
STUDENT_RECIPIENT_INVOICE_PAYMENT_CONFIRMED_V1 = auto()


class CustomEmailMessagePayloadSchema(BaseModel):
kind: Literal[EmailMessageKind.CUSTOM_V1]

theme: str
pre_header: str
header: str
content: str
button_text: str
button_link: str


class TokenEmailMessagePayloadSchema(BaseModel):
kind: Literal[
EmailMessageKind.EMAIL_CONFIRMATION_V2,
Expand Down Expand Up @@ -60,7 +73,8 @@ class RecipientInvoiceNotificationEmailMessagePayloadSchema(


AnyEmailMessagePayload = Annotated[
TokenEmailMessagePayloadSchema
CustomEmailMessagePayloadSchema
| TokenEmailMessagePayloadSchema
| ClassroomNotificationEmailMessagePayloadSchema
| RecipientInvoiceNotificationEmailMessagePayloadSchema,
Field(discriminator="kind"),
Expand Down
Loading
Loading