diff --git a/HarmonyDB and OneShelf.sln b/HarmonyDB and OneShelf.sln
index 052ed862..833ef14a 100644
--- a/HarmonyDB and OneShelf.sln
+++ b/HarmonyDB and OneShelf.sln
@@ -213,6 +213,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OneShelf.Videos.Database",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OneShelf.Videos.BusinessLogic", "OneShelf.Videos\OneShelf.Videos.BusinessLogic\OneShelf.Videos.BusinessLogic.csproj", "{76A8BAAB-7065-45F8-82E8-E188D4C27EB3}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OneShelf.Videos.Telegram", "OneShelf.Videos\OneShelf.Videos.Telegram\OneShelf.Videos.Telegram.csproj", "{F683907D-EF95-400A-AE6F-FD89457C1DAE}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OneShelf.Videos.Api", "OneShelf.Videos\OneShelf.Videos.Api\OneShelf.Videos.Api.csproj", "{BF8E6306-8A77-49A2-85B9-59B086AAA1FA}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -527,6 +531,14 @@ Global
{76A8BAAB-7065-45F8-82E8-E188D4C27EB3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{76A8BAAB-7065-45F8-82E8-E188D4C27EB3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{76A8BAAB-7065-45F8-82E8-E188D4C27EB3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F683907D-EF95-400A-AE6F-FD89457C1DAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F683907D-EF95-400A-AE6F-FD89457C1DAE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F683907D-EF95-400A-AE6F-FD89457C1DAE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F683907D-EF95-400A-AE6F-FD89457C1DAE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BF8E6306-8A77-49A2-85B9-59B086AAA1FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BF8E6306-8A77-49A2-85B9-59B086AAA1FA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BF8E6306-8A77-49A2-85B9-59B086AAA1FA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BF8E6306-8A77-49A2-85B9-59B086AAA1FA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -610,6 +622,8 @@ Global
{99134E7C-CE2F-4641-A9E3-B961F76307F0} = {5F839A7C-B540-4475-9FFE-9E5E4FC10D78}
{D702C117-2C9A-44E0-B428-30371616FBAE} = {5F839A7C-B540-4475-9FFE-9E5E4FC10D78}
{76A8BAAB-7065-45F8-82E8-E188D4C27EB3} = {5F839A7C-B540-4475-9FFE-9E5E4FC10D78}
+ {F683907D-EF95-400A-AE6F-FD89457C1DAE} = {5F839A7C-B540-4475-9FFE-9E5E4FC10D78}
+ {BF8E6306-8A77-49A2-85B9-59B086AAA1FA} = {5F839A7C-B540-4475-9FFE-9E5E4FC10D78}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {59093261-FDDA-411A-852D-EA21AEF83E07}
diff --git a/OneShelf.Videos/OneShelf.Videos.Api/.gitignore b/OneShelf.Videos/OneShelf.Videos.Api/.gitignore
new file mode 100644
index 00000000..ff5b00c5
--- /dev/null
+++ b/OneShelf.Videos/OneShelf.Videos.Api/.gitignore
@@ -0,0 +1,264 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+# Azure Functions localsettings file
+local.settings.json
+
+# User-specific files
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+
+# Visual Studio 2015 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# DNX
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding add-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# TODO: Comment the next line if you want to checkin your web deploy settings
+# but database connection strings (with potential passwords) will be unencrypted
+#*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/packages/*
+# except build/, which is used as an MSBuild target.
+!**/packages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/packages/repositories.config
+# NuGet v3's project.json files produces more ignoreable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+node_modules/
+orleans.codegen.cs
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+# CodeRush
+.cr/
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
\ No newline at end of file
diff --git a/OneShelf.Videos/OneShelf.Videos.Api/OneShelf.Videos.Api.csproj b/OneShelf.Videos/OneShelf.Videos.Api/OneShelf.Videos.Api.csproj
new file mode 100644
index 00000000..51053819
--- /dev/null
+++ b/OneShelf.Videos/OneShelf.Videos.Api/OneShelf.Videos.Api.csproj
@@ -0,0 +1,34 @@
+
+
+ net8.0
+ v4
+ Exe
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+ Never
+
+
+
+
+
+
\ No newline at end of file
diff --git a/OneShelf.Videos/OneShelf.Videos.Api/Program.cs b/OneShelf.Videos/OneShelf.Videos.Api/Program.cs
new file mode 100644
index 00000000..c85899c1
--- /dev/null
+++ b/OneShelf.Videos/OneShelf.Videos.Api/Program.cs
@@ -0,0 +1,17 @@
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using OneShelf.Videos.Telegram;
+
+var host = new HostBuilder()
+ .ConfigureFunctionsWebApplication()
+ .ConfigureServices((context, services) =>
+ {
+ services.AddApplicationInsightsTelemetryWorkerService();
+ services.ConfigureFunctionsApplicationInsights();
+
+ services.AddProcessor(context.Configuration);
+ })
+ .Build();
+
+host.Run();
diff --git a/OneShelf.Videos/OneShelf.Videos.Api/Properties/launchSettings.json b/OneShelf.Videos/OneShelf.Videos.Api/Properties/launchSettings.json
new file mode 100644
index 00000000..a79b9ff1
--- /dev/null
+++ b/OneShelf.Videos/OneShelf.Videos.Api/Properties/launchSettings.json
@@ -0,0 +1,9 @@
+{
+ "profiles": {
+ "OneShelf.Videos.Api": {
+ "commandName": "Project",
+ "commandLineArgs": "--port 7218",
+ "launchBrowser": false
+ }
+ }
+}
\ No newline at end of file
diff --git a/OneShelf.Videos/OneShelf.Videos.Api/Properties/serviceDependencies.json b/OneShelf.Videos/OneShelf.Videos.Api/Properties/serviceDependencies.json
new file mode 100644
index 00000000..df4dcc9d
--- /dev/null
+++ b/OneShelf.Videos/OneShelf.Videos.Api/Properties/serviceDependencies.json
@@ -0,0 +1,11 @@
+{
+ "dependencies": {
+ "appInsights1": {
+ "type": "appInsights"
+ },
+ "storage1": {
+ "type": "storage",
+ "connectionId": "AzureWebJobsStorage"
+ }
+ }
+}
\ No newline at end of file
diff --git a/OneShelf.Videos/OneShelf.Videos.Api/Properties/serviceDependencies.local.json b/OneShelf.Videos/OneShelf.Videos.Api/Properties/serviceDependencies.local.json
new file mode 100644
index 00000000..b804a289
--- /dev/null
+++ b/OneShelf.Videos/OneShelf.Videos.Api/Properties/serviceDependencies.local.json
@@ -0,0 +1,11 @@
+{
+ "dependencies": {
+ "appInsights1": {
+ "type": "appInsights.sdk"
+ },
+ "storage1": {
+ "type": "storage.emulator",
+ "connectionId": "AzureWebJobsStorage"
+ }
+ }
+}
\ No newline at end of file
diff --git a/OneShelf.Videos/OneShelf.Videos.Api/Telegram/BotFunctions.cs b/OneShelf.Videos/OneShelf.Videos.Api/Telegram/BotFunctions.cs
new file mode 100644
index 00000000..e5523d4f
--- /dev/null
+++ b/OneShelf.Videos/OneShelf.Videos.Api/Telegram/BotFunctions.cs
@@ -0,0 +1,47 @@
+using System.Text.Json;
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Azure.Functions.Worker.Http;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using OneShelf.Telegram.Services;
+using OneShelf.Videos.Telegram.Model;
+using Telegram.BotAPI.GettingUpdates;
+
+namespace OneShelf.Videos.Api.Telegram;
+
+public class BotFunctions
+{
+ private const string QueueNameUpdates = "updates";
+ private const string SecretTokenHeaderName = "X-Telegram-Bot-Api-Secret-Token";
+
+ private readonly ILogger _logger;
+ private readonly Pipeline _pipeline;
+ private readonly TelegramOptions _options;
+
+ public BotFunctions(ILogger logger, Pipeline pipeline, IOptions options)
+ {
+ _logger = logger;
+ _pipeline = pipeline;
+ _options = options.Value;
+ }
+
+ [Function("UpdatesQueueTrigger")]
+ public async Task UpdatesQueueTrigger(
+ [QueueTrigger(QueueNameUpdates)] string myQueueItem)
+ {
+ var update = JsonSerializer.Deserialize(myQueueItem) ?? throw new("Empty request body.");
+ await await _pipeline.ProcessSyncSafeAndDispose(update, -1);
+ }
+
+ [Function(nameof(IncomingUpdate))]
+ [QueueOutput(QueueNameUpdates)]
+ public async Task IncomingUpdate(
+ [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestData req)
+ {
+ if (!req.Headers.GetValues(SecretTokenHeaderName).Contains(_options.WebHooksSecretToken)) throw new("Bad secret token.");
+
+ var requestBody = await req.ReadAsStringAsync() ?? throw new("Empty request body.");
+
+ return requestBody;
+ }
+}
\ No newline at end of file
diff --git a/OneShelf.Videos/OneShelf.Videos.Api/Telegram/ManagementFunctions.cs b/OneShelf.Videos/OneShelf.Videos.Api/Telegram/ManagementFunctions.cs
new file mode 100644
index 00000000..41508f02
--- /dev/null
+++ b/OneShelf.Videos/OneShelf.Videos.Api/Telegram/ManagementFunctions.cs
@@ -0,0 +1,110 @@
+using System.Net;
+using System.Text.Json;
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Azure.Functions.Worker.Http;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using OneShelf.Videos.Database;
+using OneShelf.Videos.Telegram.Model;
+using Telegram.BotAPI;
+using Telegram.BotAPI.GettingUpdates;
+
+namespace OneShelf.Videos.Api.Telegram
+{
+ public class ManagementFunctions
+ {
+ private readonly ILogger _logger;
+ private readonly TelegramOptions _telegramOptions;
+ private readonly TelegramBotClient _api;
+ private readonly VideosDatabase _videosDatabase;
+
+ public ManagementFunctions(ILogger logger, IOptions telegramOptions, VideosDatabase videosDatabase)
+ {
+ _logger = logger;
+ _videosDatabase = videosDatabase;
+ _telegramOptions = telegramOptions.Value;
+ _api = new(_telegramOptions.Token);
+ }
+
+ [Function(nameof(Check))]
+ public async Task Check([HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequestData req)
+ {
+ _logger.LogInformation("C# HTTP trigger function processed a request.");
+
+ var response = req.CreateResponse(HttpStatusCode.OK);
+ response.Headers.Add("Content-Type", "text/plain; charset=utf-8");
+
+ await response.WriteStringAsync("Welcome to Azure Functions!");
+
+ return response;
+ }
+
+ [Function(nameof(CheckDb))]
+ public async Task CheckDb([HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequestData req)
+ {
+ _logger.LogInformation("C# HTTP trigger function processed a request.");
+
+ var response = req.CreateResponse(HttpStatusCode.OK);
+ response.Headers.Add("Content-Type", "text/plain; charset=utf-8");
+
+ await response.WriteStringAsync($"Welcome to Azure Functions! Chats Count = {await _videosDatabase.Topics.CountAsync()}");
+
+ return response;
+ }
+
+ [Function(nameof(MigrateDb))]
+ public async Task MigrateDb([HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequestData req)
+ {
+ _logger.LogInformation("C# HTTP trigger function processed a request.");
+
+ await _videosDatabase.Database.MigrateAsync();
+
+ var response = req.CreateResponse(HttpStatusCode.OK);
+ response.Headers.Add("Content-Type", "text/plain; charset=utf-8");
+
+ await response.WriteStringAsync("Done.");
+
+ return response;
+ }
+
+ [Function(nameof(SetWebHook))]
+ public async Task SetWebHook([HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequestData req)
+ {
+ _logger.LogInformation("C# HTTP trigger function processed a request.");
+
+ var response = req.CreateResponse(HttpStatusCode.OK);
+ response.Headers.Add("Content-Type", "text/plain; charset=utf-8");
+
+ await response.WriteStringAsync((await _api.SetWebhookAsync($"https://{req.Url.Host}/api/{nameof(BotFunctions.IncomingUpdate)}", secretToken: _telegramOptions.WebHooksSecretToken)).ToString());
+
+ return response;
+ }
+
+ [Function(nameof(DeleteWebHook))]
+ public async Task DeleteWebHook([HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequestData req)
+ {
+ _logger.LogInformation("C# HTTP trigger function processed a request.");
+
+ var response = req.CreateResponse(HttpStatusCode.OK);
+ response.Headers.Add("Content-Type", "text/plain; charset=utf-8");
+
+ await response.WriteStringAsync((await _api.DeleteWebhookAsync()).ToString());
+
+ return response;
+ }
+
+ [Function(nameof(GetWebHook))]
+ public async Task GetWebHook([HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequestData req)
+ {
+ _logger.LogInformation("C# HTTP trigger function processed a request.");
+
+ var response = req.CreateResponse(HttpStatusCode.OK);
+ response.Headers.Add("Content-Type", "text/plain; charset=utf-8");
+
+ await response.WriteStringAsync(JsonSerializer.Serialize(await _api.GetWebhookInfoAsync()));
+
+ return response;
+ }
+ }
+}
diff --git a/OneShelf.Videos/OneShelf.Videos.Api/host.json b/OneShelf.Videos/OneShelf.Videos.Api/host.json
new file mode 100644
index 00000000..ee5cf5f8
--- /dev/null
+++ b/OneShelf.Videos/OneShelf.Videos.Api/host.json
@@ -0,0 +1,12 @@
+{
+ "version": "2.0",
+ "logging": {
+ "applicationInsights": {
+ "samplingSettings": {
+ "isEnabled": true,
+ "excludedTypes": "Request"
+ },
+ "enableLiveMetricsFilters": true
+ }
+ }
+}
\ No newline at end of file
diff --git a/OneShelf.Videos/OneShelf.Videos.Api/readme.md b/OneShelf.Videos/OneShelf.Videos.Api/readme.md
new file mode 100644
index 00000000..adabb85e
--- /dev/null
+++ b/OneShelf.Videos/OneShelf.Videos.Api/readme.md
@@ -0,0 +1,64 @@
+# A note for myself
+
+- Issue: there's a 20MB bot download limit.
+ - Consequently, a bot is not needed.
+- Required cloud connection string: `VideosDatabase`
+- Required cloud app settings:
+```
+{
+ "name": "CasCap__GooglePhotosOptions__ClientId",
+ "value": //
+ "slotSetting": false
+ },
+ {
+ "name": "CasCap__GooglePhotosOptions__ClientSecret",
+ "value": //
+ "slotSetting": false
+ },
+ {
+ "name": "CasCap__GooglePhotosOptions__FileDataStoreFullPathOverride",
+ "value": "/sharepath1/_auth",
+ "slotSetting": false
+ },
+ {
+ "name": "CasCap__GooglePhotosOptions__Scopes__0",
+ "value": "ReadOnly",
+ "slotSetting": false
+ },
+ {
+ "name": "CasCap__GooglePhotosOptions__Scopes__1",
+ "value": "AppendOnly",
+ "slotSetting": false
+ },
+ {
+ "name": "CasCap__GooglePhotosOptions__Scopes__2",
+ "value": "AppCreatedData",
+ "slotSetting": false
+ },
+ {
+ "name": "CasCap__GooglePhotosOptions__User",
+ "value": //
+ "slotSetting": false
+ },
+ {
+ "name": "TelegramOptions__AdminId",
+ "value": //
+ "slotSetting": false
+ },
+ {
+ "name": "TelegramOptions__Token",
+ "value": //
+ "slotSetting": false
+ },
+ {
+ "name": "TelegramOptions__WebHooksSecretToken",
+ "value": //
+ "slotSetting": false
+ },
+ {
+ "name": "VideosOptions__BasePath",
+ "value": "/sharepath1",
+ "slotSetting": false
+ }
+```
+- Cloud drive mapping: `az webapp config storage-account add --access-key (storage account access key) -t AzureFiles --account-name (storage account name) --custom-id (storage account name) --name (function app name) --resource-group (function app resource group) --sn (file share name) --mount-path /sharepath1`
\ No newline at end of file
diff --git a/OneShelf.Videos/OneShelf.Videos.Database/Models/TelegramUpdate.cs b/OneShelf.Videos/OneShelf.Videos.Database/Models/TelegramUpdate.cs
new file mode 100644
index 00000000..06eeb6cf
--- /dev/null
+++ b/OneShelf.Videos/OneShelf.Videos.Database/Models/TelegramUpdate.cs
@@ -0,0 +1,12 @@
+namespace OneShelf.Videos.Database.Models;
+
+public class TelegramUpdate
+{
+ public int Id { get; set; }
+
+ public required int TelegramUpdateId { get; set; }
+
+ public required DateTime CreatedOn { get; set; }
+
+ public required string Json { get; set; }
+}
\ No newline at end of file
diff --git a/OneShelf.Videos/OneShelf.Videos.Database/VideosDatabase.cs b/OneShelf.Videos/OneShelf.Videos.Database/VideosDatabase.cs
index 3b705686..53617197 100644
--- a/OneShelf.Videos/OneShelf.Videos.Database/VideosDatabase.cs
+++ b/OneShelf.Videos/OneShelf.Videos.Database/VideosDatabase.cs
@@ -38,6 +38,8 @@ public VideosDatabase(DbContextOptions options)
public required DbSet Albums { get; set; }
public required DbSet AlbumConstraints { get; set; }
public required DbSet UploadedAlbums { get; set; }
+
+ public required DbSet TelegramUpdates { get; set; }
public async Task UpdateMediaTopics()
{
diff --git a/OneShelf.Videos/OneShelf.Videos.Telegram/Commands/GetFileSize.cs b/OneShelf.Videos/OneShelf.Videos.Telegram/Commands/GetFileSize.cs
new file mode 100644
index 00000000..1145777b
--- /dev/null
+++ b/OneShelf.Videos/OneShelf.Videos.Telegram/Commands/GetFileSize.cs
@@ -0,0 +1,20 @@
+using OneShelf.Telegram.Model.CommandAttributes;
+using OneShelf.Telegram.Model.Ios;
+using OneShelf.Telegram.Services.Base;
+
+namespace OneShelf.Videos.Telegram.Commands;
+
+[AdminCommand("get_file_size", "Файлик", "Посмотреть файл")]
+public class GetFileSize : Command
+{
+ public GetFileSize(Io io)
+ : base(io)
+ {
+ }
+
+ protected override async Task ExecuteQuickly()
+ {
+ var path = Io.FreeChoice("Path to file (\\ will be replaced with /):");
+ Io.WriteLine(new FileInfo(path.Replace('\\', '/')).Length.ToString());
+ }
+}
\ No newline at end of file
diff --git a/OneShelf.Videos/OneShelf.Videos.Telegram/Commands/ListAlbums.cs b/OneShelf.Videos/OneShelf.Videos.Telegram/Commands/ListAlbums.cs
new file mode 100644
index 00000000..f44c53bf
--- /dev/null
+++ b/OneShelf.Videos/OneShelf.Videos.Telegram/Commands/ListAlbums.cs
@@ -0,0 +1,32 @@
+using OneShelf.Telegram.Model.CommandAttributes;
+using OneShelf.Telegram.Model.Ios;
+using OneShelf.Telegram.Services.Base;
+using OneShelf.Videos.BusinessLogic.Services;
+
+namespace OneShelf.Videos.Telegram.Commands;
+
+[AdminCommand("list_albums", "Альбомы", "Посмотреть альбомы")]
+public class ListAlbums : Command
+{
+ private readonly Service2 _service2;
+
+ public ListAlbums(Io io, Service2 service2)
+ : base(io)
+ {
+ _service2 = service2;
+ }
+
+ protected override async Task ExecuteQuickly()
+ {
+ Scheduled(List());
+ }
+
+ private async Task List()
+ {
+ var albums = await _service2.ListAlbums();
+ foreach (var album in albums)
+ {
+ Io.WriteLine(album);
+ }
+ }
+}
\ No newline at end of file
diff --git a/OneShelf.Videos/OneShelf.Videos.Telegram/Commands/ViewTopics.cs b/OneShelf.Videos/OneShelf.Videos.Telegram/Commands/ViewTopics.cs
new file mode 100644
index 00000000..5e52930d
--- /dev/null
+++ b/OneShelf.Videos/OneShelf.Videos.Telegram/Commands/ViewTopics.cs
@@ -0,0 +1,32 @@
+using Microsoft.EntityFrameworkCore;
+using OneShelf.Telegram.Model.CommandAttributes;
+using OneShelf.Telegram.Model.Ios;
+using OneShelf.Telegram.Services.Base;
+using OneShelf.Videos.Database;
+
+namespace OneShelf.Videos.Telegram.Commands;
+
+[AdminCommand("view_topics", "Топики", "Посмотреть топики")]
+public class ViewTopics : Command
+{
+ private readonly VideosDatabase _videosDatabase;
+
+ public ViewTopics(Io io, VideosDatabase videosDatabase)
+ : base(io)
+ {
+ _videosDatabase = videosDatabase;
+ }
+
+ protected override async Task ExecuteQuickly()
+ {
+ foreach (var topic in await _videosDatabase.Topics
+ .Include(x => x.LiveChat)
+ .Include(x => x.LiveTopic)
+ .Include(x => x.StaticChat)
+ .Include(x => x.StaticTopic)
+ .ToListAsync())
+ {
+ Io.WriteLine($"{(topic.LiveChat != null ? "L" : "S")}: {topic.LiveChat?.Title ?? topic.StaticChat!.Name} / {topic.LiveTopic?.Title ?? topic.StaticTopic!.Title}");
+ }
+ }
+}
\ No newline at end of file
diff --git a/OneShelf.Videos/OneShelf.Videos.Telegram/Model/TelegramOptions.cs b/OneShelf.Videos/OneShelf.Videos.Telegram/Model/TelegramOptions.cs
new file mode 100644
index 00000000..837a5b64
--- /dev/null
+++ b/OneShelf.Videos/OneShelf.Videos.Telegram/Model/TelegramOptions.cs
@@ -0,0 +1,8 @@
+namespace OneShelf.Videos.Telegram.Model;
+
+public record TelegramOptions : OneShelf.Telegram.Options.TelegramOptions
+{
+ public required string Token { get; init; }
+
+ public required string WebHooksSecretToken { get; init; }
+}
\ No newline at end of file
diff --git a/OneShelf.Videos/OneShelf.Videos.Telegram/OneShelf.Videos.Telegram.csproj b/OneShelf.Videos/OneShelf.Videos.Telegram/OneShelf.Videos.Telegram.csproj
new file mode 100644
index 00000000..c368c890
--- /dev/null
+++ b/OneShelf.Videos/OneShelf.Videos.Telegram/OneShelf.Videos.Telegram.csproj
@@ -0,0 +1,16 @@
+
+
+
+ net8.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
diff --git a/OneShelf.Videos/OneShelf.Videos.Telegram/PipelineHandlers/UpdatesCollector.cs b/OneShelf.Videos/OneShelf.Videos.Telegram/PipelineHandlers/UpdatesCollector.cs
new file mode 100644
index 00000000..24a57adc
--- /dev/null
+++ b/OneShelf.Videos/OneShelf.Videos.Telegram/PipelineHandlers/UpdatesCollector.cs
@@ -0,0 +1,42 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using OneShelf.Telegram.Services.Base;
+using OneShelf.Videos.Database;
+using OneShelf.Videos.Database.Models;
+using OneShelf.Videos.Telegram.Services;
+using Telegram.BotAPI.GettingUpdates;
+
+namespace OneShelf.Videos.Telegram.PipelineHandlers;
+
+public class UpdatesCollector : PipelineHandler
+{
+ private readonly VideosDatabase _videosDatabase;
+ private readonly Scope _scope;
+
+ public UpdatesCollector(IScopedAbstractions scopedAbstractions, VideosDatabase videosDatabase, Scope scope)
+ : base(scopedAbstractions)
+ {
+ _videosDatabase = videosDatabase;
+ _scope = scope;
+ }
+
+ protected override async Task HandleSync(Update update)
+ {
+ var dbUpdate = new TelegramUpdate
+ {
+ TelegramUpdateId = update.UpdateId,
+ CreatedOn = DateTime.Now,
+ Json = JsonSerializer.Serialize(update, new JsonSerializerOptions
+ {
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ }),
+ };
+
+ _videosDatabase.TelegramUpdates.Add(dbUpdate);
+ await _videosDatabase.SaveChangesAsync();
+
+ _scope.Initialize(dbUpdate.Id);
+
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/OneShelf.Videos/OneShelf.Videos.Telegram/ServiceCollectionExtensions.cs b/OneShelf.Videos/OneShelf.Videos.Telegram/ServiceCollectionExtensions.cs
new file mode 100644
index 00000000..a02a472d
--- /dev/null
+++ b/OneShelf.Videos/OneShelf.Videos.Telegram/ServiceCollectionExtensions.cs
@@ -0,0 +1,41 @@
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using OneShelf.Telegram;
+using OneShelf.Telegram.Commands;
+using OneShelf.Telegram.PipelineHandlers;
+using OneShelf.Videos.BusinessLogic;
+using OneShelf.Videos.Telegram.Commands;
+using OneShelf.Videos.Telegram.Model;
+using OneShelf.Videos.Telegram.PipelineHandlers;
+using OneShelf.Videos.Telegram.Services;
+
+namespace OneShelf.Videos.Telegram;
+
+public static class ServiceCollectionExtensions
+{
+ public static IServiceCollection AddProcessor(this IServiceCollection services, IConfiguration configuration)
+ {
+ services.Configure(options => configuration.Bind(nameof(TelegramOptions), options));
+
+ services
+ .AddTelegram(configuration, o =>
+ o
+ .AddCommand()
+ .AddCommand()
+
+ .AddCommand()
+ .AddCommand()
+ .AddCommand()
+ .AddCommand()
+
+ .AddPipelineHandlerInOrder()
+ .AddPipelineHandlerInOrder()
+ );
+
+ services
+ .AddVideosBusinessLogic(configuration)
+ .AddScoped();
+
+ return services;
+ }
+}
\ No newline at end of file
diff --git a/OneShelf.Videos/OneShelf.Videos.Telegram/Services/Scope.cs b/OneShelf.Videos/OneShelf.Videos.Telegram/Services/Scope.cs
new file mode 100644
index 00000000..7b3ec2e2
--- /dev/null
+++ b/OneShelf.Videos/OneShelf.Videos.Telegram/Services/Scope.cs
@@ -0,0 +1,16 @@
+namespace OneShelf.Videos.Telegram.Services;
+
+public class Scope
+{
+ private int? _updateId;
+
+ public int UpdateId => _updateId ?? throw new("Not initialized.");
+
+ public void Initialize(int updateId)
+ {
+ if (_updateId.HasValue)
+ throw new("Already initialized.");
+
+ _updateId = updateId;
+ }
+}
\ No newline at end of file
diff --git a/OneShelf.Videos/OneShelf.Videos.Telegram/Services/ScopedAbstractions.cs b/OneShelf.Videos/OneShelf.Videos.Telegram/Services/ScopedAbstractions.cs
new file mode 100644
index 00000000..4efcc3d0
--- /dev/null
+++ b/OneShelf.Videos/OneShelf.Videos.Telegram/Services/ScopedAbstractions.cs
@@ -0,0 +1,31 @@
+using Microsoft.Extensions.Options;
+using OneShelf.Telegram.Model;
+using OneShelf.Telegram.Services.Base;
+using OneShelf.Videos.Telegram.Model;
+using Telegram.BotAPI.GettingUpdates;
+
+namespace OneShelf.Videos.Telegram.Services;
+
+public class ScopedAbstractions : IScopedAbstractions
+{
+ private readonly TelegramOptions _options;
+
+ public ScopedAbstractions(IOptions options)
+ {
+ _options = options.Value;
+ }
+
+ public async Task Initialize(int domainId)
+ {
+ }
+
+ public Role GetNonAdminRole(long userId) => Role.Regular;
+
+ public string GetBotToken() => _options.Token;
+
+ public IEnumerable GetDomainAdministratorIds() => [];
+
+ public async Task OnDialogInteraction(Update update, long userId, string? text)
+ {
+ }
+}
\ No newline at end of file
diff --git a/OneShelf.Videos/OneShelf.Videos.Telegram/Services/SingletonAbstractions.cs b/OneShelf.Videos/OneShelf.Videos.Telegram/Services/SingletonAbstractions.cs
new file mode 100644
index 00000000..8817efea
--- /dev/null
+++ b/OneShelf.Videos/OneShelf.Videos.Telegram/Services/SingletonAbstractions.cs
@@ -0,0 +1,56 @@
+using OneShelf.Telegram.Commands;
+using OneShelf.Telegram.Model;
+using OneShelf.Telegram.Services.Base;
+using OneShelf.Videos.Telegram.Commands;
+
+namespace OneShelf.Videos.Telegram.Services;
+
+public class SingletonAbstractions : ISingletonAbstractions
+{
+ public List> GetCommandsGrid() => [
+ [
+ typeof(Start),
+ typeof(Help),
+ ],
+ [
+ typeof(ViewTopics),
+ typeof(GetFileSize),
+ typeof(ListAlbums),
+ ],
+ [
+ typeof(UpdateCommands),
+ ]
+ ];
+
+ public Type? GetDefaultCommand() => null;
+
+ public Type GetHelpCommand() => typeof(Help);
+
+ public Markdown GetStartResponse()
+ {
+ var result = new Markdown();
+ result.AppendLine("Ничё не знаю.");
+ return result;
+ }
+
+ public Markdown GetHelpResponseHeader()
+ {
+ var result = new Markdown();
+ result.AppendLine("Ничё не умею.");
+ return result;
+ }
+
+ public string? DialogContinuation => "Выберите следующую команду или посмотрите помощь - /help.";
+
+ public string CommandNotFound => "Такой команды нет. Посмотрите помощь - /help.";
+
+ public string BackgroundErrors => "Произошли ошибки в фоне.";
+
+ public string BackgroundOperationComplete => "Фоновая операция завершилась.";
+
+ public string OperationError => "Извините, случилась ошибка при выполнении операции.";
+
+ public string? NoOperationPlaceholder => "Команда...";
+
+ public string MiddleCommandResponsePostfix => "(или /start чтобы вернуться в начало)";
+}
\ No newline at end of file