mirror of
https://github.com/opengram-server/opengram.git
synced 2026-07-20 04:30:19 +03:00
Initial commit
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
**/.classpath
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/azds.yaml
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
||||
@@ -0,0 +1,60 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- <VersionSuffix>0.6.$([System.DateTime]::Now.ToString(MMdd)).$([System.DateTime]::Now.ToString("HHmm"))</VersionSuffix> -->
|
||||
<VersionSuffix>0.32.206.$([System.DateTime]::Now.ToString("Mdd"))</VersionSuffix>
|
||||
<AssemblyVersion Condition="'$(VersionSuffix)' =='' ">0.0.0.1</AssemblyVersion>
|
||||
<AssemblyVersion Condition="'$(VersionSuffix)' !='' ">$(VersionSuffix)</AssemblyVersion>
|
||||
<Version Condition="'$(VersionSuffix)' =='' ">0.0.1.0</Version>
|
||||
<Version Condition="'$(VersionSuffix)' !='' ">$(VersionSuffix)</Version>
|
||||
<NoWarn>$(NoWarn);CS1591;CS0436;NU1803;</NoWarn>
|
||||
<Authors>MyTelegram</Authors>
|
||||
<RepositoryUrl>https://github.com/loyldg/mytelegram</RepositoryUrl>
|
||||
<Company>MyTelegram</Company>
|
||||
<Copyright>Copyright (c) MyTelegram 2021 - $([System.DateTime]::Now.ToString("yyyy"))</Copyright>
|
||||
|
||||
<IsWindows Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))' == 'true'">true</IsWindows>
|
||||
<IsOSX Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::OSX)))' == 'true'">true</IsOSX>
|
||||
<IsLinux Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))' == 'true'">true</IsLinux>
|
||||
</PropertyGroup>
|
||||
<!--
|
||||
<PropertyGroup>
|
||||
<OutputPath>$(MSBuildThisFileDirectory)out\local\$(AssemblyVersion)\$(Configuration)\$(MSBuildProjectName.Replace("MyTelegram.", ""))</OutputPath>
|
||||
</PropertyGroup> -->
|
||||
|
||||
<PropertyGroup Condition="'$(IsWindows)'=='true'">
|
||||
<DefineConstants>Windows</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(IsOSX)'=='true'">
|
||||
<DefineConstants>OSX</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(IsLinux)'=='true'">
|
||||
<DefineConstants>Linux</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<!--
|
||||
<PropertyGroup>
|
||||
<DebuggerSupport>false</DebuggerSupport>
|
||||
<EnableUnsafeUTF7Encoding>false</EnableUnsafeUTF7Encoding>
|
||||
<EventSourceSupport>false</EventSourceSupport>
|
||||
<HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
<MetadataUpdaterSupport>false</MetadataUpdaterSupport>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<TrimMode>link</TrimMode>
|
||||
<TrimmerRemoveSymbols>true</TrimmerRemoveSymbols>
|
||||
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
|
||||
<IlcOptimizationPreference>Size</IlcOptimizationPreference>
|
||||
</PropertyGroup>
|
||||
-->
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ConfigureAwait.Fody" PrivateAssets="All" />
|
||||
<PackageReference Include="Fody" >
|
||||
<PrivateAssets>All</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,64 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
|
||||
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
|
||||
<PackageVersion Include="BloomFilter.NetCore" Version="2.1.1" />
|
||||
<PackageVersion Include="CommunityToolkit.HighPerformance" Version="8.4.0" />
|
||||
<PackageVersion Include="ConfigureAwait.Fody" PrivateAssets="All" Version="3.3.2" />
|
||||
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageVersion Include="Cube.Timer" Version="1.0.5" />
|
||||
<PackageVersion Include="EventFlow" Version="1.2.1" />
|
||||
<PackageVersion Include="EventFlow.MongoDB" Version="1.2.1" />
|
||||
<PackageVersion Include="Fody" Version="6.9.2" />
|
||||
<PackageVersion Include="Google.Protobuf" Version="3.31.1" />
|
||||
<PackageVersion Include="Grpc.AspNetCore" Version="2.66.0" />
|
||||
<PackageVersion Include="Grpc.Net.Client" Version="2.71.0" />
|
||||
<PackageVersion Include="Grpc.Tools" Version="2.72.0" />
|
||||
<PackageVersion Include="HashedWheelTimer" Version="1.0.3" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options.DataAnnotations" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageVersion Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1-Preview.1" />
|
||||
<PackageVersion Include="MessagePack" Version="2.5.192" />
|
||||
<PackageVersion Include="Minio" Version="6.0.3" />
|
||||
<PackageVersion Include="MongoDB.Driver" Version="3.0.0" />
|
||||
<PackageVersion Include="Polly" Version="8.6.2" />
|
||||
<PackageVersion Include="RabbitMQ.Client" Version="7.1.2" />
|
||||
<PackageVersion Include="Rebus" Version="8.8.0" />
|
||||
<PackageVersion Include="Rebus.Kafka" Version="2.0.0" />
|
||||
<PackageVersion Include="Rebus.MsgPack" Version="7.0.0" />
|
||||
<PackageVersion Include="Rebus.RabbitMq" Version="10.0.1" />
|
||||
<PackageVersion Include="Rebus.ServiceProvider" Version="10.3.0" />
|
||||
<PackageVersion Include="Serilog.Extensions.Hosting" Version="9.0.0" />
|
||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="9.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
<PackageVersion Include="SharpZipLib" Version="1.4.2" />
|
||||
<PackageVersion Include="Shouldly" Version="4.3.0" />
|
||||
<PackageVersion Include="Sigil" Version="5.0.0" />
|
||||
<PackageVersion Include="Standart.Hash.xxHash" Version="4.0.5" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="7.2.0" />
|
||||
<PackageVersion Include="System.ComponentModel.TypeConverter" Version="4.3.0" />
|
||||
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.7" />
|
||||
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||
<PackageVersion Include="Twilio" Version="7.12.0" />
|
||||
<PackageVersion Include="Vonage" Version="8.2.0" />
|
||||
<PackageVersion Include="xunit" Version="2.9.3" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.3" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,289 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.2.32210.308
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{2DA560C9-CF55-4AE8-9876-035A24D70098}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{91ACB8C4-58D1-42AC-A233-248BC18925B7}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyTelegram.Schema", "src\MyTelegram.Schema\MyTelegram.Schema.csproj", "{635E30FB-0B64-41D9-ACCC-BD927FD1A2EB}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyTelegram.Schema.Tests", "test\MyTelegram.Schema.Tests\MyTelegram.Schema.Tests.csproj", "{F86A6EBD-7FCC-4349-865B-BADF2AC331C2}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Domain", "Domain", "{EB17BE1D-A92C-49B5-BFF1-DD75BC7DC94B}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Application", "Application", "{DB4DF262-E785-497F-B465-440175026F97}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyTelegram.Core", "src\MyTelegram.Core\MyTelegram.Core.csproj", "{77D2BB76-B7AF-4200-B799-F19F879F7CB4}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyTelegram.Domain", "src\MyTelegram.Domain\MyTelegram.Domain.csproj", "{88DF870D-E5E7-4CC0-8FF0-60EF746C28AF}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyTelegram.Domain.Shared", "src\MyTelegram.Domain.Shared\MyTelegram.Domain.Shared.csproj", "{117B0121-B9E5-4481-8054-F33D1DA86AEC}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyTelegram.GrpcService", "src\MyTelegram.GrpcService\MyTelegram.GrpcService.csproj", "{B9DC33D2-34E6-4B6B-A22E-D7EFA1380004}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyTelegram.Queries", "src\MyTelegram.Queries\MyTelegram.Queries.csproj", "{6BCC94F3-717C-42EB-B051-DCFD15C9A068}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyTelegram.ReadModel", "src\MyTelegram.ReadModel\MyTelegram.ReadModel.csproj", "{7602FA14-87A9-490E-91E2-BBC0633FE194}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyTelegram.ReadModel.MongoDB", "src\MyTelegram.ReadModel.MongoDB\MyTelegram.ReadModel.MongoDB.csproj", "{1B3840FD-9C14-489F-8E03-DFEDA01207CF}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ReadModel", "ReadModel", "{3BF8F0CE-05C3-4386-B960-C56F6719D294}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyTelegram.Domain.Tests", "test\MyTelegram.Domain.Tests\MyTelegram.Domain.Tests.csproj", "{1076A82B-A813-44A5-93F6-4CCA1C15090E}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infrastructure", "Infrastructure", "{5F3FB626-5C24-4663-9B74-C9257417305E}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Cuckoo", "src\Microsoft.Cuckoo\Microsoft.Cuckoo.csproj", "{9B208770-781B-42B0-8F2A-E87B0D5341AD}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyTelegram.ReadModel.InMemory", "src\MyTelegram.ReadModel.InMemory\MyTelegram.ReadModel.InMemory.csproj", "{11F1970C-D03D-4153-B27E-C4774AB4509A}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyTelegram.Domain.IntegrationTests", "test\MyTelegram.Domain.IntegrationTests\MyTelegram.Domain.IntegrationTests.csproj", "{BF85677F-198E-4CAE-A13D-38CA555C9F76}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyTelegram.TestBase", "src\MyTelegram.TestBase\MyTelegram.TestBase.csproj", "{D6842BB0-435B-408A-8040-11FAD9479E79}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyTelegram.QueryHandlers.InMemory", "src\MyTelegram.QueryHandlers.InMemory\MyTelegram.QueryHandlers.InMemory.csproj", "{65D57042-EE58-4EBC-A5EE-B618BB99641D}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyTelegram.SmsSender", "src\MyTelegram.SmsSender\MyTelegram.SmsSender.csproj", "{165F6836-4B8F-48E0-AFC4-8D2A0286F3F5}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyTelegram.EventBus", "src\MyTelegram.EventBus\MyTelegram.EventBus.csproj", "{F23531FF-582D-48BA-988A-C484B86B44B2}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyTelegram.EventFlow.MongoDB", "src\MyTelegram.EventFlow.MongoDB\MyTelegram.EventFlow.MongoDB.csproj", "{175B77B0-25DB-47C4-881B-DEC3FAADF78A}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyTelegram.Caching.Redis", "src\MyTelegram.Caching.Redis\MyTelegram.Caching.Redis.csproj", "{7F5612B7-188E-42D1-9F41-3CC32062519B}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyTelegram.MTProto", "src\MyTelegram.MTProto\MyTelegram.MTProto.csproj", "{44C09B7D-9917-4AFE-809C-2DA8E93048DD}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyTelegram.MTProto.Tests", "test\MyTelegram.MTProto.Tests\MyTelegram.MTProto.Tests.csproj", "{816A9714-67FD-42BA-8411-3AD9B0A90701}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyTelegram.GatewayServer", "src\MyTelegram.GatewayServer\MyTelegram.GatewayServer.csproj", "{5BFC0B2E-0DD2-48D4-AD4F-7245687C34F7}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyTelegram.Messenger", "src\MyTelegram.Messenger\MyTelegram.Messenger.csproj", "{9967162A-3ABF-44A9-B796-D1925B257805}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyTelegram.Messenger.CommandServer", "src\MyTelegram.Messenger.CommandServer\MyTelegram.Messenger.CommandServer.csproj", "{18483359-47B7-4FB7-9BAE-F598105B15F5}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyTelegram.Messenger.QueryServer", "src\MyTelegram.Messenger.QueryServer\MyTelegram.Messenger.QueryServer.csproj", "{ACCB7A7F-19E2-4593-B6A0-97989D6B78E6}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyTelegram.Services", "src\MyTelegram.Services\MyTelegram.Services.csproj", "{0A9D2AE0-F943-408E-B740-2101DDF865CE}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyTelegram.EventFlow", "src\MyTelegram.EventFlow\MyTelegram.EventFlow.csproj", "{D4E1CD1A-EA33-48B8-A965-D55F82CCFBC5}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyTelegram.TestApp", "src\MyTelegram.TestApp\MyTelegram.TestApp.csproj", "{8D070E04-ABCE-439F-A7A9-864D73225D34}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyTelegram.AuthServer", "src\MyTelegram.AuthServer\MyTelegram.AuthServer.csproj", "{A45E8362-191B-4CF4-BC0C-89F81C5A17FE}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyTelegram.ReadModel.Interfaces", "src\MyTelegram.ReadModel.Interfaces\MyTelegram.ReadModel.Interfaces.csproj", "{A5AD515A-B43E-C625-1B44-589AAA14686C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyTelegram.Services.Tests", "test\MyTelegram.Services.Tests\MyTelegram.Services.Tests.csproj", "{8CD9759A-0026-33FC-DE4B-6BE3229B5109}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyTelegram.QueryHandlers.MongoDB", "src\MyTelegram.QueryHandlers.MongoDB\MyTelegram.QueryHandlers.MongoDB.csproj", "{710834A8-C271-4853-7B3D-1661A2535D9C}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyTelegram.Converters", "src\MyTelegram.Converters\MyTelegram.Converters.csproj", "{2970172B-D8E3-F5AC-ABE1-F96E39F15A80}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyTelegram.DataSeeder", "src\MyTelegram.DataSeeder\MyTelegram.DataSeeder.csproj", "{AE915013-50A2-6610-F705-53059D40139B}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyTelegram.Abstractions", "src\MyTelegram.Abstractions\MyTelegram.Abstractions.csproj", "{81E53953-EFD5-F7BE-C844-FEC29CA7B3F7}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyTelegram.EventBus.RabbitMQ", "src\MyTelegram.EventBus.RabbitMQ\MyTelegram.EventBus.RabbitMQ.csproj", "{87FBEAB7-2F9A-EAAC-084E-F9408151E00B}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{635E30FB-0B64-41D9-ACCC-BD927FD1A2EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{635E30FB-0B64-41D9-ACCC-BD927FD1A2EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{635E30FB-0B64-41D9-ACCC-BD927FD1A2EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{635E30FB-0B64-41D9-ACCC-BD927FD1A2EB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F86A6EBD-7FCC-4349-865B-BADF2AC331C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F86A6EBD-7FCC-4349-865B-BADF2AC331C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F86A6EBD-7FCC-4349-865B-BADF2AC331C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F86A6EBD-7FCC-4349-865B-BADF2AC331C2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{77D2BB76-B7AF-4200-B799-F19F879F7CB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{77D2BB76-B7AF-4200-B799-F19F879F7CB4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{77D2BB76-B7AF-4200-B799-F19F879F7CB4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{77D2BB76-B7AF-4200-B799-F19F879F7CB4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{88DF870D-E5E7-4CC0-8FF0-60EF746C28AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{88DF870D-E5E7-4CC0-8FF0-60EF746C28AF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{88DF870D-E5E7-4CC0-8FF0-60EF746C28AF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{88DF870D-E5E7-4CC0-8FF0-60EF746C28AF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{117B0121-B9E5-4481-8054-F33D1DA86AEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{117B0121-B9E5-4481-8054-F33D1DA86AEC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{117B0121-B9E5-4481-8054-F33D1DA86AEC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{117B0121-B9E5-4481-8054-F33D1DA86AEC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B9DC33D2-34E6-4B6B-A22E-D7EFA1380004}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B9DC33D2-34E6-4B6B-A22E-D7EFA1380004}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B9DC33D2-34E6-4B6B-A22E-D7EFA1380004}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B9DC33D2-34E6-4B6B-A22E-D7EFA1380004}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6BCC94F3-717C-42EB-B051-DCFD15C9A068}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6BCC94F3-717C-42EB-B051-DCFD15C9A068}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6BCC94F3-717C-42EB-B051-DCFD15C9A068}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6BCC94F3-717C-42EB-B051-DCFD15C9A068}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{7602FA14-87A9-490E-91E2-BBC0633FE194}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7602FA14-87A9-490E-91E2-BBC0633FE194}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7602FA14-87A9-490E-91E2-BBC0633FE194}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7602FA14-87A9-490E-91E2-BBC0633FE194}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1B3840FD-9C14-489F-8E03-DFEDA01207CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1B3840FD-9C14-489F-8E03-DFEDA01207CF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1B3840FD-9C14-489F-8E03-DFEDA01207CF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1B3840FD-9C14-489F-8E03-DFEDA01207CF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1076A82B-A813-44A5-93F6-4CCA1C15090E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1076A82B-A813-44A5-93F6-4CCA1C15090E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1076A82B-A813-44A5-93F6-4CCA1C15090E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1076A82B-A813-44A5-93F6-4CCA1C15090E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9B208770-781B-42B0-8F2A-E87B0D5341AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9B208770-781B-42B0-8F2A-E87B0D5341AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9B208770-781B-42B0-8F2A-E87B0D5341AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9B208770-781B-42B0-8F2A-E87B0D5341AD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{11F1970C-D03D-4153-B27E-C4774AB4509A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{11F1970C-D03D-4153-B27E-C4774AB4509A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{11F1970C-D03D-4153-B27E-C4774AB4509A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{11F1970C-D03D-4153-B27E-C4774AB4509A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{BF85677F-198E-4CAE-A13D-38CA555C9F76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BF85677F-198E-4CAE-A13D-38CA555C9F76}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BF85677F-198E-4CAE-A13D-38CA555C9F76}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BF85677F-198E-4CAE-A13D-38CA555C9F76}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D6842BB0-435B-408A-8040-11FAD9479E79}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D6842BB0-435B-408A-8040-11FAD9479E79}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D6842BB0-435B-408A-8040-11FAD9479E79}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D6842BB0-435B-408A-8040-11FAD9479E79}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{65D57042-EE58-4EBC-A5EE-B618BB99641D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{65D57042-EE58-4EBC-A5EE-B618BB99641D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{65D57042-EE58-4EBC-A5EE-B618BB99641D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{65D57042-EE58-4EBC-A5EE-B618BB99641D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{165F6836-4B8F-48E0-AFC4-8D2A0286F3F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{165F6836-4B8F-48E0-AFC4-8D2A0286F3F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{165F6836-4B8F-48E0-AFC4-8D2A0286F3F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{165F6836-4B8F-48E0-AFC4-8D2A0286F3F5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F23531FF-582D-48BA-988A-C484B86B44B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F23531FF-582D-48BA-988A-C484B86B44B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F23531FF-582D-48BA-988A-C484B86B44B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F23531FF-582D-48BA-988A-C484B86B44B2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{175B77B0-25DB-47C4-881B-DEC3FAADF78A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{175B77B0-25DB-47C4-881B-DEC3FAADF78A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{175B77B0-25DB-47C4-881B-DEC3FAADF78A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{175B77B0-25DB-47C4-881B-DEC3FAADF78A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{7F5612B7-188E-42D1-9F41-3CC32062519B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7F5612B7-188E-42D1-9F41-3CC32062519B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7F5612B7-188E-42D1-9F41-3CC32062519B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7F5612B7-188E-42D1-9F41-3CC32062519B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{44C09B7D-9917-4AFE-809C-2DA8E93048DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{44C09B7D-9917-4AFE-809C-2DA8E93048DD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{44C09B7D-9917-4AFE-809C-2DA8E93048DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{44C09B7D-9917-4AFE-809C-2DA8E93048DD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{816A9714-67FD-42BA-8411-3AD9B0A90701}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{816A9714-67FD-42BA-8411-3AD9B0A90701}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{816A9714-67FD-42BA-8411-3AD9B0A90701}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{816A9714-67FD-42BA-8411-3AD9B0A90701}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5BFC0B2E-0DD2-48D4-AD4F-7245687C34F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5BFC0B2E-0DD2-48D4-AD4F-7245687C34F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5BFC0B2E-0DD2-48D4-AD4F-7245687C34F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5BFC0B2E-0DD2-48D4-AD4F-7245687C34F7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9967162A-3ABF-44A9-B796-D1925B257805}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9967162A-3ABF-44A9-B796-D1925B257805}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9967162A-3ABF-44A9-B796-D1925B257805}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9967162A-3ABF-44A9-B796-D1925B257805}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{18483359-47B7-4FB7-9BAE-F598105B15F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{18483359-47B7-4FB7-9BAE-F598105B15F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{18483359-47B7-4FB7-9BAE-F598105B15F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{18483359-47B7-4FB7-9BAE-F598105B15F5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{ACCB7A7F-19E2-4593-B6A0-97989D6B78E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{ACCB7A7F-19E2-4593-B6A0-97989D6B78E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{ACCB7A7F-19E2-4593-B6A0-97989D6B78E6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{ACCB7A7F-19E2-4593-B6A0-97989D6B78E6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0A9D2AE0-F943-408E-B740-2101DDF865CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0A9D2AE0-F943-408E-B740-2101DDF865CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0A9D2AE0-F943-408E-B740-2101DDF865CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0A9D2AE0-F943-408E-B740-2101DDF865CE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D4E1CD1A-EA33-48B8-A965-D55F82CCFBC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D4E1CD1A-EA33-48B8-A965-D55F82CCFBC5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D4E1CD1A-EA33-48B8-A965-D55F82CCFBC5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D4E1CD1A-EA33-48B8-A965-D55F82CCFBC5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8D070E04-ABCE-439F-A7A9-864D73225D34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8D070E04-ABCE-439F-A7A9-864D73225D34}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8D070E04-ABCE-439F-A7A9-864D73225D34}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8D070E04-ABCE-439F-A7A9-864D73225D34}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A45E8362-191B-4CF4-BC0C-89F81C5A17FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A45E8362-191B-4CF4-BC0C-89F81C5A17FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A45E8362-191B-4CF4-BC0C-89F81C5A17FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A45E8362-191B-4CF4-BC0C-89F81C5A17FE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A5AD515A-B43E-C625-1B44-589AAA14686C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A5AD515A-B43E-C625-1B44-589AAA14686C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A5AD515A-B43E-C625-1B44-589AAA14686C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A5AD515A-B43E-C625-1B44-589AAA14686C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8CD9759A-0026-33FC-DE4B-6BE3229B5109}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8CD9759A-0026-33FC-DE4B-6BE3229B5109}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8CD9759A-0026-33FC-DE4B-6BE3229B5109}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8CD9759A-0026-33FC-DE4B-6BE3229B5109}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{710834A8-C271-4853-7B3D-1661A2535D9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{710834A8-C271-4853-7B3D-1661A2535D9C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{710834A8-C271-4853-7B3D-1661A2535D9C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{710834A8-C271-4853-7B3D-1661A2535D9C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{2970172B-D8E3-F5AC-ABE1-F96E39F15A80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2970172B-D8E3-F5AC-ABE1-F96E39F15A80}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2970172B-D8E3-F5AC-ABE1-F96E39F15A80}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2970172B-D8E3-F5AC-ABE1-F96E39F15A80}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AE915013-50A2-6610-F705-53059D40139B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AE915013-50A2-6610-F705-53059D40139B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AE915013-50A2-6610-F705-53059D40139B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AE915013-50A2-6610-F705-53059D40139B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{81E53953-EFD5-F7BE-C844-FEC29CA7B3F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{81E53953-EFD5-F7BE-C844-FEC29CA7B3F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{81E53953-EFD5-F7BE-C844-FEC29CA7B3F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{81E53953-EFD5-F7BE-C844-FEC29CA7B3F7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{87FBEAB7-2F9A-EAAC-084E-F9408151E00B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{87FBEAB7-2F9A-EAAC-084E-F9408151E00B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{87FBEAB7-2F9A-EAAC-084E-F9408151E00B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{87FBEAB7-2F9A-EAAC-084E-F9408151E00B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{635E30FB-0B64-41D9-ACCC-BD927FD1A2EB} = {5F3FB626-5C24-4663-9B74-C9257417305E}
|
||||
{F86A6EBD-7FCC-4349-865B-BADF2AC331C2} = {91ACB8C4-58D1-42AC-A233-248BC18925B7}
|
||||
{EB17BE1D-A92C-49B5-BFF1-DD75BC7DC94B} = {2DA560C9-CF55-4AE8-9876-035A24D70098}
|
||||
{DB4DF262-E785-497F-B465-440175026F97} = {2DA560C9-CF55-4AE8-9876-035A24D70098}
|
||||
{77D2BB76-B7AF-4200-B799-F19F879F7CB4} = {5F3FB626-5C24-4663-9B74-C9257417305E}
|
||||
{88DF870D-E5E7-4CC0-8FF0-60EF746C28AF} = {EB17BE1D-A92C-49B5-BFF1-DD75BC7DC94B}
|
||||
{117B0121-B9E5-4481-8054-F33D1DA86AEC} = {EB17BE1D-A92C-49B5-BFF1-DD75BC7DC94B}
|
||||
{B9DC33D2-34E6-4B6B-A22E-D7EFA1380004} = {DB4DF262-E785-497F-B465-440175026F97}
|
||||
{6BCC94F3-717C-42EB-B051-DCFD15C9A068} = {3BF8F0CE-05C3-4386-B960-C56F6719D294}
|
||||
{7602FA14-87A9-490E-91E2-BBC0633FE194} = {3BF8F0CE-05C3-4386-B960-C56F6719D294}
|
||||
{1B3840FD-9C14-489F-8E03-DFEDA01207CF} = {3BF8F0CE-05C3-4386-B960-C56F6719D294}
|
||||
{3BF8F0CE-05C3-4386-B960-C56F6719D294} = {2DA560C9-CF55-4AE8-9876-035A24D70098}
|
||||
{1076A82B-A813-44A5-93F6-4CCA1C15090E} = {91ACB8C4-58D1-42AC-A233-248BC18925B7}
|
||||
{5F3FB626-5C24-4663-9B74-C9257417305E} = {2DA560C9-CF55-4AE8-9876-035A24D70098}
|
||||
{9B208770-781B-42B0-8F2A-E87B0D5341AD} = {5F3FB626-5C24-4663-9B74-C9257417305E}
|
||||
{11F1970C-D03D-4153-B27E-C4774AB4509A} = {3BF8F0CE-05C3-4386-B960-C56F6719D294}
|
||||
{BF85677F-198E-4CAE-A13D-38CA555C9F76} = {91ACB8C4-58D1-42AC-A233-248BC18925B7}
|
||||
{D6842BB0-435B-408A-8040-11FAD9479E79} = {5F3FB626-5C24-4663-9B74-C9257417305E}
|
||||
{65D57042-EE58-4EBC-A5EE-B618BB99641D} = {3BF8F0CE-05C3-4386-B960-C56F6719D294}
|
||||
{165F6836-4B8F-48E0-AFC4-8D2A0286F3F5} = {DB4DF262-E785-497F-B465-440175026F97}
|
||||
{F23531FF-582D-48BA-988A-C484B86B44B2} = {5F3FB626-5C24-4663-9B74-C9257417305E}
|
||||
{175B77B0-25DB-47C4-881B-DEC3FAADF78A} = {5F3FB626-5C24-4663-9B74-C9257417305E}
|
||||
{7F5612B7-188E-42D1-9F41-3CC32062519B} = {5F3FB626-5C24-4663-9B74-C9257417305E}
|
||||
{44C09B7D-9917-4AFE-809C-2DA8E93048DD} = {5F3FB626-5C24-4663-9B74-C9257417305E}
|
||||
{816A9714-67FD-42BA-8411-3AD9B0A90701} = {91ACB8C4-58D1-42AC-A233-248BC18925B7}
|
||||
{5BFC0B2E-0DD2-48D4-AD4F-7245687C34F7} = {DB4DF262-E785-497F-B465-440175026F97}
|
||||
{9967162A-3ABF-44A9-B796-D1925B257805} = {DB4DF262-E785-497F-B465-440175026F97}
|
||||
{18483359-47B7-4FB7-9BAE-F598105B15F5} = {DB4DF262-E785-497F-B465-440175026F97}
|
||||
{ACCB7A7F-19E2-4593-B6A0-97989D6B78E6} = {DB4DF262-E785-497F-B465-440175026F97}
|
||||
{0A9D2AE0-F943-408E-B740-2101DDF865CE} = {DB4DF262-E785-497F-B465-440175026F97}
|
||||
{D4E1CD1A-EA33-48B8-A965-D55F82CCFBC5} = {EB17BE1D-A92C-49B5-BFF1-DD75BC7DC94B}
|
||||
{8D070E04-ABCE-439F-A7A9-864D73225D34} = {DB4DF262-E785-497F-B465-440175026F97}
|
||||
{A45E8362-191B-4CF4-BC0C-89F81C5A17FE} = {DB4DF262-E785-497F-B465-440175026F97}
|
||||
{A5AD515A-B43E-C625-1B44-589AAA14686C} = {3BF8F0CE-05C3-4386-B960-C56F6719D294}
|
||||
{8CD9759A-0026-33FC-DE4B-6BE3229B5109} = {91ACB8C4-58D1-42AC-A233-248BC18925B7}
|
||||
{710834A8-C271-4853-7B3D-1661A2535D9C} = {3BF8F0CE-05C3-4386-B960-C56F6719D294}
|
||||
{2970172B-D8E3-F5AC-ABE1-F96E39F15A80} = {DB4DF262-E785-497F-B465-440175026F97}
|
||||
{AE915013-50A2-6610-F705-53059D40139B} = {DB4DF262-E785-497F-B465-440175026F97}
|
||||
{81E53953-EFD5-F7BE-C844-FEC29CA7B3F7} = {5F3FB626-5C24-4663-9B74-C9257417305E}
|
||||
{87FBEAB7-2F9A-EAAC-084E-F9408151E00B} = {5F3FB626-5C24-4663-9B74-C9257417305E}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {A8E7A112-1B80-4DCC-9884-5069D0E3B651}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -0,0 +1,30 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<!-- <VersionSuffix>0.6.$([System.DateTime]::Now.ToString(MMdd)).$([System.DateTime]::Now.ToString("HHmm"))</VersionSuffix> -->
|
||||
<VersionSuffix>0.8.821.932</VersionSuffix>
|
||||
<AssemblyVersion Condition="'$(VersionSuffix)' =='' ">0.0.0.1</AssemblyVersion>
|
||||
<AssemblyVersion Condition="'$(VersionSuffix)' !='' ">$(VersionSuffix)</AssemblyVersion>
|
||||
<Version Condition="'$(VersionSuffix)' =='' ">0.0.1.0</Version>
|
||||
<Version Condition="'$(VersionSuffix)' !='' ">$(VersionSuffix)</Version>
|
||||
<NoWarn>$(NoWarn);CS1591;CS0436;NU1803;</NoWarn>
|
||||
<Authors>Loyldg</Authors>
|
||||
<RepositoryUrl>https://github.com/loyldg/mytelegram</RepositoryUrl>
|
||||
<Company>Loyldg</Company>
|
||||
<Copyright>Copyright (c) loyldg 2021 - 2023</Copyright>
|
||||
|
||||
<IsWindows Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))' == 'true'">true</IsWindows>
|
||||
<IsOSX Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::OSX)))' == 'true'">true</IsOSX>
|
||||
<IsLinux Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))' == 'true'">true</IsLinux>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(IsWindows)'=='true'">
|
||||
<DefineConstants>Windows</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(IsOSX)'=='true'">
|
||||
<DefineConstants>OSX</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(IsLinux)'=='true'">
|
||||
<DefineConstants>Linux</DefineConstants>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<!-- `key` can be any identifier for your source. -->
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
|
||||
</packageSources>
|
||||
|
||||
<!-- Define mappings by adding package patterns beneath the target source. -->
|
||||
<packageSourceMapping>
|
||||
<!-- key value for <packageSource> should match key values from <packageSources> element -->
|
||||
<packageSource key="nuget.org">
|
||||
<package pattern="*" />
|
||||
</packageSource>
|
||||
</packageSourceMapping>
|
||||
</configuration>
|
||||
@@ -0,0 +1,335 @@
|
||||
// -----------------------------------------------------------------------
|
||||
// <copyright file="CodegenHelpers.cs" company="Microsoft">
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// </copyright>
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Microsoft.Cuckoo.Benchmark")]
|
||||
[assembly: InternalsVisibleTo("Microsoft.Cuckoo.Test")]
|
||||
|
||||
namespace Microsoft.Cuckoo;
|
||||
|
||||
/// <summary>
|
||||
/// Collection helper methods for generating code and comparators.
|
||||
/// </summary>
|
||||
internal static class CodegenHelpers
|
||||
{
|
||||
public static bool BytesEquals(byte[] a, byte[] b)
|
||||
{
|
||||
return BytesEquals(a, 0, b, 0, a.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether the two arrays are equal. It assumes that both
|
||||
/// are at least the "length" long.
|
||||
/// </summary>
|
||||
/// <param name="a">First array</param>
|
||||
/// <param name="offsetA">Offset in first array to look from</param>
|
||||
/// <param name="b">Second array</param>
|
||||
/// <param name="offsetB">Offset in second array to look from</param>
|
||||
/// <param name="length">Number of bytes in each to compare</param>
|
||||
/// <returns>True if all bytes are equal, false otherwise</returns>
|
||||
public static bool BytesEquals(byte[] a, int offsetA, byte[] b, int offsetB, int length)
|
||||
{
|
||||
var baseOffset = 0;
|
||||
for (; baseOffset + Vector<byte>.Count <= length; baseOffset += Vector<byte>.Count)
|
||||
{
|
||||
var aVec = new Vector<byte>(a, baseOffset + offsetA);
|
||||
var bVec = new Vector<byte>(b, baseOffset + offsetB);
|
||||
if (aVec != bVec)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var remaining = length - baseOffset;
|
||||
for (var i = 0; i < remaining; i++)
|
||||
{
|
||||
if (a[baseOffset + offsetA + i] != b[baseOffset + offsetB + i])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether the array is all zeroes from the given offset
|
||||
/// and length.
|
||||
/// </summary>
|
||||
/// <param name="array">Array to check</param>
|
||||
/// <param name="offset">Offset to look at</param>
|
||||
/// <param name="length">Number of bytes to check</param>
|
||||
/// <returns>The number of bytes that are zero</returns>
|
||||
public static bool IsZero(byte[] array, int offset, int length)
|
||||
{
|
||||
var zeroVector = new Vector<byte>(0);
|
||||
|
||||
var baseOffset = 0;
|
||||
for (; baseOffset + Vector<byte>.Count <= length; baseOffset += Vector<byte>.Count)
|
||||
{
|
||||
if (new Vector<byte>(array, baseOffset + offset) != zeroVector)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var remaining = length - baseOffset;
|
||||
for (var i = 0; i < remaining; i++)
|
||||
{
|
||||
if (array[offset + baseOffset + i] != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a function that checks if the fingerprint at the given
|
||||
/// offset (arg 2) in the first argument (arg 1) is equal to the
|
||||
/// fingerprint in arg 3.
|
||||
/// </summary>
|
||||
/// <param name="fingerprintSize">Number of bytes in the fingerprint</param>
|
||||
/// <param name="entriesPerBucket">Number of entries in each bucket</param>
|
||||
/// <returns>The created delegate</returns>
|
||||
public static Func<byte[], int, byte[], int> CreateFingerprintComparator(uint fingerprintSize,
|
||||
uint entriesPerBucket)
|
||||
{
|
||||
int GetFingerprintIndex(byte[] originalFingerprintBytes,
|
||||
int originalFingerprintOffset,
|
||||
byte[] newFingerprintBytes)
|
||||
{
|
||||
var size = (int)fingerprintSize;
|
||||
var span = originalFingerprintBytes.AsSpan()[originalFingerprintOffset..];
|
||||
for (var entryIndex = 0; entryIndex < entriesPerBucket; entryIndex++)
|
||||
{
|
||||
var fingerprintBytes = span.Slice(size * entryIndex, size);
|
||||
if (fingerprintBytes.SequenceEqual(newFingerprintBytes))
|
||||
{
|
||||
return entryIndex;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
return GetFingerprintIndex;
|
||||
//var e1 = Emit<Func<byte[], int, byte[], int>>.NewDynamicMethod();
|
||||
|
||||
//Label nextStatement = null;
|
||||
//for (var entryIndex = 0; entryIndex < entriesPerBucket; entryIndex++)
|
||||
//{
|
||||
// if (nextStatement != null)
|
||||
// {
|
||||
// e1.MarkLabel(nextStatement);
|
||||
// }
|
||||
|
||||
// nextStatement = e1.DefineLabel();
|
||||
// for (var byteIndex = 0; byteIndex < fingerprintSize; byteIndex++)
|
||||
// {
|
||||
// // Retrieve a[offset + i]
|
||||
// e1.LoadArgument(0);
|
||||
// e1.LoadArgument(1);
|
||||
|
||||
// var offset = (int)(fingerprintSize * entryIndex + byteIndex);
|
||||
// if (offset > 0)
|
||||
// {
|
||||
// e1.LoadConstant(offset);
|
||||
// e1.Add();
|
||||
// }
|
||||
// e1.LoadElement<byte>();
|
||||
|
||||
// // Retrieve b[i]
|
||||
// e1.LoadArgument(2);
|
||||
// e1.LoadConstant(byteIndex);
|
||||
// e1.LoadElement<byte>();
|
||||
|
||||
// // Go to the false return of not equal
|
||||
// e1.UnsignedBranchIfNotEqual(nextStatement);
|
||||
// }
|
||||
|
||||
// e1.LoadConstant(entryIndex);
|
||||
// e1.Return();
|
||||
//}
|
||||
|
||||
//e1.MarkLabel(nextStatement);
|
||||
//e1.LoadConstant(-1);
|
||||
//e1.Return();
|
||||
|
||||
//return e1.CreateDelegate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a function that checks if the fingerprint at the given
|
||||
/// offset (arg 2) in the first argument (arg 1) is zero.
|
||||
/// </summary>
|
||||
/// <param name="fingerprintSize">Number of bytes in the fingerprint</param>
|
||||
/// <returns>The created delegate</returns>
|
||||
public static Func<byte[], int, bool> CreateZeroChecker(uint fingerprintSize)
|
||||
{
|
||||
Func<byte[], int, bool> func = (fingerprintBytes,
|
||||
offset) =>
|
||||
{
|
||||
var bytes = fingerprintBytes.AsSpan().Slice(offset, (int)fingerprintSize);
|
||||
foreach (var b in bytes)
|
||||
{
|
||||
if (b != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
return func;
|
||||
|
||||
//var e1 = Emit<Func<byte[], int, bool>>.NewDynamicMethod();
|
||||
//var returnFalse = e1.DefineLabel();
|
||||
|
||||
//for (var byteIndex = 0; byteIndex < fingerprintSize; byteIndex++)
|
||||
//{
|
||||
// // Load the contents array:
|
||||
// e1.LoadArgument(0);
|
||||
|
||||
// // Set the index we want to offset + i:
|
||||
// e1.LoadArgument(1);
|
||||
// if (byteIndex > 0)
|
||||
// {
|
||||
// e1.LoadConstant(byteIndex);
|
||||
// e1.Add();
|
||||
// }
|
||||
// e1.LoadElement<byte>();
|
||||
|
||||
|
||||
// // Go to the next statement (the next entry check) if it's not 0.
|
||||
// e1.LoadConstant(0);
|
||||
// e1.UnsignedBranchIfNotEqual(returnFalse);
|
||||
//}
|
||||
|
||||
//// Got down here? We're good, true true.
|
||||
//e1.LoadConstant(true);
|
||||
//e1.Return();
|
||||
|
||||
//// False branch:
|
||||
//e1.MarkLabel(returnFalse);
|
||||
//e1.LoadConstant(false);
|
||||
//e1.Return();
|
||||
|
||||
//return e1.CreateDelegate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a function that checks inserts a fingerprint (arg 3) into
|
||||
/// the index (arg 2) in the contents (arg 1) if there's any available
|
||||
/// unassigned fingerprint slot. Returns true if the insertion was
|
||||
/// made successfully.
|
||||
/// </summary>
|
||||
/// <param name="fingerprintSize">Number of bytes in the fingerprint</param>
|
||||
/// <param name="entriesPerBucket">Number of entries in each bucket</param>
|
||||
/// <returns>The created delegate</returns>
|
||||
public static Func<byte[], int, byte[], bool> CreateInsertIntoBucket(uint fingerprintSize, uint entriesPerBucket)
|
||||
{
|
||||
Func<byte[], int, byte[], bool> func = (fingerprintBytes,
|
||||
fingerprintIndex,
|
||||
newFingerprintBytes) =>
|
||||
{
|
||||
var zeroBytes = new byte[fingerprintSize];
|
||||
var bytes = fingerprintBytes.AsSpan().Slice(fingerprintIndex);
|
||||
var size = (int)fingerprintSize;
|
||||
for (var i = 0; i < bytes.Length; i += size)
|
||||
{
|
||||
var span = bytes.Slice(i, size);
|
||||
if (span.SequenceEqual(zeroBytes))
|
||||
{
|
||||
newFingerprintBytes.CopyTo(span);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
return func;
|
||||
|
||||
//var e = Emit<Func<byte[], int, byte[], bool>>.NewDynamicMethod();
|
||||
|
||||
//Label nextStatement = null;
|
||||
//for (var entryIndex = 0; entryIndex < entriesPerBucket; entryIndex++)
|
||||
//{
|
||||
// if (nextStatement != null)
|
||||
// {
|
||||
// e.MarkLabel(nextStatement);
|
||||
// }
|
||||
|
||||
// nextStatement = e.DefineLabel();
|
||||
|
||||
// // First, check that all bytes in the target spot are 0.
|
||||
// for (var byteIndex = 0; byteIndex < fingerprintSize; byteIndex++)
|
||||
// {
|
||||
// // Load the contents array:
|
||||
// e.LoadArgument(0);
|
||||
|
||||
// // Set the index we want to offset + i:
|
||||
// e.LoadArgument(1);
|
||||
// var offset = (int)(fingerprintSize * entryIndex + byteIndex);
|
||||
// if (offset > 0)
|
||||
// {
|
||||
// e.LoadConstant(offset);
|
||||
// e.Add();
|
||||
// }
|
||||
|
||||
// // Load the element at contents[offset]:
|
||||
// e.LoadElement<byte>();
|
||||
|
||||
// // Go to the next statement (the next entry check) if it's not 0.
|
||||
// e.LoadConstant(0);
|
||||
// e.UnsignedBranchIfNotEqual(nextStatement);
|
||||
// }
|
||||
|
||||
// // If we're here, we found a free space that we can put our
|
||||
// // fingerprint in.
|
||||
// for (var byteIndex = 0; byteIndex < fingerprintSize; byteIndex++)
|
||||
// {
|
||||
// // Element setting takes three values: the array to load
|
||||
// // into, the offset, and the data to load.
|
||||
|
||||
// // Array to load into:
|
||||
// e.LoadArgument(0);
|
||||
|
||||
// // Target offset in the array:
|
||||
// e.LoadArgument(1);
|
||||
// var offset = (int)(fingerprintSize * entryIndex + byteIndex);
|
||||
// if (offset > 0)
|
||||
// {
|
||||
// e.LoadConstant(offset);
|
||||
// e.Add();
|
||||
// }
|
||||
|
||||
// // Load the value to be loaded from the fingerprint:
|
||||
// e.LoadArgument(2);
|
||||
// e.LoadConstant(byteIndex);
|
||||
// e.LoadElement<byte>();
|
||||
|
||||
// // Store it!
|
||||
// e.StoreElement<byte>();
|
||||
// }
|
||||
|
||||
// // Now we're good, retur:
|
||||
// e.LoadConstant(true);
|
||||
// e.Return();
|
||||
//}
|
||||
|
||||
//e.MarkLabel(nextStatement);
|
||||
//e.LoadConstant(false);
|
||||
//e.Return();
|
||||
|
||||
//return e.CreateDelegate();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,549 @@
|
||||
// -----------------------------------------------------------------------
|
||||
// <copyright file="CuckooFilter.cs" company="Microsoft">
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// </copyright>
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
|
||||
[assembly: InternalsVisibleTo("Cuckoo.UnitTests")]
|
||||
|
||||
namespace Microsoft.Cuckoo;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of the Cuckoo filter.
|
||||
/// </summary>
|
||||
public class CuckooFilter : ICuckooFilter, IEquatable<CuckooFilter>
|
||||
{
|
||||
/// <summary>
|
||||
/// Number of bytes in a <see cref="uint" />
|
||||
/// </summary>
|
||||
private const int Uint32Bytes = sizeof(uint);
|
||||
|
||||
/// <summary>
|
||||
/// Comparator function, see <see cref="CodegenHelpers.CreateFingerprintComparator" />
|
||||
/// </summary>
|
||||
private readonly Func<byte[], int, byte[], int> _comparator;
|
||||
|
||||
/// <summary>
|
||||
/// Hashing algorithm.
|
||||
/// </summary>
|
||||
private readonly IHashAlgorithm _hashAlgorithm;
|
||||
|
||||
/// <summary>
|
||||
/// Comparator function, see <see cref="CodegenHelpers.CreateInsertIntoBucket" />
|
||||
/// </summary>
|
||||
private readonly Func<byte[], int, byte[], bool> _insertIntoBucket;
|
||||
|
||||
/// <summary>
|
||||
/// Pre-allocated buffer we hash value indices into.
|
||||
/// </summary>
|
||||
private readonly byte[] _valuesBuffer = new byte[Uint32Bytes];
|
||||
|
||||
/// <summary>
|
||||
/// Comparator function, see <see cref="CodegenHelpers.CreateZeroChecker" />
|
||||
/// </summary>
|
||||
private readonly Func<byte[], int, bool> _zeroCheck;
|
||||
|
||||
/// <summary>
|
||||
/// Random instance.
|
||||
/// </summary>
|
||||
internal readonly Random Random;
|
||||
|
||||
/// <summary>
|
||||
/// Pre-allocated buffer we hash the fingerprint into.
|
||||
/// </summary>
|
||||
private byte[] _fingerprintBuffer;
|
||||
|
||||
/// <summary>
|
||||
/// Pre-allocated buffer we use for swapping fingerprints into.
|
||||
/// </summary>
|
||||
private byte[] _fingerprintSwapBuffer;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new CuckooFilter.
|
||||
/// </summary>
|
||||
/// <param name="buckets">Number of Buckets to store</param>
|
||||
/// <param name="entriesPerBucket">
|
||||
/// Number of fingerprints stored
|
||||
/// in each bucket.
|
||||
/// </param>
|
||||
/// <param name="fingerprintLength">Length of the fingerprint to use</param>
|
||||
/// <param name="maxKicks">
|
||||
/// Maximum number of times to relocate a value
|
||||
/// on a collision.
|
||||
/// </param>
|
||||
/// <param name="hashAlgorithm">Hashing algorithm to use</param>
|
||||
/// <param name="randomSeed">Random seed value</param>
|
||||
public CuckooFilter(
|
||||
uint buckets,
|
||||
uint entriesPerBucket,
|
||||
uint fingerprintLength,
|
||||
uint? maxKicks = null,
|
||||
IHashAlgorithm hashAlgorithm = null,
|
||||
int? randomSeed = null)
|
||||
{
|
||||
Buckets = buckets;
|
||||
EntriesPerBucket = entriesPerBucket;
|
||||
_hashAlgorithm = hashAlgorithm ?? XxHashAlgorithm.Instance;
|
||||
MaxKicks = maxKicks ?? buckets;
|
||||
|
||||
if (UpperPower2(buckets) != buckets)
|
||||
{
|
||||
throw new ArgumentException("Buckets must be a power of 2", nameof(buckets));
|
||||
}
|
||||
|
||||
_fingerprintBuffer = new byte[fingerprintLength];
|
||||
Contents = CreateEmptyBucketData(buckets, entriesPerBucket, fingerprintLength);
|
||||
Random = randomSeed == null
|
||||
? new Random()
|
||||
: new Random(randomSeed.Value);
|
||||
BytesPerBucket = EntriesPerBucket * fingerprintLength;
|
||||
_fingerprintSwapBuffer = new byte[_fingerprintBuffer.Length];
|
||||
_comparator = CodegenHelpers.CreateFingerprintComparator(fingerprintLength, entriesPerBucket);
|
||||
_zeroCheck = CodegenHelpers.CreateZeroChecker(fingerprintLength);
|
||||
_insertIntoBucket = CodegenHelpers.CreateInsertIntoBucket(fingerprintLength, entriesPerBucket);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new optimally-sized CuckooFilter with a target
|
||||
/// false-positive-at-capacity.
|
||||
/// </summary>
|
||||
/// <param name="capacity">Filter capacity</param>
|
||||
/// <param name="falsePositiveRate">Desired false positive rate.</param>
|
||||
/// <param name="hashAlgorithm">Hashing algorithm to use</param>
|
||||
/// <param name="randomSeed">Random seed value</param>
|
||||
public CuckooFilter(
|
||||
uint capacity,
|
||||
double falsePositiveRate,
|
||||
IHashAlgorithm hashAlgorithm = null,
|
||||
int? randomSeed = null)
|
||||
{
|
||||
_hashAlgorithm = hashAlgorithm ?? XxHashAlgorithm.Instance;
|
||||
|
||||
// "In summary, we choose (2, 4)-cuckoo filter (i.e., each item has
|
||||
// two candidate Buckets and each bucket has up to four fingerprints)
|
||||
// as the default configuration, because it achieves the best or
|
||||
// close - to - best space efficiency for the false positive
|
||||
// rates that most practical applications""
|
||||
EntriesPerBucket = 4;
|
||||
|
||||
// Equation here from page 8, step 6, of the paper:
|
||||
// ceil(log_2 (2b / \epsilon)
|
||||
var desiredLength = Math.Log(2 * (float)EntriesPerBucket / falsePositiveRate, 2);
|
||||
var fingerprintLength = (uint)Math.Ceiling(desiredLength / 8);
|
||||
_fingerprintBuffer = new byte[fingerprintLength];
|
||||
|
||||
// Not explicitly defined in the paper, however this is the
|
||||
// algorithm used in the author's implementation:
|
||||
// https://github.com/efficient/cuckoofilter/blob/master/src/cuckoofilter.h#L89
|
||||
Buckets = UpperPower2(capacity / EntriesPerBucket);
|
||||
if ((double)capacity / Buckets / EntriesPerBucket > 0.96)
|
||||
{
|
||||
Buckets <<= 1;
|
||||
}
|
||||
|
||||
MaxKicks = Buckets;
|
||||
Random = randomSeed == null
|
||||
? new Random()
|
||||
: new Random(randomSeed.Value);
|
||||
Contents = CreateEmptyBucketData(Buckets, EntriesPerBucket, fingerprintLength);
|
||||
BytesPerBucket = EntriesPerBucket * fingerprintLength;
|
||||
_fingerprintSwapBuffer = new byte[_fingerprintBuffer.Length];
|
||||
_zeroCheck = CodegenHelpers.CreateZeroChecker(fingerprintLength);
|
||||
_comparator = CodegenHelpers.CreateFingerprintComparator(fingerprintLength, 4);
|
||||
_insertIntoBucket = CodegenHelpers.CreateInsertIntoBucket(fingerprintLength, EntriesPerBucket);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new CuckooFilter.
|
||||
/// </summary>
|
||||
/// <param name="contents">Contents of th efilter</param>
|
||||
/// <param name="entriesPerBucket">
|
||||
/// Number of fingerprints stored
|
||||
/// in each bucket.
|
||||
/// </param>
|
||||
/// <param name="fingerprintLength">Length of the fingerprint to use</param>
|
||||
/// <param name="maxKicks">
|
||||
/// Maximum number of times to relocate a value
|
||||
/// on a collision.
|
||||
/// </param>
|
||||
/// <param name="hashAlgorithm">Hashing algorithm to use</param>
|
||||
internal CuckooFilter(
|
||||
byte[] contents,
|
||||
uint entriesPerBucket,
|
||||
uint fingerprintLength,
|
||||
uint maxKicks,
|
||||
IHashAlgorithm hashAlgorithm = null)
|
||||
{
|
||||
Contents = contents;
|
||||
MaxKicks = maxKicks;
|
||||
EntriesPerBucket = entriesPerBucket;
|
||||
BytesPerBucket = entriesPerBucket * fingerprintLength;
|
||||
Buckets = (uint)contents.Length / BytesPerBucket;
|
||||
_fingerprintBuffer = new byte[fingerprintLength];
|
||||
_fingerprintSwapBuffer = new byte[_fingerprintBuffer.Length];
|
||||
_hashAlgorithm = hashAlgorithm ?? XxHashAlgorithm.Instance;
|
||||
_zeroCheck = CodegenHelpers.CreateZeroChecker(fingerprintLength);
|
||||
_comparator = CodegenHelpers.CreateFingerprintComparator(fingerprintLength, 4);
|
||||
_insertIntoBucket = CodegenHelpers.CreateInsertIntoBucket(fingerprintLength, entriesPerBucket);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of Buckets the filter contains.
|
||||
/// </summary>
|
||||
public uint Buckets { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of bytes each bucket takes.
|
||||
/// </summary>
|
||||
public uint BytesPerBucket { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of fingerprints to store per bucket.
|
||||
/// </summary>
|
||||
public uint EntriesPerBucket { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the length of the fingerprint.
|
||||
/// </summary>
|
||||
public uint FingerprintLength => (uint)_fingerprintBuffer.Length;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the max number of times we'll try to kick and item from a
|
||||
/// bucket when we insert before giving it.
|
||||
/// </summary>
|
||||
public uint MaxKicks { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Contents of the cuckoo filter.
|
||||
/// </summary>
|
||||
internal byte[] Contents { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total size, in memory, of the filter.
|
||||
/// </summary>
|
||||
public uint Size => (uint)Contents.Length;
|
||||
|
||||
/// <summary>
|
||||
/// Returns a value indicating whether the filter probably contains
|
||||
/// the given item.
|
||||
/// </summary>
|
||||
/// <param name="value">Value to check</param>
|
||||
/// <returns>True if the filter contains the value, false otherwise.</returns>
|
||||
public bool Contains(byte[] value)
|
||||
{
|
||||
var fingerprint = GetFingerprint(value);
|
||||
_hashAlgorithm.Hash(_valuesBuffer, value, Uint32Bytes);
|
||||
|
||||
var index1 = ToInt32(_valuesBuffer) & (Buckets - 1);
|
||||
if (_comparator(Contents, IndexToOffset(index1), fingerprint) != -1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var index2 = DeriveIndex2(fingerprint, index1);
|
||||
if (_comparator(Contents, IndexToOffset(index2), fingerprint) != -1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inserts the value into the filter. Whereas <see cref="TryInsert" />
|
||||
/// returns false if the value cannot be inserted, this throws.
|
||||
/// </summary>
|
||||
/// <param name="value">Value to insert</param>
|
||||
/// <exception cref="FilterFullException">
|
||||
/// Thrown if the filter is
|
||||
/// too full to accept the value.
|
||||
/// </exception>
|
||||
public void Insert(byte[] value)
|
||||
{
|
||||
if (!TryInsert(value))
|
||||
{
|
||||
throw new FilterFullException();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a value from the filter.
|
||||
/// </summary>
|
||||
/// <param name="value">Value to remove</param>
|
||||
/// <returns>True if the filter contained the value, false otherwise.</returns>
|
||||
public bool Remove(byte[] value)
|
||||
{
|
||||
var fingerprint = GetFingerprint(value);
|
||||
_hashAlgorithm.Hash(_valuesBuffer, value, Uint32Bytes);
|
||||
|
||||
var index1 = ToInt32(_valuesBuffer) & (Buckets - 1);
|
||||
|
||||
var offset = IndexToOffset(index1);
|
||||
var removal = _comparator(Contents, offset, fingerprint);
|
||||
if (removal != -1)
|
||||
{
|
||||
Array.Clear(Contents, offset + _fingerprintBuffer.Length * removal, fingerprint.Length);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
var index2 = DeriveIndex2(fingerprint, index1);
|
||||
offset = IndexToOffset(index2);
|
||||
removal = _comparator(Contents, offset, fingerprint);
|
||||
if (removal != -1)
|
||||
{
|
||||
Array.Clear(Contents, offset + _fingerprintBuffer.Length * removal, fingerprint.Length);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to insert the value into the filter.
|
||||
/// </summary>
|
||||
/// <param name="value">Value to insert</param>
|
||||
/// <returns>
|
||||
/// True if it was inserted successfully, false if the
|
||||
/// filter was too full to do so.
|
||||
/// </returns>
|
||||
public bool TryInsert(byte[] value)
|
||||
{
|
||||
return TryInsertInner(value);
|
||||
}
|
||||
|
||||
/// <summary>Indicates whether the current object is equal to another object of the same type.</summary>
|
||||
/// <param name="other">An object to compare with this object.</param>
|
||||
/// <returns>
|
||||
/// true if the current object is equal to the <paramref name="other">other</paramref> parameter; otherwise,
|
||||
/// false.
|
||||
/// </returns>
|
||||
public bool Equals(CuckooFilter other)
|
||||
{
|
||||
if (ReferenceEquals(null, other))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(this, other))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return Buckets == other.Buckets
|
||||
&& BytesPerBucket == other.BytesPerBucket
|
||||
&& CodegenHelpers.BytesEquals(Contents, other.Contents)
|
||||
&& EntriesPerBucket == other.EntriesPerBucket
|
||||
&& MaxKicks == other.MaxKicks;
|
||||
}
|
||||
|
||||
/// <summary>Determines whether the specified object is equal to the current object.</summary>
|
||||
/// <param name="obj">The object to compare with the current object.</param>
|
||||
/// <returns>true if the specified object is equal to the current object; otherwise, false.</returns>
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (ReferenceEquals(null, obj))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(this, obj))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (obj.GetType() != GetType())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Equals((CuckooFilter)obj);
|
||||
}
|
||||
|
||||
/// <summary>Serves as the default hash function.</summary>
|
||||
/// <returns>A hash code for the current object.</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
var contentHash = new byte[4];
|
||||
_hashAlgorithm.Hash(contentHash, Contents, 4);
|
||||
|
||||
var hashCode = (int)Buckets;
|
||||
hashCode = (hashCode * 397) ^ (int)BytesPerBucket;
|
||||
hashCode = (hashCode * 397) ^ (contentHash[0] | (contentHash[1] << 8)
|
||||
| (contentHash[2] << 16)
|
||||
| (contentHash[3] << 24));
|
||||
hashCode = (hashCode * 397) ^ (int)EntriesPerBucket;
|
||||
hashCode = (hashCode * 397) ^ (int)MaxKicks;
|
||||
return hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to insert the value into the filter.
|
||||
/// </summary>
|
||||
/// <param name="value">Value to insert</param>
|
||||
/// <returns>
|
||||
/// True if it was inserted successfully, false if the
|
||||
/// filter was too full to do so.
|
||||
/// </returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryInsertInner(byte[] value)
|
||||
{
|
||||
var fingerprint = GetFingerprint(value);
|
||||
_hashAlgorithm.Hash(_valuesBuffer, value, Uint32Bytes);
|
||||
var index1 = BoundToBucketCount(ToInt32(_valuesBuffer));
|
||||
|
||||
if (_insertIntoBucket(Contents, IndexToOffset(index1), fingerprint))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var index2 = DeriveIndex2(fingerprint, index1);
|
||||
if (_insertIntoBucket(Contents, IndexToOffset(index2), fingerprint))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var targetIndex = Random.Next(1) == 0
|
||||
? index1
|
||||
: index2;
|
||||
|
||||
for (var i = 0; i < MaxKicks; i++)
|
||||
{
|
||||
fingerprint = SwapIntoBucket(fingerprint, targetIndex);
|
||||
targetIndex = DeriveIndex2(fingerprint, targetIndex);
|
||||
|
||||
if (_insertIntoBucket(Contents, IndexToOffset(targetIndex), fingerprint))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted version of the filter. Used for
|
||||
/// examining internal state while testing.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
internal IList<IList<string>> DumpDebug()
|
||||
{
|
||||
var list = new List<IList<string>>();
|
||||
for (var offset = 0L; offset < Contents.Length; offset += BytesPerBucket)
|
||||
{
|
||||
var items = new List<string>();
|
||||
for (var i = 0; i < EntriesPerBucket; i++)
|
||||
{
|
||||
var target = (int)offset + i * _fingerprintBuffer.Length;
|
||||
if (!_zeroCheck(Contents, target))
|
||||
{
|
||||
items.Add(Encoding.ASCII.GetString(Contents, target, _fingerprintBuffer.Length));
|
||||
}
|
||||
}
|
||||
|
||||
list.Add(items);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private static byte[] CreateEmptyBucketData(long buckets, uint itemsPerBucket, uint bytesPerItem)
|
||||
{
|
||||
return new byte[buckets * itemsPerBucket * bytesPerItem];
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int ToInt32(byte[] data)
|
||||
{
|
||||
var x = data[0] << 24;
|
||||
x |= data[1] << 16;
|
||||
x |= data[2] << 8;
|
||||
x |= data[3];
|
||||
return x;
|
||||
}
|
||||
|
||||
private static uint UpperPower2(uint x)
|
||||
{
|
||||
x--;
|
||||
x |= x >> 1;
|
||||
x |= x >> 2;
|
||||
x |= x >> 4;
|
||||
x |= x >> 8;
|
||||
x |= x >> 16;
|
||||
x |= x >> 32;
|
||||
x++;
|
||||
return x;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the value is less than or equal to the number of Buckets.
|
||||
/// </summary>
|
||||
/// <param name="value">Value to bound</param>
|
||||
/// <returns>Truncated value</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private long BoundToBucketCount(long value)
|
||||
{
|
||||
// this.Buckets is always a power of 2, so to ensure index1 is <=1
|
||||
// the number of Buckets, we mask it against Buckets - 1. So if
|
||||
// Buckets is 16 (0b10000), we mask it against (0b01111).
|
||||
return value & (Buckets - 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the alternative index for an item, given its primary
|
||||
/// index and fingerprint.
|
||||
/// </summary>
|
||||
/// <param name="fingerprint">Fingerprint value</param>
|
||||
/// <param name="index1">Primary index</param>
|
||||
/// <returns>The secondary index</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private long DeriveIndex2(byte[] fingerprint, long index1)
|
||||
{
|
||||
_hashAlgorithm.Hash(_valuesBuffer, fingerprint, Uint32Bytes);
|
||||
return index1 ^ BoundToBucketCount(ToInt32(_valuesBuffer));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private byte[] GetFingerprint(byte[] input)
|
||||
{
|
||||
_hashAlgorithm.Hash(_fingerprintBuffer, input, _fingerprintBuffer.Length);
|
||||
if (_zeroCheck(_fingerprintBuffer, 0))
|
||||
{
|
||||
for (var i = 0; i < _fingerprintBuffer.Length; i++)
|
||||
{
|
||||
_fingerprintBuffer[i] = 0xff;
|
||||
}
|
||||
}
|
||||
|
||||
return _fingerprintBuffer;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private int IndexToOffset(long index)
|
||||
{
|
||||
return (int)(index * BytesPerBucket);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private byte[] SwapIntoBucket(byte[] fingerprint, long bucket)
|
||||
{
|
||||
var subIndex = Random.Next((int)EntriesPerBucket);
|
||||
var offset = IndexToOffset(bucket) + subIndex * fingerprint.Length;
|
||||
var newFingerprint = _fingerprintSwapBuffer;
|
||||
|
||||
Array.Copy(Contents, offset, _fingerprintSwapBuffer, 0, fingerprint.Length);
|
||||
Array.Copy(fingerprint, 0, Contents, offset, fingerprint.Length);
|
||||
_fingerprintSwapBuffer = fingerprint;
|
||||
_fingerprintBuffer = newFingerprint;
|
||||
|
||||
return newFingerprint;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// -----------------------------------------------------------------------
|
||||
// <copyright file="CuckooFilterExtensions.cs" company="Microsoft">
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// </copyright>
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.Cuckoo;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for the Cuckoo filter.
|
||||
/// </summary>
|
||||
public static class CuckooFilterExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a value indicating whether the filter probably contains
|
||||
/// the given item.
|
||||
/// </summary>
|
||||
/// <param name="filter">Cuckoo filter instance.</param>
|
||||
/// <param name="value">Value to check</param>
|
||||
/// <returns>True if the filter contains the value, false otherwise.</returns>
|
||||
public static bool ContainsString(this CuckooFilter filter, string value)
|
||||
{
|
||||
return filter.Contains(Encoding.ASCII.GetBytes(value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inserts the value into the filter. Whereas <see cref="TryInsertString" />
|
||||
/// returns false if the value cannot be inserted, this throws.
|
||||
/// </summary>
|
||||
/// <param name="filter">Cuckoo filter instance.</param>
|
||||
/// <param name="value">Value to insert</param>
|
||||
/// <exception cref="FilterFullException">
|
||||
/// Thrown if the filter is
|
||||
/// too full to accept the value.
|
||||
/// </exception>
|
||||
public static bool InsertString(this CuckooFilter filter, string value)
|
||||
{
|
||||
return filter.TryInsert(Encoding.ASCII.GetBytes(value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to insert the value into the filter.
|
||||
/// </summary>
|
||||
/// <param name="filter">Cuckoo filter instance.</param>
|
||||
/// <param name="value">Value to insert</param>
|
||||
/// <returns>
|
||||
/// True if it was inserted successfully, false if the
|
||||
/// filter was too full to do so.
|
||||
/// </returns>
|
||||
public static bool TryInsertString(this CuckooFilter filter, string value)
|
||||
{
|
||||
return filter.TryInsert(Encoding.ASCII.GetBytes(value));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// -----------------------------------------------------------------------
|
||||
// <copyright file="FilterFullException.cs" company="Microsoft">
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// </copyright>
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.Cuckoo;
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown in <see cref="CuckooFilter.Insert" /> if the filter is
|
||||
/// too full to accept more items.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class FilterFullException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new instance of the exception.
|
||||
/// </summary>
|
||||
public FilterFullException()
|
||||
: base("The cuckoo filter is too full to accept more items")
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||
<ConfigureAwait />
|
||||
</Weavers>
|
||||
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
|
||||
<xs:element name="Weavers">
|
||||
<xs:complexType>
|
||||
<xs:all>
|
||||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" />
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
<xs:attribute name="VerifyAssembly" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="GenerateXsd" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:schema>
|
||||
@@ -0,0 +1,50 @@
|
||||
// -----------------------------------------------------------------------
|
||||
// <copyright file="GzipStreamSerializer.cs" company="Microsoft">
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// </copyright>
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
|
||||
namespace Microsoft.Cuckoo;
|
||||
|
||||
/// <summary>
|
||||
/// Serializer that creates a gzipped stream.
|
||||
/// </summary>
|
||||
public class GzipStreamSerializer : SimpleStreamSerializer
|
||||
{
|
||||
private readonly CompressionLevel _compressionLevel;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new serialize that serializes into a gzipped string.
|
||||
/// </summary>
|
||||
/// <param name="compressionLevel">Gzip compression level</param>
|
||||
public GzipStreamSerializer(CompressionLevel compressionLevel = CompressionLevel.Optimal)
|
||||
{
|
||||
_compressionLevel = compressionLevel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes the CuckooFilter to from a stream.
|
||||
/// </summary>
|
||||
/// <param name="source">Source stream to read</param>
|
||||
/// <param name="hashAlgorithm">Hash algorithm</param>
|
||||
public override CuckooFilter Deserialize(Stream source, IHashAlgorithm hashAlgorithm = null)
|
||||
{
|
||||
var compressionStream = new GZipStream(source, CompressionMode.Decompress);
|
||||
return base.Deserialize(compressionStream, hashAlgorithm);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializes the CuckooFilter to a stream.
|
||||
/// </summary>
|
||||
/// <param name="target">Target stream to write to</param>
|
||||
/// <param name="filter">Filter to serialize</param>
|
||||
public override void Serialize(Stream target, CuckooFilter filter)
|
||||
{
|
||||
var compressionStream = new GZipStream(target, _compressionLevel);
|
||||
base.Serialize(compressionStream, filter);
|
||||
compressionStream.Flush();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// -----------------------------------------------------------------------
|
||||
// <copyright file="ICuckooFilter.cs" company="Microsoft">
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// </copyright>
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.Cuckoo;
|
||||
|
||||
/// <summary>
|
||||
/// Cuckoo filter interface.
|
||||
/// </summary>
|
||||
public interface ICuckooFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the total size, in memory, of the filter.
|
||||
/// </summary>
|
||||
uint Size { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns a value indicating whether the filter probably contains
|
||||
/// the given item.
|
||||
/// </summary>
|
||||
/// <param name="value">Value to check</param>
|
||||
/// <returns>True if the filter contains the value, false otherwise.</returns>
|
||||
bool Contains(byte[] value);
|
||||
|
||||
/// <summary>
|
||||
/// Inserts the value into the filter. Whereas <see cref="CuckooFilter.TryInsert" />
|
||||
/// returns false if the value cannot be inserted, this throws.
|
||||
/// </summary>
|
||||
/// <param name="value">Value to insert</param>
|
||||
/// <exception cref="FilterFullException">
|
||||
/// Thrown if the filter is
|
||||
/// too full to accept the value.
|
||||
/// </exception>
|
||||
void Insert(byte[] value);
|
||||
|
||||
/// <summary>
|
||||
/// Removes a value from the filter.
|
||||
/// </summary>
|
||||
/// <param name="value">Value to remove</param>
|
||||
/// <returns>True if the filter contained the value, false otherwise.</returns>
|
||||
bool Remove(byte[] value);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to insert the value into the filter.
|
||||
/// </summary>
|
||||
/// <param name="value">Value to insert</param>
|
||||
/// <returns>
|
||||
/// True if it was inserted successfully, false if the
|
||||
/// filter was too full to do so.
|
||||
/// </returns>
|
||||
bool TryInsert(byte[] value);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// -----------------------------------------------------------------------
|
||||
// <copyright file="ICuckooSerializer.cs" company="Microsoft">
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// </copyright>
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
using System.IO;
|
||||
|
||||
namespace Microsoft.Cuckoo;
|
||||
|
||||
public interface ICuckooSerializer
|
||||
{
|
||||
/// <summary>
|
||||
/// Deserializes the CuckooFilter to from a stream.
|
||||
/// </summary>
|
||||
/// <param name="source">Source stream to read</param>
|
||||
/// <param name="hashAlgorithm">Hash algorithm</param>
|
||||
CuckooFilter Deserialize(Stream source, IHashAlgorithm hashAlgorithm = null);
|
||||
|
||||
/// <summary>
|
||||
/// Serializes the CuckooFilter to a stream.
|
||||
/// </summary>
|
||||
/// <param name="target">Target stream to write to</param>
|
||||
/// <param name="filter">Filter to serialize</param>
|
||||
void Serialize(Stream target, CuckooFilter filter);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// -----------------------------------------------------------------------
|
||||
// <copyright file="IHashAlgorithm.cs" company="Microsoft">
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// </copyright>
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.Cuckoo;
|
||||
|
||||
/// <summary>
|
||||
/// Describes a type used for hashing a value for the cuckoo filter.
|
||||
/// </summary>
|
||||
public interface IHashAlgorithm
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a hash of the value.
|
||||
/// </summary>
|
||||
/// <param name="target">Target array to hash to</param>
|
||||
/// <param name="value">Value to hash</param>
|
||||
/// <param name="hashLength">Number of bytes to write into the target.</param>
|
||||
void Hash(byte[] target, byte[] value, int hashLength);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<PackageId>Microsoft.Cuckoo</PackageId>
|
||||
<Version>1.0.0</Version>
|
||||
<Authors>Microsoft</Authors>
|
||||
<Company>Microsoft Corporation</Company>
|
||||
<Copyright>© Microsoft Corporation. All rights reserved.</Copyright>
|
||||
<RepositoryType>GitHub</RepositoryType>
|
||||
<RepositoryUrl>https://github.com/microsoft/mixer-cuckoo</RepositoryUrl>
|
||||
<ProjectUrl>https://github.com/microsoft/mixer-cuckoo</ProjectUrl>
|
||||
<Description>A cuckoo filter implementation for C#</Description>
|
||||
<License>MIT</License>
|
||||
<RequireLicenseAcceptance>true</RequireLicenseAcceptance>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Sigil" />
|
||||
<PackageReference Include="Standart.Hash.xxHash" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Update="Fody">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,80 @@
|
||||
// -----------------------------------------------------------------------
|
||||
// <copyright file="SimpleStreamSerializer.cs" company="Microsoft">
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// </copyright>
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
using System.IO;
|
||||
|
||||
namespace Microsoft.Cuckoo;
|
||||
|
||||
/// <summary>
|
||||
/// Simple stream serializer that writes the cuckoo filter directly out
|
||||
/// to a stream. Investigate <see cref="GzipStreamSerializer" /> for a more
|
||||
/// storage-friendly version.
|
||||
/// </summary>
|
||||
public class SimpleStreamSerializer : ICuckooSerializer
|
||||
{
|
||||
/// <summary>
|
||||
/// Deserializes the CuckooFilter to from a stream.
|
||||
/// </summary>
|
||||
/// <param name="source">Source stream to read</param>
|
||||
/// <param name="hashAlgorithm">Hash algorithm</param>
|
||||
public virtual CuckooFilter Deserialize(Stream source, IHashAlgorithm hashAlgorithm = null)
|
||||
{
|
||||
var paramsReadBuffer = new byte[16];
|
||||
//source.Read(paramsReadBuffer, 0, 16);
|
||||
source.ReadExactly(paramsReadBuffer);
|
||||
|
||||
var entriesPerBucket = ReadBigEndianUint(paramsReadBuffer, 0);
|
||||
var fingerprintLength = ReadBigEndianUint(paramsReadBuffer, 4);
|
||||
var buckets = ReadBigEndianUint(paramsReadBuffer, 8);
|
||||
var maxKicks = ReadBigEndianUint(paramsReadBuffer, 12);
|
||||
|
||||
var contents = new byte[buckets * entriesPerBucket * fingerprintLength];
|
||||
//source.Read(contents, 0, contents.Length);
|
||||
source.ReadExactly(contents);
|
||||
|
||||
return new CuckooFilter(
|
||||
contents,
|
||||
entriesPerBucket,
|
||||
fingerprintLength,
|
||||
maxKicks,
|
||||
hashAlgorithm);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializes the CuckooFilter to a stream.
|
||||
/// </summary>
|
||||
/// <param name="target">Target stream to write to</param>
|
||||
/// <param name="filter">Filter to serialize</param>
|
||||
public virtual void Serialize(Stream target, CuckooFilter filter)
|
||||
{
|
||||
var writeBuffer = new byte[16];
|
||||
WriteBigEndianUint(writeBuffer, filter.EntriesPerBucket, 0);
|
||||
WriteBigEndianUint(writeBuffer, filter.FingerprintLength, 4);
|
||||
WriteBigEndianUint(writeBuffer, filter.Buckets, 8);
|
||||
WriteBigEndianUint(writeBuffer, filter.MaxKicks, 12);
|
||||
target.Write(writeBuffer, 0, 16);
|
||||
target.Write(filter.Contents, 0, filter.Contents.Length);
|
||||
}
|
||||
|
||||
private static uint ReadBigEndianUint(byte[] target, int offset)
|
||||
{
|
||||
uint x = 0;
|
||||
x |= (uint)(target[offset] << 24);
|
||||
x |= (uint)(target[offset + 1] << 16);
|
||||
x |= (uint)(target[offset + 2] << 8);
|
||||
x |= target[offset + 3];
|
||||
|
||||
return x;
|
||||
}
|
||||
|
||||
private static void WriteBigEndianUint(byte[] target, uint number, int offset)
|
||||
{
|
||||
target[offset] = (byte)(number >> 24);
|
||||
target[offset + 1] = (byte)(number >> 16);
|
||||
target[offset + 2] = (byte)(number >> 8);
|
||||
target[offset + 3] = (byte)number;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// -----------------------------------------------------------------------
|
||||
// <copyright file="XxHashAlgorithm.cs" company="Microsoft">
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// </copyright>
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
using Standart.Hash.xxHash;
|
||||
|
||||
namespace Microsoft.Cuckoo;
|
||||
|
||||
/// <summary>
|
||||
/// An IHashAlgorithm using XxHash.
|
||||
/// </summary>
|
||||
public class XxHashAlgorithm : IHashAlgorithm
|
||||
{
|
||||
internal static readonly IHashAlgorithm Instance = new XxHashAlgorithm();
|
||||
|
||||
private readonly ulong _seed;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new xxhash algorithm instance.
|
||||
/// </summary>
|
||||
/// <param name="seed">Seed value for the hash</param>
|
||||
public XxHashAlgorithm(ulong seed = 0)
|
||||
{
|
||||
_seed = seed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a hash of the value.
|
||||
/// </summary>
|
||||
/// <param name="target">Target array to hash to</param>
|
||||
/// <param name="value">Value to hash</param>
|
||||
/// <param name="hashLength">Desired length of the fingerprint.</param>
|
||||
public void Hash(byte[] target, byte[] value, int hashLength)
|
||||
{
|
||||
var hash = xxHash64.ComputeHash(value, value.Length, _seed);
|
||||
for (var i = 0; i < hashLength; i++)
|
||||
{
|
||||
target[i] = (byte)(hash & 0xFF);
|
||||
hash >>= 8;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace MyTelegram.Abstractions;
|
||||
|
||||
public enum ConnectionType
|
||||
{
|
||||
UnKnown = 0,
|
||||
Generic = 1,
|
||||
Media = 2,
|
||||
Push = 4
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace MyTelegram.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="AuthKeyId"></param>
|
||||
/// <param name="MsgKey">int128,length=16</param>
|
||||
/// <param name="EncryptedData"></param>
|
||||
/// <param name="ConnectionId"></param>
|
||||
/// <param name="ConnectionType"></param>
|
||||
/// <param name="ClientIp"></param>
|
||||
/// <param name="RequestId"></param>
|
||||
/// <param name="Date"></param>
|
||||
public record EncryptedMessage(long AuthKeyId,
|
||||
ReadOnlyMemory<byte> MsgKey,
|
||||
ReadOnlyMemory<byte> EncryptedData,
|
||||
string ConnectionId,
|
||||
ConnectionType ConnectionType,
|
||||
string ClientIp,
|
||||
Guid RequestId,
|
||||
long Date
|
||||
) : IMtpMessage
|
||||
{
|
||||
public string ConnectionId { get; set; } = ConnectionId;
|
||||
public ConnectionType ConnectionType { get; set; } = ConnectionType;
|
||||
public string ClientIp { get; set; } = ClientIp;
|
||||
[JsonIgnore]public IMemoryOwner<byte>? MemoryOwner { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace MyTelegram.Abstractions;
|
||||
|
||||
public record EncryptedMessageResponse(long AuthKeyId,
|
||||
ReadOnlyMemory<byte> Data,
|
||||
string ConnectionId,
|
||||
long SeqNumber
|
||||
) : IMayHaveMemoryOwner
|
||||
{
|
||||
public IMemoryOwner<byte>? MemoryOwner { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||
<ConfigureAwait />
|
||||
</Weavers>
|
||||
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
|
||||
<xs:element name="Weavers">
|
||||
<xs:complexType>
|
||||
<xs:all>
|
||||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" />
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
<xs:attribute name="VerifyAssembly" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="GenerateXsd" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:schema>
|
||||
@@ -0,0 +1,2 @@
|
||||
global using System.Buffers;
|
||||
global using System.Text.Json.Serialization;
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace MyTelegram.Abstractions;
|
||||
|
||||
public interface IDataProcessor<in TData>
|
||||
{
|
||||
Task ProcessAsync(TData data);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace MyTelegram.Abstractions;
|
||||
|
||||
public interface IEventDataSerializer<TEventData> : IEventDataSerializer
|
||||
{
|
||||
void Serialize(IBufferWriter<byte> writer, TEventData data);
|
||||
TEventData Deserialize(ReadOnlyMemory<byte> buffer);
|
||||
}
|
||||
|
||||
public interface IEventDataSerializer
|
||||
{
|
||||
//object? Deserialize(Type type, ReadOnlyMemory<byte> buffer);
|
||||
object Deserialize(Type type, ReadOnlyMemory<byte> buffer);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace MyTelegram.Abstractions;
|
||||
|
||||
public interface IMayHaveMemoryOwner
|
||||
{
|
||||
IMemoryOwner<byte>? MemoryOwner { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace MyTelegram.Abstractions;
|
||||
|
||||
public interface IMessageIdHelper
|
||||
{
|
||||
long GenerateMessageId();
|
||||
|
||||
long GenerateUniqueId();
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace MyTelegram.Abstractions;
|
||||
|
||||
public interface IMessageQueueProcessor<in TData>
|
||||
{
|
||||
void Enqueue(TData message,
|
||||
long key);
|
||||
|
||||
Task ProcessAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace MyTelegram.Abstractions;
|
||||
|
||||
public interface IMtpMessage : IMayHaveMemoryOwner
|
||||
{
|
||||
string ClientIp { get; set; }
|
||||
string ConnectionId { get; set; }
|
||||
ConnectionType ConnectionType { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace MyTelegram.Abstractions;
|
||||
|
||||
public interface ISingletonDependency { }
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace MyTelegram.Abstractions;
|
||||
|
||||
public interface ITransientDependency { }
|
||||
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,43 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Reflection;
|
||||
|
||||
namespace MyTelegram.Abstractions;
|
||||
public static class MyTelegramAbstractionsExtensions
|
||||
{
|
||||
public static void RegisterServices(this IServiceCollection services, Assembly? assembly = null)
|
||||
{
|
||||
assembly ??= Assembly.GetCallingAssembly();
|
||||
|
||||
var singletonBaseInterface = typeof(ISingletonDependency);
|
||||
var transientBaseInterface = typeof(ITransientDependency);
|
||||
|
||||
var types = assembly.GetTypes()
|
||||
.Where(p => p != singletonBaseInterface && p != transientBaseInterface && !p.IsAbstract)
|
||||
.ToList()
|
||||
;
|
||||
var singletonTypes = types.Where(singletonBaseInterface.IsAssignableFrom);
|
||||
var transientTypes = types.Where(transientBaseInterface.IsAssignableFrom);
|
||||
|
||||
foreach (var type in singletonTypes)
|
||||
{
|
||||
var baseInterfaces = type.GetInterfaces();
|
||||
foreach (var baseInterface in baseInterfaces)
|
||||
{
|
||||
services.AddSingleton(baseInterface, type);
|
||||
}
|
||||
|
||||
services.AddSingleton(type);
|
||||
}
|
||||
|
||||
foreach (var type in transientTypes)
|
||||
{
|
||||
var baseInterfaces = type.GetInterfaces();
|
||||
foreach (var baseInterface in baseInterfaces)
|
||||
{
|
||||
services.AddTransient(baseInterface, type);
|
||||
}
|
||||
|
||||
services.AddTransient(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace MyTelegram.Abstractions;
|
||||
public record UnencryptedMessage(
|
||||
long AuthKeyId,
|
||||
string ClientIp,
|
||||
string ConnectionId,
|
||||
ConnectionType ConnectionType,
|
||||
ReadOnlyMemory<byte> MessageData,
|
||||
int MessageDataLength,
|
||||
long MessageId,
|
||||
uint ObjectId,
|
||||
Guid RequestId,
|
||||
long Date
|
||||
) : IMtpMessage
|
||||
{
|
||||
public string ConnectionId { get; set; } = ConnectionId;
|
||||
public ConnectionType ConnectionType { get; set; } = ConnectionType;
|
||||
public string ClientIp { get; set; } = ClientIp;
|
||||
public IMemoryOwner<byte>? MemoryOwner { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace MyTelegram.Abstractions;
|
||||
public record UnencryptedMessageResponse(long AuthKeyId,
|
||||
ReadOnlyMemory<byte> Data,
|
||||
string ConnectionId,
|
||||
long ReqMsgId) : IMayHaveMemoryOwner
|
||||
{
|
||||
public IMemoryOwner<byte>? MemoryOwner { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MyTelegram.Messenger.Services.Interfaces;
|
||||
|
||||
namespace MyTelegram.Admin.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/service-notifications")]
|
||||
public class ServiceNotificationsController : ControllerBase
|
||||
{
|
||||
private readonly IServiceNotificationAppService _serviceNotificationAppService;
|
||||
private readonly ILogger<ServiceNotificationsController> _logger;
|
||||
|
||||
public ServiceNotificationsController(
|
||||
IServiceNotificationAppService serviceNotificationAppService,
|
||||
ILogger<ServiceNotificationsController> logger)
|
||||
{
|
||||
_serviceNotificationAppService = serviceNotificationAppService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpPost("send")]
|
||||
public async Task<IActionResult> SendNotification([FromBody] SendNotificationRequest request)
|
||||
{
|
||||
_logger.LogInformation("Received notification request for user {UserId}: Type={Type}, Popup={Popup}",
|
||||
request.UserId, request.Type, request.Popup);
|
||||
|
||||
try
|
||||
{
|
||||
await _serviceNotificationAppService.SendServiceNotificationAsync(
|
||||
request.UserId,
|
||||
request.Type,
|
||||
request.Message,
|
||||
request.Popup,
|
||||
media: null, // TODO: добавить поддержку медиа
|
||||
entities: null,
|
||||
invertMedia: false
|
||||
);
|
||||
|
||||
_logger.LogInformation("Notification sent successfully to user {UserId}", request.UserId);
|
||||
|
||||
return Ok(new { success = true, message = "Notification sent" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to send notification to user {UserId}", request.UserId);
|
||||
return StatusCode(500, new { success = false, error = ex.Message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class SendNotificationRequest
|
||||
{
|
||||
public long UserId { get; set; }
|
||||
public string Type { get; set; } = string.Empty;
|
||||
public string Message { get; set; } = string.Empty;
|
||||
public bool Popup { get; set; }
|
||||
public string? MediaUrl { get; set; }
|
||||
public string? MediaType { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 5000
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
|
||||
COPY ["Directory.Build.props", "."]
|
||||
COPY ["Directory.Packages.props", "."]
|
||||
COPY ["src/MyTelegram.Admin.Api/MyTelegram.Admin.Api.csproj", "src/MyTelegram.Admin.Api/"]
|
||||
COPY ["src/MyTelegram.Domain/MyTelegram.Domain.csproj", "src/MyTelegram.Domain/"]
|
||||
COPY ["src/MyTelegram.Domain.Shared/MyTelegram.Domain.Shared.csproj", "src/MyTelegram.Domain.Shared/"]
|
||||
COPY ["src/MyTelegram.Services/MyTelegram.Services.csproj", "src/MyTelegram.Services/"]
|
||||
COPY ["src/MyTelegram.Queries/MyTelegram.Queries.csproj", "src/MyTelegram.Queries/"]
|
||||
COPY ["src/MyTelegram.ReadModel.Interfaces/MyTelegram.ReadModel.Interfaces.csproj", "src/MyTelegram.ReadModel.Interfaces/"]
|
||||
COPY ["src/MyTelegram.ReadModel/MyTelegram.ReadModel.csproj", "src/MyTelegram.ReadModel/"]
|
||||
COPY ["src/MyTelegram.Schema/MyTelegram.Schema.csproj", "src/MyTelegram.Schema/"]
|
||||
COPY ["src/MyTelegram.Core/MyTelegram.Core.csproj", "src/MyTelegram.Core/"]
|
||||
COPY ["src/MyTelegram.EventFlow/MyTelegram.EventFlow.csproj", "src/MyTelegram.EventFlow/"]
|
||||
COPY ["src/MyTelegram.EventFlow.MongoDB/MyTelegram.EventFlow.MongoDB.csproj", "src/MyTelegram.EventFlow.MongoDB/"]
|
||||
COPY ["src/MyTelegram.EventBus/MyTelegram.EventBus.csproj", "src/MyTelegram.EventBus/"]
|
||||
COPY ["src/MyTelegram.EventBus.RabbitMQ/MyTelegram.EventBus.RabbitMQ.csproj", "src/MyTelegram.EventBus.RabbitMQ/"]
|
||||
COPY ["src/MyTelegram.Abstractions/MyTelegram.Abstractions.csproj", "src/MyTelegram.Abstractions/"]
|
||||
|
||||
RUN dotnet restore "./src/MyTelegram.Admin.Api/MyTelegram.Admin.Api.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/src/MyTelegram.Admin.Api"
|
||||
RUN dotnet build "./MyTelegram.Admin.Api.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
RUN dotnet publish "./MyTelegram.Admin.Api.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "MyTelegram.Admin.Api.dll"]
|
||||
@@ -0,0 +1,71 @@
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace MyTelegram.Admin.Api.Middleware;
|
||||
|
||||
/// <summary>
|
||||
/// Middleware аутентификации для Admin API.
|
||||
/// Для всех админских эндпоинтов требует API-ключ или токен.
|
||||
/// </summary>
|
||||
public class AdminAuthMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger<AdminAuthMiddleware> _logger;
|
||||
private readonly string _adminApiKey;
|
||||
|
||||
public AdminAuthMiddleware(RequestDelegate next, ILogger<AdminAuthMiddleware> logger, IConfiguration configuration)
|
||||
{
|
||||
_next = next;
|
||||
_logger = logger;
|
||||
|
||||
// Берём ключ из конфигурации или переменной окружения,
|
||||
// значение по умолчанию пригодно только для разработки
|
||||
_adminApiKey = configuration["ADMIN_API_KEY"]
|
||||
?? Environment.GetEnvironmentVariable("ADMIN_API_KEY")
|
||||
?? "CHANGE_ME_IN_PRODUCTION";
|
||||
|
||||
if (_adminApiKey == "CHANGE_ME_IN_PRODUCTION")
|
||||
{
|
||||
_logger.LogWarning("Using default Admin API key! Set ADMIN_API_KEY environment variable.");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Admin API authentication enabled with custom key");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
var path = context.Request.Path.Value;
|
||||
|
||||
// Проверяем аутентификацию для всех эндпоинтов /api/*
|
||||
if (path != null && path.StartsWith("/api/"))
|
||||
{
|
||||
// Сначала ищем ключ в заголовке X-Admin-API-Key
|
||||
if (!context.Request.Headers.TryGetValue("X-Admin-API-Key", out var apiKey) ||
|
||||
apiKey != _adminApiKey)
|
||||
{
|
||||
// Затем пробуем заголовок Authorization с Bearer-токеном
|
||||
if (!context.Request.Headers.TryGetValue("Authorization", out var authHeader) ||
|
||||
!authHeader.ToString().StartsWith("Bearer ") ||
|
||||
authHeader.ToString().Substring(7) != _adminApiKey)
|
||||
{
|
||||
_logger.LogWarning("Unauthorized Admin API access attempt - IP: {IP}, Path: {Path}",
|
||||
context.Connection.RemoteIpAddress, path);
|
||||
|
||||
context.Response.StatusCode = 401;
|
||||
context.Response.ContentType = "application/json";
|
||||
await context.Response.WriteAsJsonAsync(new
|
||||
{
|
||||
success = false,
|
||||
error = "Unauthorized: Invalid or missing API key"
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogDebug("Admin API access authorized - Path: {Path}", path);
|
||||
}
|
||||
|
||||
await _next(context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" />
|
||||
<PackageReference Include="MessagePack" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MyTelegram.Domain\MyTelegram.Domain.csproj" />
|
||||
<ProjectReference Include="..\MyTelegram.Domain.Shared\MyTelegram.Domain.Shared.csproj" />
|
||||
<ProjectReference Include="..\MyTelegram.Services\MyTelegram.Services.csproj" />
|
||||
<ProjectReference Include="..\MyTelegram.Queries\MyTelegram.Queries.csproj" />
|
||||
<ProjectReference Include="..\MyTelegram.EventFlow.MongoDB\MyTelegram.EventFlow.MongoDB.csproj" />
|
||||
<ProjectReference Include="..\MyTelegram.EventBus.RabbitMQ\MyTelegram.EventBus.RabbitMQ.csproj" />
|
||||
<ProjectReference Include="..\MyTelegram.Messenger\MyTelegram.Messenger.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,231 @@
|
||||
using EventFlow;
|
||||
using EventFlow.Extensions;
|
||||
using EventFlow.MongoDB.Extensions;
|
||||
using MyTelegram;
|
||||
using MyTelegram.Domain.Commands.User;
|
||||
using MyTelegram.Domain.Aggregates.User;
|
||||
using MyTelegram.Domain.Shared.Events;
|
||||
using MyTelegram.EventBus;
|
||||
using MyTelegram.Abstractions;
|
||||
using MyTelegram.Admin.Api.Serializers;
|
||||
using MyTelegram.EventBus.RabbitMQ.Extensions;
|
||||
using MyTelegram.EventBus.RabbitMQ;
|
||||
using MyTelegram.Messenger.Extensions;
|
||||
using MyTelegram.ReadModel.MongoDB;
|
||||
using MyTelegram.ReadModel.Impl;
|
||||
using MyTelegram.Schema;
|
||||
using MongoDB.Driver;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Слушаем порт 5000
|
||||
builder.WebHost.ConfigureKestrel(options =>
|
||||
{
|
||||
options.ListenAnyIP(5000);
|
||||
});
|
||||
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
|
||||
// Настройки RabbitMQ и MongoDB
|
||||
builder.Services.Configure<EventBusRabbitMqOptions>(builder.Configuration.GetSection("RabbitMQ:EventBus"));
|
||||
builder.Services.Configure<RabbitMqOptions>(builder.Configuration.GetSection("RabbitMQ:Connections:Default"));
|
||||
|
||||
var connectionString = builder.Configuration.GetConnectionString("Default") ?? "mongodb://mongodb:27017";
|
||||
var databaseName = builder.Configuration.GetValue<string>("App:DatabaseName") ?? "eventflow";
|
||||
|
||||
var mongoClient = new MongoClient(connectionString);
|
||||
builder.Services.AddSingleton<IMongoClient>(mongoClient);
|
||||
builder.Services.AddSingleton<IMongoDatabase>(_ => mongoClient.GetDatabase(databaseName));
|
||||
|
||||
// Подключаем EventFlow с хранилищем событий в MongoDB
|
||||
builder.Services.AddEventFlow(options =>
|
||||
{
|
||||
options.AddDefaults(typeof(UserAggregate).Assembly);
|
||||
options.UseMongoDbEventStore();
|
||||
options.ConfigureMongoDb(connectionString, databaseName);
|
||||
});
|
||||
|
||||
// Регистрируем только нужные сериализаторы, не подключая весь набор сервисов
|
||||
// мессенджера, иначе сериализаторы Mongo конфликтуют между собой
|
||||
try
|
||||
{
|
||||
builder.Services.RegisterMongoDbSerializer();
|
||||
}
|
||||
catch (MongoDB.Bson.BsonSerializationException)
|
||||
{
|
||||
// Сериализатор мог быть уже зарегистрирован, повторную регистрацию пропускаем
|
||||
}
|
||||
|
||||
// Шина событий поверх RabbitMQ
|
||||
builder.Services.AddTransient<IEventDataSerializer<BotGiftEvent>, BotGiftEventSerializer>();
|
||||
builder.Services.AddMyTelegramRabbitMqEventBus();
|
||||
|
||||
// Регистрируем сервис отправки служебных уведомлений вручную
|
||||
builder.Services.AddScoped<MyTelegram.Messenger.Services.Interfaces.IServiceNotificationAppService,
|
||||
MyTelegram.Messenger.Services.Impl.ServiceNotificationAppService>();
|
||||
|
||||
// CORS для админ-панели
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("AllowAdmin", policy =>
|
||||
{
|
||||
policy.AllowAnyOrigin()
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader();
|
||||
});
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseCors("AllowAdmin");
|
||||
|
||||
// Ограничиваем размер тела запроса, чтобы не переполнять память
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
const long maxSize = 10 * 1024 * 1024; // 10MB
|
||||
var feature = context.Features.Get<Microsoft.AspNetCore.Http.Features.IHttpMaxRequestBodySizeFeature>();
|
||||
if (feature != null && !feature.IsReadOnly)
|
||||
{
|
||||
feature.MaxRequestBodySize = maxSize;
|
||||
}
|
||||
await next();
|
||||
});
|
||||
|
||||
app.UseMiddleware<MyTelegram.Admin.Api.Middleware.AdminAuthMiddleware>();
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
app.MapPost("/api/admin/star-gifts/send", async (
|
||||
AdminSendStarGiftRequest request,
|
||||
IMongoDatabase mongoDatabase,
|
||||
IEventBus eventBus) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (request.Count <= 0)
|
||||
{
|
||||
return Results.BadRequest(new { success = false, error = "Count must be positive" });
|
||||
}
|
||||
|
||||
var giftId = request.GiftId;
|
||||
var giftCollection = mongoDatabase.GetCollection<AvailableStarGiftReadModel>("AvailableStarGiftReadModel");
|
||||
var gift = await giftCollection.Find(x => x.GiftId == giftId).FirstOrDefaultAsync();
|
||||
if (gift == null)
|
||||
{
|
||||
return Results.NotFound(new { success = false, error = "Gift not found" });
|
||||
}
|
||||
|
||||
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
var sent = 0;
|
||||
var canUpgrade = request.CanUpgrade ?? (gift.UpgradeStars.HasValue);
|
||||
|
||||
for (var i = 0; i < request.Count; i++)
|
||||
{
|
||||
var evt = new BotGiftEvent
|
||||
{
|
||||
BotUserId = request.FromUserId,
|
||||
ToUserId = request.ToUserId,
|
||||
GiftId = gift.GiftId,
|
||||
Count = 1,
|
||||
Message = request.Message,
|
||||
HideName = request.NameHidden,
|
||||
IncludeUpgrade = canUpgrade,
|
||||
Timestamp = now,
|
||||
RandomId = Random.Shared.NextInt64()
|
||||
};
|
||||
|
||||
await eventBus.PublishAsync(evt);
|
||||
sent++;
|
||||
}
|
||||
|
||||
return Results.Ok(new { success = true, sent });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Results.BadRequest(new { success = false, error = ex.Message });
|
||||
}
|
||||
});
|
||||
|
||||
// Установка верификации пользователя
|
||||
app.MapPost("/api/admin/verification/users/{userId}", async (
|
||||
long userId,
|
||||
SetUserVerificationRequest request,
|
||||
ICommandBus commandBus) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var command = new SetCustomVerificationCommand(
|
||||
UserId.Create(userId),
|
||||
RequestInfo.Empty with { Date = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() },
|
||||
true,
|
||||
request.BotUserId,
|
||||
userId,
|
||||
request.IconEmojiId,
|
||||
request.Description,
|
||||
request.CustomDescription
|
||||
);
|
||||
|
||||
await commandBus.PublishAsync(command, CancellationToken.None);
|
||||
|
||||
return Results.Ok(new { success = true, message = "Verification set successfully" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Results.BadRequest(new { success = false, error = ex.Message });
|
||||
}
|
||||
});
|
||||
|
||||
// Снятие верификации пользователя
|
||||
app.MapDelete("/api/admin/verification/users/{userId}", async (
|
||||
long userId,
|
||||
ICommandBus commandBus) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var command = new SetCustomVerificationCommand(
|
||||
UserId.Create(userId),
|
||||
RequestInfo.Empty with { Date = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() },
|
||||
false,
|
||||
null,
|
||||
userId,
|
||||
0,
|
||||
string.Empty,
|
||||
null
|
||||
);
|
||||
|
||||
await commandBus.PublishAsync(command, CancellationToken.None);
|
||||
|
||||
return Results.Ok(new { success = true, message = "Verification removed successfully" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Results.BadRequest(new { success = false, error = ex.Message });
|
||||
}
|
||||
});
|
||||
|
||||
app.Run();
|
||||
|
||||
public record AdminSendStarGiftRequest(
|
||||
long FromUserId,
|
||||
long ToUserId,
|
||||
long GiftId,
|
||||
int Count,
|
||||
bool NameHidden,
|
||||
bool? CanUpgrade,
|
||||
string? Message
|
||||
);
|
||||
|
||||
public record SetUserVerificationRequest(
|
||||
long BotUserId,
|
||||
long IconEmojiId,
|
||||
string Description,
|
||||
string? CustomDescription
|
||||
);
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.Buffers;
|
||||
using MessagePack;
|
||||
using MyTelegram.Abstractions;
|
||||
using MyTelegram.Domain.Shared.Events;
|
||||
|
||||
namespace MyTelegram.Admin.Api.Serializers;
|
||||
|
||||
public class BotGiftEventSerializer : IEventDataSerializer<BotGiftEvent>
|
||||
{
|
||||
private static readonly MessagePackSerializerOptions Options = MessagePackSerializerOptions.Standard
|
||||
.WithResolver(MessagePack.Resolvers.ContractlessStandardResolver.Instance);
|
||||
|
||||
public object Deserialize(Type type, ReadOnlyMemory<byte> buffer)
|
||||
{
|
||||
return MessagePackSerializer.Deserialize<BotGiftEvent>(buffer, Options)!;
|
||||
}
|
||||
|
||||
public BotGiftEvent Deserialize(ReadOnlyMemory<byte> buffer)
|
||||
{
|
||||
return MessagePackSerializer.Deserialize<BotGiftEvent>(buffer, Options);
|
||||
}
|
||||
|
||||
public void Serialize(IBufferWriter<byte> writer, BotGiftEvent value)
|
||||
{
|
||||
MessagePackSerializer.Serialize(writer, value, Options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"ConnectionStrings": {
|
||||
"Default": "mongodb://user:CHANGE_ME@mongodb:27017"
|
||||
},
|
||||
"App": {
|
||||
"DatabaseName": "eventflow",
|
||||
"ReadModelDatabaseName": "readmodel"
|
||||
},
|
||||
"RabbitMQ": {
|
||||
"Connections": {
|
||||
"Default": {
|
||||
"HostName": "rabbitmq",
|
||||
"Port": 5672,
|
||||
"UserName": "guest",
|
||||
"Password": "guest"
|
||||
}
|
||||
},
|
||||
"EventBus": {
|
||||
"ClientName": "MyTelegram.Admin.Api",
|
||||
"ExchangeName": "MyTelegram"
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
namespace MyTelegram.AuthServer.BackgroundServices;
|
||||
|
||||
public class MyTelegramAuthServerBackgroundService(
|
||||
ILogger<MyTelegramAuthServerBackgroundService> logger,
|
||||
IHandlerHelper handlerHelper,
|
||||
IFingerprintHelper fingerprintHelper
|
||||
) : BackgroundService
|
||||
{
|
||||
protected override Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
handlerHelper.InitAllHandlers();
|
||||
logger.LogInformation("MyTelegram auth server started");
|
||||
const long defaultFingerprint = -3591632762792723036;
|
||||
var fingerprint = fingerprintHelper.GetFingerprint();
|
||||
if (fingerprint == defaultFingerprint)
|
||||
{
|
||||
logger.LogWarning("You are currently using the default private key, which anyone can obtain from the mytelegram open source project. For security reasons, please use your own private key and replace the client's public key.");
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
# See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
|
||||
|
||||
# This stage is used when running from VS in fast mode (Default for Debug configuration)
|
||||
FROM mcr.microsoft.com/dotnet/runtime:9.0 AS base
|
||||
USER $APP_UID
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
# This stage is used to build the service project
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
COPY ["Directory.Packages.props", "."]
|
||||
COPY ["Directory.Build.props", "."]
|
||||
COPY ["nuget.config", "."]
|
||||
COPY ["src/MyTelegram.AuthServer/MyTelegram.AuthServer.csproj", "src/MyTelegram.AuthServer/"]
|
||||
COPY ["src/MyTelegram.Caching.Redis/MyTelegram.Caching.Redis.csproj", "src/MyTelegram.Caching.Redis/"]
|
||||
COPY ["src/MyTelegram.Core/MyTelegram.Core.csproj", "src/MyTelegram.Core/"]
|
||||
COPY ["src/MyTelegram.Abstractions/MyTelegram.Abstractions.csproj", "src/MyTelegram.Abstractions/"]
|
||||
COPY ["src/MyTelegram.Domain.Shared/MyTelegram.Domain.Shared.csproj", "src/MyTelegram.Domain.Shared/"]
|
||||
COPY ["src/MyTelegram.Schema/MyTelegram.Schema.csproj", "src/MyTelegram.Schema/"]
|
||||
COPY ["src/MyTelegram.EventBus.Rebus/MyTelegram.EventBus.Rebus.csproj", "src/MyTelegram.EventBus.Rebus/"]
|
||||
COPY ["src/MyTelegram.EventBus/MyTelegram.EventBus.csproj", "src/MyTelegram.EventBus/"]
|
||||
COPY ["src/MyTelegram.Services/MyTelegram.Services.csproj", "src/MyTelegram.Services/"]
|
||||
COPY ["src/MyTelegram.EventFlow/MyTelegram.EventFlow.csproj", "src/MyTelegram.EventFlow/"]
|
||||
RUN dotnet restore "./src/MyTelegram.AuthServer/MyTelegram.AuthServer.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/src/MyTelegram.AuthServer"
|
||||
RUN dotnet build "./MyTelegram.AuthServer.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
|
||||
# This stage is used to publish the service project to be copied to the final stage
|
||||
FROM build AS publish
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
RUN dotnet publish "./MyTelegram.AuthServer.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
|
||||
# This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration)
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "MyTelegram.AuthServer.dll"]
|
||||
@@ -0,0 +1,92 @@
|
||||
namespace MyTelegram.AuthServer.EventHandlers;
|
||||
|
||||
public class UnencryptedMessageHandler(
|
||||
ILogger<UnencryptedMessageHandler> logger,
|
||||
IHandlerHelper handlerHelper,
|
||||
IEventBus eventBus
|
||||
) : IEventHandler<UnencryptedMessage>, ITransientDependency
|
||||
{
|
||||
public async Task HandleEventAsync(UnencryptedMessage eventData)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!handlerHelper.TryGetHandler(eventData.ObjectId, out var handler))
|
||||
{
|
||||
logger.LogWarning(
|
||||
"Cannot find a handler with objectId {ObjectId:x2}, connectionId: {ConnectionId}",
|
||||
eventData.ObjectId,
|
||||
eventData.ConnectionId
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (logger.IsEnabled(LogLevel.Trace))
|
||||
{
|
||||
logger.LogTrace(
|
||||
"Processing unencrypted message, connectionId: {ConnectionId}, [objectId]: {ObjectId:x2}, handler: {Handler}, reqMsgId: {ReqMsgId}",
|
||||
eventData.ConnectionId,
|
||||
eventData.ObjectId,
|
||||
handler.GetType().Name,
|
||||
eventData.MessageId
|
||||
);
|
||||
}
|
||||
|
||||
var obj = eventData.MessageData.ToTObject<IObject>();
|
||||
|
||||
var r = await handler.HandleAsync(
|
||||
new RequestInput(
|
||||
eventData.ConnectionId,
|
||||
eventData.RequestId,
|
||||
eventData.ObjectId,
|
||||
eventData.MessageId,
|
||||
0,
|
||||
0,
|
||||
eventData.AuthKeyId,
|
||||
eventData.AuthKeyId,
|
||||
0,
|
||||
eventData.Date,
|
||||
DeviceType.Unknown,
|
||||
eventData.ClientIp,
|
||||
0,
|
||||
0
|
||||
),
|
||||
obj
|
||||
);
|
||||
|
||||
if (r != null!)
|
||||
{
|
||||
using var writer = new ArrayPoolBufferWriter<byte>();
|
||||
r.Serialize(writer);
|
||||
var unencryptedResponse = new UnencryptedMessageResponse(
|
||||
eventData.AuthKeyId,
|
||||
writer.WrittenMemory,
|
||||
eventData.ConnectionId,
|
||||
eventData.MessageId
|
||||
);
|
||||
await eventBus.PublishAsync(unencryptedResponse);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(
|
||||
"Process request failed, connectionId: {ConnectionId}, reqMsgId: {ReqMsgId}, error: {Error}",
|
||||
eventData.ConnectionId,
|
||||
eventData.MessageId,
|
||||
ex
|
||||
);
|
||||
}
|
||||
finally
|
||||
{
|
||||
eventData.MemoryOwner?.Dispose();
|
||||
}
|
||||
|
||||
if (logger.IsEnabled(LogLevel.Trace))
|
||||
{
|
||||
logger.LogTrace(
|
||||
"Process unencrypted message completed, reqMsgId: {ReqMsgId}",
|
||||
eventData.MessageId
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using MyTelegram.EventBus.Extensions;
|
||||
|
||||
namespace MyTelegram.AuthServer.Extensions;
|
||||
|
||||
public static class MyTelegramAuthServerExtensions
|
||||
{
|
||||
public static IServiceCollection AddAuthServer(this IServiceCollection services)
|
||||
{
|
||||
services.RegisterServices();
|
||||
|
||||
services.AddMyTelegramHandlerServices();
|
||||
|
||||
services.AddEventHandlers();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static void AddEventHandlers(this IServiceCollection services)
|
||||
{
|
||||
services.AddSubscription<UnencryptedMessage, UnencryptedMessageHandler>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||
<ConfigureAwait />
|
||||
</Weavers>
|
||||
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
|
||||
<xs:element name="Weavers">
|
||||
<xs:complexType>
|
||||
<xs:all>
|
||||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" />
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
<xs:attribute name="VerifyAssembly" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="GenerateXsd" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:schema>
|
||||
@@ -0,0 +1,25 @@
|
||||
global using Microsoft.Extensions.Configuration;
|
||||
global using Microsoft.Extensions.DependencyInjection;
|
||||
global using Microsoft.Extensions.Hosting;
|
||||
global using Microsoft.Extensions.Logging;
|
||||
global using Microsoft.Extensions.Options;
|
||||
global using MyTelegram.Abstractions;
|
||||
global using MyTelegram.AuthServer;
|
||||
global using MyTelegram.AuthServer.BackgroundServices;
|
||||
global using MyTelegram.AuthServer.EventHandlers;
|
||||
global using MyTelegram.AuthServer.Extensions;
|
||||
global using MyTelegram.AuthServer.Services;
|
||||
global using MyTelegram.Caching.Redis;
|
||||
global using MyTelegram.Core;
|
||||
global using MyTelegram.EventBus;
|
||||
global using MyTelegram.EventBus.RabbitMQ;
|
||||
global using MyTelegram.Schema;
|
||||
global using MyTelegram.Schema.Extensions;
|
||||
global using MyTelegram.Services.Extensions;
|
||||
global using MyTelegram.Services.NativeAot;
|
||||
global using MyTelegram.Services.Services;
|
||||
global using Serilog;
|
||||
global using Serilog.Sinks.SystemConsole.Themes;
|
||||
global using System.Buffers;
|
||||
global using System.Numerics;
|
||||
global using System.Security.Cryptography;
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace MyTelegram.AuthServer.Handlers;
|
||||
|
||||
public interface IMsgsAckHandler : IObjectHandler
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace MyTelegram.AuthServer.Handlers;
|
||||
|
||||
public interface IReqDhParamsHandler : IObjectHandler
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace MyTelegram.AuthServer.Handlers;
|
||||
|
||||
public interface IReqPqHandler : IObjectHandler
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace MyTelegram.AuthServer.Handlers;
|
||||
|
||||
public interface IReqPqMultiHandler : IObjectHandler
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace MyTelegram.AuthServer.Handlers;
|
||||
|
||||
public interface ISetClientDhParamsHandler : IObjectHandler
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace MyTelegram.AuthServer.Handlers;
|
||||
|
||||
public class MsgsAckHandler : BaseObjectHandler<TMsgsAck, IObject>, IMsgsAckHandler
|
||||
{
|
||||
protected override Task<IObject> HandleCoreAsync(IRequestInput input, TMsgsAck obj)
|
||||
{
|
||||
return Task.FromResult<IObject>(null!);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace MyTelegram.AuthServer.Handlers;
|
||||
|
||||
public class ReqDhParamsHandler(IStep2Helper step2ServerHelper, ILogger<ReqDhParamsHandler> logger)
|
||||
: BaseObjectHandler<RequestReqDHParams, IServerDHParams>,
|
||||
IReqDhParamsHandler
|
||||
{
|
||||
protected override async Task<IServerDHParams> HandleCoreAsync(
|
||||
IRequestInput input,
|
||||
RequestReqDHParams obj
|
||||
)
|
||||
{
|
||||
var dto = await step2ServerHelper.GetServerDhParamsAsync(obj);
|
||||
logger.LogInformation(
|
||||
"[Step2] ReqDhParamsHandler, connectionId: {ConnectionId}, reqMsgId: {ReqMsgId}",
|
||||
input.ConnectionId,
|
||||
input.ReqMsgId
|
||||
);
|
||||
|
||||
return dto.ServerDhParams;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace MyTelegram.AuthServer.Handlers;
|
||||
|
||||
public class ReqPqHandler(
|
||||
IStep1Helper step1ServerHelper,
|
||||
ILogger<ReqPqHandler> logger,
|
||||
ICacheManager<AuthCacheItem> cacheManager
|
||||
) : BaseObjectHandler<RequestReqPq, IResPQ>, IReqPqHandler
|
||||
{
|
||||
protected override async Task<IResPQ> HandleCoreAsync(IRequestInput input, RequestReqPq obj)
|
||||
{
|
||||
var dto = step1ServerHelper.GetResponse(obj.Nonce);
|
||||
var authCacheItem = new AuthCacheItem(obj.Nonce, dto.ServerNonce, dto.P, dto.Q, false);
|
||||
|
||||
var key = AuthCacheItem.GetCacheKey(dto.ServerNonce);
|
||||
|
||||
await cacheManager.SetAsync(
|
||||
key,
|
||||
authCacheItem,
|
||||
MyTelegramConsts.AuthKeyExpireSeconds
|
||||
);
|
||||
logger.LogInformation(
|
||||
"[Step1] ReqPqHandler, connectionId: {ConnectionId}, reqMsgId: {ReqMsgId}",
|
||||
input.ConnectionId,
|
||||
input.ReqMsgId
|
||||
);
|
||||
|
||||
return dto.ResPq;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace MyTelegram.AuthServer.Handlers;
|
||||
|
||||
public class ReqPqMultiHandler(
|
||||
IStep1Helper step1ServerHelper,
|
||||
ILogger<ReqPqMultiHandler> logger,
|
||||
ICacheManager<AuthCacheItem> cacheManager
|
||||
) : BaseObjectHandler<RequestReqPqMulti, IResPQ>, IReqPqMultiHandler
|
||||
{
|
||||
protected override async Task<IResPQ> HandleCoreAsync(
|
||||
IRequestInput input,
|
||||
RequestReqPqMulti obj
|
||||
)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
var dto = step1ServerHelper.GetResponse(obj.Nonce);
|
||||
|
||||
var authCacheItem = new AuthCacheItem(obj.Nonce, dto.ServerNonce, dto.P, dto.Q, false);
|
||||
var key = AuthCacheItem.GetCacheKey(dto.ServerNonce);
|
||||
await cacheManager.SetAsync(
|
||||
key,
|
||||
authCacheItem,
|
||||
MyTelegramConsts.AuthKeyExpireSeconds
|
||||
);
|
||||
sw.Stop();
|
||||
logger.LogInformation(
|
||||
"[Step1] ReqPqMultiHandler, connectionId={ConnectionId}, nonce: {Nonce} reqMsgId: {ReqMsgId}, authKeyId: {AuthKeyId} {TimeSpan}ms",
|
||||
input.ConnectionId,
|
||||
obj.Nonce.ToHexString(),
|
||||
input.ReqMsgId,
|
||||
input.AuthKeyId,
|
||||
sw.Elapsed.TotalMilliseconds
|
||||
);
|
||||
|
||||
return dto.ResPq;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
namespace MyTelegram.AuthServer.Handlers;
|
||||
|
||||
public class SetClientDhParamsHandler(
|
||||
IStep3Helper step3ServerHelper,
|
||||
ILogger<SetClientDhParamsHandler> logger,
|
||||
ICacheManager<AuthKeyCacheItem> cacheManager,
|
||||
IEventBus eventBus
|
||||
) : BaseObjectHandler<RequestSetClientDHParams, ISetClientDHParamsAnswer>, ISetClientDhParamsHandler
|
||||
{
|
||||
protected override async Task<ISetClientDHParamsAnswer> HandleCoreAsync(
|
||||
IRequestInput input,
|
||||
RequestSetClientDHParams obj
|
||||
)
|
||||
{
|
||||
var dto = await step3ServerHelper.SetClientDhParamsAnswerAsync(obj);
|
||||
logger.LogInformation(
|
||||
"[Step3] [{IsPerm}] authKey created successfully, connectionId: {ConnectionId}, authKeyId: {AuthKeyId:x2}, reqMsgId: {ReqMsgId}",
|
||||
input.ConnectionId,
|
||||
dto.IsPermanent ? "Perm" : "Temp",
|
||||
dto.AuthKeyId,
|
||||
input.ReqMsgId
|
||||
);
|
||||
|
||||
// Cached authentication data expires in 120 seconds
|
||||
var cacheKey = AuthKeyCacheItem.GetCacheKey(dto.AuthKeyId);
|
||||
await cacheManager.SetAsync(
|
||||
cacheKey,
|
||||
new AuthKeyCacheItem(dto.AuthKey, dto.ServerSalt, dto.IsPermanent),
|
||||
120
|
||||
);
|
||||
await eventBus.PublishAsync(
|
||||
new AuthKeyCreatedIntegrationEvent(
|
||||
input.ConnectionId,
|
||||
input.ReqMsgId,
|
||||
dto.AuthKey,
|
||||
dto.ServerSalt,
|
||||
dto.IsPermanent,
|
||||
dto.SetClientDhParamsAnswer.ToBytes(),
|
||||
dto.DcId
|
||||
)
|
||||
);
|
||||
|
||||
// The session server will send SetClientDhParamsAnswer to client if the perm auth key created on session server
|
||||
if (!dto.IsPermanent)
|
||||
{
|
||||
return dto.SetClientDhParamsAnswer;
|
||||
}
|
||||
|
||||
return null!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<Title>MyTelegram auth server</Title>
|
||||
<Description>MyTelegram auth server</Description>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<DockerfileContext>..\..</DockerfileContext>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TrimmerRemoveSymbols>true</TrimmerRemoveSymbols>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<TrimmableAssembly Include="HashedWheelTimer" />
|
||||
<TrimmableAssembly Include="SharpZipLib" />
|
||||
<TrimmableAssembly Include="MyTelegram.Schema" />
|
||||
<TrimmableAssembly Include="MyTelegram.Core" />
|
||||
<TrimmableAssembly Include="EventFlow" />
|
||||
<TrimmableAssembly Include="MyTelegram.Domain.Shared" />
|
||||
<TrimmableAssembly Include="MyTelegram.Services" />
|
||||
<TrimmableAssembly Include="System.Private.CoreLib" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<TrimmerRootDescriptor Include="TrimmerRoots.xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<RdXmlFile Include="rd.xml" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="appsettings.Development.json" />
|
||||
<None Remove="appsettings.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="appsettings.Development.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
</Content>
|
||||
<Content Include="appsettings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" />
|
||||
<PackageReference Include="Serilog.Sinks.Async" />
|
||||
<PackageReference Include="Serilog.Sinks.File" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" />
|
||||
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MyTelegram.Caching.Redis\MyTelegram.Caching.Redis.csproj" />
|
||||
<ProjectReference Include="..\MyTelegram.EventBus.RabbitMQ\MyTelegram.EventBus.RabbitMQ.csproj" />
|
||||
<!--<ProjectReference Include="..\MyTelegram.EventBus.RabbitMQ\MyTelegram.EventBus.RabbitMQ.csproj" />-->
|
||||
<ProjectReference Include="..\MyTelegram.Services\MyTelegram.Services.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="private.pkcs8.key">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Update="Fody">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace MyTelegram.AuthServer;
|
||||
|
||||
public class MyTelegramAuthServerOptions
|
||||
{
|
||||
public string PrivateKeyFilePath { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
using MyTelegram.EventBus.RabbitMQ.Extensions;
|
||||
using MyTelegramConsts = MyTelegram.MyTelegramConsts;
|
||||
|
||||
Console.Title = "MyTelegram auth server";
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.Enrich.FromLogContext()
|
||||
.WriteTo.Async(c => c.Console(theme: AnsiConsoleTheme.Code))
|
||||
.WriteTo.Async(c => c.File("Logs/startup-log.txt"))
|
||||
.CreateLogger();
|
||||
|
||||
Log.Information(
|
||||
"{Info} {Version}",
|
||||
"MyTelegram Auth Server",
|
||||
typeof(Program).Assembly.GetName().Version
|
||||
);
|
||||
Log.Information(
|
||||
"{Description} {Url}",
|
||||
"For more information, please visit",
|
||||
MyTelegramConsts.RepositoryUrl
|
||||
);
|
||||
|
||||
Log.Information("MyTelegram authentication server starting...");
|
||||
|
||||
//Console.ReadLine();
|
||||
var builder = Host.CreateDefaultBuilder(args);
|
||||
builder.ConfigureAppConfiguration(options =>
|
||||
{
|
||||
options.AddEnvironmentVariables();
|
||||
options.AddCommandLine(args);
|
||||
});
|
||||
|
||||
builder.UseSerilog(
|
||||
(context, configuration) => { configuration.ReadFrom.Configuration(context.Configuration); }
|
||||
);
|
||||
builder.ConfigureServices(
|
||||
(context, services) =>
|
||||
{
|
||||
services.Configure<MyTelegramAuthServerOptions>(
|
||||
context.Configuration.GetRequiredSection("App")
|
||||
);
|
||||
services.Configure<EventBusRabbitMqOptions>(
|
||||
context.Configuration.GetRequiredSection("RabbitMQ:EventBus")
|
||||
);
|
||||
services.Configure<RabbitMqOptions>(
|
||||
context.Configuration.GetRequiredSection("RabbitMQ:Connections:Default")
|
||||
);
|
||||
services.AddHostedService<MyTelegramAuthServerBackgroundService>();
|
||||
services.AddAuthServer();
|
||||
services.AddMyTelegramStackExchangeRedisCache(options =>
|
||||
{
|
||||
options.Configuration = context.Configuration.GetValue<string>("Redis:Configuration");
|
||||
});
|
||||
services.AddCacheJsonSerializer(options =>
|
||||
{
|
||||
options.TypeInfoResolverChain.Add(MyJsonSerializeContext.Default);
|
||||
});
|
||||
|
||||
services.AddMyTelegramRabbitMqEventBus();
|
||||
|
||||
//services.AddRebusEventBus(options =>
|
||||
//{
|
||||
// var eventBusOptions = context
|
||||
// .Configuration.GetRequiredSection("RabbitMQ:EventBus")
|
||||
// .Get<EventBusRabbitMqOptions>();
|
||||
// var rabbitMqOptions = context
|
||||
// .Configuration.GetRequiredSection("RabbitMQ:Connections:Default")
|
||||
// .Get<RabbitMqOptions>();
|
||||
|
||||
// options.Transport(t =>
|
||||
// {
|
||||
// t.UseRabbitMq(
|
||||
// $"amqp://{rabbitMqOptions!.UserName}:{rabbitMqOptions.Password}@{rabbitMqOptions.HostName}:{rabbitMqOptions.Port}",
|
||||
// eventBusOptions!.ClientName
|
||||
// )
|
||||
// .ExchangeNames(
|
||||
// eventBusOptions.ExchangeName,
|
||||
// eventBusOptions.TopicExchangeName ?? "RebusTopics"
|
||||
// );
|
||||
// });
|
||||
// options.AddSystemTextJson(jsonOptions =>
|
||||
// {
|
||||
// jsonOptions.TypeInfoResolverChain.Add(MyJsonSerializeContext.Default);
|
||||
// });
|
||||
//});
|
||||
}
|
||||
);
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
|
||||
await app.RunAsync();
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:52769/",
|
||||
"sslPort": 44398
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"MyTelegram.AuthServer": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "https://localhost:5001;http://localhost:5000"
|
||||
},
|
||||
"Docker": {
|
||||
"commandName": "Docker"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace MyTelegram.AuthServer.Services;
|
||||
|
||||
public class FingerprintHelper(IRsaKeyProvider rsaKeyProvider, IMyRsaHelper rsaHelper)
|
||||
: IFingerprintHelper,
|
||||
ISingletonDependency
|
||||
{
|
||||
private long _fingerprint;
|
||||
|
||||
public long GetFingerprint()
|
||||
{
|
||||
if (_fingerprint == 0)
|
||||
{
|
||||
_fingerprint = rsaHelper.GetFingerprintFromPrivateKey(
|
||||
rsaKeyProvider.GetRsaPrivateKey()
|
||||
);
|
||||
}
|
||||
|
||||
return _fingerprint;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace MyTelegram.AuthServer.Services;
|
||||
|
||||
public interface IFingerprintHelper
|
||||
{
|
||||
long GetFingerprint();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace MyTelegram.AuthServer.Services;
|
||||
|
||||
public interface IStep1Helper
|
||||
{
|
||||
Step1Output GetResponse(byte[] nonce);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace MyTelegram.AuthServer.Services;
|
||||
|
||||
public interface IStep2Helper
|
||||
{
|
||||
Task<Step2Output> GetServerDhParamsAsync(RequestReqDHParams req);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace MyTelegram.AuthServer.Services;
|
||||
|
||||
public interface IStep3Helper
|
||||
{
|
||||
Task<Step3Output> SetClientDhParamsAnswerAsync(RequestSetClientDHParams req);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
namespace MyTelegram.AuthServer.Services;
|
||||
|
||||
public class RsaKeyProvider(IOptions<MyTelegramAuthServerOptions> options)
|
||||
: IRsaKeyProvider,
|
||||
ISingletonDependency
|
||||
{
|
||||
private string? _privateKey;
|
||||
|
||||
public string GetRsaPrivateKey()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_privateKey))
|
||||
{
|
||||
return _privateKey;
|
||||
}
|
||||
|
||||
if (!File.Exists(options.Value.PrivateKeyFilePath))
|
||||
{
|
||||
throw new FileNotFoundException(
|
||||
"Private key not exists",
|
||||
options.Value.PrivateKeyFilePath
|
||||
);
|
||||
}
|
||||
|
||||
_privateKey = File.ReadAllText(options.Value.PrivateKeyFilePath);
|
||||
|
||||
if (string.IsNullOrEmpty(_privateKey))
|
||||
{
|
||||
throw new InvalidOperationException("Private key can not be null");
|
||||
}
|
||||
|
||||
return _privateKey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace MyTelegram.AuthServer.Services;
|
||||
|
||||
public class Step1Helper(IFingerprintHelper fingerprintHelper) : IStep1Helper, ISingletonDependency
|
||||
{
|
||||
public Step1Output GetResponse(byte[] nonce)
|
||||
{
|
||||
var p = AuthConsts.P;
|
||||
var q = AuthConsts.Q;
|
||||
|
||||
var serverNonce = RandomNumberGenerator.GetBytes(16);
|
||||
|
||||
var publicKeyFingerprint = fingerprintHelper.GetFingerprint();
|
||||
var pq = AuthConsts.Pq;
|
||||
var resPq = new TResPQ
|
||||
{
|
||||
Nonce = nonce,
|
||||
ServerNonce = serverNonce,
|
||||
Pq = pq,
|
||||
ServerPublicKeyFingerprints = new TVector<long> { publicKeyFingerprint }
|
||||
};
|
||||
|
||||
return new Step1Output(p, q, serverNonce, resPq);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace MyTelegram.AuthServer.Services;
|
||||
|
||||
public record Step1Output(byte[] P, byte[] Q, byte[] ServerNonce, TResPQ ResPq);
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace MyTelegram.AuthServer.Services;
|
||||
|
||||
public class Step1To3Helper
|
||||
{
|
||||
private static readonly BigInteger TwoPowOf2048Sub64 = BigInteger.Pow(2, 2048 - 64);
|
||||
|
||||
protected void CheckRequestData(
|
||||
Span<byte> expected,
|
||||
Span<byte> actual,
|
||||
[CallerArgumentExpression(nameof(actual))]
|
||||
string? message = null
|
||||
)
|
||||
{
|
||||
if (!expected.SequenceEqual(actual))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"Invalid {message}, expected: {expected.ToHexString()} actual: {actual.ToHexString()}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected string GetAuthCacheKey(byte[] serverNonce)
|
||||
{
|
||||
return AuthCacheItem.GetCacheKey(serverNonce);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// https://corefork.telegram.org/mtproto/auth_key#6-server-responds-with
|
||||
/// </summary>
|
||||
/// <param name="gaOrGb"></param>
|
||||
/// <param name="dhPrime"></param>
|
||||
/// <returns></returns>
|
||||
protected bool IsGoodGaOrGb(BigInteger gaOrGb, BigInteger dhPrime)
|
||||
{
|
||||
var dhPrimeSubTowPowOf2048Sub64 = dhPrime - TwoPowOf2048Sub64;
|
||||
var isGoodGaOrGb =
|
||||
gaOrGb > 1
|
||||
&& gaOrGb < dhPrime - 1
|
||||
&& gaOrGb > TwoPowOf2048Sub64
|
||||
&& gaOrGb < dhPrimeSubTowPowOf2048Sub64;
|
||||
|
||||
return isGoodGaOrGb;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
namespace MyTelegram.AuthServer.Services;
|
||||
|
||||
public class Step2Helper(
|
||||
ILogger<Step2Helper> logger,
|
||||
IAesHelper aesHelper,
|
||||
IMtpHelper mtpHelper,
|
||||
IMyRsaHelper myRsaHelper,
|
||||
ICacheManager<AuthCacheItem> cacheManager,
|
||||
IRsaKeyProvider rsaKeyProvider
|
||||
) : Step1To3Helper, IStep2Helper, ISingletonDependency
|
||||
{
|
||||
public async Task<Step2Output> GetServerDhParamsAsync(RequestReqDHParams req)
|
||||
{
|
||||
var cacheKey = GetAuthCacheKey(req.ServerNonce);
|
||||
var cachedAuthKey = await cacheManager.GetAsync(cacheKey);
|
||||
if (cachedAuthKey == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"GetServerDhParamsAsync: can not find cached auth key info, nonce={req.Nonce.ToHexString()}"
|
||||
);
|
||||
}
|
||||
|
||||
#region check request
|
||||
|
||||
CheckRequestData(cachedAuthKey.Nonce, req.Nonce);
|
||||
CheckRequestData(cachedAuthKey.ServerNonce, req.ServerNonce);
|
||||
CheckRequestData(cachedAuthKey.P, req.P);
|
||||
CheckRequestData(cachedAuthKey.Q, req.Q);
|
||||
|
||||
var tInnerData = DeserializeRequestTpqInnerData(req, rsaKeyProvider.GetRsaPrivateKey());
|
||||
CheckRequestData(cachedAuthKey.P, tInnerData.P);
|
||||
CheckRequestData(cachedAuthKey.Q, tInnerData.Q);
|
||||
CheckRequestData(cachedAuthKey.ServerNonce, tInnerData.ServerNonce);
|
||||
CheckRequestData(cachedAuthKey.Nonce, tInnerData.Nonce);
|
||||
|
||||
#endregion check request
|
||||
|
||||
var isPermanentAuthKey = false;
|
||||
int? dcId = null;
|
||||
switch (tInnerData)
|
||||
{
|
||||
case TPQInnerData:
|
||||
isPermanentAuthKey = true;
|
||||
break;
|
||||
case TPQInnerDataDc:
|
||||
isPermanentAuthKey = true;
|
||||
break;
|
||||
case TPQInnerDataTemp:
|
||||
|
||||
break;
|
||||
case TPQInnerDataTempDc pqInnerDataTempDc:
|
||||
dcId = pqInnerDataTempDc.Dc;
|
||||
break;
|
||||
}
|
||||
|
||||
var dh2048P = AuthConsts.Dh2048P;
|
||||
var g = AuthConsts.G;
|
||||
var aAndGa = GenerateAAndGa();
|
||||
|
||||
var newCachedAuthKey = cachedAuthKey with
|
||||
{
|
||||
IsPermanent = isPermanentAuthKey,
|
||||
NewNonce = tInnerData.NewNonce,
|
||||
A = aAndGa.a,
|
||||
Ga = aAndGa.ga,
|
||||
DcId = dcId
|
||||
};
|
||||
|
||||
var serverDhInnerData = new TServerDHInnerData
|
||||
{
|
||||
DhPrime = dh2048P,
|
||||
G = g[0],
|
||||
GA = aAndGa.ga,
|
||||
Nonce = cachedAuthKey.Nonce,
|
||||
ServerNonce = cachedAuthKey.ServerNonce,
|
||||
ServerTime = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds()
|
||||
};
|
||||
|
||||
await cacheManager.SetAsync(cacheKey, newCachedAuthKey, 600);
|
||||
|
||||
var serverDhParams = SerializeResponse(tInnerData, serverDhInnerData);
|
||||
|
||||
return new Step2Output(tInnerData.NewNonce, serverDhParams);
|
||||
}
|
||||
|
||||
private IPQInnerData DeserializeRequestTpqInnerData(
|
||||
RequestReqDHParams reqDhParams,
|
||||
string privateKey
|
||||
)
|
||||
{
|
||||
// It needs to be converted into a 256-byte array.
|
||||
// sometimes the auth key data length is only 255, and 0 needs to be added to the first position.
|
||||
var innerDataWithHash = myRsaHelper.Decrypt(reqDhParams.EncryptedData, privateKey);
|
||||
if (innerDataWithHash.Length == 256)
|
||||
{
|
||||
return ParsePqInnerData(innerDataWithHash);
|
||||
}
|
||||
|
||||
return ParsePqInnerDataOld(innerDataWithHash);
|
||||
}
|
||||
|
||||
private IPQInnerData ParsePqInnerDataOld(byte[] innerDataWithHash)
|
||||
{
|
||||
var span = innerDataWithHash.AsSpan();
|
||||
var shaHash = span[..20];
|
||||
var innerData = span[20..];
|
||||
ReadOnlyMemory<byte> buffer = innerDataWithHash.AsMemory(20, innerDataWithHash.Length - 20);
|
||||
var oldLength = buffer.Length;
|
||||
var tPqInnerData = buffer.Read<IPQInnerData>();
|
||||
var length = oldLength - buffer.Length;
|
||||
var realInnerData = innerData[..length];
|
||||
|
||||
Span<byte> calcHash = stackalloc byte[20];
|
||||
SHA1.HashData(realInnerData, calcHash);
|
||||
if (!shaHash.SequenceEqual(calcHash))
|
||||
{
|
||||
logger.LogWarning("PQInnerData SHA1 hash mismatch");
|
||||
}
|
||||
|
||||
return tPqInnerData;
|
||||
}
|
||||
|
||||
private (byte[] a, byte[] ga) GenerateAAndGa()
|
||||
{
|
||||
var g = AuthConsts.G.ToBigEndianBigInteger();
|
||||
var dhPrime = AuthConsts.DhPrime;
|
||||
while (true)
|
||||
{
|
||||
var aBytes = RandomNumberGenerator.GetBytes(256);
|
||||
var a = aBytes.ToBigEndianBigInteger();
|
||||
|
||||
var ga = BigInteger.ModPow(g, a, dhPrime);
|
||||
if (IsGoodGaOrGb(ga, dhPrime))
|
||||
{
|
||||
return (aBytes, ga.ToByteArray(true, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IPQInnerData ParsePqInnerData(ReadOnlySpan<byte> keyAesEncryptedBytes)
|
||||
{
|
||||
const int tempKeyLength = 32;
|
||||
var tempBytes = ArrayPool<byte>.Shared.Rent(keyAesEncryptedBytes.Length + 32 + 32);
|
||||
|
||||
try
|
||||
{
|
||||
var tempSpan = tempBytes.AsSpan(0, keyAesEncryptedBytes.Length + 32 + 32 + 32);
|
||||
var startIndex = keyAesEncryptedBytes.Length - tempKeyLength;
|
||||
var dataWithHash = tempSpan[..(keyAesEncryptedBytes.Length - tempKeyLength)];
|
||||
|
||||
var aesEncryptedSha256Hash = tempSpan.Slice(startIndex, 32);
|
||||
var calculatedHash = tempSpan.Slice(startIndex + 32, 32);
|
||||
var aesEncrypted = keyAesEncryptedBytes[tempKeyLength..];
|
||||
|
||||
var tempKeyXor = keyAesEncryptedBytes[..tempKeyLength];
|
||||
SHA256.HashData(aesEncrypted, aesEncryptedSha256Hash);
|
||||
var tempKey = Xor(tempKeyXor, aesEncryptedSha256Hash);
|
||||
Span<byte> tempIv1 = stackalloc byte[32];
|
||||
aesHelper.DecryptIge(aesEncrypted, tempKey, tempIv1, dataWithHash);
|
||||
|
||||
var dataPaddingReversed = dataWithHash[..^32];
|
||||
var hash = dataWithHash[^32..];
|
||||
dataPaddingReversed.Reverse();
|
||||
var dataWithPadding = dataPaddingReversed;
|
||||
using var hasher = IncrementalHash.CreateHash(HashAlgorithmName.SHA256);
|
||||
hasher.AppendData(tempKey);
|
||||
hasher.AppendData(dataWithPadding);
|
||||
hasher.GetHashAndReset(calculatedHash);
|
||||
|
||||
if (!hash.SequenceEqual(calculatedHash))
|
||||
{
|
||||
logger.LogWarning("PQInnerData hash mismatch");
|
||||
|
||||
throw new ArgumentException("PQInnerData hash mismatch");
|
||||
}
|
||||
|
||||
var tPqInnerData = tempBytes.ToTObject<IPQInnerData>();
|
||||
|
||||
return tPqInnerData;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(tempBytes);
|
||||
}
|
||||
}
|
||||
|
||||
private TServerDHParamsOk SerializeResponse(
|
||||
IPQInnerData pqInnerData,
|
||||
TServerDHInnerData dhInnerData
|
||||
)
|
||||
{
|
||||
return SerializeResponse(
|
||||
pqInnerData.Nonce,
|
||||
pqInnerData.NewNonce,
|
||||
pqInnerData.ServerNonce,
|
||||
dhInnerData
|
||||
);
|
||||
}
|
||||
|
||||
private TServerDHParamsOk SerializeResponse(
|
||||
byte[] nonce,
|
||||
byte[] newNonce,
|
||||
byte[] serverNonce,
|
||||
TServerDHInnerData dhInnerData
|
||||
)
|
||||
{
|
||||
using var writer = new ArrayPoolBufferWriter<byte>();
|
||||
dhInnerData.Serialize(writer);
|
||||
|
||||
var writtenCount = writer.WrittenCount;
|
||||
var totalLength = writtenCount + 20;// 20=SHA1 hash length
|
||||
var tempBytes = ArrayPool<byte>.Shared.Rent(totalLength + 32 + 16);
|
||||
var tempSpan = tempBytes.AsSpan();
|
||||
try
|
||||
{
|
||||
var sha1Hash = tempSpan.Slice(0, 20);
|
||||
var answerWithHashLength = writtenCount + 20;
|
||||
if (answerWithHashLength % 16 != 0)
|
||||
{
|
||||
answerWithHashLength += 16 - (answerWithHashLength % 16);
|
||||
}
|
||||
var answerWithHashSpan = tempSpan.Slice(0, answerWithHashLength);
|
||||
SHA1.HashData(writer.WrittenSpan, sha1Hash);
|
||||
sha1Hash.CopyTo(answerWithHashSpan);
|
||||
writer.WrittenSpan.CopyTo(answerWithHashSpan.Slice(20));
|
||||
var aesKey = new byte[32];
|
||||
Span<byte> aesIv = stackalloc byte[32];
|
||||
mtpHelper.CalcTempAesKeyData(newNonce, serverNonce, aesKey, aesIv);
|
||||
|
||||
aesHelper.EncryptIge(answerWithHashSpan, aesKey, aesIv, answerWithHashSpan);
|
||||
|
||||
return new TServerDHParamsOk
|
||||
{
|
||||
EncryptedAnswer = answerWithHashSpan.ToArray(),
|
||||
Nonce = nonce,
|
||||
ServerNonce = serverNonce
|
||||
};
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(tempBytes);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] Xor(ReadOnlySpan<byte> src, ReadOnlySpan<byte> dest)
|
||||
{
|
||||
var bytes = new byte[src.Length];
|
||||
for (var i = 0; i < src.Length; i++)
|
||||
{
|
||||
bytes[i] = (byte)(src[i] ^ dest[i]);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace MyTelegram.AuthServer.Services;
|
||||
|
||||
public record Step2Output(byte[] NewNonce, IServerDHParams ServerDhParams);
|
||||
@@ -0,0 +1,151 @@
|
||||
namespace MyTelegram.AuthServer.Services;
|
||||
|
||||
public class Step3Helper(
|
||||
IAesHelper aesHelper,
|
||||
IHashHelper hashHelper,
|
||||
IMtpHelper mtpHelper,
|
||||
ILogger<Step3Helper> logger,
|
||||
IAuthKeyIdHelper authKeyIdHelper,
|
||||
ICacheManager<AuthCacheItem> cacheManager
|
||||
) : Step1To3Helper, IStep3Helper, ISingletonDependency
|
||||
{
|
||||
public async Task<Step3Output> SetClientDhParamsAnswerAsync(RequestSetClientDHParams req)
|
||||
{
|
||||
var cacheKey = GetAuthCacheKey(req.ServerNonce);
|
||||
var cachedAuthKey = await cacheManager.GetAsync(cacheKey);
|
||||
if (cachedAuthKey?.A == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Cannot find cached auth key info, nonce: {req.Nonce.ToHexString()}"
|
||||
);
|
||||
}
|
||||
|
||||
if (cachedAuthKey.NewNonce == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(cachedAuthKey.NewNonce));
|
||||
}
|
||||
|
||||
CheckRequestData(cachedAuthKey.Nonce, req.Nonce, "Nonce");
|
||||
CheckRequestData(cachedAuthKey.ServerNonce, req.ServerNonce, "ServerNonce");
|
||||
|
||||
var aesKey = new byte[32];
|
||||
Span<byte> aesIv = stackalloc byte[32];
|
||||
mtpHelper.CalcTempAesKeyData(
|
||||
cachedAuthKey.NewNonce,
|
||||
cachedAuthKey.ServerNonce,
|
||||
aesKey,
|
||||
aesIv
|
||||
);
|
||||
var dhInnerData = DeserializeRequest(req, aesKey, aesIv);
|
||||
|
||||
CheckRequestData(cachedAuthKey.Nonce, dhInnerData.Nonce, "Nonce");
|
||||
CheckRequestData(cachedAuthKey.ServerNonce, dhInnerData.ServerNonce, "ServerNonce");
|
||||
var a = cachedAuthKey.A;
|
||||
var gb = dhInnerData.GB;
|
||||
|
||||
var authKeyBytes = BigInteger
|
||||
.ModPow(gb.ToBigEndianBigInteger(), a.ToBigEndianBigInteger(), AuthConsts.DhPrime)
|
||||
.ToByteArray(true, true)
|
||||
.ToBytes256();
|
||||
|
||||
var dto = new Step3Output(
|
||||
authKeyIdHelper.GetAuthKeyId(authKeyBytes),
|
||||
authKeyBytes,
|
||||
mtpHelper.ComputeSalt(cachedAuthKey.NewNonce, dhInnerData.ServerNonce),
|
||||
cachedAuthKey.IsPermanent,
|
||||
CreateDhGenOkAnswer(req, cachedAuthKey.NewNonce, authKeyBytes),
|
||||
cachedAuthKey.DcId
|
||||
);
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
private TClientDHInnerData DeserializeRequest(
|
||||
RequestSetClientDHParams serverDhParams,
|
||||
byte[] key,
|
||||
ReadOnlySpan<byte> iv
|
||||
)
|
||||
{
|
||||
var tempBytes = ArrayPool<byte>.Shared.Rent(serverDhParams.EncryptedData.Length + 20);
|
||||
var tempSpan = tempBytes.AsSpan(0, serverDhParams.EncryptedData.Length + 20);
|
||||
var answerWithHash = tempSpan.Slice(0, serverDhParams.EncryptedData.Length);
|
||||
try
|
||||
{
|
||||
aesHelper.DecryptIge(
|
||||
serverDhParams.EncryptedData,
|
||||
key,
|
||||
iv,
|
||||
answerWithHash
|
||||
);
|
||||
|
||||
var hash = answerWithHash[..20];
|
||||
var answer = answerWithHash[20..];
|
||||
ReadOnlyMemory<byte> buffer = tempBytes.AsMemory(20, answerWithHash.Length - 20);
|
||||
var oldLength = buffer.Length;
|
||||
var obj = buffer.Read<TClientDHInnerData>();
|
||||
var consumed = oldLength-buffer.Length;
|
||||
var paddingCount = (int)(answer.Length - consumed);
|
||||
var data = answer[..^paddingCount];
|
||||
var calcHash = tempSpan[^20..];
|
||||
SHA1.HashData(data, calcHash);
|
||||
if (!hash.SequenceEqual(calcHash))
|
||||
{
|
||||
logger.LogWarning("Answer sha1 hash mismatch.");
|
||||
|
||||
throw new ArgumentException($"Answer sha1 hash mismatch.");
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(tempBytes);
|
||||
}
|
||||
}
|
||||
|
||||
private TDhGenOk CreateDhGenOkAnswer(
|
||||
RequestSetClientDHParams req,
|
||||
byte[] newNonce,
|
||||
byte[] authKey
|
||||
)
|
||||
{
|
||||
var newNonceHash1 = CreateNewNonceHash(newNonce, authKey, 1);
|
||||
|
||||
return new TDhGenOk
|
||||
{
|
||||
Nonce = req.Nonce,
|
||||
ServerNonce = req.ServerNonce,
|
||||
NewNonceHash1 = newNonceHash1
|
||||
};
|
||||
}
|
||||
|
||||
//private TDhGenRetry CreateDhGenRetryRetryAnswer(RequestSetClientDHParams req, byte[] newNonce, byte[] authKey)
|
||||
//{
|
||||
// var newNonceHash2 = CreateNewNonceHash(newNonce, authKey, 2);
|
||||
|
||||
// return new TDhGenRetry
|
||||
// {
|
||||
// Nonce = req.Nonce,
|
||||
// ServerNonce = req.ServerNonce,
|
||||
// NewNonceHash2 = newNonceHash2
|
||||
// };
|
||||
//}
|
||||
|
||||
private byte[] CreateNewNonceHash(byte[] newNonce, byte[] authKey, byte n)
|
||||
{
|
||||
// https://core.telegram.org/mtproto/auth_key#9-server-responds-in-one-of-three-ways
|
||||
// new_nonce_hash1, new_nonce_hash2, and new_nonce_hash3 are obtained as the 128 lower - order bits of SHA1 of
|
||||
// the byte string derived from the new_nonce string by adding a single byte with the value of 1, 2, or 3, and followed
|
||||
// by another 8 bytes with auth_key_aux_hash.Different values are required to prevent an intruder from changing server
|
||||
// response dh_gen_ok into dh_gen_retry.
|
||||
|
||||
var authKeyAuxHash = SHA1.HashData(authKey).AsSpan(0, 8);
|
||||
Span<byte> newNonceWithAuxHashBytes = stackalloc byte[newNonce.Length + 1 + 8];
|
||||
newNonce.CopyTo(newNonceWithAuxHashBytes);
|
||||
newNonceWithAuxHashBytes[newNonce.Length] = n;
|
||||
authKeyAuxHash.CopyTo(newNonceWithAuxHashBytes[(newNonce.Length + 1)..]);
|
||||
var newNonceHashN = hashHelper.Sha1(newNonceWithAuxHashBytes);
|
||||
|
||||
return newNonceHashN.AsSpan(4).ToArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace MyTelegram.AuthServer.Services;
|
||||
|
||||
public record Step3Output(
|
||||
long AuthKeyId,
|
||||
byte[] AuthKey,
|
||||
long ServerSalt,
|
||||
bool IsPermanent,
|
||||
ISetClientDHParamsAnswer SetClientDhParamsAnswer,
|
||||
int? DcId = null
|
||||
);
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"Serilog": {
|
||||
"Using": [
|
||||
"Serilog.Sinks.Console",
|
||||
"Serilog.Sinks.File",
|
||||
"Serilog.Sinks.Async"
|
||||
],
|
||||
"MinimumLevel": {
|
||||
"Default": "Information",
|
||||
"Override": {
|
||||
"Microsoft": "Warning"
|
||||
}
|
||||
},
|
||||
"Properties": {
|
||||
"Application": "AuthServer"
|
||||
},
|
||||
"WriteTo": [
|
||||
{
|
||||
"Name": "Async",
|
||||
"Args": {
|
||||
"configure": [
|
||||
{
|
||||
"Name": "Console",
|
||||
"Args": {
|
||||
"MinimumLevel": "Information",
|
||||
"outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss:ffffff} [{Level:u3}] {Message}{NewLine}{Exception}",
|
||||
"theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "Async",
|
||||
"Args": {
|
||||
"configure": [
|
||||
{
|
||||
"Name": "File",
|
||||
"Args": {
|
||||
"rollingInterval": "Day",
|
||||
//"buffered": true,
|
||||
"path": "./Logs/log-.txt",
|
||||
"outputTemplate": "{Timestamp:o} [{Level:u3}] {Message}{NewLine}{Exception}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"App": {
|
||||
"PrivateKeyFilePath": "private.pkcs8.key"
|
||||
},
|
||||
"RabbitMQ": {
|
||||
"Connections": {
|
||||
"Default": {
|
||||
"HostName": "localhost",
|
||||
"Port": 5672,
|
||||
"UserName": "guest",
|
||||
"Password": "guest"
|
||||
}
|
||||
},
|
||||
"EventBus": {
|
||||
"ClientName": "MyTelegramAuthServer",
|
||||
"ExchangeName": "MyTelegramExchange",
|
||||
}
|
||||
},
|
||||
"Redis": {
|
||||
"Configuration": "redis:6379,password=CHANGE_ME"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MyTelegram.BotApi.Services;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace MyTelegram.BotApi.Controllers;
|
||||
|
||||
[ApiController]
|
||||
public class BotApiController(
|
||||
IBotApiService botApiService,
|
||||
ILogger<BotApiController> logger) : ControllerBase
|
||||
{
|
||||
// Проверяет токен бота. Возвращает null, если токен верный, иначе ответ с ошибкой.
|
||||
private async Task<IActionResult?> ValidateTokenAsync(string botId, string secretToken)
|
||||
{
|
||||
var token = $"{botId}:{secretToken}";
|
||||
|
||||
if (!await botApiService.ValidateBotTokenAsync(token))
|
||||
{
|
||||
logger.LogWarning("SECURITY: Invalid bot token attempted - BotId: {BotId}", botId);
|
||||
return Unauthorized(new { ok = false, error_code = 401, description = "Unauthorized: Invalid bot token" });
|
||||
}
|
||||
|
||||
return null; // токен корректен
|
||||
}
|
||||
|
||||
[HttpGet("bot{botId}:{secretToken}/getMe")]
|
||||
public async Task<IActionResult> GetMe(string botId, string secretToken)
|
||||
{
|
||||
// Проверяем токен бота
|
||||
var authError = await ValidateTokenAsync(botId, secretToken);
|
||||
if (authError != null) return authError;
|
||||
|
||||
try
|
||||
{
|
||||
var token = $"{botId}:{secretToken}";
|
||||
var result = await botApiService.GetMeAsync(token);
|
||||
return Ok(new { ok = true, result });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error in getMe");
|
||||
return Ok(new { ok = false, error_code = 400, description = "Bad Request" }); // не раскрываем детали ошибки наружу
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("bot{botId}:{secretToken}/getUpdates")]
|
||||
public async Task<IActionResult> GetUpdates(
|
||||
string botId,
|
||||
string secretToken,
|
||||
[FromQuery] int offset = 0,
|
||||
[FromQuery] int limit = 100,
|
||||
[FromQuery] int timeout = 0)
|
||||
{
|
||||
// Проверяем токен бота
|
||||
var authError = await ValidateTokenAsync(botId, secretToken);
|
||||
if (authError != null) return authError;
|
||||
|
||||
try
|
||||
{
|
||||
var token = $"{botId}:{secretToken}";
|
||||
var result = await botApiService.GetUpdatesAsync(token, offset, limit, timeout);
|
||||
return Ok(new { ok = true, result });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error in getUpdates");
|
||||
return Ok(new { ok = false, error_code = 400, description = "Bad Request" }); // не раскрываем детали ошибки наружу
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("bot{botId}:{secretToken}/sendMessage")]
|
||||
public async Task<IActionResult> SendMessage(string botId, string secretToken, [FromBody] JsonElement body)
|
||||
{
|
||||
// Проверяем токен бота
|
||||
var authError = await ValidateTokenAsync(botId, secretToken);
|
||||
if (authError != null) return authError;
|
||||
|
||||
try
|
||||
{
|
||||
var token = $"{botId}:{secretToken}";
|
||||
var result = await botApiService.SendMessageAsync(token, body);
|
||||
return Ok(new { ok = true, result });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error in sendMessage");
|
||||
return Ok(new { ok = false, error_code = 400, description = "Bad Request" });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("bot{botId}:{secretToken}/setWebhook")]
|
||||
public async Task<IActionResult> SetWebhook(string botId, string secretToken, [FromBody] JsonElement body)
|
||||
{
|
||||
// Проверяем токен бота
|
||||
var authError = await ValidateTokenAsync(botId, secretToken);
|
||||
if (authError != null) return authError;
|
||||
|
||||
try
|
||||
{
|
||||
var token = $"{botId}:{secretToken}";
|
||||
var result = await botApiService.SetWebhookAsync(token, body);
|
||||
return Ok(new { ok = true, result });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error in setWebhook");
|
||||
return Ok(new { ok = false, error_code = 400, description = "Bad Request" }); // не раскрываем детали ошибки наружу
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("bot{botId}:{secretToken}/deleteWebhook")]
|
||||
public async Task<IActionResult> DeleteWebhook(string botId, string secretToken)
|
||||
{
|
||||
// Проверяем токен бота
|
||||
var authError = await ValidateTokenAsync(botId, secretToken);
|
||||
if (authError != null) return authError;
|
||||
|
||||
try
|
||||
{
|
||||
var token = $"{botId}:{secretToken}";
|
||||
var result = await botApiService.DeleteWebhookAsync(token);
|
||||
return Ok(new { ok = true, result });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error in deleteWebhook");
|
||||
return Ok(new { ok = false, error_code = 400, description = "Bad Request" }); // не раскрываем детали ошибки наружу
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("bot{botId}:{secretToken}/getAvailableGifts")]
|
||||
public async Task<IActionResult> GetAvailableGifts(string botId, string secretToken)
|
||||
{
|
||||
// Проверяем токен бота
|
||||
var authError = await ValidateTokenAsync(botId, secretToken);
|
||||
if (authError != null) return authError;
|
||||
|
||||
try
|
||||
{
|
||||
var token = $"{botId}:{secretToken}";
|
||||
var result = await botApiService.GetAvailableGiftsAsync(token);
|
||||
return Ok(new { ok = true, result });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error in getAvailableGifts");
|
||||
return Ok(new { ok = false, error_code = 400, description = "Bad Request" });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("bot{botId}:{secretToken}/sendGift")]
|
||||
public async Task<IActionResult> SendGift(string botId, string secretToken, [FromBody] JsonElement body)
|
||||
{
|
||||
// Проверяем токен бота
|
||||
var authError = await ValidateTokenAsync(botId, secretToken);
|
||||
if (authError != null) return authError;
|
||||
|
||||
try
|
||||
{
|
||||
var token = $"{botId}:{secretToken}";
|
||||
var result = await botApiService.SendGiftAsync(token, body);
|
||||
return Ok(new { ok = true, result });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error in sendGift");
|
||||
return Ok(new { ok = false, error_code = 400, description = "Bad Request" });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("bot{botId}:{secretToken}/getWebhookInfo")]
|
||||
public async Task<IActionResult> GetWebhookInfo(string botId, string secretToken)
|
||||
{
|
||||
// Проверяем токен бота
|
||||
var authError = await ValidateTokenAsync(botId, secretToken);
|
||||
if (authError != null) return authError;
|
||||
|
||||
try
|
||||
{
|
||||
var token = $"{botId}:{secretToken}";
|
||||
var result = await botApiService.GetWebhookInfoAsync(token);
|
||||
return Ok(new { ok = true, result });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error in getWebhookInfo");
|
||||
return Ok(new { ok = false, error_code = 400, description = "Bad Request" });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("bot{botId}:{secretToken}/sendInvoice")]
|
||||
public async Task<IActionResult> SendInvoice(string botId, string secretToken, [FromBody] JsonElement body)
|
||||
{
|
||||
// Проверяем токен бота
|
||||
var authError = await ValidateTokenAsync(botId, secretToken);
|
||||
if (authError != null) return authError;
|
||||
|
||||
try
|
||||
{
|
||||
var token = $"{botId}:{secretToken}";
|
||||
var result = await botApiService.SendInvoiceAsync(token, body);
|
||||
return Ok(new { ok = true, result });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error in sendInvoice");
|
||||
return Ok(new { ok = false, error_code = 400, description = "Bad Request" });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("bot{botId}:{secretToken}/answerPreCheckoutQuery")]
|
||||
public async Task<IActionResult> AnswerPreCheckoutQuery(string botId, string secretToken, [FromBody] JsonElement body)
|
||||
{
|
||||
// Проверяем токен бота
|
||||
var authError = await ValidateTokenAsync(botId, secretToken);
|
||||
if (authError != null) return authError;
|
||||
|
||||
try
|
||||
{
|
||||
var token = $"{botId}:{secretToken}";
|
||||
var result = await botApiService.AnswerPreCheckoutQueryAsync(token, body);
|
||||
return Ok(new { ok = true, result });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error in answerPreCheckoutQuery");
|
||||
return Ok(new { ok = false, error_code = 400, description = "Bad Request" });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace MyTelegram.BotApi.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("file/bot{token}")]
|
||||
public class FileController(ILogger<FileController> logger) : ControllerBase
|
||||
{
|
||||
[HttpGet("{filePath}")]
|
||||
public async Task<IActionResult> DownloadFile([FromRoute] string token, [FromRoute] string filePath)
|
||||
{
|
||||
logger.LogInformation("File download request: {FilePath}", filePath);
|
||||
|
||||
// TODO: Implement file download from MinIO
|
||||
return NotFound(new { ok = false, error_code = 404, description = "File not found" });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MyTelegram.BotApi.Services;
|
||||
using MyTelegram.Domain.Shared.BotApi;
|
||||
|
||||
namespace MyTelegram.BotApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Внутренний API для приёма обновлений ботов от Query Server.
|
||||
/// Наружу не публикуется и доступен только из внутренней сети.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("internal/bot-updates")]
|
||||
public class InternalBotUpdatesController(
|
||||
IUpdatesManager updatesManager,
|
||||
ILogger<InternalBotUpdatesController> logger) : ControllerBase
|
||||
{
|
||||
[HttpPost("{botUserId}")]
|
||||
public async Task<IActionResult> ReceiveBotUpdate(
|
||||
long botUserId,
|
||||
[FromBody] BotUpdatePayload payload)
|
||||
{
|
||||
// Доступ по IP проверяет IpWhitelistMiddleware.
|
||||
// Эндпоинт должен быть доступен только из внутренней сети.
|
||||
|
||||
try
|
||||
{
|
||||
logger.LogInformation("Received bot update for bot {BotId}: MessageId={MessageId}, SenderId={SenderId}",
|
||||
botUserId, payload.MessageId, payload.SenderUserId);
|
||||
|
||||
// Формируем обновление в формате Bot API
|
||||
var update = new BotApiUpdate
|
||||
{
|
||||
// UpdateId генерируем через криптографический ГСЧ, а не Random.Shared
|
||||
UpdateId = System.Security.Cryptography.RandomNumberGenerator.GetInt32(1, int.MaxValue),
|
||||
Message = new BotApiMessage
|
||||
{
|
||||
MessageId = payload.MessageId,
|
||||
Date = payload.Date,
|
||||
Chat = new BotApiChat
|
||||
{
|
||||
Id = payload.SenderUserId,
|
||||
Type = "private"
|
||||
},
|
||||
From = new BotApiUser
|
||||
{
|
||||
Id = payload.SenderUserId,
|
||||
IsBot = false,
|
||||
FirstName = "User"
|
||||
},
|
||||
Text = payload.Text
|
||||
}
|
||||
};
|
||||
|
||||
// Кладём обновление в очередь
|
||||
await updatesManager.AddUpdateAsync((int)botUserId, update);
|
||||
|
||||
logger.LogInformation("Added update to queue for bot {BotId}", botUserId);
|
||||
|
||||
return Ok(new { success = true });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error receiving bot update");
|
||||
return StatusCode(500, new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("callbacks/{botUserId}")]
|
||||
public async Task<IActionResult> ReceiveBotCallback(
|
||||
long botUserId,
|
||||
[FromBody] BotCallbackPayload payload)
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.LogInformation("Received callback query for bot {BotId}: MessageId={MessageId}, FromUserId={FromUserId}, Data={Data}",
|
||||
botUserId, payload.MessageId, payload.FromUserId, payload.Data);
|
||||
|
||||
// Формируем обновление callback_query в формате Bot API
|
||||
var update = new BotApiUpdate
|
||||
{
|
||||
// UpdateId генерируем через криптографический ГСЧ
|
||||
UpdateId = System.Security.Cryptography.RandomNumberGenerator.GetInt32(1, int.MaxValue),
|
||||
CallbackQuery = new BotApiCallbackQuery
|
||||
{
|
||||
Id = Guid.NewGuid().ToString(),
|
||||
From = new BotApiUser
|
||||
{
|
||||
Id = payload.FromUserId,
|
||||
IsBot = false,
|
||||
FirstName = "User"
|
||||
},
|
||||
Message = new BotApiMessage
|
||||
{
|
||||
MessageId = payload.MessageId,
|
||||
Date = (int)payload.Timestamp,
|
||||
Chat = new BotApiChat
|
||||
{
|
||||
Id = payload.FromUserId,
|
||||
Type = "private"
|
||||
}
|
||||
},
|
||||
ChatInstance = payload.FromUserId.ToString(),
|
||||
Data = payload.Data
|
||||
}
|
||||
};
|
||||
|
||||
// Кладём обновление в очередь
|
||||
await updatesManager.AddUpdateAsync((int)botUserId, update);
|
||||
|
||||
logger.LogInformation("Added callback_query update to queue for bot {BotId}", botUserId);
|
||||
|
||||
return Ok(new { success = true });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error receiving callback query");
|
||||
return StatusCode(500, new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public record BotUpdatePayload(
|
||||
int MessageId,
|
||||
long SenderUserId,
|
||||
string Text,
|
||||
int Date
|
||||
);
|
||||
|
||||
public record BotCallbackPayload(
|
||||
long FromUserId,
|
||||
int MessageId,
|
||||
string? Data,
|
||||
long Timestamp
|
||||
);
|
||||
@@ -0,0 +1,45 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 8081
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||
WORKDIR /src
|
||||
|
||||
# Copy MSBuild props files for centralized package management
|
||||
COPY ["Directory.Packages.props", "./"]
|
||||
COPY ["Directory.Build.props", "./"]
|
||||
COPY ["common.props", "./"]
|
||||
|
||||
# Copy project files for BotApi and all dependencies
|
||||
COPY ["src/MyTelegram.BotApi/MyTelegram.BotApi.csproj", "src/MyTelegram.BotApi/"]
|
||||
COPY ["src/MyTelegram.Domain.Shared/MyTelegram.Domain.Shared.csproj", "src/MyTelegram.Domain.Shared/"]
|
||||
COPY ["src/MyTelegram.ReadModel.Interfaces/MyTelegram.ReadModel.Interfaces.csproj", "src/MyTelegram.ReadModel.Interfaces/"]
|
||||
COPY ["src/MyTelegram.ReadModel/MyTelegram.ReadModel.csproj", "src/MyTelegram.ReadModel/"]
|
||||
COPY ["src/MyTelegram.ReadModel.MongoDB/MyTelegram.ReadModel.MongoDB.csproj", "src/MyTelegram.ReadModel.MongoDB/"]
|
||||
COPY ["src/MyTelegram.EventBus.RabbitMQ/MyTelegram.EventBus.RabbitMQ.csproj", "src/MyTelegram.EventBus.RabbitMQ/"]
|
||||
COPY ["src/MyTelegram.Abstractions/MyTelegram.Abstractions.csproj", "src/MyTelegram.Abstractions/"]
|
||||
COPY ["src/MyTelegram.Domain/MyTelegram.Domain.csproj", "src/MyTelegram.Domain/"]
|
||||
COPY ["src/MyTelegram.EventFlow.MongoDB/MyTelegram.EventFlow.MongoDB.csproj", "src/MyTelegram.EventFlow.MongoDB/"]
|
||||
COPY ["src/MyTelegram.EventFlow/MyTelegram.EventFlow.csproj", "src/MyTelegram.EventFlow/"]
|
||||
COPY ["src/MyTelegram.EventBus/MyTelegram.EventBus.csproj", "src/MyTelegram.EventBus/"]
|
||||
COPY ["src/MyTelegram.Queries/MyTelegram.Queries.csproj", "src/MyTelegram.Queries/"]
|
||||
COPY ["src/MyTelegram.QueryHandlers.MongoDB/MyTelegram.QueryHandlers.MongoDB.csproj", "src/MyTelegram.QueryHandlers.MongoDB/"]
|
||||
COPY ["src/MyTelegram.Schema/MyTelegram.Schema.csproj", "src/MyTelegram.Schema/"]
|
||||
|
||||
# Restore dependencies
|
||||
RUN dotnet restore "src/MyTelegram.BotApi/MyTelegram.BotApi.csproj"
|
||||
|
||||
# Copy all source files
|
||||
COPY . .
|
||||
|
||||
# Build
|
||||
WORKDIR "/src/src/MyTelegram.BotApi"
|
||||
RUN dotnet build "MyTelegram.BotApi.csproj" -c Release -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
RUN dotnet publish "MyTelegram.BotApi.csproj" -c Release -o /app/publish /p:UseAppHost=false
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "MyTelegram.BotApi.dll"]
|
||||
@@ -0,0 +1,22 @@
|
||||
using MyTelegram.Schema;
|
||||
|
||||
namespace MyTelegram.BotApi.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Simple DTO event for bot messages (NOT EventFlow domain event)
|
||||
/// Published to RabbitMQ and handled by Command Server
|
||||
/// </summary>
|
||||
public class BotMessageEvent
|
||||
{
|
||||
public long BotUserId { get; set; }
|
||||
public long ChatId { get; set; }
|
||||
public long PeerId { get; set; }
|
||||
public PeerType PeerType { get; set; }
|
||||
public string Text { get; set; } = "";
|
||||
public string? ParseMode { get; set; }
|
||||
public bool? DisableWebPagePreview { get; set; }
|
||||
public bool? DisableNotification { get; set; }
|
||||
public int? ReplyToMessageId { get; set; }
|
||||
public long Timestamp { get; set; }
|
||||
public long RandomId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||
<ConfigureAwait />
|
||||
</Weavers>
|
||||
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
|
||||
<xs:element name="Weavers">
|
||||
<xs:complexType>
|
||||
<xs:all>
|
||||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1">
|
||||
<xs:complexType>
|
||||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" />
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
<xs:attribute name="VerifyAssembly" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="GenerateXsd" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:schema>
|
||||
@@ -0,0 +1,312 @@
|
||||
using MyTelegram.Domain.Shared.BotApi;
|
||||
using MyTelegram.Schema;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace MyTelegram.BotApi.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Helper methods for Bot API
|
||||
/// </summary>
|
||||
public static class BotApiHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Parse parse_mode and convert text to entities
|
||||
/// </summary>
|
||||
public static List<IMessageEntity>? ParseEntities(string? text, string? parseMode, List<IMessageEntity>? existingEntities = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
return existingEntities;
|
||||
}
|
||||
|
||||
var entities = existingEntities?.ToList() ?? new List<IMessageEntity>();
|
||||
|
||||
if (string.IsNullOrEmpty(parseMode))
|
||||
{
|
||||
return entities.Count > 0 ? entities : null;
|
||||
}
|
||||
|
||||
switch (parseMode.ToLowerInvariant())
|
||||
{
|
||||
case "markdown":
|
||||
ParseMarkdown(text, entities);
|
||||
break;
|
||||
case "markdownv2":
|
||||
ParseMarkdownV2(text, entities);
|
||||
break;
|
||||
case "html":
|
||||
ParseHtml(text, entities);
|
||||
break;
|
||||
}
|
||||
|
||||
return entities.Count > 0 ? entities : null;
|
||||
}
|
||||
|
||||
private static void ParseMarkdown(string text, List<IMessageEntity> entities)
|
||||
{
|
||||
// Simple Markdown parser for basic formatting
|
||||
// *bold* _italic_ `code` ```pre``` [text](url)
|
||||
|
||||
// Bold: *text*
|
||||
AddEntitiesFromPattern(text, @"\*([^\*]+)\*", entities, () => new TMessageEntityBold());
|
||||
|
||||
// Italic: _text_
|
||||
AddEntitiesFromPattern(text, @"_([^_]+)_", entities, () => new TMessageEntityItalic());
|
||||
|
||||
// Code: `text`
|
||||
AddEntitiesFromPattern(text, @"`([^`]+)`", entities, () => new TMessageEntityCode());
|
||||
|
||||
// Pre: ```text```
|
||||
AddEntitiesFromPattern(text, @"```([^`]+)```", entities, () => new TMessageEntityPre());
|
||||
|
||||
// Links: [text](url)
|
||||
var linkPattern = @"\[([^\]]+)\]\(([^\)]+)\)";
|
||||
var matches = Regex.Matches(text, linkPattern);
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
entities.Add(new TMessageEntityTextUrl
|
||||
{
|
||||
Offset = match.Index,
|
||||
Length = match.Groups[1].Length,
|
||||
Url = match.Groups[2].Value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void ParseMarkdownV2(string text, List<IMessageEntity> entities)
|
||||
{
|
||||
// MarkdownV2 with escaping support
|
||||
// Similar to Markdown but with more features
|
||||
ParseMarkdown(text, entities); // Simplified - full implementation would handle escaping
|
||||
}
|
||||
|
||||
private static void ParseHtml(string text, List<IMessageEntity> entities)
|
||||
{
|
||||
// HTML parser for basic tags
|
||||
// <b>bold</b> <i>italic</i> <code>code</code> <pre>pre</pre> <a href="url">text</a>
|
||||
|
||||
AddEntitiesFromHtmlTag(text, "b", entities, () => new TMessageEntityBold());
|
||||
AddEntitiesFromHtmlTag(text, "strong", entities, () => new TMessageEntityBold());
|
||||
AddEntitiesFromHtmlTag(text, "i", entities, () => new TMessageEntityItalic());
|
||||
AddEntitiesFromHtmlTag(text, "em", entities, () => new TMessageEntityItalic());
|
||||
AddEntitiesFromHtmlTag(text, "u", entities, () => new TMessageEntityUnderline());
|
||||
AddEntitiesFromHtmlTag(text, "s", entities, () => new TMessageEntityStrike());
|
||||
AddEntitiesFromHtmlTag(text, "code", entities, () => new TMessageEntityCode());
|
||||
AddEntitiesFromHtmlTag(text, "pre", entities, () => new TMessageEntityPre());
|
||||
|
||||
// Links: <a href="url">text</a>
|
||||
var linkPattern = @"<a\s+href=[""']([^""']+)[""']>([^<]+)</a>";
|
||||
var matches = Regex.Matches(text, linkPattern, RegexOptions.IgnoreCase);
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
entities.Add(new TMessageEntityTextUrl
|
||||
{
|
||||
Offset = match.Index,
|
||||
Length = match.Groups[2].Length,
|
||||
Url = match.Groups[1].Value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddEntitiesFromPattern(string text, string pattern, List<IMessageEntity> entities, Func<IMessageEntity> entityFactory)
|
||||
{
|
||||
var matches = Regex.Matches(text, pattern);
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
var entity = entityFactory();
|
||||
entity.Offset = match.Index;
|
||||
entity.Length = match.Groups[1].Length;
|
||||
entities.Add(entity);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddEntitiesFromHtmlTag(string text, string tag, List<IMessageEntity> entities, Func<IMessageEntity> entityFactory)
|
||||
{
|
||||
var pattern = $@"<{tag}>([^<]+)</{tag}>";
|
||||
AddEntitiesFromPattern(text, pattern, entities, entityFactory);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse reply_markup from JSON
|
||||
/// </summary>
|
||||
public static IReplyMarkup? ParseReplyMarkup(JsonElement? replyMarkupJson)
|
||||
{
|
||||
if (!replyMarkupJson.HasValue || replyMarkupJson.Value.ValueKind == JsonValueKind.Null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var json = replyMarkupJson.Value;
|
||||
|
||||
// Check if it's InlineKeyboardMarkup
|
||||
if (json.TryGetProperty("inline_keyboard", out var inlineKeyboard))
|
||||
{
|
||||
var rows = new List<IKeyboardButtonRow>();
|
||||
|
||||
foreach (var row in inlineKeyboard.EnumerateArray())
|
||||
{
|
||||
var buttons = new List<IKeyboardButton>();
|
||||
|
||||
foreach (var button in row.EnumerateArray())
|
||||
{
|
||||
var text = button.GetProperty("text").GetString() ?? "";
|
||||
|
||||
IKeyboardButton keyboardButton;
|
||||
|
||||
if (button.TryGetProperty("callback_data", out var callbackData))
|
||||
{
|
||||
keyboardButton = new TKeyboardButtonCallback
|
||||
{
|
||||
Text = text,
|
||||
Data = callbackData.GetString() ?? ""
|
||||
};
|
||||
}
|
||||
else if (button.TryGetProperty("url", out var url))
|
||||
{
|
||||
keyboardButton = new TKeyboardButtonUrl
|
||||
{
|
||||
Text = text,
|
||||
Url = url.GetString() ?? ""
|
||||
};
|
||||
}
|
||||
else if (button.TryGetProperty("switch_inline_query", out var switchInlineQuery))
|
||||
{
|
||||
keyboardButton = new TKeyboardButtonSwitchInline
|
||||
{
|
||||
Text = text,
|
||||
Query = switchInlineQuery.GetString() ?? ""
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
// Default button
|
||||
keyboardButton = new TKeyboardButton { Text = text };
|
||||
}
|
||||
|
||||
buttons.Add(keyboardButton);
|
||||
}
|
||||
|
||||
rows.Add(new TKeyboardButtonRow
|
||||
{
|
||||
Buttons = new TVector<IKeyboardButton>(buttons)
|
||||
});
|
||||
}
|
||||
|
||||
return new TReplyInlineMarkup
|
||||
{
|
||||
Rows = new TVector<IKeyboardButtonRow>(rows)
|
||||
};
|
||||
}
|
||||
|
||||
// Check if it's ReplyKeyboardMarkup
|
||||
if (json.TryGetProperty("keyboard", out var keyboard))
|
||||
{
|
||||
var rows = new List<IKeyboardButtonRow>();
|
||||
|
||||
foreach (var row in keyboard.EnumerateArray())
|
||||
{
|
||||
var buttons = new List<IKeyboardButton>();
|
||||
|
||||
foreach (var button in row.EnumerateArray())
|
||||
{
|
||||
string text;
|
||||
if (button.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
text = button.GetString() ?? "";
|
||||
}
|
||||
else
|
||||
{
|
||||
text = button.GetProperty("text").GetString() ?? "";
|
||||
}
|
||||
|
||||
IKeyboardButton keyboardButton;
|
||||
|
||||
if (button.ValueKind == JsonValueKind.Object && button.TryGetProperty("request_contact", out var requestContact) && requestContact.GetBoolean())
|
||||
{
|
||||
keyboardButton = new TKeyboardButtonRequestPhone { Text = text };
|
||||
}
|
||||
else if (button.ValueKind == JsonValueKind.Object && button.TryGetProperty("request_location", out var requestLocation) && requestLocation.GetBoolean())
|
||||
{
|
||||
keyboardButton = new TKeyboardButtonRequestGeoLocation { Text = text };
|
||||
}
|
||||
else
|
||||
{
|
||||
keyboardButton = new TKeyboardButton { Text = text };
|
||||
}
|
||||
|
||||
buttons.Add(keyboardButton);
|
||||
}
|
||||
|
||||
rows.Add(new TKeyboardButtonRow
|
||||
{
|
||||
Buttons = new TVector<IKeyboardButton>(buttons)
|
||||
});
|
||||
}
|
||||
|
||||
var resizeKeyboard = json.TryGetProperty("resize_keyboard", out var resize) && resize.GetBoolean();
|
||||
var oneTimeKeyboard = json.TryGetProperty("one_time_keyboard", out var oneTime) && oneTime.GetBoolean();
|
||||
var selective = json.TryGetProperty("selective", out var sel) && sel.GetBoolean();
|
||||
|
||||
return new TReplyKeyboardMarkup
|
||||
{
|
||||
Resize = resizeKeyboard,
|
||||
SingleUse = oneTimeKeyboard,
|
||||
Selective = selective,
|
||||
Rows = new TVector<IKeyboardButtonRow>(rows)
|
||||
};
|
||||
}
|
||||
|
||||
// Check if it's ReplyKeyboardRemove
|
||||
if (json.TryGetProperty("remove_keyboard", out var removeKeyboard) && removeKeyboard.GetBoolean())
|
||||
{
|
||||
var selective = json.TryGetProperty("selective", out var sel) && sel.GetBoolean();
|
||||
return new TReplyKeyboardHide { Selective = selective };
|
||||
}
|
||||
|
||||
// Check if it's ForceReply
|
||||
if (json.TryGetProperty("force_reply", out var forceReply) && forceReply.GetBoolean())
|
||||
{
|
||||
var selective = json.TryGetProperty("selective", out var sel) && sel.GetBoolean();
|
||||
return new TReplyKeyboardForceReply { Selective = selective };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get optional int parameter from JSON
|
||||
/// </summary>
|
||||
public static int? GetOptionalInt(JsonElement body, string propertyName)
|
||||
{
|
||||
if (body.TryGetProperty(propertyName, out var prop) && prop.ValueKind != JsonValueKind.Null)
|
||||
{
|
||||
return prop.GetInt32();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get optional string parameter from JSON
|
||||
/// </summary>
|
||||
public static string? GetOptionalString(JsonElement body, string propertyName)
|
||||
{
|
||||
if (body.TryGetProperty(propertyName, out var prop) && prop.ValueKind != JsonValueKind.Null)
|
||||
{
|
||||
return prop.GetString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get optional bool parameter from JSON
|
||||
/// </summary>
|
||||
public static bool GetOptionalBool(JsonElement body, string propertyName, bool defaultValue = false)
|
||||
{
|
||||
if (body.TryGetProperty(propertyName, out var prop) && prop.ValueKind != JsonValueKind.Null)
|
||||
{
|
||||
return prop.GetBoolean();
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace MyTelegram.BotApi.Middleware;
|
||||
|
||||
public class BotApiMiddleware(RequestDelegate next, ILogger<BotApiMiddleware> logger)
|
||||
{
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
var path = context.Request.Path.Value;
|
||||
|
||||
// Log all Bot API requests
|
||||
if (path?.StartsWith("/bot") == true)
|
||||
{
|
||||
logger.LogInformation("Bot API Request: {Method} {Path}", context.Request.Method, path);
|
||||
}
|
||||
|
||||
await next(context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using MyTelegram.BotApi.Services;
|
||||
|
||||
namespace MyTelegram.BotApi.Middleware;
|
||||
|
||||
/// <summary>
|
||||
/// Проверяет токен бота на всех запросах к Bot API до их обработки.
|
||||
/// </summary>
|
||||
public class BotTokenValidationMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger<BotTokenValidationMiddleware> _logger;
|
||||
|
||||
public BotTokenValidationMiddleware(RequestDelegate next, ILogger<BotTokenValidationMiddleware> logger)
|
||||
{
|
||||
_next = next;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context, IBotApiService botApiService)
|
||||
{
|
||||
var path = context.Request.Path.Value;
|
||||
|
||||
// Проверяем только пути вида /bot*, пропуская internal, health-check и подобные
|
||||
if (path != null && path.StartsWith("/bot") && !path.StartsWith("/botapi"))
|
||||
{
|
||||
// Достаём токен из пути: /bot{token}/method
|
||||
var match = System.Text.RegularExpressions.Regex.Match(path, @"^/bot([0-9]+:[A-Za-z0-9_-]+)/");
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
var token = match.Groups[1].Value;
|
||||
|
||||
// Сверяем токен с базой данных
|
||||
var isValid = await botApiService.ValidateBotTokenAsync(token);
|
||||
|
||||
if (!isValid)
|
||||
{
|
||||
_logger.LogWarning("SECURITY: Invalid bot token rejected - Token: {Token}, IP: {IP}",
|
||||
token, context.Connection.RemoteIpAddress);
|
||||
|
||||
context.Response.StatusCode = 401;
|
||||
context.Response.ContentType = "application/json";
|
||||
await context.Response.WriteAsJsonAsync(new
|
||||
{
|
||||
ok = false,
|
||||
error_code = 401,
|
||||
description = "Unauthorized: Invalid bot token"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Bot token validated: {Token}", token);
|
||||
}
|
||||
}
|
||||
|
||||
await _next(context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
using System.Net;
|
||||
|
||||
namespace MyTelegram.BotApi.Middleware;
|
||||
|
||||
/// <summary>
|
||||
/// Пускает к внутренним эндпоинтам только запросы с доверенных IP-адресов.
|
||||
/// </summary>
|
||||
public class IpWhitelistMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger<IpWhitelistMiddleware> _logger;
|
||||
private readonly HashSet<IPAddress> _whitelist;
|
||||
|
||||
public IpWhitelistMiddleware(RequestDelegate next, ILogger<IpWhitelistMiddleware> logger, IConfiguration configuration)
|
||||
{
|
||||
_next = next;
|
||||
_logger = logger;
|
||||
|
||||
// Читаем список разрешённых адресов из конфигурации, иначе используем значения по умолчанию
|
||||
var whitelistConfig = configuration.GetSection("Security:IpWhitelist").Get<string[]>() ?? new[]
|
||||
{
|
||||
"127.0.0.1", // localhost
|
||||
"::1", // localhost IPv6
|
||||
"172.17.0.0/16", // Docker default network
|
||||
"10.0.0.0/8" // Internal network
|
||||
};
|
||||
|
||||
_whitelist = new HashSet<IPAddress>();
|
||||
|
||||
foreach (var ip in whitelistConfig)
|
||||
{
|
||||
if (IPAddress.TryParse(ip, out var addr))
|
||||
{
|
||||
_whitelist.Add(addr);
|
||||
_logger.LogInformation("IP Whitelist: Added {IP}", ip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
var path = context.Request.Path.Value;
|
||||
|
||||
// Проверку по белому списку применяем только к внутренним эндпоинтам
|
||||
if (path != null && path.StartsWith("/internal/"))
|
||||
{
|
||||
var remoteIp = context.Connection.RemoteIpAddress;
|
||||
|
||||
if (remoteIp == null || !IsWhitelisted(remoteIp))
|
||||
{
|
||||
_logger.LogWarning("SECURITY: Internal API access denied - IP: {IP}, Path: {Path}",
|
||||
remoteIp, path);
|
||||
|
||||
context.Response.StatusCode = 403;
|
||||
context.Response.ContentType = "application/json";
|
||||
await context.Response.WriteAsJsonAsync(new
|
||||
{
|
||||
success = false,
|
||||
error = "Forbidden: Access denied to internal API"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Internal API access granted - IP: {IP}", remoteIp);
|
||||
}
|
||||
|
||||
await _next(context);
|
||||
}
|
||||
|
||||
private bool IsWhitelisted(IPAddress ip)
|
||||
{
|
||||
// Точное совпадение с адресом из списка
|
||||
if (_whitelist.Contains(ip))
|
||||
return true;
|
||||
|
||||
// localhost к внутреннему API пускаем всегда
|
||||
if (IPAddress.IsLoopback(ip))
|
||||
return true;
|
||||
|
||||
// Адреса внутренних и Docker-сетей (172.x.x.x, 10.x.x.x)
|
||||
var bytes = ip.GetAddressBytes();
|
||||
if (bytes.Length == 4)
|
||||
{
|
||||
if (bytes[0] == 172 && bytes[1] >= 16 && bytes[1] <= 31) // 172.16.0.0/12
|
||||
return true;
|
||||
if (bytes[0] == 10) // 10.0.0.0/8
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace MyTelegram.BotApi.Middleware;
|
||||
|
||||
/// <summary>
|
||||
/// Ограничивает частоту запросов по IP или токену бота, чтобы защититься от DoS-атак.
|
||||
/// </summary>
|
||||
public class RateLimitingMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger<RateLimitingMiddleware> _logger;
|
||||
|
||||
// Простой in-memory счётчик запросов. В продакшене лучше вынести в Redis.
|
||||
private static readonly ConcurrentDictionary<string, RateLimitInfo> _rateLimits = new();
|
||||
|
||||
// Пороги ограничения частоты запросов
|
||||
private const int MaxRequestsPerMinute = 120;
|
||||
private const int MaxRequestsPerSecond = 10;
|
||||
|
||||
public RateLimitingMiddleware(RequestDelegate next, ILogger<RateLimitingMiddleware> logger)
|
||||
{
|
||||
_next = next;
|
||||
_logger = logger;
|
||||
|
||||
// Фоновая задача: раз в минуту чистит устаревшие записи
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMinutes(1));
|
||||
CleanupOldEntries();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
var key = GetRateLimitKey(context);
|
||||
|
||||
if (!string.IsNullOrEmpty(key))
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var info = _rateLimits.GetOrAdd(key, _ => new RateLimitInfo());
|
||||
|
||||
bool shouldBlock = false;
|
||||
string blockReason = "";
|
||||
int retryAfter = 0;
|
||||
|
||||
lock (info)
|
||||
{
|
||||
// Убираем запросы старше одной минуты
|
||||
info.Requests.RemoveAll(t => (now - t).TotalMinutes > 1);
|
||||
|
||||
// Проверяем лимит за последнюю секунду
|
||||
var recentRequests = info.Requests.Count(t => (now - t).TotalSeconds <= 1);
|
||||
if (recentRequests >= MaxRequestsPerSecond)
|
||||
{
|
||||
shouldBlock = true;
|
||||
blockReason = "Too Many Requests: Rate limit exceeded (max 10/sec)";
|
||||
retryAfter = 1;
|
||||
}
|
||||
// Проверяем лимит за минуту
|
||||
else if (info.Requests.Count >= MaxRequestsPerMinute)
|
||||
{
|
||||
shouldBlock = true;
|
||||
blockReason = "Too Many Requests: Rate limit exceeded (max 120/min)";
|
||||
retryAfter = 60;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Засчитываем текущий запрос
|
||||
info.Requests.Add(now);
|
||||
}
|
||||
}
|
||||
|
||||
// Блокировку обрабатываем вне lock, чтобы не делать await под блокировкой
|
||||
if (shouldBlock)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Information))
|
||||
{
|
||||
_logger.LogInformation("RATE LIMIT: {Reason} - Key: {Key}, IP: {IP}",
|
||||
blockReason, key, context.Connection.RemoteIpAddress);
|
||||
}
|
||||
|
||||
context.Response.StatusCode = 429;
|
||||
context.Response.Headers["Retry-After"] = retryAfter.ToString();
|
||||
await context.Response.WriteAsJsonAsync(new
|
||||
{
|
||||
ok = false,
|
||||
error_code = 429,
|
||||
description = blockReason
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await _next(context);
|
||||
}
|
||||
|
||||
private string GetRateLimitKey(HttpContext context)
|
||||
{
|
||||
var path = context.Request.Path.Value;
|
||||
|
||||
// Для запросов к боту вытаскиваем токен из пути и считаем лимит отдельно по боту
|
||||
if (path != null && path.StartsWith("/bot"))
|
||||
{
|
||||
var match = System.Text.RegularExpressions.Regex.Match(path, @"^/bot([0-9]+):");
|
||||
if (match.Success)
|
||||
{
|
||||
return $"bot:{match.Groups[1].Value}";
|
||||
}
|
||||
}
|
||||
|
||||
// Для остальных запросов лимит считаем по IP-адресу
|
||||
var ip = context.Connection.RemoteIpAddress?.ToString();
|
||||
return ip != null ? $"ip:{ip}" : string.Empty;
|
||||
}
|
||||
|
||||
private void CleanupOldEntries()
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var keysToRemove = new List<string>();
|
||||
|
||||
foreach (var kvp in _rateLimits)
|
||||
{
|
||||
lock (kvp.Value)
|
||||
{
|
||||
kvp.Value.Requests.RemoveAll(t => (now - t).TotalMinutes > 2);
|
||||
|
||||
// Если свежих запросов не осталось, запись можно удалить
|
||||
if (kvp.Value.Requests.Count == 0)
|
||||
{
|
||||
keysToRemove.Add(kvp.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var key in keysToRemove)
|
||||
{
|
||||
_rateLimits.TryRemove(key, out _);
|
||||
}
|
||||
|
||||
if (keysToRemove.Count > 0)
|
||||
{
|
||||
_logger.LogDebug("Rate limit cleanup: Removed {Count} expired entries", keysToRemove.Count);
|
||||
}
|
||||
}
|
||||
|
||||
private class RateLimitInfo
|
||||
{
|
||||
public List<DateTimeOffset> Requests { get; } = new();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
namespace MyTelegram.BotApi.Middleware;
|
||||
|
||||
/// <summary>
|
||||
/// Ограничивает размер тела запроса, чтобы крупные запросы
|
||||
/// не приводили к переполнению памяти.
|
||||
/// </summary>
|
||||
public class RequestSizeLimitMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger<RequestSizeLimitMiddleware> _logger;
|
||||
private const long MaxRequestBodySize = 10 * 1024 * 1024; // 10MB
|
||||
|
||||
public RequestSizeLimitMiddleware(RequestDelegate next, ILogger<RequestSizeLimitMiddleware> logger)
|
||||
{
|
||||
_next = next;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
if (context.Request.ContentLength.HasValue)
|
||||
{
|
||||
if (context.Request.ContentLength.Value > MaxRequestBodySize)
|
||||
{
|
||||
_logger.LogWarning("Request body too large - Size: {Size}MB, IP: {IP}",
|
||||
context.Request.ContentLength.Value / 1024 / 1024,
|
||||
context.Connection.RemoteIpAddress);
|
||||
|
||||
context.Response.StatusCode = 413; // Payload Too Large (тело запроса слишком большое)
|
||||
await context.Response.WriteAsJsonAsync(new
|
||||
{
|
||||
ok = false,
|
||||
error_code = 413,
|
||||
description = $"Request body too large (max {MaxRequestBodySize / 1024 / 1024}MB)"
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Дополнительно задаём ограничение через feature ASP.NET Core
|
||||
var maxRequestBodySizeFeature = context.Features.Get<Microsoft.AspNetCore.Http.Features.IHttpMaxRequestBodySizeFeature>();
|
||||
if (maxRequestBodySizeFeature != null && !maxRequestBodySizeFeature.IsReadOnly)
|
||||
{
|
||||
maxRequestBodySizeFeature.MaxRequestBodySize = MaxRequestBodySize;
|
||||
}
|
||||
|
||||
await _next(context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" />
|
||||
<PackageReference Include="MongoDB.Driver" />
|
||||
<PackageReference Include="MessagePack" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Use renamed config file to avoid conflicts with QueryServer -->
|
||||
<Content Update="botapi.appsettings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- ONLY minimal dependencies for Bot API HTTP service -->
|
||||
<ProjectReference Include="..\MyTelegram.Domain.Shared\MyTelegram.Domain.Shared.csproj" />
|
||||
<ProjectReference Include="..\MyTelegram.ReadModel.Interfaces\MyTelegram.ReadModel.Interfaces.csproj" />
|
||||
<ProjectReference Include="..\MyTelegram.ReadModel\MyTelegram.ReadModel.csproj" />
|
||||
<ProjectReference Include="..\MyTelegram.ReadModel.MongoDB\MyTelegram.ReadModel.MongoDB.csproj" />
|
||||
<ProjectReference Include="..\MyTelegram.EventBus.RabbitMQ\MyTelegram.EventBus.RabbitMQ.csproj" />
|
||||
<ProjectReference Include="..\MyTelegram.Abstractions\MyTelegram.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,107 @@
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using MyTelegram.BotApi.Middleware;
|
||||
using MyTelegram.BotApi.Services;
|
||||
using MyTelegram.EventBus.RabbitMQ.Extensions;
|
||||
using MyTelegram.Abstractions;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Отдельный файл настроек, чтобы не конфликтовать с QueryServer
|
||||
// (необязателен, вместо него можно использовать переменные окружения)
|
||||
builder.Configuration.AddJsonFile("botapi.appsettings.json", optional: true, reloadOnChange: true);
|
||||
|
||||
builder.Services.AddControllers()
|
||||
.AddJsonOptions(options =>
|
||||
{
|
||||
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
|
||||
options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
|
||||
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
|
||||
});
|
||||
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
|
||||
// Настройки загрузки файлов (в локальном режиме до 2000 МБ)
|
||||
builder.Services.Configure<FormOptions>(options =>
|
||||
{
|
||||
options.MultipartBodyLengthLimit = 2000L * 1024 * 1024; // 2000 MB
|
||||
options.ValueLengthLimit = int.MaxValue;
|
||||
options.MultipartHeadersLengthLimit = int.MaxValue;
|
||||
});
|
||||
|
||||
// Настройки RabbitMQ из конфигурации
|
||||
builder.Services.Configure<MyTelegram.EventBus.RabbitMQ.EventBusRabbitMqOptions>(
|
||||
builder.Configuration.GetSection("RabbitMQ:EventBus"));
|
||||
builder.Services.Configure<MyTelegram.EventBus.RabbitMQ.RabbitMqOptions>(
|
||||
builder.Configuration.GetSection("RabbitMQ:Connections:Default"));
|
||||
|
||||
// Шина событий RabbitMQ для отправки событий в Command Server
|
||||
builder.Services.AddMyTelegramRabbitMqEventBus();
|
||||
|
||||
// Сериализаторы регистрируем вручную, чтобы не подтягивать через RegisterServices
|
||||
// лишние обработчики EventFlow
|
||||
builder.Services.AddTransient<MyTelegram.Abstractions.IEventDataSerializer<MyTelegram.Domain.Shared.Events.BotMessageEvent>, MyTelegram.BotApi.Serializers.BotMessageEventSerializer>();
|
||||
builder.Services.AddTransient<MyTelegram.Abstractions.IEventDataSerializer<MyTelegram.Domain.Shared.Events.BotGiftEvent>, MyTelegram.BotApi.Serializers.BotGiftEventSerializer>();
|
||||
|
||||
// MongoDB нужен только для BotMessagePollerService и используется на чтение
|
||||
builder.Services.AddSingleton<MongoDB.Driver.IMongoDatabase>(sp =>
|
||||
{
|
||||
var connectionString = builder.Configuration.GetValue<string>("MongoDB:ConnectionString") ?? "mongodb://mongodb:27017";
|
||||
var databaseName = builder.Configuration.GetValue<string>("MongoDB:DatabaseName") ?? "tg";
|
||||
var client = new MongoDB.Driver.MongoClient(connectionString);
|
||||
return client.GetDatabase(databaseName);
|
||||
});
|
||||
|
||||
// Bot API намеренно не использует EventFlow/ICommandBus, чтобы не вмешиваться
|
||||
// в трафик MTProto. Команды уходят в Command Server событиями через RabbitMQ.
|
||||
|
||||
// Сервисы Bot API
|
||||
builder.Services.AddSingleton<MTProtoBridge>();
|
||||
builder.Services.AddSingleton<IBotApiService, BotApiService>();
|
||||
builder.Services.AddSingleton<BotUpdateProcessor>();
|
||||
builder.Services.AddSingleton<IUpdatesManager, UpdatesManager>();
|
||||
builder.Services.AddSingleton<IWebhookManager, WebhookManager>();
|
||||
builder.Services.AddHostedService<BotMessagePollerService>();
|
||||
builder.Services.AddHttpClient();
|
||||
|
||||
// CORS
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddDefaultPolicy(policy =>
|
||||
{
|
||||
policy.AllowAnyOrigin()
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader();
|
||||
});
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Настройка конвейера обработки запросов
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseCors();
|
||||
|
||||
// Защитные middleware: ограничение размера запроса, рейт-лимит и белый список IP
|
||||
app.UseMiddleware<MyTelegram.BotApi.Middleware.RequestSizeLimitMiddleware>();
|
||||
app.UseMiddleware<MyTelegram.BotApi.Middleware.RateLimitingMiddleware>();
|
||||
app.UseMiddleware<MyTelegram.BotApi.Middleware.IpWhitelistMiddleware>();
|
||||
app.UseMiddleware<BotApiMiddleware>();
|
||||
|
||||
app.UseRouting();
|
||||
app.MapControllers();
|
||||
|
||||
// Эндпоинт проверки работоспособности
|
||||
app.MapGet("/", () => Results.Json(new
|
||||
{
|
||||
ok = true,
|
||||
result = "MyTelegram Bot API Server is running"
|
||||
}));
|
||||
|
||||
app.Run();
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.Buffers;
|
||||
using MyTelegram.Abstractions;
|
||||
using MyTelegram.Domain.Shared.Events;
|
||||
using MessagePack;
|
||||
|
||||
namespace MyTelegram.BotApi.Serializers;
|
||||
|
||||
public class BotGiftEventSerializer : IEventDataSerializer<BotGiftEvent>
|
||||
{
|
||||
private static readonly MessagePackSerializerOptions Options = MessagePackSerializerOptions.Standard
|
||||
.WithResolver(MessagePack.Resolvers.ContractlessStandardResolver.Instance);
|
||||
|
||||
public object Deserialize(Type type, ReadOnlyMemory<byte> buffer)
|
||||
{
|
||||
return MessagePackSerializer.Deserialize<BotGiftEvent>(buffer, Options)!;
|
||||
}
|
||||
|
||||
public BotGiftEvent Deserialize(ReadOnlyMemory<byte> buffer)
|
||||
{
|
||||
return MessagePackSerializer.Deserialize<BotGiftEvent>(buffer, Options);
|
||||
}
|
||||
|
||||
public void Serialize(IBufferWriter<byte> writer, BotGiftEvent value)
|
||||
{
|
||||
MessagePackSerializer.Serialize(writer, value, Options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.Buffers;
|
||||
using MyTelegram.Abstractions;
|
||||
using MyTelegram.Domain.Shared.Events;
|
||||
using MessagePack;
|
||||
|
||||
namespace MyTelegram.BotApi.Serializers;
|
||||
|
||||
public class BotMessageEventSerializer : IEventDataSerializer<BotMessageEvent>
|
||||
{
|
||||
private static readonly MessagePackSerializerOptions Options = MessagePackSerializerOptions.Standard
|
||||
.WithResolver(MessagePack.Resolvers.ContractlessStandardResolver.Instance);
|
||||
|
||||
public object Deserialize(Type type, ReadOnlyMemory<byte> buffer)
|
||||
{
|
||||
return MessagePackSerializer.Deserialize<BotMessageEvent>(buffer, Options)!;
|
||||
}
|
||||
|
||||
public BotMessageEvent Deserialize(ReadOnlyMemory<byte> buffer)
|
||||
{
|
||||
return MessagePackSerializer.Deserialize<BotMessageEvent>(buffer, Options);
|
||||
}
|
||||
|
||||
public void Serialize(IBufferWriter<byte> writer, BotMessageEvent value)
|
||||
{
|
||||
MessagePackSerializer.Serialize(writer, value, Options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,877 @@
|
||||
using MyTelegram.BotApi.Helpers;
|
||||
using MongoDB.Driver;
|
||||
using MyTelegram.Domain.Shared.BotApi;
|
||||
using MyTelegram.Domain.Shared.Events;
|
||||
using MyTelegram.ReadModel.MongoDB;
|
||||
using MyTelegram.Schema;
|
||||
using MyTelegram.EventBus;
|
||||
using System.Text.Json;
|
||||
using MongoDB.Driver;
|
||||
|
||||
namespace MyTelegram.BotApi.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Реализация сервиса Bot API
|
||||
/// </summary>
|
||||
public class BotApiService(
|
||||
IMongoDatabase database,
|
||||
MTProtoBridge mtprotoBridge,
|
||||
ILogger<BotApiService> logger,
|
||||
IUpdatesManager updatesManager,
|
||||
IEventBus eventBus) : IBotApiService
|
||||
{
|
||||
#region Basic Methods
|
||||
|
||||
// Проверяем токен бота перед обработкой запроса
|
||||
public async Task<bool> ValidateBotTokenAsync(string token)
|
||||
{
|
||||
try
|
||||
{
|
||||
var botsCollection = database.GetCollection<MyTelegram.ReadModel.Impl.BotReadModel>("ReadModel-BotReadModel");
|
||||
var bot = await botsCollection.Find(b => b.Token == token).FirstOrDefaultAsync();
|
||||
return bot != null; // токен валиден, если бот с ним найден
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error validating bot token");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<BotApiUser> GetMeAsync(string token)
|
||||
{
|
||||
var bot = await GetBotByTokenAsync(token);
|
||||
logger.LogInformation("Bot {BotId} calling getMe", bot.UserId);
|
||||
|
||||
// Берём данные пользователя бота из MongoDB
|
||||
var usersCollection = database.GetCollection<UserReadModel>("ReadModel-UserReadModel");
|
||||
var user = await usersCollection.Find(u => u.UserId == bot.UserId).FirstOrDefaultAsync();
|
||||
|
||||
return new BotApiUser
|
||||
{
|
||||
Id = bot.UserId,
|
||||
IsBot = true,
|
||||
FirstName = user?.FirstName ?? bot.BotName,
|
||||
Username = user?.UserName ?? bot.UserName,
|
||||
CanJoinGroups = bot.AllowJoinGroups,
|
||||
CanReadAllGroupMessages = bot.AllowAccessGroupMessages,
|
||||
SupportsInlineQueries = bot.InlineModeEnabled
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<List<BotApiUpdate>> GetUpdatesAsync(string token, int offset, int limit, int timeout)
|
||||
{
|
||||
// Ограничиваем таймаут двумя секундами вместо стандартных 30 ради быстрого отклика
|
||||
var actualTimeout = Math.Min(timeout, 2);
|
||||
logger.LogInformation("Getting updates: offset={Offset}, limit={Limit}, timeout={Timeout} (capped to {ActualTimeout}s)",
|
||||
offset, limit, timeout, actualTimeout);
|
||||
|
||||
// Достаём обновления из UpdatesManager (очередь в памяти)
|
||||
var updates = await updatesManager.GetUpdatesAsync(token, offset, Math.Min(limit, 100), actualTimeout, null);
|
||||
|
||||
logger.LogInformation("Returning {Count} updates", updates.Count);
|
||||
|
||||
return updates;
|
||||
}
|
||||
|
||||
public async Task<bool> SetWebhookAsync(string token, JsonElement body)
|
||||
{
|
||||
var bot = await GetBotByTokenAsync(token);
|
||||
var url = body.GetProperty("url").GetString();
|
||||
|
||||
// Проверяем URL вебхука
|
||||
var (isValid, error) = InputValidationService.ValidateUrl(url);
|
||||
if (!isValid)
|
||||
{
|
||||
logger.LogWarning("Invalid webhook URL from bot {BotId}: {Error}", bot.UserId, error);
|
||||
throw new Exception(error);
|
||||
}
|
||||
|
||||
logger.LogInformation("Bot {BotId} setting webhook: {Url}", bot.UserId, url);
|
||||
|
||||
// TODO: сохранить вебхук в базу
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteWebhookAsync(string token)
|
||||
{
|
||||
var bot = await GetBotByTokenAsync(token);
|
||||
logger.LogInformation("Bot {BotId} deleting webhook", bot.UserId);
|
||||
|
||||
// TODO: удалить вебхук из базы
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<object> GetWebhookInfoAsync(string token)
|
||||
{
|
||||
var bot = await GetBotByTokenAsync(token);
|
||||
logger.LogInformation("Bot {BotId} getting webhook info", bot.UserId);
|
||||
|
||||
return new BotApiWebhookInfo
|
||||
{
|
||||
Url = "",
|
||||
HasCustomCertificate = false,
|
||||
PendingUpdateCount = 0
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Send Message Methods
|
||||
|
||||
public async Task<BotApiMessage> SendMessageAsync(string token, JsonElement body)
|
||||
{
|
||||
var bot = await GetBotByTokenAsync(token);
|
||||
var chatId = body.GetProperty("chat_id").GetInt64();
|
||||
var text = body.GetProperty("text").GetString() ?? "";
|
||||
|
||||
// Проверяем длину и содержимое входных данных
|
||||
var (isValidText, textError, sanitizedText) = InputValidationService.ValidateMessageText(text);
|
||||
if (!isValidText)
|
||||
{
|
||||
logger.LogWarning("Invalid message text from bot {BotId}: {Error}", bot.UserId, textError);
|
||||
throw new Exception(textError);
|
||||
}
|
||||
|
||||
var (isValidChatId, chatIdError) = InputValidationService.ValidateChatId(chatId);
|
||||
if (!isValidChatId)
|
||||
{
|
||||
throw new Exception(chatIdError);
|
||||
}
|
||||
|
||||
logger.LogInformation("Bot {BotId} sending message to {ChatId}", bot.UserId, chatId);
|
||||
|
||||
// Разбираем необязательные параметры
|
||||
var parseMode = BotApiHelpers.GetOptionalString(body, "parse_mode");
|
||||
var disableWebPagePreview = BotApiHelpers.GetOptionalBool(body, "disable_web_page_preview");
|
||||
var disableNotification = BotApiHelpers.GetOptionalBool(body, "disable_notification");
|
||||
var replyToMessageId = BotApiHelpers.GetOptionalInt(body, "reply_to_message_id");
|
||||
|
||||
// Разбираем клавиатуру (reply markup)
|
||||
IReplyMarkup? replyMarkup = null;
|
||||
if (body.TryGetProperty("reply_markup", out var replyMarkupJson))
|
||||
{
|
||||
replyMarkup = BotApiHelpers.ParseReplyMarkup(replyMarkupJson);
|
||||
}
|
||||
|
||||
// Отправляем сообщение через MTProtoBridge
|
||||
logger.LogInformation("Bot {BotId} sending message via MTProtoBridge to chat {ChatId}", bot.UserId, chatId);
|
||||
|
||||
// Сейчас MTProtoBridge.SendMessageAsync возвращает заглушку
|
||||
// TODO: реализовать реальную отправку сообщений через MTProto
|
||||
var result = await mtprotoBridge.SendMessageAsync(
|
||||
botUserId: bot.UserId,
|
||||
chatId: chatId,
|
||||
text: text,
|
||||
parseMode: parseMode,
|
||||
disableWebPagePreview: disableWebPagePreview,
|
||||
disableNotification: disableNotification,
|
||||
replyToMessageId: replyToMessageId,
|
||||
replyMarkup: replyMarkup
|
||||
);
|
||||
|
||||
logger.LogInformation("Bot {BotId} message sent (stub) - MessageId={MessageId}", bot.UserId, result.MessageId);
|
||||
|
||||
// Возвращаем результат из MTProtoBridge
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<BotApiMessage> ForwardMessageAsync(string token, JsonElement body)
|
||||
{
|
||||
var bot = await GetBotByTokenAsync(token);
|
||||
var chatId = body.GetProperty("chat_id").GetInt64();
|
||||
var fromChatId = body.GetProperty("from_chat_id").GetInt64();
|
||||
var messageId = body.GetProperty("message_id").GetInt32();
|
||||
|
||||
logger.LogInformation("Bot {BotId} forwarding message {MessageId} from {FromChatId} to {ChatId}",
|
||||
bot.UserId, messageId, fromChatId, chatId);
|
||||
|
||||
var disableNotification = BotApiHelpers.GetOptionalBool(body, "disable_notification");
|
||||
|
||||
var (toPeerId, toPeerType) = BotApiConverter.FromBotApiChatId(chatId);
|
||||
var (fromPeerId, fromPeerType) = BotApiConverter.FromBotApiChatId(fromChatId);
|
||||
|
||||
// TODO: реализовать пересылку через ICommandBus
|
||||
// Пока возвращаем заглушку
|
||||
return new BotApiMessage
|
||||
{
|
||||
MessageId = messageId,
|
||||
Date = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
||||
Chat = BotApiConverter.ToBotApiChat(toPeerId, toPeerType),
|
||||
Text = "[Forwarded message]"
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<int> CopyMessageAsync(string token, JsonElement body)
|
||||
{
|
||||
var bot = await GetBotByTokenAsync(token);
|
||||
var chatId = body.GetProperty("chat_id").GetInt64();
|
||||
var fromChatId = body.GetProperty("from_chat_id").GetInt64();
|
||||
var messageId = body.GetProperty("message_id").GetInt32();
|
||||
|
||||
logger.LogInformation("Bot {BotId} copying message {MessageId} from {FromChatId} to {ChatId}",
|
||||
bot.UserId, messageId, fromChatId, chatId);
|
||||
|
||||
// TODO: реализовать копирование через ICommandBus
|
||||
return Random.Shared.Next(1, 1000000);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Edit/Delete Message Methods
|
||||
|
||||
public async Task<BotApiMessage> EditMessageTextAsync(string token, JsonElement body)
|
||||
{
|
||||
var bot = await GetBotByTokenAsync(token);
|
||||
|
||||
long? chatId = null;
|
||||
int? messageId = null;
|
||||
string? inlineMessageId = null;
|
||||
|
||||
if (body.TryGetProperty("chat_id", out var chatIdProp))
|
||||
{
|
||||
chatId = chatIdProp.GetInt64();
|
||||
}
|
||||
if (body.TryGetProperty("message_id", out var messageIdProp))
|
||||
{
|
||||
messageId = messageIdProp.GetInt32();
|
||||
}
|
||||
if (body.TryGetProperty("inline_message_id", out var inlineMessageIdProp))
|
||||
{
|
||||
inlineMessageId = inlineMessageIdProp.GetString();
|
||||
}
|
||||
|
||||
var text = body.GetProperty("text").GetString() ?? "";
|
||||
var parseMode = BotApiHelpers.GetOptionalString(body, "parse_mode");
|
||||
|
||||
logger.LogInformation("Bot {BotId} editing message {MessageId} in chat {ChatId}",
|
||||
bot.UserId, messageId, chatId);
|
||||
|
||||
// Разбираем сущности (entities) текста
|
||||
List<IMessageEntity>? entities = null;
|
||||
if (body.TryGetProperty("entities", out var entitiesJson))
|
||||
{
|
||||
entities = ParseBotApiEntities(entitiesJson);
|
||||
}
|
||||
entities = BotApiHelpers.ParseEntities(text, parseMode, entities);
|
||||
|
||||
// Разбираем клавиатуру (reply markup)
|
||||
IReplyMarkup? replyMarkup = null;
|
||||
if (body.TryGetProperty("reply_markup", out var replyMarkupJson))
|
||||
{
|
||||
replyMarkup = BotApiHelpers.ParseReplyMarkup(replyMarkupJson);
|
||||
}
|
||||
|
||||
// TODO: реализовать редактирование через ICommandBus
|
||||
|
||||
if (chatId.HasValue && messageId.HasValue)
|
||||
{
|
||||
var (peerId, peerType) = BotApiConverter.FromBotApiChatId(chatId.Value);
|
||||
|
||||
return new BotApiMessage
|
||||
{
|
||||
MessageId = messageId.Value,
|
||||
Date = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
||||
Chat = BotApiConverter.ToBotApiChat(peerId, peerType),
|
||||
Text = text,
|
||||
EditDate = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
||||
Entities = entities?.Select(BotApiConverter.ToBotApiMessageEntity).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
throw new Exception("Either chat_id and message_id or inline_message_id must be specified");
|
||||
}
|
||||
|
||||
public async Task<BotApiMessage> EditMessageReplyMarkupAsync(string token, JsonElement body)
|
||||
{
|
||||
var bot = await GetBotByTokenAsync(token);
|
||||
|
||||
long? chatId = null;
|
||||
int? messageId = null;
|
||||
|
||||
if (body.TryGetProperty("chat_id", out var chatIdProp))
|
||||
{
|
||||
chatId = chatIdProp.GetInt64();
|
||||
}
|
||||
if (body.TryGetProperty("message_id", out var messageIdProp))
|
||||
{
|
||||
messageId = messageIdProp.GetInt32();
|
||||
}
|
||||
|
||||
logger.LogInformation("Bot {BotId} editing reply markup for message {MessageId} in chat {ChatId}",
|
||||
bot.UserId, messageId, chatId);
|
||||
|
||||
// Разбираем клавиатуру (reply markup)
|
||||
IReplyMarkup? replyMarkup = null;
|
||||
if (body.TryGetProperty("reply_markup", out var replyMarkupJson))
|
||||
{
|
||||
replyMarkup = BotApiHelpers.ParseReplyMarkup(replyMarkupJson);
|
||||
}
|
||||
|
||||
// TODO: реализовать через ICommandBus
|
||||
|
||||
if (chatId.HasValue && messageId.HasValue)
|
||||
{
|
||||
var (peerId, peerType) = BotApiConverter.FromBotApiChatId(chatId.Value);
|
||||
|
||||
return new BotApiMessage
|
||||
{
|
||||
MessageId = messageId.Value,
|
||||
Date = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
||||
Chat = BotApiConverter.ToBotApiChat(peerId, peerType),
|
||||
EditDate = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds()
|
||||
};
|
||||
}
|
||||
|
||||
throw new Exception("chat_id and message_id must be specified");
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteMessageAsync(string token, JsonElement body)
|
||||
{
|
||||
var bot = await GetBotByTokenAsync(token);
|
||||
var chatId = body.GetProperty("chat_id").GetInt64();
|
||||
var messageId = body.GetProperty("message_id").GetInt32();
|
||||
|
||||
logger.LogInformation("Bot {BotId} deleting message {MessageId} from chat {ChatId}",
|
||||
bot.UserId, messageId, chatId);
|
||||
|
||||
var (peerId, peerType) = BotApiConverter.FromBotApiChatId(chatId);
|
||||
|
||||
// TODO: реализовать удаление через ICommandBus (StartDeleteMessagesCommand)
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Callback Query
|
||||
|
||||
public async Task<bool> AnswerCallbackQueryAsync(string token, JsonElement body)
|
||||
{
|
||||
var bot = await GetBotByTokenAsync(token);
|
||||
var callbackQueryId = body.GetProperty("callback_query_id").GetString() ?? "";
|
||||
|
||||
var text = BotApiHelpers.GetOptionalString(body, "text");
|
||||
var showAlert = BotApiHelpers.GetOptionalBool(body, "show_alert");
|
||||
var url = BotApiHelpers.GetOptionalString(body, "url");
|
||||
var cacheTime = BotApiHelpers.GetOptionalInt(body, "cache_time") ?? 0;
|
||||
|
||||
logger.LogInformation("Bot {BotId} answering callback query {QueryId}", bot.UserId, callbackQueryId);
|
||||
|
||||
// Разбираем идентификатор запроса
|
||||
if (!long.TryParse(callbackQueryId, out var queryId))
|
||||
{
|
||||
throw new ArgumentException("Invalid callback_query_id format", nameof(callbackQueryId));
|
||||
}
|
||||
|
||||
// TODO: реализовать ответ на callback-запрос
|
||||
logger.LogWarning("Bot {BotId} answerCallbackQuery not implemented - use Python bot API instead", bot.UserId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Chat Action
|
||||
|
||||
public async Task SendChatActionAsync(string token, JsonElement body)
|
||||
{
|
||||
var bot = await GetBotByTokenAsync(token);
|
||||
var chatId = body.GetProperty("chat_id").GetInt64();
|
||||
var action = body.GetProperty("action").GetString() ?? "typing";
|
||||
|
||||
logger.LogInformation("Bot {BotId} sending chat action {Action} to {ChatId}", bot.UserId, action, chatId);
|
||||
|
||||
var (peerId, peerType) = BotApiConverter.FromBotApiChatId(chatId);
|
||||
|
||||
// Сопоставляем действие Bot API с MTProto SendMessageAction
|
||||
ISendMessageAction sendMessageAction = action switch
|
||||
{
|
||||
"typing" => new TSendMessageTypingAction(),
|
||||
"upload_photo" => new TSendMessageUploadPhotoAction { Progress = 0 },
|
||||
"record_video" => new TSendMessageRecordVideoAction(),
|
||||
"upload_video" => new TSendMessageUploadVideoAction { Progress = 0 },
|
||||
"record_voice" => new TSendMessageRecordAudioAction(),
|
||||
"upload_voice" => new TSendMessageUploadAudioAction { Progress = 0 },
|
||||
"upload_document" => new TSendMessageUploadDocumentAction { Progress = 0 },
|
||||
"choose_sticker" => new TSendMessageChooseStickerAction(),
|
||||
"find_location" => new TSendMessageGeoLocationAction(),
|
||||
"record_video_note" => new TSendMessageRecordRoundAction(),
|
||||
"upload_video_note" => new TSendMessageUploadRoundAction { Progress = 0 },
|
||||
_ => new TSendMessageTypingAction()
|
||||
};
|
||||
|
||||
// TODO: отправить действие через MTProto (метод messages.setTyping)
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Media Methods
|
||||
|
||||
public Task<BotApiMessage> SendPhotoAsync(string token, long chatId, string? photo, IFormFile? photoFile)
|
||||
{
|
||||
logger.LogInformation("sendPhoto called for chat {ChatId}", chatId);
|
||||
throw new NotImplementedException("sendPhoto - requires file upload implementation");
|
||||
}
|
||||
|
||||
public Task<BotApiMessage> SendAudioAsync(string token, long chatId, string? audio, IFormFile? audioFile)
|
||||
{
|
||||
logger.LogInformation("sendAudio called for chat {ChatId}", chatId);
|
||||
throw new NotImplementedException("sendAudio - requires file upload implementation");
|
||||
}
|
||||
|
||||
public Task<BotApiMessage> SendDocumentAsync(string token, long chatId, string? document, IFormFile? documentFile)
|
||||
{
|
||||
logger.LogInformation("sendDocument called for chat {ChatId}", chatId);
|
||||
throw new NotImplementedException("sendDocument - requires file upload implementation");
|
||||
}
|
||||
|
||||
public Task<BotApiMessage> SendVideoAsync(string token, long chatId, string? video, IFormFile? videoFile)
|
||||
{
|
||||
logger.LogInformation("sendVideo called for chat {ChatId}", chatId);
|
||||
throw new NotImplementedException("sendVideo - requires file upload implementation");
|
||||
}
|
||||
|
||||
public Task<BotApiMessage> SendAnimationAsync(string token, long chatId, string? animation, IFormFile? animationFile)
|
||||
{
|
||||
logger.LogInformation("sendAnimation called for chat {ChatId}", chatId);
|
||||
throw new NotImplementedException("sendAnimation - requires file upload implementation");
|
||||
}
|
||||
|
||||
public Task<BotApiMessage> SendVoiceAsync(string token, long chatId, string? voice, IFormFile? voiceFile)
|
||||
{
|
||||
logger.LogInformation("sendVoice called for chat {ChatId}", chatId);
|
||||
throw new NotImplementedException("sendVoice - requires file upload implementation");
|
||||
}
|
||||
|
||||
public Task<BotApiMessage> SendVideoNoteAsync(string token, long chatId, string? videoNote, IFormFile? videoNoteFile)
|
||||
{
|
||||
logger.LogInformation("sendVideoNote called for chat {ChatId}", chatId);
|
||||
throw new NotImplementedException("sendVideoNote - requires file upload implementation");
|
||||
}
|
||||
|
||||
public Task<BotApiMessage> SendStickerAsync(string token, long chatId, string? sticker, IFormFile? stickerFile)
|
||||
{
|
||||
logger.LogInformation("sendSticker called for chat {ChatId}", chatId);
|
||||
throw new NotImplementedException("sendSticker - requires file upload implementation");
|
||||
}
|
||||
|
||||
public Task<List<BotApiMessage>> SendMediaGroupAsync(string token, JsonElement body)
|
||||
{
|
||||
throw new NotImplementedException("sendMediaGroup - requires media group implementation");
|
||||
}
|
||||
|
||||
public Task<BotApiMessage> SendLocationAsync(string token, JsonElement body)
|
||||
{
|
||||
throw new NotImplementedException("sendLocation");
|
||||
}
|
||||
|
||||
public Task<BotApiMessage> SendVenueAsync(string token, JsonElement body)
|
||||
{
|
||||
throw new NotImplementedException("sendVenue");
|
||||
}
|
||||
|
||||
public Task<BotApiMessage> SendContactAsync(string token, JsonElement body)
|
||||
{
|
||||
throw new NotImplementedException("sendContact");
|
||||
}
|
||||
|
||||
public Task<BotApiMessage> SendPollAsync(string token, JsonElement body)
|
||||
{
|
||||
throw new NotImplementedException("sendPoll");
|
||||
}
|
||||
|
||||
public Task<BotApiMessage> SendDiceAsync(string token, JsonElement body)
|
||||
{
|
||||
throw new NotImplementedException("sendDice");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Other Methods
|
||||
|
||||
public Task<object> GetUserProfilePhotosAsync(string token, long userId, int? offset, int? limit)
|
||||
{
|
||||
throw new NotImplementedException("getUserProfilePhotos");
|
||||
}
|
||||
|
||||
public Task<object> GetFileAsync(string token, string fileId)
|
||||
{
|
||||
throw new NotImplementedException("getFile");
|
||||
}
|
||||
|
||||
public Task BanChatMemberAsync(string token, JsonElement body)
|
||||
{
|
||||
throw new NotImplementedException("banChatMember");
|
||||
}
|
||||
|
||||
public Task UnbanChatMemberAsync(string token, JsonElement body)
|
||||
{
|
||||
throw new NotImplementedException("unbanChatMember");
|
||||
}
|
||||
|
||||
public Task RestrictChatMemberAsync(string token, JsonElement body)
|
||||
{
|
||||
throw new NotImplementedException("restrictChatMember");
|
||||
}
|
||||
|
||||
public Task PromoteChatMemberAsync(string token, JsonElement body)
|
||||
{
|
||||
throw new NotImplementedException("promoteChatMember");
|
||||
}
|
||||
|
||||
public Task SetChatAdministratorCustomTitleAsync(string token, JsonElement body)
|
||||
{
|
||||
throw new NotImplementedException("setChatAdministratorCustomTitle");
|
||||
}
|
||||
|
||||
public Task BanChatSenderChatAsync(string token, JsonElement body)
|
||||
{
|
||||
throw new NotImplementedException("banChatSenderChat");
|
||||
}
|
||||
|
||||
public Task UnbanChatSenderChatAsync(string token, JsonElement body)
|
||||
{
|
||||
throw new NotImplementedException("unbanChatSenderChat");
|
||||
}
|
||||
|
||||
public Task SetChatPermissionsAsync(string token, JsonElement body)
|
||||
{
|
||||
throw new NotImplementedException("setChatPermissions");
|
||||
}
|
||||
|
||||
public Task<string> ExportChatInviteLinkAsync(string token, JsonElement body)
|
||||
{
|
||||
throw new NotImplementedException("exportChatInviteLink");
|
||||
}
|
||||
|
||||
public Task<object> CreateChatInviteLinkAsync(string token, JsonElement body)
|
||||
{
|
||||
throw new NotImplementedException("createChatInviteLink");
|
||||
}
|
||||
|
||||
public Task<object> EditChatInviteLinkAsync(string token, JsonElement body)
|
||||
{
|
||||
throw new NotImplementedException("editChatInviteLink");
|
||||
}
|
||||
|
||||
public Task<object> RevokeChatInviteLinkAsync(string token, JsonElement body)
|
||||
{
|
||||
throw new NotImplementedException("revokeChatInviteLink");
|
||||
}
|
||||
|
||||
public Task ApproveChatJoinRequestAsync(string token, JsonElement body)
|
||||
{
|
||||
throw new NotImplementedException("approveChatJoinRequest");
|
||||
}
|
||||
|
||||
public Task DeclineChatJoinRequestAsync(string token, JsonElement body)
|
||||
{
|
||||
throw new NotImplementedException("declineChatJoinRequest");
|
||||
}
|
||||
|
||||
public Task SetChatPhotoAsync(string token, long chatId, IFormFile photo)
|
||||
{
|
||||
throw new NotImplementedException("setChatPhoto");
|
||||
}
|
||||
|
||||
public Task DeleteChatPhotoAsync(string token, JsonElement body)
|
||||
{
|
||||
throw new NotImplementedException("deleteChatPhoto");
|
||||
}
|
||||
|
||||
public Task SetChatTitleAsync(string token, JsonElement body)
|
||||
{
|
||||
throw new NotImplementedException("setChatTitle");
|
||||
}
|
||||
|
||||
public Task SetChatDescriptionAsync(string token, JsonElement body)
|
||||
{
|
||||
throw new NotImplementedException("setChatDescription");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private async Task<MyTelegram.ReadModel.Impl.BotReadModel> GetBotByTokenAsync(string token)
|
||||
{
|
||||
// Query bot directly from MongoDB
|
||||
var botsCollection = database.GetCollection<MyTelegram.ReadModel.Impl.BotReadModel>("ReadModel-BotReadModel");
|
||||
var bot = await botsCollection.Find(b => b.Token == token).FirstOrDefaultAsync();
|
||||
|
||||
if (bot == null)
|
||||
{
|
||||
throw new Exception("Invalid bot token");
|
||||
}
|
||||
return bot;
|
||||
}
|
||||
|
||||
private List<IMessageEntity>? ParseBotApiEntities(JsonElement entitiesJson)
|
||||
{
|
||||
if (entitiesJson.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var entities = new List<IMessageEntity>();
|
||||
|
||||
foreach (var entity in entitiesJson.EnumerateArray())
|
||||
{
|
||||
var type = entity.GetProperty("type").GetString();
|
||||
var offset = entity.GetProperty("offset").GetInt32();
|
||||
var length = entity.GetProperty("length").GetInt32();
|
||||
|
||||
IMessageEntity messageEntity = type switch
|
||||
{
|
||||
"bold" => new TMessageEntityBold { Offset = offset, Length = length },
|
||||
"italic" => new TMessageEntityItalic { Offset = offset, Length = length },
|
||||
"underline" => new TMessageEntityUnderline { Offset = offset, Length = length },
|
||||
"strikethrough" => new TMessageEntityStrike { Offset = offset, Length = length },
|
||||
"spoiler" => new TMessageEntitySpoiler { Offset = offset, Length = length },
|
||||
"code" => new TMessageEntityCode { Offset = offset, Length = length },
|
||||
"pre" => new TMessageEntityPre
|
||||
{
|
||||
Offset = offset,
|
||||
Length = length,
|
||||
Language = entity.TryGetProperty("language", out var lang) ? lang.GetString() ?? "" : ""
|
||||
},
|
||||
"text_link" => new TMessageEntityTextUrl
|
||||
{
|
||||
Offset = offset,
|
||||
Length = length,
|
||||
Url = entity.GetProperty("url").GetString() ?? ""
|
||||
},
|
||||
"text_mention" => new TMessageEntityMentionName
|
||||
{
|
||||
Offset = offset,
|
||||
Length = length,
|
||||
UserId = entity.GetProperty("user").GetProperty("id").GetInt64()
|
||||
},
|
||||
"custom_emoji" => new TMessageEntityCustomEmoji
|
||||
{
|
||||
Offset = offset,
|
||||
Length = length,
|
||||
DocumentId = long.Parse(entity.GetProperty("custom_emoji_id").GetString() ?? "0")
|
||||
},
|
||||
_ => new TMessageEntityUnknown { Offset = offset, Length = length }
|
||||
};
|
||||
|
||||
entities.Add(messageEntity);
|
||||
}
|
||||
|
||||
return entities.Count > 0 ? entities : null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Star Gifts
|
||||
|
||||
public async Task<object> GetAvailableGiftsAsync(string token)
|
||||
{
|
||||
var bot = await GetBotByTokenAsync(token);
|
||||
logger.LogInformation("Bot {BotId} calling getAvailableGifts", bot.UserId);
|
||||
|
||||
// Загружаем подарки напрямую из MongoDB
|
||||
var giftsCollection = database.GetCollection<MyTelegram.ReadModel.Impl.AvailableStarGiftReadModel>("AvailableStarGiftReadModel");
|
||||
var giftsReadModels = await giftsCollection.Find(_ => true).ToListAsync();
|
||||
|
||||
var gifts = new List<object>();
|
||||
|
||||
foreach (var gift in giftsReadModels)
|
||||
{
|
||||
// Подгружаем документ стикера для подарка
|
||||
object? sticker = null;
|
||||
if (gift.Sticker > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
var documentsCollection = database.GetCollection<DocumentReadModel>("ReadModel-DocumentReadModel");
|
||||
var document = await documentsCollection.Find(d => d.DocumentId == gift.Sticker).FirstOrDefaultAsync();
|
||||
|
||||
if (document != null)
|
||||
{
|
||||
sticker = new
|
||||
{
|
||||
file_id = $"doc_{document.DocumentId}",
|
||||
file_unique_id = $"doc_{document.DocumentId}_unique",
|
||||
width = 512,
|
||||
height = 512,
|
||||
is_animated = document.MimeType == "application/x-tgsticker",
|
||||
is_video = false,
|
||||
type = "regular",
|
||||
thumbnail = (object?)null,
|
||||
emoji = gift.FirstSaleDate > 0 ? "⭐" : "🎁",
|
||||
set_name = "StarGifts",
|
||||
file_size = document.Size
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Failed to load sticker for gift {GiftId}", gift.GiftId);
|
||||
}
|
||||
}
|
||||
|
||||
gifts.Add(new
|
||||
{
|
||||
id = gift.GiftId.ToString(),
|
||||
sticker,
|
||||
star_count = gift.Stars,
|
||||
total_count = gift.AvailabilityTotal,
|
||||
remaining_count = gift.AvailabilityRemains
|
||||
});
|
||||
}
|
||||
|
||||
return new { gifts };
|
||||
}
|
||||
|
||||
public async Task<bool> SendGiftAsync(string token, JsonElement body)
|
||||
{
|
||||
var bot = await GetBotByTokenAsync(token);
|
||||
var userId = body.GetProperty("user_id").GetInt64();
|
||||
var giftIdStr = body.GetProperty("gift_id").GetString();
|
||||
var giftId = long.Parse(giftIdStr!);
|
||||
var text = BotApiHelpers.GetOptionalString(body, "text");
|
||||
var parseMode = BotApiHelpers.GetOptionalString(body, "text_parse_mode");
|
||||
_ = parseMode;
|
||||
var hideName = body.TryGetProperty("hide_name", out var hideNameElement) && hideNameElement.GetBoolean();
|
||||
var includeUpgrade = body.TryGetProperty("include_upgrade", out var includeUpgradeElement) && includeUpgradeElement.GetBoolean();
|
||||
var count = body.TryGetProperty("count", out var countElement) ? Math.Max(1, countElement.GetInt32()) : 1;
|
||||
|
||||
logger.LogInformation("Bot {BotId} sending gift {GiftId} to user {UserId} (Count={Count}, HideName={HideName}, IncludeUpgrade={IncludeUpgrade})",
|
||||
bot.UserId, giftId, userId, count, hideName, includeUpgrade);
|
||||
|
||||
// Загружаем сведения о подарке из базы
|
||||
var giftsCollection = database.GetCollection<MyTelegram.ReadModel.Impl.AvailableStarGiftReadModel>("AvailableStarGiftReadModel");
|
||||
var gift = await giftsCollection.Find(g => g.GiftId == giftId).FirstOrDefaultAsync();
|
||||
|
||||
if (gift == null)
|
||||
{
|
||||
logger.LogWarning("Gift {GiftId} not found", giftId);
|
||||
return false;
|
||||
}
|
||||
|
||||
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
var randomId = Random.Shared.NextInt64();
|
||||
|
||||
var giftEvent = new BotGiftEvent
|
||||
{
|
||||
BotUserId = bot.UserId,
|
||||
ToUserId = userId,
|
||||
GiftId = giftId,
|
||||
Count = count,
|
||||
Message = text,
|
||||
HideName = hideName,
|
||||
IncludeUpgrade = includeUpgrade,
|
||||
Timestamp = now,
|
||||
RandomId = randomId
|
||||
};
|
||||
|
||||
await eventBus.PublishAsync(giftEvent);
|
||||
|
||||
logger.LogInformation("Published BotGiftEvent for bot {BotId} to user {UserId} (GiftId={GiftId}, Count={Count})",
|
||||
bot.UserId, userId, giftId, count);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Stars Payment API
|
||||
|
||||
public async Task<BotApiMessage> SendInvoiceAsync(string token, JsonElement body)
|
||||
{
|
||||
var bot = await GetBotByTokenAsync(token);
|
||||
var chatId = body.GetProperty("chat_id").GetInt64();
|
||||
var title = body.GetProperty("title").GetString() ?? "";
|
||||
var description = body.GetProperty("description").GetString() ?? "";
|
||||
var payload = body.GetProperty("payload").GetString() ?? "";
|
||||
var currency = body.GetProperty("currency").GetString() ?? "XTR"; // валюта Stars
|
||||
|
||||
// Разбираем массив цен
|
||||
var pricesJson = body.GetProperty("prices");
|
||||
var prices = new List<BotApiLabeledPrice>();
|
||||
int totalAmount = 0;
|
||||
|
||||
foreach (var priceElement in pricesJson.EnumerateArray())
|
||||
{
|
||||
var label = priceElement.GetProperty("label").GetString() ?? "";
|
||||
var amount = priceElement.GetProperty("amount").GetInt32();
|
||||
prices.Add(new BotApiLabeledPrice { Label = label, Amount = amount });
|
||||
totalAmount += amount;
|
||||
}
|
||||
|
||||
logger.LogInformation("Bot {BotId} sending invoice to {ChatId}: {Title} - {Amount} {Currency}",
|
||||
bot.UserId, chatId, title, totalAmount, currency);
|
||||
|
||||
// Формируем счёт (invoice)
|
||||
var invoice = new BotApiInvoice
|
||||
{
|
||||
Title = title,
|
||||
Description = description,
|
||||
StartParameter = payload,
|
||||
Currency = currency,
|
||||
TotalAmount = totalAmount
|
||||
};
|
||||
|
||||
// Создаём сообщение со счётом
|
||||
var message = new BotApiMessage
|
||||
{
|
||||
MessageId = (int)(DateTimeOffset.UtcNow.ToUnixTimeSeconds() % int.MaxValue),
|
||||
Date = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
||||
Chat = new BotApiChat
|
||||
{
|
||||
Id = chatId,
|
||||
Type = "private"
|
||||
},
|
||||
From = new BotApiUser
|
||||
{
|
||||
Id = bot.UserId,
|
||||
IsBot = true,
|
||||
FirstName = bot.BotName
|
||||
},
|
||||
Invoice = invoice
|
||||
};
|
||||
|
||||
// Сохраняем счёт в памяти для проверки на этапе pre-checkout
|
||||
await updatesManager.StoreInvoiceAsync(payload, new
|
||||
{
|
||||
bot_id = bot.UserId,
|
||||
chat_id = chatId,
|
||||
title,
|
||||
description,
|
||||
payload,
|
||||
currency,
|
||||
total_amount = totalAmount,
|
||||
prices
|
||||
});
|
||||
|
||||
logger.LogInformation("Invoice created with payload: {Payload}", payload);
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
public async Task<bool> AnswerPreCheckoutQueryAsync(string token, JsonElement body)
|
||||
{
|
||||
var bot = await GetBotByTokenAsync(token);
|
||||
var preCheckoutQueryId = body.GetProperty("pre_checkout_query_id").GetString() ?? "";
|
||||
var ok = body.TryGetProperty("ok", out var okElement) && okElement.GetBoolean();
|
||||
var errorMessage = BotApiHelpers.GetOptionalString(body, "error_message");
|
||||
|
||||
logger.LogInformation("Bot {BotId} answering pre-checkout query {QueryId}: ok={Ok}",
|
||||
bot.UserId, preCheckoutQueryId, ok);
|
||||
|
||||
if (!ok && !string.IsNullOrEmpty(errorMessage))
|
||||
{
|
||||
logger.LogWarning("Pre-checkout declined: {Error}", errorMessage);
|
||||
}
|
||||
|
||||
// Передаём ответ пользователю через UpdatesManager
|
||||
await updatesManager.AnswerPreCheckoutAsync(preCheckoutQueryId, ok, errorMessage);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
using MongoDB.Driver;
|
||||
using MongoDB.Bson;
|
||||
using MyTelegram.Domain.Shared;
|
||||
using MyTelegram.Domain.Shared.BotApi;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace MyTelegram.BotApi.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Фоновый сервис, который опрашивает MongoDB на наличие новых сообщений,
|
||||
/// адресованных ботам.
|
||||
/// </summary>
|
||||
public class BotMessagePollerService(
|
||||
IServiceProvider serviceProvider,
|
||||
ILogger<BotMessagePollerService> logger) : BackgroundService
|
||||
{
|
||||
private readonly TimeSpan _pollInterval = TimeSpan.FromMilliseconds(100); // короткий интервал опроса для быстрого отклика
|
||||
private readonly HashSet<int> _processedMessageIds = new(); // уже обработанные сообщения, чтобы не дублировать
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.LogInformation("Bot Message Poller Service starting...");
|
||||
|
||||
// Даём остальным сервисам время на инициализацию
|
||||
await Task.Delay(TimeSpan.FromSeconds(3), stoppingToken);
|
||||
|
||||
logger.LogInformation("Bot Message Poller Service started successfully");
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var updatesManager = scope.ServiceProvider.GetRequiredService<IUpdatesManager>();
|
||||
|
||||
// Подключение к MongoDB
|
||||
var configuration = scope.ServiceProvider.GetRequiredService<IConfiguration>();
|
||||
var connectionString = configuration["MongoDB:ConnectionString"];
|
||||
var databaseName = configuration["MongoDB:DatabaseName"];
|
||||
|
||||
var client = new MongoClient(connectionString);
|
||||
var database = client.GetDatabase(databaseName);
|
||||
|
||||
// Берём всех активных ботов
|
||||
var botsCollection = database.GetCollection<BsonDocument>("ReadModel-BotReadModel");
|
||||
var bots = await botsCollection.Find(Builders<BsonDocument>.Filter.Eq("IsActive", true))
|
||||
.ToListAsync(stoppingToken);
|
||||
|
||||
if (bots.Count == 0)
|
||||
{
|
||||
await Task.Delay(_pollInterval, stoppingToken);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Идентификаторы ботов (UserId в MongoDB может быть Int32 или Int64)
|
||||
var botIds = bots.Select(b => {
|
||||
var userId = b["UserId"];
|
||||
return userId.IsInt32 ? (long)userId.AsInt32 : userId.AsInt64;
|
||||
}).ToList();
|
||||
logger.LogInformation("Polling for {Count} bots: {BotIds}", botIds.Count, string.Join(", ", botIds));
|
||||
|
||||
// Ищем новые сообщения для любого из ботов.
|
||||
// Имя коллекции "ReadModel-MessageReadModel" задаётся соглашением EventFlow.
|
||||
var messagesCollection = database.GetCollection<BsonDocument>("ReadModel-MessageReadModel");
|
||||
|
||||
// Берём последние 10 сообщений без фильтра по MessageId — дубликаты отсеиваем в памяти.
|
||||
// ToPeerId в MongoDB хранится как Long.
|
||||
var filter = Builders<BsonDocument>.Filter.And(
|
||||
Builders<BsonDocument>.Filter.Eq("ToPeerType", (int)PeerType.User),
|
||||
Builders<BsonDocument>.Filter.In("ToPeerId", botIds)
|
||||
);
|
||||
|
||||
var messages = await messagesCollection.Find(filter)
|
||||
.Sort(Builders<BsonDocument>.Sort.Descending("MessageId"))
|
||||
.Limit(10)
|
||||
.ToListAsync(stoppingToken);
|
||||
|
||||
if (messages.Count > 0)
|
||||
{
|
||||
logger.LogInformation("Found {Count} new messages for bot", messages.Count);
|
||||
}
|
||||
|
||||
foreach (var msg in messages)
|
||||
{
|
||||
var messageId = msg["MessageId"].AsInt32;
|
||||
|
||||
// Пропускаем уже обработанные сообщения
|
||||
if (_processedMessageIds.Contains(messageId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var senderUserId = msg["SenderUserId"].AsInt64;
|
||||
var toPeerId = msg["ToPeerId"].AsInt64; // идентификатор бота
|
||||
var text = msg.Contains("Message") ? msg["Message"].AsString : "";
|
||||
var date = msg["Date"].AsInt32;
|
||||
|
||||
// Пропускаем сообщения от самого бота, чтобы не получить бесконечный цикл
|
||||
if (senderUserId == toPeerId)
|
||||
{
|
||||
logger.LogDebug("Skipping message from bot {BotId} to itself", toPeerId);
|
||||
_processedMessageIds.Add(messageId);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Формируем обновление в формате Bot API
|
||||
var update = new BotApiUpdate
|
||||
{
|
||||
UpdateId = messageId,
|
||||
Message = new BotApiMessage
|
||||
{
|
||||
MessageId = messageId,
|
||||
From = new BotApiUser
|
||||
{
|
||||
Id = senderUserId,
|
||||
IsBot = false,
|
||||
FirstName = "User"
|
||||
},
|
||||
Chat = new BotApiChat
|
||||
{
|
||||
Id = senderUserId, // в личных чатах ID чата совпадает с ID отправителя
|
||||
Type = "private"
|
||||
},
|
||||
Date = date,
|
||||
Text = text
|
||||
}
|
||||
};
|
||||
|
||||
await updatesManager.AddUpdateAsync((int)toPeerId, update);
|
||||
_processedMessageIds.Add(messageId);
|
||||
|
||||
logger.LogInformation("Added update for bot {BotId}: MessageId={MessageId}, Text={Text}", toPeerId, messageId, text);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error polling for bot messages");
|
||||
}
|
||||
|
||||
await Task.Delay(_pollInterval, stoppingToken);
|
||||
}
|
||||
|
||||
logger.LogInformation("Bot Message Poller Service stopped");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Bot Message Poller Service crashed on startup");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
using MyTelegram.Domain.Shared.BotApi;
|
||||
using MyTelegram.ReadModel.MongoDB;
|
||||
using MyTelegram.ReadModel.Interfaces;
|
||||
using MyTelegram.Schema;
|
||||
using MongoDB.Driver;
|
||||
using MongoDB.Bson;
|
||||
|
||||
namespace MyTelegram.BotApi.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Processes incoming messages and creates bot updates
|
||||
/// </summary>
|
||||
public class BotUpdateProcessor(
|
||||
IMongoDatabase mongoDatabase,
|
||||
ILogger<BotUpdateProcessor> logger)
|
||||
{
|
||||
/// <summary>
|
||||
/// Process new message and create bot update if recipient is a bot
|
||||
/// </summary>
|
||||
public async Task ProcessNewMessageAsync(
|
||||
IMessageReadModel message,
|
||||
IUser? sender,
|
||||
IChat? chat)
|
||||
{
|
||||
// Check if recipient is a bot - query directly from MongoDB
|
||||
var usersCollection = mongoDatabase.GetCollection<UserReadModel>("ReadModel-UserReadModel");
|
||||
var recipientUser = await usersCollection.Find(u => u.UserId == message.ToPeerId).FirstOrDefaultAsync();
|
||||
|
||||
if (recipientUser == null || !recipientUser.Bot)
|
||||
{
|
||||
return; // Not a bot, skip
|
||||
}
|
||||
|
||||
// Пропускаем сообщения, отправленные самим ботом, чтобы не зациклиться
|
||||
if (message.SenderUserId == recipientUser.UserId)
|
||||
{
|
||||
logger.LogDebug("Ignoring message from bot {BotId} to itself - preventing infinite loop", recipientUser.UserId);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogInformation("Processing message {MessageId} for bot {BotId} from user {SenderId}",
|
||||
message.MessageId, recipientUser.UserId, message.SenderUserId);
|
||||
|
||||
// Get bot info from MongoDB
|
||||
var botsCollection = mongoDatabase.GetCollection<MyTelegram.ReadModel.Impl.BotReadModel>("ReadModel-BotReadModel");
|
||||
var bot = await botsCollection.Find(b => b.UserId == recipientUser.UserId).FirstOrDefaultAsync();
|
||||
|
||||
if (bot == null)
|
||||
{
|
||||
logger.LogWarning("Bot {BotId} not found", recipientUser.UserId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create Bot API Update
|
||||
var botApiMessage = ConvertToBotApiMessage(message, sender, chat);
|
||||
var update = new BotApiUpdate
|
||||
{
|
||||
UpdateId = await GetNextUpdateIdAsync(bot.UserId),
|
||||
Message = botApiMessage
|
||||
};
|
||||
|
||||
// Save update to database
|
||||
await SaveUpdateAsync(bot.UserId, update, "message");
|
||||
|
||||
logger.LogInformation("Created update {UpdateId} for bot {BotId}",
|
||||
update.UpdateId, bot.UserId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process callback query and create bot update
|
||||
/// </summary>
|
||||
public async Task ProcessCallbackQueryAsync(
|
||||
long botUserId,
|
||||
long queryId,
|
||||
long fromUserId,
|
||||
long chatId,
|
||||
int messageId,
|
||||
string data)
|
||||
{
|
||||
logger.LogInformation("Processing callback query {QueryId} for bot {BotId}",
|
||||
queryId, botUserId);
|
||||
|
||||
// Get bot info from MongoDB
|
||||
var botsCollection = mongoDatabase.GetCollection<MyTelegram.ReadModel.Impl.BotReadModel>("ReadModel-BotReadModel");
|
||||
var bot = await botsCollection.Find(b => b.UserId == botUserId).FirstOrDefaultAsync();
|
||||
|
||||
if (bot == null)
|
||||
{
|
||||
logger.LogWarning("Bot {BotId} not found for callback query", botUserId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get user info from MongoDB
|
||||
var usersCollection = mongoDatabase.GetCollection<UserReadModel>("ReadModel-UserReadModel");
|
||||
var user = await usersCollection.Find(u => u.UserId == fromUserId).FirstOrDefaultAsync();
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Create callback query update
|
||||
var callbackQuery = new BotApiCallbackQuery
|
||||
{
|
||||
Id = queryId.ToString(),
|
||||
From = new BotApiUser
|
||||
{
|
||||
Id = user.UserId,
|
||||
IsBot = user.Bot,
|
||||
FirstName = user.FirstName ?? "",
|
||||
LastName = user.LastName,
|
||||
Username = user.UserName
|
||||
},
|
||||
ChatInstance = $"{chatId}_{messageId}",
|
||||
Data = data
|
||||
};
|
||||
|
||||
var update = new BotApiUpdate
|
||||
{
|
||||
UpdateId = await GetNextUpdateIdAsync(botUserId),
|
||||
CallbackQuery = callbackQuery
|
||||
};
|
||||
|
||||
// Save update
|
||||
await SaveUpdateAsync(botUserId, update, "callback_query");
|
||||
|
||||
logger.LogInformation("Created callback query update {UpdateId} for bot {BotId}",
|
||||
update.UpdateId, botUserId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get updates for bot (for getUpdates method)
|
||||
/// </summary>
|
||||
public async Task<List<BotApiUpdate>> GetUpdatesAsync(
|
||||
long botUserId,
|
||||
long offset,
|
||||
int limit)
|
||||
{
|
||||
// NOTE: Using simple document structure instead of ReadModel
|
||||
var collection = mongoDatabase.GetCollection<MongoDB.Bson.BsonDocument>("bot_updates");
|
||||
|
||||
// Get updates starting from offset
|
||||
var filter = MongoDB.Driver.Builders<MongoDB.Bson.BsonDocument>.Filter.And(
|
||||
MongoDB.Driver.Builders<MongoDB.Bson.BsonDocument>.Filter.Eq("BotUserId", botUserId),
|
||||
MongoDB.Driver.Builders<MongoDB.Bson.BsonDocument>.Filter.Gte("UpdateId", offset)
|
||||
);
|
||||
|
||||
var docs = await collection
|
||||
.Find(filter)
|
||||
.Sort(MongoDB.Driver.Builders<MongoDB.Bson.BsonDocument>.Sort.Ascending("UpdateId"))
|
||||
.Limit(limit)
|
||||
.ToListAsync();
|
||||
|
||||
// Convert BsonDocuments to BotApiUpdate
|
||||
var updates = new List<BotApiUpdate>();
|
||||
foreach (var doc in docs)
|
||||
{
|
||||
var updateBson = doc["Update"].AsBsonDocument;
|
||||
var updateJson = updateBson.ToJson();
|
||||
var update = System.Text.Json.JsonSerializer.Deserialize<BotApiUpdate>(updateJson);
|
||||
if (update != null)
|
||||
{
|
||||
updates.Add(update);
|
||||
}
|
||||
}
|
||||
|
||||
// Mark as delivered
|
||||
if (docs.Count > 0)
|
||||
{
|
||||
var updateIds = docs.Select(x => x["UpdateId"].AsInt64).ToList();
|
||||
var updateFilter = MongoDB.Driver.Builders<MongoDB.Bson.BsonDocument>.Filter.And(
|
||||
MongoDB.Driver.Builders<MongoDB.Bson.BsonDocument>.Filter.Eq("BotUserId", botUserId),
|
||||
MongoDB.Driver.Builders<MongoDB.Bson.BsonDocument>.Filter.In("UpdateId", updateIds)
|
||||
);
|
||||
|
||||
var updateDefinition = MongoDB.Driver.Builders<MongoDB.Bson.BsonDocument>.Update
|
||||
.Set("IsDelivered", true)
|
||||
.Set("DeliveredAt", DateTime.UtcNow);
|
||||
|
||||
await collection.UpdateManyAsync(updateFilter, updateDefinition);
|
||||
}
|
||||
|
||||
return updates;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete old delivered updates (cleanup)
|
||||
/// </summary>
|
||||
public async Task CleanupOldUpdatesAsync(long botUserId, long confirmedOffset)
|
||||
{
|
||||
var collection = mongoDatabase.GetCollection<MongoDB.Bson.BsonDocument>("bot_updates");
|
||||
|
||||
var filter = MongoDB.Driver.Builders<MongoDB.Bson.BsonDocument>.Filter.And(
|
||||
MongoDB.Driver.Builders<MongoDB.Bson.BsonDocument>.Filter.Eq("BotUserId", botUserId),
|
||||
MongoDB.Driver.Builders<MongoDB.Bson.BsonDocument>.Filter.Lt("UpdateId", confirmedOffset),
|
||||
MongoDB.Driver.Builders<MongoDB.Bson.BsonDocument>.Filter.Eq("IsDelivered", true)
|
||||
);
|
||||
|
||||
var result = await collection.DeleteManyAsync(filter);
|
||||
|
||||
if (result.DeletedCount > 0)
|
||||
{
|
||||
logger.LogInformation("Cleaned up {Count} old updates for bot {BotId}",
|
||||
result.DeletedCount, botUserId);
|
||||
}
|
||||
}
|
||||
|
||||
#region Private Methods
|
||||
|
||||
private async Task<long> GetNextUpdateIdAsync(long botUserId)
|
||||
{
|
||||
// Update LastUpdateId in BotReadModel and return new value
|
||||
var botsCollection = mongoDatabase.GetCollection<MyTelegram.ReadModel.Impl.BotReadModel>("ReadModel-BotReadModel");
|
||||
|
||||
var filter = MongoDB.Driver.Builders<MyTelegram.ReadModel.Impl.BotReadModel>.Filter.Eq(x => x.UserId, botUserId);
|
||||
var update = MongoDB.Driver.Builders<MyTelegram.ReadModel.Impl.BotReadModel>.Update.Inc(x => x.LastUpdateId, 1);
|
||||
|
||||
var options = new FindOneAndUpdateOptions<MyTelegram.ReadModel.Impl.BotReadModel>
|
||||
{
|
||||
ReturnDocument = ReturnDocument.After
|
||||
};
|
||||
|
||||
var bot = await botsCollection.FindOneAndUpdateAsync(filter, update, options);
|
||||
return bot?.LastUpdateId ?? 1;
|
||||
}
|
||||
|
||||
private async Task SaveUpdateAsync(long botUserId, BotApiUpdate update, string updateType)
|
||||
{
|
||||
var collection = mongoDatabase.GetCollection<MongoDB.Bson.BsonDocument>("bot_updates");
|
||||
|
||||
// Create simple document
|
||||
var doc = new MongoDB.Bson.BsonDocument
|
||||
{
|
||||
{ "BotUserId", botUserId },
|
||||
{ "UpdateId", update.UpdateId },
|
||||
{ "UpdateType", updateType },
|
||||
{ "Update", MongoDB.Bson.BsonDocument.Parse(System.Text.Json.JsonSerializer.Serialize(update)) },
|
||||
{ "IsDelivered", false },
|
||||
{ "CreatedAt", DateTime.UtcNow }
|
||||
};
|
||||
|
||||
await collection.InsertOneAsync(doc);
|
||||
}
|
||||
|
||||
private BotApiMessage ConvertToBotApiMessage(
|
||||
IMessageReadModel message,
|
||||
IUser? sender,
|
||||
IChat? chat)
|
||||
{
|
||||
var botApiMessage = new BotApiMessage
|
||||
{
|
||||
MessageId = message.MessageId,
|
||||
Date = message.Date,
|
||||
Text = message.Message,
|
||||
Chat = chat != null
|
||||
? BotApiConverter.ToBotApiChat(chat)
|
||||
: BotApiConverter.ToBotApiChat(message.ToPeerId, message.ToPeerType)
|
||||
};
|
||||
|
||||
// Add sender info
|
||||
if (sender != null)
|
||||
{
|
||||
botApiMessage.From = BotApiConverter.ToBotApiUser(sender);
|
||||
}
|
||||
|
||||
// Add entities
|
||||
if (message.Entities2 != null && message.Entities2.Count > 0)
|
||||
{
|
||||
botApiMessage.Entities = message.Entities2
|
||||
.Select(e => BotApiConverter.ToBotApiMessageEntity(e))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
return botApiMessage;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user