Перевести ядро на единый контракт источника

Карточка, очередь и отсев работают с SourceItem (SourceRef + SourceContent); Telegram-поля убраны из домена, персистентности, конвейера и wire, остались только в адаптере приёма. Tenant-миграции пересозданы с нуля. Фронт переведён на generic source/content с просмотрщиком вложений.
This commit is contained in:
Rustam Khalimov
2026-09-11 14:58:54 +03:00
parent 770dba7257
commit ebc761d40e
94 changed files with 4150 additions and 9398 deletions
@@ -17,15 +17,15 @@ public static class ContactsQualifier
private const int MaxContactLength = 300;
private const int MinTgNameLength = 4;
private const int MinHandleLength = 4;
private const int MaxTgNameLength = 32;
private const int MaxHandleLength = 32;
internal const int MaxNormalizedPhoneLength = 18;
private const string TgBotSuffix = "bot";
private const string BotSuffix = "bot";
private static readonly IReadOnlySet<string> TgServiceNames = new HashSet<string>(StringComparer.Ordinal)
private static readonly IReadOnlySet<string> ReservedProfileNames = new HashSet<string>(StringComparer.Ordinal)
{
"joinchat", "share", "s", "c", "addstickers", "addtheme", "proxy", "bg", "login",
};
@@ -35,9 +35,9 @@ public static class ContactsQualifier
"teletype.in", "forms.gle", "docs.google.com", "youtube.com", "youtu.be", "clck.ru",
};
private static readonly Regex TelegramNameRe = new(@"\A[A-Za-z0-9_]{4,32}\z", RegexOptions.CultureInvariant);
private static readonly Regex ProfileHandleRe = new(@"\A[A-Za-z0-9_]{4,32}\z", RegexOptions.CultureInvariant);
private static readonly Regex TelegramLinkRe = new(
private static readonly Regex ProfileLinkRe = new(
@"\Ahttps?://(?:www\.)?t\.me/([A-Za-z0-9_]{4,32})/?\z",
RegexOptions.CultureInvariant);
@@ -78,7 +78,7 @@ public static class ContactsQualifier
/// <summary>
/// Классифицирует один сырой контакт → {type, value} или null.
/// </summary>
/// <param name="raw">Сырое значение контакта («@user», «https://t.me/x», телефон, email, ссылка).</param>
/// <param name="raw">Сырое значение контакта («@user», ссылка на профиль, телефон, email, ссылка).</param>
/// <returns>Квалифицированный контакт <see cref="CardContactDto"/> либо null — пусто/мусор/бот/сервисная ссылка/ «постовый» сайт.</returns>
public static CardContactDto? Qualify(string? raw)
{
@@ -91,7 +91,7 @@ public static class ContactsQualifier
if (s.StartsWith('@'))
{
string name = s[1..].Trim();
if (TelegramNameRe.IsMatch(name) && !name.EndsWith(TgBotSuffix, StringComparison.OrdinalIgnoreCase))
if (ProfileHandleRe.IsMatch(name) && !name.EndsWith(BotSuffix, StringComparison.OrdinalIgnoreCase))
{
return new CardContactDto("tg", "@" + name);
}
@@ -99,12 +99,12 @@ public static class ContactsQualifier
return null;
}
Match link = TelegramLinkRe.Match(s);
Match link = ProfileLinkRe.Match(s);
if (link.Success)
{
string name = link.Groups[1].Value;
if (!TgServiceNames.Contains(name.ToLowerInvariant())
&& !name.EndsWith(TgBotSuffix, StringComparison.OrdinalIgnoreCase))
if (!ReservedProfileNames.Contains(name.ToLowerInvariant())
&& !name.EndsWith(BotSuffix, StringComparison.OrdinalIgnoreCase))
{
return new CardContactDto("tg", "@" + name);
}