Skip to content

Commit f660753

Browse files
authored
Tg group owner (#31)
## Изменения Добвалена возможность стать оунером группы
1 parent 6cb242f commit f660753

13 files changed

+213
-31
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""Create group request
2+
3+
Revision ID: 27dda7e6236a
4+
Revises: 62addefd9655
5+
Create Date: 2024-04-15 03:59:03.133907
6+
7+
"""
8+
9+
import sqlalchemy as sa
10+
from alembic import op
11+
12+
13+
# revision identifiers, used by Alembic.
14+
revision = '27dda7e6236a'
15+
down_revision = '62addefd9655'
16+
branch_labels = None
17+
depends_on = None
18+
19+
20+
def upgrade():
21+
op.create_table(
22+
'create_group_request',
23+
sa.Column('id', sa.Integer(), nullable=False),
24+
sa.Column('secret_key', sa.String(), nullable=False),
25+
sa.Column('owner_id', sa.Integer(), nullable=False),
26+
sa.Column('mapped_group_id', sa.Integer(), nullable=True),
27+
sa.Column('create_ts', sa.DateTime(), nullable=False),
28+
sa.Column('valid_ts', sa.DateTime(), nullable=False),
29+
sa.ForeignKeyConstraint(
30+
['mapped_group_id'],
31+
['group.id'],
32+
),
33+
sa.PrimaryKeyConstraint('id'),
34+
)
35+
36+
37+
def downgrade():
38+
op.drop_table('create_group_request')

social/exceptions.py

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
class SocialApiError(Exception):
2+
"""Корневая ошибка Social API"""
3+
4+
5+
class GroupRequestNotFound(SocialApiError):
6+
"""Не найдено запроса на создание группы"""
7+
8+
def __init__(self, user_id: int, secret_key: str, *args) -> None:
9+
self.user_id = user_id
10+
self.secret_key = secret_key
11+
super().__init__(*args)

social/handlers_telegram/base.py

+11
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
from functools import lru_cache
33
from textwrap import dedent
44

5+
from fastapi_sqlalchemy import db
56
from telegram import Update
67
from telegram.ext import Application, CommandHandler, ContextTypes
78

89
from social.settings import get_settings
10+
from social.utils.telegram_groups import approve_telegram_group
911

1012
from .handlers_viribus import register_handlers
1113
from .utils import CustomContext
@@ -24,6 +26,7 @@ def get_application():
2426
logger.info("Telegram API initialized successfully")
2527
# Общие хэндлеры
2628
app.add_handler(CommandHandler(callback=send_help, command="help"))
29+
app.add_handler(CommandHandler(callback=validate_group, command="validate", has_args=1))
2730

2831
# Хэндлеры конкретных чатов
2932
register_handlers(app)
@@ -43,3 +46,11 @@ async def send_help(update: Update, context: CustomContext):
4346
),
4447
parse_mode='markdown',
4548
)
49+
50+
51+
async def validate_group(update: Update, context: CustomContext):
52+
logger.info("Validation message received")
53+
with db():
54+
approve_telegram_group(update)
55+
res = await update.effective_message.delete()
56+
logger.info(f"Validation message handled, delete status = {res}")

social/models/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
from .create_group_request import CreateGroupRequest
12
from .group import TelegramChannel, TelegramChat, VkChat, VkGroup
23
from .webhook_storage import WebhookStorage
34

45

5-
__all__ = ['WebhookStorage', 'TelegramChannel', 'TelegramChat', 'VkGroup', 'VkChat']
6+
__all__ = ['WebhookStorage', 'TelegramChannel', 'TelegramChat', 'VkGroup', 'VkChat', 'CreateGroupRequest']

social/models/create_group_request.py

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from datetime import UTC, datetime, timedelta
2+
3+
import sqlalchemy as sa
4+
from sqlalchemy.orm import Mapped, mapped_column, relationship
5+
6+
from social.utils.string import random_string
7+
8+
from .base import Base
9+
from .group import Group
10+
11+
12+
class CreateGroupRequest(Base):
13+
id: Mapped[int] = mapped_column(primary_key=True)
14+
secret_key: Mapped[str] = mapped_column(default=lambda: random_string(32))
15+
owner_id: Mapped[int]
16+
mapped_group_id: Mapped[int | None] = mapped_column(sa.ForeignKey("group.id"))
17+
18+
create_ts: Mapped[datetime] = mapped_column(default=lambda: datetime.now(UTC))
19+
valid_ts: Mapped[datetime] = mapped_column(default=lambda: datetime.now(UTC) + timedelta(days=1))
20+
21+
mapped_group: Mapped[Group | None] = relationship(Group)

social/models/group.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class Group(Base):
1212
owner_id: Mapped[int | None]
1313

1414
is_deleted: Mapped[bool] = mapped_column(default=False)
15-
last_active_ts: Mapped[datetime | None]
15+
last_active_ts: Mapped[datetime]
1616

1717
create_ts: Mapped[datetime] = mapped_column(default=lambda: datetime.now(UTC))
1818
update_ts: Mapped[datetime] = mapped_column(default=lambda: datetime.now(UTC), onupdate=lambda: datetime.now(UTC))
@@ -44,7 +44,7 @@ class VkChat(Group):
4444

4545
class TelegramChannel(Group):
4646
id: Mapped[int] = mapped_column(sa.ForeignKey("group.id"), primary_key=True)
47-
channel_id: Mapped[int]
47+
channel_id: Mapped[int] = mapped_column(sa.BigInteger)
4848

4949
__mapper_args__ = {
5050
"polymorphic_identity": "tg_channel",
@@ -53,7 +53,7 @@ class TelegramChannel(Group):
5353

5454
class TelegramChat(Group):
5555
id: Mapped[int] = mapped_column(sa.ForeignKey("group.id"), primary_key=True)
56-
chat_id: Mapped[int]
56+
chat_id: Mapped[int] = mapped_column(sa.BigInteger)
5757

5858
__mapper_args__ = {
5959
"polymorphic_identity": "tg_chat",

social/routes/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import exceptions # noqa

social/routes/base.py

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from .discord import router as discord_router
1212
from .github import router as github_router
13+
from .group import router as group_router
1314
from .telegram import router as telegram_router
1415
from .vk import router as vk_router
1516

@@ -56,6 +57,7 @@ async def lifespan(app: FastAPI):
5657
)
5758

5859

60+
app.include_router(group_router)
5961
app.include_router(github_router)
6062
app.include_router(telegram_router)
6163
app.include_router(vk_router)

social/routes/exceptions.py

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from fastapi import Request
2+
from fastapi.responses import JSONResponse
3+
4+
from social.exceptions import GroupRequestNotFound
5+
6+
from .base import app
7+
8+
9+
@app.exception_handler(GroupRequestNotFound)
10+
def group_request_not_found(request: Request, exc: GroupRequestNotFound) -> JSONResponse:
11+
return JSONResponse(
12+
status_code=404,
13+
content={
14+
'details': 'Group request not found',
15+
'ru': 'Запрос на создание группы не найден',
16+
'user_id': exc.user_id,
17+
'secret_key': exc.secret_key,
18+
},
19+
)

social/routes/group.py

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import logging
2+
from datetime import UTC, datetime
3+
4+
from auth_lib.fastapi import UnionAuth
5+
from fastapi import APIRouter, Depends
6+
from fastapi_sqlalchemy import db
7+
from pydantic import BaseModel
8+
9+
from social.exceptions import GroupRequestNotFound
10+
from social.models.create_group_request import CreateGroupRequest
11+
from social.settings import get_settings
12+
13+
14+
router = APIRouter(prefix="/group", tags=['User defined groups'])
15+
settings = get_settings()
16+
logger = logging.getLogger(__name__)
17+
18+
19+
class GroupRequestGet(BaseModel):
20+
secret_key: str
21+
valid_ts: datetime
22+
23+
24+
class GroupGet(BaseModel):
25+
id: int
26+
27+
28+
@router.post('')
29+
def create_group_request(
30+
user: dict[str] = Depends(UnionAuth(["social.group.create"])),
31+
) -> GroupRequestGet:
32+
obj = CreateGroupRequest(owner_id=user.get("id"))
33+
db.session.add(obj)
34+
db.session.commit()
35+
return obj
36+
37+
38+
@router.get('')
39+
def validate_group_request(
40+
secret_key: str,
41+
user: dict[str] = Depends(UnionAuth(["social.group.create"])),
42+
) -> GroupGet | GroupRequestGet:
43+
obj = (
44+
db.session.query(CreateGroupRequest)
45+
.where(CreateGroupRequest.secret_key == secret_key, CreateGroupRequest.owner_id == user.get("id"))
46+
.one_or_none()
47+
)
48+
if obj is None or obj.valid_ts.replace(tzinfo=UTC) < datetime.now(UTC):
49+
raise GroupRequestNotFound(user_id=user.get("id"), secret_key=secret_key)
50+
51+
if obj.mapped_group_id is not None:
52+
return GroupGet.model_validate(obj.mapped_group, from_attributes=True)
53+
54+
return GroupRequestGet.model_validate(obj, from_attributes=True)

social/routes/telegram.py

+3-24
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import logging
2-
from asyncio import create_task
3-
from datetime import UTC, datetime
42

53
from fastapi import APIRouter, Request
64
from fastapi_sqlalchemy import db
75
from telegram import Update
86

97
from social.handlers_telegram import get_application
10-
from social.models import TelegramChannel, TelegramChat
118
from social.models.webhook_storage import WebhookStorage, WebhookSystems
129
from social.settings import get_settings
10+
from social.utils.telegram_groups import create_telegram_group
1311

1412

1513
router = APIRouter(prefix="/telegram", tags=["webhooks"])
@@ -33,27 +31,8 @@ async def telegram_webhook(request: Request):
3331
db.session.commit()
3432

3533
update = Update.de_json(data=request_data, bot=application.bot)
36-
add_msg = create_task(application.update_queue.put(update))
34+
await application.update_queue.put(update)
3735
try:
38-
chat = update.effective_chat
39-
obj = None
40-
if chat.type in ['group', 'supergroup']:
41-
obj = db.session.query(TelegramChat).where(TelegramChat.chat_id == chat.id).one_or_none()
42-
if obj is None:
43-
obj = TelegramChat(chat_id=chat.id)
44-
db.session.add(obj)
45-
elif chat.type == 'channel':
46-
obj = db.session.query(TelegramChannel).where(TelegramChannel.channel_id == chat.id).one_or_none()
47-
if obj is None:
48-
obj = TelegramChannel(channel_id=chat.id)
49-
db.session.add(obj)
50-
51-
obj.last_active_ts = datetime.now(UTC)
52-
db.session.commit()
53-
logger.debug(obj)
36+
create_telegram_group(update)
5437
except Exception as exc:
5538
logger.exception(exc)
56-
finally:
57-
await add_msg
58-
59-
return

social/routes/vk.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from fastapi_sqlalchemy import db
88
from pydantic import BaseModel, ConfigDict
99

10-
from social.handlers_telegram import get_application
1110
from social.models.group import VkChat, VkGroup
1211
from social.models.webhook_storage import WebhookStorage, WebhookSystems
1312
from social.settings import get_settings
@@ -17,7 +16,6 @@
1716
router = APIRouter(prefix="/vk", tags=['vk'])
1817
settings = get_settings()
1918
logger = logging.getLogger(__name__)
20-
application = get_application()
2119

2220

2321
class VkGroupCreate(BaseModel):
@@ -81,7 +79,8 @@ async def vk_webhook(request: Request) -> str:
8179

8280
@router.put('/{group_id}')
8381
def create_or_replace_group(
84-
group_id: int, group_info: VkGroupCreate,
82+
group_id: int,
83+
group_info: VkGroupCreate,
8584
user: dict[str] = Depends(UnionAuth(["social.group.create"])),
8685
) -> VkGroupCreateResponse:
8786
group = db.session.query(VkGroup).where(VkGroup.group_id == group_id).one_or_none()

social/utils/telegram_groups.py

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import logging
2+
from datetime import UTC, datetime
3+
4+
from fastapi_sqlalchemy import db
5+
from telegram import Update
6+
7+
from social.models import CreateGroupRequest, TelegramChannel, TelegramChat
8+
9+
10+
logger = logging.getLogger(__name__)
11+
12+
13+
def create_telegram_group(update: Update):
14+
chat = update.effective_chat
15+
obj = None
16+
if chat.type in ['group', 'supergroup']:
17+
obj = db.session.query(TelegramChat).where(TelegramChat.chat_id == chat.id).one_or_none()
18+
if obj is None:
19+
obj = TelegramChat(chat_id=chat.id)
20+
db.session.add(obj)
21+
elif chat.type == 'channel':
22+
obj = db.session.query(TelegramChannel).where(TelegramChannel.channel_id == chat.id).one_or_none()
23+
if obj is None:
24+
obj = TelegramChannel(channel_id=chat.id)
25+
db.session.add(obj)
26+
27+
if not obj:
28+
return
29+
30+
obj.last_active_ts = datetime.now(UTC)
31+
db.session.commit()
32+
return obj
33+
34+
35+
def approve_telegram_group(update: Update):
36+
logger.debug("Validation started")
37+
group = create_telegram_group(update)
38+
text = update.effective_message.text
39+
if not text or not group:
40+
logger.error("Telegram group not validated (secret=%s, group=%s)", text, group)
41+
return
42+
text = text.removeprefix('/validate').removeprefix('@ViribusSocialBot').strip()
43+
db.session.query(CreateGroupRequest).where(CreateGroupRequest.secret_key == text).update(
44+
{CreateGroupRequest.mapped_group_id: group.id}
45+
)
46+
logger.info("Telegram group %d validated (secret=%s)", group.id, text)

0 commit comments

Comments
 (0)