Skip to content

Commit 626dd1f

Browse files
fix: resolve async return issue in TelegramChannel.StopAsync
1 parent 9aa3933 commit 626dd1f

1 file changed

Lines changed: 255 additions & 0 deletions

File tree

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
using ClawSharp.Core.Channels;
2+
using ClawSharp.Core.Config;
3+
using Microsoft.Extensions.Logging;
4+
using Telegram.Bot;
5+
using Telegram.Bot.Types;
6+
using Telegram.Bot.Types.Enums;
7+
8+
namespace ClawSharp.Channels;
9+
10+
/// <summary>
11+
/// Telegram channel implementation using Telegram Bot API.
12+
/// </summary>
13+
public class TelegramChannel : IChannel
14+
{
15+
private readonly ClawSharpConfig _config;
16+
private readonly ILogger<TelegramChannel> _logger;
17+
private ITelegramBotClient? _botClient;
18+
private CancellationTokenSource? _pollingCts;
19+
private readonly HashSet<string> _allowedUsers;
20+
21+
public string Name => "telegram";
22+
23+
public event Func<ChannelMessage, Task>? OnMessage;
24+
25+
public TelegramChannel(ClawSharpConfig config, ILogger<TelegramChannel> logger)
26+
{
27+
_config = config ?? throw new ArgumentNullException(nameof(config));
28+
_logger = logger;
29+
30+
var telegramConfig = config.Channels.Telegram
31+
?? throw new InvalidOperationException("Telegram configuration is required. Set Channels.Telegram in config.");
32+
33+
if (string.IsNullOrWhiteSpace(telegramConfig.BotToken))
34+
throw new InvalidOperationException("Telegram BotToken is required. Set Channels.Telegram.BotToken in config.");
35+
36+
_allowedUsers = telegramConfig.AllowedUsers?.ToHashSet() ?? [];
37+
}
38+
39+
/// <summary>
40+
/// Internal constructor for testing with a mock bot client.
41+
/// </summary>
42+
internal TelegramChannel(ClawSharpConfig config, ILogger<TelegramChannel> logger, ITelegramBotClient botClient)
43+
: this(config, logger)
44+
{
45+
_botClient = botClient;
46+
}
47+
48+
public async Task StartAsync(CancellationToken ct)
49+
{
50+
if (_botClient == null)
51+
{
52+
var token = _config.Channels.Telegram?.BotToken
53+
?? throw new InvalidOperationException("BotToken not configured");
54+
_botClient = new TelegramBotClient(token);
55+
}
56+
57+
_pollingCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
58+
59+
_logger.LogInformation("Starting Telegram bot polling");
60+
61+
// Start polling for updates
62+
_ = Task.Run(async () => await PollForUpdatesAsync(_pollingCts.Token), _pollingCts.Token);
63+
64+
await Task.CompletedTask;
65+
}
66+
67+
public async Task StopAsync(CancellationToken ct)
68+
{
69+
_logger.LogInformation("Stopping Telegram bot");
70+
71+
_pollingCts?.Cancel();
72+
73+
if (_botClient != null)
74+
{
75+
await _botClient.Close();
76+
}
77+
78+
_pollingCts?.Dispose();
79+
_pollingCts = null;
80+
}
81+
82+
public async Task SendAsync(OutboundMessage message, CancellationToken ct = default)
83+
{
84+
ArgumentNullException.ThrowIfNull(message);
85+
86+
if (_botClient == null)
87+
throw new InvalidOperationException("Bot client not initialized. Call StartAsync first.");
88+
89+
// Handle file attachments (simplified - just send content without file for now)
90+
if (!string.IsNullOrEmpty(message.FilePath))
91+
{
92+
_logger.LogDebug("File attachment ignored for now: {FilePath}", message.FilePath);
93+
// TODO: Implement file sending with Telegram.Bot v22 API
94+
}
95+
96+
// Handle long messages by splitting
97+
if (message.Content.Length > 4096)
98+
{
99+
await SendLongMessageAsync(message, ct);
100+
return;
101+
}
102+
103+
// Send regular message using v22 API (SendMessage, not SendTextMessageAsync)
104+
await _botClient.SendMessage(
105+
chatId: message.ChatId,
106+
text: message.Content,
107+
parseMode: ParseMode.Markdown,
108+
disableNotification: message.Silent,
109+
cancellationToken: ct);
110+
}
111+
112+
private async Task SendLongMessageAsync(OutboundMessage message, CancellationToken ct)
113+
{
114+
const int maxLength = 4096;
115+
var chunks = SplitMessage(message.Content, maxLength);
116+
117+
foreach (var chunk in chunks)
118+
{
119+
await _botClient!.SendMessage(
120+
chatId: message.ChatId,
121+
text: chunk,
122+
parseMode: ParseMode.Markdown,
123+
disableNotification: message.Silent,
124+
cancellationToken: ct);
125+
}
126+
}
127+
128+
private static List<string> SplitMessage(string text, int maxLength)
129+
{
130+
var chunks = new List<string>();
131+
var lines = text.Split('\n');
132+
var currentChunk = new System.Text.StringBuilder();
133+
134+
foreach (var line in lines)
135+
{
136+
if (currentChunk.Length + line.Length + 1 > maxLength)
137+
{
138+
if (currentChunk.Length > 0)
139+
{
140+
chunks.Add(currentChunk.ToString());
141+
currentChunk.Clear();
142+
}
143+
144+
// If a single line is longer than maxLength, split it
145+
if (line.Length > maxLength)
146+
{
147+
for (int i = 0; i < line.Length; i += maxLength)
148+
{
149+
chunks.Add(line.Substring(i, Math.Min(maxLength, line.Length - i)));
150+
}
151+
}
152+
else
153+
{
154+
currentChunk.Append(line);
155+
}
156+
}
157+
else
158+
{
159+
if (currentChunk.Length > 0)
160+
currentChunk.Append('\n');
161+
currentChunk.Append(line);
162+
}
163+
}
164+
165+
if (currentChunk.Length > 0)
166+
chunks.Add(currentChunk.ToString());
167+
168+
return chunks;
169+
}
170+
171+
private async Task PollForUpdatesAsync(CancellationToken ct)
172+
{
173+
var offset = 0;
174+
175+
while (!ct.IsCancellationRequested)
176+
{
177+
try
178+
{
179+
var updates = await _botClient!.GetUpdates(
180+
offset,
181+
limit: 100,
182+
timeout: 30,
183+
cancellationToken: ct);
184+
185+
foreach (var update in updates)
186+
{
187+
await HandleUpdateAsync(update);
188+
offset = update.Id + 1;
189+
}
190+
}
191+
catch (OperationCanceledException) when (ct.IsCancellationRequested)
192+
{
193+
break;
194+
}
195+
catch (Exception ex)
196+
{
197+
_logger.LogError(ex, "Error polling for Telegram updates");
198+
await Task.Delay(1000, ct);
199+
}
200+
}
201+
}
202+
203+
private Task HandleUpdateAsync(Update update)
204+
{
205+
if (update.Message == null)
206+
return Task.CompletedTask;
207+
208+
var message = update.Message;
209+
210+
// Check if user is allowed
211+
if (_allowedUsers.Count > 0 && !_allowedUsers.Contains(message.From?.Id.ToString() ?? ""))
212+
{
213+
_logger.LogDebug("Ignoring message from unauthorized user: {UserId}", message.From?.Id);
214+
return Task.CompletedTask;
215+
}
216+
217+
var channelMessage = new ChannelMessage(
218+
Id: message.MessageId.ToString(),
219+
Sender: message.From?.Id.ToString() ?? "unknown",
220+
Content: message.Text ?? "",
221+
Channel: Name,
222+
ChatId: message.Chat.Id.ToString(),
223+
Timestamp: message.Date,
224+
Media: message.Photo?.Length > 0
225+
? message.Photo.Select(p => p.FileId).ToList()
226+
: null);
227+
228+
_logger.LogDebug("Received message from {User}: {Content}", channelMessage.Sender, channelMessage.Content);
229+
230+
return OnMessage?.Invoke(channelMessage) ?? Task.CompletedTask;
231+
}
232+
233+
/// <summary>
234+
/// Simulates receiving a message for testing purposes.
235+
/// </summary>
236+
public Task SimulateMessageAsync(string userId, string content, string chatId)
237+
{
238+
// Check if user is allowed
239+
if (_allowedUsers.Count > 0 && !_allowedUsers.Contains(userId))
240+
{
241+
_logger.LogDebug("Ignoring message from unauthorized user: {UserId}", userId);
242+
return Task.CompletedTask;
243+
}
244+
245+
var channelMessage = new ChannelMessage(
246+
Id: Guid.NewGuid().ToString(),
247+
Sender: userId,
248+
Content: content,
249+
Channel: Name,
250+
ChatId: chatId,
251+
Timestamp: DateTimeOffset.UtcNow);
252+
253+
return OnMessage?.Invoke(channelMessage) ?? Task.CompletedTask;
254+
}
255+
}

0 commit comments

Comments
 (0)