diff --git a/Dodo1000Bot.Api.Dialogflow.Tests/DialogflowServiceTests.cs b/Dodo1000Bot.Api.Dialogflow.Tests/DialogflowServiceTests.cs index 3ce6187..c0e8ce6 100644 --- a/Dodo1000Bot.Api.Dialogflow.Tests/DialogflowServiceTests.cs +++ b/Dodo1000Bot.Api.Dialogflow.Tests/DialogflowServiceTests.cs @@ -19,6 +19,7 @@ internal class DialogflowServiceTests private Mock _mapperMock; private Mock _usersServiceMock; private Mock _customNotificationsRepositoryMock; + private Mock _notificationsService; private DialogflowService _target; @@ -34,12 +35,15 @@ public void SetUp() _mapperMock = _mockRepository.Create(); _usersServiceMock = _mockRepository.Create(); _customNotificationsRepositoryMock = _mockRepository.Create(); + _notificationsService = _mockRepository.Create(); _target = new DialogflowService( _log, _conversationServiceMock.Object, _mapperMock.Object, - _usersServiceMock.Object, _customNotificationsRepositoryMock.Object); + _usersServiceMock.Object, + _customNotificationsRepositoryMock.Object, + _notificationsService.Object); _fixture = new Fixture { OmitAutoProperties = true }; } diff --git a/Dodo1000Bot.Api.Dialogflow/DialogflowService.cs b/Dodo1000Bot.Api.Dialogflow/DialogflowService.cs index 9c4add9..90a86b3 100644 --- a/Dodo1000Bot.Api.Dialogflow/DialogflowService.cs +++ b/Dodo1000Bot.Api.Dialogflow/DialogflowService.cs @@ -20,13 +20,15 @@ public class DialogflowService : MessengerService, I private readonly IUsersService _usersService; private readonly ICustomNotificationsRepository _customNotificationsRepository; + private readonly INotificationsService _notificationsService; public DialogflowService( ILogger log, IConversationService conversationService, IMapper mapper, - IUsersService usersService, - ICustomNotificationsRepository customNotificationsRepository) : base(log, conversationService, mapper) + IUsersService usersService, + ICustomNotificationsRepository customNotificationsRepository, + INotificationsService notificationsService) : base(log, conversationService, mapper) { _usersService = usersService; _customNotificationsRepository = customNotificationsRepository; @@ -35,8 +37,10 @@ public DialogflowService( { { "SaveUser", SaveUser }, { "DeleteUser", DeleteUser }, - { "SaveCustomNotification", SaveCustomNotification } + { "SaveCustomNotification", SaveCustomNotification }, + { "SendToAdmin", SendToAdmin } }; + _notificationsService = notificationsService; } protected override async Task ProcessCommand(Request request, CancellationToken cancellationToken) @@ -102,7 +106,7 @@ internal async Task DeleteUser(Request request, CancellationToken cancellationTo /// private async Task SaveCustomNotification(Request request, CancellationToken cancellationToken) { - var notification = new Notification + var notification = new Notification(NotificationType.Custom) { Payload = new NotificationPayload { @@ -112,4 +116,23 @@ private async Task SaveCustomNotification(Request request, CancellationToken can await _customNotificationsRepository.Save(notification, cancellationToken); } + + /// + /// Action for key "SendToAdmin" + /// + /// + /// + /// + private async Task SendToAdmin(Request request, CancellationToken cancellationToken) + { + var notification = new Notification(NotificationType.Admin) + { + Payload = new NotificationPayload + { + Text = request.Text + } + }; + + await _notificationsService.SendToAdmin(notification, cancellationToken); + } } \ No newline at end of file diff --git a/Dodo1000Bot.Api/Dodo1000Bot.Api.csproj b/Dodo1000Bot.Api/Dodo1000Bot.Api.csproj index faaffe4..a8b6024 100644 --- a/Dodo1000Bot.Api/Dodo1000Bot.Api.csproj +++ b/Dodo1000Bot.Api/Dodo1000Bot.Api.csproj @@ -2,7 +2,7 @@ net6.0 - 1.21.0 + 1.22.0 f449de95-800a-40ef-8716-6e80b7f0977d diff --git a/Dodo1000Bot.Api/Migrations/202308292353_AddColumn_users_IsAdmin.cs b/Dodo1000Bot.Api/Migrations/202308292353_AddColumn_users_IsAdmin.cs new file mode 100644 index 0000000..f9c6ebe --- /dev/null +++ b/Dodo1000Bot.Api/Migrations/202308292353_AddColumn_users_IsAdmin.cs @@ -0,0 +1,21 @@ +using FluentMigrator; + +namespace Dodo1000Bot.Api.Migrations; + +[Migration(202308292353)] +public class AddColumn_Users_IsAdmin: Migration +{ + public override void Up() + { + Execute.Sql + (@" + ALTER TABLE users + ADD COLUMN `IsAdmin` BIT NOT NULL DEFAULT 0; + "); + } + + public override void Down() + { + throw new System.NotImplementedException(); + } +} \ No newline at end of file diff --git a/Dodo1000Bot.Api/Migrations/202310221801_AddColumn_notifications_Type.cs b/Dodo1000Bot.Api/Migrations/202310221801_AddColumn_notifications_Type.cs new file mode 100644 index 0000000..4fef69d --- /dev/null +++ b/Dodo1000Bot.Api/Migrations/202310221801_AddColumn_notifications_Type.cs @@ -0,0 +1,22 @@ +using FluentMigrator; + +namespace Dodo1000Bot.Api.Migrations; + +[Migration(202310221801)] +public class AddColumn_notifications_Type: Migration +{ + public override void Up() + { + Execute.Sql + (@" + ALTER TABLE notifications + ADD COLUMN `Type` TINYINT(2) NOT NULL DEFAULT 0 AFTER `Id`, + ADD INDEX `Type` (`Type` ASC) VISIBLE; + "); + } + + public override void Down() + { + throw new System.NotImplementedException(); + } +} \ No newline at end of file diff --git a/Dodo1000Bot.Messengers.Telegram/TelegramNotifyService.cs b/Dodo1000Bot.Messengers.Telegram/TelegramNotifyService.cs index 37e6a96..0067fdb 100644 --- a/Dodo1000Bot.Messengers.Telegram/TelegramNotifyService.cs +++ b/Dodo1000Bot.Messengers.Telegram/TelegramNotifyService.cs @@ -8,16 +8,17 @@ using Dodo1000Bot.Services; using Microsoft.Extensions.Logging; using Telegram.Bot; +using Telegram.Bot.Types; using Telegram.Bot.Exceptions; using Telegram.Bot.Types.Enums; namespace Dodo1000Bot.Messengers.Telegram; -public class TelegramNotifyService: INotifyService +public class TelegramNotifyService : INotifyService { + private readonly ILogger _log; private readonly IUsersRepository _usersRepository; private readonly ITelegramBotClient _client; - private readonly ILogger _log; public TelegramNotifyService(IUsersRepository usersRepository, ITelegramBotClient client, ILogger log) @@ -37,7 +38,7 @@ public async Task> NotifyAbout(IList users = await _usersRepository.GetUsers(Source.Telegram, cancellationToken); + IList users = await _usersRepository.GetUsers(Source.Telegram, cancellationToken); if (users?.Any() != true) { @@ -54,7 +55,7 @@ public async Task> NotifyAbout(IList> PushNotificationsToUser(IList notifications, User user, CancellationToken cancellationToken) + private async Task> PushNotificationsToUser(IList notifications, Models.Domain.User user, CancellationToken cancellationToken) { var pushedNotifications = new List(); @@ -96,4 +97,22 @@ await _client.SendTextMessageAsync(messengerUserId, notification.Payload.Text, return pushedNotifications; } + + public async Task SendToAdmin(Notification notification, CancellationToken cancellationToken) + { + var admins = await _usersRepository.GetAdmins(Source.Telegram, cancellationToken); + + foreach (var admin in admins) + { + try + { + await _client.SendTextMessageAsync(admin.MessengerUserId, notification.Payload.Text, parseMode: ParseMode.Html, + cancellationToken: cancellationToken); + } + catch (Exception e) + { + _log.LogError(e, "Error while send notification to {MessengerUserId}", admin.MessengerUserId); + } + } + } } \ No newline at end of file diff --git a/Dodo1000Bot.Models/Domain/Notification.cs b/Dodo1000Bot.Models/Domain/Notification.cs index bc077fb..3564b9e 100644 --- a/Dodo1000Bot.Models/Domain/Notification.cs +++ b/Dodo1000Bot.Models/Domain/Notification.cs @@ -2,11 +2,16 @@ { public class Notification { + public Notification(NotificationType type) + { + Type = type; + } public int Id { get; set; } + public NotificationType Type { get; } + public NotificationPayload Payload { get; set; } - public string Distinction => Payload.ToString().ToUpper() - .Replace(" ", string.Empty); + public string Distinction => $"{Payload}"; } } \ No newline at end of file diff --git a/Dodo1000Bot.Models/Domain/NotificationPayload.cs b/Dodo1000Bot.Models/Domain/NotificationPayload.cs index d756419..5d5ab43 100644 --- a/Dodo1000Bot.Models/Domain/NotificationPayload.cs +++ b/Dodo1000Bot.Models/Domain/NotificationPayload.cs @@ -17,7 +17,7 @@ public class NotificationPayload public override string ToString() { - return $"{HappenedAt:d}{Text}"; + return $"{HappenedAt:d}{Text}".ToUpper().Replace(" ", string.Empty); } } } diff --git a/Dodo1000Bot.Models/NotificationType.cs b/Dodo1000Bot.Models/NotificationType.cs new file mode 100644 index 0000000..3eba7b9 --- /dev/null +++ b/Dodo1000Bot.Models/NotificationType.cs @@ -0,0 +1,17 @@ +namespace Dodo1000Bot.Models; + +public enum NotificationType +{ + Custom, + TotalOverall, + TotalAtBrands, + TotalAtCountries, + TotalCountriesAtBrands, + NewCountry, + NewUnit, + OrdersPerMinute, + YearRevenue, + SubscribersCount, + Admin, + Emoji, +} \ No newline at end of file diff --git a/Dodo1000Bot.Services/Interfaces/INotificationsService.cs b/Dodo1000Bot.Services/Interfaces/INotificationsService.cs index 773b861..47c74ad 100644 --- a/Dodo1000Bot.Services/Interfaces/INotificationsService.cs +++ b/Dodo1000Bot.Services/Interfaces/INotificationsService.cs @@ -11,4 +11,6 @@ public interface INotificationsService Task PushNotifications(CancellationToken cancellationToken); Task Delete(int notificationId, CancellationToken cancellationToken); + + Task SendToAdmin(Notification notification, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/Dodo1000Bot.Services/Interfaces/INotifyService.cs b/Dodo1000Bot.Services/Interfaces/INotifyService.cs index 94dd5c8..e11a409 100644 --- a/Dodo1000Bot.Services/Interfaces/INotifyService.cs +++ b/Dodo1000Bot.Services/Interfaces/INotifyService.cs @@ -9,4 +9,6 @@ public interface INotifyService { Task> NotifyAbout(IList notifications, CancellationToken cancellationToken); + + Task SendToAdmin(Notification notification, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/Dodo1000Bot.Services/Interfaces/IUsersRepository.cs b/Dodo1000Bot.Services/Interfaces/IUsersRepository.cs index 451e635..b2550a3 100644 --- a/Dodo1000Bot.Services/Interfaces/IUsersRepository.cs +++ b/Dodo1000Bot.Services/Interfaces/IUsersRepository.cs @@ -15,5 +15,7 @@ public interface IUsersRepository Task Delete(User user, CancellationToken cancellationToken); Task Count(CancellationToken cancellationToken); + + Task> GetAdmins(Source messengerType, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/Dodo1000Bot.Services/NotificationsService.cs b/Dodo1000Bot.Services/NotificationsService.cs index 902c143..8c353b1 100644 --- a/Dodo1000Bot.Services/NotificationsService.cs +++ b/Dodo1000Bot.Services/NotificationsService.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -54,4 +55,19 @@ public async Task Delete(int notificationId, CancellationToken cancellationToken { await _notificationsRepository.Delete(notificationId, cancellationToken); } + + public async Task SendToAdmin(Notification notification, CancellationToken cancellationToken) + { + try + { + IEnumerable tasks = + _notifyServices.Select(s => s.SendToAdmin(notification, cancellationToken)); + + await Task.WhenAll(tasks); + } + catch (Exception e) + { + _logger.LogError(e, "Can't send to admin"); + } + } } \ No newline at end of file diff --git a/Dodo1000Bot.Services/Repositories/CustomNotificationsRepository.cs b/Dodo1000Bot.Services/Repositories/CustomNotificationsRepository.cs index 821c0eb..bcae472 100644 --- a/Dodo1000Bot.Services/Repositories/CustomNotificationsRepository.cs +++ b/Dodo1000Bot.Services/Repositories/CustomNotificationsRepository.cs @@ -2,6 +2,7 @@ using System.Threading; using System.Threading.Tasks; using Dapper; +using Dodo1000Bot.Models; using Dodo1000Bot.Models.Domain; using MySql.Data.MySqlClient; @@ -42,7 +43,7 @@ public async Task Get(int id, CancellationToken cancellationToken) return null; } - var notification = new Notification + var notification = new Notification(NotificationType.Custom) { Id = record.Id, Payload = JsonSerializer.Deserialize(record.Payload) diff --git a/Dodo1000Bot.Services/Repositories/NotificationsRepository.cs b/Dodo1000Bot.Services/Repositories/NotificationsRepository.cs index 76c26c2..84ed2b4 100644 --- a/Dodo1000Bot.Services/Repositories/NotificationsRepository.cs +++ b/Dodo1000Bot.Services/Repositories/NotificationsRepository.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Dapper; +using Dodo1000Bot.Models; using Dodo1000Bot.Models.Domain; using MySql.Data.MySqlClient; @@ -25,9 +26,10 @@ public async Task Save(Notification notification, CancellationToken cancellation var payload = JsonSerializer.Serialize(notification?.Payload); await _connection.ExecuteAsync(new CommandDefinition( - "INSERT IGNORE INTO notifications (Payload, Distinction) VALUES (@payload, @distinction)", + "INSERT IGNORE INTO notifications (Type, Payload, Distinction) VALUES (@type, @payload, @distinction)", new { + notification?.Type, payload, notification?.Distinction }, cancellationToken: cancellationToken)); @@ -36,12 +38,12 @@ await _connection.ExecuteAsync(new CommandDefinition( public async Task> GetNotPushedNotifications(CancellationToken cancellationToken) { var records = await _connection.QueryAsync(new CommandDefinition( - @"SELECT n.Id, n.Payload FROM notifications n + @"SELECT n.Id, n.Type, n.Payload FROM notifications n LEFT JOIN pushed_notifications pn ON n.Id = pn.notificationId WHERE pn.id IS NULL", cancellationToken: cancellationToken)); - var notifications = records.Select(r => new Notification + var notifications = records.Select(r => new Notification(r.Type is NotificationType ? (NotificationType)r.Type : NotificationType.Custom) { Id = r.Id, Payload = JsonSerializer.Deserialize(r.Payload) diff --git a/Dodo1000Bot.Services/Repositories/UsersRepository.cs b/Dodo1000Bot.Services/Repositories/UsersRepository.cs index d02fb97..ef9aea4 100644 --- a/Dodo1000Bot.Services/Repositories/UsersRepository.cs +++ b/Dodo1000Bot.Services/Repositories/UsersRepository.cs @@ -61,5 +61,18 @@ public async Task Count(CancellationToken cancellationToken) return count; } + + public async Task> GetAdmins(Source messengerType, CancellationToken cancellationToken) + { + var users = await _connection.QueryAsync(new CommandDefinition( + @"SELECT Id, MessengerUserId, MessengerType FROM users + WHERE MessengerType = @messengerType AND IsAdmin = 1", + new + { + messengerType + }, cancellationToken: cancellationToken)); + + return users.ToImmutableArray(); + } } } diff --git a/Dodo1000Bot.Services/StatisticsService.cs b/Dodo1000Bot.Services/StatisticsService.cs index bc7467f..6493f07 100644 --- a/Dodo1000Bot.Services/StatisticsService.cs +++ b/Dodo1000Bot.Services/StatisticsService.cs @@ -3,6 +3,7 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; +using Dodo1000Bot.Models; using Dodo1000Bot.Models.Domain; using Dodo1000Bot.Models.RealtimeBoard; using Microsoft.Extensions.Logging; @@ -49,7 +50,7 @@ internal async Task AboutOrdersPerMinute(Statistics statistics, CancellationToke return; } - var notification = new Notification + var notification = new Notification(NotificationType.Emoji) { Payload = new NotificationPayload { @@ -60,7 +61,7 @@ internal async Task AboutOrdersPerMinute(Statistics statistics, CancellationToke await _notificationsService.Save(notification, cancellationToken); - notification = new Notification + notification = new Notification(NotificationType.OrdersPerMinute) { Payload = new NotificationPayload { @@ -85,7 +86,7 @@ internal async Task AboutYearRevenue(Statistics statistics, CancellationToken ca return; } - var notification = new Notification + var notification = new Notification(NotificationType.YearRevenue) { Payload = new NotificationPayload { diff --git a/Dodo1000Bot.Services/UnitsService.cs b/Dodo1000Bot.Services/UnitsService.cs index 6bf44cf..2ef2934 100644 --- a/Dodo1000Bot.Services/UnitsService.cs +++ b/Dodo1000Bot.Services/UnitsService.cs @@ -144,7 +144,7 @@ private async Task AboutTotalOverall(BrandListTotalUnitCountListModel unitsCount return; } - var notification = new Notification + var notification = new Notification(NotificationType.Emoji) { Payload = new NotificationPayload { @@ -155,11 +155,12 @@ private async Task AboutTotalOverall(BrandListTotalUnitCountListModel unitsCount await _notificationsService.Save(notification, cancellationToken); - notification = new Notification + notification = new Notification(NotificationType.TotalOverall) { Payload = new NotificationPayload { - Text = $"Wow! 🎉 \r\nThere are {totalOverall} restaurants of all Dodo brands! 🥳" + Text = $"Wow! 🎉 \r\nThere are {totalOverall} restaurants of all Dodo brands! 🥳 \r\n" + + $"You can see them all on https://realtime.dodobrands.io" } }; @@ -177,18 +178,18 @@ private async Task AboutTotalAtBrands(BrandListTotalUnitCountListModel unitsCoun continue; } - var notification = new Notification + var notification = new Notification(NotificationType.Emoji) { Payload = new NotificationPayload { - Text = $"😮", + Text = "😮", HappenedAt = DateTime.Now } }; await _notificationsService.Save(notification, cancellationToken); - notification = new Notification + notification = new Notification(NotificationType.TotalAtBrands) { Payload = new NotificationPayload { @@ -229,7 +230,7 @@ internal async Task AboutTotalCountriesAtBrands(BrandListTotalUnitCountListModel continue; } - var notification = new Notification + var notification = new Notification(NotificationType.TotalCountriesAtBrands) { Payload = new NotificationPayload { @@ -348,7 +349,7 @@ internal async Task CheckUnitsOfBrandAtCountryAndNotify(Brands brand, int countr foreach (var unit in difference) { - var notification = new Notification + var notification = new Notification(NotificationType.NewUnit) { Payload = new NotificationPayload { @@ -401,7 +402,7 @@ private async Task CheckAndNotify1000(UnitCountModel totalAtCountry, Brands bran totalAtCountry.CountryCode, nameof(ICountriesService)); } - var notification = new Notification + var notification = new Notification(NotificationType.Emoji) { Payload = new NotificationPayload { @@ -412,12 +413,12 @@ private async Task CheckAndNotify1000(UnitCountModel totalAtCountry, Brands bran await _notificationsService.Save(notification, cancellationToken); - notification = new Notification + notification = new Notification(NotificationType.TotalAtCountries) { Payload = new NotificationPayload { Text = - $"Incredible! 🥳 \r\nThere are {totalAtCountry.PizzeriaCount} {brand} in the {countryName}! ❤️‍🔥" + $"Incredible! 🥳 \r\nThere are {totalAtCountry.PizzeriaCount} {brand} restaurants in the {countryName}! ❤️‍🔥" } }; @@ -441,7 +442,7 @@ private async Task CheckDifferenceAndNotify(Brands brand, IEnumerable