Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
root = true

[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

# Microsoft .NET properties
csharp_preferred_modifier_order = public, private, protected, internal, file, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async, required:suggestion
dotnet_style_qualification_for_event = false:warning
dotnet_style_qualification_for_field = false:warning
dotnet_style_qualification_for_method = false:warning
dotnet_style_qualification_for_property = false:warning

csharp_style_namespace_declarations = file_scoped:warning

# ReSharper properties
resharper_namespace_body = file_scoped

[*.cs]
charset = utf-8-bom
indent_size = 4

[*.md]
trim_trailing_whitespace = false
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -398,3 +398,4 @@ FodyWeavers.xsd
*.sln.iml

.idea/
Place4.TelegramBot/appsettings.Development.json
4 changes: 3 additions & 1 deletion Place4.AppHost/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Projects;

var builder = DistributedApplication.CreateBuilder(args);
builder.AddProject<Projects.Place4_TelegramBot>("bot", launchProfileName: "https");
builder.AddProject<Place4_TelegramBot>("bot", launchProfileName: "https");

builder.Build().Run();
4 changes: 2 additions & 2 deletions Place4.ServiceDefaults/Extensions.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
namespace Place4.ServiceDefaults;

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.DependencyInjection;
Expand All @@ -8,8 +10,6 @@
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;

namespace Place4.ServiceDefaults;

// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry.
// This project should be referenced by each service project in your solution.
// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults
Expand Down
8 changes: 8 additions & 0 deletions Place4.TelegramBot/BotConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Place4.TelegramBot;

public class BotConfiguration
{
public required string BotToken { get; init; }
public required string LogChatId { get; init; }
public int LogChatMessageThreadId { get; init; }
}
25 changes: 25 additions & 0 deletions Place4.TelegramBot/Logging/TelegramLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace Place4.TelegramBot.Logging;

using Telegram.Bot;

public class TelegramLogger(string name, ITelegramBotClient telegramBotClient, BotConfiguration configuration) : ILogger
{
public IDisposable? BeginScope<TState>(TState state) where TState: notnull => default;

public bool IsEnabled(LogLevel logLevel)
{
return logLevel > LogLevel.Information;
}

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
if (!IsEnabled(logLevel))
{
return;
}
telegramBotClient.SendMessage(
configuration.LogChatId,
formatter(state, exception),
messageThreadId: configuration.LogChatMessageThreadId).GetAwaiter().GetResult();
}
}
34 changes: 34 additions & 0 deletions Place4.TelegramBot/Logging/TelegramLoggerProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
namespace Place4.TelegramBot.Logging;

using System.Collections.Concurrent;
using Microsoft.Extensions.Options;
using Telegram.Bot;

public class TelegramLoggerProvider : ILoggerProvider
{
private readonly ITelegramBotClient _telegramBotClient;
private readonly IDisposable? _onChangeToken;
private BotConfiguration _currentConfig;
private readonly ConcurrentDictionary<string, TelegramLogger> _loggers =
new(StringComparer.OrdinalIgnoreCase);

public TelegramLoggerProvider(
IOptionsMonitor<BotConfiguration> config,
ITelegramBotClient telegramBotClient)
{
_telegramBotClient = telegramBotClient;
_currentConfig = config.CurrentValue;
_onChangeToken = config.OnChange(updatedConfig => _currentConfig = updatedConfig);
}

public ILogger CreateLogger(string categoryName) =>
_loggers.GetOrAdd(categoryName, name => new TelegramLogger(name, _telegramBotClient, GetCurrentConfig()));

private BotConfiguration GetCurrentConfig() => _currentConfig;

public void Dispose()
{
_loggers.Clear();
_onChangeToken?.Dispose();
}
}
20 changes: 20 additions & 0 deletions Place4.TelegramBot/Program.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,30 @@
using Microsoft.Extensions.Options;
using Place4.TelegramBot;
using Place4.TelegramBot.Logging;
using Place4.TelegramBot.UpdateHandlers;
using Telegram.Bot;
using Telegram.Bot.Polling;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
builder.Services.AddHttpClient("telegram_bot_client").RemoveAllLoggers()
.AddTypedClient<ITelegramBotClient>((httpClient, sp) =>
{
var botConfiguration = sp.GetService<IOptions<BotConfiguration>>()?.Value;
ArgumentNullException.ThrowIfNull(botConfiguration);
TelegramBotClientOptions options = new(botConfiguration.BotToken);
return new TelegramBotClient(options, httpClient);
});
builder.Services.AddHostedService<TelegramPollingJob>();
builder.Services.AddSingleton<IUpdateHandler, UpdateHandler>();
builder.Services.AddSingleton<ITelegramUpdateHandler, ParrotUpdateHandler>();
builder.Services.AddSingleton<ITelegramUpdateHandler, NewUserUpdateHandler>();

builder.Services.Configure<BotConfiguration>(builder.Configuration.GetSection("BotConfiguration"));
builder.Services.AddSingleton<ILoggerProvider, TelegramLoggerProvider>();

var app = builder.Build();

Expand Down
41 changes: 41 additions & 0 deletions Place4.TelegramBot/TelegramPollingJob.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
namespace Place4.TelegramBot;

using Telegram.Bot;
using Telegram.Bot.Polling;
using Telegram.Bot.Types.Enums;

public class TelegramPollingJob(
ITelegramBotClient telegramBotClient,
IUpdateHandler updateHandler,
ILogger<TelegramPollingJob> logger) :
BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
logger.LogInformation("Starting polling service");
await DoWork(stoppingToken);
}

private async Task DoWork(CancellationToken stoppingToken)
{
var receiverOptions = new ReceiverOptions
{
AllowedUpdates = [UpdateType.Message, UpdateType.ChatMember],
DropPendingUpdates = true
};
// Make sure we receive updates until Cancellation Requested
while (!stoppingToken.IsCancellationRequested)
{
try
{
await telegramBotClient.ReceiveAsync(updateHandler, receiverOptions, stoppingToken);
}
catch (Exception ex)
{
logger.LogError("Polling failed with exception: {Exception}", ex);
// Cooldown if something goes wrong
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
}
}
}
30 changes: 30 additions & 0 deletions Place4.TelegramBot/UpdateHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
namespace Place4.TelegramBot;

using Telegram.Bot;
using Telegram.Bot.Polling;
using Telegram.Bot.Types;
using UpdateHandlers;

public class UpdateHandler(IEnumerable<ITelegramUpdateHandler> updateHandlers, ILogger<UpdateHandler> logger) : IUpdateHandler
{
public async Task HandleUpdateAsync(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken)
{
foreach (var updateHandler in updateHandlers)
{
try
{
await updateHandler.HandleUpdateAsync(botClient, update, cancellationToken);
}
catch (Exception ex)
{
await HandleErrorAsync(botClient, ex, HandleErrorSource.HandleUpdateError, cancellationToken);
}
}
}

public Task HandleErrorAsync(ITelegramBotClient botClient, Exception exception, HandleErrorSource source, CancellationToken cancellationToken)
{
logger.LogError(exception, "Error occured in {Source}", source);
return Task.CompletedTask;
}
}
9 changes: 9 additions & 0 deletions Place4.TelegramBot/UpdateHandlers/ITelegramUpdateHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Place4.TelegramBot.UpdateHandlers;

using Telegram.Bot;
using Telegram.Bot.Types;

public interface ITelegramUpdateHandler
{
Task HandleUpdateAsync(ITelegramBotClient telegramBotClient, Update update, CancellationToken cancellationToken);
}
37 changes: 37 additions & 0 deletions Place4.TelegramBot/UpdateHandlers/NewUserUpdateHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
namespace Place4.TelegramBot.UpdateHandlers;

using Telegram.Bot;
using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums;

public class NewUserUpdateHandler : ITelegramUpdateHandler
{
public async Task HandleUpdateAsync(ITelegramBotClient telegramBotClient, Update update, CancellationToken cancellationToken)
{
var newUserUpdate = IsNewUserUpdate(update);
if (newUserUpdate.chat == null || newUserUpdate.user == null)
{
return;
}

var chatId = newUserUpdate.chat.Id;
var message = $"Welcome, [{newUserUpdate.user.FirstName} {newUserUpdate.user.LastName}](tg://user?id={newUserUpdate.user.Id})";
await telegramBotClient.SendMessage(
chatId,
message,
cancellationToken: cancellationToken,
parseMode: ParseMode.Markdown);
}

private static (Chat? chat, User? user) IsNewUserUpdate(Update update)
{
var isFakeNewMember = update is { Type: UpdateType.Message, Message.Text: "I am new" }; // for test only
var isNewMember = update is { Type: UpdateType.ChatMember, ChatMember.NewChatMember: not null };
if (isNewMember)
{
return (update.ChatMember?.Chat, update.ChatMember?.NewChatMember?.User);
}

return isFakeNewMember ? (update.Message?.Chat, update.Message?.From) : (null, null);
}
}
25 changes: 25 additions & 0 deletions Place4.TelegramBot/UpdateHandlers/ParrotUpdateHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace Place4.TelegramBot.UpdateHandlers;

using Telegram.Bot;
using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums;

public class ParrotUpdateHandler : ITelegramUpdateHandler
{
public async Task HandleUpdateAsync(ITelegramBotClient telegramBotClient, Update update, CancellationToken cancellationToken)
{
if (update.Type != UpdateType.Message || update.Message?.From == null)
{
return;
}
var chatId = update.Message.Chat.Id;
var message = $"Hi, [{update.Message.From.FirstName} {update.Message.From.LastName}](tg://user?id={update.Message.From.Id}). You said: {update.Message.Text}";
await telegramBotClient.SendMessage(
chatId,
message,
cancellationToken: cancellationToken,
messageThreadId: update.Message.MessageThreadId,
parseMode: ParseMode.Markdown);
}

}
7 changes: 6 additions & 1 deletion Place4.TelegramBot/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,10 @@
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
"AllowedHosts": "*",
"BotConfiguration": {
"BotToken": "your_token",
"LogChatId": "your_chat_id",
"LogChatMessageThreadId": 0
}
}