парсер новый

This commit is contained in:
Artem Tsyrulnikov
2026-02-04 14:54:25 +03:00
parent 6fc6a60bce
commit d15f2d2bad
6 changed files with 187 additions and 10 deletions

View File

@@ -0,0 +1,120 @@
# ruff: noqa
# mypy: ignore-errors
from tortoise import BaseDBAsyncClient
RUN_IN_TRANSACTION = True
async def upgrade(db: BaseDBAsyncClient) -> str:
return """
-- Drop old constraint (replaced by stricter one)
ALTER TABLE "channel" DROP CONSTRAINT IF EXISTS "channel_identity_required";
-- Add CHECK constraint: channel must have exactly one of username or invite_link
ALTER TABLE "channel"
ADD CONSTRAINT "check_channel_username_xor_invite_link"
CHECK (
(username IS NOT NULL AND invite_link IS NULL) OR
(username IS NULL AND invite_link IS NOT NULL)
);
COMMENT ON CONSTRAINT "check_channel_username_xor_invite_link" ON "channel"
IS 'Ensures channel has exactly one identifier: username OR invite_link (not both, not neither)';
"""
async def downgrade(db: BaseDBAsyncClient) -> str:
return """
ALTER TABLE "channel"
DROP CONSTRAINT IF EXISTS "check_channel_username_xor_invite_link";
"""
MODELS_STATE = (
"eJztXWtz2roW/SsMn3pmejoJkJBm7twZHm7LKQGGR9pzmo7HGIX4xtgc2zTNdPrfr+QHlu"
"UHfgE27C80lbRlaW299tKW9Ku6UhdI1t91ngRFQXL1tvKrqggrhP9go95WqsJ67UaQAEOY"
"y2ZakUo01w1NEA0c/CjIOsJBC6SLmrQ2JFXBocpGlkmgKuKEkrJ0gzaK9O8G8Ya6RMYT0n"
"DEt+84WFIW6CfSnf+un/lHCckLT2GlBfm2Gc4br2szbDbrdT+YKcnn5ryoypuV4qZevxpP"
"qrJNvtlIi3dEhsQtkYI0wUALqhqklHaFnSCrxDjA0DZoW9SFG7BAj8JGJmBU//O4UUSCQc"
"X8Evlp/LeaAB5RVQi0kmIQLH79tmrl1tkMrZJPdT61xm/q13+YtVR1Y6mZkSYi1d+moGAI"
"lqiJqwukqCFSbV4w/IB2cYwhrVAwqF5JBtyFLfrO+SMNyE6Ai7LbwhyYHfjSYVrFdVgMFf"
"nV1mAExtPeHTeZtu5GpCYrXf9XNiFqTTkSUzNDX5nQN5ZKVNw/rH6zzaTypTf9VCH/rfwz"
"HHCs4rbppv9USZmEjaHyivrCCwuqsTmhDjA4pavYzXqRUrFeSVDsURVrF97VKx6LUTq9ei"
"Vz0Ktd2gOqtSRqdKod2UENrI6lJqz4oLmsLS17ihGsR0aQUSSGaj8TWsYOuSRf+fN9rVav"
"N2sX9eubq0azeXVzcYPTmkXyRzUj1N3ufewNpl7tkYDfXowlQ0Z+dPEaRwvB1hFgUMVVKe"
"hAtxJ+8jJSlsYT/m/t6ioCs/vW2Fwp4FRMux/YUTUrzgviRkea+XcCHGmZVFCyY8ve2+f+"
"cRREEek6/yToT8k6PCOYqsMffqw+To9fG7of3FBk7dTlQrR22Wg2burXjS2Q25Ao/PxYSc"
"oPyUC8LCnPSfo2I5ZL9z70QHl5UWvE6OEkWWgXtyIZSHXe6q3SPGjiaauqjAQlBFdWlkF2"
"joX3NQltR9O8l2Xt4bDvWZG1e0wfHszu2hyG2sQZJ8Jty22uhAB4fKYsVxIwF8TnF0Fb8J"
"4YagyQBRGtkBI0FLRt2Q+fx0gWzIr6kbapkJGTTzGn/t9OA3JCqwHmAlm1ZoUBZ1FmBDT1"
"f0jMDIKVS8lwIL1Fralh/ccftaqt2BBBEZZmqcm3yZccopDwP9IPVA0iEZ24t5EsIp0KaE"
"SgEYFtAhoRFAs0ItCI8WnEpMxMJlbmNAkuA/0M6AVTHBpGwP4MavuFhTCquXNfp56W7gD1"
"5q719Q9Pa+8PBx+d5BSwnf6wzeA53xgGLoEf0r8mw0EwpJQIg+pMwbX9tpBE421FlnTj+7"
"4wplZa840kG5KivyMf3NNii2ARjTwLMjN4kAxY5HVDMDYBwJPhgFM2KxP8Hi6ioIjIpwRX"
"+nAtu4rX/LYJ4MW42upMe/fcbcVK8KAQdHBAF4do4hMOslpRwgHkJsbwcRM6eNz4hg5hmR"
"ZtW/SAUBtINwhSfqyneBrqDT7iudhK8qCMxsPuDGtgOLitYAt6sREd+zg51RaLaIug2VjQ"
"HZNh/sqTfYbADbRwKy5YOoNVt3v9kvNeRRYTzseLJETPK5WnLVxg2HwspA9FP4QfVA1JS+"
"UzevUNAwdgmPYDnY9gwsGa8LIlVpjWgetoGQsmxK1Jp9XlqlE9OQcUZ3Y2Be2zOxEMHp08"
"SE64aWUw6/erv+OQ4Su0kAReMtAqIwPqcHp3JMNCL3R38MFH2B04mom7AwqkrSRdx1nzuq"
"iSATETIl9U7VlfY1RIJxxt856QrMsF0iF4c6sfRZDn2462m0HnV9u0ufLo39wv2BO+qktm"
"Rt+BYt/L+gwo9hNnYoFiP1HFAsV+qhS7tYQ28fcpMpxo90qVhSv2cjf1Wgzupl4L5W5I1O"
"8ALB8lGQUSD7vgpATLiejVZRxIcapQTM24IFD1Ov+MXpNj6sqV0pVvL4hu17k+NMN9SSmR"
"fHzyy+dRylgLiShZysg4e1aR9ozKSIjRrlhFBS8eK+Y2kGBiMZwN26c531eXkjJVn5FSDb"
"Dlqdi3UYa8TNJh09tJCN5wYKqDRQemOigWTHUw1RMcqnVm0NgHPh2BfMzJEzimiH6uJayR"
"FH3BK1nOMe6UOsNGTzdV6TCeFUWFK6Tr2O4ItKdDmQiv0Jkebk3hGZaPO9iJUBCH9ccpLv"
"UQ5oVzZNrB9T4JYB08rinhpMPak2yflMM32k/OdjP+DjwE8BBgrgIPcb6KBR7iVNftJTyI"
"87C5aFxekt/6lfnbrJB/GmZQo2YGXbh/N+pubP3CP/VVB0MeozidTW4rmbJ+UL6Me1POzm"
"XhJq4/mr83nsRuJiIWbPXIsRZ+zE1Gw8HEyePaTNsgv1eWnGhG1BFVnppb3G0JlSk3vpvw"
"rdFoPLxv9a3sapeubN0seX1OFYaOpWpaX7iFr19VfNWe+8SspO9xIYb8qPW3XRdEIUF/lp"
"Jx8Ri1et0dck1KSwu7DEqnNehwfc4ja39B9KW/IIeHeh2OHwyn/PCzR29WRe3PIlcNjSs2"
"xCm6hcx10FdI/mNcrPvWYOr9CgWzwDYrO3xuY+KpJ26yTFPxFDuseTSpfO0GU403kXnZvD"
"gHpWrhB6VqvoNS2wV+igmFlYUp5chTylp4TatKjyQo8siKFHF+Ed6Eu5cHngyO67NV/dD7"
"SuaFRwmL4YlidHdbEderNMNfzidzTZB+CPImyIlGVoUQ7tQrxoD7SOTK1j+6w1m7z1VGY6"
"7Tm/TsQ+fbDmFGmqzM9ua1MdfqB4E5R4+qhvi5oC0FScnegkPygwYd0aC9mCVv2X55aOLs"
"WilLw/bncuTmPOH6H3hsrtwNbys6kh/5taau1AcFT2KDbmuMWzqpyULQUt2mcHkd52w/O9"
"lSZ/uv2VaOm+cqaIUTfhuLK1ES9+VDX8ciqiuH5I+LKSUCoAaCesZ35+7F4Z4Chk+/bRGe"
"C1g9R7Z6aNVkmWGD8jnyUaDqCK8wep3bynozlyXxQXEJQmGN51tsThRg/UgDl/TSuCDZUo"
"5d+R9os5/CSnrixSN1Jt4mhTopBLc2nXpzg1ubnGawj1ubDn46rdhXNu06nBYwZeQBn5tT"
"aVufdypM6mMXRODk8eaBk1nJHz/QN/Nt5hkhmVBZlRgRuPMqBKSDeKua3SnKY9XpbzG8Vs"
"1+bqaF07LgpQrOjAflfMBL9WwUC16qp8rJgpdqjl6qE27Q5UfDyTTAS9J0eGxcUH6CAf6Z"
"Pt/I7ZcVku2+3E4Dv2jXyXGgnXQ+cd1Zn/M4j15TWTWiP0FjQXu2Xrs+mlhAcT7jOJrSjp"
"U+V1X6Oza0QdW1sDMZ+smnbc4Bda64wg3LO/SGagvzB6UzvBv1uSnX5buc+a+tCFqtTdYp"
"tvHe57oahoynfVyysbTfah1V/gzVHeVL7FGLKWn7Oy/o2hA/2uLXqE674sao3ieu89mpFs"
"+1xv2/adUHNJgrKtzbLzNiXfFBsbAyphsZPdTQZROcrmhViOgqsC3nWSEP1JE9wizTtiUd"
"sfWk2V+7jvMC7HX4+6/XvtdfXdM0IT3PyJ0JQc++U5oUNVfkPPaBojY06KO8Wbc08n329l"
"ibGkyf2k3LO3RSVvjiMcaF3c6gulXiZyf2yiGGUYe7GcO98ITfmJ0L6tYNuBwfOESgmoBD"
"BMUCh3jqHOKxbqg6Rm/M+YoqeHc2X598Z2R41NQV7yzO0g8wAdnAaHPsQ9DEw1p/SjVxsL"
"KgzGMfhAbX6Wp26ge8CU/Om7BAXmF0f/0hoRedf5J0Q9WkrI5zBIp7kuEnM7/XQi9hDuwt"
"x0ITwnqx8EUzYDytvtd98GE0h48M0Z5pgQwDMgw4EyDDQLFAhp26RWNNsaK6CdqIDWXDGK"
"lzpcOoNUPCXuCVPMzodjjTsHS94Gi+HIVSWwZnjoM6IhTXlg/zRDj21fv2kd8gq8w9DRxh"
"jFGJcrbAXpzTaDZsFBsCZhiYYbBaBzMMFAtm2KkvQEt4rgkvAuxrMJgDSq3OtHfP3VasBA"
"9Kb+CESIoTRrbOcVAXp9LEJxyU6jLHnO+bWm9wWQQdL0Wsy6NIhrxT45Sq2ZXnIRVG3+x1"
"iOvALuNcD38Zfj38pe96eNh/TXVUgV1kxwWOlTsT6GDr2mkIOWxdB7bDHPD7QudVWgTZHp"
"Z++9+50ynjJneC662KtLkdeKYtLx+IMmMBdwcd3hvCc+9UAOfG3ksVTrzpbMq8/R+YI2cG"
"HnuWmrDinTdEgYMDDg6oGuDgQLHAwZ06B3fEBxIOb6Ac4IGEU+Y0Z4PJrD3pjHttwmFuFH"
"uhNk/HY8ZCPwJ8P/Z0idLMOH5xGJ6O7aMCV7Wk5T99Zk0C+IJkzwTCKIcfuL0l8+0tnpaV"
"A45TO7+ZnV1poQzqckVyp/IAHcDvsIoI53d8TQAuhgbyJp/FFtj4QN6AYoG8ORPraDuTBs"
"1lbWkZepSFEcznKMvuCS2Xgyzva7V6vVm7qF/fXDWazaubi+2JFn9U1NGWdu8jOd3i0Z7/"
"uAtZpCR9h4+WKeX7e7WrqxjkDE4Vys6YcczBIUnTjcRvGnqlAE0HTVlIAaZH6Lyx9Nn5cf"
"xN4J2mxG4E3pE0HLOhgqYq/tmNXExTv2B+FwlM7TATe7dpDRZ19AIELGowvMCiBsWCRZ3/"
"tFd0NSazqAu4abZv+zrHPbPYvtu45R/eh7tAK0OPRacuJVwt9RlltS36JKcpyaiYE0EsOH"
"RzW9H0M8rLgbtn5lZiTKiTEwBMMDBk8M3T4b9koPht89h7744FfqCd9wNNZ/vaeM+0me6e"
"4wow8z2HvMJtfc+5MjD4weDPx34AuxAMflAsGPxnYvAn3UHLtHl2jC64/51I4Qee1jRer/"
"PP6DUJlj7BUu5I5naMJBWLYt9Sl/XUt3vXXfGacCwDEOxhsIcPfeSdbS5RtpzbomJYdDa7"
"k79h5796Es68g80HpgHYfKBYsPnOxeYr4RntNVIWRD0+BVZH3KDbG3y8rdhJHpRWp8ONpu"
"Y1k6KI1rgkD8qYux9+JkEa+qE+F+LiSWuNs+Dnrwn32X2CZ3IylfVKTwjb+R3lhdsgU0MX"
"cQoa7jN8m+o+w+CxLwcQS3/02Teg7wYwp9PjpYeuoIfFvYxRFC+y26fdS2UBJwKcCHAihb"
"K5gBM5G8UCJwKcSGE4kQRvcdz3TD7EXmg+KO3+sGPSIXNZFVPSIVFHyx06pBlKhzR9l9eB"
"RQ8WPVj0xTCrklv0YJBmNkiDLvDfz9X9xVwlxnLsKNbbBkVC6WB8BtWOdjEb3iYXl+Pg11"
"6xvdEd9ALC/STQHkB7gHUMtAcoFmiPU6c9vDNtGurDm8ORjwVUW9273oD/MOuTVywXK0nh"
"H3G6B2U0Hv7FdaYTfsy1urcVx2ucJz2Uiv0y7k05KvpFkwz0oHSwFKFUHPHtkX1b3o23M3"
"AT2DmM+q0Od8cN3BJs3ytzyuCmcErhJrFzaQ1a/b+nvY6TCV6uya+GJDp5uPGk5QxnU77T"
"73U+T+iULxLW0MbgRVkSn3VaZvhlwG9rQouoL4r3loLExy4acU5dNMIPXTRYtihwDZecAT"
"k/BikODZLX3eR78Js/Oh9Shg1n1o6Nb6Vt7d4Uppplj+dvsIE1todhAKyxk1+0gzV2oooF"
"awysMbDGwBorjjXmFCehHcaIZViM7R52CmOAFex1t7IiZ40JSXHzSJ0fasCaAGtSTNYkoG"
"/nAGX8az2O1aF3vzDoGbJ2g+fMqTmgl+uNskeCj1lixGh8R3rjsrAIxnvi8jisZwtpkvhU"
"DeA37Zi3UUym4KYpDGEZ+mJV3IeqbBVmIyozEi3WO1W1y0azcVO/bmyfp9qGRLkOOy9Qhf"
"OTP5AWbiIHo0eJwMVl7sVluGskANFOXk4ALy8uYgCIU4W/t07iGLtXVYzAqeKvyXAQYvO6"
"IgyQMwVX8NtCEo23FVnSje/FhDUCRVJrD9XmgPfmrvWVxbXTH7bZ1TbJoJ3sJrj8p5ff/w"
"cQREBB"
)

View File

@@ -24,7 +24,7 @@ async def create_channels(self: 'Usecase', input: dto.CreateChannelsInput) -> dt
raise ValueError('Telegram channel not found by invite link') raise ValueError('Telegram channel not found by invite link')
parsed_username = parser_response.username or None parsed_username = parser_response.username or None
is_private = channel_input.invite_link != "" is_private = channel_input.invite_link != ''
channel = await self.database.get_channel(telegram_id=parser_response.telegram_id) channel = await self.database.get_channel(telegram_id=parser_response.telegram_id)
if not channel and parsed_username: if not channel and parsed_username:
@@ -34,9 +34,16 @@ async def create_channels(self: 'Usecase', input: dto.CreateChannelsInput) -> dt
if channel: if channel:
status = 'updated' status = 'updated'
updated = False updated = False
# Канал стал публичным (появился username)
if parsed_username is not None and channel.username != parsed_username: if parsed_username is not None and channel.username != parsed_username:
channel.username = parsed_username channel.username = parsed_username
channel.invite_link = None # Очищаем invite_link у публичных каналов
updated = True updated = True
# Канал остаётся приватным или обновляется
elif parsed_username is None and channel_input.invite_link and channel.invite_link != channel_input.invite_link:
channel.invite_link = channel_input.invite_link
updated = True
if parser_response.title is not None and channel.title != parser_response.title: if parser_response.title is not None and channel.title != parser_response.title:
channel.title = parser_response.title channel.title = parser_response.title
updated = True updated = True
@@ -49,12 +56,6 @@ async def create_channels(self: 'Usecase', input: dto.CreateChannelsInput) -> dt
if parser_response.pts is not None and channel.pts != parser_response.pts: if parser_response.pts is not None and channel.pts != parser_response.pts:
channel.pts = parser_response.pts channel.pts = parser_response.pts
updated = True updated = True
if is_private and channel.pts != 0:
channel.pts = 0
updated = True
if channel_input.invite_link and channel.invite_link != channel_input.invite_link:
channel.invite_link = channel_input.invite_link
updated = True
if updated: if updated:
await self.database.update_channel(channel) await self.database.update_channel(channel)
else: else:
@@ -64,7 +65,8 @@ async def create_channels(self: 'Usecase', input: dto.CreateChannelsInput) -> dt
title=parser_response.title, title=parser_response.title,
access_hash=parser_response.access_hash, access_hash=parser_response.access_hash,
pts=0 if is_private else parser_response.pts, pts=0 if is_private else parser_response.pts,
invite_link=channel_input.invite_link, # Для публичных каналов (есть username) invite_link не храним
invite_link=None if parsed_username else channel_input.invite_link,
) )
await self.database.create_channel(channel) await self.database.create_channel(channel)

View File

@@ -10,6 +10,59 @@ import (
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
) )
func (d *Database) UpdateChannelIfNotAccessible(ctx context.Context, channel domain.Channel) error {
query := `UPDATE channel
SET telegram_id = $2,
username = $3,
title = $4,
access_hash = $5,
pts = 0,
is_accessible = TRUE,
invite_link = $6,
updated_at = CURRENT_TIMESTAMP
WHERE telegram_id = $1
AND is_accessible = FALSE;`
txOrPool := transaction.TryExtractTX(ctx)
username := strings.TrimSpace(channel.Username)
var usernameDTO pgtype.Text
if username != "" {
usernameDTO = pgtype.Text{String: username, Valid: true}
} else {
usernameDTO = pgtype.Text{Valid: false}
}
inviteLink := strings.TrimSpace(channel.InviteLink)
var inviteLinkDTO pgtype.Text
if inviteLink != "" {
inviteLinkDTO = pgtype.Text{String: inviteLink, Valid: true}
} else {
inviteLinkDTO = pgtype.Text{Valid: false}
}
result, err := txOrPool.Exec(ctx, query,
channel.TelegramID,
pgtype.Int8{Int64: channel.TelegramID, Valid: true},
usernameDTO,
pgtype.Text{String: channel.Title, Valid: true},
pgtype.Int8{Int64: channel.AccessHash, Valid: true},
inviteLinkDTO,
)
if err != nil {
return fmt.Errorf("txOrPool.Exec: %w", err)
}
// Log if no rows were updated (channel either doesn't exist or is already accessible)
rowsAffected := result.RowsAffected()
if rowsAffected == 0 {
// Channel doesn't exist or is already accessible - this is fine
return nil
}
return nil
}
func (d *Database) UpdateChannel(ctx context.Context, channel domain.Channel) error { func (d *Database) UpdateChannel(ctx context.Context, channel domain.Channel) error {
query := `UPDATE channel query := `UPDATE channel
SET telegram_id = $2, SET telegram_id = $2,

View File

@@ -13,7 +13,7 @@ func (uc *UseCase) FetchChannelMeta(ctx context.Context, username string) (domai
return domain.Channel{}, fmt.Errorf("uc.telegram.ParseChannelMeta: %s", err) return domain.Channel{}, fmt.Errorf("uc.telegram.ParseChannelMeta: %s", err)
} }
err = uc.database.UpdateChannelIfNotAccessible(ctx, channel.TelegramID) err = uc.database.UpdateChannelIfNotAccessible(ctx, channel)
if err != nil { if err != nil {
return domain.Channel{}, fmt.Errorf("uc.database.UpdateChannelIfNotAccessible: %s", err) return domain.Channel{}, fmt.Errorf("uc.database.UpdateChannelIfNotAccessible: %s", err)
} }

View File

@@ -13,7 +13,7 @@ func (uc *UseCase) FetchChannelMetaByInvite(ctx context.Context, inviteLink stri
return domain.Channel{}, fmt.Errorf("uc.telegram.ParseChannelMetaByInvite: %s", err) return domain.Channel{}, fmt.Errorf("uc.telegram.ParseChannelMetaByInvite: %s", err)
} }
err = uc.database.UpdateChannelIfNotAccessible(ctx, channel.TelegramID) err = uc.database.UpdateChannelIfNotAccessible(ctx, channel)
if err != nil { if err != nil {
return domain.Channel{}, fmt.Errorf("uc.database.UpdateChannelIfNotAccessible: %s", err) return domain.Channel{}, fmt.Errorf("uc.database.UpdateChannelIfNotAccessible: %s", err)
} }

View File

@@ -14,6 +14,7 @@ import (
func (uc *UseCase) FetchChannels(ctx context.Context) error { func (uc *UseCase) FetchChannels(ctx context.Context) error {
channels := uc.database.GetChannels(ctx) channels := uc.database.GetChannels(ctx)
log.Debug().Msg("start fetch channels")
for _, c := range channels { for _, c := range channels {
if c.Pts == 0 { if c.Pts == 0 {
@@ -115,6 +116,7 @@ func (uc *UseCase) rejoinChannel(ctx context.Context, channel domain.Channel) {
} }
func (uc *UseCase) initializeChannel(ctx context.Context, channel domain.Channel) { func (uc *UseCase) initializeChannel(ctx context.Context, channel domain.Channel) {
log.Debug().Stringer("ch", channel).Msg("init channel")
var ( var (
updated domain.Channel updated domain.Channel
err error err error