├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── config.yml ├── dev.yml ├── release.yml └── workflows │ ├── autolock.yml │ ├── dev.yml │ ├── release.yml │ └── telegram-api.yml ├── .gitignore ├── EXAMPLES.md ├── Examples ├── ASPnet_webapp.zip ├── Program_DownloadSavedMedia.cs ├── Program_GetAllChats.cs ├── Program_Heroku.cs ├── Program_ListenUpdates.cs ├── Program_ReactorError.cs ├── Program_SecretChats.cs └── WinForms_app.zip ├── FAQ.md ├── LICENSE.txt ├── README.md ├── generator ├── MTProtoGenerator.cs └── MTProtoGenerator.csproj ├── logo.png └── src ├── Client.Helpers.cs ├── Client.cs ├── Compat.cs ├── Encryption.cs ├── Helpers.cs ├── SecretChats.cs ├── Services.cs ├── Session.cs ├── TL.MTProto.cs ├── TL.Schema.cs ├── TL.SchemaFuncs.cs ├── TL.Secret.cs ├── TL.Table.cs ├── TL.Xtended.cs ├── TL.cs ├── TlsStream.cs ├── UpdateManager.cs ├── WTelegramClient.csproj └── WTelegramClient.sln /.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/WTelegramClientBot?start=donate"] 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: You have a question about the Telegram API or how to do something with WTelegramClient? 3 | url: https://stackoverflow.com/questions/ask?tags=c%23+wtelegramclient+telegram-api 4 | about: The answer to your question can be helpful to the community so it's better to ask them on StackOverflow ---> 5 | -------------------------------------------------------------------------------- /.github/dev.yml: -------------------------------------------------------------------------------- 1 | pr: none 2 | trigger: 3 | branches: 4 | include: [ master ] 5 | paths: 6 | exclude: [ '.github', '*.md', 'Examples' ] 7 | 8 | name: 4.3.2-dev.$(Rev:r) 9 | 10 | pool: 11 | vmImage: ubuntu-latest 12 | 13 | variables: 14 | buildConfiguration: 'Release' 15 | Release_Notes: $[replace(variables['Build.SourceVersionMessage'], '"', '''''')] 16 | 17 | stages: 18 | - stage: publish 19 | jobs: 20 | - job: publish 21 | steps: 22 | - task: UseDotNet@2 23 | displayName: 'Use .NET Core sdk' 24 | inputs: 25 | packageType: 'sdk' 26 | version: '9.x' 27 | includePreviewVersions: true 28 | 29 | - task: DotNetCoreCLI@2 30 | inputs: 31 | command: 'pack' 32 | packagesToPack: 'src/WTelegramClient.csproj' 33 | includesymbols: true 34 | versioningScheme: 'byEnvVar' 35 | versionEnvVar: 'Build.BuildNumber' 36 | buildProperties: NoWarn="0419;1573;1591";ContinuousIntegrationBuild=true;Version=$(Build.BuildNumber);"ReleaseNotes=$(Release_Notes)" 37 | 38 | - task: NuGetCommand@2 39 | inputs: 40 | command: 'push' 41 | packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg' 42 | publishPackageMetadata: true 43 | nuGetFeedType: 'external' 44 | publishFeedCredentials: 'nuget.org' 45 | 46 | - stage: notify 47 | jobs: 48 | - job: notify 49 | pool: 50 | server 51 | steps: 52 | - task: InvokeRESTAPI@1 53 | inputs: 54 | connectionType: 'connectedServiceName' 55 | serviceConnection: 'Telegram Deploy Notice' 56 | method: 'POST' 57 | body: | 58 | { 59 | "status": "success", 60 | "complete": true, 61 | "message": "{ \"commitId\": \"$(Build.SourceVersion)\", \"buildNumber\": \"$(Build.BuildNumber)\", \"teamProjectName\": \"$(System.TeamProject)\", \"commitMessage\": \"$(Release_Notes)\" }" 62 | } 63 | waitForCompletion: 'false' 64 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | pr: none 2 | trigger: none 3 | 4 | name: 4.3.$(Rev:r) 5 | 6 | pool: 7 | vmImage: ubuntu-latest 8 | 9 | variables: 10 | buildConfiguration: 'Release' 11 | Release_Notes: $[replace(variables['releaseNotes'], '"', '''''')] 12 | 13 | stages: 14 | - stage: publish 15 | jobs: 16 | - job: publish 17 | steps: 18 | - checkout: self 19 | persistCredentials: true 20 | 21 | - task: UseDotNet@2 22 | displayName: 'Use .NET Core sdk' 23 | inputs: 24 | packageType: 'sdk' 25 | version: '9.x' 26 | includePreviewVersions: true 27 | 28 | - task: DotNetCoreCLI@2 29 | inputs: 30 | command: 'pack' 31 | packagesToPack: 'src/WTelegramClient.csproj' 32 | includesymbols: true 33 | versioningScheme: 'byEnvVar' 34 | versionEnvVar: 'Build.BuildNumber' 35 | buildProperties: NoWarn="0419;1573;1591";ContinuousIntegrationBuild=true;Version=$(Build.BuildNumber);"ReleaseNotes=$(Release_Notes)" 36 | 37 | - task: NuGetCommand@2 38 | inputs: 39 | command: 'push' 40 | packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg' 41 | nuGetFeedType: 'external' 42 | publishFeedCredentials: 'nuget.org' 43 | 44 | - script: | 45 | git tag $(Build.BuildNumber) 46 | git push --tags 47 | workingDirectory: $(Build.SourcesDirectory) 48 | displayName: Git Tag 49 | 50 | - stage: notify 51 | jobs: 52 | - job: notify 53 | pool: 54 | server 55 | steps: 56 | - task: InvokeRESTAPI@1 57 | inputs: 58 | connectionType: 'connectedServiceName' 59 | serviceConnection: 'Telegram Deploy Notice' 60 | method: 'POST' 61 | body: | 62 | { 63 | "status": "success", 64 | "complete": true, 65 | "message": "{ \"commitId\": \"$(Build.SourceVersion)\", \"buildNumber\": \"$(Build.BuildNumber)\", \"teamProjectName\": \"$(System.TeamProject)\"}" 66 | } 67 | waitForCompletion: 'false' 68 | -------------------------------------------------------------------------------- /.github/workflows/autolock.yml: -------------------------------------------------------------------------------- 1 | name: 'Auto-Lock Issues' 2 | 3 | on: 4 | schedule: 5 | - cron: '17 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 | push: 5 | branches: [ master ] 6 | paths-ignore: [ '.**', 'Examples/**', '**.md' ] 7 | 8 | env: 9 | PROJECT_PATH: src/WTelegramClient.csproj 10 | CONFIGURATION: Release 11 | RELEASE_NOTES: ${{ github.event.head_commit.message }} 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 100 20 | - name: Determine version 21 | run: | 22 | git fetch --depth=100 --tags 23 | DESCR_TAG=$(git describe --tags) 24 | COMMITS=${DESCR_TAG#*-} 25 | COMMITS=${COMMITS%-*} 26 | LAST_TAG=${DESCR_TAG%%-*} 27 | NEXT_VERSION=${LAST_TAG%.*}.$((${LAST_TAG##*.} + 1))-dev.$COMMITS 28 | RELEASE_VERSION=${{vars.RELEASE_VERSION}}-dev.$COMMITS 29 | if [[ "$RELEASE_VERSION" > "$NEXT_VERSION" ]] then VERSION=$RELEASE_VERSION; else VERSION=$NEXT_VERSION; fi 30 | echo Last tag: $LAST_TAG · Next version: $NEXT_VERSION · Release version: $RELEASE_VERSION · Build version: $VERSION 31 | echo "VERSION=$VERSION" >> $GITHUB_ENV 32 | - name: Setup .NET 33 | uses: actions/setup-dotnet@v4 34 | with: 35 | dotnet-version: 8.0.x 36 | - name: Pack 37 | run: dotnet pack $PROJECT_PATH --configuration $CONFIGURATION -p:Version=$VERSION "-p:ReleaseNotes=\"$RELEASE_NOTES\"" --output packages 38 | # - name: Upload artifact 39 | # uses: actions/upload-artifact@v4 40 | # with: 41 | # name: packages 42 | # path: packages/*.nupkg 43 | - name: Nuget push 44 | run: dotnet nuget push packages/*.nupkg --api-key ${{secrets.NUGETAPIKEY}} --skip-duplicate --source https://api.nuget.org/v3/index.json 45 | - name: Deployment Notification 46 | env: 47 | JSON: | 48 | { 49 | "status": "success", "complete": true, "commitMessage": ${{ toJSON(github.event.head_commit.message) }}, 50 | "message": "{ \"commitId\": \"${{ github.sha }}\", \"buildNumber\": \"${{ env.VERSION }}\", \"repoName\": \"${{ github.repository }}\"}" 51 | } 52 | run: | 53 | curl -X POST -H "Content-Type: application/json" -d "$JSON" ${{ secrets.DEPLOYED_WEBHOOK }} 54 | -------------------------------------------------------------------------------- /.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/WTelegramClient.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 | -------------------------------------------------------------------------------- /.github/workflows/telegram-api.yml: -------------------------------------------------------------------------------- 1 | name: 'Telegram API issues' 2 | 3 | on: 4 | issues: 5 | types: [labeled] 6 | 7 | permissions: 8 | issues: write 9 | 10 | jobs: 11 | action: 12 | if: contains(github.event.issue.labels.*.name, 'telegram api') 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: dessant/support-requests@v3.0.0 16 | with: 17 | support-label: 'telegram api' 18 | issue-comment: > 19 | Please note that **Github issues** should be used only for problems with the library code itself. 20 | 21 | 22 | For questions about Telegram API usage, you can search the [API official documentation](https://core.telegram.org/api#getting-started) and the [full list of methods](https://core.telegram.org/methods). 23 | 24 | WTelegramClient covers 100% of the API and let you do anything you can do in an official client. 25 | 26 | 27 | If the above links didn't answer your problem, [click here to ask your question on **StackOverflow**](https://stackoverflow.com/questions/ask?tags=c%23+wtelegramclient+telegram-api) so the whole community can help and benefit. 28 | close-issue: true 29 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /Examples/ASPnet_webapp.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiz0u/WTelegramClient/5358471574404a4103c386a9c5bc18c4fec25fcc/Examples/ASPnet_webapp.zip -------------------------------------------------------------------------------- /Examples/Program_DownloadSavedMedia.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using TL; 6 | 7 | namespace WTelegramClientTest 8 | { 9 | static class Program_DownloadSavedMedia 10 | { 11 | // go to Project Properties > Debug > Launch Profiles > Environment variables and add at least these: api_id, api_hash, phone_number 12 | static async Task Main(string[] _) 13 | { 14 | Console.WriteLine("The program will download photos/medias from messages you send/forward to yourself (Saved Messages)"); 15 | var cts = new CancellationTokenSource(); 16 | await using var client = new WTelegram.Client(Environment.GetEnvironmentVariable); 17 | var user = await client.LoginUserIfNeeded(); 18 | client.OnUpdates += Client_OnUpdates; 19 | Console.ReadKey(); 20 | cts.Cancel(); 21 | 22 | async Task Client_OnUpdates(UpdatesBase updates) 23 | { 24 | foreach (var update in updates.UpdateList) 25 | { 26 | if (update is not UpdateNewMessage { message: Message message }) 27 | continue; // if it's not about a new message, ignore the update 28 | if (message.peer_id.ID != user.ID) 29 | continue; // if it's not in the "Saved messages" chat, ignore it 30 | 31 | if (message.media is MessageMediaDocument { document: Document document }) 32 | { 33 | var filename = document.Filename; // use document original filename, or build a name from document ID & MIME type: 34 | filename ??= $"{document.id}.{document.mime_type[(document.mime_type.IndexOf('/') + 1)..]}"; 35 | Console.WriteLine("Downloading " + filename); 36 | using var fileStream = File.Create(filename); 37 | await client.DownloadFileAsync(document, fileStream, progress: (p, t) => cts.Token.ThrowIfCancellationRequested()); 38 | Console.WriteLine("Download finished"); 39 | } 40 | else if (message.media is MessageMediaPhoto { photo: Photo photo }) 41 | { 42 | var filename = $"{photo.id}.jpg"; 43 | Console.WriteLine("Downloading " + filename); 44 | using var fileStream = File.Create(filename); 45 | var type = await client.DownloadFileAsync(photo, fileStream); 46 | fileStream.Close(); // necessary for the renaming 47 | Console.WriteLine("Download finished"); 48 | if (type is not Storage_FileType.unknown and not Storage_FileType.partial) 49 | File.Move(filename, $"{photo.id}.{type}", true); // rename extension 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Examples/Program_GetAllChats.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using TL; 4 | 5 | namespace WTelegramClientTest 6 | { 7 | static class Program_GetAllChats 8 | { 9 | // This code is similar to what you should have obtained if you followed the README introduction 10 | // I've just added a few comments to explain further what's going on 11 | 12 | // go to Project Properties > Debug > Launch Profiles > Environment variables and add at least these: api_id, api_hash, phone_number 13 | static string Config(string what) 14 | { 15 | if (what == "api_id") return Environment.GetEnvironmentVariable("api_id"); 16 | if (what == "api_hash") return Environment.GetEnvironmentVariable("api_hash"); 17 | if (what == "phone_number") return Environment.GetEnvironmentVariable("phone_number"); 18 | if (what == "verification_code") return null; // let WTelegramClient ask the user with a console prompt 19 | if (what == "first_name") return "John"; // if sign-up is required 20 | if (what == "last_name") return "Doe"; // if sign-up is required 21 | if (what == "password") return "secret!"; // if user has enabled 2FA 22 | return null; 23 | } 24 | 25 | static async Task Main(string[] _) 26 | { 27 | await using var client = new WTelegram.Client(Config); 28 | var user = await client.LoginUserIfNeeded(); 29 | Console.WriteLine($"We are logged-in as {user.username ?? user.first_name + " " + user.last_name} (id {user.id})"); 30 | 31 | var chats = await client.Messages_GetAllChats(); // chats = groups/channels (does not include users dialogs) 32 | Console.WriteLine("This user has joined the following:"); 33 | foreach (var (id, chat) in chats.chats) 34 | switch (chat) 35 | { 36 | case Chat smallgroup when smallgroup.IsActive: 37 | Console.WriteLine($"{id}: Small group: {smallgroup.title} with {smallgroup.participants_count} members"); 38 | break; 39 | case Channel channel when channel.IsChannel: 40 | Console.WriteLine($"{id}: Channel {channel.username}: {channel.title}"); 41 | //Console.WriteLine($" → access_hash = {channel.access_hash:X}"); 42 | break; 43 | case Channel group: // no broadcast flag => it's a big group, also called supergroup or megagroup 44 | Console.WriteLine($"{id}: Group {group.username}: {group.title}"); 45 | //Console.WriteLine($" → access_hash = {group.access_hash:X}"); 46 | break; 47 | } 48 | 49 | Console.Write("Type a chat ID to send a message: "); 50 | long chatId = long.Parse(Console.ReadLine()); 51 | var target = chats.chats[chatId]; 52 | Console.WriteLine($"Sending a message in chat {chatId}: {target.Title}"); 53 | // Next line implicitely creates an adequate InputPeer from ChatBase: (with the access_hash if these is one) 54 | InputPeer peer = target; 55 | await client.SendMessageAsync(peer, "Hello, World"); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Examples/Program_Heroku.cs: -------------------------------------------------------------------------------- 1 | using Npgsql; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using TL; 8 | 9 | // This is an example userbot designed to run on a Heroku account with a PostgreSQL database for session storage 10 | // This userbot simply answer "Pong" when someone sends him a "Ping" private message (or in Saved Messages) 11 | // To use/install/deploy this userbot ➡️ follow the steps at the end of this file 12 | // When run locally, close the window or type ALT-F4 to exit cleanly and save session (similar to Heroku SIGTERM) 13 | 14 | namespace WTelegramClientTest 15 | { 16 | static class Program_Heroku 17 | { 18 | static WTelegram.Client Client; 19 | static User My; 20 | static readonly Dictionary Users = []; 21 | static readonly Dictionary Chats = []; 22 | 23 | // See steps at the end of this file to setup required Environment variables 24 | static async Task Main(string[] _) 25 | { 26 | var exit = new SemaphoreSlim(0); 27 | AppDomain.CurrentDomain.ProcessExit += (s, e) => exit.Release(); // detect SIGTERM to exit gracefully 28 | var store = new PostgreStore(Environment.GetEnvironmentVariable("DATABASE_URL"), Environment.GetEnvironmentVariable("SESSION_NAME")); 29 | // if DB does not contain a session yet, client will be run in interactive mode 30 | Client = new WTelegram.Client(store.Length == 0 ? null : Environment.GetEnvironmentVariable, store); 31 | await using (Client) 32 | { 33 | Client.OnUpdates += Client_OnUpdates; 34 | My = await Client.LoginUserIfNeeded(); 35 | Console.WriteLine($"We are logged-in as {My.username ?? My.first_name + " " + My.last_name} (id {My.id})"); 36 | var dialogs = await Client.Messages_GetAllDialogs(); 37 | dialogs.CollectUsersChats(Users, Chats); 38 | await exit.WaitAsync(); 39 | } 40 | } 41 | 42 | private static async Task Client_OnUpdates(UpdatesBase updates) 43 | { 44 | updates.CollectUsersChats(Users, Chats); 45 | foreach (var update in updates.UpdateList) 46 | { 47 | Console.WriteLine(update.GetType().Name); 48 | if (update is UpdateNewMessage { message: Message { peer_id: PeerUser { user_id: var user_id } } msg }) // private message 49 | if (!msg.flags.HasFlag(Message.Flags.out_)) // ignore our own outgoing messages 50 | if (Users.TryGetValue(user_id, out var user)) 51 | { 52 | Console.WriteLine($"New message from {user}: {msg.message}"); 53 | if (msg.message.Equals("Ping", StringComparison.OrdinalIgnoreCase)) 54 | await Client.SendMessageAsync(user, "Pong"); 55 | } 56 | } 57 | } 58 | } 59 | 60 | #region PostgreSQL session store 61 | class PostgreStore : Stream 62 | { 63 | private readonly NpgsqlConnection _sql; 64 | private readonly string _sessionName; 65 | private readonly byte[] _data; 66 | private readonly int _dataLen; 67 | 68 | /// Heroku DB URL of the form "postgres://user:password@host:port/database" 69 | /// Entry name for the session data in the WTelegram_sessions table (default: "Heroku") 70 | public PostgreStore(string databaseUrl, string sessionName = null) 71 | { 72 | _sessionName = sessionName ?? "Heroku"; 73 | var parts = databaseUrl.Split(':', '/', '@'); 74 | _sql = new NpgsqlConnection($"User ID={parts[3]};Password={parts[4]};Host={parts[5]};Port={parts[6]};Database={parts[7]};Pooling=true;SSL Mode=Require;Trust Server Certificate=True;"); 75 | _sql.Open(); 76 | using (var create = new NpgsqlCommand("CREATE TABLE IF NOT EXISTS WTelegram_sessions (name text NOT NULL PRIMARY KEY, data bytea)", _sql)) 77 | create.ExecuteNonQuery(); 78 | using var cmd = new NpgsqlCommand($"SELECT data FROM WTelegram_sessions WHERE name = '{_sessionName}'", _sql); 79 | using var rdr = cmd.ExecuteReader(); 80 | if (rdr.Read()) 81 | _dataLen = (_data = rdr[0] as byte[]).Length; 82 | } 83 | 84 | protected override void Dispose(bool disposing) 85 | { 86 | _sql.Dispose(); 87 | } 88 | 89 | public override int Read(byte[] buffer, int offset, int count) 90 | { 91 | Array.Copy(_data, 0, buffer, offset, count); 92 | return count; 93 | } 94 | 95 | public override void Write(byte[] buffer, int offset, int count) // Write call and buffer modifications are done within a lock() 96 | { 97 | using var cmd = new NpgsqlCommand($"INSERT INTO WTelegram_sessions (name, data) VALUES ('{_sessionName}', @data) ON CONFLICT (name) DO UPDATE SET data = EXCLUDED.data", _sql); 98 | cmd.Parameters.AddWithValue("data", count == buffer.Length ? buffer : buffer[offset..(offset + count)]); 99 | cmd.ExecuteNonQuery(); 100 | } 101 | 102 | public override long Length => _dataLen; 103 | public override long Position { get => 0; set { } } 104 | public override bool CanSeek => false; 105 | public override bool CanRead => true; 106 | public override bool CanWrite => true; 107 | public override long Seek(long offset, SeekOrigin origin) => 0; 108 | public override void SetLength(long value) { } 109 | public override void Flush() { } 110 | } 111 | #endregion 112 | } 113 | 114 | /****************************************************************************************************************************** 115 | HOW TO USE AND DEPLOY THIS EXAMPLE HEROKU USERBOT: 116 | - From your Heroku.com account dashboard, create a new app 117 | - Navigate to the app Resources and add the add-on "Heroku Postgres" 118 | - Navigate to the app Settings, click Reveal Config Vars and save the Heroku git URL and the value of DATABASE_URL 119 | - Add a new var named "api_hash" with your api hash obtained from https://my.telegram.org/apps 120 | - Add a new var named "phone_number" with the phone_number of the user this userbot will manage 121 | - Scroll down to Buildpacks and add this URL: https://github.com/jincod/dotnetcore-buildpack.git 122 | - In Visual Studio, Clone the Heroku git repository and add some standard .gitignore .gitattributes files 123 | - In this repository folder, create a new .NET Console project with this Program.cs file 124 | - Add these Nuget packages: WTelegramClient and Npgsql 125 | - In Project properties > Debug > Launch Profiles > Environment variables, configure the same values for DATABASE_URL, api_hash, phone_number 126 | - Run the project in Visual Studio. The first time, it should ask you interactively for elements to complete the connection 127 | - On the following runs, the PostgreSQL database contains the session data and it should connect automatically 128 | - You can test the userbot by sending him "Ping" in private message (or saved messages). It should respond with "Pong" 129 | - You can now commit & push your git sources to Heroku, they will be compiled and deployed/run as a web app 130 | - The userbot should work fine for 1 minute, but it will be taken down because it is not a web app. Let's fix that 131 | - Navigate to the app Resources, copy the line starting with: web cd $HOME/....... 132 | - Back in Visual Studio (or Explorer), at the root of the repository create a Procfile text file (without extension) 133 | - Paste inside the line you copied, and replace the initial "web" with "worker:" (don't forget the colon) 134 | - Commit and push the Git changes to Heroku. Wait until the deployment is complete. 135 | - Verify that the Resources "web" line has changed to "worker" and is enabled (use the pencil icon if necessary) 136 | - Now your userbot should be running 24/7. 137 | - To prevent AUTH_KEY_DUPLICATED issues, set a SESSION_NAME env variable in your local VS project with a value like "PC" 138 | DISCLAIMER: I'm not affiliated nor expert with Heroku, so if you have any problem with the above I might not be able to help 139 | ******************************************************************************************************************************/ 140 | -------------------------------------------------------------------------------- /Examples/Program_ListenUpdates.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using TL; 4 | 5 | namespace WTelegramClientTest 6 | { 7 | static class Program_ListenUpdates 8 | { 9 | static WTelegram.Client Client; 10 | static WTelegram.UpdateManager Manager; 11 | static User My; 12 | 13 | // go to Project Properties > Debug > Launch Profiles > Environment variables and add at least these: api_id, api_hash, phone_number 14 | static async Task Main(string[] _) 15 | { 16 | Console.WriteLine("The program will display updates received for the logged-in user. Press any key to terminate"); 17 | WTelegram.Helpers.Log = (l, s) => System.Diagnostics.Debug.WriteLine(s); 18 | Client = new WTelegram.Client(Environment.GetEnvironmentVariable); 19 | await using (Client) 20 | { 21 | Manager = Client.WithUpdateManager(Client_OnUpdate/*, "Updates.state"*/); 22 | My = await Client.LoginUserIfNeeded(); 23 | // Note: on login, Telegram may sends a bunch of updates/messages that happened in the past and were not acknowledged 24 | Console.WriteLine($"We are logged-in as {My.username ?? My.first_name + " " + My.last_name} (id {My.id})"); 25 | // We collect all infos about the users/chats so that updates can be printed with their names 26 | var dialogs = await Client.Messages_GetAllDialogs(); // dialogs = groups/channels/users 27 | dialogs.CollectUsersChats(Manager.Users, Manager.Chats); 28 | 29 | Console.ReadKey(); 30 | } // WTelegram.Client gets disposed when exiting this scope 31 | 32 | //Manager.SaveState("Updates.state"); // if you want to resume missed updates on the next run (see WithUpdateManager above) 33 | } 34 | 35 | // if not using async/await, we could just return Task.CompletedTask 36 | private static async Task Client_OnUpdate(Update update) 37 | { 38 | switch (update) 39 | { 40 | case UpdateNewMessage unm: await HandleMessage(unm.message); break; 41 | case UpdateEditMessage uem: await HandleMessage(uem.message, true); break; 42 | // Note: UpdateNewChannelMessage and UpdateEditChannelMessage are also handled by above cases 43 | case UpdateDeleteChannelMessages udcm: Console.WriteLine($"{udcm.messages.Length} message(s) deleted in {Chat(udcm.channel_id)}"); break; 44 | case UpdateDeleteMessages udm: Console.WriteLine($"{udm.messages.Length} message(s) deleted"); break; 45 | case UpdateUserTyping uut: Console.WriteLine($"{User(uut.user_id)} is {uut.action}"); break; 46 | case UpdateChatUserTyping ucut: Console.WriteLine($"{Peer(ucut.from_id)} is {ucut.action} in {Chat(ucut.chat_id)}"); break; 47 | case UpdateChannelUserTyping ucut2: Console.WriteLine($"{Peer(ucut2.from_id)} is {ucut2.action} in {Chat(ucut2.channel_id)}"); break; 48 | case UpdateChatParticipants { participants: ChatParticipants cp }: Console.WriteLine($"{cp.participants.Length} participants in {Chat(cp.chat_id)}"); break; 49 | case UpdateUserStatus uus: Console.WriteLine($"{User(uus.user_id)} is now {uus.status.GetType().Name[10..]}"); break; 50 | case UpdateUserName uun: Console.WriteLine($"{User(uun.user_id)} has changed profile name: {uun.first_name} {uun.last_name}"); break; 51 | case UpdateUser uu: Console.WriteLine($"{User(uu.user_id)} has changed infos/photo"); break; 52 | default: Console.WriteLine(update.GetType().Name); break; // there are much more update types than the above example cases 53 | } 54 | } 55 | 56 | // in this example method, we're not using async/await, so we just return Task.CompletedTask 57 | private static Task HandleMessage(MessageBase messageBase, bool edit = false) 58 | { 59 | if (edit) Console.Write("(Edit): "); 60 | switch (messageBase) 61 | { 62 | case Message m: Console.WriteLine($"{Peer(m.from_id) ?? m.post_author} in {Peer(m.peer_id)}> {m.message}"); break; 63 | case MessageService ms: Console.WriteLine($"{Peer(ms.from_id)} in {Peer(ms.peer_id)} [{ms.action.GetType().Name[13..]}]"); break; 64 | } 65 | return Task.CompletedTask; 66 | } 67 | 68 | private static string User(long id) => Manager.Users.TryGetValue(id, out var user) ? user.ToString() : $"User {id}"; 69 | private static string Chat(long id) => Manager.Chats.TryGetValue(id, out var chat) ? chat.ToString() : $"Chat {id}"; 70 | private static string Peer(Peer peer) => Manager.UserOrChat(peer)?.ToString() ?? $"Peer {peer?.ID}"; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Examples/Program_ReactorError.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using TL; 4 | 5 | namespace WTelegramClientTest 6 | { 7 | static class Program_ReactorError 8 | { 9 | static WTelegram.Client Client; 10 | 11 | // go to Project Properties > Debug > Launch Profiles > Environment variables and add at least these: api_id, api_hash, phone_number 12 | static async Task Main(string[] _) 13 | { 14 | Console.WriteLine("The program demonstrate how to handle ReactorError. Press any key to terminate"); 15 | WTelegram.Helpers.Log = (l, s) => System.Diagnostics.Debug.WriteLine(s); 16 | try 17 | { 18 | await CreateAndConnect(); 19 | Console.ReadKey(); 20 | } 21 | finally 22 | { 23 | if (Client != null) await Client.DisposeAsync(); 24 | } 25 | } 26 | 27 | private static async Task CreateAndConnect() 28 | { 29 | Client = new WTelegram.Client(Environment.GetEnvironmentVariable); 30 | Client.OnUpdates += Client_OnUpdates; 31 | Client.OnOther += Client_OnOther; 32 | var my = await Client.LoginUserIfNeeded(); 33 | Console.WriteLine($"We are logged-in as " + my); 34 | } 35 | 36 | private static async Task Client_OnOther(IObject arg) 37 | { 38 | if (arg is ReactorError err) 39 | { 40 | // typically: network connection was totally lost 41 | Console.WriteLine($"Fatal reactor error: {err.Exception.Message}"); 42 | while (true) 43 | { 44 | Console.WriteLine("Disposing the client and trying to reconnect in 5 seconds..."); 45 | if (Client != null) await Client.DisposeAsync(); 46 | Client = null; 47 | await Task.Delay(5000); 48 | try 49 | { 50 | await CreateAndConnect(); 51 | break; 52 | } 53 | catch (Exception ex) when (ex is not ObjectDisposedException) 54 | { 55 | Console.WriteLine("Connection still failing: " + ex.Message); 56 | } 57 | } 58 | } 59 | else 60 | Console.WriteLine("Other: " + arg.GetType().Name); 61 | } 62 | 63 | private static Task Client_OnUpdates(UpdatesBase updates) 64 | { 65 | foreach (var update in updates.UpdateList) 66 | Console.WriteLine(update.GetType().Name); 67 | return Task.CompletedTask; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Examples/Program_SecretChats.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using TL; 7 | using WTelegram; 8 | 9 | namespace WTelegramClientTest 10 | { 11 | static class Program_SecretChats 12 | { 13 | static Client Client; 14 | static SecretChats Secrets; 15 | static ISecretChat ActiveChat; // the secret chat currently selected 16 | static readonly Dictionary Users = []; 17 | static readonly Dictionary Chats = []; 18 | 19 | // go to Project Properties > Debug > Launch Profiles > Environment variables and add at least these: api_id, api_hash, phone_number 20 | static async Task Main() 21 | { 22 | Helpers.Log = (l, s) => System.Diagnostics.Debug.WriteLine(s); 23 | Client = new Client(Environment.GetEnvironmentVariable); 24 | Secrets = new SecretChats(Client, "Secrets.bin"); 25 | AppDomain.CurrentDomain.ProcessExit += (s, e) => { Secrets.Dispose(); Client.Dispose(); }; 26 | SelectActiveChat(); 27 | 28 | Client.OnUpdates += Client_OnUpdates; 29 | var myself = await Client.LoginUserIfNeeded(); 30 | Users[myself.id] = myself; 31 | Console.WriteLine($"We are logged-in as {myself}"); 32 | 33 | var dialogs = await Client.Messages_GetAllDialogs(); // load the list of users/chats 34 | dialogs.CollectUsersChats(Users, Chats); 35 | Console.WriteLine(@"Available commands: 36 | /request Initiate Secret Chat with user (see /users) 37 | /discard [delete] Terminate active secret chat [and delete history] 38 | /select Select another Secret Chat 39 | /photo filename.jpg Send a JPEG photo 40 | /read Mark active discussion as read 41 | /users List collected users and their IDs 42 | Type a command, or a message to send to the active secret chat:"); 43 | do 44 | { 45 | try 46 | { 47 | var line = Console.ReadLine(); 48 | if (line.StartsWith('/')) 49 | { 50 | if (line == "/discard delete") { await Secrets.Discard(ActiveChat.ChatId, true); SelectActiveChat(); } 51 | else if (line == "/discard") { await Secrets.Discard(ActiveChat.ChatId, false); SelectActiveChat(); } 52 | else if (line == "/read") await Client.Messages_ReadEncryptedHistory(ActiveChat.Peer, DateTime.UtcNow); 53 | else if (line == "/users") foreach (var user in Users.Values) Console.WriteLine($"{user.id,-10} {user}"); 54 | else if (line.StartsWith("/select ")) SelectActiveChat(int.Parse(line[8..])); 55 | else if (line.StartsWith("/request ")) 56 | if (Users.TryGetValue(long.Parse(line[9..]), out var user)) 57 | SelectActiveChat(await Secrets.Request(user)); 58 | else 59 | Console.WriteLine("User not found"); 60 | else if (line.StartsWith("/photo ")) 61 | { 62 | var media = new TL.Layer46.DecryptedMessageMediaPhoto { caption = line[7..] }; 63 | var file = await Secrets.UploadFile(File.OpenRead(line[7..]), media); 64 | var sent = await Secrets.SendMessage(ActiveChat.ChatId, new TL.Layer73.DecryptedMessage { random_id = Helpers.RandomLong(), 65 | media = media, flags = TL.Layer73.DecryptedMessage.Flags.has_media }, file: file); 66 | } 67 | else Console.WriteLine("Unrecognized command"); 68 | } 69 | else if (ActiveChat == null) Console.WriteLine("No active secret chat"); 70 | else await Secrets.SendMessage(ActiveChat.ChatId, new TL.Layer73.DecryptedMessage { message = line, random_id = Helpers.RandomLong() }); 71 | } 72 | catch (Exception ex) 73 | { 74 | Console.WriteLine(ex); 75 | } 76 | } while (true); 77 | } 78 | 79 | private static async Task Client_OnUpdates(UpdatesBase updates) 80 | { 81 | updates.CollectUsersChats(Users, Chats); 82 | foreach (var update in updates.UpdateList) 83 | switch (update) 84 | { 85 | case UpdateEncryption ue: // Change in Secret Chat status 86 | await Secrets.HandleUpdate(ue); 87 | break; 88 | case UpdateNewEncryptedMessage unem: // Encrypted message or service message: 89 | if (unem.message.ChatId != ActiveChat?.ChatId) SelectActiveChat(unem.message.ChatId); 90 | foreach (var msg in Secrets.DecryptMessage(unem.message)) 91 | { 92 | if (msg.Media != null && unem.message is EncryptedMessage { file: EncryptedFile ef }) 93 | { 94 | int slash = msg.Media.MimeType?.IndexOf('/') ?? 0; // quick & dirty conversion from MIME type to file extension 95 | var filename = $"{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.{(slash > 0 ? msg.Media.MimeType[(slash + 1)..] : "bin")}"; 96 | Console.WriteLine($"{unem.message.ChatId}> {msg.Message} [attached file downloaded to {filename}]"); 97 | using var output = File.Create(filename); 98 | await Secrets.DownloadFile(ef, msg.Media, output); 99 | } 100 | else if (msg.Action == null) Console.WriteLine($"{unem.message.ChatId}> {msg.Message}"); 101 | else Console.WriteLine($"{unem.message.ChatId}> Service Message {msg.Action.GetType().Name[22..]}"); 102 | } 103 | break; 104 | case UpdateEncryptedChatTyping: 105 | case UpdateEncryptedMessagesRead: 106 | //Console.WriteLine(update.GetType().Name); 107 | break; 108 | } 109 | } 110 | 111 | private static void SelectActiveChat(int newActiveChat = 0) 112 | { 113 | ActiveChat = Secrets.Chats.FirstOrDefault(sc => newActiveChat == 0 || sc.ChatId == newActiveChat); 114 | Console.WriteLine("Active secret chat ID: " + ActiveChat?.ChatId); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Examples/WinForms_app.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiz0u/WTelegramClient/5358471574404a4103c386a9c5bc18c4fec25fcc/Examples/WinForms_app.zip -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | Before asking questions, make sure to **[read through the ReadMe first](README.md)**, 4 | take a look at the [example programs](EXAMPLES.md) or [StackOverflow questions](https://stackoverflow.com/questions/tagged/wtelegramclient), 5 | and refer to the [API method list](https://corefork.telegram.org/methods) for the full range of Telegram services available in this library. 6 | 7 | ➡️ Use Ctrl-F to search this page for the information you seek 8 | 9 | 10 | ## 1. How to remove the Console logs? 11 | 12 | Writing the library logs to the Console is the default behavior of the `WTelegram.Helpers.Log` delegate. 13 | You can change the delegate with the `+=` operator to **also** write them somewhere else, or with the `=` operator to prevent them from being printed to screen and instead write them somewhere (file, logger, ...). 14 | In any case, it is not recommended to totally ignore those logs because you wouldn't be able to diagnose a problem after it happens. 15 | 16 | Read the [example about logging settings](EXAMPLES.md#logging) for how to write logs to a file. 17 | 18 | 19 | ## 2. How to handle multiple user accounts 20 | 21 | The WTelegram.session file contains the authentication keys negociated for the current user. 22 | 23 | You could switch the current user via an `Auth_Logout` followed by a `LoginUserIfNeeded` but that would require the user to sign in with a verification_code each time. 24 | 25 | Instead, if you want to deal with multiple users from the same machine, the recommended solution is to have a different session file for each user. 26 | This can be done by having your Config callback reply with a different filename (or folder) for "**session_pathname**" for each user. 27 | This way, you can keep separate session files (each with their authentication keys) for each user. 28 | 29 | If you need to manage these user accounts in parallel, you can create multiple instances of WTelegram.Client, 30 | and give them a Config callback that will select a different session file ; 31 | for example: `new WTelegram.Client(what => Config(what, "session42"))` 32 | 33 | Also please note that the session files are encrypted with your api_hash (or session_key), so if you change it, the existing session files can't be read anymore. 34 | Your api_id/api_hash represents your application, and shouldn't change with each user account the application will manage. 35 | 36 | 37 | 38 | ## 3. How to use the library in a WinForms, WPF or ASP.NET application 39 | 40 | The library should work without a problem in such applications. 41 | The difficulty might be in your Config callback when the user must enter the verification code or password, as you can't use `Console.ReadLine` here. 42 | 43 | For GUI apps, an easy solution is to call `Interaction.InputBox("Enter verification code")` instead. 44 | This might require adding a reference *(and `using`)* to the Microsoft.VisualBasic assembly. 45 | 46 | A more complex solution requires the use of a `ManualResetEventSlim` that you will wait for in Config callback, 47 | and when the user has provided the verification_code through your app, you "set" the event to release your Config callback so it can return the code. 48 | 49 | Another solution is to use the [alternative login method](README.md#alternative-simplified-configuration--login), 50 | calling `client.Login(...)` as the user provides the requested configuration elements. 51 | You can download such full example apps [for WinForms](Examples/WinForms_app.zip) and [for ASP.NET](Examples/ASPnet_webapp.zip) 52 | 53 | 54 | ## 4. How to use IDs and access_hash? Why the error `CHANNEL_INVALID` or `USER_ID_INVALID`? 55 | 56 | ⚠️ In Telegram Client API *(contrary to Bot API)*, you **cannot** interact with channels/users/etc. with only their IDs. 57 | 58 | You also need to obtain their `access_hash` which is specific to the resource you want to access AND to the currently logged-in user. 59 | This serves as a proof that the logged-in user is entitled to access that channel/user/photo/document/... 60 | (otherwise, anybody with the ID could access it) 61 | 62 | > A small private group `Chat` don't need an access_hash and can be queried using their `chat_id` only. 63 | However most common chat groups are not `Chat` but a `Channel` supergroup (without the `broadcast` flag). See [Terminology in ReadMe](README.md#terminology). 64 | Some TL methods only applies to private `Chat`, some only applies to `Channel` and some to both. 65 | 66 | The `access_hash` must usually be provided within the `Input...` structure you pass in argument to an API method (`InputPeer`, `InputChannel`, `InputUser`, etc...). 67 | 68 | You obtain the `access_hash` through TL **description structures** like `Channel`, `User`, `Photo`, `Document` that you receive through updates 69 | or when you query them through API methods like `Messages_GetAllChats`, `Messages_GetAllDialogs`, `Contacts_ResolveUsername`, etc... 70 | 71 | You can use the [`UserOrChat` and `CollectUsersChats` methods](EXAMPLES.md#collect-users-chats) to help you in obtaining/collecting 72 | the description structures you receive via API calls or updates. 73 | 74 | Once you obtained the description structure, there are 2 methods for building your `Input...` request structure: 75 | * **Recommended:** Just pass that description structure you already have, in place of the `Input...` argument, it will work! 76 | *The implicit conversion operators on base classes like `ChatBase/UserBase` will create the `Input...` structure for you automatically.* 77 | * Alternatively, you can manually create the `Input...` structure yourself by extracting the `access_hash` from the description structure 78 | 79 | *Note: An `access_hash` obtained from a User/Channel structure with flag `min` may not be usable for most requests. See [Min constructors](https://core.telegram.org/api/min).* 80 | 81 | 82 | ## 5. I need to test a feature that has been recently developed but seems not available in my program 83 | 84 | The developmental versions of the library are now available as **pre-release** on Nuget (with `-dev` in the version number) 85 | 86 | So make sure you tick the checkbox "Include prerelease" in Nuget manager and/or navigate to the Versions list then select the highest `x.x.x-dev.x` version to install in your program. 87 | 88 | 89 | ## 6. Telegram asks me to signup (firstname, lastname) even for an existing account 90 | This happens when you connect to Telegram Test servers instead of Production servers. 91 | On these separate test servers, all created accounts and chats are periodically deleted, so you shouldn't use them under normal circumstances. 92 | 93 | You can verify this is your issue by looking at [WTelegram logs](EXAMPLES.md#logging) on the line `Connected to (Test) DC x...` 94 | 95 | This wrong-server problem typically happens when you use WTelegramClient Github source project in your application in DEBUG builds. 96 | It is **not recommended** to use WTelegramClient in source code form. 97 | Instead, you should use the Nuget manager to **install package WTelegramClient** into your application. 98 | *And remember to delete the WTelegram.session file to force a new login on the correct server.* 99 | 100 | If you use the Github source project in an old .NET Framework 4.x or .NET Core x.x application, you may also experience the following error 101 | > System.TypeInitializationException (FileNotFoundException for "System.Text.Json Version=5.0.0.0 ...") 102 | 103 | To fix this, you should also switch to using the [WTelegramClient Nuget package](https://www.nuget.org/packages/WTelegramClient) as it will install the required dependencies for it to work. 104 | 105 | 106 | ## 7. I get errors FLOOD_WAIT_X or PEER_FLOOD, PHONE_NUMBER_BANNED, USER_DEACTIVATED_BAN. I can't import phone numbers. 107 | 108 | You can get these kind of problems if you abuse Telegram [Terms of Service](https://telegram.org/tos), or the [API Terms of Service](https://core.telegram.org/api/terms), or make excessive requests. 109 | 110 | You can try to wait more between the requests, wait for a day or two to see if the requests become possible again. 111 | 112 | >ℹ️ For FLOOD_WAIT_X with X < 60 seconds (see `client.FloodRetryThreshold`), WTelegramClient will automatically wait the specified delay and retry the request for you. 113 | For longer delays, you can catch the thrown `RpcException` and check the value of property X. 114 | 115 | An account that was restricted due to reported spam might receive PEER_FLOOD errors. Read [Telegram Spam FAQ](https://telegram.org/faq_spam) to learn more. 116 | 117 | If you think your phone number was banned from Telegram for a wrong reason, you may try to contact [recover@telegram.org](mailto:recover@telegram.org), explaining what you were doing. 118 | 119 | In any case, WTelegramClient is not responsible for the bad usage of the library and we are not affiliated to Telegram teams, so there is nothing we can do. 120 | 121 | 122 | ## 8. How to NOT get banned from Telegram? 123 | 124 | **Do not share publicly your app's ID and hash!** They cannot be regenerated and are bound to your Telegram account. 125 | 126 | From the [official documentation](https://core.telegram.org/api/obtaining_api_id): 127 | 128 | > Note that all API client libraries are strictly monitored to prevent abuse. 129 | > If you use the Telegram API for flooding, spamming, faking subscriber and view counters of channels, you **will be banned forever**. 130 | > Due to excessive abuse of the Telegram API, **all accounts that sign up or log in using unofficial Telegram clients are automatically 131 | > put under observation** to avoid violations of the [Terms of Service](https://core.telegram.org/api/terms). 132 | 133 | Here are some advices from [another similar library](https://github.com/gotd/td/blob/main/.github/SUPPORT.md#how-to-not-get-banned): 134 | 135 | 1. This client is unofficial, Telegram treats such clients suspiciously, especially fresh ones. 136 | 2. Use regular bots instead of userbots whenever possible. 137 | 3. If you still want to automate things with a user, use it passively (i.e. receive more than sending). 138 | 4. When using it with a user: 139 | * Do not use QR code login, this will result in permaban. 140 | * Do it with extreme care. 141 | * Do not use VoIP numbers. 142 | * Do not abuse, spam or use it for other suspicious activities. 143 | * Implement a rate limiting system. 144 | 145 | Some additional advices from me: 146 | 147 | 5. Avoid repetitive polling or repetitive sequence of actions/requests: Save the initial results of your queries, and update those results when you're informed of a change through `OnUpdates` events. 148 | 6. Don't buy fake user accounts/sessions and don't extract api_id/hash/authkey/sessions from official clients, this is [specifically forbidden by API TOS](https://core.telegram.org/api/terms#2-transparency). You must use your own api_id and create your own sessions associated with it. 149 | 7. If a phone number is brand new, it will be closely monitored by Telegram for abuse, and it can even already be considered a bad user due to bad behavior from the previous owner of that phone number (which may happen often with VoIP or other easy-to-buy-online numbers, so expect fast ban) 150 | 8. You may want to use your new phone number account with an official Telegram client and act like a normal user for some time (some weeks/months), before using it for automation with WTelegramClient. 151 | 9. When creating a new API ID/Hash, I recommend you use your own phone number with long history of normal Telegram usage, rather than a brand new phone number with short history. 152 | In particular, DON'T create an API ID/Hash for every phone numbers you will control. One API ID/Hash represents your application, which can be used to control several user accounts. 153 | 10. If you actually do use the library to spam, scam, or other stuff annoying to everybody, GTFO and don't cry that you got banned using WTelegramClient. Some people don't seem to realize by themselves that what they plan to do with the library is actually negative for the community and are surprised that they got caught. 154 | We don't support such use of the library, and will not help people asking for support if we suspect them of mass-user manipulation. 155 | 11. If your client displays Telegram channels to your users, you have to support and display [official sponsored messages](https://core.telegram.org/api/sponsored-messages). 156 | 157 | 158 | ## 9. Why the error `CHAT_ID_INVALID`? 159 | 160 | Most chat groups you see are likely of type `Channel`, not `Chat`. 161 | This difference is important to understand. Please [read about the Terminology in ReadMe](README.md#terminology). 162 | 163 | You typically get the error `CHAT_ID_INVALID` when you try to call API methods designed specifically for a `Chat`, with the ID of a `Channel`. 164 | All API methods taking a `long chat_id` as a direct method parameter are for Chats and cannot be used with Channels. 165 | 166 | There is probably another method achieving the same result but specifically designed for Channels, and it will have a similar name, but beginning with `Channels_` ... 167 | 168 | However, note that those Channel-compatible methods will require an `InputChannel` or `InputPeerChannel` object as argument instead of a simple channel ID. 169 | That object must be created with both fields `channel_id` and `access_hash` correctly filled. You can read more about this in [FAQ #4](#access-hash). 170 | 171 | 172 | 173 | ## 10. `chats.chats[id]` fails. My chats list is empty or does not contain the chat I'm looking for. 174 | 175 | There can be several reasons why `chats.chats` doesn't contain the chat you expect: 176 | - You're searching for a user instead of a chat ID. 177 | Private messages with a user are not called "chats". See [Terminology in ReadMe](README.md#terminology). 178 | To obtain the list of users (as well as chats and channels) the logged-in user is currenly engaged in a discussion with, you should [use the API method `Messages_GetAllDialogs`](EXAMPLES.md#list-dialogs) 179 | - The currently logged-in user account has not joined this particular chat. 180 | API method [`Messages_GetAllChats`](https://corefork.telegram.org/method/messages.getAllChats) will only return those chat groups/channels the user is in, not all Telegram chat groups. 181 | If you're looking for other Telegram groups/channels/users, try API methods [`Contacts_ResolveUsername`](EXAMPLES.md#msg-by-name) or `Contacts_Search` 182 | - You're trying to use a Bot API (or TDLib) numerical ID, like -1001234567890 183 | Telegram Client API don't use these kind of IDs for chats. Remove the -100 prefix and try again with the rest (1234567890). 184 | - the `chats.chats` dictionary is empty. 185 | This is the case if you are logged-in as a brand new user account (that hasn't join any chat groups/channels) 186 | or if you are connected to a Test DC (a Telegram datacenter server for tests) instead of Production DC 187 | ([read FAQ #6](#wrong-server) for more) 188 | 189 | To help determine if `chats.chats` is empty or does not contain a certain chat, you should [dump the chat list to the screen](EXAMPLES.md#list-chats) 190 | or simply use a debugger: Place a breakpoint after the `Messages_GetAllChats` call, run the program up to there, and use a Watch pane to display the content of the chats.chats dictionary. 191 | 192 | 193 | ## 11. I get "Connection shut down" errors in my logs 194 | 195 | There are various reasons why you may get this error. Here are the explanation and how to solve it: 196 | 197 | 1) On secondary DCs *(DC used to download files)*, a Connection shut down is considered "normal" 198 | Your main DC is the one WTelegramClient connects to during login. Secondary DC connections are established and maintained when you download files. 199 | The DC number for an operation or error is indicated with a prefix like "2>" on the log line. 200 | If Telegram servers decide to shutdown this secondary connection, it's not an issue, WTelegramClient will re-establish the connection later if necessary. 201 | 202 | 2) Occasional connection shutdowns on the main DC should be caught by WTelegramClient and the reactor should automatically reconnect to the DC 203 | *(up to `MaxAutoReconnects` times)*. 204 | This should be transparent and pending API calls should automatically be resent upon reconnection. 205 | You can choose to increase `MaxAutoReconnects` if it happens too often because your Internet connection is unstable. 206 | 207 | 3) If you reach `MaxAutoReconnects` disconnections or a reconnection fails, then the **OnOther** event handler will receive a `ReactorError` object to notify you of the problem, 208 | and pending API calls throw the network IOException. 209 | In this case, the recommended action would be to dispose the client and recreate one (see example [Program_ReactorError.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ReactorError.cs)) 210 | 211 | 4) In case of slow Internet connection or if you break in the debugger for some time, 212 | you might also get Connection shutdown because your client couldn't send Pings to Telegram in the allotted time. 213 | In this case, you can use the `PingInterval` property to increase the delay between pings *(for example 300 seconds instead of 60)*. 214 | 215 | 5) If you're using an [MTProxy](EXAMPLES.md#proxy), some of them are known to be quite unstable. You may want to try switching to another MTProxy that is more stable. 216 | 217 | 218 | ## 12. How to migrate from TLSharp? How to sign-in/sign-up/register account properly? 219 | 220 | First, make sure you read the [ReadMe documentation](README.md) completely, it contains essential information and a quick tutorial to easily understand how to correctly use the library. 221 | 222 | WTelegramClient approach is much more simpler and secure than TLSharp. 223 | 224 | All client APIs have dedicated async methods that you can call like this: `await client.Method_Name(param1, param2, ...)` 225 | See the [full method list](https://core.telegram.org/methods) (just replace the dot with an underscore in the names) 226 | 227 | A session file is created or resumed automatically on startup, and maintained up-to-date automatically throughout the session. 228 | That session file is incompatible with TLSharp so you cannot reuse a TLSharp .dat file. You'll need to create a new session. 229 | To fight against the reselling of fake user accounts, we don't support the import/export of session files from external sources. 230 | 231 | **DON'T** call methods Auth_SendCode/SignIn/SignUp/... because all the login phase is handled automatically by calling `await client.LoginUserIfNeeded()` after creating the client. 232 | Your Config callback just need to provide the various login answers if they are needed (see [ReadMe](README.md) and [FAQ #4](#GUI)). 233 | In particular, it will detect and handle automatically and properly the various login cases/particularity like: 234 | * Login not necessary (when a session is resumed with an already logged-in user) 235 | * Logout required (if you want to change the logged-in user) 236 | * 2FA password required (your Config needs to provide "password") 237 | * Email registration procedure required (your Config needs to provide "email", "email_verification_code") 238 | * Account registration/sign-up required (your Config needs to provide "first_name", "last_name") 239 | * Request to resend the verification code through alternate ways (if your Config answer an empty "verification_code" initially) 240 | * Transient failures, slowness to respond, wrong code/password, checks for encryption key safety, etc.. 241 | 242 | Contrary to TLSharp, WTelegramClient supports MTProto v2.0 (more secured), transport obfuscation, protocol security checks, MTProto [Proxy](EXAMPLES.md#proxy), real-time updates, multiple DC connections, API documentation in Intellisense... 243 | 244 | 245 | ## 13. How to host my userbot online? 246 | 247 | If you need your userbot to run 24/7, you would typically design your userbot as a Console program, compiled for Linux or Windows, 248 | and hosted online on any [VPS Hosting](https://www.google.com/search?q=vps+hosting) (Virtual Private Server). 249 | Pure WebApp hosts might not be adequate as they will recycle (stop) your app if there is no incoming HTTP requests. 250 | 251 | There are many cheap VPS Hosting offers available, for example Heroku: 252 | See [Examples/Program_Heroku.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_Heroku.cs?ts=4#L9) for such an implementation and the steps to host/deploy it. 253 | 254 | 255 | ## 14. Secret Chats implementation details 256 | 257 | The following choices were made while implementing Secret Chats in WTelegramClient: 258 | - It may not support remote antique Telegram clients *(prior to 2018, still using insecure MTProto 1.0)* 259 | - It doesn't store outgoing messages *(so if remote client was offline for a week and ask us to resend old messages, we will send void data)* 260 | - It doesn't store incoming messages on disk *(it's up to you if you want to store them)* 261 | - If you pass `DecryptMessage` parameter `fillGaps: true` *(default)*, incoming messages are ensured to be delivered to you in correct order. 262 | If for some reason, we received them in incorrect order, messages are kept in memory until the requested missing messages are obtained. 263 | If those missing messages are never obtained during the session, incoming messages might get stuck and lost. 264 | - SecretChats file data is only valid for the current user, so make sure to pick the right file *(or a new file name)* if you change logged-in user. 265 | - If you want to accept incoming Secret Chats request only from specific user, you must check it in OnUpdates before: 266 | `await Secrets.HandleUpdate(ue, ue.chat is EncryptedChatRequested ecr && ecr.admin_id == EXPECTED_USER_ID);` 267 | - As recommended, new encryption keys are negotiated every 100 sent/received messages or after one week. 268 | If remote client doesn't complete this negotiation before reaching 200 messages, the Secret Chat is aborted. 269 | 270 | 271 | ## 15. The example codes don't compile on my machine 272 | 273 | The snippets of example codes found in the [ReadMe](README.md) or [Examples](EXAMPLES.md) pages were written for .NET 5 / C# 9 minimum. 274 | If you're having compiler problem on code constructs such as `using`, `foreach`, `[^1]` or about "Deconstruct", 275 | that typically means you're still using an obsolete version of .NET (Framework 4.x or Core) 276 | 277 | Here are the recommended actions to fix your problem: 278 | - Create a new project for .NET 6+ (in Visual Studio 2019 or more recent): 279 | - Select File > New > Project 280 | - Search for "C# Console" 281 | - Select the **Console App**, but NOT Console App (.NET Framework) ! 282 | - On the framework selection page, choose .NET 6.0 or more recent 283 | - Now you can start developing for WTelegramClient 🙂 284 | - If you don't want to target a recent version of .NET, you can upgrade your existing project to use the latest version of the C# language: 285 | - Close Visual Studio 286 | - Edit your *.csproj file **with Notepad** 287 | - Within the first ``, add the following line: 288 | `latest` 289 | - Save, close Notepad and reopen your project in Visual Studio 290 | - If you still have issues on some `foreach` constructs, add this class somewhere in your project: 291 | ```csharp 292 | static class Extensions 293 | { 294 | public static void Deconstruct(this KeyValuePair tuple, out T1 key, out T2 value) 295 | { 296 | key = tuple.Key; 297 | value = tuple.Value; 298 | } 299 | } 300 | ``` 301 | 302 | Also, remember to add a `using TL;` at the top of your files to have access to all the Telegram API methods. 303 | 304 | 305 | # Troubleshooting guide 306 | 307 | Here is a list of common issues and how to fix them so that your program work correctly: 308 | 309 | 1) Are you using the Nuget package or the library source code? 310 | It is not recommended to copy/compile the source code of the library for a normal usage. 311 | When built in DEBUG mode, the source code connects to Telegram test servers (see also [FAQ #6](#wrong-server)). 312 | So you can either: 313 | - **Recommended:** Use the [official Nuget package](https://www.nuget.org/packages/WTelegramClient) 314 | - Build your code in RELEASE mode 315 | - Modify your config callback to reply to "server_address" with the IP address of Telegram production servers (as found on your API development tools) 316 | 317 | 2) Did you call `Login` or `LoginUserIfNeeded` succesfully? 318 | If you don't complete authentication as a user (or bot), you have access to a very limited subset of Telegram APIs. 319 | Make sure your calls succeed and don't throw an exception. 320 | 321 | 3) Did you use `await` with every Client methods? 322 | This library is completely Task-based. You should learn, understand and use the [asynchronous model of C# programming](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/) before proceeding further. 323 | Using `.Result` or `.Wait()` can lead to deadlocks. 324 | 325 | 4) Is your program ending immediately instead of waiting for Updates? 326 | Your program must be running/waiting continuously in order for the background Task to receive and process the Updates. 327 | So make sure your main program doesn't end immediately or dispose the client too soon (via `using`?). 328 | For a console program, this is typical done by waiting for a key or some close event. 329 | 330 | 5) Is every Telegram API call rejected? (typically with an exception message like `AUTH_RESTART`) 331 | The user authentification might have failed at some point (or the user revoked the authorization). 332 | It is therefore necessary to go through the authentification again. This can be done by deleting the WTelegram.session file, or at runtime by calling `client.Reset()` 333 | 334 | 335 | # About the UpdateManager 336 | 337 | The UpdateManager does the following: 338 | - ensure the correct sequential order of receiving updates (Telegram may send them in wrong order) 339 | - fetch the missing updates if there was a gap (missing update) in the flow of incoming updates 340 | - resume the flow of updates where you left off if you stopped your program (with saved state) 341 | - collect the users & chats from updates automatically for you _(by default)_ 342 | - simplifies the handling of the various containers of update (UpdatesBase) 343 | 344 | To use the UpdateManager, instead of setting `client.OnUpdates`, you call: 345 | ```csharp 346 | // if you don't care about missed updates while your program was down: 347 | var manager = client.WithUpdateManager(OnUpdate); 348 | 349 | // if you want to recover missed updates using the state saved on the last run of your program 350 | var manager = client.WithUpdateManager(OnUpdate, "Updates.state"); 351 | // to save the state later, preferably after disposing the client: 352 | manager.SaveState("Updates.state") 353 | ``` 354 | 355 | Your `OnUpdate` method will directly take a single `Update` as parameter, instead of a container of updates. 356 | The `manager.Users` and `manager.Chats` dictionaries will collect the users/chats data from updates. 357 | You can also feed them manually from result of your API calls by calling `result.CollectUsersChats(manager.Users, manager.Chats);` and resolve Peer fields via `manager.UserOrChat(peer)` 358 | See [Examples/Program_ListenUpdates.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ListenUpdates.cs?ts=4#L21) for an example of implementation. 359 | 360 | Notes: 361 | - set `manager.Log` if you want different logger settings than the client 362 | - `WithUpdateManager()` has other parameters for advanced use -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2024 Olivier Marcoux 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![API Layer](https://img.shields.io/badge/API_Layer-203-blueviolet)](https://corefork.telegram.org/methods) 2 | [![NuGet version](https://img.shields.io/nuget/v/WTelegramClient?color=00508F)](https://www.nuget.org/packages/WTelegramClient/) 3 | [![NuGet prerelease](https://img.shields.io/nuget/vpre/WTelegramClient?color=C09030&label=dev+nuget)](https://www.nuget.org/packages/WTelegramClient/absoluteLatest) 4 | [![Donate](https://img.shields.io/badge/Help_this_project:-Donate-ff4444)](https://buymeacoffee.com/wizou) 5 | 6 | ## *_Telegram Client API library written 100% in C# and .NET_* 7 | 8 | This library allows you to connect to Telegram and control a user programmatically (or a bot, but [WTelegramBot](https://www.nuget.org/packages/WTelegramBot) is much easier for that). 9 | All the Telegram Client APIs (MTProto) are supported so you can do everything the user could do with a full Telegram GUI client. 10 | 11 | Library was developed solely by one unemployed guy. [Donations are welcome](https://buymeacoffee.com/wizou). 12 | 13 | This ReadMe is a **quick but important tutorial** to learn the fundamentals about this library. Please read it all. 14 | 15 | > ⚠️ This library requires understanding advanced C# techniques such as **asynchronous programming** or **subclass pattern matching**... 16 | > If you are a beginner in C#, starting a project based on this library might not be a great idea. 17 | 18 | # How to use 19 | 20 | After installing WTelegramClient through [Nuget](https://www.nuget.org/packages/WTelegramClient/), your first Console program will be as simple as: 21 | ```csharp 22 | static async Task Main(string[] _) 23 | { 24 | using var client = new WTelegram.Client(); 25 | var myself = await client.LoginUserIfNeeded(); 26 | Console.WriteLine($"We are logged-in as {myself} (id {myself.id})"); 27 | } 28 | ``` 29 | When run, this will prompt you interactively for your App **api_hash** and **api_id** (that you obtain through Telegram's 30 | [API development tools](https://my.telegram.org/apps) page) and try to connect to Telegram servers. 31 | Those api hash/id represent your application and one can be used for handling many user accounts. 32 | 33 | Then it will attempt to sign-in *(login)* as a user for which you must enter the **phone_number** and the **verification_code** 34 | that will be sent to this user (for example through SMS, Email, or another Telegram client app the user is connected to). 35 | 36 | If the verification succeeds but the phone number is unknown to Telegram, the user might be prompted to sign-up 37 | *(register their account by accepting the Terms of Service)* and provide their **first_name** and **last_name**. 38 | If the account already exists and has enabled two-step verification (2FA) a **password** might be required. 39 | In some case, Telegram may request that you associate an **email** with your account for receiving login verification codes, 40 | you may skip this step by leaving **email** empty, otherwise the email address will first receive an **email_verification_code**. 41 | All these login scenarios are handled automatically within the call to `LoginUserIfNeeded`. 42 | 43 | After login, you now have access to the **[full range of Telegram Client APIs](https://corefork.telegram.org/methods)**. 44 | All those API methods require `using TL;` namespace and are called with an underscore instead of a dot in the method name, like this: `await client.Method_Name(...)` 45 | 46 | # Saved session 47 | If you run this program again, you will notice that only **api_hash** is requested, the other prompts are gone and you are automatically logged-on and ready to go. 48 | 49 | This is because WTelegramClient saves (typically in the encrypted file **bin\WTelegram.session**) its state 50 | and the authentication keys that were negotiated with Telegram so that you needn't sign-in again every time. 51 | 52 | That file path is configurable (**session_pathname**), and under various circumstances *(changing user or server address, write permissions)* 53 | you may want to change it or simply delete the existing session file in order to restart the authentification process. 54 | 55 | # Non-interactive configuration 56 | Your next step will probably be to provide a configuration to the client so that the required elements are not prompted through the Console but answered by your program. 57 | 58 | To do this, you need to write a method that will provide the answers, and pass it on the constructor: 59 | ```csharp 60 | static string Config(string what) 61 | { 62 | switch (what) 63 | { 64 | case "api_id": return "YOUR_API_ID"; 65 | case "api_hash": return "YOUR_API_HASH"; 66 | case "phone_number": return "+12025550156"; 67 | case "verification_code": Console.Write("Code: "); return Console.ReadLine(); 68 | case "first_name": return "John"; // if sign-up is required 69 | case "last_name": return "Doe"; // if sign-up is required 70 | case "password": return "secret!"; // if user has enabled 2FA 71 | default: return null; // let WTelegramClient decide the default config 72 | } 73 | } 74 | ... 75 | using var client = new WTelegram.Client(Config); 76 | ``` 77 | There are other configuration items that are queried to your method but returning `null` let WTelegramClient choose a default adequate value. 78 | Those shown above are the only ones that have no default values and should be provided by your method. 79 | 80 | Returning `null` for verification_code or password will show a prompt for console apps, or an error otherwise 81 | *(see [FAQ #3](https://wiz0u.github.io/WTelegramClient/FAQ#GUI) for WinForms)* 82 | Returning `""` for verification_code requests the resending of the code through another system (SMS or Call). 83 | 84 | Another simple approach is to pass `Environment.GetEnvironmentVariable` as the config callback and define the configuration items as environment variables 85 | *(undefined variables get the default `null` behavior)*. 86 | 87 | Finally, if you want to redirect the library logs to your logger instead of the Console, you can install a delegate in the `WTelegram.Helpers.Log` static property. 88 | Its `int` argument is the log severity, compatible with the [LogLevel enum](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.loglevel). 89 | 90 | # Alternative simplified configuration & login 91 | Since version 3.0.0, a new approach to login/configuration has been added. Some people might find it easier to deal with: 92 | 93 | ```csharp 94 | WTelegram.Client client = new WTelegram.Client(YOUR_API_ID, "YOUR_API_HASH"); // this constructor doesn't need a Config method 95 | await DoLogin("+12025550156"); // initial call with user's phone_number 96 | ... 97 | //client.Dispose(); // the client must be disposed when you're done running your userbot. 98 | 99 | async Task DoLogin(string loginInfo) // (add this method to your code) 100 | { 101 | while (client.User == null) 102 | switch (await client.Login(loginInfo)) // returns which config is needed to continue login 103 | { 104 | case "verification_code": Console.Write("Code: "); loginInfo = Console.ReadLine(); break; 105 | case "name": loginInfo = "John Doe"; break; // if sign-up is required (first/last_name) 106 | case "password": loginInfo = "secret!"; break; // if user has enabled 2FA 107 | default: loginInfo = null; break; 108 | } 109 | Console.WriteLine($"We are logged-in as {client.User} (id {client.User.id})"); 110 | } 111 | ``` 112 | 113 | With this method, you can choose in some cases to interrupt the login loop via a `return` instead of `break`, and resume it later 114 | by calling `DoLogin(requestedCode)` again once you've obtained the requested code/password/etc... 115 | See [WinForms example](https://wiz0u.github.io/WTelegramClient/Examples/WinForms_app.zip) and [ASP.NET example](https://wiz0u.github.io/WTelegramClient/Examples/ASPnet_webapp.zip) 116 | 117 | # Example of API call 118 | 119 | > The Telegram API makes extensive usage of base and derived classes, so be ready to use the various C# syntaxes 120 | to check/cast base classes into the more useful derived classes (`is`, `as`, `case DerivedType` ) 121 | 122 | All the Telegram API classes/methods are fully documented through Intellisense: Place your mouse over a class/method name, 123 | or start typing the call arguments to see a tooltip displaying their description, the list of derived classes and a web link to the official API page. 124 | 125 | The Telegram [API object classes](https://corefork.telegram.org/schema) are defined in the `TL` namespace, 126 | and the [API functions](https://corefork.telegram.org/methods) are available as async methods of `Client`. 127 | 128 | Below is an example of calling the [messages.getAllChats](https://corefork.telegram.org/method/messages.getAllChats) API function, 129 | enumerating the various groups/channels the user is in, and then using `client.SendMessageAsync` helper function to easily send a message: 130 | ```csharp 131 | using TL; 132 | ... 133 | var chats = await client.Messages_GetAllChats(); 134 | Console.WriteLine("This user has joined the following:"); 135 | foreach (var (id, chat) in chats.chats) 136 | if (chat.IsActive) 137 | Console.WriteLine($"{id,10}: {chat}"); 138 | Console.Write("Type a chat ID to send a message: "); 139 | long chatId = long.Parse(Console.ReadLine()); 140 | var target = chats.chats[chatId]; 141 | Console.WriteLine($"Sending a message in chat {chatId}: {target.Title}"); 142 | await client.SendMessageAsync(target, "Hello, World"); 143 | ``` 144 | 145 | ➡️ You can find lots of useful code snippets in [EXAMPLES](https://wiz0u.github.io/WTelegramClient/EXAMPLES) 146 | and more detailed programs in the [Examples subdirectory](https://github.com/wiz0u/WTelegramClient/tree/master/Examples). 147 | ➡️ Check [the FAQ](https://wiz0u.github.io/WTelegramClient/FAQ#compile) if example codes don't compile correctly on your machine, or other troubleshooting. 148 | 149 | 150 | # Terminology in Telegram Client API 151 | 152 | In the API, Telegram uses some terms/classnames that can be confusing as they differ from the terms shown to end-users: 153 | - `Channel`: A (large or public) chat group *(sometimes called [supergroup](https://corefork.telegram.org/api/channel#supergroups))*, 154 | or a [broadcast channel](https://corefork.telegram.org/api/channel#channels) (the `broadcast` flag differentiate those) 155 | - `Chat`: A private [basic chat group](https://corefork.telegram.org/api/channel#basic-groups) with less than 200 members 156 | (it may be migrated to a supergroup `Channel` with a new ID when it gets bigger or public, in which case the old `Chat` will still exist but will be `deactivated`) 157 | **⚠️ Most chat groups you see are really of type `Channel`, not `Chat`!** 158 | - **chats**: In plural or general meaning, it means either `Chat` or `Channel` *(therefore, no private user discussions)* 159 | - `Peer`: Either a `Chat`, a `Channel` or a `User` 160 | - **Dialog**: Status of chat with a `Peer` *(draft, last message, unread count, pinned...)*. It represents each line from your Telegram chat list. 161 | - **Access Hash**: Telegram requires you to provide a specific `access_hash` for users, channels, and other resources before interacting with them. 162 | See [FAQ #4](https://wiz0u.github.io/WTelegramClient/FAQ#access-hash) to learn more about it. 163 | - **DC** (DataCenter): There are a few datacenters depending on where in the world the user (or an uploaded media file) is from. 164 | - **Session** or **Authorization**: Pairing between a device and a phone number. You can have several active sessions for the same phone number. 165 | - **Participant**: A member/subscriber of a chat group or channel 166 | 167 | # Other things to know 168 | 169 | The Client class offers `OnUpdates` and `OnOther` events that are triggered when Telegram servers sends Updates (like new messages or status) or other notifications, independently of your API requests. 170 | You can also use the [UpdateManager class](https://wiz0u.github.io/WTelegramClient/FAQ#manager) to simplify the handling of such updates. 171 | See [Examples/Program_ListenUpdates.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ListenUpdates.cs?ts=4#L21) and [Examples/Program_ReactorError.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ReactorError.cs?ts=4#L30) 172 | 173 | An invalid API request can result in a `RpcException` being raised, reflecting the [error code and status text](https://revgram.github.io/errors.html) of the problem. 174 | 175 | To [prevent getting banned](https://wiz0u.github.io/WTelegramClient/FAQ#prevent-ban) during dev, you can connect to [test servers](https://docs.pyrogram.org/topics/test-servers), by adding this line in your Config callback: 176 | `case "server_address": return "2>149.154.167.40:443"; // test DC` 177 | 178 | The other configuration items that you can provide include: **session_pathname, email, email_verification_code, session_key, device_model, system_version, app_version, system_lang_code, lang_pack, lang_code, firebase, user_id, bot_token** 179 | 180 | Optional API parameters have a default value of `null` when unset. Passing `null` for a required string/array is the same as *empty* (0-length). 181 | Required API parameters/fields can sometimes be set to 0 or `null` when unused (check API documentation or experiment). 182 | 183 | I've added several useful converters, implicit cast or helper properties to various API objects so that they are more easy to manipulate. 184 | 185 | Beyond the TL async methods, the Client class offers a few other methods to simplify the sending/receiving of files, medias or messages, 186 | as well as generic handling of chats/channels. 187 | 188 | This library works best with **.NET 5.0+** (faster, no dependencies) and is also available for **.NET Standard 2.0** (.NET Framework 4.6.1+ & .NET Core 2.0+) and **Xamarin/Mono.Android** 189 | 190 | # Library uses and limitations 191 | This library can be used for any Telegram scenario including: 192 | - Sequential or parallel automated steps based on API requests/responses 193 | - Real-time [monitoring](https://wiz0u.github.io/WTelegramClient/EXAMPLES#updates) of incoming Updates/Messages 194 | - [Download](https://wiz0u.github.io/WTelegramClient/EXAMPLES#download)/[upload](https://wiz0u.github.io/WTelegramClient/EXAMPLES#upload) of files/media 195 | - Exchange end-to-end encrypted messages/files in [Secret Chats](https://wiz0u.github.io/WTelegramClient/EXAMPLES#e2e) 196 | - Building a full-featured interactive client 197 | 198 | It has been tested in a Console app, [in Windows Forms](https://wiz0u.github.io/WTelegramClient/Examples/WinForms_app.zip), 199 | [in ASP.NET webservice](https://wiz0u.github.io/WTelegramClient/Examples/ASPnet_webapp.zip), and in Xamarin/Android. 200 | 201 | Don't use this library for Spam or Scam. Respect Telegram [Terms of Service](https://telegram.org/tos) 202 | as well as the [API Terms of Service](https://core.telegram.org/api/terms) or you might get banned from Telegram servers. 203 | 204 | If you read all this ReadMe, the [Frequently Asked Questions](https://wiz0u.github.io/WTelegramClient/FAQ), 205 | the [Examples codes](https://wiz0u.github.io/WTelegramClient/EXAMPLES) and still have questions, feedback is welcome in our Telegram group [@WTelegramClient](https://t.me/WTelegramClient) 206 | 207 | If you like this library, you can [buy me a coffee](https://buymeacoffee.com/wizou) ❤ This will help the project keep going. 208 | 209 | © 2021-2025 Olivier Marcoux 210 | -------------------------------------------------------------------------------- /generator/MTProtoGenerator.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | using Microsoft.CodeAnalysis.CSharp.Syntax; 4 | using System.Collections.Generic; 5 | using System.Collections.Immutable; 6 | using System.Diagnostics; 7 | using System.Linq; 8 | using System.Text; 9 | 10 | #pragma warning disable RS1024 // Symbols should be compared for equality 11 | 12 | namespace TL.Generator; 13 | 14 | [Generator] 15 | public class MTProtoGenerator : IIncrementalGenerator 16 | { 17 | public void Initialize(IncrementalGeneratorInitializationContext context) 18 | { 19 | var classDeclarations = context.SyntaxProvider.ForAttributeWithMetadataName("TL.TLDefAttribute", 20 | (_, _) => true, (context, _) => (ClassDeclarationSyntax)context.TargetNode); 21 | var source = context.CompilationProvider.Combine(classDeclarations.Collect()); 22 | context.RegisterSourceOutput(source, Execute); 23 | } 24 | 25 | static void Execute(SourceProductionContext context, (Compilation compilation, ImmutableArray classes) unit) 26 | { 27 | var object_ = unit.compilation.GetSpecialType(SpecialType.System_Object); 28 | if (unit.compilation.GetTypeByMetadataName("TL.TLDefAttribute") is not { } tlDefAttribute) return; 29 | if (unit.compilation.GetTypeByMetadataName("TL.IfFlagAttribute") is not { } ifFlagAttribute) return; 30 | if (unit.compilation.GetTypeByMetadataName("TL.Layer") is not { } layer) return; 31 | if (unit.compilation.GetTypeByMetadataName("TL.IObject") is not { } iobject) return; 32 | var nullables = LoadNullables(layer); 33 | var namespaces = new Dictionary>(); // namespace,class,methods 34 | var tableTL = new StringBuilder(); 35 | var source = new StringBuilder(); 36 | source 37 | .AppendLine("using System;") 38 | .AppendLine("using System.Collections.Generic;") 39 | .AppendLine("using System.ComponentModel;") 40 | .AppendLine("using System.IO;") 41 | .AppendLine("using System.Linq;") 42 | .AppendLine("using TL;") 43 | .AppendLine() 44 | .AppendLine("#pragma warning disable CS0109") 45 | .AppendLine(); 46 | tableTL 47 | .AppendLine("\t\tpublic static readonly Dictionary> Table = new()") 48 | .AppendLine("\t\t{"); 49 | 50 | foreach (var classDecl in unit.classes) 51 | { 52 | var semanticModel = unit.compilation.GetSemanticModel(classDecl.SyntaxTree); 53 | if (semanticModel.GetDeclaredSymbol(classDecl) is not { } symbol) continue; 54 | var tldef = symbol.GetAttributes().FirstOrDefault(a => a.AttributeClass == tlDefAttribute); 55 | if (tldef == null) continue; 56 | var id = (uint)tldef.ConstructorArguments[0].Value; 57 | StringBuilder writeTl = new(), readTL = new(); 58 | var ns = symbol.BaseType.ContainingNamespace.ToString(); 59 | var name = symbol.BaseType.Name; 60 | if (ns != "System") 61 | { 62 | if (!namespaces.TryGetValue(ns, out var parentClasses)) namespaces[ns] = parentClasses = []; 63 | parentClasses.TryGetValue(name, out var parentMethods); 64 | if (symbol.BaseType.IsAbstract) 65 | { 66 | if (parentMethods == null) 67 | { 68 | if (name is "Peer") 69 | writeTl.AppendLine("\t\tpublic virtual void WriteTL(BinaryWriter writer) => throw new NotSupportedException();"); 70 | else 71 | writeTl.AppendLine("\t\tpublic abstract void WriteTL(BinaryWriter writer);"); 72 | parentClasses[name] = writeTl.ToString(); 73 | writeTl.Clear(); 74 | } 75 | } 76 | else if (parentMethods?.Contains(" virtual ") == false) 77 | parentClasses[name] = parentMethods.Replace("public void WriteTL(", "public virtual void WriteTL("); 78 | } 79 | ns = symbol.ContainingNamespace.ToString(); 80 | name = symbol.Name; 81 | if (!namespaces.TryGetValue(ns, out var classes)) namespaces[ns] = classes = []; 82 | if (name is "_Message" or "RpcResult" or "MsgCopy") 83 | { 84 | classes[name] = "\t\tpublic void WriteTL(BinaryWriter writer) => throw new NotSupportedException();"; 85 | continue; 86 | } 87 | if (id == 0x3072CFA1) // GzipPacked 88 | tableTL.AppendLine($"\t\t\t[0x{id:X8}] = reader => (IObject)reader.ReadTLGzipped(typeof(IObject)),"); 89 | else if (name != "Null" && (ns != "TL.Methods" || name == "Ping")) 90 | tableTL.AppendLine($"\t\t\t[0x{id:X8}] = {(ns == "TL" ? "" : ns + '.')}{name}.ReadTL,"); 91 | var override_ = symbol.BaseType == object_ ? "" : "override "; 92 | if (name == "Messages_AffectedMessages") override_ = "virtual "; 93 | //if (symbol.Constructors[0].IsImplicitlyDeclared) 94 | // ctorTL.AppendLine($"\t\tpublic {name}() {{ }}"); 95 | if (symbol.IsGenericType) name += ""; 96 | readTL 97 | .AppendLine($"\t\tpublic static new {name} ReadTL(BinaryReader reader)") 98 | .AppendLine("\t\t{") 99 | .AppendLine($"\t\t\tvar r = new {name}();"); 100 | writeTl 101 | .AppendLine("\t\t[EditorBrowsable(EditorBrowsableState.Never)]") 102 | .AppendLine($"\t\tpublic {override_}void WriteTL(BinaryWriter writer)") 103 | .AppendLine("\t\t{") 104 | .AppendLine($"\t\t\twriter.Write(0x{id:X8});"); 105 | var members = symbol.GetMembers().ToList(); 106 | for (var parent = symbol.BaseType; parent != object_; parent = parent.BaseType) 107 | { 108 | var inheritBefore = (bool?)tldef.NamedArguments.FirstOrDefault(k => k.Key == "inheritBefore").Value.Value ?? false; 109 | if (inheritBefore) members.InsertRange(0, parent.GetMembers()); 110 | else members.AddRange(parent.GetMembers()); 111 | tldef = parent.GetAttributes().FirstOrDefault(a => a.AttributeClass == tlDefAttribute); 112 | } 113 | foreach (var member in members.OfType()) 114 | { 115 | if (member.DeclaredAccessibility != Accessibility.Public || member.IsStatic) continue; 116 | readTL.Append("\t\t\t"); 117 | writeTl.Append("\t\t\t"); 118 | var ifFlag = (int?)member.GetAttributes().FirstOrDefault(a => a.AttributeClass == ifFlagAttribute)?.ConstructorArguments[0].Value; 119 | if (ifFlag != null) 120 | { 121 | readTL.Append(ifFlag < 32 ? $"if (((uint)r.flags & 0x{1 << ifFlag:X}) != 0) " 122 | : $"if (((uint)r.flags2 & 0x{1 << (ifFlag - 32):X}) != 0) "); 123 | writeTl.Append(ifFlag < 32 ? $"if (((uint)flags & 0x{1 << ifFlag:X}) != 0) " 124 | : $"if (((uint)flags2 & 0x{1 << (ifFlag - 32):X}) != 0) "); 125 | } 126 | string memberType = member.Type.ToString(); 127 | switch (memberType) 128 | { 129 | case "int": 130 | readTL.AppendLine($"r.{member.Name} = reader.ReadInt32();"); 131 | writeTl.AppendLine($"writer.Write({member.Name});"); 132 | break; 133 | case "long": 134 | readTL.AppendLine($"r.{member.Name} = reader.ReadInt64();"); 135 | writeTl.AppendLine($"writer.Write({member.Name});"); 136 | break; 137 | case "double": 138 | readTL.AppendLine($"r.{member.Name} = reader.ReadDouble();"); 139 | writeTl.AppendLine($"writer.Write({member.Name});"); 140 | break; 141 | case "bool": 142 | readTL.AppendLine($"r.{member.Name} = reader.ReadTLBool();"); 143 | writeTl.AppendLine($"writer.Write({member.Name} ? 0x997275B5 : 0xBC799737);"); 144 | break; 145 | case "System.DateTime": 146 | readTL.AppendLine($"r.{member.Name} = reader.ReadTLStamp();"); 147 | writeTl.AppendLine($"writer.WriteTLStamp({member.Name});"); 148 | break; 149 | case "string": 150 | readTL.AppendLine($"r.{member.Name} = reader.ReadTLString();"); 151 | writeTl.AppendLine($"writer.WriteTLString({member.Name});"); 152 | break; 153 | case "byte[]": 154 | readTL.AppendLine($"r.{member.Name} = reader.ReadTLBytes();"); 155 | writeTl.AppendLine($"writer.WriteTLBytes({member.Name});"); 156 | break; 157 | case "TL.Int128": 158 | readTL.AppendLine($"r.{member.Name} = new Int128(reader);"); 159 | writeTl.AppendLine($"writer.Write({member.Name});"); 160 | break; 161 | case "TL.Int256": 162 | readTL.AppendLine($"r.{member.Name} = new Int256(reader);"); 163 | writeTl.AppendLine($"writer.Write({member.Name});"); 164 | break; 165 | case "System.Collections.Generic.List": 166 | readTL.AppendLine($"r.{member.Name} = reader.ReadTLRawVector<_Message>(0x5BB8E511);"); 167 | writeTl.AppendLine($"writer.WriteTLMessages({member.Name});"); 168 | break; 169 | case "TL.IObject": case "TL.IMethod": 170 | readTL.AppendLine($"r.{member.Name} = {(memberType == "TL.IObject" ? "" : $"({memberType})")}reader.ReadTLObject();"); 171 | writeTl.AppendLine($"{member.Name}.WriteTL(writer);"); 172 | break; 173 | case "System.Collections.Generic.Dictionary": 174 | readTL.AppendLine($"r.{member.Name} = reader.ReadTLDictionary();"); 175 | writeTl.AppendLine($"writer.WriteTLVector({member.Name}.Values.ToArray());"); 176 | break; 177 | case "System.Collections.Generic.Dictionary": 178 | readTL.AppendLine($"r.{member.Name} = reader.ReadTLDictionary();"); 179 | writeTl.AppendLine($"writer.WriteTLVector({member.Name}.Values.ToArray());"); 180 | break; 181 | default: 182 | if (member.Type is IArrayTypeSymbol arrayType) 183 | { 184 | if (name is "FutureSalts") 185 | readTL.AppendLine($"r.{member.Name} = reader.ReadTLRawVector<{memberType.Substring(0, memberType.Length - 2)}>(0x0949D9DC).ToArray();"); 186 | else 187 | readTL.AppendLine($"r.{member.Name} = reader.ReadTLVector<{memberType.Substring(0, memberType.Length - 2)}>();"); 188 | writeTl.AppendLine($"writer.WriteTLVector({member.Name});"); 189 | } 190 | else if (member.Type.BaseType.SpecialType == SpecialType.System_Enum) 191 | { 192 | readTL.AppendLine($"r.{member.Name} = ({memberType})reader.ReadUInt32();"); 193 | writeTl.AppendLine($"writer.Write((uint){member.Name});"); 194 | } 195 | else if (memberType.StartsWith("TL.")) 196 | { 197 | readTL.AppendLine($"r.{member.Name} = ({memberType})reader.ReadTLObject();"); 198 | var nullStr = nullables.TryGetValue(memberType, out uint nullCtor) ? $"0x{nullCtor:X8}" : "Layer.NullCtor"; 199 | writeTl.AppendLine($"if ({member.Name} != null) {member.Name}.WriteTL(writer); else writer.Write({nullStr});"); 200 | } 201 | else 202 | writeTl.AppendLine($"Cannot serialize {memberType}"); 203 | break; 204 | } 205 | } 206 | readTL.AppendLine("\t\t\treturn r;"); 207 | readTL.AppendLine("\t\t}"); 208 | writeTl.AppendLine("\t\t}"); 209 | readTL.Append(writeTl.ToString()); 210 | classes[name] = readTL.ToString(); 211 | } 212 | 213 | foreach (var nullable in nullables) 214 | tableTL.AppendLine($"\t\t\t[0x{nullable.Value:X8}] = null,"); 215 | tableTL.AppendLine("\t\t};"); 216 | namespaces["TL"]["Layer"] = tableTL.ToString(); 217 | foreach (var namesp in namespaces) 218 | { 219 | source.Append("namespace ").AppendLine(namesp.Key).Append('{'); 220 | foreach (var method in namesp.Value) 221 | source.AppendLine().Append("\tpartial class ").AppendLine(method.Key).AppendLine("\t{").Append(method.Value).AppendLine("\t}"); 222 | source.AppendLine("}").AppendLine(); 223 | } 224 | string text = source.ToString(); 225 | Debug.Write(text); 226 | context.AddSource("TL.Generated.cs", text); 227 | } 228 | 229 | private static Dictionary LoadNullables(INamedTypeSymbol layer) 230 | { 231 | var nullables = layer.GetMembers("Nullables").Single() as IFieldSymbol; 232 | var initializer = nullables.DeclaringSyntaxReferences[0].GetSyntax().ToString(); 233 | var table = new Dictionary(); 234 | foreach (var line in initializer.Split('\n')) 235 | { 236 | int index = line.IndexOf("[typeof("); 237 | if (index == -1) continue; 238 | int index2 = line.IndexOf(')', index += 8); 239 | string className = "TL." + line.Substring(index, index2 - index); 240 | index = line.IndexOf("= 0x", index2); 241 | if (index == -1) continue; 242 | index2 = line.IndexOf(',', index += 4); 243 | table[className] = uint.Parse(line.Substring(index, index2 - index), System.Globalization.NumberStyles.HexNumber); 244 | } 245 | return table; 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /generator/MTProtoGenerator.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0 4 | true 5 | true 6 | true 7 | True 8 | latest 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 20 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiz0u/WTelegramClient/5358471574404a4103c386a9c5bc18c4fec25fcc/logo.png -------------------------------------------------------------------------------- /src/Compat.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers.Binary; 3 | using System.Collections.Generic; 4 | using System.ComponentModel; 5 | using System.Linq; 6 | using System.Net; 7 | using System.Numerics; 8 | using System.Security.Cryptography; 9 | using System.Threading.Tasks; 10 | 11 | #if NETCOREAPP2_1_OR_GREATER 12 | namespace WTelegram 13 | { 14 | static partial class Compat 15 | { 16 | internal static BigInteger BigEndianInteger(byte[] value) => new(value, true, true); 17 | internal static IPEndPoint IPEndPoint_Parse(string addr) => IPEndPoint.Parse(addr); 18 | } 19 | } 20 | #else // Compatibility shims for methods missing in netstandard2.0: 21 | namespace WTelegram 22 | { 23 | static partial class Compat 24 | { 25 | internal static BigInteger BigEndianInteger(byte[] value) 26 | { 27 | var data = new byte[value.Length + 1]; 28 | value.CopyTo(data, 1); 29 | Array.Reverse(data); 30 | return new BigInteger(data); 31 | } 32 | 33 | internal static byte[] ToByteArray(this BigInteger bigInteger, bool isUnsigned, bool isBigEndian) 34 | { 35 | if (!isBigEndian || !isUnsigned) throw new ArgumentException("Unexpected parameters to ToByteArray"); 36 | var result = bigInteger.ToByteArray(); 37 | if (result[^1] == 0) result = result[0..^1]; 38 | Array.Reverse(result); 39 | return result; 40 | } 41 | 42 | internal static long GetBitLength(this BigInteger bigInteger) 43 | { 44 | var bytes = bigInteger.ToByteArray(); 45 | var length = bytes.LongLength * 8L; 46 | int lastByte = bytes[^1]; 47 | while ((lastByte & 0x80) == 0) { length--; lastByte = (lastByte << 1) + 1; } 48 | return length; 49 | } 50 | 51 | public static V GetValueOrDefault(this IReadOnlyDictionary dictionary, K key, V defaultValue = default) 52 | => dictionary.TryGetValue(key, out V value) ? value : defaultValue; 53 | 54 | public static void Deconstruct(this KeyValuePair kvp, out K key, out V value) { key = kvp.Key; value = kvp.Value; } 55 | 56 | internal static IPEndPoint IPEndPoint_Parse(string addr) 57 | { 58 | int colon = addr.LastIndexOf(':'); 59 | return new IPEndPoint(IPAddress.Parse(addr[0..colon]), int.Parse(addr[(colon + 1)..])); 60 | } 61 | 62 | internal static void ImportFromPem(this RSA rsa, string pem) 63 | { 64 | var header = pem.IndexOf("-----BEGIN RSA PUBLIC KEY-----"); 65 | var footer = pem.IndexOf("-----END RSA PUBLIC KEY-----"); 66 | if (header == -1 || footer <= header) throw new ArgumentException("Invalid RSA Public Key"); 67 | byte[] bytes = System.Convert.FromBase64String(pem[(header+30)..footer]); 68 | if (bytes.Length != 270 || BinaryPrimitives.ReadInt64BigEndian(bytes) != 0x3082010A02820101 || bytes[265] != 0x02 || bytes[266] != 0x03) 69 | throw new ArgumentException("Unrecognized sequence in RSA Public Key"); 70 | rsa.ImportParameters(new RSAParameters { Modulus = bytes[8..265], Exponent = bytes[267..270] }); 71 | } 72 | } 73 | } 74 | 75 | static class Convert 76 | { 77 | internal static string ToHexString(byte[] data) => BitConverter.ToString(data).Replace("-", ""); 78 | internal static byte[] FromHexString(string hex) => Enumerable.Range(0, hex.Length / 2).Select(i => System.Convert.ToByte(hex.Substring(i * 2, 2), 16)).ToArray(); 79 | } 80 | public class RandomNumberGenerator 81 | { 82 | internal static readonly RNGCryptoServiceProvider RNG = new(); 83 | public static RandomNumberGenerator Create() => new(); 84 | public void GetBytes(byte[] data) => RNG.GetBytes(data); 85 | public void GetBytes(byte[] data, int offset, int count) => RNG.GetBytes(data, offset, count); 86 | } 87 | #endif 88 | 89 | #if NETSTANDARD2_0 90 | namespace System.Runtime.CompilerServices 91 | { 92 | internal static class RuntimeHelpers 93 | { 94 | public static T[] GetSubArray(T[] array, Range range) 95 | { 96 | if (array == null) throw new ArgumentNullException(); 97 | var (offset, length) = range.GetOffsetAndLength(array.Length); 98 | if (length == 0) return []; 99 | var dest = typeof(T).IsValueType || typeof(T[]) == array.GetType() ? new T[length] 100 | : (T[])Array.CreateInstance(array.GetType().GetElementType()!, length); 101 | Array.Copy(array, offset, dest, 0, length); 102 | return dest; 103 | } 104 | } 105 | [EditorBrowsable(EditorBrowsableState.Never)] 106 | internal class IsExternalInit { } 107 | } 108 | #endif 109 | 110 | namespace WTelegram 111 | { 112 | static partial class Compat 113 | { 114 | internal static Task WaitAsync(this Task source, int timeout) 115 | #if NET8_0_OR_GREATER 116 | => source?.WaitAsync(TimeSpan.FromMilliseconds(timeout)) ?? Task.CompletedTask; 117 | #else 118 | => source == null ? Task.CompletedTask : Task.WhenAny(source, Task.Delay(timeout)); 119 | #endif 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Helpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Numerics; 5 | using System.Reflection; 6 | using System.Text.Json; 7 | using System.Text.Json.Serialization; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | #if NET8_0_OR_GREATER 12 | [JsonSerializable(typeof(WTelegram.Session))] 13 | [JsonSerializable(typeof(Dictionary))] 14 | [JsonSerializable(typeof(IDictionary))] 15 | [JsonSerializable(typeof(System.Collections.Immutable.ImmutableDictionary))] 16 | internal partial class WTelegramContext : JsonSerializerContext { } 17 | #endif 18 | 19 | namespace WTelegram 20 | { 21 | public static class Helpers 22 | { 23 | /// Callback for logging a line (string) with its associated severity level (int) 24 | public static Action Log { get; set; } = DefaultLogger; 25 | 26 | /// For serializing indented Json with fields included 27 | public static readonly JsonSerializerOptions JsonOptions = new() { IncludeFields = true, WriteIndented = true, 28 | #if NET8_0_OR_GREATER 29 | TypeInfoResolver = JsonSerializer.IsReflectionEnabledByDefault ? null : WTelegramContext.Default, 30 | #endif 31 | IgnoreReadOnlyProperties = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; 32 | 33 | private static readonly ConsoleColor[] LogLevelToColor = [ ConsoleColor.DarkGray, ConsoleColor.DarkCyan, 34 | ConsoleColor.Cyan, ConsoleColor.Yellow, ConsoleColor.Red, ConsoleColor.Magenta, ConsoleColor.DarkBlue ]; 35 | private static void DefaultLogger(int level, string message) 36 | { 37 | Console.ForegroundColor = LogLevelToColor[level]; 38 | Console.WriteLine(message); 39 | Console.ResetColor(); 40 | } 41 | 42 | public static V GetOrCreate(this Dictionary dictionary, K key) where V : new() 43 | => dictionary.TryGetValue(key, out V value) ? value : dictionary[key] = new V(); 44 | 45 | /// Get a cryptographic random 64-bit value 46 | public static long RandomLong() 47 | { 48 | #if NETCOREAPP2_1_OR_GREATER 49 | long value = 0; 50 | System.Security.Cryptography.RandomNumberGenerator.Fill(System.Runtime.InteropServices.MemoryMarshal.AsBytes( 51 | System.Runtime.InteropServices.MemoryMarshal.CreateSpan(ref value, 1))); 52 | return value; 53 | #else 54 | var span = new byte[8]; 55 | Encryption.RNG.GetBytes(span); 56 | return BitConverter.ToInt64(span, 0); 57 | #endif 58 | } 59 | 60 | public static async Task FullReadAsync(this Stream stream, byte[] buffer, int length, CancellationToken ct) 61 | { 62 | for (int offset = 0; offset < length;) 63 | { 64 | #pragma warning disable CA1835 65 | var read = await stream.ReadAsync(buffer, offset, length - offset, ct); 66 | #pragma warning restore CA1835 67 | if (read == 0) return offset; 68 | offset += read; 69 | } 70 | return length; 71 | } 72 | 73 | internal static byte[] ToBigEndian(ulong value) // variable-size buffer 74 | { 75 | int i = 1; 76 | for (ulong temp = value; (temp >>= 8) != 0; ) i++; 77 | var result = new byte[i]; 78 | for (; --i >= 0; value >>= 8) 79 | result[i] = (byte)value; 80 | return result; 81 | } 82 | 83 | internal static ulong FromBigEndian(byte[] bytes) // variable-size buffer 84 | { 85 | if (bytes.Length > 8) throw new ArgumentException($"expected bytes length <= 8 but got {bytes.Length}"); 86 | ulong result = 0; 87 | foreach (byte b in bytes) 88 | result = (result << 8) + b; 89 | return result; 90 | } 91 | 92 | internal static byte[] To256Bytes(this BigInteger bi) 93 | { 94 | var bigEndian = bi.ToByteArray(true, true); 95 | if (bigEndian.Length == 256) return bigEndian; 96 | var result = new byte[256]; 97 | bigEndian.CopyTo(result, 256 - bigEndian.Length); 98 | return result; 99 | } 100 | 101 | internal static ulong PQFactorize(ulong pq) // ported from https://github.com/tdlib/td/blob/master/tdutils/td/utils/crypto.cpp#L103 102 | { 103 | if (pq < 2) return 1; 104 | var random = new Random(); 105 | ulong g = 0; 106 | for (int i = 0, iter = 0; i < 3 || iter < 1000; i++) 107 | { 108 | ulong q = (ulong)random.Next(17, 32) % (pq - 1); 109 | ulong x = ((ulong)random.Next() + (ulong)random.Next() << 31) % (pq - 1) + 1; 110 | ulong y = x; 111 | int lim = 1 << (Math.Min(5, i) + 18); 112 | for (int j = 1; j < lim; j++) 113 | { 114 | iter++; 115 | // x = (q + x * x) % pq 116 | ulong res = q, a = x; 117 | while (x != 0) 118 | { 119 | if ((x & 1) != 0) 120 | res = (res + a) % pq; 121 | a = (a + a) % pq; 122 | x >>= 1; 123 | } 124 | x = res; 125 | ulong z = x < y ? pq + x - y : x - y; 126 | g = gcd(z, pq); 127 | if (g != 1) 128 | break; 129 | 130 | if ((j & (j - 1)) == 0) 131 | y = x; 132 | } 133 | if (g > 1 && g < pq) 134 | break; 135 | } 136 | if (g != 0) 137 | { 138 | ulong other = pq / g; 139 | if (other < g) 140 | g = other; 141 | } 142 | return g; 143 | 144 | static ulong gcd(ulong left, ulong right) 145 | { 146 | while (right != 0) 147 | { 148 | ulong num = left % right; 149 | left = right; 150 | right = num; 151 | } 152 | return left; 153 | } 154 | } 155 | 156 | public static int MillerRabinIterations { get; set; } = 64; // 64 is OpenSSL default for 2048-bits numbers 157 | /// Miller–Rabin primality test 158 | /// The number to check for primality 159 | public static bool IsProbablePrime(this BigInteger n) 160 | { 161 | var n_minus_one = n - BigInteger.One; 162 | if (n_minus_one.Sign <= 0) return false; 163 | 164 | int s; 165 | var d = n_minus_one; 166 | for (s = 0; d.IsEven; s++) d >>= 1; 167 | 168 | var bitLen = n.GetBitLength(); 169 | var randomBytes = new byte[bitLen / 8 + 1]; 170 | var lastByteMask = (byte)((1 << (int)(bitLen % 8)) - 1); 171 | BigInteger a; 172 | if (MillerRabinIterations < 15) // 15 is the minimum recommended by Telegram 173 | Log(3, $"MillerRabinIterations ({MillerRabinIterations}) is below the minimal level of safety (15)"); 174 | for (int i = 0; i < MillerRabinIterations; i++) 175 | { 176 | do 177 | { 178 | Encryption.RNG.GetBytes(randomBytes); 179 | randomBytes[^1] &= lastByteMask; // we don't want more bits than necessary 180 | a = new BigInteger(randomBytes); 181 | } 182 | while (a < 3 || a >= n_minus_one); 183 | a--; 184 | 185 | var x = BigInteger.ModPow(a, d, n); 186 | if (x.IsOne || x == n_minus_one) continue; 187 | 188 | int r; 189 | for (r = s - 1; r > 0; r--) 190 | { 191 | x = BigInteger.ModPow(x, 2, n); 192 | if (x.IsOne) return false; 193 | if (x == n_minus_one) break; 194 | } 195 | if (r == 0) return false; 196 | } 197 | return true; 198 | } 199 | 200 | internal static readonly byte[] StrippedThumbJPG = // see https://core.telegram.org/api/files#stripped-thumbnails 201 | [ 202 | 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 203 | 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43, 0x00, 0x28, 0x1c, 204 | 0x1e, 0x23, 0x1e, 0x19, 0x28, 0x23, 0x21, 0x23, 0x2d, 0x2b, 0x28, 0x30, 0x3c, 0x64, 0x41, 0x3c, 0x37, 0x37, 205 | 0x3c, 0x7b, 0x58, 0x5d, 0x49, 0x64, 0x91, 0x80, 0x99, 0x96, 0x8f, 0x80, 0x8c, 0x8a, 0xa0, 0xb4, 0xe6, 0xc3, 206 | 0xa0, 0xaa, 0xda, 0xad, 0x8a, 0x8c, 0xc8, 0xff, 0xcb, 0xda, 0xee, 0xf5, 0xff, 0xff, 0xff, 0x9b, 0xc1, 0xff, 207 | 0xff, 0xff, 0xfa, 0xff, 0xe6, 0xfd, 0xff, 0xf8, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x2b, 0x2d, 0x2d, 0x3c, 0x35, 208 | 0x3c, 0x76, 0x41, 0x41, 0x76, 0xf8, 0xa5, 0x8c, 0xa5, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 209 | 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 210 | 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 211 | 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x00, 0x00, 0x00, 0x03, 0x01, 0x22, 0x00, 212 | 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00, 0x1f, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 213 | 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 214 | 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 215 | 0x04, 0x04, 0x00, 0x00, 0x01, 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 216 | 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 217 | 0xd1, 0xf0, 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28, 218 | 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 219 | 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 220 | 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 221 | 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 222 | 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 223 | 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1, 0xf2, 0xf3, 0xf4, 224 | 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xc4, 0x00, 0x1f, 0x01, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 225 | 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 226 | 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00, 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 227 | 0x04, 0x04, 0x00, 0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, 0x06, 0x12, 0x41, 228 | 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 229 | 0x52, 0xf0, 0x15, 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a, 0x26, 230 | 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 231 | 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 232 | 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 233 | 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 234 | 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 235 | 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf2, 0xf3, 0xf4, 236 | 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 237 | 0x3f, 0x00 238 | ]; 239 | 240 | internal static string GetSystemVersion() 241 | { 242 | var os = System.Runtime.InteropServices.RuntimeInformation.OSDescription; 243 | int space = os.IndexOf(' ') + 1, dot = os.IndexOf('.'); 244 | return os[(os.IndexOf(' ', space) < 0 ? 0 : space)..(dot < 0 ? os.Length : dot)]; 245 | } 246 | 247 | internal static string GetAppVersion() 248 | => (Assembly.GetEntryAssembly() ?? Array.Find(AppDomain.CurrentDomain.GetAssemblies(), a => a.EntryPoint != null))?.GetName().Version.ToString() ?? "0.0"; 249 | 250 | public class IndirectStream(Stream innerStream) : Stream 251 | { 252 | public long? ContentLength; 253 | protected readonly Stream _innerStream = innerStream; 254 | public override bool CanRead => _innerStream.CanRead; 255 | public override bool CanSeek => ContentLength.HasValue || _innerStream.CanSeek; 256 | public override bool CanWrite => _innerStream.CanWrite; 257 | public override long Length => ContentLength ?? _innerStream.Length; 258 | public override long Position { get => _innerStream.Position; set => _innerStream.Position = value; } 259 | public override void Flush() => _innerStream.Flush(); 260 | public override long Seek(long offset, SeekOrigin origin) => _innerStream.Seek(offset, origin); 261 | public override void SetLength(long value) => _innerStream.SetLength(value); 262 | public override void Write(byte[] buffer, int offset, int count) => _innerStream.Write(buffer, offset, count); 263 | public override int Read(byte[] buffer, int offset, int count) => _innerStream.Read(buffer, offset, count); 264 | protected override void Dispose(bool disposing) => _innerStream.Dispose(); 265 | } 266 | } 267 | 268 | public class WTException : ApplicationException 269 | { 270 | public WTException(string message) : base(message) { } 271 | public WTException(string message, Exception innerException) : base(message, innerException) { } 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /src/Session.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers.Binary; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Net; 7 | using System.Security.Cryptography; 8 | using System.Text.Json; 9 | 10 | // Don't change this code to lower the security. It's following Telegram security recommendations https://corefork.telegram.org/mtproto/description 11 | 12 | namespace WTelegram 13 | { 14 | internal sealed partial class Session : IDisposable 15 | { 16 | public int ApiId; 17 | public long UserId; 18 | public int MainDC; 19 | public Dictionary DCSessions = []; 20 | public TL.DcOption[] DcOptions; 21 | 22 | public sealed class DCSession 23 | { 24 | public byte[] AuthKey; // 2048-bit = 256 bytes 25 | public long UserId; 26 | public long OldSalt; // still accepted for a further 1800 seconds 27 | public long Salt; 28 | public SortedList Salts; 29 | public TL.DcOption DataCenter; 30 | public int Layer; 31 | 32 | internal long id = Helpers.RandomLong(); 33 | internal long authKeyID; 34 | internal int seqno; 35 | internal long serverTicksOffset; 36 | internal long lastSentMsgId; 37 | internal bool withoutUpdates; 38 | internal Client Client; 39 | internal int DcID => DataCenter?.id ?? 0; 40 | internal IPEndPoint EndPoint => DataCenter == null ? null : new(IPAddress.Parse(DataCenter.ip_address), DataCenter.port); 41 | internal void Renew() { Helpers.Log(3, $"Renewing session on DC {DcID}..."); id = Helpers.RandomLong(); seqno = 0; lastSentMsgId = 0; } 42 | public void DisableUpdates(bool disable = true) { if (withoutUpdates != disable) { withoutUpdates = disable; Renew(); } } 43 | 44 | const int MsgIdsN = 512; 45 | private long[] _msgIds; 46 | private int _msgIdsHead; 47 | internal bool CheckNewMsgId(long msg_id) 48 | { 49 | if (_msgIds == null) 50 | { 51 | _msgIds = new long[MsgIdsN]; 52 | _msgIds[0] = msg_id; 53 | msg_id -= 300L << 32; // until the array is filled with real values, allow ids up to 300 seconds in the past 54 | for (int i = 1; i < MsgIdsN; i++) _msgIds[i] = msg_id; 55 | return true; 56 | } 57 | int newHead = (_msgIdsHead + 1) % MsgIdsN; 58 | if (msg_id > _msgIds[_msgIdsHead]) 59 | _msgIds[_msgIdsHead = newHead] = msg_id; 60 | else if (msg_id <= _msgIds[newHead]) 61 | return false; 62 | else 63 | { 64 | int min = 0, max = MsgIdsN - 1; 65 | while (min <= max) // binary search (rotated at newHead) 66 | { 67 | int mid = (min + max) / 2; 68 | int sign = msg_id.CompareTo(_msgIds[(mid + newHead) % MsgIdsN]); 69 | if (sign == 0) return false; 70 | else if (sign < 0) max = mid - 1; 71 | else min = mid + 1; 72 | } 73 | _msgIdsHead = newHead; 74 | for (min = (min + newHead) % MsgIdsN; newHead != min;) 75 | _msgIds[newHead] = _msgIds[newHead = newHead == 0 ? MsgIdsN - 1 : newHead - 1]; 76 | _msgIds[min] = msg_id; 77 | } 78 | return true; 79 | } 80 | } 81 | 82 | public DateTime SessionStart => _sessionStart; 83 | private readonly DateTime _sessionStart = DateTime.UtcNow; 84 | private readonly SHA256 _sha256 = SHA256.Create(); 85 | private Stream _store; 86 | private byte[] _reuseKey; // used only if AES Encryptor.CanReuseTransform = false (Mono) 87 | private byte[] _encrypted = new byte[16]; 88 | private ICryptoTransform _encryptor; 89 | private Utf8JsonWriter _jsonWriter; 90 | private readonly MemoryStream _jsonStream = new(4096); 91 | 92 | public void Dispose() 93 | { 94 | _sha256.Dispose(); 95 | _store.Dispose(); 96 | _encryptor.Dispose(); 97 | _jsonWriter.Dispose(); 98 | _jsonStream.Dispose(); 99 | } 100 | 101 | internal static Session LoadOrCreate(Stream store, byte[] rgbKey) 102 | { 103 | using var aes = Aes.Create(); 104 | Session session = null; 105 | try 106 | { 107 | var length = (int)store.Length; 108 | if (length > 0) 109 | { 110 | var input = new byte[length]; 111 | if (store.Read(input, 0, length) != length) 112 | throw new WTException($"Can't read session block ({store.Position}, {length})"); 113 | using var sha256 = SHA256.Create(); 114 | using var decryptor = aes.CreateDecryptor(rgbKey, input[0..16]); 115 | var utf8Json = decryptor.TransformFinalBlock(input, 16, input.Length - 16); 116 | if (!sha256.ComputeHash(utf8Json, 32, utf8Json.Length - 32).SequenceEqual(utf8Json[0..32])) 117 | throw new WTException("Integrity check failed in session loading"); 118 | session = JsonSerializer.Deserialize(utf8Json.AsSpan(32), Helpers.JsonOptions); 119 | Helpers.Log(2, "Loaded previous session"); 120 | using var sha1 = SHA1.Create(); 121 | foreach (var dcs in session.DCSessions.Values) 122 | dcs.authKeyID = BinaryPrimitives.ReadInt64LittleEndian(sha1.ComputeHash(dcs.AuthKey).AsSpan(12)); 123 | } 124 | session ??= new Session(); 125 | session._store = store; 126 | Encryption.RNG.GetBytes(session._encrypted, 0, 16); 127 | session._encryptor = aes.CreateEncryptor(rgbKey, session._encrypted); 128 | if (!session._encryptor.CanReuseTransform) session._reuseKey = rgbKey; 129 | session._jsonWriter = new Utf8JsonWriter(session._jsonStream, default); 130 | return session; 131 | } 132 | catch (Exception ex) 133 | { 134 | store.Dispose(); 135 | throw new WTException($"Exception while reading session file: {ex.Message}\nUse the correct api_hash/id/key, or delete the file to start a new session", ex); 136 | } 137 | } 138 | 139 | internal void Save() // must be called with lock(session) 140 | { 141 | JsonSerializer.Serialize(_jsonWriter, this, Helpers.JsonOptions); 142 | var utf8Json = _jsonStream.GetBuffer(); 143 | var utf8JsonLen = (int)_jsonStream.Position; 144 | int encryptedLen = 64 + (utf8JsonLen & ~15); 145 | lock (_store) // while updating _encrypted buffer and writing to store 146 | { 147 | if (encryptedLen > _encrypted.Length) 148 | Array.Copy(_encrypted, _encrypted = new byte[encryptedLen + 256], 16); 149 | _encryptor.TransformBlock(_sha256.ComputeHash(utf8Json, 0, utf8JsonLen), 0, 32, _encrypted, 16); 150 | _encryptor.TransformBlock(utf8Json, 0, encryptedLen - 64, _encrypted, 48); 151 | _encryptor.TransformFinalBlock(utf8Json, encryptedLen - 64, utf8JsonLen & 15).CopyTo(_encrypted, encryptedLen - 16); 152 | if (!_encryptor.CanReuseTransform) // under Mono, AES encryptor is not reusable 153 | using (var aes = Aes.Create()) 154 | _encryptor = aes.CreateEncryptor(_reuseKey, _encrypted[0..16]); 155 | try 156 | { 157 | _store.Position = 0; 158 | _store.Write(_encrypted, 0, encryptedLen); 159 | _store.SetLength(encryptedLen); 160 | } 161 | catch (Exception ex) 162 | { 163 | Helpers.Log(4, $"{_store} raised {ex}"); 164 | } 165 | } 166 | _jsonStream.Position = 0; 167 | _jsonWriter.Reset(); 168 | } 169 | } 170 | 171 | internal sealed class SessionStore : FileStream // This class is designed to be high-performance and failure-resilient with Writes (but when you're Andrei, you can't understand that) 172 | { 173 | public override long Length { get; } 174 | public override long Position { get => base.Position; set { } } 175 | public override void SetLength(long value) { } 176 | private readonly byte[] _header = new byte[8]; 177 | private int _nextPosition = 8; 178 | 179 | public SessionStore(string pathname) 180 | : base(pathname, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None, 1) // no in-app buffering 181 | { 182 | if (base.Read(_header, 0, 8) == 8) 183 | { 184 | var position = BinaryPrimitives.ReadInt32LittleEndian(_header); 185 | var length = BinaryPrimitives.ReadInt32LittleEndian(_header.AsSpan(4)); 186 | base.Position = position; 187 | Length = length; 188 | _nextPosition = position + length; 189 | } 190 | } 191 | 192 | public override void Write(byte[] buffer, int offset, int count) 193 | { 194 | if (_nextPosition > count * 3) _nextPosition = 8; 195 | base.Position = _nextPosition; 196 | base.Write(buffer, offset, count); 197 | BinaryPrimitives.WriteInt32LittleEndian(_header, _nextPosition); 198 | BinaryPrimitives.WriteInt32LittleEndian(_header.AsSpan(4), count); 199 | _nextPosition += count; 200 | base.Position = 0; 201 | base.Write(_header, 0, 8); 202 | } 203 | } 204 | 205 | internal sealed class ActionStore(byte[] initial, Action save) : MemoryStream(initial ?? []) 206 | { 207 | public override void Write(byte[] buffer, int offset, int count) => save(buffer[offset..(offset + count)]); 208 | public override void SetLength(long value) { } 209 | } 210 | } -------------------------------------------------------------------------------- /src/TL.MTProto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using TL.Methods; 4 | using Client = WTelegram.Client; 5 | 6 | namespace TL 7 | { 8 | #pragma warning disable IDE1006, CS1574 9 | [TLDef(0x05162463)] //resPQ#05162463 nonce:int128 server_nonce:int128 pq:bytes server_public_key_fingerprints:Vector = ResPQ 10 | public sealed partial class ResPQ : IObject 11 | { 12 | public Int128 nonce; 13 | public Int128 server_nonce; 14 | public byte[] pq; 15 | public long[] server_public_key_fingerprints; 16 | } 17 | 18 | [TLDef(0x83C95AEC)] //p_q_inner_data#83c95aec pq:bytes p:bytes q:bytes nonce:int128 server_nonce:int128 new_nonce:int256 = P_Q_inner_data 19 | public partial class PQInnerData : IObject 20 | { 21 | public byte[] pq; 22 | public byte[] p; 23 | public byte[] q; 24 | public Int128 nonce; 25 | public Int128 server_nonce; 26 | public Int256 new_nonce; 27 | } 28 | [TLDef(0xA9F55F95, inheritBefore = true)] //p_q_inner_data_dc#a9f55f95 pq:bytes p:bytes q:bytes nonce:int128 server_nonce:int128 new_nonce:int256 dc:int = P_Q_inner_data 29 | public sealed partial class PQInnerDataDc : PQInnerData 30 | { 31 | public int dc; 32 | } 33 | [TLDef(0x3C6A84D4, inheritBefore = true)] //p_q_inner_data_temp#3c6a84d4 pq:bytes p:bytes q:bytes nonce:int128 server_nonce:int128 new_nonce:int256 expires_in:int = P_Q_inner_data 34 | public sealed partial class PQInnerDataTemp : PQInnerData 35 | { 36 | public int expires_in; 37 | } 38 | [TLDef(0x56FDDF88, inheritBefore = true)] //p_q_inner_data_temp_dc#56fddf88 pq:bytes p:bytes q:bytes nonce:int128 server_nonce:int128 new_nonce:int256 dc:int expires_in:int = P_Q_inner_data 39 | public sealed partial class PQInnerDataTempDc : PQInnerData 40 | { 41 | public int dc; 42 | public int expires_in; 43 | } 44 | 45 | [TLDef(0x75A3F765)] //bind_auth_key_inner#75a3f765 nonce:long temp_auth_key_id:long perm_auth_key_id:long temp_session_id:long expires_at:int = BindAuthKeyInner 46 | public sealed partial class BindAuthKeyInner : IObject 47 | { 48 | public long nonce; 49 | public long temp_auth_key_id; 50 | public long perm_auth_key_id; 51 | public long temp_session_id; 52 | public DateTime expires_at; 53 | } 54 | 55 | public abstract partial class ServerDHParams : IObject 56 | { 57 | public Int128 nonce; 58 | public Int128 server_nonce; 59 | } 60 | [TLDef(0x79CB045D, inheritBefore = true)] //server_DH_params_fail#79cb045d nonce:int128 server_nonce:int128 new_nonce_hash:int128 = Server_DH_Params 61 | public sealed partial class ServerDHParamsFail : ServerDHParams 62 | { 63 | public Int128 new_nonce_hash; 64 | } 65 | [TLDef(0xD0E8075C, inheritBefore = true)] //server_DH_params_ok#d0e8075c nonce:int128 server_nonce:int128 encrypted_answer:bytes = Server_DH_Params 66 | public sealed partial class ServerDHParamsOk : ServerDHParams 67 | { 68 | public byte[] encrypted_answer; 69 | } 70 | 71 | [TLDef(0xB5890DBA)] //server_DH_inner_data#b5890dba nonce:int128 server_nonce:int128 g:int dh_prime:bytes g_a:bytes server_time:int = Server_DH_inner_data 72 | public sealed partial class ServerDHInnerData : IObject 73 | { 74 | public Int128 nonce; 75 | public Int128 server_nonce; 76 | public int g; 77 | public byte[] dh_prime; 78 | public byte[] g_a; 79 | public DateTime server_time; 80 | } 81 | 82 | [TLDef(0x6643B654)] //client_DH_inner_data#6643b654 nonce:int128 server_nonce:int128 retry_id:long g_b:bytes = Client_DH_Inner_Data 83 | public sealed partial class ClientDHInnerData : IObject 84 | { 85 | public Int128 nonce; 86 | public Int128 server_nonce; 87 | public long retry_id; 88 | public byte[] g_b; 89 | } 90 | 91 | public abstract partial class SetClientDHParamsAnswer : IObject 92 | { 93 | public Int128 nonce; 94 | public Int128 server_nonce; 95 | } 96 | [TLDef(0x3BCBF734, inheritBefore = true)] //dh_gen_ok#3bcbf734 nonce:int128 server_nonce:int128 new_nonce_hash1:int128 = Set_client_DH_params_answer 97 | public sealed partial class DhGenOk : SetClientDHParamsAnswer 98 | { 99 | public Int128 new_nonce_hash1; 100 | } 101 | [TLDef(0x46DC1FB9, inheritBefore = true)] //dh_gen_retry#46dc1fb9 nonce:int128 server_nonce:int128 new_nonce_hash2:int128 = Set_client_DH_params_answer 102 | public sealed partial class DhGenRetry : SetClientDHParamsAnswer 103 | { 104 | public Int128 new_nonce_hash2; 105 | } 106 | [TLDef(0xA69DAE02, inheritBefore = true)] //dh_gen_fail#a69dae02 nonce:int128 server_nonce:int128 new_nonce_hash3:int128 = Set_client_DH_params_answer 107 | public sealed partial class DhGenFail : SetClientDHParamsAnswer 108 | { 109 | public Int128 new_nonce_hash3; 110 | } 111 | 112 | public enum DestroyAuthKeyRes : uint 113 | { 114 | ///See 115 | Ok = 0xF660E1D4, 116 | ///See 117 | None = 0x0A9F2259, 118 | ///See 119 | Fail = 0xEA109B13, 120 | } 121 | 122 | [TLDef(0x62D6B459)] //msgs_ack#62d6b459 msg_ids:Vector = MsgsAck 123 | public sealed partial class MsgsAck : IObject 124 | { 125 | public long[] msg_ids; 126 | } 127 | 128 | [TLDef(0xA7EFF811)] //bad_msg_notification#a7eff811 bad_msg_id:long bad_msg_seqno:int error_code:int = BadMsgNotification 129 | public partial class BadMsgNotification : IObject 130 | { 131 | public long bad_msg_id; 132 | public int bad_msg_seqno; 133 | public int error_code; 134 | } 135 | [TLDef(0xEDAB447B, inheritBefore = true)] //bad_server_salt#edab447b bad_msg_id:long bad_msg_seqno:int error_code:int new_server_salt:long = BadMsgNotification 136 | public sealed partial class BadServerSalt : BadMsgNotification 137 | { 138 | public long new_server_salt; 139 | } 140 | 141 | [TLDef(0xDA69FB52)] //msgs_state_req#da69fb52 msg_ids:Vector = MsgsStateReq 142 | public sealed partial class MsgsStateReq : IObject 143 | { 144 | public long[] msg_ids; 145 | } 146 | 147 | [TLDef(0x04DEB57D)] //msgs_state_info#04deb57d req_msg_id:long info:bytes = MsgsStateInfo 148 | public sealed partial class MsgsStateInfo : IObject 149 | { 150 | public long req_msg_id; 151 | public byte[] info; 152 | } 153 | 154 | [TLDef(0x8CC0D131)] //msgs_all_info#8cc0d131 msg_ids:Vector info:bytes = MsgsAllInfo 155 | public sealed partial class MsgsAllInfo : IObject 156 | { 157 | public long[] msg_ids; 158 | public byte[] info; 159 | } 160 | 161 | public abstract partial class MsgDetailedInfoBase : IObject 162 | { 163 | public virtual long AnswerMsgId => default; 164 | public virtual int Bytes => default; 165 | public virtual int Status => default; 166 | } 167 | [TLDef(0x276D3EC6)] //msg_detailed_info#276d3ec6 msg_id:long answer_msg_id:long bytes:int status:int = MsgDetailedInfo 168 | public sealed partial class MsgDetailedInfo : MsgDetailedInfoBase 169 | { 170 | public long msg_id; 171 | public long answer_msg_id; 172 | public int bytes; 173 | public int status; 174 | 175 | public override long AnswerMsgId => answer_msg_id; 176 | public override int Bytes => bytes; 177 | public override int Status => status; 178 | } 179 | [TLDef(0x809DB6DF)] //msg_new_detailed_info#809db6df answer_msg_id:long bytes:int status:int = MsgDetailedInfo 180 | public sealed partial class MsgNewDetailedInfo : MsgDetailedInfoBase 181 | { 182 | public long answer_msg_id; 183 | public int bytes; 184 | public int status; 185 | 186 | public override long AnswerMsgId => answer_msg_id; 187 | public override int Bytes => bytes; 188 | public override int Status => status; 189 | } 190 | 191 | [TLDef(0x7D861A08)] //msg_resend_req#7d861a08 msg_ids:Vector = MsgResendReq 192 | public sealed partial class MsgResendReq : IObject 193 | { 194 | public long[] msg_ids; 195 | } 196 | 197 | [TLDef(0x2144CA19)] //rpc_error#2144ca19 error_code:int error_message:string = RpcError 198 | public sealed partial class RpcError : IObject 199 | { 200 | public int error_code; 201 | public string error_message; 202 | } 203 | 204 | public abstract partial class RpcDropAnswer : IObject { } 205 | [TLDef(0x5E2AD36E)] //rpc_answer_unknown#5e2ad36e = RpcDropAnswer 206 | public sealed partial class RpcAnswerUnknown : RpcDropAnswer { } 207 | [TLDef(0xCD78E586)] //rpc_answer_dropped_running#cd78e586 = RpcDropAnswer 208 | public sealed partial class RpcAnswerDroppedRunning : RpcDropAnswer { } 209 | [TLDef(0xA43AD8B7)] //rpc_answer_dropped#a43ad8b7 msg_id:long seq_no:int bytes:int = RpcDropAnswer 210 | public sealed partial class RpcAnswerDropped : RpcDropAnswer 211 | { 212 | public long msg_id; 213 | public int seq_no; 214 | public int bytes; 215 | } 216 | 217 | [TLDef(0x0949D9DC)] //future_salt#0949d9dc valid_since:int valid_until:int salt:long = FutureSalt 218 | public sealed partial class FutureSalt : IObject 219 | { 220 | public DateTime valid_since; 221 | public DateTime valid_until; 222 | public long salt; 223 | } 224 | 225 | [TLDef(0xAE500895)] //future_salts#ae500895 req_msg_id:long now:int salts:vector = FutureSalts 226 | public sealed partial class FutureSalts : IObject 227 | { 228 | public long req_msg_id; 229 | public DateTime now; 230 | public FutureSalt[] salts; 231 | } 232 | 233 | [TLDef(0x347773C5)] //pong#347773c5 msg_id:long ping_id:long = Pong 234 | public sealed partial class Pong : IObject 235 | { 236 | public long msg_id; 237 | public long ping_id; 238 | } 239 | 240 | public abstract partial class DestroySessionRes : IObject 241 | { 242 | public long session_id; 243 | } 244 | [TLDef(0xE22045FC)] //destroy_session_ok#e22045fc session_id:long = DestroySessionRes 245 | public sealed partial class DestroySessionOk : DestroySessionRes { } 246 | [TLDef(0x62D350C9)] //destroy_session_none#62d350c9 session_id:long = DestroySessionRes 247 | public sealed partial class DestroySessionNone : DestroySessionRes { } 248 | 249 | public abstract partial class NewSession : IObject { } 250 | [TLDef(0x9EC20908)] //new_session_created#9ec20908 first_msg_id:long unique_id:long server_salt:long = NewSession 251 | public sealed partial class NewSessionCreated : NewSession 252 | { 253 | public long first_msg_id; 254 | public long unique_id; 255 | public long server_salt; 256 | } 257 | 258 | [TLDef(0x9299359F)] //http_wait#9299359f max_delay:int wait_after:int max_wait:int = HttpWait 259 | public sealed partial class HttpWait : IObject 260 | { 261 | public int max_delay; 262 | public int wait_after; 263 | public int max_wait; 264 | } 265 | 266 | [TLDef(0xD433AD73)] //ipPort#d433ad73 ipv4:int port:int = IpPort 267 | public partial class IpPort : IObject 268 | { 269 | public int ipv4; 270 | public int port; 271 | } 272 | [TLDef(0x37982646, inheritBefore = true)] //ipPortSecret#37982646 ipv4:int port:int secret:bytes = IpPort 273 | public sealed partial class IpPortSecret : IpPort 274 | { 275 | public byte[] secret; 276 | } 277 | 278 | [TLDef(0x4679B65F)] //accessPointRule#4679b65f phone_prefix_rules:bytes dc_id:int ips:vector = AccessPointRule 279 | public sealed partial class AccessPointRule : IObject 280 | { 281 | public byte[] phone_prefix_rules; 282 | public int dc_id; 283 | public IpPort[] ips; 284 | } 285 | 286 | [TLDef(0x5A592A6C)] //help.configSimple#5a592a6c date:int expires:int rules:vector = help.ConfigSimple 287 | public sealed partial class Help_ConfigSimple : IObject 288 | { 289 | public DateTime date; 290 | public DateTime expires; 291 | public AccessPointRule[] rules; 292 | } 293 | 294 | // ---functions--- 295 | 296 | public static class MTProtoExtensions 297 | { 298 | public static Task ReqPq(this Client client, Int128 nonce) 299 | => client.InvokeBare(new ReqPq 300 | { 301 | nonce = nonce, 302 | }); 303 | 304 | public static Task ReqPqMulti(this Client client, Int128 nonce) 305 | => client.InvokeBare(new ReqPqMulti 306 | { 307 | nonce = nonce, 308 | }); 309 | 310 | public static Task ReqDHParams(this Client client, Int128 nonce, Int128 server_nonce, byte[] p, byte[] q, long public_key_fingerprint, byte[] encrypted_data) 311 | => client.InvokeBare(new ReqDHParams 312 | { 313 | nonce = nonce, 314 | server_nonce = server_nonce, 315 | p = p, 316 | q = q, 317 | public_key_fingerprint = public_key_fingerprint, 318 | encrypted_data = encrypted_data, 319 | }); 320 | 321 | public static Task SetClientDHParams(this Client client, Int128 nonce, Int128 server_nonce, byte[] encrypted_data) 322 | => client.InvokeBare(new SetClientDHParams 323 | { 324 | nonce = nonce, 325 | server_nonce = server_nonce, 326 | encrypted_data = encrypted_data, 327 | }); 328 | 329 | public static Task DestroyAuthKey(this Client client) 330 | => client.InvokeBare(new DestroyAuthKey 331 | { 332 | }); 333 | 334 | public static Task RpcDropAnswer(this Client client, long req_msg_id) 335 | => client.InvokeBare(new Methods.RpcDropAnswer 336 | { 337 | req_msg_id = req_msg_id, 338 | }); 339 | 340 | public static Task GetFutureSalts(this Client client, int num) 341 | => client.Invoke(new GetFutureSalts 342 | { 343 | num = num, 344 | }); 345 | 346 | public static Task Ping(this Client client, long ping_id) 347 | => client.Invoke(new Ping 348 | { 349 | ping_id = ping_id, 350 | }); 351 | 352 | public static Task PingDelayDisconnect(this Client client, long ping_id, int disconnect_delay) 353 | => client.Invoke(new PingDelayDisconnect 354 | { 355 | ping_id = ping_id, 356 | disconnect_delay = disconnect_delay, 357 | }); 358 | 359 | public static Task DestroySession(this Client client, long session_id) 360 | => client.InvokeBare(new DestroySession 361 | { 362 | session_id = session_id, 363 | }); 364 | } 365 | } 366 | 367 | namespace TL.Methods 368 | { 369 | #pragma warning disable IDE1006 370 | [TLDef(0x60469778)] //req_pq#60469778 nonce:int128 = ResPQ 371 | public sealed partial class ReqPq : IMethod 372 | { 373 | public Int128 nonce; 374 | } 375 | 376 | [TLDef(0xBE7E8EF1)] //req_pq_multi#be7e8ef1 nonce:int128 = ResPQ 377 | public sealed partial class ReqPqMulti : IMethod 378 | { 379 | public Int128 nonce; 380 | } 381 | 382 | [TLDef(0xD712E4BE)] //req_DH_params#d712e4be nonce:int128 server_nonce:int128 p:bytes q:bytes public_key_fingerprint:long encrypted_data:bytes = Server_DH_Params 383 | public sealed partial class ReqDHParams : IMethod 384 | { 385 | public Int128 nonce; 386 | public Int128 server_nonce; 387 | public byte[] p; 388 | public byte[] q; 389 | public long public_key_fingerprint; 390 | public byte[] encrypted_data; 391 | } 392 | 393 | [TLDef(0xF5045F1F)] //set_client_DH_params#f5045f1f nonce:int128 server_nonce:int128 encrypted_data:bytes = Set_client_DH_params_answer 394 | public sealed partial class SetClientDHParams : IMethod 395 | { 396 | public Int128 nonce; 397 | public Int128 server_nonce; 398 | public byte[] encrypted_data; 399 | } 400 | 401 | [TLDef(0xD1435160)] //destroy_auth_key#d1435160 = DestroyAuthKeyRes 402 | public sealed partial class DestroyAuthKey : IMethod { } 403 | 404 | [TLDef(0x58E4A740)] //rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer 405 | public sealed partial class RpcDropAnswer : IMethod 406 | { 407 | public long req_msg_id; 408 | } 409 | 410 | [TLDef(0xB921BD04)] //get_future_salts#b921bd04 num:int = FutureSalts 411 | public sealed partial class GetFutureSalts : IMethod 412 | { 413 | public int num; 414 | } 415 | 416 | [TLDef(0x7ABE77EC)] //ping#7abe77ec ping_id:long = Pong 417 | public sealed partial class Ping : IMethod 418 | { 419 | public long ping_id; 420 | } 421 | 422 | [TLDef(0xF3427B8C)] //ping_delay_disconnect#f3427b8c ping_id:long disconnect_delay:int = Pong 423 | public sealed partial class PingDelayDisconnect : IMethod 424 | { 425 | public long ping_id; 426 | public int disconnect_delay; 427 | } 428 | 429 | [TLDef(0xE7512126)] //destroy_session#e7512126 session_id:long = DestroySessionRes 430 | public sealed partial class DestroySession : IMethod 431 | { 432 | public long session_id; 433 | } 434 | } 435 | -------------------------------------------------------------------------------- /src/TL.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.IO.Compression; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Security.Cryptography; 8 | using System.Text; 9 | 10 | #pragma warning disable IDE1006 // Naming Styles 11 | 12 | namespace TL 13 | { 14 | #if MTPG 15 | public interface IObject { void WriteTL(BinaryWriter writer); } 16 | #else 17 | public interface IObject { } 18 | #endif 19 | public interface IMethod : IObject { } 20 | public interface IPeerResolver { IPeerInfo UserOrChat(Peer peer); } 21 | 22 | [AttributeUsage(AttributeTargets.Class)] 23 | public sealed class TLDefAttribute(uint ctorNb) : Attribute 24 | { 25 | public readonly uint CtorNb = ctorNb; 26 | public bool inheritBefore; 27 | } 28 | 29 | [AttributeUsage(AttributeTargets.Field)] 30 | public sealed class IfFlagAttribute(int bit) : Attribute 31 | { 32 | public readonly int Bit = bit; 33 | } 34 | 35 | public sealed class RpcException(int code, string message, int x = -1) : WTelegram.WTException(message) 36 | { 37 | public readonly int Code = code; 38 | /// The value of X in the message, -1 if no variable X was found 39 | public readonly int X = x; 40 | public override string ToString() { var str = base.ToString(); return str.Insert(str.IndexOf(':') + 1, " " + Code); } 41 | } 42 | 43 | public sealed partial class ReactorError : IObject 44 | { 45 | public Exception Exception; 46 | public void WriteTL(BinaryWriter writer) => throw new NotSupportedException(); 47 | } 48 | 49 | public static class Serialization 50 | { 51 | public static void WriteTLObject(this BinaryWriter writer, T obj) where T : IObject 52 | { 53 | if (obj == null) { writer.WriteTLNull(typeof(T)); return; } 54 | #if MTPG 55 | obj.WriteTL(writer); 56 | #else 57 | var type = obj.GetType(); 58 | var tlDef = type.GetCustomAttribute(); 59 | var ctorNb = tlDef.CtorNb; 60 | writer.Write(ctorNb); 61 | IEnumerable fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public); 62 | if (tlDef.inheritBefore) fields = fields.GroupBy(f => f.DeclaringType).Reverse().SelectMany(g => g); 63 | ulong flags = 0; 64 | IfFlagAttribute ifFlag; 65 | foreach (var field in fields) 66 | { 67 | if (((ifFlag = field.GetCustomAttribute()) != null) && (flags & (1UL << ifFlag.Bit)) == 0) continue; 68 | object value = field.GetValue(obj); 69 | writer.WriteTLValue(value, field.FieldType); 70 | if (field.FieldType.IsEnum) 71 | if (field.Name == "flags") flags = (uint)value; 72 | else if (field.Name == "flags2") flags |= (ulong)(uint)value << 32; 73 | } 74 | #endif 75 | } 76 | 77 | public static IObject ReadTLObject(this BinaryReader reader, uint ctorNb = 0) 78 | { 79 | if (ctorNb == 0) ctorNb = reader.ReadUInt32(); 80 | #if MTPG 81 | if (!Layer.Table.TryGetValue(ctorNb, out var ctor)) 82 | throw new WTelegram.WTException($"Cannot find type for ctor #{ctorNb:x}"); 83 | return ctor?.Invoke(reader); 84 | #else 85 | if (ctorNb == Layer.GZipedCtor) return (IObject)reader.ReadTLGzipped(typeof(IObject)); 86 | if (!Layer.Table.TryGetValue(ctorNb, out var type)) 87 | throw new WTelegram.WTException($"Cannot find type for ctor #{ctorNb:x}"); 88 | if (type == null) return null; // nullable ctor (class meaning is associated with null) 89 | var tlDef = type.GetCustomAttribute(); 90 | var obj = Activator.CreateInstance(type, true); 91 | IEnumerable fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public); 92 | if (tlDef.inheritBefore) fields = fields.GroupBy(f => f.DeclaringType).Reverse().SelectMany(g => g); 93 | ulong flags = 0; 94 | IfFlagAttribute ifFlag; 95 | foreach (var field in fields) 96 | { 97 | if (((ifFlag = field.GetCustomAttribute()) != null) && (flags & (1UL << ifFlag.Bit)) == 0) continue; 98 | object value = reader.ReadTLValue(field.FieldType); 99 | field.SetValue(obj, value); 100 | if (field.FieldType.IsEnum) 101 | if (field.Name == "flags") flags = (uint)value; 102 | else if (field.Name == "flags2") flags |= (ulong)(uint)value << 32; 103 | } 104 | return (IObject)obj; 105 | #endif 106 | } 107 | 108 | internal static void WriteTLValue(this BinaryWriter writer, object value, Type valueType) 109 | { 110 | if (value == null) 111 | { 112 | writer.WriteTLNull(valueType); 113 | return; 114 | } 115 | var type = value.GetType(); 116 | switch (Type.GetTypeCode(type)) 117 | { 118 | case TypeCode.Int32: writer.Write((int)value); break; 119 | case TypeCode.Int64: writer.Write((long)value); break; 120 | case TypeCode.UInt32: writer.Write((uint)value); break; 121 | case TypeCode.UInt64: writer.Write((ulong)value); break; 122 | case TypeCode.Double: writer.Write((double)value); break; 123 | case TypeCode.String: writer.WriteTLString((string)value); break; 124 | case TypeCode.Boolean: writer.Write((bool)value ? 0x997275B5 : 0xBC799737); break; 125 | case TypeCode.DateTime: writer.WriteTLStamp((DateTime)value); break; 126 | case TypeCode.Object: 127 | if (type.IsArray) 128 | if (value is byte[] bytes) 129 | writer.WriteTLBytes(bytes); 130 | else 131 | writer.WriteTLVector((Array)value); 132 | else if (value is IObject tlObject) 133 | WriteTLObject(writer, tlObject); 134 | else if (value is List<_Message> messages) 135 | writer.WriteTLMessages(messages); 136 | else if (value is Int128 int128) 137 | writer.Write(int128); 138 | else if (value is Int256 int256) 139 | writer.Write(int256); 140 | else if (type.IsEnum) // needed for Mono (enums in generic types are seen as TypeCode.Object) 141 | writer.Write((uint)value); 142 | else 143 | ShouldntBeHere(); 144 | break; 145 | default: 146 | ShouldntBeHere(); 147 | break; 148 | } 149 | } 150 | 151 | internal static object ReadTLValue(this BinaryReader reader, Type type) 152 | { 153 | switch (Type.GetTypeCode(type)) 154 | { 155 | case TypeCode.Int32: return reader.ReadInt32(); 156 | case TypeCode.Int64: return reader.ReadInt64(); 157 | case TypeCode.UInt32: return reader.ReadUInt32(); 158 | case TypeCode.UInt64: return reader.ReadUInt64(); 159 | case TypeCode.Double: return reader.ReadDouble(); 160 | case TypeCode.String: return reader.ReadTLString(); 161 | case TypeCode.DateTime: return reader.ReadTLStamp(); 162 | case TypeCode.Boolean: 163 | return reader.ReadUInt32() switch 164 | { 165 | 0x997275b5 => true, 166 | 0xbc799737 => false, 167 | Layer.RpcErrorCtor => reader.ReadTLObject(Layer.RpcErrorCtor), 168 | var value => throw new WTelegram.WTException($"Invalid boolean value #{value:x}") 169 | }; 170 | case TypeCode.Object: 171 | if (type.IsArray) 172 | { 173 | if (type == typeof(byte[])) 174 | return reader.ReadTLBytes(); 175 | else 176 | return reader.ReadTLVector(type); 177 | } 178 | else if (type == typeof(Int128)) 179 | return new Int128(reader); 180 | else if (type == typeof(Int256)) 181 | return new Int256(reader); 182 | else if (type == typeof(Dictionary)) 183 | return reader.ReadTLDictionary(); 184 | else if (type == typeof(Dictionary)) 185 | return reader.ReadTLDictionary(); 186 | else 187 | return reader.ReadTLObject(); 188 | default: 189 | ShouldntBeHere(); 190 | return null; 191 | } 192 | } 193 | 194 | internal static void WriteTLMessages(this BinaryWriter writer, List<_Message> messages) 195 | { 196 | writer.Write(messages.Count); 197 | foreach (var msg in messages) 198 | { 199 | writer.Write(msg.msg_id); 200 | writer.Write(msg.seq_no); 201 | var patchPos = writer.BaseStream.Position; 202 | writer.Write(0); // patched below 203 | if ((msg.seq_no & 1) != 0) 204 | WTelegram.Helpers.Log(1, $" → {msg.body.GetType().Name.TrimEnd('_'),-38} #{(short)msg.msg_id.GetHashCode():X4}"); 205 | else 206 | WTelegram.Helpers.Log(1, $" → {msg.body.GetType().Name.TrimEnd('_'),-38}"); 207 | writer.WriteTLObject(msg.body); 208 | writer.BaseStream.Position = patchPos; 209 | writer.Write((int)(writer.BaseStream.Length - patchPos - 4)); // patch bytes field 210 | writer.Seek(0, SeekOrigin.End); 211 | } 212 | } 213 | 214 | internal static void WriteTLVector(this BinaryWriter writer, Array array) 215 | { 216 | writer.Write(Layer.VectorCtor); 217 | if (array == null) { writer.Write(0); return; } 218 | int count = array.Length; 219 | writer.Write(count); 220 | var elementType = array.GetType().GetElementType(); 221 | for (int i = 0; i < count; i++) 222 | writer.WriteTLValue(array.GetValue(i), elementType); 223 | } 224 | 225 | internal static List ReadTLRawVector(this BinaryReader reader, uint ctorNb) 226 | { 227 | int count = reader.ReadInt32(); 228 | var list = new List(count); 229 | for (int i = 0; i < count; i++) 230 | list.Add((T)reader.ReadTLObject(ctorNb)); 231 | return list; 232 | } 233 | 234 | internal static T[] ReadTLVector(this BinaryReader reader) 235 | { 236 | var elementType = typeof(T); 237 | if (reader.ReadUInt32() is not Layer.VectorCtor and uint ctorNb) 238 | throw new WTelegram.WTException($"Cannot deserialize {elementType.Name}[] with ctor #{ctorNb:x}"); 239 | int count = reader.ReadInt32(); 240 | var array = new T[count]; 241 | for (int i = 0; i < count; i++) 242 | array[i] = (T)reader.ReadTLValue(elementType); 243 | return array; 244 | } 245 | 246 | internal static Array ReadTLVector(this BinaryReader reader, Type type) 247 | { 248 | var elementType = type.GetElementType(); 249 | uint ctorNb = reader.ReadUInt32(); 250 | if (ctorNb == Layer.VectorCtor) 251 | { 252 | int count = reader.ReadInt32(); 253 | Array array = (Array)Activator.CreateInstance(type, count); 254 | if (elementType.IsEnum) 255 | for (int i = 0; i < count; i++) 256 | array.SetValue(Enum.ToObject(elementType, reader.ReadTLValue(elementType)), i); 257 | else 258 | for (int i = 0; i < count; i++) 259 | array.SetValue(reader.ReadTLValue(elementType), i); 260 | return array; 261 | } 262 | else if (ctorNb < 1024 && !elementType.IsAbstract && elementType.GetCustomAttribute() is TLDefAttribute attr) 263 | { 264 | int count = (int)ctorNb; 265 | Array array = (Array)Activator.CreateInstance(type, count); 266 | for (int i = 0; i < count; i++) 267 | array.SetValue(reader.ReadTLObject(attr.CtorNb), i); 268 | return array; 269 | } 270 | else 271 | throw new WTelegram.WTException($"Cannot deserialize {type.Name} with ctor #{ctorNb:x}"); 272 | } 273 | 274 | internal static Dictionary ReadTLDictionary(this BinaryReader reader) where T : class, IPeerInfo 275 | { 276 | uint ctorNb = reader.ReadUInt32(); 277 | if (ctorNb != Layer.VectorCtor) 278 | throw new WTelegram.WTException($"Cannot deserialize Vector<{typeof(T).Name}> with ctor #{ctorNb:x}"); 279 | int count = reader.ReadInt32(); 280 | var dict = new Dictionary(count); 281 | for (int i = 0; i < count; i++) 282 | { 283 | var obj = reader.ReadTLObject(); 284 | if (obj is T value) dict[value.ID] = value; 285 | else if (obj is UserEmpty ue) dict[ue.id] = null; 286 | else throw new InvalidCastException($"ReadTLDictionary got '{obj?.GetType().Name}' instead of '{typeof(T).Name}'"); 287 | } 288 | return dict; 289 | } 290 | 291 | internal static void WriteTLStamp(this BinaryWriter writer, DateTime datetime) 292 | => writer.Write(datetime == DateTime.MaxValue ? int.MaxValue : (int)(datetime.ToUniversalTime().Ticks / 10000000 - 62135596800L)); 293 | 294 | internal static DateTime ReadTLStamp(this BinaryReader reader) 295 | { 296 | int unixstamp = reader.ReadInt32(); 297 | return unixstamp == int.MaxValue ? DateTime.MaxValue : new((unixstamp + 62135596800L) * 10000000, DateTimeKind.Utc); 298 | } 299 | 300 | internal static void WriteTLString(this BinaryWriter writer, string str) 301 | { 302 | if (str == null) 303 | writer.Write(0); 304 | else 305 | writer.WriteTLBytes(Encoding.UTF8.GetBytes(str)); 306 | } 307 | 308 | internal static string ReadTLString(this BinaryReader reader) 309 | => Encoding.UTF8.GetString(reader.ReadTLBytes()); 310 | 311 | internal static void WriteTLBytes(this BinaryWriter writer, byte[] bytes) 312 | { 313 | if (bytes == null) { writer.Write(0); return; } 314 | int length = bytes.Length; 315 | if (length < 254) 316 | writer.Write((byte)length); 317 | else 318 | { 319 | writer.Write(length << 8 | 254); 320 | length += 3; 321 | } 322 | writer.Write(bytes); 323 | while (++length % 4 != 0) writer.Write((byte)0); 324 | } 325 | 326 | internal static byte[] ReadTLBytes(this BinaryReader reader) 327 | { 328 | byte[] bytes; 329 | int length = reader.ReadByte(); 330 | if (length < 254) 331 | bytes = reader.ReadBytes(length); 332 | else 333 | { 334 | length = reader.ReadUInt16() + (reader.ReadByte() << 16); 335 | bytes = reader.ReadBytes(length); 336 | length += 3; 337 | } 338 | while (++length % 4 != 0) reader.ReadByte(); 339 | return bytes; 340 | } 341 | 342 | internal static void WriteTLNull(this BinaryWriter writer, Type type) 343 | { 344 | if (type == typeof(string)) { } 345 | else if (!type.IsArray) 346 | { 347 | writer.Write(Layer.Nullables.TryGetValue(type, out uint nullCtor) ? nullCtor : Layer.NullCtor); 348 | return; 349 | } 350 | else if (type != typeof(byte[])) 351 | writer.Write(Layer.VectorCtor); // not raw bytes but a vector => needs a VectorCtor 352 | writer.Write(0); // null arrays/strings are serialized as empty 353 | } 354 | 355 | internal static object ReadTLGzipped(this BinaryReader reader, Type type) 356 | { 357 | using var gzipReader = new BinaryReader(new GZipStream(new MemoryStream(reader.ReadTLBytes()), CompressionMode.Decompress)); 358 | return gzipReader.ReadTLValue(type); 359 | } 360 | 361 | internal static bool ReadTLBool(this BinaryReader reader) => reader.ReadUInt32() switch 362 | { 363 | 0x997275b5 => true, 364 | 0xbc799737 => false, 365 | var value => throw new WTelegram.WTException($"Invalid boolean value #{value:x}") 366 | }; 367 | 368 | #if DEBUG 369 | private static void ShouldntBeHere() => System.Diagnostics.Debugger.Break(); 370 | #else 371 | private static void ShouldntBeHere() => throw new NotImplementedException("You've reached an unexpected point in code"); 372 | #endif 373 | } 374 | 375 | public struct Int128 376 | { 377 | public byte[] raw; 378 | 379 | public Int128(BinaryReader reader) => raw = reader.ReadBytes(16); 380 | public Int128(RandomNumberGenerator rng) => rng.GetBytes(raw = new byte[16]); 381 | public static bool operator ==(Int128 left, Int128 right) { for (int i = 0; i < 16; i++) if (left.raw[i] != right.raw[i]) return false; return true; } 382 | public static bool operator !=(Int128 left, Int128 right) { for (int i = 0; i < 16; i++) if (left.raw[i] != right.raw[i]) return true; return false; } 383 | public override readonly bool Equals(object obj) => obj is Int128 other && this == other; 384 | public override readonly int GetHashCode() => BitConverter.ToInt32(raw, 0); 385 | public override readonly string ToString() => Convert.ToHexString(raw); 386 | public static implicit operator byte[](Int128 int128) => int128.raw; 387 | } 388 | 389 | public struct Int256 390 | { 391 | public byte[] raw; 392 | 393 | public Int256(BinaryReader reader) => raw = reader.ReadBytes(32); 394 | public Int256(RandomNumberGenerator rng) => rng.GetBytes(raw = new byte[32]); 395 | public static bool operator ==(Int256 left, Int256 right) { for (int i = 0; i < 32; i++) if (left.raw[i] != right.raw[i]) return false; return true; } 396 | public static bool operator !=(Int256 left, Int256 right) { for (int i = 0; i < 32; i++) if (left.raw[i] != right.raw[i]) return true; return false; } 397 | public override readonly bool Equals(object obj) => obj is Int256 other && this == other; 398 | public override readonly int GetHashCode() => BitConverter.ToInt32(raw, 0); 399 | public override readonly string ToString() => Convert.ToHexString(raw); 400 | public static implicit operator byte[](Int256 int256) => int256.raw; 401 | } 402 | 403 | public sealed partial class UpdateAffectedMessages : Update // auto-generated for OnOwnUpdates in case of such API call result 404 | { 405 | public long mbox_id; 406 | public int pts; 407 | public int pts_count; 408 | public override (long, int, int) GetMBox() => (mbox_id, pts, pts_count); 409 | #if MTPG 410 | public override void WriteTL(BinaryWriter writer) => throw new NotSupportedException(); 411 | #endif 412 | } 413 | 414 | // Below TL types are commented "parsed manually" from https://github.com/telegramdesktop/tdesktop/blob/dev/Telegram/Resources/tl/mtproto.tl 415 | 416 | [TLDef(0x7A19CB76)] //RSA_public_key#7a19cb76 n:bytes e:bytes = RSAPublicKey 417 | public sealed partial class RSAPublicKey : IObject 418 | { 419 | public byte[] n; 420 | public byte[] e; 421 | } 422 | 423 | [TLDef(0xF35C6D01)] //rpc_result#f35c6d01 req_msg_id:long result:Object = RpcResult 424 | public sealed partial class RpcResult : IObject 425 | { 426 | public long req_msg_id; 427 | public object result; 428 | } 429 | 430 | [TLDef(0x5BB8E511)] //message#5bb8e511 msg_id:long seqno:int bytes:int body:Object = Message 431 | public sealed partial class _Message(long msgId, int seqNo, IObject obj) : IObject 432 | { 433 | public long msg_id = msgId; 434 | public int seq_no = seqNo; 435 | public int bytes; 436 | public IObject body = obj; 437 | } 438 | 439 | [TLDef(0x73F1F8DC)] //msg_container#73f1f8dc messages:vector<%Message> = MessageContainer 440 | public sealed partial class MsgContainer : IObject { public List<_Message> messages; } 441 | [TLDef(0xE06046B2)] //msg_copy#e06046b2 orig_message:Message = MessageCopy 442 | public sealed partial class MsgCopy : IObject { public _Message orig_message; } 443 | 444 | [TLDef(0x3072CFA1)] //gzip_packed#3072cfa1 packed_data:bytes = Object 445 | public sealed partial class GzipPacked : IObject { public byte[] packed_data; } 446 | } 447 | -------------------------------------------------------------------------------- /src/TlsStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers.Binary; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Security.Cryptography; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | // necessary for .NET Standard 2.0 compilation: 10 | #pragma warning disable CA1835 // Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync' 11 | 12 | namespace WTelegram 13 | { 14 | internal sealed class TlsStream(Stream innerStream) : Helpers.IndirectStream(innerStream) 15 | { 16 | private int _tlsFrameleft; 17 | private readonly byte[] _tlsSendHeader = [0x17, 0x03, 0x03, 0, 0]; 18 | private readonly byte[] _tlsReadHeader = new byte[5]; 19 | static readonly byte[] TlsServerHello3 = [0x14, 0x03, 0x03, 0x00, 0x01, 0x01, 0x17, 0x03, 0x03]; 20 | static readonly byte[] TlsClientPrefix = [0x14, 0x03, 0x03, 0x00, 0x01, 0x01]; 21 | 22 | public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken ct) 23 | { 24 | if (_tlsFrameleft == 0) 25 | { 26 | if (await _innerStream.FullReadAsync(_tlsReadHeader, 5, ct) != 5) 27 | return 0; 28 | if (_tlsReadHeader[0] != 0x17 || _tlsReadHeader[1] != 0x03 || _tlsReadHeader[2] != 0x03) 29 | throw new WTException("Could not read frame data : Invalid TLS header"); 30 | _tlsFrameleft = (_tlsReadHeader[3] << 8) + _tlsReadHeader[4]; 31 | } 32 | var read = await _innerStream.ReadAsync(buffer, offset, Math.Min(count, _tlsFrameleft), ct); 33 | _tlsFrameleft -= read; 34 | return read; 35 | } 36 | 37 | public override async Task WriteAsync(byte[] buffer, int start, int count, CancellationToken ct) 38 | { 39 | for (int offset = 0; offset < count;) 40 | { 41 | int len = Math.Min(count - offset, 2878); 42 | _tlsSendHeader[3] = (byte)(len >> 8); 43 | _tlsSendHeader[4] = (byte)len; 44 | await _innerStream.WriteAsync(_tlsSendHeader, 0, _tlsSendHeader.Length, ct); 45 | await _innerStream.WriteAsync(buffer, start + offset, len, ct); 46 | offset += len; 47 | } 48 | } 49 | 50 | public static async Task HandshakeAsync(Stream stream, byte[] key, byte[] domain, CancellationToken ct) 51 | { 52 | var clientHello = TlsClientHello(key, domain); 53 | await stream.WriteAsync(clientHello, 0, clientHello.Length, ct); 54 | 55 | var part1 = new byte[5]; 56 | if (await stream.FullReadAsync(part1, 5, ct) == 5) 57 | if (part1[0] == 0x16 && part1[1] == 0x03 && part1[2] == 0x03) 58 | { 59 | var part2size = BinaryPrimitives.ReadUInt16BigEndian(part1.AsSpan(3)); 60 | var part23 = new byte[part2size + TlsServerHello3.Length + 2]; 61 | if (await stream.FullReadAsync(part23, part23.Length, ct) == part23.Length) 62 | if (TlsServerHello3.SequenceEqual(part23.Skip(part2size).Take(TlsServerHello3.Length))) 63 | { 64 | var part4size = BinaryPrimitives.ReadUInt16BigEndian(part23.AsSpan(part23.Length - 2)); 65 | var part4 = new byte[part4size]; 66 | if (await stream.FullReadAsync(part4, part4size, ct) == part4size) 67 | { 68 | var serverDigest = part23[6..38]; 69 | Array.Clear(part23, 6, 32); // clear server digest from received parts 70 | var hmc = new HMACSHA256(key); // hash the client digest + all received parts 71 | hmc.TransformBlock(clientHello, 11, 32, null, 0); 72 | hmc.TransformBlock(part1, 0, part1.Length, null, 0); 73 | hmc.TransformBlock(part23, 0, part23.Length, null, 0); 74 | hmc.TransformFinalBlock(part4, 0, part4.Length); 75 | if (serverDigest.SequenceEqual(hmc.Hash)) 76 | { 77 | Helpers.Log(2, "TLS Handshake succeeded"); 78 | await stream.WriteAsync(TlsClientPrefix, 0, TlsClientPrefix.Length, ct); 79 | return new TlsStream(stream); 80 | } 81 | } 82 | } 83 | } 84 | throw new WTException("TLS Handshake failed"); 85 | } 86 | 87 | static readonly byte[] TlsClientHello1 = [ // https://tls13.xargs.org/#client-hello/annotated 88 | 0x16, 0x03, 0x01, 0x02, 0x00, 0x01, 0x00, 0x01, 0xfc, 0x03, 0x03 ]; 89 | // digest[32] 90 | // 0x20 91 | // random[32] 92 | // 0x00, 0x20 93 | // grease(0) GREASE are two identical bytes ending with nibble 'A' 94 | static readonly byte[] TlsClientHello2 = [ 95 | 0x13, 0x01, 0x13, 0x02, 0x13, 0x03, 0xc0, 0x2b, 0xc0, 0x2f, 0xc0, 0x2c, 0xc0, 0x30, 0xcc, 0xa9, 96 | 0xcc, 0xa8, 0xc0, 0x13, 0xc0, 0x14, 0x00, 0x9c, 0x00, 0x9d, 0x00, 0x2f, 0x00, 0x35, 97 | 0x01, 0x00, 0x01, 0x93 ]; 98 | // grease(2) 99 | // 0x00, 0x00 100 | static readonly byte[] TlsClientHello3 = [ 101 | // 0x00, 0x00, len { len { 0x00 len { domain } } } len is 16-bit big-endian length of the following block of data 102 | 0x00, 0x05, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 103 | 0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, 0x4A, 0x4A/*=grease(4)*/, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 104 | 0x00, 0x0b, 0x00, 0x02, 0x01, 0x00, 105 | 0x00, 0x0d, 0x00, 0x12, 0x00, 0x10, 0x04, 0x03, 0x08, 0x04, 0x04, 0x01, 0x05, 0x03, 0x08, 0x05, 0x05, 0x01, 0x08, 0x06, 0x06, 0x01, 106 | 0x00, 0x10, 0x00, 0x0e, 0x00, 0x0c, 0x02, 0x68, 0x32, 0x08, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x31, 0x2e, 0x31, 107 | 0x00, 0x12, 0x00, 0x00, 108 | 0x00, 0x17, 0x00, 0x00, 109 | 0x00, 0x1b, 0x00, 0x03, 0x02, 0x00, 0x02, 110 | 0x00, 0x23, 0x00, 0x00, 111 | 0x00, 0x2b, 0x00, 0x07, 0x06, 0x6A, 0x6A/*=grease(6)*/, 0x03, 0x04, 0x03, 0x03, 112 | 0x00, 0x2d, 0x00, 0x02, 0x01, 0x01, 113 | 0x00, 0x33, 0x00, 0x2b, 0x00, 0x29, 0x4A, 0x4A/*=grease(4)*/, 0x00, 0x01, 0x00, 0x00, 0x1d, 0x00, 0x20, /* random[32] */ 114 | 0x44, 0x69, 0x00, 0x05, 0x00, 0x03, 0x02, 0x68, 0x32, 115 | 0xff, 0x01, 0x00, 0x01, 0x00, 116 | ]; 117 | // grease(3) 118 | static readonly byte[] TlsClientHello4 = [ 119 | 0x00, 0x01, 0x00, 0x00, 0x15 ]; 120 | // len { padding } padding with NUL bytes to reach 517 bytes 121 | 122 | static byte[] TlsClientHello(byte[] key, byte[] domain) 123 | { 124 | var greases = new byte[7]; 125 | Encryption.RNG.GetBytes(greases); 126 | for (int i = 0; i < 7; i++) greases[i] = (byte)((greases[i] & 0xF0) + 0x0A); 127 | if (greases[3] == greases[2]) greases[3] ^= 0x10; 128 | 129 | var buffer = new byte[517]; 130 | TlsClientHello1.CopyTo(buffer, 0); 131 | Encryption.RNG.GetBytes(buffer, 44, 32); 132 | buffer[43] = buffer[77] = 0x20; 133 | buffer[78] = buffer[79] = greases[0]; 134 | TlsClientHello2.CopyTo(buffer, 80); 135 | buffer[114] = buffer[115] = greases[2]; 136 | 137 | int dlen = domain.Length; 138 | var server_name = new byte[dlen + 9]; 139 | server_name[3] = (byte)(dlen + 5); 140 | server_name[5] = (byte)(dlen + 3); 141 | server_name[8] = (byte)dlen; 142 | domain.CopyTo(server_name, 9); 143 | 144 | var key_share = new byte[47]; 145 | Array.Copy(TlsClientHello3, 105, key_share, 0, 15); 146 | key_share[6] = key_share[7] = greases[4]; 147 | Encryption.RNG.GetBytes(key_share, 15, 32); // public key 148 | key_share[46] &= 0x7F; // must be positive 149 | 150 | var random = new Random(); 151 | var permutations = new ArraySegment[15]; 152 | for (var i = 0; i < permutations.Length; i++) 153 | { 154 | var j = random.Next(0, i + 1); 155 | if (i != j) permutations[i] = permutations[j]; 156 | permutations[j] = i switch 157 | { 158 | 0 => new(server_name), 159 | 1 => new(TlsClientHello3, 0, 9), 160 | 2 => PatchGrease(TlsClientHello3[9..23], 6, greases[4]), 161 | 3 => new(TlsClientHello3, 23, 6), 162 | 4 => new(TlsClientHello3, 29, 22), 163 | 5 => new(TlsClientHello3, 51, 18), 164 | 6 => new(TlsClientHello3, 69, 4), 165 | 7 => new(TlsClientHello3, 73, 4), 166 | 8 => new(TlsClientHello3, 77, 7), 167 | 9 => new(TlsClientHello3, 84, 4), 168 | 10 => PatchGrease(TlsClientHello3[88..99], 5, greases[6]), 169 | 11 => new(TlsClientHello3, 99, 6), 170 | 12 => new(key_share), 171 | 13 => new(TlsClientHello3, 120, 9), 172 | _ => new(TlsClientHello3, 129, 5), 173 | }; 174 | } 175 | int offset = 118; 176 | foreach (var perm in permutations) 177 | { 178 | Array.Copy(perm.Array, perm.Offset, buffer, offset, perm.Count); 179 | offset += perm.Count; 180 | } 181 | buffer[offset++] = buffer[offset++] = greases[3]; 182 | TlsClientHello4.CopyTo(buffer, offset); 183 | buffer[offset + 6] = (byte)(510 - offset); 184 | 185 | // patch-in digest with timestamp 186 | using var hmac = new HMACSHA256(key); 187 | var digest = hmac.ComputeHash(buffer); 188 | var stamp = BinaryPrimitives.ReadInt32LittleEndian(digest.AsSpan(28)); 189 | stamp ^= (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds(); 190 | BinaryPrimitives.WriteInt32LittleEndian(digest.AsSpan(28), stamp); 191 | digest.CopyTo(buffer, 11); 192 | return buffer; 193 | 194 | static ArraySegment PatchGrease(byte[] buffer, int offset, byte grease) 195 | { 196 | buffer[offset] = buffer[offset + 1] = grease; 197 | return new(buffer); 198 | } 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/UpdateManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.ComponentModel; 5 | using System.Linq; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using TL; 9 | 10 | namespace WTelegram 11 | { 12 | public class UpdateManager 13 | { 14 | /// Collected info about Users (only if using the default collector) 15 | public readonly Dictionary Users; 16 | /// Collected info about Chats (only if using the default collector) 17 | public readonly Dictionary Chats; 18 | /// Timout to detect lack of updates and force refetch them 19 | public TimeSpan InactivityThreshold { get; set; } = TimeSpan.FromMinutes(15); 20 | /// Logging callback (defaults to WTelegram.Helpers.Log ; can be null for performance) 21 | public Action Log { get; set; } = Helpers.Log; 22 | /// Current set of update states (for saving and later resume) 23 | public ImmutableDictionary State 24 | { 25 | get 26 | { 27 | _sem.Wait(); 28 | try { return _local.ToImmutableDictionary(); } 29 | finally { _sem.Release(); } 30 | } 31 | } 32 | 33 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006")] 34 | public sealed class MBoxState { public int pts { get; set; } public long access_hash { get; set; } } 35 | 36 | private readonly Client _client; 37 | private readonly Func _onUpdate; 38 | private readonly IPeerCollector _collector; 39 | private readonly bool _reentrant; 40 | private readonly TaskScheduler _scheduler; 41 | private readonly SemaphoreSlim _sem = new(1); 42 | private readonly List<(Update update, UpdatesBase updates, bool own, DateTime stamp)> _pending = []; 43 | private readonly Dictionary _local; // -2 for seq/date, -1 for qts, 0 for common pts, >0 for channel pts 44 | private const int L_SEQ = -2, L_QTS = -1, L_PTS = 0; 45 | private const long UndefinedSeqDate = 3155378975999999999L; // DateTime.MaxValue.Ticks 46 | private static readonly TimeSpan HalfSec = new(TimeSpan.TicksPerSecond / 2); 47 | private Task _recoveringGaps; 48 | private DateTime _lastUpdateStamp = DateTime.UtcNow; 49 | 50 | /// Manager ensuring that you receive Telegram updates in correct order, without missing any 51 | /// the WTelegram Client to manage 52 | /// Event to be called on sequential individual update 53 | /// (optional) Resume session by recovering all updates that occured since this state 54 | /// Custom users/chats collector. By default, those are collected in properties Users/Chats 55 | /// if your method can be called again even when last async call didn't return yet 56 | public UpdateManager(Client client, Func onUpdate, IDictionary state = null, IPeerCollector collector = null, bool reentrant = false) 57 | { 58 | _client = client; 59 | _onUpdate = onUpdate; 60 | _collector = collector ?? new Services.CollectorPeer(Users = [], Chats = []); 61 | _scheduler = SynchronizationContext.Current == null ? TaskScheduler.Current : TaskScheduler.FromCurrentSynchronizationContext(); 62 | 63 | if (state == null || state.Count < 3) 64 | _local = new() { [L_SEQ] = new() { access_hash = UndefinedSeqDate }, [L_QTS] = new(), [L_PTS] = new() }; 65 | else 66 | _local = state as Dictionary ?? new Dictionary(state); 67 | _reentrant = reentrant; 68 | client.OnOther += OnOther; 69 | client.OnUpdates += u => OnUpdates(u, false); 70 | client.OnOwnUpdates += u => OnUpdates(u, true); 71 | } 72 | 73 | private async Task OnOther(IObject obj) 74 | { 75 | switch (obj) 76 | { 77 | case Pong when DateTime.UtcNow - _lastUpdateStamp > InactivityThreshold: 78 | if (_local[L_PTS].pts != 0) await ResyncState(); 79 | break; 80 | case User user when user.flags.HasFlag(User.Flags.self): 81 | _collector.Collect([user]); 82 | goto newSession; 83 | case NewSessionCreated when _client.User != null: 84 | newSession: 85 | await Task.Delay(HalfSec); // let the opportunity to call DropPendingUpdates/StopResync before a big resync 86 | if (_local[L_PTS].pts != 0) await ResyncState(); 87 | else await ResyncState(await _client.Updates_GetState()); 88 | break; 89 | case Updates_State state: 90 | await ResyncState(state); 91 | break; 92 | } 93 | } 94 | 95 | private async Task ResyncState(Updates_State state = null) 96 | { 97 | if (state != null) state.qts = 0; // for some reason Updates_GetState returns an invalid qts, so better consider we have no qts. 98 | else state = new() { qts = int.MaxValue }; 99 | await _sem.WaitAsync(); 100 | try 101 | { 102 | var local = _local[L_PTS]; 103 | Log?.Invoke(2, $"Got Updates_State {local.pts}->{state.pts}, date={new DateTime(_local[L_SEQ].access_hash, DateTimeKind.Utc)}->{state.date}, seq={_local[L_SEQ].pts}->{state.seq}"); 104 | if (local.pts == 0 || local.pts >= state.pts && _local[L_SEQ].pts >= state.seq && _local[L_QTS].pts >= state.qts) 105 | await HandleDifference(null, null, state, null); 106 | else if (await GetDifference(L_PTS, state.pts, local)) 107 | await ApplyFilledGaps(); 108 | } 109 | finally { _sem.Release(); } 110 | } 111 | 112 | private async Task OnUpdates(UpdatesBase updates, bool own) 113 | { 114 | RaiseCollect(updates.Users, updates.Chats); 115 | await _sem.WaitAsync(); 116 | try 117 | { 118 | await HandleUpdates(updates, own); 119 | } 120 | finally { _sem.Release(); } 121 | } 122 | 123 | private async Task HandleUpdates(UpdatesBase updates, bool own) 124 | { 125 | var now = _lastUpdateStamp = DateTime.UtcNow; 126 | var updateList = updates.UpdateList; 127 | if (updates is UpdateShortSentMessage sent) 128 | updateList = [new UpdateNewMessage { pts = sent.pts, pts_count = sent.pts_count, message = new Message { 129 | flags = (Message.Flags)sent.flags, 130 | id = sent.id, date = sent.date, entities = sent.entities, media = sent.media, ttl_period = sent.ttl_period, 131 | } }]; 132 | else if (updates is UpdateShortMessage usm && !_collector.HasUser(usm.user_id)) 133 | RaiseCollect(await _client.Updates_GetDifference(usm.pts - usm.pts_count, usm.date, 0)); 134 | else if (updates is UpdateShortChatMessage uscm && (!_collector.HasUser(uscm.from_id) || !_collector.HasChat(uscm.chat_id))) 135 | RaiseCollect(await _client.Updates_GetDifference(uscm.pts - uscm.pts_count, uscm.date, 0)); 136 | 137 | bool ptsChanged = false, gotUPts = false; 138 | int seq = 0; 139 | try 140 | { 141 | if (updates is UpdatesTooLong) 142 | { 143 | var local_pts = _local[L_PTS]; 144 | ptsChanged = await GetDifference(L_PTS, local_pts.pts, local_pts); 145 | return; 146 | } 147 | foreach (var update in updateList) 148 | { 149 | if (update == null) continue; 150 | var (mbox_id, pts, pts_count) = update.GetMBox(); 151 | if (pts == 0) (mbox_id, pts, pts_count) = updates.GetMBox(); 152 | MBoxState local = null; 153 | if (pts != 0) 154 | { 155 | local = _local.GetOrCreate(mbox_id); 156 | if (mbox_id > 0 && local.access_hash == 0) 157 | if (updates.Chats.TryGetValue(mbox_id, out var chat) && chat is Channel channel && !channel.flags.HasFlag(Channel.Flags.min)) 158 | local.access_hash = channel.access_hash; 159 | var diff = local.pts + pts_count - pts; 160 | if (diff > 0 && pts_count != 0) // the update was already applied, and must be ignored. 161 | { 162 | Log?.Invoke(1, $"({mbox_id,10}, {local.pts,6}+{pts_count}->{pts,-6}) {update,-30} ignored {ExtendedLog(update)}"); 163 | continue; 164 | } 165 | if (diff < 0) // there's an update gap that must be filled. 166 | { 167 | Log?.Invoke(1, $"({mbox_id,10}, {local.pts,6}+{pts_count}->{pts,-6}) {update,-30} pending {ExtendedLog(update)}"); 168 | _pending.Add((update, updates, own, now + HalfSec)); 169 | _recoveringGaps ??= Task.Delay(HalfSec).ContinueWith(RecoverGaps, _scheduler); 170 | continue; 171 | } 172 | // the update can be applied. 173 | } 174 | Log?.Invoke(1, $"({mbox_id,10}, {local?.pts,6}+{pts_count}->{pts,-6}) {update,-30} applied {ExtendedLog(update)}"); 175 | if (mbox_id == L_SEQ && update is UpdatePtsChanged) gotUPts = true; 176 | if (pts_count > 0 && pts != 0) 177 | { 178 | ptsChanged = true; 179 | if (mbox_id == L_SEQ) 180 | seq = pts; 181 | else if (pts_count != 0) 182 | local.pts = pts; 183 | } 184 | if (!own) await RaiseUpdate(update); 185 | } 186 | } 187 | finally 188 | { 189 | if (seq > 0) // update local_seq & date after the updates were applied 190 | { 191 | var local_seq = _local[L_SEQ]; 192 | local_seq.pts = seq; 193 | local_seq.access_hash = updates.Date.Ticks; 194 | } 195 | if (gotUPts) ptsChanged = await GetDifference(L_PTS, _local[L_PTS].pts = 1, _local[L_PTS]); 196 | if (ptsChanged) await ApplyFilledGaps(); 197 | } 198 | } 199 | 200 | private async Task ApplyFilledGaps() 201 | { 202 | if (_pending.Count != 0) Log?.Invoke(2, $"Trying to apply {_pending.Count} pending updates after filled gaps"); 203 | int removed = 0; 204 | for (int i = 0; i < _pending.Count; ) 205 | { 206 | var (update, updates, own, _) = _pending[i]; 207 | var (mbox_id, pts, pts_count) = update.GetMBox(); 208 | if (pts == 0) (mbox_id, pts, pts_count) = updates.GetMBox(); 209 | var local = _local[mbox_id]; 210 | var diff = local.pts + pts_count - pts; 211 | if (diff < 0) 212 | ++i; // there's still a gap, skip it 213 | else 214 | { 215 | _pending.RemoveAt(i); 216 | ++removed; 217 | if (diff > 0) // the update was already applied, remove & ignore 218 | Log?.Invoke(1, $"({mbox_id,10}, {local.pts,6}+{pts_count}->{pts,-6}) {update,-30} obsolete {ExtendedLog(update)}"); 219 | else 220 | { 221 | Log?.Invoke(1, $"({mbox_id,10}, {local.pts,6}+{pts_count}->{pts,-6}) {update,-30} applied now {ExtendedLog(update)}"); 222 | // the update can be applied. 223 | local.pts = pts; 224 | if (mbox_id == L_SEQ) local.access_hash = updates.Date.Ticks; 225 | if (!own) await RaiseUpdate(update); 226 | i = 0; // rescan pending updates from start 227 | } 228 | } 229 | } 230 | return removed; 231 | } 232 | 233 | private async Task RecoverGaps(Task _) // https://corefork.telegram.org/api/updates#recovering-gaps 234 | { 235 | await _sem.WaitAsync(); 236 | try 237 | { 238 | _recoveringGaps = null; 239 | if (_pending.Count == 0) return; 240 | Log?.Invoke(2, $"Trying to recover gaps for {_pending.Count} pending updates"); 241 | var now = DateTime.UtcNow; 242 | while (_pending.Count != 0) 243 | { 244 | var (update, updates, own, stamp) = _pending[0]; 245 | if (stamp > now) 246 | { 247 | _recoveringGaps = Task.Delay(stamp - now).ContinueWith(RecoverGaps, _scheduler); 248 | return; 249 | } 250 | var (mbox_id, pts, pts_count) = update.GetMBox(); 251 | if (pts == 0) (mbox_id, pts, pts_count) = updates.GetMBox(); 252 | var local = _local[mbox_id]; 253 | bool getDiffSuccess = false; 254 | if (local.pts == 0) 255 | Log?.Invoke(2, $"({mbox_id,10}, new +{pts_count}->{pts,-6}) {update,-30} First appearance of MBox {ExtendedLog(update)}"); 256 | else if (local.access_hash == -1) // no valid access_hash for this channel, so just raise this update 257 | Log?.Invoke(3, $"({mbox_id,10}, {local.pts,6}+{pts_count}->{pts,-6}) {update,-30} No access_hash to recover {ExtendedLog(update)}"); 258 | else if (local.pts + pts_count - pts >= 0) 259 | getDiffSuccess = true; 260 | else 261 | { 262 | Log?.Invoke(1, $"({mbox_id,10}, {local.pts,6}+{pts_count}->{pts,-6}) {update,-30} Calling GetDifference {ExtendedLog(update)}"); 263 | getDiffSuccess = await GetDifference(mbox_id, pts, local); 264 | } 265 | if (!getDiffSuccess) // no getDiff => just raise received pending updates in order 266 | { 267 | local.pts = pts - pts_count; 268 | for (int i = 1; i < _pending.Count; i++) // find lowest pending pts-pts_count for this mbox 269 | { 270 | var pending = _pending[i]; 271 | var mbox = pending.update.GetMBox(); 272 | if (mbox.pts == 0) mbox = pending.updates.GetMBox(); 273 | if (mbox.mbox_id == mbox_id) local.pts = Math.Min(local.pts, mbox.pts - mbox.pts_count); 274 | } 275 | } 276 | 277 | if (await ApplyFilledGaps() == 0) 278 | { 279 | Log?.Invoke(3, $"({mbox_id,10}, {local.pts,6}+{pts_count}->{pts,-6}) {update,-30} forcibly removed!"); 280 | _pending.RemoveAt(0); 281 | local.pts = pts; 282 | if (!own) await RaiseUpdate(update); 283 | } 284 | } 285 | } 286 | finally { _sem.Release(); } 287 | } 288 | 289 | public async Task StopResync() 290 | { 291 | await _sem.WaitAsync(); 292 | try 293 | { 294 | foreach (var local in _local.Values) 295 | local.pts = 0; 296 | _pending.Clear(); 297 | } 298 | finally { _sem.Release(); } 299 | } 300 | 301 | private async Task GetInputChannel(long channel_id, MBoxState local) 302 | { 303 | if (channel_id <= 0) return null; 304 | if (local?.access_hash is not null and not 0) 305 | return new InputChannel(channel_id, local.access_hash); 306 | var inputChannel = new InputChannel(channel_id, 0); 307 | try 308 | { 309 | var mc = await _client.Channels_GetChannels(inputChannel); 310 | if (mc.chats.TryGetValue(channel_id, out var chat) && chat is Channel channel) 311 | inputChannel.access_hash = channel.access_hash; 312 | } 313 | catch (Exception) 314 | { 315 | inputChannel.access_hash = -1; // no valid access_hash available 316 | } 317 | local ??= _local[channel_id] = new(); 318 | local.access_hash = inputChannel.access_hash; 319 | return inputChannel; 320 | } 321 | 322 | private async Task GetDifference(long mbox_id, int expected_pts, MBoxState local) 323 | { 324 | try 325 | { 326 | moreDiffNeeded: 327 | if (mbox_id <= 0) 328 | { 329 | Log?.Invoke(0, $"Local states {string.Join(" ", _local.Select(l => $"{l.Key}:{l.Value.pts}"))}"); 330 | var local_seq = _local[L_SEQ]; 331 | var diff = await _client.Updates_GetDifference(_local[L_PTS].pts, qts: _local[L_QTS].pts, 332 | date: new DateTime(local_seq.access_hash, DateTimeKind.Utc)); 333 | Log?.Invoke(1, $"{diff.GetType().Name[8..]}: {diff.NewMessages.Length} msg, {diff.OtherUpdates.Length} upd, pts={diff.State?.pts}, date={diff.State?.date}, seq={diff.State?.seq}, msgIDs={string.Join(" ", diff.NewMessages.Select(m => m.ID))}"); 334 | switch (diff) 335 | { 336 | case Updates_Difference ud: 337 | await HandleDifference(ud.new_messages, ud.new_encrypted_messages, ud.state, 338 | new UpdatesCombined { updates = ud.other_updates, users = ud.users, chats = ud.chats, 339 | date = ud.state.date, seq_start = local_seq.pts + 1, seq = ud.state.seq }); 340 | break; 341 | case Updates_DifferenceSlice uds: 342 | await HandleDifference(uds.new_messages, uds.new_encrypted_messages, uds.intermediate_state, 343 | new UpdatesCombined { updates = uds.other_updates, users = uds.users, chats = uds.chats, 344 | date = uds.intermediate_state.date, seq_start = local_seq.pts + 1, seq = uds.intermediate_state.seq }); 345 | goto moreDiffNeeded; 346 | case Updates_DifferenceTooLong udtl: 347 | _local[L_PTS].pts = udtl.pts; 348 | goto moreDiffNeeded; 349 | case Updates_DifferenceEmpty ude: 350 | local_seq.pts = ude.seq; 351 | local_seq.access_hash = ude.date.Ticks; 352 | _lastUpdateStamp = DateTime.UtcNow; 353 | break; 354 | } 355 | } 356 | else 357 | { 358 | var channel = await GetInputChannel(mbox_id, local); 359 | if (channel.access_hash == -1) return false; 360 | try 361 | { 362 | var diff = await _client.Updates_GetChannelDifference(channel, null, local.pts); 363 | Log?.Invoke(1, $"{diff.GetType().Name[8..]}({mbox_id}): {diff.NewMessages.Length} msg, {diff.OtherUpdates.Length} upd, pts={diff.Pts}, msgIDs={string.Join(" ", diff.NewMessages.Select(m => m.ID))}"); 364 | switch (diff) 365 | { 366 | case Updates_ChannelDifference ucd: 367 | local.pts = ucd.pts; 368 | await HandleDifference(ucd.new_messages, null, null, 369 | new UpdatesCombined { updates = ucd.other_updates, users = ucd.users, chats = ucd.chats }); 370 | if (!ucd.flags.HasFlag(Updates_ChannelDifference.Flags.final)) goto moreDiffNeeded; 371 | break; 372 | case Updates_ChannelDifferenceTooLong ucdtl: 373 | if (ucdtl.dialog is Dialog dialog) local.pts = dialog.pts; 374 | await HandleDifference(ucdtl.messages, null, null, 375 | new UpdatesCombined { updates = null, users = ucdtl.users, chats = ucdtl.chats }); 376 | break; 377 | case Updates_ChannelDifferenceEmpty ucde: 378 | local.pts = ucde.pts; 379 | break; 380 | } 381 | } 382 | catch (RpcException ex) when (ex.Message is "CHANNEL_PRIVATE" or "CHANNEL_INVALID") 383 | { 384 | local.access_hash = -1; // access_hash is no longer valid 385 | throw; 386 | } 387 | } 388 | return true; 389 | } 390 | catch (Exception ex) 391 | { 392 | Log?.Invoke(4, $"GetDifference({mbox_id}, {local.pts}->{expected_pts}) raised {ex}"); 393 | if (ex.Message == "PERSISTENT_TIMESTAMP_INVALID") // oh boy, we're lost! 394 | if (mbox_id <= 0) 395 | await HandleDifference(null, null, await _client.Updates_GetState(), null); 396 | else if ((await _client.Channels_GetFullChannel(await GetInputChannel(mbox_id, local))).full_chat is ChannelFull full) 397 | local.pts = full.pts; 398 | } 399 | finally 400 | { 401 | if (local.pts < expected_pts) local.pts = expected_pts; 402 | } 403 | return false; 404 | } 405 | 406 | private async Task HandleDifference(MessageBase[] new_messages, EncryptedMessageBase[] enc_messages, Updates_State state, UpdatesCombined updates) 407 | { 408 | if (updates != null) 409 | RaiseCollect(updates.users, updates.chats); 410 | try 411 | { 412 | int updatesCount = updates?.updates.Length ?? 0; 413 | if (updatesCount != 0) 414 | for (int i = 0; i < updates.updates.Length; i++) 415 | { 416 | var update = updates.updates[i]; 417 | if (update is UpdateMessageID or UpdateStoryID) 418 | { 419 | await RaiseUpdate(update); 420 | updates.updates[i] = null; 421 | --updatesCount; 422 | } 423 | } 424 | if (new_messages?.Length > 0) 425 | { 426 | var update = state == null ? new UpdateNewChannelMessage() : new UpdateNewMessage() { pts = state.pts, pts_count = 1 }; 427 | foreach (var msg in new_messages) 428 | { 429 | if (_pending.Any(p => p is { own: true, update: UpdateNewMessage { message: { Peer.ID: var peer_id, ID: var msg_id } } } 430 | && peer_id == msg.Peer.ID && msg_id == msg.ID)) 431 | continue; 432 | update.message = msg; 433 | await RaiseUpdate(update); 434 | } 435 | } 436 | if (enc_messages?.Length > 0) 437 | { 438 | var update = new UpdateNewEncryptedMessage(); 439 | if (state != null) update.qts = state.qts; 440 | foreach (var msg in enc_messages) 441 | { 442 | if (_pending.Any(p => p is { own: true, update: UpdateNewEncryptedMessage { message: { ChatId: var chat_id, RandomId: var random_id } } } 443 | && chat_id == msg.ChatId && random_id == msg.RandomId)) 444 | continue; 445 | update.message = msg; 446 | await RaiseUpdate(update); 447 | } 448 | } 449 | if (updatesCount != 0) 450 | { 451 | // try to remove matching pending OwnUpdates from this updates list (starting from most-recent) 452 | for (int p = _pending.Count - 1, u = updates.updates.Length; p >= 0 && u > 0; p--) 453 | { 454 | if (_pending[p].own == false) continue; 455 | var updateP = _pending[p].update; 456 | var (mbox_idP, ptsP, pts_countP) = updateP.GetMBox(); 457 | if (ptsP == 0) (mbox_idP, ptsP, pts_countP) = _pending[p].updates.GetMBox(); 458 | Type updatePtype = null; 459 | while (--u >= 0) 460 | { 461 | var update = updates.updates[u]; 462 | if (update == null) continue; 463 | var (mbox_id, pts, pts_count) = update.GetMBox(); 464 | if (pts == 0) (mbox_id, pts, pts_count) = updates.GetMBox(); 465 | if (mbox_idP == mbox_id && ptsP <= pts) 466 | { 467 | updatePtype ??= updateP.GetType(); 468 | if (updatePtype == (update is UpdateDeleteMessages ? typeof(UpdateAffectedMessages) : update.GetType())) 469 | { 470 | updates.updates[u] = null; 471 | --updatesCount; 472 | break; 473 | } 474 | } 475 | } 476 | } 477 | if (updatesCount != 0) 478 | await HandleUpdates(updates, false); 479 | } 480 | } 481 | finally 482 | { 483 | if (state != null) 484 | { 485 | _local[L_PTS].pts = state.pts; 486 | _local[L_QTS].pts = state.qts; 487 | var local_seq = _local[L_SEQ]; 488 | local_seq.pts = state.seq; 489 | local_seq.access_hash = state.date.Ticks; 490 | } 491 | } 492 | } 493 | 494 | private void RaiseCollect(Updates_DifferenceBase diff) 495 | { 496 | if (diff is Updates_DifferenceSlice uds) 497 | RaiseCollect(uds.users, uds.chats); 498 | else if (diff is Updates_Difference ud) 499 | RaiseCollect(ud.users, ud.chats); 500 | } 501 | 502 | private void RaiseCollect(Dictionary users, Dictionary chats) 503 | { 504 | try 505 | { 506 | foreach (var chat in chats.Values) 507 | if (chat is Channel channel && !channel.flags.HasFlag(Channel.Flags.min)) 508 | if (_local.TryGetValue(channel.id, out var local)) 509 | local.access_hash = channel.access_hash; 510 | _collector.Collect(users.Values); 511 | _collector.Collect(chats.Values); 512 | } 513 | catch (Exception ex) 514 | { 515 | Log?.Invoke(4, $"Collect({users?.Count},{chats?.Count}) raised {ex}"); 516 | } 517 | } 518 | 519 | private async Task RaiseUpdate(Update update) 520 | { 521 | try 522 | { 523 | var task = _onUpdate(update); 524 | if (!_reentrant) await task; 525 | } 526 | catch (Exception ex) 527 | { 528 | Log?.Invoke(4, $"onUpdate({update?.GetType().Name}) raised {ex}"); 529 | } 530 | } 531 | 532 | private static string ExtendedLog(Update update) => update switch 533 | { 534 | UpdateNewMessage unm => $"| msgID={unm.message.ID}", 535 | UpdateEditMessage uem => $"| msgID={uem.message.ID}", 536 | UpdateDeleteMessages udm => $"| count={udm.messages.Length}", 537 | _ => null 538 | }; 539 | 540 | /// Load latest dialogs states, checking for missing updates 541 | /// structure returned by Messages_Get*Dialogs calls 542 | /// Dangerous! Load full history of unknown new channels as updates 543 | public async Task LoadDialogs(Messages_Dialogs dialogs, bool fullLoadNewChans = false) 544 | { 545 | await _sem.WaitAsync(); 546 | try 547 | { 548 | foreach (var dialog in dialogs.dialogs.OfType()) 549 | { 550 | if (dialog.peer is not PeerChannel pc) continue; 551 | var local = _local.GetOrCreate(pc.channel_id); 552 | if (dialogs.chats.TryGetValue(pc.channel_id, out var chat) && chat is Channel channel) 553 | local.access_hash = channel.access_hash; 554 | if (local.pts is 0) 555 | if (fullLoadNewChans) local.pts = 1; 556 | else local.pts = dialog.pts; 557 | if (local.pts < dialog.pts) 558 | { 559 | Log?.Invoke(1, $"LoadDialogs {pc.channel_id} has {local.pts} < {dialog.pts} ({dialog.folder_id})"); 560 | await GetDifference(pc.channel_id, dialog.pts, local); 561 | } 562 | } 563 | } 564 | finally { _sem.Release(); } 565 | } 566 | 567 | /// Save the current state of the manager to JSON file 568 | /// File path to write 569 | /// Note: This does not save the the content of collected Users/Chats dictionaries 570 | public void SaveState(string statePath) 571 | => System.IO.File.WriteAllText(statePath, System.Text.Json.JsonSerializer.Serialize(State, Helpers.JsonOptions)); 572 | public static Dictionary LoadState(string statePath) => !System.IO.File.Exists(statePath) ? null 573 | : System.Text.Json.JsonSerializer.Deserialize>(System.IO.File.ReadAllText(statePath), Helpers.JsonOptions); 574 | /// returns a or for the given Peer (only if using the default collector) 575 | public IPeerInfo UserOrChat(Peer peer) => peer?.UserOrChat(Users, Chats); 576 | } 577 | 578 | public interface IPeerCollector 579 | { 580 | void Collect(IEnumerable users); 581 | void Collect(IEnumerable chats); 582 | bool HasUser(long id); 583 | bool HasChat(long id); 584 | } 585 | } 586 | 587 | namespace TL 588 | { 589 | using WTelegram; 590 | 591 | [EditorBrowsable(EditorBrowsableState.Never)] 592 | public static class UpdateManagerExtensions 593 | { 594 | /// Manager ensuring that you receive Telegram updates in correct order, without missing any 595 | /// Event to be called on sequential individual update 596 | /// Resume session by recovering all updates that occured since the state saved in this file 597 | /// Custom users/chats collector. By default, those are collected in properties Users/Chats 598 | /// if your method can be called again even when last async call didn't return yet 599 | public static UpdateManager WithUpdateManager(this Client client, Func onUpdate, string statePath, IPeerCollector collector = null, bool reentrant = false) 600 | => new(client, onUpdate, UpdateManager.LoadState(statePath), collector, reentrant); 601 | 602 | /// Manager ensuring that you receive Telegram updates in correct order, without missing any 603 | /// Event to be called on sequential individual update 604 | /// (optional) Resume session by recovering all updates that occured since this state 605 | /// Custom users/chats collector. By default, those are collected in properties Users/Chats 606 | /// if your method can be called again even when last async call didn't return yet 607 | public static UpdateManager WithUpdateManager(this Client client, Func onUpdate, IDictionary state = null, IPeerCollector collector = null, bool reentrant = false) 608 | => new(client, onUpdate, state, collector, reentrant); 609 | } 610 | } -------------------------------------------------------------------------------- /src/WTelegramClient.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Library 5 | netstandard2.0;net5.0;net8.0 6 | latest 7 | WTelegram 8 | true 9 | true 10 | true 11 | snupkg 12 | true 13 | WTelegramClient 14 | 0.0.0 15 | Wizou 16 | Telegram Client API (MTProto) library written 100% in C# and .NET Standard | Latest API layer: 203 17 | 18 | Release Notes: 19 | $(ReleaseNotes.Replace("|", "%0D%0A").Replace(" - ","%0D%0A- ").Replace(" ", "%0D%0A%0D%0A")) 20 | Copyright © Olivier Marcoux 2021-2025 21 | MIT 22 | https://wiz0u.github.io/WTelegramClient 23 | logo.png 24 | true 25 | https://github.com/wiz0u/WTelegramClient.git 26 | git 27 | Telegram;MTProto;Client;Api;UserBot 28 | README.md 29 | $(ReleaseNotes.Replace("|", "%0D%0A").Replace(" - ","%0D%0A- ").Replace(" ", "%0D%0A%0D%0A")) 30 | NETSDK1138;CS0419;CS1573;CS1591 31 | TRACE;OBFUSCATION;MTPG 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/WTelegramClient.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31515.178 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WTelegramClient", "WTelegramClient.csproj", "{ABB3CB38-A5EC-4428-BB72-06C6BA99DD81}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {ABB3CB38-A5EC-4428-BB72-06C6BA99DD81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {ABB3CB38-A5EC-4428-BB72-06C6BA99DD81}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {ABB3CB38-A5EC-4428-BB72-06C6BA99DD81}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {ABB3CB38-A5EC-4428-BB72-06C6BA99DD81}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {5A8D162E-4943-4748-A046-A17BA8C10ACC} 24 | EndGlobalSection 25 | EndGlobal 26 | --------------------------------------------------------------------------------