diff --git a/backend/src/TeleWave.Application/Common/Interfaces/ITelegramApi.cs b/backend/src/TeleWave.Application/Common/Interfaces/ITelegramApi.cs
index 0ce923c..e919e34 100644
--- a/backend/src/TeleWave.Application/Common/Interfaces/ITelegramApi.cs
+++ b/backend/src/TeleWave.Application/Common/Interfaces/ITelegramApi.cs
@@ -40,7 +40,8 @@ public interface ITelegramApi
CancellationToken cancellationToken
);
- Task SendMessageAsync(
+ /// Отправляет сообщение и возвращает его идентификатор — по нему потом правят и удаляют.
+ Task SendMessageAsync(
TelegramSettings settings,
long chatId,
string text,
@@ -58,6 +59,17 @@ public interface ITelegramApi
CancellationToken cancellationToken
);
+ ///
+ /// Снимает своё сообщение. В личном чате бот вправе удалять только собственные — команды
+ /// зрителя остаются, и это не в нашей власти.
+ ///
+ Task DeleteMessageAsync(
+ TelegramSettings settings,
+ long chatId,
+ long messageId,
+ CancellationToken cancellationToken
+ );
+
/// Гасит «часики» на нажатой кнопке; текст — всплывающая подсказка.
Task AnswerCallbackAsync(
TelegramSettings settings,
diff --git a/backend/src/TeleWave.Application/Notifications/TelegramBotService.cs b/backend/src/TeleWave.Application/Notifications/TelegramBotService.cs
index 096a1f0..bdb8dfd 100644
--- a/backend/src/TeleWave.Application/Notifications/TelegramBotService.cs
+++ b/backend/src/TeleWave.Application/Notifications/TelegramBotService.cs
@@ -60,9 +60,13 @@ public sealed class TelegramBotService(IAppDbContext dbContext, ITelegramApi api
// Любое другое сообщение — это «я не знаю, что нажать»: показываем меню, если чат известен.
var subscriber = await FindAsync(chatId, cancellationToken);
if (subscriber is null)
- await api.SendMessageAsync(settings, chatId, Texts.NeedLink, null, cancellationToken);
- else
- await ShowMenuAsync(settings, subscriber, chatId, null, cancellationToken);
+ {
+ await NoticeAsync(settings, null, chatId, Texts.NeedLink, cancellationToken);
+ return;
+ }
+
+ await ShowMenuAsync(settings, subscriber, chatId, null, cancellationToken);
+ await dbContext.SaveChangesAsync(cancellationToken);
}
///
@@ -89,20 +93,14 @@ public sealed class TelegramBotService(IAppDbContext dbContext, ITelegramApi api
// Уже привязанный чат просто открывает меню: заставлять его снова ходить за кодом незачем.
if (subscriber is null)
{
- await api.SendMessageAsync(
- settings,
- chatId,
- Texts.NeedLink,
- null,
- cancellationToken
- );
+ await NoticeAsync(settings, null, chatId, Texts.NeedLink, cancellationToken);
return;
}
subscriber.Resume();
subscriber.Touch(now);
- await dbContext.SaveChangesAsync(cancellationToken);
await ShowMenuAsync(settings, subscriber, chatId, null, cancellationToken);
+ await dbContext.SaveChangesAsync(cancellationToken);
return;
}
@@ -112,7 +110,7 @@ public sealed class TelegramBotService(IAppDbContext dbContext, ITelegramApi api
);
if (link is null || !link.IsUsable(now))
{
- await api.SendMessageAsync(settings, chatId, Texts.BadCode, null, cancellationToken);
+ await NoticeAsync(settings, subscriber, chatId, Texts.BadCode, cancellationToken);
return;
}
@@ -128,9 +126,9 @@ public sealed class TelegramBotService(IAppDbContext dbContext, ITelegramApi api
subscriber.Rebind(link.UserId, from, now);
}
- await dbContext.SaveChangesAsync(cancellationToken);
- await api.SendMessageAsync(settings, chatId, Texts.Linked, null, cancellationToken);
+ await NoticeAsync(settings, subscriber, chatId, Texts.Linked, cancellationToken);
await ShowMenuAsync(settings, subscriber, chatId, null, cancellationToken);
+ await dbContext.SaveChangesAsync(cancellationToken);
}
private async Task HandleStopAsync(
@@ -145,8 +143,8 @@ public sealed class TelegramBotService(IAppDbContext dbContext, ITelegramApi api
// Подписки не трогаем: вернувшись по /start, зритель получит свои каналы, а не пустой список.
subscriber.Stop();
+ await NoticeAsync(settings, subscriber, chatId, Texts.Stopped, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
- await api.SendMessageAsync(settings, chatId, Texts.Stopped, null, cancellationToken);
}
private async Task HandleCallbackAsync(
@@ -188,7 +186,8 @@ public sealed class TelegramBotService(IAppDbContext dbContext, ITelegramApi api
subscriber.Touch(now);
await dbContext.SaveChangesAsync(cancellationToken);
await AnswerAsync(settings, update, null, cancellationToken);
- await SendScheduleAsync(settings, channel, chatId, now, cancellationToken);
+ await SendScheduleAsync(settings, subscriber, channel, chatId, now, cancellationToken);
+ await dbContext.SaveChangesAsync(cancellationToken);
return;
}
@@ -220,6 +219,28 @@ public sealed class TelegramBotService(IAppDbContext dbContext, ITelegramApi api
update.CallbackMessageId,
cancellationToken
);
+ await dbContext.SaveChangesAsync(cancellationToken);
+ }
+
+ ///
+ /// Одноразовое сообщение бота: программа передач, подтверждение привязки, отказ. Предыдущее
+ /// такое же снимается — три программы на разное время в чате не нужны никому.
+ ///
+ private async Task NoticeAsync(
+ TelegramSettings settings,
+ TelegramSubscriber? subscriber,
+ long chatId,
+ string text,
+ CancellationToken cancellationToken
+ )
+ {
+ if (subscriber?.NoticeMessageId is { } previous)
+ await api.DeleteMessageAsync(settings, chatId, previous, cancellationToken);
+
+ var messageId = await api.SendMessageAsync(settings, chatId, text, null, cancellationToken);
+
+ // Незнакомому чату помнить нечего: подписчика ещё нет, и следующее сообщение будет первым.
+ subscriber?.SetNoticeMessage(messageId);
}
private Task AnswerAsync(
@@ -254,7 +275,7 @@ public sealed class TelegramBotService(IAppDbContext dbContext, ITelegramApi api
if (channels.Count == 0)
{
- await api.SendMessageAsync(settings, chatId, Texts.NoChannels, null, cancellationToken);
+ await NoticeAsync(settings, subscriber, chatId, Texts.NoChannels, cancellationToken);
return;
}
@@ -273,7 +294,10 @@ public sealed class TelegramBotService(IAppDbContext dbContext, ITelegramApi api
]);
}
+ // Нажали кнопку — правим то же сообщение. Пришли заново — старое меню снимаем: две панели
+ // с разными галочками в одном чате означают, что одна из них врёт.
if (messageId is { } id)
+ {
await api.EditMessageAsync(
settings,
chatId,
@@ -282,8 +306,21 @@ public sealed class TelegramBotService(IAppDbContext dbContext, ITelegramApi api
keyboard,
cancellationToken
);
- else
- await api.SendMessageAsync(settings, chatId, Texts.Menu, keyboard, cancellationToken);
+ subscriber.SetMenuMessage(id);
+ return;
+ }
+
+ if (subscriber.MenuMessageId is { } previous)
+ await api.DeleteMessageAsync(settings, chatId, previous, cancellationToken);
+
+ var sent = await api.SendMessageAsync(
+ settings,
+ chatId,
+ Texts.Menu,
+ keyboard,
+ cancellationToken
+ );
+ subscriber.SetMenuMessage(sent);
}
private static TelegramButton Button(
@@ -310,6 +347,7 @@ public sealed class TelegramBotService(IAppDbContext dbContext, ITelegramApi api
///
private async Task SendScheduleAsync(
TelegramSettings settings,
+ TelegramSubscriber subscriber,
Channel channel,
long chatId,
DateTimeOffset now,
@@ -340,7 +378,7 @@ public sealed class TelegramBotService(IAppDbContext dbContext, ITelegramApi api
if (entries.Count == 0)
{
- await api.SendMessageAsync(settings, chatId, Texts.NoSchedule, null, cancellationToken);
+ await NoticeAsync(settings, subscriber, chatId, Texts.NoSchedule, cancellationToken);
return;
}
@@ -396,6 +434,9 @@ public sealed class TelegramBotService(IAppDbContext dbContext, ITelegramApi api
public const string Menu =
"Оповещения\nНажмите, чтобы включить или выключить. ✅ — включено.\n"
+ // Кнопка-заголовок ничем не выдаёт, что она кнопка: про программу передач надо сказать
+ // словами, иначе её найдут только промахнувшись мимо тумблера.
+ + "Программа передач — нажмите на название канала.\n"
+ "Остановить всё — командой /stop.";
public const string NoChannels = "Пока нет ни одного включённого канала.";
diff --git a/backend/src/TeleWave.Domain/Notifications/TelegramSubscriber.cs b/backend/src/TeleWave.Domain/Notifications/TelegramSubscriber.cs
index a7e8424..eff9aec 100644
--- a/backend/src/TeleWave.Domain/Notifications/TelegramSubscriber.cs
+++ b/backend/src/TeleWave.Domain/Notifications/TelegramSubscriber.cs
@@ -33,6 +33,19 @@ public class TelegramSubscriber
///
public bool IsStopped { get; private set; }
+ ///
+ /// Последнее меню подписок в чате. Бот убирает за собой: при новом /start старое меню
+ /// снимается, иначе чат превращается в ленту одинаковых панелей, и непонятно, какая из них
+ /// показывает правду.
+ ///
+ public long? MenuMessageId { get; private set; }
+
+ ///
+ /// Последнее одноразовое сообщение бота — программа передач, подтверждение привязки. Живёт
+ /// до следующего такого же: держать в чате три программы на разное время бессмысленно.
+ ///
+ public long? NoticeMessageId { get; private set; }
+
public IReadOnlyList Subscriptions => _subscriptions;
private TelegramSubscriber() { }
@@ -64,6 +77,11 @@ public class TelegramSubscriber
public void Touch(DateTimeOffset now) => LastSeenAt = now;
+ /// Запоминает, каким сообщением сейчас показано меню (null — его в чате нет).
+ public void SetMenuMessage(long? messageId) => MenuMessageId = messageId;
+
+ public void SetNoticeMessage(long? messageId) => NoticeMessageId = messageId;
+
public void Stop() => IsStopped = true;
public void Resume() => IsStopped = false;
diff --git a/backend/src/TeleWave.Infrastructure/Migrations/20260731052216_TelegramBotMessages.Designer.cs b/backend/src/TeleWave.Infrastructure/Migrations/20260731052216_TelegramBotMessages.Designer.cs
new file mode 100644
index 0000000..71323c3
--- /dev/null
+++ b/backend/src/TeleWave.Infrastructure/Migrations/20260731052216_TelegramBotMessages.Designer.cs
@@ -0,0 +1,1599 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using TeleWave.Infrastructure.Persistence;
+
+#nullable disable
+
+namespace TeleWave.Infrastructure.Migrations
+{
+ [DbContext(typeof(AppDbContext))]
+ [Migration("20260731052216_TelegramBotMessages")]
+ partial class TelegramBotMessages
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.10")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("text");
+
+ b.Property("ClaimValue")
+ .HasColumnType("text");
+
+ b.Property("RoleId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetRoleClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ClaimType")
+ .HasColumnType("text");
+
+ b.Property("ClaimValue")
+ .HasColumnType("text");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserClaims", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.Property("LoginProvider")
+ .HasColumnType("text");
+
+ b.Property("ProviderKey")
+ .HasColumnType("text");
+
+ b.Property("ProviderDisplayName")
+ .HasColumnType("text");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("LoginProvider", "ProviderKey");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserLogins", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.Property("RoleId")
+ .HasColumnType("uuid");
+
+ b.HasKey("UserId", "RoleId");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetUserRoles", (string)null);
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b =>
+ {
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.Property("LoginProvider")
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .HasColumnType("text");
+
+ b.Property("Value")
+ .HasColumnType("text");
+
+ b.HasKey("UserId", "LoginProvider", "Name");
+
+ b.ToTable("AspNetUserTokens", (string)null);
+ });
+
+ modelBuilder.Entity("TeleWave.Domain.Auth.RefreshToken", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ExpiresAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ReplacedByTokenHash")
+ .HasColumnType("text");
+
+ b.Property("RevokedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("TokenHash")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TokenHash")
+ .IsUnique();
+
+ b.HasIndex("UserId");
+
+ b.ToTable("RefreshTokens");
+ });
+
+ modelBuilder.Entity("TeleWave.Domain.Broadcast.BumperAsset", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid");
+
+ b.Property("BackgroundAssetId")
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("MediaAssetId")
+ .HasColumnType("uuid");
+
+ b.Property("PosterShowId")
+ .HasColumnType("uuid");
+
+ b.Property("RenderedLinesJson")
+ .IsRequired()
+ .HasColumnType("jsonb");
+
+ b.Property("Signature")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)");
+
+ b.Property("TemplateId")
+ .HasColumnType("uuid");
+
+ b.Property("VariantId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("MediaAssetId");
+
+ b.HasIndex("Signature")
+ .IsUnique();
+
+ b.ToTable("BumperAssets");
+ });
+
+ modelBuilder.Entity("TeleWave.Domain.Broadcast.BumperTemplate", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid");
+
+ b.Property("AccentColor")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)");
+
+ b.Property("AudioDurationSeconds")
+ .HasColumnType("double precision");
+
+ b.Property("AudioExtension")
+ .HasMaxLength(16)
+ .HasColumnType("character varying(16)");
+
+ b.Property("BackgroundColor")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)");
+
+ b.Property("BackgroundColor2")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)");
+
+ b.Property("BackgroundImageId")
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Font")
+ .HasColumnType("integer");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("Revision")
+ .HasColumnType("integer");
+
+ b.Property("TextColor")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name");
+
+ b.ToTable("BumperTemplates");
+ });
+
+ modelBuilder.Entity("TeleWave.Domain.Broadcast.BumperTextVariant", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid");
+
+ b.Property("Background")
+ .HasColumnType("integer");
+
+ b.Property("BackgroundClipShowId")
+ .HasColumnType("uuid");
+
+ b.Property("BumperTemplateId")
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("Position")
+ .HasColumnType("integer");
+
+ b.Property("Trigger")
+ .HasColumnType("integer");
+
+ b.Property("Weight")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(1);
+
+ b.HasKey("Id");
+
+ b.HasIndex("BumperTemplateId", "Position");
+
+ b.ToTable("BumperTextVariants");
+ });
+
+ modelBuilder.Entity("TeleWave.Domain.Broadcast.Channel", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid");
+
+ b.Property("AnalogFilterStrength")
+ .HasColumnType("double precision");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("DayStartTime")
+ .HasColumnType("time without time zone");
+
+ b.Property("EpochUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("FillerAssetId")
+ .HasColumnType("uuid");
+
+ b.Property("IsEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("LogoCorner")
+ .HasColumnType("integer");
+
+ b.Property("LogoImageId")
+ .HasColumnType("uuid");
+
+ b.Property("LogoOpacity")
+ .HasColumnType("double precision");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("Number")
+ .HasColumnType("integer");
+
+ b.Property("ShowClock")
+ .HasColumnType("boolean");
+
+ b.Property("Slug")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)");
+
+ b.Property("TemplateId")
+ .HasColumnType("uuid");
+
+ b.Property("UtcOffsetMinutes")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Number")
+ .IsUnique()
+ .HasFilter("\"Number\" IS NOT NULL");
+
+ b.HasIndex("Slug")
+ .IsUnique();
+
+ b.ToTable("Channels");
+ });
+
+ modelBuilder.Entity("TeleWave.Domain.Broadcast.ScheduleEntry", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid");
+
+ b.Property("BumperVariantId")
+ .HasColumnType("uuid");
+
+ b.Property("ChannelId")
+ .HasColumnType("uuid");
+
+ b.Property("CollectionId")
+ .HasColumnType("uuid");
+
+ b.Property("EndsAtUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("EpisodeIndex")
+ .HasColumnType("integer");
+
+ b.Property("Kind")
+ .HasColumnType("integer");
+
+ b.Property("MediaAssetId")
+ .HasColumnType("uuid");
+
+ b.Property("ShowId")
+ .HasColumnType("uuid");
+
+ b.Property("SlotId")
+ .HasColumnType("uuid");
+
+ b.Property("StartsAtUtc")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("TraceJson")
+ .HasColumnType("jsonb");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ChannelId", "EndsAtUtc");
+
+ b.HasIndex("ChannelId", "StartsAtUtc");
+
+ b.HasIndex("ChannelId", "ShowId", "StartsAtUtc");
+
+ b.ToTable("ScheduleEntries");
+ });
+
+ modelBuilder.Entity("TeleWave.Domain.Images.Image", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid");
+
+ b.Property("Category")
+ .HasColumnType("integer");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("FileExtension")
+ .IsRequired()
+ .HasMaxLength(16)
+ .HasColumnType("character varying(16)");
+
+ b.Property("OriginalFileName")
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Category", "CreatedAt");
+
+ b.ToTable("Images");
+ });
+
+ modelBuilder.Entity("TeleWave.Domain.Library.Collection", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Description")
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("PosterImageId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.ToTable("Collections");
+ });
+
+ modelBuilder.Entity("TeleWave.Domain.Library.CollectionItem", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid");
+
+ b.Property("CollectionId")
+ .HasColumnType("uuid");
+
+ b.Property("Position")
+ .HasColumnType("integer");
+
+ b.Property("ShowId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ShowId");
+
+ b.HasIndex("CollectionId", "Position");
+
+ b.HasIndex("CollectionId", "ShowId")
+ .IsUnique();
+
+ b.ToTable("CollectionItems");
+ });
+
+ modelBuilder.Entity("TeleWave.Domain.Library.Genre", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("IsSystem")
+ .HasColumnType("boolean");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)");
+
+ b.Property("Slug")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("SortOrder")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Slug")
+ .IsUnique();
+
+ b.ToTable("Genres");
+ });
+
+ modelBuilder.Entity("TeleWave.Domain.Library.GenreAlias", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid");
+
+ b.Property("GenreId")
+ .HasColumnType("uuid");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GenreId");
+
+ b.HasIndex("Value")
+ .IsUnique();
+
+ b.ToTable("GenreAliases");
+ });
+
+ modelBuilder.Entity("TeleWave.Domain.Library.Show", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid");
+
+ b.Property("Audience")
+ .HasColumnType("integer");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Description")
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b.Property("FranchiseExternalId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("FranchiseName")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("Kind")
+ .HasColumnType("integer");
+
+ b.Property("MetadataExternalId")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("MetadataProvider")
+ .HasMaxLength(16)
+ .HasColumnType("character varying(16)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("OriginalName")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("PosterImageId")
+ .HasColumnType("uuid");
+
+ b.Property("Year")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.ToTable("Shows");
+ });
+
+ modelBuilder.Entity("TeleWave.Domain.Library.ShowEpisode", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid");
+
+ b.Property("AirDate")
+ .HasColumnType("date");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Episode")
+ .HasColumnType("integer");
+
+ b.Property("MediaAssetId")
+ .HasColumnType("uuid");
+
+ b.Property("Overview")
+ .HasMaxLength(4096)
+ .HasColumnType("character varying(4096)");
+
+ b.Property("Position")
+ .HasColumnType("integer");
+
+ b.Property("Season")
+ .HasColumnType("integer");
+
+ b.Property("ShowId")
+ .HasColumnType("uuid");
+
+ b.Property("StillImageId")
+ .HasColumnType("uuid");
+
+ b.Property("Title")
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("MediaAssetId");
+
+ b.HasIndex("ShowId", "Position");
+
+ b.ToTable("ShowEpisode");
+ });
+
+ modelBuilder.Entity("TeleWave.Domain.Library.ShowGenre", b =>
+ {
+ b.Property("ShowId")
+ .HasColumnType("uuid");
+
+ b.Property("GenreId")
+ .HasColumnType("uuid");
+
+ b.Property("IsPrimary")
+ .HasColumnType("boolean");
+
+ b.HasKey("ShowId", "GenreId");
+
+ b.HasIndex("GenreId");
+
+ b.ToTable("ShowGenres");
+ });
+
+ modelBuilder.Entity("TeleWave.Domain.Media.MediaAsset", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid");
+
+ b.Property("AudioCodec")
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Duration")
+ .HasColumnType("interval");
+
+ b.Property("ErrorMessage")
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b.Property("Height")
+ .HasColumnType("integer");
+
+ b.Property("OriginalExtension")
+ .IsRequired()
+ .HasMaxLength(16)
+ .HasColumnType("character varying(16)");
+
+ b.Property("OriginalFileName")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)");
+
+ b.Property("ProcessingDuration")
+ .HasColumnType("interval");
+
+ b.Property("ProcessingStartedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("RelativePath")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("SegmentCount")
+ .HasColumnType("integer");
+
+ b.Property("SegmentSeconds")
+ .HasColumnType("integer");
+
+ b.Property("Source")
+ .HasColumnType("integer");
+
+ b.Property("Status")
+ .HasColumnType("integer");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("VideoCodec")
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)");
+
+ b.Property("Width")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatedAt");
+
+ b.HasIndex("Status");
+
+ b.ToTable("MediaAssets");
+ });
+
+ modelBuilder.Entity("TeleWave.Domain.Notifications.TelegramLinkCode", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid");
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ExpiresAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UsedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Code")
+ .IsUnique();
+
+ b.ToTable("TelegramLinkCodes");
+ });
+
+ modelBuilder.Entity("TeleWave.Domain.Notifications.TelegramSettings", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid");
+
+ b.Property("BotToken")
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)");
+
+ b.Property("BotUsername")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("IsEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("LastContactAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("LastError")
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)");
+
+ b.Property("LastUpdateId")
+ .HasColumnType("bigint");
+
+ b.Property("NotifiedUntil")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ProxyHost")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("ProxyKind")
+ .HasColumnType("integer");
+
+ b.Property("ProxyPassword")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("ProxyPort")
+ .HasColumnType("integer");
+
+ b.Property("ProxyUsername")
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)");
+
+ b.Property("Transport")
+ .HasColumnType("integer");
+
+ b.Property("WebhookSecret")
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)");
+
+ b.Property("WebhookUrl")
+ .HasMaxLength(512)
+ .HasColumnType("character varying(512)");
+
+ b.HasKey("Id");
+
+ b.ToTable("TelegramSettings");
+ });
+
+ modelBuilder.Entity("TeleWave.Domain.Notifications.TelegramSubscriber", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid");
+
+ b.Property("ChatId")
+ .HasColumnType("bigint");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("DisplayName")
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)");
+
+ b.Property("IsStopped")
+ .HasColumnType("boolean");
+
+ b.Property("LastSeenAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("MenuMessageId")
+ .HasColumnType("bigint");
+
+ b.Property("NoticeMessageId")
+ .HasColumnType("bigint");
+
+ b.Property("UserId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ChatId")
+ .IsUnique();
+
+ b.HasIndex("UserId");
+
+ b.ToTable("TelegramSubscribers");
+ });
+
+ modelBuilder.Entity("TeleWave.Domain.Notifications.TelegramSubscription", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid");
+
+ b.Property("ChannelId")
+ .HasColumnType("uuid");
+
+ b.Property("Kind")
+ .HasColumnType("integer");
+
+ b.Property("SubscriberId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ChannelId", "Kind");
+
+ b.HasIndex("SubscriberId", "ChannelId", "Kind")
+ .IsUnique();
+
+ b.ToTable("TelegramSubscriptions");
+ });
+
+ modelBuilder.Entity("TeleWave.Domain.Programming.GridLayer", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid");
+
+ b.Property("ApplicabilityJson")
+ .HasColumnType("jsonb");
+
+ b.Property("IsBackground")
+ .HasColumnType("boolean");
+
+ b.Property("IsEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)");
+
+ b.Property("Priority")
+ .HasColumnType("integer");
+
+ b.Property("TemplateId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("TemplateId", "Priority");
+
+ b.ToTable("GridLayers");
+ });
+
+ modelBuilder.Entity("TeleWave.Domain.Programming.Group", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Description")
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)");
+
+ b.Property("FilterJson")
+ .HasColumnType("jsonb");
+
+ b.Property("ItemCount")
+ .HasColumnType("integer");
+
+ b.Property("Mode")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(0);
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("StatsComputedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("TotalDuration")
+ .HasColumnType("interval");
+
+ b.Property("UnitCount")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.ToTable("Groups");
+ });
+
+ modelBuilder.Entity("TeleWave.Domain.Programming.GroupItem", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid");
+
+ b.Property("ElementId")
+ .HasColumnType("uuid");
+
+ b.Property("ElementKind")
+ .HasColumnType("integer");
+
+ b.Property("GroupId")
+ .HasColumnType("uuid");
+
+ b.Property("Position")
+ .HasColumnType("integer");
+
+ b.Property("Role")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(0);
+
+ b.Property("Weight")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ElementKind", "ElementId");
+
+ b.HasIndex("GroupId", "Position");
+
+ b.HasIndex("GroupId", "ElementKind", "ElementId")
+ .IsUnique();
+
+ b.ToTable("GroupItems");
+ });
+
+ modelBuilder.Entity("TeleWave.Domain.Programming.JunctionElement", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid");
+
+ b.Property("AmountMode")
+ .HasColumnType("integer");
+
+ b.Property("AmountValue")
+ .HasColumnType("integer");
+
+ b.Property("BumperTemplateId")
+ .HasColumnType("uuid");
+
+ b.Property("BumperVariantId")
+ .HasColumnType("uuid");
+
+ b.Property("ChoiceKey")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)");
+
+ b.Property("ChoiceWeight")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(1);
+
+ b.Property("ConditionsJson")
+ .HasColumnType("jsonb");
+
+ b.Property("GroupId")
+ .HasColumnType("uuid");
+
+ b.Property("IsRequired")
+ .HasColumnType("boolean");
+
+ b.Property("JunctionTemplateId")
+ .HasColumnType("uuid");
+
+ b.Property("Kind")
+ .HasColumnType("integer");
+
+ b.Property("Position")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("BumperTemplateId");
+
+ b.HasIndex("BumperVariantId");
+
+ b.HasIndex("GroupId");
+
+ b.HasIndex("JunctionTemplateId", "Position");
+
+ b.ToTable("JunctionElements");
+ });
+
+ modelBuilder.Entity("TeleWave.Domain.Programming.JunctionTemplate", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("MaxTotalSeconds")
+ .HasColumnType("integer");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name");
+
+ b.ToTable("JunctionTemplates");
+ });
+
+ modelBuilder.Entity("TeleWave.Domain.Programming.ScheduleTemplate", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid");
+
+ b.Property("AppliedRevision")
+ .HasColumnType("integer");
+
+ b.Property("AppliedSnapshotJson")
+ .HasColumnType("jsonb");
+
+ b.Property("ChannelId")
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("DefaultJunctionId")
+ .HasColumnType("uuid");
+
+ b.Property("FallbackGroupId")
+ .HasColumnType("uuid");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("Revision")
+ .HasColumnType("integer");
+
+ b.Property("RulesJson")
+ .HasColumnType("jsonb");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ChannelId");
+
+ b.ToTable("ScheduleTemplates");
+ });
+
+ modelBuilder.Entity("TeleWave.Domain.Programming.Slot", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid");
+
+ b.Property("BlockMode")
+ .HasColumnType("integer");
+
+ b.Property("BlockValue")
+ .HasColumnType("integer");
+
+ b.Property("Daypart")
+ .HasColumnType("integer");
+
+ b.Property("GroupId")
+ .HasColumnType("uuid");
+
+ b.Property("IsAnchor")
+ .HasColumnType("boolean");
+
+ b.Property("JunctionAfterId")
+ .HasColumnType("uuid");
+
+ b.Property("JunctionBetweenId")
+ .HasColumnType("uuid");
+
+ b.Property