├── .editorconfig ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ ├── autolock.yml │ ├── dev.yml │ └── release.yml ├── .gitignore ├── CHANGES.md ├── Examples └── ConsoleApp │ ├── ConsoleApp.csproj │ └── Program.cs ├── LICENSE.txt ├── README.md ├── TelegramBricks.svg ├── WTelegramBot.sln ├── logo.png └── src ├── Bot.Methods.cs ├── Bot.TL.cs ├── Bot.Updates.cs ├── Bot.cs ├── BotCollectorPeer.cs ├── BotHelpers.cs ├── Database.Strings.cs ├── Database.cs ├── GlobalSuppressions.cs ├── TBTypes.cs ├── TypesTLConverters.cs ├── WTelegramBot.csproj ├── WTelegramBotClient.ApiMethods.cs ├── WTelegramBotClient.SendRequest.cs ├── WTelegramBotClient.cs └── WTelegramBotClientOptions.cs /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://EditorConfig.org 2 | 3 | [*TelegramBotClient*.cs] 4 | indent_style = space 5 | indent_size = 4 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: wiz0u 2 | custom: ["https://www.buymeacoffee.com/wizou", "http://t.me/WTelegramBot?start=donate"] 3 | -------------------------------------------------------------------------------- /.github/workflows/autolock.yml: -------------------------------------------------------------------------------- 1 | name: 'Auto-Lock Issues' 2 | 3 | on: 4 | schedule: 5 | - cron: '18 2 * * 1' 6 | workflow_dispatch: 7 | 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | discussions: write 12 | 13 | concurrency: 14 | group: lock-threads 15 | 16 | jobs: 17 | action: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: dessant/lock-threads@v5 21 | with: 22 | issue-inactive-days: '60' 23 | pr-inactive-days: '60' 24 | discussion-inactive-days: '60' 25 | -------------------------------------------------------------------------------- /.github/workflows/dev.yml: -------------------------------------------------------------------------------- 1 | name: Dev build 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ master ] 7 | # paths-ignore: [ '.**', 'Examples/**', '**.md' ] 8 | 9 | env: 10 | PROJECT_PATH: src/WTelegramBot.csproj 11 | CONFIGURATION: Release 12 | RELEASE_NOTES: ${{ github.event.head_commit.message }} 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 100 21 | submodules: true 22 | - name: Determine version 23 | run: | 24 | git fetch --depth=100 --tags 25 | DESCR_TAG=$(git describe --tags) 26 | COMMITS=${DESCR_TAG#*-} 27 | COMMITS=${COMMITS%-*} 28 | LAST_TAG=${DESCR_TAG%%-*} 29 | NEXT_VERSION=${LAST_TAG%.*}.$((${LAST_TAG##*.} + 1))-dev.$COMMITS 30 | RELEASE_VERSION=${{vars.RELEASE_VERSION}}-dev.$COMMITS 31 | if [[ "$RELEASE_VERSION" > "$NEXT_VERSION" ]] then VERSION=$RELEASE_VERSION; else VERSION=$NEXT_VERSION; fi 32 | echo Last tag: $LAST_TAG · Next version: $NEXT_VERSION · Release version: $RELEASE_VERSION · Build version: $VERSION 33 | echo "VERSION=$VERSION" >> $GITHUB_ENV 34 | - name: Setup .NET 35 | uses: actions/setup-dotnet@v4 36 | with: 37 | dotnet-version: 8.0.x 38 | - name: Pack 39 | run: dotnet pack $PROJECT_PATH --configuration $CONFIGURATION -p:Version=$VERSION "-p:ReleaseNotes=\"$RELEASE_NOTES\"" --output packages 40 | # - name: Upload artifact 41 | # uses: actions/upload-artifact@v4 42 | # with: 43 | # name: packages 44 | # path: packages/*.nupkg 45 | - name: Nuget push 46 | run: dotnet nuget push packages/*.nupkg --api-key ${{secrets.NUGETAPIKEY}} --skip-duplicate --source https://api.nuget.org/v3/index.json 47 | - name: Deployment Notification 48 | env: 49 | JSON: | 50 | { 51 | "status": "success", "complete": true, "commitMessage": ${{ toJSON(github.event.head_commit.message) }}, 52 | "message": "{ \"commitId\": \"${{ github.sha }}\", \"buildNumber\": \"${{ env.VERSION }}\", \"repoName\": \"${{ github.repository }}\"}" 53 | } 54 | run: | 55 | curl -X POST -H "Content-Type: application/json" -d "$JSON" ${{ secrets.DEPLOYED_WEBHOOK }} 56 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release build 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | release_notes: 7 | description: 'Release notes' 8 | required: true 9 | version: 10 | description: "Release version (leave empty for automatic versioning)" 11 | 12 | run-name: '📌 Release build ${{ inputs.version }}' 13 | 14 | env: 15 | PROJECT_PATH: src/WTelegramBot.csproj 16 | CONFIGURATION: Release 17 | RELEASE_NOTES: ${{ inputs.release_notes }} 18 | VERSION: ${{ inputs.version }} 19 | 20 | jobs: 21 | build: 22 | runs-on: ubuntu-latest 23 | permissions: 24 | contents: write # For git tag 25 | steps: 26 | - uses: actions/checkout@v4 27 | with: 28 | fetch-depth: 100 29 | - name: Determine version 30 | if: ${{ env.VERSION == '' }} 31 | run: | 32 | git fetch --depth=100 --tags 33 | DESCR_TAG=$(git describe --tags) 34 | LAST_TAG=${DESCR_TAG%%-*} 35 | NEXT_VERSION=${LAST_TAG%.*}.$((${LAST_TAG##*.} + 1)) 36 | RELEASE_VERSION=${{vars.RELEASE_VERSION}} 37 | if [[ "$RELEASE_VERSION" > "$NEXT_VERSION" ]] then VERSION=$RELEASE_VERSION; else VERSION=$NEXT_VERSION; fi 38 | echo Last tag: $LAST_TAG · Next version: $NEXT_VERSION · Release version: $RELEASE_VERSION · Build version: $VERSION 39 | echo "VERSION=$VERSION" >> $GITHUB_ENV 40 | - name: Setup .NET 41 | uses: actions/setup-dotnet@v4 42 | with: 43 | dotnet-version: 8.0.x 44 | - name: Pack 45 | run: dotnet pack $PROJECT_PATH --configuration $CONFIGURATION -p:Version=$VERSION "-p:ReleaseNotes=\"$RELEASE_NOTES\"" --output packages 46 | # - name: Upload artifact 47 | # uses: actions/upload-artifact@v4 48 | # with: 49 | # name: packages 50 | # path: packages/*.nupkg 51 | - name: Nuget push 52 | run: dotnet nuget push packages/*.nupkg --api-key ${{secrets.NUGETAPIKEY}} --skip-duplicate --source https://api.nuget.org/v3/index.json 53 | - name: Git tag 54 | run: | 55 | git tag $VERSION 56 | git push --tags 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | launchSettings.json 7 | 8 | # User-specific files 9 | *.rsuser 10 | *.suo 11 | *.user 12 | *.userosscache 13 | *.sln.docstates 14 | 15 | # User-specific files (MonoDevelop/Xamarin Studio) 16 | *.userprefs 17 | 18 | # Mono auto generated files 19 | mono_crash.* 20 | 21 | # Build results 22 | [Dd]ebug/ 23 | [Dd]ebugPublic/ 24 | [Rr]elease/ 25 | [Rr]eleases/ 26 | x64/ 27 | x86/ 28 | [Ww][Ii][Nn]32/ 29 | [Aa][Rr][Mm]/ 30 | [Aa][Rr][Mm]64/ 31 | bld/ 32 | [Bb]in/ 33 | [Oo]bj/ 34 | [Oo]ut/ 35 | [Ll]og/ 36 | [Ll]ogs/ 37 | 38 | # Visual Studio 2015/2017 cache/options directory 39 | .vs/ 40 | # Uncomment if you have tasks that create the project's static files in wwwroot 41 | #wwwroot/ 42 | 43 | # Visual Studio 2017 auto generated files 44 | Generated\ Files/ 45 | 46 | # MSTest test Results 47 | [Tt]est[Rr]esult*/ 48 | [Bb]uild[Ll]og.* 49 | 50 | # NUnit 51 | *.VisualState.xml 52 | TestResult.xml 53 | nunit-*.xml 54 | 55 | # Build Results of an ATL Project 56 | [Dd]ebugPS/ 57 | [Rr]eleasePS/ 58 | dlldata.c 59 | 60 | # Benchmark Results 61 | BenchmarkDotNet.Artifacts/ 62 | 63 | # .NET Core 64 | project.lock.json 65 | project.fragment.lock.json 66 | artifacts/ 67 | 68 | # ASP.NET Scaffolding 69 | ScaffoldingReadMe.txt 70 | 71 | # StyleCop 72 | StyleCopReport.xml 73 | 74 | # Files built by Visual Studio 75 | *_i.c 76 | *_p.c 77 | *_h.h 78 | *.ilk 79 | *.meta 80 | *.obj 81 | *.iobj 82 | *.pch 83 | *.pdb 84 | *.ipdb 85 | *.pgc 86 | *.pgd 87 | *.rsp 88 | *.sbr 89 | *.tlb 90 | *.tli 91 | *.tlh 92 | *.tmp 93 | *.tmp_proj 94 | *_wpftmp.csproj 95 | *.log 96 | *.vspscc 97 | *.vssscc 98 | .builds 99 | *.pidb 100 | *.svclog 101 | *.scc 102 | 103 | # Chutzpah Test files 104 | _Chutzpah* 105 | 106 | # Visual C++ cache files 107 | ipch/ 108 | *.aps 109 | *.ncb 110 | *.opendb 111 | *.opensdf 112 | *.sdf 113 | *.cachefile 114 | *.VC.db 115 | *.VC.VC.opendb 116 | 117 | # Visual Studio profiler 118 | *.psess 119 | *.vsp 120 | *.vspx 121 | *.sap 122 | 123 | # Visual Studio Trace Files 124 | *.e2e 125 | 126 | # TFS 2012 Local Workspace 127 | $tf/ 128 | 129 | # Guidance Automation Toolkit 130 | *.gpState 131 | 132 | # ReSharper is a .NET coding add-in 133 | _ReSharper*/ 134 | *.[Rr]e[Ss]harper 135 | *.DotSettings.user 136 | 137 | # TeamCity is a build add-in 138 | _TeamCity* 139 | 140 | # DotCover is a Code Coverage Tool 141 | *.dotCover 142 | 143 | # AxoCover is a Code Coverage Tool 144 | .axoCover/* 145 | !.axoCover/settings.json 146 | 147 | # Coverlet is a free, cross platform Code Coverage Tool 148 | coverage*.json 149 | coverage*.xml 150 | coverage*.info 151 | 152 | # Visual Studio code coverage results 153 | *.coverage 154 | *.coveragexml 155 | 156 | # NCrunch 157 | _NCrunch_* 158 | .*crunch*.local.xml 159 | nCrunchTemp_* 160 | 161 | # MightyMoose 162 | *.mm.* 163 | AutoTest.Net/ 164 | 165 | # Web workbench (sass) 166 | .sass-cache/ 167 | 168 | # Installshield output folder 169 | [Ee]xpress/ 170 | 171 | # DocProject is a documentation generator add-in 172 | DocProject/buildhelp/ 173 | DocProject/Help/*.HxT 174 | DocProject/Help/*.HxC 175 | DocProject/Help/*.hhc 176 | DocProject/Help/*.hhk 177 | DocProject/Help/*.hhp 178 | DocProject/Help/Html2 179 | DocProject/Help/html 180 | 181 | # Click-Once directory 182 | publish/ 183 | 184 | # Publish Web Output 185 | *.[Pp]ublish.xml 186 | *.azurePubxml 187 | # Note: Comment the next line if you want to checkin your web deploy settings, 188 | # but database connection strings (with potential passwords) will be unencrypted 189 | *.pubxml 190 | *.publishproj 191 | 192 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 193 | # checkin your Azure Web App publish settings, but sensitive information contained 194 | # in these scripts will be unencrypted 195 | PublishScripts/ 196 | 197 | # NuGet Packages 198 | *.nupkg 199 | # NuGet Symbol Packages 200 | *.snupkg 201 | # The packages folder can be ignored because of Package Restore 202 | **/[Pp]ackages/* 203 | # except build/, which is used as an MSBuild target. 204 | !**/[Pp]ackages/build/ 205 | # Uncomment if necessary however generally it will be regenerated when needed 206 | #!**/[Pp]ackages/repositories.config 207 | # NuGet v3's project.json files produces more ignorable files 208 | *.nuget.props 209 | *.nuget.targets 210 | 211 | # Microsoft Azure Build Output 212 | csx/ 213 | *.build.csdef 214 | 215 | # Microsoft Azure Emulator 216 | ecf/ 217 | rcf/ 218 | 219 | # Windows Store app package directories and files 220 | AppPackages/ 221 | BundleArtifacts/ 222 | Package.StoreAssociation.xml 223 | _pkginfo.txt 224 | *.appx 225 | *.appxbundle 226 | *.appxupload 227 | 228 | # Visual Studio cache files 229 | # files ending in .cache can be ignored 230 | *.[Cc]ache 231 | # but keep track of directories ending in .cache 232 | !?*.[Cc]ache/ 233 | 234 | # Others 235 | ClientBin/ 236 | ~$* 237 | *~ 238 | *.dbmdl 239 | *.dbproj.schemaview 240 | *.jfm 241 | *.pfx 242 | *.publishsettings 243 | orleans.codegen.cs 244 | 245 | # Including strong name files can present a security risk 246 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 247 | #*.snk 248 | 249 | # Since there are multiple workflows, uncomment next line to ignore bower_components 250 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 251 | #bower_components/ 252 | 253 | # RIA/Silverlight projects 254 | Generated_Code/ 255 | 256 | # Backup & report files from converting an old project file 257 | # to a newer Visual Studio version. Backup files are not needed, 258 | # because we have git ;-) 259 | _UpgradeReport_Files/ 260 | Backup*/ 261 | UpgradeLog*.XML 262 | UpgradeLog*.htm 263 | ServiceFabricBackup/ 264 | *.rptproj.bak 265 | 266 | # SQL Server files 267 | *.mdf 268 | *.ldf 269 | *.ndf 270 | 271 | # Business Intelligence projects 272 | *.rdl.data 273 | *.bim.layout 274 | *.bim_*.settings 275 | *.rptproj.rsuser 276 | *- [Bb]ackup.rdl 277 | *- [Bb]ackup ([0-9]).rdl 278 | *- [Bb]ackup ([0-9][0-9]).rdl 279 | 280 | # Microsoft Fakes 281 | FakesAssemblies/ 282 | 283 | # GhostDoc plugin setting file 284 | *.GhostDoc.xml 285 | 286 | # Node.js Tools for Visual Studio 287 | .ntvs_analysis.dat 288 | node_modules/ 289 | 290 | # Visual Studio 6 build log 291 | *.plg 292 | 293 | # Visual Studio 6 workspace options file 294 | *.opt 295 | 296 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 297 | *.vbw 298 | 299 | # Visual Studio LightSwitch build output 300 | **/*.HTMLClient/GeneratedArtifacts 301 | **/*.DesktopClient/GeneratedArtifacts 302 | **/*.DesktopClient/ModelManifest.xml 303 | **/*.Server/GeneratedArtifacts 304 | **/*.Server/ModelManifest.xml 305 | _Pvt_Extensions 306 | 307 | # Paket dependency manager 308 | .paket/paket.exe 309 | paket-files/ 310 | 311 | # FAKE - F# Make 312 | .fake/ 313 | 314 | # CodeRush personal settings 315 | .cr/personal 316 | 317 | # Python Tools for Visual Studio (PTVS) 318 | __pycache__/ 319 | *.pyc 320 | 321 | # Cake - Uncomment if you are using it 322 | # tools/** 323 | # !tools/packages.config 324 | 325 | # Tabs Studio 326 | *.tss 327 | 328 | # Telerik's JustMock configuration file 329 | *.jmconfig 330 | 331 | # BizTalk build output 332 | *.btp.cs 333 | *.btm.cs 334 | *.odx.cs 335 | *.xsd.cs 336 | 337 | # OpenCover UI analysis results 338 | OpenCover/ 339 | 340 | # Azure Stream Analytics local run output 341 | ASALocalRun/ 342 | 343 | # MSBuild Binary and Structured Log 344 | *.binlog 345 | 346 | # NVidia Nsight GPU debugger configuration file 347 | *.nvuser 348 | 349 | # MFractors (Xamarin productivity tool) working folder 350 | .mfractor/ 351 | 352 | # Local History for Visual Studio 353 | .localhistory/ 354 | 355 | # BeatPulse healthcheck temp database 356 | healthchecksdb 357 | 358 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 359 | MigrationBackup/ 360 | 361 | # Ionide (cross platform F# VS Code tools) working folder 362 | .ionide/ 363 | 364 | # Fody - auto-generated XML schema 365 | FodyWeavers.xsd 366 | /Examples/NativeAOT 367 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Migration from Telegram.Bot to WTelegramBot 2 | 3 | > [!NOTE] 4 | > If you're writing new code, you don't need to read this document, and you should use the `WTelegram.Bot` class directly. 5 | 6 | WTelegramBot library incorporates `Telegram.Bot` namespaces and types, and provides both classic `TelegramBotClient` and the new `WTelegramBotClient`, offering the same services, with the latter being more advanced and based on WTelegram.Bot. 7 | Migration effort for existing code should be minimal. 8 | 9 | ### Changes needed in your code: 10 | - Change the nuget package dependency from Telegram.Bot to [WTelegramBot](https://www.nuget.org/packages/WTelegramBot), 11 | and eventually add a database package. 12 | - Use class `WTelegramBotClient` instead of `TelegramBotClient` 13 | - Provide an ApiId and ApiHash _([obtained here](https://my.telegram.org/apps))_ 14 | as well as a DbConnection, typically SqliteConnection _(MySQL, PosgreSQL, SQLServer, and others are also supported)_ 15 | - `WTelegramBotClient` is `IDisposable`, so you should call `.Dispose()` when you're done using it. 16 | 17 | Example of changes: 18 | ```csharp 19 | === In your .csproj === 20 | 21 | 22 | 23 | === In your code === 24 | global using TelegramBotClient = Telegram.Bot.WTelegramBotClient; 25 | ... 26 | var dbConnection = new Microsoft.Data.Sqlite.SqliteConnection("Data Source=WTelegramBot.sqlite"); 27 | Bot = new WTelegramBotClient(BotToken, ApiId, ApiHash, dbConnection); 28 | ... 29 | Bot.Dispose(); 30 | ``` 31 | 32 | 33 | Other points to note: 34 | - Error messages on `ApiRequestException` may sometimes differ from the usual Bot API errors 35 | - FileID/FileUniqueID/InlineMessageId strings are not compatible with official Bot API ones, they are to be used with this library only. 36 | - There is no native support for Webhooks (but see [support for ASP.NET apps](#support-for-aspnet-apps)) 37 | - Methods DeleteWebhookAsync & LogOutAsync are forwarded to the Cloud Bot API. Use method CloseAsync to logout locally. 38 | - Texts in Markdown (V1) will be parsed as MarkdownV2. some discrepancy or error may arise due to reserved characters 39 | 40 | ## Support for ASP.NET apps 41 | 42 | If you can't establish a permanent TCP connection to Telegram server, `WTelegramBotClient` now supports using a `HttpClient` on the constructor (like TelegramBotClient). 43 | This is however not recommended as it is less efficient and does not work well in parallel calls. 44 | 45 | The recommended code for client instantiation would be something like: 46 | 47 | ```csharp 48 | services.AddSingleton(sp => 49 | { 50 | BotConfiguration? config = sp.GetService>()?.Value; 51 | var dbConnection = new Microsoft.Data.Sqlite.SqliteConnection("Data Source=WTelegramBot.sqlite"); 52 | return new WTelegramBotClient(config.BotToken, config.ApiId, config.ApiHash, dbConnection); 53 | }); 54 | ``` 55 | > [!TIP] 56 | > _For better performance, you can remove the `` so the singleton is typed `WTelegramBotClient`, but make sure your dependency-injection code use `WTelegramBotClient` everywhere_ 57 | 58 | Webhooks are not natively supported, but you can use a background service for polling instead: 59 | - Add this line to your Web App services configuration: 60 | `builder.Services.AddHostedService();` 61 | - Add the following PollingService.cs class: 62 | ```csharp 63 | using Telegram.Bot; 64 | using Telegram.Bot.Polling; 65 | 66 | public class PollingService(ITelegramBotClient bot, UpdateHandler updateHandler) : BackgroundService 67 | { 68 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 69 | { 70 | await bot.ReceiveAsync(updateHandler, new ReceiverOptions { AllowedUpdates = [], DropPendingUpdates = true }, stoppingToken); 71 | } 72 | } 73 | ``` 74 | - Make sure to implement your UpdateHandler class as deriving from `Telegram.Bot.Polling.IUpdateHandler` 75 | - Remember to `DeleteWebhookAsync` from Telegram Bot API when switching to WTelegramBot 76 | - You should also make sure your hosting service won't stop/recycle your app after some HTTP inactivity timeout. 77 | _(some host providers have an "always on" option, or alternatively you can ping your service with an HTTP request every 5 min to keep it alive)_ 78 | 79 | -------------------------------------------------------------------------------- /Examples/ConsoleApp/ConsoleApp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | latest 7 | enable 8 | enable 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Examples/ConsoleApp/Program.cs: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------------------------- 2 | // This example demonstrates a lot of things you cannot normally do with Telegram.Bot / Bot API 3 | // ---------------------------------------------------------------------------------------------- 4 | using System.Text; 5 | using Telegram.Bot; 6 | using Telegram.Bot.Types; 7 | using Telegram.Bot.Types.Enums; 8 | 9 | // This code needs these 3 variables in Project Properties > Debug > Launch Profiles > Environment variables 10 | // Get your Api Id/Hash from https://my.telegram.org/apps 11 | int apiId = int.Parse(Environment.GetEnvironmentVariable("ApiId")!); 12 | string apiHash = Environment.GetEnvironmentVariable("ApiHash")!; 13 | string botToken = Environment.GetEnvironmentVariable("BotToken")!; 14 | 15 | StreamWriter WTelegramLogs = new StreamWriter("WTelegramBot.log", true, Encoding.UTF8) { AutoFlush = true }; 16 | WTelegram.Helpers.Log = (lvl, str) => WTelegramLogs.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [{"TDIWE!"[lvl]}] {str}"); 17 | 18 | // Using SQLite DB for storage. Other DBs below (remember to add/uncomment the adequate PackageReference in .csproj) 19 | using var connection = new Microsoft.Data.Sqlite.SqliteConnection(@"Data Source=WTelegramBot.sqlite"); 20 | //SQL Server: using var connection = new Microsoft.Data.SqlClient.SqlConnection(@"Data Source=(LocalDB)\MSSQLLocalDB;AttachDbFilename=PATH_TO.mdf;Integrated Security=True;Connect Timeout=60"); 21 | //MySQL: using var connection = new MySql.Data.MySqlClient.MySqlConnection(@"Data Source=..."); 22 | //PosgreSQL: using var connection = new Npgsql.NpgsqlConnection(@"Data Source=..."); 23 | 24 | using var bot = new WTelegram.Bot(botToken, apiId, apiHash, connection); 25 | // use new WTelegramBotClient(...) instead, if you want the power of WTelegram with Telegram.Bot compatibility for existing code 26 | // use new TelegramBotClient(...) instead, if you just want Telegram.Bot classic code 27 | var my = await bot.GetMe(); 28 | Console.WriteLine($"I am @{my.Username}"); 29 | 30 | // get details about a user via the public username (even if not in discussion with bot) 31 | if (await bot.InputUser("@spotifysavebot") is { user_id: var userId }) 32 | { 33 | var userDetails = await bot.GetChat(userId); 34 | var full = (TL.Users_UserFull)userDetails.TLInfo!; 35 | var tlUser = full.users[userId]; 36 | var fullUser = full.full_user; 37 | if (tlUser.flags.HasFlag(TL.User.Flags.bot)) Console.WriteLine($"{tlUser} is a bot"); 38 | if (tlUser.flags.HasFlag(TL.User.Flags.scam)) Console.WriteLine($"{tlUser} is reported as scam"); 39 | if (tlUser.flags.HasFlag(TL.User.Flags.verified)) Console.WriteLine($"{tlUser} is verified"); 40 | if (tlUser.flags.HasFlag(TL.User.Flags.restricted)) Console.WriteLine($"{tlUser} is restricted: {tlUser.restriction_reason?[0].reason}"); 41 | if (fullUser.bot_info is { commands: { } botCommands }) 42 | { 43 | Console.WriteLine($"{tlUser} has {botCommands.Length} bot commands:"); 44 | foreach (var command in botCommands) 45 | Console.WriteLine($" /{command.command,-20} {command.description}"); 46 | } 47 | } 48 | 49 | //--------------------------------------------------------------------------------------- 50 | // get details about a public chat (even if bot is not a member of that chat) 51 | var chatDetails = await bot.GetChat("@tdlibchat"); 52 | if (chatDetails.TLInfo is TL.Messages_ChatFull { full_chat: TL.ChannelFull channelFull }) 53 | { 54 | Console.WriteLine($"@{chatDetails.Username} has {channelFull.participants_count} members, {channelFull.online_count} online"); 55 | if (channelFull.slowmode_seconds > 0) 56 | Console.WriteLine($"@{chatDetails.Username} has slowmode enabled: {channelFull.slowmode_seconds} seconds"); 57 | if (channelFull.available_reactions is TL.ChatReactionsAll { flags: TL.ChatReactionsAll.Flags.allow_custom }) 58 | Console.WriteLine($"@{chatDetails.Username} allows custom emojis as reactions"); 59 | } 60 | 61 | //--------------------------------------------------------------------------------------- 62 | // get list of members (you can increase the limit but Telegram might also impose a limit anyway) 63 | var members = await bot.GetChatMemberList(chatDetails.Id, limit: 1000); 64 | Console.WriteLine($"I fetched the info of {members.Length} members"); 65 | 66 | //--------------------------------------------------------------------------------------- 67 | // get a range of posted messages 68 | var messages = await bot.GetMessagesById("@tginfoen", Enumerable.Range(1904, 5)); 69 | Console.WriteLine($"I fetched {messages.Count} messages from @tginfoen:"); 70 | foreach (var m in messages) 71 | Console.WriteLine($" {m.MessageId}: {m.Type}"); 72 | 73 | //--------------------------------------------------------------------------------------- 74 | // show some message info not accessible in Bot API 75 | var msg = messages[0]; 76 | var tlMsg = msg.TLMessage() as TL.Message; 77 | Console.WriteLine($"Info for message {tlMsg.id}: Views = {tlMsg.views} Shares = {tlMsg.forwards} Pinned = {tlMsg.flags.HasFlag(TL.Message.Flags.pinned)}"); 78 | 79 | Console.WriteLine("___________________________________________________\n"); 80 | Console.WriteLine("I'm listening now. Send me a command in private or in a group where I am... Or press Escape to exit"); 81 | await bot.DropPendingUpdates(); 82 | bot.WantUnknownTLUpdates = true; 83 | bot.OnError += (e, s) => Console.Error.WriteLineAsync(e.ToString()); 84 | bot.OnMessage += OnMessage; 85 | bot.OnUpdate += OnUpdate; 86 | while (Console.ReadKey(true).Key != ConsoleKey.Escape) { } 87 | Console.WriteLine("Exiting..."); 88 | 89 | 90 | async Task OnMessage(WTelegram.Types.Message msg, UpdateType type) 91 | { 92 | if (msg.Text == null) return; 93 | var text = msg.Text.ToLower(); 94 | // commands accepted: 95 | if (text == "/start") 96 | { 97 | //---> It's easy to reply to a message by giving its id to replyParameters: (was broken in Telegram.Bot v20.0.0) 98 | await bot.SendMessage(msg.Chat, $"Hello, {msg.From}!\nTry commands /pic /react /lastseen /getchat /setphoto", replyParameters: msg); 99 | } 100 | else if (text == "/pic") 101 | { 102 | //---> It's easy to send a file by id or by url by just passing the string: (was broken in Telegram.Bot v19.0.0) 103 | await bot.SendPhoto(msg.Chat, "https://picsum.photos/310/200.jpg"); // easily send file by URL or FileID 104 | } 105 | else if (text == "/react") 106 | { 107 | //---> It's easy to send reaction emojis by just giving the emoji string or id 108 | await bot.SetMessageReaction(msg.Chat, msg.MessageId, ["👍"]); 109 | } 110 | else if (text == "/lastseen") 111 | { 112 | //---> Show more user info that is normally not accessible in Bot API: 113 | var tlUser = msg.From?.TLUser(); 114 | await bot.SendMessage(msg.Chat, $"Your last seen is: {tlUser?.status?.ToString()?[13..]}"); 115 | } 116 | else if (text == "/getchat") 117 | { 118 | var chat = await bot.GetChat(msg.Chat); 119 | //---> Demonstrate how to serialize structure to Json, and post it in
 code
120 | 		var dump = System.Text.Json.JsonSerializer.Serialize(chat, JsonBotAPI.Options);
121 | 		dump = $"
{TL.HtmlText.Escape(dump)}
"; 122 | await bot.SendMessage(msg.Chat, dump, parseMode: ParseMode.Html); 123 | } 124 | else if (text == "/setphoto") 125 | { 126 | var prevPhotos = await bot.GetUserProfilePhotos(my.Id); 127 | var jpegData = await new HttpClient().GetByteArrayAsync("https://picsum.photos/256/256.jpg"); 128 | await bot.SetMyPhoto(InputFile.FromStream(new MemoryStream(jpegData))); 129 | await bot.SendMessage(msg.Chat, "New bot profile photo set. Check my profile to see it. Restoring it in 20 seconds"); 130 | if (prevPhotos.TotalCount > 0) 131 | { 132 | await Task.Delay(20000); 133 | await bot.SetMyPhoto(prevPhotos.Photos[0][^1].FileId); // restore previous photo 134 | await bot.SendMessage(msg.Chat, "Bot profile photo restored"); 135 | } 136 | } 137 | } 138 | 139 | Task OnUpdate(WTelegram.Types.Update update) 140 | { 141 | if (update.Type == UpdateType.Unknown) 142 | { 143 | //---> Show some update types that are unsupported by Bot API but can be handled via TLUpdate 144 | if (update.TLUpdate is TL.UpdateDeleteChannelMessages udcm) 145 | Console.WriteLine($"{udcm.messages.Length} message(s) deleted in {bot.Chat(udcm.channel_id)?.Title}"); 146 | else if (update.TLUpdate is TL.UpdateDeleteMessages udm) 147 | Console.WriteLine($"{udm.messages.Length} message(s) deleted in user chat or small private group"); 148 | else if (update.TLUpdate is TL.UpdateReadChannelOutbox urco) 149 | Console.WriteLine($"Someone read {bot.Chat(urco.channel_id)?.Title} up to message {urco.max_id}"); 150 | } 151 | return Task.CompletedTask; 152 | } 153 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiz0u/WTelegramBot/373c09ccee04b0997de3d6eae87a7a7b4d22678d/LICENSE.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Bot API 9.0](https://img.shields.io/badge/Bot_API-9.0-blueviolet)](https://core.telegram.org/bots/api) 2 | [![NuGet version](https://img.shields.io/nuget/v/WTelegramBot?color=00508F)](https://www.nuget.org/packages/WTelegramBot/) 3 | [![NuGet prerelease](https://img.shields.io/nuget/vpre/WTelegramBot?color=C09030&label=dev+nuget)](https://www.nuget.org/packages/WTelegramBot/absoluteLatest) 4 | [![Donate](https://img.shields.io/badge/Help_this_project:-Donate-ff4444)](https://www.buymeacoffee.com/wizou) 5 | 6 | # Powerful Telegram Bot API library for .NET 7 | 8 | WTelegramBot is a full rewrite in pure C# of Telegram Bot API server, presenting the same methods as the Telegram.Bot library for easy [migration](https://github.com/wiz0u/WTelegramBot/blob/master/CHANGES.md). 9 | 10 | The library is built on top of [WTelegramClient](https://wiz0u.github.io/WTelegramClient) to connect directly to Telegram Client API and gives you additional control over your bot, updates and call methods normally impossible to use with Bot API. 11 | 12 | ## Advantages of WTelegram.Bot 13 | 14 | Using class `WTelegram.Bot` you have access to a clean set of developer-friendly methods to easily access the Bot API 15 | 16 | You can also call Client API methods that are possible for bots but not accessible from Bot API! 17 | Some examples: 18 | - Fetch past messages of group/channel 19 | - Get group/channel members list 20 | - Resolve user/chat usernames 21 | - Get full details of users/chats 22 | - Send/receive big files 23 | - Connect using a MTProxy 24 | - Change the bot's profile picture 25 | 26 | You also get access to raw Updates information from Client API, in addition to the usual Bot API updates. 27 | They contain more information than the limited set of Bot API updates! 28 | Some examples: 29 | - Detect deletion of messages _(not always immediate)_ 30 | - Get more info on message media _(like date of original media upload, sticker duration, ...)_ 31 | - Notification when your messages were read in a group 32 | 33 | See the [Example app](https://github.com/wiz0u/WTelegramBot/tree/master/Examples/ConsoleApp) for a nice demonstration of features. 34 | 35 | ➡️ There are still a lot of restrictions to bots, even via Client API, so don't expect to be able to do many fancy things 36 | 37 | 38 | ## Difference between classes `WTelegram.Bot` and `TelegramBotClient` 39 | 40 | The library contains a compatibility layer as `Telegram.Bot.WTelegramBotClient` inheriting from WTelegram.Bot. 41 | [Click here to easily migrate](https://github.com/wiz0u/WTelegramBot/blob/master/CHANGES.md) your existing Telegram.Bot code. 42 | 43 | If you're not migrating an existing codebase, it is recommended that you use `WTelegram.Bot` class directly. 44 | Here are the main differences: 45 | * The method names don't have the *Async suffix (even though they should still be invoked with `await`) so they are more close to official [Bot API method names](https://core.telegram.org/bots/api#available-methods). 46 | * The optional parameters follow a more logical order for developers, with the more rarely used optional parameters near the end. 47 | * There is no CancellationToken parameter because it doesn't make sense to abort an immediate TCP request to Client API. 48 | _(Even with HTTP Bot API, it didn't make much sense: You can use cancellationToken.ThrowIfCancellationRequested() at various points of your own code if you want it to be cancellable)_ 49 | * In case of an error, WTelegram.Bot will throw `WTelegram.WTException` like `TL.RpcException` showing the raw Telegram error, instead of an ApiRequestException 50 | * `WTelegram.Bot` and `WTelegramBotClient` are `IDisposable`, so remember to call `.Dispose()` 51 | 52 | ## How to access the advanced features? 53 | 54 | The [Example app](https://github.com/wiz0u/WTelegramBot/tree/master/Examples/ConsoleApp) demonstrates all of the features below. 55 | 56 | On each Update/Message/User/Chat you receive, there is an extra field named "`TL...`" that contains the corresponding raw Client API structure, which may contain extra information not transcribed into the Bot API 57 | 58 | You can also enable property `WantUnknownTLUpdates` to receive updates that usually would have been silently ignored by Bot API 59 | (they will be posted as Update of type Unknown with the TLUpdate field filled) 60 | 61 | Some extended API calls can be made via `WTelegram.Bot` special methods: 62 | - `GetChatMemberList`: fetch a list of chat members 63 | - `GetMessagesById`: fetch posted messages (or range of messages) based on their message IDs 64 | - `InputUser`: can resolve a username into a user ID that you can then use with GetChat 65 | - `GetChat`: can obtain details about any group/channel based on their public name, or a user ID resolved by InputUser 66 | - `SetMyPhoto`: change the bot's profile picture 67 | 68 | Other extended API calls not usually accessible to Bot API can be made via the `Bot.Client` property which is the underlying [WTelegramClient](https://wiz0u.github.io/WTelegramClient/) instance. 69 | * This way, you can use new features available only in Client API latest layers without waiting months for it to be available in Bot API 70 | 71 | For more information about calling Client API methods, you can read that [library's documentation](https://wiz0u.github.io/WTelegramClient/EXAMPLES) 72 | or search through the [official Client API documentation](https://corefork.telegram.org/methods), 73 | but make sure to look for the mention "**Bots can use this method**" (other methods can't be called). 74 | 75 | > Note: If you want to experiment with these, you'll need to add a `using TL;` on top of your code, and these calls might throw `TL.RpcException` instead of `ApiRequestException` 76 | 77 | Some other `WTelegram.Bot` methods (for example, beginning with Input*) and extension methods can help you convert Bot API ids or structure to/from Client API. 78 | 79 | 80 | ## Help with the library 81 | 82 | This library is still quite new but I tested it extensively to make sure it covers all of the Bot API successfully. 83 | 84 | If you have questions about the (official) Bot API methods from TelegramBotClient, you can ask them in [Telegram.Bot support chat](https://t.me/joinchat/B35YY0QbLfd034CFnvCtCA). 85 | 86 | If your question is more specific to WTelegram.Bot, or an issue with library behaviour, you can ask them in [@WTelegramClient](https://t.me/WTelegramClient). 87 | 88 | If you like this library, you can [buy me a coffee](https://www.buymeacoffee.com/wizou) ❤ This will help the project keep going. 89 | 90 | © 2021-2025 Olivier Marcoux 91 | -------------------------------------------------------------------------------- /WTelegramBot.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.5.33530.505 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WTelegramBot", "src\WTelegramBot.csproj", "{1237F115-59C4-4085-B724-F4B512B39ADD}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleApp", "Examples\ConsoleApp\ConsoleApp.csproj", "{1D2C245C-21DE-450B-9B2E-2E541A2B746A}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {1237F115-59C4-4085-B724-F4B512B39ADD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {1237F115-59C4-4085-B724-F4B512B39ADD}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {1237F115-59C4-4085-B724-F4B512B39ADD}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {1237F115-59C4-4085-B724-F4B512B39ADD}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {1D2C245C-21DE-450B-9B2E-2E541A2B746A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {1D2C245C-21DE-450B-9B2E-2E541A2B746A}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {1D2C245C-21DE-450B-9B2E-2E541A2B746A}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {1D2C245C-21DE-450B-9B2E-2E541A2B746A}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {5E0CD176-0F53-4816-A223-B779B9EF9660} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiz0u/WTelegramBot/373c09ccee04b0997de3d6eae87a7a7b4d22678d/logo.png -------------------------------------------------------------------------------- /src/Bot.Updates.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using TL; 3 | using Chat = WTelegram.Types.Chat; 4 | using Message = WTelegram.Types.Message; 5 | using Update = WTelegram.Types.Update; 6 | using User = WTelegram.Types.User; 7 | 8 | namespace WTelegram; 9 | 10 | public partial class Bot 11 | { 12 | /// Converts Client API TL.Update to Bot Telegram.Bot.Types.Update 13 | protected async Task MakeUpdate(TL.Update update) 14 | { 15 | switch (update) 16 | { 17 | case UpdateNewMessage unm: 18 | if (unm.message is TL.Message msg && msg.flags.HasFlag(TL.Message.Flags.out_)) return null; 19 | bool isChannelPost = (await ChatFromPeer(unm.message.Peer))?.Type == ChatType.Channel; 20 | if (NotAllowed(isChannelPost ? UpdateType.ChannelPost : UpdateType.Message)) return null; 21 | var message = await MakeMessageAndReply(unm.message); 22 | if (message == null) return null; 23 | return isChannelPost ? new Update { ChannelPost = message, TLUpdate = update } 24 | : new Update { Message = message, TLUpdate = update }; 25 | case UpdateEditMessage uem: 26 | if (uem.message is TL.Message emsg && emsg.flags.HasFlag(TL.Message.Flags.out_)) return null; 27 | isChannelPost = (await ChatFromPeer(uem.message.Peer))?.Type == ChatType.Channel; 28 | if (NotAllowed(isChannelPost ? UpdateType.ChannelPost : UpdateType.Message)) return null; 29 | return isChannelPost ? new Update { EditedChannelPost = await MakeMessageAndReply(uem.message), TLUpdate = update } 30 | : new Update { EditedMessage = await MakeMessageAndReply(uem.message), TLUpdate = update }; 31 | case UpdateBotInlineQuery ubiq: 32 | if (NotAllowed(UpdateType.InlineQuery)) return null; 33 | return new Update 34 | { 35 | InlineQuery = new InlineQuery 36 | { 37 | Id = ubiq.query_id.ToString(), 38 | From = await UserOrResolve(ubiq.user_id), 39 | Query = ubiq.query, 40 | Offset = ubiq.offset, 41 | ChatType = ubiq.peer_type switch 42 | { 43 | InlineQueryPeerType.SameBotPM => ChatType.Sender, 44 | InlineQueryPeerType.PM or InlineQueryPeerType.BotPM => ChatType.Private, 45 | InlineQueryPeerType.Chat => ChatType.Group, 46 | InlineQueryPeerType.Megagroup => ChatType.Supergroup, 47 | InlineQueryPeerType.Broadcast => ChatType.Channel, 48 | _ => null, 49 | }, 50 | Location = ubiq.geo.Location() 51 | }, 52 | TLUpdate = update 53 | }; 54 | case UpdateBotInlineSend ubis: 55 | if (NotAllowed(UpdateType.ChosenInlineResult)) return null; 56 | return new Update 57 | { 58 | ChosenInlineResult = new ChosenInlineResult 59 | { 60 | ResultId = ubis.id, 61 | From = await UserOrResolve(ubis.user_id), 62 | Location = ubis.geo.Location(), 63 | InlineMessageId = ubis.msg_id.InlineMessageId(), 64 | Query = ubis.query, 65 | }, 66 | TLUpdate = update 67 | }; 68 | case UpdateBotCallbackQuery ubcq: 69 | if (NotAllowed(UpdateType.CallbackQuery)) return null; 70 | return new Update 71 | { 72 | CallbackQuery = new CallbackQuery 73 | { 74 | Id = ubcq.query_id.ToString(), 75 | From = await UserOrResolve(ubcq.user_id), 76 | Message = await GetMIMessage(await ChatFromPeer(ubcq.peer, true), ubcq.msg_id, replyToo: true), 77 | ChatInstance = ubcq.chat_instance.ToString(), 78 | Data = ubcq.data.NullOrUtf8(), 79 | GameShortName = ubcq.game_short_name 80 | }, 81 | TLUpdate = update 82 | }; 83 | case UpdateInlineBotCallbackQuery ubicq: 84 | if (NotAllowed(UpdateType.CallbackQuery)) return null; 85 | return new Update 86 | { 87 | CallbackQuery = new CallbackQuery 88 | { 89 | Id = ubicq.query_id.ToString(), 90 | From = await UserOrResolve(ubicq.user_id), 91 | InlineMessageId = ubicq.msg_id.InlineMessageId(), 92 | ChatInstance = ubicq.chat_instance.ToString(), 93 | Data = ubicq.data.NullOrUtf8(), 94 | GameShortName = ubicq.game_short_name 95 | }, 96 | TLUpdate = update 97 | }; 98 | case UpdateChannelParticipant uchp: 99 | if (NotAllowed((uchp.new_participant ?? uchp.prev_participant)?.UserId == BotId ? UpdateType.MyChatMember : UpdateType.ChatMember)) return null; 100 | return MakeUpdate(new ChatMemberUpdated 101 | { 102 | Chat = await ChannelOrResolve(uchp.channel_id), 103 | From = await UserOrResolve(uchp.actor_id), 104 | Date = uchp.date, 105 | OldChatMember = uchp.prev_participant.ChatMember(await UserOrResolve((uchp.prev_participant ?? uchp.new_participant)!.UserId)), 106 | NewChatMember = uchp.new_participant.ChatMember(await UserOrResolve((uchp.new_participant ?? uchp.prev_participant)!.UserId)), 107 | InviteLink = await MakeChatInviteLink(uchp.invite), 108 | ViaJoinRequest = uchp.invite is ChatInvitePublicJoinRequests, 109 | ViaChatFolderInviteLink = uchp.flags.HasFlag(UpdateChannelParticipant.Flags.via_chatlist) 110 | }, update); 111 | case UpdateChatParticipant ucp: 112 | if (NotAllowed(ucp.new_participant.UserId == BotId ? UpdateType.MyChatMember : UpdateType.ChatMember)) return null; 113 | return MakeUpdate(new ChatMemberUpdated 114 | { 115 | Chat = await ChatOrResolve(ucp.chat_id), 116 | From = await UserOrResolve(ucp.actor_id), 117 | Date = ucp.date, 118 | OldChatMember = ucp.prev_participant.ChatMember(await UserOrResolve(ucp.prev_participant.UserId)), 119 | NewChatMember = ucp.new_participant.ChatMember(await UserOrResolve(ucp.new_participant.UserId)), 120 | InviteLink = await MakeChatInviteLink(ucp.invite) 121 | }, update); 122 | case UpdateBotStopped ubs: 123 | if (NotAllowed(ubs.user_id == BotId ? UpdateType.MyChatMember : UpdateType.ChatMember)) return null; 124 | var user = await UserOrResolve(ubs.user_id); 125 | var cmMember = new ChatMemberMember { User = user }; 126 | var cmBanned = new ChatMemberBanned { User = user }; 127 | return MakeUpdate(new ChatMemberUpdated 128 | { 129 | Chat = user.Chat(), 130 | From = user, 131 | Date = ubs.date, 132 | OldChatMember = ubs.stopped ? cmMember : cmBanned, 133 | NewChatMember = ubs.stopped ? cmBanned : cmMember 134 | }, update); 135 | case UpdateMessagePoll ump: 136 | if (NotAllowed(UpdateType.Poll)) return null; 137 | return new Update { Poll = MakePoll(ump.poll, ump.results), TLUpdate = update }; 138 | case UpdateMessagePollVote umpv: 139 | if (NotAllowed(UpdateType.PollAnswer)) return null; 140 | return new Update 141 | { 142 | PollAnswer = new Telegram.Bot.Types.PollAnswer 143 | { 144 | PollId = umpv.poll_id.ToString(), 145 | VoterChat = umpv.peer is PeerChannel pc ? await ChannelOrResolve(pc.channel_id) : null, 146 | User = umpv.peer is PeerUser pu ? await UserOrResolve(pu.user_id) : null, 147 | OptionIds = [.. umpv.options.Select(o => (int)o[0])] 148 | }, 149 | TLUpdate = update 150 | }; 151 | case TL.UpdateBotChatInviteRequester ubcir: 152 | if (NotAllowed(UpdateType.ChatJoinRequest)) return null; 153 | return new Update 154 | { 155 | ChatJoinRequest = new Telegram.Bot.Types.ChatJoinRequest 156 | { 157 | Chat = (await ChatFromPeer(ubcir.peer))!, 158 | From = await UserOrResolve(ubcir.user_id), 159 | Date = ubcir.date, 160 | Bio = ubcir.about, 161 | UserChatId = ubcir.user_id, 162 | InviteLink = await MakeChatInviteLink(ubcir.invite) 163 | }, 164 | TLUpdate = update 165 | }; 166 | case TL.UpdateBotShippingQuery ubsq: 167 | if (NotAllowed(UpdateType.ShippingQuery)) return null; 168 | return new Update 169 | { 170 | ShippingQuery = new Telegram.Bot.Types.Payments.ShippingQuery 171 | { 172 | Id = ubsq.query_id.ToString(), 173 | From = await UserOrResolve(ubsq.user_id), 174 | InvoicePayload = Encoding.UTF8.GetString(ubsq.payload), 175 | ShippingAddress = ubsq.shipping_address.ShippingAddress() 176 | }, 177 | TLUpdate = update 178 | }; 179 | case TL.UpdateBotPrecheckoutQuery ubpq: 180 | if (NotAllowed(UpdateType.PreCheckoutQuery)) return null; 181 | return new Update 182 | { 183 | PreCheckoutQuery = new Telegram.Bot.Types.Payments.PreCheckoutQuery 184 | { 185 | Id = ubpq.query_id.ToString(), 186 | From = await UserOrResolve(ubpq.user_id), 187 | Currency = ubpq.currency, 188 | TotalAmount = (int)ubpq.total_amount, 189 | InvoicePayload = Encoding.UTF8.GetString(ubpq.payload), 190 | ShippingOptionId = ubpq.shipping_option_id, 191 | OrderInfo = ubpq.info.OrderInfo() 192 | }, 193 | TLUpdate = update 194 | }; 195 | case TL.UpdateBotBusinessConnect ubbc: 196 | if (NotAllowed(UpdateType.BusinessConnection)) return null; 197 | return new Update { BusinessConnection = await MakeBusinessConnection(ubbc.connection), TLUpdate = update }; 198 | case TL.UpdateBotNewBusinessMessage ubnbm: 199 | if (NotAllowed(UpdateType.BusinessMessage)) return null; 200 | var replyToMessage = await MakeMessage(ubnbm.reply_to_message); 201 | if (replyToMessage != null) replyToMessage.BusinessConnectionId = ubnbm.connection_id; 202 | message = await MakeMessageAndReply(ubnbm.message, replyToMessage, ubnbm.connection_id); 203 | return message == null ? null : new Update { BusinessMessage = message, TLUpdate = update }; 204 | case TL.UpdateBotEditBusinessMessage ubebm: 205 | if (NotAllowed(UpdateType.EditedBusinessMessage)) return null; 206 | replyToMessage = await MakeMessage(ubebm.reply_to_message); 207 | if (replyToMessage != null) replyToMessage.BusinessConnectionId = ubebm.connection_id; 208 | message = await MakeMessageAndReply(ubebm.message, replyToMessage, ubebm.connection_id); 209 | return message == null ? null : new Update { EditedBusinessMessage = message, TLUpdate = update }; 210 | case TL.UpdateBotDeleteBusinessMessage ubdbm: 211 | if (NotAllowed(UpdateType.DeletedBusinessMessages)) return null; 212 | return new Update 213 | { 214 | DeletedBusinessMessages = new BusinessMessagesDeleted 215 | { 216 | BusinessConnectionId = ubdbm.connection_id, 217 | Chat = await ChatFromPeer(ubdbm.peer, true), 218 | MessageIds = ubdbm.messages 219 | }, 220 | TLUpdate = update 221 | }; 222 | case TL.UpdateBotMessageReaction ubmr: 223 | if (NotAllowed(UpdateType.MessageReaction)) return null; 224 | return new Update 225 | { 226 | MessageReaction = new MessageReactionUpdated 227 | { 228 | Chat = await ChatFromPeer(ubmr.peer, true), 229 | MessageId = ubmr.msg_id, 230 | User = await UserFromPeer(ubmr.actor), 231 | ActorChat = await ChatFromPeer(ubmr.actor), 232 | Date = ubmr.date, 233 | OldReaction = [.. ubmr.old_reactions.Select(TypesTLConverters.ReactionType)], 234 | NewReaction = [.. ubmr.new_reactions.Select(TypesTLConverters.ReactionType)], 235 | }, 236 | TLUpdate = update 237 | }; 238 | case TL.UpdateBotMessageReactions ubmrs: 239 | if (NotAllowed(UpdateType.MessageReactionCount)) return null; 240 | return new Update 241 | { 242 | MessageReactionCount = new MessageReactionCountUpdated 243 | { 244 | Chat = await ChatFromPeer(ubmrs.peer, true), 245 | MessageId = ubmrs.msg_id, 246 | Date = ubmrs.date, 247 | Reactions = [.. ubmrs.reactions.Select(rc => new Telegram.Bot.Types.ReactionCount { Type = rc.reaction.ReactionType(), TotalCount = rc.count })], 248 | }, 249 | TLUpdate = update 250 | }; 251 | case TL.UpdateBotChatBoost ubcb: 252 | bool expired = ubcb.boost.expires < ubcb.boost.date; 253 | if (NotAllowed(expired ? UpdateType.RemovedChatBoost : UpdateType.ChatBoost)) return null; 254 | var cb = new ChatBoostUpdated 255 | { 256 | Chat = await ChatFromPeer(ubcb.peer, true), 257 | Boost = await MakeBoost(ubcb.boost) 258 | }; 259 | return new Update 260 | { 261 | ChatBoost = expired ? null : cb, 262 | RemovedChatBoost = !expired ? null : new ChatBoostRemoved 263 | { 264 | Chat = cb.Chat, 265 | BoostId = cb.Boost.BoostId, 266 | RemoveDate = cb.Boost.AddDate, 267 | Source = cb.Boost.Source, 268 | }, 269 | TLUpdate = update 270 | }; 271 | case TL.UpdateBotPurchasedPaidMedia ubppm: 272 | if (NotAllowed(UpdateType.PurchasedPaidMedia)) return null; 273 | return new Update 274 | { 275 | PurchasedPaidMedia = new PaidMediaPurchased 276 | { 277 | From = await UserOrResolve(ubppm.user_id), 278 | PaidMediaPayload = ubppm.payload, 279 | }, 280 | TLUpdate = update 281 | }; 282 | //TL.UpdateDraftMessage seems used to update ourself user info 283 | default: 284 | return null; 285 | } 286 | } 287 | 288 | private Update? MakeUpdate(ChatMemberUpdated chatMember, TL.Update update) => chatMember.NewChatMember?.User.Id == BotId 289 | ? new Update { MyChatMember = chatMember, TLUpdate = update } 290 | : new Update { ChatMember = chatMember, TLUpdate = update }; 291 | 292 | [return: NotNullIfNotNull(nameof(invite))] 293 | private async Task MakeChatInviteLink(ExportedChatInvite? invite) 294 | => invite switch 295 | { 296 | null => null, 297 | ChatInvitePublicJoinRequests => null, 298 | ChatInviteExported cie => new ChatInviteLink 299 | { 300 | InviteLink = cie.link, 301 | Creator = await UserOrResolve(cie.admin_id), 302 | CreatesJoinRequest = cie.flags.HasFlag(ChatInviteExported.Flags.request_needed), 303 | IsPrimary = cie.flags.HasFlag(ChatInviteExported.Flags.permanent), 304 | IsRevoked = cie.flags.HasFlag(ChatInviteExported.Flags.revoked), 305 | Name = cie.title, 306 | ExpireDate = cie.expire_date.NullIfDefault(), 307 | MemberLimit = cie.usage_limit.NullIfZero(), 308 | PendingJoinRequestCount = cie.flags.HasFlag(ChatInviteExported.Flags.has_requested) ? cie.requested : null, 309 | SubscriptionPeriod = cie.subscription_pricing?.period, 310 | SubscriptionPrice = cie.subscription_pricing is { amount: var amount } ? (int)amount : null, 311 | }, 312 | _ => throw new WTException("Unexpected ExportedChatInvite: " + invite) 313 | }; 314 | 315 | /// User or a stub on failure 316 | public async Task UserOrResolve(long userId) 317 | { 318 | lock (_users) 319 | if (_users.TryGetValue(userId, out var user)) 320 | return user; 321 | try 322 | { 323 | var users = await Client.Users_GetUsers(new InputUser(userId, 0)); 324 | if (users.Length != 0 && users[0] is TL.User user) 325 | lock (_users) 326 | return _users[userId] = user.User(); 327 | } 328 | catch (RpcException) { } 329 | return new User { Id = userId, FirstName = "" }; 330 | } 331 | 332 | /// null if peer is not PeerUser ; User or a stub on failure 333 | private async Task UserFromPeer(Peer peer) => peer is not PeerUser pu ? null : await UserOrResolve(pu.user_id); 334 | 335 | private async Task ChannelOrResolve(long id) 336 | { 337 | if (Chat(id) is { } chat) 338 | return chat; 339 | try 340 | { 341 | var chats = await Client.Channels_GetChannels(new InputChannel(id, 0)); 342 | if (chats.chats.TryGetValue(id, out var chatBase)) 343 | lock (_chats) 344 | return _chats[id] = chatBase.Chat(); 345 | } 346 | catch (RpcException) { } 347 | return new Chat { Id = ZERO_CHANNEL_ID - id, Type = ChatType.Supergroup }; 348 | } 349 | 350 | private async Task ChatOrResolve(long chatId) 351 | { 352 | if (Chat(chatId) is { } chat) 353 | return chat; 354 | try 355 | { 356 | var chats = await Client.Messages_GetChats(chatId); 357 | if (chats.chats.TryGetValue(chatId, out var chatBase)) 358 | lock (_chats) 359 | return _chats[chatId] = chatBase.Chat(); 360 | } 361 | catch (RpcException) { } 362 | return new Chat { Id = -chatId, Type = ChatType.Group }; 363 | } 364 | 365 | private async Task ChatFromPeer(Peer? peer, [DoesNotReturnIf(true)] bool allowUser = false) => peer switch 366 | { 367 | null => null, 368 | PeerUser pu => allowUser ? (await UserOrResolve(pu.user_id)).Chat() : null, 369 | PeerChannel pc => await ChannelOrResolve(pc.channel_id), 370 | _ => await ChatOrResolve(peer.ID), 371 | }; 372 | 373 | private async Task ChatFromPeer(InputPeer peer) => peer switch 374 | { 375 | InputPeerUser pu => (await UserOrResolve(pu.user_id)).Chat(), 376 | InputPeerChannel ipc => await ChannelOrResolve(ipc.channel_id), 377 | _ => await ChatOrResolve(peer.ID) 378 | }; 379 | 380 | /// Handle UpdatesBase returned by various Client API and build the returned Bot Message 381 | protected async Task PostedMsg(Task updatesTask, InputPeer peer, string? text = null, Message? replyToMessage = null, string? bConnId = null) 382 | { 383 | var updates = await updatesTask; 384 | updates.UserOrChat(_collector); 385 | if (updates is UpdateShortSentMessage sent) 386 | return await FillTextAndMedia(new Message 387 | { 388 | Id = sent.id, 389 | From = await UserOrResolve(BotId), 390 | Date = sent.date, 391 | Chat = await ChatFromPeer(peer)!, 392 | ReplyToMessage = replyToMessage 393 | }, text, sent.entities, sent.media); 394 | foreach (var update in updates.UpdateList) 395 | { 396 | switch (update) 397 | { 398 | case UpdateNewMessage { message: { } message }: return (await MakeMessageAndReply(message, replyToMessage))!; 399 | case UpdateNewScheduledMessage { message: { } schedMsg }: return (await MakeMessageAndReply(schedMsg, replyToMessage))!; 400 | case UpdateEditMessage { message: { } editMsg }: return (await MakeMessageAndReply(editMsg, replyToMessage))!; 401 | case UpdateBotNewBusinessMessage { message: { } bizMsg }: return (await MakeMessageAndReply(bizMsg, replyToMessage, bConnId))!; 402 | } 403 | } 404 | throw new WTException("Failed to retrieve sent message"); 405 | } 406 | 407 | private async Task PostedMsgs(Task updatesTask, int nbMsg, long startRandomId, Message? replyToMessage, string? bConnId = null) 408 | { 409 | var updates = await updatesTask; 410 | updates.UserOrChat(_collector); 411 | var result = new List(nbMsg); 412 | foreach (var update in updates.UpdateList) 413 | { 414 | Message? msg = null; 415 | switch (update) 416 | { 417 | case UpdateNewMessage { message: TL.Message message }: msg = await MakeMessageAndReply(message, replyToMessage); break; 418 | case UpdateNewScheduledMessage { message: TL.Message schedMsg }: msg = await MakeMessageAndReply(schedMsg, replyToMessage); break; 419 | case UpdateBotNewBusinessMessage { message: { } bizMsg } biz: msg = await MakeMessageAndReply(bizMsg, replyToMessage, bConnId); break; 420 | } 421 | if (msg != null) result.Add(msg); 422 | } 423 | return [.. result.OrderBy(msg => msg.MessageId)]; 424 | } 425 | 426 | /// Converts Client API TL.MessageBase to Bot Telegram.Bot.Types.Message and assign the ReplyToMessage/ExternalReply 427 | public async Task MakeMessageAndReply(MessageBase? msgBase, Message? replyToMessage = null, string? bConnId = null) 428 | { 429 | var msg = await MakeMessage(msgBase); 430 | if (msg == null) return null; 431 | msg.BusinessConnectionId = bConnId; 432 | if (msgBase?.ReplyTo == null) return msg; 433 | if (msgBase.ReplyTo is MessageReplyHeader reply_to) 434 | { 435 | if (replyToMessage != null) 436 | msg.ReplyToMessage = replyToMessage; 437 | else if (reply_to.reply_to_msg_id > 0 && reply_to.reply_from == null) 438 | { 439 | var replyToPeer = reply_to.reply_to_peer_id ?? msgBase.Peer; 440 | msg.ReplyToMessage = await GetMessage(await ChatFromPeer(replyToPeer, true), reply_to.reply_to_msg_id); 441 | } 442 | else if (reply_to.reply_to_top_id > 0) 443 | msg.ReplyToMessage = await GetMessage(await ChatFromPeer(msgBase.Peer, true), reply_to.reply_to_top_id); 444 | if (reply_to.reply_from?.date > default(DateTime)) 445 | { 446 | var ext = await FillTextAndMedia(new Message(), null, null!, reply_to.reply_media); 447 | msg.ExternalReply = new ExternalReplyInfo 448 | { 449 | MessageId = reply_to.reply_to_msg_id, 450 | Chat = await ChatFromPeer(reply_to.reply_to_peer_id), 451 | HasMediaSpoiler = ext.HasMediaSpoiler, 452 | LinkPreviewOptions = ext.LinkPreviewOptions, 453 | Origin = (await MakeOrigin(reply_to.reply_from))!, 454 | Animation = ext.Animation, Audio = ext.Audio, Contact = ext.Contact, Dice = ext.Dice, Document = ext.Document, 455 | Game = ext.Game, Giveaway = ext.Giveaway, GiveawayWinners = ext.GiveawayWinners, Invoice = ext.Invoice, 456 | Location = ext.Location, Photo = ext.Photo, Poll = ext.Poll, Sticker = ext.Sticker, Story = ext.Story, 457 | Venue = ext.Venue, Video = ext.Video, VideoNote = ext.VideoNote, Voice = ext.Voice, PaidMedia = ext.PaidMedia 458 | }; 459 | } 460 | if (reply_to.quote_text != null) 461 | msg.Quote = new TextQuote 462 | { 463 | Text = reply_to.quote_text, 464 | Entities = MakeEntities(reply_to.quote_entities), 465 | Position = reply_to.quote_offset, 466 | IsManual = reply_to.flags.HasFlag(MessageReplyHeader.Flags.quote) 467 | }; 468 | if (msg.IsTopicMessage |= reply_to.flags.HasFlag(MessageReplyHeader.Flags.forum_topic)) 469 | msg.MessageThreadId = reply_to.reply_to_top_id > 0 ? reply_to.reply_to_top_id : reply_to.reply_to_msg_id; 470 | } 471 | else if (msgBase.ReplyTo is MessageReplyStoryHeader mrsh) 472 | msg.ReplyToStory = new Story 473 | { 474 | Chat = await ChatFromPeer(mrsh.peer, true), 475 | Id = mrsh.story_id 476 | }; 477 | return msg; 478 | } 479 | 480 | /// Converts Client API TL.MessageBase to Bot Telegram.Bot.Types.Message 481 | [return: NotNullIfNotNull(nameof(msgBase))] 482 | protected async Task MakeMessage(MessageBase? msgBase) 483 | { 484 | switch (msgBase) 485 | { 486 | case TL.Message message: 487 | var msg = new WTelegram.Types.Message 488 | { 489 | TLMessage = message, 490 | Id = message.flags.HasFlag(TL.Message.Flags.from_scheduled) ? 0 : message.id, 491 | From = await UserFromPeer(message.from_id), 492 | SenderChat = await ChatFromPeer(message.from_id), 493 | Date = message.date, 494 | Chat = await ChatFromPeer(message.peer_id, allowUser: true), 495 | AuthorSignature = message.post_author, 496 | ReplyMarkup = message.reply_markup.InlineKeyboardMarkup(), 497 | SenderBoostCount = message.from_boosts_applied > 0 ? message.from_boosts_applied : null, 498 | SenderBusinessBot = User(message.via_business_bot_id), 499 | IsFromOffline = message.flags2.HasFlag(TL.Message.Flags2.offline), 500 | EffectId = message.flags2.HasFlag(TL.Message.Flags2.has_effect) ? message.effect.ToString() : null, 501 | PaidStarCount = message.paid_message_stars.IntIfPositive() 502 | }; 503 | if (message.fwd_from is { } fwd) 504 | { 505 | msg.ForwardOrigin = await MakeOrigin(fwd); 506 | msg.IsAutomaticForward = msg.Chat.Type == ChatType.Supergroup && await ChatFromPeer(fwd.saved_from_peer) is Chat { Type: ChatType.Channel } && fwd.saved_from_msg_id != 0; 507 | } 508 | await FixMsgFrom(msg, message.from_id, message.peer_id); 509 | if (message.via_bot_id != 0) msg.ViaBot = await UserOrResolve(message.via_bot_id); 510 | if (message.edit_date != default) msg.EditDate = message.edit_date; 511 | if (message.flags.HasFlag(TL.Message.Flags.noforwards)) msg.HasProtectedContent = true; 512 | if (message.grouped_id != 0) msg.MediaGroupId = message.grouped_id.ToString(); 513 | return CacheMessage(await FillTextAndMedia(msg, message.message, message.entities, message.media, message.flags.HasFlag(TL.Message.Flags.invert_media)), msgBase); 514 | case TL.MessageService msgSvc: 515 | msg = new WTelegram.Types.Message 516 | { 517 | TLMessage = msgSvc, 518 | Id = msgSvc.id, 519 | From = await UserFromPeer(msgSvc.from_id), 520 | SenderChat = await ChatFromPeer(msgSvc.from_id), 521 | Date = msgSvc.date, 522 | Chat = await ChatFromPeer(msgSvc.peer_id, allowUser: true), 523 | }; 524 | if (msgSvc.action is MessageActionTopicCreate) 525 | { 526 | msg.IsTopicMessage = true; 527 | msg.MessageThreadId = msgSvc.id; 528 | } 529 | await FixMsgFrom(msg, msgSvc.from_id, msgSvc.peer_id); 530 | if (await MakeServiceMessage(msgSvc, msg) == null) return CacheMessage(null, msgBase); 531 | return CacheMessage(msg, msgBase); 532 | case null: 533 | return null; 534 | default: 535 | return CacheMessage(new WTelegram.Types.Message 536 | { 537 | TLMessage = msgBase, 538 | Id = msgBase.ID, 539 | Chat = await ChatFromPeer(msgBase.Peer, allowUser: true)!, 540 | }, msgBase); 541 | } 542 | 543 | async Task FixMsgFrom(Message msg, Peer from_id, Peer peer_id) 544 | { 545 | if (msg.From == null) 546 | switch (msg.Chat.Type) 547 | { 548 | case ChatType.Channel: break; 549 | case ChatType.Private: 550 | msg.From = await UserFromPeer(peer_id); 551 | break; 552 | default: 553 | if (from_id == null) 554 | { 555 | msg.From = GroupAnonymousBot; 556 | msg.SenderChat = msg.Chat; 557 | } 558 | else if (msg.IsAutomaticForward == true) 559 | msg.From = ServiceNotification; 560 | break; 561 | } 562 | } 563 | } 564 | 565 | private async Task MakeOrigin(MessageFwdHeader fwd) 566 | { 567 | MessageOrigin? origin = fwd.from_id switch 568 | { 569 | PeerUser pu => new MessageOriginUser { SenderUser = await UserOrResolve(pu.user_id) }, 570 | PeerChat pc => new MessageOriginChat { SenderChat = await ChatOrResolve(pc.chat_id), AuthorSignature = fwd.post_author }, 571 | PeerChannel pch => new MessageOriginChannel 572 | { 573 | Chat = await ChannelOrResolve(pch.channel_id), 574 | AuthorSignature = fwd.post_author, 575 | MessageId = fwd.channel_post 576 | }, 577 | _ => fwd.from_name != null ? new MessageOriginHiddenUser { SenderUserName = fwd.from_name } : null 578 | }; 579 | if (origin != null) origin.Date = fwd.date; 580 | return origin; 581 | } 582 | 583 | private async Task FillTextAndMedia(Message msg, string? text, TL.MessageEntity[] entities, MessageMedia media, bool invert_media = false) 584 | { 585 | switch (media) 586 | { 587 | case null: 588 | if (entities?.Any(e => e is MessageEntityUrl or MessageEntityTextUrl) == true) 589 | msg.LinkPreviewOptions = new LinkPreviewOptions { IsDisabled = true }; 590 | msg.Text = text; 591 | msg.Entities = MakeEntities(entities); 592 | return msg; 593 | case MessageMediaWebPage mmwp: 594 | msg.LinkPreviewOptions = mmwp.LinkPreviewOptions(invert_media); 595 | msg.Text = text; 596 | msg.Entities = MakeEntities(entities); 597 | return msg; 598 | case MessageMediaDocument { document: TL.Document document } mmd: 599 | if (mmd.flags.HasFlag(MessageMediaDocument.Flags.spoiler)) msg.HasMediaSpoiler = true; 600 | msg.ShowCaptionAboveMedia = invert_media; 601 | var thumb = document.LargestThumbSize; 602 | if (mmd.flags.HasFlag(MessageMediaDocument.Flags.voice)) 603 | { 604 | var audio = document.GetAttribute(); 605 | msg.Voice = new Telegram.Bot.Types.Voice 606 | { 607 | FileSize = document.size, 608 | Duration = (int)(audio?.duration + 0.5 ?? 0.0), 609 | MimeType = document.mime_type 610 | }.SetFileIds(document.ToFileLocation(), document.dc_id); 611 | } 612 | else if (mmd.flags.HasFlag(MessageMediaDocument.Flags.round)) 613 | { 614 | var video = document.GetAttribute(); 615 | msg.VideoNote = new Telegram.Bot.Types.VideoNote 616 | { 617 | FileSize = document.size, 618 | Length = video?.w ?? 0, 619 | Duration = (int)(video?.duration + 0.5 ?? 0.0), 620 | Thumbnail = thumb?.PhotoSize(document.ToFileLocation(thumb), document.dc_id) 621 | }.SetFileIds(document.ToFileLocation(), document.dc_id); 622 | } 623 | else if (mmd.flags.HasFlag(MessageMediaDocument.Flags.video)) 624 | msg.Video = document.Video(mmd); 625 | else if (document.GetAttribute() is { } audio) 626 | { 627 | msg.Audio = new Telegram.Bot.Types.Audio 628 | { 629 | FileSize = document.size, 630 | Duration = (int)(audio?.duration + 0.5 ?? 0.0), 631 | Performer = audio?.performer, 632 | Title = audio?.title, 633 | FileName = document.Filename, 634 | MimeType = document.mime_type, 635 | Thumbnail = thumb?.PhotoSize(document.ToFileLocation(thumb), document.dc_id) 636 | }.SetFileIds(document.ToFileLocation(), document.dc_id); 637 | } 638 | else if (document.GetAttribute() is { } sticker) 639 | { 640 | msg.Sticker = await MakeSticker(document, sticker); 641 | } 642 | else 643 | { 644 | msg.Document = document.Document(thumb); 645 | if (document.GetAttribute() != null) 646 | msg.Animation = MakeAnimation(msg.Document!, document.GetAttribute()); 647 | } 648 | break; 649 | case MessageMediaPhoto { photo: TL.Photo photo } mmp: 650 | if (mmp.flags.HasFlag(MessageMediaPhoto.Flags.spoiler)) msg.HasMediaSpoiler = true; 651 | msg.ShowCaptionAboveMedia = invert_media; 652 | msg.Photo = photo.PhotoSizes(); 653 | break; 654 | case MessageMediaVenue mmv: 655 | msg.Venue = new Venue 656 | { 657 | Location = mmv.geo.Location(), 658 | Title = mmv.title, 659 | Address = mmv.address, 660 | FoursquareId = mmv.provider == "foursquare" ? mmv.venue_id : null, 661 | FoursquareType = mmv.provider == "foursquare" ? mmv.venue_type : null, 662 | GooglePlaceId = mmv.provider == "gplaces" ? mmv.venue_id : null, 663 | GooglePlaceType = mmv.provider == "gplaces" ? mmv.venue_id : null 664 | }; 665 | break; 666 | case MessageMediaContact mmc: 667 | msg.Contact = new Telegram.Bot.Types.Contact 668 | { 669 | PhoneNumber = mmc.phone_number, 670 | FirstName = mmc.first_name, 671 | LastName = mmc.last_name, 672 | UserId = mmc.user_id, 673 | Vcard = mmc.vcard, 674 | }; 675 | break; 676 | case MessageMediaGeo mmg: 677 | msg.Location = mmg.geo.Location(); 678 | break; 679 | case MessageMediaGeoLive mmgl: 680 | msg.Location = mmgl.geo.Location(); 681 | msg.Location.LivePeriod = mmgl.period; 682 | msg.Location.Heading = mmgl.flags.HasFlag(MessageMediaGeoLive.Flags.has_heading) ? mmgl.heading : null; 683 | msg.Location.ProximityAlertRadius = mmgl.flags.HasFlag(MessageMediaGeoLive.Flags.has_proximity_notification_radius) ? mmgl.proximity_notification_radius : null; 684 | break; 685 | case MessageMediaPoll { poll: TL.Poll poll, results: TL.PollResults pollResults }: 686 | msg.Poll = MakePoll(poll, pollResults); 687 | return msg; 688 | case MessageMediaDice mmd: 689 | msg.Dice = new Dice { Emoji = mmd.emoticon, Value = mmd.value }; 690 | return msg; 691 | case MessageMediaInvoice mmi: 692 | msg.Invoice = new Telegram.Bot.Types.Payments.Invoice 693 | { 694 | Title = mmi.title, 695 | Description = mmi.description, 696 | StartParameter = mmi.start_param, 697 | Currency = mmi.currency, 698 | TotalAmount = (int)mmi.total_amount 699 | }; 700 | return msg; 701 | case MessageMediaGame mmg: 702 | msg.Game = new Telegram.Bot.Types.Game 703 | { 704 | Title = mmg.game.title, 705 | Description = mmg.game.description, 706 | Photo = mmg.game.photo.PhotoSizes()!, 707 | Text = text.NullIfEmpty(), 708 | TextEntities = MakeEntities(entities) 709 | }; 710 | if (mmg.game.document is TL.Document doc && doc.GetAttribute() != null) 711 | { 712 | thumb = doc.LargestThumbSize; 713 | msg.Game.Animation = MakeAnimation(doc.Document(thumb)!, doc.GetAttribute()); 714 | } 715 | return msg; 716 | case MessageMediaStory mms: 717 | msg.Story = new Story 718 | { 719 | Chat = await ChatFromPeer(mms.peer, true), 720 | Id = mms.id 721 | }; 722 | break; 723 | case MessageMediaGiveaway mmg: 724 | msg.Giveaway = new Giveaway 725 | { 726 | Chats = await mmg.channels.Select(ChannelOrResolve).WhenAllSequential(), 727 | WinnersSelectionDate = mmg.until_date, 728 | WinnerCount = mmg.quantity, 729 | OnlyNewMembers = mmg.flags.HasFlag(MessageMediaGiveaway.Flags.only_new_subscribers), 730 | HasPublicWinners = mmg.flags.HasFlag(MessageMediaGiveaway.Flags.winners_are_visible), 731 | PrizeDescription = mmg.prize_description, 732 | CountryCodes = mmg.countries_iso2, 733 | PremiumSubscriptionMonthCount = mmg.months.NullIfZero(), 734 | PrizeStarCount = ((int)mmg.stars).NullIfZero(), 735 | }; 736 | break; 737 | case MessageMediaGiveawayResults mmgr: 738 | msg.GiveawayWinners = new GiveawayWinners 739 | { 740 | Chat = await ChannelOrResolve(mmgr.channel_id), 741 | GiveawayMessageId = mmgr.launch_msg_id, 742 | WinnersSelectionDate = mmgr.until_date, 743 | WinnerCount = mmgr.winners_count, 744 | Winners = await mmgr.winners.Select(UserOrResolve).WhenAllSequential(), 745 | AdditionalChatCount = mmgr.additional_peers_count, 746 | PremiumSubscriptionMonthCount = mmgr.months, 747 | UnclaimedPrizeCount = mmgr.unclaimed_count, 748 | OnlyNewMembers = mmgr.flags.HasFlag(MessageMediaGiveawayResults.Flags.only_new_subscribers), 749 | WasRefunded = mmgr.flags.HasFlag(MessageMediaGiveawayResults.Flags.refunded), 750 | PrizeDescription = mmgr.prize_description, 751 | PrizeStarCount = ((int)mmgr.stars).NullIfZero(), 752 | }; 753 | break; 754 | case MessageMediaPaidMedia mmpm: 755 | msg.PaidMedia = new PaidMediaInfo 756 | { 757 | StarCount = (int)mmpm.stars_amount, 758 | PaidMedia = [.. mmpm.extended_media.Select(TypesTLConverters.PaidMedia)] 759 | }; 760 | break; 761 | default: 762 | break; 763 | } 764 | if (text != "") msg.Caption = text; 765 | msg.CaptionEntities = MakeEntities(entities); 766 | return msg; 767 | } 768 | 769 | private async Task MakeServiceMessage(MessageService msgSvc, Message msg) 770 | { 771 | return msgSvc.action switch 772 | { 773 | MessageActionChatAddUser macau => msg.NewChatMembers = await macau.users.Select(UserOrResolve).WhenAllSequential(), 774 | MessageActionChatDeleteUser macdu => msg.LeftChatMember = await UserOrResolve(macdu.user_id), 775 | MessageActionChatEditTitle macet => msg.NewChatTitle = macet.title, 776 | MessageActionChatEditPhoto macep => msg.NewChatPhoto = macep.photo.PhotoSizes(), 777 | MessageActionChatDeletePhoto macdp => msg.DeleteChatPhoto = true, 778 | MessageActionChatCreate => msg.GroupChatCreated = true, 779 | MessageActionChannelCreate => (await ChatFromPeer(msgSvc.peer_id))?.Type == ChatType.Channel 780 | ? msg.SupergroupChatCreated = true : msg.ChannelChatCreated = true, 781 | MessageActionSetMessagesTTL macsmt => msg.MessageAutoDeleteTimerChanged = 782 | new MessageAutoDeleteTimerChanged { MessageAutoDeleteTime = macsmt.period }, 783 | MessageActionChatMigrateTo macmt => msg.MigrateToChatId = ZERO_CHANNEL_ID - macmt.channel_id, 784 | MessageActionChannelMigrateFrom macmf => msg.MigrateFromChatId = -macmf.chat_id, 785 | MessageActionPinMessage macpm => msg.PinnedMessage = await GetMIMessage( 786 | await ChatFromPeer(msgSvc.peer_id, allowUser: true), msgSvc.reply_to is MessageReplyHeader mrh ? mrh.reply_to_msg_id : 0), 787 | MessageActionChatJoinedByLink or MessageActionChatJoinedByRequest => msg.NewChatMembers = [msg.From!], 788 | MessageActionPaymentSentMe mapsm => msg.SuccessfulPayment = new Telegram.Bot.Types.Payments.SuccessfulPayment 789 | { 790 | Currency = mapsm.currency, 791 | TotalAmount = (int)mapsm.total_amount, 792 | InvoicePayload = Encoding.UTF8.GetString(mapsm.payload), 793 | ShippingOptionId = mapsm.shipping_option_id, 794 | OrderInfo = mapsm.info.OrderInfo(), 795 | TelegramPaymentChargeId = mapsm.charge.id, 796 | ProviderPaymentChargeId = mapsm.charge.provider_charge_id, 797 | SubscriptionExpirationDate = mapsm.subscription_until_date.NullIfDefault(), 798 | IsRecurring = mapsm.flags.HasFlag(MessageActionPaymentSentMe.Flags.recurring_used), 799 | IsFirstRecurring = mapsm.flags.HasFlag(MessageActionPaymentSentMe.Flags.recurring_init), 800 | }, 801 | MessageActionRequestedPeer { peers.Length: > 0 } marp => marp.peers[0] is PeerUser 802 | ? msg.UsersShared = new UsersShared { RequestId = marp.button_id, Users = marp.peers.Select(p => new SharedUser { UserId = p.ID }).ToArray() } 803 | : msg.ChatShared = new ChatShared { RequestId = marp.button_id, ChatId = marp.peers[0].ToChatId() }, 804 | MessageActionRequestedPeerSentMe { peers.Length: > 0 } marpsm => marpsm.peers[0] is RequestedPeerUser 805 | ? msg.UsersShared = new UsersShared { RequestId = marpsm.button_id, Users = marpsm.peers.Select(p => p.ToSharedUser()).ToArray() } 806 | : msg.ChatShared = marpsm.peers[0].ToSharedChat(marpsm.button_id), 807 | MessageActionBotAllowed maba => maba switch 808 | { 809 | { domain: not null } => msg.ConnectedWebsite = maba.domain, 810 | { app: not null } => msg.WriteAccessAllowed = new WriteAccessAllowed { 811 | WebAppName = maba.app.short_name, 812 | FromRequest = maba.flags.HasFlag(MessageActionBotAllowed.Flags.from_request), 813 | FromAttachmentMenu = maba.flags.HasFlag(MessageActionBotAllowed.Flags.attach_menu) }, 814 | _ => null 815 | }, 816 | MessageActionSecureValuesSentMe masvsm => msg.PassportData = masvsm.PassportData(), 817 | MessageActionGeoProximityReached magpr => msg.ProximityAlertTriggered = new ProximityAlertTriggered 818 | { 819 | Traveler = (await UserFromPeer(magpr.from_id))!, 820 | Watcher = (await UserFromPeer(magpr.to_id))!, 821 | Distance = magpr.distance 822 | }, 823 | MessageActionGroupCallScheduled magcs => msg.VideoChatScheduled = new VideoChatScheduled { StartDate = magcs.schedule_date }, 824 | MessageActionGroupCall magc => magc.flags.HasFlag(MessageActionGroupCall.Flags.has_duration) 825 | ? msg.VideoChatEnded = new VideoChatEnded { Duration = magc.duration } 826 | : msg.VideoChatStarted = new VideoChatStarted(), 827 | MessageActionInviteToGroupCall maitgc => msg.VideoChatParticipantsInvited = new VideoChatParticipantsInvited { 828 | Users = await maitgc.users.Select(UserOrResolve).WhenAllSequential() }, 829 | MessageActionWebViewDataSentMe mawvdsm => msg.WebAppData = new WebAppData { ButtonText = mawvdsm.text, Data = mawvdsm.data }, 830 | MessageActionTopicCreate matc => msg.ForumTopicCreated = new ForumTopicCreated { Name = matc.title, IconColor = matc.icon_color, 831 | IconCustomEmojiId = matc.flags.HasFlag(MessageActionTopicCreate.Flags.has_icon_emoji_id) ? matc.icon_emoji_id.ToString() : null }, 832 | MessageActionTopicEdit mate => mate.flags.HasFlag(MessageActionTopicEdit.Flags.has_closed) ? 833 | mate.closed ? msg.ForumTopicClosed = new() : msg.ForumTopicReopened = new() 834 | : mate.flags.HasFlag(MessageActionTopicEdit.Flags.has_hidden) 835 | ? mate.hidden ? msg.GeneralForumTopicHidden = new() : msg.GeneralForumTopicUnhidden = new() 836 | : msg.ForumTopicEdited = new ForumTopicEdited { Name = mate.title, IconCustomEmojiId = mate.icon_emoji_id != 0 837 | ? mate.icon_emoji_id.ToString() : mate.flags.HasFlag(MessageActionTopicEdit.Flags.has_icon_emoji_id) ? "" : null }, 838 | MessageActionBoostApply maba => msg.BoostAdded = new ChatBoostAdded { BoostCount = maba.boosts }, 839 | MessageActionGiveawayLaunch magl => msg.GiveawayCreated = new GiveawayCreated { 840 | PrizeStarCount = ((int)magl.stars).NullIfZero() 841 | }, 842 | MessageActionGiveawayResults magr => msg.GiveawayCompleted = new GiveawayCompleted { 843 | WinnerCount = magr.winners_count, UnclaimedPrizeCount = magr.unclaimed_count, 844 | GiveawayMessage = msgSvc.reply_to is MessageReplyHeader mrh ? await GetMessage(await ChatFromPeer(msgSvc.peer_id, true), mrh.reply_to_msg_id) : null, 845 | IsStarGiveaway = magr.flags.HasFlag(MessageActionGiveawayResults.Flags.stars) 846 | }, 847 | MessageActionSetChatWallPaper mascwp => msg.ChatBackgroundSet = new ChatBackground { Type = mascwp.wallpaper.BackgroundType() }, 848 | MessageActionPaymentRefunded mapr => msg.RefundedPayment = new RefundedPayment { 849 | Currency = mapr.currency, TotalAmount = (int)mapr.total_amount, 850 | InvoicePayload = mapr.payload.NullOrUtf8() ?? "", 851 | TelegramPaymentChargeId = mapr.charge.id, ProviderPaymentChargeId = mapr.charge.provider_charge_id 852 | }, 853 | MessageActionStarGift masg => masg.gift is not StarGift gift ? null : msg.Gift = new GiftInfo 854 | { 855 | Gift = MakeGift(gift), 856 | OwnedGiftId = masg.peer != null ? $"{masg.peer.ID}_{masg.saved_id}" : msgSvc.id.ToString(), 857 | ConvertStarCount = masg.convert_stars.IntIfPositive(), 858 | PrepaidUpgradeStarCount = masg.upgrade_stars.IntIfPositive(), 859 | CanBeUpgraded = masg.flags.HasFlag(MessageActionStarGift.Flags.can_upgrade), 860 | Text = masg.message?.text, 861 | Entities = MakeEntities(masg.message?.entities), 862 | IsPrivate = masg.flags.HasFlag(MessageActionStarGift.Flags.name_hidden) 863 | }, 864 | MessageActionStarGiftUnique masgu => masgu.flags.HasFlag(MessageActionStarGiftUnique.Flags.refunded) 865 | ? masgu.gift is not StarGift gift ? null : msg.Gift = new GiftInfo { Gift = MakeGift(gift) } 866 | : masgu.gift is not StarGiftUnique giftUnique ? null : msg.UniqueGift = new UniqueGiftInfo { 867 | Gift = await MakeUniqueGift(giftUnique), 868 | Origin = masgu.flags.HasFlag(MessageActionStarGiftUnique.Flags.upgrade) ? "upgrade" : "transfer", 869 | OwnedGiftId = masgu.peer != null ? $"{masgu.peer.ID}_{masgu.saved_id}" : msgSvc.id.ToString(), 870 | TransferStarCount = masgu.flags.HasFlag(MessageActionStarGiftUnique.Flags.has_transfer_stars) ? (int)masgu.transfer_stars : null 871 | }, 872 | MessageActionPaidMessagesPrice mapmp => msg.PaidMessagePriceChanged = new PaidMessagePriceChanged { 873 | PaidMessageStarCount = checked((int)mapmp.stars) }, 874 | _ => null, 875 | }; 876 | } 877 | 878 | private static Animation MakeAnimation(Telegram.Bot.Types.Document msgDoc, DocumentAttributeVideo video) => new() 879 | { 880 | FileSize = msgDoc.FileSize, 881 | Width = video?.w ?? 0, 882 | Height = video?.h ?? 0, 883 | Duration = (int)(video?.duration + 0.5 ?? 0.0), 884 | Thumbnail = msgDoc.Thumbnail, 885 | FileName = msgDoc.FileName, 886 | MimeType = msgDoc.MimeType, 887 | FileId = msgDoc.FileId, 888 | FileUniqueId = msgDoc.FileUniqueId 889 | }; 890 | 891 | private Telegram.Bot.Types.Poll MakePoll(TL.Poll poll, PollResults pollResults) 892 | { 893 | int? correctOption = pollResults.results == null ? null : Array.FindIndex(pollResults.results, pav => pav.flags.HasFlag(PollAnswerVoters.Flags.correct)); 894 | return new Telegram.Bot.Types.Poll 895 | { 896 | Id = poll.id.ToString(), 897 | Question = poll.question.text, 898 | Options = [.. poll.answers.Select((pa, i) => new PollOption { Text = pa.text.text, VoterCount = pollResults.results?[i].voters ?? 0 })], 899 | TotalVoterCount = pollResults.total_voters, 900 | IsClosed = poll.flags.HasFlag(TL.Poll.Flags.closed), 901 | IsAnonymous = !poll.flags.HasFlag(TL.Poll.Flags.public_voters), 902 | Type = poll.flags.HasFlag(TL.Poll.Flags.quiz) ? PollType.Quiz : PollType.Regular, 903 | AllowsMultipleAnswers = poll.flags.HasFlag(TL.Poll.Flags.multiple_choice), 904 | CorrectOptionId = correctOption < 0 ? null : correctOption, 905 | Explanation = pollResults.solution, 906 | ExplanationEntities = MakeEntities(pollResults.solution_entities), 907 | OpenPeriod = poll.close_period.NullIfZero(), 908 | CloseDate = poll.close_date.NullIfDefault() 909 | }; 910 | } 911 | 912 | private TL.PollAnswer MakePollAnswer(InputPollOption ipo, int index) 913 | { 914 | var text = ipo.Text; 915 | var entities = ApplyParse(ipo.TextParseMode, ref text, ipo.TextEntities); 916 | return new() 917 | { 918 | text = new() { text = text, entities = entities }, 919 | option = [(byte)index] 920 | }; 921 | } 922 | } 923 | -------------------------------------------------------------------------------- /src/Bot.cs: -------------------------------------------------------------------------------- 1 | using System.Data.Common; 2 | using System.Reflection; 3 | using TL; 4 | using Chat = WTelegram.Types.Chat; 5 | using Message = WTelegram.Types.Message; 6 | using Update = WTelegram.Types.Update; 7 | using User = WTelegram.Types.User; 8 | 9 | namespace WTelegram; 10 | 11 | /// A client offering the Telegram Bot API via the more powerful Client API 12 | public partial class Bot : IDisposable 13 | { 14 | /// This gives you access to the underlying Client API 15 | public Client Client { get; } 16 | /// The underlying UpdateManager (can be useful as Peer resolver for Client API calls) 17 | public readonly UpdateManager Manager; 18 | /// 19 | public long BotId { get; } 20 | 21 | private Func? _onUpdate; 22 | private Func? _onMessage; 23 | CancellationTokenSource? _receivingEvents; 24 | /// Handler to be called when there is an incoming update 25 | public event Func? OnUpdate { add { _onUpdate += value; StartEventReceiving(); } remove { _onUpdate -= value; StopEventReceiving(); } } 26 | /// Handler to be called when there is an incoming message or edited message 27 | public event Func? OnMessage { add { _onMessage += value; StartEventReceiving(); } remove { _onMessage -= value; StopEventReceiving(); } } 28 | /// Handler to be called when there was a polling error or an exception in your handlers 29 | public event Func? OnError; 30 | 31 | /// Generate Unknown Updates for all raw TL Updates that usually would have been silently ignored by Bot API (see ) 32 | public bool WantUnknownTLUpdates { get; set; } 33 | 34 | internal const long ZERO_CHANNEL_ID = -1000000000000; 35 | static readonly User GroupAnonymousBot = new() { Id = 1087968824, Username = "GroupAnonymousBot", FirstName = "Group", IsBot = true }; 36 | static readonly User ServiceNotification = new() { Id = 777000, FirstName = "Telegram" }; 37 | 38 | /// Task launched from constructor 39 | protected readonly Task _initTask; 40 | private readonly Database _database; 41 | internal readonly Database.CachedTable _chats; 42 | internal readonly Database.CachedTable _users; 43 | private readonly BotCollectorPeer _collector; 44 | private readonly SemaphoreSlim _pendingCounter = new(0); 45 | /// Cache StickerSet ID => Name 46 | protected Dictionary StickerSetNames = []; 47 | /// Cache used by 48 | protected Dictionary<(long peerId, int msgId), Message?> CachedMessages = []; 49 | /// The HttpClient used for HTTP mode, or null for TCP mode. 50 | protected readonly HttpClient? _httpClient; 51 | private const int DefaultAllowedUpdates = 0b111_1110_0101_1111_1111_1110; /// all except Unknown=0, ChatMember=13, MessageReaction=15, MessageReactionCount=16 52 | private bool NotAllowed(UpdateType updateType) => (_state.AllowedUpdates & (1 << (int)updateType)) == 0; 53 | private readonly State _state = new(); 54 | internal class State 55 | { 56 | public List PendingUpdates = []; 57 | public byte[]? SessionData; 58 | public int LastUpdateId; 59 | public int AllowedUpdates = -1; // if GetUpdates is never called, OnMessage/OnUpdate should receive all updates 60 | } 61 | 62 | /// Create a new instance in TCP mode. 63 | /// The bot token 64 | /// API id (see https://my.telegram.org/apps) 65 | /// API hash (see https://my.telegram.org/apps) 66 | /// DB connection for storage and later resume 67 | /// Template for SQL strings (auto-detect by default) 68 | public Bot(string botToken, int apiId, string apiHash, DbConnection dbConnection, SqlCommands sqlCommands = SqlCommands.Detect) 69 | : this(botToken, apiId, apiHash, dbConnection, null, sqlCommands) { } 70 | 71 | /// Create a new instance. 72 | /// The bot token 73 | /// API id (see https://my.telegram.org/apps) 74 | /// API hash (see https://my.telegram.org/apps) 75 | /// DB connection for storage and later resume 76 | /// An , or null for TCP mode 77 | /// Template for SQL strings (auto-detect by default) 78 | public Bot(string botToken, int apiId, string apiHash, DbConnection dbConnection, HttpClient? httpClient, SqlCommands sqlCommands = SqlCommands.Detect) 79 | : this(what => what switch 80 | { 81 | "api_id" => apiId.ToString(), 82 | "api_hash" => apiHash, 83 | "bot_token" => botToken, 84 | "device_model" => "server", 85 | _ => null 86 | }, 87 | dbConnection, sqlCommands == SqlCommands.Detect ? null : Database.DefaultSqlCommands[(int)sqlCommands], httpClient) 88 | { } 89 | 90 | /// Create a new instance. 91 | /// Configuration callback ("MTProxy" can be used for connection) 92 | /// DB connection for storage and later resume 93 | /// SQL queries for your specific DB engine (null for auto-detect) 94 | /// An , or null for TCP mode 95 | public Bot(Func configProvider, DbConnection dbConnection, string[]? sqlCommands = null, HttpClient? httpClient = null) 96 | { 97 | var botToken = configProvider("bot_token") ?? throw new ArgumentNullException(nameof(configProvider), "bot_token is unset"); 98 | BotId = long.Parse(botToken[0..botToken.IndexOf(':')]); 99 | sqlCommands ??= Database.DefaultSqlCommands[(int)Database.DetectType(dbConnection)]; 100 | _collector = new(this); 101 | var version = Assembly.GetExecutingAssembly().GetCustomAttribute()?.InformationalVersion; 102 | Helpers.Log(1, $"WTelegramBot {version} using {dbConnection.GetType().Name} {dbConnection.DataSource}"); 103 | _database = new Database(dbConnection, sqlCommands, _state); 104 | _database.GetTables(out _users, out _chats); 105 | Client = new Client(configProvider, _database.LoadSessionState()); 106 | if ((_httpClient = httpClient) != null) 107 | Client.HttpMode(httpClient); 108 | Client.MTProxyUrl = configProvider("MTProxy"); 109 | Manager = Client.WithUpdateManager(OnTLUpdate, _database.LoadMBoxStates(), _collector); 110 | _initTask = new Task(() => InitLogin(botToken)); 111 | } 112 | 113 | private async Task InitLogin(string botToken) 114 | { 115 | if (Client.UserId != BotId) 116 | { 117 | var me = await Client.LoginBotIfNeeded(botToken); 118 | _collector.Collect([me]); 119 | } 120 | else 121 | await Client.ConnectAsync(true); 122 | try 123 | { 124 | foreach (var (id, update) in _database.LoadTLUpdates().ToList()) 125 | { 126 | var botUpdate = await MakeUpdate(update); 127 | botUpdate ??= new Update { TLUpdate = update }; 128 | botUpdate.Id = id; 129 | _state.PendingUpdates.Add(botUpdate); 130 | } 131 | var bot = User(BotId)!; 132 | Manager.Log(1, $"Connected as @{bot.Username} ({bot.Id}) | LastUpdateId = {_state.LastUpdateId} | {_state.PendingUpdates.Count} pending updates"); 133 | 134 | if (_state.PendingUpdates.Count != 0) 135 | { 136 | _state.LastUpdateId = Math.Max(_state.LastUpdateId, _state.PendingUpdates[^1].Id); 137 | _pendingCounter.Release(); 138 | } 139 | } 140 | catch { } // we can't reconstruct the PendingUpdates, too bad ¯\_(ツ)_/¯ 141 | } 142 | 143 | async Task InitComplete() 144 | { 145 | if (_initTask.Status == TaskStatus.Created) 146 | try { _initTask.Start(); } 147 | catch (InvalidOperationException) { } // already started 148 | await await _initTask; 149 | } 150 | 151 | /// You must call Dispose to properly save state, close connection and dispose resources 152 | public void Dispose() 153 | { 154 | _receivingEvents?.Cancel(); 155 | Client.Dispose(); 156 | SaveState(); 157 | _database.Dispose(); 158 | _receivingEvents?.Dispose(); 159 | _receivingEvents = null; 160 | GC.SuppressFinalize(this); 161 | } 162 | 163 | /// Save current state to database 164 | public void SaveState() 165 | { 166 | _database.SaveMBoxStates(Manager.State); 167 | _database.SaveSessionState(); 168 | lock (_state.PendingUpdates) 169 | _database.SaveTLUpdates(_state.PendingUpdates); 170 | } 171 | 172 | /// Will remove all received updates 173 | public async Task DropPendingUpdates() 174 | { 175 | await GetUpdates(int.MaxValue, 1, 0, null); 176 | } 177 | 178 | /// Use this method to receive incoming updates using long polling 179 | /// Identifier of the first update to be returned, typically the Id of the last update you handled plus one. Negative values are offset from the end of the pending updates queue 180 | /// Limits the number of updates to be retrieved (1-100) 181 | /// Timeout in seconds for long polling. 0 to return immediately. Recommended value: 25 182 | /// A list of the you want your bot to receive. Specify an empty list to receive 183 | /// all update types except . If null, the previous setting will be used. 184 | /// A cancellation token that can be used by other objects or threads to receive notice of cancellation 185 | /// In order to avoid getting duplicate updates, recalculate after each server response 186 | /// An Array of objects is returned. 187 | public async Task GetUpdates(int offset = 0, int limit = 100, int timeout = 0, IEnumerable? allowedUpdates = null, CancellationToken cancellationToken = default) 188 | { 189 | if (offset is -1 or int.MaxValue) // drop updates => stop eventual resync process 190 | await Manager.StopResync(); 191 | if (allowedUpdates != null) 192 | { 193 | var bitset = allowedUpdates.Aggregate(0, (bs, ut) => bs | (1 << (int)ut)); 194 | _state.AllowedUpdates = bitset == 0 ? DefaultAllowedUpdates : bitset < 0 ? -1 : bitset; 195 | } 196 | Update[] result; 197 | limit = limit < 1 ? 1 : limit > 100 ? 100 : limit; 198 | while (_httpClient != null && timeout > 25) 199 | { 200 | result = await GetUpdates(offset, limit, 25, null, cancellationToken); 201 | if (result.Length != 0) return result; 202 | timeout -= 25; 203 | } 204 | timeout *= 1000; 205 | bool endHttpWait = false; 206 | for (int maxWait = 0; ; maxWait = timeout) 207 | { 208 | if (_httpClient != null) 209 | { 210 | // - HttpWait is buggy atm, except the default null behaviour which waits up to 25 seconds. 211 | // - For other timeout we need to call it once with wait_after=0 to obtain existing unfetched updates, then wait_after=timeout to wait for more 212 | // - HttpWait may produce ClientAPI updates that do not translate to BotAPI updates, so StartHttpWait continues the loop 213 | // - HttpWait is not cancellable, so in case we obtain some update in-between (via delayed GetDiff), we mark to end the loop 214 | if (timeout == 0) 215 | await Client.HttpWait(new()); 216 | else if (maxWait != 0) 217 | _ = StartHttpWait(maxWait); 218 | } 219 | if (await _pendingCounter.WaitAsync(maxWait, cancellationToken)) 220 | try 221 | { 222 | if (_httpClient != null && maxWait != 0) endHttpWait = true; 223 | lock (_state.PendingUpdates) 224 | { 225 | if (offset < 0) 226 | _state.PendingUpdates.RemoveRange(0, Math.Max(_state.PendingUpdates.Count + offset, 0)); 227 | else if (_state.PendingUpdates.FindIndex(u => u.Id >= offset) is >= 0 and int index) 228 | _state.PendingUpdates.RemoveRange(0, index); 229 | else 230 | _state.PendingUpdates.Clear(); 231 | if (_state.PendingUpdates.Count != 0) 232 | { 233 | _pendingCounter.Release(); 234 | result = new Update[Math.Min(limit, _state.PendingUpdates.Count)]; 235 | _state.PendingUpdates.CopyTo(0, result, 0, result.Length); 236 | return result; 237 | } 238 | } 239 | } 240 | finally 241 | { 242 | SaveState(); 243 | } 244 | if (maxWait == timeout) break; 245 | } 246 | return []; 247 | 248 | async Task? StartHttpWait(int milliseconds) 249 | { 250 | var endTime = DateTime.UtcNow.AddMilliseconds(milliseconds); 251 | while (!endHttpWait && milliseconds > 0) 252 | { 253 | await Client.HttpWait(milliseconds == 25000 ? null : new() { max_delay = 0, wait_after = milliseconds, max_wait = 0 }); 254 | milliseconds = (int)(endTime - DateTime.UtcNow).TotalMilliseconds; 255 | } 256 | } 257 | } 258 | 259 | private async Task OnTLUpdate(TL.Update update) 260 | { 261 | try { await InitComplete(); } catch { } 262 | var botUpdate = await MakeUpdate(update); 263 | if (botUpdate == null && WantUnknownTLUpdates) 264 | botUpdate = new Update { TLUpdate = update }; 265 | if (botUpdate != null) 266 | { 267 | botUpdate.Id = ++_state.LastUpdateId; 268 | bool wasEmpty; 269 | lock (_state.PendingUpdates) 270 | { 271 | wasEmpty = _state.PendingUpdates.Count == 0; 272 | _state.PendingUpdates.Add(botUpdate); 273 | } 274 | if (wasEmpty) _pendingCounter.Release(); 275 | if (_onUpdate != null || _onMessage != null) 276 | { 277 | try 278 | { 279 | var task = _onMessage == null ? _onUpdate?.Invoke(botUpdate) : botUpdate switch 280 | { 281 | { Message: { } m } => _onMessage?.Invoke((Message)m, UpdateType.Message), 282 | { EditedMessage: { } em } => _onMessage?.Invoke((Message)em, UpdateType.EditedMessage), 283 | { ChannelPost: { } cp } => _onMessage?.Invoke((Message)cp, UpdateType.ChannelPost), 284 | { EditedChannelPost: { } ecp } => _onMessage?.Invoke((Message)ecp, UpdateType.EditedChannelPost), 285 | { BusinessMessage: { } bm } => _onMessage?.Invoke((Message)bm, UpdateType.BusinessMessage), 286 | { EditedBusinessMessage: { } ebm } => _onMessage?.Invoke((Message)ebm, UpdateType.EditedBusinessMessage), 287 | _ => _onUpdate?.Invoke(botUpdate) // if OnMessage is set, we call OnUpdate only for non-message updates 288 | }; 289 | if (task != null) await task.ConfigureAwait(true); 290 | } 291 | catch (Exception ex) 292 | { 293 | var task = OnError?.Invoke(ex, Telegram.Bot.Polling.HandleErrorSource.HandleUpdateError); 294 | if (task != null) await task.ConfigureAwait(true); 295 | else System.Diagnostics.Trace.WriteLine(ex); // fallback logging if OnError is unset 296 | } 297 | return; 298 | } 299 | } 300 | } 301 | 302 | /// Obtain a InputUser from username, or null if resolve failed 303 | public async Task InputUser(string username) 304 | { 305 | username = username.TrimStart('@'); 306 | lock (_users) 307 | if (_users.SearchCache(user => user.Username?.Equals(username, StringComparison.OrdinalIgnoreCase) == true) is User user) 308 | return user; 309 | try 310 | { 311 | var resolved = await Client.Contacts_ResolveUsername(username); 312 | if (resolved.User is { } resolvedUser) 313 | lock (_users) 314 | return _users[resolvedUser.id] = resolvedUser.User(); 315 | } 316 | catch (RpcException) { } 317 | return null; 318 | } 319 | 320 | /// Obtain a InputUser for this user (useful with Client API calls) 321 | public InputUser InputUser(long userId) => User(userId) ?? new InputUser(userId, 0); 322 | /// Obtain a InputPeerUser for this user (useful with Client API calls) 323 | public InputPeerUser InputPeerUser(long userId) => User(userId) ?? new InputPeerUser(userId, 0); 324 | 325 | /// return User if found in known users (DB), or null 326 | public User? User(long userId) 327 | { 328 | if (userId == 0) return null; 329 | lock (_users) 330 | if (_users.TryGetValue(userId, out var user)) 331 | return user; 332 | return null; 333 | } 334 | 335 | /// Obtain a InputChannel for this chat (useful with Client API calls)May throw exception if chat is unknown 336 | public async Task InputChannel(ChatId chatId) 337 | { 338 | await InitComplete(); 339 | if (chatId.Identifier is long id && id >= ZERO_CHANNEL_ID) 340 | throw new WTException("Bad Request: method is available for supergroup and channel chats only"); 341 | return (InputPeerChannel)await InputPeerChat(chatId); 342 | } 343 | 344 | /// return Chat if found in known chats (DB), or null 345 | public Chat? Chat(long chatId) 346 | { 347 | if (chatId < 0) chatId = chatId > ZERO_CHANNEL_ID ? -chatId : ZERO_CHANNEL_ID - chatId; 348 | lock (_chats) 349 | if (_chats.TryGetValue(chatId, out var chat)) 350 | return chat; 351 | return null; 352 | } 353 | 354 | /// Obtain a InputPeerChat for this chat (useful with Client API calls)May throw exception if chat is unknown 355 | public async Task InputPeerChat(ChatId chatId) 356 | { 357 | await InitComplete(); 358 | if (chatId.Identifier is long id) 359 | if (id >= 0) 360 | return InputPeerUser(id); 361 | else if (id > ZERO_CHANNEL_ID) 362 | return new InputPeerChat(-id); 363 | else 364 | { 365 | if (Chat(id = ZERO_CHANNEL_ID - id) is { } chat) 366 | return chat; 367 | var chats = await Client.Channels_GetChannels(new InputChannel(id, 0)); 368 | if (chats.chats.TryGetValue(id, out var chatBase)) 369 | { 370 | lock (_chats) 371 | _chats[id] = chatBase.Chat(); 372 | return chatBase; 373 | } 374 | throw new WTException($"Bad Request: Chat not found {chatId}"); 375 | } 376 | else 377 | { 378 | var username = chatId.Username?.TrimStart('@'); 379 | lock (_chats) 380 | if (_chats.SearchCache(chat => chat.Username?.Equals(username, StringComparison.OrdinalIgnoreCase) == true) is Chat chat) 381 | return chat; 382 | var resolved = await Client.Contacts_ResolveUsername(username); 383 | if (resolved.Chat is { } chatBase) 384 | lock (_chats) 385 | return _chats[chatBase.ID] = chatBase.Chat(); 386 | throw new WTException($"Bad Request: Chat not found {chatId}"); 387 | } 388 | } 389 | 390 | private async Task ParseInlineMsgID(string inlineMessageId) 391 | { 392 | await InitComplete(); 393 | return inlineMessageId.ParseInlineMsgID(); 394 | } 395 | 396 | /// Free up some memory by clearing internal caches that can be reconstructed automaticallyCall this periodically for heavily used bots if you feel too much memory is used by TelegramBotClient 397 | public void ClearCaches() 398 | { 399 | lock (_users) _users.ClearCache(); 400 | lock (_chats) _chats.ClearCache(); 401 | lock (StickerSetNames) StickerSetNames.Clear(); 402 | lock (CachedMessages) CachedMessages.Clear(); 403 | } 404 | 405 | private void StartEventReceiving() 406 | { 407 | if (_httpClient == null) return; 408 | lock (_initTask) 409 | { 410 | if (_receivingEvents != null) return; 411 | _receivingEvents = new CancellationTokenSource(); 412 | } 413 | _ = StartReceiving(); 414 | } 415 | 416 | private void StopEventReceiving() 417 | { 418 | lock (_initTask) 419 | { 420 | if (_receivingEvents == null || _onUpdate != null || _onMessage != null) return; 421 | _receivingEvents?.Cancel(); 422 | _receivingEvents = null; 423 | } 424 | } 425 | 426 | private async Task StartReceiving() // HTTP mode: poll for updates (events are called in OnTLUpdate) 427 | { 428 | _state.AllowedUpdates = -1; 429 | int messageOffset = 0; 430 | while (!_receivingEvents!.IsCancellationRequested) 431 | { 432 | try 433 | { 434 | var updates = await GetUpdates(messageOffset, 100, 25, null, _receivingEvents.Token).ConfigureAwait(false); 435 | if (updates.Length > 0) messageOffset = updates[^1].Id + 1; 436 | } 437 | catch (Exception ex) when (ex is not OperationCanceledException) 438 | { 439 | var task = OnError?.Invoke(ex, Telegram.Bot.Polling.HandleErrorSource.PollingError); 440 | if (task != null) await task.ConfigureAwait(true); 441 | else System.Diagnostics.Trace.WriteLine(ex); // fallback logging if OnError is unset 442 | } 443 | } 444 | } 445 | } 446 | -------------------------------------------------------------------------------- /src/BotCollectorPeer.cs: -------------------------------------------------------------------------------- 1 | using TL; 2 | using System.Collections; 3 | using User = WTelegram.Types.User; 4 | 5 | namespace WTelegram; 6 | 7 | class BotCollectorPeer(Bot client) : Peer, WTelegram.IPeerCollector, IReadOnlyDictionary 8 | { 9 | public override long ID => 0; 10 | protected override IPeerInfo? UserOrChat(Dictionary users, Dictionary chats) 11 | { 12 | if (users != null) Collect(users.Values); 13 | if (chats != null) Collect(chats.Values); 14 | return null; 15 | } 16 | 17 | public void Collect(IEnumerable users) 18 | { 19 | lock (client._users) 20 | foreach (var user in users) 21 | if (user != null) 22 | if (!user.flags.HasFlag(TL.User.Flags.min) || !client._users.TryGetValue(user.id, out var prevUser)) 23 | client._users[user.id] = user.User(); 24 | else 25 | { 26 | prevUser.FirstName ??= user.first_name; 27 | prevUser.LastName ??= user.last_name; 28 | if (user.lang_code != null) prevUser.LanguageCode = user.lang_code; 29 | if (prevUser.IsBot) 30 | { 31 | prevUser.CanJoinGroups = !user.flags.HasFlag(TL.User.Flags.bot_nochats); 32 | prevUser.CanReadAllGroupMessages = user.flags.HasFlag(TL.User.Flags.bot_chat_history); 33 | prevUser.SupportsInlineQueries = user.flags.HasFlag(TL.User.Flags.has_bot_inline_placeholder); 34 | prevUser.CanConnectToBusiness = user.flags2.HasFlag(TL.User.Flags2.bot_business); 35 | } 36 | client._users[user.id] = prevUser; 37 | } 38 | } 39 | public void Collect(IEnumerable chats) 40 | { 41 | lock (client._chats) 42 | foreach (var chat in chats) 43 | if (chat is not Channel channel) 44 | client._chats[chat.ID] = chat.Chat(); 45 | else if (!channel.flags.HasFlag(Channel.Flags.min) || !client._chats.TryGetValue(channel.id, out var prevChat)) 46 | client._chats[channel.id] = channel.Chat(); 47 | else 48 | { 49 | prevChat.Title = channel.title; 50 | prevChat.Username = channel.MainUsername; 51 | client._chats[channel.id] = prevChat; 52 | } 53 | } 54 | 55 | public bool HasUser(long id) { lock (client._users) return client._users.TryGetValue(id, out _); } 56 | public bool HasChat(long id) { lock (client._chats) return client._chats.TryGetValue(id, out _); } 57 | 58 | public TL.User this[long key] => throw new NotImplementedException(); 59 | public IEnumerable Keys => throw new NotImplementedException(); 60 | public IEnumerable Values => throw new NotImplementedException(); 61 | public int Count => throw new NotImplementedException(); 62 | public bool ContainsKey(long key) => throw new NotImplementedException(); 63 | public IEnumerator> GetEnumerator() => throw new NotImplementedException(); 64 | IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException(); 65 | public bool TryGetValue(long key, out TL.User value) // used only for fetching access_hash in Markdown/HtmlToToEntities 66 | { 67 | User? user; 68 | lock (client._users) 69 | if (!client._users.TryGetValue(key, out user)) { value = null!; return false; } 70 | value = new TL.User { id = key, access_hash = user.AccessHash }; 71 | return true; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/BotHelpers.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.Data.Common; 3 | using System.Reflection; 4 | using System.Text; 5 | 6 | namespace WTelegram; 7 | 8 | /// Helpers methods 9 | public static class BotHelpers 10 | { 11 | /// Used to guess MimeType based on file extension, when uploading documents 12 | public static readonly Dictionary ExtToMimeType = new(StringComparer.OrdinalIgnoreCase) 13 | { 14 | [".jpg"] = "image/jpeg", 15 | [".jpeg"] = "image/jpeg", 16 | [".png"] = "image/png", 17 | [".bmp"] = "image/bmp", 18 | [".gif"] = "image/gif", 19 | [".webp"] = "image/webp", 20 | [".webm"] = "video/webm", 21 | [".mp4"] = "video/mp4", 22 | [".mov"] = "video/quicktime", 23 | [".avi"] = "video/x-msvideo", 24 | [".aac"] = "audio/aac", 25 | [".mid"] = "audio/midi", 26 | [".midi"] = "audio/midi", 27 | [".ogg"] = "audio/ogg", 28 | [".mp3"] = "audio/mpeg", 29 | [".wav"] = "audio/x-wav", 30 | [".flac"] = "audio/flac", 31 | [".tgs"] = "application/x-tgsticker", 32 | [".pdf"] = "application/pdf", 33 | }; 34 | 35 | // Task.WhenAll may lead to unnecessary multiple parallel resolve of the same users/stickerset 36 | internal async static Task WhenAllSequential(this IEnumerable> tasks) 37 | { 38 | var result = new List(); 39 | foreach (var task in tasks) 40 | result.Add(await task); 41 | return [.. result]; 42 | } 43 | 44 | internal static string? NullIfEmpty(this string? s) => s == "" ? null : s; 45 | internal static int? NullIfZero(this int i) => i == 0 ? null : i; 46 | internal static DateTime? NullIfDefault(this DateTime d) => d == default ? null : d; 47 | internal static string? NullOrUtf8(this byte[] b) => b == null ? null : Encoding.UTF8.GetString(b); 48 | internal static int? IntIfPositive(this long l) => l > 0 ? checked((int)l) : null; 49 | 50 | internal static void ExecuteSave(this DbCommand cmd) 51 | { 52 | try 53 | { 54 | cmd.ExecuteNonQuery(); 55 | } 56 | catch (Exception ex) 57 | { 58 | Helpers.Log(4, $"{ex.GetType().Name} while saving to DB: {ex.Message} "); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Database.Strings.cs: -------------------------------------------------------------------------------- 1 | using System.Data.Common; 2 | 3 | namespace WTelegram; 4 | 5 | #pragma warning disable CS1591, CA1069 6 | public enum SqlCommands { Detect = -1, Sqlite = 0, Postgresql = 0, SQLServer = 1, MySQL = 2 }; 7 | #pragma warning restore CS1591, CA1069 8 | internal partial class Database 9 | { 10 | const int DbSetup = 0, LoadSession = 1, SaveSession = 2, LoadUpdates = 3, DelUpdates = 4, SaveUpdates = 5; 11 | const int LoadMBox = 6, SaveMBox = 7, LoadUser = 8, SaveUser = 9, LoadChat = 10, SaveChat = 11; 12 | public static readonly string[][] DefaultSqlCommands = 13 | [ 14 | [ // Sqlite or PostgreSQL 15 | /*DbSetup*/ "CREATE TABLE IF NOT EXISTS WTB_MBoxState (MBox BIGINT NOT NULL PRIMARY KEY, pts INT NOT NULL, access_hash BIGINT NOT NULL) ;\n" + 16 | "CREATE TABLE IF NOT EXISTS WTB_Session (Name VARCHAR(32) NOT NULL PRIMARY KEY, Data BYTEA NOT NULL, LastUpdateId INT NOT NULL, AllowedUpdates INT NOT NULL) ;\n" + 17 | "CREATE TABLE IF NOT EXISTS WTB_Updates (Id INT NOT NULL PRIMARY KEY, TLData BYTEA NOT NULL) ;\n" + 18 | "CREATE TABLE IF NOT EXISTS WTB_Users (Id BIGINT NOT NULL PRIMARY KEY, AccessHash BIGINT NOT NULL, Flags INT NOT NULL, FirstName VARCHAR(255) NOT NULL, LastName VARCHAR(255) NOT NULL, Username VARCHAR(255) NOT NULL, LanguageCode VARCHAR(16) NOT NULL) ;\n" + 19 | "CREATE TABLE IF NOT EXISTS WTB_Chats (Id BIGINT NOT NULL PRIMARY KEY, AccessHash BIGINT NOT NULL, Flags INT NOT NULL, FirstName VARCHAR(255) NOT NULL, LastName VARCHAR(255) NOT NULL, Username VARCHAR(255) NOT NULL, Type INT NOT NULL) ;\n", 20 | /*LoadSess*/"SELECT Data, LastUpdateId, AllowedUpdates FROM WTB_Session WHERE Name = 'WTelegramBot'", 21 | /*SaveSess*/"INSERT INTO WTB_Session (Name, Data, LastUpdateId, AllowedUpdates) VALUES ('WTelegramBot', @Data, @LastUpdateId, @AllowedUpdates) ON CONFLICT (Name) DO UPDATE SET Data = EXCLUDED.Data, LastUpdateId = EXCLUDED.LastUpdateId, AllowedUpdates = EXCLUDED.AllowedUpdates", 22 | /*LoadUpd*/ "SELECT Id, TLData FROM WTB_Updates ORDER BY Id", 23 | /*DelUpd*/ "DELETE FROM WTB_Updates", 24 | /*SaveUpd*/ "INSERT INTO WTB_Updates (Id, TLData) VALUES (@Id, @TLData)", 25 | /*LoadMBox*/"SELECT MBox, pts, access_hash FROM WTB_MBoxState", 26 | /*SaveMBox*/"INSERT INTO WTB_MBoxState(mbox, pts, access_hash) VALUES(@MBox, @pts, @access_hash) ON CONFLICT(MBox) DO UPDATE SET pts=EXCLUDED.pts, access_hash=EXCLUDED.access_hash", 27 | /*LoadUser*/"SELECT AccessHash, Flags, FirstName, LastName, Username, LanguageCode FROM WTB_Users WHERE Id = @Id;", 28 | /*SaveUser*/"INSERT INTO WTB_Users (Id, AccessHash, Flags, FirstName, LastName, Username, LanguageCode) VALUES(@Id, @AccessHash, @Flags, @FirstName, @LastName, @Username, @LanguageCode) ON CONFLICT(Id) DO UPDATE SET AccessHash=EXCLUDED.AccessHash, Flags=EXCLUDED.Flags, FirstName=EXCLUDED.FirstName, LastName=EXCLUDED.LastName, Username=EXCLUDED.Username, LanguageCode=EXCLUDED.LanguageCode", 29 | /*LoadChat*/"SELECT AccessHash, Flags, FirstName, LastName, Username, Type FROM WTB_Chats WHERE Id = -@Id OR Id = -1000000000000-@Id;", 30 | /*SaveChat*/"INSERT INTO WTB_Chats (Id, AccessHash, Flags, FirstName, LastName, Username, Type) VALUES(@Id, @AccessHash, @Flags, @FirstName, @LastName, @Username, @Type) ON CONFLICT(Id) DO UPDATE SET AccessHash=EXCLUDED.AccessHash, Flags=EXCLUDED.Flags, FirstName=EXCLUDED.FirstName, LastName=EXCLUDED.LastName, Username=EXCLUDED.Username, Type=EXCLUDED.Type", 31 | ], 32 | [ // SQL Server 33 | /*DbSetup*/ "IF OBJECT_ID('WTB_MBoxState') IS NULL CREATE TABLE WTB_MBoxState (MBox BIGINT NOT NULL PRIMARY KEY, pts INT NOT NULL, access_hash BIGINT NOT NULL) ;\n" + 34 | "IF OBJECT_ID('WTB_Session') IS NULL CREATE TABLE WTB_Session (Name VARCHAR(32) NOT NULL PRIMARY KEY, Data VARBINARY(MAX) NOT NULL, LastUpdateId INT NOT NULL, AllowedUpdates INT NOT NULL) ;\n" + 35 | "IF OBJECT_ID('WTB_Updates') IS NULL CREATE TABLE WTB_Updates (Id INT NOT NULL PRIMARY KEY, TLData VARBINARY(MAX) NOT NULL) ;\n" + 36 | "IF OBJECT_ID('WTB_Users') IS NULL CREATE TABLE WTB_Users (Id BIGINT NOT NULL PRIMARY KEY, AccessHash BIGINT NOT NULL, Flags INT NOT NULL, FirstName VARCHAR(255) NOT NULL, LastName VARCHAR(255) NOT NULL, Username VARCHAR(255) NOT NULL, LanguageCode VARCHAR(16) NOT NULL) ;\n" + 37 | "IF OBJECT_ID('WTB_Chats') IS NULL CREATE TABLE WTB_Chats (Id BIGINT NOT NULL PRIMARY KEY, AccessHash BIGINT NOT NULL, Flags INT NOT NULL, FirstName VARCHAR(255) NOT NULL, LastName VARCHAR(255) NOT NULL, Username VARCHAR(255) NOT NULL, Type INT NOT NULL) ;\n", 38 | /*LoadSess*/"SELECT Data, LastUpdateId, AllowedUpdates FROM WTB_Session WHERE Name = 'WTelegramBot'", 39 | /*SaveSess*/"MERGE INTO WTB_Session USING (VALUES ('WTelegramBot', @Data, @LastUpdateId, @AllowedUpdates)) AS NEW (Name, Data, LastUpdateId, AllowedUpdates) ON WTB_Session.Name = NEW.Name\nWHEN MATCHED THEN UPDATE SET Data = NEW.Data, LastUpdateId = NEW.LastUpdateId, AllowedUpdates = NEW.AllowedUpdates\nWHEN NOT MATCHED THEN INSERT (Name, Data, LastUpdateId, AllowedUpdates) VALUES (NEW.Name, NEW.Data, NEW.LastUpdateId, NEW.AllowedUpdates);", 40 | /*LoadUpd*/ "SELECT Id, TLData FROM WTB_Updates ORDER BY Id", 41 | /*DelUpd*/ "DELETE FROM WTB_Updates", 42 | /*SaveUpd*/ "INSERT INTO WTB_Updates (Id, TLData) VALUES (@Id, @TLData)", 43 | /*LoadMBox*/"SELECT MBox, pts, access_hash FROM WTB_MBoxState", 44 | /*SaveMBox*/"MERGE INTO WTB_MBoxState USING (VALUES (@MBox, @pts, @access_hash)) AS NEW (mbox, pts, access_hash) ON WTB_MBoxState.MBox = NEW.MBox\nWHEN MATCHED THEN UPDATE SET pts=NEW.pts, access_hash=NEW.access_hash\nWHEN NOT MATCHED THEN INSERT (mbox, pts, access_hash) VALUES (NEW.mbox, NEW.pts, NEW.access_hash);", 45 | /*LoadUser*/"SELECT AccessHash, Flags, FirstName, LastName, Username, LanguageCode FROM WTB_Users WHERE Id = @Id;", 46 | /*SaveUser*/"MERGE INTO WTB_Users USING (VALUES (@Id, @AccessHash, @Flags, @FirstName, @LastName, @Username, @LanguageCode)) AS NEW (Id, AccessHash, Flags, FirstName, LastName, Username, LanguageCode) ON WTB_Users.Id = NEW.Id\nWHEN MATCHED THEN UPDATE SET AccessHash=NEW.AccessHash, Flags=NEW.Flags, FirstName=NEW.FirstName, LastName=NEW.LastName, Username=NEW.Username, LanguageCode=NEW.LanguageCode\nWHEN NOT MATCHED THEN INSERT (Id, AccessHash, Flags, FirstName, LastName, Username, LanguageCode) VALUES (NEW.Id, NEW.AccessHash, NEW.Flags, NEW.FirstName, NEW.LastName, NEW.Username, NEW.LanguageCode);", 47 | /*LoadChat*/"SELECT AccessHash, Flags, FirstName, LastName, Username, Type FROM WTB_Chats WHERE Id = -@Id OR Id = -1000000000000-@Id;", 48 | /*SaveChat*/"MERGE INTO WTB_Chats USING (VALUES (@Id, @AccessHash, @Flags, @FirstName, @LastName, @Username, @Type)) AS NEW (Id, AccessHash, Flags, FirstName, LastName, Username, Type) ON WTB_Chats.Id = NEW.Id\nWHEN MATCHED THEN UPDATE SET AccessHash=NEW.AccessHash, Flags=NEW.Flags, FirstName=NEW.FirstName, LastName=NEW.LastName, Username=NEW.Username, Type=NEW.Type\nWHEN NOT MATCHED THEN INSERT (Id, AccessHash, Flags, FirstName, LastName, Username, Type) VALUES (NEW.Id, NEW.AccessHash, NEW.Flags, NEW.FirstName, NEW.LastName, NEW.Username, NEW.Type);", 49 | ], 50 | [ // MySQL 51 | /*DbSetup*/ "CREATE TABLE IF NOT EXISTS WTB_MBoxState (MBox BIGINT NOT NULL PRIMARY KEY, pts INT NOT NULL, access_hash BIGINT NOT NULL) ;\n" + 52 | "CREATE TABLE IF NOT EXISTS WTB_Session (Name VARCHAR(32) NOT NULL PRIMARY KEY, Data BYTEA NOT NULL, LastUpdateId INT NOT NULL, AllowedUpdates INT NOT NULL) ;\n" + 53 | "CREATE TABLE IF NOT EXISTS WTB_Updates (Id INT NOT NULL PRIMARY KEY, TLData BYTEA NOT NULL) ;\n" + 54 | "CREATE TABLE IF NOT EXISTS WTB_Users (Id BIGINT NOT NULL PRIMARY KEY, AccessHash BIGINT NOT NULL, Flags INT NOT NULL, FirstName VARCHAR(255) NOT NULL, LastName VARCHAR(255) NOT NULL, Username VARCHAR(255) NOT NULL, LanguageCode VARCHAR(16) NOT NULL) ;\n" + 55 | "CREATE TABLE IF NOT EXISTS WTB_Chats (Id BIGINT NOT NULL PRIMARY KEY, AccessHash BIGINT NOT NULL, Flags INT NOT NULL, FirstName VARCHAR(255) NOT NULL, LastName VARCHAR(255) NOT NULL, Username VARCHAR(255) NOT NULL, Type INT NOT NULL) ;\n", 56 | /*LoadSess*/"SELECT Data, LastUpdateId, AllowedUpdates FROM WTB_Session WHERE Name = 'WTelegramBot'", 57 | /*SaveSess*/"REPLACE INTO WTB_Session (Name, Data, LastUpdateId, AllowedUpdates) VALUES ('WTelegramBot', @Data, @LastUpdateId, @AllowedUpdates)", 58 | /*LoadUpd*/ "SELECT Id, TLData FROM WTB_Updates ORDER BY Id", 59 | /*DelUpd*/ "DELETE FROM WTB_Updates", 60 | /*SaveUpd*/ "INSERT INTO WTB_Updates (Id, TLData) VALUES (@Id, @TLData)", 61 | /*LoadMBox*/"SELECT MBox, pts, access_hash FROM WTB_MBoxState", 62 | /*SaveMBox*/"REPLACE INTO WTB_MBoxState(mbox, pts, access_hash) VALUES(@MBox, @pts, @access_hash)", 63 | /*LoadUser*/"SELECT AccessHash, Flags, FirstName, LastName, Username, LanguageCode FROM WTB_Users WHERE Id = @Id;", 64 | /*SaveUser*/"REPLACE INTO WTB_Users (Id, AccessHash, Flags, FirstName, LastName, Username, LanguageCode) VALUES(@Id, @AccessHash, @Flags, @FirstName, @LastName, @Username, @LanguageCode)", 65 | /*LoadChat*/"SELECT AccessHash, Flags, FirstName, LastName, Username, Type FROM WTB_Chats WHERE Id = -@Id OR Id = -1000000000000-@Id;", 66 | /*SaveChat*/"REPLACE INTO WTB_Chats (Id, AccessHash, Flags, FirstName, LastName, Username, Type) VALUES(@Id, @AccessHash, @Flags, @FirstName, @LastName, @Username, @Type)", 67 | ] 68 | ]; 69 | 70 | static readonly Dictionary DetectMapping = new() 71 | { 72 | ["sqlite"] = SqlCommands.Sqlite, 73 | ["postgre"] = SqlCommands.Postgresql, 74 | ["pgsql"] = SqlCommands.Postgresql, 75 | [".sqlclient"] = SqlCommands.SQLServer, 76 | ["sqlserver"] = SqlCommands.SQLServer, 77 | ["mysql"] = SqlCommands.MySQL, 78 | }; 79 | 80 | internal static SqlCommands DetectType(DbConnection dbConnection) 81 | { 82 | var type = dbConnection.GetType().FullName!; 83 | foreach (var mapping in DetectMapping) 84 | if (type.IndexOf(mapping.Key, StringComparison.OrdinalIgnoreCase) >= 0) 85 | return mapping.Value; 86 | return 0; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Database.cs: -------------------------------------------------------------------------------- 1 | using System.Data.Common; 2 | using TL; 3 | using Chat = WTelegram.Types.Chat; 4 | using Update = WTelegram.Types.Update; 5 | using User = WTelegram.Types.User; 6 | 7 | namespace WTelegram; 8 | 9 | internal partial class Database : IDisposable 10 | { 11 | private readonly DbConnection _connection; 12 | private readonly DbCommand[] _cmd = new DbCommand[DefaultSqlCommands[0].Length]; 13 | private readonly Bot.State _state; 14 | 15 | public Database(DbConnection connection, string[] sqlCommands, Bot.State state) 16 | { 17 | if (sqlCommands.Length != DefaultSqlCommands[0].Length) 18 | throw new ArgumentException($"Expected {DefaultSqlCommands[0].Length} SQL commands", nameof(sqlCommands)); 19 | _connection = connection; 20 | _state = state; 21 | for (int i = 0; i < sqlCommands.Length; i++) 22 | { 23 | var command = _cmd[i] = _connection.CreateCommand(); 24 | command.CommandText = sqlCommands[i]; 25 | string defCmd = DefaultSqlCommands[0][i]; 26 | for (int at = 0; (at = defCmd.IndexOf('@', at + 1)) > 0;) 27 | { 28 | var end = defCmd.IndexOfAny([',', ' ', ')', ';'], at + 1); 29 | var param = command.CreateParameter(); 30 | param.ParameterName = defCmd[at..end]; 31 | if (!command.Parameters.Contains(param.ParameterName)) 32 | command.Parameters.Add(param); 33 | } 34 | } 35 | connection.Open(); 36 | _cmd[DbSetup].ExecuteNonQuery(); 37 | } 38 | 39 | public void Dispose() 40 | { 41 | foreach (var cmd in _cmd) cmd.Dispose(); 42 | _connection.Dispose(); 43 | } 44 | 45 | internal Stream LoadSessionState() 46 | { 47 | using (var reader = _cmd[LoadSession].ExecuteReader()) 48 | if (reader.Read()) 49 | { 50 | _state.SessionData = reader.GetValue(0) as byte[]; 51 | _state.LastUpdateId = reader.GetInt32(1); 52 | _state.AllowedUpdates = reader.GetInt32(2); 53 | } 54 | return new SessionStore(_state.SessionData, SaveSessionState); 55 | } 56 | 57 | internal void SaveSessionState(byte[]? sessionData = null) 58 | { 59 | if (sessionData != null) _state.SessionData = sessionData; 60 | var cmd = _cmd[SaveSession]; 61 | cmd.Parameters[0].Value = _state.SessionData; 62 | cmd.Parameters[1].Value = _state.LastUpdateId; 63 | cmd.Parameters[2].Value = _state.AllowedUpdates; 64 | cmd.ExecuteSave(); 65 | } 66 | 67 | class SessionStore(byte[]? _data, Action save) : Stream 68 | { 69 | private int _dataLen = _data?.Length ?? 0; 70 | private DateTime _lastWrite; 71 | private Task? _delayedWrite; 72 | 73 | protected override void Dispose(bool disposing) => _delayedWrite?.Wait(); 74 | 75 | public override int Read(byte[] buffer, int offset, int count) 76 | { 77 | Array.Copy(_data!, 0, buffer, offset, count); 78 | return count; 79 | } 80 | 81 | public override void Write(byte[] buffer, int offset, int count) 82 | { 83 | _data = buffer; _dataLen = count; 84 | if (_delayedWrite != null) return; 85 | var left = 1000 - (int)(DateTime.UtcNow - _lastWrite).TotalMilliseconds; 86 | if (left < 0) 87 | { 88 | save(buffer[offset..(offset + count)]); 89 | _lastWrite = DateTime.UtcNow; 90 | } 91 | else 92 | _delayedWrite = Task.Delay(left).ContinueWith(t => { lock (this) { _delayedWrite = null; Write(_data, 0, _dataLen); } }); 93 | } 94 | 95 | public override long Length => _dataLen; 96 | public override long Position { get => 0; set { } } 97 | public override bool CanSeek => false; 98 | public override bool CanRead => true; 99 | public override bool CanWrite => true; 100 | public override long Seek(long offset, SeekOrigin origin) => 0; 101 | public override void SetLength(long value) { } 102 | public override void Flush() { } 103 | } 104 | 105 | internal IEnumerable<(int id, TL.Update update)> LoadTLUpdates() 106 | { 107 | using var reader = _cmd[LoadUpdates].ExecuteReader(); 108 | while (reader.Read()) 109 | using (var breader = new BinaryReader(reader.GetStream(1))) 110 | yield return (reader.GetInt32(0), (TL.Update)breader.ReadTLObject(0)); 111 | } 112 | 113 | internal void SaveTLUpdates(IEnumerable updates) 114 | { 115 | _cmd[DelUpdates].ExecuteNonQuery(); 116 | var cmd = _cmd[SaveUpdates]; 117 | using var memStream = new MemoryStream(1024); 118 | foreach (var botUpdate in updates) 119 | { 120 | if (botUpdate.TLUpdate == null) continue; 121 | memStream.SetLength(0); 122 | using (var writer = new BinaryWriter(memStream, System.Text.Encoding.UTF8, leaveOpen: true)) 123 | botUpdate.TLUpdate.WriteTL(writer); 124 | cmd.Parameters[0].Value = botUpdate.Id; 125 | cmd.Parameters[1].Value = memStream.ToArray(); 126 | cmd.ExecuteNonQuery(); 127 | } 128 | } 129 | 130 | internal Dictionary LoadMBoxStates() 131 | { 132 | using var reader = _cmd[LoadMBox].ExecuteReader(); 133 | var result = new Dictionary(); 134 | while (reader.Read()) 135 | result[reader.GetInt64(0)] = new() { pts = reader.GetInt32(1), access_hash = reader.GetInt64(2) }; 136 | return result; 137 | } 138 | 139 | internal void SaveMBoxStates(IReadOnlyDictionary state) 140 | { 141 | var cmd = _cmd[SaveMBox]; 142 | foreach (var mboxState in state) 143 | { 144 | cmd.Parameters[0].Value = mboxState.Key; 145 | cmd.Parameters[1].Value = mboxState.Value.pts; 146 | cmd.Parameters[2].Value = mboxState.Value.access_hash; 147 | cmd.ExecuteNonQuery(); 148 | } 149 | } 150 | 151 | internal void GetTables(out CachedTable users, out CachedTable chats) 152 | { 153 | users = new CachedTable(DoLoadUser, DoSaveUser); 154 | chats = new CachedTable(DoLoadChat, DoSaveChat); 155 | } 156 | 157 | private User? DoLoadUser(long id) 158 | { 159 | _cmd[LoadUser].Parameters[0].Value = id; 160 | using var reader = _cmd[LoadUser].ExecuteReader(); 161 | if (!reader.Read()) return null; 162 | var flags = reader.GetInt32(1); 163 | return new User 164 | { 165 | Id = id, 166 | AccessHash = reader.GetInt64(0), 167 | FirstName = reader.GetString(2), 168 | LastName = reader.GetString(3).NullIfEmpty(), 169 | Username = reader.GetString(4).NullIfEmpty(), 170 | LanguageCode = reader.GetString(5).NullIfEmpty(), 171 | IsBot = (flags & 1) != 0, IsPremium = (flags & 2) != 0, AddedToAttachmentMenu = (flags & 4) != 0, 172 | CanJoinGroups = (flags & 8) != 0, CanReadAllGroupMessages = (flags & 16) != 0, SupportsInlineQueries = (flags & 32) != 0, 173 | }; 174 | } 175 | 176 | private void DoSaveUser(User user) 177 | { 178 | var param = _cmd[SaveUser].Parameters; 179 | param[0].Value = user.Id; 180 | param[1].Value = user.AccessHash; 181 | param[2].Value = (user.IsBot ? 1 : 0) | (user.IsPremium == true ? 2 : 0) | (user.AddedToAttachmentMenu == true ? 4 : 0) 182 | | (user.CanJoinGroups == true ? 8 : 0) | (user.CanReadAllGroupMessages == true ? 16 : 0) | (user.SupportsInlineQueries == true ? 32 : 0); 183 | param[3].Value = user.FirstName; 184 | param[4].Value = user.LastName ?? ""; 185 | param[5].Value = user.Username ?? ""; 186 | param[6].Value = user.LanguageCode ?? ""; 187 | _cmd[SaveUser].ExecuteSave(); 188 | } 189 | 190 | private Chat? DoLoadChat(long id) 191 | { 192 | _cmd[LoadChat].Parameters[0].Value = id; 193 | using var reader = _cmd[LoadChat].ExecuteReader(); 194 | if (!reader.Read()) return null; 195 | var flags = reader.GetInt32(1); 196 | var type = (ChatType)reader.GetInt32(5); 197 | var firstName = reader.GetString(2); 198 | return new Chat 199 | { 200 | Id = type == ChatType.Group ? -id : Bot.ZERO_CHANNEL_ID - id, 201 | AccessHash = reader.GetInt64(0), 202 | Type = type, 203 | Title = type == ChatType.Private ? null : firstName, 204 | FirstName = type == ChatType.Private ? firstName : null, 205 | LastName = type == ChatType.Private ? reader.GetString(3).NullIfEmpty() : null, 206 | Username = reader.GetString(4).NullIfEmpty(), 207 | IsForum = (flags & 1) != 0, 208 | }; 209 | } 210 | 211 | private void DoSaveChat(Chat chat) 212 | { 213 | var param = _cmd[SaveChat].Parameters; 214 | param[0].Value = chat.Id; 215 | param[1].Value = chat.AccessHash; 216 | param[2].Value = chat.IsForum == true ? 1 : 0; 217 | param[3].Value = (chat.Type == ChatType.Private ? chat.FirstName : chat.Title) ?? ""; 218 | param[4].Value = chat.LastName ?? ""; 219 | param[5].Value = chat.Username ?? ""; 220 | param[6].Value = (int)chat.Type; 221 | _cmd[SaveChat].ExecuteSave(); 222 | } 223 | 224 | internal class CachedTable(Func load, Action save) where T : class 225 | { 226 | private readonly Dictionary _cache = []; 227 | 228 | public T this[long key] { set => save(_cache[key] = value); } 229 | public bool TryGetValue(long key, [MaybeNullWhen(false)] out T value) 230 | { 231 | if (_cache.TryGetValue(key, out value)) return true; 232 | value = load(key); 233 | if (value == null) return false; 234 | _cache[key] = value; 235 | return true; 236 | } 237 | 238 | internal void ClearCache() => _cache.Clear(); 239 | internal T? SearchCache(Predicate predicate) 240 | { 241 | foreach (var chat in _cache.Values) 242 | if (predicate(chat)) 243 | return chat; 244 | return null; 245 | } 246 | } 247 | } 248 | 249 | //TODO use bulk insert or selective update instead of full reupload of the tables 250 | -------------------------------------------------------------------------------- /src/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage 2 | // attributes that are applied to this project. 3 | // Project-level suppressions either have no target or are given 4 | // a specific target and scoped to a namespace, type, member, etc. 5 | 6 | global using System.ComponentModel; 7 | global using System.Diagnostics.CodeAnalysis; 8 | global using System.Text.Json; 9 | global using System.Text.Json.Serialization; 10 | global using Telegram.Bot.Extensions; 11 | global using Telegram.Bot.Requests.Abstractions; 12 | global using Telegram.Bot.Serialization; 13 | global using Telegram.Bot.Types; 14 | global using Telegram.Bot.Types.Enums; 15 | global using Telegram.Bot.Types.InlineQueryResults; 16 | global using Telegram.Bot.Types.Passport; 17 | global using Telegram.Bot.Types.Payments; 18 | global using Telegram.Bot.Types.ReplyMarkups; 19 | global using BotCommand = Telegram.Bot.Types.BotCommand; 20 | global using BotCommandScope = Telegram.Bot.Types.BotCommandScope; 21 | global using ForumTopic = Telegram.Bot.Types.ForumTopic; 22 | global using InputFile = Telegram.Bot.Types.InputFile; 23 | global using InputMedia = Telegram.Bot.Types.InputMedia; 24 | global using LabeledPrice = Telegram.Bot.Types.Payments.LabeledPrice; 25 | global using ShippingOption = Telegram.Bot.Types.Payments.ShippingOption; 26 | global using ReplyMarkup = Telegram.Bot.Types.ReplyMarkups.ReplyMarkup; 27 | 28 | [assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Scope = "member", Target = "~M:WTelegram.Bot.GetFile(System.String)~System.Threading.Tasks.Task{Telegram.Bot.Types.TGFile}")] 29 | [assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Scope = "member", Target = "~P:Telegram.Bot.WTelegramBotClient.LocalBotServer")] 30 | [assembly: SuppressMessage("Performance", "CA1866:Use char overload")] 31 | 32 | #if NETSTANDARD2_0 33 | #pragma warning disable CS9113 34 | namespace System.Runtime.CompilerServices 35 | { 36 | internal static class RuntimeHelpers 37 | { 38 | public static T[] GetSubArray(T[] array, Range range) 39 | { 40 | if (array == null) throw new ArgumentNullException(); 41 | var (offset, length) = range.GetOffsetAndLength(array.Length); 42 | if (length == 0) return []; 43 | var dest = typeof(T).IsValueType || typeof(T[]) == array.GetType() ? new T[length] 44 | : (T[])Array.CreateInstance(array.GetType().GetElementType()!, length); 45 | Array.Copy(array, offset, dest, 0, length); 46 | return dest; 47 | } 48 | } 49 | [EditorBrowsable(EditorBrowsableState.Never)] 50 | internal class IsExternalInit { } 51 | [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] 52 | internal sealed class CallerArgumentExpressionAttribute(string parameterName) : Attribute { } 53 | } 54 | #endif 55 | 56 | // These stubs are just to make the compiler happy with existing Telegram.Bot code 57 | 58 | namespace JetBrains.Annotations 59 | { 60 | [AttributeUsage(AttributeTargets.Class|AttributeTargets.Interface)] 61 | class PublicAPIAttribute : Attribute; 62 | } 63 | -------------------------------------------------------------------------------- /src/TBTypes.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace WTelegram.Types 6 | { 7 | /// Chat type for WTelegram.Bot with Client API infos 8 | public class Chat : Telegram.Bot.Types.Chat 9 | { 10 | /// The corresponding Client API chat structure. Real type can be TL.User, TL.Chat, TL.Channel... 11 | public TL.IObject? TLInfo; 12 | 13 | /// Client API access_hash of the chat 14 | public long AccessHash { get; set; } 15 | /// Useful operator for Client API calls 16 | public static implicit operator TL.InputPeer(Chat chat) => chat.Type switch 17 | { 18 | ChatType.Private => new TL.InputPeerUser(chat.Id, chat.AccessHash), 19 | ChatType.Group => new TL.InputPeerChat(-chat.Id), 20 | _ => new TL.InputPeerChannel(-1000000000000 - chat.Id, chat.AccessHash), 21 | }; 22 | } 23 | 24 | /// ChatFullInfo type for WTelegram.Bot with Client API infos 25 | public class ChatFullInfo : Telegram.Bot.Types.ChatFullInfo 26 | { 27 | /// The corresponding Client API chat full structure. Real type can be TL.Users_UserFull, TL.Messages_ChatFull) 28 | public TL.IObject? TLInfo; 29 | 30 | /// Client API access_hash of the chat 31 | public long AccessHash { get; set; } 32 | /// Useful operator for Client API calls 33 | public static implicit operator TL.InputPeer(ChatFullInfo chat) => chat.Type switch 34 | { 35 | ChatType.Private => new TL.InputPeerUser(chat.Id, chat.AccessHash), 36 | ChatType.Group => new TL.InputPeerChat(-chat.Id), 37 | _ => new TL.InputPeerChannel(-1000000000000 - chat.Id, chat.AccessHash), 38 | }; 39 | } 40 | 41 | /// User type for WTelegram.Bot with Client API infos 42 | public partial class User : Telegram.Bot.Types.User 43 | { 44 | /// The corresponding Client API user structure 45 | public TL.User? TLUser; 46 | /// Client API access_hash of the user 47 | public long AccessHash { get; set; } 48 | /// Useful operator for Client API calls 49 | [return: NotNullIfNotNull(nameof(user))] 50 | public static implicit operator TL.InputPeerUser?(User? user) => user == null ? null : new(user.Id, user.AccessHash); 51 | /// Useful operator for Client API calls 52 | [return: NotNullIfNotNull(nameof(user))] 53 | public static implicit operator TL.InputUser?(User? user) => user == null ? null : new(user.Id, user.AccessHash); 54 | 55 | /// 56 | public override string ToString() => 57 | $"{(Username is null ? $"{FirstName}{LastName?.Insert(0, " ")}" : $"@{Username}")} ({Id})"; 58 | } 59 | 60 | /// Update type for WTelegram.Bot with Client API infos 61 | public partial class Update : Telegram.Bot.Types.Update 62 | { 63 | /// The corresponding Client API update structure 64 | public TL.Update? TLUpdate; 65 | } 66 | 67 | /// Message type for WTelegram.Bot with Client API infos 68 | public partial class Message : Telegram.Bot.Types.Message 69 | { 70 | /// The corresponding Client API message structure 71 | public TL.MessageBase? TLMessage; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/TypesTLConverters.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using TL; 3 | using WTelegram; 4 | 5 | namespace Telegram.Bot.Types; 6 | 7 | /// Extension methods for converting between Client API and Bot API 8 | public static class TypesTLConverters 9 | { 10 | const long ZERO_CHANNEL_ID = -1000000000000; 11 | 12 | /// The corresponding Client API chat structure. Real type can be TL.User, TL.Chat, TL.Channel... 13 | public static TL.IObject? TLInfo(this Chat chat) => (chat as WTelegram.Types.Chat)?.TLInfo; 14 | /// The corresponding Client API chat full structure. Real type can be TL.Users_UserFull, TL.Messages_ChatFull) 15 | public static TL.IObject? TLInfo(this ChatFullInfo chat) => (chat as WTelegram.Types.ChatFullInfo)?.TLInfo; 16 | /// The corresponding Client API user structure 17 | public static TL.User? TLUser(this User user) => (user as WTelegram.Types.User)?.TLUser; 18 | /// The corresponding Client API update structure 19 | public static TL.Update? TLUpdate(this Update update) => (update as WTelegram.Types.Update)?.TLUpdate; 20 | /// The corresponding Client API message structure 21 | public static TL.MessageBase? TLMessage(this Message message) => (message as WTelegram.Types.Message)?.TLMessage; 22 | 23 | /// Convert TL.User to Bot Types.User 24 | [return: NotNullIfNotNull(nameof(user))] 25 | public static WTelegram.Types.User? User(this TL.User? user) 26 | { 27 | if (user == null) return null; 28 | var result = new WTelegram.Types.User 29 | { 30 | TLUser = user, 31 | Id = user.id, 32 | IsBot = user.IsBot, 33 | FirstName = user.first_name, 34 | LastName = user.last_name, 35 | Username = user.MainUsername, 36 | LanguageCode = user.lang_code, 37 | IsPremium = user.flags.HasFlag(TL.User.Flags.premium), 38 | AddedToAttachmentMenu = user.flags.HasFlag(TL.User.Flags.attach_menu_enabled), 39 | HasMainWebApp = user.flags2.HasFlag(TL.User.Flags2.bot_has_main_app), 40 | AccessHash = user.access_hash 41 | }; 42 | if (user.IsBot) 43 | { 44 | result.CanJoinGroups = !user.flags.HasFlag(TL.User.Flags.bot_nochats); 45 | result.CanReadAllGroupMessages = user.flags.HasFlag(TL.User.Flags.bot_chat_history); 46 | result.SupportsInlineQueries = user.flags.HasFlag(TL.User.Flags.has_bot_inline_placeholder); 47 | result.CanConnectToBusiness = user.flags2.HasFlag(TL.User.Flags2.bot_business); 48 | } 49 | return result; 50 | } 51 | 52 | /// Convert TL.Chat to Bot Types.Chat 53 | [return: NotNullIfNotNull(nameof(chat))] 54 | public static WTelegram.Types.Chat? Chat(this ChatBase? chat) 55 | { 56 | var channel = chat as Channel; 57 | return chat == null ? null : new() 58 | { 59 | TLInfo = chat, 60 | Id = (channel == null ? 0 : ZERO_CHANNEL_ID) - chat.ID, 61 | Type = channel == null ? ChatType.Group : channel.IsChannel ? ChatType.Channel : ChatType.Supergroup, 62 | Title = chat.Title, 63 | Username = channel?.MainUsername, 64 | IsForum = channel?.flags.HasFlag(Channel.Flags.forum) ?? false, 65 | AccessHash = channel?.access_hash ?? 0 66 | }; 67 | } 68 | 69 | /// Convert TL.User to Bot Types.Chat 70 | [return: NotNullIfNotNull(nameof(user))] 71 | public static WTelegram.Types.Chat? Chat(this TL.User? user) => user == null ? null : new() 72 | { 73 | TLInfo = user, 74 | Id = user.id, 75 | Type = ChatType.Private, 76 | Username = user.MainUsername, 77 | FirstName = user.first_name, 78 | LastName = user.last_name, 79 | AccessHash = user.access_hash 80 | }; 81 | 82 | /// Convert Bot Types.User to Bot Types.Chat 83 | [return: NotNullIfNotNull(nameof(user))] 84 | public static WTelegram.Types.Chat? Chat(this WTelegram.Types.User? user) => user == null ? null : new() 85 | { 86 | TLInfo = user.TLUser, 87 | Id = user.Id, 88 | Type = ChatType.Private, 89 | Username = user.Username, 90 | FirstName = user.FirstName, 91 | LastName = user.LastName, 92 | AccessHash = user.AccessHash 93 | }; 94 | 95 | 96 | /// Convert TL.ChatParticipantBase to Types.ChatMember 97 | public static ChatMember ChatMember(this ChatParticipantBase? participant, User user) 98 | => participant switch 99 | { 100 | ChatParticipantCreator => new ChatMemberOwner { User = user }, 101 | ChatParticipantAdmin => new ChatMemberAdministrator 102 | { 103 | User = user, 104 | CanManageChat = true, 105 | CanChangeInfo = true, 106 | //CanPostMessages, CanEditMessages: set only for channels 107 | CanDeleteMessages = true, 108 | CanInviteUsers = true, 109 | CanRestrictMembers = true, 110 | CanPinMessages = true, 111 | //CanManageTopics: set only for supergroups 112 | CanPromoteMembers = false, 113 | CanManageVideoChats = true, 114 | //CanPostStories, CanEditStories, CanDeleteStories: set only for channels 115 | IsAnonymous = false, 116 | }, 117 | ChatParticipant => new ChatMemberMember { User = user }, 118 | _ => new ChatMemberLeft { User = user } 119 | }; 120 | 121 | internal static ChatMember ChatMember(this ChannelParticipantBase? participant, User user) 122 | => participant switch 123 | { 124 | ChannelParticipantSelf cps => new ChatMemberMember { User = user, UntilDate = cps.subscription_until_date.NullIfDefault() }, 125 | ChannelParticipant cp => new ChatMemberMember { User = user, UntilDate = cp.subscription_until_date.NullIfDefault() }, 126 | ChannelParticipantCreator cpc => new ChatMemberOwner { User = user, CustomTitle = cpc.rank, IsAnonymous = cpc.admin_rights.flags.HasFlag(TL.ChatAdminRights.Flags.anonymous) }, 127 | ChannelParticipantAdmin cpa => new ChatMemberAdministrator 128 | { 129 | User = user, 130 | CustomTitle = cpa.rank, 131 | CanBeEdited = cpa.flags.HasFlag(ChannelParticipantAdmin.Flags.can_edit), 132 | IsAnonymous = cpa.admin_rights.flags.HasFlag(TL.ChatAdminRights.Flags.anonymous), 133 | CanManageChat = cpa.admin_rights.flags != 0, 134 | CanPostMessages = cpa.admin_rights.flags.HasFlag(TL.ChatAdminRights.Flags.post_messages), 135 | CanEditMessages = cpa.admin_rights.flags.HasFlag(TL.ChatAdminRights.Flags.edit_messages), 136 | CanDeleteMessages = cpa.admin_rights.flags.HasFlag(TL.ChatAdminRights.Flags.delete_messages), 137 | CanManageVideoChats = cpa.admin_rights.flags.HasFlag(TL.ChatAdminRights.Flags.manage_call), 138 | CanRestrictMembers = cpa.admin_rights.flags.HasFlag(TL.ChatAdminRights.Flags.ban_users), 139 | CanPromoteMembers = cpa.admin_rights.flags.HasFlag(TL.ChatAdminRights.Flags.add_admins), 140 | CanChangeInfo = cpa.admin_rights.flags.HasFlag(TL.ChatAdminRights.Flags.change_info), 141 | CanInviteUsers = cpa.admin_rights.flags.HasFlag(TL.ChatAdminRights.Flags.invite_users), 142 | CanPinMessages = cpa.admin_rights.flags.HasFlag(TL.ChatAdminRights.Flags.pin_messages), 143 | CanManageTopics = cpa.admin_rights.flags.HasFlag(TL.ChatAdminRights.Flags.manage_topics), 144 | CanPostStories = cpa.admin_rights.flags.HasFlag(TL.ChatAdminRights.Flags.post_stories), 145 | CanEditStories = cpa.admin_rights.flags.HasFlag(TL.ChatAdminRights.Flags.edit_stories), 146 | CanDeleteStories = cpa.admin_rights.flags.HasFlag(TL.ChatAdminRights.Flags.delete_stories), 147 | }, 148 | ChannelParticipantBanned cpb => 149 | cpb.banned_rights.flags.HasFlag(ChatBannedRights.Flags.view_messages) 150 | ? new ChatMemberBanned { User = user, UntilDate = UntilDate(cpb.banned_rights.until_date) } 151 | : new ChatMemberRestricted 152 | { 153 | User = user, 154 | IsMember = !cpb.flags.HasFlag(ChannelParticipantBanned.Flags.left), 155 | UntilDate = UntilDate(cpb.banned_rights.until_date), 156 | CanChangeInfo = !cpb.banned_rights.flags.HasFlag(ChatBannedRights.Flags.change_info), 157 | CanInviteUsers = !cpb.banned_rights.flags.HasFlag(ChatBannedRights.Flags.invite_users), 158 | CanPinMessages = !cpb.banned_rights.flags.HasFlag(ChatBannedRights.Flags.pin_messages), 159 | CanSendMessages = !cpb.banned_rights.flags.HasFlag(ChatBannedRights.Flags.send_messages), 160 | CanSendAudios = !cpb.banned_rights.flags.HasFlag(ChatBannedRights.Flags.send_audios), 161 | CanSendDocuments = !cpb.banned_rights.flags.HasFlag(ChatBannedRights.Flags.send_docs), 162 | CanSendPhotos = !cpb.banned_rights.flags.HasFlag(ChatBannedRights.Flags.send_photos), 163 | CanSendVideos = !cpb.banned_rights.flags.HasFlag(ChatBannedRights.Flags.send_videos), 164 | CanSendVideoNotes = !cpb.banned_rights.flags.HasFlag(ChatBannedRights.Flags.send_roundvideos), 165 | CanSendVoiceNotes = !cpb.banned_rights.flags.HasFlag(ChatBannedRights.Flags.send_voices), 166 | CanSendPolls = !cpb.banned_rights.flags.HasFlag(ChatBannedRights.Flags.send_polls), 167 | CanSendOtherMessages = !cpb.banned_rights.flags.HasFlag(ChatBannedRights.Flags.send_stickers | ChatBannedRights.Flags.send_gifs | ChatBannedRights.Flags.send_games), 168 | CanAddWebPagePreviews = !cpb.banned_rights.flags.HasFlag(ChatBannedRights.Flags.embed_links), 169 | CanManageTopics = !cpb.banned_rights.flags.HasFlag(ChatBannedRights.Flags.manage_topics), 170 | }, 171 | _ /*ChannelParticipantLeft*/ => new ChatMemberLeft { User = user, }, 172 | }; 173 | 174 | private static DateTime? UntilDate(DateTime until_date) => until_date == DateTime.MaxValue ? null : until_date; 175 | 176 | [return: NotNullIfNotNull(nameof(banned_rights))] 177 | internal static ChatPermissions? ChatPermissions(this ChatBannedRights? banned_rights) => banned_rights == null ? null : new ChatPermissions 178 | { 179 | CanSendMessages = !banned_rights.flags.HasFlag(ChatBannedRights.Flags.send_messages), 180 | CanSendAudios = !banned_rights.flags.HasFlag(ChatBannedRights.Flags.send_audios), 181 | CanSendDocuments = !banned_rights.flags.HasFlag(ChatBannedRights.Flags.send_docs), 182 | CanSendPhotos = !banned_rights.flags.HasFlag(ChatBannedRights.Flags.send_photos), 183 | CanSendVideos = !banned_rights.flags.HasFlag(ChatBannedRights.Flags.send_videos), 184 | CanSendVideoNotes = !banned_rights.flags.HasFlag(ChatBannedRights.Flags.send_roundvideos), 185 | CanSendVoiceNotes = !banned_rights.flags.HasFlag(ChatBannedRights.Flags.send_voices), 186 | CanSendPolls = !banned_rights.flags.HasFlag(ChatBannedRights.Flags.send_polls), 187 | CanSendOtherMessages = !banned_rights.flags.HasFlag(ChatBannedRights.Flags.send_stickers | ChatBannedRights.Flags.send_gifs | ChatBannedRights.Flags.send_games), 188 | CanAddWebPagePreviews = !banned_rights.flags.HasFlag(ChatBannedRights.Flags.embed_links), 189 | CanChangeInfo = !banned_rights.flags.HasFlag(ChatBannedRights.Flags.change_info), 190 | CanInviteUsers = !banned_rights.flags.HasFlag(ChatBannedRights.Flags.invite_users), 191 | CanPinMessages = !banned_rights.flags.HasFlag(ChatBannedRights.Flags.pin_messages), 192 | CanManageTopics = !banned_rights.flags.HasFlag(ChatBannedRights.Flags.manage_topics) 193 | }; 194 | 195 | internal static ChatPermissions LegacyMode(this ChatPermissions permissions, bool? useIndependentChatPermissions) 196 | { 197 | if (useIndependentChatPermissions != true) 198 | { 199 | if (permissions.CanSendPolls == true) permissions.CanSendMessages = true; 200 | if (permissions.CanSendOtherMessages == true || permissions.CanAddWebPagePreviews == true) 201 | permissions.CanSendAudios = permissions.CanSendDocuments = permissions.CanSendPhotos = permissions.CanSendVideos = 202 | permissions.CanSendVideoNotes = permissions.CanSendVoiceNotes = permissions.CanSendMessages = true; 203 | } 204 | return permissions; 205 | } 206 | 207 | internal static ChatBannedRights ToChatBannedRights(this ChatPermissions permissions, DateTime? untilDate = default) => new() 208 | { 209 | until_date = untilDate ?? default, 210 | flags = (permissions.CanSendMessages == true ? 0 : ChatBannedRights.Flags.send_messages) 211 | | (permissions.CanSendAudios == true ? 0 : ChatBannedRights.Flags.send_audios) 212 | | (permissions.CanSendDocuments == true ? 0 : ChatBannedRights.Flags.send_docs) 213 | | (permissions.CanSendPhotos == true ? 0 : ChatBannedRights.Flags.send_photos) 214 | | (permissions.CanSendVideos == true ? 0 : ChatBannedRights.Flags.send_videos) 215 | | (permissions.CanSendVideoNotes == true ? 0 : ChatBannedRights.Flags.send_roundvideos) 216 | | (permissions.CanSendVoiceNotes == true ? 0 : ChatBannedRights.Flags.send_voices) 217 | | (permissions.CanSendPolls == true ? 0 : ChatBannedRights.Flags.send_polls) 218 | | (permissions.CanSendOtherMessages == true ? 0 : ChatBannedRights.Flags.send_stickers | ChatBannedRights.Flags.send_gifs | ChatBannedRights.Flags.send_games | ChatBannedRights.Flags.send_inline) 219 | | (permissions.CanAddWebPagePreviews == true ? 0 : ChatBannedRights.Flags.embed_links) 220 | | (permissions.CanChangeInfo == true ? 0 : ChatBannedRights.Flags.change_info) 221 | | (permissions.CanInviteUsers == true ? 0 : ChatBannedRights.Flags.invite_users) 222 | | (permissions.CanPinMessages == true ? 0 : ChatBannedRights.Flags.pin_messages) 223 | | (permissions.CanManageTopics == true ? 0 : ChatBannedRights.Flags.manage_topics) 224 | }; 225 | 226 | [return: NotNullIfNotNull(nameof(location))] 227 | internal static ChatLocation? ChatLocation(this ChannelLocation? location) 228 | => location == null ? null : new ChatLocation { Location = Location(location.geo_point), Address = location.address }; 229 | 230 | [return: NotNullIfNotNull(nameof(geo))] 231 | internal static Location? Location(this GeoPoint? geo) 232 | => geo == null ? null : new Location 233 | { 234 | Longitude = geo.lon, 235 | Latitude = geo.lat, 236 | HorizontalAccuracy = geo.flags.HasFlag(GeoPoint.Flags.has_accuracy_radius) ? geo.accuracy_radius : null 237 | }; 238 | 239 | internal static InlineKeyboardMarkup? InlineKeyboardMarkup(this TL.ReplyMarkup? reply_markup) => reply_markup is not ReplyInlineMarkup rim ? null : 240 | new InlineKeyboardMarkup(rim.rows.Select(row => row.buttons.Select(btn => btn switch 241 | { 242 | KeyboardButtonUrl kbu => InlineKeyboardButton.WithUrl(kbu.text, kbu.url), 243 | KeyboardButtonCallback kbc => InlineKeyboardButton.WithCallbackData(kbc.text, Encoding.UTF8.GetString(kbc.data)), 244 | KeyboardButtonGame kbg => InlineKeyboardButton.WithCallbackGame(kbg.text), 245 | KeyboardButtonBuy kbb => InlineKeyboardButton.WithPay(kbb.text), 246 | KeyboardButtonSwitchInline kbsi => kbsi.flags.HasFlag(KeyboardButtonSwitchInline.Flags.same_peer) ? 247 | InlineKeyboardButton.WithSwitchInlineQueryCurrentChat(kbsi.text, kbsi.query) : 248 | InlineKeyboardButton.WithSwitchInlineQuery(kbsi.text, kbsi.query), 249 | KeyboardButtonCopy kbco => InlineKeyboardButton.WithCopyText(kbco.text, kbco.copy_text), 250 | KeyboardButtonUrlAuth kbua => InlineKeyboardButton.WithLoginUrl(kbua.text, new LoginUrl 251 | { 252 | Url = kbua.url, 253 | ForwardText = kbua.fwd_text, 254 | }), 255 | KeyboardButtonWebView kbwv => InlineKeyboardButton.WithWebApp(kbwv.text, new WebAppInfo { Url = kbwv.url }), 256 | _ => new InlineKeyboardButton(btn.Text), 257 | }))); 258 | 259 | internal static Video Video(this TL.Document document, MessageMediaDocument mmd) 260 | { 261 | var thumb = document.LargestThumbSize; 262 | var video = document.GetAttribute(); 263 | return new Video 264 | { 265 | FileSize = document.size, 266 | Width = video?.w ?? 0, 267 | Height = video?.h ?? 0, 268 | Duration = (int)(video?.duration + 0.5 ?? 0.0), 269 | Thumbnail = thumb?.PhotoSize(document.ToFileLocation(thumb), document.dc_id), 270 | FileName = document.Filename, 271 | MimeType = document.mime_type, 272 | Cover = mmd.video_cover?.PhotoSizes(), 273 | StartTimestamp = mmd.video_timestamp.NullIfZero(), 274 | }.SetFileIds(document.ToFileLocation(), document.dc_id); 275 | } 276 | 277 | /// Convert TL.Photo into Bot Types.PhotoSize[] 278 | public static PhotoSize[]? PhotoSizes(this PhotoBase photoBase) 279 | => (photoBase is not Photo photo) ? null : photo.sizes.Select(ps => ps.PhotoSize(photo.ToFileLocation(ps), photo.dc_id)).ToArray(); 280 | 281 | /// Convert TL.PhotoSize into Bot Types.PhotoSize 282 | public static PhotoSize PhotoSize(this PhotoSizeBase ps, InputFileLocationBase location, int dc_id) 283 | => new PhotoSize() 284 | { 285 | Width = ps.Width, 286 | Height = ps.Height, 287 | FileSize = ps.FileSize, 288 | }.SetFileIds(location, dc_id, ps.Type); 289 | 290 | /// Encode TL.InputFileLocation as FileId/FileUniqueId strings into a Bot File structure 291 | public static T SetFileIds(this T file, InputFileLocationBase location, int dc_id, string? type = null) where T : FileBase 292 | { 293 | using var memStream = new MemoryStream(128); 294 | using (var writer = new BinaryWriter(memStream)) 295 | { 296 | writer.WriteTLObject(location); 297 | writer.Write((byte)dc_id); 298 | writer.Write((int)(file.FileSize ?? 0)); 299 | writer.Write((byte)42); 300 | } 301 | var bytes = memStream.ToArray(); 302 | file.FileId = ToBase64(bytes); 303 | bytes[12] = (byte)(type?[0] ?? 0); 304 | file.FileUniqueId = ToBase64(bytes, 3, 10); 305 | return file; 306 | } 307 | 308 | /// Decode FileId into TL.InputFileLocation 309 | public static (TGFile file, InputFileLocationBase location, int dc_id) ParseFileId(this string fileId, bool generateFile = false) 310 | { 311 | var idBytes = fileId.FromBase64(); 312 | if (idBytes[^1] != 42) throw new WTException("Unsupported file_id format"); 313 | using var memStream = new MemoryStream(idBytes); 314 | using var reader = new BinaryReader(memStream); 315 | var location = (InputFileLocationBase)reader.ReadTLObject(); 316 | byte dc_id = reader.ReadByte(); 317 | int fileSize = reader.ReadInt32(); 318 | if (!generateFile) return (null!, location, dc_id); 319 | 320 | idBytes[12] = idBytes[^8]; // patch byte following id with InputPhotoFileLocation.thumb_size 321 | var fileUniqueId = ToBase64(idBytes, 3, 10); 322 | var filename = location.GetType().Name; 323 | if (filename.StartsWith("Input")) filename = filename[5..]; 324 | if (filename.EndsWith("FileLocation")) filename = filename[..^12]; 325 | filename = $"{filename}_{fileUniqueId}"; 326 | if (filename.Contains("Photo")) filename += ".jpg"; 327 | return (new TGFile 328 | { 329 | FilePath = $"{fileId}/{filename}", 330 | FileId = fileId, 331 | FileUniqueId = ToBase64(idBytes, 3, 10), 332 | FileSize = fileSize 333 | }, location, dc_id); 334 | } 335 | 336 | internal static ChatPhoto? ChatPhoto(this PhotoBase? photoBase) 337 | { 338 | if (photoBase is not Photo photo) return null; 339 | var small = photo.sizes?.FirstOrDefault(ps => ps.Type == "a"); 340 | var big = photo.sizes?.FirstOrDefault(ps => ps.Type == "c"); 341 | small ??= photo.sizes?.Aggregate((agg, next) => next.Width > 0 && (long)next.Width * next.Height < (long)agg.Width * agg.Height ? next : agg)!; 342 | big ??= photo.LargestPhotoSize; 343 | using var memStream = new MemoryStream(128); 344 | using var writer = new BinaryWriter(memStream); 345 | writer.WriteTLObject(photo.ToFileLocation(big)); 346 | writer.Write((byte)photo.dc_id); 347 | writer.Write(big.FileSize); 348 | writer.Write((byte)42); 349 | var bytes = memStream.ToArray(); 350 | var chatPhoto = new ChatPhoto() 351 | { BigFileId = ToBase64(bytes) }; 352 | bytes[12] = (byte)big.Type[0]; // patch byte following id with thumb_size 353 | chatPhoto.BigFileUniqueId = ToBase64(bytes, 3, 10); 354 | memStream.SetLength(0); 355 | writer.WriteTLObject(photo.ToFileLocation(small)); 356 | writer.Write((byte)photo.dc_id); 357 | writer.Write(small.FileSize); 358 | writer.Write((byte)42); 359 | bytes = memStream.ToArray(); 360 | chatPhoto.SmallFileId = ToBase64(bytes); 361 | bytes[12] = (byte)small.Type[0]; // patch byte following id with thumb_size 362 | chatPhoto.SmallFileUniqueId = ToBase64(bytes, 3, 10); 363 | return chatPhoto; 364 | } 365 | 366 | internal static string? InlineMessageId(this InputBotInlineMessageIDBase? msg_id) 367 | { 368 | if (msg_id == null) return null; 369 | using var memStream = new MemoryStream(128); 370 | using (var writer = new BinaryWriter(memStream)) 371 | msg_id.WriteTL(writer); 372 | var bytes = memStream.ToArray(); 373 | return ToBase64(bytes); 374 | } 375 | 376 | internal static InputBotInlineMessageIDBase ParseInlineMsgID(this string inlineMessageId) 377 | { 378 | var idBytes = inlineMessageId.FromBase64(); 379 | using var memStream = new MemoryStream(idBytes); 380 | using var reader = new BinaryReader(memStream); 381 | return (InputBotInlineMessageIDBase)reader.ReadTLObject(); 382 | } 383 | 384 | private static byte[] FromBase64(this string str) => Convert.FromBase64String(str.Replace('_', '/').Replace('-', '+') + new string('=', (2147483644 - str.Length) % 4)); 385 | private static string ToBase64(this byte[] bytes) => ToBase64(bytes, 0, bytes.Length); 386 | private static string ToBase64(this byte[] bytes, int offset, int length) => Convert.ToBase64String(bytes, offset, length).TrimEnd('=').Replace('+', '-').Replace('/', '_'); 387 | 388 | internal static SendMessageAction ChatAction(this ChatAction action) => action switch 389 | { 390 | Enums.ChatAction.Typing => new SendMessageTypingAction(), 391 | Enums.ChatAction.UploadPhoto => new SendMessageUploadPhotoAction(), 392 | Enums.ChatAction.RecordVideo => new SendMessageRecordVideoAction(), 393 | Enums.ChatAction.UploadVideo => new SendMessageUploadVideoAction(), 394 | Enums.ChatAction.RecordVoice => new SendMessageRecordAudioAction(), 395 | Enums.ChatAction.UploadVoice => new SendMessageUploadAudioAction(), 396 | Enums.ChatAction.UploadDocument => new SendMessageUploadDocumentAction(), 397 | Enums.ChatAction.FindLocation => new SendMessageGeoLocationAction(), 398 | Enums.ChatAction.RecordVideoNote => new SendMessageRecordRoundAction(), 399 | Enums.ChatAction.UploadVideoNote => new SendMessageUploadRoundAction(), 400 | Enums.ChatAction.ChooseSticker => new SendMessageChooseStickerAction(), 401 | _ => throw new RpcException(400, "Wrong parameter action in request") 402 | }; 403 | 404 | internal static BotMenuButtonBase? BotMenuButton(this MenuButton? menuButton) 405 | => menuButton switch 406 | { 407 | MenuButtonDefault => null, 408 | MenuButtonCommands => new BotMenuButtonCommands(), 409 | MenuButtonWebApp mbwa => new BotMenuButton { text = mbwa.Text, url = mbwa.WebApp.Url }, 410 | _ => throw new WTException("MenuButton has unsupported type") 411 | }; 412 | 413 | internal static MenuButton MenuButton(this BotMenuButtonBase? menuButton) 414 | => menuButton switch 415 | { 416 | null => new MenuButtonDefault(), 417 | BotMenuButtonCommands => new MenuButtonCommands(), 418 | BotMenuButton bmb => new MenuButtonWebApp { Text = bmb.text, WebApp = new WebAppInfo { Url = bmb.url } }, 419 | _ => throw new WTException("Unrecognized BotMenuButtonBase") 420 | }; 421 | 422 | internal static ChatAdminRights ChatAdminRights(this ChatAdministratorRights? rights) 423 | => rights == null ? new() : new() 424 | { 425 | flags = (rights.IsAnonymous == true ? TL.ChatAdminRights.Flags.anonymous : 0) 426 | | (rights.CanManageChat == true ? TL.ChatAdminRights.Flags.other : 0) 427 | | (rights.CanDeleteMessages == true ? TL.ChatAdminRights.Flags.delete_messages : 0) 428 | | (rights.CanManageVideoChats == true ? TL.ChatAdminRights.Flags.manage_call : 0) 429 | | (rights.CanRestrictMembers == true ? TL.ChatAdminRights.Flags.ban_users : 0) 430 | | (rights.CanPromoteMembers == true ? TL.ChatAdminRights.Flags.add_admins : 0) 431 | | (rights.CanChangeInfo == true ? TL.ChatAdminRights.Flags.change_info : 0) 432 | | (rights.CanInviteUsers == true ? TL.ChatAdminRights.Flags.invite_users : 0) 433 | | (rights.CanPostMessages == true ? TL.ChatAdminRights.Flags.post_messages : 0) 434 | | (rights.CanEditMessages == true ? TL.ChatAdminRights.Flags.edit_messages : 0) 435 | | (rights.CanPinMessages == true ? TL.ChatAdminRights.Flags.pin_messages : 0) 436 | | (rights.CanManageTopics == true ? TL.ChatAdminRights.Flags.manage_topics : 0) 437 | | (rights.CanPostStories == true ? TL.ChatAdminRights.Flags.post_stories : 0) 438 | | (rights.CanEditStories == true ? TL.ChatAdminRights.Flags.edit_stories : 0) 439 | | (rights.CanDeleteStories == true ? TL.ChatAdminRights.Flags.delete_stories : 0) 440 | }; 441 | 442 | internal static ChatAdministratorRights ChatAdministratorRights(this ChatAdminRights? rights) 443 | => rights == null ? new() : new() 444 | { 445 | IsAnonymous = rights.flags.HasFlag(TL.ChatAdminRights.Flags.anonymous), 446 | CanManageChat = rights.flags.HasFlag(TL.ChatAdminRights.Flags.other), 447 | CanDeleteMessages = rights.flags.HasFlag(TL.ChatAdminRights.Flags.delete_messages), 448 | CanManageVideoChats = rights.flags.HasFlag(TL.ChatAdminRights.Flags.manage_call), 449 | CanRestrictMembers = rights.flags.HasFlag(TL.ChatAdminRights.Flags.ban_users), 450 | CanPromoteMembers = rights.flags.HasFlag(TL.ChatAdminRights.Flags.add_admins), 451 | CanChangeInfo = rights.flags.HasFlag(TL.ChatAdminRights.Flags.change_info), 452 | CanInviteUsers = rights.flags.HasFlag(TL.ChatAdminRights.Flags.invite_users), 453 | CanPostMessages = rights.flags.HasFlag(TL.ChatAdminRights.Flags.post_messages), 454 | CanEditMessages = rights.flags.HasFlag(TL.ChatAdminRights.Flags.edit_messages), 455 | CanPinMessages = rights.flags.HasFlag(TL.ChatAdminRights.Flags.pin_messages), 456 | CanManageTopics = rights.flags.HasFlag(TL.ChatAdminRights.Flags.manage_topics), 457 | CanPostStories = rights.flags.HasFlag(TL.ChatAdminRights.Flags.post_stories), 458 | CanEditStories = rights.flags.HasFlag(TL.ChatAdminRights.Flags.edit_stories), 459 | CanDeleteStories = rights.flags.HasFlag(TL.ChatAdminRights.Flags.delete_stories), 460 | }; 461 | 462 | [return: NotNullIfNotNull(nameof(maskPosition))] 463 | internal static MaskCoords? MaskCoord(this MaskPosition? maskPosition) 464 | => maskPosition == null ? null : new MaskCoords 465 | { n = (int)maskPosition.Point - 1, x = maskPosition.XShift, y = maskPosition.YShift, zoom = maskPosition.Scale }; 466 | 467 | internal static TL.LabeledPrice[] LabeledPrices(this IEnumerable prices) 468 | => [.. prices.Select(p => new TL.LabeledPrice { label = p.Label, amount = p.Amount })]; 469 | 470 | internal static TL.BotCommand BotCommand(this BotCommand bc) 471 | => new() { command = bc.Command.StartsWith("/") ? bc.Command[1..] : bc.Command, description = bc.Description }; 472 | internal static BotCommand BotCommand(this TL.BotCommand bc) 473 | => new() { Command = bc.command, Description = bc.description }; 474 | 475 | [return: NotNullIfNotNull(nameof(pa))] 476 | internal static Payments.ShippingAddress? ShippingAddress(this PostAddress? pa) => pa == null ? null : new() 477 | { 478 | CountryCode = pa.country_iso2, 479 | State = pa.state, 480 | City = pa.city, 481 | StreetLine1 = pa.street_line1, 482 | StreetLine2 = pa.street_line2, 483 | PostCode = pa.post_code 484 | }; 485 | 486 | [return: NotNullIfNotNull(nameof(pri))] 487 | internal static Payments.OrderInfo? OrderInfo(this PaymentRequestedInfo? pri) => pri == null ? null : new() 488 | { 489 | Name = pri.name, 490 | PhoneNumber = pri.phone, 491 | Email = pri.email, 492 | ShippingAddress = pri.shipping_address.ShippingAddress() 493 | }; 494 | 495 | internal static InlineQueryPeerType[] InlineQueryPeerTypes(this SwitchInlineQueryChosenChat swiqcc) 496 | => InlineQueryPeerTypes(swiqcc.AllowUserChats, swiqcc.AllowBotChats, swiqcc.AllowGroupChats, swiqcc.AllowChannelChats); 497 | 498 | internal static InlineQueryPeerType[] InlineQueryPeerTypes(bool allowUserChats, bool allowBotChats, bool allowGroupChats, bool allowChannelChats) 499 | { 500 | var result = new List(); 501 | if (allowUserChats) result.Add(InlineQueryPeerType.PM); 502 | if (allowBotChats) result.Add(InlineQueryPeerType.BotPM); 503 | if (allowGroupChats) { result.Add(InlineQueryPeerType.Chat); result.Add(InlineQueryPeerType.Megagroup); } 504 | if (allowChannelChats) result.Add(InlineQueryPeerType.Broadcast); 505 | return [.. result]; 506 | } 507 | 508 | internal static SecureValueErrorBase SecureValueError(PassportElementError error) 509 | { 510 | var type = error.Type.ToSecureValueType(); 511 | var text = error.Message; 512 | return error switch 513 | { 514 | PassportElementErrorDataField e => new SecureValueErrorData { type = type, text = text, data_hash = e.DataHash.FromBase64(), field = e.FieldName }, 515 | PassportElementErrorFrontSide e => new SecureValueErrorFrontSide { type = type, text = text, file_hash = e.FileHash.FromBase64() }, 516 | PassportElementErrorReverseSide e => new SecureValueErrorReverseSide { type = type, text = text, file_hash = e.FileHash.FromBase64() }, 517 | PassportElementErrorSelfie e => new SecureValueErrorSelfie { type = type, text = text, file_hash = e.FileHash.FromBase64() }, 518 | PassportElementErrorFile e => new SecureValueErrorFile { type = type, text = text, file_hash = e.FileHash.FromBase64() }, 519 | PassportElementErrorFiles e => new SecureValueErrorFiles { type = type, text = text, file_hash = [.. e.FileHashes.Select(FromBase64)] }, 520 | PassportElementErrorTranslationFile e => new SecureValueErrorTranslationFile { type = type, text = text, file_hash = e.FileHash.FromBase64() }, 521 | PassportElementErrorTranslationFiles e => new SecureValueErrorTranslationFiles { type = type, text = text, file_hash = [.. e.FileHashes.Select(FromBase64)] }, 522 | PassportElementErrorUnspecified e => new SecureValueError { type = type, text = text, hash = e.ElementHash.FromBase64() }, 523 | _ => throw new WTException("Unrecognized PassportElementError") 524 | }; 525 | } 526 | 527 | internal static SecureValueType ToSecureValueType(this EncryptedPassportElementType type) => type switch 528 | { 529 | EncryptedPassportElementType.PersonalDetails => SecureValueType.PersonalDetails, 530 | EncryptedPassportElementType.Passport => SecureValueType.Passport, 531 | EncryptedPassportElementType.DriverLicense => SecureValueType.DriverLicense, 532 | EncryptedPassportElementType.IdentityCard => SecureValueType.IdentityCard, 533 | EncryptedPassportElementType.InternalPassport => SecureValueType.InternalPassport, 534 | EncryptedPassportElementType.Address => SecureValueType.Address, 535 | EncryptedPassportElementType.UtilityBill => SecureValueType.UtilityBill, 536 | EncryptedPassportElementType.BankStatement => SecureValueType.BankStatement, 537 | EncryptedPassportElementType.RentalAgreement => SecureValueType.RentalAgreement, 538 | EncryptedPassportElementType.PassportRegistration => SecureValueType.PassportRegistration, 539 | EncryptedPassportElementType.TemporaryRegistration => SecureValueType.TemporaryRegistration, 540 | EncryptedPassportElementType.PhoneNumber => SecureValueType.Phone, 541 | EncryptedPassportElementType.Email => SecureValueType.Email, 542 | _ => throw new WTException("Unrecognized EncryptedPassportElementType") 543 | }; 544 | 545 | internal static EncryptedPassportElement EncryptedPassportElement(this TL.SecureValue sv) => new() 546 | { 547 | Type = sv.type switch 548 | { 549 | SecureValueType.PersonalDetails => EncryptedPassportElementType.PersonalDetails, 550 | SecureValueType.Passport => EncryptedPassportElementType.Passport, 551 | SecureValueType.DriverLicense => EncryptedPassportElementType.DriverLicense, 552 | SecureValueType.IdentityCard => EncryptedPassportElementType.IdentityCard, 553 | SecureValueType.InternalPassport => EncryptedPassportElementType.InternalPassport, 554 | SecureValueType.Address => EncryptedPassportElementType.Address, 555 | SecureValueType.UtilityBill => EncryptedPassportElementType.UtilityBill, 556 | SecureValueType.BankStatement => EncryptedPassportElementType.BankStatement, 557 | SecureValueType.RentalAgreement => EncryptedPassportElementType.RentalAgreement, 558 | SecureValueType.PassportRegistration => EncryptedPassportElementType.PassportRegistration, 559 | SecureValueType.TemporaryRegistration => EncryptedPassportElementType.TemporaryRegistration, 560 | SecureValueType.Phone => EncryptedPassportElementType.PhoneNumber, 561 | SecureValueType.Email => EncryptedPassportElementType.Email, 562 | _ => 0, 563 | }, 564 | Data = sv.data?.data.ToBase64(), 565 | PhoneNumber = sv.plain_data is SecurePlainPhone spp ? spp.phone : null, 566 | Email = sv.plain_data is SecurePlainEmail spe ? spe.email : null, 567 | Files = sv.files?.Select(PassportFile).ToArray(), 568 | FrontSide = sv.front_side?.PassportFile(), 569 | ReverseSide = sv.reverse_side?.PassportFile(), 570 | Selfie = sv.selfie?.PassportFile(), 571 | Translation = sv.translation?.Select(PassportFile).ToArray(), 572 | Hash = sv.hash.ToBase64() 573 | }; 574 | 575 | internal static PassportData PassportData(this MessageActionSecureValuesSentMe masvsm) => new() 576 | { 577 | Data = [.. masvsm.values.Select(EncryptedPassportElement)], 578 | Credentials = new EncryptedCredentials { Data = masvsm.credentials.data.ToBase64(), Hash = masvsm.credentials.hash.ToBase64(), Secret = masvsm.credentials.secret.ToBase64() } 579 | }; 580 | 581 | private static PassportFile PassportFile(this SecureFile file) 582 | => new PassportFile { FileSize = file.size, FileDate = file.date }.SetFileIds(file.ToFileLocation(), file.dc_id); 583 | 584 | internal static SharedUser ToSharedUser(this RequestedPeer peer) => peer is not RequestedPeerUser rpu ? null! : 585 | new SharedUser { UserId = rpu.user_id, FirstName = rpu.first_name, LastName = rpu.last_name, Username = rpu.username, Photo = rpu.photo.PhotoSizes() }; 586 | 587 | internal static ChatShared? ToSharedChat(this RequestedPeer peer, int requestId) => peer switch 588 | { 589 | RequestedPeerChat rpc => new ChatShared { RequestId = requestId, ChatId = -rpc.chat_id, Title = rpc.title, Photo = rpc.photo.PhotoSizes() }, 590 | RequestedPeerChannel rpch => new ChatShared { RequestId = requestId, ChatId = ZERO_CHANNEL_ID - rpch.channel_id, Title = rpch.title, Username = rpch.username, Photo = rpch.photo.PhotoSizes() }, 591 | _ => null 592 | }; 593 | 594 | internal static TL.Reaction Reaction(this ReactionType reaction) => reaction switch 595 | { 596 | ReactionTypeEmoji rte => new TL.ReactionEmoji { emoticon = rte.Emoji }, 597 | ReactionTypeCustomEmoji rtce => new TL.ReactionCustomEmoji { document_id = long.Parse(rtce.CustomEmojiId) }, 598 | ReactionTypePaid => new TL.ReactionPaid { }, 599 | _ => throw new WTException("Unrecognized ReactionType") 600 | }; 601 | internal static ReactionType ReactionType(this TL.Reaction reaction) => reaction switch 602 | { 603 | TL.ReactionEmoji rte => new ReactionTypeEmoji { Emoji = rte.emoticon }, 604 | TL.ReactionCustomEmoji rce => new ReactionTypeCustomEmoji { CustomEmojiId = rce.document_id.ToString() }, 605 | TL.ReactionPaid => new ReactionTypePaid { }, 606 | _ => throw new WTException("Unrecognized Reaction") 607 | }; 608 | 609 | internal static InputMediaWebPage? InputMediaWebPage(this LinkPreviewOptions? lpo) => lpo?.Url == null ? null : new InputMediaWebPage 610 | { 611 | url = lpo.Url, 612 | flags = TL.InputMediaWebPage.Flags.optional 613 | | (lpo.PreferLargeMedia == true ? TL.InputMediaWebPage.Flags.force_large_media : 0) 614 | | (lpo.PreferSmallMedia == true ? TL.InputMediaWebPage.Flags.force_small_media : 0) 615 | }; 616 | 617 | internal static LinkPreviewOptions LinkPreviewOptions(this MessageMediaWebPage mmwp, bool invert_media) => new() 618 | { 619 | Url = mmwp.webpage.Url, 620 | PreferLargeMedia = mmwp.flags.HasFlag(MessageMediaWebPage.Flags.force_large_media), 621 | PreferSmallMedia = mmwp.flags.HasFlag(MessageMediaWebPage.Flags.force_small_media), 622 | ShowAboveText = invert_media 623 | }; 624 | 625 | internal static Birthdate? Birthdate(this TL.Birthday? birthday) => birthday == null ? null : new Birthdate 626 | { 627 | Day = birthday.day, 628 | Month = birthday.month, 629 | Year = birthday.flags.HasFlag(Birthday.Flags.has_year) ? birthday.year : null 630 | }; 631 | 632 | internal static BusinessLocation? BusinessLocation(this TL.BusinessLocation? loc) => loc == null ? null : new BusinessLocation 633 | { 634 | Address = loc.address, 635 | Location = loc.geo_point.Location() 636 | }; 637 | 638 | internal static BusinessOpeningHours? BusinessOpeningHours(this TL.BusinessWorkHours? hours) => hours == null ? null : new BusinessOpeningHours 639 | { 640 | TimeZoneName = hours.timezone_id, 641 | OpeningHours = [.. hours.weekly_open.Select(wo => 642 | new BusinessOpeningHoursInterval { OpeningMinute = wo.start_minute, ClosingMinute = wo.end_minute })] 643 | }; 644 | 645 | internal static Document? Document(this TL.DocumentBase document, TL.PhotoSizeBase? thumb = null) 646 | => document is not TL.Document doc ? null : new Document 647 | { 648 | FileSize = doc.size, 649 | Thumbnail = thumb?.PhotoSize(doc.ToFileLocation(thumb), doc.dc_id), 650 | FileName = doc.Filename, 651 | MimeType = doc.mime_type 652 | }.SetFileIds(doc.ToFileLocation(), doc.dc_id); 653 | 654 | internal static BackgroundType BackgroundType(this WallPaperBase wallpaper) => wallpaper switch 655 | { 656 | WallPaperNoFile wpnf => wpnf.settings.emoticon != null 657 | ? new BackgroundTypeChatTheme { ThemeName = wpnf.settings.emoticon } 658 | : new BackgroundTypeFill { Fill = wpnf.settings.BackgroundFill()!, DarkThemeDimming = wpnf.settings?.intensity ?? 0 }, 659 | WallPaper wp => wp.flags.HasFlag(WallPaper.Flags.pattern) 660 | ? new BackgroundTypePattern 661 | { 662 | Document = wp.document.Document()!, 663 | Fill = wp.settings.BackgroundFill()!, 664 | IsMoving = wp.settings?.flags.HasFlag(WallPaperSettings.Flags.motion) == true, 665 | Intensity = Math.Abs(wp.settings?.intensity ?? 0), 666 | IsInverted = wp.settings?.intensity < 0 667 | } 668 | : new BackgroundTypeWallpaper 669 | { 670 | Document = wp.document.Document()!, 671 | DarkThemeDimming = wp.settings?.intensity ?? 0, 672 | IsBlurred = wp.settings?.flags.HasFlag(WallPaperSettings.Flags.blur) == true, 673 | IsMoving = wp.settings?.flags.HasFlag(WallPaperSettings.Flags.motion) == true, 674 | }, 675 | _ => throw new WTException("Unrecognized WallPaperBase") 676 | }; 677 | 678 | private static BackgroundFill? BackgroundFill(this WallPaperSettings? settings) => settings == null ? null : 679 | settings.flags.HasFlag(WallPaperSettings.Flags.has_third_background_color) ? new BackgroundFillFreeformGradient 680 | { 681 | Colors = settings.flags.HasFlag(WallPaperSettings.Flags.has_fourth_background_color) 682 | ? [settings.background_color, settings.second_background_color, settings.third_background_color, settings.fourth_background_color] 683 | : [settings.background_color, settings.second_background_color, settings.third_background_color] 684 | } 685 | : settings.second_background_color == settings.background_color ? new BackgroundFillSolid { Color = settings.background_color } 686 | : new BackgroundFillGradient 687 | { 688 | TopColor = settings.background_color, 689 | BottomColor = settings.second_background_color, 690 | RotationAngle = settings.rotation 691 | }; 692 | 693 | internal static PaidMedia PaidMedia(MessageExtendedMediaBase memb) => memb switch 694 | { 695 | MessageExtendedMediaPreview memp => new PaidMediaPreview { Width = memp.w, Height = memp.h, Duration = memp.video_duration }, 696 | MessageExtendedMedia mem => PaidMedia(mem.media), 697 | _ => throw new WTException("Unrecognized MessageExtendedMediaBase") 698 | }; 699 | 700 | internal static PaidMedia PaidMedia(TL.MessageMedia media) => media switch 701 | { 702 | MessageMediaPhoto mmp => new PaidMediaPhoto { Photo = mmp.photo.PhotoSizes()! }, 703 | MessageMediaDocument { document: TL.Document document } mmd when mmd.flags.HasFlag(MessageMediaDocument.Flags.video) => 704 | new PaidMediaVideo { Video = document.Video(mmd) }, 705 | _ => throw new WTException("Unrecognized Paid MessageMedia") 706 | }; 707 | 708 | internal static bool IsPositive(this StarsAmount amount) => amount.amount > 0 || (amount.amount == 0 && amount.nanos > 0); 709 | 710 | internal static long ToChatId(this Peer peer) 711 | => peer switch { PeerChat pc => -pc.chat_id, PeerChannel pch => ZERO_CHANNEL_ID - pch.channel_id, _ => peer.ID }; 712 | 713 | internal static BusinessBotRights BusinessBotRights(this TL.BusinessBotRights rights) => new() 714 | { 715 | CanReply = rights.flags.HasFlag(TL.BusinessBotRights.Flags.reply), 716 | CanReadMessages = rights.flags.HasFlag(TL.BusinessBotRights.Flags.read_messages), 717 | CanDeleteSentMessages = rights.flags.HasFlag(TL.BusinessBotRights.Flags.delete_sent_messages), 718 | CanDeleteAllMessages = rights.flags.HasFlag(TL.BusinessBotRights.Flags.delete_received_messages), 719 | CanEditName = rights.flags.HasFlag(TL.BusinessBotRights.Flags.edit_name), 720 | CanEditBio = rights.flags.HasFlag(TL.BusinessBotRights.Flags.edit_bio), 721 | CanEditProfilePhoto = rights.flags.HasFlag(TL.BusinessBotRights.Flags.edit_profile_photo), 722 | CanEditUsername = rights.flags.HasFlag(TL.BusinessBotRights.Flags.edit_username), 723 | CanChangeGiftSettings = rights.flags.HasFlag(TL.BusinessBotRights.Flags.change_gift_settings), 724 | CanViewGiftsAndStars = rights.flags.HasFlag(TL.BusinessBotRights.Flags.view_gifts), 725 | CanConvertGiftsToStars = rights.flags.HasFlag(TL.BusinessBotRights.Flags.sell_gifts), 726 | CanTransferAndUpgradeGifts = rights.flags.HasFlag(TL.BusinessBotRights.Flags.transfer_and_upgrade_gifts), 727 | CanTransferStars = rights.flags.HasFlag(TL.BusinessBotRights.Flags.transfer_stars), 728 | CanManageStories = rights.flags.HasFlag(TL.BusinessBotRights.Flags.manage_stories), 729 | }; 730 | 731 | internal static AcceptedGiftTypes AcceptedGiftTypes(this DisallowedGiftsSettings.Flags flags) => new() 732 | { 733 | UnlimitedGifts = !flags.HasFlag(DisallowedGiftsSettings.Flags.disallow_unlimited_stargifts), 734 | LimitedGifts = !flags.HasFlag(DisallowedGiftsSettings.Flags.disallow_limited_stargifts), 735 | UniqueGifts = !flags.HasFlag(DisallowedGiftsSettings.Flags.disallow_unique_stargifts), 736 | PremiumSubscription = !flags.HasFlag(DisallowedGiftsSettings.Flags.disallow_premium_gifts), 737 | }; 738 | 739 | internal static MediaArea MediaArea(this StoryArea area) 740 | { 741 | return area.Type switch 742 | { 743 | StoryAreaTypeLocation satl => new MediaAreaGeoPoint 744 | { 745 | coordinates = area.Position.Coordinates(), 746 | geo = new() { lat = satl.Latitude, lon = satl.Longitude }, 747 | address = satl.Address.GeoPointAddress(), 748 | flags = satl.Address != null ? MediaAreaGeoPoint.Flags.has_address : 0 749 | }, 750 | StoryAreaTypeSuggestedReaction satsr => new MediaAreaSuggestedReaction 751 | { 752 | coordinates = area.Position.Coordinates(), 753 | reaction = satsr.ReactionType.Reaction(), 754 | flags = (satsr.IsDark ? MediaAreaSuggestedReaction.Flags.dark : 0) | 755 | (satsr.IsFlipped ? MediaAreaSuggestedReaction.Flags.flipped : 0) 756 | }, 757 | StoryAreaTypeLink satl => new MediaAreaUrl 758 | { 759 | coordinates = area.Position.Coordinates(), 760 | url = satl.Url 761 | }, 762 | StoryAreaTypeWeather satw => new MediaAreaWeather 763 | { 764 | coordinates = area.Position.Coordinates(), 765 | emoji = satw.Emoji, 766 | temperature_c = satw.Temperature, 767 | color = satw.BackgroundColor 768 | }, 769 | StoryAreaTypeUniqueGift satug => new MediaAreaStarGift 770 | { 771 | coordinates = area.Position.Coordinates(), 772 | slug = satug.Name 773 | }, 774 | _ => throw new WTException("Unsupported " + area.Type), 775 | }; 776 | } 777 | 778 | internal static MediaAreaCoordinates Coordinates(this StoryAreaPosition pos) => new() 779 | { 780 | x = pos.XPercentage, 781 | y = pos.YPercentage, 782 | w = pos.WidthPercentage, 783 | h = pos.HeightPercentage, 784 | rotation = pos.RotationAngle, 785 | radius = pos.CornerRadiusPercentage, 786 | flags = pos.CornerRadiusPercentage > 0 ? MediaAreaCoordinates.Flags.has_radius : 0 787 | }; 788 | 789 | internal static GeoPointAddress? GeoPointAddress(this LocationAddress? addr) => addr == null ? null : new() 790 | { 791 | country_iso2 = addr.CountryCode, 792 | state = addr.State, 793 | city = addr.City, 794 | street = addr.Street, 795 | flags = (addr.State != null ? TL.GeoPointAddress.Flags.has_state : 0) | 796 | (addr.City != null ? TL.GeoPointAddress.Flags.has_city : 0) | 797 | (addr.Street != null ? TL.GeoPointAddress.Flags.has_street : 0) 798 | }; 799 | } 800 | -------------------------------------------------------------------------------- /src/WTelegramBot.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0;net6.0;net8.0 5 | enable 6 | enable 7 | latest 8 | Telegram.Bot 9 | true 10 | true 11 | true 12 | snupkg 13 | true 14 | WTelegramBot 15 | 0.0.0 16 | Wizou 17 | Telegram Bot API (local server) library providing more extended features 18 | 19 | Release Notes: 20 | $(ReleaseNotes.Replace("|", "%0D%0A").Replace(" - ","%0D%0A- ").Replace(" ", "%0D%0A%0D%0A")) 21 | Copyright © Olivier Marcoux 2024-2025 22 | MIT 23 | https://github.com/wiz0u/WTelegramBot 24 | logo.png 25 | true 26 | https://github.com/wiz0u/WTelegramBot.git 27 | git 28 | Telegram;Bot;Api;Client 29 | README.md 30 | $(ReleaseNotes.Replace("|", "%0D%0A").Replace(" - ","%0D%0A- ").Replace(" ", "%0D%0A%0D%0A")) 31 | NETSDK1138;CS1574;CS0419;CA1510 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/WTelegramBotClient.SendRequest.cs: -------------------------------------------------------------------------------- 1 | using Telegram.Bot.Exceptions; 2 | using Telegram.Bot.Requests; 3 | using Telegram.Bot.Requests.Abstractions; 4 | 5 | namespace Telegram.Bot; 6 | 7 | #pragma warning disable CS8604 8 | 9 | public partial class WTelegramBotClient : ITelegramBotClient 10 | { 11 | /// 12 | public async Task SendRequest(IRequest request, CancellationToken cancellationToken) 13 | { 14 | return (TResponse)(object)(request switch 15 | { 16 | GetUpdatesRequest r => await GetUpdates(r.Offset, r.Limit, r.Timeout, r.AllowedUpdates, cancellationToken), 17 | SetWebhookRequest r => await SetWebhook(r.Url, r.Certificate, r.IpAddress, r.MaxConnections, r.AllowedUpdates, r.DropPendingUpdates, r.SecretToken, cancellationToken).ReturnTrue(), 18 | DeleteWebhookRequest r => await DeleteWebhook(r.DropPendingUpdates, cancellationToken).ReturnTrue(), 19 | GetWebhookInfoRequest => await GetWebhookInfo(cancellationToken), 20 | GetMeRequest => await GetMe(cancellationToken), 21 | LogOutRequest => await LogOut(cancellationToken).ReturnTrue(), 22 | CloseRequest => await Close(cancellationToken).ReturnTrue(), 23 | SendMessageRequest r => await SendMessage(r.ChatId, r.Text, r.ParseMode, r.ReplyParameters, r.ReplyMarkup, r.LinkPreviewOptions, r.MessageThreadId, r.Entities, r.DisableNotification, r.ProtectContent, r.MessageEffectId, r.BusinessConnectionId, r.AllowPaidBroadcast, cancellationToken), 24 | ForwardMessageRequest r => await ForwardMessage(r.ChatId, r.FromChatId, r.MessageId, r.MessageThreadId, r.DisableNotification, r.ProtectContent, r.VideoStartTimestamp, cancellationToken), 25 | ForwardMessagesRequest r => await ForwardMessages(r.ChatId, r.FromChatId, r.MessageIds, r.MessageThreadId, r.DisableNotification, r.ProtectContent, cancellationToken), 26 | CopyMessageRequest r => await CopyMessage(r.ChatId, r.FromChatId, r.MessageId, r.Caption, r.ParseMode, r.ReplyParameters, r.ReplyMarkup, r.MessageThreadId, r.CaptionEntities, r.ShowCaptionAboveMedia, r.DisableNotification, r.ProtectContent, r.AllowPaidBroadcast, r.VideoStartTimestamp, cancellationToken), 27 | CopyMessagesRequest r => await CopyMessages(r.ChatId, r.FromChatId, r.MessageIds, r.RemoveCaption, r.MessageThreadId, r.DisableNotification, r.ProtectContent, cancellationToken), 28 | SendPhotoRequest r => await SendPhoto(r.ChatId, r.Photo, r.Caption, r.ParseMode, r.ReplyParameters, r.ReplyMarkup, r.MessageThreadId, r.CaptionEntities, r.ShowCaptionAboveMedia, r.HasSpoiler, r.DisableNotification, r.ProtectContent, r.MessageEffectId, r.BusinessConnectionId, r.AllowPaidBroadcast, cancellationToken), 29 | SendAudioRequest r => await SendAudio(r.ChatId, r.Audio, r.Caption, r.ParseMode, r.ReplyParameters, r.ReplyMarkup, r.Duration, r.Performer, r.Title, r.Thumbnail, r.MessageThreadId, r.CaptionEntities, r.DisableNotification, r.ProtectContent, r.MessageEffectId, r.BusinessConnectionId, r.AllowPaidBroadcast, cancellationToken), 30 | SendDocumentRequest r => await SendDocument(r.ChatId, r.Document, r.Caption, r.ParseMode, r.ReplyParameters, r.ReplyMarkup, r.Thumbnail, r.MessageThreadId, r.CaptionEntities, r.DisableContentTypeDetection, r.DisableNotification, r.ProtectContent, r.MessageEffectId, r.BusinessConnectionId, r.AllowPaidBroadcast, cancellationToken), 31 | SendVideoRequest r => await SendVideo(r.ChatId, r.Video, r.Caption, r.ParseMode, r.ReplyParameters, r.ReplyMarkup, r.Duration, r.Width, r.Height, r.Thumbnail, r.MessageThreadId, r.CaptionEntities, r.ShowCaptionAboveMedia, r.HasSpoiler, r.SupportsStreaming, r.DisableNotification, r.ProtectContent, r.MessageEffectId, r.BusinessConnectionId, r.AllowPaidBroadcast, r.Cover, r.StartTimestamp, cancellationToken), 32 | SendAnimationRequest r => await SendAnimation(r.ChatId, r.Animation, r.Caption, r.ParseMode, r.ReplyParameters, r.ReplyMarkup, r.Duration, r.Width, r.Height, r.Thumbnail, r.MessageThreadId, r.CaptionEntities, r.ShowCaptionAboveMedia, r.HasSpoiler, r.DisableNotification, r.ProtectContent, r.MessageEffectId, r.BusinessConnectionId, r.AllowPaidBroadcast, cancellationToken), 33 | SendVoiceRequest r => await SendVoice(r.ChatId, r.Voice, r.Caption, r.ParseMode, r.ReplyParameters, r.ReplyMarkup, r.Duration, r.MessageThreadId, r.CaptionEntities, r.DisableNotification, r.ProtectContent, r.MessageEffectId, r.BusinessConnectionId, r.AllowPaidBroadcast, cancellationToken), 34 | SendVideoNoteRequest r => await SendVideoNote(r.ChatId, r.VideoNote, r.ReplyParameters, r.ReplyMarkup, r.Duration, r.Length, r.Thumbnail, r.MessageThreadId, r.DisableNotification, r.ProtectContent, r.MessageEffectId, r.BusinessConnectionId, r.AllowPaidBroadcast, cancellationToken), 35 | SendPaidMediaRequest r => await SendPaidMedia(r.ChatId, r.StarCount, r.Media, r.Caption, r.ParseMode, r.ReplyParameters, r.ReplyMarkup, r.Payload, r.CaptionEntities, r.ShowCaptionAboveMedia, r.DisableNotification, r.ProtectContent, r.BusinessConnectionId, r.AllowPaidBroadcast, cancellationToken), 36 | SendMediaGroupRequest r => await SendMediaGroup(r.ChatId, r.Media, r.ReplyParameters, r.MessageThreadId, r.DisableNotification, r.ProtectContent, r.MessageEffectId, r.BusinessConnectionId, r.AllowPaidBroadcast, cancellationToken), 37 | SendLocationRequest r => await SendLocation(r.ChatId, r.Latitude, r.Longitude, r.ReplyParameters, r.ReplyMarkup, r.HorizontalAccuracy, r.LivePeriod, r.Heading, r.ProximityAlertRadius, r.MessageThreadId, r.DisableNotification, r.ProtectContent, r.MessageEffectId, r.BusinessConnectionId, r.AllowPaidBroadcast, cancellationToken), 38 | SendVenueRequest r => await SendVenue(r.ChatId, r.Latitude, r.Longitude, r.Title, r.Address, r.ReplyParameters, r.ReplyMarkup, r.FoursquareId, r.FoursquareType, r.GooglePlaceId, r.GooglePlaceType, r.MessageThreadId, r.DisableNotification, r.ProtectContent, r.MessageEffectId, r.BusinessConnectionId, r.AllowPaidBroadcast, cancellationToken), 39 | SendContactRequest r => await SendContact(r.ChatId, r.PhoneNumber, r.FirstName, r.LastName, r.Vcard, r.ReplyParameters, r.ReplyMarkup, r.MessageThreadId, r.DisableNotification, r.ProtectContent, r.MessageEffectId, r.BusinessConnectionId, r.AllowPaidBroadcast, cancellationToken), 40 | SendPollRequest r => await SendPoll(r.ChatId, r.Question, r.Options, r.IsAnonymous ?? true, r.Type, r.AllowsMultipleAnswers, r.CorrectOptionId, r.ReplyParameters, r.ReplyMarkup, r.Explanation, r.ExplanationParseMode, r.ExplanationEntities, r.QuestionParseMode, r.QuestionEntities, r.OpenPeriod, r.CloseDate, r.IsClosed, r.MessageThreadId, r.DisableNotification, r.ProtectContent, r.MessageEffectId, r.BusinessConnectionId, r.AllowPaidBroadcast, cancellationToken), 41 | SendDiceRequest r => await SendDice(r.ChatId, r.Emoji, r.ReplyParameters, r.ReplyMarkup, r.MessageThreadId, r.DisableNotification, r.ProtectContent, r.MessageEffectId, r.BusinessConnectionId, r.AllowPaidBroadcast, cancellationToken), 42 | SendChatActionRequest r => await SendChatAction(r.ChatId, r.Action, r.MessageThreadId, r.BusinessConnectionId, cancellationToken).ReturnTrue(), 43 | SetMessageReactionRequest r => await SetMessageReaction(r.ChatId, r.MessageId, r.Reaction, r.IsBig, cancellationToken).ReturnTrue(), 44 | GetUserProfilePhotosRequest r => await GetUserProfilePhotos(r.UserId, r.Offset, r.Limit, cancellationToken), 45 | SetUserEmojiStatusRequest r => await SetUserEmojiStatus(r.UserId, r.EmojiStatusCustomEmojiId, r.EmojiStatusExpirationDate, cancellationToken).ReturnTrue(), 46 | GetFileRequest r => await GetFile(r.FileId, cancellationToken), 47 | BanChatMemberRequest r => await BanChatMember(r.ChatId, r.UserId, r.UntilDate, r.RevokeMessages, cancellationToken).ReturnTrue(), 48 | UnbanChatMemberRequest r => await UnbanChatMember(r.ChatId, r.UserId, r.OnlyIfBanned, cancellationToken).ReturnTrue(), 49 | RestrictChatMemberRequest r => await RestrictChatMember(r.ChatId, r.UserId, r.Permissions, r.UseIndependentChatPermissions, r.UntilDate, cancellationToken).ReturnTrue(), 50 | PromoteChatMemberRequest r => await PromoteChatMember(r.ChatId, r.UserId, r.IsAnonymous, r.CanManageChat, r.CanPostMessages, r.CanEditMessages, r.CanDeleteMessages, r.CanPostStories, r.CanEditStories, r.CanDeleteStories, r.CanManageVideoChats, r.CanRestrictMembers, r.CanPromoteMembers, r.CanChangeInfo, r.CanInviteUsers, r.CanPinMessages, r.CanManageTopics, cancellationToken).ReturnTrue(), 51 | SetChatAdministratorCustomTitleRequest r => await SetChatAdministratorCustomTitle(r.ChatId, r.UserId, r.CustomTitle, cancellationToken).ReturnTrue(), 52 | BanChatSenderChatRequest r => await BanChatSenderChat(r.ChatId, r.SenderChatId, cancellationToken).ReturnTrue(), 53 | UnbanChatSenderChatRequest r => await UnbanChatSenderChat(r.ChatId, r.SenderChatId, cancellationToken).ReturnTrue(), 54 | SetChatPermissionsRequest r => await SetChatPermissions(r.ChatId, r.Permissions, r.UseIndependentChatPermissions, cancellationToken).ReturnTrue(), 55 | ExportChatInviteLinkRequest r => await ExportChatInviteLink(r.ChatId, cancellationToken), 56 | CreateChatInviteLinkRequest r => await CreateChatInviteLink(r.ChatId, r.Name, r.ExpireDate, r.MemberLimit, r.CreatesJoinRequest, cancellationToken), 57 | EditChatInviteLinkRequest r => await EditChatInviteLink(r.ChatId, r.InviteLink, r.Name, r.ExpireDate, r.MemberLimit, r.CreatesJoinRequest, cancellationToken), 58 | CreateChatSubscriptionInviteLinkRequest r => await CreateChatSubscriptionInviteLink(r.ChatId, r.SubscriptionPeriod, r.SubscriptionPrice, r.Name, cancellationToken), 59 | EditChatSubscriptionInviteLinkRequest r => await EditChatSubscriptionInviteLink(r.ChatId, r.InviteLink, r.Name, cancellationToken), 60 | RevokeChatInviteLinkRequest r => await RevokeChatInviteLink(r.ChatId, r.InviteLink, cancellationToken), 61 | ApproveChatJoinRequestRequest r => await ApproveChatJoinRequest(r.ChatId, r.UserId, cancellationToken), 62 | DeclineChatJoinRequestRequest r => await DeclineChatJoinRequest(r.ChatId, r.UserId, cancellationToken), 63 | SetChatPhotoRequest r => await SetChatPhoto(r.ChatId, r.Photo, cancellationToken).ReturnTrue(), 64 | DeleteChatPhotoRequest r => await DeleteChatPhoto(r.ChatId, cancellationToken).ReturnTrue(), 65 | SetChatTitleRequest r => await SetChatTitle(r.ChatId, r.Title, cancellationToken).ReturnTrue(), 66 | SetChatDescriptionRequest r => await SetChatDescription(r.ChatId, r.Description, cancellationToken).ReturnTrue(), 67 | PinChatMessageRequest r => await PinChatMessage(r.ChatId, r.MessageId, r.DisableNotification, r.BusinessConnectionId, cancellationToken).ReturnTrue(), 68 | UnpinChatMessageRequest r => await UnpinChatMessage(r.ChatId, r.MessageId, r.BusinessConnectionId, cancellationToken).ReturnTrue(), 69 | UnpinAllChatMessagesRequest r => await UnpinAllChatMessages(r.ChatId, cancellationToken).ReturnTrue(), 70 | LeaveChatRequest r => await LeaveChat(r.ChatId, cancellationToken).ReturnTrue(), 71 | GetChatRequest r => await GetChat(r.ChatId, cancellationToken), 72 | GetChatAdministratorsRequest r => await GetChatAdministrators(r.ChatId, cancellationToken), 73 | GetChatMemberCountRequest r => await GetChatMemberCount(r.ChatId, cancellationToken), 74 | GetChatMemberRequest r => await GetChatMember(r.ChatId, r.UserId, cancellationToken), 75 | SetChatStickerSetRequest r => await SetChatStickerSet(r.ChatId, r.StickerSetName, cancellationToken).ReturnTrue(), 76 | DeleteChatStickerSetRequest r => await DeleteChatStickerSet(r.ChatId, cancellationToken).ReturnTrue(), 77 | GetForumTopicIconStickersRequest => await GetForumTopicIconStickers(cancellationToken), 78 | CreateForumTopicRequest r => await CreateForumTopic(r.ChatId, r.Name, r.IconColor, r.IconCustomEmojiId, cancellationToken), 79 | EditForumTopicRequest r => await EditForumTopic(r.ChatId, r.MessageThreadId, r.Name, r.IconCustomEmojiId, cancellationToken).ReturnTrue(), 80 | CloseForumTopicRequest r => await CloseForumTopic(r.ChatId, r.MessageThreadId, cancellationToken).ReturnTrue(), 81 | ReopenForumTopicRequest r => await ReopenForumTopic(r.ChatId, r.MessageThreadId, cancellationToken).ReturnTrue(), 82 | DeleteForumTopicRequest r => await DeleteForumTopic(r.ChatId, r.MessageThreadId, cancellationToken).ReturnTrue(), 83 | UnpinAllForumTopicMessagesRequest r => await UnpinAllForumTopicMessages(r.ChatId, r.MessageThreadId, cancellationToken).ReturnTrue(), 84 | EditGeneralForumTopicRequest r => await EditGeneralForumTopic(r.ChatId, r.Name, cancellationToken).ReturnTrue(), 85 | CloseGeneralForumTopicRequest r => await CloseGeneralForumTopic(r.ChatId, cancellationToken).ReturnTrue(), 86 | ReopenGeneralForumTopicRequest r => await ReopenGeneralForumTopic(r.ChatId, cancellationToken).ReturnTrue(), 87 | HideGeneralForumTopicRequest r => await HideGeneralForumTopic(r.ChatId, cancellationToken).ReturnTrue(), 88 | UnhideGeneralForumTopicRequest r => await UnhideGeneralForumTopic(r.ChatId, cancellationToken).ReturnTrue(), 89 | UnpinAllGeneralForumTopicMessagesRequest r => await UnpinAllGeneralForumTopicMessages(r.ChatId, cancellationToken).ReturnTrue(), 90 | AnswerCallbackQueryRequest r => await AnswerCallbackQuery(r.CallbackQueryId, r.Text, r.ShowAlert, r.Url, r.CacheTime, cancellationToken).ReturnTrue(), 91 | GetUserChatBoostsRequest r => await GetUserChatBoosts(r.ChatId, r.UserId, cancellationToken), 92 | GetBusinessConnectionRequest r => await GetBusinessConnection(r.BusinessConnectionId, cancellationToken), 93 | SetMyCommandsRequest r => await SetMyCommands(r.Commands, r.Scope, r.LanguageCode, cancellationToken).ReturnTrue(), 94 | DeleteMyCommandsRequest r => await DeleteMyCommands(r.Scope, r.LanguageCode, cancellationToken).ReturnTrue(), 95 | GetMyCommandsRequest r => await GetMyCommands(r.Scope, r.LanguageCode, cancellationToken), 96 | SetMyNameRequest r => await SetMyName(r.Name, r.LanguageCode, cancellationToken).ReturnTrue(), 97 | GetMyNameRequest r => await GetMyName(r.LanguageCode, cancellationToken), 98 | SetMyDescriptionRequest r => await SetMyDescription(r.Description, r.LanguageCode, cancellationToken).ReturnTrue(), 99 | GetMyDescriptionRequest r => await GetMyDescription(r.LanguageCode, cancellationToken), 100 | SetMyShortDescriptionRequest r => await SetMyShortDescription(r.ShortDescription, r.LanguageCode, cancellationToken).ReturnTrue(), 101 | GetMyShortDescriptionRequest r => await GetMyShortDescription(r.LanguageCode, cancellationToken), 102 | SetChatMenuButtonRequest r => await SetChatMenuButton(r.ChatId, r.MenuButton, cancellationToken).ReturnTrue(), 103 | GetChatMenuButtonRequest r => await GetChatMenuButton(r.ChatId, cancellationToken), 104 | SetMyDefaultAdministratorRightsRequest r => await SetMyDefaultAdministratorRights(r.Rights, r.ForChannels, cancellationToken).ReturnTrue(), 105 | GetMyDefaultAdministratorRightsRequest r => await GetMyDefaultAdministratorRights(r.ForChannels, cancellationToken), 106 | EditMessageTextRequest r => await EditMessageText(r.ChatId, r.MessageId, r.Text, r.ParseMode, r.Entities, r.LinkPreviewOptions, r.ReplyMarkup, r.BusinessConnectionId, cancellationToken), 107 | EditInlineMessageTextRequest r => await EditMessageText(r.InlineMessageId, r.Text, r.ParseMode, r.Entities, r.LinkPreviewOptions, r.ReplyMarkup, r.BusinessConnectionId, cancellationToken).ReturnTrue(), 108 | EditMessageCaptionRequest r => await EditMessageCaption(r.ChatId, r.MessageId, r.Caption, r.ParseMode, r.CaptionEntities, r.ShowCaptionAboveMedia, r.ReplyMarkup, r.BusinessConnectionId, cancellationToken), 109 | EditInlineMessageCaptionRequest r => await EditMessageCaption(r.InlineMessageId, r.Caption, r.ParseMode, r.CaptionEntities, r.ShowCaptionAboveMedia, r.ReplyMarkup, r.BusinessConnectionId, cancellationToken).ReturnTrue(), 110 | EditMessageMediaRequest r => await EditMessageMedia(r.ChatId, r.MessageId, r.Media, r.ReplyMarkup, r.BusinessConnectionId, cancellationToken), 111 | EditInlineMessageMediaRequest r => await EditMessageMedia(r.InlineMessageId, r.Media, r.ReplyMarkup, r.BusinessConnectionId, cancellationToken).ReturnTrue(), 112 | EditMessageLiveLocationRequest r => await EditMessageLiveLocation(r.ChatId, r.MessageId, r.Latitude, r.Longitude, r.LivePeriod, r.HorizontalAccuracy, r.Heading, r.ProximityAlertRadius, r.ReplyMarkup, r.BusinessConnectionId, cancellationToken), 113 | EditInlineMessageLiveLocationRequest r => await EditMessageLiveLocation(r.InlineMessageId, r.Latitude, r.Longitude, r.LivePeriod, r.HorizontalAccuracy, r.Heading, r.ProximityAlertRadius, r.ReplyMarkup, r.BusinessConnectionId, cancellationToken).ReturnTrue(), 114 | StopMessageLiveLocationRequest r => await StopMessageLiveLocation(r.ChatId, r.MessageId, r.ReplyMarkup, r.BusinessConnectionId, cancellationToken), 115 | StopInlineMessageLiveLocationRequest r => await StopMessageLiveLocation(r.InlineMessageId, r.ReplyMarkup, r.BusinessConnectionId, cancellationToken).ReturnTrue(), 116 | EditMessageReplyMarkupRequest r => await EditMessageReplyMarkup(r.ChatId, r.MessageId, r.ReplyMarkup, r.BusinessConnectionId, cancellationToken), 117 | EditInlineMessageReplyMarkupRequest r => await EditMessageReplyMarkup(r.InlineMessageId, r.ReplyMarkup, r.BusinessConnectionId, cancellationToken).ReturnTrue(), 118 | StopPollRequest r => await StopPoll(r.ChatId, r.MessageId, r.ReplyMarkup, r.BusinessConnectionId, cancellationToken), 119 | DeleteMessageRequest r => await DeleteMessage(r.ChatId, r.MessageId, cancellationToken).ReturnTrue(), 120 | DeleteMessagesRequest r => await DeleteMessages(r.ChatId, r.MessageIds, cancellationToken).ReturnTrue(), 121 | GetAvailableGiftsRequest => await GetAvailableGifts(cancellationToken), 122 | SendGiftRequest r => await SendGift(r.ChatId ?? r.UserId, r.GiftId, r.Text, r.TextParseMode, r.TextEntities, r.PayForUpgrade, cancellationToken).ReturnTrue(), 123 | GiftPremiumSubscriptionRequest r => await GiftPremiumSubscription(r.UserId, r.MonthCount, r.StarCount, r.Text, r.TextParseMode, r.TextEntities, cancellationToken).ReturnTrue(), 124 | VerifyUserRequest r => await VerifyUser(r.UserId, r.CustomDescription, cancellationToken).ReturnTrue(), 125 | VerifyChatRequest r => await VerifyChat(r.ChatId, r.CustomDescription, cancellationToken).ReturnTrue(), 126 | RemoveUserVerificationRequest r => await RemoveUserVerification(r.UserId, cancellationToken).ReturnTrue(), 127 | RemoveChatVerificationRequest r => await RemoveChatVerification(r.ChatId, cancellationToken).ReturnTrue(), 128 | ReadBusinessMessageRequest r => await ReadBusinessMessage(r.BusinessConnectionId, r.ChatId, r.MessageId, cancellationToken).ReturnTrue(), 129 | DeleteBusinessMessagesRequest r => await DeleteBusinessMessages(r.BusinessConnectionId, r.MessageIds, cancellationToken).ReturnTrue(), 130 | SetBusinessAccountNameRequest r => await SetBusinessAccountName(r.BusinessConnectionId, r.FirstName, r.LastName, cancellationToken).ReturnTrue(), 131 | SetBusinessAccountUsernameRequest r => await SetBusinessAccountUsername(r.BusinessConnectionId, r.Username, cancellationToken).ReturnTrue(), 132 | SetBusinessAccountBioRequest r => await SetBusinessAccountBio(r.BusinessConnectionId, r.Bio, cancellationToken).ReturnTrue(), 133 | SetBusinessAccountProfilePhotoRequest r => await SetBusinessAccountProfilePhoto(r.BusinessConnectionId, r.Photo, r.IsPublic, cancellationToken).ReturnTrue(), 134 | RemoveBusinessAccountProfilePhotoRequest r => await RemoveBusinessAccountProfilePhoto(r.BusinessConnectionId, r.IsPublic, cancellationToken).ReturnTrue(), 135 | SetBusinessAccountGiftSettingsRequest r => await SetBusinessAccountGiftSettings(r.BusinessConnectionId, r.ShowGiftButton, r.AcceptedGiftTypes, cancellationToken).ReturnTrue(), 136 | GetBusinessAccountStarBalanceRequest r => await GetBusinessAccountStarBalance(r.BusinessConnectionId, cancellationToken), 137 | TransferBusinessAccountStarsRequest r => await TransferBusinessAccountStars(r.BusinessConnectionId, r.StarCount, cancellationToken).ReturnTrue(), 138 | GetBusinessAccountGiftsRequest r => await GetBusinessAccountGifts(r.BusinessConnectionId, r.ExcludeUnsaved, r.ExcludeSaved, r.ExcludeUnlimited, r.ExcludeLimited, r.ExcludeUnique, r.SortByPrice, r.Offset, r.Limit, cancellationToken), 139 | ConvertGiftToStarsRequest r => await ConvertGiftToStars(r.BusinessConnectionId, r.OwnedGiftId, cancellationToken).ReturnTrue(), 140 | UpgradeGiftRequest r => await UpgradeGift(r.BusinessConnectionId, r.OwnedGiftId, r.KeepOriginalDetails, r.StarCount, cancellationToken).ReturnTrue(), 141 | TransferGiftRequest r => await TransferGift(r.BusinessConnectionId, r.OwnedGiftId, r.NewOwnerChatId, r.StarCount, cancellationToken).ReturnTrue(), 142 | PostStoryRequest r => await PostStory(r.BusinessConnectionId, r.Content, r.ActivePeriod, r.Caption, r.ParseMode, r.CaptionEntities, r.Areas, r.PostToChatPage, r.ProtectContent, cancellationToken), 143 | EditStoryRequest r => await EditStory(r.BusinessConnectionId, r.StoryId, r.Content, r.Caption, r.ParseMode, r.CaptionEntities, r.Areas, cancellationToken), 144 | DeleteStoryRequest r => await DeleteStory(r.BusinessConnectionId, r.StoryId, cancellationToken).ReturnTrue(), 145 | SendStickerRequest r => await SendSticker(r.ChatId, r.Sticker, r.ReplyParameters, r.ReplyMarkup, r.Emoji, r.MessageThreadId, r.DisableNotification, r.ProtectContent, r.MessageEffectId, r.BusinessConnectionId, r.AllowPaidBroadcast, cancellationToken), 146 | GetStickerSetRequest r => await GetStickerSet(r.Name, cancellationToken), 147 | GetCustomEmojiStickersRequest r => await GetCustomEmojiStickers(r.CustomEmojiIds, cancellationToken), 148 | UploadStickerFileRequest r => await UploadStickerFile(r.UserId, r.Sticker, r.StickerFormat, cancellationToken), 149 | CreateNewStickerSetRequest r => await CreateNewStickerSet(r.UserId, r.Name, r.Title, r.Stickers, r.StickerType, r.NeedsRepainting, cancellationToken).ReturnTrue(), 150 | AddStickerToSetRequest r => await AddStickerToSet(r.UserId, r.Name, r.Sticker, cancellationToken).ReturnTrue(), 151 | SetStickerPositionInSetRequest r => await SetStickerPositionInSet(r.Sticker, r.Position, cancellationToken).ReturnTrue(), 152 | DeleteStickerFromSetRequest r => await DeleteStickerFromSet(r.Sticker, cancellationToken).ReturnTrue(), 153 | ReplaceStickerInSetRequest r => await ReplaceStickerInSet(r.UserId, r.Name, r.OldSticker, r.Sticker, cancellationToken).ReturnTrue(), 154 | SetStickerEmojiListRequest r => await SetStickerEmojiList(r.Sticker, r.EmojiList, cancellationToken).ReturnTrue(), 155 | SetStickerKeywordsRequest r => await SetStickerKeywords(r.Sticker, r.Keywords, cancellationToken).ReturnTrue(), 156 | SetStickerMaskPositionRequest r => await SetStickerMaskPosition(r.Sticker, r.MaskPosition, cancellationToken).ReturnTrue(), 157 | SetStickerSetTitleRequest r => await SetStickerSetTitle(r.Name, r.Title, cancellationToken).ReturnTrue(), 158 | SetStickerSetThumbnailRequest r => await SetStickerSetThumbnail(r.Name, r.UserId, r.Format, r.Thumbnail, cancellationToken).ReturnTrue(), 159 | SetCustomEmojiStickerSetThumbnailRequest r => await SetCustomEmojiStickerSetThumbnail(r.Name, r.CustomEmojiId, cancellationToken).ReturnTrue(), 160 | DeleteStickerSetRequest r => await DeleteStickerSet(r.Name, cancellationToken).ReturnTrue(), 161 | AnswerInlineQueryRequest r => await AnswerInlineQuery(r.InlineQueryId, r.Results, r.CacheTime, r.IsPersonal, r.NextOffset, r.Button, cancellationToken).ReturnTrue(), 162 | AnswerWebAppQueryRequest r => await AnswerWebAppQuery(r.WebAppQueryId, r.Result, cancellationToken), 163 | SavePreparedInlineMessageRequest r => await SavePreparedInlineMessage(r.UserId, r.Result, r.AllowUserChats, r.AllowBotChats, r.AllowGroupChats, r.AllowChannelChats, cancellationToken), 164 | SendInvoiceRequest r => await SendInvoice(r.ChatId, r.Title, r.Description, r.Payload, r.Currency, r.Prices, r.ProviderToken, r.ProviderData, r.MaxTipAmount, r.SuggestedTipAmounts, r.PhotoUrl, r.PhotoSize, r.PhotoWidth, r.PhotoHeight, r.NeedName, r.NeedPhoneNumber, r.NeedEmail, r.NeedShippingAddress, r.SendPhoneNumberToProvider, r.SendEmailToProvider, r.IsFlexible, r.ReplyParameters, r.ReplyMarkup, r.StartParameter, r.MessageThreadId, r.DisableNotification, r.ProtectContent, r.MessageEffectId, r.AllowPaidBroadcast, cancellationToken), 165 | CreateInvoiceLinkRequest r => await CreateInvoiceLink(r.Title, r.Description, r.Payload, r.Currency, r.Prices, r.ProviderToken, r.ProviderData, r.MaxTipAmount, r.SuggestedTipAmounts, r.PhotoUrl, r.PhotoSize, r.PhotoWidth, r.PhotoHeight, r.NeedName, r.NeedPhoneNumber, r.NeedEmail, r.NeedShippingAddress, r.SendPhoneNumberToProvider, r.SendEmailToProvider, r.IsFlexible, r.SubscriptionPeriod, r.BusinessConnectionId, cancellationToken), 166 | AnswerShippingQueryRequest r => await AnswerShippingQuery(r.ShippingQueryId, r.ShippingOptions, r.ErrorMessage, cancellationToken).ReturnTrue(), 167 | AnswerPreCheckoutQueryRequest r => await AnswerPreCheckoutQuery(r.PreCheckoutQueryId, r.ErrorMessage, cancellationToken).ReturnTrue(), 168 | GetStarTransactionsRequest r => await GetStarTransactions(r.Offset, r.Limit, cancellationToken), 169 | RefundStarPaymentRequest r => await RefundStarPayment(r.UserId, r.TelegramPaymentChargeId, cancellationToken).ReturnTrue(), 170 | EditUserStarSubscriptionRequest r => await EditUserStarSubscription(r.UserId, r.TelegramPaymentChargeId, r.IsCanceled, cancellationToken).ReturnTrue(), 171 | SetPassportDataErrorsRequest r => await SetPassportDataErrors(r.UserId, r.Errors, cancellationToken).ReturnTrue(), 172 | SendGameRequest r => await SendGame(r.ChatId, r.GameShortName, r.ReplyParameters, r.ReplyMarkup, r.MessageThreadId, r.DisableNotification, r.ProtectContent, r.MessageEffectId, r.BusinessConnectionId, r.AllowPaidBroadcast, cancellationToken), 173 | SetGameScoreRequest r => await SetGameScore(r.UserId, r.Score, r.ChatId, r.MessageId, r.Force, r.DisableEditMessage, cancellationToken), 174 | SetInlineGameScoreRequest r => await SetGameScore(r.UserId, r.Score, r.InlineMessageId, r.Force, r.DisableEditMessage, cancellationToken).ReturnTrue(), 175 | GetGameHighScoresRequest r => await GetGameHighScores(r.UserId, r.ChatId, r.MessageId, cancellationToken), 176 | GetInlineGameHighScoresRequest r => await GetGameHighScores(r.UserId, r.InlineMessageId, cancellationToken), 177 | _ => throw new ApiRequestException("Not found", 404) 178 | }); 179 | } 180 | } -------------------------------------------------------------------------------- /src/WTelegramBotClient.cs: -------------------------------------------------------------------------------- 1 | using System.Data.Common; 2 | using Telegram.Bot.Exceptions; 3 | 4 | namespace Telegram.Bot; 5 | 6 | /// A client to use the Telegram Bot API 7 | /// Create a new instance. 8 | /// Configuration for 9 | /// A custom 10 | /// Global cancellation token 11 | /// Thrown if is 12 | public partial class WTelegramBotClient(WTelegramBotClientOptions options, HttpClient? httpClient = default, CancellationToken cancellationToken = default) 13 | : WTelegram.Bot(options.WTCConfig, options.DbConnection, options.SqlCommands, httpClient), ITelegramBotClient, IDisposable 14 | { 15 | /// Bot token 16 | public string Token => options.Token; 17 | 18 | /// 19 | public bool LocalBotServer => options.LocalBotServer; 20 | 21 | /// 22 | public TimeSpan Timeout 23 | { 24 | get => _httpClient?.Timeout ?? _timeout; 25 | set { if (_httpClient != null) _httpClient.Timeout = value; else _timeout = value; } 26 | } 27 | private TimeSpan _timeout = TimeSpan.FromSeconds(100); 28 | 29 | /// Global cancellation token 30 | public CancellationToken GlobalCancelToken { get; } = cancellationToken; 31 | 32 | /// 33 | public IExceptionParser ExceptionsParser { get; set; } = new DefaultExceptionParser(); 34 | /// 35 | [Obsolete("Not supported by WTelegramBot")] 36 | event AsyncEventHandler? ITelegramBotClient.OnMakingApiRequest { add { } remove { } } 37 | /// 38 | [Obsolete("Not supported by WTelegramBot")] 39 | event AsyncEventHandler? ITelegramBotClient.OnApiResponseReceived { add { } remove { } } 40 | 41 | /// Create a new instance. 42 | /// The bot token 43 | /// API id (see https://my.telegram.org/apps) 44 | /// API hash (see https://my.telegram.org/apps) 45 | /// DB connection for storage and later resume 46 | /// A custom 47 | /// Global cancellation token 48 | /// Thrown if format is invalid 49 | public WTelegramBotClient(string token, int apiId, string apiHash, DbConnection dbConnection, HttpClient? httpClient = null, CancellationToken cancellationToken = default) : 50 | this(new WTelegramBotClientOptions(token, apiId, apiHash, dbConnection), httpClient, cancellationToken) 51 | { } 52 | 53 | /// 54 | [Obsolete("Method MakeRequestAsync was renamed as SendRequest")] 55 | public Task MakeRequestAsync(IRequest request, CancellationToken cancellationToken) 56 | => SendRequest(request, cancellationToken); 57 | 58 | /// 59 | [Obsolete("Method MakeRequest was renamed as SendRequest")] 60 | public Task MakeRequest(IRequest request, CancellationToken cancellationToken) 61 | => SendRequest(request, cancellationToken); 62 | 63 | /// 64 | public async Task TestApi(CancellationToken cancellationToken = default) 65 | { 66 | try 67 | { 68 | await GetMe(cancellationToken); 69 | return true; 70 | } 71 | catch (ApiRequestException e) when (e.ErrorCode is 400 or 401) { return false; } 72 | } 73 | 74 | /// 75 | public Task DownloadFile(TGFile file, Stream destination, CancellationToken cancellationToken = default) 76 | => DownloadFile(file.FilePath!, destination, cancellationToken); 77 | 78 | /// 79 | public new async Task DownloadFile(string filePath, Stream destination, CancellationToken cancellationToken = default) 80 | { 81 | using var cts = CancellationTokenSource.CreateLinkedTokenSource(GlobalCancelToken, cancellationToken); 82 | await base.DownloadFile(filePath, destination, cts.Token); 83 | } 84 | 85 | /// Use this method to get basic info about a file download it. For the moment, bots can download files of up to 20MB in size. 86 | /// File identifier to get info about 87 | /// Destination stream to write file to 88 | /// A cancellation token that can be used by other objects or threads to receive notice of cancellation 89 | /// On success, a object is returned. 90 | public new async Task GetInfoAndDownloadFile( 91 | string fileId, 92 | Stream destination, 93 | CancellationToken cancellationToken = default 94 | ) => 95 | await ThrowIfCancelled(cancellationToken).GetInfoAndDownloadFile(fileId, destination, cancellationToken).ThrowAsApi(this); 96 | 97 | /// Convert WTelegram Exception into ApiRequestException 98 | internal ApiRequestException MakeException(WTelegram.WTException ex) 99 | { 100 | if (ex is not TL.RpcException rpcEx) return new ApiRequestException(ex.Message, 400, ex); 101 | var msg = ex.Message switch 102 | { 103 | "MESSAGE_NOT_MODIFIED" => "message is not modified: specified new message content and reply markup are exactly the same as a current content and reply markup of the message", 104 | "WC_CONVERT_URL_INVALID" or "EXTERNAL_URL_INVALID" => "Wrong HTTP URL specified", 105 | "WEBPAGE_CURL_FAILED" => "Failed to get HTTP URL content", 106 | "WEBPAGE_MEDIA_EMPTY" => "Wrong type of the web page content", 107 | "MEDIA_GROUPED_INVALID" => "Can't use the media of the specified type in the album", 108 | "REPLY_MARKUP_TOO_LONG" => "reply markup is too long", 109 | "INPUT_USER_DEACTIVATED" => "user is deactivated", // force 403 110 | "USER_IS_BLOCKED" => "bot was blocked by the user", // force 403 111 | "USER_ADMIN_INVALID" => "user is an administrator of the chat", 112 | "File generation failed" => "can't upload file by URL", 113 | "CHAT_ABOUT_NOT_MODIFIED" => "chat description is not modified", 114 | "PACK_SHORT_NAME_INVALID" => "invalid sticker set name is specified", 115 | "PACK_SHORT_NAME_OCCUPIED" => "sticker set name is already occupied", 116 | "STICKER_EMOJI_INVALID" => "invalid sticker emojis", 117 | "QUERY_ID_INVALID" => "query is too old and response timeout expired or query ID is invalid", 118 | "MESSAGE_DELETE_FORBIDDEN" => "message can't be deleted", 119 | _ => ex.Message, 120 | }; 121 | msg = rpcEx.Code switch 122 | { 123 | 401 => "Unauthorized: " + msg, 124 | 403 => "Forbidden: " + msg, 125 | 500 => "Internal Server Error: " + msg, 126 | _ => "Bad Request: " + msg, 127 | }; 128 | return ExceptionsParser.Parse(new() { Description = msg, ErrorCode = rpcEx.Code }); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/WTelegramBotClientOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Data.Common; 2 | using WTelegram; 3 | 4 | namespace Telegram.Bot; 5 | 6 | /// This class is used to provide configuration for 7 | public class WTelegramBotClientOptions : TelegramBotClientOptions 8 | { 9 | /// Your api_id, obtained at https://my.telegram.org/apps 10 | public int ApiId { get; } 11 | /// Your api_hash, obtained at https://my.telegram.org/apps 12 | public string ApiHash { get; } 13 | /// Connection to Database for loading/storing the bot state 14 | public DbConnection DbConnection { get; } 15 | /// You can set the SQL queries for your specific DB engine 16 | public string[] SqlCommands { get; set; } 17 | 18 | /// Create a new instance. 19 | /// API token 20 | /// API id (see https://my.telegram.org/apps) 21 | /// API hash (see https://my.telegram.org/apps) 22 | /// DB connection for storage and later resume 23 | /// Template for SQL strings 24 | /// Indicates that test environment will be used 25 | /// Thrown if format is invalid 26 | public WTelegramBotClientOptions(string token, int apiId, string apiHash, DbConnection dbConnection, SqlCommands sqlCommands = WTelegram.SqlCommands.Detect, bool useTestEnvironment = false) 27 | : base(token, useTestEnvironment: useTestEnvironment) 28 | { 29 | ApiId = apiId; 30 | ApiHash = apiHash; 31 | DbConnection = dbConnection; 32 | if (sqlCommands == WTelegram.SqlCommands.Detect) sqlCommands = Database.DetectType(dbConnection); 33 | SqlCommands = Database.DefaultSqlCommands[(int)sqlCommands]; 34 | } 35 | 36 | /// The Config callback used by WTelegramClient 37 | public virtual string? WTCConfig(string what) => what switch 38 | { 39 | "api_id" => ApiId.ToString(), 40 | "api_hash" => ApiHash, 41 | "bot_token" => Token, 42 | "device_model" => "server", 43 | "server_address" => UseTestEnvironment ? "2>149.154.167.40:443" : "2>149.154.167.50:443", 44 | _ => null 45 | }; 46 | } 47 | --------------------------------------------------------------------------------