447 lines
22 KiB
C#
447 lines
22 KiB
C#
using System.CommandLine;
|
|
using System.CommandLine.Parsing;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.CLI.Commands;
|
|
|
|
public static class NotificationCommands
|
|
{
|
|
/// <summary>
|
|
/// Builds the <c>notification</c> command group with sub-commands for managing notification lists and SMTP configuration.
|
|
/// </summary>
|
|
/// <param name="urlOption">Global <c>--url</c> option for the management API endpoint.</param>
|
|
/// <param name="formatOption">Global <c>--format</c> option for output format.</param>
|
|
/// <param name="usernameOption">Global <c>--username</c> option for authentication.</param>
|
|
/// <param name="passwordOption">Global <c>--password</c> option for authentication.</param>
|
|
/// <returns>The configured <c>notification</c> command with all sub-commands registered.</returns>
|
|
public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
|
|
{
|
|
var command = new Command("notification") { Description = "Manage notification lists" };
|
|
|
|
command.Add(BuildList(urlOption, formatOption, usernameOption, passwordOption));
|
|
command.Add(BuildGet(urlOption, formatOption, usernameOption, passwordOption));
|
|
command.Add(BuildCreate(urlOption, formatOption, usernameOption, passwordOption));
|
|
command.Add(BuildUpdate(urlOption, formatOption, usernameOption, passwordOption));
|
|
command.Add(BuildDelete(urlOption, formatOption, usernameOption, passwordOption));
|
|
command.Add(BuildSmtp(urlOption, formatOption, usernameOption, passwordOption));
|
|
command.Add(BuildSms(urlOption, formatOption, usernameOption, passwordOption));
|
|
|
|
return command;
|
|
}
|
|
|
|
// ------------------------------------------------------------------------
|
|
// Notification list create/update options (static so the parsed values can
|
|
// be read back both from the SetAction and from the testable Build* helpers).
|
|
// ------------------------------------------------------------------------
|
|
private static readonly Option<string> ListCreateNameOption =
|
|
new("--name") { Description = "Notification list name", Required = true };
|
|
private static readonly Option<string?> ListCreateEmailsOption =
|
|
new("--emails") { Description = "Comma-separated recipient emails (required for --type email; rejected for --type sms)" };
|
|
private static readonly Option<string?> ListCreatePhonesOption =
|
|
new("--phones") { Description = "Comma-separated recipient phone numbers in E.164 (required for --type sms; rejected for --type email)" };
|
|
private static readonly Option<string> ListCreateTypeOption = CreateListTypeOption();
|
|
|
|
private static readonly Option<int> ListUpdateIdOption =
|
|
new("--id") { Description = "Notification list ID", Required = true };
|
|
private static readonly Option<string> ListUpdateNameOption =
|
|
new("--name") { Description = "List name", Required = true };
|
|
private static readonly Option<string?> ListUpdateEmailsOption =
|
|
new("--emails") { Description = "Comma-separated recipient emails (required for --type email; rejected for --type sms)" };
|
|
private static readonly Option<string?> ListUpdatePhonesOption =
|
|
new("--phones") { Description = "Comma-separated recipient phone numbers in E.164 (required for --type sms; rejected for --type email)" };
|
|
private static readonly Option<string> ListUpdateTypeOption = CreateListTypeOption();
|
|
|
|
private static Option<string> CreateListTypeOption()
|
|
{
|
|
var option = new Option<string>("--type")
|
|
{
|
|
Description = "Delivery channel: email or sms (case-insensitive; default email)",
|
|
DefaultValueFactory = _ => "email",
|
|
};
|
|
return option;
|
|
}
|
|
|
|
private static Command BuildGet(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
|
|
{
|
|
var idOption = new Option<int>("--id") { Description = "Notification list ID", Required = true };
|
|
var cmd = new Command("get") { Description = "Get a notification list by ID" };
|
|
cmd.Add(idOption);
|
|
cmd.SetAction(async (ParseResult result) =>
|
|
{
|
|
var id = result.GetValue(idOption);
|
|
return await CommandHelpers.ExecuteCommandAsync(
|
|
result, urlOption, formatOption, usernameOption, passwordOption, new GetNotificationListCommand(id));
|
|
});
|
|
return cmd;
|
|
}
|
|
|
|
private static Command BuildUpdate(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
|
|
{
|
|
var cmd = new Command("update") { Description = "Update a notification list" };
|
|
cmd.Add(ListUpdateIdOption);
|
|
cmd.Add(ListUpdateNameOption);
|
|
cmd.Add(ListUpdateTypeOption);
|
|
cmd.Add(ListUpdateEmailsOption);
|
|
cmd.Add(ListUpdatePhonesOption);
|
|
cmd.SetAction(async (ParseResult result) =>
|
|
{
|
|
UpdateNotificationListCommand command;
|
|
try
|
|
{
|
|
command = BuildUpdateNotificationListCommand(result);
|
|
}
|
|
catch (ArgumentException ex)
|
|
{
|
|
OutputFormatter.WriteError(ex.Message, "INVALID_ARGUMENT");
|
|
return 1;
|
|
}
|
|
|
|
return await CommandHelpers.ExecuteCommandAsync(
|
|
result, urlOption, formatOption, usernameOption, passwordOption, command);
|
|
});
|
|
return cmd;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds the <see cref="UpdateNotificationListCommand"/> from a parsed
|
|
/// <c>notification list update</c> invocation, applying the channel-aware
|
|
/// recipient validation. Throws <see cref="ArgumentException"/> when the
|
|
/// channel and the supplied recipient flags are inconsistent.
|
|
/// </summary>
|
|
/// <param name="result">The parsed command-line result.</param>
|
|
/// <returns>A validated <see cref="UpdateNotificationListCommand"/>.</returns>
|
|
internal static UpdateNotificationListCommand BuildUpdateNotificationListCommand(ParseResult result)
|
|
{
|
|
var id = result.GetValue(ListUpdateIdOption);
|
|
var name = result.GetValue(ListUpdateNameOption)!;
|
|
var (type, emails, phones) = ResolveListChannel(
|
|
result.GetValue(ListUpdateTypeOption)!,
|
|
result.GetValue(ListUpdateEmailsOption),
|
|
result.GetValue(ListUpdatePhonesOption));
|
|
return new UpdateNotificationListCommand(id, name, emails, type, phones);
|
|
}
|
|
|
|
private static Command BuildSmtp(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
|
|
{
|
|
var group = new Command("smtp") { Description = "Manage SMTP configuration" };
|
|
|
|
var listCmd = new Command("list") { Description = "List SMTP configurations" };
|
|
listCmd.SetAction(async (ParseResult result) =>
|
|
{
|
|
return await CommandHelpers.ExecuteCommandAsync(
|
|
result, urlOption, formatOption, usernameOption, passwordOption, new ListSmtpConfigsCommand());
|
|
});
|
|
group.Add(listCmd);
|
|
|
|
var updateCmd = new Command("update") { Description = "Update SMTP configuration" };
|
|
updateCmd.Add(SmtpIdOption);
|
|
updateCmd.Add(SmtpServerOption);
|
|
updateCmd.Add(SmtpPortOption);
|
|
updateCmd.Add(SmtpAuthModeOption);
|
|
updateCmd.Add(SmtpFromOption);
|
|
updateCmd.Add(SmtpTlsModeOption);
|
|
updateCmd.Add(SmtpCredentialsOption);
|
|
updateCmd.Add(SmtpOAuth2AuthorityOption);
|
|
updateCmd.Add(SmtpOAuth2ScopeOption);
|
|
updateCmd.Add(SmtpTransportOption);
|
|
updateCmd.SetAction(async (ParseResult result) =>
|
|
{
|
|
return await CommandHelpers.ExecuteCommandAsync(
|
|
result, urlOption, formatOption, usernameOption, passwordOption,
|
|
BuildUpdateSmtpConfigCommand(result));
|
|
});
|
|
group.Add(updateCmd);
|
|
|
|
return group;
|
|
}
|
|
|
|
// SMTP update options are static so the parsed values can be read back both
|
|
// from the SetAction and from BuildUpdateSmtpConfigCommand (used by tests).
|
|
private static readonly Option<int> SmtpIdOption =
|
|
new("--id") { Description = "SMTP config ID", Required = true };
|
|
private static readonly Option<string> SmtpServerOption =
|
|
new("--server") { Description = "SMTP server", Required = true };
|
|
private static readonly Option<int> SmtpPortOption =
|
|
new("--port") { Description = "SMTP port", Required = true };
|
|
private static readonly Option<string> SmtpAuthModeOption =
|
|
new("--auth-mode") { Description = "Auth mode", Required = true };
|
|
private static readonly Option<string> SmtpFromOption =
|
|
new("--from-address") { Description = "From email address", Required = true };
|
|
private static readonly Option<string?> SmtpTlsModeOption = CreateTlsModeOption();
|
|
private static readonly Option<string?> SmtpCredentialsOption =
|
|
new("--credentials")
|
|
{
|
|
Description = "SMTP credentials — 'username:password' for Basic, or client secret " +
|
|
"for OAuth2 (optional; preserves existing if omitted)",
|
|
};
|
|
private static readonly Option<string?> SmtpOAuth2AuthorityOption =
|
|
new("--oauth2-authority")
|
|
{
|
|
Description = "OAuth2 token-endpoint URL (optional; a '{tenant}' placeholder is " +
|
|
"substituted from the credential; preserves existing / M365 default if omitted)",
|
|
};
|
|
private static readonly Option<string?> SmtpOAuth2ScopeOption =
|
|
new("--oauth2-scope")
|
|
{
|
|
Description = "OAuth2 scope requested from the token endpoint " +
|
|
"(optional; preserves existing / M365 default if omitted)",
|
|
};
|
|
|
|
private static readonly Option<string?> SmtpTransportOption = CreateTransportOption();
|
|
|
|
private static Option<string?> CreateTransportOption()
|
|
{
|
|
var option = new Option<string?>("--transport")
|
|
{
|
|
Description = "Email transport: Smtp or Ews (optional; preserves existing if omitted). " +
|
|
"Under Ews, --server is the full EWS URL (https://…/ews/exchange.asmx), " +
|
|
"--auth-mode must be basic, and --port/--tls-mode are unused.",
|
|
};
|
|
option.AcceptOnlyFromAmong("Smtp", "Ews");
|
|
return option;
|
|
}
|
|
|
|
private static Option<string?> CreateTlsModeOption()
|
|
{
|
|
var option = new Option<string?>("--tls-mode")
|
|
{
|
|
Description = "TLS mode: None, StartTLS, or SSL (optional; preserves existing if omitted)",
|
|
};
|
|
option.AcceptOnlyFromAmong("None", "StartTLS", "SSL");
|
|
return option;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds the <see cref="UpdateSmtpConfigCommand"/> from a parsed <c>smtp update</c>
|
|
/// invocation. The optional <c>--tls-mode</c> / <c>--credentials</c> /
|
|
/// <c>--oauth2-authority</c> / <c>--oauth2-scope</c> / <c>--transport</c> flags map to
|
|
/// null when omitted so the server-side handler preserves the existing values.
|
|
/// </summary>
|
|
/// <param name="result">The parsed command-line result from the <c>smtp update</c> invocation.</param>
|
|
/// <returns>An <see cref="UpdateSmtpConfigCommand"/> populated from the parsed result.</returns>
|
|
internal static UpdateSmtpConfigCommand BuildUpdateSmtpConfigCommand(ParseResult result)
|
|
{
|
|
var id = result.GetValue(SmtpIdOption);
|
|
var server = result.GetValue(SmtpServerOption)!;
|
|
var port = result.GetValue(SmtpPortOption);
|
|
var authMode = result.GetValue(SmtpAuthModeOption)!;
|
|
var from = result.GetValue(SmtpFromOption)!;
|
|
var tlsMode = result.GetValue(SmtpTlsModeOption);
|
|
var credentials = result.GetValue(SmtpCredentialsOption);
|
|
var oauth2Authority = result.GetValue(SmtpOAuth2AuthorityOption);
|
|
var oauth2Scope = result.GetValue(SmtpOAuth2ScopeOption);
|
|
var transport = result.GetValue(SmtpTransportOption);
|
|
return new UpdateSmtpConfigCommand(id, server, port, authMode, from, tlsMode, credentials, oauth2Authority, oauth2Scope, transport);
|
|
}
|
|
|
|
private static Command BuildSms(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
|
|
{
|
|
var group = new Command("sms") { Description = "Manage SMS (Twilio) configuration" };
|
|
|
|
var listCmd = new Command("list") { Description = "List SMS configurations (Auth Token shown as a presence flag only)" };
|
|
listCmd.SetAction(async (ParseResult result) =>
|
|
{
|
|
return await CommandHelpers.ExecuteCommandAsync(
|
|
result, urlOption, formatOption, usernameOption, passwordOption, new ListSmsConfigsCommand());
|
|
});
|
|
group.Add(listCmd);
|
|
|
|
var updateCmd = new Command("update") { Description = "Update SMS configuration" };
|
|
updateCmd.Add(SmsIdOption);
|
|
updateCmd.Add(SmsAccountSidOption);
|
|
updateCmd.Add(SmsFromNumberOption);
|
|
updateCmd.Add(SmsMessagingServiceSidOption);
|
|
updateCmd.Add(SmsApiBaseUrlOption);
|
|
updateCmd.Add(SmsAuthTokenOption);
|
|
updateCmd.SetAction(async (ParseResult result) =>
|
|
{
|
|
return await CommandHelpers.ExecuteCommandAsync(
|
|
result, urlOption, formatOption, usernameOption, passwordOption,
|
|
BuildUpdateSmsConfigCommand(result));
|
|
});
|
|
group.Add(updateCmd);
|
|
|
|
return group;
|
|
}
|
|
|
|
// SMS update options are static so the parsed values can be read back both
|
|
// from the SetAction and from BuildUpdateSmsConfigCommand (used by tests).
|
|
private static readonly Option<int> SmsIdOption =
|
|
new("--id") { Description = "SMS config ID", Required = true };
|
|
private static readonly Option<string> SmsAccountSidOption =
|
|
new("--account-sid") { Description = "Twilio Account SID", Required = true };
|
|
private static readonly Option<string?> SmsFromNumberOption =
|
|
new("--from-number")
|
|
{
|
|
Description = "Sender phone number (E.164). Provide this and/or --messaging-service-sid.",
|
|
};
|
|
private static readonly Option<string?> SmsMessagingServiceSidOption =
|
|
new("--messaging-service-sid")
|
|
{
|
|
Description = "Twilio Messaging Service SID (optional; OMITTING IT CLEARS the stored value)",
|
|
};
|
|
private static readonly Option<string?> SmsApiBaseUrlOption =
|
|
new("--api-base-url")
|
|
{
|
|
Description = "API base URL override (optional; preserves existing if omitted)",
|
|
};
|
|
private static readonly Option<string?> SmsAuthTokenOption =
|
|
new("--auth-token")
|
|
{
|
|
Description = "Twilio Auth Token (optional; PRESERVES the stored token if omitted; never printed back)",
|
|
};
|
|
|
|
/// <summary>
|
|
/// Builds the <see cref="UpdateSmsConfigCommand"/> from a parsed <c>sms update</c>
|
|
/// invocation. Note the asymmetric preserve-vs-clear semantics enforced server-side:
|
|
/// omitting <c>--auth-token</c> / <c>--api-base-url</c> maps to null so the handler
|
|
/// PRESERVES the existing values, whereas omitting <c>--messaging-service-sid</c> also
|
|
/// maps to null but the handler OVERWRITES (clears) the stored value.
|
|
/// </summary>
|
|
/// <param name="result">The parsed command-line result from the <c>sms update</c> invocation.</param>
|
|
/// <returns>An <see cref="UpdateSmsConfigCommand"/> populated from the parsed result.</returns>
|
|
internal static UpdateSmsConfigCommand BuildUpdateSmsConfigCommand(ParseResult result)
|
|
{
|
|
var id = result.GetValue(SmsIdOption);
|
|
var accountSid = result.GetValue(SmsAccountSidOption)!;
|
|
var fromNumber = result.GetValue(SmsFromNumberOption);
|
|
var messagingServiceSid = result.GetValue(SmsMessagingServiceSidOption);
|
|
var apiBaseUrl = result.GetValue(SmsApiBaseUrlOption);
|
|
var authToken = result.GetValue(SmsAuthTokenOption);
|
|
// Whole-replace update: the sender identity must be re-supplied. A Twilio config
|
|
// needs a From number and/or a Messaging Service SID, so reject the case where the
|
|
// update would leave neither (mirrors the UI + delivery-adapter validation).
|
|
if (string.IsNullOrWhiteSpace(fromNumber) && string.IsNullOrWhiteSpace(messagingServiceSid))
|
|
{
|
|
throw new ArgumentException(
|
|
"Provide --from-number and/or --messaging-service-sid (at least one sender identity is required).");
|
|
}
|
|
return new UpdateSmsConfigCommand(id, accountSid, fromNumber, messagingServiceSid, apiBaseUrl, authToken);
|
|
}
|
|
|
|
private static Command BuildList(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
|
|
{
|
|
var cmd = new Command("list") { Description = "List all notification lists" };
|
|
cmd.SetAction(async (ParseResult result) =>
|
|
{
|
|
return await CommandHelpers.ExecuteCommandAsync(
|
|
result, urlOption, formatOption, usernameOption, passwordOption, new ListNotificationListsCommand());
|
|
});
|
|
return cmd;
|
|
}
|
|
|
|
private static Command BuildCreate(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
|
|
{
|
|
var cmd = new Command("create") { Description = "Create a notification list" };
|
|
cmd.Add(ListCreateNameOption);
|
|
cmd.Add(ListCreateTypeOption);
|
|
cmd.Add(ListCreateEmailsOption);
|
|
cmd.Add(ListCreatePhonesOption);
|
|
cmd.SetAction(async (ParseResult result) =>
|
|
{
|
|
CreateNotificationListCommand command;
|
|
try
|
|
{
|
|
command = BuildCreateNotificationListCommand(result);
|
|
}
|
|
catch (ArgumentException ex)
|
|
{
|
|
OutputFormatter.WriteError(ex.Message, "INVALID_ARGUMENT");
|
|
return 1;
|
|
}
|
|
|
|
return await CommandHelpers.ExecuteCommandAsync(
|
|
result, urlOption, formatOption, usernameOption, passwordOption, command);
|
|
});
|
|
return cmd;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds the <see cref="CreateNotificationListCommand"/> from a parsed
|
|
/// <c>notification list create</c> invocation, applying the channel-aware
|
|
/// recipient validation. Throws <see cref="ArgumentException"/> when the
|
|
/// channel and the supplied recipient flags are inconsistent.
|
|
/// </summary>
|
|
/// <param name="result">The parsed command-line result.</param>
|
|
/// <returns>A validated <see cref="CreateNotificationListCommand"/>.</returns>
|
|
internal static CreateNotificationListCommand BuildCreateNotificationListCommand(ParseResult result)
|
|
{
|
|
var name = result.GetValue(ListCreateNameOption)!;
|
|
var (type, emails, phones) = ResolveListChannel(
|
|
result.GetValue(ListCreateTypeOption)!,
|
|
result.GetValue(ListCreateEmailsOption),
|
|
result.GetValue(ListCreatePhonesOption));
|
|
return new CreateNotificationListCommand(name, emails, type, phones);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses the <c>--type</c> value (case-insensitive) and validates that the supplied
|
|
/// recipient flags match the channel: <c>email</c> requires <c>--emails</c> and rejects
|
|
/// <c>--phones</c>; <c>sms</c> requires <c>--phones</c> and rejects <c>--emails</c>.
|
|
/// </summary>
|
|
/// <param name="typeRaw">The raw <c>--type</c> value.</param>
|
|
/// <param name="emailsRaw">The raw <c>--emails</c> value, or null when omitted.</param>
|
|
/// <param name="phonesRaw">The raw <c>--phones</c> value, or null when omitted.</param>
|
|
/// <returns>The resolved channel and recipient lists (the off-channel list is empty).</returns>
|
|
/// <exception cref="ArgumentException">Thrown when the type is unknown or the recipient flags are inconsistent with the channel.</exception>
|
|
private static (NotificationType Type, IReadOnlyList<string> Emails, IReadOnlyList<string>? Phones) ResolveListChannel(
|
|
string typeRaw, string? emailsRaw, string? phonesRaw)
|
|
{
|
|
if (!Enum.TryParse<NotificationType>(typeRaw, ignoreCase: true, out var type))
|
|
{
|
|
throw new ArgumentException($"Invalid --type '{typeRaw}'. Expected 'email' or 'sms'.");
|
|
}
|
|
|
|
var emails = SplitRecipients(emailsRaw);
|
|
var phones = SplitRecipients(phonesRaw);
|
|
|
|
if (type == NotificationType.Email)
|
|
{
|
|
if (phones.Count > 0)
|
|
{
|
|
throw new ArgumentException("--phones is not valid for --type email; use --emails.");
|
|
}
|
|
|
|
if (emails.Count == 0)
|
|
{
|
|
throw new ArgumentException("--emails is required for --type email.");
|
|
}
|
|
|
|
return (type, emails, null);
|
|
}
|
|
|
|
// Sms
|
|
if (emails.Count > 0)
|
|
{
|
|
throw new ArgumentException("--emails is not valid for --type sms; use --phones.");
|
|
}
|
|
|
|
if (phones.Count == 0)
|
|
{
|
|
throw new ArgumentException("--phones is required for --type sms.");
|
|
}
|
|
|
|
return (type, Array.Empty<string>(), phones);
|
|
}
|
|
|
|
private static IReadOnlyList<string> SplitRecipients(string? raw) =>
|
|
string.IsNullOrWhiteSpace(raw)
|
|
? Array.Empty<string>()
|
|
: raw.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
|
|
|
|
private static Command BuildDelete(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
|
|
{
|
|
var idOption = new Option<int>("--id") { Description = "Notification list ID", Required = true };
|
|
var cmd = new Command("delete") { Description = "Delete a notification list" };
|
|
cmd.Add(idOption);
|
|
cmd.SetAction(async (ParseResult result) =>
|
|
{
|
|
var id = result.GetValue(idOption);
|
|
return await CommandHelpers.ExecuteCommandAsync(
|
|
result, urlOption, formatOption, usernameOption, passwordOption, new DeleteNotificationListCommand(id));
|
|
});
|
|
return cmd;
|
|
}
|
|
}
|