тестово events события вместо telegram updates
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
from typing import Any
|
||||
from typing import Annotated, Literal, Union
|
||||
|
||||
from fastapi.routing import APIRouter
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from src import deps, dto
|
||||
|
||||
@@ -58,210 +58,74 @@ async def attach_channel_to_workspace(input: AttachChannelToWorkspaceRequest) ->
|
||||
return await deps.get_usecase().attach_channel_to_workspace(input=input_data)
|
||||
|
||||
|
||||
@internal_router.post('/telegram/updates')
|
||||
async def handle_telegram_update(update: dict[str, Any]) -> None:
|
||||
"""Обрабатывает системные события от Golang бота (echotron формат)"""
|
||||
import json
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Логируем что пришло для отладки
|
||||
log.info('Received update from Golang bot, keys: %s', list(update.keys()))
|
||||
|
||||
# Обрабатываем ChatJoinRequest (запрос на вступление в канал)
|
||||
if 'chat_join_request' in update:
|
||||
log.info('ChatJoinRequest event: %s', json.dumps(update['chat_join_request'], ensure_ascii=False))
|
||||
await _handle_chat_join_request(update['chat_join_request'])
|
||||
return
|
||||
|
||||
# Обрабатываем ChatMember (пользователь вступил/вышел из канала)
|
||||
if 'chat_member' in update:
|
||||
event = update['chat_member']
|
||||
old_status = event.get('old_chat_member', {}).get('status')
|
||||
new_status = event.get('new_chat_member', {}).get('status')
|
||||
log.info('ChatMember event: status change %s -> %s', old_status, new_status)
|
||||
await _handle_chat_member_updated(event)
|
||||
return
|
||||
|
||||
# Обрабатываем MyChatMember (бот добавлен/удален из канала)
|
||||
if 'my_chat_member' in update:
|
||||
event = update['my_chat_member']
|
||||
old_status = event.get('old_chat_member', {}).get('status')
|
||||
new_status = event.get('new_chat_member', {}).get('status')
|
||||
log.info('MyChatMember event: bot status changed %s -> %s in chat', old_status, new_status)
|
||||
await _handle_my_chat_member(event)
|
||||
return
|
||||
|
||||
log.warning('Unknown update type from Golang bot: %s', list(update.keys()))
|
||||
class SubscriptionEventRequest(BaseModel):
|
||||
type: Literal['subscription']
|
||||
user_telegram_id: int
|
||||
invite_link: str
|
||||
username: str | None = None
|
||||
first_name: str | None = None
|
||||
last_name: str | None = None
|
||||
|
||||
|
||||
async def _handle_chat_join_request(event: dict[str, Any]) -> None:
|
||||
"""Обработка запроса на вступление в канал через invite link"""
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
chat = event.get('chat', {})
|
||||
if chat.get('type') not in ['channel', 'supergroup']:
|
||||
return
|
||||
|
||||
from_user = event.get('from')
|
||||
if not from_user or not from_user.get('id'):
|
||||
user_chat_id = event.get('user_chat_id')
|
||||
if not user_chat_id:
|
||||
log.error('No user or user_chat_id in chat_join_request event')
|
||||
return
|
||||
from_user = {'id': user_chat_id}
|
||||
|
||||
invite_link_obj = event.get('invite_link', {})
|
||||
invite_link = invite_link_obj.get('invite_link')
|
||||
if not invite_link:
|
||||
log.debug('No invite_link in chat_join_request event')
|
||||
return
|
||||
|
||||
await deps.get_usecase().handle_subscription(
|
||||
user_telegram_id=from_user['id'],
|
||||
username=from_user.get('username'),
|
||||
invite_link=invite_link,
|
||||
first_name=from_user.get('first_name'),
|
||||
last_name=from_user.get('last_name'),
|
||||
)
|
||||
class UnsubscriptionEventRequest(BaseModel):
|
||||
type: Literal['unsubscription']
|
||||
user_telegram_id: int
|
||||
channel_telegram_id: int
|
||||
|
||||
|
||||
async def _handle_chat_member_updated(event: dict[str, Any]) -> None:
|
||||
"""Обработка изменения статуса участника канала (вступление/выход)"""
|
||||
import logging
|
||||
class BotAddedEventRequest(dto.ConnectProjectInput):
|
||||
type: Literal['bot_added']
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
chat = event.get('chat', {})
|
||||
if chat.get('type') not in ['channel', 'supergroup']:
|
||||
return
|
||||
class BotRemovedEventRequest(dto.DisconnectProjectByTgIdInput):
|
||||
type: Literal['bot_removed']
|
||||
|
||||
member_user = event.get('new_chat_member', {}).get('user')
|
||||
if not member_user:
|
||||
log.error('No user in chat_member_updated event')
|
||||
return
|
||||
|
||||
old_member = event.get('old_chat_member', {})
|
||||
new_member = event.get('new_chat_member', {})
|
||||
class BotPermissionsEventRequest(dto.UpdateProjectPermissionsInput):
|
||||
type: Literal['bot_permissions']
|
||||
|
||||
old_status = old_member.get('status')
|
||||
new_status = new_member.get('status')
|
||||
|
||||
# Проверяем что пользователь присоединился к каналу
|
||||
was_not_member = old_status in ['left', 'kicked']
|
||||
is_now_member = new_status in ['member', 'administrator', 'creator']
|
||||
user_joined = was_not_member and is_now_member
|
||||
EventRequest = Annotated[
|
||||
Union[
|
||||
SubscriptionEventRequest,
|
||||
UnsubscriptionEventRequest,
|
||||
BotAddedEventRequest,
|
||||
BotRemovedEventRequest,
|
||||
BotPermissionsEventRequest,
|
||||
],
|
||||
Field(discriminator='type'),
|
||||
]
|
||||
|
||||
# Проверяем что пользователь покинул канал
|
||||
was_member = old_status in ['member', 'administrator', 'creator', 'restricted']
|
||||
is_now_not_member = new_status in ['left', 'kicked']
|
||||
user_left = was_member and is_now_not_member
|
||||
|
||||
if user_joined:
|
||||
invite_link_obj = event.get('invite_link', {})
|
||||
invite_link = invite_link_obj.get('invite_link') if invite_link_obj else None
|
||||
if not invite_link:
|
||||
log.debug('No invite_link in chat_member_updated event (user joined)')
|
||||
return
|
||||
|
||||
@internal_router.post('/events')
|
||||
async def handle_event(input: EventRequest) -> None:
|
||||
if isinstance(input, SubscriptionEventRequest):
|
||||
await deps.get_usecase().handle_subscription(
|
||||
user_telegram_id=member_user['id'],
|
||||
username=member_user.get('username'),
|
||||
invite_link=invite_link,
|
||||
first_name=member_user.get('first_name'),
|
||||
last_name=member_user.get('last_name'),
|
||||
user_telegram_id=input.user_telegram_id,
|
||||
username=input.username,
|
||||
invite_link=input.invite_link,
|
||||
first_name=input.first_name,
|
||||
last_name=input.last_name,
|
||||
)
|
||||
return
|
||||
|
||||
elif user_left:
|
||||
if isinstance(input, UnsubscriptionEventRequest):
|
||||
await deps.get_usecase().handle_unsubscription(
|
||||
user_telegram_id=member_user['id'],
|
||||
channel_telegram_id=chat['id'],
|
||||
user_telegram_id=input.user_telegram_id,
|
||||
channel_telegram_id=input.channel_telegram_id,
|
||||
)
|
||||
|
||||
|
||||
async def _handle_my_chat_member(event: dict[str, Any]) -> None:
|
||||
"""Обработка добавления/удаления бота из канала"""
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
chat = event.get('chat', {})
|
||||
if chat.get('type') not in ['channel', 'supergroup']:
|
||||
return
|
||||
|
||||
from_user = event.get('from')
|
||||
if not from_user:
|
||||
log.error('No user in my_chat_member event')
|
||||
if isinstance(input, BotAddedEventRequest):
|
||||
data = dto.ConnectProjectInput(**input.dict(exclude={'type'}))
|
||||
await deps.get_usecase().tg_add_project(input=data)
|
||||
return
|
||||
|
||||
old_member = event.get('old_chat_member', {})
|
||||
new_member = event.get('new_chat_member', {})
|
||||
|
||||
old_status = old_member.get('status')
|
||||
new_status = new_member.get('status')
|
||||
|
||||
# Проверяем что бот был удален из канала
|
||||
was_member = old_status in ['administrator', 'member']
|
||||
is_now_not_member = new_status in ['left', 'kicked']
|
||||
bot_removed = was_member and is_now_not_member
|
||||
|
||||
if bot_removed:
|
||||
# Отключаем проект
|
||||
disconnect_input = dto.DisconnectProjectByTgIdInput(
|
||||
telegram_id=chat['id'],
|
||||
user_telegram_id=from_user['id'],
|
||||
)
|
||||
try:
|
||||
await deps.get_usecase().disconnect_project_by_tg_id(input=disconnect_input)
|
||||
log.info('Bot removed from channel %s by user %s', chat['id'], from_user['id'])
|
||||
except Exception as e:
|
||||
log.error('Failed to disconnect project: %s', e)
|
||||
if isinstance(input, BotRemovedEventRequest):
|
||||
data = dto.DisconnectProjectByTgIdInput(**input.dict(exclude={'type'}))
|
||||
await deps.get_usecase().disconnect_project_by_tg_id(input=data)
|
||||
return
|
||||
|
||||
# Проверяем изменение прав администратора
|
||||
permissions_changed = old_status == 'administrator' and new_status == 'administrator'
|
||||
|
||||
# Извлекаем права бота
|
||||
bot_permissions = dto.ChannelBotPermissions(
|
||||
is_admin=(new_status == 'administrator'),
|
||||
can_invite_users=new_member.get('can_invite_users', False),
|
||||
can_restrict_members=new_member.get('can_restrict_members', False),
|
||||
)
|
||||
|
||||
if permissions_changed:
|
||||
# Обновляем права бота
|
||||
permissions_input = dto.UpdateProjectPermissionsInput(
|
||||
telegram_id=chat['id'],
|
||||
permissions=bot_permissions,
|
||||
chat_title=chat.get('title') or f'Channel {chat["id"]}',
|
||||
user_telegram_id=from_user['id'],
|
||||
)
|
||||
try:
|
||||
await deps.get_usecase().update_project_permissions(input=permissions_input)
|
||||
log.info('Bot permissions updated in channel %s', chat['id'])
|
||||
except Exception as e:
|
||||
log.error('Failed to update permissions: %s', e)
|
||||
return
|
||||
|
||||
# Проверяем что бот был добавлен в канал
|
||||
was_not_member = old_status in ['left', 'kicked']
|
||||
is_now_member = new_status in ['administrator', 'member']
|
||||
bot_added = was_not_member and is_now_member
|
||||
|
||||
if bot_added:
|
||||
# Добавляем канал как проект
|
||||
connect_input = dto.ConnectProjectInput(
|
||||
telegram_id=chat['id'],
|
||||
title=chat.get('title') or f'Channel {chat["id"]}',
|
||||
username=chat.get('username'),
|
||||
user_telegram_id=from_user['id'],
|
||||
bot_permissions=bot_permissions,
|
||||
)
|
||||
try:
|
||||
await deps.get_usecase().tg_add_project(input=connect_input)
|
||||
log.info('Bot added to channel %s by user %s', chat['id'], from_user['id'])
|
||||
except Exception as e:
|
||||
log.error('Failed to add project: %s', e)
|
||||
if isinstance(input, BotPermissionsEventRequest):
|
||||
data = dto.UpdateProjectPermissionsInput(**input.dict(exclude={'type'}))
|
||||
await deps.get_usecase().update_project_permissions(input=data)
|
||||
|
||||
@@ -302,15 +302,12 @@ func (c *Client) UpdateProjectInviteLinkType(
|
||||
return &project, err
|
||||
}
|
||||
|
||||
func (c *Client) ForwardTelegramUpdate(
|
||||
ctx context.Context,
|
||||
update any,
|
||||
) error {
|
||||
func (c *Client) SendEvent(ctx context.Context, payload any) error {
|
||||
return c.do(
|
||||
ctx,
|
||||
http.MethodPost,
|
||||
"api/v1/internal/telegram/updates",
|
||||
update,
|
||||
"api/v1/internal/events",
|
||||
payload,
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package bot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -118,6 +117,204 @@ func (b *Bot) SetState(s State, mode RenderMode) {
|
||||
s.Enter(b, mode)
|
||||
}
|
||||
|
||||
func (b *Bot) forwardSystemEvent(u *echotron.Update) {
|
||||
ctx := context.Background()
|
||||
|
||||
switch {
|
||||
case u.ChatJoinRequest != nil:
|
||||
req := u.ChatJoinRequest
|
||||
if !isChannelChat(req.Chat.Type) {
|
||||
return
|
||||
}
|
||||
|
||||
inviteLink := ""
|
||||
if req.InviteLink != nil {
|
||||
inviteLink = req.InviteLink.InviteLink
|
||||
}
|
||||
if inviteLink == "" {
|
||||
log.Debug().Msg("No invite link in chat join request")
|
||||
return
|
||||
}
|
||||
|
||||
userID := req.From.ID
|
||||
username := stringPtr(req.From.Username)
|
||||
firstName := stringPtr(req.From.FirstName)
|
||||
lastName := stringPtr(req.From.LastName)
|
||||
if userID == 0 && req.UserChatID != 0 {
|
||||
userID = req.UserChatID
|
||||
}
|
||||
if userID == 0 {
|
||||
log.Error().Msg("No user id in chat join request")
|
||||
return
|
||||
}
|
||||
|
||||
payload := map[string]any{
|
||||
"type": "subscription",
|
||||
"user_telegram_id": userID,
|
||||
"invite_link": inviteLink,
|
||||
"username": username,
|
||||
"first_name": firstName,
|
||||
"last_name": lastName,
|
||||
}
|
||||
if err := b.Backend.SendEvent(ctx, payload); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to send subscription event")
|
||||
}
|
||||
|
||||
case u.ChatMember != nil:
|
||||
event := u.ChatMember
|
||||
if !isChannelChat(event.Chat.Type) {
|
||||
return
|
||||
}
|
||||
|
||||
memberUser := event.NewChatMember.User
|
||||
if memberUser == nil {
|
||||
log.Error().Msg("No user in chat member update")
|
||||
return
|
||||
}
|
||||
|
||||
oldStatus := event.OldChatMember.Status
|
||||
newStatus := event.NewChatMember.Status
|
||||
|
||||
wasNotMember := oldStatus == "left" || oldStatus == "kicked"
|
||||
isNowMember := newStatus == "member" || newStatus == "administrator" || newStatus == "creator"
|
||||
userJoined := wasNotMember && isNowMember
|
||||
|
||||
wasMember := oldStatus == "member" || oldStatus == "administrator" || oldStatus == "creator" || oldStatus == "restricted"
|
||||
isNowNotMember := newStatus == "left" || newStatus == "kicked"
|
||||
userLeft := wasMember && isNowNotMember
|
||||
|
||||
if userJoined {
|
||||
inviteLink := ""
|
||||
if event.InviteLink != nil {
|
||||
inviteLink = event.InviteLink.InviteLink
|
||||
}
|
||||
if inviteLink == "" {
|
||||
log.Debug().Msg("No invite link in chat member update")
|
||||
return
|
||||
}
|
||||
|
||||
username := stringPtr(memberUser.Username)
|
||||
firstName := stringPtr(memberUser.FirstName)
|
||||
lastName := stringPtr(memberUser.LastName)
|
||||
payload := map[string]any{
|
||||
"type": "subscription",
|
||||
"user_telegram_id": memberUser.ID,
|
||||
"invite_link": inviteLink,
|
||||
"username": username,
|
||||
"first_name": firstName,
|
||||
"last_name": lastName,
|
||||
}
|
||||
if err := b.Backend.SendEvent(ctx, payload); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to send subscription event")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if userLeft {
|
||||
payload := map[string]any{
|
||||
"type": "unsubscription",
|
||||
"user_telegram_id": memberUser.ID,
|
||||
"channel_telegram_id": event.Chat.ID,
|
||||
}
|
||||
if err := b.Backend.SendEvent(ctx, payload); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to send unsubscription event")
|
||||
}
|
||||
}
|
||||
|
||||
case u.MyChatMember != nil:
|
||||
event := u.MyChatMember
|
||||
if !isChannelChat(event.Chat.Type) {
|
||||
return
|
||||
}
|
||||
|
||||
actorID := event.From.ID
|
||||
if actorID == 0 {
|
||||
log.Error().Msg("No user in my chat member update")
|
||||
return
|
||||
}
|
||||
|
||||
oldStatus := event.OldChatMember.Status
|
||||
newStatus := event.NewChatMember.Status
|
||||
|
||||
wasMember := oldStatus == "administrator" || oldStatus == "member"
|
||||
isNowNotMember := newStatus == "left" || newStatus == "kicked"
|
||||
botRemoved := wasMember && isNowNotMember
|
||||
|
||||
permissionsChanged := oldStatus == "administrator" && newStatus == "administrator"
|
||||
|
||||
if botRemoved {
|
||||
payload := map[string]any{
|
||||
"type": "bot_removed",
|
||||
"telegram_id": event.Chat.ID,
|
||||
"user_telegram_id": actorID,
|
||||
}
|
||||
if err := b.Backend.SendEvent(ctx, payload); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to send bot removed event")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if permissionsChanged {
|
||||
title := event.Chat.Title
|
||||
if title == "" {
|
||||
title = fmt.Sprintf("Channel %d", event.Chat.ID)
|
||||
}
|
||||
payload := map[string]any{
|
||||
"type": "bot_permissions",
|
||||
"telegram_id": event.Chat.ID,
|
||||
"chat_title": title,
|
||||
"user_telegram_id": actorID,
|
||||
"permissions": map[string]any{
|
||||
"is_admin": newStatus == "administrator",
|
||||
"can_invite_users": event.NewChatMember.CanInviteUsers,
|
||||
"can_restrict_members": event.NewChatMember.CanRestrictMembers,
|
||||
},
|
||||
}
|
||||
if err := b.Backend.SendEvent(ctx, payload); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to send bot permissions event")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
wasNotMember := oldStatus == "left" || oldStatus == "kicked"
|
||||
isNowMember := newStatus == "administrator" || newStatus == "member"
|
||||
botAdded := wasNotMember && isNowMember
|
||||
|
||||
if botAdded {
|
||||
title := event.Chat.Title
|
||||
if title == "" {
|
||||
title = fmt.Sprintf("Channel %d", event.Chat.ID)
|
||||
}
|
||||
payload := map[string]any{
|
||||
"type": "bot_added",
|
||||
"telegram_id": event.Chat.ID,
|
||||
"title": title,
|
||||
"username": stringPtr(event.Chat.Username),
|
||||
"user_telegram_id": actorID,
|
||||
"bot_permissions": map[string]any{
|
||||
"is_admin": newStatus == "administrator",
|
||||
"can_invite_users": event.NewChatMember.CanInviteUsers,
|
||||
"can_restrict_members": event.NewChatMember.CanRestrictMembers,
|
||||
},
|
||||
}
|
||||
if err := b.Backend.SendEvent(ctx, payload); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to send bot added event")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isChannelChat(chatType string) bool {
|
||||
return chatType == "channel" || chatType == "supergroup"
|
||||
}
|
||||
|
||||
func stringPtr(value string) *string {
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
return &value
|
||||
}
|
||||
|
||||
func (b *Bot) Render(text string, keyboard echotron.InlineKeyboardMarkup, mode RenderMode) {
|
||||
if mode == EditMessage {
|
||||
b.Edit(text, keyboard)
|
||||
@@ -238,21 +435,9 @@ func (b *Bot) Update(u *echotron.Update) {
|
||||
Bool("has_edited_message", u.EditedMessage != nil).
|
||||
Msg("Update received")
|
||||
|
||||
// 1. Системные события (ChatJoinRequest, ChatMember, etc.) -> форвардим асинхронно
|
||||
// 1. Системные события -> отправляем доменные события в backend
|
||||
if u.ChatJoinRequest != nil || u.ChatMember != nil || u.MyChatMember != nil {
|
||||
go func() {
|
||||
ctx := context.Background()
|
||||
raw, err := json.Marshal(u)
|
||||
if err != nil {
|
||||
log.Error().RawJSON("update", raw).Msg("Failed to marshal update")
|
||||
}
|
||||
err = b.Backend.ForwardTelegramUpdate(ctx, u)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to forward system event to backend")
|
||||
return
|
||||
}
|
||||
log.Info().Msg("System event forwarded to backend")
|
||||
}()
|
||||
go b.forwardSystemEvent(u)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user