├── .github
├── nuget.config
└── workflows
│ ├── ci.yml
│ └── release.yml
├── .gitignore
├── .gitpod.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── Telegram.Bot.Extensions.Polling.sln
├── package-icon.png
├── src
└── Telegram.Bot.Extensions.Polling
│ ├── Abstractions
│ ├── IUpdateHandler.cs
│ ├── IUpdateReceiver.cs
│ └── ReceiverOptions.cs
│ ├── AsyncEnumerableReceivers
│ ├── BlockingUpdateReceiver.cs
│ └── QueuedUpdateReceiver.cs
│ ├── DefaultUpdateHandler.cs
│ ├── DefaultUpdateReceiver.cs
│ ├── Extensions
│ ├── TelegramBotClientExtensions.cs
│ └── TelegramBotClientPollingExtensions.cs
│ └── Telegram.Bot.Extensions.Polling.csproj
└── test
└── Telegram.Bot.Extensions.Polling.Tests
├── AsyncEnumerableReceivers
├── BlockingUpdateReceiverTests.cs
└── QueuedUpdateReceiverTests.cs
├── MockClientTests.cs
├── MockTelegramBotClient.cs
├── ReceiveAsyncTests.cs
└── Telegram.Bot.Extensions.Polling.Tests.csproj
/.github/nuget.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on:
4 | push:
5 | branches-ignore: [ master ]
6 |
7 | env:
8 | USE_CI_FEED: true
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v2
15 | - name: Setup .NET
16 | uses: actions/setup-dotnet@v1
17 | with:
18 | dotnet-version: 6.0.x
19 | - name: Copy nuget.config with development feed from Azure
20 | if: ${{ env.USE_CI_FEED == 'true' }}
21 | shell: pwsh
22 | run: Copy-Item -Path .github/nuget.config
23 | - name: Restore dependencies
24 | run: dotnet restore
25 | - name: Build
26 | run: dotnet build --no-restore
27 | - name: Test
28 | run: dotnet test --no-build --verbosity normal
29 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 |
7 | env:
8 | USE_CI_FEED: false
9 | VERSION: 2.0.0-alpha.1
10 | IS_PRERELEASE: false
11 | PROJECT_PATH: src/Telegram.Bot.Extensions.Polling/Telegram.Bot.Extensions.Polling.csproj
12 | CONFIGURATION: Release
13 |
14 | jobs:
15 | build:
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: actions/checkout@v2
19 | - name: Setup .NET
20 | uses: actions/setup-dotnet@v1
21 | with:
22 | dotnet-version: 6.0.x
23 | - name: Restore dependencies
24 | run: dotnet restore
25 | - name: Build
26 | run: >
27 | dotnet build
28 | --no-restore
29 | --configuration ${{ env.CONFIGURATION }}
30 | -p:Version=${{ env.VERSION }}
31 | ${{ env.PROJECT_PATH }}
32 | - name: Pack
33 | run: >
34 | dotnet pack
35 | --no-build
36 | --output artifacts
37 | --configuration ${{ env.CONFIGURATION }}
38 | -p:Version=${{ env.VERSION }}
39 | ${{ env.PROJECT_PATH }}
40 | - name: Publish package to NuGet
41 | run: >
42 | dotnet nuget push
43 | artifacts/*.nupkg
44 | --api-key ${{ secrets.NUGET_API_KEY }}
45 | --skip-duplicate
46 | --source https://api.nuget.org/v3/index.json
47 | - name: Create Release
48 | uses: softprops/action-gh-release@v1
49 | with:
50 | files: artifacts/*.nupkg
51 | tag_name: v${{ env.VERSION }}
52 | prerelease: ${{ env.IS_PRERELEASE }}
53 | fail_on_unmatched_files: true
54 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vs
2 | .vscode
3 | bin
4 | obj
5 | .idea
6 | *.user
7 |
--------------------------------------------------------------------------------
/.gitpod.yml:
--------------------------------------------------------------------------------
1 | image: gitpod/workspace-dotnet
2 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/)
6 | and this project adheres to [Semantic Versioning](https://semver.org/).
7 |
8 |
21 |
22 |
23 |
24 | ## [2.0.0] - Unreleases
25 |
26 | ### Changed
27 | - Dependency on package `JetBrains.Annotations` is made private
28 | - `Telegram.Bot` version upgraded to `18.0.0`
29 |
30 | ## [1.0.1] - 2021-12-19
31 |
32 | ### Fixed
33 |
34 | - Fixed timeout for throwing out pending updates on start
35 |
36 | ## [1.0.0] - 2021-11-17
37 |
38 | ### Changed
39 | - All method arguments that accept `ReceiverOptions` are renamed from `receiveOptions` to `receiverOptions`
40 |
41 | ## [v1.0.0-alpha.1] - 2021-07-20
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Telegram Bots
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ⚠️ This package is no longer maintained since all it's functionality has been merged into [Telegram.Bot](https://github.com/TelegramBots/Telegram.Bot) starting from v18.
2 |
3 | ---
4 |
5 | # Telegram.Bot.Extensions.Polling
6 |
7 | Provides `ITelegramBotClient` extensions for polling updates.
8 |
9 | ## Usage
10 |
11 | ```csharp
12 | using System;
13 | using System.Threading;
14 | using Telegram.Bot;
15 | using Telegram.Bot.Exceptions;
16 | using Telegram.Bot.Types;
17 | using Telegram.Bot.Extensions.Polling;
18 |
19 | async Task HandleUpdateAsync(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken)
20 | {
21 | if (update.Message is Message message)
22 | {
23 | await botClient.SendTextMessageAsync(message.Chat, "Hello");
24 | }
25 | }
26 |
27 | async Task HandleErrorAsync(ITelegramBotClient botClient, Exception exception, CancellationToken cancellationToken)
28 | {
29 | if (exception is ApiRequestException apiRequestException)
30 | {
31 | await botClient.SendTextMessageAsync(123, apiRequestException.ToString());
32 | }
33 | }
34 |
35 | ITelegramBotClient bot = new TelegramBotClient("");
36 | ```
37 |
38 | You have two ways of starting to receive updates
39 | 1. `StartReceiving` does not block the caller thread. Receiving is done on the ThreadPool.
40 |
41 | ```c#
42 | using System.Threading;
43 | using Telegram.Bot.Extensions.Polling;
44 |
45 | var cts = new CancellationTokenSource();
46 | var cancellationToken = cts.Token;
47 | var receiverOptions = new ReceiverOptions
48 | {
49 | AllowedUpdates = {} // receive all update types
50 | };
51 | bot.StartReceiving(
52 | HandleUpdateAsync,
53 | HandleErrorAsync,
54 | receiverOptions,
55 | cancellationToken
56 | );
57 | ```
58 |
59 | 2. Awaiting `ReceiveAsync` will block until cancellation in triggered (both methods accept a CancellationToken)
60 |
61 | ```c#
62 | using System.Threading;
63 | using Telegram.Bot.Extensions.Polling;
64 |
65 | var cts = new CancellationTokenSource();
66 | var cancellationToken = cts.Token;
67 |
68 | var receiverOptions = new ReceiverOptions
69 | {
70 | AllowedUpdates = {} // receive all update types
71 | };
72 |
73 | try
74 | {
75 | await bot.ReceiveAsync(
76 | HandleUpdateAsync,
77 | HandleErrorAsync,
78 | receiverOptions,
79 | cancellationToken
80 | );
81 | }
82 | catch (OperationCancelledException exception)
83 | {
84 | }
85 | ```
86 |
87 | Trigger cancellation by calling `cts.Cancel()` somewhere to stop receiving update in both methods.
88 |
89 | ---
90 |
91 | In case you want to throw out all pending updates on start there is an option
92 | `ReceiveOptions.ThrowPendingUpdates`.
93 | If set to `true` `ReceiveOptions.Offset` property will be ignored even if it's set to non-null value
94 | and all implemented update receivers will attempt to throw out all pending updates before starting
95 | to call your handlers. In that case `ReceiveOptions.AllowedUpdates` property should be set to
96 | desired values otherwise it will be effectively set to allow all updates.
97 |
98 | Example
99 |
100 | ```csharp
101 | using System.Threading;
102 | using Telegram.Bot.Extensions.Polling;
103 | using Telegram.Bot.Types.Enums;
104 |
105 | var cts = new CancellationTokenSource();
106 | var cancellationToken = cts.Token;
107 |
108 | var receiverOptions = new ReceiverOptions
109 | {
110 | AllowedUpdates = new { UpdateType.Message, UpdateType.CallbackQuery }
111 | ThrowPendingUpdates = true
112 | };
113 |
114 | try
115 | {
116 | await bot.ReceiveAsync(
117 | HandleUpdateAsync,
118 | HandleErrorAsync,
119 | receiverOptions,
120 | cancellationToken
121 | );
122 | }
123 | catch (OperationCancelledException exception)
124 | {
125 | }
126 | ```
127 |
128 | ## Update streams
129 |
130 | With .Net Core 3.1+ comes support for an `IAsyncEnumerable` to stream Updates as they are received.
131 |
132 | There are two implementations:
133 | - `BlockingUpdateReceiver` blocks execution on every new `getUpdates` request
134 | - `QueuedUpdateReceiver` enqueues updates in an internal queue in a background process to make `Update` interation faster so you don't have to wait on `getUpdates` requests to finish
135 |
136 | Example:
137 |
138 | ```csharp
139 | using Telegram.Bot;
140 | using Telegram.Bot.Types;
141 | using Telegram.Bot.Extensions.Polling;
142 |
143 | var bot = new TelegramBotClient("");
144 | var receiverOptions = new ReceiverOptions
145 | {
146 | AllowedUpdates = {} // receive all update types
147 | };
148 | var updateReceiver = new QueuedUpdateReceiver(bot, receiverOptions);
149 |
150 | // to cancel
151 | var cts = new CancellationTokenSource();
152 |
153 | try
154 | {
155 | await foreach (Update update in updateReceiver.WithCancellation(cts.Token))
156 | {
157 | if (update.Message is Message message)
158 | {
159 | await bot.SendTextMessageAsync(
160 | message.Chat,
161 | $"Still have to process {updateReceiver.PendingUpdates} updates"
162 | );
163 | }
164 | }
165 | }
166 | catch (OperationCanceledException exception)
167 | {
168 | }
169 | ```
170 |
171 |
--------------------------------------------------------------------------------
/Telegram.Bot.Extensions.Polling.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.28606.126
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Telegram.Bot.Extensions.Polling", "src\Telegram.Bot.Extensions.Polling\Telegram.Bot.Extensions.Polling.csproj", "{437A7244-12E6-45F8-A230-9E1241DFA857}"
7 | EndProject
8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{338ECE13-AF3D-4C77-A3B9-AF0E7C7298C8}"
9 | EndProject
10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C49D6AD4-FC8B-4F2D-AEF8-A77A09120A01}"
11 | ProjectSection(SolutionItems) = preProject
12 | .gitignore = .gitignore
13 | LICENSE = LICENSE
14 | README.md = README.md
15 | CHANGELOG.md = CHANGELOG.md
16 | EndProjectSection
17 | EndProject
18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{D9549736-D112-4D83-A116-0AEF455A82F8}"
19 | EndProject
20 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Telegram.Bot.Extensions.Polling.Tests", "test\Telegram.Bot.Extensions.Polling.Tests\Telegram.Bot.Extensions.Polling.Tests.csproj", "{16570A8F-7E19-41EA-AEE1-9FD378585169}"
21 | EndProject
22 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{055FB192-3BB9-4FB9-B5C3-1ED8370CEFE9}"
23 | ProjectSection(SolutionItems) = preProject
24 | .github\nuget.config = .github\nuget.config
25 | EndProjectSection
26 | EndProject
27 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{AA1E8187-E2AB-4D11-8517-95B3D67821F1}"
28 | ProjectSection(SolutionItems) = preProject
29 | .github\workflows\ci.yml = .github\workflows\ci.yml
30 | .github\workflows\release.yml = .github\workflows\release.yml
31 | EndProjectSection
32 | EndProject
33 | Global
34 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
35 | Debug|Any CPU = Debug|Any CPU
36 | Release|Any CPU = Release|Any CPU
37 | EndGlobalSection
38 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
39 | {437A7244-12E6-45F8-A230-9E1241DFA857}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
40 | {437A7244-12E6-45F8-A230-9E1241DFA857}.Debug|Any CPU.Build.0 = Debug|Any CPU
41 | {437A7244-12E6-45F8-A230-9E1241DFA857}.Release|Any CPU.ActiveCfg = Release|Any CPU
42 | {437A7244-12E6-45F8-A230-9E1241DFA857}.Release|Any CPU.Build.0 = Release|Any CPU
43 | {16570A8F-7E19-41EA-AEE1-9FD378585169}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
44 | {16570A8F-7E19-41EA-AEE1-9FD378585169}.Debug|Any CPU.Build.0 = Debug|Any CPU
45 | {16570A8F-7E19-41EA-AEE1-9FD378585169}.Release|Any CPU.ActiveCfg = Release|Any CPU
46 | {16570A8F-7E19-41EA-AEE1-9FD378585169}.Release|Any CPU.Build.0 = Release|Any CPU
47 | EndGlobalSection
48 | GlobalSection(SolutionProperties) = preSolution
49 | HideSolutionNode = FALSE
50 | EndGlobalSection
51 | GlobalSection(NestedProjects) = preSolution
52 | {437A7244-12E6-45F8-A230-9E1241DFA857} = {338ECE13-AF3D-4C77-A3B9-AF0E7C7298C8}
53 | {16570A8F-7E19-41EA-AEE1-9FD378585169} = {D9549736-D112-4D83-A116-0AEF455A82F8}
54 | {055FB192-3BB9-4FB9-B5C3-1ED8370CEFE9} = {C49D6AD4-FC8B-4F2D-AEF8-A77A09120A01}
55 | {AA1E8187-E2AB-4D11-8517-95B3D67821F1} = {055FB192-3BB9-4FB9-B5C3-1ED8370CEFE9}
56 | EndGlobalSection
57 | GlobalSection(ExtensibilityGlobals) = postSolution
58 | SolutionGuid = {90F2F608-0C73-4233-8D9F-8D375CC014D0}
59 | EndGlobalSection
60 | EndGlobal
61 |
--------------------------------------------------------------------------------
/package-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TelegramBots/Telegram.Bot.Extensions.Polling/1b5f5d282b15170441687d2eefa6286bc78e1c91/package-icon.png
--------------------------------------------------------------------------------
/src/Telegram.Bot.Extensions.Polling/Abstractions/IUpdateHandler.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 | using JetBrains.Annotations;
5 | using Telegram.Bot.Types;
6 |
7 | // ReSharper disable once CheckNamespace
8 | namespace Telegram.Bot.Extensions.Polling;
9 |
10 | ///
11 | /// Processes s and errors.
12 | /// See for a simple implementation
13 | ///
14 | [PublicAPI]
15 | public interface IUpdateHandler
16 | {
17 | ///
18 | /// Handles an
19 | ///
20 | ///
21 | /// The instance of the bot receiving the
22 | ///
23 | /// The to handle
24 | ///
25 | /// The which will notify that method execution should be cancelled
26 | ///
27 | ///
28 | Task HandleUpdateAsync(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken);
29 |
30 | ///
31 | /// Handles an
32 | ///
33 | ///
34 | /// The instance of the bot receiving the
35 | ///
36 | /// The to handle
37 | ///
38 | /// The which will notify that method execution should be cancelled
39 | ///
40 | ///
41 | Task HandleErrorAsync(ITelegramBotClient botClient, Exception exception, CancellationToken cancellationToken);
42 | }
43 |
--------------------------------------------------------------------------------
/src/Telegram.Bot.Extensions.Polling/Abstractions/IUpdateReceiver.cs:
--------------------------------------------------------------------------------
1 | using System.Threading;
2 | using System.Threading.Tasks;
3 | using JetBrains.Annotations;
4 | using Telegram.Bot.Types;
5 |
6 | // ReSharper disable once CheckNamespace
7 | namespace Telegram.Bot.Extensions.Polling;
8 |
9 | ///
10 | /// Requests new s and processes them using provided instance
11 | ///
12 | [PublicAPI]
13 | public interface IUpdateReceiver
14 | {
15 | ///
16 | /// Starts receiving s invoking
17 | /// for each .
18 | /// This method will block if awaited.
19 | ///
20 | ///
21 | /// The used for processing s
22 | ///
23 | ///
24 | /// The with which you can stop receiving
25 | ///
26 | ///
27 | /// A that will be completed when cancellation will be requested through
28 | ///
29 | ///
30 | Task ReceiveAsync(
31 | IUpdateHandler updateHandler,
32 | CancellationToken cancellationToken = default
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/src/Telegram.Bot.Extensions.Polling/Abstractions/ReceiverOptions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using JetBrains.Annotations;
3 | using Telegram.Bot.Types;
4 | using Telegram.Bot.Types.Enums;
5 |
6 | // ReSharper disable once CheckNamespace
7 | namespace Telegram.Bot.Extensions.Polling;
8 |
9 | ///
10 | /// Options to configure getUpdates requests
11 | ///
12 | [PublicAPI]
13 | public sealed class ReceiverOptions
14 | {
15 | int? _limit;
16 |
17 | ///
18 | /// Identifier of the first update to be returned. Will be ignored if
19 | /// is set to true.
20 | ///
21 | public int? Offset { get; set; }
22 |
23 | ///
24 | /// Indicates which s are allowed to be received.
25 | /// In case of null the previous setting will be used
26 | ///
27 | public UpdateType[]? AllowedUpdates { get; set; }
28 |
29 | ///
30 | /// Limits the number of updates to be retrieved. Values between 1-100 are accepted.
31 | /// Defaults to 100 when is set to null.
32 | ///
33 | ///
34 | /// Thrown when the value doesn't satisfies constraints
35 | ///
36 | public int? Limit
37 | {
38 | get => _limit;
39 | set
40 | {
41 | if (value is < 1 or > 100)
42 | {
43 | throw new ArgumentOutOfRangeException(
44 | paramName: nameof(value),
45 | actualValue: value,
46 | message: $"'{nameof(Limit)}' can not be less than 1 or greater than 100"
47 | );
48 | }
49 | _limit = value;
50 | }
51 | }
52 |
53 | ///
54 | /// Indicates if all pending s should be thrown out before start
55 | /// polling. If set to true should be set to not
56 | /// null, otherwise will effectively be set to
57 | /// receive all s.
58 | ///
59 | public bool ThrowPendingUpdates { get; set; }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Telegram.Bot.Extensions.Polling/AsyncEnumerableReceivers/BlockingUpdateReceiver.cs:
--------------------------------------------------------------------------------
1 | #if NETCOREAPP3_1_OR_GREATER
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 | using JetBrains.Annotations;
7 | using Telegram.Bot.Extensions.Polling.Extensions;
8 | using Telegram.Bot.Requests;
9 | using Telegram.Bot.Types;
10 | using Telegram.Bot.Types.Enums;
11 |
12 | // ReSharper disable once CheckNamespace
13 | namespace Telegram.Bot.Extensions.Polling;
14 | ///
15 | /// Supports asynchronous iteration over s
16 | ///
17 | [PublicAPI]
18 | public class BlockingUpdateReceiver : IAsyncEnumerable
19 | {
20 | readonly ReceiverOptions? _receiverOptions;
21 | readonly ITelegramBotClient _botClient;
22 | readonly Func? _errorHandler;
23 |
24 | int _inProcess;
25 |
26 | ///
27 | /// Constructs a new for the specified
28 | ///
29 | /// The used for making GetUpdates calls
30 | ///
31 | ///
32 | /// The function used to handle s thrown by ReceiveUpdates
33 | ///
34 | public BlockingUpdateReceiver(
35 | ITelegramBotClient botClient,
36 | ReceiverOptions? receiverOptions = default,
37 | Func? errorHandler = default)
38 | {
39 | _botClient = botClient ?? throw new ArgumentNullException(nameof(botClient));
40 | _receiverOptions = receiverOptions;
41 | _errorHandler = errorHandler;
42 | }
43 |
44 | ///
45 | /// Gets the . This method may only be called once.
46 | ///
47 | ///
48 | /// The with which you can stop receiving
49 | ///
50 | public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default)
51 | {
52 | if (Interlocked.CompareExchange(ref _inProcess, 1, 0) == 1)
53 | {
54 | throw new InvalidOperationException(nameof(GetAsyncEnumerator) + " may only be called once");
55 | }
56 |
57 | return new Enumerator(receiver: this, cancellationToken: cancellationToken);
58 | }
59 |
60 | class Enumerator : IAsyncEnumerator
61 | {
62 | readonly BlockingUpdateReceiver _receiver;
63 | readonly CancellationTokenSource _cts;
64 | readonly CancellationToken _token;
65 | readonly UpdateType[]? _allowedUpdates;
66 | readonly int? _limit;
67 |
68 | Update[] _updateArray = Array.Empty();
69 | int _updateIndex;
70 | int _messageOffset;
71 | bool _updatesThrown;
72 |
73 | public Enumerator(BlockingUpdateReceiver receiver, CancellationToken cancellationToken)
74 | {
75 | _receiver = receiver;
76 | _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, default);
77 | _token = _cts.Token;
78 | _messageOffset = receiver._receiverOptions?.Offset ?? 0;
79 | _limit = receiver._receiverOptions?.Limit ?? default;
80 | _allowedUpdates = receiver._receiverOptions?.AllowedUpdates;
81 | }
82 |
83 | public ValueTask MoveNextAsync()
84 | {
85 | _token.ThrowIfCancellationRequested();
86 |
87 | _updateIndex += 1;
88 |
89 | return _updateIndex < _updateArray.Length
90 | ? new(true)
91 | : new(ReceiveUpdatesAsync());
92 | }
93 |
94 | async Task ReceiveUpdatesAsync()
95 | {
96 | var shouldThrowPendingUpdates = (
97 | _updatesThrown,
98 | _receiver._receiverOptions?.ThrowPendingUpdates ?? false
99 | );
100 |
101 | if (shouldThrowPendingUpdates is (false, true))
102 | {
103 | try
104 | {
105 | _messageOffset = await _receiver._botClient.ThrowOutPendingUpdatesAsync(
106 | cancellationToken: _token
107 | );
108 | }
109 | catch (OperationCanceledException)
110 | {
111 | // ignored
112 | }
113 | finally
114 | {
115 | _updatesThrown = true;
116 | }
117 | }
118 |
119 | _updateArray = Array.Empty();
120 | _updateIndex = 0;
121 |
122 | while (_updateArray.Length == 0)
123 | {
124 | try
125 | {
126 | _updateArray = await _receiver._botClient
127 | .MakeRequestAsync(
128 | request: new GetUpdatesRequest
129 | {
130 | Offset = _messageOffset,
131 | Timeout = (int) _receiver._botClient.Timeout.TotalSeconds,
132 | Limit = _limit,
133 | AllowedUpdates = _allowedUpdates,
134 | },
135 | cancellationToken: _token
136 | )
137 | .ConfigureAwait(false);
138 | }
139 | catch (OperationCanceledException)
140 | {
141 | throw;
142 | }
143 | catch (Exception ex) when (_receiver._errorHandler is not null)
144 | {
145 | await _receiver._errorHandler(ex, _token).ConfigureAwait(false);
146 | }
147 | }
148 |
149 | _messageOffset = _updateArray[^1].Id + 1;
150 | return true;
151 | }
152 |
153 | public Update Current => _updateArray[_updateIndex];
154 |
155 | public ValueTask DisposeAsync()
156 | {
157 | _cts.Cancel();
158 | _cts.Dispose();
159 | return new();
160 | }
161 | }
162 | }
163 | #endif
164 |
--------------------------------------------------------------------------------
/src/Telegram.Bot.Extensions.Polling/AsyncEnumerableReceivers/QueuedUpdateReceiver.cs:
--------------------------------------------------------------------------------
1 | #if NETCOREAPP3_1_OR_GREATER
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Diagnostics;
5 | using System.Threading;
6 | using System.Threading.Channels;
7 | using System.Threading.Tasks;
8 | using JetBrains.Annotations;
9 | using Telegram.Bot.Extensions.Polling.Extensions;
10 | using Telegram.Bot.Requests;
11 | using Telegram.Bot.Types;
12 | using Telegram.Bot.Types.Enums;
13 |
14 | // ReSharper disable once CheckNamespace
15 | namespace Telegram.Bot.Extensions.Polling;
16 |
17 | ///
18 | /// Supports asynchronous iteration over s.
19 | /// Updates are received on a different thread and enqueued.
20 | ///
21 | [PublicAPI]
22 | public class QueuedUpdateReceiver : IAsyncEnumerable
23 | {
24 | readonly ITelegramBotClient _botClient;
25 | readonly ReceiverOptions? _receiverOptions;
26 | readonly Func? _errorHandler;
27 |
28 | int _inProcess;
29 | Enumerator? _enumerator;
30 |
31 | ///
32 | /// Constructs a new for the specified
33 | ///
34 | /// The used for making GetUpdates calls
35 | ///
36 | ///
37 | /// The function used to handle s thrown by GetUpdates requests
38 | ///
39 | public QueuedUpdateReceiver(
40 | ITelegramBotClient botClient,
41 | ReceiverOptions? receiverOptions = default,
42 | Func? errorHandler = default)
43 | {
44 | _botClient = botClient ?? throw new ArgumentNullException(nameof(botClient));
45 | _receiverOptions = receiverOptions;
46 | _errorHandler = errorHandler;
47 | }
48 |
49 | ///
50 | /// Indicates how many s are ready to be returned the enumerator
51 | ///
52 | public int PendingUpdates => _enumerator?.PendingUpdates ?? 0;
53 |
54 | ///
55 | /// Gets the . This method may only be called once.
56 | ///
57 | ///
58 | /// The with which you can stop receiving
59 | ///
60 | public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default)
61 | {
62 | if (Interlocked.CompareExchange(ref _inProcess, 1, 0) == 1)
63 | {
64 | throw new InvalidOperationException(nameof(GetAsyncEnumerator) + " may only be called once");
65 | }
66 |
67 | _enumerator = new(receiver: this, cancellationToken: cancellationToken);
68 |
69 | return _enumerator;
70 | }
71 |
72 | class Enumerator : IAsyncEnumerator
73 | {
74 | readonly QueuedUpdateReceiver _receiver;
75 | readonly CancellationTokenSource _cts;
76 | readonly CancellationToken _token;
77 | readonly UpdateType[]? _allowedUpdates;
78 | readonly int? _limit;
79 |
80 | Exception? _uncaughtException;
81 |
82 | readonly Channel _channel;
83 | Update? _current;
84 |
85 | int _pendingUpdates;
86 | int _messageOffset;
87 |
88 | public int PendingUpdates => _pendingUpdates;
89 |
90 | public Enumerator(QueuedUpdateReceiver receiver, CancellationToken cancellationToken)
91 | {
92 | _receiver = receiver;
93 | _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, default);
94 | _token = _cts.Token;
95 | _messageOffset = receiver._receiverOptions?.Offset ?? 0;
96 | _limit = receiver._receiverOptions?.Limit ?? default;
97 | _allowedUpdates = receiver._receiverOptions?.AllowedUpdates;
98 |
99 | _channel = Channel.CreateUnbounded(
100 | new()
101 | {
102 | SingleReader = true,
103 | SingleWriter = true
104 | }
105 | );
106 |
107 | Task.Run(ReceiveUpdatesAsync);
108 | }
109 |
110 | public ValueTask MoveNextAsync()
111 | {
112 | if (_uncaughtException is not null) { throw _uncaughtException; }
113 |
114 | _token.ThrowIfCancellationRequested();
115 |
116 | if (_channel.Reader.TryRead(out _current))
117 | {
118 | Interlocked.Decrement(ref _pendingUpdates);
119 | return new(true);
120 | }
121 |
122 | return new(ReadAsync());
123 | }
124 |
125 | async Task ReadAsync()
126 | {
127 | _current = await _channel.Reader.ReadAsync(_token);
128 | Interlocked.Decrement(ref _pendingUpdates);
129 | return true;
130 | }
131 |
132 | async Task ReceiveUpdatesAsync()
133 | {
134 | if (_receiver._receiverOptions?.ThrowPendingUpdates is true)
135 | {
136 | try
137 | {
138 | _messageOffset = await _receiver._botClient.ThrowOutPendingUpdatesAsync(
139 | cancellationToken: _token
140 | );
141 | }
142 | catch (OperationCanceledException)
143 | {
144 | // ignored
145 | }
146 | }
147 |
148 | while (!_cts.IsCancellationRequested)
149 | {
150 | try
151 | {
152 | Update[] updateArray = await _receiver._botClient
153 | .MakeRequestAsync(
154 | request: new GetUpdatesRequest
155 | {
156 | Offset = _messageOffset,
157 | Timeout = (int)_receiver._botClient.Timeout.TotalSeconds,
158 | AllowedUpdates = _allowedUpdates,
159 | Limit = _limit,
160 | },
161 | cancellationToken: _token
162 | )
163 | .ConfigureAwait(false);
164 |
165 | if (updateArray.Length > 0)
166 | {
167 | _messageOffset = updateArray[^1].Id + 1;
168 |
169 | Interlocked.Add(ref _pendingUpdates, updateArray.Length);
170 |
171 | ChannelWriter writer = _channel.Writer;
172 | foreach (Update update in updateArray)
173 | {
174 | var success = writer.TryWrite(update);
175 | Debug.Assert(success, "TryWrite should succeed as we are using an unbounded channel");
176 | }
177 | }
178 | }
179 | catch (OperationCanceledException)
180 | {
181 | // Ignore
182 | }
183 | catch (Exception ex)
184 | {
185 | Debug.Assert(_uncaughtException is null);
186 |
187 | // If there is no errorHandler or the errorHandler throws, stop receiving
188 | if (_receiver._errorHandler is null)
189 | {
190 | _uncaughtException = ex;
191 | _cts.Cancel();
192 | }
193 | else
194 | {
195 | try
196 | {
197 | await _receiver._errorHandler(ex, _token).ConfigureAwait(false);
198 | }
199 | catch (Exception errorHandlerException)
200 | {
201 | _uncaughtException = new AggregateException(
202 | message: "Exception was not caught by the errorHandler.",
203 | ex,
204 | errorHandlerException
205 | );
206 | _cts.Cancel();
207 | }
208 | }
209 |
210 | if (_uncaughtException is not null)
211 | {
212 | _uncaughtException = new Exception(
213 | message: "Exception was not caught by the errorHandler.",
214 | innerException: _uncaughtException
215 | );
216 | }
217 | }
218 | }
219 | }
220 |
221 | public Update Current => _current!; // _current being null indicates MoveNextAsync was never called
222 |
223 | public ValueTask DisposeAsync()
224 | {
225 | _cts.Cancel();
226 | _cts.Dispose();
227 | return new();
228 | }
229 | }
230 | }
231 | #endif
232 |
--------------------------------------------------------------------------------
/src/Telegram.Bot.Extensions.Polling/DefaultUpdateHandler.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 | using JetBrains.Annotations;
5 | using Telegram.Bot.Types;
6 |
7 | namespace Telegram.Bot.Extensions.Polling;
8 |
9 | ///
10 | /// A very simple implementation
11 | ///
12 | [PublicAPI]
13 | public class DefaultUpdateHandler : IUpdateHandler
14 | {
15 | readonly Func _updateHandler;
16 | readonly Func _errorHandler;
17 |
18 | ///
19 | /// Constructs a new with the specified callback functions
20 | ///
21 | /// The function to invoke when an update is received
22 | /// The function to invoke when an error occurs
23 | public DefaultUpdateHandler(
24 | Func updateHandler,
25 | Func errorHandler)
26 | {
27 | _updateHandler = updateHandler ?? throw new ArgumentNullException(nameof(updateHandler));
28 | _errorHandler = errorHandler ?? throw new ArgumentNullException(nameof(errorHandler));
29 | }
30 |
31 | ///
32 | public async Task HandleUpdateAsync(
33 | ITelegramBotClient botClient,
34 | Update update,
35 | CancellationToken cancellationToken
36 | ) =>
37 | await _updateHandler(botClient, update, cancellationToken);
38 |
39 | ///
40 | public async Task HandleErrorAsync(
41 | ITelegramBotClient botClient,
42 | Exception exception,
43 | CancellationToken cancellationToken
44 | ) =>
45 | await _errorHandler(botClient, exception, cancellationToken);
46 | }
47 |
--------------------------------------------------------------------------------
/src/Telegram.Bot.Extensions.Polling/DefaultUpdateReceiver.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 | using JetBrains.Annotations;
5 | using Telegram.Bot.Extensions.Polling.Extensions;
6 | using Telegram.Bot.Requests;
7 | using Telegram.Bot.Types;
8 |
9 | namespace Telegram.Bot.Extensions.Polling;
10 |
11 | ///
12 | /// A simple > implementation that requests new updates and handles them sequentially
13 | ///
14 | [PublicAPI]
15 | public class DefaultUpdateReceiver : IUpdateReceiver
16 | {
17 | static readonly Update[] EmptyUpdates = Array.Empty();
18 |
19 | readonly ITelegramBotClient _botClient;
20 | readonly ReceiverOptions? _receiverOptions;
21 |
22 | ///
23 | /// Constructs a new with the specified >
24 | /// instance and optional
25 | ///
26 | /// The used for making GetUpdates calls
27 | /// Options used to configure getUpdates requests
28 | public DefaultUpdateReceiver(
29 | ITelegramBotClient botClient,
30 | ReceiverOptions? receiverOptions = default)
31 | {
32 | _botClient = botClient ?? throw new ArgumentNullException(nameof(botClient));
33 | _receiverOptions = receiverOptions;
34 | }
35 |
36 | ///
37 | public async Task ReceiveAsync(
38 | IUpdateHandler updateHandler,
39 | CancellationToken cancellationToken = default)
40 | {
41 | if (updateHandler is null) { throw new ArgumentNullException(nameof(updateHandler)); }
42 |
43 | var allowedUpdates = _receiverOptions?.AllowedUpdates;
44 | var limit = _receiverOptions?.Limit ?? default;
45 | var messageOffset = _receiverOptions?.Offset ?? 0;
46 | var emptyUpdates = EmptyUpdates;
47 |
48 | if (_receiverOptions?.ThrowPendingUpdates is true)
49 | {
50 | try
51 | {
52 | messageOffset = await _botClient.ThrowOutPendingUpdatesAsync(
53 | cancellationToken: cancellationToken
54 | );
55 | }
56 | catch (OperationCanceledException)
57 | {
58 | // ignored
59 | }
60 | }
61 |
62 | while (!cancellationToken.IsCancellationRequested)
63 | {
64 | var timeout = (int) _botClient.Timeout.TotalSeconds;
65 | var updates = emptyUpdates;
66 | try
67 | {
68 | var request = new GetUpdatesRequest
69 | {
70 | Limit = limit,
71 | Offset = messageOffset,
72 | Timeout = timeout,
73 | AllowedUpdates = allowedUpdates,
74 | };
75 | updates = await _botClient.MakeRequestAsync(
76 | request: request,
77 | cancellationToken:
78 | cancellationToken
79 | ).ConfigureAwait(false);
80 | }
81 | catch (OperationCanceledException)
82 | {
83 | // Ignore
84 | }
85 | catch (Exception exception)
86 | {
87 | try
88 | {
89 | await updateHandler.HandleErrorAsync(
90 | botClient: _botClient,
91 | exception: exception,
92 | cancellationToken: cancellationToken
93 | ).ConfigureAwait(false);
94 | }
95 | catch (OperationCanceledException)
96 | {
97 | // ignored
98 | }
99 | }
100 |
101 | foreach (var update in updates)
102 | {
103 | try
104 | {
105 | await updateHandler.HandleUpdateAsync(
106 | botClient: _botClient,
107 | update: update,
108 | cancellationToken: cancellationToken
109 | ).ConfigureAwait(false);
110 |
111 | messageOffset = update.Id + 1;
112 | }
113 | catch (OperationCanceledException)
114 | {
115 | // ignored
116 | }
117 | }
118 | }
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/Telegram.Bot.Extensions.Polling/Extensions/TelegramBotClientExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 | using Telegram.Bot.Requests;
5 | using Telegram.Bot.Types;
6 | using Telegram.Bot.Types.Enums;
7 |
8 | namespace Telegram.Bot.Extensions.Polling.Extensions;
9 |
10 | internal static class TelegramBotClientExtensions
11 | {
12 | ///
13 | /// Will attempt to throw the last update using offset set to -1.
14 | ///
15 | ///
16 | ///
17 | ///
18 | /// Update ID of the last increased by 1 if there were any
19 | ///
20 | internal static async Task ThrowOutPendingUpdatesAsync(
21 | this ITelegramBotClient botClient,
22 | CancellationToken cancellationToken = default)
23 | {
24 | var request = new GetUpdatesRequest
25 | {
26 | Limit = 1,
27 | Offset = -1,
28 | Timeout = 0,
29 | AllowedUpdates = Array.Empty(),
30 | };
31 | var updates = await botClient.MakeRequestAsync(request: request, cancellationToken: cancellationToken)
32 | .ConfigureAwait(false);
33 |
34 | #if NETCOREAPP3_1_OR_GREATER
35 | if (updates.Length > 0) { return updates[^1].Id + 1; }
36 | #else
37 | if (updates.Length > 0) { return updates[updates.Length - 1].Id + 1; }
38 | #endif
39 | return 0;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Telegram.Bot.Extensions.Polling/Extensions/TelegramBotClientPollingExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 | using Telegram.Bot.Extensions.Polling;
5 | using Telegram.Bot.Types;
6 | using JetBrains.Annotations;
7 |
8 | // ReSharper disable once CheckNamespace
9 | namespace Telegram.Bot;
10 |
11 | ///
12 | /// Provides extension methods for that allow for polling
13 | ///
14 | public static class TelegramBotClientPollingExtensions
15 | {
16 | ///
17 | /// Starts receiving s on the ThreadPool, invoking
18 | /// for each.
19 | ///
20 | /// This method does not block. GetUpdates will be called AFTER the
21 | /// returns
22 | ///
23 | ///
24 | ///
25 | /// The used for processing s
26 | ///
27 | /// The used for making GetUpdates calls
28 | /// Options used to configure getUpdates request
29 | ///
30 | /// The with which you can stop receiving
31 | ///
32 | [PublicAPI]
33 | public static void StartReceiving(
34 | this ITelegramBotClient botClient,
35 | ReceiverOptions? receiverOptions = default,
36 | CancellationToken cancellationToken = default
37 | ) where TUpdateHandler : IUpdateHandler, new() =>
38 | StartReceiving(
39 | botClient: botClient,
40 | updateHandler: new TUpdateHandler(),
41 | receiverOptions: receiverOptions,
42 | cancellationToken: cancellationToken
43 | );
44 |
45 | ///
46 | /// Starts receiving s on the ThreadPool, invoking
47 | /// for each.
48 | ///
49 | /// This method does not block. GetUpdates will be called AFTER the returns
50 | ///
51 | ///
52 | /// The used for making GetUpdates calls
53 | /// Delegate used for processing s
54 | /// Delegate used for processing polling errors
55 | /// Options used to configure getUpdates request
56 | ///
57 | /// The with which you can stop receiving
58 | ///
59 | [PublicAPI]
60 | public static void StartReceiving(
61 | this ITelegramBotClient botClient,
62 | Func updateHandler,
63 | Func errorHandler,
64 | ReceiverOptions? receiverOptions = default,
65 | CancellationToken cancellationToken = default
66 | ) =>
67 | StartReceiving(
68 | botClient: botClient,
69 | updateHandler: new DefaultUpdateHandler(
70 | updateHandler: updateHandler,
71 | errorHandler: errorHandler
72 | ),
73 | receiverOptions: receiverOptions,
74 | cancellationToken: cancellationToken
75 | );
76 |
77 | ///
78 | /// Starts receiving s on the ThreadPool, invoking
79 | /// for each.
80 | ///
81 | /// This method does not block. GetUpdates will be called AFTER the returns
82 | ///
83 | ///
84 | /// The used for making GetUpdates calls
85 | /// Delegate used for processing s
86 | /// Delegate used for processing polling errors
87 | /// Options used to configure getUpdates request
88 | ///
89 | /// The with which you can stop receiving
90 | ///
91 | [PublicAPI]
92 | public static void StartReceiving(
93 | this ITelegramBotClient botClient,
94 | Action updateHandler,
95 | Action errorHandler,
96 | ReceiverOptions? receiverOptions = default,
97 | CancellationToken cancellationToken = default
98 | ) =>
99 | StartReceiving(
100 | botClient: botClient,
101 | updateHandler: new DefaultUpdateHandler(
102 | updateHandler: (bot, update, token) =>
103 | {
104 | updateHandler.Invoke(bot, update, token);
105 | return Task.CompletedTask;
106 | },
107 | errorHandler: (bot, exception, token) =>
108 | {
109 | errorHandler.Invoke(bot, exception, token);
110 | return Task.CompletedTask;
111 | }
112 | ),
113 | receiverOptions: receiverOptions,
114 | cancellationToken: cancellationToken
115 | );
116 |
117 | ///
118 | /// Starts receiving s on the ThreadPool, invoking
119 | /// for each.
120 | ///
121 | /// This method does not block. GetUpdates will be called AFTER the
122 | /// returns
123 | ///
124 | ///
125 | /// The used for making GetUpdates calls
126 | ///
127 | /// The used for processing s
128 | ///
129 | /// Options used to configure getUpdates request
130 | ///
131 | /// The with which you can stop receiving
132 | ///
133 | [PublicAPI]
134 | public static void StartReceiving(
135 | this ITelegramBotClient botClient,
136 | IUpdateHandler updateHandler,
137 | ReceiverOptions? receiverOptions = default,
138 | CancellationToken cancellationToken = default)
139 | {
140 | if (botClient is null) { throw new ArgumentNullException(nameof(botClient)); }
141 | if (updateHandler is null) { throw new ArgumentNullException(nameof(updateHandler)); }
142 |
143 | // ReSharper disable once MethodSupportsCancellation
144 | Task.Run(async () =>
145 | {
146 | try
147 | {
148 | await ReceiveAsync(
149 | botClient: botClient,
150 | updateHandler: updateHandler,
151 | receiverOptions: receiverOptions,
152 | cancellationToken: cancellationToken
153 | );
154 | }
155 | catch (OperationCanceledException)
156 | {
157 | // ignored
158 | }
159 | catch (Exception ex)
160 | {
161 | try
162 | {
163 | await updateHandler.HandleErrorAsync(
164 | botClient: botClient,
165 | exception: ex,
166 | cancellationToken: cancellationToken
167 | );
168 | }
169 | catch (OperationCanceledException)
170 | {
171 | // ignored
172 | }
173 | }
174 | });
175 | }
176 |
177 | ///
178 | /// Starts receiving s on the ThreadPool, invoking
179 | /// for each.
180 | ///
181 | /// This method will block if awaited. GetUpdates will be called AFTER the
182 | /// returns
183 | ///
184 | ///
185 | ///
186 | /// The used for processing s
187 | ///
188 | /// The used for making GetUpdates calls
189 | /// Options used to configure getUpdates request
190 | ///
191 | /// The with which you can stop receiving
192 | ///
193 | ///
194 | /// A that will be completed when cancellation will be requested through
195 | ///
196 | ///
197 | [PublicAPI]
198 | public static async Task ReceiveAsync(
199 | this ITelegramBotClient botClient,
200 | ReceiverOptions? receiverOptions = default,
201 | CancellationToken cancellationToken = default
202 | ) where TUpdateHandler : IUpdateHandler, new() =>
203 | await ReceiveAsync(
204 | botClient: botClient,
205 | updateHandler: new TUpdateHandler(),
206 | receiverOptions: receiverOptions,
207 | cancellationToken: cancellationToken
208 | );
209 |
210 | ///
211 | /// Starts receiving s on the ThreadPool, invoking
212 | /// for each.
213 | ///
214 | /// This method will block if awaited. GetUpdates will be called AFTER the
215 | /// returns
216 | ///
217 | ///
218 | /// The used for making GetUpdates calls
219 | /// Delegate used for processing s
220 | /// Delegate used for processing polling errors
221 | /// Options used to configure getUpdates requests
222 | ///
223 | /// The with which you can stop receiving
224 | ///
225 | ///
226 | /// A that will be completed when cancellation will be requested through
227 | ///
228 | ///
229 | [PublicAPI]
230 | public static async Task ReceiveAsync(
231 | this ITelegramBotClient botClient,
232 | Func updateHandler,
233 | Func errorHandler,
234 | ReceiverOptions? receiverOptions = default,
235 | CancellationToken cancellationToken = default
236 | ) =>
237 | await ReceiveAsync(
238 | botClient: botClient,
239 | updateHandler: new DefaultUpdateHandler(
240 | updateHandler: updateHandler,
241 | errorHandler: errorHandler
242 | ),
243 | receiverOptions: receiverOptions,
244 | cancellationToken: cancellationToken
245 | );
246 |
247 | ///
248 | /// Starts receiving s on the ThreadPool, invoking
249 | /// for each.
250 | ///
251 | /// This method will block if awaited. GetUpdates will be called AFTER the
252 | /// returns
253 | ///
254 | ///
255 | /// The used for making GetUpdates calls
256 | /// Delegate used for processing s
257 | /// Delegate used for processing polling errors
258 | /// Options used to configure getUpdates requests
259 | ///
260 | /// The with which you can stop receiving
261 | ///
262 | ///
263 | /// A that will be completed when cancellation will be requested through
264 | ///
265 | ///
266 | [PublicAPI]
267 | public static async Task ReceiveAsync(
268 | this ITelegramBotClient botClient,
269 | Action updateHandler,
270 | Action errorHandler,
271 | ReceiverOptions? receiverOptions = default,
272 | CancellationToken cancellationToken = default
273 | ) =>
274 | await ReceiveAsync(
275 | botClient: botClient,
276 | updateHandler: new DefaultUpdateHandler(
277 | updateHandler: (bot, update, token) =>
278 | {
279 | updateHandler.Invoke(bot, update, token);
280 | return Task.CompletedTask;
281 | },
282 | errorHandler: (bot, exception, token) =>
283 | {
284 | errorHandler.Invoke(bot, exception, token);
285 | return Task.CompletedTask;
286 | }
287 | ),
288 | receiverOptions: receiverOptions,
289 | cancellationToken: cancellationToken
290 | );
291 |
292 | ///
293 | /// Starts receiving s on the ThreadPool, invoking
294 | /// for each.
295 | ///
296 | /// This method will block if awaited. GetUpdates will be called AFTER the
297 | /// returns
298 | ///
299 | ///
300 | /// The used for making GetUpdates calls
301 | ///
302 | /// The used for processing s
303 | ///
304 | /// Options used to configure getUpdates requests
305 | ///
306 | /// The with which you can stop receiving
307 | ///
308 | ///
309 | /// A that will be completed when cancellation will be requested through
310 | ///
311 | ///
312 | [PublicAPI]
313 | public static async Task ReceiveAsync(
314 | this ITelegramBotClient botClient,
315 | IUpdateHandler updateHandler,
316 | ReceiverOptions? receiverOptions = default,
317 | CancellationToken cancellationToken = default
318 | ) =>
319 | await new DefaultUpdateReceiver(botClient: botClient, receiverOptions: receiverOptions)
320 | .ReceiveAsync(updateHandler: updateHandler, cancellationToken: cancellationToken);
321 | }
322 |
--------------------------------------------------------------------------------
/src/Telegram.Bot.Extensions.Polling/Telegram.Bot.Extensions.Polling.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Library
5 | netstandard2.0;netcoreapp3.1
6 | MihaZupan,tuscen,TelegramBots
7 | Copyright © Telegram Bots 2022
8 | MIT
9 | https://github.com/TelegramBots/Telegram.Bot.Extensions.Polling
10 | https://github.com/TelegramBots/Telegram.Bot.Extensions.Polling.git
11 | Telegram;Bot;Api;Polling
12 | Provides ITelegramBotClient extensions for polling updates.
13 | package-icon.png
14 | TelegramBots
15 | 10
16 | enable
17 | enable
18 | true
19 | True
20 | True
21 | true
22 | snupkg
23 | true
24 |
25 | $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
26 |
27 |
28 |
29 |
30 |
31 |
32 | true
33 | true
34 |
35 |
36 |
37 |
38 | true
39 | /
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/test/Telegram.Bot.Extensions.Polling.Tests/AsyncEnumerableReceivers/BlockingUpdateReceiverTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 | using Telegram.Bot.Types;
5 | using Xunit;
6 |
7 | namespace Telegram.Bot.Extensions.Polling.Tests.AsyncEnumerableReceivers;
8 |
9 | public class BlockingUpdateReceiverTests
10 | {
11 | [Fact]
12 | public void CallingGetEnumeratorTwiceThrows()
13 | {
14 | var mockClient = new MockTelegramBotClient();
15 | var receiver = new BlockingUpdateReceiver(mockClient);
16 |
17 | _ = receiver.GetAsyncEnumerator();
18 |
19 | Assert.Throws(() => receiver.GetAsyncEnumerator());
20 | }
21 |
22 | [Fact]
23 | public async Task DoesntReceiveWhileProcessing()
24 | {
25 | var mockClient = new MockTelegramBotClient("foo", "bar");
26 | var receiver = new BlockingUpdateReceiver(mockClient);
27 |
28 | Assert.Equal(2, mockClient.MessageGroupsLeft);
29 |
30 | await foreach (Update update in receiver)
31 | {
32 | Assert.Equal("foo", update.Message!.Text);
33 | await Task.Delay(100);
34 | Assert.Equal(1, mockClient.MessageGroupsLeft);
35 | break;
36 | }
37 |
38 | Assert.Equal(1, mockClient.MessageGroupsLeft);
39 | }
40 |
41 | [Fact]
42 | public async Task ReceivesOnlyOnMoveNextAsync()
43 | {
44 | var mockClient = new MockTelegramBotClient("foo", "bar");
45 | var receiver = new BlockingUpdateReceiver(mockClient);
46 |
47 | Assert.Equal(2, mockClient.MessageGroupsLeft);
48 |
49 | await using var enumerator = receiver.GetAsyncEnumerator();
50 |
51 | Assert.Equal(2, mockClient.MessageGroupsLeft);
52 |
53 | Assert.True(await enumerator.MoveNextAsync());
54 | Assert.Equal("foo", enumerator.Current.Message!.Text);
55 |
56 | Assert.Equal(1, mockClient.MessageGroupsLeft);
57 |
58 | Assert.True(await enumerator.MoveNextAsync());
59 | Assert.Equal("bar", enumerator.Current.Message!.Text);
60 |
61 | Assert.Equal(0, mockClient.MessageGroupsLeft);
62 | }
63 |
64 | [Fact]
65 | public async Task ThrowsOnMoveNextIfCancelled()
66 | {
67 | var mockClient = new MockTelegramBotClient("foo", "bar");
68 | var receiver = new BlockingUpdateReceiver(mockClient);
69 |
70 | var cts = new CancellationTokenSource();
71 |
72 | await using var enumerator = receiver.GetAsyncEnumerator(cts.Token);
73 |
74 | Assert.True(await enumerator.MoveNextAsync());
75 | Assert.Equal("foo", enumerator.Current.Message!.Text);
76 |
77 | mockClient.Options.RequestDelay = 1000;
78 | cts.CancelAfter(200);
79 |
80 | await Assert.ThrowsAnyAsync(async () => await enumerator.MoveNextAsync());
81 | }
82 |
83 | [Fact]
84 | public async Task MoveNextThrowsIfEnumeratorIsDisposed()
85 | {
86 | var mockClient = new MockTelegramBotClient("foo");
87 | var receiver = new BlockingUpdateReceiver(mockClient);
88 |
89 | var enumerator = receiver.GetAsyncEnumerator();
90 | await enumerator.MoveNextAsync();
91 |
92 | await enumerator.DisposeAsync();
93 |
94 | await Assert.ThrowsAnyAsync(
95 | async () => await enumerator.MoveNextAsync()
96 | );
97 | }
98 |
99 | [Fact]
100 | public async Task ExceptionIsCaughtByErrorHandler()
101 | {
102 | var mockClient = new MockTelegramBotClient
103 | {
104 | Options =
105 | {
106 | ExceptionToThrow = new("Oops")
107 | }
108 | };
109 |
110 | Exception exceptionFromErrorHandler = null;
111 |
112 | var receiver = new BlockingUpdateReceiver(mockClient, errorHandler: (ex, ct) =>
113 | {
114 | Assert.Same(mockClient.Options.ExceptionToThrow, ex);
115 | throw exceptionFromErrorHandler = new("Oops2");
116 | });
117 |
118 | await using var enumerator = receiver.GetAsyncEnumerator();
119 |
120 | Exception ex = await Assert.ThrowsAsync(async () => await enumerator.MoveNextAsync());
121 | Assert.Same(exceptionFromErrorHandler, ex);
122 | }
123 |
124 | [Fact]
125 | public async Task ExceptionIsNotCaughtIfThereIsNoErrorHandler()
126 | {
127 | var mockClient = new MockTelegramBotClient
128 | {
129 | Options =
130 | {
131 | ExceptionToThrow = new("Oops")
132 | }
133 | };
134 |
135 | var receiver = new BlockingUpdateReceiver(mockClient);
136 |
137 | await using var enumerator = receiver.GetAsyncEnumerator();
138 |
139 | Exception ex = await Assert.ThrowsAsync(async () => await enumerator.MoveNextAsync());
140 | Assert.Same(mockClient.Options.ExceptionToThrow, ex);
141 | }
142 |
143 | [Fact]
144 | public async Task ThrowOutPendingUpdates()
145 | {
146 | var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(4));
147 |
148 | var bot = new MockTelegramBotClient(
149 | new MockClientOptions
150 | {
151 | Messages = new [] {"foo-bar", "baz", "quux"},
152 | HandleNegativeOffset = true,
153 | }
154 | );
155 |
156 | var receiver = new BlockingUpdateReceiver(bot, new() {ThrowPendingUpdates = true});
157 |
158 | await using var enumerator = receiver.GetAsyncEnumerator(cancellationTokenSource.Token);
159 |
160 | try
161 | {
162 | await enumerator.MoveNextAsync();
163 | }
164 | catch (OperationCanceledException)
165 | {
166 | // ignored
167 | }
168 |
169 | Assert.Equal(0, bot.MessageGroupsLeft);
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/test/Telegram.Bot.Extensions.Polling.Tests/AsyncEnumerableReceivers/QueuedUpdateReceiverTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 | using Xunit;
5 |
6 | namespace Telegram.Bot.Extensions.Polling.Tests.AsyncEnumerableReceivers;
7 |
8 | public class QueuedUpdateReceiverTests
9 | {
10 | [Fact]
11 | public void CallingGetEnumeratorTwiceThrows()
12 | {
13 | var mockClient = new MockTelegramBotClient();
14 | var receiver = new QueuedUpdateReceiver(mockClient);
15 |
16 | _ = receiver.GetAsyncEnumerator();
17 |
18 | Assert.Throws(() => receiver.GetAsyncEnumerator());
19 | }
20 |
21 | [Fact]
22 | public async Task ReceivesUpdatesInTheBackground()
23 | {
24 | var mockClient = new MockTelegramBotClient("1", "2", "3");
25 | var receiver = new QueuedUpdateReceiver(mockClient);
26 |
27 | Assert.Equal(3, mockClient.MessageGroupsLeft);
28 |
29 | await foreach (var update in receiver)
30 | {
31 | Assert.Equal("1", update.Message!.Text);
32 | await Task.Delay(100);
33 | break;
34 | }
35 |
36 | Assert.Equal(0, mockClient.MessageGroupsLeft);
37 | Assert.Equal(2, receiver.PendingUpdates);
38 | }
39 |
40 | [Fact]
41 | public async Task ReturnsReceivedPendingUpdates()
42 | {
43 | var mockClient = new MockTelegramBotClient("foo-bar", "123", "one-two-three", "456");
44 | var receiver = new QueuedUpdateReceiver(mockClient);
45 |
46 | mockClient.Options.RequestDelay = 250;
47 |
48 | Assert.Equal(4, mockClient.MessageGroupsLeft);
49 | Assert.Equal(0, receiver.PendingUpdates);
50 |
51 | await using var enumerator = receiver.GetAsyncEnumerator();
52 |
53 | Assert.True(await enumerator.MoveNextAsync());
54 |
55 | Assert.Equal(3, mockClient.MessageGroupsLeft);
56 | Assert.Equal(1, receiver.PendingUpdates);
57 | Assert.Equal("foo", enumerator.Current.Message!.Text);
58 |
59 | Assert.True(await enumerator.MoveNextAsync());
60 |
61 | Assert.Equal(3, mockClient.MessageGroupsLeft);
62 | Assert.Equal(0, receiver.PendingUpdates);
63 | Assert.Equal("bar", enumerator.Current.Message!.Text);
64 |
65 | Assert.True(await enumerator.MoveNextAsync());
66 |
67 | Assert.Equal(2, mockClient.MessageGroupsLeft);
68 | Assert.Equal(0, receiver.PendingUpdates);
69 | Assert.Equal("123", enumerator.Current.Message!.Text);
70 |
71 | Assert.True(await enumerator.MoveNextAsync());
72 |
73 | Assert.Equal(1, mockClient.MessageGroupsLeft);
74 | Assert.Equal(2, receiver.PendingUpdates);
75 | Assert.Equal("one", enumerator.Current.Message!.Text);
76 |
77 | Assert.True(await enumerator.MoveNextAsync());
78 |
79 | Assert.Equal(1, mockClient.MessageGroupsLeft);
80 | Assert.Equal(1, receiver.PendingUpdates);
81 | Assert.Equal("two", enumerator.Current.Message!.Text);
82 |
83 | Assert.True(await enumerator.MoveNextAsync());
84 |
85 | Assert.Equal(1, mockClient.MessageGroupsLeft);
86 | Assert.Equal(0, receiver.PendingUpdates);
87 | Assert.Equal("three", enumerator.Current.Message!.Text);
88 |
89 | Assert.True(await enumerator.MoveNextAsync());
90 |
91 | Assert.Equal(0, mockClient.MessageGroupsLeft);
92 | Assert.Equal(0, receiver.PendingUpdates);
93 | Assert.Equal("456", enumerator.Current.Message!.Text);
94 | }
95 |
96 | [Fact]
97 | public async Task ThrowsOnMoveNextIfCancelled()
98 | {
99 | var mockClient = new MockTelegramBotClient("foo", "bar");
100 | var receiver = new QueuedUpdateReceiver(mockClient);
101 |
102 | var cts = new CancellationTokenSource();
103 |
104 | await using var enumerator = receiver.GetAsyncEnumerator(cts.Token);
105 |
106 | Assert.True(await enumerator.MoveNextAsync());
107 | Assert.Equal("foo", enumerator.Current.Message!.Text);
108 |
109 | cts.Cancel();
110 | await Assert.ThrowsAnyAsync(async () => await enumerator.MoveNextAsync());
111 | }
112 |
113 | [Fact]
114 | public async Task DoesntReceiveAfterCancellation()
115 | {
116 | var mockClient = new MockTelegramBotClient("foo", "bar", "foo");
117 | var receiver = new QueuedUpdateReceiver(mockClient);
118 |
119 | mockClient.Options.RequestDelay = 200;
120 |
121 | var cts = new CancellationTokenSource();
122 |
123 | await using var enumerator = receiver.GetAsyncEnumerator(cts.Token);
124 |
125 | Assert.True(await enumerator.MoveNextAsync());
126 | Assert.Equal(2, mockClient.MessageGroupsLeft);
127 | Assert.Equal("foo", enumerator.Current.Message!.Text);
128 |
129 | cts.CancelAfter(50);
130 |
131 | await Assert.ThrowsAnyAsync(async () => await enumerator.MoveNextAsync());
132 |
133 | await Task.Delay(500);
134 |
135 | Assert.Equal(2, mockClient.MessageGroupsLeft);
136 | }
137 |
138 | [Fact]
139 | public void ProvidingNullBotClientThrows()
140 | {
141 | Assert.Throws(() => new QueuedUpdateReceiver(botClient: null!));
142 | }
143 |
144 | [Fact]
145 | public async Task MoveNextThrowsIfEnumeratorIsDisposed()
146 | {
147 | var mockClient = new MockTelegramBotClient("foo");
148 | var receiver = new QueuedUpdateReceiver(mockClient);
149 |
150 | var enumerator = receiver.GetAsyncEnumerator();
151 |
152 | await enumerator.MoveNextAsync();
153 | await enumerator.DisposeAsync();
154 |
155 | await Assert.ThrowsAnyAsync(async () => await enumerator.MoveNextAsync());
156 | }
157 |
158 | [Fact]
159 | public async Task ExceptionIsCaughtByErrorHandler()
160 | {
161 | var mockClient = new MockTelegramBotClient
162 | {
163 | Options =
164 | {
165 | ExceptionToThrow = new("Oops")
166 | }
167 | };
168 |
169 | var cts = new CancellationTokenSource();
170 |
171 | bool seenException = false;
172 | var receiver = new QueuedUpdateReceiver(mockClient, errorHandler: (ex, ct) =>
173 | {
174 | Assert.Same(mockClient.Options.ExceptionToThrow, ex);
175 | seenException = true;
176 | cts.Cancel();
177 | return Task.CompletedTask;
178 | });
179 |
180 | await using var enumerator = receiver.GetAsyncEnumerator(cts.Token);
181 |
182 | await Assert.ThrowsAsync(async () => await enumerator.MoveNextAsync());
183 |
184 | Assert.True(seenException);
185 | }
186 |
187 | [Fact]
188 | public async Task ReceivingIsCanceledOnExceptionIfThereIsNoErrorHandler()
189 | {
190 | var mockClient = new MockTelegramBotClient
191 | {
192 | Options =
193 | {
194 | ExceptionToThrow = new("Oops")
195 | }
196 | };
197 |
198 | var receiver = new QueuedUpdateReceiver(mockClient);
199 |
200 | await using var enumerator = receiver.GetAsyncEnumerator();
201 |
202 | await Assert.ThrowsAsync(async () => await enumerator.MoveNextAsync());
203 |
204 | Exception ex = await Assert.ThrowsAsync(async () => await enumerator.MoveNextAsync());
205 | Assert.Same(mockClient.Options.ExceptionToThrow, ex.InnerException);
206 | }
207 |
208 | [Fact]
209 | public async Task UncaughtExceptionIsStoredIfErrorHandlerThrows()
210 | {
211 | var mockClient = new MockTelegramBotClient
212 | {
213 | Options =
214 | {
215 | ExceptionToThrow = new("Oops")
216 | }
217 | };
218 |
219 | Exception exceptionFromErrorHandler = null;
220 |
221 | var receiver = new QueuedUpdateReceiver(mockClient, errorHandler: (ex, ct) =>
222 | {
223 | Assert.Same(mockClient.Options.ExceptionToThrow, ex);
224 | throw exceptionFromErrorHandler = new("Oops2");
225 | });
226 |
227 | await using var enumerator = receiver.GetAsyncEnumerator();
228 |
229 | await Assert.ThrowsAsync(async () => await enumerator.MoveNextAsync());
230 |
231 | Exception ex = await Assert.ThrowsAsync(async () => await enumerator.MoveNextAsync());
232 |
233 | var aggregateEx = ex.InnerException as AggregateException;
234 | Assert.NotNull(aggregateEx);
235 | Assert.Equal(2, aggregateEx.InnerExceptions.Count);
236 | Assert.Same(mockClient.Options.ExceptionToThrow, aggregateEx.InnerExceptions[0]);
237 | Assert.Same(exceptionFromErrorHandler, aggregateEx.InnerExceptions[1]);
238 | }
239 | }
240 |
--------------------------------------------------------------------------------
/test/Telegram.Bot.Extensions.Polling.Tests/MockClientTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Telegram.Bot.Requests;
4 | using Xunit;
5 |
6 | namespace Telegram.Bot.Extensions.Polling.Tests;
7 |
8 | public class TestMockClient
9 | {
10 | [Fact]
11 | public async Task WorksAsync()
12 | {
13 | var bot = new MockTelegramBotClient("hello-world", "foo-bar-123");
14 | Assert.Equal(2, bot.MessageGroupsLeft);
15 |
16 | var updates = await bot.MakeRequestAsync(new GetUpdatesRequest());
17 | Assert.Equal(2, updates.Length);
18 | Assert.Equal(1, bot.MessageGroupsLeft);
19 | Assert.Equal("hello", updates[0].Message!.Text);
20 | Assert.Equal("world", updates[1].Message!.Text);
21 |
22 | updates = await bot.MakeRequestAsync(new GetUpdatesRequest());
23 | Assert.Equal(3, updates.Length);
24 | Assert.Equal(0, bot.MessageGroupsLeft);
25 | Assert.Equal("foo", updates[0].Message!.Text);
26 | Assert.Equal("bar", updates[1].Message!.Text);
27 | Assert.Equal("123", updates[2].Message!.Text);
28 |
29 | updates = await bot.MakeRequestAsync(new GetUpdatesRequest());
30 | Assert.Empty(updates);
31 | Assert.Equal(0, bot.MessageGroupsLeft);
32 | }
33 |
34 | [Fact]
35 | public async Task ThrowsExceptionIfExceptionToThrownIsSet()
36 | {
37 | var bot = new MockTelegramBotClient("foo")
38 | {
39 | Options =
40 | {
41 | ExceptionToThrow = new("Oops")
42 | }
43 | };
44 |
45 | Exception ex = await Assert.ThrowsAsync(
46 | async () => await bot.MakeRequestAsync(new GetUpdatesRequest())
47 | );
48 | Assert.Equal("Oops", ex.Message);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/test/Telegram.Bot.Extensions.Polling.Tests/MockTelegramBotClient.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 | using Telegram.Bot.Args;
8 | using Telegram.Bot.Exceptions;
9 | using Telegram.Bot.Requests;
10 | using Telegram.Bot.Requests.Abstractions;
11 | using Telegram.Bot.Types;
12 |
13 | #pragma warning disable CS0067
14 |
15 | namespace Telegram.Bot.Extensions.Polling.Tests;
16 |
17 | public class MockClientOptions
18 | {
19 | public bool HandleNegativeOffset { get; set; }
20 | public string[] Messages { get; set; } = Array.Empty();
21 | public int RequestDelay { get; set; } = 10;
22 | public Exception ExceptionToThrow { get; set; }
23 |
24 | }
25 |
26 | public class MockTelegramBotClient : ITelegramBotClient
27 | {
28 | readonly Queue _messages;
29 |
30 | public int MessageGroupsLeft => _messages.Count;
31 | public MockClientOptions Options { get; }
32 |
33 | public MockTelegramBotClient(MockClientOptions options = default)
34 | {
35 | Options = options ?? new();
36 | _messages = new Queue(
37 | Options.Messages.Select(message => message.Split('-').ToArray())
38 | );
39 | }
40 |
41 | public MockTelegramBotClient(params string[] messages)
42 | {
43 | Options = new();
44 | _messages = new(messages.Select(message => message.Split('-').ToArray()));
45 | }
46 |
47 | public async Task MakeRequestAsync(
48 | IRequest request,
49 | CancellationToken cancellationToken = default)
50 | {
51 | if (request is not GetUpdatesRequest getUpdatesRequest) { throw new NotImplementedException(); }
52 |
53 | await Task.Delay(Options.RequestDelay, cancellationToken);
54 |
55 | if (Options.ExceptionToThrow is not null) { throw Options.ExceptionToThrow; }
56 |
57 | if (Options.HandleNegativeOffset && getUpdatesRequest.Offset == -1)
58 | {
59 | var messageCount = _messages.Select(group => @group.Length).Sum() + 1;
60 | var lastMessage = _messages.Last().Last();
61 |
62 | _messages.Clear();
63 |
64 | return (TResponse)(object) new[]
65 | {
66 | new Update
67 | {
68 | Message = new()
69 | {
70 | Text = lastMessage
71 | },
72 | Id = messageCount
73 | }
74 | };
75 | }
76 |
77 | if (!_messages.TryDequeue(out var messages))
78 | {
79 | return (TResponse)(object)Array.Empty();
80 | }
81 |
82 | return (TResponse)(object)messages.Select((_, i) => new Update
83 | {
84 | Message = new()
85 | {
86 | Text = messages[i]
87 | },
88 | Id = getUpdatesRequest.Offset ?? 0 + i + 1
89 | }).ToArray();
90 |
91 | }
92 |
93 | public TimeSpan Timeout { get; set; } = TimeSpan.FromMilliseconds(50);
94 |
95 | public IExceptionParser ExceptionsParser { get; set; }
96 |
97 |
98 | // ---------------
99 | // NOT IMPLEMENTED
100 | // ---------------
101 |
102 | public long? BotId => throw new NotImplementedException();
103 | public event AsyncEventHandler OnMakingApiRequest;
104 | public event AsyncEventHandler OnApiResponseReceived;
105 | public Task DownloadFileAsync(string filePath, Stream destination, CancellationToken cancellationToken = default) => throw new NotImplementedException();
106 | public Task TestApiAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException();
107 | }
108 |
--------------------------------------------------------------------------------
/test/Telegram.Bot.Extensions.Polling.Tests/ReceiveAsyncTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 | using Telegram.Bot.Types;
5 | using Xunit;
6 |
7 | namespace Telegram.Bot.Extensions.Polling.Tests;
8 |
9 | public class ReceiveAsyncTests
10 | {
11 | [Fact]
12 | public async Task ReceivesUpdatesAndRespectsTheCancellationToken()
13 | {
14 | var bot = new MockTelegramBotClient("start-end", "foo");
15 |
16 | CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
17 |
18 | int updateCount = 0;
19 | async Task HandleUpdate(ITelegramBotClient botClient, Update update, CancellationToken token)
20 | {
21 | updateCount++;
22 | Assert.Contains(update.Message!.Text, "start end");
23 | await Task.Delay(10, cancellationTokenSource.Token);
24 | if (update.Message.Text == "end")
25 | {
26 | cancellationTokenSource.Cancel();
27 | }
28 | }
29 |
30 | var updateHandler = new DefaultUpdateHandler(
31 | updateHandler: HandleUpdate,
32 | errorHandler: async (_, _, token) => await Task.Delay(10, token)
33 | );
34 |
35 | var cancellationToken = cancellationTokenSource.Token;
36 | await bot.ReceiveAsync(updateHandler, cancellationToken: cancellationToken);
37 |
38 | Assert.True(cancellationToken.IsCancellationRequested);
39 | Assert.Equal(2, updateCount);
40 | Assert.Equal(1, bot.MessageGroupsLeft);
41 | }
42 |
43 | [Fact]
44 | public async Task UserExceptionsPropagateToSurface()
45 | {
46 | var bot = new MockTelegramBotClient("foo-bar", "throw");
47 |
48 | int updateCount = 0;
49 | async Task HandleUpdate(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken)
50 | {
51 | updateCount++;
52 | await Task.Delay(10, cancellationToken);
53 | if (update.Message!.Text == "throw")
54 | throw new InvalidOperationException("Oops");
55 | }
56 |
57 | var updateHandler = new DefaultUpdateHandler(
58 | updateHandler: HandleUpdate,
59 | errorHandler: async (_, _, token) => await Task.Delay(10, token)
60 | );
61 |
62 | try
63 | {
64 | await bot.ReceiveAsync(updateHandler);
65 | Assert.True(false);
66 | }
67 | catch (Exception ex)
68 | {
69 | Assert.IsType(ex);
70 | Assert.Contains("Oops", ex.Message);
71 | }
72 |
73 | Assert.Equal(3, updateCount);
74 | Assert.Equal(0, bot.MessageGroupsLeft);
75 | }
76 |
77 | [Fact]
78 | public async Task ThrowOutPendingUpdates()
79 | {
80 | var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(4));
81 |
82 | var bot = new MockTelegramBotClient(
83 | new MockClientOptions
84 | {
85 | Messages = new [] {"foo-bar", "baz", "quux"},
86 | HandleNegativeOffset = true
87 | }
88 | );
89 |
90 | int handleCount = 0;
91 |
92 | Task HandleUpdate(
93 | ITelegramBotClient botClient,
94 | Update update,
95 | CancellationToken cancellationToken)
96 | {
97 | handleCount += 1;
98 | return Task.CompletedTask;
99 | };
100 |
101 | var updateHandler = new DefaultUpdateHandler(
102 | updateHandler: HandleUpdate,
103 | errorHandler: (_, _, _) => Task.CompletedTask
104 | );
105 |
106 | await bot.ReceiveAsync(
107 | updateHandler,
108 | new() { ThrowPendingUpdates = true },
109 | cancellationTokenSource.Token
110 | );
111 |
112 | Assert.Equal(0, handleCount);
113 | Assert.Equal(0, bot.MessageGroupsLeft);
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/test/Telegram.Bot.Extensions.Polling.Tests/Telegram.Bot.Extensions.Polling.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net6.0
6 | 10
7 |
8 |
9 |
10 |
11 |
12 |
13 | all
14 | runtime; build; native; contentfiles; analyzers; buildtransitive
15 |
16 |
17 | all
18 | runtime; build; native; contentfiles; analyzers; buildtransitive
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------