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