Compare commits

...

4 Commits

12 changed files with 349 additions and 222 deletions

View File

@ -66,9 +66,10 @@ public class Snapshoter : ISnapshoter
try
{
await process.WaitForExitAsync(timeoutSignal.Token);
} catch (OperationCanceledException)
} catch (OperationCanceledException ex)
{
_logger.LogError("Taking snapshot timed out");
timer.Stop();
_logger.LogError("Taking snapshot timed out after {Elapsed} ms", timer.ElapsedMilliseconds);
process.Kill();
return null;
}

View File

@ -0,0 +1,41 @@
using AutoScan;
using CasaBotApp.TelegramBot;
using Microsoft.Extensions.Logging;
namespace CasaBotApp.Controllers;
public class AutoScanController : IController
{
private readonly BotHandler _botHandler;
private readonly AutoScanApp _autoScanApp;
private readonly ILogger<AutoScanController> _logger;
public AutoScanController(BotHandler botHandler, AutoScanApp autoScanApp, ILogger<AutoScanController> logger)
{
_botHandler = botHandler;
_autoScanApp = autoScanApp;
_logger = logger;
}
public void Register()
{
_autoScanApp.OnScanCompleted = async () =>
{
_logger.LogInformation("Scan completed at {At}", DateTime.Now);
try
{
var images = _autoScanApp.GetLastScanPictures();
if (images.Length == 0)
{
await _botHandler.UpdateText("No images found");
return;
}
await _botHandler.UpdateText($"Scan completed, found {images.Length} images");
await _botHandler.UpdatePhotos(images);
}catch(Exception ex)
{
_logger.LogError(ex, "Error while sending message");
}
};
}
}

View File

@ -0,0 +1,143 @@
using AutoScan;
using AutoScan.Interfaces;
using CasaBotApp.TelegramBot;
using ControlServer;
using Microsoft.Extensions.Logging;
using System.Diagnostics;
using Telegram.Bots.Types;
using BotCommand = CasaBotApp.TelegramBot.BotCommand;
namespace CasaBotApp.Controllers;
public class BotController : IController
{
private readonly BotHandler _botHandler;
private readonly ILogger<BotController> _logger;
private readonly AutoScanApp _autoScanApp;
private readonly ISnapshoter _snapshoter;
private readonly IControlServer _controlServer;
public BotController(BotHandler botHandler, ILogger<BotController> logger, AutoScanApp autoScanApp, ISnapshoter snapshoter, IControlServer controlServer)
{
_botHandler = botHandler;
_logger = logger;
_autoScanApp = autoScanApp;
_snapshoter = snapshoter;
_controlServer = controlServer;
}
public void Register()
{
_logger.LogInformation("Registering bot commands");
var methods = GetType().GetMethods()
.Where(m => m.GetCustomAttributes(typeof(BotCommandAttribute), false).Length > 0)
.ToArray();
foreach (var method in methods)
{
try
{
var attribute = (BotCommandAttribute)method.GetCustomAttributes(typeof(BotCommandAttribute), false)[0];
var command = new BotCommand
{
Command = attribute.Command,
Description = attribute.Description,
Action = method.CreateDelegate<Func<TextMessage, BotCommand, Task>>(this),
};
_botHandler.RegisterCommand(command);
_logger.LogInformation("Registered command: {Command} - {Description}", command.Command, command.Description);
}
catch (Exception e)
{
_logger.LogError(e, "Error registering command {Command}", method.Name);
}
}
HandleReply();
}
[BotCommand("/soyandre", "Soy Andre")]
public async Task HiAndre(TextMessage msg, BotCommand ctx)
{
_logger.LogInformation("Andre stoped by");
await ctx.Responder(msg, "Hola vida, te amo mucho ❤️");
}
[BotCommand("/startScan", "Start a scan of last night images")]
public async Task StartScan(TextMessage msg, BotCommand ctx)
{
await ctx.Responder(msg, "Starting scan 🔍📼");
await _autoScanApp.StartNewScan();
}
[BotCommand("/lastscan", "Send the images from the last scan")]
public async Task LastScan(TextMessage msg, BotCommand ctx)
{
var images = _autoScanApp.GetLastScanPictures();
if (images.Length == 0)
{
await ctx.Responder(msg, "No images found");
return;
}
await _botHandler.SendPhotos(msg.Chat.Id, images);
}
[BotCommand("/now", "Get the current snapshot")]
public async Task CurrentSnapshot(TextMessage msg, BotCommand ctx)
{
var stopwatch = Stopwatch.StartNew();
stopwatch.Start();
var outputPath = await _snapshoter.TakeSnapshot();
stopwatch.Stop();
if (string.IsNullOrEmpty(outputPath))
{
await ctx.Responder(msg, "Error taking snapshot, try later");
return;
}
_ = _botHandler.SendPhoto(msg.Chat.Id, outputPath, $"Snapshot: {DateTime.Now:g} ({stopwatch.ElapsedMilliseconds} ms)");
}
[BotCommand("/disarm", "Disarm the Door Sensor")]
public async Task Disarm(TextMessage msg, BotCommand ctx)
{
await ctx.Responder(msg, "Disarming the door sensor");
_controlServer.RequestDisarm();
}
private void HandleReply()
{
_botHandler.OnReply = async msg =>
{
var originalMsg = msg.ReplyToMessage;
// Check if the original message is a photo and has a caption
if (originalMsg is not PhotoMessage photoMessage || photoMessage.Caption is null)
return;
var videoPath = _autoScanApp.GetVideoPath(photoMessage.Caption);
if (string.IsNullOrEmpty(videoPath))
{
await _botHandler.SendText(msg.Chat.Id, "No video found for this image");
return;
}
await _botHandler.SendVideo(msg.Chat.Id, videoPath);
};
}
[AttributeUsage(AttributeTargets.Method)]
private class BotCommandAttribute : Attribute
{
public string Command { get; }
public string Description { get; }
public BotCommandAttribute(string command, string description)
{
Command = command;
Description = description;
}
}
}

View File

@ -0,0 +1,19 @@
namespace CasaBotApp.Controllers;
public class CasaBotOrchestrator
{
private readonly IEnumerable<IController> _controllers;
public CasaBotOrchestrator(IEnumerable<IController> controllers)
{
_controllers = controllers;
}
public void RegisterControllers()
{
foreach (var controller in _controllers)
{
controller.Register();
}
}
}

View File

@ -0,0 +1,6 @@
namespace CasaBotApp.Controllers;
public interface IController
{
void Register();
}

View File

@ -0,0 +1,56 @@
using AutoScan.Interfaces;
using CasaBotApp.TelegramBot;
using ControlServer;
using Microsoft.Extensions.Logging;
namespace CasaBotApp.Controllers;
public class ServerController : IController
{
private readonly IControlServer _controlServer;
private readonly BotHandler _botHandler;
private readonly ISnapshoter _snapshoter;
private readonly ILogger<ServerController> _logger;
private readonly IShinobiLinkFactory _shinobiLinkFactory;
public ServerController(IControlServer controlServer, ISnapshoter snapshoter, ILogger<ServerController> logger, IShinobiLinkFactory shinobiLinkFactory, BotHandler botHandler)
{
_controlServer = controlServer;
_snapshoter = snapshoter;
_logger = logger;
_shinobiLinkFactory = shinobiLinkFactory;
_botHandler = botHandler;
}
public void Register()
{
_controlServer.OnEvent(async sensorEvent =>
{
var mediaPath = await _snapshoter.TakeSnapshot();
if (string.IsNullOrEmpty(mediaPath))
{
await _botHandler.AlertText("Unauthorized access detected 🚨🚨🚨, but no media available");
return;
}
if (sensorEvent.Type == EventType.Fired)
{
await _botHandler.AlertPhoto(mediaPath,
"Unauthorized access detected 🚨 🚨 🚨",
[
new(OptionType.Url, "Camera Feed", _shinobiLinkFactory.BuildFeedLink()),
new(OptionType.Action, "Authorize", $"authorize-{sensorEvent.EventId}", (_, _ ) =>
{
_logger.LogWarning("Authorizing event {EventId}", sensorEvent.EventId);
_controlServer.AuthorizeEvent(sensorEvent.EventId);
return Task.FromResult("Entrance authorized");
}),
]);
}
if (sensorEvent.Type == EventType.DisarmedEntrance)
{
await _botHandler.UpdateText("Authorize access");
}
});
}
}

View File

@ -0,0 +1,17 @@
using CasaBotApp.Controllers;
using Microsoft.Extensions.DependencyInjection;
namespace CasaBotApp.Extensions;
public static class AlarmBotOrchestration
{
public static void AddCasaBotOrchestration(this IServiceCollection services)
{
services.AddSingleton<CasaBotOrchestrator>();
services.AddTransient<IController, AutoScanController>();
services.AddTransient<IController, BotController>();
services.AddTransient<IController, ServerController>();
}
}

View File

@ -1,172 +0,0 @@
using AutoScan;
using AutoScan.Interfaces;
using CasaBotApp.TelegramBot;
using ControlServer;
using Microsoft.Extensions.Logging;
using System.Diagnostics;
using Telegram.Bots.Types;
using BotCommand = CasaBotApp.TelegramBot.BotCommand;
namespace CasaBotApp.Extensions;
public class AlarmBotOrquestrator
{
private readonly ILogger<AlarmBotOrquestrator> _logger;
private readonly BotHandler _botHandler;
private readonly AutoScanApp _autoScanApp;
private readonly IControlServer _controlServer;
private readonly IShinobiLinkFactory _shinobiLinkFactory;
private ISnapshoter _snapshoter;
public AlarmBotOrquestrator(ILogger<AlarmBotOrquestrator> logger, BotHandler botHandler, AutoScanApp autoScanApp,
IControlServer controlServer, IShinobiLinkFactory shinobiLinkFactory, ISnapshoter snapshoter)
{
_logger = logger;
_botHandler = botHandler;
_autoScanApp = autoScanApp;
_controlServer = controlServer;
_shinobiLinkFactory = shinobiLinkFactory;
_snapshoter = snapshoter;
}
public void RegisterCommands()
{
_botHandler.RegisterCommand(new BotCommand
{
Command = "/soyandre",
Description = "Soy Andre",
Action = async (message, ctx) =>
{
await ctx.Responder(message, "Hola vida, te amo mucho ❤️");
}
});
_botHandler.RegisterCommand(new BotCommand
{
Command = "/startScan",
Description = "Start a scan of last night images",
Action = async (message, ctx) =>
{
await ctx.Responder(message, "Starting scan 🔍📼");
await _autoScanApp.StartNewScan();
}
});
_botHandler.RegisterCommand(new BotCommand
{
Command = "/lastscan",
Description = "Send the images from the last scan",
Action = async (message, ctx) =>
{
var images = _autoScanApp.GetLastScanPictures();
if (images.Length == 0)
{
await ctx.Responder(message, "No images found");
return;
}
await _botHandler.SendPhotos(message.Chat.Id, images);
}
});
_botHandler.RegisterCommand(new BotCommand()
{
Command = "/now",
Description = "Send the current snapshot",
Action = async (msg, ctx) =>
{
var stopwatch = Stopwatch.StartNew();
stopwatch.Start();
var outputPath = await _snapshoter.TakeSnapshot();
stopwatch.Stop();
if (string.IsNullOrEmpty(outputPath))
{
await ctx.Responder(msg, "Error taking snapshot");
return;
}
_ = _botHandler.SendPhoto(msg.Chat.Id, outputPath, "Current snapshot");
_ = _botHandler.SendText(msg.Chat.Id, $"It took {stopwatch.ElapsedMilliseconds} ms to take the picture");
}
});
_botHandler.RegisterCommand(new BotCommand()
{
Command = "/disarm",
Description = "Disarm the Door Sensor",
Action = async (msg, ctx) =>
{
await ctx.Responder(msg, "Disarming the door sensor");
_controlServer.RequestDisarm();
}
});
_botHandler.OnReply = async msg =>
{
var originalMsg = msg.ReplyToMessage;
// Check if the original message is a photo and has a caption
if (originalMsg is not PhotoMessage photoMessage || photoMessage.Caption is null)
return;
var videoPath = _autoScanApp.GetVideoPath(photoMessage.Caption);
if (string.IsNullOrEmpty(videoPath))
{
await _botHandler.SendText(msg.Chat.Id, "No video found for this image");
return;
}
await _botHandler.SendVideo(msg.Chat.Id, videoPath);
};
}
public void RegisterAutoScanApp()
{
_autoScanApp.OnScanCompleted = async () =>
{
_logger.LogInformation("Scan completed at {At}", DateTime.Now);
try
{
var images = _autoScanApp.GetLastScanPictures();
if (images.Length == 0)
{
await _botHandler.UpdateText("No images found");
return;
}
await _botHandler.UpdateText($"Scan completed, found {images.Length} images");
await _botHandler.UpdatePhotos(images);
}catch(Exception ex)
{
_logger.LogError(ex, "Error while sending message");
}
};
}
public void RegisterControlServer()
{
_controlServer.OnEvent(async sensorEvent =>
{
var mediaPath = await _snapshoter.TakeSnapshot();
if (string.IsNullOrEmpty(mediaPath))
{
await _botHandler.AlertText("Unauthorized access detected 🚨🚨🚨, but no media available");
return;
}
if (sensorEvent.Type == EventType.Fired)
{
await _botHandler.AlertPhoto(mediaPath,
"Unauthorized access detected 🚨 🚨 🚨",
[
new(OptionType.Url, "Camera Feed", _shinobiLinkFactory.BuildFeedLink()),
new(OptionType.Action, "Authorize", $"authorize-{sensorEvent.EventId}", (_, _ ) =>
{
_logger.LogWarning("Authorizing event {EventId}", sensorEvent.EventId);
_controlServer.AuthorizeEvent(sensorEvent.EventId);
return Task.FromResult("Entrance authorized");
}),
]);
}
if (sensorEvent.Type == EventType.DisarmedEntrance)
{
await _botHandler.UpdateText("Authorize access");
}
});
}
}

View File

@ -1,5 +1,6 @@
using AutoScan;
using AutoScan.Options;
using CasaBotApp.Controllers;
using CasaBotApp.Extensions;
using CasaBotApp.TelegramBot;
using ControlServer;
@ -40,7 +41,7 @@ hostBuilder.ConfigureServices((_, services) =>
services.AddPolling<BotHandler>();
services.AddSingleton<IUpdateHandler>(sp => sp.GetService<BotHandler>()!);
services.AddTransient<AlarmBotOrquestrator>();
services.AddCasaBotOrchestration();
// To get notifications when a retry is performed
@ -65,16 +66,14 @@ var host = hostBuilder.Build();
var logger = host.Services.GetService<ILogger<Program>>()!;
var autoScanApp = host.Services.GetService<AutoScanApp>()!;
var commandRegister = host.Services.GetRequiredService<AlarmBotOrquestrator>();
var orchestrator = host.Services.GetRequiredService<CasaBotOrchestrator>();
using var cts = new CancellationTokenSource();
_ = autoScanApp.Run(cts.Token);
commandRegister.RegisterAutoScanApp();
commandRegister.RegisterCommands();
commandRegister.RegisterControlServer();
orchestrator.RegisterControllers();
logger.LogInformation("Bot started");
await host.RunAsync(cts.Token);

View File

@ -16,22 +16,21 @@ namespace CasaBotApp.TelegramBot;
public class BotHandler : IUpdateHandler
{
private readonly ILogger<BotHandler> _logger;
private readonly TelegramOptions _telegramOptions;
private readonly List<Chat> _subscribers = [];
private readonly List<Chat> _subscribersAlarm = [];
private readonly Dictionary<string, BotCommand> _commands;
//TODO hacerlo mejor.
private readonly Dictionary<string, CallbackQueueItem> _callbackFunctions = new();
private record CallbackQueueItem(DateTime inserted, Func<string, long, Task<string>> callback);
public Func<TextMessage, Task>? OnReply { get; set; } = null;
public Func<TextMessage, Task>? OnReply { get; set; }
private readonly IBotClient _bot;
public BotHandler(IBotClient bot, IOptions<TelegramOptions> telegramConfiguration, ILogger<BotHandler> logger)
{
_logger = logger;
_telegramOptions = telegramConfiguration.Value;
var telegramOptions = telegramConfiguration.Value;
_bot = bot;
_commands = [];
RegisterCommand(new()
@ -41,28 +40,19 @@ public class BotHandler : IUpdateHandler
Action = RegisterUser,
Responder = Respond
});
{
RegisterCommand(new()
{
Command = "/registeralarm",
Description = "Register to receive alarms",
Action = RegisterUserAlarm,
Responder = Respond
});
}
RegisterCommand(new()
{
Command = "/photo",
Description = "Get a photo",
Action = SendImageTest,
Command = "/registeralarm",
Description = "Register to receive alarms",
Action = RegisterUserAlarm,
Responder = Respond
});
foreach (var subs in _telegramOptions.SubscribedChatIds)
foreach (var subs in telegramOptions.SubscribedChatIds)
{
Subscribe(subs);
}
foreach(var sub in _telegramOptions.SubscribedAlarmsChatIds)
foreach(var sub in telegramOptions.SubscribedAlarmsChatIds)
{
SubscribeAlarm(sub);
}
@ -105,12 +95,13 @@ public class BotHandler : IUpdateHandler
}
/// <summary>
/// Send text message to all subscribers
/// Send a text message to all subscribers
/// </summary>
/// <param name="message"></param>
public Task UpdateText(string message) => UpdateTextInt(_subscribers, message);
public Task AlertText(string message) => UpdateTextInt(_subscribersAlarm, message);
public async Task UpdateTextInt(List<Chat> subscribers,string message)
private async Task UpdateTextInt(List<Chat> subscribers,string message)
{
if (subscribers.Count == 0)
{
@ -137,7 +128,7 @@ public class BotHandler : IUpdateHandler
/// <summary>
/// Send photo to all subscribers
/// Send a photo to all subscribers
/// </summary>
/// <param name="path"></param>
/// <param name="caption">Optional message with photo</param>
@ -145,6 +136,13 @@ public class BotHandler : IUpdateHandler
public Task UpdatePhoto(string path, string? caption = null, IEnumerable<MsgOption>? options = null) =>
UpdatePhotoInt(_subscribers, path, caption, options);
/// <summary>
/// Send a photo to all alert subscribers
/// </summary>
/// <param name="path"></param>
/// <param name="caption"></param>
/// <param name="options"></param>
/// <returns></returns>
public Task AlertPhoto(string path, string? caption = null, IEnumerable<MsgOption>? options = null) =>
UpdatePhotoInt(_subscribersAlarm, path, caption, options);
@ -194,7 +192,7 @@ public class BotHandler : IUpdateHandler
}
}
private async Task HandleQueueExpiration()
private Task HandleQueueExpiration()
{
//remove expired items with more than 3 hs
var expired = _callbackFunctions.Where(x => x.Value.inserted.AddHours(3) < DateTime.Now).ToArray();
@ -203,6 +201,7 @@ public class BotHandler : IUpdateHandler
_callbackFunctions.Remove(item.Key);
_logger.LogDebug("Removed expired callback function {Key}", item.Key);
}
return Task.CompletedTask;
}
@ -251,8 +250,8 @@ public class BotHandler : IUpdateHandler
if (_subscribers.Count < 1) return;
var caches = response.Result.Select(x => (x as PhotoMessage).PhotoSet.LastOrDefault()?.Id).ToList();
var media = caches.Select(x => new CachedPhoto(x)).ToList();
var caches = response.Result.Select(x => (x as PhotoMessage)!.PhotoSet.LastOrDefault()?.Id).ToList();
var media = caches.Select(x => new CachedPhoto(x!)).ToList();
//send to the rest of the subscribers
foreach (var subscriber in _subscribers.Skip(1).ToList())
@ -272,14 +271,6 @@ public class BotHandler : IUpdateHandler
}
}
private async Task SendImageTest(TextMessage msg, BotCommand _)
{
await using var stream = File.OpenRead(@"C:\Users\GuillermoMarcel\Pictures\prueba.jpeg");
var send = new SendPhotoFile(msg.Chat.Id.ToString(), stream);
await _bot.HandleAsync(send);
}
public async Task SendPhoto(long chatId, string content, string caption)
{
await using var stream = File.OpenRead(content);
@ -370,7 +361,15 @@ public class BotHandler : IUpdateHandler
private async Task OnMessage(TextMessage msg)
{
if(!_commands.TryGetValue(msg.Text, out var command))
var cmd = msg.Text;
//Deep link support
// link format: https://t.me/your_bot?start=command
// what we see: "/start command"
if (cmd.StartsWith("/start "))
{
cmd = $"/{cmd[7..]}";
}
if(!_commands.TryGetValue(cmd, out var command))
{
if (msg.ReplyToMessage != null && OnReply is not null)
{
@ -381,12 +380,12 @@ public class BotHandler : IUpdateHandler
return;
}
if (command?.Action is null)
if (command.Action is null)
{
_logger.LogError("Command {Command} has no action", msg.Text);
}
await command!.Action!(msg, command);
await command.Action!(msg, command);
}
private async Task SendHelp(TextMessage msg)
@ -411,7 +410,7 @@ public class BotHandler : IUpdateHandler
_subscribers.Add(msg.Chat);
_logger.LogInformation("User {User} ({id}) registered to receive messages", msg.Chat.FirstName, msg.Chat.Id);
await Respond(msg, "You are registered to receive messages every minute");
await Respond(msg, "You are registered to receive non-alert messages (Nightly scans)");
}
private async Task RegisterUserAlarm(TextMessage msg, BotCommand _)
{
@ -434,6 +433,13 @@ public class BotHandler : IUpdateHandler
_bot.HandleAsync(new SendPhotoFile(chat.Id.ToString(), path));
/// <summary>
/// Entry point for the bot
/// </summary>
/// <param name="bot">The bot client used to receive and send messages</param>
/// <param name="update">Type of update and message info</param>
/// <param name="cst"></param>
/// <returns></returns>
public Task HandleAsync(IBotClient bot, Update update, CancellationToken cst)
{
try

View File

@ -76,8 +76,10 @@ public class ControlServer : IControlServer
}
response.Close();
if (notify)
if (notify && _onEventRecived is not null)
{
//don't await this, we don't care about the result.
//and we don't want to block the server thread.
_ = _onEventRecived?.Invoke(sensorEvent);
}
}
@ -106,8 +108,8 @@ public class ControlServer : IControlServer
writer.Write(message);
}
public record UpdateResponse(bool disarm, Card[] cards);
public record Card(string id, string name);
private record UpdateResponse(bool disarm, Card[] cards);
private record Card(string id, string name);
private bool HandleSensorEvent(SensorEvent se)
{
if (se.Type == EventType.Update && _disarmRequestPending)
@ -118,9 +120,10 @@ public class ControlServer : IControlServer
}
_events.TryGetValue(se.EventId, out var storedEvent);
//New One
if (storedEvent is null)
{
//New One
//ESP does not send this type of event yet
if (se.Type == EventType.DisarmedEntrance)
{
_disarmRequestPending = false;
@ -129,11 +132,18 @@ public class ControlServer : IControlServer
return true;
}
//Alarm is armed and fired.
if (se.Type == EventType.Fired)
{
//Check pending desarmed.
se.Authorization = _disarmRequestPending ? Authorization.Authorized : Authorization.Unauthorized;
_disarmRequestPending = false;
//Check pending disarm request.
if (_disarmRequestPending)
{
se.Authorization = Authorization.Authorized;
se.Type = EventType.DisarmedEntrance;
_disarmRequestPending = false;
return true;
}
se.Authorization = Authorization.Unauthorized;
_events.Add(se.EventId, se);
return true;
}

View File

@ -3,6 +3,7 @@ namespace ControlServer;
public record SensorEvent(long EventId, EventType Type, string Data)
{
public Authorization Authorization { get; set; }
public EventType Type { get; set; } = Type;
};
public enum EventType