mirror of
https://github.com/wiz0u/WTelegramClient.git
synced 2026-01-28 19:24:19 +01:00
Compare commits
286 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f531f4966 | ||
|
|
1912632722 | ||
|
|
9c839128bb | ||
|
|
d3ad4789a1 | ||
|
|
208ab626c1 | ||
|
|
bfc8e0e1b5 | ||
|
|
e923d65d53 | ||
|
|
4ad2f0a212 | ||
|
|
30bc536ebc | ||
|
|
d6fdcab440 | ||
|
|
9ec2f31f72 | ||
|
|
4ccfddd22e | ||
|
|
9693037ef2 | ||
|
|
40bcf69bfb | ||
|
|
4875f75774 | ||
|
|
2e95576be5 | ||
|
|
48d005b605 | ||
|
|
a5323eaa86 | ||
|
|
610d059b4c | ||
|
|
3f1036a559 | ||
|
|
4578dea3a3 | ||
|
|
253249e06a | ||
|
|
eb52dccfa7 | ||
|
|
a9bbdb9fc4 | ||
|
|
5f411d45f9 | ||
|
|
e16e39bfba | ||
|
|
7faa3873f8 | ||
|
|
d9e4b7cc0f | ||
|
|
30a982b0ac | ||
|
|
e9543a690b | ||
|
|
a8fa32dfd5 | ||
|
|
56ba15bc13 | ||
|
|
a3f41330b5 | ||
|
|
52d948af2a | ||
|
|
bdcf389ed2 | ||
|
|
4f7954db61 | ||
|
|
fa90e236e7 | ||
|
|
25990a8477 | ||
|
|
3ff1200068 | ||
|
|
04e043222e | ||
|
|
d49d620edd | ||
|
|
5358471574 | ||
|
|
8836f8372b | ||
|
|
6fb59286bd | ||
|
|
eaea2d051a | ||
|
|
6d238dc528 | ||
|
|
f495f59bc8 | ||
|
|
3f1d4eba92 | ||
|
|
e6a4b802e7 | ||
|
|
0867c044fa | ||
|
|
1ecd7047ef | ||
|
|
e67a688baa | ||
|
|
b626c6c644 | ||
|
|
1fab219ef6 | ||
|
|
edc6019f2e | ||
|
|
e6dde32538 | ||
|
|
e5953994a7 | ||
|
|
fdaaeb901a | ||
|
|
cbcb11e25e | ||
|
|
b0f336994b | ||
|
|
b5aea68f66 | ||
|
|
d8f7304067 | ||
|
|
2451068a71 | ||
|
|
d42c211f30 | ||
|
|
48a1406acc | ||
|
|
ccad6f9f31 | ||
|
|
ddc0a3caef | ||
|
|
b40557f78c | ||
|
|
9208d0b92c | ||
|
|
fc441121a3 | ||
|
|
9ad6220527 | ||
|
|
60dab9e6f3 | ||
|
|
18fc1b32af | ||
|
|
f1c1d0a6a2 | ||
|
|
33dd2d02d6 | ||
|
|
3d0de9ef9f | ||
|
|
0cc8a324cb | ||
|
|
6317fed8e0 | ||
|
|
322f5f132f | ||
|
|
e758e9136c | ||
|
|
cb8bcb5b8b | ||
|
|
9a4643ecef | ||
|
|
6354d0e8b7 | ||
|
|
75f5832ef6 | ||
|
|
e4d66925e3 | ||
|
|
3b52b3a200 | ||
|
|
73e4b6c871 | ||
|
|
62c105959c | ||
|
|
a19db86c1d | ||
|
|
4f9accdfc8 | ||
|
|
dcfd89c2a8 | ||
|
|
6cfa2a4da6 | ||
|
|
f0a649c147 | ||
|
|
b6cb62793c | ||
|
|
68a1c8650f | ||
|
|
62a691359b | ||
|
|
be7027b318 | ||
|
|
9fe6a9d74f | ||
|
|
9315913519 | ||
|
|
15b6346d2a | ||
|
|
aef4fb795a | ||
|
|
dea7691075 | ||
|
|
a28b984395 | ||
|
|
e5c6086e11 | ||
|
|
6afb0803bb | ||
|
|
8654f99d2b | ||
|
|
9712233c00 | ||
|
|
f7b3a56ce3 | ||
|
|
d5101b4f3b | ||
|
|
4946322045 | ||
|
|
7643ed5ba4 | ||
|
|
88c05287be | ||
|
|
b9299810b8 | ||
|
|
aa75b20820 | ||
|
|
1a00ae5a77 | ||
|
|
85cc404213 | ||
|
|
865c841bd6 | ||
|
|
c1a18a63c0 | ||
|
|
c8a0882587 | ||
|
|
37b8f6c054 | ||
|
|
7d388e6e75 | ||
|
|
4422aad6be | ||
|
|
3a7c5a0957 | ||
|
|
3c19be32c7 | ||
|
|
4381781af8 | ||
|
|
69f9e0c418 | ||
|
|
b6dbf9564f | ||
|
|
8228fede0f | ||
|
|
6dcce7f784 | ||
|
|
1d07039f04 | ||
|
|
8c271f50f6 | ||
|
|
741422e17f | ||
|
|
f8fab6c3e9 | ||
|
|
1a4b606216 | ||
|
|
fc08140995 | ||
|
|
f3ca76bb8f | ||
|
|
abeed476e7 | ||
|
|
210a3365e5 | ||
|
|
3d224afb23 | ||
|
|
270a7d89e6 | ||
|
|
3918e68945 | ||
|
|
55feb857d7 | ||
|
|
e2323092dc | ||
|
|
9fe1196606 | ||
|
|
018f535655 | ||
|
|
3304ba4bac | ||
|
|
39760f9306 | ||
|
|
fa83787e7f | ||
|
|
1d00dc2f9b | ||
|
|
659906ce01 | ||
|
|
d00725e234 | ||
|
|
0738adc3bf | ||
|
|
fd3bb731ba | ||
|
|
8eb5b29d97 | ||
|
|
d0460f296c | ||
|
|
b5ca3fcc0e | ||
|
|
345f10971b | ||
|
|
f958b4081d | ||
|
|
b9f3b2ebb4 | ||
|
|
33f239fc8e | ||
|
|
b9aad47c8e | ||
|
|
cf1c29e9ef | ||
|
|
1c298bfa83 | ||
|
|
6a1114ccd5 | ||
|
|
0312821068 | ||
|
|
125c1caeeb | ||
|
|
288bf7ccf7 | ||
|
|
a424219cb6 | ||
|
|
0ad7d696a5 | ||
|
|
6f3c8732ec | ||
|
|
a0841fee4c | ||
|
|
7dc578f91d | ||
|
|
91a8eab86a | ||
|
|
48a1322452 | ||
|
|
a17f13475d | ||
|
|
2d7a64fc2d | ||
|
|
e6fa972295 | ||
|
|
cce7a64cd9 | ||
|
|
5f51b1f77e | ||
|
|
7c65ce70ec | ||
|
|
d7ecd49b5c | ||
|
|
b6c98658db | ||
|
|
8f6e6440ba | ||
|
|
5febd2d27b | ||
|
|
807ee0cc9a | ||
|
|
5624eda8a0 | ||
|
|
35fab21493 | ||
|
|
9209d792a5 | ||
|
|
8f44137366 | ||
|
|
35f2f2530a | ||
|
|
6b44dbae8a | ||
|
|
3861255710 | ||
|
|
96ff52fab8 | ||
|
|
df2b2a7907 | ||
|
|
eb375824e4 | ||
|
|
136df62b8f | ||
|
|
4a1b2f5f91 | ||
|
|
9e92d3d814 | ||
|
|
c059ebf208 | ||
|
|
fb8d1c2d07 | ||
|
|
2b7868ee16 | ||
|
|
88e2f5d71e | ||
|
|
4b7205cb68 | ||
|
|
6d43da3d75 | ||
|
|
e954fdc628 | ||
|
|
609244a848 | ||
|
|
5dc8291972 | ||
|
|
fe26ee1b24 | ||
|
|
07c9118ccd | ||
|
|
d16fa21258 | ||
|
|
028afa4465 | ||
|
|
e4ed02c9a7 | ||
|
|
c60e4f9be0 | ||
|
|
392793b390 | ||
|
|
e7be5ac36f | ||
|
|
8f90e88074 | ||
|
|
38efb05923 | ||
|
|
e20d4d715c | ||
|
|
c872a51a31 | ||
|
|
2b65e8f1ed | ||
|
|
8a9f886b62 | ||
|
|
c631072ae4 | ||
|
|
1048af4dcf | ||
|
|
bcc62a1356 | ||
|
|
994e0deade | ||
|
|
472b10f155 | ||
|
|
d50ac0ba51 | ||
|
|
a31bcc3df6 | ||
|
|
abc9435625 | ||
|
|
befac0e781 | ||
|
|
a5d44bbd93 | ||
|
|
19d2c566eb | ||
|
|
2e41a1734a | ||
|
|
25bfacb3f1 | ||
|
|
fb8d694886 | ||
|
|
6f8c964f60 | ||
|
|
a69e032354 | ||
|
|
3603b9e2e3 | ||
|
|
2937d1c7b9 | ||
|
|
0825527860 | ||
|
|
e4c961a697 | ||
|
|
d054ae7eea | ||
|
|
fd48ea2974 | ||
|
|
fc54a93d00 | ||
|
|
660933d8c7 | ||
|
|
f4d435b807 | ||
|
|
de4b5e606d | ||
|
|
7feb4a40ec | ||
|
|
b282d2e007 | ||
|
|
d1e583cc86 | ||
|
|
131fd36106 | ||
|
|
e8b0bb9245 | ||
|
|
98f6a26b09 | ||
|
|
be0a357b9b | ||
|
|
30fc1cad8d | ||
|
|
c052ac2e2c | ||
|
|
30618cb316 | ||
|
|
753ac12eb1 | ||
|
|
5adde27f88 | ||
|
|
35c492de4f | ||
|
|
e33184fabb | ||
|
|
6a75f0a9d8 | ||
|
|
7c7a2a0625 | ||
|
|
ee2f0bfee1 | ||
|
|
acb7ff1d74 | ||
|
|
24a46206e1 | ||
|
|
22e64ea3ee | ||
|
|
d53dc5f07c | ||
|
|
0c6a8dd0a9 | ||
|
|
2f79411fce | ||
|
|
ddfa095f1a | ||
|
|
9af6404eff | ||
|
|
621a88bb9f | ||
|
|
81870b2de1 | ||
|
|
b307534078 | ||
|
|
2f3106fe69 | ||
|
|
fd9177f805 | ||
|
|
b63829393e | ||
|
|
c646cac738 | ||
|
|
22ea4c6de8 | ||
|
|
86796ebf0c | ||
|
|
5d0fd6452f | ||
|
|
514015639d | ||
|
|
7948dbd8e3 | ||
|
|
08a0802ed3 | ||
|
|
b5d7ef311d |
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
|
|
@ -1 +1,2 @@
|
|||
custom: ["http://t.me/WTelegramBot?start=donate"]
|
||||
github: wiz0u
|
||||
custom: ["https://www.buymeacoffee.com/wizou", "http://t.me/WTelegramClientBot?start=donate"]
|
||||
|
|
|
|||
77
.github/dev.yml
vendored
77
.github/dev.yml
vendored
|
|
@ -1,38 +1,63 @@
|
|||
pr: none
|
||||
pr: none
|
||||
trigger:
|
||||
- master
|
||||
branches:
|
||||
include: [ master ]
|
||||
paths:
|
||||
exclude: [ '.github', '*.md', 'Examples' ]
|
||||
|
||||
name: 3.2.2-dev.$(Rev:r)
|
||||
name: 4.3.2-dev.$(Rev:r)
|
||||
|
||||
pool:
|
||||
vmImage: ubuntu-latest
|
||||
|
||||
variables:
|
||||
buildConfiguration: 'Release'
|
||||
Release_Notes: $[replace(variables['Build.SourceVersionMessage'], '"', '''''')]
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Use .NET Core sdk'
|
||||
inputs:
|
||||
packageType: 'sdk'
|
||||
version: '6.0.x'
|
||||
includePreviewVersions: true
|
||||
stages:
|
||||
- stage: publish
|
||||
jobs:
|
||||
- job: publish
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Use .NET Core sdk'
|
||||
inputs:
|
||||
packageType: 'sdk'
|
||||
version: '9.x'
|
||||
includePreviewVersions: true
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
inputs:
|
||||
command: 'pack'
|
||||
packagesToPack: '**/*.csproj'
|
||||
includesymbols: true
|
||||
versioningScheme: 'byEnvVar'
|
||||
versionEnvVar: 'Build.BuildNumber'
|
||||
buildProperties: 'NoWarn="0419;1573;1591";Version=$(Build.BuildNumber);ContinuousIntegrationBuild=true;ReleaseNotes="$(Build.SourceVersionMessage)"'
|
||||
# buildProperties: 'NoWarn="0419;1573;1591";AllowedOutputExtensionsInPackageBuildOutputFolder=".dll;.xml;.pdb"'
|
||||
- task: DotNetCoreCLI@2
|
||||
inputs:
|
||||
command: 'pack'
|
||||
packagesToPack: 'src/WTelegramClient.csproj'
|
||||
includesymbols: true
|
||||
versioningScheme: 'byEnvVar'
|
||||
versionEnvVar: 'Build.BuildNumber'
|
||||
buildProperties: NoWarn="0419;1573;1591";ContinuousIntegrationBuild=true;Version=$(Build.BuildNumber);"ReleaseNotes=$(Release_Notes)"
|
||||
|
||||
- task: NuGetCommand@2
|
||||
inputs:
|
||||
command: 'push'
|
||||
packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg'
|
||||
publishPackageMetadata: true
|
||||
nuGetFeedType: 'external'
|
||||
publishFeedCredentials: 'nuget.org'
|
||||
- task: NuGetCommand@2
|
||||
inputs:
|
||||
command: 'push'
|
||||
packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg'
|
||||
publishPackageMetadata: true
|
||||
nuGetFeedType: 'external'
|
||||
publishFeedCredentials: 'nuget.org'
|
||||
|
||||
- stage: notify
|
||||
jobs:
|
||||
- job: notify
|
||||
pool:
|
||||
server
|
||||
steps:
|
||||
- task: InvokeRESTAPI@1
|
||||
inputs:
|
||||
connectionType: 'connectedServiceName'
|
||||
serviceConnection: 'Telegram Deploy Notice'
|
||||
method: 'POST'
|
||||
body: |
|
||||
{
|
||||
"status": "success",
|
||||
"complete": true,
|
||||
"message": "{ \"commitId\": \"$(Build.SourceVersion)\", \"buildNumber\": \"$(Build.BuildNumber)\", \"teamProjectName\": \"$(System.TeamProject)\", \"commitMessage\": \"$(Release_Notes)\" }"
|
||||
}
|
||||
waitForCompletion: 'false'
|
||||
|
|
|
|||
17
.github/release.yml
vendored
17
.github/release.yml
vendored
|
|
@ -1,13 +1,14 @@
|
|||
pr: none
|
||||
trigger: none
|
||||
|
||||
name: 3.2.$(Rev:r)
|
||||
name: 4.3.$(Rev:r)
|
||||
|
||||
pool:
|
||||
vmImage: ubuntu-latest
|
||||
|
||||
variables:
|
||||
buildConfiguration: 'Release'
|
||||
Release_Notes: $[replace(variables['releaseNotes'], '"', '''''')]
|
||||
|
||||
stages:
|
||||
- stage: publish
|
||||
|
|
@ -21,17 +22,17 @@ stages:
|
|||
displayName: 'Use .NET Core sdk'
|
||||
inputs:
|
||||
packageType: 'sdk'
|
||||
version: '6.0.x'
|
||||
version: '9.x'
|
||||
includePreviewVersions: true
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
inputs:
|
||||
command: 'pack'
|
||||
packagesToPack: '**/*.csproj'
|
||||
packagesToPack: 'src/WTelegramClient.csproj'
|
||||
includesymbols: true
|
||||
versioningScheme: 'byEnvVar'
|
||||
versionEnvVar: 'Build.BuildNumber'
|
||||
buildProperties: 'NoWarn="0419;1573;1591";Version=$(Build.BuildNumber);ContinuousIntegrationBuild=true;ReleaseNotes="$(ReleaseNotes)"'
|
||||
buildProperties: NoWarn="0419;1573;1591";ContinuousIntegrationBuild=true;Version=$(Build.BuildNumber);"ReleaseNotes=$(Release_Notes)"
|
||||
|
||||
- task: NuGetCommand@2
|
||||
inputs:
|
||||
|
|
@ -58,13 +59,9 @@ stages:
|
|||
serviceConnection: 'Telegram Deploy Notice'
|
||||
method: 'POST'
|
||||
body: |
|
||||
{
|
||||
{
|
||||
"status": "success",
|
||||
"complete": true,
|
||||
"message": "{
|
||||
\"commitId\": \"$(Build.SourceVersion)\",
|
||||
\"buildNumber\": \"$(Build.BuildNumber)\",
|
||||
\"teamProjectName\": \"$(system.TeamProject)\"
|
||||
}"
|
||||
"message": "{ \"commitId\": \"$(Build.SourceVersion)\", \"buildNumber\": \"$(Build.BuildNumber)\", \"teamProjectName\": \"$(System.TeamProject)\"}"
|
||||
}
|
||||
waitForCompletion: 'false'
|
||||
|
|
|
|||
24
.github/workflows/autolock.yml
vendored
Normal file
24
.github/workflows/autolock.yml
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
name: 'Auto-Lock Issues'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '17 2 * * 1'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
discussions: write
|
||||
|
||||
concurrency:
|
||||
group: lock-threads
|
||||
|
||||
jobs:
|
||||
action:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v5
|
||||
with:
|
||||
issue-inactive-days: '60'
|
||||
pr-inactive-days: '60'
|
||||
discussion-inactive-days: '60'
|
||||
68
.github/workflows/dev.yml
vendored
Normal file
68
.github/workflows/dev.yml
vendored
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
name: Dev build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths-ignore: [ '.**', 'Examples/**', '**.md' ]
|
||||
|
||||
env:
|
||||
PROJECT_PATH: src/WTelegramClient.csproj
|
||||
CONFIGURATION: Release
|
||||
RELEASE_NOTES: ${{ github.event.head_commit.message }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
permissions:
|
||||
id-token: write # enable GitHub OIDC token issuance for this job
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 30
|
||||
- name: Determine version
|
||||
run: |
|
||||
git fetch --depth=30 --tags
|
||||
DESCR_TAG=$(git describe --tags)
|
||||
COMMITS=${DESCR_TAG#*-}
|
||||
COMMITS=${COMMITS%-*}
|
||||
LAST_TAG=${DESCR_TAG%%-*}
|
||||
NEXT_VERSION=${LAST_TAG%.*}.$((${LAST_TAG##*.} + 1))-dev.$COMMITS
|
||||
RELEASE_VERSION=${{vars.RELEASE_VERSION}}-dev.$COMMITS
|
||||
if [[ "$RELEASE_VERSION" > "$NEXT_VERSION" ]] then VERSION=$RELEASE_VERSION; else VERSION=$NEXT_VERSION; fi
|
||||
echo Last tag: $LAST_TAG · Next version: $NEXT_VERSION · Release version: $RELEASE_VERSION · Build version: $VERSION
|
||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||
|
||||
# - name: Setup .NET
|
||||
# uses: actions/setup-dotnet@v4
|
||||
# with:
|
||||
# dotnet-version: 8.0.x
|
||||
- name: Pack
|
||||
run: |
|
||||
RELEASE_NOTES=${RELEASE_NOTES//$'\n'/%0A}
|
||||
RELEASE_NOTES=${RELEASE_NOTES//\"/%22}
|
||||
RELEASE_NOTES=${RELEASE_NOTES//,/%2C}
|
||||
RELEASE_NOTES=${RELEASE_NOTES//;/%3B}
|
||||
dotnet pack $PROJECT_PATH --configuration $CONFIGURATION -p:Version=$VERSION -p:ReleaseNotes="$RELEASE_NOTES" --output packages
|
||||
# - name: Upload artifact
|
||||
# uses: actions/upload-artifact@v4
|
||||
# with:
|
||||
# name: packages
|
||||
# path: packages/*.nupkg
|
||||
|
||||
- name: NuGet login (OIDC → temp API key)
|
||||
uses: NuGet/login@v1
|
||||
id: login
|
||||
with:
|
||||
user: ${{ secrets.NUGET_USER }}
|
||||
- name: Nuget push
|
||||
run: dotnet nuget push packages/*.nupkg --api-key ${{steps.login.outputs.NUGET_API_KEY}} --skip-duplicate --source https://api.nuget.org/v3/index.json
|
||||
|
||||
- name: Deployment Notification
|
||||
env:
|
||||
JSON: |
|
||||
{
|
||||
"status": "success", "complete": true, "commitMessage": ${{ toJSON(env.RELEASE_NOTES) }},
|
||||
"message": "{ \"commitId\": \"${{ github.sha }}\", \"buildNumber\": \"${{ env.VERSION }}\", \"repoName\": \"${{ github.repository }}\"}"
|
||||
}
|
||||
run: |
|
||||
curl -X POST -H "Content-Type: application/json" -d "$JSON" ${{ secrets.DEPLOYED_WEBHOOK }}
|
||||
82
.github/workflows/release.yml
vendored
Normal file
82
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
name: Release build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_notes:
|
||||
description: 'Release notes'
|
||||
required: true
|
||||
version:
|
||||
description: "Release version (leave empty for automatic versioning)"
|
||||
|
||||
run-name: '📌 Release build ${{ inputs.version }}'
|
||||
|
||||
env:
|
||||
PROJECT_PATH: src/WTelegramClient.csproj
|
||||
CONFIGURATION: Release
|
||||
RELEASE_NOTES: ${{ inputs.release_notes }}
|
||||
VERSION: ${{ inputs.version }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # For git tag
|
||||
id-token: write # enable GitHub OIDC token issuance for this job
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 100
|
||||
- name: Determine version
|
||||
if: ${{ env.VERSION == '' }}
|
||||
run: |
|
||||
git fetch --depth=100 --tags
|
||||
DESCR_TAG=$(git describe --tags)
|
||||
LAST_TAG=${DESCR_TAG%%-*}
|
||||
NEXT_VERSION=${LAST_TAG%.*}.$((${LAST_TAG##*.} + 1))
|
||||
RELEASE_VERSION=${{vars.RELEASE_VERSION}}
|
||||
if [[ "$RELEASE_VERSION" > "$NEXT_VERSION" ]] then VERSION=$RELEASE_VERSION; else VERSION=$NEXT_VERSION; fi
|
||||
echo Last tag: $LAST_TAG · Next version: $NEXT_VERSION · Release version: $RELEASE_VERSION · Build version: $VERSION
|
||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
- name: Pack
|
||||
run: |
|
||||
RELEASE_NOTES=${RELEASE_NOTES//|/%0A}
|
||||
RELEASE_NOTES=${RELEASE_NOTES// - /%0A- }
|
||||
RELEASE_NOTES=${RELEASE_NOTES// /%0A%0A}
|
||||
RELEASE_NOTES=${RELEASE_NOTES//$'\n'/%0A}
|
||||
RELEASE_NOTES=${RELEASE_NOTES//\"/%22}
|
||||
RELEASE_NOTES=${RELEASE_NOTES//,/%2C}
|
||||
RELEASE_NOTES=${RELEASE_NOTES//;/%3B}
|
||||
dotnet pack $PROJECT_PATH --configuration $CONFIGURATION -p:Version=$VERSION -p:ReleaseNotes="$RELEASE_NOTES" --output packages
|
||||
# - name: Upload artifact
|
||||
# uses: actions/upload-artifact@v4
|
||||
# with:
|
||||
# name: packages
|
||||
# path: packages/*.nupkg
|
||||
|
||||
- name: NuGet login (OIDC → temp API key)
|
||||
uses: NuGet/login@v1
|
||||
id: login
|
||||
with:
|
||||
user: ${{ secrets.NUGET_USER }}
|
||||
- name: Nuget push
|
||||
run: dotnet nuget push packages/*.nupkg --api-key ${{steps.login.outputs.NUGET_API_KEY}} --skip-duplicate --source https://api.nuget.org/v3/index.json
|
||||
|
||||
- name: Git tag
|
||||
run: |
|
||||
git tag $VERSION
|
||||
git push --tags
|
||||
- name: Deployment Notification
|
||||
env:
|
||||
JSON: |
|
||||
{
|
||||
"status": "success", "complete": true, "commitMessage": ${{ toJSON(env.RELEASE_NOTES) }},
|
||||
"message": "{ \"commitId\": \"${{ github.sha }}\", \"buildNumber\": \"${{ env.VERSION }}\", \"repoName\": \"${{ github.repository }}\"}"
|
||||
}
|
||||
run: |
|
||||
curl -X POST -H "Content-Type: application/json" -d "$JSON" ${{ secrets.DEPLOYED_WEBHOOK }}
|
||||
29
.github/workflows/telegram-api.yml
vendored
Normal file
29
.github/workflows/telegram-api.yml
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
name: 'Telegram API issues'
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
action:
|
||||
if: contains(github.event.issue.labels.*.name, 'telegram api')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/support-requests@v4
|
||||
with:
|
||||
support-label: 'telegram api'
|
||||
issue-comment: >
|
||||
Please note that **Github issues** should be used only for problems with the library code itself.
|
||||
|
||||
|
||||
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).
|
||||
|
||||
WTelegramClient covers 100% of the API and let you do anything you can do in an official client.
|
||||
|
||||
|
||||
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.
|
||||
close-issue: true
|
||||
issue-close-reason: 'not planned'
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -3,6 +3,8 @@
|
|||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||
|
||||
launchSettings.json
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
|
|
|
|||
133
EXAMPLES.md
133
EXAMPLES.md
|
|
@ -11,7 +11,7 @@ await client.LoginUserIfNeeded();
|
|||
```
|
||||
|
||||
In this case, environment variables are used for configuration so make sure to
|
||||
go to your **Project Properties > Debug > Environment variables**
|
||||
go to your **Project Properties > Debug > Launch Profiles > Environment variables**
|
||||
and add at least these variables with adequate values: **api_id, api_hash, phone_number**
|
||||
|
||||
Remember that these are just simple example codes that you should adjust to your needs.
|
||||
|
|
@ -39,7 +39,7 @@ WTelegram.Helpers.Log += (lvl, str) => System.Diagnostics.Debug.WriteLine(str);
|
|||
// • In ASP.NET service, you will typically send logs to an ILogger:
|
||||
WTelegram.Helpers.Log = (lvl, str) => _logger.Log((LogLevel)lvl, str);
|
||||
|
||||
// • Disable logging (THIS IS NOT RECOMMENDED as you won't be able to diagnose any upcoming problem):
|
||||
// • Disable logging (⛔️𝗗𝗢𝗡'𝗧 𝗗𝗢 𝗧𝗛𝗜𝗦 as you won't be able to diagnose any upcoming problem):
|
||||
WTelegram.Helpers.Log = (lvl, str) => { };
|
||||
```
|
||||
|
||||
|
|
@ -95,7 +95,7 @@ foreach (Dialog dialog in dialogs.dialogs)
|
|||
|
||||
Notes:
|
||||
- The lists returned by Messages_GetAllDialogs contains the `access_hash` for those chats and users.
|
||||
- See also the `Main` method in [Examples/Program_ListenUpdates.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ListenUpdates.cs?ts=4#L20).
|
||||
- See also the `Main` method in [Examples/Program_ListenUpdates.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ListenUpdates.cs?ts=4#L18).
|
||||
- To retrieve the dialog information about a specific [peer](README.md#terminology), use `client.Messages_GetPeerDialogs(inputPeer)`
|
||||
|
||||
<a name="list-chats"></a>
|
||||
|
|
@ -114,7 +114,7 @@ Notes:
|
|||
- The list returned by Messages_GetAllChats contains the `access_hash` for those chats. Read [FAQ #4](FAQ.md#access-hash) about this.
|
||||
- If a basic chat group has been migrated to a supergroup, you may find both the old `Chat` and a `Channel` with different IDs in the `chats.chats` result,
|
||||
but the old `Chat` will be marked with flag [deactivated] and should not be used anymore. See [Terminology in ReadMe](README.md#terminology).
|
||||
- You can find a longer version of this method call in [Examples/Program_GetAllChats.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_GetAllChats.cs?ts=4#L32)
|
||||
- You can find a longer version of this method call in [Examples/Program_GetAllChats.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_GetAllChats.cs?ts=4#L31)
|
||||
|
||||
<a name="list-members"></a>
|
||||
## List the members from a chat
|
||||
|
|
@ -160,10 +160,10 @@ foreach (var participant in participants.participants) // This is the better way
|
|||
*Note: It is not possible to list only the Deleted Accounts. Those will be automatically removed by Telegram from your group after a while*
|
||||
|
||||
<a name="history"></a>
|
||||
## Fetch all messages (history) from a chat
|
||||
## Fetch all messages (history) from a chat/user
|
||||
```csharp
|
||||
var chats = await client.Messages_GetAllChats();
|
||||
InputPeer peer = chats.chats[1234567890]; // the chat we want
|
||||
InputPeer peer = chats.chats[1234567890]; // the chat (or User) we want
|
||||
for (int offset_id = 0; ;)
|
||||
{
|
||||
var messages = await client.Messages_GetHistory(peer, offset_id);
|
||||
|
|
@ -180,25 +180,27 @@ for (int offset_id = 0; ;)
|
|||
}
|
||||
```
|
||||
Notes:
|
||||
- `peer` can also be a User, obtained through methods like [`Messages_GetAllDialogs`](#list-dialogs)
|
||||
- To stop at a specific msg ID, use Messages_GetHistory `min_id` argument. For example, `min_id: dialog.read_inbox_max_id`
|
||||
- To mark the message history as read, use: `await client.ReadHistory(peer);`
|
||||
|
||||
<a name="updates"></a>
|
||||
## Monitor all Telegram events happening for the user
|
||||
|
||||
This is done through the `client.OnUpdate` callback event.
|
||||
Your event handler implementation can either return `Task.CompletedTask` or be an `async Task` method.
|
||||
This is done through the `client.OnUpdates` callback event, or via the [UpdateManager class](FAQ.md#manager) that simplifies the handling of updates.
|
||||
|
||||
See [Examples/Program_ListenUpdates.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ListenUpdates.cs?ts=4#L23).
|
||||
See [Examples/Program_ListenUpdates.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ListenUpdates.cs?ts=4#L21).
|
||||
|
||||
<a name="monitor-msg"></a>
|
||||
## Monitor new messages being posted in chats in real-time
|
||||
|
||||
You have to handle `client.OnUpdate` events containing an `UpdateNewMessage`.
|
||||
You have to handle update events containing an `UpdateNewMessage`.
|
||||
This can be done through the `client.OnUpdates` callback event, or via the [UpdateManager class](FAQ.md#manager) that simplifies the handling of updates.
|
||||
|
||||
See the `DisplayMessage` method in [Examples/Program_ListenUpdates.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ListenUpdates.cs?ts=4#L23).
|
||||
See the `HandleMessage` method in [Examples/Program_ListenUpdates.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ListenUpdates.cs?ts=4#L21).
|
||||
|
||||
You can filter specific chats the message are posted in, by looking at the `Message.peer_id` field.
|
||||
You can filter specific chats the message are posted in, by looking at the `Message.peer_id` field.
|
||||
See also [explanation below](#message-user) to extract user/chat info from messages.
|
||||
|
||||
<a name="download"></a>
|
||||
## Downloading photos, medias, files
|
||||
|
|
@ -206,7 +208,9 @@ You can filter specific chats the message are posted in, by looking at the `Mess
|
|||
This is done using the helper method `client.DownloadFileAsync(file, outputStream)`
|
||||
that simplifies the download of a photo/document/file once you get a reference to its location *(through updates or API calls)*.
|
||||
|
||||
See [Examples/Program_DownloadSavedMedia.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_DownloadSavedMedia.cs?ts=4#L31) that download all media files you forward to yourself (Saved Messages)
|
||||
See [Examples/Program_DownloadSavedMedia.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_DownloadSavedMedia.cs?ts=4#L28) that download all media files you forward to yourself (Saved Messages)
|
||||
|
||||
_Note: To abort an ongoing download, you can throw an exception via the `progress` callback argument. Example: `(t,s) => ct.ThrowIfCancellationRequested()`_
|
||||
|
||||
<a name="upload"></a>
|
||||
## Upload a media file and post it with caption to a chat
|
||||
|
|
@ -220,6 +224,41 @@ var inputFile = await client.UploadFileAsync(Filepath);
|
|||
await client.SendMediaAsync(peer, "Here is the photo", inputFile);
|
||||
```
|
||||
|
||||
<a name="upload-video"></a>
|
||||
## Upload a streamable video with optional custom thumbnail
|
||||
```csharp
|
||||
var chats = await client.Messages_GetAllChats();
|
||||
InputPeer peer = chats.chats[1234567890]; // the chat we want
|
||||
const string videoPath = @"C:\...\video.mp4";
|
||||
const string thumbnailPath = @"C:\...\thumbnail.jpg";
|
||||
|
||||
// Extract video information using FFMpegCore or similar library
|
||||
var mediaInfo = await FFmpeg.GetMediaInfo(videoPath);
|
||||
var videoStream = mediaInfo.VideoStreams.FirstOrDefault();
|
||||
int width = videoStream?.Width ?? 0;
|
||||
int height = videoStream?.Height ?? 0;
|
||||
int duration = (int)mediaInfo.Duration.TotalSeconds;
|
||||
|
||||
// Upload video file
|
||||
var inputFile = await Client.UploadFileAsync(videoPath);
|
||||
|
||||
// Prepare InputMedia structure with video attributes
|
||||
var media = new InputMediaUploadedDocument(inputFile, "video/mp4",
|
||||
new DocumentAttributeVideo { w = width, h = height, duration = duration,
|
||||
flags = DocumentAttributeVideo.Flags.supports_streaming });
|
||||
if (thumbnailPath != null)
|
||||
{
|
||||
// upload custom thumbnail and complete InputMedia structure
|
||||
var inputThumb = await client.UploadFileAsync(thumbnailPath);
|
||||
media.thumb = inputThumb;
|
||||
media.flags |= InputMediaUploadedDocument.Flags.has_thumb;
|
||||
}
|
||||
|
||||
// Send the media message
|
||||
await client.SendMessageAsync(peer, "caption", media);
|
||||
```
|
||||
*Note: This example requires FFMpegCore NuGet package for video metadata extraction. You can also manually set width, height, and duration if you know the video properties.*
|
||||
|
||||
<a name="album"></a>
|
||||
## Send a grouped media album using photos from various sources
|
||||
```csharp
|
||||
|
|
@ -236,6 +275,7 @@ var inputMedias = new List<InputMedia>
|
|||
photoFromTelegram, // PhotoBase has implicit conversion to InputMediaPhoto
|
||||
new InputMediaUploadedPhoto { file = uploadedFile },
|
||||
new InputMediaPhotoExternal { url = photoUrl },
|
||||
//or Document, InputMediaDocument, InputMediaUploadedDocument, InputMediaDocumentExternal...
|
||||
};
|
||||
await client.SendAlbumAsync(InputPeer.Self, inputMedias, "My first album");
|
||||
```
|
||||
|
|
@ -252,10 +292,10 @@ var history = await client.Messages_GetHistory(from_chat, limit: 1);
|
|||
var msg = history.Messages[0] as Message; // last message of source chat
|
||||
|
||||
// • Forward the message (only the source message id is necessary)
|
||||
await client.Messages_ForwardMessages(from_chat, new[] { msg.ID }, new[] { WTelegram.Helpers.RandomLong() }, to_chat);
|
||||
await client.ForwardMessagesAsync(from_chat, [msg.ID], to_chat);
|
||||
|
||||
// • Copy the message without the "Forwarded" header (only the source message id is necessary)
|
||||
await client.Messages_ForwardMessages(from_chat, new[] { msg.ID }, new[] { WTelegram.Helpers.RandomLong() }, to_chat, drop_author: true);
|
||||
await client.ForwardMessagesAsync(from_chat, [msg.ID], to_chat, drop_author: true);
|
||||
|
||||
// • Alternative solution to copy the message (the full message is needed)
|
||||
await client.SendMessageAsync(to_chat, msg.message, msg.media?.ToInputMedia(), entities: msg.entities);
|
||||
|
|
@ -269,6 +309,7 @@ InputPeer peer = chats.chats[1234567890]; // the chat we want
|
|||
DateTime when = DateTime.UtcNow.AddMinutes(3);
|
||||
await client.SendMessageAsync(peer, "This will be posted in 3 minutes", schedule_date: when);
|
||||
```
|
||||
*Note: Make sure your computer clock is synchronized with Internet time*
|
||||
|
||||
<a name="fun"></a>
|
||||
## Fun with stickers, GIFs, dice, and animated emojies
|
||||
|
|
@ -300,7 +341,7 @@ await client.SendMediaAsync(InputPeer.Self, null, inputFile);
|
|||
|
||||
// • Send a random dice/game-of-chance effect from the list of available "dices", see https://core.telegram.org/api/dice
|
||||
var appConfig = await client.Help_GetAppConfig();
|
||||
var emojies_send_dice = appConfig["emojies_send_dice"] as string[];
|
||||
var emojies_send_dice = appConfig.config["emojies_send_dice"] as string[];
|
||||
var dice_emoji = emojies_send_dice[new Random().Next(emojies_send_dice.Length)];
|
||||
var diceMsg = await client.SendMessageAsync(InputPeer.Self, null, new InputMediaDice { emoticon = dice_emoji });
|
||||
Console.WriteLine("Dice result:" + ((MessageMediaDice)diceMsg.media).value);
|
||||
|
|
@ -373,8 +414,9 @@ var chatInvite = await client.Messages_CheckChatInvite("HASH"); // optional: get
|
|||
await client.Messages_ImportChatInvite("HASH"); // join the channel/group
|
||||
// Note: This works also with HASH invite links from public channel/group
|
||||
```
|
||||
Note: `CheckChatInvite` can return [3 different types of invitation object](https://corefork.telegram.org/type/ChatInvite)
|
||||
`CheckChatInvite` can return [3 different types of invitation object](https://corefork.telegram.org/type/ChatInvite)
|
||||
|
||||
You can also use helper methods `AnalyzeInviteLink` and `GetMessageByLink` to more easily fetch information from links.
|
||||
<a name="add-members"></a>
|
||||
## Add/Invite/Remove someone in a chat
|
||||
```csharp
|
||||
|
|
@ -384,9 +426,9 @@ var chat = chats.chats[1234567890]; // the target chat
|
|||
After the above code, once you [have obtained](FAQ.md#access-hash) an `InputUser` or `User`, you can:
|
||||
```csharp
|
||||
// • Directly add the user to a Chat/Channel/group:
|
||||
await client.AddChatUser(chat, user);
|
||||
// You may get exception USER_PRIVACY_RESTRICTED if the user has denied the right to be added to a chat
|
||||
// or exception USER_NOT_MUTUAL_CONTACT if the user left the chat previously and you want to add him again
|
||||
var miu = await client.AddChatUser(chat, user);
|
||||
// You may get exception USER_NOT_MUTUAL_CONTACT if the user left the chat previously and you want to add him again
|
||||
// or a result with miu.missing_invitees listing users that denied the right to be added to a chat
|
||||
|
||||
// • Obtain the main invite link for the chat, and send it to the user:
|
||||
var mcf = await client.GetFullChat(chat);
|
||||
|
|
@ -448,16 +490,17 @@ finally
|
|||
## Collect Users/Chats description structures and access hash
|
||||
|
||||
Many API calls return a structure with a `users` and a `chats` field at the root of the structure.
|
||||
This is also the case for updates passed to `client.OnUpdate`.
|
||||
This is also the case for updates passed to `client.OnUpdates`.
|
||||
|
||||
These two dictionaries give details about the various users/chats that will be typically referenced in subobjects deeper in the structure,
|
||||
typically in the form of a `Peer` object or a `user_id` field.
|
||||
These two dictionaries give details *(including access hash)* about the various users/chats that will be typically referenced in subobjects deeper in the structure,
|
||||
typically in the form of a `Peer` object or a `user_id`/`chat_id` field.
|
||||
|
||||
In such case, the root structure inherits the `IPeerResolver` interface, and you can use the `UserOrChat(peer)` method to resolve a `Peer`
|
||||
into either a `User` or `ChatBase` (`Chat`,`Channel`...) description structure *(depending on the kind of peer it was describing)*
|
||||
|
||||
You can also use the `CollectUsersChats` helper method to collect these 2 fields into 2 aggregate dictionaries to remember details
|
||||
*(including access hashes)* about all the users/chats you've encountered so far.
|
||||
This method also helps dealing with [incomplete `min` structures](https://core.telegram.org/api/min).
|
||||
|
||||
Example of usage:
|
||||
```csharp
|
||||
|
|
@ -467,10 +510,9 @@ private Dictionary<long, ChatBase> _chats = new();
|
|||
var dialogs = await client.Messages_GetAllDialogs();
|
||||
dialogs.CollectUsersChats(_users, _chats);
|
||||
|
||||
private async Task OnUpdate(IObject arg)
|
||||
private async Task OnUpdates(UpdatesBase updates)
|
||||
{
|
||||
if (arg is not UpdatesBase updates) return;
|
||||
updates.CollectUsersChats(_users, _chats);
|
||||
updates.CollectUsersChats(_users, _chats);
|
||||
...
|
||||
}
|
||||
|
||||
|
|
@ -482,6 +524,41 @@ else if (firstPeer is ChatBase firstChat) Console.WriteLine($"First dialog is {f
|
|||
|
||||
*Note: If you need to save/restore those dictionaries between runs of your program, it's up to you to serialize their content to disk*
|
||||
|
||||
<a name="message-user"></a>
|
||||
## Get chat and user info from a message
|
||||
First, you should read the above [section about collecting users/chats](#collect-users-chats), and the [FAQ about dealing with IDs](FAQ.md#access-hash).
|
||||
|
||||
A message contains those two fields/properties:
|
||||
- `peer_id`/`Peer` that identify WHERE the message was posted
|
||||
- `from_id`/`From` that identify WHO posted the message (it can be `null` in some case of anonymous posting)
|
||||
|
||||
These two fields derive from class `Peer` and can be of type `PeerChat`, `PeerChannel` or `PeerUser` depending on the nature of WHERE & WHO
|
||||
(private chat with a user? message posted BY a channel IN a chat? ...)
|
||||
|
||||
> ✳️ It is recommended that you use the [UpdateManager class](FAQ.md#manager), as it handles automatically all of the details below, and you just need to use `Manager.UserOrChat(peer)` or Manager.Users/Chats dictionaries
|
||||
|
||||
The root structure where you obtained the message (typically `UpdatesBase` or `Messages_MessagesBase`) inherits from `IPeerResolver`.
|
||||
This allows you to call `.UserOrChat(peer)` on the root structure, in order to resolve those fields into a `User` class, or a `ChatBase`-derived class
|
||||
(typically `Chat` or `Channel`) which will give you details about the peer, instead of just the ID, and can be implicitly converted to `InputPeer`.
|
||||
|
||||
However, in some case _(typically when dealing with updates)_, Telegram might choose to not include details about a peer
|
||||
because it expects you to already know about it (`UserOrChat` returns `null`).
|
||||
That's why you should collect users/chats details each time you're dealing with Updates or other API results inheriting from `IPeerResolver`,
|
||||
and use the collected dictionaries to find details about users/chats
|
||||
([see previous section](#collect-users-chats) and [Program_ListenUpdates.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ListenUpdates.cs?ts=4#L21) example)
|
||||
|
||||
And finally, it may happen that you receive updates of type `UpdateShortMessage` or `UpdateShortChatMessage` with totally unknown peers (even in your collected dictionaries).
|
||||
In this case, [Telegram recommends](https://core.telegram.org/api/updates#recovering-gaps) that you use the [`Updates_GetDifference`](https://corefork.telegram.org/method/updates.getDifference) method to retrieve the full information associated with the short message.
|
||||
Here is an example showing how to deal with `UpdateShortMessage`: (same for `UpdateShortChatMessage`)
|
||||
```csharp
|
||||
if (updates is UpdateShortMessage usm && !_users.ContainsKey(usm.user_id))
|
||||
{
|
||||
var fullDiff = await client.Updates_GetDifference(usm.pts - usm.pts_count, usm.date, 0)
|
||||
fullDiff.CollectUsersChats(_users, _chats);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
<a name="proxy"></a>
|
||||
## Use a proxy or MTProxy to connect to Telegram
|
||||
SOCKS/HTTPS proxies can be used through the `client.TcpHandler` delegate and a proxy library like [StarkSoftProxy](https://www.nuget.org/packages/StarkSoftProxy/) or [xNetStandard](https://www.nuget.org/packages/xNetStandard/):
|
||||
|
|
@ -502,7 +579,7 @@ using var client = new WTelegram.Client(Environment.GetEnvironmentVariable);
|
|||
client.MTProxyUrl = "https://t.me/proxy?server=...&port=...&secret=...";
|
||||
await client.LoginUserIfNeeded();
|
||||
```
|
||||
You can find a list of working MTProxies in channels like [@ProxyMTProto](https://t.me/ProxyMTProto) or [@MTProxyT](https://t.me/MTProxyT) *(right-click the "Connect" buttons)*
|
||||
You can find a list of working MTProxies in channels like [@ProxyMTProto](https://t.me/s/ProxyMTProto) or [@MTProxyT](https://t.me/s/MTProxyT) *(right-click the "Connect" buttons)*
|
||||
If your Telegram client is already connected to such MTPROTO proxy, you can also export its URL by clicking on the shield button  and then **⋮** > **Share**
|
||||
|
||||
*Note: WTelegramClient always uses transport obfuscation when connecting to Telegram servers, even without MTProxy*
|
||||
|
|
@ -544,4 +621,4 @@ This can be done easily using the helper class `WTelegram.SecretChats` offering
|
|||
You can view a full working example at [Examples/Program_SecretChats.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_SecretChats.cs?ts=4#L11).
|
||||
|
||||
Secret Chats have been tested successfully with Telegram Android & iOS official clients.
|
||||
You can also check our [FAQ for more implementation details](FAQ.md#14-secret-chats-implementation-details).
|
||||
You can also check our [FAQ for more implementation details](FAQ.md#14-secret-chats-implementation-details).
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,7 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TL;
|
||||
|
||||
|
|
@ -9,19 +8,20 @@ namespace WTelegramClientTest
|
|||
{
|
||||
static class Program_DownloadSavedMedia
|
||||
{
|
||||
// go to Project Properties > Debug > Environment variables and add at least these: api_id, api_hash, phone_number
|
||||
// go to Project Properties > Debug > Launch Profiles > Environment variables and add at least these: api_id, api_hash, phone_number
|
||||
static async Task Main(string[] _)
|
||||
{
|
||||
Console.WriteLine("The program will download photos/medias from messages you send/forward to yourself (Saved Messages)");
|
||||
using var client = new WTelegram.Client(Environment.GetEnvironmentVariable);
|
||||
var cts = new CancellationTokenSource();
|
||||
await using var client = new WTelegram.Client(Environment.GetEnvironmentVariable);
|
||||
var user = await client.LoginUserIfNeeded();
|
||||
client.OnUpdate += Client_OnUpdate;
|
||||
client.OnUpdates += Client_OnUpdates;
|
||||
Console.ReadKey();
|
||||
cts.Cancel();
|
||||
|
||||
async Task Client_OnUpdate(IObject arg)
|
||||
async Task Client_OnUpdates(UpdatesBase updates)
|
||||
{
|
||||
if (arg is not Updates { updates: var updates } upd) return;
|
||||
foreach (var update in updates)
|
||||
foreach (var update in updates.UpdateList)
|
||||
{
|
||||
if (update is not UpdateNewMessage { message: Message message })
|
||||
continue; // if it's not about a new message, ignore the update
|
||||
|
|
@ -34,7 +34,7 @@ namespace WTelegramClientTest
|
|||
filename ??= $"{document.id}.{document.mime_type[(document.mime_type.IndexOf('/') + 1)..]}";
|
||||
Console.WriteLine("Downloading " + filename);
|
||||
using var fileStream = File.Create(filename);
|
||||
await client.DownloadFileAsync(document, fileStream);
|
||||
await client.DownloadFileAsync(document, fileStream, progress: (p, t) => cts.Token.ThrowIfCancellationRequested());
|
||||
Console.WriteLine("Download finished");
|
||||
}
|
||||
else if (message.media is MessageMediaPhoto { photo: Photo photo })
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using TL;
|
||||
|
||||
|
|
@ -10,7 +9,7 @@ namespace WTelegramClientTest
|
|||
// This code is similar to what you should have obtained if you followed the README introduction
|
||||
// I've just added a few comments to explain further what's going on
|
||||
|
||||
// go to Project Properties > Debug > Environment variables and add at least these: api_id, api_hash, phone_number
|
||||
// go to Project Properties > Debug > Launch Profiles > Environment variables and add at least these: api_id, api_hash, phone_number
|
||||
static string Config(string what)
|
||||
{
|
||||
if (what == "api_id") return Environment.GetEnvironmentVariable("api_id");
|
||||
|
|
@ -25,7 +24,7 @@ namespace WTelegramClientTest
|
|||
|
||||
static async Task Main(string[] _)
|
||||
{
|
||||
using var client = new WTelegram.Client(Config);
|
||||
await using var client = new WTelegram.Client(Config);
|
||||
var user = await client.LoginUserIfNeeded();
|
||||
Console.WriteLine($"We are logged-in as {user.username ?? user.first_name + " " + user.last_name} (id {user.id})");
|
||||
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ namespace WTelegramClientTest
|
|||
{
|
||||
static WTelegram.Client Client;
|
||||
static User My;
|
||||
static readonly Dictionary<long, User> Users = new();
|
||||
static readonly Dictionary<long, ChatBase> Chats = new();
|
||||
static readonly Dictionary<long, User> Users = [];
|
||||
static readonly Dictionary<long, ChatBase> Chats = [];
|
||||
|
||||
// See steps at the end of this file to setup required Environment variables
|
||||
static async Task Main(string[] _)
|
||||
|
|
@ -28,9 +28,9 @@ namespace WTelegramClientTest
|
|||
var store = new PostgreStore(Environment.GetEnvironmentVariable("DATABASE_URL"), Environment.GetEnvironmentVariable("SESSION_NAME"));
|
||||
// if DB does not contain a session yet, client will be run in interactive mode
|
||||
Client = new WTelegram.Client(store.Length == 0 ? null : Environment.GetEnvironmentVariable, store);
|
||||
using (Client)
|
||||
await using (Client)
|
||||
{
|
||||
Client.OnUpdate += Client_OnUpdate;
|
||||
Client.OnUpdates += Client_OnUpdates;
|
||||
My = await Client.LoginUserIfNeeded();
|
||||
Console.WriteLine($"We are logged-in as {My.username ?? My.first_name + " " + My.last_name} (id {My.id})");
|
||||
var dialogs = await Client.Messages_GetAllDialogs();
|
||||
|
|
@ -39,9 +39,8 @@ namespace WTelegramClientTest
|
|||
}
|
||||
}
|
||||
|
||||
private static async Task Client_OnUpdate(IObject arg)
|
||||
private static async Task Client_OnUpdates(UpdatesBase updates)
|
||||
{
|
||||
if (arg is not UpdatesBase updates) return;
|
||||
updates.CollectUsersChats(Users, Chats);
|
||||
foreach (var update in updates.UpdateList)
|
||||
{
|
||||
|
|
@ -63,10 +62,8 @@ namespace WTelegramClientTest
|
|||
{
|
||||
private readonly NpgsqlConnection _sql;
|
||||
private readonly string _sessionName;
|
||||
private byte[] _data;
|
||||
private int _dataLen;
|
||||
private DateTime _lastWrite;
|
||||
private Task _delayedWrite;
|
||||
private readonly byte[] _data;
|
||||
private readonly int _dataLen;
|
||||
|
||||
/// <param name="databaseUrl">Heroku DB URL of the form "postgres://user:password@host:port/database"</param>
|
||||
/// <param name="sessionName">Entry name for the session data in the WTelegram_sessions table (default: "Heroku")</param>
|
||||
|
|
@ -76,7 +73,7 @@ namespace WTelegramClientTest
|
|||
var parts = databaseUrl.Split(':', '/', '@');
|
||||
_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;");
|
||||
_sql.Open();
|
||||
using (var create = new NpgsqlCommand($"CREATE TABLE IF NOT EXISTS WTelegram_sessions (name text NOT NULL PRIMARY KEY, data bytea)", _sql))
|
||||
using (var create = new NpgsqlCommand("CREATE TABLE IF NOT EXISTS WTelegram_sessions (name text NOT NULL PRIMARY KEY, data bytea)", _sql))
|
||||
create.ExecuteNonQuery();
|
||||
using var cmd = new NpgsqlCommand($"SELECT data FROM WTelegram_sessions WHERE name = '{_sessionName}'", _sql);
|
||||
using var rdr = cmd.ExecuteReader();
|
||||
|
|
@ -86,7 +83,6 @@ namespace WTelegramClientTest
|
|||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
_delayedWrite?.Wait();
|
||||
_sql.Dispose();
|
||||
}
|
||||
|
||||
|
|
@ -98,18 +94,9 @@ namespace WTelegramClientTest
|
|||
|
||||
public override void Write(byte[] buffer, int offset, int count) // Write call and buffer modifications are done within a lock()
|
||||
{
|
||||
_data = buffer; _dataLen = count;
|
||||
if (_delayedWrite != null) return;
|
||||
var left = 1000 - (int)(DateTime.UtcNow - _lastWrite).TotalMilliseconds;
|
||||
if (left < 0)
|
||||
{
|
||||
using var cmd = new NpgsqlCommand($"INSERT INTO WTelegram_sessions (name, data) VALUES ('{_sessionName}', @data) ON CONFLICT (name) DO UPDATE SET data = EXCLUDED.data", _sql);
|
||||
cmd.Parameters.AddWithValue("data", count == buffer.Length ? buffer : buffer[offset..(offset + count)]);
|
||||
cmd.ExecuteNonQuery();
|
||||
_lastWrite = DateTime.UtcNow;
|
||||
}
|
||||
else // delay writings for a full second
|
||||
_delayedWrite = Task.Delay(left).ContinueWith(t => { lock (this) { _delayedWrite = null; Write(_data, 0, _dataLen); } });
|
||||
using var cmd = new NpgsqlCommand($"INSERT INTO WTelegram_sessions (name, data) VALUES ('{_sessionName}', @data) ON CONFLICT (name) DO UPDATE SET data = EXCLUDED.data", _sql);
|
||||
cmd.Parameters.AddWithValue("data", count == buffer.Length ? buffer : buffer[offset..(offset + count)]);
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
public override long Length => _dataLen;
|
||||
|
|
@ -135,7 +122,7 @@ HOW TO USE AND DEPLOY THIS EXAMPLE HEROKU USERBOT:
|
|||
- In Visual Studio, Clone the Heroku git repository and add some standard .gitignore .gitattributes files
|
||||
- In this repository folder, create a new .NET Console project with this Program.cs file
|
||||
- Add these Nuget packages: WTelegramClient and Npgsql
|
||||
- In Project properties > Debug > Environment variables, configure the same values for DATABASE_URL, api_hash, phone_number
|
||||
- In Project properties > Debug > Launch Profiles > Environment variables, configure the same values for DATABASE_URL, api_hash, phone_number
|
||||
- Run the project in Visual Studio. The first time, it should ask you interactively for elements to complete the connection
|
||||
- On the following runs, the PostgreSQL database contains the session data and it should connect automatically
|
||||
- You can test the userbot by sending him "Ping" in private message (or saved messages). It should respond with "Pong"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using TL;
|
||||
|
||||
|
|
@ -8,56 +7,54 @@ namespace WTelegramClientTest
|
|||
static class Program_ListenUpdates
|
||||
{
|
||||
static WTelegram.Client Client;
|
||||
static WTelegram.UpdateManager Manager;
|
||||
static User My;
|
||||
static readonly Dictionary<long, User> Users = new();
|
||||
static readonly Dictionary<long, ChatBase> Chats = new();
|
||||
|
||||
// go to Project Properties > Debug > Environment variables and add at least these: api_id, api_hash, phone_number
|
||||
// go to Project Properties > Debug > Launch Profiles > Environment variables and add at least these: api_id, api_hash, phone_number
|
||||
static async Task Main(string[] _)
|
||||
{
|
||||
Console.WriteLine("The program will display updates received for the logged-in user. Press any key to terminate");
|
||||
WTelegram.Helpers.Log = (l, s) => System.Diagnostics.Debug.WriteLine(s);
|
||||
Client = new WTelegram.Client(Environment.GetEnvironmentVariable);
|
||||
using (Client)
|
||||
await using (Client)
|
||||
{
|
||||
Client.OnUpdate += Client_OnUpdate;
|
||||
Manager = Client.WithUpdateManager(Client_OnUpdate/*, "Updates.state"*/);
|
||||
My = await Client.LoginUserIfNeeded();
|
||||
Users[My.id] = My;
|
||||
// Note: on login, Telegram may sends a bunch of updates/messages that happened in the past and were not acknowledged
|
||||
Console.WriteLine($"We are logged-in as {My.username ?? My.first_name + " " + My.last_name} (id {My.id})");
|
||||
// We collect all infos about the users/chats so that updates can be printed with their names
|
||||
var dialogs = await Client.Messages_GetAllDialogs(); // dialogs = groups/channels/users
|
||||
dialogs.CollectUsersChats(Users, Chats);
|
||||
dialogs.CollectUsersChats(Manager.Users, Manager.Chats);
|
||||
|
||||
Console.ReadKey();
|
||||
}
|
||||
} // WTelegram.Client gets disposed when exiting this scope
|
||||
|
||||
//Manager.SaveState("Updates.state"); // if you want to resume missed updates on the next run (see WithUpdateManager above)
|
||||
}
|
||||
|
||||
// if not using async/await, we could just return Task.CompletedTask
|
||||
private static async Task Client_OnUpdate(IObject arg)
|
||||
private static async Task Client_OnUpdate(Update update)
|
||||
{
|
||||
if (arg is not UpdatesBase updates) return;
|
||||
updates.CollectUsersChats(Users, Chats);
|
||||
foreach (var update in updates.UpdateList)
|
||||
switch (update)
|
||||
{
|
||||
case UpdateNewMessage unm: await DisplayMessage(unm.message); break;
|
||||
case UpdateEditMessage uem: await DisplayMessage(uem.message, true); break;
|
||||
// Note: UpdateNewChannelMessage and UpdateEditChannelMessage are also handled by above cases
|
||||
case UpdateDeleteChannelMessages udcm: Console.WriteLine($"{udcm.messages.Length} message(s) deleted in {Chat(udcm.channel_id)}"); break;
|
||||
case UpdateDeleteMessages udm: Console.WriteLine($"{udm.messages.Length} message(s) deleted"); break;
|
||||
case UpdateUserTyping uut: Console.WriteLine($"{User(uut.user_id)} is {uut.action}"); break;
|
||||
case UpdateChatUserTyping ucut: Console.WriteLine($"{Peer(ucut.from_id)} is {ucut.action} in {Chat(ucut.chat_id)}"); break;
|
||||
case UpdateChannelUserTyping ucut2: Console.WriteLine($"{Peer(ucut2.from_id)} is {ucut2.action} in {Chat(ucut2.channel_id)}"); break;
|
||||
case UpdateChatParticipants { participants: ChatParticipants cp }: Console.WriteLine($"{cp.participants.Length} participants in {Chat(cp.chat_id)}"); break;
|
||||
case UpdateUserStatus uus: Console.WriteLine($"{User(uus.user_id)} is now {uus.status.GetType().Name[10..]}"); break;
|
||||
case UpdateUserName uun: Console.WriteLine($"{User(uun.user_id)} has changed profile name: {uun.first_name} {uun.last_name}"); break;
|
||||
case UpdateUser uu: Console.WriteLine($"{User(uu.user_id)} has changed infos/photo"); break;
|
||||
default: Console.WriteLine(update.GetType().Name); break; // there are much more update types than the above example cases
|
||||
}
|
||||
switch (update)
|
||||
{
|
||||
case UpdateNewMessage unm: await HandleMessage(unm.message); break;
|
||||
case UpdateEditMessage uem: await HandleMessage(uem.message, true); break;
|
||||
// Note: UpdateNewChannelMessage and UpdateEditChannelMessage are also handled by above cases
|
||||
case UpdateDeleteChannelMessages udcm: Console.WriteLine($"{udcm.messages.Length} message(s) deleted in {Chat(udcm.channel_id)}"); break;
|
||||
case UpdateDeleteMessages udm: Console.WriteLine($"{udm.messages.Length} message(s) deleted"); break;
|
||||
case UpdateUserTyping uut: Console.WriteLine($"{User(uut.user_id)} is {uut.action}"); break;
|
||||
case UpdateChatUserTyping ucut: Console.WriteLine($"{Peer(ucut.from_id)} is {ucut.action} in {Chat(ucut.chat_id)}"); break;
|
||||
case UpdateChannelUserTyping ucut2: Console.WriteLine($"{Peer(ucut2.from_id)} is {ucut2.action} in {Chat(ucut2.channel_id)}"); break;
|
||||
case UpdateChatParticipants { participants: ChatParticipants cp }: Console.WriteLine($"{cp.participants.Length} participants in {Chat(cp.chat_id)}"); break;
|
||||
case UpdateUserStatus uus: Console.WriteLine($"{User(uus.user_id)} is now {uus.status.GetType().Name[10..]}"); break;
|
||||
case UpdateUserName uun: Console.WriteLine($"{User(uun.user_id)} has changed profile name: {uun.first_name} {uun.last_name}"); break;
|
||||
case UpdateUser uu: Console.WriteLine($"{User(uu.user_id)} has changed infos/photo"); break;
|
||||
default: Console.WriteLine(update.GetType().Name); break; // there are much more update types than the above example cases
|
||||
}
|
||||
}
|
||||
|
||||
// in this example method, we're not using async/await, so we just return Task.CompletedTask
|
||||
private static Task DisplayMessage(MessageBase messageBase, bool edit = false)
|
||||
private static Task HandleMessage(MessageBase messageBase, bool edit = false)
|
||||
{
|
||||
if (edit) Console.Write("(Edit): ");
|
||||
switch (messageBase)
|
||||
|
|
@ -68,9 +65,8 @@ namespace WTelegramClientTest
|
|||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static string User(long id) => Users.TryGetValue(id, out var user) ? user.ToString() : $"User {id}";
|
||||
private static string Chat(long id) => Chats.TryGetValue(id, out var chat) ? chat.ToString() : $"Chat {id}";
|
||||
private static string Peer(Peer peer) => peer is null ? null : peer is PeerUser user ? User(user.user_id)
|
||||
: peer is PeerChat or PeerChannel ? Chat(peer.ID) : $"Peer {peer.ID}";
|
||||
private static string User(long id) => Manager.Users.TryGetValue(id, out var user) ? user.ToString() : $"User {id}";
|
||||
private static string Chat(long id) => Manager.Chats.TryGetValue(id, out var chat) ? chat.ToString() : $"Chat {id}";
|
||||
private static string Peer(Peer peer) => Manager.UserOrChat(peer)?.ToString() ?? $"Peer {peer?.ID}";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
70
Examples/Program_ReactorError.cs
Normal file
70
Examples/Program_ReactorError.cs
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using TL;
|
||||
|
||||
namespace WTelegramClientTest
|
||||
{
|
||||
static class Program_ReactorError
|
||||
{
|
||||
static WTelegram.Client Client;
|
||||
|
||||
// go to Project Properties > Debug > Launch Profiles > Environment variables and add at least these: api_id, api_hash, phone_number
|
||||
static async Task Main(string[] _)
|
||||
{
|
||||
Console.WriteLine("The program demonstrate how to handle ReactorError. Press any key to terminate");
|
||||
WTelegram.Helpers.Log = (l, s) => System.Diagnostics.Debug.WriteLine(s);
|
||||
try
|
||||
{
|
||||
await CreateAndConnect();
|
||||
Console.ReadKey();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Client != null) await Client.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task CreateAndConnect()
|
||||
{
|
||||
Client = new WTelegram.Client(Environment.GetEnvironmentVariable);
|
||||
Client.OnUpdates += Client_OnUpdates;
|
||||
Client.OnOther += Client_OnOther;
|
||||
var my = await Client.LoginUserIfNeeded();
|
||||
Console.WriteLine($"We are logged-in as " + my);
|
||||
}
|
||||
|
||||
private static async Task Client_OnOther(IObject arg)
|
||||
{
|
||||
if (arg is ReactorError err)
|
||||
{
|
||||
// typically: network connection was totally lost
|
||||
Console.WriteLine($"Fatal reactor error: {err.Exception.Message}");
|
||||
while (true)
|
||||
{
|
||||
Console.WriteLine("Disposing the client and trying to reconnect in 5 seconds...");
|
||||
if (Client != null) await Client.DisposeAsync();
|
||||
Client = null;
|
||||
await Task.Delay(5000);
|
||||
try
|
||||
{
|
||||
await CreateAndConnect();
|
||||
break;
|
||||
}
|
||||
catch (Exception ex) when (ex is not ObjectDisposedException)
|
||||
{
|
||||
Console.WriteLine("Connection still failing: " + ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
Console.WriteLine("Other: " + arg.GetType().Name);
|
||||
}
|
||||
|
||||
private static Task Client_OnUpdates(UpdatesBase updates)
|
||||
{
|
||||
foreach (var update in updates.UpdateList)
|
||||
Console.WriteLine(update.GetType().Name);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,10 +13,10 @@ namespace WTelegramClientTest
|
|||
static Client Client;
|
||||
static SecretChats Secrets;
|
||||
static ISecretChat ActiveChat; // the secret chat currently selected
|
||||
static readonly Dictionary<long, User> Users = new();
|
||||
static readonly Dictionary<long, ChatBase> Chats = new();
|
||||
static readonly Dictionary<long, User> Users = [];
|
||||
static readonly Dictionary<long, ChatBase> Chats = [];
|
||||
|
||||
// go to Project Properties > Debug > Environment variables and add at least these: api_id, api_hash, phone_number
|
||||
// go to Project Properties > Debug > Launch Profiles > Environment variables and add at least these: api_id, api_hash, phone_number
|
||||
static async Task Main()
|
||||
{
|
||||
Helpers.Log = (l, s) => System.Diagnostics.Debug.WriteLine(s);
|
||||
|
|
@ -25,7 +25,7 @@ namespace WTelegramClientTest
|
|||
AppDomain.CurrentDomain.ProcessExit += (s, e) => { Secrets.Dispose(); Client.Dispose(); };
|
||||
SelectActiveChat();
|
||||
|
||||
Client.OnUpdate += Client_OnUpdate;
|
||||
Client.OnUpdates += Client_OnUpdates;
|
||||
var myself = await Client.LoginUserIfNeeded();
|
||||
Users[myself.id] = myself;
|
||||
Console.WriteLine($"We are logged-in as {myself}");
|
||||
|
|
@ -76,9 +76,8 @@ Type a command, or a message to send to the active secret chat:");
|
|||
} while (true);
|
||||
}
|
||||
|
||||
private static async Task Client_OnUpdate(IObject arg)
|
||||
private static async Task Client_OnUpdates(UpdatesBase updates)
|
||||
{
|
||||
if (arg is not UpdatesBase updates) return;
|
||||
updates.CollectUsersChats(Users, Chats);
|
||||
foreach (var update in updates.UpdateList)
|
||||
switch (update)
|
||||
|
|
|
|||
Binary file not shown.
48
FAQ.md
48
FAQ.md
|
|
@ -22,7 +22,8 @@ The WTelegram.session file contains the authentication keys negociated for the c
|
|||
|
||||
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.
|
||||
|
||||
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. This can be done by having your Config callback reply with a different filename (or folder) for "**session_pathname**" for each user.
|
||||
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.
|
||||
This can be done by having your Config callback reply with a different filename (or folder) for "**session_pathname**" for each user.
|
||||
This way, you can keep separate session files (each with their authentication keys) for each user.
|
||||
|
||||
If you need to manage these user accounts in parallel, you can create multiple instances of WTelegram.Client,
|
||||
|
|
@ -58,8 +59,8 @@ You also need to obtain their `access_hash` which is specific to the resource yo
|
|||
This serves as a proof that the logged-in user is entitled to access that channel/user/photo/document/...
|
||||
(otherwise, anybody with the ID could access it)
|
||||
|
||||
> A small private `Chat` don't need an access_hash and can be queried using their `chat_id` only.
|
||||
However most common chat groups are not `Chat` but a `Channel` supergroup (without the `broadcast` flag). See [Terminology in ReadMe](README.md#terminology).
|
||||
> A small private group `Chat` don't need an access_hash and can be queried using their `chat_id` only.
|
||||
<ins>However</ins> most common chat groups are not `Chat` but a `Channel` supergroup (without the `broadcast` flag). See [Terminology in ReadMe](README.md#terminology).
|
||||
Some TL methods only applies to private `Chat`, some only applies to `Channel` and some to both.
|
||||
|
||||
The `access_hash` must usually be provided within the `Input...` structure you pass in argument to an API method (`InputPeer`, `InputChannel`, `InputUser`, etc...).
|
||||
|
|
@ -107,6 +108,7 @@ To fix this, you should also switch to using the [WTelegramClient Nuget package]
|
|||
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.
|
||||
|
||||
You can try to wait more between the requests, wait for a day or two to see if the requests become possible again.
|
||||
|
||||
>ℹ️ For FLOOD_WAIT_X with X < 60 seconds (see `client.FloodRetryThreshold`), WTelegramClient will automatically wait the specified delay and retry the request for you.
|
||||
For longer delays, you can catch the thrown `RpcException` and check the value of property X.
|
||||
|
||||
|
|
@ -142,7 +144,7 @@ Here are some advices from [another similar library](https://github.com/gotd/td/
|
|||
|
||||
Some additional advices from me:
|
||||
|
||||
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 `OnUpdate` events.
|
||||
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.
|
||||
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.
|
||||
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)
|
||||
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.
|
||||
|
|
@ -202,9 +204,9 @@ If Telegram servers decide to shutdown this secondary connection, it's not an is
|
|||
This should be transparent and pending API calls should automatically be resent upon reconnection.
|
||||
You can choose to increase `MaxAutoReconnects` if it happens too often because your Internet connection is unstable.
|
||||
|
||||
3) If you reach `MaxAutoReconnects` disconnections, then the **OnUpdate** event handler will receive a `ReactorError` object to notify you of the problem.
|
||||
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,
|
||||
and pending API calls throw the network IOException.
|
||||
In this case, the recommended action would be to dispose the client and recreate one
|
||||
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))
|
||||
|
||||
4) In case of slow Internet connection or if you break in the debugger for some time,
|
||||
you might also get Connection shutdown because your client couldn't send Pings to Telegram in the allotted time.
|
||||
|
|
@ -234,7 +236,7 @@ In particular, it will detect and handle automatically and properly the various
|
|||
* 2FA password required (your Config needs to provide "password")
|
||||
* Email registration procedure required (your Config needs to provide "email", "email_verification_code")
|
||||
* Account registration/sign-up required (your Config needs to provide "first_name", "last_name")
|
||||
* Request to resend the verification code through alternate ways like SMS (if your Config answer an empty "verification_code" initially)
|
||||
* Request to resend the verification code through alternate ways (if your Config answer an empty "verification_code" initially)
|
||||
* Transient failures, slowness to respond, wrong code/password, checks for encryption key safety, etc..
|
||||
|
||||
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...
|
||||
|
|
@ -260,7 +262,7 @@ The following choices were made while implementing Secret Chats in WTelegramClie
|
|||
If for some reason, we received them in incorrect order, messages are kept in memory until the requested missing messages are obtained.
|
||||
If those missing messages are never obtained during the session, incoming messages might get stuck and lost.
|
||||
- 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.
|
||||
- If you want to accept incoming Secret Chats request only from specific user, you must check it in OnUpdate before:
|
||||
- If you want to accept incoming Secret Chats request only from specific user, you must check it in OnUpdates before:
|
||||
`await Secrets.HandleUpdate(ue, ue.chat is EncryptedChatRequested ecr && ecr.admin_id == EXPECTED_USER_ID);`
|
||||
- As recommended, new encryption keys are negotiated every 100 sent/received messages or after one week.
|
||||
If remote client doesn't complete this negotiation before reaching 200 messages, the Secret Chat is aborted.
|
||||
|
|
@ -328,3 +330,33 @@ For a console program, this is typical done by waiting for a key or some close e
|
|||
5) Is every Telegram API call rejected? (typically with an exception message like `AUTH_RESTART`)
|
||||
The user authentification might have failed at some point (or the user revoked the authorization).
|
||||
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()`
|
||||
|
||||
<a name="manager"></a>
|
||||
# About the UpdateManager
|
||||
|
||||
The UpdateManager does the following:
|
||||
- ensure the correct sequential order of receiving updates (Telegram may send them in wrong order)
|
||||
- fetch the missing updates if there was a gap (missing update) in the flow of incoming updates
|
||||
- resume the flow of updates where you left off if you stopped your program (with saved state)
|
||||
- collect the users & chats from updates automatically for you _(by default)_
|
||||
- simplifies the handling of the various containers of update (UpdatesBase)
|
||||
|
||||
To use the UpdateManager, instead of setting `client.OnUpdates`, you call:
|
||||
```csharp
|
||||
// if you don't care about missed updates while your program was down:
|
||||
var manager = client.WithUpdateManager(OnUpdate);
|
||||
|
||||
// if you want to recover missed updates using the state saved on the last run of your program
|
||||
var manager = client.WithUpdateManager(OnUpdate, "Updates.state");
|
||||
// to save the state later, preferably after disposing the client:
|
||||
manager.SaveState("Updates.state")
|
||||
```
|
||||
|
||||
Your `OnUpdate` method will directly take a single `Update` as parameter, instead of a container of updates.
|
||||
The `manager.Users` and `manager.Chats` dictionaries will collect the users/chats data from updates.
|
||||
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)`
|
||||
See [Examples/Program_ListenUpdates.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ListenUpdates.cs?ts=4#L21) for an example of implementation.
|
||||
|
||||
Notes:
|
||||
- set `manager.Log` if you want different logger settings than the client
|
||||
- `WithUpdateManager()` has other parameters for advanced use
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021 Olivier Marcoux
|
||||
Copyright (c) 2021-2024 Olivier Marcoux
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
|
|||
39
README.md
39
README.md
|
|
@ -1,17 +1,19 @@
|
|||
[](https://corefork.telegram.org/methods)
|
||||
[](https://corefork.telegram.org/methods)
|
||||
[](https://www.nuget.org/packages/WTelegramClient/)
|
||||
[](https://dev.azure.com/wiz0u/WTelegramClient/_build?definitionId=7)
|
||||
[](http://t.me/WTelegramBot?start=donate)
|
||||
[](https://www.nuget.org/packages/WTelegramClient/absoluteLatest)
|
||||
[](https://buymeacoffee.com/wizou)
|
||||
|
||||
## _Telegram Client API library written 100% in C# and .NET_
|
||||
## *_Telegram Client API library written 100% in C# and .NET_*
|
||||
|
||||
This library allows you to connect to Telegram and control a user programmatically (or a bot, but [Telegram.Bot](https://github.com/TelegramBots/Telegram.Bot) is much easier for that).
|
||||
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).
|
||||
All the Telegram Client APIs (MTProto) are supported so you can do everything the user could do with a full Telegram GUI client.
|
||||
|
||||
Library was developed solely by one unemployed guy. [Donations](https://buymeacoffee.com/wizou) or [Patreon memberships are welcome](https://patreon.com/wizou).
|
||||
|
||||
This ReadMe is a **quick but important tutorial** to learn the fundamentals about this library. Please read it all.
|
||||
|
||||
>⚠️ This library requires understanding advanced C# techniques such as **asynchronous programming** or **subclass pattern matching**...
|
||||
>If you are a beginner in C#, starting a project based on this library might not be a great idea.
|
||||
> ⚠️ This library requires understanding advanced C# techniques such as **asynchronous programming** or **subclass pattern matching**...
|
||||
> If you are a beginner in C#, starting a project based on this library might not be a great idea.
|
||||
|
||||
# How to use
|
||||
|
||||
|
|
@ -47,7 +49,7 @@ If you run this program again, you will notice that only **api_hash** is request
|
|||
This is because WTelegramClient saves (typically in the encrypted file **bin\WTelegram.session**) its state
|
||||
and the authentication keys that were negotiated with Telegram so that you needn't sign-in again every time.
|
||||
|
||||
That file path is configurable (**session_pathname**), and under various circumstances (changing user or server address)
|
||||
That file path is configurable (**session_pathname**), and under various circumstances *(changing user or server address, write permissions)*
|
||||
you may want to change it or simply delete the existing session file in order to restart the authentification process.
|
||||
|
||||
# Non-interactive configuration
|
||||
|
|
@ -91,6 +93,8 @@ Since version 3.0.0, a new approach to login/configuration has been added. Some
|
|||
```csharp
|
||||
WTelegram.Client client = new WTelegram.Client(YOUR_API_ID, "YOUR_API_HASH"); // this constructor doesn't need a Config method
|
||||
await DoLogin("+12025550156"); // initial call with user's phone_number
|
||||
...
|
||||
//client.Dispose(); // the client must be disposed when you're done running your userbot.
|
||||
|
||||
async Task DoLogin(string loginInfo) // (add this method to your code)
|
||||
{
|
||||
|
|
@ -112,7 +116,7 @@ See [WinForms example](https://wiz0u.github.io/WTelegramClient/Examples/WinForms
|
|||
|
||||
# Example of API call
|
||||
|
||||
>ℹ️ The Telegram API makes extensive usage of base and derived classes, so be ready to use the various C# syntaxes
|
||||
> The Telegram API makes extensive usage of base and derived classes, so be ready to use the various C# syntaxes
|
||||
to check/cast base classes into the more useful derived classes (`is`, `as`, `case DerivedType` )
|
||||
|
||||
All the Telegram API classes/methods are fully documented through Intellisense: Place your mouse over a class/method name,
|
||||
|
|
@ -158,15 +162,20 @@ or a [broadcast channel](https://corefork.telegram.org/api/channel#channels) (th
|
|||
See [FAQ #4](https://wiz0u.github.io/WTelegramClient/FAQ#access-hash) to learn more about it.
|
||||
- **DC** (DataCenter): There are a few datacenters depending on where in the world the user (or an uploaded media file) is from.
|
||||
- **Session** or **Authorization**: Pairing between a device and a phone number. You can have several active sessions for the same phone number.
|
||||
- **Participant**: A member/subscriber of a chat group or channel
|
||||
|
||||
# Other things to know
|
||||
|
||||
The Client class also offers an `OnUpdate` event that is triggered when Telegram servers sends Updates (like new messages or status), independently of your API requests.
|
||||
See [Examples/Program_ListenUpdates.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ListenUpdates.cs?ts=4#L23)
|
||||
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.
|
||||
You can also use the [UpdateManager class](https://wiz0u.github.io/WTelegramClient/FAQ#manager) to simplify the handling of such updates.
|
||||
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)
|
||||
|
||||
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.
|
||||
|
||||
The other configuration items that you can override include: **session_pathname, email, email_verification_code, session_key, server_address, device_model, system_version, app_version, system_lang_code, lang_pack, lang_code, user_id, bot_token**
|
||||
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:
|
||||
`case "server_address": return "2>149.154.167.40:443"; // test DC`
|
||||
|
||||
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**
|
||||
|
||||
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).
|
||||
Required API parameters/fields can sometimes be set to 0 or `null` when unused (check API documentation or experiment).
|
||||
|
|
@ -182,7 +191,7 @@ This library works best with **.NET 5.0+** (faster, no dependencies) and is also
|
|||
This library can be used for any Telegram scenario including:
|
||||
- Sequential or parallel automated steps based on API requests/responses
|
||||
- Real-time [monitoring](https://wiz0u.github.io/WTelegramClient/EXAMPLES#updates) of incoming Updates/Messages
|
||||
- Download/upload of files/media
|
||||
- [Download](https://wiz0u.github.io/WTelegramClient/EXAMPLES#download)/[upload](https://wiz0u.github.io/WTelegramClient/EXAMPLES#upload) of files/media
|
||||
- Exchange end-to-end encrypted messages/files in [Secret Chats](https://wiz0u.github.io/WTelegramClient/EXAMPLES#e2e)
|
||||
- Building a full-featured interactive client
|
||||
|
||||
|
|
@ -195,4 +204,6 @@ as well as the [API Terms of Service](https://core.telegram.org/api/terms) or yo
|
|||
If you read all this ReadMe, the [Frequently Asked Questions](https://wiz0u.github.io/WTelegramClient/FAQ),
|
||||
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)
|
||||
|
||||
If you like this library, please [consider a donation](http://t.me/WTelegramBot?start=donate) ❤ This will help the project keep going.
|
||||
If you like this library, you can [buy me a coffee](https://buymeacoffee.com/wizou) ❤ This will help the project keep going.
|
||||
|
||||
© 2021-2025 Olivier Marcoux
|
||||
|
|
|
|||
266
generator/MTProtoGenerator.cs
Normal file
266
generator/MTProtoGenerator.cs
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
#pragma warning disable RS1024 // Symbols should be compared for equality
|
||||
|
||||
namespace TL.Generator;
|
||||
|
||||
[Generator]
|
||||
public class MTProtoGenerator : IIncrementalGenerator
|
||||
{
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
var classDeclarations = context.SyntaxProvider.ForAttributeWithMetadataName("TL.TLDefAttribute",
|
||||
(_, _) => true, (context, _) => (ClassDeclarationSyntax)context.TargetNode);
|
||||
var source = context.CompilationProvider.Combine(classDeclarations.Collect());
|
||||
context.RegisterSourceOutput(source, Execute);
|
||||
}
|
||||
|
||||
static void Execute(SourceProductionContext context, (Compilation compilation, ImmutableArray<ClassDeclarationSyntax> classes) unit)
|
||||
{
|
||||
var object_ = unit.compilation.GetSpecialType(SpecialType.System_Object);
|
||||
if (unit.compilation.GetTypeByMetadataName("TL.TLDefAttribute") is not { } tlDefAttribute) return;
|
||||
if (unit.compilation.GetTypeByMetadataName("TL.IfFlagAttribute") is not { } ifFlagAttribute) return;
|
||||
if (unit.compilation.GetTypeByMetadataName("TL.Layer") is not { } layer) return;
|
||||
if (unit.compilation.GetTypeByMetadataName("TL.IObject") is not { } iobject) return;
|
||||
var nullables = LoadNullables(layer);
|
||||
var namespaces = new Dictionary<string, Dictionary<string, string>>(); // namespace,class,methods
|
||||
var tableTL = new StringBuilder();
|
||||
var methodsTL = new StringBuilder();
|
||||
var source = new StringBuilder();
|
||||
source
|
||||
.AppendLine("using System;")
|
||||
.AppendLine("using System.Collections.Generic;")
|
||||
.AppendLine("using System.ComponentModel;")
|
||||
.AppendLine("using System.IO;")
|
||||
.AppendLine("using System.Linq;")
|
||||
.AppendLine("using TL;")
|
||||
.AppendLine()
|
||||
.AppendLine("#pragma warning disable CS0109")
|
||||
.AppendLine();
|
||||
tableTL
|
||||
.AppendLine("\t\tpublic static readonly Dictionary<uint, Func<BinaryReader, IObject>> Table = new()")
|
||||
.AppendLine("\t\t{");
|
||||
methodsTL
|
||||
.AppendLine("\t\tpublic static readonly Dictionary<uint, Func<BinaryReader, IObject>> Methods = new()")
|
||||
.AppendLine("\t\t{");
|
||||
|
||||
foreach (var classDecl in unit.classes)
|
||||
{
|
||||
var semanticModel = unit.compilation.GetSemanticModel(classDecl.SyntaxTree);
|
||||
if (semanticModel.GetDeclaredSymbol(classDecl) is not { } symbol) continue;
|
||||
var tldef = symbol.GetAttributes().FirstOrDefault(a => a.AttributeClass == tlDefAttribute);
|
||||
if (tldef == null) continue;
|
||||
var id = (uint)tldef.ConstructorArguments[0].Value;
|
||||
StringBuilder writeTl = new(), readTL = new();
|
||||
var ns = symbol.BaseType.ContainingNamespace.ToString();
|
||||
var name = symbol.BaseType.Name;
|
||||
if (ns != "System")
|
||||
{
|
||||
if (!namespaces.TryGetValue(ns, out var parentClasses)) namespaces[ns] = parentClasses = [];
|
||||
parentClasses.TryGetValue(name, out var parentMethods);
|
||||
if (symbol.BaseType.IsAbstract)
|
||||
{
|
||||
if (parentMethods == null)
|
||||
{
|
||||
if (name is "Peer")
|
||||
writeTl.AppendLine("\t\tpublic virtual void WriteTL(BinaryWriter writer) => throw new NotSupportedException();");
|
||||
else
|
||||
writeTl.AppendLine("\t\tpublic abstract void WriteTL(BinaryWriter writer);");
|
||||
parentClasses[name] = writeTl.ToString();
|
||||
writeTl.Clear();
|
||||
}
|
||||
}
|
||||
else if (parentMethods?.Contains(" virtual ") == false)
|
||||
parentClasses[name] = parentMethods.Replace("public void WriteTL(", "public virtual void WriteTL(");
|
||||
}
|
||||
ns = symbol.ContainingNamespace.ToString();
|
||||
name = symbol.Name;
|
||||
if (!namespaces.TryGetValue(ns, out var classes)) namespaces[ns] = classes = [];
|
||||
if (name is "_Message" or "MsgCopy")
|
||||
{
|
||||
classes[name] = "\t\tpublic void WriteTL(BinaryWriter writer) => throw new NotSupportedException();";
|
||||
continue;
|
||||
}
|
||||
if (id == 0x3072CFA1) // GzipPacked
|
||||
tableTL.AppendLine($"\t\t\t[0x{id:X8}] = reader => (IObject)reader.ReadTLGzipped(typeof(IObject)),");
|
||||
else if (name != "Null")
|
||||
{
|
||||
if (ns == "TL.Methods")
|
||||
methodsTL.AppendLine($"\t\t\t[0x{id:X8}] = {(ns == "TL" ? "" : ns + '.')}{name}{(symbol.IsGenericType ? "<object>" : "")}.ReadTL,");
|
||||
if (ns != "TL.Methods" || name == "Ping")
|
||||
tableTL.AppendLine($"\t\t\t[0x{id:X8}] = {(ns == "TL" ? "" : ns + '.')}{name}.ReadTL,");
|
||||
}
|
||||
var override_ = symbol.BaseType == object_ ? "" : "override ";
|
||||
if (name == "Messages_AffectedMessages") override_ = "virtual ";
|
||||
//if (symbol.Constructors[0].IsImplicitlyDeclared)
|
||||
// ctorTL.AppendLine($"\t\tpublic {name}() {{ }}");
|
||||
if (symbol.IsGenericType) name += "<X>";
|
||||
readTL
|
||||
.AppendLine($"\t\tpublic static new {name} ReadTL(BinaryReader reader)")
|
||||
.AppendLine("\t\t{")
|
||||
.AppendLine($"\t\t\tvar r = new {name}();");
|
||||
writeTl
|
||||
.AppendLine("\t\t[EditorBrowsable(EditorBrowsableState.Never)]")
|
||||
.AppendLine($"\t\tpublic {override_}void WriteTL(BinaryWriter writer)")
|
||||
.AppendLine("\t\t{")
|
||||
.AppendLine($"\t\t\twriter.Write(0x{id:X8});");
|
||||
var members = symbol.GetMembers().ToList();
|
||||
for (var parent = symbol.BaseType; parent != object_; parent = parent.BaseType)
|
||||
{
|
||||
var inheritBefore = (bool?)tldef.NamedArguments.FirstOrDefault(k => k.Key == "inheritBefore").Value.Value ?? false;
|
||||
if (inheritBefore) members.InsertRange(0, parent.GetMembers());
|
||||
else members.AddRange(parent.GetMembers());
|
||||
tldef = parent.GetAttributes().FirstOrDefault(a => a.AttributeClass == tlDefAttribute);
|
||||
}
|
||||
foreach (var member in members.OfType<IFieldSymbol>())
|
||||
{
|
||||
if (member.DeclaredAccessibility != Accessibility.Public || member.IsStatic) continue;
|
||||
readTL.Append("\t\t\t");
|
||||
writeTl.Append("\t\t\t");
|
||||
var ifFlag = (int?)member.GetAttributes().FirstOrDefault(a => a.AttributeClass == ifFlagAttribute)?.ConstructorArguments[0].Value;
|
||||
if (ifFlag != null)
|
||||
{
|
||||
readTL.Append(ifFlag < 32 ? $"if (((uint)r.flags & 0x{1 << ifFlag:X}) != 0) "
|
||||
: $"if (((uint)r.flags2 & 0x{1 << (ifFlag - 32):X}) != 0) ");
|
||||
writeTl.Append(ifFlag < 32 ? $"if (((uint)flags & 0x{1 << ifFlag:X}) != 0) "
|
||||
: $"if (((uint)flags2 & 0x{1 << (ifFlag - 32):X}) != 0) ");
|
||||
}
|
||||
string memberType = member.Type.ToString();
|
||||
switch (memberType)
|
||||
{
|
||||
case "int":
|
||||
readTL.AppendLine($"r.{member.Name} = reader.ReadInt32();");
|
||||
writeTl.AppendLine($"writer.Write({member.Name});");
|
||||
break;
|
||||
case "long":
|
||||
readTL.AppendLine($"r.{member.Name} = reader.ReadInt64();");
|
||||
writeTl.AppendLine($"writer.Write({member.Name});");
|
||||
break;
|
||||
case "double":
|
||||
readTL.AppendLine($"r.{member.Name} = reader.ReadDouble();");
|
||||
writeTl.AppendLine($"writer.Write({member.Name});");
|
||||
break;
|
||||
case "bool":
|
||||
readTL.AppendLine($"r.{member.Name} = reader.ReadTLBool();");
|
||||
writeTl.AppendLine($"writer.Write({member.Name} ? 0x997275B5 : 0xBC799737);");
|
||||
break;
|
||||
case "System.DateTime":
|
||||
readTL.AppendLine($"r.{member.Name} = reader.ReadTLStamp();");
|
||||
writeTl.AppendLine($"writer.WriteTLStamp({member.Name});");
|
||||
break;
|
||||
case "string":
|
||||
readTL.AppendLine($"r.{member.Name} = reader.ReadTLString();");
|
||||
writeTl.AppendLine($"writer.WriteTLString({member.Name});");
|
||||
break;
|
||||
case "byte[]":
|
||||
readTL.AppendLine($"r.{member.Name} = reader.ReadTLBytes();");
|
||||
writeTl.AppendLine($"writer.WriteTLBytes({member.Name});");
|
||||
break;
|
||||
case "TL.Int128":
|
||||
readTL.AppendLine($"r.{member.Name} = new Int128(reader);");
|
||||
writeTl.AppendLine($"writer.Write({member.Name});");
|
||||
break;
|
||||
case "TL.Int256":
|
||||
readTL.AppendLine($"r.{member.Name} = new Int256(reader);");
|
||||
writeTl.AppendLine($"writer.Write({member.Name});");
|
||||
break;
|
||||
case "System.Collections.Generic.List<TL._Message>":
|
||||
readTL.AppendLine($"r.{member.Name} = reader.ReadTLRawVector<_Message>(0x5BB8E511);");
|
||||
writeTl.AppendLine($"writer.WriteTLMessages({member.Name});");
|
||||
break;
|
||||
case "TL.IObject": case "TL.IMethod<X>":
|
||||
readTL.AppendLine($"r.{member.Name} = {(memberType == "TL.IObject" ? "reader.ReadTLObject()" : "reader.ReadTLMethod<X>()")};");
|
||||
writeTl.AppendLine($"{member.Name}.WriteTL(writer);");
|
||||
break;
|
||||
case "System.Collections.Generic.Dictionary<long, TL.User>":
|
||||
readTL.AppendLine($"r.{member.Name} = reader.ReadTLDictionary<User>();");
|
||||
writeTl.AppendLine($"writer.WriteTLVector({member.Name}?.Values.ToArray());");
|
||||
break;
|
||||
case "System.Collections.Generic.Dictionary<long, TL.ChatBase>":
|
||||
readTL.AppendLine($"r.{member.Name} = reader.ReadTLDictionary<ChatBase>();");
|
||||
writeTl.AppendLine($"writer.WriteTLVector({member.Name}?.Values.ToArray());");
|
||||
break;
|
||||
case "object":
|
||||
readTL.AppendLine($"r.{member.Name} = reader.ReadTLObject();");
|
||||
writeTl.AppendLine($"writer.WriteTLValue({member.Name}, {member.Name}.GetType());");
|
||||
break;
|
||||
default:
|
||||
if (member.Type is IArrayTypeSymbol arrayType)
|
||||
{
|
||||
if (name is "FutureSalts")
|
||||
{
|
||||
readTL.AppendLine($"r.{member.Name} = reader.ReadTLRawVector<{memberType.Substring(0, memberType.Length - 2)}>(0x0949D9DC).ToArray();");
|
||||
writeTl.AppendLine($"writer.WriteTLRawVector({member.Name}, 16);");
|
||||
}
|
||||
else
|
||||
{
|
||||
readTL.AppendLine($"r.{member.Name} = reader.ReadTLVector<{memberType.Substring(0, memberType.Length - 2)}>();");
|
||||
writeTl.AppendLine($"writer.WriteTLVector({member.Name});");
|
||||
}
|
||||
}
|
||||
else if (member.Type.BaseType.SpecialType == SpecialType.System_Enum)
|
||||
{
|
||||
readTL.AppendLine($"r.{member.Name} = ({memberType})reader.ReadUInt32();");
|
||||
writeTl.AppendLine($"writer.Write((uint){member.Name});");
|
||||
}
|
||||
else if (memberType.StartsWith("TL."))
|
||||
{
|
||||
readTL.AppendLine($"r.{member.Name} = ({memberType})reader.ReadTLObject();");
|
||||
var nullStr = nullables.TryGetValue(memberType, out uint nullCtor) ? $"0x{nullCtor:X8}" : "Layer.NullCtor";
|
||||
writeTl.AppendLine($"if ({member.Name} != null) {member.Name}.WriteTL(writer); else writer.Write({nullStr});");
|
||||
}
|
||||
else
|
||||
writeTl.AppendLine($"Cannot serialize {memberType}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
readTL.AppendLine("\t\t\treturn r;");
|
||||
readTL.AppendLine("\t\t}");
|
||||
writeTl.AppendLine("\t\t}");
|
||||
readTL.Append(writeTl.ToString());
|
||||
classes[name] = readTL.ToString();
|
||||
}
|
||||
|
||||
foreach (var nullable in nullables)
|
||||
tableTL.AppendLine($"\t\t\t[0x{nullable.Value:X8}] = null,");
|
||||
tableTL.AppendLine("\t\t};");
|
||||
methodsTL.AppendLine("\t\t};");
|
||||
namespaces["TL"]["Layer"] = tableTL.ToString() + methodsTL.ToString();
|
||||
foreach (var namesp in namespaces)
|
||||
{
|
||||
source.Append("namespace ").AppendLine(namesp.Key).Append('{');
|
||||
foreach (var method in namesp.Value)
|
||||
source.AppendLine().Append("\tpartial class ").AppendLine(method.Key).AppendLine("\t{").Append(method.Value).AppendLine("\t}");
|
||||
source.AppendLine("}").AppendLine();
|
||||
}
|
||||
string text = source.ToString();
|
||||
Debug.Write(text);
|
||||
context.AddSource("TL.Generated.cs", text);
|
||||
}
|
||||
|
||||
private static Dictionary<string, uint> LoadNullables(INamedTypeSymbol layer)
|
||||
{
|
||||
var nullables = layer.GetMembers("Nullables").Single() as IFieldSymbol;
|
||||
var initializer = nullables.DeclaringSyntaxReferences[0].GetSyntax().ToString();
|
||||
var table = new Dictionary<string, uint>();
|
||||
foreach (var line in initializer.Split('\n'))
|
||||
{
|
||||
int index = line.IndexOf("[typeof(");
|
||||
if (index == -1) continue;
|
||||
int index2 = line.IndexOf(')', index += 8);
|
||||
string className = "TL." + line.Substring(index, index2 - index);
|
||||
index = line.IndexOf("= 0x", index2);
|
||||
if (index == -1) continue;
|
||||
index2 = line.IndexOf(',', index += 4);
|
||||
table[className] = uint.Parse(line.Substring(index, index2 - index), System.Globalization.NumberStyles.HexNumber);
|
||||
}
|
||||
return table;
|
||||
}
|
||||
}
|
||||
20
generator/MTProtoGenerator.csproj
Normal file
20
generator/MTProtoGenerator.csproj
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<IsRoslynComponent>true</IsRoslynComponent>
|
||||
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||
<IncludeBuildOutput>true</IncludeBuildOutput>
|
||||
<EnableNETAnalyzers>True</EnableNETAnalyzers>
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" PrivateAssets="all" />
|
||||
<PackageReference Include="System.Collections.Immutable" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<!--<ItemGroup>
|
||||
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
|
||||
</ItemGroup>-->
|
||||
</Project>
|
||||
|
|
@ -2,8 +2,6 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TL;
|
||||
|
|
@ -15,99 +13,62 @@ namespace WTelegram
|
|||
{
|
||||
partial class Client
|
||||
{
|
||||
#region Collect Access Hash system
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
/// <summary>Enable the collection of id/access_hash pairs (deprecated)</summary>
|
||||
[Obsolete("This system will be removed in a future version. You should use CollectUsersChats helper on API results or updates instead. See https://wiz0u.github.io/WTelegramClient/EXAMPLES#collect-users-chats")]
|
||||
public bool CollectAccessHash { get; set; }
|
||||
public IEnumerable<KeyValuePair<long, long>> AllAccessHashesFor<T>() where T : IObject => _accessHashes.GetValueOrDefault(typeof(T));
|
||||
private readonly Dictionary<Type, Dictionary<long, long>> _accessHashes = new();
|
||||
private static readonly FieldInfo userFlagsField = typeof(User).GetField("flags");
|
||||
private static readonly FieldInfo channelFlagsField = typeof(Channel).GetField("flags");
|
||||
|
||||
/// <summary>Retrieve the access_hash associated with this id (for a TL class) if it was collected</summary>
|
||||
/// <remarks>This requires <see cref="CollectAccessHash"/> to be set to <see langword="true"/> first.
|
||||
/// <para>See <see href="https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_CollectAccessHash.cs?ts=4#L22">Examples/Program_CollectAccessHash.cs</see> for how to use this</para></remarks>
|
||||
/// <typeparam name="T">a TL object class. For example User, Channel or Photo</typeparam>
|
||||
public long GetAccessHashFor<T>(long id) where T : IObject
|
||||
{
|
||||
if (!CollectAccessHash) Helpers.Log(4, "GetAccessHashFor doesn't do what you think. See https://github.com/wiz0u/WTelegramClient/blob/master/FAQ.md#access-hash");
|
||||
lock (_accessHashes)
|
||||
return _accessHashes.GetOrCreate(typeof(T)).TryGetValue(id, out var access_hash) ? access_hash : 0;
|
||||
}
|
||||
public void SetAccessHashFor<T>(long id, long access_hash) where T : IObject
|
||||
{
|
||||
lock (_accessHashes)
|
||||
_accessHashes.GetOrCreate(typeof(T))[id] = access_hash;
|
||||
}
|
||||
internal void CollectField(FieldInfo fieldInfo, object obj, object access_hash)
|
||||
{
|
||||
if (fieldInfo.Name != "access_hash") return;
|
||||
if (access_hash is not long accessHash) return;
|
||||
var type = fieldInfo.ReflectedType;
|
||||
if ((type == typeof(User) && ((User.Flags)userFlagsField.GetValue(obj)).HasFlag(User.Flags.min)) ||
|
||||
(type == typeof(Channel) && ((Channel.Flags)channelFlagsField.GetValue(obj)).HasFlag(Channel.Flags.min)))
|
||||
return; // access_hash from Min constructors are mostly useless. see https://core.telegram.org/api/min
|
||||
if (type.GetField("id") is not FieldInfo idField) return;
|
||||
if (idField.GetValue(obj) is not long id)
|
||||
if (idField.GetValue(obj) is not int idInt) return;
|
||||
else id = idInt;
|
||||
lock (_accessHashes)
|
||||
_accessHashes.GetOrCreate(type)[id] = accessHash;
|
||||
}
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
#endregion
|
||||
|
||||
#region Client TL Helpers
|
||||
/// <summary>Used to indicate progression of file download/upload</summary>
|
||||
/// <param name="transmitted">transmitted bytes</param>
|
||||
/// <param name="totalSize">total size of file in bytes, or 0 if unknown</param>
|
||||
public delegate void ProgressCallback(long transmitted, long totalSize);
|
||||
|
||||
/// <summary>Helper function to upload a file to Telegram</summary>
|
||||
/// <summary>Helper method to upload a file to Telegram</summary>
|
||||
/// <param name="pathname">Path to the file to upload</param>
|
||||
/// <param name="progress">(optional) Callback for tracking the progression of the transfer</param>
|
||||
/// <returns>an <see cref="InputFile"/> or <see cref="InputFileBig"/> than can be used in various requests</returns>
|
||||
public Task<InputFileBase> UploadFileAsync(string pathname, ProgressCallback progress = null)
|
||||
=> UploadFileAsync(File.OpenRead(pathname), Path.GetFileName(pathname), progress);
|
||||
|
||||
/// <summary>Helper function to upload a file to Telegram</summary>
|
||||
/// <summary>Helper method to upload a file to Telegram</summary>
|
||||
/// <param name="stream">Content of the file to upload. This method close/dispose the stream</param>
|
||||
/// <param name="filename">Name of the file</param>
|
||||
/// <param name="progress">(optional) Callback for tracking the progression of the transfer</param>
|
||||
/// <returns>an <see cref="InputFile"/> or <see cref="InputFileBig"/> than can be used in various requests</returns>
|
||||
public async Task<InputFileBase> UploadFileAsync(Stream stream, string filename, ProgressCallback progress = null)
|
||||
{
|
||||
using var md5 = MD5.Create();
|
||||
var client = await GetClientForDC(-_dcSession.DcID, true);
|
||||
using (stream)
|
||||
{
|
||||
long transmitted = 0, length = stream.Length;
|
||||
var isBig = length >= 10 * 1024 * 1024;
|
||||
int file_total_parts = (int)((length - 1) / FilePartSize) + 1;
|
||||
const long SMALL_FILE_MAX_SIZE = 10 << 20;
|
||||
bool hasLength = stream.CanSeek;
|
||||
long transmitted = 0, length = hasLength ? stream.Length : -1;
|
||||
bool isBig = !hasLength || length > SMALL_FILE_MAX_SIZE;
|
||||
int file_total_parts = hasLength ? (int)((length - 1) / FilePartSize) + 1 : -1;
|
||||
long file_id = Helpers.RandomLong();
|
||||
int file_part = 0, read;
|
||||
var tasks = new Dictionary<int, Task>();
|
||||
bool abort = false;
|
||||
for (long bytesLeft = length; !abort && bytesLeft != 0; file_part++)
|
||||
for (long bytesLeft = hasLength ? length : long.MaxValue; !abort && bytesLeft != 0; file_part++)
|
||||
{
|
||||
var bytes = new byte[Math.Min(FilePartSize, bytesLeft)];
|
||||
read = await stream.FullReadAsync(bytes, bytes.Length, default);
|
||||
await _parallelTransfers.WaitAsync();
|
||||
bytesLeft -= read;
|
||||
if (!hasLength && read < bytes.Length)
|
||||
{
|
||||
file_total_parts = file_part;
|
||||
if (read == 0) break; else file_total_parts++;
|
||||
bytes = bytes[..read];
|
||||
bytesLeft = 0;
|
||||
}
|
||||
var task = SavePart(file_part, bytes);
|
||||
lock (tasks) tasks[file_part] = task;
|
||||
if (!isBig)
|
||||
md5.TransformBlock(bytes, 0, read, null, 0);
|
||||
if (read < FilePartSize && bytesLeft != 0) throw new ApplicationException($"Failed to fully read stream ({read},{bytesLeft})");
|
||||
if (read < FilePartSize && bytesLeft != 0) throw new WTException($"Failed to fully read stream ({read},{bytesLeft})");
|
||||
|
||||
async Task SavePart(int file_part, byte[] bytes)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (isBig)
|
||||
await this.Upload_SaveBigFilePart(file_id, file_part, file_total_parts, bytes);
|
||||
await client.Upload_SaveBigFilePart(file_id, file_part, file_total_parts, bytes);
|
||||
else
|
||||
await this.Upload_SaveFilePart(file_id, file_part, bytes);
|
||||
await client.Upload_SaveFilePart(file_id, file_part, bytes);
|
||||
lock (tasks) { transmitted += bytes.Length; tasks.Remove(file_part); }
|
||||
progress?.Invoke(transmitted, length);
|
||||
}
|
||||
|
|
@ -123,77 +84,89 @@ namespace WTelegram
|
|||
}
|
||||
}
|
||||
Task[] remainingTasks;
|
||||
lock (tasks) remainingTasks = tasks.Values.ToArray();
|
||||
lock (tasks) remainingTasks = [.. tasks.Values];
|
||||
await Task.WhenAll(remainingTasks); // wait completion and eventually propagate any task exception
|
||||
if (!isBig) md5.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
|
||||
return isBig ? new InputFileBig { id = file_id, parts = file_total_parts, name = filename }
|
||||
: new InputFile { id = file_id, parts = file_total_parts, name = filename, md5_checksum = md5.Hash };
|
||||
: new InputFile { id = file_id, parts = file_total_parts, name = filename };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Search messages in chat with <see href="https://corefork.telegram.org/type/MessagesFilter">filter</see> and text <para>See <a href="https://corefork.telegram.org/method/messages.search"/></para></summary>
|
||||
/// <typeparam name="T">See <see cref="MessagesFilter"/> for a list of possible filter types</typeparam>
|
||||
/// <param name="peer">User or chat, histories with which are searched, or <see langword="null"/> constructor for global search</param>
|
||||
/// <param name="text">Text search request</param>
|
||||
/// <param name="q">Text search request</param>
|
||||
/// <param name="offset_id">Only return messages starting from the specified message ID</param>
|
||||
/// <param name="limit"><a href="https://corefork.telegram.org/api/offsets">Number of results to return</a></param>
|
||||
public Task<Messages_MessagesBase> Messages_Search<T>(InputPeer peer, string text = null, int offset_id = 0, int limit = int.MaxValue) where T : MessagesFilter, new()
|
||||
=> this.Messages_Search(peer, text, new T(), offset_id: offset_id, limit: limit);
|
||||
public Task<Messages_MessagesBase> Messages_Search<T>(InputPeer peer, string q = null, int offset_id = 0, int limit = int.MaxValue) where T : MessagesFilter, new()
|
||||
=> this.Messages_Search(peer, q, new T(), offset_id: offset_id, limit: limit);
|
||||
|
||||
/// <summary>Search messages globally with <see href="https://corefork.telegram.org/type/MessagesFilter">filter</see> and text <para>See <a href="https://corefork.telegram.org/method/messages.searchGlobal"/></para></summary>
|
||||
/// <typeparam name="T">See <see cref="MessagesFilter"/> for a list of possible filter types</typeparam>
|
||||
/// <param name="text">Text search request</param>
|
||||
/// <param name="q">Query</param>
|
||||
/// <param name="offset_id">Only return messages starting from the specified message ID</param>
|
||||
/// <param name="limit"><a href="https://corefork.telegram.org/api/offsets">Number of results to return</a></param>
|
||||
public Task<Messages_MessagesBase> Messages_SearchGlobal<T>(string text = null, int offset_id = 0, int limit = int.MaxValue) where T : MessagesFilter, new()
|
||||
=> this.Messages_SearchGlobal(text, new T(), offset_id: offset_id, limit: limit);
|
||||
public Task<Messages_MessagesBase> Messages_SearchGlobal<T>(string q = null, int offset_id = 0, int limit = int.MaxValue) where T : MessagesFilter, new()
|
||||
=> this.Messages_SearchGlobal(q, new T(), offset_id: offset_id, limit: limit);
|
||||
|
||||
/// <summary>Helper function to send a media message more easily</summary>
|
||||
/// <summary>Helper method to send a media message more easily</summary>
|
||||
/// <param name="peer">Destination of message (chat group, channel, user chat, etc..) </param>
|
||||
/// <param name="caption">Caption for the media <i>(in plain text)</i> or <see langword="null"/></param>
|
||||
/// <param name="mediaFile">Media file already uploaded to TG <i>(see <see cref="UploadFileAsync">UploadFileAsync</see>)</i></param>
|
||||
/// <param name="mimeType"><see langword="null"/> for automatic detection, <c>"photo"</c> for an inline photo, or a MIME type to send as a document</param>
|
||||
/// <param name="uploadedFile">Media file already uploaded to TG <i>(see <see cref="UploadFileAsync">UploadFileAsync</see>)</i></param>
|
||||
/// <param name="mimeType"><see langword="null"/> for automatic detection, <c>"photo"</c> for an inline photo, <c>"video"</c> for a streamable MP4 video, or a MIME type to send as a document</param>
|
||||
/// <param name="reply_to_msg_id">Your message is a reply to an existing message with this ID, in the same chat</param>
|
||||
/// <param name="entities">Text formatting entities for the caption. You can use <see cref="Markdown.MarkdownToEntities">MarkdownToEntities</see> to create these</param>
|
||||
/// <param name="schedule_date">UTC timestamp when the message should be sent</param>
|
||||
/// <returns>The transmitted message confirmed by Telegram</returns>
|
||||
public Task<Message> SendMediaAsync(InputPeer peer, string caption, InputFileBase mediaFile, string mimeType = null, int reply_to_msg_id = 0, MessageEntity[] entities = null, DateTime schedule_date = default)
|
||||
public Task<Message> SendMediaAsync(InputPeer peer, string caption, InputFileBase uploadedFile, string mimeType = null, int reply_to_msg_id = 0, MessageEntity[] entities = null, DateTime schedule_date = default)
|
||||
{
|
||||
mimeType ??= Path.GetExtension(mediaFile.Name)?.ToLowerInvariant() switch
|
||||
mimeType ??= Path.GetExtension(uploadedFile.Name)?.ToLowerInvariant() switch
|
||||
{
|
||||
".jpg" or ".jpeg" or ".png" or ".bmp" => "photo",
|
||||
".mp4" => "video",
|
||||
".gif" => "image/gif",
|
||||
".webp" => "image/webp",
|
||||
".mp4" => "video/mp4",
|
||||
".mp3" => "audio/mpeg",
|
||||
".wav" => "audio/x-wav",
|
||||
_ => "", // send as generic document with undefined MIME type
|
||||
};
|
||||
if (mimeType == "photo")
|
||||
return SendMessageAsync(peer, caption, new InputMediaUploadedPhoto { file = mediaFile }, reply_to_msg_id, entities, schedule_date);
|
||||
return SendMessageAsync(peer, caption, new InputMediaUploadedDocument(mediaFile, mimeType), reply_to_msg_id, entities, schedule_date);
|
||||
return SendMessageAsync(peer, caption, new InputMediaUploadedPhoto { file = uploadedFile }, reply_to_msg_id, entities, schedule_date);
|
||||
else if (mimeType == "video")
|
||||
return SendMessageAsync(peer, caption, new InputMediaUploadedDocument(uploadedFile, "video/mp4", new DocumentAttributeVideo { flags = DocumentAttributeVideo.Flags.supports_streaming }), reply_to_msg_id, entities, schedule_date);
|
||||
else
|
||||
return SendMessageAsync(peer, caption, new InputMediaUploadedDocument(uploadedFile, mimeType), reply_to_msg_id, entities, schedule_date);
|
||||
}
|
||||
|
||||
/// <summary>Helper function to send a text or media message easily</summary>
|
||||
public enum LinkPreview { Disabled = 0, BelowText = 1, AboveText = 2 };
|
||||
/// <summary>Helper method to send a text or media message easily</summary>
|
||||
/// <param name="peer">Destination of message (chat group, channel, user chat, etc..) </param>
|
||||
/// <param name="text">The plain text of the message (or media caption)</param>
|
||||
/// <param name="media">An instance of <see cref="InputMedia">InputMedia</see>-derived class, or <see langword="null"/> if there is no associated media</param>
|
||||
/// <param name="reply_to_msg_id">Your message is a reply to an existing message with this ID, in the same chat</param>
|
||||
/// <param name="entities">Text formatting entities. You can use <see cref="HtmlText.HtmlToEntities">HtmlToEntities</see> or <see cref="Markdown.MarkdownToEntities">MarkdownToEntities</see> to create these</param>
|
||||
/// <param name="schedule_date">UTC timestamp when the message should be sent</param>
|
||||
/// <param name="disable_preview">Should website/media preview be shown or not, for URLs in your message</param>
|
||||
/// <param name="preview">Should website/media preview be shown below, above or not, for URL links in your message</param>
|
||||
/// <returns>The transmitted message as confirmed by Telegram</returns>
|
||||
public async Task<Message> SendMessageAsync(InputPeer peer, string text, InputMedia media = null, int reply_to_msg_id = 0, MessageEntity[] entities = null, DateTime schedule_date = default, bool disable_preview = false)
|
||||
public async Task<Message> SendMessageAsync(InputPeer peer, string text, InputMedia media = null, int reply_to_msg_id = 0, MessageEntity[] entities = null, DateTime schedule_date = default, LinkPreview preview = LinkPreview.BelowText)
|
||||
{
|
||||
UpdatesBase updates;
|
||||
long random_id = Helpers.RandomLong();
|
||||
if (media == null)
|
||||
updates = await this.Messages_SendMessage(peer, text, random_id, no_webpage: disable_preview, entities: entities,
|
||||
reply_to_msg_id: reply_to_msg_id == 0 ? null : reply_to_msg_id, schedule_date: schedule_date == default ? null : schedule_date);
|
||||
updates = await this.Messages_SendMessage(peer, text, random_id, entities: entities,
|
||||
no_webpage: preview == LinkPreview.Disabled, invert_media: preview == LinkPreview.AboveText,
|
||||
reply_to: reply_to_msg_id == 0 ? null : new InputReplyToMessage { reply_to_msg_id = reply_to_msg_id }, schedule_date: schedule_date == default ? null : schedule_date);
|
||||
else
|
||||
updates = await this.Messages_SendMedia(peer, media, text, random_id, entities: entities,
|
||||
reply_to_msg_id: reply_to_msg_id == 0 ? null : reply_to_msg_id, schedule_date: schedule_date == default ? null : schedule_date);
|
||||
RaiseUpdate(updates);
|
||||
reply_to: reply_to_msg_id == 0 ? null : new InputReplyToMessage { reply_to_msg_id = reply_to_msg_id }, schedule_date: schedule_date == default ? null : schedule_date);
|
||||
if (updates is UpdateShortSentMessage sent)
|
||||
return new Message
|
||||
{
|
||||
flags = (Message.Flags)sent.flags | (reply_to_msg_id == 0 ? 0 : Message.Flags.has_reply_to) | (peer is InputPeerSelf ? 0 : Message.Flags.has_from_id),
|
||||
id = sent.id, date = sent.date, message = text, entities = sent.entities, media = sent.media, ttl_period = sent.ttl_period,
|
||||
reply_to = reply_to_msg_id == 0 ? null : new MessageReplyHeader { reply_to_msg_id = reply_to_msg_id, flags = MessageReplyHeader.Flags.has_reply_to_msg_id },
|
||||
from_id = peer is InputPeerSelf ? null : new PeerUser { user_id = _session.UserId },
|
||||
peer_id = InputToPeer(peer)
|
||||
};
|
||||
int msgId = -1;
|
||||
foreach (var update in updates.UpdateList)
|
||||
{
|
||||
|
|
@ -204,35 +177,25 @@ namespace WTelegram
|
|||
case UpdateNewScheduledMessage { message: Message schedMsg } when schedMsg.id == msgId: return schedMsg;
|
||||
}
|
||||
}
|
||||
if (updates is UpdateShortSentMessage sent)
|
||||
{
|
||||
return new Message
|
||||
{
|
||||
flags = (Message.Flags)sent.flags | (reply_to_msg_id == 0 ? 0 : Message.Flags.has_reply_to) | (peer is InputPeerSelf ? 0 : Message.Flags.has_from_id),
|
||||
id = sent.id, date = sent.date, message = text, entities = sent.entities, media = sent.media, ttl_period = sent.ttl_period,
|
||||
reply_to = reply_to_msg_id == 0 ? null : new MessageReplyHeader { reply_to_msg_id = reply_to_msg_id },
|
||||
from_id = peer is InputPeerSelf ? null : new PeerUser { user_id = _session.UserId },
|
||||
peer_id = InputToPeer(peer)
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Helper function to send an album (media group) of photos or documents more easily</summary>
|
||||
/// <summary>Helper method to send an album (media group) of photos or documents more easily</summary>
|
||||
/// <param name="peer">Destination of message (chat group, channel, user chat, etc..) </param>
|
||||
/// <param name="medias">An array or List of <see cref="InputMedia">InputMedia</see>-derived class</param>
|
||||
/// <param name="caption">Caption for the media <i>(in plain text)</i> or <see langword="null"/></param>
|
||||
/// <param name="reply_to_msg_id">Your message is a reply to an existing message with this ID, in the same chat</param>
|
||||
/// <param name="entities">Text formatting entities for the caption. You can use <see cref="Markdown.MarkdownToEntities">MarkdownToEntities</see> to create these</param>
|
||||
/// <param name="schedule_date">UTC timestamp when the message should be sent</param>
|
||||
/// <returns>The last of the media group messages, confirmed by Telegram</returns>
|
||||
/// <param name="videoUrlAsFile">Any <see cref="InputMediaDocumentExternal"/> URL pointing to a video should be considered as non-streamable</param>
|
||||
/// <returns>The media group messages, as received by Telegram</returns>
|
||||
/// <remarks>
|
||||
/// * The caption/entities are set on the last media<br/>
|
||||
/// * <see cref="InputMediaDocumentExternal"/> and <see cref="InputMediaPhotoExternal"/> are supported by downloading the file from the web via HttpClient and sending it to Telegram.
|
||||
/// * The caption/entities are set on the first media<br/>
|
||||
/// * <see cref="InputMediaDocumentExternal"/> and <see cref="InputMediaPhotoExternal"/> are supported natively for bot accounts, and for user accounts by downloading the file from the web via HttpClient and sending it to Telegram.
|
||||
/// WTelegramClient proxy settings don't apply to HttpClient<br/>
|
||||
/// * You may run into errors if you mix, in the same album, photos and file documents having no thumbnails/video attributes
|
||||
/// </remarks>
|
||||
public async Task<Message[]> SendAlbumAsync(InputPeer peer, ICollection<InputMedia> medias, string caption = null, int reply_to_msg_id = 0, MessageEntity[] entities = null, DateTime schedule_date = default)
|
||||
public async Task<Message[]> SendAlbumAsync(InputPeer peer, ICollection<InputMedia> medias, string caption = null, int reply_to_msg_id = 0, MessageEntity[] entities = null, DateTime schedule_date = default, bool videoUrlAsFile = false)
|
||||
{
|
||||
System.Net.Http.HttpClient httpClient = null;
|
||||
int i = 0, length = medias.Count;
|
||||
|
|
@ -253,42 +216,56 @@ namespace WTelegram
|
|||
var mmd = (MessageMediaDocument)await this.Messages_UploadMedia(peer, imud);
|
||||
ism.media = mmd.document;
|
||||
break;
|
||||
case InputMediaDocumentExternal imde:
|
||||
string mimeType = null;
|
||||
var inputFile = await UploadFromUrl(imde.url);
|
||||
ism.media = new InputMediaUploadedDocument(inputFile, mimeType);
|
||||
goto retry;
|
||||
case InputMediaPhotoExternal impe:
|
||||
inputFile = await UploadFromUrl(impe.url);
|
||||
if (User.IsBot)
|
||||
try
|
||||
{
|
||||
mmp = (MessageMediaPhoto)await this.Messages_UploadMedia(peer, impe);
|
||||
ism.media = mmp.photo;
|
||||
break;
|
||||
}
|
||||
catch (RpcException) { }
|
||||
var inputFile = await UploadFromUrl(impe.url);
|
||||
ism.media = new InputMediaUploadedPhoto { file = inputFile };
|
||||
goto retry;
|
||||
case InputMediaDocumentExternal imde:
|
||||
if (!videoUrlAsFile && User.IsBot)
|
||||
try
|
||||
{
|
||||
mmd = (MessageMediaDocument)await this.Messages_UploadMedia(peer, imde);
|
||||
ism.media = mmd.document;
|
||||
break;
|
||||
}
|
||||
catch (RpcException) { }
|
||||
string mimeType = null;
|
||||
inputFile = await UploadFromUrl(imde.url);
|
||||
if (videoUrlAsFile || mimeType?.StartsWith("video/") != true)
|
||||
ism.media = new InputMediaUploadedDocument(inputFile, mimeType);
|
||||
else
|
||||
ism.media = new InputMediaUploadedDocument(inputFile, mimeType, new DocumentAttributeVideo { flags = DocumentAttributeVideo.Flags.supports_streaming });
|
||||
goto retry;
|
||||
|
||||
async Task<InputFileBase> UploadFromUrl(string url)
|
||||
{
|
||||
var filename = Path.GetFileName(new Uri(url).LocalPath);
|
||||
httpClient ??= new();
|
||||
var response = await httpClient.GetAsync(url);
|
||||
using var response = await httpClient.GetAsync(url, System.Net.Http.HttpCompletionOption.ResponseHeadersRead);
|
||||
response.EnsureSuccessStatusCode();
|
||||
using var stream = await response.Content.ReadAsStreamAsync();
|
||||
mimeType = response.Content.Headers.ContentType?.MediaType;
|
||||
if (response.Content.Headers.ContentLength is long length)
|
||||
return await UploadFileAsync(new Helpers.IndirectStream(stream) { ContentLength = length }, filename);
|
||||
else
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
await stream.CopyToAsync(ms);
|
||||
ms.Position = 0;
|
||||
return await UploadFileAsync(ms, filename);
|
||||
}
|
||||
return await UploadFileAsync(stream, filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
var lastMedia = multiMedia[^1];
|
||||
lastMedia.message = caption;
|
||||
lastMedia.entities = entities;
|
||||
if (entities != null) lastMedia.flags = InputSingleMedia.Flags.has_entities;
|
||||
var firstMedia = multiMedia[0];
|
||||
firstMedia.message = caption;
|
||||
firstMedia.entities = entities;
|
||||
if (entities != null) firstMedia.flags = InputSingleMedia.Flags.has_entities;
|
||||
|
||||
var updates = await this.Messages_SendMultiMedia(peer, multiMedia, reply_to_msg_id: reply_to_msg_id, schedule_date: schedule_date);
|
||||
RaiseUpdate(updates);
|
||||
var updates = await this.Messages_SendMultiMedia(peer, multiMedia, reply_to: reply_to_msg_id == 0 ? null : new InputReplyToMessage { reply_to_msg_id = reply_to_msg_id }, schedule_date: schedule_date);
|
||||
var msgIds = new int[length];
|
||||
var result = new Message[length];
|
||||
foreach (var update in updates.UpdateList)
|
||||
|
|
@ -303,6 +280,34 @@ namespace WTelegram
|
|||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Helper method to forwards messages more easily by their IDs.</summary>
|
||||
/// <param name="drop_author">Whether to forward messages without quoting the original author</param>
|
||||
/// <param name="drop_media_captions">Whether to strip captions from media</param>
|
||||
/// <param name="from_peer">Source of messages</param>
|
||||
/// <param name="msg_ids">IDs of messages</param>
|
||||
/// <param name="to_peer">Destination peer</param>
|
||||
/// <param name="top_msg_id">Destination <a href="https://corefork.telegram.org/api/forum#forum-topics">forum topic</a></param>
|
||||
/// <returns>The resulting forwarded messages, as received by Telegram <para>Some of them might be <see langword="null"/> if they could not all be forwarded</para></returns>
|
||||
public async Task<Message[]> ForwardMessagesAsync(InputPeer from_peer, int[] msg_ids, InputPeer to_peer, int top_msg_id = 0, bool drop_author = false, bool drop_media_captions = false)
|
||||
{
|
||||
int msgCount = msg_ids.Length;
|
||||
var random_id = Helpers.RandomLong();
|
||||
var random_ids = Enumerable.Range(0, msgCount).Select(i => random_id + i).ToArray();
|
||||
var updates = await this.Messages_ForwardMessages(from_peer, msg_ids, random_ids, to_peer, top_msg_id == 0 ? null : top_msg_id, drop_author: drop_author, drop_media_captions: drop_media_captions);
|
||||
var msgIds = new int[msgCount];
|
||||
var result = new Message[msgCount];
|
||||
foreach (var update in updates.UpdateList)
|
||||
{
|
||||
switch (update)
|
||||
{
|
||||
case UpdateMessageID updMsgId: msgIds[updMsgId.random_id - random_id] = updMsgId.id; break;
|
||||
case UpdateNewMessage { message: Message message }: result[Array.IndexOf(msgIds, message.id)] = message; break;
|
||||
case UpdateNewScheduledMessage { message: Message schedMsg }: result[Array.IndexOf(msgIds, schedMsg.id)] = schedMsg; break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Peer InputToPeer(InputPeer peer) => peer switch
|
||||
{
|
||||
InputPeerSelf => new PeerUser { user_id = _session.UserId },
|
||||
|
|
@ -329,6 +334,18 @@ namespace WTelegram
|
|||
return await DownloadFileAsync(fileLocation, outputStream, photo.dc_id, photoSize.FileSize, progress);
|
||||
}
|
||||
|
||||
/// <summary>Download an animated photo from Telegram into the outputStream</summary>
|
||||
/// <param name="photo">The photo to download</param>
|
||||
/// <param name="outputStream">Stream to write the file content to. This method does not close/dispose the stream</param>
|
||||
/// <param name="videoSize">A specific size/version of the animated photo. Use <c>photo.LargestVideoSize</c> to download the largest version of the animated photo</param>
|
||||
/// <param name="progress">(optional) Callback for tracking the progression of the transfer</param>
|
||||
/// <returns>The file type of the photo</returns>
|
||||
public async Task<Storage_FileType> DownloadFileAsync(Photo photo, Stream outputStream, VideoSize videoSize, ProgressCallback progress = null)
|
||||
{
|
||||
var fileLocation = photo.ToFileLocation(videoSize);
|
||||
return await DownloadFileAsync(fileLocation, outputStream, photo.dc_id, videoSize.size, progress);
|
||||
}
|
||||
|
||||
/// <summary>Download a document from Telegram into the outputStream</summary>
|
||||
/// <param name="document">The document to download</param>
|
||||
/// <param name="outputStream">Stream to write the file content to. This method does not close/dispose the stream</param>
|
||||
|
|
@ -344,6 +361,18 @@ namespace WTelegram
|
|||
return thumbSize == null ? document.mime_type : "image/" + fileType;
|
||||
}
|
||||
|
||||
/// <summary>Download a document from Telegram into the outputStream</summary>
|
||||
/// <param name="document">The document to download</param>
|
||||
/// <param name="outputStream">Stream to write the file content to. This method does not close/dispose the stream</param>
|
||||
/// <param name="videoSize">A specific size/version of the animated photo. Use <c>photo.LargestVideoSize</c> to download the largest version of the animated photo</param>
|
||||
/// <param name="progress">(optional) Callback for tracking the progression of the transfer</param>
|
||||
/// <returns>MIME type of the document/thumbnail</returns>
|
||||
public async Task<Storage_FileType> DownloadFileAsync(Document document, Stream outputStream, VideoSize videoSize, ProgressCallback progress = null)
|
||||
{
|
||||
var fileLocation = document.ToFileLocation(videoSize);
|
||||
return await DownloadFileAsync(fileLocation, outputStream, document.dc_id, videoSize.size, progress);
|
||||
}
|
||||
|
||||
/// <summary>Download a file from Telegram into the outputStream</summary>
|
||||
/// <param name="fileLocation">Telegram file identifier, typically obtained with a .ToFileLocation() call</param>
|
||||
/// <param name="outputStream">Stream to write file content to. This method does not close/dispose the stream</param>
|
||||
|
|
@ -354,10 +383,10 @@ namespace WTelegram
|
|||
public async Task<Storage_FileType> DownloadFileAsync(InputFileLocationBase fileLocation, Stream outputStream, int dc_id = 0, long fileSize = 0, ProgressCallback progress = null)
|
||||
{
|
||||
Storage_FileType fileType = Storage_FileType.unknown;
|
||||
var client = dc_id == 0 ? this : await GetClientForDC(dc_id, true);
|
||||
var client = dc_id == 0 ? this : await GetClientForDC(-dc_id, true);
|
||||
using var writeSem = new SemaphoreSlim(1);
|
||||
bool canSeek = outputStream.CanSeek;
|
||||
long streamStartPos = outputStream.Position;
|
||||
long streamStartPos = canSeek ? outputStream.Position : 0;
|
||||
long fileOffset = 0, maxOffsetSeen = 0;
|
||||
long transmitted = 0;
|
||||
var tasks = new Dictionary<long, Task>();
|
||||
|
|
@ -374,7 +403,7 @@ namespace WTelegram
|
|||
if (fileSize != 0 && fileOffset >= fileSize)
|
||||
{
|
||||
if (await task != ((fileSize - 1) % FilePartSize) + 1)
|
||||
throw new ApplicationException("Downloaded file size does not match expected file size");
|
||||
throw new WTException("Downloaded file size does not match expected file size");
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -387,7 +416,7 @@ namespace WTelegram
|
|||
}
|
||||
catch (RpcException ex) when (ex.Code == 303 && ex.Message == "FILE_MIGRATE_X")
|
||||
{
|
||||
client = await GetClientForDC(ex.X, true);
|
||||
client = await GetClientForDC(-ex.X, true);
|
||||
fileBase = await client.Upload_GetFile(fileLocation, offset, FilePartSize);
|
||||
}
|
||||
catch (RpcException ex) when (ex.Code == 400 && ex.Message == "OFFSET_INVALID")
|
||||
|
|
@ -405,7 +434,7 @@ namespace WTelegram
|
|||
_parallelTransfers.Release();
|
||||
}
|
||||
if (fileBase is not Upload_File fileData)
|
||||
throw new ApplicationException("Upload_GetFile returned unsupported " + fileBase?.GetType().Name);
|
||||
throw new WTException("Upload_GetFile returned unsupported " + fileBase?.GetType().Name);
|
||||
if (fileData.bytes.Length != FilePartSize) abort = true;
|
||||
if (fileData.bytes.Length != 0)
|
||||
{
|
||||
|
|
@ -413,7 +442,7 @@ namespace WTelegram
|
|||
await writeSem.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (streamStartPos + offset != outputStream.Position) // if we're about to write out of order
|
||||
if (canSeek && streamStartPos + offset != outputStream.Position) // if we're about to write out of order
|
||||
{
|
||||
await outputStream.FlushAsync(); // async flush, otherwise Seek would do a sync flush
|
||||
outputStream.Seek(streamStartPos + offset, SeekOrigin.Begin);
|
||||
|
|
@ -421,6 +450,7 @@ namespace WTelegram
|
|||
await outputStream.WriteAsync(fileData.bytes, 0, fileData.bytes.Length);
|
||||
maxOffsetSeen = Math.Max(maxOffsetSeen, offset + fileData.bytes.Length);
|
||||
transmitted += fileData.bytes.Length;
|
||||
progress?.Invoke(transmitted, fileSize);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
|
@ -430,7 +460,6 @@ namespace WTelegram
|
|||
finally
|
||||
{
|
||||
writeSem.Release();
|
||||
progress?.Invoke(transmitted, fileSize);
|
||||
}
|
||||
}
|
||||
lock (tasks) tasks.Remove(offset);
|
||||
|
|
@ -438,7 +467,7 @@ namespace WTelegram
|
|||
}
|
||||
}
|
||||
Task[] remainingTasks;
|
||||
lock (tasks) remainingTasks = tasks.Values.ToArray();
|
||||
lock (tasks) remainingTasks = [.. tasks.Values];
|
||||
await Task.WhenAll(remainingTasks); // wait completion and eventually propagate any task exception
|
||||
await outputStream.FlushAsync();
|
||||
if (canSeek) outputStream.Seek(streamStartPos + maxOffsetSeen, SeekOrigin.Begin);
|
||||
|
|
@ -496,6 +525,17 @@ namespace WTelegram
|
|||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Get all chats, channels and supergroups</summary>
|
||||
public async Task<Messages_Chats> Messages_GetAllChats()
|
||||
{
|
||||
var dialogs = await Messages_GetAllDialogs();
|
||||
var result = new Messages_Chats { chats = [] };
|
||||
foreach (var dialog in dialogs.dialogs)
|
||||
if (dialog.Peer is (PeerChat or PeerChannel) and { ID: var id })
|
||||
result.chats[id] = dialogs.chats[id];
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Returns the current user dialog list. <para>Possible <see cref="RpcException"/> codes: 400 (<a href="https://corefork.telegram.org/method/messages.getDialogs#possible-errors">details</a>)</para></summary>
|
||||
/// <param name="folder_id"><a href="https://corefork.telegram.org/api/folders#peer-folders">Peer folder ID, for more info click here</a></param>
|
||||
/// <returns>See <a href="https://corefork.telegram.org/constructor/messages.dialogs"/></returns>
|
||||
|
|
@ -507,23 +547,30 @@ namespace WTelegram
|
|||
case Messages_DialogsSlice mds:
|
||||
var dialogList = new List<DialogBase>();
|
||||
var messageList = new List<MessageBase>();
|
||||
while (dialogs.Dialogs.Length != 0)
|
||||
int skip = 0;
|
||||
while (dialogs.Dialogs.Length > skip)
|
||||
{
|
||||
dialogList.AddRange(dialogs.Dialogs);
|
||||
dialogList.AddRange(skip == 0 ? dialogs.Dialogs : dialogs.Dialogs[skip..]);
|
||||
messageList.AddRange(dialogs.Messages);
|
||||
var lastDialog = dialogs.Dialogs[^1];
|
||||
var lastMsg = dialogs.Messages.LastOrDefault(m => m.Peer.ID == lastDialog.Peer.ID && m.ID == lastDialog.TopMessage);
|
||||
var offsetPeer = dialogs.UserOrChat(lastDialog).ToInputPeer();
|
||||
dialogs = await this.Messages_GetDialogs(lastMsg?.Date ?? default, lastDialog.TopMessage, offsetPeer, folder_id: folder_id);
|
||||
skip = 0;
|
||||
int last = dialogs.Dialogs.Length - 1;
|
||||
var lastDialog = dialogs.Dialogs[last];
|
||||
retryDate:
|
||||
var lastPeer = dialogs.UserOrChat(lastDialog).ToInputPeer();
|
||||
var lastMsgId = lastDialog.TopMessage;
|
||||
var lastDate = dialogs.Messages.LastOrDefault(m => m.Peer.ID == lastDialog.Peer.ID && m.ID == lastDialog.TopMessage)?.Date ?? default;
|
||||
if (lastDate == default)
|
||||
if (--last < 0) break; else { ++skip; lastDialog = dialogs.Dialogs[last]; goto retryDate; }
|
||||
dialogs = await this.Messages_GetDialogs(lastDate, lastMsgId, lastPeer, folder_id: folder_id);
|
||||
if (dialogs is not Messages_Dialogs md) break;
|
||||
foreach (var (key, value) in md.chats) mds.chats[key] = value;
|
||||
foreach (var (key, value) in md.users) mds.users[key] = value;
|
||||
}
|
||||
mds.dialogs = dialogList.ToArray();
|
||||
mds.messages = messageList.ToArray();
|
||||
mds.dialogs = [.. dialogList];
|
||||
mds.messages = [.. messageList];
|
||||
return mds;
|
||||
case Messages_Dialogs md: return md;
|
||||
default: throw new ApplicationException("Messages_GetDialogs returned unexpected " + dialogs?.GetType().Name);
|
||||
default: throw new WTException("Messages_GetDialogs returned unexpected " + dialogs?.GetType().Name);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -533,12 +580,12 @@ namespace WTelegram
|
|||
/// <param name="alphabet1">first letters used to search for in participants names<br/>(default values crafted with ♥ to find most latin and cyrillic names)</param>
|
||||
/// <param name="alphabet2">second (and further) letters used to search for in participants names</param>
|
||||
/// <param name="cancellationToken">Can be used to abort the work of this method</param>
|
||||
/// <returns>Field count indicates the total count of members. Field participants contains those that were successfully fetched</returns>
|
||||
/// <returns>Field <c>count</c> indicates the total count of members. Field <c>participants</c> contains those that were successfully fetched</returns>
|
||||
/// <remarks>⚠ This method can take a few minutes to complete on big broadcast channels. It likely won't be able to obtain the full total count of members</remarks>
|
||||
public async Task<Channels_ChannelParticipants> Channels_GetAllParticipants(InputChannelBase channel, bool includeKickBan = false, string alphabet1 = "АБCДЕЄЖФГHИІJКЛМНОПQРСТУВWХЦЧШЩЫЮЯЗ", string alphabet2 = "АCЕHИJЛМНОРСТУВWЫ", CancellationToken cancellationToken = default)
|
||||
{
|
||||
alphabet2 ??= alphabet1;
|
||||
var result = new Channels_ChannelParticipants { chats = new(), users = new() };
|
||||
var result = new Channels_ChannelParticipants { chats = [], users = [] };
|
||||
var user_ids = new HashSet<long>();
|
||||
var participants = new List<ChannelParticipantBase>();
|
||||
|
||||
|
|
@ -554,7 +601,7 @@ namespace WTelegram
|
|||
await GetWithFilter(new ChannelParticipantsKicked { q = "" }, (f, c) => new ChannelParticipantsKicked { q = f.q + c }, alphabet1);
|
||||
await GetWithFilter(new ChannelParticipantsBanned { q = "" }, (f, c) => new ChannelParticipantsBanned { q = f.q + c }, alphabet1);
|
||||
}
|
||||
result.participants = participants.ToArray();
|
||||
result.participants = [.. participants];
|
||||
return result;
|
||||
|
||||
async Task GetWithFilter<T>(T filter, Func<T, char, T> recurse = null, string alphabet = null) where T : ChannelParticipantsFilter
|
||||
|
|
@ -570,7 +617,7 @@ namespace WTelegram
|
|||
foreach (var kvp in ccp.users) result.users[kvp.Key] = kvp.Value;
|
||||
lock (participants)
|
||||
foreach (var participant in ccp.participants)
|
||||
if (user_ids.Add(participant.UserID))
|
||||
if (user_ids.Add(participant.UserId))
|
||||
participants.Add(participant);
|
||||
offset += ccp.participants.Length;
|
||||
if (offset >= ccp.count || ccp.participants.Length == 0) break;
|
||||
|
|
@ -591,25 +638,50 @@ namespace WTelegram
|
|||
{
|
||||
var admins = admin == null ? null : new[] { admin };
|
||||
var result = await this.Channels_GetAdminLog(channel, q, events_filter: events_filter, admins: admins);
|
||||
if (result.events.Length < 100) return result;
|
||||
var resultFull = result;
|
||||
List<ChannelAdminLogEvent> events = new(result.events);
|
||||
do
|
||||
var events = new List<ChannelAdminLogEvent>(result.events);
|
||||
while (result.events.Length > 0)
|
||||
{
|
||||
result = await this.Channels_GetAdminLog(channel, q, max_id: result.events[^1].id, events_filter: events_filter, admins: admins);
|
||||
events.AddRange(result.events);
|
||||
foreach (var kvp in result.chats) resultFull.chats[kvp.Key] = kvp.Value;
|
||||
foreach (var kvp in result.users) resultFull.users[kvp.Key] = kvp.Value;
|
||||
} while (result.events.Length >= 100);
|
||||
resultFull.events = events.ToArray();
|
||||
}
|
||||
resultFull.events = [.. events];
|
||||
return resultFull;
|
||||
}
|
||||
|
||||
/// <summary>Helper simplified method: Get all <a href="https://corefork.telegram.org/api/forum">topics of a forum</a> <para>See <a href="https://corefork.telegram.org/method/channels.getForumTopics"/></para> <para>Possible <see cref="RpcException"/> codes: 400 (<a href="https://corefork.telegram.org/method/channels.getForumTopics#possible-errors">details</a>)</para></summary>
|
||||
/// <param name="peer">Supergroup or Bot peer</param>
|
||||
/// <param name="q">Search query</param>
|
||||
public async Task<Messages_ForumTopics> Channels_GetAllForumTopics(InputPeer peer, string q = null)
|
||||
{
|
||||
var result = await this.Messages_GetForumTopics(peer, offset_date: DateTime.MaxValue, q: q);
|
||||
if (result.topics.Length < result.count)
|
||||
{
|
||||
var topics = result.topics.ToList();
|
||||
var messages = result.messages.ToList();
|
||||
while (true)
|
||||
{
|
||||
var more_topics = await this.Messages_GetForumTopics(peer, messages[^1].Date, messages[^1].ID, topics[^1].ID);
|
||||
if (more_topics.topics.Length == 0) break;
|
||||
topics.AddRange(more_topics.topics);
|
||||
messages.AddRange(more_topics.messages);
|
||||
foreach (var kvp in more_topics.chats) result.chats[kvp.Key] = kvp.Value;
|
||||
foreach (var kvp in more_topics.users) result.users[kvp.Key] = kvp.Value;
|
||||
if (topics.Count >= more_topics.count) break;
|
||||
}
|
||||
result.topics = [.. topics];
|
||||
result.messages = [.. messages];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private const string OnlyChatChannel = "This method works on Chat & Channel only";
|
||||
/// <summary>Generic helper: Adds a single user to a Chat or Channel <para>See <a href="https://corefork.telegram.org/method/messages.addChatUser"/><br/> and <a href="https://corefork.telegram.org/method/channels.inviteToChannel"/></para> <para>Possible <see cref="RpcException"/> codes: 400,403</para></summary>
|
||||
/// <param name="peer">Chat/Channel</param>
|
||||
/// <param name="user">User to be added</param>
|
||||
public Task<UpdatesBase> AddChatUser(InputPeer peer, InputUserBase user) => peer switch
|
||||
public Task<Messages_InvitedUsers> AddChatUser(InputPeer peer, InputUserBase user) => peer switch
|
||||
{
|
||||
InputPeerChat chat => this.Messages_AddChatUser(chat.chat_id, user, int.MaxValue),
|
||||
InputPeerChannel channel => this.Channels_InviteToChannel(channel, user),
|
||||
|
|
@ -645,11 +717,11 @@ namespace WTelegram
|
|||
{
|
||||
case InputPeerChat chat:
|
||||
await this.Messages_EditChatAdmin(chat.chat_id, user, is_admin);
|
||||
return new Updates { date = DateTime.UtcNow, users = new(), updates = Array.Empty<Update>(),
|
||||
return new Updates { date = DateTime.UtcNow, users = [], updates = [],
|
||||
chats = (await this.Messages_GetChats(chat.chat_id)).chats };
|
||||
case InputPeerChannel channel:
|
||||
return await this.Channels_EditAdmin(channel, user,
|
||||
new ChatAdminRights { flags = is_admin ? (ChatAdminRights.Flags)0x8BF : 0 }, null);
|
||||
new ChatAdminRights { flags = is_admin ? (ChatAdminRights.Flags)0x1E8BF : 0 }, null);
|
||||
default:
|
||||
throw new ArgumentException(OnlyChatChannel);
|
||||
}
|
||||
|
|
@ -692,7 +764,7 @@ namespace WTelegram
|
|||
{
|
||||
case InputPeerChat chat:
|
||||
await this.Messages_DeleteChat(chat.chat_id);
|
||||
return new Updates { date = DateTime.UtcNow, users = new(), updates = Array.Empty<Update>(),
|
||||
return new Updates { date = DateTime.UtcNow, users = [], updates = [],
|
||||
chats = (await this.Messages_GetChats(chat.chat_id)).chats };
|
||||
case InputPeerChannel channel:
|
||||
return await this.Channels_DeleteChannel(channel);
|
||||
|
|
@ -701,6 +773,11 @@ namespace WTelegram
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>If you want to get all messages from a chat, use method Messages_GetHistory</summary>
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822")]
|
||||
public Task<Messages_MessagesBase> GetMessages(InputPeer peer)
|
||||
=> throw new WTException("If you want to get all messages from a chat, use method Messages_GetHistory");
|
||||
|
||||
/// <summary>Generic helper: Get individual messages by IDs [bots: ✓] <para>See <a href="https://corefork.telegram.org/method/messages.getMessages"/><br/> and <a href="https://corefork.telegram.org/method/channels.getMessages"/></para> <para>Possible <see cref="RpcException"/> codes: 400</para></summary>
|
||||
/// <param name="peer">User/Chat/Channel</param>
|
||||
/// <param name="id">IDs of messages to get</param>
|
||||
|
|
@ -718,6 +795,156 @@ namespace WTelegram
|
|||
/// <param name="max_id">If a positive value is passed, only messages with identifiers less or equal than the given one will be marked read</param>
|
||||
public async Task<bool> ReadHistory(InputPeer peer, int max_id = default)
|
||||
=> peer is InputPeerChannel channel ? await this.Channels_ReadHistory(channel, max_id) : (await this.Messages_ReadHistory(peer, max_id)) != null;
|
||||
#endregion
|
||||
|
||||
private static readonly char[] UrlSeparator = ['?', '#', '/'];
|
||||
|
||||
/// <summary>Return information about a chat/channel based on Invite Link or Public Link</summary>
|
||||
/// <param name="url">Public link or Invite link, like https://t.me/+InviteHash, https://t.me/joinchat/InviteHash or https://t.me/channelname<br/>Works also without https:// prefix</param>
|
||||
/// <param name="join"><see langword="true"/> to also join the chat/channel</param>
|
||||
/// <param name="chats">previously collected chats, to prevent unnecessary ResolveUsername</param>
|
||||
/// <returns>a Chat or Channel, possibly partial Channel information only (with flag <see cref="Channel.Flags.min"/>)</returns>
|
||||
public async Task<ChatBase> AnalyzeInviteLink(string url, bool join = false, IDictionary<long, ChatBase> chats = null)
|
||||
{
|
||||
int start = url.IndexOf("//");
|
||||
start = url.IndexOf('/', start + 2) + 1;
|
||||
int end = url.IndexOfAny(UrlSeparator, start);
|
||||
if (end == -1) end = url.Length;
|
||||
if (start == 0 || end == start) throw new ArgumentException("Invalid URL");
|
||||
string hash;
|
||||
if (url[start] == '+')
|
||||
hash = url[(start + 1)..end];
|
||||
else if (string.Compare(url, start, "joinchat/", 0, 9, StringComparison.OrdinalIgnoreCase) == 0)
|
||||
hash = url[(end + 1)..];
|
||||
else
|
||||
{
|
||||
var chat = await CachedOrResolveUsername(url[start..end], chats);
|
||||
if (join && chat is Channel channel)
|
||||
try
|
||||
{
|
||||
var res = await this.Channels_JoinChannel(channel);
|
||||
chat = res.Chats[channel.id];
|
||||
}
|
||||
catch (RpcException ex) when (ex.Code == 400 && ex.Message == "INVITE_REQUEST_SENT") { }
|
||||
return chat;
|
||||
}
|
||||
var chatInvite = await this.Messages_CheckChatInvite(hash);
|
||||
if (join)
|
||||
try
|
||||
{
|
||||
var res = await this.Messages_ImportChatInvite(hash);
|
||||
if (res.Chats.Values.FirstOrDefault() is ChatBase chat) return chat;
|
||||
}
|
||||
catch (RpcException ex) when (ex.Code == 400 && ex.Message == "INVITE_REQUEST_SENT") { }
|
||||
switch (chatInvite)
|
||||
{
|
||||
case ChatInviteAlready cia: return cia.chat;
|
||||
case ChatInvitePeek cip: return cip.chat;
|
||||
case ChatInvite ci:
|
||||
ChatPhoto chatPhoto = null;
|
||||
if (ci.photo is Photo photo)
|
||||
{
|
||||
var stripped_thumb = photo.sizes.OfType<PhotoStrippedSize>().FirstOrDefault()?.bytes;
|
||||
chatPhoto = new ChatPhoto
|
||||
{
|
||||
dc_id = photo.dc_id,
|
||||
photo_id = photo.id,
|
||||
stripped_thumb = stripped_thumb,
|
||||
flags = (stripped_thumb != null ? ChatPhoto.Flags.has_stripped_thumb : 0) |
|
||||
(photo.flags.HasFlag(Photo.Flags.has_video_sizes) ? ChatPhoto.Flags.has_video : 0),
|
||||
};
|
||||
}
|
||||
var rrAbout = ci.about == null ? null : new RestrictionReason[] { new() { text = ci.about } };
|
||||
return !ci.flags.HasFlag(ChatInvite.Flags.channel)
|
||||
? new Chat { title = ci.title, photo = chatPhoto, participants_count = ci.participants_count,
|
||||
flags = ci.flags.HasFlag(ChatInvite.Flags.request_needed) ? (Chat.Flags)Channel.Flags.join_request : 0 }
|
||||
: new Channel { title = ci.title, photo = chatPhoto, participants_count = ci.participants_count,
|
||||
restriction_reason = rrAbout, flags = Channel.Flags.min |
|
||||
(ci.flags.HasFlag(ChatInvite.Flags.broadcast) ? Channel.Flags.broadcast : 0) |
|
||||
(ci.flags.HasFlag(ChatInvite.Flags.public_) ? Channel.Flags.has_username : 0) |
|
||||
(ci.flags.HasFlag(ChatInvite.Flags.megagroup) ? Channel.Flags.megagroup : 0) |
|
||||
(ci.flags.HasFlag(ChatInvite.Flags.verified) ? Channel.Flags.verified : 0) |
|
||||
(ci.flags.HasFlag(ChatInvite.Flags.scam) ? Channel.Flags.scam : 0) |
|
||||
(ci.flags.HasFlag(ChatInvite.Flags.fake) ? Channel.Flags.fake : 0) |
|
||||
(ci.flags.HasFlag(ChatInvite.Flags.request_needed) ? Channel.Flags.join_request : 0) };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Return chat and message details based on a Message Link (URL)</summary>
|
||||
/// <param name="url">Message Link, like https://t.me/c/1234567890/1234 or t.me/channelname/1234</param>
|
||||
/// <param name="chats">previously collected chats, to prevent unnecessary ResolveUsername</param>
|
||||
/// <returns>Structure containing the message, chat and user details</returns>
|
||||
/// <remarks>If link is for private group (<c>t.me/c/..</c>), user must have joined that group</remarks>
|
||||
public async Task<Messages_ChannelMessages> GetMessageByLink(string url, IDictionary<long, ChatBase> chats = null)
|
||||
{
|
||||
int start = url.IndexOf("//");
|
||||
start = url.IndexOf('/', start + 2) + 1;
|
||||
int slash = url.IndexOf('/', start + 2);
|
||||
int msgStart = slash + 1;
|
||||
int end = url.IndexOfAny(UrlSeparator, msgStart);
|
||||
if (end == -1) end = url.Length;
|
||||
else if (url[end] == '/' && char.IsDigit(url[msgStart]) && url.Length > end + 1 && char.IsDigit(url[end + 1]))
|
||||
{
|
||||
end = url.IndexOfAny(UrlSeparator, msgStart = end + 1);
|
||||
if (end == -1) end = url.Length;
|
||||
}
|
||||
if (start == 0 || slash == -1 || end <= slash + 1 || !char.IsDigit(url[msgStart])) throw new ArgumentException("Invalid URL");
|
||||
int msgId = int.Parse(url[msgStart..end]);
|
||||
ChatBase chat;
|
||||
if (url[start] is 'c' or 'C' && url[start + 1] == '/')
|
||||
{
|
||||
long chatId = long.Parse(url[(start + 2)..slash]);
|
||||
if (chats?.TryGetValue(chatId, out chat) != true)
|
||||
{
|
||||
var mc = await this.Channels_GetChannels(new InputChannel(chatId, 0));
|
||||
if (!mc.chats.TryGetValue(chatId, out chat))
|
||||
throw new WTException($"Channel {chatId} not found");
|
||||
else if (chats != null)
|
||||
chats[chatId] = chat;
|
||||
}
|
||||
}
|
||||
else
|
||||
chat = await CachedOrResolveUsername(url[start..slash], chats);
|
||||
if (chat is not Channel channel) throw new WTException($"URL does not identify a valid Channel");
|
||||
return await this.Channels_GetMessages(channel, msgId) as Messages_ChannelMessages;
|
||||
}
|
||||
|
||||
private async Task<ChatBase> CachedOrResolveUsername(string username, IDictionary<long, ChatBase> chats = null)
|
||||
{
|
||||
if (chats == null)
|
||||
return (await this.Contacts_ResolveUsername(username)).Chat;
|
||||
ChatBase chat;
|
||||
lock (chats)
|
||||
chat = chats.Values.OfType<Channel>().FirstOrDefault(ch => ch.ActiveUsernames.Contains(username, StringComparer.OrdinalIgnoreCase));
|
||||
if (chat == null)
|
||||
{
|
||||
chat = (await this.Contacts_ResolveUsername(username)).Chat;
|
||||
if (chat != null) lock (chats) chats[chat.ID] = chat;
|
||||
}
|
||||
return chat;
|
||||
}
|
||||
|
||||
/// <summary>Receive updates for a given group/channel until cancellation is requested.</summary>
|
||||
/// <param name="channel">Group/channel to monitor without joining</param>
|
||||
/// <param name="ct">Cancel token to stop the monitoring</param>
|
||||
/// <remarks>After cancelling, you may still receive updates for a few more seconds</remarks>
|
||||
public async void OpenChat(InputChannel channel, CancellationToken ct)
|
||||
{
|
||||
var cts = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token, ct);
|
||||
try
|
||||
{
|
||||
while (!cts.IsCancellationRequested)
|
||||
{
|
||||
var diff = await this.Updates_GetChannelDifference(channel, null, 1, 1, true);
|
||||
var timeout = diff.Timeout * 1000;
|
||||
await Task.Delay(timeout != 0 ? timeout : 30000, cts.Token);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!cts.IsCancellationRequested)
|
||||
Console.WriteLine($"An exception occured for OpenChat {channel.channel_id}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
901
src/Client.cs
901
src/Client.cs
File diff suppressed because it is too large
Load diff
|
|
@ -6,11 +6,12 @@ using System.Linq;
|
|||
using System.Net;
|
||||
using System.Numerics;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#if NETCOREAPP2_1_OR_GREATER
|
||||
namespace WTelegram
|
||||
{
|
||||
static class Compat
|
||||
static partial class Compat
|
||||
{
|
||||
internal static BigInteger BigEndianInteger(byte[] value) => new(value, true, true);
|
||||
internal static IPEndPoint IPEndPoint_Parse(string addr) => IPEndPoint.Parse(addr);
|
||||
|
|
@ -19,7 +20,7 @@ namespace WTelegram
|
|||
#else // Compatibility shims for methods missing in netstandard2.0:
|
||||
namespace WTelegram
|
||||
{
|
||||
static class Compat
|
||||
static partial class Compat
|
||||
{
|
||||
internal static BigInteger BigEndianInteger(byte[] value)
|
||||
{
|
||||
|
|
@ -47,7 +48,7 @@ namespace WTelegram
|
|||
return length;
|
||||
}
|
||||
|
||||
public static V GetValueOrDefault<K, V>(this Dictionary<K, V> dictionary, K key, V defaultValue = default)
|
||||
public static V GetValueOrDefault<K, V>(this IReadOnlyDictionary<K, V> dictionary, K key, V defaultValue = default)
|
||||
=> dictionary.TryGetValue(key, out V value) ? value : defaultValue;
|
||||
|
||||
public static void Deconstruct<K, V>(this KeyValuePair<K, V> kvp, out K key, out V value) { key = kvp.Key; value = kvp.Value; }
|
||||
|
|
@ -76,6 +77,13 @@ static class Convert
|
|||
internal static string ToHexString(byte[] data) => BitConverter.ToString(data).Replace("-", "");
|
||||
internal static byte[] FromHexString(string hex) => Enumerable.Range(0, hex.Length / 2).Select(i => System.Convert.ToByte(hex.Substring(i * 2, 2), 16)).ToArray();
|
||||
}
|
||||
public class RandomNumberGenerator
|
||||
{
|
||||
internal static readonly RNGCryptoServiceProvider RNG = new();
|
||||
public static RandomNumberGenerator Create() => new();
|
||||
public void GetBytes(byte[] data) => RNG.GetBytes(data);
|
||||
public void GetBytes(byte[] data, int offset, int count) => RNG.GetBytes(data, offset, count);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if NETSTANDARD2_0
|
||||
|
|
@ -87,7 +95,7 @@ namespace System.Runtime.CompilerServices
|
|||
{
|
||||
if (array == null) throw new ArgumentNullException();
|
||||
var (offset, length) = range.GetOffsetAndLength(array.Length);
|
||||
if (length == 0) return Array.Empty<T>();
|
||||
if (length == 0) return [];
|
||||
var dest = typeof(T).IsValueType || typeof(T[]) == array.GetType() ? new T[length]
|
||||
: (T[])Array.CreateInstance(array.GetType().GetElementType()!, length);
|
||||
Array.Copy(array, offset, dest, 0, length);
|
||||
|
|
@ -97,4 +105,17 @@ namespace System.Runtime.CompilerServices
|
|||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
internal class IsExternalInit { }
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace WTelegram
|
||||
{
|
||||
static partial class Compat
|
||||
{
|
||||
internal static Task WaitAsync(this Task source, int timeout)
|
||||
#if NET8_0_OR_GREATER
|
||||
=> source?.WaitAsync(TimeSpan.FromMilliseconds(timeout)) ?? Task.CompletedTask;
|
||||
#else
|
||||
=> source == null ? Task.CompletedTask : Task.WhenAny(source, Task.Delay(timeout));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,17 +13,17 @@ using static WTelegram.Compat;
|
|||
|
||||
namespace WTelegram
|
||||
{
|
||||
internal static class Encryption
|
||||
public static class Encryption
|
||||
{
|
||||
private static readonly Dictionary<long, RSAPublicKey> PublicKeys = new();
|
||||
internal static readonly RNGCryptoServiceProvider RNG = new();
|
||||
private static readonly Dictionary<long, RSAPublicKey> PublicKeys = [];
|
||||
internal static readonly RandomNumberGenerator RNG = RandomNumberGenerator.Create();
|
||||
internal static readonly Aes AesECB = Aes.Create();
|
||||
|
||||
static Encryption()
|
||||
{
|
||||
AesECB.Mode = CipherMode.ECB;
|
||||
AesECB.Padding = PaddingMode.Zeros;
|
||||
if (AesECB.BlockSize != 128) throw new ApplicationException("AES Blocksize is not 16 bytes");
|
||||
if (AesECB.BlockSize != 128) throw new WTException("AES Blocksize is not 16 bytes");
|
||||
}
|
||||
|
||||
internal static async Task CreateAuthorizationKey(Client client, Session.DCSession session)
|
||||
|
|
@ -33,12 +33,12 @@ namespace WTelegram
|
|||
var sha256 = SHA256.Create();
|
||||
|
||||
//1)
|
||||
var nonce = new Int128(RNG);
|
||||
var nonce = new TL.Int128(RNG);
|
||||
var resPQ = await client.ReqPqMulti(nonce);
|
||||
//2)
|
||||
if (resPQ.nonce != nonce) throw new ApplicationException("Nonce mismatch");
|
||||
if (resPQ.nonce != nonce) throw new WTException("Nonce mismatch");
|
||||
var fingerprint = resPQ.server_public_key_fingerprints.FirstOrDefault(PublicKeys.ContainsKey);
|
||||
if (fingerprint == 0) throw new ApplicationException("Couldn't match any server_public_key_fingerprints");
|
||||
if (fingerprint == 0) throw new WTException("Couldn't match any server_public_key_fingerprints");
|
||||
var publicKey = PublicKeys[fingerprint];
|
||||
Helpers.Log(2, $"Selected public key with fingerprint {fingerprint:X}");
|
||||
//3)
|
||||
|
|
@ -73,7 +73,7 @@ namespace WTelegram
|
|||
clearStream.Write(aes_key, 0, 32); // write aes_key as prefix for initial Sha256 computation
|
||||
writer.WriteTLObject(pqInnerData);
|
||||
int clearLength = (int)clearStream.Position - 32; // length before padding
|
||||
if (clearLength > 144) throw new ApplicationException("PQInnerData too big");
|
||||
if (clearLength > 144) throw new WTException("PQInnerData too big");
|
||||
byte[] clearBuffer = clearStream.GetBuffer();
|
||||
RNG.GetBytes(clearBuffer, 32 + clearLength, 192 - clearLength);
|
||||
sha256.ComputeHash(clearBuffer, 0, 32 + 192).CopyTo(clearBuffer, 224); // append Sha256
|
||||
|
|
@ -91,28 +91,28 @@ namespace WTelegram
|
|||
var serverDHparams = await client.ReqDHParams(pqInnerData.nonce, pqInnerData.server_nonce, pqInnerData.p, pqInnerData.q, fingerprint, encrypted_data);
|
||||
//5)
|
||||
var localTime = DateTimeOffset.UtcNow;
|
||||
if (serverDHparams is not ServerDHParamsOk serverDHparamsOk) throw new ApplicationException("not server_DH_params_ok");
|
||||
if (serverDHparamsOk.nonce != nonce) throw new ApplicationException("Nonce mismatch");
|
||||
if (serverDHparamsOk.server_nonce != resPQ.server_nonce) throw new ApplicationException("Server Nonce mismatch");
|
||||
var (tmp_aes_key, tmp_aes_iv) = ConstructTmpAESKeyIV(resPQ.server_nonce, pqInnerData.new_nonce);
|
||||
if (serverDHparams is not ServerDHParamsOk serverDHparamsOk) throw new WTException("not server_DH_params_ok");
|
||||
if (serverDHparamsOk.nonce != nonce) throw new WTException("Nonce mismatch");
|
||||
if (serverDHparamsOk.server_nonce != resPQ.server_nonce) throw new WTException("Server Nonce mismatch");
|
||||
var (tmp_aes_key, tmp_aes_iv) = ConstructTmpAESKeyIV(sha1, resPQ.server_nonce, pqInnerData.new_nonce);
|
||||
var answer = AES_IGE_EncryptDecrypt(serverDHparamsOk.encrypted_answer, tmp_aes_key, tmp_aes_iv, false);
|
||||
|
||||
using var answerReader = new TL.BinaryReader(new MemoryStream(answer), client);
|
||||
using var answerReader = new BinaryReader(new MemoryStream(answer));
|
||||
var answerHash = answerReader.ReadBytes(20);
|
||||
var answerObj = answerReader.ReadTLObject();
|
||||
if (answerObj is not ServerDHInnerData serverDHinnerData) throw new ApplicationException("not server_DH_inner_data");
|
||||
if (answerObj is not ServerDHInnerData serverDHinnerData) throw new WTException("not server_DH_inner_data");
|
||||
long padding = answerReader.BaseStream.Length - answerReader.BaseStream.Position;
|
||||
if (padding >= 16) throw new ApplicationException("Too much pad");
|
||||
if (padding >= 16) throw new WTException("Too much pad");
|
||||
if (!Enumerable.SequenceEqual(sha1.ComputeHash(answer, 20, answer.Length - (int)padding - 20), answerHash))
|
||||
throw new ApplicationException("Answer SHA1 mismatch");
|
||||
if (serverDHinnerData.nonce != nonce) throw new ApplicationException("Nonce mismatch");
|
||||
if (serverDHinnerData.server_nonce != resPQ.server_nonce) throw new ApplicationException("Server Nonce mismatch");
|
||||
throw new WTException("Answer SHA1 mismatch");
|
||||
if (serverDHinnerData.nonce != nonce) throw new WTException("Nonce mismatch");
|
||||
if (serverDHinnerData.server_nonce != resPQ.server_nonce) throw new WTException("Server Nonce mismatch");
|
||||
var g_a = BigEndianInteger(serverDHinnerData.g_a);
|
||||
var dh_prime = BigEndianInteger(serverDHinnerData.dh_prime);
|
||||
CheckGoodPrime(dh_prime, serverDHinnerData.g);
|
||||
session.LastSentMsgId = 0;
|
||||
session.ServerTicksOffset = (serverDHinnerData.server_time - localTime).Ticks;
|
||||
Helpers.Log(1, $"Time offset: {session.ServerTicksOffset} | Server: {serverDHinnerData.server_time.TimeOfDay} UTC | Local: {localTime.TimeOfDay} UTC");
|
||||
session.lastSentMsgId = 0;
|
||||
session.serverTicksOffset = (serverDHinnerData.server_time - localTime).Ticks;
|
||||
Helpers.Log(1, $"Time offset: {session.serverTicksOffset} | Server: {serverDHinnerData.server_time.TimeOfDay} UTC | Local: {localTime.TimeOfDay} UTC");
|
||||
//6)
|
||||
var salt = new byte[256];
|
||||
RNG.GetBytes(salt);
|
||||
|
|
@ -149,46 +149,47 @@ namespace WTelegram
|
|||
var authKeyHash = sha1.ComputeHash(authKey);
|
||||
retry_id = BinaryPrimitives.ReadInt64LittleEndian(authKeyHash); // (auth_key_aux_hash)
|
||||
//9)
|
||||
if (setClientDHparamsAnswer is not DhGenOk dhGenOk) throw new ApplicationException("not dh_gen_ok");
|
||||
if (dhGenOk.nonce != nonce) throw new ApplicationException("Nonce mismatch");
|
||||
if (dhGenOk.server_nonce != resPQ.server_nonce) throw new ApplicationException("Server Nonce mismatch");
|
||||
if (setClientDHparamsAnswer is not DhGenOk dhGenOk) throw new WTException("not dh_gen_ok");
|
||||
if (dhGenOk.nonce != nonce) throw new WTException("Nonce mismatch");
|
||||
if (dhGenOk.server_nonce != resPQ.server_nonce) throw new WTException("Server Nonce mismatch");
|
||||
var expected_new_nonceN = new byte[32 + 1 + 8];
|
||||
pqInnerData.new_nonce.raw.CopyTo(expected_new_nonceN, 0);
|
||||
expected_new_nonceN[32] = 1;
|
||||
Array.Copy(authKeyHash, 0, expected_new_nonceN, 33, 8); // (auth_key_aux_hash)
|
||||
if (!Enumerable.SequenceEqual(dhGenOk.new_nonce_hash1.raw, sha1.ComputeHash(expected_new_nonceN).Skip(4)))
|
||||
throw new ApplicationException("setClientDHparamsAnswer.new_nonce_hashN mismatch");
|
||||
throw new WTException("setClientDHparamsAnswer.new_nonce_hashN mismatch");
|
||||
|
||||
session.AuthKeyID = BinaryPrimitives.ReadInt64LittleEndian(authKeyHash.AsSpan(12));
|
||||
session.authKeyID = BinaryPrimitives.ReadInt64LittleEndian(authKeyHash.AsSpan(12));
|
||||
session.AuthKey = authKey;
|
||||
session.Salt = BinaryPrimitives.ReadInt64LittleEndian(pqInnerData.new_nonce.raw) ^ BinaryPrimitives.ReadInt64LittleEndian(resPQ.server_nonce.raw);
|
||||
session.OldSalt = session.Salt;
|
||||
}
|
||||
|
||||
(byte[] key, byte[] iv) ConstructTmpAESKeyIV(Int128 server_nonce, Int256 new_nonce)
|
||||
{
|
||||
byte[] tmp_aes_key = new byte[32], tmp_aes_iv = new byte[32];
|
||||
sha1.TransformBlock(new_nonce, 0, 32, null, 0);
|
||||
sha1.TransformFinalBlock(server_nonce, 0, 16);
|
||||
sha1.Hash.CopyTo(tmp_aes_key, 0); // tmp_aes_key := SHA1(new_nonce + server_nonce)
|
||||
sha1.Initialize();
|
||||
sha1.TransformBlock(server_nonce, 0, 16, null, 0);
|
||||
sha1.TransformFinalBlock(new_nonce, 0, 32);
|
||||
Array.Copy(sha1.Hash, 0, tmp_aes_key, 20, 12); // + SHA1(server_nonce, new_nonce)[0:12]
|
||||
Array.Copy(sha1.Hash, 12, tmp_aes_iv, 0, 8); // tmp_aes_iv != SHA1(server_nonce, new_nonce)[12:8]
|
||||
sha1.Initialize();
|
||||
sha1.TransformBlock(new_nonce, 0, 32, null, 0);
|
||||
sha1.TransformFinalBlock(new_nonce, 0, 32);
|
||||
sha1.Hash.CopyTo(tmp_aes_iv, 8); // + SHA(new_nonce + new_nonce)
|
||||
Array.Copy(new_nonce, 0, tmp_aes_iv, 28, 4); // + new_nonce[0:4]
|
||||
sha1.Initialize();
|
||||
return (tmp_aes_key, tmp_aes_iv);
|
||||
}
|
||||
public static (byte[] key, byte[] iv) ConstructTmpAESKeyIV(SHA1 sha1, TL.Int128 server_nonce, Int256 new_nonce)
|
||||
{
|
||||
byte[] tmp_aes_key = new byte[32], tmp_aes_iv = new byte[32];
|
||||
sha1.TransformBlock(new_nonce, 0, 32, null, 0);
|
||||
sha1.TransformFinalBlock(server_nonce, 0, 16);
|
||||
sha1.Hash.CopyTo(tmp_aes_key, 0); // tmp_aes_key := SHA1(new_nonce + server_nonce)
|
||||
sha1.Initialize();
|
||||
sha1.TransformBlock(server_nonce, 0, 16, null, 0);
|
||||
sha1.TransformFinalBlock(new_nonce, 0, 32);
|
||||
Array.Copy(sha1.Hash, 0, tmp_aes_key, 20, 12); // + SHA1(server_nonce, new_nonce)[0:12]
|
||||
Array.Copy(sha1.Hash, 12, tmp_aes_iv, 0, 8); // tmp_aes_iv != SHA1(server_nonce, new_nonce)[12:8]
|
||||
sha1.Initialize();
|
||||
sha1.TransformBlock(new_nonce, 0, 32, null, 0);
|
||||
sha1.TransformFinalBlock(new_nonce, 0, 32);
|
||||
sha1.Hash.CopyTo(tmp_aes_iv, 8); // + SHA(new_nonce + new_nonce)
|
||||
Array.Copy(new_nonce, 0, tmp_aes_iv, 28, 4); // + new_nonce[0:4]
|
||||
sha1.Initialize();
|
||||
return (tmp_aes_key, tmp_aes_iv);
|
||||
}
|
||||
|
||||
internal static void CheckGoodPrime(BigInteger p, int g)
|
||||
{
|
||||
Helpers.Log(2, "Verifying encryption key safety... (this should happen only once per DC)");
|
||||
// check that 2^2047 <= p < 2^2048
|
||||
if (p.GetBitLength() != 2048) throw new ApplicationException("p is not 2048-bit number");
|
||||
if (p.GetBitLength() != 2048) throw new WTException("p is not 2048-bit number");
|
||||
// check that g generates a cyclic subgroup of prime order (p - 1) / 2, i.e. is a quadratic residue mod p.
|
||||
if (g switch
|
||||
{
|
||||
|
|
@ -200,16 +201,16 @@ namespace WTelegram
|
|||
7 => (int)(p % 7) is not 3 and not 5 and not 6,
|
||||
_ => true,
|
||||
})
|
||||
throw new ApplicationException("Bad prime mod 4g");
|
||||
throw new WTException("Bad prime mod 4g");
|
||||
// check whether p is a safe prime (meaning that both p and (p - 1) / 2 are prime)
|
||||
if (SafePrimes.Contains(p)) return;
|
||||
if (!p.IsProbablePrime()) throw new ApplicationException("p is not a prime number");
|
||||
if (!((p - 1) / 2).IsProbablePrime()) throw new ApplicationException("(p - 1) / 2 is not a prime number");
|
||||
if (!p.IsProbablePrime()) throw new WTException("p is not a prime number");
|
||||
if (!((p - 1) / 2).IsProbablePrime()) throw new WTException("(p - 1) / 2 is not a prime number");
|
||||
SafePrimes.Add(p);
|
||||
}
|
||||
|
||||
private static readonly HashSet<BigInteger> SafePrimes = new() { new(new byte[] // C71CAEB9C6B1C904...
|
||||
{
|
||||
private static readonly HashSet<BigInteger> SafePrimes = [ new( // C71CAEB9C6B1C904...
|
||||
[
|
||||
0x5B, 0xCC, 0x2F, 0xB9, 0xE3, 0xD8, 0x9C, 0x11, 0x03, 0x04, 0xB1, 0x34, 0xF0, 0xAD, 0x4F, 0x6F,
|
||||
0xBF, 0x54, 0x24, 0x4B, 0xD0, 0x15, 0x4E, 0x2E, 0xEE, 0x05, 0xB1, 0x35, 0xF6, 0x15, 0x81, 0x0D,
|
||||
0x1F, 0x85, 0x29, 0xE9, 0x0C, 0x85, 0x56, 0xD9, 0x59, 0xF9, 0x7B, 0xF4, 0x49, 0x28, 0xED, 0x0D,
|
||||
|
|
@ -226,16 +227,18 @@ namespace WTelegram
|
|||
0xDB, 0xF4, 0x30, 0x25, 0xD2, 0x93, 0x94, 0x22, 0x58, 0x40, 0xC1, 0xA7, 0x0A, 0x8A, 0x19, 0x48,
|
||||
0x0F, 0x93, 0x3D, 0x56, 0x37, 0xD0, 0x34, 0x49, 0xC1, 0x21, 0x3E, 0x8E, 0x23, 0x40, 0x0D, 0x98,
|
||||
0x73, 0x3F, 0xF1, 0x70, 0x2F, 0x52, 0x6C, 0x8E, 0x04, 0xC9, 0xB1, 0xC6, 0xB9, 0xAE, 0x1C, 0xC7, 0x00
|
||||
})};
|
||||
])];
|
||||
|
||||
internal static void CheckGoodGaAndGb(BigInteger g, BigInteger dh_prime)
|
||||
{
|
||||
// check that g, g_a and g_b are greater than 1 and less than dh_prime - 1.
|
||||
// We recommend checking that g_a and g_b are between 2^{2048-64} and dh_prime - 2^{2048-64} as well.
|
||||
if (g.GetBitLength() < 2048 - 64 || (dh_prime - g).GetBitLength() < 2048 - 64)
|
||||
throw new ApplicationException("g^a or g^b is not between 2^{2048-64} and dh_prime - 2^{2048-64}");
|
||||
throw new WTException("g^a or g^b is not between 2^{2048-64} and dh_prime - 2^{2048-64}");
|
||||
}
|
||||
|
||||
/// <summary>Load a specific Telegram server public key</summary>
|
||||
/// <param name="pem">A string starting with <c>-----BEGIN RSA PUBLIC KEY-----</c></param>
|
||||
public static void LoadPublicKey(string pem)
|
||||
{
|
||||
using var rsa = RSA.Create();
|
||||
|
|
@ -244,10 +247,7 @@ namespace WTelegram
|
|||
var rsaParam = rsa.ExportParameters(false);
|
||||
if (rsaParam.Modulus[0] == 0) rsaParam.Modulus = rsaParam.Modulus[1..];
|
||||
var publicKey = new RSAPublicKey { n = rsaParam.Modulus, e = rsaParam.Exponent };
|
||||
using var memStream = new MemoryStream(280);
|
||||
using (var writer = new BinaryWriter(memStream))
|
||||
writer.WriteTLObject(publicKey);
|
||||
var bareData = memStream.ToArray();
|
||||
var bareData = publicKey.ToBytes();
|
||||
var fingerprint = BinaryPrimitives.ReadInt64LittleEndian(sha1.ComputeHash(bareData, 4, bareData.Length - 4).AsSpan(12)); // 64 lower-order bits of SHA1
|
||||
PublicKeys[fingerprint] = publicKey;
|
||||
Helpers.Log(1, $"Loaded a public key with fingerprint {fingerprint:X}");
|
||||
|
|
@ -275,7 +275,7 @@ j4WcDuXc2CTHgH8gFTNhp/Y8/SpDOhvn9QIDAQAB
|
|||
-----END RSA PUBLIC KEY-----");
|
||||
}
|
||||
|
||||
internal static byte[] EncryptDecryptMessage(Span<byte> input, bool encrypt, int x, byte[] authKey, byte[] msgKey, int msgKeyOffset, SHA256 sha256)
|
||||
public static byte[] EncryptDecryptMessage(Span<byte> input, bool encrypt, int x, byte[] authKey, byte[] msgKey, int msgKeyOffset, SHA256 sha256)
|
||||
{
|
||||
// first, construct AES key & IV
|
||||
byte[] aes_key = new byte[32], aes_iv = new byte[32];
|
||||
|
|
@ -296,16 +296,16 @@ j4WcDuXc2CTHgH8gFTNhp/Y8/SpDOhvn9QIDAQAB
|
|||
return AES_IGE_EncryptDecrypt(input, aes_key, aes_iv, encrypt);
|
||||
}
|
||||
|
||||
internal static byte[] AES_IGE_EncryptDecrypt(Span<byte> input, byte[] aes_key, byte[] aes_iv, bool encrypt)
|
||||
public static byte[] AES_IGE_EncryptDecrypt(Span<byte> input, byte[] aes_key, byte[] aes_iv, bool encrypt)
|
||||
{
|
||||
if (input.Length % 16 != 0) throw new ApplicationException("AES_IGE input size not divisible by 16");
|
||||
if (input.Length % 16 != 0) throw new WTException("AES_IGE input size not divisible by 16");
|
||||
|
||||
using var aesCrypto = encrypt ? AesECB.CreateEncryptor(aes_key, null) : AesECB.CreateDecryptor(aes_key, null);
|
||||
var output = new byte[input.Length];
|
||||
var prevBytes = (byte[])aes_iv.Clone();
|
||||
var span = MemoryMarshal.Cast<byte, long>(input);
|
||||
var sout = MemoryMarshal.Cast<byte, long>(output);
|
||||
var prev = MemoryMarshal.Cast<byte, long>(prevBytes);
|
||||
var sout = MemoryMarshal.Cast<byte, long>(output.AsSpan());
|
||||
var prev = MemoryMarshal.Cast<byte, long>(prevBytes.AsSpan());
|
||||
if (!encrypt) { (prev[2], prev[0]) = (prev[0], prev[2]); (prev[3], prev[1]) = (prev[1], prev[3]); }
|
||||
for (int i = 0, count = input.Length / 8; i < count;)
|
||||
{
|
||||
|
|
@ -318,33 +318,26 @@ j4WcDuXc2CTHgH8gFTNhp/Y8/SpDOhvn9QIDAQAB
|
|||
}
|
||||
|
||||
#if OBFUSCATION
|
||||
internal class AesCtr : IDisposable
|
||||
public sealed class AesCtr(byte[] key, byte[] ivec) : IDisposable
|
||||
{
|
||||
readonly ICryptoTransform encryptor;
|
||||
readonly byte[] ivec;
|
||||
readonly byte[] ecount = new byte[16];
|
||||
int num;
|
||||
readonly ICryptoTransform _encryptor = AesECB.CreateEncryptor(key, null);
|
||||
readonly byte[] _ecount = new byte[16];
|
||||
int _num;
|
||||
|
||||
public AesCtr(byte[] key, byte[] iv)
|
||||
public void Dispose() => _encryptor.Dispose();
|
||||
|
||||
public void EncryptDecrypt(Span<byte> buffer)
|
||||
{
|
||||
encryptor = AesECB.CreateEncryptor(key, null);
|
||||
ivec = iv;
|
||||
}
|
||||
|
||||
public void Dispose() => encryptor.Dispose();
|
||||
|
||||
public void EncryptDecrypt(byte[] buffer, int length)
|
||||
{
|
||||
for (int i = 0; i < length; i++)
|
||||
for (int i = 0; i < buffer.Length; i++)
|
||||
{
|
||||
if (num == 0)
|
||||
if (_num == 0)
|
||||
{
|
||||
encryptor.TransformBlock(ivec, 0, 16, ecount, 0);
|
||||
_encryptor.TransformBlock(ivec, 0, 16, _ecount, 0);
|
||||
for (int n = 15; n >= 0; n--) // increment big-endian counter
|
||||
if (++ivec[n] != 0) break;
|
||||
}
|
||||
buffer[i] ^= ecount[num];
|
||||
num = (num + 1) % 16;
|
||||
buffer[i] ^= _ecount[_num];
|
||||
_num = (_num + 1) % 16;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -379,7 +372,7 @@ j4WcDuXc2CTHgH8gFTNhp/Y8/SpDOhvn9QIDAQAB
|
|||
var sendCtr = new AesCtr(sendKey, sendIV);
|
||||
var recvCtr = new AesCtr(recvKey, recvIV);
|
||||
var encrypted = (byte[])preamble.Clone();
|
||||
sendCtr.EncryptDecrypt(encrypted, 64);
|
||||
sendCtr.EncryptDecrypt(encrypted);
|
||||
for (int i = 56; i < 64; i++)
|
||||
preamble[i] = encrypted[i];
|
||||
return (sendCtr, recvCtr, preamble);
|
||||
|
|
@ -396,7 +389,7 @@ j4WcDuXc2CTHgH8gFTNhp/Y8/SpDOhvn9QIDAQAB
|
|||
RNG.GetBytes(algo.salt1, salt1len, 32);
|
||||
}
|
||||
else
|
||||
throw new ApplicationException("2FA authentication uses an unsupported algo: " + accountPassword.current_algo?.GetType().Name);
|
||||
throw new WTException("2FA authentication uses an unsupported algo: " + accountPassword.current_algo?.GetType().Name);
|
||||
|
||||
var g = new BigInteger(algo.g);
|
||||
var p = BigEndianInteger(algo.p);
|
||||
|
|
@ -524,17 +517,17 @@ j4WcDuXc2CTHgH8gFTNhp/Y8/SpDOhvn9QIDAQAB
|
|||
#endif
|
||||
}
|
||||
|
||||
internal class AES_IGE_Stream : Helpers.IndirectStream
|
||||
internal sealed class AES_IGE_Stream : Helpers.IndirectStream
|
||||
{
|
||||
private readonly ICryptoTransform aesCrypto;
|
||||
private readonly byte[] prevBytes;
|
||||
private readonly ICryptoTransform _aesCrypto;
|
||||
private readonly byte[] _prevBytes;
|
||||
|
||||
public AES_IGE_Stream(Stream stream, long size, byte[] key, byte[] iv) : this(stream, key, iv, false) { ContentLength = size; }
|
||||
public AES_IGE_Stream(Stream stream, byte[] key, byte[] iv, bool encrypt) : base(stream)
|
||||
{
|
||||
aesCrypto = encrypt ? Encryption.AesECB.CreateEncryptor(key, null) : Encryption.AesECB.CreateDecryptor(key, null);
|
||||
if (encrypt) prevBytes = (byte[])iv.Clone();
|
||||
else { prevBytes = new byte[32]; Array.Copy(iv, 0, prevBytes, 16, 16); Array.Copy(iv, 16, prevBytes, 0, 16); }
|
||||
_aesCrypto = encrypt ? Encryption.AesECB.CreateEncryptor(key, null) : Encryption.AesECB.CreateDecryptor(key, null);
|
||||
if (encrypt) _prevBytes = (byte[])iv.Clone();
|
||||
else { _prevBytes = new byte[32]; Array.Copy(iv, 0, _prevBytes, 16, 16); Array.Copy(iv, 16, _prevBytes, 0, 16); }
|
||||
}
|
||||
|
||||
public override long Length => base.Length + 15 & ~15;
|
||||
|
|
@ -563,11 +556,11 @@ j4WcDuXc2CTHgH8gFTNhp/Y8/SpDOhvn9QIDAQAB
|
|||
{
|
||||
count = count + 15 & ~15;
|
||||
var span = MemoryMarshal.Cast<byte, long>(buffer.AsSpan(offset, count));
|
||||
var prev = MemoryMarshal.Cast<byte, long>(prevBytes);
|
||||
var prev = MemoryMarshal.Cast<byte, long>(_prevBytes.AsSpan());
|
||||
for (offset = 0, count /= 8; offset < count;)
|
||||
{
|
||||
prev[0] ^= span[offset]; prev[1] ^= span[offset + 1];
|
||||
aesCrypto.TransformBlock(prevBytes, 0, 16, prevBytes, 0);
|
||||
_aesCrypto.TransformBlock(_prevBytes, 0, 16, _prevBytes, 0);
|
||||
prev[0] ^= prev[2]; prev[1] ^= prev[3];
|
||||
prev[2] = span[offset]; prev[3] = span[offset + 1];
|
||||
span[offset++] = prev[0]; span[offset++] = prev[1];
|
||||
|
|
|
|||
117
src/Helpers.cs
117
src/Helpers.cs
|
|
@ -4,9 +4,18 @@ using System.IO;
|
|||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#if NET8_0_OR_GREATER
|
||||
[JsonSerializable(typeof(WTelegram.Session))]
|
||||
[JsonSerializable(typeof(Dictionary<long, WTelegram.UpdateManager.MBoxState>))]
|
||||
[JsonSerializable(typeof(IDictionary<long, WTelegram.UpdateManager.MBoxState>))]
|
||||
[JsonSerializable(typeof(System.Collections.Immutable.ImmutableDictionary<long, WTelegram.UpdateManager.MBoxState>))]
|
||||
internal partial class WTelegramContext : JsonSerializerContext { }
|
||||
#endif
|
||||
|
||||
namespace WTelegram
|
||||
{
|
||||
public static class Helpers
|
||||
|
|
@ -16,10 +25,93 @@ namespace WTelegram
|
|||
|
||||
/// <summary>For serializing indented Json with fields included</summary>
|
||||
public static readonly JsonSerializerOptions JsonOptions = new() { IncludeFields = true, WriteIndented = true,
|
||||
IgnoreReadOnlyProperties = true, DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull };
|
||||
#if NET8_0_OR_GREATER
|
||||
TypeInfoResolver = JsonSerializer.IsReflectionEnabledByDefault ? null : WTelegramContext.Default,
|
||||
Converters = { new TLJsonConverter(), new JsonStringEnumConverter() },
|
||||
#endif
|
||||
IgnoreReadOnlyProperties = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault };
|
||||
|
||||
private static readonly ConsoleColor[] LogLevelToColor = new[] { ConsoleColor.DarkGray, ConsoleColor.DarkCyan,
|
||||
ConsoleColor.Cyan, ConsoleColor.Yellow, ConsoleColor.Red, ConsoleColor.Magenta, ConsoleColor.DarkBlue };
|
||||
#if NET8_0_OR_GREATER
|
||||
public sealed class TLJsonConverter : JsonConverter<object>
|
||||
{
|
||||
public override bool CanConvert(Type typeToConvert)
|
||||
=> typeToConvert.IsAbstract || typeToConvert == typeof(Dictionary<long, TL.User>) || typeToConvert == typeof(Dictionary<long, TL.ChatBase>);
|
||||
public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (typeToConvert == typeof(Dictionary<long, TL.User>))
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.StartArray) throw new JsonException("Expected array for users dictionary");
|
||||
var users = new Dictionary<long, TL.User>();
|
||||
while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
|
||||
{
|
||||
var user = JsonSerializer.Deserialize<TL.User>(ref reader, options);
|
||||
if (user != null) users[user.id] = user;
|
||||
}
|
||||
return users;
|
||||
}
|
||||
else if (typeToConvert == typeof(Dictionary<long, TL.ChatBase>))
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.StartArray) throw new JsonException("Expected array for chats dictionary");
|
||||
var chats = new Dictionary<long, TL.ChatBase>();
|
||||
while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
|
||||
{
|
||||
var chat = (TL.ChatBase)Read(ref reader, typeof(TL.ChatBase), options);
|
||||
if (chat != null) chats[chat.ID] = chat;
|
||||
}
|
||||
return chats;
|
||||
}
|
||||
else if (reader.TokenType == JsonTokenType.Null)
|
||||
return null;
|
||||
else if (reader.TokenType == JsonTokenType.StartObject)
|
||||
{
|
||||
var typeReader = reader;
|
||||
if (!typeReader.Read() || typeReader.TokenType != JsonTokenType.PropertyName || typeReader.GetString() != "$")
|
||||
throw new JsonException("Expected $ type property");
|
||||
if (!typeReader.Read() || typeReader.TokenType != JsonTokenType.String)
|
||||
throw new JsonException("Invalid $ type property");
|
||||
var type = typeReader.GetString();
|
||||
var actualType = typeToConvert.Assembly.GetType("TL." + type);
|
||||
if (!typeToConvert.IsAssignableFrom(actualType))
|
||||
throw new JsonException($"Incompatible $ type: {type} -> {typeToConvert}");
|
||||
return JsonSerializer.Deserialize(ref reader, actualType, options);
|
||||
}
|
||||
throw new JsonException($"Unexpected token type: {reader.TokenType}");
|
||||
}
|
||||
public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value is Dictionary<long, TL.User> users)
|
||||
{
|
||||
writer.WriteStartArray();
|
||||
foreach (var element in users.Values)
|
||||
JsonSerializer.Serialize(writer, element, options);
|
||||
writer.WriteEndArray();
|
||||
}
|
||||
else if (value is Dictionary<long, TL.ChatBase> chats)
|
||||
{
|
||||
writer.WriteStartArray();
|
||||
foreach (var element in chats.Values)
|
||||
Write(writer, element, options);
|
||||
writer.WriteEndArray();
|
||||
}
|
||||
else if (value is null)
|
||||
writer.WriteNullValue();
|
||||
else
|
||||
{
|
||||
var actualType = value.GetType();
|
||||
var jsonObject = JsonSerializer.SerializeToElement(value, actualType, options);
|
||||
writer.WriteStartObject();
|
||||
writer.WriteString("$", actualType.Name);
|
||||
foreach (var property in jsonObject.EnumerateObject())
|
||||
if (char.IsLower(property.Name[0]))
|
||||
property.WriteTo(writer);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
private static readonly ConsoleColor[] LogLevelToColor = [ ConsoleColor.DarkGray, ConsoleColor.DarkCyan,
|
||||
ConsoleColor.Cyan, ConsoleColor.Yellow, ConsoleColor.Red, ConsoleColor.Magenta, ConsoleColor.DarkBlue ];
|
||||
private static void DefaultLogger(int level, string message)
|
||||
{
|
||||
Console.ForegroundColor = LogLevelToColor[level];
|
||||
|
|
@ -186,7 +278,7 @@ namespace WTelegram
|
|||
}
|
||||
|
||||
internal static readonly byte[] StrippedThumbJPG = // see https://core.telegram.org/api/files#stripped-thumbnails
|
||||
{
|
||||
[
|
||||
0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49,
|
||||
0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43, 0x00, 0x28, 0x1c,
|
||||
0x1e, 0x23, 0x1e, 0x19, 0x28, 0x23, 0x21, 0x23, 0x2d, 0x2b, 0x28, 0x30, 0x3c, 0x64, 0x41, 0x3c, 0x37, 0x37,
|
||||
|
|
@ -223,7 +315,7 @@ namespace WTelegram
|
|||
0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf2, 0xf3, 0xf4,
|
||||
0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00,
|
||||
0x3f, 0x00
|
||||
};
|
||||
];
|
||||
|
||||
internal static string GetSystemVersion()
|
||||
{
|
||||
|
|
@ -235,22 +327,27 @@ namespace WTelegram
|
|||
internal static string GetAppVersion()
|
||||
=> (Assembly.GetEntryAssembly() ?? Array.Find(AppDomain.CurrentDomain.GetAssemblies(), a => a.EntryPoint != null))?.GetName().Version.ToString() ?? "0.0";
|
||||
|
||||
public class IndirectStream : Stream
|
||||
public class IndirectStream(Stream innerStream) : Stream
|
||||
{
|
||||
public IndirectStream(Stream innerStream) => _innerStream = innerStream;
|
||||
public long? ContentLength;
|
||||
protected readonly Stream _innerStream;
|
||||
protected readonly Stream _innerStream = innerStream;
|
||||
public override bool CanRead => _innerStream.CanRead;
|
||||
public override bool CanSeek => _innerStream.CanSeek;
|
||||
public override bool CanSeek => ContentLength.HasValue || _innerStream.CanSeek;
|
||||
public override bool CanWrite => _innerStream.CanWrite;
|
||||
public override long Length => ContentLength ?? _innerStream.Length;
|
||||
public override long Position { get => _innerStream.Position; set => _innerStream.Position = value; }
|
||||
public override void Flush() => _innerStream.Flush();
|
||||
public override int Read(byte[] buffer, int offset, int count) => _innerStream.Read(buffer, offset, count);
|
||||
public override long Seek(long offset, SeekOrigin origin) => _innerStream.Seek(offset, origin);
|
||||
public override void SetLength(long value) => _innerStream.SetLength(value);
|
||||
public override void Write(byte[] buffer, int offset, int count) => _innerStream.Write(buffer, offset, count);
|
||||
public override int Read(byte[] buffer, int offset, int count) => _innerStream.Read(buffer, offset, count);
|
||||
protected override void Dispose(bool disposing) => _innerStream.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public class WTException : ApplicationException
|
||||
{
|
||||
public WTException(string message) : base(message) { }
|
||||
public WTException(string message, Exception innerException) : base(message, innerException) { }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,48 +21,49 @@ namespace WTelegram
|
|||
int RemoteLayer { get; }
|
||||
}
|
||||
|
||||
[TLDef(0xFEFEFEFE)] [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles")]
|
||||
internal sealed partial class SecretChat : IObject, ISecretChat
|
||||
{
|
||||
[Flags] public enum Flags : uint { requestChat = 1, renewKey = 2, acceptKey = 4, originator = 8, commitKey = 16 }
|
||||
public Flags flags;
|
||||
public InputEncryptedChat peer = new();
|
||||
public byte[] salt; // contains future/discarded authKey during acceptKey/commitKey
|
||||
public byte[] authKey;
|
||||
public DateTime key_created;
|
||||
public int key_useCount;
|
||||
public long participant_id;
|
||||
public int remoteLayer = 46;
|
||||
public int in_seq_no = -2, out_seq_no = 0;
|
||||
public long exchange_id;
|
||||
|
||||
public int ChatId => peer.chat_id;
|
||||
public long RemoteUserId => participant_id;
|
||||
public InputEncryptedChat Peer => peer;
|
||||
public int RemoteLayer => remoteLayer;
|
||||
|
||||
internal long key_fingerprint;
|
||||
internal SortedList<int, TL.Layer23.DecryptedMessageLayer> pendingMsgs = [];
|
||||
internal void Discarded() // clear out fields for more security
|
||||
{
|
||||
Array.Clear(authKey, 0, authKey.Length);
|
||||
key_fingerprint = participant_id = peer.access_hash = peer.chat_id = in_seq_no = out_seq_no = remoteLayer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles")]
|
||||
public sealed class SecretChats : IDisposable
|
||||
{
|
||||
public event Action OnChanged;
|
||||
|
||||
private readonly Client client;
|
||||
private readonly FileStream storage;
|
||||
private readonly Dictionary<int, SecretChat> chats = new();
|
||||
private readonly Dictionary<int, SecretChat> chats = [];
|
||||
private Messages_DhConfig dh;
|
||||
private BigInteger dh_prime;
|
||||
private readonly SHA256 sha256 = SHA256.Create();
|
||||
private readonly SHA1 sha1 = SHA1.Create();
|
||||
private readonly Random random = new();
|
||||
private const int ThresholdPFS = 100;
|
||||
|
||||
[TLDef(0xFEFEFEFE)]
|
||||
internal class SecretChat : IObject, ISecretChat
|
||||
{
|
||||
[Flags] public enum Flags : uint { requestChat = 1, renewKey = 2, acceptKey = 4, originator = 8, commitKey = 16 }
|
||||
public Flags flags;
|
||||
public InputEncryptedChat peer = new();
|
||||
public byte[] salt; // contains future/discarded authKey during acceptKey/commitKey
|
||||
public byte[] authKey;
|
||||
public DateTime key_created;
|
||||
public int key_useCount;
|
||||
public long participant_id;
|
||||
public int remoteLayer = 46;
|
||||
public int in_seq_no = -2, out_seq_no = 0;
|
||||
public long exchange_id;
|
||||
|
||||
public int ChatId => peer.chat_id;
|
||||
public long RemoteUserId => participant_id;
|
||||
public InputEncryptedChat Peer => peer;
|
||||
public int RemoteLayer => remoteLayer;
|
||||
|
||||
internal long key_fingerprint;
|
||||
internal SortedList<int, TL.Layer23.DecryptedMessageLayer> pendingMsgs = new();
|
||||
internal void Discarded() // clear out fields for more security
|
||||
{
|
||||
Array.Clear(authKey, 0, authKey.Length);
|
||||
key_fingerprint = participant_id = peer.access_hash = peer.chat_id = in_seq_no = out_seq_no = remoteLayer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Instantiate a Secret Chats manager</summary>
|
||||
/// <param name="client">The Telegram client</param>
|
||||
|
|
@ -79,7 +80,7 @@ namespace WTelegram
|
|||
}
|
||||
public void Dispose() { OnChanged?.Invoke(); storage?.Dispose(); sha256.Dispose(); sha1.Dispose(); }
|
||||
|
||||
public List<ISecretChat> Chats => chats.Values.ToList<ISecretChat>();
|
||||
public List<ISecretChat> Chats => [.. chats.Values];
|
||||
|
||||
public bool IsChatActive(int chat_id) => !(chats.GetValueOrDefault(chat_id)?.flags.HasFlag(SecretChat.Flags.requestChat) ?? true);
|
||||
|
||||
|
|
@ -94,8 +95,8 @@ namespace WTelegram
|
|||
}
|
||||
public void Load(Stream input)
|
||||
{
|
||||
using var reader = new TL.BinaryReader(input, null, true);
|
||||
if (reader.ReadInt32() != 0) throw new ApplicationException("Unrecognized Secrets format");
|
||||
using var reader = new BinaryReader(input, Encoding.UTF8, true);
|
||||
if (reader.ReadInt32() != 0) throw new WTException("Unrecognized Secrets format");
|
||||
dh = (Messages_DhConfig)reader.ReadTLObject();
|
||||
if (dh?.p != null) dh_prime = BigEndianInteger(dh.p);
|
||||
int count = reader.ReadInt32();
|
||||
|
|
@ -128,15 +129,15 @@ namespace WTelegram
|
|||
{
|
||||
var mdhcb = await client.Messages_GetDhConfig(dh?.version ?? 0, 256);
|
||||
if (mdhcb is Messages_DhConfigNotModified { random: var random })
|
||||
_ = dh ?? throw new ApplicationException("DhConfigNotModified on zero version");
|
||||
_ = dh ?? throw new WTException("DhConfigNotModified on zero version");
|
||||
else if (mdhcb is Messages_DhConfig dhc)
|
||||
{
|
||||
var p = BigEndianInteger(dhc.p);
|
||||
CheckGoodPrime(p, dhc.g);
|
||||
(dh, dh_prime, random, dh.random) = (dhc, p, dhc.random, null);
|
||||
}
|
||||
else throw new ApplicationException("Unexpected DHConfig response: " + mdhcb?.GetType().Name);
|
||||
if (random.Length != 256) throw new ApplicationException("Invalid DHConfig random");
|
||||
else throw new WTException("Unexpected DHConfig response: " + mdhcb?.GetType().Name);
|
||||
if (random.Length != 256) throw new WTException("Invalid DHConfig random");
|
||||
var salt = new byte[256];
|
||||
RNG.GetBytes(salt);
|
||||
for (int i = 0; i < 256; i++) salt[i] ^= random[i];
|
||||
|
|
@ -146,7 +147,7 @@ namespace WTelegram
|
|||
/// <summary>Initiate a secret chat with the given user.<br/>(chat must be acknowledged by remote user before being active)</summary>
|
||||
/// <param name="user">The remote user</param>
|
||||
/// <returns>Secret Chat ID</returns>
|
||||
/// <exception cref="ApplicationException"></exception>
|
||||
/// <exception cref="WTException"></exception>
|
||||
public async Task<int> Request(InputUserBase user)
|
||||
{
|
||||
int chat_id;
|
||||
|
|
@ -164,16 +165,16 @@ namespace WTelegram
|
|||
CheckGoodGaAndGb(g_a, dh_prime);
|
||||
var ecb = await client.Messages_RequestEncryption(user, chat_id, g_a.To256Bytes());
|
||||
if (ecb is not EncryptedChatWaiting ecw || ecw.id != chat_id || ecw.participant_id != chat.participant_id)
|
||||
throw new ApplicationException("Invalid " + ecb?.GetType().Name);
|
||||
throw new WTException("Invalid " + ecb?.GetType().Name);
|
||||
chat.peer.access_hash = ecw.access_hash;
|
||||
return chat_id;
|
||||
}
|
||||
|
||||
/// <summary>Processes the <see cref="UpdateEncryption"/> you received from Telegram (<see cref="Client.OnUpdate"/>).</summary>
|
||||
/// <remarks>If update.chat is <see cref="EncryptedChatRequested"/>, you might want to first make sure you want to accept this secret chat initiated by user <see cref="EncryptedChatRequested.admin_id"/></remarks>
|
||||
/// <summary>Processes the <see cref="UpdateEncryption"/> you received from Telegram (<see cref="Client.OnUpdates"/>).</summary>
|
||||
/// <param name="update">If update.chat is <see cref="EncryptedChatRequested"/>, you might want to first make sure you want to accept this secret chat initiated by user <see cref="EncryptedChatRequested.admin_id"/></param>
|
||||
/// <param name="acceptChatRequests">Incoming requests for secret chats are automatically: accepted (<see langword="true"/>), rejected (<see langword="false"/>) or ignored (<see langword="null"/>)</param>
|
||||
/// <returns><see langword="true"/> if the update was handled successfully</returns>
|
||||
/// <exception cref="ApplicationException"></exception>
|
||||
/// <exception cref="WTException"></exception>
|
||||
public async Task<bool> HandleUpdate(UpdateEncryption update, bool? acceptChatRequests = true)
|
||||
{
|
||||
try
|
||||
|
|
@ -188,8 +189,8 @@ namespace WTelegram
|
|||
var gab = BigInteger.ModPow(g_b, a, dh_prime);
|
||||
chat.flags &= ~SecretChat.Flags.requestChat;
|
||||
SetAuthKey(chat, gab.To256Bytes());
|
||||
if (ec.key_fingerprint != chat.key_fingerprint) throw new ApplicationException("Invalid fingerprint on accepted secret chat");
|
||||
if (ec.access_hash != chat.peer.access_hash || ec.participant_id != chat.participant_id) throw new ApplicationException("Invalid peer on accepted secret chat");
|
||||
if (ec.key_fingerprint != chat.key_fingerprint) throw new WTException("Invalid fingerprint on accepted secret chat");
|
||||
if (ec.access_hash != chat.peer.access_hash || ec.participant_id != chat.participant_id) throw new WTException("Invalid peer on accepted secret chat");
|
||||
await SendNotifyLayer(chat);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -227,7 +228,7 @@ namespace WTelegram
|
|||
var ecb = await client.Messages_AcceptEncryption(chat.peer, g_b.ToByteArray(true, true), chat.key_fingerprint);
|
||||
if (ecb is not EncryptedChat ec || ec.id != ecr.id || ec.access_hash != ecr.access_hash ||
|
||||
ec.admin_id != ecr.admin_id || ec.key_fingerprint != chat.key_fingerprint)
|
||||
throw new ApplicationException("Inconsistent accepted secret chat");
|
||||
throw new WTException("Inconsistent accepted secret chat");
|
||||
await SendNotifyLayer(chat);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -274,7 +275,7 @@ namespace WTelegram
|
|||
/// <returns>Confirmation of sent message</returns>
|
||||
public async Task<Messages_SentEncryptedMessage> SendMessage(int chatId, DecryptedMessageBase msg, bool silent = false, InputEncryptedFileBase file = null)
|
||||
{
|
||||
if (!chats.TryGetValue(chatId, out var chat)) throw new ApplicationException("Secret chat not found");
|
||||
if (!chats.TryGetValue(chatId, out var chat)) throw new WTException("Secret chat not found");
|
||||
try
|
||||
{
|
||||
var dml = new TL.Layer23.DecryptedMessageLayer
|
||||
|
|
@ -336,14 +337,14 @@ namespace WTelegram
|
|||
private IObject Decrypt(SecretChat chat, byte[] data, int dataLen)
|
||||
{
|
||||
if (dataLen < 32) // authKeyId+msgKey+(length+ctorNb)
|
||||
throw new ApplicationException($"Encrypted packet too small: {data.Length}");
|
||||
throw new WTException($"Encrypted packet too small: {data.Length}");
|
||||
var authKey = chat.authKey;
|
||||
long authKeyId = BinaryPrimitives.ReadInt64LittleEndian(data);
|
||||
if (authKeyId == chat.key_fingerprint)
|
||||
if (!chat.flags.HasFlag(SecretChat.Flags.commitKey)) CheckPFS(chat);
|
||||
else { chat.flags &= ~SecretChat.Flags.commitKey; Array.Clear(chat.salt, 0, chat.salt.Length); }
|
||||
else if (chat.flags.HasFlag(SecretChat.Flags.commitKey) && authKeyId == BinaryPrimitives.ReadInt64LittleEndian(sha1.ComputeHash(chat.salt).AsSpan(12))) authKey = chat.salt;
|
||||
else throw new ApplicationException($"Received a packet encrypted with unexpected key {authKeyId:X}");
|
||||
else throw new WTException($"Received a packet encrypted with unexpected key {authKeyId:X}");
|
||||
int x = (int)(chat.flags & SecretChat.Flags.originator);
|
||||
byte[] decrypted_data = EncryptDecryptMessage(data.AsSpan(24, dataLen - 24), false, x, authKey, data, 8, sha256);
|
||||
var length = BinaryPrimitives.ReadInt32LittleEndian(decrypted_data);
|
||||
|
|
@ -354,12 +355,12 @@ namespace WTelegram
|
|||
sha256.TransformBlock(authKey, 88 + x, 32, null, 0);
|
||||
sha256.TransformFinalBlock(decrypted_data, 0, decrypted_data.Length);
|
||||
if (success = data.AsSpan(8, 16).SequenceEqual(sha256.Hash.AsSpan(8, 16)))
|
||||
if (decrypted_data.Length - 4 - length is < 12 or > 1024) throw new ApplicationException($"Invalid MTProto2 padding length: {decrypted_data.Length - 4}-{length}");
|
||||
if (decrypted_data.Length - 4 - length is < 12 or > 1024) throw new WTException($"Invalid MTProto2 padding length: {decrypted_data.Length - 4}-{length}");
|
||||
else if (chat.remoteLayer < Layer.MTProto2) chat.remoteLayer = Layer.MTProto2;
|
||||
}
|
||||
if (!success) throw new ApplicationException("Could not decrypt message");
|
||||
if (length % 4 != 0) throw new ApplicationException($"Invalid message_data_length: {length}");
|
||||
using var reader = new TL.BinaryReader(new MemoryStream(decrypted_data, 4, length), null);
|
||||
if (!success) throw new WTException("Could not decrypt message");
|
||||
if (length % 4 != 0) throw new WTException($"Invalid message_data_length: {length}");
|
||||
using var reader = new BinaryReader(new MemoryStream(decrypted_data, 4, length));
|
||||
return reader.ReadTLObject();
|
||||
}
|
||||
|
||||
|
|
@ -370,19 +371,19 @@ namespace WTelegram
|
|||
/// You can use the generic properties to access their fields
|
||||
/// <para>May return an empty array if msg was already previously received or is not the next message in sequence.
|
||||
/// <br/>May return multiple messages if missing messages are finally received (using <paramref name="fillGaps"/> = true)</para></returns>
|
||||
/// <exception cref="ApplicationException"></exception>
|
||||
/// <exception cref="WTException"></exception>
|
||||
public ICollection<DecryptedMessageBase> DecryptMessage(EncryptedMessageBase msg, bool fillGaps = true)
|
||||
{
|
||||
if (!chats.TryGetValue(msg.ChatId, out var chat)) throw new ApplicationException("Secret chat not found");
|
||||
if (!chats.TryGetValue(msg.ChatId, out var chat)) throw new WTException("Secret chat not found");
|
||||
try
|
||||
{
|
||||
var obj = Decrypt(chat, msg.Bytes, msg.Bytes.Length);
|
||||
if (obj is not TL.Layer23.DecryptedMessageLayer dml) throw new ApplicationException("Decrypted object is not DecryptedMessageLayer");
|
||||
if (dml.random_bytes.Length < 15) throw new ApplicationException("Not enough random_bytes");
|
||||
if (((dml.out_seq_no ^ dml.in_seq_no) & 1) != 1 || ((dml.out_seq_no ^ chat.in_seq_no) & 1) != 0) throw new ApplicationException("Invalid seq_no parities");
|
||||
if (obj is not TL.Layer23.DecryptedMessageLayer dml) throw new WTException("Decrypted object is not DecryptedMessageLayer");
|
||||
if (dml.random_bytes.Length < 15) throw new WTException("Not enough random_bytes");
|
||||
if (((dml.out_seq_no ^ dml.in_seq_no) & 1) != 1 || ((dml.out_seq_no ^ chat.in_seq_no) & 1) != 0) throw new WTException("Invalid seq_no parities");
|
||||
if (dml.layer > chat.remoteLayer) chat.remoteLayer = dml.layer;
|
||||
//Debug.WriteLine($"<\t{dml.in_seq_no}\t{dml.out_seq_no}\t\t\t\t\t\texpected:{chat.out_seq_no}/{chat.in_seq_no + 2}");
|
||||
if (dml.out_seq_no <= chat.in_seq_no) return Array.Empty<DecryptedMessageBase>(); // already received message
|
||||
if (dml.out_seq_no <= chat.in_seq_no) return []; // already received message
|
||||
var pendingMsgSeqNo = chat.pendingMsgs.Keys;
|
||||
if (fillGaps && dml.out_seq_no > chat.in_seq_no + 2)
|
||||
{
|
||||
|
|
@ -392,12 +393,12 @@ namespace WTelegram
|
|||
if (dml.out_seq_no > lastPending + 2) // send request to resend missing gap asynchronously
|
||||
_ = SendMessage(chat.ChatId, new TL.Layer23.DecryptedMessageService { random_id = Helpers.RandomLong(),
|
||||
action = new TL.Layer23.DecryptedMessageActionResend { start_seq_no = lastPending + 2, end_seq_no = dml.out_seq_no - 2 } });
|
||||
return Array.Empty<DecryptedMessageBase>();
|
||||
return [];
|
||||
}
|
||||
chat.in_seq_no = dml.out_seq_no;
|
||||
if (pendingMsgSeqNo.Count == 0 || pendingMsgSeqNo[0] != dml.out_seq_no + 2)
|
||||
if (HandleAction(chat, dml.message.Action)) return Array.Empty<DecryptedMessageBase>();
|
||||
else return new[] { dml.message };
|
||||
if (HandleAction(chat, dml.message.Action)) return [];
|
||||
else return [dml.message];
|
||||
else // we have pendingMsgs completing the sequence in order
|
||||
{
|
||||
var list = new List<DecryptedMessageBase>();
|
||||
|
|
@ -510,7 +511,7 @@ namespace WTelegram
|
|||
}
|
||||
break; // we lost, process with the larger exchange_id RequestKey
|
||||
case 0: break;
|
||||
default: throw new ApplicationException("Invalid RequestKey");
|
||||
default: throw new WTException("Invalid RequestKey");
|
||||
}
|
||||
var g_a = BigEndianInteger(request.g_a);
|
||||
var salt = new byte[256];
|
||||
|
|
@ -529,9 +530,9 @@ namespace WTelegram
|
|||
break;
|
||||
case TL.Layer23.DecryptedMessageActionAcceptKey accept:
|
||||
if ((chat.flags & (SecretChat.Flags.requestChat | SecretChat.Flags.renewKey | SecretChat.Flags.acceptKey)) != SecretChat.Flags.renewKey)
|
||||
throw new ApplicationException("Invalid AcceptKey");
|
||||
throw new WTException("Invalid AcceptKey");
|
||||
if (accept.exchange_id != chat.exchange_id)
|
||||
throw new ApplicationException("AcceptKey: exchange_id mismatch");
|
||||
throw new WTException("AcceptKey: exchange_id mismatch");
|
||||
var a = BigEndianInteger(chat.salt);
|
||||
g_b = BigEndianInteger(accept.g_b);
|
||||
CheckGoodGaAndGb(g_b, dh_prime);
|
||||
|
|
@ -539,7 +540,7 @@ namespace WTelegram
|
|||
var authKey = gab.To256Bytes();
|
||||
key_fingerprint = BinaryPrimitives.ReadInt64LittleEndian(sha1.ComputeHash(authKey).AsSpan(12));
|
||||
if (accept.key_fingerprint != key_fingerprint)
|
||||
throw new ApplicationException("AcceptKey: key_fingerprint mismatch");
|
||||
throw new WTException("AcceptKey: key_fingerprint mismatch");
|
||||
_ = SendMessage(chat.ChatId, new TL.Layer23.DecryptedMessageService { random_id = Helpers.RandomLong(),
|
||||
action = new TL.Layer23.DecryptedMessageActionCommitKey { exchange_id = accept.exchange_id, key_fingerprint = accept.key_fingerprint } });
|
||||
chat.salt = chat.authKey; // A may only discard the previous key after a message encrypted with the new key has been received.
|
||||
|
|
@ -548,10 +549,10 @@ namespace WTelegram
|
|||
break;
|
||||
case TL.Layer23.DecryptedMessageActionCommitKey commit:
|
||||
if ((chat.flags & (SecretChat.Flags.requestChat | SecretChat.Flags.renewKey | SecretChat.Flags.acceptKey)) != SecretChat.Flags.acceptKey)
|
||||
throw new ApplicationException("Invalid RequestKey");
|
||||
throw new WTException("Invalid RequestKey");
|
||||
key_fingerprint = BinaryPrimitives.ReadInt64LittleEndian(sha1.ComputeHash(chat.salt).AsSpan(12));
|
||||
if (commit.exchange_id != chat.exchange_id | commit.key_fingerprint != key_fingerprint)
|
||||
throw new ApplicationException("CommitKey: data mismatch");
|
||||
throw new WTException("CommitKey: data mismatch");
|
||||
chat.flags &= ~SecretChat.Flags.acceptKey;
|
||||
authKey = chat.authKey;
|
||||
SetAuthKey(chat, chat.salt);
|
||||
|
|
@ -614,7 +615,7 @@ namespace WTelegram
|
|||
var res = md5.TransformFinalBlock(iv, 0, 32);
|
||||
long fingerprint = BinaryPrimitives.ReadInt64LittleEndian(md5.Hash);
|
||||
fingerprint ^= fingerprint >> 32;
|
||||
if (encryptedFile.key_fingerprint != (int)fingerprint) throw new ApplicationException("Encrypted file fingerprint mismatch");
|
||||
if (encryptedFile.key_fingerprint != (int)fingerprint) throw new WTException("Encrypted file fingerprint mismatch");
|
||||
|
||||
using var decryptStream = new AES_IGE_Stream(outputStream, size, key, iv);
|
||||
var fileLocation = encryptedFile.ToFileLocation();
|
||||
|
|
|
|||
|
|
@ -1,69 +1,154 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using WTelegram; // for GetValueOrDefault
|
||||
using WTelegram;
|
||||
|
||||
namespace TL
|
||||
{
|
||||
public static class Extensions
|
||||
public static class Services
|
||||
{
|
||||
private class CollectorPeer : Peer
|
||||
public sealed partial class CollectorPeer(IDictionary<long, User> _users, IDictionary<long, ChatBase> _chats) : Peer, IPeerCollector
|
||||
{
|
||||
public override long ID => 0;
|
||||
internal Dictionary<long, User> _users;
|
||||
internal Dictionary<long, ChatBase> _chats;
|
||||
protected internal override IPeerInfo UserOrChat(Dictionary<long, User> users, Dictionary<long, ChatBase> chats)
|
||||
{
|
||||
if (users != null) Collect(users.Values);
|
||||
if (chats != null) Collect(chats.Values);
|
||||
return null;
|
||||
}
|
||||
|
||||
public void Collect(IEnumerable<TL.User> users)
|
||||
{
|
||||
lock (_users)
|
||||
foreach (var user in users.Values)
|
||||
foreach (var user in users)
|
||||
if (user != null)
|
||||
if (!user.flags.HasFlag(User.Flags.min) || !_users.TryGetValue(user.id, out var prevUser) || prevUser.flags.HasFlag(User.Flags.min))
|
||||
_users[user.id] = user;
|
||||
lock (_chats)
|
||||
foreach (var kvp in chats)
|
||||
if (kvp.Value is not Channel channel)
|
||||
_chats[kvp.Key] = kvp.Value;
|
||||
else if (!channel.flags.HasFlag(Channel.Flags.min) || !_chats.TryGetValue(channel.id, out var prevChat) || prevChat is not Channel prevChannel || prevChannel.flags.HasFlag(Channel.Flags.min))
|
||||
_chats[kvp.Key] = channel;
|
||||
return null;
|
||||
else
|
||||
{ // update previously full user from min user:
|
||||
// see https://github.com/tdlib/td/blob/master/td/telegram/UserManager.cpp#L2689
|
||||
// and https://github.com/telegramdesktop/tdesktop/blob/dev/Telegram/SourceFiles/data/data_session.cpp#L515
|
||||
const User.Flags updated_flags = User.Flags.deleted | User.Flags.bot | User.Flags.bot_chat_history |
|
||||
User.Flags.bot_nochats | User.Flags.verified | User.Flags.restricted | User.Flags.has_bot_inline_placeholder |
|
||||
User.Flags.bot_inline_geo | User.Flags.support | User.Flags.scam | User.Flags.fake | User.Flags.bot_attach_menu |
|
||||
User.Flags.premium | User.Flags.has_emoji_status;
|
||||
const User.Flags2 updated_flags2 = User.Flags2.has_usernames | User.Flags2.stories_unavailable |
|
||||
User.Flags2.has_color | User.Flags2.has_profile_color | User.Flags2.contact_require_premium |
|
||||
User.Flags2.bot_business | User.Flags2.bot_has_main_app | User.Flags2.bot_forum_view;
|
||||
// tdlib updated flags: deleted | bot | bot_chat_history | bot_nochats | verified | bot_inline_geo
|
||||
// | support | scam | fake | bot_attach_menu | premium
|
||||
// tdesktop non-updated flags: bot | bot_chat_history | bot_nochats | bot_attach_menu
|
||||
// updated flags2: stories_unavailable | main_app | bot_business | bot_forum_view (tdlib) | contact_require_premium (tdesktop)
|
||||
prevUser.flags = (prevUser.flags & ~updated_flags) | (user.flags & updated_flags);
|
||||
prevUser.flags2 = (prevUser.flags2 & ~updated_flags2) | (user.flags2 & updated_flags2);
|
||||
prevUser.first_name ??= user.first_name; // tdlib: not updated ; tdesktop: updated only if unknown
|
||||
prevUser.last_name ??= user.last_name; // tdlib: not updated ; tdesktop: updated only if unknown
|
||||
//prevUser.username ??= user.username; // tdlib/tdesktop: not updated
|
||||
prevUser.phone ??= user.phone; // tdlib: updated only if unknown ; tdesktop: not updated
|
||||
if (prevUser.flags.HasFlag(User.Flags.apply_min_photo) && user.photo != null)
|
||||
{
|
||||
prevUser.photo = user.photo; // tdlib/tdesktop: updated on apply_min_photo
|
||||
prevUser.flags |= User.Flags.has_photo;
|
||||
}
|
||||
prevUser.bot_info_version = user.bot_info_version; // tdlib: updated ; tdesktop: not updated
|
||||
prevUser.restriction_reason = user.restriction_reason; // tdlib: updated ; tdesktop: not updated
|
||||
prevUser.bot_inline_placeholder = user.bot_inline_placeholder;// tdlib: updated ; tdesktop: ignored
|
||||
if (user.lang_code != null)
|
||||
prevUser.lang_code = user.lang_code; // tdlib: updated if present ; tdesktop: ignored
|
||||
prevUser.emoji_status = user.emoji_status; // tdlib/tdesktop: updated
|
||||
//prevUser.usernames = user.usernames; // tdlib/tdesktop: not updated
|
||||
if (user.stories_max_id != null)
|
||||
prevUser.stories_max_id = user.stories_max_id; // tdlib: updated if > 0 ; tdesktop: not updated
|
||||
prevUser.color = user.color; // tdlib/tdesktop: updated
|
||||
prevUser.profile_color = user.profile_color; // tdlib/tdesktop: unimplemented yet
|
||||
_users[user.id] = prevUser;
|
||||
}
|
||||
}
|
||||
|
||||
public void Collect(IEnumerable<ChatBase> chats)
|
||||
{
|
||||
lock (_chats)
|
||||
foreach (var chat in chats)
|
||||
if (chat is not Channel channel)
|
||||
_chats[chat.ID] = chat;
|
||||
else if (!_chats.TryGetValue(channel.id, out var prevChat) || prevChat is not Channel prevChannel)
|
||||
_chats[channel.id] = channel;
|
||||
else if (!channel.flags.HasFlag(Channel.Flags.min) || prevChannel.flags.HasFlag(Channel.Flags.min))
|
||||
{
|
||||
if (channel.participants_count == 0) channel.participants_count = prevChannel.participants_count; // non-min channel can lack this info
|
||||
_chats[channel.id] = channel;
|
||||
}
|
||||
else
|
||||
{ // update previously full channel from min channel:
|
||||
const Channel.Flags updated_flags = (Channel.Flags)0x7FDC0BE0;
|
||||
const Channel.Flags2 updated_flags2 = (Channel.Flags2)0x781;
|
||||
// tdesktop updated flags: broadcast | verified | megagroup | signatures | scam | has_link | slowmode_enabled
|
||||
// | call_active | call_not_empty | fake | gigagroup | noforwards | join_to_send | join_request | forum
|
||||
// tdlib nonupdated flags: broadcast | signatures | call_active | call_not_empty | noforwards
|
||||
prevChannel.flags = (prevChannel.flags & ~updated_flags) | (channel.flags & updated_flags);
|
||||
prevChannel.flags2 = (prevChannel.flags2 & ~updated_flags2) | (channel.flags2 & updated_flags2);
|
||||
prevChannel.title = channel.title; // tdlib/tdesktop: updated
|
||||
prevChannel.username = channel.username; // tdlib/tdesktop: updated
|
||||
prevChannel.photo = channel.photo; // tdlib: updated if not banned ; tdesktop: updated
|
||||
prevChannel.restriction_reason = channel.restriction_reason; // tdlib: updated ; tdesktop: not updated
|
||||
prevChannel.default_banned_rights = channel.default_banned_rights; // tdlib/tdesktop: updated
|
||||
if (channel.participants_count > 0)
|
||||
prevChannel.participants_count = channel.participants_count; // tdlib/tdesktop: updated if present
|
||||
prevChannel.usernames = channel.usernames; // tdlib/tdesktop: updated
|
||||
prevChannel.color = channel.color; // tdlib: not updated ; tdesktop: updated
|
||||
prevChannel.profile_color = channel.profile_color; // tdlib/tdesktop: ignored
|
||||
prevChannel.emoji_status = channel.emoji_status; // tdlib: not updated ; tdesktop: updated
|
||||
prevChannel.level = channel.level; // tdlib: not updated ; tdesktop: updated
|
||||
_chats[channel.id] = prevChannel;
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasUser(long id) { lock (_users) return _users.ContainsKey(id); }
|
||||
public bool HasChat(long id) { lock (_chats) return _chats.ContainsKey(id); }
|
||||
}
|
||||
|
||||
/// <summary>Accumulate users/chats found in this structure in your dictionaries, ignoring <see href="https://core.telegram.org/api/min">Min constructors</see> when the full object is already stored</summary>
|
||||
/// <param name="structure">The structure having a <c>users</c></param>
|
||||
public static void CollectUsersChats(this IPeerResolver structure, Dictionary<long, User> users, Dictionary<long, ChatBase> chats)
|
||||
=> structure.UserOrChat(new CollectorPeer { _users = users, _chats = chats });
|
||||
public static void CollectUsersChats(this IPeerResolver structure, IDictionary<long, User> users, IDictionary<long, ChatBase> chats)
|
||||
=> structure.UserOrChat(new CollectorPeer(users, chats));
|
||||
|
||||
public static Task<Messages_Chats> Messages_GetChats(this WTelegram.Client _) => throw new ApplicationException("The method you're looking for is Messages_GetAllChats");
|
||||
public static Task<Messages_Chats> Channels_GetChannels(this WTelegram.Client _) => throw new ApplicationException("The method you're looking for is Messages_GetAllChats");
|
||||
public static Task<UserBase[]> Users_GetUsers(this WTelegram.Client _) => throw new ApplicationException("The method you're looking for is Messages_GetAllDialogs");
|
||||
public static Task<Messages_MessagesBase> Messages_GetMessages(this WTelegram.Client _) => throw new ApplicationException("If you want to get the messages from a chat, use Messages_GetHistory");
|
||||
[EditorBrowsable(EditorBrowsableState.Never)][Obsolete("The method you're looking for is Messages_GetAllChats", true)]
|
||||
public static Task<Messages_Chats> Messages_GetChats(this Client _) => throw new WTException("The method you're looking for is Messages_GetAllChats");
|
||||
[EditorBrowsable(EditorBrowsableState.Never)][Obsolete("The method you're looking for is Messages_GetAllChats", true)]
|
||||
public static Task<Messages_Chats> Channels_GetChannels(this Client _) => throw new WTException("The method you're looking for is Messages_GetAllChats");
|
||||
[EditorBrowsable(EditorBrowsableState.Never)][Obsolete("The method you're looking for is Messages_GetAllDialogs", true)]
|
||||
public static Task<UserBase[]> Users_GetUsers(this Client _) => throw new WTException("The method you're looking for is Messages_GetAllDialogs");
|
||||
[EditorBrowsable(EditorBrowsableState.Never)][Obsolete("If you want to get all messages from a chat, use method Messages_GetHistory", true)]
|
||||
public static Task<Messages_MessagesBase> Messages_GetMessages(this Client _) => throw new WTException("If you want to get all messages from a chat, use method Messages_GetHistory");
|
||||
}
|
||||
|
||||
public static class Markdown
|
||||
{
|
||||
/// <summary>Converts a <a href="https://core.telegram.org/bots/api/#markdownv2-style">Markdown text</a> into the (plain text + entities) format used by Telegram messages</summary>
|
||||
/// <param name="client">Client, used for getting access_hash for <c>tg://user?id=</c> URLs</param>
|
||||
/// <param name="_">not used anymore, you can pass null</param>
|
||||
/// <param name="text">[in] The Markdown text<br/>[out] The same (plain) text, stripped of all Markdown notation</param>
|
||||
/// <param name="premium">Generate premium entities if any</param>
|
||||
/// <param name="users">Dictionary used for <c>tg://user?id=</c> notation</param>
|
||||
/// <returns>The array of formatting entities that you can pass (along with the plain text) to <see cref="WTelegram.Client.SendMessageAsync">SendMessageAsync</see> or <see cref="WTelegram.Client.SendMediaAsync">SendMediaAsync</see></returns>
|
||||
public static MessageEntity[] MarkdownToEntities(this WTelegram.Client client, ref string text, bool premium = false, Dictionary<long, User> users = null)
|
||||
/// <returns>The array of formatting entities that you can pass (along with the plain text) to <see cref="Client.SendMessageAsync">SendMessageAsync</see> or <see cref="Client.SendMediaAsync">SendMediaAsync</see></returns>
|
||||
public static MessageEntity[] MarkdownToEntities(this Client _, ref string text, bool premium = false, IReadOnlyDictionary<long, User> users = null)
|
||||
{
|
||||
var entities = new List<MessageEntity>();
|
||||
MessageEntityBlockquote lastBlockQuote = null;
|
||||
int offset, inCode = 0;
|
||||
var sb = new StringBuilder(text);
|
||||
for (int offset = 0; offset < sb.Length;)
|
||||
for (offset = 0; offset < sb.Length;)
|
||||
{
|
||||
switch (sb[offset])
|
||||
{
|
||||
case '\r': sb.Remove(offset, 1); break;
|
||||
case '\\': sb.Remove(offset++, 1); break;
|
||||
case '*': ProcessEntity<MessageEntityBold>(); break;
|
||||
case '~': ProcessEntity<MessageEntityStrike>(); break;
|
||||
case '_':
|
||||
case '*' when inCode == 0: ProcessEntity<MessageEntityBold>(); break;
|
||||
case '~' when inCode == 0: ProcessEntity<MessageEntityStrike>(); break;
|
||||
case '_' when inCode == 0:
|
||||
if (offset + 1 < sb.Length && sb[offset + 1] == '_')
|
||||
{
|
||||
sb.Remove(offset, 1);
|
||||
|
|
@ -73,7 +158,7 @@ namespace TL
|
|||
ProcessEntity<MessageEntityItalic>();
|
||||
break;
|
||||
case '|':
|
||||
if (offset + 1 < sb.Length && sb[offset + 1] == '|')
|
||||
if (inCode == 0 && offset + 1 < sb.Length && sb[offset + 1] == '|')
|
||||
{
|
||||
sb.Remove(offset, 1);
|
||||
ProcessEntity<MessageEntitySpoiler>();
|
||||
|
|
@ -82,6 +167,7 @@ namespace TL
|
|||
offset++;
|
||||
break;
|
||||
case '`':
|
||||
int count = entities.Count;
|
||||
if (offset + 2 < sb.Length && sb[offset + 1] == '`' && sb[offset + 2] == '`')
|
||||
{
|
||||
int len = 3;
|
||||
|
|
@ -98,16 +184,26 @@ namespace TL
|
|||
}
|
||||
else
|
||||
ProcessEntity<MessageEntityCode>();
|
||||
if (entities.Count > count) inCode++; else inCode--;
|
||||
break;
|
||||
case '!' when offset + 1 < sb.Length && sb[offset + 1] == '[':
|
||||
case '>' when inCode == 0 && offset == 0 || sb[offset - 1] == '\n':
|
||||
sb.Remove(offset, 1);
|
||||
goto case '[';
|
||||
case '[':
|
||||
if (lastBlockQuote == null)
|
||||
entities.Add(lastBlockQuote = new MessageEntityBlockquote { offset = offset, length = -1 });
|
||||
break;
|
||||
case '\n' when lastBlockQuote != null:
|
||||
if (offset + 1 >= sb.Length || sb[offset + 1] != '>') CloseBlockQuote();
|
||||
offset++;
|
||||
break;
|
||||
case '!' when inCode == 0 && offset + 1 < sb.Length && sb[offset + 1] == '[':
|
||||
sb.Remove(offset, 1);
|
||||
break;
|
||||
case '[' when inCode == 0:
|
||||
entities.Add(new MessageEntityTextUrl { offset = offset, length = -1 });
|
||||
sb.Remove(offset, 1);
|
||||
break;
|
||||
case ']':
|
||||
if (offset + 2 < sb.Length && sb[offset + 1] == '(')
|
||||
if (inCode == 0 && offset + 2 < sb.Length && sb[offset + 1] == '(')
|
||||
{
|
||||
var lastIndex = entities.FindLastIndex(e => e.length == -1);
|
||||
if (lastIndex >= 0 && entities[lastIndex] is MessageEntityTextUrl textUrl)
|
||||
|
|
@ -121,7 +217,7 @@ namespace TL
|
|||
else if (c == ')') break;
|
||||
}
|
||||
textUrl.url = sb.ToString(offset + 2, offset2 - offset - 3);
|
||||
if (textUrl.url.StartsWith("tg://user?id=") && long.TryParse(textUrl.url[13..], out var id) && (users?.GetValueOrDefault(id)?.access_hash ?? client.GetAccessHashFor<User>(id)) is long hash)
|
||||
if (textUrl.url.StartsWith("tg://user?id=") && long.TryParse(textUrl.url[13..], out var id) && users?.GetValueOrDefault(id)?.access_hash is long hash)
|
||||
entities[lastIndex] = new InputMessageEntityMentionName { offset = textUrl.offset, length = textUrl.length, user_id = new InputUser(id, hash) };
|
||||
else if ((textUrl.url.StartsWith("tg://emoji?id=") || textUrl.url.StartsWith("emoji?id=")) && long.TryParse(textUrl.url[(textUrl.url.IndexOf('=') + 1)..], out id))
|
||||
if (premium) entities[lastIndex] = new MessageEntityCustomEmoji { offset = textUrl.offset, length = textUrl.length, document_id = id };
|
||||
|
|
@ -137,15 +233,31 @@ namespace TL
|
|||
|
||||
void ProcessEntity<T>() where T : MessageEntity, new()
|
||||
{
|
||||
sb.Remove(offset, 1);
|
||||
if (entities.LastOrDefault(e => e.length == -1) is T prevEntity)
|
||||
prevEntity.length = offset - prevEntity.offset;
|
||||
if (offset == prevEntity.offset)
|
||||
entities.Remove(prevEntity);
|
||||
else
|
||||
prevEntity.length = offset - prevEntity.offset;
|
||||
else
|
||||
entities.Add(new T { offset = offset, length = -1 });
|
||||
sb.Remove(offset, 1);
|
||||
}
|
||||
}
|
||||
if (lastBlockQuote != null) CloseBlockQuote();
|
||||
HtmlText.FixUps(sb, entities);
|
||||
text = sb.ToString();
|
||||
return entities.Count == 0 ? null : entities.ToArray();
|
||||
return entities.Count == 0 ? null : [.. entities];
|
||||
|
||||
void CloseBlockQuote()
|
||||
{
|
||||
if (entities[^1] is MessageEntitySpoiler { length: -1 } mes && mes.offset == offset)
|
||||
{
|
||||
entities.RemoveAt(entities.Count - 1);
|
||||
lastBlockQuote.flags = MessageEntityBlockquote.Flags.collapsed;
|
||||
}
|
||||
lastBlockQuote.length = offset - lastBlockQuote.offset;
|
||||
lastBlockQuote = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Converts the (plain text + entities) format used by Telegram messages into a <a href="https://core.telegram.org/bots/api/#markdownv2-style">Markdown text</a></summary>
|
||||
|
|
@ -154,26 +266,30 @@ namespace TL
|
|||
/// <param name="entities">The array of formatting entities, typically obtained from <see cref="Message.entities"/></param>
|
||||
/// <param name="premium">Convert premium entities (might lead to non-standard markdown)</param>
|
||||
/// <returns>The message text with MarkdownV2 formattings</returns>
|
||||
public static string EntitiesToMarkdown(this WTelegram.Client client, string message, MessageEntity[] entities, bool premium = false)
|
||||
public static string EntitiesToMarkdown(this Client client, string message, MessageEntity[] entities, bool premium = false)
|
||||
{
|
||||
if (entities == null || entities.Length == 0) return Escape(message);
|
||||
var closings = new List<(int offset, string md)>();
|
||||
var sb = new StringBuilder(message);
|
||||
int entityIndex = 0;
|
||||
var nextEntity = entities[entityIndex];
|
||||
bool inBlockQuote = false;
|
||||
char lastCh = '\0';
|
||||
for (int offset = 0, i = 0; ; offset++, i++)
|
||||
{
|
||||
while (closings.Count != 0 && offset == closings[0].offset)
|
||||
{
|
||||
var md = closings[0].md;
|
||||
if (i > 0 && md[0] == '_' && sb[i - 1] == '_') md = '\r' + md;
|
||||
sb.Insert(i, md); i += md.Length;
|
||||
closings.RemoveAt(0);
|
||||
if (i > 0 && md[0] == '_' && sb[i - 1] == '_') md = '\r' + md;
|
||||
if (md[0] == '>') { inBlockQuote = false; md = md[1..]; if (lastCh != '\n' && i < sb.Length && sb[i] != '\n') md += '\n'; }
|
||||
sb.Insert(i, md); i += md.Length;
|
||||
}
|
||||
if (i == sb.Length) break;
|
||||
if (lastCh == '\n' && inBlockQuote) sb.Insert(i++, '>');
|
||||
for (; offset == nextEntity?.offset; nextEntity = ++entityIndex < entities.Length ? entities[entityIndex] : null)
|
||||
{
|
||||
if (entityToMD.TryGetValue(nextEntity.GetType(), out var md))
|
||||
if (EntityToMD.TryGetValue(nextEntity.GetType(), out var md))
|
||||
{
|
||||
var closing = (nextEntity.offset + nextEntity.length, md);
|
||||
if (md[0] is '[' or '!')
|
||||
|
|
@ -188,6 +304,9 @@ namespace TL
|
|||
if (premium) closing.md = $"](tg://emoji?id={mecu.document_id})";
|
||||
else continue;
|
||||
}
|
||||
else if (nextEntity is MessageEntityBlockquote mebq)
|
||||
{ inBlockQuote = true; if (lastCh is not '\n' and not '\0') md = "\n>";
|
||||
if (mebq.flags == MessageEntityBlockquote.Flags.collapsed) closing.md = ">||"; }
|
||||
else if (nextEntity is MessageEntityPre mep)
|
||||
md = $"```{mep.language}\n";
|
||||
int index = ~closings.BinarySearch(closing, Comparer<(int, string)>.Create((x, y) => x.Item1.CompareTo(y.Item1) | 1));
|
||||
|
|
@ -196,18 +315,21 @@ namespace TL
|
|||
sb.Insert(i, md); i += md.Length;
|
||||
}
|
||||
}
|
||||
switch (sb[i])
|
||||
switch (lastCh = sb[i])
|
||||
{
|
||||
case '_': case '*': case '~': case '`': case '#': case '+': case '-': case '=': case '.': case '!':
|
||||
case '_': case '*': case '~': case '#': case '+': case '-': case '=': case '.': case '!':
|
||||
case '[': case ']': case '(': case ')': case '{': case '}': case '>': case '|': case '\\':
|
||||
sb.Insert(i, '\\'); i++;
|
||||
if (closings.Count != 0 && closings[0].md[0] == '`') break;
|
||||
goto case '`';
|
||||
case '`':
|
||||
sb.Insert(i++, '\\');
|
||||
break;
|
||||
}
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
static readonly Dictionary<Type, string> entityToMD = new()
|
||||
static readonly Dictionary<Type, string> EntityToMD = new()
|
||||
{
|
||||
[typeof(MessageEntityBold)] = "*",
|
||||
[typeof(MessageEntityItalic)] = "_",
|
||||
|
|
@ -220,6 +342,7 @@ namespace TL
|
|||
[typeof(MessageEntityStrike)] = "~",
|
||||
[typeof(MessageEntitySpoiler)] = "||",
|
||||
[typeof(MessageEntityCustomEmoji)] = "![",
|
||||
[typeof(MessageEntityBlockquote)] = ">",
|
||||
};
|
||||
|
||||
/// <summary>Insert backslashes in front of Markdown reserved characters</summary>
|
||||
|
|
@ -227,6 +350,7 @@ namespace TL
|
|||
/// <returns>The escaped text, ready to be used in <see cref="MarkdownToEntities">MarkdownToEntities</see> without problems</returns>
|
||||
public static string Escape(string text)
|
||||
{
|
||||
if (text == null) return null;
|
||||
StringBuilder sb = null;
|
||||
for (int index = 0, added = 0; index < text.Length; index++)
|
||||
{
|
||||
|
|
@ -246,12 +370,12 @@ namespace TL
|
|||
public static class HtmlText
|
||||
{
|
||||
/// <summary>Converts an <a href="https://core.telegram.org/bots/api/#html-style">HTML-formatted text</a> into the (plain text + entities) format used by Telegram messages</summary>
|
||||
/// <param name="client">Client, used for getting access_hash for <c>tg://user?id=</c> URLs</param>
|
||||
/// <param name="_">not used anymore, you can pass null</param>
|
||||
/// <param name="text">[in] The HTML-formatted text<br/>[out] The same (plain) text, stripped of all HTML tags</param>
|
||||
/// <param name="premium">Generate premium entities if any</param>
|
||||
/// <param name="users">Dictionary used for <c>tg://user?id=</c> notation</param>
|
||||
/// <returns>The array of formatting entities that you can pass (along with the plain text) to <see cref="WTelegram.Client.SendMessageAsync">SendMessageAsync</see> or <see cref="WTelegram.Client.SendMediaAsync">SendMediaAsync</see></returns>
|
||||
public static MessageEntity[] HtmlToEntities(this WTelegram.Client client, ref string text, bool premium = false, Dictionary<long, User> users = null)
|
||||
/// <returns>The array of formatting entities that you can pass (along with the plain text) to <see cref="Client.SendMessageAsync">SendMessageAsync</see> or <see cref="Client.SendMediaAsync">SendMediaAsync</see></returns>
|
||||
public static MessageEntity[] HtmlToEntities(this Client _, ref string text, bool premium = false, IReadOnlyDictionary<long, User> users = null)
|
||||
{
|
||||
var entities = new List<MessageEntity>();
|
||||
var sb = new StringBuilder(text);
|
||||
|
|
@ -261,17 +385,18 @@ namespace TL
|
|||
char c = sb[offset];
|
||||
if (c == '&')
|
||||
{
|
||||
for (end = offset + 1; end < sb.Length; end++)
|
||||
if (sb[end] == ';') break;
|
||||
if (end >= sb.Length) break;
|
||||
var html = HttpUtility.HtmlDecode(sb.ToString(offset, end - offset + 1));
|
||||
end = offset + 1;
|
||||
if (end < sb.Length && sb[end] == '#') end++;
|
||||
while (end < sb.Length && sb[end] is >= 'a' and <= 'z' or >= 'A' and <= 'Z' or >= '0' and <= '9') end++;
|
||||
var html = HttpUtility.HtmlDecode(end >= sb.Length || sb[end] != ';'
|
||||
? sb.ToString(offset, end - offset) + ";" : sb.ToString(offset, ++end - offset));
|
||||
if (html.Length == 1)
|
||||
{
|
||||
sb[offset] = html[0];
|
||||
sb.Remove(++offset, end - offset + 1);
|
||||
sb.Remove(++offset, end - offset);
|
||||
}
|
||||
else
|
||||
offset = end + 1;
|
||||
offset = end;
|
||||
}
|
||||
else if (c == '<')
|
||||
{
|
||||
|
|
@ -288,11 +413,16 @@ namespace TL
|
|||
case "u": case "ins": ProcessEntity<MessageEntityUnderline>(); break;
|
||||
case "s": case "strike": case "del": ProcessEntity<MessageEntityStrike>(); break;
|
||||
case "span class=\"tg-spoiler\"":
|
||||
case "span class='tg-spoiler'":
|
||||
case "span" when closing:
|
||||
case "tg-spoiler": ProcessEntity<MessageEntitySpoiler>(); break;
|
||||
case "code": ProcessEntity<MessageEntityCode>(); break;
|
||||
case "pre": ProcessEntity<MessageEntityPre>(); break;
|
||||
case "tg-emoji" when closing: ProcessEntity<MessageEntityCustomEmoji>(); break;
|
||||
case "blockquote": ProcessEntity<MessageEntityBlockquote>(); break;
|
||||
case "blockquote expandable":
|
||||
entities.Add(new MessageEntityBlockquote { offset = offset, length = -1, flags = MessageEntityBlockquote.Flags.collapsed });
|
||||
break;
|
||||
default:
|
||||
if (closing)
|
||||
{
|
||||
|
|
@ -303,20 +433,22 @@ namespace TL
|
|||
prevEntity.length = offset - prevEntity.offset;
|
||||
}
|
||||
}
|
||||
else if (tag.StartsWith("a href=\"") && tag.EndsWith("\""))
|
||||
else if ((tag[^1] == '"' && tag.StartsWith("a href=\""))
|
||||
|| (tag[^1] == '\'' && tag.StartsWith("a href='")))
|
||||
{
|
||||
tag = tag[8..^1];
|
||||
if (tag.StartsWith("tg://user?id=") && long.TryParse(tag[13..], out var user_id) && (users?.GetValueOrDefault(user_id)?.access_hash ?? client.GetAccessHashFor<User>(user_id)) is long hash)
|
||||
tag = HttpUtility.HtmlDecode(tag[8..^1]);
|
||||
if (tag.StartsWith("tg://user?id=") && long.TryParse(tag[13..], out var user_id) && users?.GetValueOrDefault(user_id)?.access_hash is long hash)
|
||||
entities.Add(new InputMessageEntityMentionName { offset = offset, length = -1, user_id = new InputUser(user_id, hash) });
|
||||
else
|
||||
entities.Add(new MessageEntityTextUrl { offset = offset, length = -1, url = tag });
|
||||
}
|
||||
else if (tag.StartsWith("code class=\"language-") && tag.EndsWith("\""))
|
||||
else if ((tag[^1] == '"' && tag.StartsWith("code class=\"language-"))
|
||||
|| (tag[^1] == '\'' && tag.StartsWith("code class='language-")))
|
||||
{
|
||||
if (entities.LastOrDefault(e => e.length == -1) is MessageEntityPre prevEntity)
|
||||
prevEntity.language = tag[21..^1];
|
||||
}
|
||||
else if (premium && (tag.StartsWith("tg-emoji emoji-id=\"") || tag.StartsWith("tg-emoji id=\"")))
|
||||
else if (premium && (tag.StartsWith("tg-emoji emoji-id=\"") || tag.StartsWith("tg-emoji emoji-id='")))
|
||||
entities.Add(new MessageEntityCustomEmoji { offset = offset, length = -1, document_id = long.Parse(tag[(tag.IndexOf('=') + 2)..^1]) });
|
||||
break;
|
||||
}
|
||||
|
|
@ -332,8 +464,22 @@ namespace TL
|
|||
else
|
||||
offset++;
|
||||
}
|
||||
FixUps(sb, entities);
|
||||
text = sb.ToString();
|
||||
return entities.Count == 0 ? null : entities.ToArray();
|
||||
return entities.Count == 0 ? null : [.. entities];
|
||||
}
|
||||
|
||||
internal static void FixUps(StringBuilder sb, List<MessageEntity> entities)
|
||||
{
|
||||
int newlen = sb.Length;
|
||||
while (--newlen >= 0 && char.IsWhiteSpace(sb[newlen]));
|
||||
if (++newlen != sb.Length) sb.Length = newlen;
|
||||
for (int i = 0; i < entities.Count; i++)
|
||||
{
|
||||
var entity = entities[i];
|
||||
if (entity.offset + entity.length > newlen) entity.length = newlen - entity.offset;
|
||||
if (entity.length == 0) entities.RemoveAt(i--);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Converts the (plain text + entities) format used by Telegram messages into an <a href="https://core.telegram.org/bots/api/#html-style">HTML-formatted text</a></summary>
|
||||
|
|
@ -342,7 +488,7 @@ namespace TL
|
|||
/// <param name="entities">The array of formatting entities, typically obtained from <see cref="Message.entities"/></param>
|
||||
/// <param name="premium">Convert premium entities</param>
|
||||
/// <returns>The message text with HTML formatting tags</returns>
|
||||
public static string EntitiesToHtml(this WTelegram.Client client, string message, MessageEntity[] entities, bool premium = false)
|
||||
public static string EntitiesToHtml(this Client client, string message, MessageEntity[] entities, bool premium = false)
|
||||
{
|
||||
if (entities == null || entities.Length == 0) return Escape(message);
|
||||
var closings = new List<(int offset, string tag)>();
|
||||
|
|
@ -360,13 +506,13 @@ namespace TL
|
|||
if (i == sb.Length) break;
|
||||
for (; offset == nextEntity?.offset; nextEntity = ++entityIndex < entities.Length ? entities[entityIndex] : null)
|
||||
{
|
||||
if (entityToTag.TryGetValue(nextEntity.GetType(), out var tag))
|
||||
if (EntityToTag.TryGetValue(nextEntity.GetType(), out var tag))
|
||||
{
|
||||
var closing = (nextEntity.offset + nextEntity.length, $"</{tag}>");
|
||||
if (tag[0] == 'a')
|
||||
{
|
||||
if (nextEntity is MessageEntityTextUrl metu)
|
||||
tag = $"<a href=\"{metu.url}\">";
|
||||
tag = $"<a href=\"{Escape(metu.url)}\">";
|
||||
else if (nextEntity is MessageEntityMentionName memn)
|
||||
tag = $"<a href=\"tg://user?id={memn.user_id}\">";
|
||||
else if (nextEntity is InputMessageEntityMentionName imemn)
|
||||
|
|
@ -380,6 +526,8 @@ namespace TL
|
|||
closing.Item2 = "</code></pre>";
|
||||
tag = $"<pre><code class=\"language-{mep.language}\">";
|
||||
}
|
||||
else if (nextEntity is MessageEntityBlockquote { flags: MessageEntityBlockquote.Flags.collapsed })
|
||||
tag = "<blockquote expandable>";
|
||||
else
|
||||
tag = $"<{tag}>";
|
||||
int index = ~closings.BinarySearch(closing, Comparer<(int, string)>.Create((x, y) => x.Item1.CompareTo(y.Item1) | 1));
|
||||
|
|
@ -397,7 +545,7 @@ namespace TL
|
|||
return sb.ToString();
|
||||
}
|
||||
|
||||
static readonly Dictionary<Type, string> entityToTag = new()
|
||||
static readonly Dictionary<Type, string> EntityToTag = new()
|
||||
{
|
||||
[typeof(MessageEntityBold)] = "b",
|
||||
[typeof(MessageEntityItalic)] = "i",
|
||||
|
|
@ -410,12 +558,13 @@ namespace TL
|
|||
[typeof(MessageEntityStrike)] = "s",
|
||||
[typeof(MessageEntitySpoiler)] = "tg-spoiler",
|
||||
[typeof(MessageEntityCustomEmoji)] = "tg-emoji",
|
||||
[typeof(MessageEntityBlockquote)] = "blockquote",
|
||||
};
|
||||
|
||||
/// <summary>Replace special HTML characters with their &xx; equivalent</summary>
|
||||
/// <param name="text">The text to make HTML-safe</param>
|
||||
/// <returns>The HTML-safe text, ready to be used in <see cref="HtmlToEntities">HtmlToEntities</see> without problems</returns>
|
||||
public static string Escape(string text)
|
||||
=> text.Replace("&", "&").Replace("<", "<").Replace(">", ">");
|
||||
=> text?.Replace("&", "&").Replace("<", "<").Replace(">", ">");
|
||||
}
|
||||
}
|
||||
107
src/Session.cs
107
src/Session.cs
|
|
@ -7,68 +7,73 @@ using System.Net;
|
|||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
|
||||
// Don't change this code to lower the security. It's following Telegram security recommendations https://corefork.telegram.org/mtproto/description
|
||||
|
||||
namespace WTelegram
|
||||
{
|
||||
internal class Session : IDisposable
|
||||
internal sealed partial class Session : IDisposable
|
||||
{
|
||||
public int ApiId;
|
||||
public long UserId;
|
||||
public int MainDC;
|
||||
public Dictionary<int, DCSession> DCSessions = new();
|
||||
public Dictionary<int, DCSession> DCSessions = [];
|
||||
public TL.DcOption[] DcOptions;
|
||||
|
||||
public class DCSession
|
||||
public sealed class DCSession
|
||||
{
|
||||
public long Id;
|
||||
public long AuthKeyID;
|
||||
public byte[] AuthKey; // 2048-bit = 256 bytes
|
||||
public long UserId;
|
||||
public long OldSalt; // still accepted for a further 1800 seconds
|
||||
public long Salt;
|
||||
public int Seqno;
|
||||
public long ServerTicksOffset;
|
||||
public long LastSentMsgId;
|
||||
public SortedList<DateTime, long> Salts;
|
||||
public TL.DcOption DataCenter;
|
||||
public bool WithoutUpdates;
|
||||
public int Layer;
|
||||
|
||||
internal long id = Helpers.RandomLong();
|
||||
internal long authKeyID;
|
||||
internal int seqno;
|
||||
internal long serverTicksOffset;
|
||||
internal long lastSentMsgId;
|
||||
internal bool withoutUpdates;
|
||||
internal Client Client;
|
||||
internal int DcID => DataCenter?.id ?? 0;
|
||||
internal int DcID => DataCenter == null ? 0 : DataCenter.flags.HasFlag(TL.DcOption.Flags.media_only) ? -DataCenter.id : DataCenter.id;
|
||||
internal IPEndPoint EndPoint => DataCenter == null ? null : new(IPAddress.Parse(DataCenter.ip_address), DataCenter.port);
|
||||
internal void Renew() { Helpers.Log(3, $"Renewing session on DC {DcID}..."); Id = Helpers.RandomLong(); Seqno = 0; LastSentMsgId = 0; }
|
||||
public void DisableUpdates(bool disable = true) { if (WithoutUpdates != disable) { WithoutUpdates = disable; Renew(); } }
|
||||
internal void Renew() { Helpers.Log(3, $"Renewing session on DC {DcID}..."); id = Helpers.RandomLong(); seqno = 0; lastSentMsgId = 0; }
|
||||
public void DisableUpdates(bool disable = true) { if (withoutUpdates != disable) { withoutUpdates = disable; Renew(); } }
|
||||
|
||||
const int msgIdsN = 512;
|
||||
private long[] msgIds;
|
||||
private int msgIdsHead;
|
||||
const int MsgIdsN = 512;
|
||||
private long[] _msgIds;
|
||||
private int _msgIdsHead;
|
||||
internal bool CheckNewMsgId(long msg_id)
|
||||
{
|
||||
if (msgIds == null)
|
||||
if (_msgIds == null)
|
||||
{
|
||||
msgIds = new long[msgIdsN];
|
||||
msgIds[0] = msg_id;
|
||||
_msgIds = new long[MsgIdsN];
|
||||
_msgIds[0] = msg_id;
|
||||
msg_id -= 300L << 32; // until the array is filled with real values, allow ids up to 300 seconds in the past
|
||||
for (int i = 1; i < msgIdsN; i++) msgIds[i] = msg_id;
|
||||
for (int i = 1; i < MsgIdsN; i++) _msgIds[i] = msg_id;
|
||||
return true;
|
||||
}
|
||||
int newHead = (msgIdsHead + 1) % msgIdsN;
|
||||
if (msg_id > msgIds[msgIdsHead])
|
||||
msgIds[msgIdsHead = newHead] = msg_id;
|
||||
else if (msg_id <= msgIds[newHead])
|
||||
int newHead = (_msgIdsHead + 1) % MsgIdsN;
|
||||
if (msg_id > _msgIds[_msgIdsHead])
|
||||
_msgIds[_msgIdsHead = newHead] = msg_id;
|
||||
else if (msg_id <= _msgIds[newHead])
|
||||
return false;
|
||||
else
|
||||
{
|
||||
int min = 0, max = msgIdsN - 1;
|
||||
int min = 0, max = MsgIdsN - 1;
|
||||
while (min <= max) // binary search (rotated at newHead)
|
||||
{
|
||||
int mid = (min + max) / 2;
|
||||
int sign = msg_id.CompareTo(msgIds[(mid + newHead) % msgIdsN]);
|
||||
int sign = msg_id.CompareTo(_msgIds[(mid + newHead) % MsgIdsN]);
|
||||
if (sign == 0) return false;
|
||||
else if (sign < 0) max = mid - 1;
|
||||
else min = mid + 1;
|
||||
}
|
||||
msgIdsHead = newHead;
|
||||
for (min = (min + newHead) % msgIdsN; newHead != min;)
|
||||
msgIds[newHead] = msgIds[newHead = newHead == 0 ? msgIdsN - 1 : newHead - 1];
|
||||
msgIds[min] = msg_id;
|
||||
_msgIdsHead = newHead;
|
||||
for (min = (min + newHead) % MsgIdsN; newHead != min;)
|
||||
_msgIds[newHead] = _msgIds[newHead = newHead == 0 ? MsgIdsN - 1 : newHead - 1];
|
||||
_msgIds[min] = msg_id;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -104,28 +109,31 @@ namespace WTelegram
|
|||
{
|
||||
var input = new byte[length];
|
||||
if (store.Read(input, 0, length) != length)
|
||||
throw new ApplicationException($"Can't read session block ({store.Position}, {length})");
|
||||
throw new WTException($"Can't read session block ({store.Position}, {length})");
|
||||
using var sha256 = SHA256.Create();
|
||||
using var decryptor = aes.CreateDecryptor(rgbKey, input[0..16]);
|
||||
var utf8Json = decryptor.TransformFinalBlock(input, 16, input.Length - 16);
|
||||
if (!sha256.ComputeHash(utf8Json, 32, utf8Json.Length - 32).SequenceEqual(utf8Json[0..32]))
|
||||
throw new ApplicationException("Integrity check failed in session loading");
|
||||
throw new WTException("Integrity check failed in session loading");
|
||||
session = JsonSerializer.Deserialize<Session>(utf8Json.AsSpan(32), Helpers.JsonOptions);
|
||||
Helpers.Log(2, "Loaded previous session");
|
||||
using var sha1 = SHA1.Create();
|
||||
foreach (var dcs in session.DCSessions.Values)
|
||||
dcs.authKeyID = BinaryPrimitives.ReadInt64LittleEndian(sha1.ComputeHash(dcs.AuthKey).AsSpan(12));
|
||||
}
|
||||
session ??= new Session();
|
||||
session._store = store;
|
||||
Encryption.RNG.GetBytes(session._encrypted, 0, 16);
|
||||
session._encryptor = aes.CreateEncryptor(rgbKey, session._encrypted);
|
||||
if (!session._encryptor.CanReuseTransform) session._reuseKey = rgbKey;
|
||||
session._jsonWriter = new Utf8JsonWriter(session._jsonStream, default);
|
||||
return session;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
store.Dispose();
|
||||
throw new ApplicationException($"Exception while reading session file: {ex.Message}\nUse the correct api_hash/id/key, or delete the file to start a new session", ex);
|
||||
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);
|
||||
}
|
||||
session ??= new Session();
|
||||
session._store = store;
|
||||
Encryption.RNG.GetBytes(session._encrypted, 0, 16);
|
||||
session._encryptor = aes.CreateEncryptor(rgbKey, session._encrypted);
|
||||
if (!session._encryptor.CanReuseTransform) session._reuseKey = rgbKey;
|
||||
session._jsonWriter = new Utf8JsonWriter(session._jsonStream, default);
|
||||
return session;
|
||||
}
|
||||
|
||||
internal void Save() // must be called with lock(session)
|
||||
|
|
@ -144,16 +152,23 @@ namespace WTelegram
|
|||
if (!_encryptor.CanReuseTransform) // under Mono, AES encryptor is not reusable
|
||||
using (var aes = Aes.Create())
|
||||
_encryptor = aes.CreateEncryptor(_reuseKey, _encrypted[0..16]);
|
||||
_store.Position = 0;
|
||||
_store.Write(_encrypted, 0, encryptedLen);
|
||||
_store.SetLength(encryptedLen);
|
||||
try
|
||||
{
|
||||
_store.Position = 0;
|
||||
_store.Write(_encrypted, 0, encryptedLen);
|
||||
_store.SetLength(encryptedLen);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Helpers.Log(4, $"{_store} raised {ex}");
|
||||
}
|
||||
}
|
||||
_jsonStream.Position = 0;
|
||||
_jsonWriter.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
internal class SessionStore : FileStream
|
||||
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)
|
||||
{
|
||||
public override long Length { get; }
|
||||
public override long Position { get => base.Position; set { } }
|
||||
|
|
@ -186,4 +201,10 @@ namespace WTelegram
|
|||
base.Write(_header, 0, 8);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ActionStore(byte[] initial, Action<byte[]> save) : MemoryStream(initial ?? [])
|
||||
{
|
||||
public override void Write(byte[] buffer, int offset, int count) => save(buffer[offset..(offset + count)]);
|
||||
public override void SetLength(long value) { }
|
||||
}
|
||||
}
|
||||
|
|
@ -5,8 +5,9 @@ using Client = WTelegram.Client;
|
|||
|
||||
namespace TL
|
||||
{
|
||||
#pragma warning disable IDE1006, CS1574
|
||||
[TLDef(0x05162463)] //resPQ#05162463 nonce:int128 server_nonce:int128 pq:bytes server_public_key_fingerprints:Vector<long> = ResPQ
|
||||
public class ResPQ : IObject
|
||||
public sealed partial class ResPQ : IObject
|
||||
{
|
||||
public Int128 nonce;
|
||||
public Int128 server_nonce;
|
||||
|
|
@ -15,7 +16,7 @@ namespace TL
|
|||
}
|
||||
|
||||
[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
|
||||
public class PQInnerData : IObject
|
||||
public partial class PQInnerData : IObject
|
||||
{
|
||||
public byte[] pq;
|
||||
public byte[] p;
|
||||
|
|
@ -25,24 +26,24 @@ namespace TL
|
|||
public Int256 new_nonce;
|
||||
}
|
||||
[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
|
||||
public class PQInnerDataDc : PQInnerData
|
||||
public sealed partial class PQInnerDataDc : PQInnerData
|
||||
{
|
||||
public int dc;
|
||||
}
|
||||
[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
|
||||
public class PQInnerDataTemp : PQInnerData
|
||||
public sealed partial class PQInnerDataTemp : PQInnerData
|
||||
{
|
||||
public int expires_in;
|
||||
}
|
||||
[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
|
||||
public class PQInnerDataTempDc : PQInnerData
|
||||
public sealed partial class PQInnerDataTempDc : PQInnerData
|
||||
{
|
||||
public int dc;
|
||||
public int expires_in;
|
||||
}
|
||||
|
||||
[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
|
||||
public class BindAuthKeyInner : IObject
|
||||
public sealed partial class BindAuthKeyInner : IObject
|
||||
{
|
||||
public long nonce;
|
||||
public long temp_auth_key_id;
|
||||
|
|
@ -51,24 +52,24 @@ namespace TL
|
|||
public DateTime expires_at;
|
||||
}
|
||||
|
||||
public abstract class ServerDHParams : IObject
|
||||
public abstract partial class ServerDHParams : IObject
|
||||
{
|
||||
public Int128 nonce;
|
||||
public Int128 server_nonce;
|
||||
}
|
||||
[TLDef(0x79CB045D, inheritBefore = true)] //server_DH_params_fail#79cb045d nonce:int128 server_nonce:int128 new_nonce_hash:int128 = Server_DH_Params
|
||||
public class ServerDHParamsFail : ServerDHParams
|
||||
public sealed partial class ServerDHParamsFail : ServerDHParams
|
||||
{
|
||||
public Int128 new_nonce_hash;
|
||||
}
|
||||
[TLDef(0xD0E8075C, inheritBefore = true)] //server_DH_params_ok#d0e8075c nonce:int128 server_nonce:int128 encrypted_answer:bytes = Server_DH_Params
|
||||
public class ServerDHParamsOk : ServerDHParams
|
||||
public sealed partial class ServerDHParamsOk : ServerDHParams
|
||||
{
|
||||
public byte[] encrypted_answer;
|
||||
}
|
||||
|
||||
[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
|
||||
public class ServerDHInnerData : IObject
|
||||
public sealed partial class ServerDHInnerData : IObject
|
||||
{
|
||||
public Int128 nonce;
|
||||
public Int128 server_nonce;
|
||||
|
|
@ -79,7 +80,7 @@ namespace TL
|
|||
}
|
||||
|
||||
[TLDef(0x6643B654)] //client_DH_inner_data#6643b654 nonce:int128 server_nonce:int128 retry_id:long g_b:bytes = Client_DH_Inner_Data
|
||||
public class ClientDHInnerData : IObject
|
||||
public sealed partial class ClientDHInnerData : IObject
|
||||
{
|
||||
public Int128 nonce;
|
||||
public Int128 server_nonce;
|
||||
|
|
@ -87,84 +88,82 @@ namespace TL
|
|||
public byte[] g_b;
|
||||
}
|
||||
|
||||
public abstract class SetClientDHParamsAnswer : IObject
|
||||
public abstract partial class SetClientDHParamsAnswer : IObject
|
||||
{
|
||||
public Int128 nonce;
|
||||
public Int128 server_nonce;
|
||||
}
|
||||
[TLDef(0x3BCBF734, inheritBefore = true)] //dh_gen_ok#3bcbf734 nonce:int128 server_nonce:int128 new_nonce_hash1:int128 = Set_client_DH_params_answer
|
||||
public class DhGenOk : SetClientDHParamsAnswer
|
||||
public sealed partial class DhGenOk : SetClientDHParamsAnswer
|
||||
{
|
||||
public Int128 new_nonce_hash1;
|
||||
}
|
||||
[TLDef(0x46DC1FB9, inheritBefore = true)] //dh_gen_retry#46dc1fb9 nonce:int128 server_nonce:int128 new_nonce_hash2:int128 = Set_client_DH_params_answer
|
||||
public class DhGenRetry : SetClientDHParamsAnswer
|
||||
public sealed partial class DhGenRetry : SetClientDHParamsAnswer
|
||||
{
|
||||
public Int128 new_nonce_hash2;
|
||||
}
|
||||
[TLDef(0xA69DAE02, inheritBefore = true)] //dh_gen_fail#a69dae02 nonce:int128 server_nonce:int128 new_nonce_hash3:int128 = Set_client_DH_params_answer
|
||||
public class DhGenFail : SetClientDHParamsAnswer
|
||||
public sealed partial class DhGenFail : SetClientDHParamsAnswer
|
||||
{
|
||||
public Int128 new_nonce_hash3;
|
||||
}
|
||||
|
||||
public enum DestroyAuthKeyRes : uint
|
||||
{
|
||||
///<summary>See <a href="https://corefork.telegram.org/constructor/destroy_auth_key_ok"/></summary>
|
||||
Ok = 0xF660E1D4,
|
||||
///<summary>See <a href="https://corefork.telegram.org/constructor/destroy_auth_key_none"/></summary>
|
||||
None = 0x0A9F2259,
|
||||
///<summary>See <a href="https://corefork.telegram.org/constructor/destroy_auth_key_fail"/></summary>
|
||||
Fail = 0xEA109B13,
|
||||
}
|
||||
public abstract partial class DestroyAuthKeyRes : IObject { }
|
||||
[TLDef(0xF660E1D4)] //destroy_auth_key_ok#f660e1d4 = DestroyAuthKeyRes
|
||||
public sealed partial class DestroyAuthKeyOk : DestroyAuthKeyRes { }
|
||||
[TLDef(0x0A9F2259)] //destroy_auth_key_none#0a9f2259 = DestroyAuthKeyRes
|
||||
public sealed partial class DestroyAuthKeyNone : DestroyAuthKeyRes { }
|
||||
[TLDef(0xEA109B13)] //destroy_auth_key_fail#ea109b13 = DestroyAuthKeyRes
|
||||
public sealed partial class DestroyAuthKeyFail : DestroyAuthKeyRes { }
|
||||
|
||||
[TLDef(0x62D6B459)] //msgs_ack#62d6b459 msg_ids:Vector<long> = MsgsAck
|
||||
public class MsgsAck : IObject
|
||||
public sealed partial class MsgsAck : IObject
|
||||
{
|
||||
public long[] msg_ids;
|
||||
}
|
||||
|
||||
[TLDef(0xA7EFF811)] //bad_msg_notification#a7eff811 bad_msg_id:long bad_msg_seqno:int error_code:int = BadMsgNotification
|
||||
public class BadMsgNotification : IObject
|
||||
public partial class BadMsgNotification : IObject
|
||||
{
|
||||
public long bad_msg_id;
|
||||
public int bad_msg_seqno;
|
||||
public int error_code;
|
||||
}
|
||||
[TLDef(0xEDAB447B, inheritBefore = true)] //bad_server_salt#edab447b bad_msg_id:long bad_msg_seqno:int error_code:int new_server_salt:long = BadMsgNotification
|
||||
public class BadServerSalt : BadMsgNotification
|
||||
public sealed partial class BadServerSalt : BadMsgNotification
|
||||
{
|
||||
public long new_server_salt;
|
||||
}
|
||||
|
||||
[TLDef(0xDA69FB52)] //msgs_state_req#da69fb52 msg_ids:Vector<long> = MsgsStateReq
|
||||
public class MsgsStateReq : IObject
|
||||
public sealed partial class MsgsStateReq : IObject
|
||||
{
|
||||
public long[] msg_ids;
|
||||
}
|
||||
|
||||
[TLDef(0x04DEB57D)] //msgs_state_info#04deb57d req_msg_id:long info:bytes = MsgsStateInfo
|
||||
public class MsgsStateInfo : IObject
|
||||
public sealed partial class MsgsStateInfo : IObject
|
||||
{
|
||||
public long req_msg_id;
|
||||
public byte[] info;
|
||||
}
|
||||
|
||||
[TLDef(0x8CC0D131)] //msgs_all_info#8cc0d131 msg_ids:Vector<long> info:bytes = MsgsAllInfo
|
||||
public class MsgsAllInfo : IObject
|
||||
public sealed partial class MsgsAllInfo : IObject
|
||||
{
|
||||
public long[] msg_ids;
|
||||
public byte[] info;
|
||||
}
|
||||
|
||||
public abstract class MsgDetailedInfoBase : IObject
|
||||
public abstract partial class MsgDetailedInfoBase : IObject
|
||||
{
|
||||
public virtual long AnswerMsgId { get; }
|
||||
public virtual int Bytes { get; }
|
||||
public virtual int Status { get; }
|
||||
public virtual long AnswerMsgId => default;
|
||||
public virtual int Bytes => default;
|
||||
public virtual int Status => default;
|
||||
}
|
||||
[TLDef(0x276D3EC6)] //msg_detailed_info#276d3ec6 msg_id:long answer_msg_id:long bytes:int status:int = MsgDetailedInfo
|
||||
public class MsgDetailedInfo : MsgDetailedInfoBase
|
||||
public sealed partial class MsgDetailedInfo : MsgDetailedInfoBase
|
||||
{
|
||||
public long msg_id;
|
||||
public long answer_msg_id;
|
||||
|
|
@ -176,7 +175,7 @@ namespace TL
|
|||
public override int Status => status;
|
||||
}
|
||||
[TLDef(0x809DB6DF)] //msg_new_detailed_info#809db6df answer_msg_id:long bytes:int status:int = MsgDetailedInfo
|
||||
public class MsgNewDetailedInfo : MsgDetailedInfoBase
|
||||
public sealed partial class MsgNewDetailedInfo : MsgDetailedInfoBase
|
||||
{
|
||||
public long answer_msg_id;
|
||||
public int bytes;
|
||||
|
|
@ -188,25 +187,25 @@ namespace TL
|
|||
}
|
||||
|
||||
[TLDef(0x7D861A08)] //msg_resend_req#7d861a08 msg_ids:Vector<long> = MsgResendReq
|
||||
public class MsgResendReq : IObject
|
||||
public sealed partial class MsgResendReq : IObject
|
||||
{
|
||||
public long[] msg_ids;
|
||||
}
|
||||
|
||||
[TLDef(0x2144CA19)] //rpc_error#2144ca19 error_code:int error_message:string = RpcError
|
||||
public class RpcError : IObject
|
||||
public sealed partial class RpcError : IObject
|
||||
{
|
||||
public int error_code;
|
||||
public string error_message;
|
||||
}
|
||||
|
||||
public abstract class RpcDropAnswer : IObject { }
|
||||
public abstract partial class RpcDropAnswer : IObject { }
|
||||
[TLDef(0x5E2AD36E)] //rpc_answer_unknown#5e2ad36e = RpcDropAnswer
|
||||
public class RpcAnswerUnknown : RpcDropAnswer { }
|
||||
public sealed partial class RpcAnswerUnknown : RpcDropAnswer { }
|
||||
[TLDef(0xCD78E586)] //rpc_answer_dropped_running#cd78e586 = RpcDropAnswer
|
||||
public class RpcAnswerDroppedRunning : RpcDropAnswer { }
|
||||
public sealed partial class RpcAnswerDroppedRunning : RpcDropAnswer { }
|
||||
[TLDef(0xA43AD8B7)] //rpc_answer_dropped#a43ad8b7 msg_id:long seq_no:int bytes:int = RpcDropAnswer
|
||||
public class RpcAnswerDropped : RpcDropAnswer
|
||||
public sealed partial class RpcAnswerDropped : RpcDropAnswer
|
||||
{
|
||||
public long msg_id;
|
||||
public int seq_no;
|
||||
|
|
@ -214,7 +213,7 @@ namespace TL
|
|||
}
|
||||
|
||||
[TLDef(0x0949D9DC)] //future_salt#0949d9dc valid_since:int valid_until:int salt:long = FutureSalt
|
||||
public class FutureSalt : IObject
|
||||
public sealed partial class FutureSalt : IObject
|
||||
{
|
||||
public DateTime valid_since;
|
||||
public DateTime valid_until;
|
||||
|
|
@ -222,7 +221,7 @@ namespace TL
|
|||
}
|
||||
|
||||
[TLDef(0xAE500895)] //future_salts#ae500895 req_msg_id:long now:int salts:vector<future_salt> = FutureSalts
|
||||
public class FutureSalts : IObject
|
||||
public sealed partial class FutureSalts : IObject
|
||||
{
|
||||
public long req_msg_id;
|
||||
public DateTime now;
|
||||
|
|
@ -230,24 +229,24 @@ namespace TL
|
|||
}
|
||||
|
||||
[TLDef(0x347773C5)] //pong#347773c5 msg_id:long ping_id:long = Pong
|
||||
public class Pong : IObject
|
||||
public sealed partial class Pong : IObject
|
||||
{
|
||||
public long msg_id;
|
||||
public long ping_id;
|
||||
}
|
||||
|
||||
public abstract class DestroySessionRes : IObject
|
||||
public abstract partial class DestroySessionRes : IObject
|
||||
{
|
||||
public long session_id;
|
||||
}
|
||||
[TLDef(0xE22045FC)] //destroy_session_ok#e22045fc session_id:long = DestroySessionRes
|
||||
public class DestroySessionOk : DestroySessionRes { }
|
||||
public sealed partial class DestroySessionOk : DestroySessionRes { }
|
||||
[TLDef(0x62D350C9)] //destroy_session_none#62d350c9 session_id:long = DestroySessionRes
|
||||
public class DestroySessionNone : DestroySessionRes { }
|
||||
public sealed partial class DestroySessionNone : DestroySessionRes { }
|
||||
|
||||
public abstract class NewSession : IObject { }
|
||||
public abstract partial class NewSession : IObject { }
|
||||
[TLDef(0x9EC20908)] //new_session_created#9ec20908 first_msg_id:long unique_id:long server_salt:long = NewSession
|
||||
public class NewSessionCreated : NewSession
|
||||
public sealed partial class NewSessionCreated : NewSession
|
||||
{
|
||||
public long first_msg_id;
|
||||
public long unique_id;
|
||||
|
|
@ -255,7 +254,7 @@ namespace TL
|
|||
}
|
||||
|
||||
[TLDef(0x9299359F)] //http_wait#9299359f max_delay:int wait_after:int max_wait:int = HttpWait
|
||||
public class HttpWait : IObject
|
||||
public sealed partial class HttpWait : IObject
|
||||
{
|
||||
public int max_delay;
|
||||
public int wait_after;
|
||||
|
|
@ -263,19 +262,19 @@ namespace TL
|
|||
}
|
||||
|
||||
[TLDef(0xD433AD73)] //ipPort#d433ad73 ipv4:int port:int = IpPort
|
||||
public class IpPort : IObject
|
||||
public partial class IpPort : IObject
|
||||
{
|
||||
public int ipv4;
|
||||
public int port;
|
||||
}
|
||||
[TLDef(0x37982646, inheritBefore = true)] //ipPortSecret#37982646 ipv4:int port:int secret:bytes = IpPort
|
||||
public class IpPortSecret : IpPort
|
||||
public sealed partial class IpPortSecret : IpPort
|
||||
{
|
||||
public byte[] secret;
|
||||
}
|
||||
|
||||
[TLDef(0x4679B65F)] //accessPointRule#4679b65f phone_prefix_rules:bytes dc_id:int ips:vector<IpPort> = AccessPointRule
|
||||
public class AccessPointRule : IObject
|
||||
public sealed partial class AccessPointRule : IObject
|
||||
{
|
||||
public byte[] phone_prefix_rules;
|
||||
public int dc_id;
|
||||
|
|
@ -283,7 +282,7 @@ namespace TL
|
|||
}
|
||||
|
||||
[TLDef(0x5A592A6C)] //help.configSimple#5a592a6c date:int expires:int rules:vector<AccessPointRule> = help.ConfigSimple
|
||||
public class Help_ConfigSimple : IObject
|
||||
public sealed partial class Help_ConfigSimple : IObject
|
||||
{
|
||||
public DateTime date;
|
||||
public DateTime expires;
|
||||
|
|
@ -326,12 +325,12 @@ namespace TL
|
|||
});
|
||||
|
||||
public static Task<DestroyAuthKeyRes> DestroyAuthKey(this Client client)
|
||||
=> client.InvokeBare(new DestroyAuthKey
|
||||
=> client.Invoke(new DestroyAuthKey
|
||||
{
|
||||
});
|
||||
|
||||
public static Task<RpcDropAnswer> RpcDropAnswer(this Client client, long req_msg_id)
|
||||
=> client.InvokeBare(new Methods.RpcDropAnswer
|
||||
=> client.Invoke(new Methods.RpcDropAnswer
|
||||
{
|
||||
req_msg_id = req_msg_id,
|
||||
});
|
||||
|
|
@ -356,7 +355,7 @@ namespace TL
|
|||
});
|
||||
|
||||
public static Task<DestroySessionRes> DestroySession(this Client client, long session_id)
|
||||
=> client.InvokeBare(new DestroySession
|
||||
=> client.Invoke(new DestroySession
|
||||
{
|
||||
session_id = session_id,
|
||||
});
|
||||
|
|
@ -365,20 +364,21 @@ namespace TL
|
|||
|
||||
namespace TL.Methods
|
||||
{
|
||||
#pragma warning disable IDE1006
|
||||
[TLDef(0x60469778)] //req_pq#60469778 nonce:int128 = ResPQ
|
||||
public class ReqPq : IMethod<ResPQ>
|
||||
public sealed partial class ReqPq : IMethod<ResPQ>
|
||||
{
|
||||
public Int128 nonce;
|
||||
}
|
||||
|
||||
[TLDef(0xBE7E8EF1)] //req_pq_multi#be7e8ef1 nonce:int128 = ResPQ
|
||||
public class ReqPqMulti : IMethod<ResPQ>
|
||||
public sealed partial class ReqPqMulti : IMethod<ResPQ>
|
||||
{
|
||||
public Int128 nonce;
|
||||
}
|
||||
|
||||
[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
|
||||
public class ReqDHParams : IMethod<ServerDHParams>
|
||||
public sealed partial class ReqDHParams : IMethod<ServerDHParams>
|
||||
{
|
||||
public Int128 nonce;
|
||||
public Int128 server_nonce;
|
||||
|
|
@ -389,7 +389,7 @@ namespace TL.Methods
|
|||
}
|
||||
|
||||
[TLDef(0xF5045F1F)] //set_client_DH_params#f5045f1f nonce:int128 server_nonce:int128 encrypted_data:bytes = Set_client_DH_params_answer
|
||||
public class SetClientDHParams : IMethod<SetClientDHParamsAnswer>
|
||||
public sealed partial class SetClientDHParams : IMethod<SetClientDHParamsAnswer>
|
||||
{
|
||||
public Int128 nonce;
|
||||
public Int128 server_nonce;
|
||||
|
|
@ -397,35 +397,35 @@ namespace TL.Methods
|
|||
}
|
||||
|
||||
[TLDef(0xD1435160)] //destroy_auth_key#d1435160 = DestroyAuthKeyRes
|
||||
public class DestroyAuthKey : IMethod<DestroyAuthKeyRes> { }
|
||||
public sealed partial class DestroyAuthKey : IMethod<DestroyAuthKeyRes> { }
|
||||
|
||||
[TLDef(0x58E4A740)] //rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer
|
||||
public class RpcDropAnswer : IMethod<TL.RpcDropAnswer>
|
||||
public sealed partial class RpcDropAnswer : IMethod<TL.RpcDropAnswer>
|
||||
{
|
||||
public long req_msg_id;
|
||||
}
|
||||
|
||||
[TLDef(0xB921BD04)] //get_future_salts#b921bd04 num:int = FutureSalts
|
||||
public class GetFutureSalts : IMethod<FutureSalts>
|
||||
public sealed partial class GetFutureSalts : IMethod<FutureSalts>
|
||||
{
|
||||
public int num;
|
||||
}
|
||||
|
||||
[TLDef(0x7ABE77EC)] //ping#7abe77ec ping_id:long = Pong
|
||||
public class Ping : IMethod<Pong>
|
||||
public sealed partial class Ping : IMethod<Pong>
|
||||
{
|
||||
public long ping_id;
|
||||
}
|
||||
|
||||
[TLDef(0xF3427B8C)] //ping_delay_disconnect#f3427b8c ping_id:long disconnect_delay:int = Pong
|
||||
public class PingDelayDisconnect : IMethod<Pong>
|
||||
public sealed partial class PingDelayDisconnect : IMethod<Pong>
|
||||
{
|
||||
public long ping_id;
|
||||
public int disconnect_delay;
|
||||
}
|
||||
|
||||
[TLDef(0xE7512126)] //destroy_session#e7512126 session_id:long = DestroySessionRes
|
||||
public class DestroySession : IMethod<DestroySessionRes>
|
||||
public sealed partial class DestroySession : IMethod<DestroySessionRes>
|
||||
{
|
||||
public long session_id;
|
||||
}
|
||||
|
|
|
|||
11991
src/TL.Schema.cs
11991
src/TL.Schema.cs
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
220
src/TL.Secret.cs
220
src/TL.Secret.cs
|
|
@ -2,61 +2,64 @@
|
|||
|
||||
namespace TL
|
||||
{
|
||||
/// <summary>Object describes the contents of an encrypted message. <para>See <a href="https://corefork.telegram.org/type/DecryptedMessage"/></para></summary>
|
||||
public abstract class DecryptedMessageBase : IObject
|
||||
#pragma warning disable IDE1006, CS1574
|
||||
/// <summary>Object describes the contents of an encrypted message. <para>See <a href="https://corefork.telegram.org/type/DecryptedMessage"/></para> <para>Derived classes: <see cref="DecryptedMessage"/>, <see cref="DecryptedMessageService"/></para></summary>
|
||||
public abstract partial class DecryptedMessageBase : IObject
|
||||
{
|
||||
/// <summary>Flags, see <a href="https://corefork.telegram.org/mtproto/TL-combinators#conditional-fields">TL conditional fields</a> (added in layer 45)</summary>
|
||||
public virtual uint FFlags { get; }
|
||||
public virtual uint FFlags => default;
|
||||
/// <summary>Random message ID, assigned by the author of message.<br/>Must be equal to the ID passed to sending method.</summary>
|
||||
public virtual long RandomId { get; }
|
||||
public virtual long RandomId => default;
|
||||
/// <summary>Message lifetime. Has higher priority than <see cref="Layer8.DecryptedMessageActionSetMessageTTL"/>.<br/>Parameter added in Layer 17.</summary>
|
||||
public virtual int Ttl { get; }
|
||||
public virtual int Ttl => default;
|
||||
/// <summary>Message text</summary>
|
||||
public virtual string Message { get; }
|
||||
public virtual string Message => default;
|
||||
/// <summary>Media content</summary>
|
||||
public virtual DecryptedMessageMedia Media { get; }
|
||||
public virtual DecryptedMessageMedia Media => default;
|
||||
/// <summary>Message <a href="https://corefork.telegram.org/api/entities">entities</a> for styled text (parameter added in layer 45)</summary>
|
||||
public virtual MessageEntity[] Entities { get; }
|
||||
public virtual MessageEntity[] Entities => default;
|
||||
/// <summary>Specifies the ID of the inline bot that generated the message (parameter added in layer 45)</summary>
|
||||
public virtual string ViaBotName { get; }
|
||||
public virtual string ViaBotName => default;
|
||||
/// <summary>Random message ID of the message this message replies to (parameter added in layer 45)</summary>
|
||||
public virtual long ReplyToRandom { get; }
|
||||
public virtual long ReplyToRandom => default;
|
||||
/// <summary>Random group ID, assigned by the author of message.<br/>Multiple encrypted messages with a photo attached and with the same group ID indicate an <a href="https://corefork.telegram.org/api/files#albums-grouped-media">album or grouped media</a> (parameter added in layer 45)</summary>
|
||||
public virtual long Grouped { get; }
|
||||
public virtual byte[] RandomBytes { get; }
|
||||
public virtual DecryptedMessageAction Action { get; }
|
||||
public virtual long Grouped => default;
|
||||
/// <summary>Random bytes, removed in layer 17.</summary>
|
||||
public virtual byte[] RandomBytes => default;
|
||||
public virtual DecryptedMessageAction Action => default;
|
||||
}
|
||||
|
||||
/// <summary>Object describes media contents of an encrypted message. <para>See <a href="https://corefork.telegram.org/type/DecryptedMessageMedia"/></para></summary>
|
||||
/// <summary>Object describes media contents of an encrypted message. <para>See <a href="https://corefork.telegram.org/type/DecryptedMessageMedia"/></para> <para>Derived classes: <see cref="DecryptedMessageMediaPhoto"/>, <see cref="DecryptedMessageMediaVideo"/>, <see cref="DecryptedMessageMediaGeoPoint"/>, <see cref="DecryptedMessageMediaContact"/>, <see cref="DecryptedMessageMediaDocument"/>, <see cref="DecryptedMessageMediaAudio"/>, <see cref="DecryptedMessageMediaExternalDocument"/>, <see cref="DecryptedMessageMediaVenue"/>, <see cref="DecryptedMessageMediaWebPage"/></para></summary>
|
||||
/// <remarks>a <see langword="null"/> value means <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaEmpty">decryptedMessageMediaEmpty</a></remarks>
|
||||
public abstract class DecryptedMessageMedia : IObject
|
||||
public abstract partial class DecryptedMessageMedia : IObject
|
||||
{
|
||||
public virtual string MimeType { get; }
|
||||
internal virtual (long size, byte[] key, byte[] iv) SizeKeyIV { get => default; set => throw new ApplicationException("Incompatible DecryptedMessageMedia"); }
|
||||
public virtual string MimeType => default;
|
||||
internal virtual (long size, byte[] key, byte[] iv) SizeKeyIV { get => default; set => throw new WTelegram.WTException("Incompatible DecryptedMessageMedia"); }
|
||||
}
|
||||
|
||||
/// <summary>Object describes the action to which a service message is linked. <para>See <a href="https://corefork.telegram.org/type/DecryptedMessageAction"/></para></summary>
|
||||
public abstract class DecryptedMessageAction : IObject { }
|
||||
/// <summary>Object describes the action to which a service message is linked. <para>See <a href="https://corefork.telegram.org/type/DecryptedMessageAction"/></para> <para>Derived classes: <see cref="DecryptedMessageActionSetMessageTTL"/>, <see cref="DecryptedMessageActionReadMessages"/>, <see cref="DecryptedMessageActionDeleteMessages"/>, <see cref="DecryptedMessageActionScreenshotMessages"/>, <see cref="DecryptedMessageActionFlushHistory"/>, <see cref="DecryptedMessageActionResend"/>, <see cref="DecryptedMessageActionNotifyLayer"/>, <see cref="DecryptedMessageActionTyping"/>, <see cref="DecryptedMessageActionRequestKey"/>, <see cref="DecryptedMessageActionAcceptKey"/>, <see cref="DecryptedMessageActionAbortKey"/>, <see cref="DecryptedMessageActionCommitKey"/>, <see cref="DecryptedMessageActionNoop"/></para></summary>
|
||||
public abstract partial class DecryptedMessageAction : IObject { }
|
||||
|
||||
/// <summary>Indicates the location of a photo, will be deprecated soon <para>See <a href="https://corefork.telegram.org/type/FileLocation"/></para></summary>
|
||||
public abstract class FileLocationBase : IObject
|
||||
/// <summary>Indicates the location of a photo, will be deprecated soon <para>See <a href="https://corefork.telegram.org/type/FileLocation"/></para> <para>Derived classes: <see cref="FileLocationUnavailable"/>, <see cref="FileLocation"/></para></summary>
|
||||
public abstract partial class FileLocationBase : IObject
|
||||
{
|
||||
/// <summary>Server volume</summary>
|
||||
public virtual long VolumeId { get; }
|
||||
/// <summary>File ID</summary>
|
||||
public virtual int LocalId { get; }
|
||||
/// <summary>Checksum to access the file</summary>
|
||||
public virtual long Secret { get; }
|
||||
/// <summary>Volume ID</summary>
|
||||
public virtual long VolumeId => default;
|
||||
/// <summary>Local ID</summary>
|
||||
public virtual int LocalId => default;
|
||||
/// <summary>Secret</summary>
|
||||
public virtual long Secret => default;
|
||||
}
|
||||
|
||||
namespace Layer8
|
||||
{
|
||||
/// <summary>Contents of an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessage"/></para></summary>
|
||||
[TLDef(0x1F814F1F)]
|
||||
public class DecryptedMessage : DecryptedMessageBase
|
||||
public sealed partial class DecryptedMessage : DecryptedMessageBase
|
||||
{
|
||||
/// <summary>Random message ID, assigned by the author of message.<br/>Must be equal to the ID passed to sending method.</summary>
|
||||
public long random_id;
|
||||
/// <summary>Random bytes, removed in layer 17.</summary>
|
||||
public byte[] random_bytes;
|
||||
/// <summary>Message text</summary>
|
||||
public string message;
|
||||
|
|
@ -69,20 +72,23 @@ namespace TL
|
|||
public override string Message => message;
|
||||
/// <summary>Media content</summary>
|
||||
public override DecryptedMessageMedia Media => media;
|
||||
/// <summary>Random bytes, removed in layer 17.</summary>
|
||||
public override byte[] RandomBytes => random_bytes;
|
||||
}
|
||||
/// <summary>Contents of an encrypted service message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageService"/></para></summary>
|
||||
[TLDef(0xAA48327D)]
|
||||
public class DecryptedMessageService : DecryptedMessageBase
|
||||
public sealed partial class DecryptedMessageService : DecryptedMessageBase
|
||||
{
|
||||
/// <summary>Random message ID, assigned by the message author.<br/>Must be equal to the ID passed to the sending method.</summary>
|
||||
public long random_id;
|
||||
/// <summary>Random bytes, removed in Layer 17.</summary>
|
||||
public byte[] random_bytes;
|
||||
/// <summary>Action relevant to the service message</summary>
|
||||
public DecryptedMessageAction action;
|
||||
|
||||
/// <summary>Random message ID, assigned by the message author.<br/>Must be equal to the ID passed to the sending method.</summary>
|
||||
public override long RandomId => random_id;
|
||||
/// <summary>Random bytes, removed in Layer 17.</summary>
|
||||
public override byte[] RandomBytes => random_bytes;
|
||||
/// <summary>Action relevant to the service message</summary>
|
||||
public override DecryptedMessageAction Action => action;
|
||||
|
|
@ -90,7 +96,7 @@ namespace TL
|
|||
|
||||
/// <summary>Photo attached to an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaPhoto"/></para></summary>
|
||||
[TLDef(0x32798A8C)]
|
||||
public class DecryptedMessageMediaPhoto : DecryptedMessageMedia
|
||||
public sealed partial class DecryptedMessageMediaPhoto : DecryptedMessageMedia
|
||||
{
|
||||
/// <summary>Content of thumbnail file (JPEGfile, quality 55, set in a square 90x90)</summary>
|
||||
public byte[] thumb;
|
||||
|
|
@ -114,7 +120,7 @@ namespace TL
|
|||
}
|
||||
/// <summary>Video attached to an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaVideo"/></para></summary>
|
||||
[TLDef(0x4CEE6EF3)]
|
||||
public class DecryptedMessageMediaVideo : DecryptedMessageMedia
|
||||
public sealed partial class DecryptedMessageMediaVideo : DecryptedMessageMedia
|
||||
{
|
||||
/// <summary>Content of thumbnail file (JPEG file, quality 55, set in a square 90x90)</summary>
|
||||
public byte[] thumb;
|
||||
|
|
@ -139,7 +145,7 @@ namespace TL
|
|||
}
|
||||
/// <summary>GeoPoint attached to an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaGeoPoint"/></para></summary>
|
||||
[TLDef(0x35480A59)]
|
||||
public class DecryptedMessageMediaGeoPoint : DecryptedMessageMedia
|
||||
public sealed partial class DecryptedMessageMediaGeoPoint : DecryptedMessageMedia
|
||||
{
|
||||
/// <summary>Latitude of point</summary>
|
||||
public double lat;
|
||||
|
|
@ -148,7 +154,7 @@ namespace TL
|
|||
}
|
||||
/// <summary>Contact attached to an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaContact"/></para></summary>
|
||||
[TLDef(0x588A0A97)]
|
||||
public class DecryptedMessageMediaContact : DecryptedMessageMedia
|
||||
public sealed partial class DecryptedMessageMediaContact : DecryptedMessageMedia
|
||||
{
|
||||
/// <summary>Phone number</summary>
|
||||
public string phone_number;
|
||||
|
|
@ -161,7 +167,7 @@ namespace TL
|
|||
}
|
||||
/// <summary>Document attached to a message in a secret chat. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaDocument"/></para></summary>
|
||||
[TLDef(0xB095434B)]
|
||||
public class DecryptedMessageMediaDocument : DecryptedMessageMedia
|
||||
public sealed partial class DecryptedMessageMediaDocument : DecryptedMessageMedia
|
||||
{
|
||||
/// <summary>Thumbnail-file contents (JPEG-file, quality 55, set in a 90x90 square)</summary>
|
||||
public byte[] thumb;
|
||||
|
|
@ -169,6 +175,7 @@ namespace TL
|
|||
public int thumb_w;
|
||||
/// <summary>Thumbnail height</summary>
|
||||
public int thumb_h;
|
||||
/// <summary>File name, moved to <c>attributes</c> in Layer 45.</summary>
|
||||
public string file_name;
|
||||
/// <summary>File MIME-type</summary>
|
||||
public string mime_type;
|
||||
|
|
@ -186,7 +193,7 @@ namespace TL
|
|||
}
|
||||
/// <summary>Audio file attached to a secret chat message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaAudio"/></para></summary>
|
||||
[TLDef(0x6080758F)]
|
||||
public class DecryptedMessageMediaAudio : DecryptedMessageMedia
|
||||
public sealed partial class DecryptedMessageMediaAudio : DecryptedMessageMedia
|
||||
{
|
||||
/// <summary>Audio duration in seconds</summary>
|
||||
public int duration;
|
||||
|
|
@ -202,42 +209,42 @@ namespace TL
|
|||
|
||||
/// <summary>Setting of a message lifetime after reading. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionSetMessageTTL"/></para></summary>
|
||||
[TLDef(0xA1733AEC)]
|
||||
public class DecryptedMessageActionSetMessageTTL : DecryptedMessageAction
|
||||
public sealed partial class DecryptedMessageActionSetMessageTTL : DecryptedMessageAction
|
||||
{
|
||||
/// <summary>Lifetime in seconds</summary>
|
||||
public int ttl_seconds;
|
||||
}
|
||||
/// <summary>Messages marked as read. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionReadMessages"/></para></summary>
|
||||
[TLDef(0x0C4F40BE)]
|
||||
public class DecryptedMessageActionReadMessages : DecryptedMessageAction
|
||||
public sealed partial class DecryptedMessageActionReadMessages : DecryptedMessageAction
|
||||
{
|
||||
/// <summary>List of message IDs</summary>
|
||||
public long[] random_ids;
|
||||
}
|
||||
/// <summary>Deleted messages. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionDeleteMessages"/></para></summary>
|
||||
[TLDef(0x65614304)]
|
||||
public class DecryptedMessageActionDeleteMessages : DecryptedMessageAction
|
||||
public sealed partial class DecryptedMessageActionDeleteMessages : DecryptedMessageAction
|
||||
{
|
||||
/// <summary>List of deleted message IDs</summary>
|
||||
public long[] random_ids;
|
||||
}
|
||||
/// <summary>A screenshot was taken. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionScreenshotMessages"/></para></summary>
|
||||
[TLDef(0x8AC1F475)]
|
||||
public class DecryptedMessageActionScreenshotMessages : DecryptedMessageAction
|
||||
public sealed partial class DecryptedMessageActionScreenshotMessages : DecryptedMessageAction
|
||||
{
|
||||
/// <summary>List of affected message ids (that appeared on the screenshot)</summary>
|
||||
public long[] random_ids;
|
||||
}
|
||||
/// <summary>The entire message history has been deleted. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionFlushHistory"/></para></summary>
|
||||
[TLDef(0x6719E45C)]
|
||||
public class DecryptedMessageActionFlushHistory : DecryptedMessageAction { }
|
||||
public sealed partial class DecryptedMessageActionFlushHistory : DecryptedMessageAction { }
|
||||
}
|
||||
|
||||
namespace Layer23
|
||||
{
|
||||
/// <summary>Image description. <para>See <a href="https://corefork.telegram.org/constructor/photoSize"/></para></summary>
|
||||
[TLDef(0x77BFB61B)]
|
||||
public partial class PhotoSize : PhotoSizeBase
|
||||
public sealed partial class PhotoSize : PhotoSizeBase
|
||||
{
|
||||
/// <summary><a href="https://corefork.telegram.org/api/files#image-thumbnail-types">Thumbnail type »</a></summary>
|
||||
public string type;
|
||||
|
|
@ -254,7 +261,7 @@ namespace TL
|
|||
}
|
||||
/// <summary>Description of an image and its content. <para>See <a href="https://corefork.telegram.org/constructor/photoCachedSize"/></para></summary>
|
||||
[TLDef(0xE9A734FA)]
|
||||
public partial class PhotoCachedSize : PhotoSizeBase
|
||||
public sealed partial class PhotoCachedSize : PhotoSizeBase
|
||||
{
|
||||
/// <summary>Thumbnail type</summary>
|
||||
public string type;
|
||||
|
|
@ -272,23 +279,23 @@ namespace TL
|
|||
|
||||
/// <summary>User is uploading a video. <para>See <a href="https://corefork.telegram.org/constructor/sendMessageUploadVideoAction"/></para></summary>
|
||||
[TLDef(0x92042FF7)]
|
||||
public class SendMessageUploadVideoAction : SendMessageAction { }
|
||||
public sealed partial class SendMessageUploadVideoAction : SendMessageAction { }
|
||||
/// <summary>User is uploading a voice message. <para>See <a href="https://corefork.telegram.org/constructor/sendMessageUploadAudioAction"/></para></summary>
|
||||
[TLDef(0xE6AC8A6F)]
|
||||
public class SendMessageUploadAudioAction : SendMessageAction { }
|
||||
public sealed partial class SendMessageUploadAudioAction : SendMessageAction { }
|
||||
/// <summary>User is uploading a photo. <para>See <a href="https://corefork.telegram.org/constructor/sendMessageUploadPhotoAction"/></para></summary>
|
||||
[TLDef(0x990A3C1A)]
|
||||
public class SendMessageUploadPhotoAction : SendMessageAction { }
|
||||
public sealed partial class SendMessageUploadPhotoAction : SendMessageAction { }
|
||||
/// <summary>User is uploading a file. <para>See <a href="https://corefork.telegram.org/constructor/sendMessageUploadDocumentAction"/></para></summary>
|
||||
[TLDef(0x8FAEE98E)]
|
||||
public class SendMessageUploadDocumentAction : SendMessageAction { }
|
||||
public sealed partial class SendMessageUploadDocumentAction : SendMessageAction { }
|
||||
|
||||
/// <summary>Defines a sticker <para>See <a href="https://corefork.telegram.org/constructor/documentAttributeSticker"/></para></summary>
|
||||
[TLDef(0xFB0A5727)]
|
||||
public class DocumentAttributeSticker : DocumentAttribute { }
|
||||
public sealed partial class DocumentAttributeSticker : DocumentAttribute { }
|
||||
/// <summary>Defines a video <para>See <a href="https://corefork.telegram.org/constructor/documentAttributeVideo"/></para></summary>
|
||||
[TLDef(0x5910CCCB)]
|
||||
public class DocumentAttributeVideo : DocumentAttribute
|
||||
public sealed partial class DocumentAttributeVideo : DocumentAttribute
|
||||
{
|
||||
/// <summary>Duration in seconds</summary>
|
||||
public int duration;
|
||||
|
|
@ -299,7 +306,7 @@ namespace TL
|
|||
}
|
||||
/// <summary>Represents an audio file <para>See <a href="https://corefork.telegram.org/constructor/documentAttributeAudio"/></para></summary>
|
||||
[TLDef(0x051448E5)]
|
||||
public class DocumentAttributeAudio : DocumentAttribute
|
||||
public sealed partial class DocumentAttributeAudio : DocumentAttribute
|
||||
{
|
||||
/// <summary>Duration in seconds</summary>
|
||||
public int duration;
|
||||
|
|
@ -307,7 +314,7 @@ namespace TL
|
|||
|
||||
/// <summary>Contents of an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessage"/></para></summary>
|
||||
[TLDef(0x204D3878)]
|
||||
public class DecryptedMessage : DecryptedMessageBase
|
||||
public sealed partial class DecryptedMessage : DecryptedMessageBase
|
||||
{
|
||||
/// <summary>Random message ID, assigned by the author of message.<br/>Must be equal to the ID passed to sending method.</summary>
|
||||
public long random_id;
|
||||
|
|
@ -329,7 +336,7 @@ namespace TL
|
|||
}
|
||||
/// <summary>Contents of an encrypted service message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageService"/></para></summary>
|
||||
[TLDef(0x73164160)]
|
||||
public class DecryptedMessageService : DecryptedMessageBase
|
||||
public sealed partial class DecryptedMessageService : DecryptedMessageBase
|
||||
{
|
||||
/// <summary>Random message ID, assigned by the message author.<br/>Must be equal to the ID passed to the sending method.</summary>
|
||||
public long random_id;
|
||||
|
|
@ -344,7 +351,7 @@ namespace TL
|
|||
|
||||
/// <summary>Video attached to an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaVideo"/></para></summary>
|
||||
[TLDef(0x524A415D)]
|
||||
public class DecryptedMessageMediaVideo : DecryptedMessageMedia
|
||||
public sealed partial class DecryptedMessageMediaVideo : DecryptedMessageMedia
|
||||
{
|
||||
/// <summary>Content of thumbnail file (JPEG file, quality 55, set in a square 90x90)</summary>
|
||||
public byte[] thumb;
|
||||
|
|
@ -374,7 +381,7 @@ namespace TL
|
|||
}
|
||||
/// <summary>Audio file attached to a secret chat message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaAudio"/></para></summary>
|
||||
[TLDef(0x57E0A9CB)]
|
||||
public class DecryptedMessageMediaAudio : DecryptedMessageMedia
|
||||
public sealed partial class DecryptedMessageMediaAudio : DecryptedMessageMedia
|
||||
{
|
||||
/// <summary>Audio duration in seconds</summary>
|
||||
public int duration;
|
||||
|
|
@ -394,7 +401,7 @@ namespace TL
|
|||
}
|
||||
/// <summary>Non-e2e documented forwarded from non-secret chat <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaExternalDocument"/></para></summary>
|
||||
[TLDef(0xFA95B0DD)]
|
||||
public class DecryptedMessageMediaExternalDocument : DecryptedMessageMedia
|
||||
public sealed partial class DecryptedMessageMediaExternalDocument : DecryptedMessageMedia
|
||||
{
|
||||
/// <summary>Document ID</summary>
|
||||
public long id;
|
||||
|
|
@ -419,7 +426,7 @@ namespace TL
|
|||
|
||||
/// <summary>Request for the other party in a Secret Chat to automatically resend a contiguous range of previously sent messages, as explained in <a href="https://corefork.telegram.org/api/end-to-end/seq_no">Sequence number is Secret Chats</a>. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionResend"/></para></summary>
|
||||
[TLDef(0x511110B0)]
|
||||
public class DecryptedMessageActionResend : DecryptedMessageAction
|
||||
public sealed partial class DecryptedMessageActionResend : DecryptedMessageAction
|
||||
{
|
||||
/// <summary><c>out_seq_no</c> of the first message to be resent, with correct parity</summary>
|
||||
public int start_seq_no;
|
||||
|
|
@ -428,21 +435,21 @@ namespace TL
|
|||
}
|
||||
/// <summary>A notification stating the API layer that is used by the client. You should use your current layer and take notice of the layer used on the other side of a conversation when sending messages. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionNotifyLayer"/></para></summary>
|
||||
[TLDef(0xF3048883)]
|
||||
public class DecryptedMessageActionNotifyLayer : DecryptedMessageAction
|
||||
public sealed partial class DecryptedMessageActionNotifyLayer : DecryptedMessageAction
|
||||
{
|
||||
/// <summary>Layer number, must be <strong>17</strong> or higher (this constructor was introduced in Layer 17.</summary>
|
||||
public int layer;
|
||||
}
|
||||
/// <summary>User is preparing a message: typing, recording, uploading, etc. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionTyping"/></para></summary>
|
||||
[TLDef(0xCCB27641)]
|
||||
public class DecryptedMessageActionTyping : DecryptedMessageAction
|
||||
public sealed partial class DecryptedMessageActionTyping : DecryptedMessageAction
|
||||
{
|
||||
/// <summary>Type of action</summary>
|
||||
public SendMessageAction action;
|
||||
}
|
||||
/// <summary>Request rekeying, see <a href="https://corefork.telegram.org/api/end-to-end/pfs">rekeying process</a> <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionRequestKey"/></para></summary>
|
||||
[TLDef(0xF3C9611B)]
|
||||
public class DecryptedMessageActionRequestKey : DecryptedMessageAction
|
||||
public sealed partial class DecryptedMessageActionRequestKey : DecryptedMessageAction
|
||||
{
|
||||
/// <summary>Exchange ID</summary>
|
||||
public long exchange_id;
|
||||
|
|
@ -451,7 +458,7 @@ namespace TL
|
|||
}
|
||||
/// <summary>Accept new key <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionAcceptKey"/></para></summary>
|
||||
[TLDef(0x6FE1735B)]
|
||||
public class DecryptedMessageActionAcceptKey : DecryptedMessageAction
|
||||
public sealed partial class DecryptedMessageActionAcceptKey : DecryptedMessageAction
|
||||
{
|
||||
/// <summary>Exchange ID</summary>
|
||||
public long exchange_id;
|
||||
|
|
@ -462,14 +469,14 @@ namespace TL
|
|||
}
|
||||
/// <summary>Abort rekeying <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionAbortKey"/></para></summary>
|
||||
[TLDef(0xDD05EC6B)]
|
||||
public class DecryptedMessageActionAbortKey : DecryptedMessageAction
|
||||
public sealed partial class DecryptedMessageActionAbortKey : DecryptedMessageAction
|
||||
{
|
||||
/// <summary>Exchange ID</summary>
|
||||
public long exchange_id;
|
||||
}
|
||||
/// <summary>Commit new key, see <a href="https://corefork.telegram.org/api/end-to-end/pfs">rekeying process</a> <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionCommitKey"/></para></summary>
|
||||
[TLDef(0xEC2E0B9B)]
|
||||
public class DecryptedMessageActionCommitKey : DecryptedMessageAction
|
||||
public sealed partial class DecryptedMessageActionCommitKey : DecryptedMessageAction
|
||||
{
|
||||
/// <summary>Exchange ID, see <a href="https://corefork.telegram.org/api/end-to-end/pfs">rekeying process</a></summary>
|
||||
public long exchange_id;
|
||||
|
|
@ -478,11 +485,11 @@ namespace TL
|
|||
}
|
||||
/// <summary>NOOP action <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionNoop"/></para></summary>
|
||||
[TLDef(0xA82FDD63)]
|
||||
public class DecryptedMessageActionNoop : DecryptedMessageAction { }
|
||||
public sealed partial class DecryptedMessageActionNoop : DecryptedMessageAction { }
|
||||
|
||||
/// <summary>Sets the layer number for the contents of an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageLayer"/></para></summary>
|
||||
[TLDef(0x1BE31789)]
|
||||
public class DecryptedMessageLayer : IObject
|
||||
public sealed partial class DecryptedMessageLayer : IObject
|
||||
{
|
||||
/// <summary>Set of random bytes to prevent content recognition in short encrypted messages.<br/>Clients are required to check that there are at least 15 random bytes included in each message. Messages with less than 15 random bytes must be ignored.<br/>Parameter moved here from <see cref="DecryptedMessage"/> in Layer 17.</summary>
|
||||
public byte[] random_bytes;
|
||||
|
|
@ -498,40 +505,40 @@ namespace TL
|
|||
|
||||
/// <summary>File is currently unavailable. <para>See <a href="https://corefork.telegram.org/constructor/fileLocationUnavailable"/></para></summary>
|
||||
[TLDef(0x7C596B46)]
|
||||
public class FileLocationUnavailable : FileLocationBase
|
||||
public sealed partial class FileLocationUnavailable : FileLocationBase
|
||||
{
|
||||
/// <summary>Server volume</summary>
|
||||
/// <summary>Volume ID</summary>
|
||||
public long volume_id;
|
||||
/// <summary>File ID</summary>
|
||||
/// <summary>Local ID</summary>
|
||||
public int local_id;
|
||||
/// <summary>Checksum to access the file</summary>
|
||||
/// <summary>Secret</summary>
|
||||
public long secret;
|
||||
|
||||
/// <summary>Server volume</summary>
|
||||
/// <summary>Volume ID</summary>
|
||||
public override long VolumeId => volume_id;
|
||||
/// <summary>File ID</summary>
|
||||
/// <summary>Local ID</summary>
|
||||
public override int LocalId => local_id;
|
||||
/// <summary>Checksum to access the file</summary>
|
||||
/// <summary>Secret</summary>
|
||||
public override long Secret => secret;
|
||||
}
|
||||
/// <summary>File location. <para>See <a href="https://corefork.telegram.org/constructor/fileLocation"/></para></summary>
|
||||
[TLDef(0x53D69076)]
|
||||
public class FileLocation : FileLocationBase
|
||||
public sealed partial class FileLocation : FileLocationBase
|
||||
{
|
||||
/// <summary>Number of the data center holding the file</summary>
|
||||
/// <summary>DC ID</summary>
|
||||
public int dc_id;
|
||||
/// <summary>Server volume</summary>
|
||||
/// <summary>Volume ID</summary>
|
||||
public long volume_id;
|
||||
/// <summary>File ID</summary>
|
||||
/// <summary>Local ID</summary>
|
||||
public int local_id;
|
||||
/// <summary>Checksum to access the file</summary>
|
||||
/// <summary>Secret</summary>
|
||||
public long secret;
|
||||
|
||||
/// <summary>Server volume</summary>
|
||||
/// <summary>Volume ID</summary>
|
||||
public override long VolumeId => volume_id;
|
||||
/// <summary>File ID</summary>
|
||||
/// <summary>Local ID</summary>
|
||||
public override int LocalId => local_id;
|
||||
/// <summary>Checksum to access the file</summary>
|
||||
/// <summary>Secret</summary>
|
||||
public override long Secret => secret;
|
||||
}
|
||||
}
|
||||
|
|
@ -540,7 +547,7 @@ namespace TL
|
|||
{
|
||||
/// <summary>Represents an audio file <para>See <a href="https://corefork.telegram.org/constructor/documentAttributeAudio"/></para></summary>
|
||||
[TLDef(0xDED218E0)]
|
||||
public class DocumentAttributeAudio : DocumentAttribute
|
||||
public sealed partial class DocumentAttributeAudio : DocumentAttribute
|
||||
{
|
||||
/// <summary>Duration in seconds</summary>
|
||||
public int duration;
|
||||
|
|
@ -555,7 +562,7 @@ namespace TL
|
|||
{
|
||||
/// <summary>Defines a sticker <para>See <a href="https://corefork.telegram.org/constructor/documentAttributeSticker"/></para></summary>
|
||||
[TLDef(0x3A556302)]
|
||||
public class DocumentAttributeSticker : DocumentAttribute
|
||||
public sealed partial class DocumentAttributeSticker : DocumentAttribute
|
||||
{
|
||||
/// <summary>Alternative emoji representation of sticker</summary>
|
||||
public string alt;
|
||||
|
|
@ -565,7 +572,7 @@ namespace TL
|
|||
|
||||
/// <summary>Message entity representing a <a href="https://corefork.telegram.org/api/mentions">user mention</a>: for <em>creating</em> a mention use <see cref="InputMessageEntityMentionName"/>. <para>See <a href="https://corefork.telegram.org/constructor/messageEntityMentionName"/></para></summary>
|
||||
[TLDef(0x352DCA58, inheritBefore = true)]
|
||||
public class MessageEntityMentionName : MessageEntityMention
|
||||
public sealed partial class MessageEntityMentionName : MessageEntity
|
||||
{
|
||||
/// <summary>Identifier of the user that was mentioned</summary>
|
||||
public int user_id;
|
||||
|
|
@ -573,9 +580,9 @@ namespace TL
|
|||
|
||||
/// <summary>Contents of an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessage"/></para></summary>
|
||||
[TLDef(0x36B091DE)]
|
||||
public class DecryptedMessage : DecryptedMessageBase
|
||||
public sealed partial class DecryptedMessage : DecryptedMessageBase
|
||||
{
|
||||
/// <summary>Flags, see <a href="https://corefork.telegram.org/mtproto/TL-combinators#conditional-fields">TL conditional fields</a> (added in layer 45)</summary>
|
||||
/// <summary>Extra bits of information, use <c>flags.HasFlag(...)</c> to test for those</summary>
|
||||
public Flags flags;
|
||||
/// <summary>Random message ID, assigned by the author of message.<br/>Must be equal to the ID passed to sending method.</summary>
|
||||
public long random_id;
|
||||
|
|
@ -624,7 +631,7 @@ namespace TL
|
|||
|
||||
/// <summary>Photo attached to an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaPhoto"/></para></summary>
|
||||
[TLDef(0xF1FA8D78)]
|
||||
public class DecryptedMessageMediaPhoto : DecryptedMessageMedia
|
||||
public sealed partial class DecryptedMessageMediaPhoto : DecryptedMessageMedia
|
||||
{
|
||||
/// <summary>Content of thumbnail file (JPEGfile, quality 55, set in a square 90x90)</summary>
|
||||
public byte[] thumb;
|
||||
|
|
@ -650,7 +657,7 @@ namespace TL
|
|||
}
|
||||
/// <summary>Video attached to an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaVideo"/></para></summary>
|
||||
[TLDef(0x970C8C0E)]
|
||||
public class DecryptedMessageMediaVideo : DecryptedMessageMedia
|
||||
public sealed partial class DecryptedMessageMediaVideo : DecryptedMessageMedia
|
||||
{
|
||||
/// <summary>Content of thumbnail file (JPEG file, quality 55, set in a square 90x90)</summary>
|
||||
public byte[] thumb;
|
||||
|
|
@ -682,7 +689,7 @@ namespace TL
|
|||
}
|
||||
/// <summary>Document attached to a message in a secret chat. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaDocument"/></para></summary>
|
||||
[TLDef(0x7AFE8AE2)]
|
||||
public class DecryptedMessageMediaDocument : DecryptedMessageMedia
|
||||
public sealed partial class DecryptedMessageMediaDocument : DecryptedMessageMedia
|
||||
{
|
||||
/// <summary>Thumbnail-file contents (JPEG-file, quality 55, set in a 90x90 square)</summary>
|
||||
public byte[] thumb;
|
||||
|
|
@ -710,7 +717,7 @@ namespace TL
|
|||
}
|
||||
/// <summary>Venue <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaVenue"/></para></summary>
|
||||
[TLDef(0x8A0DF56F)]
|
||||
public class DecryptedMessageMediaVenue : DecryptedMessageMedia
|
||||
public sealed partial class DecryptedMessageMediaVenue : DecryptedMessageMedia
|
||||
{
|
||||
/// <summary>Latitude of venue</summary>
|
||||
public double lat;
|
||||
|
|
@ -720,14 +727,14 @@ namespace TL
|
|||
public string title;
|
||||
/// <summary>Address</summary>
|
||||
public string address;
|
||||
/// <summary>Venue provider: currently only "foursquare" needs to be supported</summary>
|
||||
/// <summary>Venue provider: currently only "foursquare" and "gplaces" (Google Places) need to be supported</summary>
|
||||
public string provider;
|
||||
/// <summary>Venue ID in the provider's database</summary>
|
||||
public string venue_id;
|
||||
}
|
||||
/// <summary>Webpage preview <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaWebPage"/></para></summary>
|
||||
[TLDef(0xE50511D8)]
|
||||
public class DecryptedMessageMediaWebPage : DecryptedMessageMedia
|
||||
public sealed partial class DecryptedMessageMediaWebPage : DecryptedMessageMedia
|
||||
{
|
||||
/// <summary>URL of webpage</summary>
|
||||
public string url;
|
||||
|
|
@ -738,16 +745,36 @@ namespace TL
|
|||
{
|
||||
/// <summary>User is uploading a round video <para>See <a href="https://corefork.telegram.org/constructor/sendMessageUploadRoundAction"/></para></summary>
|
||||
[TLDef(0xBB718624)]
|
||||
public class SendMessageUploadRoundAction : SendMessageAction { }
|
||||
public sealed partial class SendMessageUploadRoundAction : SendMessageAction { }
|
||||
|
||||
/// <summary>Defines a video <para>See <a href="https://corefork.telegram.org/constructor/documentAttributeVideo"/></para></summary>
|
||||
[TLDef(0x0EF02CE6)]
|
||||
public sealed partial class DocumentAttributeVideo : DocumentAttribute
|
||||
{
|
||||
/// <summary>Extra bits of information, use <c>flags.HasFlag(...)</c> to test for those</summary>
|
||||
public Flags flags;
|
||||
/// <summary>Duration in seconds</summary>
|
||||
public int duration;
|
||||
/// <summary>Video width</summary>
|
||||
public int w;
|
||||
/// <summary>Video height</summary>
|
||||
public int h;
|
||||
|
||||
[Flags] public enum Flags : uint
|
||||
{
|
||||
/// <summary>Whether this is a round video</summary>
|
||||
round_message = 0x1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace Layer73
|
||||
{
|
||||
/// <summary>Contents of an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessage"/></para></summary>
|
||||
[TLDef(0x91CC4674)]
|
||||
public class DecryptedMessage : DecryptedMessageBase
|
||||
public sealed partial class DecryptedMessage : DecryptedMessageBase
|
||||
{
|
||||
/// <summary>Flags, see <a href="https://corefork.telegram.org/mtproto/TL-combinators#conditional-fields">TL conditional fields</a> (added in layer 45)</summary>
|
||||
/// <summary>Extra bits of information, use <c>flags.HasFlag(...)</c> to test for those</summary>
|
||||
public Flags flags;
|
||||
/// <summary>Random message ID, assigned by the author of message.<br/>Must be equal to the ID passed to sending method.</summary>
|
||||
public long random_id;
|
||||
|
|
@ -770,7 +797,6 @@ namespace TL
|
|||
{
|
||||
/// <summary>Field <see cref="reply_to_random_id"/> has a value</summary>
|
||||
has_reply_to_random_id = 0x8,
|
||||
/// <summary>Whether this is a silent message (no notification triggered)</summary>
|
||||
silent = 0x20,
|
||||
/// <summary>Field <see cref="entities"/> has a value</summary>
|
||||
has_entities = 0x80,
|
||||
|
|
@ -804,13 +830,17 @@ namespace TL
|
|||
}
|
||||
|
||||
namespace Layer101
|
||||
{ }
|
||||
{
|
||||
/// <summary>Message entity representing a block quote. <para>See <a href="https://corefork.telegram.org/constructor/messageEntityBlockquote"/></para></summary>
|
||||
[TLDef(0x020DF5D0)]
|
||||
public sealed partial class MessageEntityBlockquote : MessageEntity { }
|
||||
}
|
||||
|
||||
namespace Layer143
|
||||
{
|
||||
/// <summary>Document attached to a message in a secret chat. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaDocument"/></para></summary>
|
||||
[TLDef(0x6ABD9782)]
|
||||
public class DecryptedMessageMediaDocument : DecryptedMessageMedia
|
||||
public sealed partial class DecryptedMessageMediaDocument : DecryptedMessageMedia
|
||||
{
|
||||
/// <summary>Thumbnail-file contents (JPEG-file, quality 55, set in a 90x90 square)</summary>
|
||||
public byte[] thumb;
|
||||
|
|
|
|||
640
src/TL.Table.cs
640
src/TL.Table.cs
File diff suppressed because it is too large
Load diff
|
|
@ -12,6 +12,7 @@ namespace TL
|
|||
{
|
||||
long ID { get; }
|
||||
bool IsActive { get; }
|
||||
string MainUsername { get; }
|
||||
InputPeer ToInputPeer();
|
||||
}
|
||||
|
||||
|
|
@ -26,7 +27,7 @@ namespace TL
|
|||
}
|
||||
partial class InputPeerChat
|
||||
{
|
||||
/// <summary>⚠ Only for small private Chat. Chat groups of type Channel must use InputPeerChannel. See <see href="https://github.com/wiz0u/WTelegramClient/blob/master/README.md#terminology">Terminology</see> in README</summary>
|
||||
/// <summary>⚠ <b>This type is only for basic Chat</b>. See <see href="https://wiz0u.github.io/WTelegramClient/#terminology">Terminology</see> in the README to understand what this means<br/>Chat groups of type Channel must use <see cref="InputPeerChannel"/>.</summary>
|
||||
/// <param name="chat_id">Chat identifier</param>
|
||||
public InputPeerChat(long chat_id) => this.chat_id = chat_id;
|
||||
internal InputPeerChat() { }
|
||||
|
|
@ -85,16 +86,36 @@ namespace TL
|
|||
else
|
||||
return new InputChatUploadedPhoto { file = this, flags = InputChatUploadedPhoto.Flags.has_file };
|
||||
}
|
||||
/// <summary>Random file identifier created by the client</summary>
|
||||
public abstract long ID { get; set; }
|
||||
/// <summary>Number of parts saved</summary>
|
||||
public abstract int Parts { get; set; }
|
||||
/// <summary>Full name of the file</summary>
|
||||
public abstract string Name { get; set; }
|
||||
}
|
||||
partial class InputFile
|
||||
{
|
||||
public override InputEncryptedFileBase ToInputEncryptedFile(int key_fingerprint) => new InputEncryptedFileUploaded { id = id, parts = parts, md5_checksum = md5_checksum, key_fingerprint = key_fingerprint };
|
||||
public override InputSecureFileBase ToInputSecureFile(byte[] file_hash, byte[] secret) => new InputSecureFileUploaded { id = id, parts = parts, md5_checksum = md5_checksum, file_hash = file_hash, secret = secret };
|
||||
public override long ID { get => id; set => id = value; }
|
||||
public override int Parts { get => parts; set => parts = value; }
|
||||
public override string Name { get => name; set => name = value; }
|
||||
}
|
||||
partial class InputFileBig
|
||||
{
|
||||
public override InputEncryptedFileBase ToInputEncryptedFile(int key_fingerprint) => new InputEncryptedFileBigUploaded { id = id, parts = parts, key_fingerprint = key_fingerprint };
|
||||
public override InputSecureFileBase ToInputSecureFile(byte[] file_hash, byte[] secret) => new InputSecureFileUploaded { id = id, parts = parts, file_hash = file_hash, secret = secret };
|
||||
public override long ID { get => id; set => id = value; }
|
||||
public override int Parts { get => parts; set => parts = value; }
|
||||
public override string Name { get => name; set => name = value; }
|
||||
}
|
||||
partial class InputFileStoryDocument // apparently this is used only in InputMediaUploadedDocument.file
|
||||
{
|
||||
public override InputEncryptedFileBase ToInputEncryptedFile(int key_fingerprint) => throw new NotSupportedException();
|
||||
public override InputSecureFileBase ToInputSecureFile(byte[] file_hash, byte[] secret) => throw new NotSupportedException();
|
||||
public override long ID { get => 0; set => throw new NotSupportedException(); }
|
||||
public override int Parts { get => 0; set => throw new NotSupportedException(); }
|
||||
public override string Name { get => null; set => throw new NotSupportedException(); }
|
||||
}
|
||||
|
||||
partial class InputMediaUploadedDocument
|
||||
|
|
@ -104,7 +125,16 @@ namespace TL
|
|||
{
|
||||
file = inputFile;
|
||||
mime_type = mimeType;
|
||||
if (inputFile.Name is string filename) attributes = new[] { new DocumentAttributeFilename { file_name = filename } };
|
||||
if (inputFile.Name is string filename) attributes = [new DocumentAttributeFilename { file_name = filename }];
|
||||
}
|
||||
public InputMediaUploadedDocument(InputFileBase inputFile, string mimeType, params DocumentAttribute[] attribs)
|
||||
{
|
||||
file = inputFile;
|
||||
mime_type = mimeType;
|
||||
if (inputFile.Name is string filename && !attribs.Any(a => a is DocumentAttributeFilename))
|
||||
attributes = [.. attribs, new DocumentAttributeFilename { file_name = filename }];
|
||||
else
|
||||
attributes = attribs;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -142,6 +172,7 @@ namespace TL
|
|||
{
|
||||
public abstract long ID { get; }
|
||||
public abstract bool IsActive { get; }
|
||||
public abstract string MainUsername { get; }
|
||||
public abstract InputPeer ToInputPeer();
|
||||
protected abstract InputUser ToInputUser();
|
||||
public static implicit operator InputPeer(UserBase user) => user?.ToInputPeer();
|
||||
|
|
@ -151,6 +182,7 @@ namespace TL
|
|||
{
|
||||
public override long ID => id;
|
||||
public override bool IsActive => false;
|
||||
public override string MainUsername => null;
|
||||
public override string ToString() => null;
|
||||
public override InputPeer ToInputPeer() => null;
|
||||
protected override InputUser ToInputUser() => null;
|
||||
|
|
@ -159,19 +191,31 @@ namespace TL
|
|||
{
|
||||
public override long ID => id;
|
||||
public override bool IsActive => (flags & Flags.deleted) == 0;
|
||||
public bool IsBot => (flags & Flags.bot) != 0;
|
||||
public string MainUsername => username ?? usernames?.FirstOrDefault(u => u.flags.HasFlag(Username.Flags.active))?.username;
|
||||
public override string ToString() => username != null ? '@' + username : last_name == null ? first_name : $"{first_name} {last_name}";
|
||||
public override string MainUsername => username ?? usernames?.FirstOrDefault(u => u.flags.HasFlag(Username.Flags.active))?.username;
|
||||
public override string ToString() => MainUsername is string uname ? '@' + uname : last_name == null ? first_name : $"{first_name} {last_name}";
|
||||
public override InputPeer ToInputPeer() => new InputPeerUser(id, access_hash);
|
||||
protected override InputUser ToInputUser() => new(id, access_hash);
|
||||
/// <summary>An estimation of the number of days ago the user was last seen (Online=0, Recently=1, LastWeek=5, LastMonth=20, LongTimeAgo=150)</summary>
|
||||
public TimeSpan LastSeenAgo => status?.LastSeenAgo ?? TimeSpan.FromDays(150);
|
||||
public bool IsBot => (flags & Flags.bot) != 0;
|
||||
public IEnumerable<string> ActiveUsernames
|
||||
{
|
||||
get
|
||||
{
|
||||
if (username != null)
|
||||
yield return username;
|
||||
if (usernames != null)
|
||||
foreach (var un in usernames)
|
||||
if (un.flags.HasFlag(Username.Flags.active))
|
||||
yield return un.username;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <remarks>a <c>null</c> value means <a href="https://corefork.telegram.org/constructor/userStatusEmpty">userStatusEmpty</a> = last seen a long time ago, more than a month (or blocked/deleted users)</remarks>
|
||||
partial class UserStatus { internal abstract TimeSpan LastSeenAgo { get; } }
|
||||
partial class UserStatusOnline { internal override TimeSpan LastSeenAgo => TimeSpan.Zero; }
|
||||
partial class UserStatusOffline { internal override TimeSpan LastSeenAgo => DateTime.UtcNow - new DateTime((was_online + 62135596800L) * 10000000, DateTimeKind.Utc); }
|
||||
partial class UserStatusOffline { internal override TimeSpan LastSeenAgo => DateTime.UtcNow - was_online; }
|
||||
/// <remarks>covers anything between 1 second and 2-3 days</remarks>
|
||||
partial class UserStatusRecently { internal override TimeSpan LastSeenAgo => TimeSpan.FromDays(1); }
|
||||
/// <remarks>between 2-3 and seven days</remarks>
|
||||
|
|
@ -183,11 +227,15 @@ namespace TL
|
|||
{
|
||||
/// <summary>Is this chat among current user active chats?</summary>
|
||||
public abstract bool IsActive { get; }
|
||||
/// <summary>Is this chat a broadcast channel?</summary>
|
||||
public virtual bool IsChannel => false;
|
||||
public bool IsGroup => !IsChannel;
|
||||
public virtual string MainUsername => null;
|
||||
public abstract ChatPhoto Photo { get; }
|
||||
/// <summary>returns true if you're banned of any of these rights</summary>
|
||||
public abstract bool IsBanned(ChatBannedRights.Flags flags = 0);
|
||||
public abstract InputPeer ToInputPeer();
|
||||
public static implicit operator InputPeer(ChatBase chat) => chat.ToInputPeer();
|
||||
public static implicit operator InputPeer(ChatBase chat) => chat?.ToInputPeer();
|
||||
}
|
||||
partial class ChatEmpty
|
||||
{
|
||||
|
|
@ -216,19 +264,30 @@ namespace TL
|
|||
partial class Channel
|
||||
{
|
||||
public override bool IsActive => (flags & Flags.left) == 0;
|
||||
public string MainUsername => username ?? usernames?.FirstOrDefault(u => u.flags.HasFlag(Username.Flags.active))?.username;
|
||||
public override bool IsChannel => (flags & Flags.broadcast) != 0;
|
||||
public override string MainUsername => username ?? usernames?.FirstOrDefault(un => un.flags.HasFlag(Username.Flags.active))?.username;
|
||||
public override ChatPhoto Photo => photo;
|
||||
public override bool IsBanned(ChatBannedRights.Flags flags = 0) => ((banned_rights?.flags ?? 0) & flags) != 0 || ((default_banned_rights?.flags ?? 0) & flags) != 0;
|
||||
public override InputPeer ToInputPeer() => new InputPeerChannel(id, access_hash);
|
||||
public static implicit operator InputChannel(Channel channel) => new(channel.id, channel.access_hash);
|
||||
public override string ToString() =>
|
||||
(flags.HasFlag(Flags.broadcast) ? "Channel " : "Group ") + (username != null ? '@' + username : $"\"{title}\"");
|
||||
public bool IsChannel => (flags & Flags.broadcast) != 0;
|
||||
public bool IsGroup => (flags & Flags.broadcast) == 0;
|
||||
public override string ToString() => (flags.HasFlag(Flags.broadcast) ? "Channel " : "Group ") + (MainUsername is string uname ? '@' + uname : $"\"{title}\"");
|
||||
public IEnumerable<string> ActiveUsernames
|
||||
{
|
||||
get
|
||||
{
|
||||
if (username != null)
|
||||
yield return username;
|
||||
if (usernames != null)
|
||||
foreach (var un in usernames)
|
||||
if (un.flags.HasFlag(Username.Flags.active))
|
||||
yield return un.username;
|
||||
}
|
||||
}
|
||||
}
|
||||
partial class ChannelForbidden
|
||||
{
|
||||
public override bool IsActive => false;
|
||||
public override bool IsChannel => (flags & Flags.broadcast) != 0;
|
||||
public override ChatPhoto Photo => null;
|
||||
public override bool IsBanned(ChatBannedRights.Flags flags = 0) => true;
|
||||
public override InputPeer ToInputPeer() => new InputPeerChannel(id, access_hash);
|
||||
|
|
@ -245,9 +304,10 @@ namespace TL
|
|||
partial class ChatParticipantAdmin { public override bool IsAdmin => true; }
|
||||
|
||||
partial class ChatParticipantsBase { public abstract ChatParticipantBase[] Participants { get; }}
|
||||
partial class ChatParticipantsForbidden { public override ChatParticipantBase[] Participants => Array.Empty<ChatParticipantBase>(); }
|
||||
partial class ChatParticipantsForbidden { public override ChatParticipantBase[] Participants => []; }
|
||||
partial class ChatParticipants { public override ChatParticipantBase[] Participants => participants; }
|
||||
|
||||
partial class MessageBase { public MessageReplyHeader ReplyHeader => ReplyTo as MessageReplyHeader; }
|
||||
partial class MessageEmpty { public override string ToString() => "(no message)"; }
|
||||
partial class Message { public override string ToString() => $"{(from_id ?? peer_id)?.ID}> {message} {media}"; }
|
||||
partial class MessageService { public override string ToString() => $"{(from_id ?? peer_id)?.ID} [{action.GetType().Name[13..]}]"; }
|
||||
|
|
@ -267,6 +327,7 @@ namespace TL
|
|||
correct_answers = results.results?.Where(pav => pav.flags.HasFlag(PollAnswerVoters.Flags.correct)).Select(pav => pav.option).ToArray(),
|
||||
flags = (results.results != null ? InputMediaPoll.Flags.has_correct_answers : 0) | (results.solution != null ? InputMediaPoll.Flags.has_solution : 0) }; }
|
||||
partial class MessageMediaDice { public override InputMedia ToInputMedia() => new InputMediaDice { emoticon = emoticon }; }
|
||||
partial class MessageMediaWebPage { public override InputMedia ToInputMedia() => new InputMediaWebPage { flags = (InputMediaWebPage.Flags)((int)flags & 3), url = webpage.Url }; }
|
||||
|
||||
partial class PhotoBase
|
||||
{
|
||||
|
|
@ -286,7 +347,9 @@ namespace TL
|
|||
protected override InputPhoto ToInputPhoto() => new() { id = id, access_hash = access_hash, file_reference = file_reference };
|
||||
public InputPhotoFileLocation ToFileLocation() => ToFileLocation(LargestPhotoSize);
|
||||
public InputPhotoFileLocation ToFileLocation(PhotoSizeBase photoSize) => new() { id = id, access_hash = access_hash, file_reference = file_reference, thumb_size = photoSize.Type };
|
||||
public InputPhotoFileLocation ToFileLocation(VideoSize videoSize) => new() { id = id, access_hash = access_hash, file_reference = file_reference, thumb_size = videoSize.type };
|
||||
public PhotoSizeBase LargestPhotoSize => sizes.Aggregate((agg, next) => (long)next.Width * next.Height > (long)agg.Width * agg.Height ? next : agg);
|
||||
public VideoSize LargestVideoSize => video_sizes?.OfType<VideoSize>().DefaultIfEmpty().Aggregate((agg, next) => (long)next.w * next.h > (long)agg.w * agg.h ? next : agg);
|
||||
}
|
||||
|
||||
partial class PhotoSizeBase
|
||||
|
|
@ -352,6 +415,13 @@ namespace TL
|
|||
public static implicit operator InputGeoPoint(GeoPoint geo) => new() { lat = geo.lat, lon = geo.lon, accuracy_radius = geo.accuracy_radius, flags = (InputGeoPoint.Flags)geo.flags };
|
||||
}
|
||||
|
||||
partial class InputNotifyPeerBase
|
||||
{
|
||||
public static implicit operator InputNotifyPeerBase(InputPeer peer) => new InputNotifyPeer { peer = peer };
|
||||
public static implicit operator InputNotifyPeerBase(ChatBase chat) => new InputNotifyPeer { peer = chat };
|
||||
public static implicit operator InputNotifyPeerBase(UserBase user) => new InputNotifyPeer { peer = user };
|
||||
}
|
||||
|
||||
partial class WallPaperBase { public static implicit operator InputWallPaperBase(WallPaperBase wp) => wp.ToInputWallPaper();
|
||||
protected abstract InputWallPaperBase ToInputWallPaper(); }
|
||||
partial class WallPaper { protected override InputWallPaperBase ToInputWallPaper() => new InputWallPaper { id = id, access_hash = access_hash }; }
|
||||
|
|
@ -381,25 +451,28 @@ namespace TL
|
|||
public abstract Update[] UpdateList { get; }
|
||||
public virtual Dictionary<long, User> Users => NoUsers;
|
||||
public virtual Dictionary<long, ChatBase> Chats => NoChats;
|
||||
private static readonly Dictionary<long, User> NoUsers = new();
|
||||
private static readonly Dictionary<long, ChatBase> NoChats = new();
|
||||
private static readonly Dictionary<long, User> NoUsers = [];
|
||||
private static readonly Dictionary<long, ChatBase> NoChats = [];
|
||||
public virtual (long mbox_id, int pts, int pts_count) GetMBox() => default;
|
||||
}
|
||||
partial class UpdatesCombined
|
||||
{
|
||||
public override Update[] UpdateList => updates;
|
||||
public override Dictionary<long, User> Users => users;
|
||||
public override Dictionary<long, ChatBase> Chats => chats;
|
||||
public override (long mbox_id, int pts, int pts_count) GetMBox() => (-2, seq, seq - seq_start + 1);
|
||||
}
|
||||
partial class Updates
|
||||
{
|
||||
public override Update[] UpdateList => updates;
|
||||
public override Dictionary<long, User> Users => users;
|
||||
public override Dictionary<long, ChatBase> Chats => chats;
|
||||
public override (long mbox_id, int pts, int pts_count) GetMBox() => (-2, seq, 1);
|
||||
}
|
||||
partial class UpdatesTooLong { public override Update[] UpdateList => Array.Empty<Update>(); }
|
||||
partial class UpdateShort { public override Update[] UpdateList => new[] { update }; }
|
||||
partial class UpdateShortSentMessage { public override Update[] UpdateList => Array.Empty<Update>(); }
|
||||
partial class UpdateShortMessage { public override Update[] UpdateList => new[] { new UpdateNewMessage
|
||||
partial class UpdatesTooLong { public override Update[] UpdateList => []; }
|
||||
partial class UpdateShort { public override Update[] UpdateList => [update]; }
|
||||
partial class UpdateShortSentMessage { public override Update[] UpdateList => []; }
|
||||
partial class UpdateShortMessage { public override Update[] UpdateList => [ new UpdateNewMessage
|
||||
{
|
||||
message = new Message
|
||||
{
|
||||
|
|
@ -409,8 +482,8 @@ namespace TL
|
|||
peer_id = new PeerUser { user_id = user_id },
|
||||
fwd_from = fwd_from, via_bot_id = via_bot_id, ttl_period = ttl_period
|
||||
}, pts = pts, pts_count = pts_count
|
||||
} }; }
|
||||
partial class UpdateShortChatMessage { public override Update[] UpdateList => new[] { new UpdateNewMessage
|
||||
} ]; }
|
||||
partial class UpdateShortChatMessage { public override Update[] UpdateList => [ new UpdateNewMessage
|
||||
{
|
||||
message = new Message
|
||||
{
|
||||
|
|
@ -420,7 +493,7 @@ namespace TL
|
|||
peer_id = new PeerChat { chat_id = chat_id },
|
||||
fwd_from = fwd_from, via_bot_id = via_bot_id, ttl_period = ttl_period
|
||||
}, pts = pts, pts_count = pts_count
|
||||
} }; }
|
||||
} ]; }
|
||||
|
||||
partial class InputEncryptedChat { public static implicit operator int(InputEncryptedChat chat) => chat.chat_id;
|
||||
public static implicit operator InputEncryptedChat(EncryptedChatBase chat) => new() { chat_id = chat.ID, access_hash = chat.AccessHash }; }
|
||||
|
|
@ -452,11 +525,13 @@ namespace TL
|
|||
partial class Document
|
||||
{
|
||||
public override long ID => id;
|
||||
public override string ToString() => Filename is string filename ? base.ToString() + ": " + filename : base.ToString();
|
||||
public string Filename => attributes.OfType<DocumentAttributeFilename>().FirstOrDefault()?.file_name;
|
||||
public override string ToString() => $"{Filename ?? $"Document {mime_type}"} {size:N0} bytes";
|
||||
public string Filename => GetAttribute<DocumentAttributeFilename>()?.file_name;
|
||||
protected override InputDocument ToInputDocument() => new() { id = id, access_hash = access_hash, file_reference = file_reference };
|
||||
public InputDocumentFileLocation ToFileLocation(PhotoSizeBase thumbSize = null) => new() { id = id, access_hash = access_hash, file_reference = file_reference, thumb_size = thumbSize?.Type };
|
||||
public InputDocumentFileLocation ToFileLocation(VideoSize videoSize) => new() { id = id, access_hash = access_hash, file_reference = file_reference, thumb_size = videoSize.type };
|
||||
public PhotoSizeBase LargestThumbSize => thumbs?.Aggregate((agg, next) => (long)next.Width * next.Height > (long)agg.Width * agg.Height ? next : agg);
|
||||
public T GetAttribute<T>() where T : DocumentAttribute => attributes.OfType<T>().FirstOrDefault();
|
||||
}
|
||||
|
||||
partial class SendMessageAction
|
||||
|
|
@ -487,6 +562,19 @@ namespace TL
|
|||
partial class StickerSet
|
||||
{
|
||||
public static implicit operator InputStickerSetID(StickerSet stickerSet) => new() { id = stickerSet.id, access_hash = stickerSet.access_hash };
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060")]
|
||||
public InputStickerSetThumb ToFileLocation(PhotoSizeBase thumbSize) => new() { stickerset = this, thumb_version = thumb_version };
|
||||
public PhotoSizeBase LargestThumbSize => thumbs?.Aggregate((agg, next) => (long)next.Width * next.Height > (long)agg.Width * agg.Height ? next : agg);
|
||||
}
|
||||
|
||||
partial class MessageEntity
|
||||
{
|
||||
public string Type {
|
||||
get { var name = GetType().Name; return name[(name.IndexOf("MessageEntity") + 13)..]; }
|
||||
set { if (value != Type) throw new NotSupportedException("Can't change Type. You need to create a new instance of the right TL.MessageEntity* subclass"); }
|
||||
}
|
||||
public int Offset { get => offset; set => offset = value; }
|
||||
public int Length { get => length; set => length = value; }
|
||||
}
|
||||
|
||||
partial class InputChannel
|
||||
|
|
@ -503,8 +591,10 @@ namespace TL
|
|||
public static implicit operator InputPeer(Contacts_ResolvedPeer resolved) => resolved?.UserOrChat.ToInputPeer();
|
||||
/// <returns>A <see cref="TL.User"/>, or <see langword="null"/> if the username was for a channel</returns>
|
||||
public User User => peer is PeerUser pu ? users[pu.user_id] : null;
|
||||
/// <returns>A <see cref="Channel"/> or <see cref="ChannelForbidden"/>, or <see langword="null"/> if the username was for a user</returns>
|
||||
/// <returns>A <see cref="TL.Channel"/> or <see cref="TL.ChannelForbidden"/>, or <see langword="null"/> if the username was for a user</returns>
|
||||
public ChatBase Chat => peer is PeerChannel or PeerChat ? chats[peer.ID] : null;
|
||||
/// <returns>A <see cref="TL.Channel"/>, or <see langword="null"/> if the username was for a user or for a forbidden channel</returns>
|
||||
public Channel Channel => peer is PeerChannel pc ? chats[pc.channel_id] as Channel : null;
|
||||
}
|
||||
|
||||
partial class Updates_ChannelDifferenceBase
|
||||
|
|
@ -513,13 +603,15 @@ namespace TL
|
|||
public abstract Update[] OtherUpdates { get; }
|
||||
public abstract bool Final { get; }
|
||||
public abstract int Timeout { get; }
|
||||
public abstract int Pts { get; }
|
||||
}
|
||||
partial class Updates_ChannelDifferenceEmpty
|
||||
{
|
||||
public override MessageBase[] NewMessages => Array.Empty<MessageBase>();
|
||||
public override Update[] OtherUpdates => Array.Empty<Update>();
|
||||
public override MessageBase[] NewMessages => [];
|
||||
public override Update[] OtherUpdates => [];
|
||||
public override bool Final => flags.HasFlag(Flags.final);
|
||||
public override int Timeout => timeout;
|
||||
public override int Pts => pts;
|
||||
}
|
||||
partial class Updates_ChannelDifference
|
||||
{
|
||||
|
|
@ -527,6 +619,7 @@ namespace TL
|
|||
public override Update[] OtherUpdates => other_updates;
|
||||
public override bool Final => flags.HasFlag(Flags.final);
|
||||
public override int Timeout => timeout;
|
||||
public override int Pts => pts;
|
||||
}
|
||||
partial class Updates_ChannelDifferenceTooLong
|
||||
{
|
||||
|
|
@ -534,38 +627,41 @@ namespace TL
|
|||
public override Update[] OtherUpdates => null;
|
||||
public override bool Final => flags.HasFlag(Flags.final);
|
||||
public override int Timeout => timeout;
|
||||
public override int Pts => dialog is Dialog d ? d.pts : 0;
|
||||
}
|
||||
|
||||
partial class ChannelParticipantBase
|
||||
{
|
||||
public virtual bool IsAdmin => false;
|
||||
public abstract long UserID { get; }
|
||||
public abstract long UserId { get; }
|
||||
}
|
||||
partial class ChannelParticipantCreator
|
||||
{
|
||||
public override bool IsAdmin => true;
|
||||
public override long UserID => user_id;
|
||||
public override long UserId => user_id;
|
||||
}
|
||||
partial class ChannelParticipantAdmin
|
||||
{
|
||||
public override bool IsAdmin => true;
|
||||
public override long UserID => user_id;
|
||||
public override long UserId => user_id;
|
||||
}
|
||||
partial class ChannelParticipant { public override long UserID => user_id; }
|
||||
partial class ChannelParticipantSelf { public override long UserID => user_id; }
|
||||
partial class ChannelParticipantBanned { public override long UserID => peer is PeerUser pu ? pu.user_id : 0; }
|
||||
partial class ChannelParticipantLeft { public override long UserID => peer is PeerUser pu ? pu.user_id : 0; }
|
||||
partial class ChannelParticipant { public override long UserId => user_id; }
|
||||
partial class ChannelParticipantSelf { public override long UserId => user_id; }
|
||||
partial class ChannelParticipantBanned { public override long UserId => peer is PeerUser pu ? pu.user_id : 0; }
|
||||
partial class ChannelParticipantLeft { public override long UserId => peer is PeerUser pu ? pu.user_id : 0; }
|
||||
|
||||
partial class Messages_PeerDialogs { public IPeerInfo UserOrChat(DialogBase dialog) => dialog.Peer?.UserOrChat(users, chats); }
|
||||
|
||||
partial class Game { public static implicit operator InputGameID(Game game) => new() { id = game.id, access_hash = game.access_hash }; }
|
||||
|
||||
partial class WebDocumentBase { public T GetAttribute<T>() where T : DocumentAttribute => Attributes.OfType<T>().FirstOrDefault(); }
|
||||
partial class WebDocument { public static implicit operator InputWebFileLocation(WebDocument doc) => new() { url = doc.url, access_hash = doc.access_hash }; }
|
||||
|
||||
partial class PhoneCallBase { public static implicit operator InputPhoneCall(PhoneCallBase call) => new() { id = call.ID, access_hash = call.AccessHash }; }
|
||||
|
||||
partial class ChannelAdminLogEventsFilter
|
||||
{
|
||||
public static implicit operator ChannelAdminLogEventsFilter(Flags flags) => new() { flags = flags };
|
||||
public static implicit operator ChannelAdminLogEventsFilter(Flags flags) => flags == 0 ? null : new() { flags = flags };
|
||||
}
|
||||
|
||||
partial class InputMessage
|
||||
|
|
@ -576,6 +672,8 @@ namespace TL
|
|||
partial class InputDialogPeerBase
|
||||
{
|
||||
public static implicit operator InputDialogPeerBase(InputPeer peer) => new InputDialogPeer { peer = peer };
|
||||
public static implicit operator InputDialogPeerBase(ChatBase chat) => new InputDialogPeer { peer = chat };
|
||||
public static implicit operator InputDialogPeerBase(UserBase user) => new InputDialogPeer { peer = user };
|
||||
}
|
||||
|
||||
partial class SecureFile
|
||||
|
|
@ -585,7 +683,19 @@ namespace TL
|
|||
}
|
||||
|
||||
partial class JsonObjectValue { public override string ToString() => $"{HttpUtility.JavaScriptStringEncode(key, true)}:{value}"; }
|
||||
partial class JSONValue { public abstract object ToNative(); }
|
||||
partial class JSONValue { public abstract object ToNative();
|
||||
private static JsonObjectValue FromJsonProperty(System.Text.Json.JsonProperty p) => new() { key = p.Name, value = FromJsonElement(p.Value) };
|
||||
public static JSONValue FromJsonElement(System.Text.Json.JsonElement elem) => elem.ValueKind switch
|
||||
{
|
||||
System.Text.Json.JsonValueKind.True or
|
||||
System.Text.Json.JsonValueKind.False => new JsonBool { value = elem.GetBoolean() },
|
||||
System.Text.Json.JsonValueKind.Object => new JsonObject { value = [.. elem.EnumerateObject().Select(FromJsonProperty)] },
|
||||
System.Text.Json.JsonValueKind.Array => new JsonArray { value = [.. elem.EnumerateArray().Select(FromJsonElement)] },
|
||||
System.Text.Json.JsonValueKind.String => new JsonString { value = elem.GetString() },
|
||||
System.Text.Json.JsonValueKind.Number => new JsonNumber { value = elem.GetDouble() },
|
||||
_ => new JsonNull(),
|
||||
};
|
||||
}
|
||||
partial class JsonNull { public override object ToNative() => null; public override string ToString() => "null"; }
|
||||
partial class JsonBool { public override object ToNative() => value; public override string ToString() => value ? "true" : "false"; }
|
||||
partial class JsonNumber { public override object ToNative() => value; public override string ToString() => value.ToString(CultureInfo.InvariantCulture); }
|
||||
|
|
@ -599,7 +709,7 @@ namespace TL
|
|||
sb.Append(i == 0 ? "" : ",").Append(value[i]);
|
||||
return sb.Append(']').ToString();
|
||||
}
|
||||
public object[] ToNativeArray() => value.Select(v => v.ToNative()).ToArray();
|
||||
public object[] ToNativeArray() => [.. value.Select(v => v.ToNative())];
|
||||
public override object ToNative()
|
||||
{
|
||||
if (value.Length == 0) return Array.Empty<object>();
|
||||
|
|
@ -647,6 +757,20 @@ namespace TL
|
|||
}
|
||||
}
|
||||
|
||||
partial class Theme { public static implicit operator InputTheme(Theme theme) => new() { id = theme.id, access_hash = theme.access_hash }; }
|
||||
partial class GroupCallBase { public static implicit operator InputGroupCall(GroupCallBase call) => new() { id = call.ID, access_hash = call.AccessHash }; }
|
||||
partial class Theme { public static implicit operator InputTheme(Theme theme) => new() { id = theme.id, access_hash = theme.access_hash }; }
|
||||
partial class MessageReplyHeader { public int TopicID => flags.HasFlag(Flags.forum_topic) ? flags.HasFlag(Flags.has_reply_to_top_id) ? reply_to_top_id : reply_to_msg_id : 0; }
|
||||
partial class GroupCallBase { public static implicit operator InputGroupCall(GroupCallBase call) => new() { id = call.ID, access_hash = call.AccessHash }; }
|
||||
|
||||
partial class EmojiStatusBase { public virtual long DocumentId => 0; }
|
||||
partial class EmojiStatus { public override long DocumentId => document_id; }
|
||||
partial class EmojiStatusCollectible{ public override long DocumentId => document_id; }
|
||||
|
||||
partial class ForumTopicBase { public virtual string Title => null; }
|
||||
partial class ForumTopic { public override string Title => title; }
|
||||
|
||||
partial class RequestedPeer { public abstract long ID { get; } }
|
||||
partial class RequestedPeerUser { public override long ID => user_id; }
|
||||
partial class RequestedPeerChat { public override long ID => chat_id; }
|
||||
partial class RequestedPeerChannel { public override long ID => channel_id; }
|
||||
|
||||
}
|
||||
258
src/TL.cs
258
src/TL.cs
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
|
|
@ -7,52 +8,62 @@ using System.Reflection;
|
|||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
#pragma warning disable IDE1006 // Naming Styles
|
||||
|
||||
namespace TL
|
||||
{
|
||||
#if MTPG
|
||||
public interface IObject { void WriteTL(BinaryWriter writer); }
|
||||
#else
|
||||
public interface IObject { }
|
||||
public interface IMethod<ReturnType> : IObject { }
|
||||
#endif
|
||||
public interface IMethod<out ReturnType> : IObject { }
|
||||
public interface IPeerResolver { IPeerInfo UserOrChat(Peer peer); }
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class TLDefAttribute : Attribute
|
||||
public sealed class TLDefAttribute(uint ctorNb) : Attribute
|
||||
{
|
||||
public readonly uint CtorNb;
|
||||
public TLDefAttribute(uint ctorNb) => CtorNb = ctorNb;
|
||||
public readonly uint CtorNb = ctorNb;
|
||||
public bool inheritBefore;
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Field)]
|
||||
public class IfFlagAttribute : Attribute
|
||||
public sealed class IfFlagAttribute(int bit) : Attribute
|
||||
{
|
||||
public readonly int Bit;
|
||||
public IfFlagAttribute(int bit) => Bit = bit;
|
||||
public readonly int Bit = bit;
|
||||
}
|
||||
|
||||
public class RpcException : Exception
|
||||
public sealed class RpcException(int code, string message, int x = -1) : WTelegram.WTException(message)
|
||||
{
|
||||
public readonly int Code;
|
||||
public readonly int Code = code;
|
||||
/// <summary>The value of X in the message, -1 if no variable X was found</summary>
|
||||
public readonly int X;
|
||||
public RpcException(int code, string message, int x = -1) : base(message) { Code = code; X = x; }
|
||||
public readonly int X = x;
|
||||
public override string ToString() { var str = base.ToString(); return str.Insert(str.IndexOf(':') + 1, " " + Code); }
|
||||
}
|
||||
|
||||
public class ReactorError : IObject
|
||||
public sealed partial class ReactorError : IObject
|
||||
{
|
||||
public Exception Exception;
|
||||
public void WriteTL(BinaryWriter writer) => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
internal class BinaryReader : System.IO.BinaryReader
|
||||
public static class Serialization
|
||||
{
|
||||
public readonly WTelegram.Client Client;
|
||||
public BinaryReader(Stream stream, WTelegram.Client client, bool leaveOpen = false) : base(stream, Encoding.UTF8, leaveOpen) => Client = client;
|
||||
}
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static byte[] ToBytes<T>(this T obj) where T : IObject
|
||||
{
|
||||
using var ms = new MemoryStream(384);
|
||||
using var writer = new BinaryWriter(ms);
|
||||
writer.WriteTLObject(obj);
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
internal static class Serialization
|
||||
{
|
||||
internal static void WriteTLObject<T>(this BinaryWriter writer, T obj) where T : IObject
|
||||
public static void WriteTLObject<T>(this BinaryWriter writer, T obj) where T : IObject
|
||||
{
|
||||
if (obj == null) { writer.WriteTLNull(typeof(T)); return; }
|
||||
#if MTPG
|
||||
obj.WriteTL(writer);
|
||||
#else
|
||||
var type = obj.GetType();
|
||||
var tlDef = type.GetCustomAttribute<TLDefAttribute>();
|
||||
var ctorNb = tlDef.CtorNb;
|
||||
|
|
@ -70,16 +81,20 @@ namespace TL
|
|||
if (field.Name == "flags") flags = (uint)value;
|
||||
else if (field.Name == "flags2") flags |= (ulong)(uint)value << 32;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
internal static IObject ReadTLObject(this BinaryReader reader, uint ctorNb = 0)
|
||||
public static IObject ReadTLObject(this BinaryReader reader, uint ctorNb = 0)
|
||||
{
|
||||
if (ctorNb == 0) ctorNb = reader.ReadUInt32();
|
||||
if (ctorNb == Layer.GZipedCtor)
|
||||
using (var gzipReader = new BinaryReader(new GZipStream(new MemoryStream(reader.ReadTLBytes()), CompressionMode.Decompress), reader.Client))
|
||||
return ReadTLObject(gzipReader);
|
||||
#if MTPG
|
||||
if (!Layer.Table.TryGetValue(ctorNb, out var ctor))
|
||||
throw new WTelegram.WTException($"Cannot find type for ctor #{ctorNb:x}");
|
||||
return ctor?.Invoke(reader);
|
||||
#else
|
||||
if (ctorNb == Layer.GZipedCtor) return (IObject)reader.ReadTLGzipped(typeof(IObject));
|
||||
if (!Layer.Table.TryGetValue(ctorNb, out var type))
|
||||
throw new ApplicationException($"Cannot find type for ctor #{ctorNb:x}");
|
||||
throw new WTelegram.WTException($"Cannot find type for ctor #{ctorNb:x}");
|
||||
if (type == null) return null; // nullable ctor (class meaning is associated with null)
|
||||
var tlDef = type.GetCustomAttribute<TLDefAttribute>();
|
||||
var obj = Activator.CreateInstance(type, true);
|
||||
|
|
@ -95,11 +110,20 @@ namespace TL
|
|||
if (field.FieldType.IsEnum)
|
||||
if (field.Name == "flags") flags = (uint)value;
|
||||
else if (field.Name == "flags2") flags |= (ulong)(uint)value << 32;
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
if (reader.Client?.CollectAccessHash == true) reader.Client.CollectField(field, obj, value);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
return (IObject)obj;
|
||||
#endif
|
||||
}
|
||||
|
||||
public static IMethod<X> ReadTLMethod<X>(this BinaryReader reader)
|
||||
{
|
||||
uint ctorNb = reader.ReadUInt32();
|
||||
if (!Layer.Methods.TryGetValue(ctorNb, out var ctor))
|
||||
throw new WTelegram.WTException($"Cannot find method for ctor #{ctorNb:x}");
|
||||
var method = ctor?.Invoke(reader);
|
||||
if (method is IMethod<bool> && typeof(X) == typeof(object))
|
||||
method = new BoolMethod { query = method };
|
||||
return (IMethod<X>)method;
|
||||
}
|
||||
|
||||
internal static void WriteTLValue(this BinaryWriter writer, object value, Type valueType)
|
||||
|
|
@ -124,16 +148,16 @@ namespace TL
|
|||
if (type.IsArray)
|
||||
if (value is byte[] bytes)
|
||||
writer.WriteTLBytes(bytes);
|
||||
else if (value is _Message[] messages)
|
||||
writer.WriteTLMessages(messages);
|
||||
else
|
||||
writer.WriteTLVector((Array)value);
|
||||
else if (value is IObject tlObject)
|
||||
WriteTLObject(writer, tlObject);
|
||||
else if (value is List<_Message> messages)
|
||||
writer.WriteTLMessages(messages);
|
||||
else if (value is Int128 int128)
|
||||
writer.Write(int128);
|
||||
else if (value is Int256 int256)
|
||||
writer.Write(int256);
|
||||
else if (value is IObject tlObject)
|
||||
WriteTLObject(writer, tlObject);
|
||||
else if (type.IsEnum) // needed for Mono (enums in generic types are seen as TypeCode.Object)
|
||||
writer.Write((uint)value);
|
||||
else
|
||||
|
|
@ -162,7 +186,7 @@ namespace TL
|
|||
0x997275b5 => true,
|
||||
0xbc799737 => false,
|
||||
Layer.RpcErrorCtor => reader.ReadTLObject(Layer.RpcErrorCtor),
|
||||
var value => throw new ApplicationException($"Invalid boolean value #{value:x}")
|
||||
var value => throw new WTelegram.WTException($"Invalid boolean value #{value:x}")
|
||||
};
|
||||
case TypeCode.Object:
|
||||
if (type.IsArray)
|
||||
|
|
@ -177,9 +201,9 @@ namespace TL
|
|||
else if (type == typeof(Int256))
|
||||
return new Int256(reader);
|
||||
else if (type == typeof(Dictionary<long, User>))
|
||||
return reader.ReadTLDictionary<User>(u => u.ID);
|
||||
return reader.ReadTLDictionary<User>();
|
||||
else if (type == typeof(Dictionary<long, ChatBase>))
|
||||
return reader.ReadTLDictionary<ChatBase>(c => c.ID);
|
||||
return reader.ReadTLDictionary<ChatBase>();
|
||||
else
|
||||
return reader.ReadTLObject();
|
||||
default:
|
||||
|
|
@ -188,6 +212,26 @@ namespace TL
|
|||
}
|
||||
}
|
||||
|
||||
internal static void WriteTLMessages(this BinaryWriter writer, List<_Message> messages)
|
||||
{
|
||||
writer.Write(messages.Count);
|
||||
foreach (var msg in messages)
|
||||
{
|
||||
writer.Write(msg.msg_id);
|
||||
writer.Write(msg.seqno);
|
||||
var patchPos = writer.BaseStream.Position;
|
||||
writer.Write(0); // patched below
|
||||
if ((msg.seqno & 1) != 0)
|
||||
WTelegram.Helpers.Log(1, $" → {msg.body.GetType().Name.TrimEnd('_'),-38} #{(short)msg.msg_id.GetHashCode():X4}");
|
||||
else
|
||||
WTelegram.Helpers.Log(1, $" → {msg.body.GetType().Name.TrimEnd('_'),-38}");
|
||||
writer.WriteTLObject(msg.body);
|
||||
writer.BaseStream.Position = patchPos;
|
||||
writer.Write((int)(writer.BaseStream.Length - patchPos - 4)); // patch bytes field
|
||||
writer.Seek(0, SeekOrigin.End);
|
||||
}
|
||||
}
|
||||
|
||||
internal static void WriteTLVector(this BinaryWriter writer, Array array)
|
||||
{
|
||||
writer.Write(Layer.VectorCtor);
|
||||
|
|
@ -199,24 +243,40 @@ namespace TL
|
|||
writer.WriteTLValue(array.GetValue(i), elementType);
|
||||
}
|
||||
|
||||
internal static void WriteTLMessages(this BinaryWriter writer, _Message[] messages)
|
||||
internal static void WriteTLRawVector(this BinaryWriter writer, Array array, int elementSize)
|
||||
{
|
||||
writer.Write(messages.Length);
|
||||
foreach (var msg in messages)
|
||||
var startPos = writer.BaseStream.Position;
|
||||
int count = array.Length;
|
||||
var elementType = array.GetType().GetElementType();
|
||||
for (int i = count - 1; i >= 0; i--)
|
||||
{
|
||||
writer.Write(msg.msg_id);
|
||||
writer.Write(msg.seqno);
|
||||
var patchPos = writer.BaseStream.Position;
|
||||
writer.Write(0); // patched below
|
||||
writer.WriteTLObject(msg.body);
|
||||
if ((msg.seqno & 1) != 0)
|
||||
WTelegram.Helpers.Log(1, $" → {msg.body.GetType().Name.TrimEnd('_'),-38} #{(short)msg.msg_id.GetHashCode():X4}");
|
||||
else
|
||||
WTelegram.Helpers.Log(1, $" → {msg.body.GetType().Name.TrimEnd('_'),-38}");
|
||||
writer.BaseStream.Position = patchPos;
|
||||
writer.Write((int)(writer.BaseStream.Length - patchPos - 4)); // patch bytes field
|
||||
writer.Seek(0, SeekOrigin.End);
|
||||
writer.BaseStream.Position = startPos + i * elementSize;
|
||||
writer.WriteTLValue(array.GetValue(i), elementType);
|
||||
}
|
||||
writer.BaseStream.Position = startPos;
|
||||
writer.Write(count);
|
||||
writer.BaseStream.Position = startPos + count * elementSize + 4;
|
||||
}
|
||||
|
||||
internal static List<T> ReadTLRawVector<T>(this BinaryReader reader, uint ctorNb)
|
||||
{
|
||||
int count = reader.ReadInt32();
|
||||
var list = new List<T>(count);
|
||||
for (int i = 0; i < count; i++)
|
||||
list.Add((T)reader.ReadTLObject(ctorNb));
|
||||
return list;
|
||||
}
|
||||
|
||||
internal static T[] ReadTLVector<T>(this BinaryReader reader)
|
||||
{
|
||||
var elementType = typeof(T);
|
||||
if (reader.ReadUInt32() is not Layer.VectorCtor and uint ctorNb)
|
||||
throw new WTelegram.WTException($"Cannot deserialize {elementType.Name}[] with ctor #{ctorNb:x}");
|
||||
int count = reader.ReadInt32();
|
||||
var array = new T[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
array[i] = (T)reader.ReadTLValue(elementType);
|
||||
return array;
|
||||
}
|
||||
|
||||
internal static Array ReadTLVector(this BinaryReader reader, Type type)
|
||||
|
|
@ -244,33 +304,35 @@ namespace TL
|
|||
return array;
|
||||
}
|
||||
else
|
||||
throw new ApplicationException($"Cannot deserialize {type.Name} with ctor #{ctorNb:x}");
|
||||
throw new WTelegram.WTException($"Cannot deserialize {type.Name} with ctor #{ctorNb:x}");
|
||||
}
|
||||
|
||||
internal static Dictionary<long, T> ReadTLDictionary<T>(this BinaryReader reader, Func<T, long> getID) where T : class
|
||||
internal static Dictionary<long, T> ReadTLDictionary<T>(this BinaryReader reader) where T : class, IPeerInfo
|
||||
{
|
||||
uint ctorNb = reader.ReadUInt32();
|
||||
var elementType = typeof(T);
|
||||
if (ctorNb != Layer.VectorCtor)
|
||||
throw new ApplicationException($"Cannot deserialize Vector<{elementType.Name}> with ctor #{ctorNb:x}");
|
||||
throw new WTelegram.WTException($"Cannot deserialize Vector<{typeof(T).Name}> with ctor #{ctorNb:x}");
|
||||
int count = reader.ReadInt32();
|
||||
var dict = new Dictionary<long, T>(count);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var value = (T)reader.ReadTLValue(elementType);
|
||||
dict[getID(value)] = value is UserEmpty ? null : value;
|
||||
var obj = reader.ReadTLObject();
|
||||
if (obj is T value) dict[value.ID] = value;
|
||||
else if (obj is UserEmpty ue) dict[ue.id] = null;
|
||||
else throw new InvalidCastException($"ReadTLDictionary got '{obj?.GetType().Name}' instead of '{typeof(T).Name}'");
|
||||
}
|
||||
return dict;
|
||||
}
|
||||
|
||||
internal static void WriteTLStamp(this BinaryWriter writer, DateTime datetime)
|
||||
=> writer.Write(datetime == DateTime.MaxValue ? int.MaxValue : (int)(datetime.ToUniversalTime().Ticks / 10000000 - 62135596800L));
|
||||
=> writer.Write((int)Math.Min(Math.Max(datetime.ToUniversalTime().Ticks / 10000000 - 62135596800L, 0), int.MaxValue));
|
||||
|
||||
internal static DateTime ReadTLStamp(this BinaryReader reader)
|
||||
internal static DateTime ReadTLStamp(this BinaryReader reader) => reader.ReadInt32() switch
|
||||
{
|
||||
int unixstamp = reader.ReadInt32();
|
||||
return unixstamp == int.MaxValue ? DateTime.MaxValue : new((unixstamp + 62135596800L) * 10000000, DateTimeKind.Utc);
|
||||
}
|
||||
<= 0 => default,
|
||||
int.MaxValue => DateTime.MaxValue,
|
||||
int unixstamp => new((unixstamp + 62135596800L) * 10000000, DateTimeKind.Utc)
|
||||
};
|
||||
|
||||
internal static void WriteTLString(this BinaryWriter writer, string str)
|
||||
{
|
||||
|
|
@ -327,6 +389,19 @@ namespace TL
|
|||
writer.Write(0); // null arrays/strings are serialized as empty
|
||||
}
|
||||
|
||||
internal static object ReadTLGzipped(this BinaryReader reader, Type type)
|
||||
{
|
||||
using var gzipReader = new BinaryReader(new GZipStream(new MemoryStream(reader.ReadTLBytes()), CompressionMode.Decompress));
|
||||
return gzipReader.ReadTLValue(type);
|
||||
}
|
||||
|
||||
internal static bool ReadTLBool(this BinaryReader reader) => reader.ReadUInt32() switch
|
||||
{
|
||||
0x997275b5 => true,
|
||||
0xbc799737 => false,
|
||||
var value => throw new WTelegram.WTException($"Invalid boolean value #{value:x}")
|
||||
};
|
||||
|
||||
#if DEBUG
|
||||
private static void ShouldntBeHere() => System.Diagnostics.Debugger.Break();
|
||||
#else
|
||||
|
|
@ -338,13 +413,13 @@ namespace TL
|
|||
{
|
||||
public byte[] raw;
|
||||
|
||||
public Int128(System.IO.BinaryReader reader) => raw = reader.ReadBytes(16);
|
||||
public Int128(RNGCryptoServiceProvider rng) => rng.GetBytes(raw = new byte[16]);
|
||||
public Int128(BinaryReader reader) => raw = reader.ReadBytes(16);
|
||||
public Int128(RandomNumberGenerator rng) => rng.GetBytes(raw = new byte[16]);
|
||||
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; }
|
||||
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; }
|
||||
public override bool Equals(object obj) => obj is Int128 other && this == other;
|
||||
public override int GetHashCode() => BitConverter.ToInt32(raw, 0);
|
||||
public override string ToString() => Convert.ToHexString(raw);
|
||||
public override readonly bool Equals(object obj) => obj is Int128 other && this == other;
|
||||
public override readonly int GetHashCode() => BitConverter.ToInt32(raw, 0);
|
||||
public override readonly string ToString() => Convert.ToHexString(raw);
|
||||
public static implicit operator byte[](Int128 int128) => int128.raw;
|
||||
}
|
||||
|
||||
|
|
@ -352,48 +427,69 @@ namespace TL
|
|||
{
|
||||
public byte[] raw;
|
||||
|
||||
public Int256(System.IO.BinaryReader reader) => raw = reader.ReadBytes(32);
|
||||
public Int256(RNGCryptoServiceProvider rng) => rng.GetBytes(raw = new byte[32]);
|
||||
public Int256(BinaryReader reader) => raw = reader.ReadBytes(32);
|
||||
public Int256(RandomNumberGenerator rng) => rng.GetBytes(raw = new byte[32]);
|
||||
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; }
|
||||
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; }
|
||||
public override bool Equals(object obj) => obj is Int256 other && this == other;
|
||||
public override int GetHashCode() => BitConverter.ToInt32(raw, 0);
|
||||
public override string ToString() => Convert.ToHexString(raw);
|
||||
public override readonly bool Equals(object obj) => obj is Int256 other && this == other;
|
||||
public override readonly int GetHashCode() => BitConverter.ToInt32(raw, 0);
|
||||
public override readonly string ToString() => Convert.ToHexString(raw);
|
||||
public static implicit operator byte[](Int256 int256) => int256.raw;
|
||||
}
|
||||
|
||||
public sealed partial class UpdateAffectedMessages : Update // auto-generated for OnOwnUpdates in case of such API call result
|
||||
{
|
||||
public long mbox_id;
|
||||
public int pts;
|
||||
public int pts_count;
|
||||
public override (long, int, int) GetMBox() => (mbox_id, pts, pts_count);
|
||||
#if MTPG
|
||||
public override void WriteTL(BinaryWriter writer) => throw new NotSupportedException();
|
||||
#endif
|
||||
}
|
||||
|
||||
// Below TL types are commented "parsed manually" from https://github.com/telegramdesktop/tdesktop/blob/dev/Telegram/Resources/tl/mtproto.tl
|
||||
|
||||
[TLDef(0x7A19CB76)] //RSA_public_key#7a19cb76 n:bytes e:bytes = RSAPublicKey
|
||||
public class RSAPublicKey : IObject
|
||||
public sealed partial class RSAPublicKey : IObject
|
||||
{
|
||||
public byte[] n;
|
||||
public byte[] e;
|
||||
}
|
||||
|
||||
[TLDef(0xF35C6D01)] //rpc_result#f35c6d01 req_msg_id:long result:Object = RpcResult
|
||||
public class RpcResult : IObject
|
||||
public sealed partial class RpcResult : IObject
|
||||
{
|
||||
public long req_msg_id;
|
||||
public object result;
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006")]
|
||||
[TLDef(0x5BB8E511)] //message#5bb8e511 msg_id:long seqno:int bytes:int body:Object = Message
|
||||
public class _Message
|
||||
public sealed partial class _Message(long msgId, int seqno, IObject obj) : IObject
|
||||
{
|
||||
public _Message(long msgId, int seqNo, IObject obj) { msg_id = msgId; seqno = seqNo; body = obj; }
|
||||
public long msg_id;
|
||||
public int seqno;
|
||||
public long msg_id = msgId;
|
||||
public int seqno = seqno;
|
||||
public int bytes;
|
||||
public IObject body;
|
||||
public IObject body = obj;
|
||||
}
|
||||
|
||||
[TLDef(0x73F1F8DC)] //msg_container#73f1f8dc messages:vector<%Message> = MessageContainer
|
||||
public class MsgContainer : IObject { public _Message[] messages; }
|
||||
public sealed partial class MsgContainer : IObject { public List<_Message> messages; }
|
||||
[TLDef(0xE06046B2)] //msg_copy#e06046b2 orig_message:Message = MessageCopy
|
||||
public class MsgCopy : IObject { public _Message orig_message; }
|
||||
public sealed partial class MsgCopy : IObject { public _Message orig_message; }
|
||||
|
||||
[TLDef(0x3072CFA1)] //gzip_packed#3072cfa1 packed_data:bytes = Object
|
||||
public class GzipPacked : IObject { public byte[] packed_data; }
|
||||
public sealed partial class GzipPacked : IObject { public byte[] packed_data; }
|
||||
|
||||
public sealed class Null<X> : IObject
|
||||
{
|
||||
public readonly static Null<X> Instance = new();
|
||||
public void WriteTL(BinaryWriter writer) => writer.WriteTLNull(typeof(X));
|
||||
}
|
||||
|
||||
public sealed class BoolMethod : IMethod<object>
|
||||
{
|
||||
public IObject query;
|
||||
public void WriteTL(BinaryWriter writer) => query.WriteTL(writer);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
134
src/TlsStream.cs
134
src/TlsStream.cs
|
|
@ -11,14 +11,13 @@ using System.Threading.Tasks;
|
|||
|
||||
namespace WTelegram
|
||||
{
|
||||
class TlsStream : Helpers.IndirectStream
|
||||
internal sealed class TlsStream(Stream innerStream) : Helpers.IndirectStream(innerStream)
|
||||
{
|
||||
public TlsStream(Stream innerStream) : base(innerStream) { }
|
||||
private int _tlsFrameleft;
|
||||
private readonly byte[] _tlsSendHeader = new byte[] { 0x17, 0x03, 0x03, 0, 0 };
|
||||
private readonly byte[] _tlsSendHeader = [0x17, 0x03, 0x03, 0, 0];
|
||||
private readonly byte[] _tlsReadHeader = new byte[5];
|
||||
static readonly byte[] TlsServerHello3 = new byte[] { 0x14, 0x03, 0x03, 0x00, 0x01, 0x01, 0x17, 0x03, 0x03 };
|
||||
static readonly byte[] TlsClientPrefix = new byte[] { 0x14, 0x03, 0x03, 0x00, 0x01, 0x01 };
|
||||
static readonly byte[] TlsServerHello3 = [0x14, 0x03, 0x03, 0x00, 0x01, 0x01, 0x17, 0x03, 0x03];
|
||||
static readonly byte[] TlsClientPrefix = [0x14, 0x03, 0x03, 0x00, 0x01, 0x01];
|
||||
|
||||
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken ct)
|
||||
{
|
||||
|
|
@ -27,7 +26,7 @@ namespace WTelegram
|
|||
if (await _innerStream.FullReadAsync(_tlsReadHeader, 5, ct) != 5)
|
||||
return 0;
|
||||
if (_tlsReadHeader[0] != 0x17 || _tlsReadHeader[1] != 0x03 || _tlsReadHeader[2] != 0x03)
|
||||
throw new ApplicationException("Could not read frame data : Invalid TLS header");
|
||||
throw new WTException("Could not read frame data : Invalid TLS header");
|
||||
_tlsFrameleft = (_tlsReadHeader[3] << 8) + _tlsReadHeader[4];
|
||||
}
|
||||
var read = await _innerStream.ReadAsync(buffer, offset, Math.Min(count, _tlsFrameleft), ct);
|
||||
|
|
@ -82,42 +81,46 @@ namespace WTelegram
|
|||
}
|
||||
}
|
||||
}
|
||||
throw new ApplicationException("TLS Handshake failed");
|
||||
throw new WTException("TLS Handshake failed");
|
||||
}
|
||||
|
||||
static readonly byte[] TlsClientHello1 = new byte[11] {
|
||||
0x16, 0x03, 0x01, 0x02, 0x00, 0x01, 0x00, 0x01, 0xfc, 0x03, 0x03 };
|
||||
static readonly byte[] TlsClientHello1 = [ // https://tls13.xargs.org/#client-hello/annotated
|
||||
0x16, 0x03, 0x01, 0x02, 0x00, 0x01, 0x00, 0x01, 0xfc, 0x03, 0x03 ];
|
||||
// digest[32]
|
||||
// 0x20
|
||||
// random[32]
|
||||
// 0x00, 0x20, grease(0) GREASE are two identical bytes ending with nibble 'A'
|
||||
static readonly byte[] TlsClientHello2 = new byte[34] {
|
||||
// 0x00, 0x20
|
||||
// grease(0) GREASE are two identical bytes ending with nibble 'A'
|
||||
static readonly byte[] TlsClientHello2 = [
|
||||
0x13, 0x01, 0x13, 0x02, 0x13, 0x03, 0xc0, 0x2b, 0xc0, 0x2f, 0xc0, 0x2c, 0xc0, 0x30, 0xcc, 0xa9,
|
||||
0xcc, 0xa8, 0xc0, 0x13, 0xc0, 0x14, 0x00, 0x9c, 0x00, 0x9d, 0x00, 0x2f, 0x00, 0x35, 0x01, 0x00, 0x01, 0x93 };
|
||||
// grease(2), 0x00, 0x00, 0x00, 0x00
|
||||
// len { len { 0x00 len { domain } } } len is 16-bit big-endian length of the following block of data
|
||||
static readonly byte[] TlsClientHello3 = new byte[101] {
|
||||
0x00, 0x17, 0x00, 0x00, 0xff, 0x01, 0x00, 0x01, 0x00, 0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08,
|
||||
0x4A, 0x4A, // = grease(4)
|
||||
0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 0x00, 0x0b, 0x00, 0x02, 0x01, 0x00, 0x00, 0x23, 0x00, 0x00,
|
||||
0x00, 0x10, 0x00, 0x0e, 0x00, 0x0c, 0x02, 0x68, 0x32, 0x08, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x31,
|
||||
0x2e, 0x31, 0x00, 0x05, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x12, 0x00,
|
||||
0x10, 0x04, 0x03, 0x08, 0x04, 0x04, 0x01, 0x05, 0x03, 0x08, 0x05, 0x05, 0x01, 0x08, 0x06, 0x06,
|
||||
0x01, 0x00, 0x12, 0x00, 0x00, 0x00, 0x33, 0x00, 0x2b, 0x00, 0x29,
|
||||
0x4A, 0x4A, // = grease(4)
|
||||
0x00, 0x01, 0x00, 0x00, 0x1d, 0x00, 0x20 };
|
||||
// random[32] = public key
|
||||
static readonly byte[] TlsClientHello4 = new byte[35] {
|
||||
0x00, 0x2d, 0x00, 0x02, 0x01, 0x01, 0x00, 0x2b, 0x00, 0x0b, 0x0a,
|
||||
0x6A, 0x6A, // = grease(6)
|
||||
0x03, 0x04, 0x03, 0x03, 0x03, 0x02, 0x03, 0x01, 0x00, 0x1b, 0x00, 0x03, 0x02, 0x00, 0x02,
|
||||
0x3A, 0x3A, // = grease(3)
|
||||
0x00, 0x01, 0x00, 0x00, 0x15 };
|
||||
0xcc, 0xa8, 0xc0, 0x13, 0xc0, 0x14, 0x00, 0x9c, 0x00, 0x9d, 0x00, 0x2f, 0x00, 0x35,
|
||||
0x01, 0x00, 0x01, 0x93 ];
|
||||
// grease(2)
|
||||
// 0x00, 0x00
|
||||
static readonly byte[] TlsClientHello3 = [
|
||||
// 0x00, 0x00, len { len { 0x00 len { domain } } } len is 16-bit big-endian length of the following block of data
|
||||
0x00, 0x05, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, 0x4A, 0x4A/*=grease(4)*/, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18,
|
||||
0x00, 0x0b, 0x00, 0x02, 0x01, 0x00,
|
||||
0x00, 0x0d, 0x00, 0x12, 0x00, 0x10, 0x04, 0x03, 0x08, 0x04, 0x04, 0x01, 0x05, 0x03, 0x08, 0x05, 0x05, 0x01, 0x08, 0x06, 0x06, 0x01,
|
||||
0x00, 0x10, 0x00, 0x0e, 0x00, 0x0c, 0x02, 0x68, 0x32, 0x08, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x31, 0x2e, 0x31,
|
||||
0x00, 0x12, 0x00, 0x00,
|
||||
0x00, 0x17, 0x00, 0x00,
|
||||
0x00, 0x1b, 0x00, 0x03, 0x02, 0x00, 0x02,
|
||||
0x00, 0x23, 0x00, 0x00,
|
||||
0x00, 0x2b, 0x00, 0x07, 0x06, 0x6A, 0x6A/*=grease(6)*/, 0x03, 0x04, 0x03, 0x03,
|
||||
0x00, 0x2d, 0x00, 0x02, 0x01, 0x01,
|
||||
0x00, 0x33, 0x00, 0x2b, 0x00, 0x29, 0x4A, 0x4A/*=grease(4)*/, 0x00, 0x01, 0x00, 0x00, 0x1d, 0x00, 0x20, /* random[32] */
|
||||
0x44, 0x69, 0x00, 0x05, 0x00, 0x03, 0x02, 0x68, 0x32,
|
||||
0xff, 0x01, 0x00, 0x01, 0x00,
|
||||
];
|
||||
// grease(3)
|
||||
static readonly byte[] TlsClientHello4 = [
|
||||
0x00, 0x01, 0x00, 0x00, 0x15 ];
|
||||
// len { padding } padding with NUL bytes to reach 517 bytes
|
||||
|
||||
static byte[] TlsClientHello(byte[] key, byte[] domain)
|
||||
{
|
||||
int dlen = domain.Length;
|
||||
var greases = new byte[7];
|
||||
Encryption.RNG.GetBytes(greases);
|
||||
for (int i = 0; i < 7; i++) greases[i] = (byte)((greases[i] & 0xF0) + 0x0A);
|
||||
|
|
@ -130,19 +133,54 @@ namespace WTelegram
|
|||
buffer[78] = buffer[79] = greases[0];
|
||||
TlsClientHello2.CopyTo(buffer, 80);
|
||||
buffer[114] = buffer[115] = greases[2];
|
||||
buffer[121] = (byte)(dlen + 5);
|
||||
buffer[123] = (byte)(dlen + 3);
|
||||
buffer[126] = (byte)dlen;
|
||||
domain.CopyTo(buffer, 127);
|
||||
TlsClientHello3.CopyTo(buffer, 127 + dlen);
|
||||
buffer[142 + dlen] = buffer[143 + dlen] = greases[4];
|
||||
buffer[219 + dlen] = buffer[220 + dlen] = greases[4];
|
||||
Encryption.RNG.GetBytes(buffer, 228 + dlen, 32); // public key
|
||||
buffer[228 + dlen + 31] &= 0x7F; // must be positive
|
||||
TlsClientHello4.CopyTo(buffer, 260 + dlen);
|
||||
buffer[271 + dlen] = buffer[272 + dlen] = greases[6];
|
||||
buffer[288 + dlen] = buffer[289 + dlen] = greases[3];
|
||||
buffer[296 + dlen] = (byte)(220 - dlen);
|
||||
|
||||
int dlen = domain.Length;
|
||||
var server_name = new byte[dlen + 9];
|
||||
server_name[3] = (byte)(dlen + 5);
|
||||
server_name[5] = (byte)(dlen + 3);
|
||||
server_name[8] = (byte)dlen;
|
||||
domain.CopyTo(server_name, 9);
|
||||
|
||||
var key_share = new byte[47];
|
||||
Array.Copy(TlsClientHello3, 105, key_share, 0, 15);
|
||||
key_share[6] = key_share[7] = greases[4];
|
||||
Encryption.RNG.GetBytes(key_share, 15, 32); // public key
|
||||
key_share[46] &= 0x7F; // must be positive
|
||||
|
||||
var random = new Random();
|
||||
var permutations = new ArraySegment<byte>[15];
|
||||
for (var i = 0; i < permutations.Length; i++)
|
||||
{
|
||||
var j = random.Next(0, i + 1);
|
||||
if (i != j) permutations[i] = permutations[j];
|
||||
permutations[j] = i switch
|
||||
{
|
||||
0 => new(server_name),
|
||||
1 => new(TlsClientHello3, 0, 9),
|
||||
2 => PatchGrease(TlsClientHello3[9..23], 6, greases[4]),
|
||||
3 => new(TlsClientHello3, 23, 6),
|
||||
4 => new(TlsClientHello3, 29, 22),
|
||||
5 => new(TlsClientHello3, 51, 18),
|
||||
6 => new(TlsClientHello3, 69, 4),
|
||||
7 => new(TlsClientHello3, 73, 4),
|
||||
8 => new(TlsClientHello3, 77, 7),
|
||||
9 => new(TlsClientHello3, 84, 4),
|
||||
10 => PatchGrease(TlsClientHello3[88..99], 5, greases[6]),
|
||||
11 => new(TlsClientHello3, 99, 6),
|
||||
12 => new(key_share),
|
||||
13 => new(TlsClientHello3, 120, 9),
|
||||
_ => new(TlsClientHello3, 129, 5),
|
||||
};
|
||||
}
|
||||
int offset = 118;
|
||||
foreach (var perm in permutations)
|
||||
{
|
||||
Array.Copy(perm.Array, perm.Offset, buffer, offset, perm.Count);
|
||||
offset += perm.Count;
|
||||
}
|
||||
buffer[offset++] = buffer[offset++] = greases[3];
|
||||
TlsClientHello4.CopyTo(buffer, offset);
|
||||
buffer[offset + 6] = (byte)(510 - offset);
|
||||
|
||||
// patch-in digest with timestamp
|
||||
using var hmac = new HMACSHA256(key);
|
||||
|
|
@ -152,6 +190,12 @@ namespace WTelegram
|
|||
BinaryPrimitives.WriteInt32LittleEndian(digest.AsSpan(28), stamp);
|
||||
digest.CopyTo(buffer, 11);
|
||||
return buffer;
|
||||
|
||||
static ArraySegment<byte> PatchGrease(byte[] buffer, int offset, byte grease)
|
||||
{
|
||||
buffer[offset] = buffer[offset + 1] = grease;
|
||||
return new(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
610
src/UpdateManager.cs
Normal file
610
src/UpdateManager.cs
Normal file
|
|
@ -0,0 +1,610 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TL;
|
||||
|
||||
namespace WTelegram
|
||||
{
|
||||
public class UpdateManager
|
||||
{
|
||||
/// <summary>Collected info about Users <i>(only if using the default collector)</i></summary>
|
||||
public readonly Dictionary<long, User> Users;
|
||||
/// <summary>Collected info about Chats <i>(only if using the default collector)</i></summary>
|
||||
public readonly Dictionary<long, ChatBase> Chats;
|
||||
/// <summary>Timout to detect lack of updates and force refetch them</summary>
|
||||
public TimeSpan InactivityThreshold { get; set; } = TimeSpan.FromMinutes(15);
|
||||
/// <summary>Logging callback (defaults to WTelegram.Helpers.Log ; can be null for performance)</summary>
|
||||
public Action<int, string> Log { get; set; } = Helpers.Log;
|
||||
/// <summary>Current set of update states (for saving and later resume)</summary>
|
||||
public ImmutableDictionary<long, MBoxState> State
|
||||
{
|
||||
get
|
||||
{
|
||||
_sem.Wait();
|
||||
try { return _local.ToImmutableDictionary(); }
|
||||
finally { _sem.Release(); }
|
||||
}
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006")]
|
||||
public sealed class MBoxState { public int pts { get; set; } public long access_hash { get; set; } }
|
||||
|
||||
private readonly Client _client;
|
||||
private readonly Func<Update, Task> _onUpdate;
|
||||
private readonly IPeerCollector _collector;
|
||||
private readonly bool _reentrant;
|
||||
private readonly TaskScheduler _scheduler;
|
||||
private readonly SemaphoreSlim _sem = new(1);
|
||||
private readonly List<(Update update, UpdatesBase updates, bool own, DateTime stamp)> _pending = [];
|
||||
private readonly Dictionary<long, MBoxState> _local; // -2 for seq/date, -1 for qts, 0 for common pts, >0 for channel pts
|
||||
private const int L_SEQ = -2, L_QTS = -1, L_PTS = 0;
|
||||
private const long UndefinedSeqDate = 3155378975999999999L; // DateTime.MaxValue.Ticks
|
||||
private static readonly TimeSpan HalfSec = new(TimeSpan.TicksPerSecond / 2);
|
||||
private Task _recoveringGaps;
|
||||
private DateTime _lastUpdateStamp = DateTime.UtcNow;
|
||||
|
||||
/// <summary>Manager ensuring that you receive Telegram updates in correct order, without missing any</summary>
|
||||
/// <param name="client">the WTelegram Client to manage</param>
|
||||
/// <param name="onUpdate">Event to be called on sequential individual update</param>
|
||||
/// <param name="state">(optional) Resume session by recovering all updates that occured since this state</param>
|
||||
/// <param name="collector">Custom users/chats collector. By default, those are collected in properties Users/Chats</param>
|
||||
/// <param name="reentrant"><see langword="true"/> if your <paramref name="onUpdate"/> method can be called again even when last async call didn't return yet</param>
|
||||
public UpdateManager(Client client, Func<Update, Task> onUpdate, IDictionary<long, MBoxState> state = null, IPeerCollector collector = null, bool reentrant = false)
|
||||
{
|
||||
_client = client;
|
||||
_onUpdate = onUpdate;
|
||||
_collector = collector ?? new Services.CollectorPeer(Users = [], Chats = []);
|
||||
_scheduler = SynchronizationContext.Current == null ? TaskScheduler.Current : TaskScheduler.FromCurrentSynchronizationContext();
|
||||
|
||||
if (state == null || state.Count < 3)
|
||||
_local = new() { [L_SEQ] = new() { access_hash = UndefinedSeqDate }, [L_QTS] = new(), [L_PTS] = new() };
|
||||
else
|
||||
_local = state as Dictionary<long, MBoxState> ?? new Dictionary<long, MBoxState>(state);
|
||||
_reentrant = reentrant;
|
||||
client.OnOther += OnOther;
|
||||
client.OnUpdates += u => OnUpdates(u, false);
|
||||
client.OnOwnUpdates += u => OnUpdates(u, true);
|
||||
}
|
||||
|
||||
private async Task OnOther(IObject obj)
|
||||
{
|
||||
switch (obj)
|
||||
{
|
||||
case Pong when DateTime.UtcNow - _lastUpdateStamp > InactivityThreshold:
|
||||
if (_local[L_PTS].pts != 0) await ResyncState();
|
||||
break;
|
||||
case User user when user.flags.HasFlag(User.Flags.self):
|
||||
_collector.Collect([user]);
|
||||
goto newSession;
|
||||
case NewSessionCreated when _client.User != null:
|
||||
newSession:
|
||||
await Task.Delay(HalfSec); // let the opportunity to call DropPendingUpdates/StopResync before a big resync
|
||||
if (_local[L_PTS].pts != 0) await ResyncState();
|
||||
else await ResyncState(await _client.Updates_GetState());
|
||||
break;
|
||||
case Updates_State state:
|
||||
await ResyncState(state);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ResyncState(Updates_State state = null)
|
||||
{
|
||||
if (state != null) state.qts = 0; // for some reason Updates_GetState returns an invalid qts, so better consider we have no qts.
|
||||
else state = new() { qts = int.MaxValue };
|
||||
await _sem.WaitAsync();
|
||||
try
|
||||
{
|
||||
var local = _local[L_PTS];
|
||||
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}");
|
||||
if (local.pts == 0 || local.pts >= state.pts && _local[L_SEQ].pts >= state.seq && _local[L_QTS].pts >= state.qts)
|
||||
await HandleDifference(null, null, state, null);
|
||||
else if (await GetDifference(L_PTS, state.pts, local))
|
||||
await ApplyFilledGaps();
|
||||
}
|
||||
finally { _sem.Release(); }
|
||||
}
|
||||
|
||||
private async Task OnUpdates(UpdatesBase updates, bool own)
|
||||
{
|
||||
RaiseCollect(updates.Users, updates.Chats);
|
||||
await _sem.WaitAsync();
|
||||
try
|
||||
{
|
||||
await HandleUpdates(updates, own);
|
||||
}
|
||||
finally { _sem.Release(); }
|
||||
}
|
||||
|
||||
private async Task HandleUpdates(UpdatesBase updates, bool own)
|
||||
{
|
||||
var now = _lastUpdateStamp = DateTime.UtcNow;
|
||||
var updateList = updates.UpdateList;
|
||||
if (updates is UpdateShortSentMessage sent)
|
||||
updateList = [new UpdateNewMessage { pts = sent.pts, pts_count = sent.pts_count, message = new Message {
|
||||
flags = (Message.Flags)sent.flags,
|
||||
id = sent.id, date = sent.date, entities = sent.entities, media = sent.media, ttl_period = sent.ttl_period,
|
||||
} }];
|
||||
else if (updates is UpdateShortMessage usm && !_collector.HasUser(usm.user_id))
|
||||
RaiseCollect(await _client.Updates_GetDifference(usm.pts - usm.pts_count, usm.date, 0));
|
||||
else if (updates is UpdateShortChatMessage uscm && (!_collector.HasUser(uscm.from_id) || !_collector.HasChat(uscm.chat_id)))
|
||||
RaiseCollect(await _client.Updates_GetDifference(uscm.pts - uscm.pts_count, uscm.date, 0));
|
||||
|
||||
bool ptsChanged = false, gotUPts = false;
|
||||
int seq = 0;
|
||||
try
|
||||
{
|
||||
if (updates is UpdatesTooLong)
|
||||
{
|
||||
var local_pts = _local[L_PTS];
|
||||
ptsChanged = await GetDifference(L_PTS, local_pts.pts, local_pts);
|
||||
return;
|
||||
}
|
||||
foreach (var update in updateList)
|
||||
{
|
||||
if (update == null) continue;
|
||||
var (mbox_id, pts, pts_count) = update.GetMBox();
|
||||
if (pts == 0) (mbox_id, pts, pts_count) = updates.GetMBox();
|
||||
MBoxState local = null;
|
||||
if (pts != 0)
|
||||
{
|
||||
local = _local.GetOrCreate(mbox_id);
|
||||
if (mbox_id > 0 && local.access_hash == 0)
|
||||
if (updates.Chats.TryGetValue(mbox_id, out var chat) && chat is Channel channel && !channel.flags.HasFlag(Channel.Flags.min))
|
||||
local.access_hash = channel.access_hash;
|
||||
var diff = local.pts + pts_count - pts;
|
||||
if (diff > 0 && pts_count != 0) // the update was already applied, and must be ignored.
|
||||
{
|
||||
Log?.Invoke(1, $"({mbox_id,10}, {local.pts,6}+{pts_count}->{pts,-6}) {update,-30} ignored {ExtendedLog(update)}");
|
||||
continue;
|
||||
}
|
||||
if (diff < 0) // there's an update gap that must be filled.
|
||||
{
|
||||
Log?.Invoke(1, $"({mbox_id,10}, {local.pts,6}+{pts_count}->{pts,-6}) {update,-30} pending {ExtendedLog(update)}");
|
||||
_pending.Add((update, updates, own, now + HalfSec));
|
||||
_recoveringGaps ??= Task.Delay(HalfSec).ContinueWith(RecoverGaps, _scheduler);
|
||||
continue;
|
||||
}
|
||||
// the update can be applied.
|
||||
}
|
||||
Log?.Invoke(1, $"({mbox_id,10}, {local?.pts,6}+{pts_count}->{pts,-6}) {update,-30} applied {ExtendedLog(update)}");
|
||||
if (mbox_id == L_SEQ && update is UpdatePtsChanged) gotUPts = true;
|
||||
if (pts_count > 0 && pts != 0)
|
||||
{
|
||||
ptsChanged = true;
|
||||
if (mbox_id == L_SEQ)
|
||||
seq = pts;
|
||||
else if (pts_count != 0)
|
||||
local.pts = pts;
|
||||
}
|
||||
if (!own) await RaiseUpdate(update);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (seq > 0) // update local_seq & date after the updates were applied
|
||||
{
|
||||
var local_seq = _local[L_SEQ];
|
||||
local_seq.pts = seq;
|
||||
local_seq.access_hash = updates.Date.Ticks;
|
||||
}
|
||||
if (gotUPts) ptsChanged = await GetDifference(L_PTS, _local[L_PTS].pts = 1, _local[L_PTS]);
|
||||
if (ptsChanged) await ApplyFilledGaps();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<int> ApplyFilledGaps()
|
||||
{
|
||||
if (_pending.Count != 0) Log?.Invoke(2, $"Trying to apply {_pending.Count} pending updates after filled gaps");
|
||||
int removed = 0;
|
||||
for (int i = 0; i < _pending.Count; )
|
||||
{
|
||||
var (update, updates, own, _) = _pending[i];
|
||||
var (mbox_id, pts, pts_count) = update.GetMBox();
|
||||
if (pts == 0) (mbox_id, pts, pts_count) = updates.GetMBox();
|
||||
var local = _local[mbox_id];
|
||||
var diff = local.pts + pts_count - pts;
|
||||
if (diff < 0)
|
||||
++i; // there's still a gap, skip it
|
||||
else
|
||||
{
|
||||
_pending.RemoveAt(i);
|
||||
++removed;
|
||||
if (diff > 0) // the update was already applied, remove & ignore
|
||||
Log?.Invoke(1, $"({mbox_id,10}, {local.pts,6}+{pts_count}->{pts,-6}) {update,-30} obsolete {ExtendedLog(update)}");
|
||||
else
|
||||
{
|
||||
Log?.Invoke(1, $"({mbox_id,10}, {local.pts,6}+{pts_count}->{pts,-6}) {update,-30} applied now {ExtendedLog(update)}");
|
||||
// the update can be applied.
|
||||
local.pts = pts;
|
||||
if (mbox_id == L_SEQ) local.access_hash = updates.Date.Ticks;
|
||||
if (!own) await RaiseUpdate(update);
|
||||
i = 0; // rescan pending updates from start
|
||||
}
|
||||
}
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
private async Task RecoverGaps(Task _) // https://corefork.telegram.org/api/updates#recovering-gaps
|
||||
{
|
||||
await _sem.WaitAsync();
|
||||
try
|
||||
{
|
||||
_recoveringGaps = null;
|
||||
if (_pending.Count == 0) return;
|
||||
Log?.Invoke(2, $"Trying to recover gaps for {_pending.Count} pending updates");
|
||||
var now = DateTime.UtcNow;
|
||||
while (_pending.Count != 0)
|
||||
{
|
||||
var (update, updates, own, stamp) = _pending[0];
|
||||
if (stamp > now)
|
||||
{
|
||||
_recoveringGaps = Task.Delay(stamp - now).ContinueWith(RecoverGaps, _scheduler);
|
||||
return;
|
||||
}
|
||||
var (mbox_id, pts, pts_count) = update.GetMBox();
|
||||
if (pts == 0) (mbox_id, pts, pts_count) = updates.GetMBox();
|
||||
var local = _local[mbox_id];
|
||||
bool getDiffSuccess = false;
|
||||
if (local.pts == 0)
|
||||
Log?.Invoke(2, $"({mbox_id,10}, new +{pts_count}->{pts,-6}) {update,-30} First appearance of MBox {ExtendedLog(update)}");
|
||||
else if (local.access_hash == -1) // no valid access_hash for this channel, so just raise this update
|
||||
Log?.Invoke(3, $"({mbox_id,10}, {local.pts,6}+{pts_count}->{pts,-6}) {update,-30} No access_hash to recover {ExtendedLog(update)}");
|
||||
else if (local.pts + pts_count - pts >= 0)
|
||||
getDiffSuccess = true;
|
||||
else
|
||||
{
|
||||
Log?.Invoke(1, $"({mbox_id,10}, {local.pts,6}+{pts_count}->{pts,-6}) {update,-30} Calling GetDifference {ExtendedLog(update)}");
|
||||
getDiffSuccess = await GetDifference(mbox_id, pts, local);
|
||||
}
|
||||
if (!getDiffSuccess) // no getDiff => just raise received pending updates in order
|
||||
{
|
||||
local.pts = pts - pts_count;
|
||||
for (int i = 1; i < _pending.Count; i++) // find lowest pending pts-pts_count for this mbox
|
||||
{
|
||||
var pending = _pending[i];
|
||||
var mbox = pending.update.GetMBox();
|
||||
if (mbox.pts == 0) mbox = pending.updates.GetMBox();
|
||||
if (mbox.mbox_id == mbox_id) local.pts = Math.Min(local.pts, mbox.pts - mbox.pts_count);
|
||||
}
|
||||
}
|
||||
|
||||
if (await ApplyFilledGaps() == 0)
|
||||
{
|
||||
Log?.Invoke(3, $"({mbox_id,10}, {local.pts,6}+{pts_count}->{pts,-6}) {update,-30} forcibly removed!");
|
||||
_pending.RemoveAt(0);
|
||||
local.pts = pts;
|
||||
if (!own) await RaiseUpdate(update);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally { _sem.Release(); }
|
||||
}
|
||||
|
||||
public async Task StopResync()
|
||||
{
|
||||
await _sem.WaitAsync();
|
||||
try
|
||||
{
|
||||
foreach (var local in _local.Values)
|
||||
local.pts = 0;
|
||||
_pending.Clear();
|
||||
}
|
||||
finally { _sem.Release(); }
|
||||
}
|
||||
|
||||
private async Task<InputChannel> GetInputChannel(long channel_id, MBoxState local)
|
||||
{
|
||||
if (channel_id <= 0) return null;
|
||||
if (local?.access_hash is not null and not 0)
|
||||
return new InputChannel(channel_id, local.access_hash);
|
||||
var inputChannel = new InputChannel(channel_id, 0);
|
||||
try
|
||||
{
|
||||
var mc = await _client.Channels_GetChannels(inputChannel);
|
||||
if (mc.chats.TryGetValue(channel_id, out var chat) && chat is Channel channel)
|
||||
inputChannel.access_hash = channel.access_hash;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
inputChannel.access_hash = -1; // no valid access_hash available
|
||||
}
|
||||
local ??= _local[channel_id] = new();
|
||||
local.access_hash = inputChannel.access_hash;
|
||||
return inputChannel;
|
||||
}
|
||||
|
||||
private async Task<bool> GetDifference(long mbox_id, int expected_pts, MBoxState local)
|
||||
{
|
||||
try
|
||||
{
|
||||
moreDiffNeeded:
|
||||
if (mbox_id <= 0)
|
||||
{
|
||||
Log?.Invoke(0, $"Local states {string.Join(" ", _local.Select(l => $"{l.Key}:{l.Value.pts}"))}");
|
||||
var local_seq = _local[L_SEQ];
|
||||
var diff = await _client.Updates_GetDifference(_local[L_PTS].pts, qts: _local[L_QTS].pts,
|
||||
date: new DateTime(local_seq.access_hash, DateTimeKind.Utc));
|
||||
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))}");
|
||||
switch (diff)
|
||||
{
|
||||
case Updates_Difference ud:
|
||||
await HandleDifference(ud.new_messages, ud.new_encrypted_messages, ud.state,
|
||||
new UpdatesCombined { updates = ud.other_updates, users = ud.users, chats = ud.chats,
|
||||
date = ud.state.date, seq_start = local_seq.pts + 1, seq = ud.state.seq });
|
||||
break;
|
||||
case Updates_DifferenceSlice uds:
|
||||
await HandleDifference(uds.new_messages, uds.new_encrypted_messages, uds.intermediate_state,
|
||||
new UpdatesCombined { updates = uds.other_updates, users = uds.users, chats = uds.chats,
|
||||
date = uds.intermediate_state.date, seq_start = local_seq.pts + 1, seq = uds.intermediate_state.seq });
|
||||
goto moreDiffNeeded;
|
||||
case Updates_DifferenceTooLong udtl:
|
||||
_local[L_PTS].pts = udtl.pts;
|
||||
goto moreDiffNeeded;
|
||||
case Updates_DifferenceEmpty ude:
|
||||
local_seq.pts = ude.seq;
|
||||
local_seq.access_hash = ude.date.Ticks;
|
||||
_lastUpdateStamp = DateTime.UtcNow;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var channel = await GetInputChannel(mbox_id, local);
|
||||
if (channel.access_hash == -1) return false;
|
||||
try
|
||||
{
|
||||
var diff = await _client.Updates_GetChannelDifference(channel, null, local.pts);
|
||||
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))}");
|
||||
switch (diff)
|
||||
{
|
||||
case Updates_ChannelDifference ucd:
|
||||
local.pts = ucd.pts;
|
||||
await HandleDifference(ucd.new_messages, null, null,
|
||||
new UpdatesCombined { updates = ucd.other_updates, users = ucd.users, chats = ucd.chats });
|
||||
if (!ucd.flags.HasFlag(Updates_ChannelDifference.Flags.final)) goto moreDiffNeeded;
|
||||
break;
|
||||
case Updates_ChannelDifferenceTooLong ucdtl:
|
||||
if (ucdtl.dialog is Dialog dialog) local.pts = dialog.pts;
|
||||
await HandleDifference(ucdtl.messages, null, null,
|
||||
new UpdatesCombined { updates = null, users = ucdtl.users, chats = ucdtl.chats });
|
||||
break;
|
||||
case Updates_ChannelDifferenceEmpty ucde:
|
||||
local.pts = ucde.pts;
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (RpcException ex) when (ex.Message is "CHANNEL_PRIVATE" or "CHANNEL_INVALID")
|
||||
{
|
||||
local.access_hash = -1; // access_hash is no longer valid
|
||||
throw;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log?.Invoke(4, $"GetDifference({mbox_id}, {local.pts}->{expected_pts}) raised {ex}");
|
||||
if (ex.Message == "PERSISTENT_TIMESTAMP_INVALID") // oh boy, we're lost!
|
||||
if (mbox_id <= 0)
|
||||
await HandleDifference(null, null, await _client.Updates_GetState(), null);
|
||||
else if ((await _client.Channels_GetFullChannel(await GetInputChannel(mbox_id, local))).full_chat is ChannelFull full)
|
||||
local.pts = full.pts;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (local.pts < expected_pts) local.pts = expected_pts;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task HandleDifference(MessageBase[] new_messages, EncryptedMessageBase[] enc_messages, Updates_State state, UpdatesCombined updates)
|
||||
{
|
||||
if (updates != null)
|
||||
RaiseCollect(updates.users, updates.chats);
|
||||
try
|
||||
{
|
||||
int updatesCount = updates?.updates.Length ?? 0;
|
||||
if (updatesCount != 0)
|
||||
for (int i = 0; i < updates.updates.Length; i++)
|
||||
{
|
||||
var update = updates.updates[i];
|
||||
if (update is UpdateMessageID or UpdateStoryID)
|
||||
{
|
||||
await RaiseUpdate(update);
|
||||
updates.updates[i] = null;
|
||||
--updatesCount;
|
||||
}
|
||||
}
|
||||
if (new_messages?.Length > 0)
|
||||
{
|
||||
var update = state == null ? new UpdateNewChannelMessage() : new UpdateNewMessage() { pts = state.pts, pts_count = 1 };
|
||||
foreach (var msg in new_messages)
|
||||
{
|
||||
if (_pending.Any(p => p is { own: true, update: UpdateNewMessage { message: { Peer.ID: var peer_id, ID: var msg_id } } }
|
||||
&& peer_id == msg.Peer.ID && msg_id == msg.ID))
|
||||
continue;
|
||||
update.message = msg;
|
||||
await RaiseUpdate(update);
|
||||
}
|
||||
}
|
||||
if (enc_messages?.Length > 0)
|
||||
{
|
||||
var update = new UpdateNewEncryptedMessage();
|
||||
if (state != null) update.qts = state.qts;
|
||||
foreach (var msg in enc_messages)
|
||||
{
|
||||
if (_pending.Any(p => p is { own: true, update: UpdateNewEncryptedMessage { message: { ChatId: var chat_id, RandomId: var random_id } } }
|
||||
&& chat_id == msg.ChatId && random_id == msg.RandomId))
|
||||
continue;
|
||||
update.message = msg;
|
||||
await RaiseUpdate(update);
|
||||
}
|
||||
}
|
||||
if (updatesCount != 0)
|
||||
{
|
||||
// try to remove matching pending OwnUpdates from this updates list (starting from most-recent)
|
||||
for (int p = _pending.Count - 1, u = updates.updates.Length; p >= 0 && u > 0; p--)
|
||||
{
|
||||
if (_pending[p].own == false) continue;
|
||||
var updateP = _pending[p].update;
|
||||
var (mbox_idP, ptsP, pts_countP) = updateP.GetMBox();
|
||||
if (ptsP == 0) (mbox_idP, ptsP, pts_countP) = _pending[p].updates.GetMBox();
|
||||
Type updatePtype = null;
|
||||
while (--u >= 0)
|
||||
{
|
||||
var update = updates.updates[u];
|
||||
if (update == null) continue;
|
||||
var (mbox_id, pts, pts_count) = update.GetMBox();
|
||||
if (pts == 0) (mbox_id, pts, pts_count) = updates.GetMBox();
|
||||
if (mbox_idP == mbox_id && ptsP <= pts)
|
||||
{
|
||||
updatePtype ??= updateP.GetType();
|
||||
if (updatePtype == (update is UpdateDeleteMessages ? typeof(UpdateAffectedMessages) : update.GetType()))
|
||||
{
|
||||
updates.updates[u] = null;
|
||||
--updatesCount;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (updatesCount != 0)
|
||||
await HandleUpdates(updates, false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (state != null)
|
||||
{
|
||||
_local[L_PTS].pts = state.pts;
|
||||
_local[L_QTS].pts = state.qts;
|
||||
var local_seq = _local[L_SEQ];
|
||||
local_seq.pts = state.seq;
|
||||
local_seq.access_hash = state.date.Ticks;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RaiseCollect(Updates_DifferenceBase diff)
|
||||
{
|
||||
if (diff is Updates_DifferenceSlice uds)
|
||||
RaiseCollect(uds.users, uds.chats);
|
||||
else if (diff is Updates_Difference ud)
|
||||
RaiseCollect(ud.users, ud.chats);
|
||||
}
|
||||
|
||||
private void RaiseCollect(Dictionary<long, User> users, Dictionary<long, ChatBase> chats)
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var chat in chats.Values)
|
||||
if (chat is Channel channel && !channel.flags.HasFlag(Channel.Flags.min))
|
||||
if (_local.TryGetValue(channel.id, out var local))
|
||||
local.access_hash = channel.access_hash;
|
||||
_collector.Collect(users.Values);
|
||||
_collector.Collect(chats.Values);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log?.Invoke(4, $"Collect({users?.Count},{chats?.Count}) raised {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RaiseUpdate(Update update)
|
||||
{
|
||||
try
|
||||
{
|
||||
var task = _onUpdate(update);
|
||||
if (!_reentrant) await task;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log?.Invoke(4, $"onUpdate({update?.GetType().Name}) raised {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
private static string ExtendedLog(Update update) => update switch
|
||||
{
|
||||
UpdateNewMessage unm => $"| msgID={unm.message.ID}",
|
||||
UpdateEditMessage uem => $"| msgID={uem.message.ID}",
|
||||
UpdateDeleteMessages udm => $"| count={udm.messages.Length}",
|
||||
_ => null
|
||||
};
|
||||
|
||||
/// <summary>Load latest dialogs states, checking for missing updates</summary>
|
||||
/// <param name="dialogs">structure returned by Messages_Get*Dialogs calls</param>
|
||||
/// <param name="fullLoadNewChans">Dangerous! Load full history of unknown new channels as updates</param>
|
||||
public async Task LoadDialogs(Messages_Dialogs dialogs, bool fullLoadNewChans = false)
|
||||
{
|
||||
await _sem.WaitAsync();
|
||||
try
|
||||
{
|
||||
foreach (var dialog in dialogs.dialogs.OfType<Dialog>())
|
||||
{
|
||||
if (dialog.peer is not PeerChannel pc) continue;
|
||||
var local = _local.GetOrCreate(pc.channel_id);
|
||||
if (dialogs.chats.TryGetValue(pc.channel_id, out var chat) && chat is Channel channel)
|
||||
local.access_hash = channel.access_hash;
|
||||
if (local.pts is 0)
|
||||
if (fullLoadNewChans) local.pts = 1;
|
||||
else local.pts = dialog.pts;
|
||||
if (local.pts < dialog.pts)
|
||||
{
|
||||
Log?.Invoke(1, $"LoadDialogs {pc.channel_id} has {local.pts} < {dialog.pts} ({dialog.folder_id})");
|
||||
await GetDifference(pc.channel_id, dialog.pts, local);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally { _sem.Release(); }
|
||||
}
|
||||
|
||||
/// <summary>Save the current state of the manager to JSON file</summary>
|
||||
/// <param name="statePath">File path to write</param>
|
||||
/// <remarks>Note: This does not save the content of collected Users/Chats dictionaries</remarks>
|
||||
public void SaveState(string statePath)
|
||||
=> System.IO.File.WriteAllText(statePath, System.Text.Json.JsonSerializer.Serialize(State, Helpers.JsonOptions));
|
||||
public static Dictionary<long, MBoxState> LoadState(string statePath) => !System.IO.File.Exists(statePath) ? null
|
||||
: System.Text.Json.JsonSerializer.Deserialize<Dictionary<long, MBoxState>>(System.IO.File.ReadAllText(statePath), Helpers.JsonOptions);
|
||||
/// <summary>returns a <see cref="User"/> or <see cref="ChatBase"/> for the given Peer <i>(only if using the default collector)</i></summary>
|
||||
public IPeerInfo UserOrChat(Peer peer) => peer?.UserOrChat(Users, Chats);
|
||||
}
|
||||
|
||||
public interface IPeerCollector
|
||||
{
|
||||
void Collect(IEnumerable<User> users);
|
||||
void Collect(IEnumerable<ChatBase> chats);
|
||||
bool HasUser(long id);
|
||||
bool HasChat(long id);
|
||||
}
|
||||
}
|
||||
|
||||
namespace TL
|
||||
{
|
||||
using WTelegram;
|
||||
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public static class UpdateManagerExtensions
|
||||
{
|
||||
/// <summary>Manager ensuring that you receive Telegram updates in correct order, without missing any</summary>
|
||||
/// <param name="onUpdate">Event to be called on sequential individual update</param>
|
||||
/// <param name="statePath">Resume session by recovering all updates that occured since the state saved in this file</param>
|
||||
/// <param name="collector">Custom users/chats collector. By default, those are collected in properties Users/Chats</param>
|
||||
/// <param name="reentrant"><see langword="true"/> if your <paramref name="onUpdate"/> method can be called again even when last async call didn't return yet</param>
|
||||
public static UpdateManager WithUpdateManager(this Client client, Func<TL.Update, Task> onUpdate, string statePath, IPeerCollector collector = null, bool reentrant = false)
|
||||
=> new(client, onUpdate, UpdateManager.LoadState(statePath), collector, reentrant);
|
||||
|
||||
/// <summary>Manager ensuring that you receive Telegram updates in correct order, without missing any</summary>
|
||||
/// <param name="onUpdate">Event to be called on sequential individual update</param>
|
||||
/// <param name="state">(optional) Resume session by recovering all updates that occured since this state</param>
|
||||
/// <param name="collector">Custom users/chats collector. By default, those are collected in properties Users/Chats</param>
|
||||
/// <param name="reentrant"><see langword="true"/> if your <paramref name="onUpdate"/> method can be called again even when last async call didn't return yet</param>
|
||||
public static UpdateManager WithUpdateManager(this Client client, Func<TL.Update, Task> onUpdate, IDictionary<long, UpdateManager.MBoxState> state = null, IPeerCollector collector = null, bool reentrant = false)
|
||||
=> new(client, onUpdate, state, collector, reentrant);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
<PropertyGroup>
|
||||
<OutputType>Library</OutputType>
|
||||
<TargetFrameworks>netstandard2.0;net5.0</TargetFrameworks>
|
||||
<TargetFrameworks>netstandard2.0;net5.0;net8.0</TargetFrameworks>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<RootNamespace>WTelegram</RootNamespace>
|
||||
<Deterministic>true</Deterministic>
|
||||
|
|
@ -11,31 +11,31 @@
|
|||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<PackageId>WTelegramClient</PackageId>
|
||||
<Version>0.0.0</Version>
|
||||
<Authors>Wizou</Authors>
|
||||
<Description>Telegram Client API (MTProto) library written 100% in C# and .NET Standard | Latest API layer: 152 Release Notes: $(ReleaseNotes.Replace("|", "%0D%0A").Replace(" - ","%0D%0A- ").Replace(" ", "%0D%0A%0D%0A"))</Description>
|
||||
<Copyright>Copyright © Olivier Marcoux 2021-2023</Copyright>
|
||||
<VersionPrefix>0.0.0</VersionPrefix>
|
||||
<VersionSuffix>layer.220</VersionSuffix>
|
||||
<Description>Telegram Client API (MTProto) library written 100% in C# and .NET Standard | Latest API layer: 220
|
||||
|
||||
Release Notes:
|
||||
$(ReleaseNotes)</Description>
|
||||
<Copyright>Copyright © Olivier Marcoux 2021-2025</Copyright>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<PackageProjectUrl>https://github.com/wiz0u/WTelegramClient</PackageProjectUrl>
|
||||
<PackageProjectUrl>https://wiz0u.github.io/WTelegramClient</PackageProjectUrl>
|
||||
<PackageIcon>logo.png</PackageIcon>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<RepositoryUrl>https://github.com/wiz0u/WTelegramClient.git</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PackageTags>Telegram;MTProto;Client;Api;UserBot;TLSharp</PackageTags>
|
||||
<PackageTags>Telegram;MTProto;Client;Api;UserBot</PackageTags>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageReleaseNotes>$(ReleaseNotes.Replace("|", "%0D%0A").Replace(" - ","%0D%0A- ").Replace(" ", "%0D%0A%0D%0A"))</PackageReleaseNotes>
|
||||
<NoWarn>IDE0079;0419;1573;1591;NETSDK1138</NoWarn>
|
||||
<DefineConstants>TRACE;OBFUSCATION</DefineConstants>
|
||||
<PackageReleaseNotes>$(ReleaseNotes)</PackageReleaseNotes>
|
||||
<NoWarn>NETSDK1138;CS0419;CS1573;CS1591</NoWarn>
|
||||
<DefineConstants>TRACE;OBFUSCATION;MTPG</DefineConstants>
|
||||
<!--<IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">true</IsAotCompatible>-->
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove=".gitattributes" />
|
||||
<None Remove=".gitignore" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\.github\dev.yml" Link="Data\dev.yml" />
|
||||
<None Include="..\.github\release.yml" Link="Data\release.yml" />
|
||||
<None Include="..\.github\workflows\dev.yml" Link="Data\dev.yml" />
|
||||
<None Include="..\.github\workflows\release.yml" Link="Data\release.yml" />
|
||||
<None Include="..\EXAMPLES.md" Link="Data\EXAMPLES.md" />
|
||||
<None Include="..\FAQ.md" Link="Data\FAQ.md" />
|
||||
<None Include="..\README.md" Link="Data\README.md" Pack="true" PackagePath="\" />
|
||||
|
|
@ -46,11 +46,19 @@
|
|||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
|
||||
</ItemGroup>-->
|
||||
|
||||
|
||||
<ItemGroup Condition="$(DefineConstants.Contains('MTPG'))">
|
||||
<ProjectReference Include="..\generator\MTProtoGenerator.csproj" OutputItemType="Analyzer" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
|
||||
<PackageReference Include="IndexRange" Version="1.0.2" />
|
||||
<PackageReference Include="IndexRange" Version="1.0.3" />
|
||||
<PackageReference Include="System.Memory" Version="4.5.5" />
|
||||
<PackageReference Include="System.Text.Json" Version="6.0.5" />
|
||||
<PackageReference Include="System.Text.Json" Version="6.0.10" />
|
||||
<PackageReference Include="System.Collections.Immutable" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net5.0'">
|
||||
<PackageReference Include="System.Collections.Immutable" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
Loading…
Reference in a new issue