Skip to content

Commit

Permalink
Merge pull request #1 from vova-lantsov-dev/dev
Browse files Browse the repository at this point in the history
Release 1.1
  • Loading branch information
vova-lantsov-dev authored Jan 29, 2023
2 parents f203c08 + f00b499 commit 908f8d8
Show file tree
Hide file tree
Showing 11 changed files with 589 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using ElectricityOffNotifier.AppHost.Auth;
using ElectricityOffNotifier.AppHost.Helpers;
using ElectricityOffNotifier.AppHost.Models;
using ElectricityOffNotifier.AppHost.Services;
using ElectricityOffNotifier.Data;
using ElectricityOffNotifier.Data.Models;
using ElectricityOffNotifier.Data.Models.Enums;
using FluentValidation;
using FluentValidation.Results;
using Microsoft.AspNetCore.Mvc;
Expand All @@ -18,6 +20,7 @@ public sealed class ProducerController : ControllerBase
private readonly ElectricityDbContext _context;
private readonly IConfiguration _configuration;
private readonly IValidator<ProducerRegisterModel> _producerRegisterModelValidator;
private readonly IElectricityCheckerManager _electricityCheckerManager;

private readonly Password _accessTokenGenerator = new(
includeLowercase: true,
Expand All @@ -26,12 +29,16 @@ public sealed class ProducerController : ControllerBase
includeSpecial: false,
passwordLength: 20);

public ProducerController(ElectricityDbContext context, IConfiguration configuration,
IValidator<ProducerRegisterModel> producerRegisterModelValidator)
public ProducerController(
ElectricityDbContext context,
IConfiguration configuration,
IValidator<ProducerRegisterModel> producerRegisterModelValidator,
IElectricityCheckerManager electricityCheckerManager)
{
_context = context;
_configuration = configuration;
_producerRegisterModelValidator = producerRegisterModelValidator;
_electricityCheckerManager = electricityCheckerManager;
}

[HttpPost]
Expand Down Expand Up @@ -60,6 +67,8 @@ public async Task<ActionResult> Register([FromBody] ProducerRegisterModel model,
.Where(c => c.AddressId == model.AddressId)
.Select(c => new Checker {Id = c.Id})
.FirstOrDefaultAsync(cancellationToken);
bool existingChecker = checker != null;

if (checker == null)
{
checker = new Checker
Expand All @@ -79,6 +88,19 @@ public async Task<ActionResult> Register([FromBody] ProducerRegisterModel model,
}

await _context.SaveChangesAsync(CancellationToken.None);

if (!existingChecker)
{
_electricityCheckerManager.StartChecker(checker.Id, model.Mode switch
{
ProducerMode.Webhook => new[] { producer.Id },
_ => Array.Empty<int>()
});
}
else if (model.Mode == ProducerMode.Webhook)
{
_electricityCheckerManager.AddWebhookProducer(checker.Id, producer.Id);
}

return Ok(new { accessToken });
}
Expand Down
103 changes: 99 additions & 4 deletions src/ElectricityOffNotifier.AppHost/Services/BotUpdateHandler.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Text;
using System.Globalization;
using System.Text;
using ElectricityOffNotifier.AppHost.Auth;
using ElectricityOffNotifier.Data;
using ElectricityOffNotifier.Data.Models;
using Microsoft.EntityFrameworkCore;
Expand All @@ -14,13 +16,15 @@ internal sealed class BotUpdateHandler : IUpdateHandler
private readonly IServiceProvider _serviceProvider;
private readonly ITemplateService _templateService;
private readonly ILogger<BotUpdateHandler> _logger;
private readonly IConfiguration _configuration;

public BotUpdateHandler(IServiceProvider serviceProvider, ITemplateService templateService,
ILogger<BotUpdateHandler> logger)
ILogger<BotUpdateHandler> logger, IConfiguration configuration)
{
_serviceProvider = serviceProvider;
_templateService = templateService;
_logger = logger;
_configuration = configuration;
}

public async Task HandleUpdateAsync(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken)
Expand Down Expand Up @@ -255,8 +259,99 @@ await botClient.SendTextMessageAsync(chatId, "Цей чат ще не зареє
msgToSend.AppendJoin("\n", chatInfo.Subscribers.Select((s, i) =>
$"{i + 1}) Subscriber id: {s.Id}\nCulture: {s.Culture}\nTime zone: {s.TimeZone}\nProducer mode: {s.Producer.Mode:G}\nProducer id: {s.Producer.Id}\nProducer enabled: {s.Producer.IsEnabled}"));

await botClient.SendTextMessageAsync(chatId, msgToSend.ToString(), messageThreadId,
replyToMessageId: messageId, cancellationToken: cancellationToken);
try
{
await botClient.SendTextMessageAsync(chatId, msgToSend.ToString(), messageThreadId,
replyToMessageId: messageId, cancellationToken: cancellationToken);
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Error occurred while sending Telegram message");
}

break;
}

case
{
Message:
{
Text: { Length: > 7 } text,
MessageId: var messageId,
Chat.Id: var chatId,
MessageThreadId: var messageThreadId
}
}
when text.StartsWith("!skip ") && isAdmin:
{
string[] separated = text[6..].Split(' ');
if (separated.Length != 2)
{
try
{
await botClient.SendTextMessageAsync(chatId,"Неправильний формат повідомлення.",
messageThreadId, replyToMessageId: messageId, cancellationToken: cancellationToken);
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Error occurred while sending Telegram message");
}

return;
}

await using AsyncServiceScope scope = _serviceProvider.CreateAsyncScope();
var context = scope.ServiceProvider.GetRequiredService<ElectricityDbContext>();

Producer? producer = await context.Producers
.AsNoTracking()
.Include(ci => ci.Subscribers.OrderByDescending(s => s.Id).Take(1))
.FirstOrDefaultAsync(
ci => ci.AccessTokenHash ==
separated[0].ToHmacSha256ByteArray(_configuration["Auth:SecretKey"]!), cancellationToken);
if (producer == null)
{
try
{
await botClient.SendTextMessageAsync(chatId, "Неправильний API ключ.",
messageThreadId, replyToMessageId: messageId, cancellationToken: cancellationToken);
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Error occurred while sending Telegram message");
}

return;
}

string cultureName = producer.Subscribers.FirstOrDefault() is { } subscriber
? subscriber.Culture
: "uk-UA";
IFormatProvider culture = TemplateService.GetCulture(cultureName);

if (!TimeSpan.TryParse(separated[1], culture, out TimeSpan timeSpan))
{
try
{
await botClient.SendTextMessageAsync(chatId, "Не вдається прочитати тривалість з повідомлення",
messageThreadId, replyToMessageId: messageId, cancellationToken: cancellationToken);
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Error occurred while sending Telegram message");
}

return;
}

var updatedProducer = new Producer
{
Id = producer.Id,
SkippedUntil = DateTime.UtcNow + timeSpan
};
context.Attach(updatedProducer).Property(p => p.SkippedUntil).IsModified = true;

await context.SaveChangesAsync(CancellationToken.None);

break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ public void StartChecker(int checkerId, int[] webhookProducerIds)
}
}

public void AddWebhookProducer(int checkerId, int webhookProducerId)
{
_recurringJobManager.AddOrUpdate(
$"c{checkerId}-p{webhookProducerId}-webhook",
() => ProcessWebhookAsync(webhookProducerId, CancellationToken.None),
"*/15 * * * * *");
}

public async Task CheckAsync(int checkerId, CancellationToken cancellationToken)
{
if (DateTime.Now - StartupTime < TimeSpan.FromMinutes(1))
Expand All @@ -60,13 +68,23 @@ public async Task CheckAsync(int checkerId, CancellationToken cancellationToken)
.Include(c => c.Entries.OrderByDescending(e => e.DateTime).Take(1))
.Include(c => c.Address)
.ThenInclude(a => a.City)
.Include(c => c.Producers)
.FirstAsync(c => c.Id == checkerId, cancellationToken);

_logger.LogDebug("Checker {CheckerId} has {EntriesCount} entries", checkerId, checker.Entries.Count);

if (checker.Entries.Count < 1)
return;

{
DateTime now = DateTime.UtcNow;
if (checker.Producers.Count > 0 && checker.Producers.All(p => p.SkippedUntil > now))
{
_logger.LogDebug("Checker {CheckerId} has only skipped producers", checkerId);
return;
}
}

async Task LoadSubscribersAsync()
{
checker.Subscribers = await dbContext.Subscribers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@
public interface IElectricityCheckerManager
{
void StartChecker(int checkerId, int[] webhookProducerIds);

void AddWebhookProducer(int checkerId, int webhookProducerId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,18 +68,18 @@ public bool ValidateMessageTemplate(string input)

private static DateTime GetLocalTime(DateTime utcTime, string timeZone)
{
static Lazy<TimeZoneInfo> CreateTimeZoneFactory(string timeZone) =>
new(() => TZConvert.GetTimeZoneInfo(timeZone));

TimeZoneInfo tz = TimeZones.GetOrAdd(timeZone, CreateTimeZoneFactory).Value;

return TimeZoneInfo.ConvertTime(utcTime, tz);
}

private static IFormatProvider GetCulture(string cultureName)
internal static IFormatProvider GetCulture(string cultureName)
{
return Cultures
.GetOrAdd(cultureName, c => new Lazy<IFormatProvider>(() => new CultureInfo(c)))
.Value;
}

private static Lazy<TimeZoneInfo> CreateTimeZoneFactory(string timeZone) =>
new(() => TZConvert.GetTimeZoneInfo(timeZone));
}
3 changes: 2 additions & 1 deletion src/ElectricityOffNotifier.AppHost/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"LogLevel": {
"Default": "Warning",
"ElectricityOffNotifier.AppHost.Services": "Information",
"Microsoft.AspNetCore.DataProtection": "Error"
"Microsoft.AspNetCore.DataProtection": "Error",
"ElectricityOffNotifier.AppHost.Services.BotUpdateHandler": "Debug"
}
}
},
Expand Down
11 changes: 11 additions & 0 deletions src/ElectricityOffNotifier.Data/ElectricityDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
.HasConversion(
dt => DateTime.SpecifyKind(dt, DateTimeKind.Unspecified),
dt => DateTime.SpecifyKind(dt, DateTimeKind.Utc));

modelBuilder
.Entity<Producer>()
.Property(ce => ce.SkippedUntil)
.HasConversion(
dt => DateTime.SpecifyKind(dt, DateTimeKind.Unspecified),
dt => DateTime.SpecifyKind(dt, DateTimeKind.Utc));

modelBuilder.Entity<Producer>()
.Property(p => p.Mode)
Expand All @@ -50,5 +57,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
entity.Property(ci => ci.MessageDownTemplate)
.HasDefaultValue("Повідомлення за адресою <b>{{Address}}</b>:\n\n<b>Електропостачання відсутнє!</b>\n{{#SinceRegion}}\nЧас початку відключення: {{SinceDate}}\nСвітло було протягом {{DurationHours}} год. {{DurationMinutes}} хв.\n{{/SinceRegion}}");
});

modelBuilder.Entity<Producer>()
.Property(p => p.SkippedUntil)
.HasDefaultValue(DateTime.UnixEpoch);
}
}
Loading

0 comments on commit 908f8d8

Please sign in to comment.