Add AMI integration

This commit is contained in:
snow flurry 2023-10-21 13:46:56 -07:00
parent 69c9a0333c
commit 4268e4d7d9
9 changed files with 109 additions and 4 deletions

View file

@ -53,5 +53,7 @@ namespace PhoneToolMX.Models.ViewModels
HoldMusic = extEnt.HoldMusic?.Id,
};
}
// TODO: fix hack
public string NotifyOnChange() => (ExtId == 0 ? Id + 1000 : ExtId).ToString();
}
}

View file

@ -6,10 +6,12 @@ namespace PhoneToolMX.Models.ViewModels
public interface IViewModel
{
public int? Id { get; set; }
public IOwnedModel ToEntity(PTMXContext ctx);
public IOwnedModel ToEntity(PTMXContext ctx, IOwnedModel current);
public IViewModel FromEntity(IOwnedModel entity);
public string NotifyOnChange();
}
}

View file

@ -8,7 +8,7 @@ namespace PhoneToolMX.Models.ViewModels
public class PhoneVM : IViewModel
{
public int? Id { get; set; }
[Header("MAC Address")]
[Required]
[RegularExpression("(?:[0-9a-fA-F]{2}[-:]?){6}", ErrorMessage = "Must be of the form XX:XX:XX:XX:XX:XX, XX-XX-XX-XX-XX-XX, or XXXXXXXXXXXX")]
@ -66,5 +66,10 @@ namespace PhoneToolMX.Models.ViewModels
PrimaryExtension = phoneEnt.PrimaryExtension?.Id,
};
}
// a bit of a hack, but we can just choose any extension that belongs to this phone
public string NotifyOnChange() => Extensions.Count > 0
? (Extensions.First() + 1000).ToString()
: null;
}
}

View file

@ -1,3 +1,4 @@
using AsterNET.Manager;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
@ -5,6 +6,7 @@ using PhoneToolMX.Data;
using PhoneToolMX.Helpers;
using PhoneToolMX.Models;
using PhoneToolMX.Models.ViewModels;
using PhoneToolMX.Services;
using System.Security.Authentication;
using System.Security.Claims;
@ -118,7 +120,7 @@ namespace PhoneToolMX.Controllers
SetMessage(
FormMessageType.Success,
$"{typeof(T).Name} {GetFriendlyName(new TViewModel().FromEntity(entity.Entity))} was created."
$"{typeof(T).Name} {GetFriendlyName(entity.Entity)} was created."
);
return RedirectToAction("Edit", new { id = entity.Entity.Id });
@ -170,9 +172,22 @@ namespace PhoneToolMX.Controllers
_context.Set<T>().Update(currentModel);
await _context.SaveChangesAsync();
// Try to notify the relevant endpoint to update its config
var amiStatus = "";
if (HttpContext.RequestServices.GetService<IAsteriskManager>() is {} ami) {
try { await ami.SendNotifyAsync(vm.NotifyOnChange());
amiStatus = "Your phone's configuration should be updated shortly.";
}
catch (BadResponseException e) { amiStatus = $"You'll need to manually update your phone settings (AMI Response: {e.Message})."; }
catch (ManagerException e) { amiStatus = $"You'll need to manually update your phone settings (AMI Client Error: {e.Message})."; }
catch (Exception e) {
amiStatus = $"You'll need to manually update your phone settings (Unknown Error: {e.Message}).";
}
}
SetMessage(
FormMessageType.Success,
$"{typeof(T).Name} {GetFriendlyName(vm)} was updated."
$"{typeof(T).Name} {GetFriendlyName(currentModel)} was updated.{" " + amiStatus}"
);
return RedirectToAction("Edit", new { id = entity.Id });

View file

@ -10,6 +10,7 @@ using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using NuGet.Packaging;
using PhoneToolMX.Models;
using PhoneToolMX.Services;
using System.Net;
using System.Security.Authentication;
using System.Security.Claims;
@ -112,6 +113,11 @@ builder.Services.AddAuthorization(opts =>
policy => policy.RequireRole("Administrator"));
});
// AMI for PJSIP notifications
if (builder.Configuration.GetSection("ami").Exists()) {
builder.Services.AddSingleton<IAsteriskManager, AsteriskManager>();
}
var app = builder.Build();
// Configure the HTTP request pipeline.

View file

@ -0,0 +1,36 @@
using System.Collections;
using System.Net.Security;
using System.Net.Sockets;
using System.Text;
using AsterNET.Manager;
namespace PhoneToolMX.Services
{
public class AsteriskManager : IAsteriskManager
{
private IConfigurationSection amiConf;
private ManagerConnection conn;
public AsteriskManager(IConfiguration config)
{
amiConf = config.GetRequiredSection("ami");
conn = new ManagerConnection(amiConf["host"],
int.Parse(amiConf["port"]),
amiConf["username"],
amiConf["secret"]);
conn.Login();
}
/// <summary>
/// Sends a <c>polycom-check-cfg</c> notification to the requested PJSIP endpoint.
/// </summary>
/// <param name="endpoint">The PJSIP endpoint to notify, without the <c>PJSIP/</c> prefix.</param>
/// <exception cref="BadResponseException">If the request fails, this exception will be thrown.</exception>
public async Task SendNotifyAsync(string endpoint)
{
var response = await conn.SendActionAsync(new PJSIPNotifyAction(endpoint, "polycom-check-cfg"));
if (!response.IsSuccess()) {
throw new BadResponseException(response.Message);
}
}
}
}

View file

@ -0,0 +1,7 @@
namespace PhoneToolMX.Services
{
public class BadResponseException : Exception
{
public BadResponseException(string message) : base(message) {}
}
}

View file

@ -0,0 +1,7 @@
namespace PhoneToolMX.Services
{
public interface IAsteriskManager
{
public Task SendNotifyAsync(string endpoint);
}
}

View file

@ -0,0 +1,25 @@
namespace PhoneToolMX.Services
{
public class PJSIPNotifyAction : AsterNET.Manager.Action.ManagerAction
{
public override string Action => "PJSIPNotify";
/// <summary>
/// The endpoint to which to send the NOTIFY.
/// </summary>
public string Endpoint { get; set; }
/// <summary>
/// The config section name from <c>pjsip_notify.conf</c> to use.
/// One of Option or Variable must be specified.
/// </summary>
public string Option { get; set; }
public PJSIPNotifyAction(string endpoint, string option)
{
Endpoint = endpoint;
Option = option;
}
}
}