add voice service call service and more

This commit is contained in:
2025-11-25 16:49:18 +03:30
parent 60d20a2734
commit 9ba81d944f
49 changed files with 4428 additions and 19 deletions

View File

@@ -0,0 +1,18 @@
namespace GreenHome.VoiceCall.Avanak;
/// <summary>
/// Configuration options for Avanak Voice Call service
/// </summary>
public sealed class AvanakVoiceCallOptions
{
/// <summary>
/// Avanak API base URL
/// </summary>
public string BaseUrl { get; set; } = "https://portal.avanak.ir/Rest";
/// <summary>
/// Avanak authorization token
/// </summary>
public required string Token { get; set; }
}

View File

@@ -0,0 +1,220 @@
using System.Net.Http.Json;
using System.Net.Http;
using System.Text.Json;
using Microsoft.Extensions.Logging;
namespace GreenHome.VoiceCall.Avanak;
/// <summary>
/// Avanak Voice Call service implementation
/// </summary>
public sealed class AvanakVoiceCallService : IVoiceCallService
{
private readonly HttpClient httpClient;
private readonly AvanakVoiceCallOptions options;
private readonly ILogger<AvanakVoiceCallService> logger;
public AvanakVoiceCallService(
HttpClient httpClient,
AvanakVoiceCallOptions options,
ILogger<AvanakVoiceCallService> logger)
{
this.httpClient = httpClient;
this.options = options;
this.logger = logger;
}
/// <inheritdoc/>
public async Task<AccountStatusResponse?> GetAccountStatusAsync(CancellationToken cancellationToken = default)
{
try
{
var response = await httpClient.PostAsync(
"AccountStatus",
null,
cancellationToken);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<AccountStatusResponse>(
cancellationToken: cancellationToken);
return result;
}
catch (HttpRequestException ex)
{
logger.LogError(ex, "Error getting account status");
throw;
}
catch (Exception ex)
{
logger.LogError(ex, "Unexpected error getting account status");
throw;
}
}
/// <inheritdoc/>
public async Task<GenerateTTSResponse?> GenerateTTSAsync(GenerateTTSRequest request, CancellationToken cancellationToken = default)
{
try
{
var response = await httpClient.PostAsJsonAsync(
"GenerateTTS",
request,
cancellationToken);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<GenerateTTSResponse>(
cancellationToken: cancellationToken);
return result;
}
catch (HttpRequestException ex)
{
logger.LogError(ex, "Error generating TTS for text: {Text}", request.Text);
throw;
}
catch (Exception ex)
{
logger.LogError(ex, "Unexpected error generating TTS");
throw;
}
}
/// <inheritdoc/>
public async Task<QuickSendResponse?> QuickSendAsync(QuickSendRequest request, CancellationToken cancellationToken = default)
{
try
{
var response = await httpClient.PostAsJsonAsync(
"QuickSend",
request,
cancellationToken);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<QuickSendResponse>(
cancellationToken: cancellationToken);
return result;
}
catch (HttpRequestException ex)
{
logger.LogError(ex, "Error sending quick voice call to {Recipient}", request.Recipient);
throw;
}
catch (Exception ex)
{
logger.LogError(ex, "Unexpected error sending quick voice call");
throw;
}
}
/// <inheritdoc/>
public async Task<QuickSendWithTTSResponse?> QuickSendWithTTSAsync(QuickSendWithTTSRequest request, CancellationToken cancellationToken = default)
{
try
{
// Build form data according to Avanak API documentation
var formData = new List<KeyValuePair<string, string>>
{
new("Text", request.Text),
new("Number", request.Number)
};
if (request.Vote.HasValue)
{
formData.Add(new KeyValuePair<string, string>("Vote", request.Vote.Value.ToString().ToLower()));
}
if (request.ServerID.HasValue)
{
formData.Add(new KeyValuePair<string, string>("ServerID", request.ServerID.Value.ToString()));
}
if (!string.IsNullOrWhiteSpace(request.CallFromMobile))
{
formData.Add(new KeyValuePair<string, string>("CallFromMobile", request.CallFromMobile));
}
if (request.RecordVoice.HasValue)
{
formData.Add(new KeyValuePair<string, string>("RecordVoice", request.RecordVoice.Value.ToString().ToLower()));
}
if (request.RecordVoiceDuration.HasValue)
{
formData.Add(new KeyValuePair<string, string>("RecordVoiceDuration", request.RecordVoiceDuration.Value.ToString()));
}
var content = new FormUrlEncodedContent(formData);
var response = await httpClient.PostAsync(
"QuickSendWithTTS",
content,
cancellationToken);
response.EnsureSuccessStatusCode();
var jsonString = await response.Content.ReadAsStringAsync(cancellationToken);
var result = JsonSerializer.Deserialize<QuickSendWithTTSResponse>(jsonString, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
if (result != null && !result.IsSuccess)
{
logger.LogWarning(
"QuickSendWithTTS failed with ReturnValue={ReturnValue} for Number={Number}. " +
"Error codes: -25=QuickSend disabled, 0=No permission/demo user, -2=Wrong number, " +
"-3=Insufficient credit, -5=Text too long (>1000 chars), -6=Invalid send time, " +
"-7/-9=TTS generation error, -8=Text too short (<3 sec), -10=Empty text, " +
"-11=Duplicate send limit, -71=Invalid record duration, -72=No record permission, -111=Too Many Requests",
result.ReturnValue, request.Number);
}
return result;
}
catch (HttpRequestException ex)
{
logger.LogError(ex, "Error sending quick voice call with TTS to {Number}", request.Number);
throw;
}
catch (Exception ex)
{
logger.LogError(ex, "Unexpected error sending quick voice call with TTS");
throw;
}
}
/// <inheritdoc/>
public async Task<GetQuickSendResponse?> GetQuickSendStatusAsync(GetQuickSendRequest request, CancellationToken cancellationToken = default)
{
try
{
var response = await httpClient.PostAsJsonAsync(
"GetQuickSend",
request,
cancellationToken);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<GetQuickSendResponse>(
cancellationToken: cancellationToken);
return result;
}
catch (HttpRequestException ex)
{
logger.LogError(ex, "Error getting quick send status for {QuickSendId}", request.QuickSendId);
throw;
}
catch (Exception ex)
{
logger.LogError(ex, "Unexpected error getting quick send status");
throw;
}
}
}

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
<PackageReference Include="System.Text.Json" Version="9.0.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,47 @@
namespace GreenHome.VoiceCall.Avanak;
/// <summary>
/// Interface for Avanak Voice Call service
/// </summary>
public interface IVoiceCallService
{
/// <summary>
/// Gets account status
/// </summary>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Account status response</returns>
Task<AccountStatusResponse?> GetAccountStatusAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Generates TTS (Text-to-Speech) audio from text
/// </summary>
/// <param name="request">TTS generation request</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>TTS generation response</returns>
Task<GenerateTTSResponse?> GenerateTTSAsync(GenerateTTSRequest request, CancellationToken cancellationToken = default);
/// <summary>
/// Sends quick voice call with uploaded audio file
/// </summary>
/// <param name="request">Quick send request</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Quick send response</returns>
Task<QuickSendResponse?> QuickSendAsync(QuickSendRequest request, CancellationToken cancellationToken = default);
/// <summary>
/// Sends quick voice call with TTS (Text-to-Speech)
/// </summary>
/// <param name="request">Quick send with TTS request</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Quick send with TTS response</returns>
Task<QuickSendWithTTSResponse?> QuickSendWithTTSAsync(QuickSendWithTTSRequest request, CancellationToken cancellationToken = default);
/// <summary>
/// Gets quick send status
/// </summary>
/// <param name="request">Get quick send request</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Quick send status response</returns>
Task<GetQuickSendResponse?> GetQuickSendStatusAsync(GetQuickSendRequest request, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,263 @@
using System.Text.Json.Serialization;
namespace GreenHome.VoiceCall.Avanak;
/// <summary>
/// Request model for AccountStatus method
/// </summary>
public sealed class AccountStatusRequest
{
// No parameters needed for AccountStatus
}
/// <summary>
/// Response model for AccountStatus method
/// </summary>
public sealed class AccountStatusResponse
{
[JsonPropertyName("status")]
public bool Status { get; set; }
[JsonPropertyName("message")]
public string? Message { get; set; }
[JsonPropertyName("data")]
public AccountStatusData? Data { get; set; }
}
/// <summary>
/// Account status data
/// </summary>
public sealed class AccountStatusData
{
[JsonPropertyName("balance")]
public decimal? Balance { get; set; }
[JsonPropertyName("credit")]
public decimal? Credit { get; set; }
[JsonPropertyName("expireDate")]
public string? ExpireDate { get; set; }
}
/// <summary>
/// Request model for GenerateTTS method
/// </summary>
public sealed class GenerateTTSRequest
{
/// <summary>
/// Text to convert to speech
/// </summary>
[JsonPropertyName("text")]
public required string Text { get; set; }
/// <summary>
/// Voice type (optional)
/// </summary>
[JsonPropertyName("voiceType")]
public string? VoiceType { get; set; }
}
/// <summary>
/// Response model for GenerateTTS method
/// </summary>
public sealed class GenerateTTSResponse
{
[JsonPropertyName("status")]
public bool Status { get; set; }
[JsonPropertyName("message")]
public string? Message { get; set; }
[JsonPropertyName("data")]
public GenerateTTSData? Data { get; set; }
}
/// <summary>
/// GenerateTTS data
/// </summary>
public sealed class GenerateTTSData
{
[JsonPropertyName("messageId")]
public string? MessageId { get; set; }
[JsonPropertyName("audioUrl")]
public string? AudioUrl { get; set; }
[JsonPropertyName("duration")]
public int? Duration { get; set; }
}
/// <summary>
/// Request model for QuickSend method
/// </summary>
public sealed class QuickSendRequest
{
/// <summary>
/// Recipient phone number
/// </summary>
[JsonPropertyName("recipient")]
public required string Recipient { get; set; }
/// <summary>
/// Message ID (uploaded audio file ID)
/// </summary>
[JsonPropertyName("messageId")]
public required string MessageId { get; set; }
}
/// <summary>
/// Response model for QuickSend method
/// </summary>
public sealed class QuickSendResponse
{
[JsonPropertyName("status")]
public bool Status { get; set; }
[JsonPropertyName("message")]
public string? Message { get; set; }
[JsonPropertyName("data")]
public QuickSendData? Data { get; set; }
}
/// <summary>
/// QuickSend data
/// </summary>
public sealed class QuickSendData
{
[JsonPropertyName("quickSendId")]
public string? QuickSendId { get; set; }
[JsonPropertyName("trackingId")]
public string? TrackingId { get; set; }
}
/// <summary>
/// Request model for QuickSendWithTTS method
/// </summary>
public sealed class QuickSendWithTTSRequest
{
/// <summary>
/// Text to convert to speech and send (required)
/// </summary>
[JsonPropertyName("Text")]
public required string Text { get; set; }
/// <summary>
/// Phone number (required)
/// </summary>
[JsonPropertyName("Number")]
public required string Number { get; set; }
/// <summary>
/// Enable voting option (optional)
/// </summary>
[JsonPropertyName("Vote")]
public bool? Vote { get; set; }
/// <summary>
/// Server ID (optional, default: 0)
/// </summary>
[JsonPropertyName("ServerID")]
public int? ServerID { get; set; }
/// <summary>
/// Mobile number to add at the end of voice (optional)
/// </summary>
[JsonPropertyName("CallFromMobile")]
public string? CallFromMobile { get; set; }
/// <summary>
/// Record voice (optional)
/// </summary>
[JsonPropertyName("RecordVoice")]
public bool? RecordVoice { get; set; }
/// <summary>
/// Record voice duration (optional)
/// </summary>
[JsonPropertyName("RecordVoiceDuration")]
public short? RecordVoiceDuration { get; set; }
}
/// <summary>
/// Response model for QuickSendWithTTS method
/// ReturnValue > 0 means success and contains the quickSendId
/// </summary>
public sealed class QuickSendWithTTSResponse
{
/// <summary>
/// Return value: > 0 means success (contains quickSendId), negative values indicate errors
/// </summary>
[JsonPropertyName("ReturnValue")]
public int ReturnValue { get; set; }
/// <summary>
/// QuickSendId (when ReturnValue > 0)
/// </summary>
public long QuickSendId => ReturnValue > 0 ? ReturnValue : 0;
/// <summary>
/// Indicates if the operation was successful
/// </summary>
public bool IsSuccess => ReturnValue > 0;
}
/// <summary>
/// Request model for GetQuickSend method
/// </summary>
public sealed class GetQuickSendRequest
{
/// <summary>
/// QuickSend ID or Tracking ID
/// </summary>
[JsonPropertyName("quickSendId")]
public required string QuickSendId { get; set; }
}
/// <summary>
/// Response model for GetQuickSend method
/// </summary>
public sealed class GetQuickSendResponse
{
[JsonPropertyName("status")]
public bool Status { get; set; }
[JsonPropertyName("message")]
public string? Message { get; set; }
[JsonPropertyName("data")]
public GetQuickSendData? Data { get; set; }
}
/// <summary>
/// GetQuickSend data
/// </summary>
public sealed class GetQuickSendData
{
[JsonPropertyName("quickSendId")]
public string? QuickSendId { get; set; }
[JsonPropertyName("trackingId")]
public string? TrackingId { get; set; }
[JsonPropertyName("recipient")]
public string? Recipient { get; set; }
[JsonPropertyName("status")]
public string? Status { get; set; }
[JsonPropertyName("deliveryStatus")]
public string? DeliveryStatus { get; set; }
[JsonPropertyName("sentAt")]
public string? SentAt { get; set; }
[JsonPropertyName("deliveredAt")]
public string? DeliveredAt { get; set; }
[JsonPropertyName("duration")]
public int? Duration { get; set; }
}

View File

@@ -0,0 +1,110 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace GreenHome.VoiceCall.Avanak;
/// <summary>
/// Extension methods for registering Avanak Voice Call service
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Adds Avanak Voice Call service to the service collection
/// </summary>
/// <param name="services">The service collection</param>
/// <param name="configuration">Configuration section for AvanakVoiceCall</param>
/// <returns>The service collection for chaining</returns>
public static IServiceCollection AddAvanakVoiceCall(
this IServiceCollection services,
IConfiguration configuration)
{
return services.AddAvanakVoiceCall(configuration.GetSection("AvanakVoiceCall"));
}
/// <summary>
/// Adds Avanak Voice Call service to the service collection
/// </summary>
/// <param name="services">The service collection</param>
/// <param name="configurationSection">Configuration section for AvanakVoiceCall</param>
/// <returns>The service collection for chaining</returns>
public static IServiceCollection AddAvanakVoiceCall(
this IServiceCollection services,
IConfigurationSection? configurationSection = null)
{
// Configure options
AvanakVoiceCallOptions? options = null;
if (configurationSection != null)
{
options = configurationSection.Get<AvanakVoiceCallOptions>();
}
if (options == null)
{
throw new InvalidOperationException(
"AvanakVoiceCall configuration section is missing. " +
"Please add 'AvanakVoiceCall' section to your appsettings.json with 'Token' property.");
}
if (string.IsNullOrWhiteSpace(options.Token))
{
throw new InvalidOperationException(
"AvanakVoiceCall Token is required.");
}
// Register options as singleton
services.AddSingleton(options);
// Register HttpClient and service
services.AddHttpClient<IVoiceCallService, AvanakVoiceCallService>(client =>
{
// Ensure BaseUrl ends with / for proper relative path handling
var baseUrl = options.BaseUrl.TrimEnd('/') + "/";
client.BaseAddress = new Uri(baseUrl);
// Note: Content-Type will be set automatically by HttpClient based on content type (JSON or FormUrlEncoded)
client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", options.Token);
client.Timeout = TimeSpan.FromSeconds(30);
});
return services;
}
/// <summary>
/// Adds Avanak Voice Call service to the service collection with explicit options
/// </summary>
/// <param name="services">The service collection</param>
/// <param name="configureOptions">Action to configure options</param>
/// <returns>The service collection for chaining</returns>
public static IServiceCollection AddAvanakVoiceCall(
this IServiceCollection services,
Action<AvanakVoiceCallOptions> configureOptions)
{
var options = new AvanakVoiceCallOptions
{
Token = string.Empty // Will be set by configureOptions
};
configureOptions(options);
if (string.IsNullOrWhiteSpace(options.Token))
{
throw new InvalidOperationException(
"AvanakVoiceCall Token is required.");
}
// Register options as singleton
services.AddSingleton(options);
// Register HttpClient and service
services.AddHttpClient<IVoiceCallService, AvanakVoiceCallService>(client =>
{
// Ensure BaseUrl ends with / for proper relative path handling
var baseUrl = options.BaseUrl.TrimEnd('/') + "/";
client.BaseAddress = new Uri(baseUrl);
// Note: Content-Type will be set automatically by HttpClient based on content type (JSON or FormUrlEncoded)
client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", options.Token);
client.Timeout = TimeSpan.FromSeconds(30);
});
return services;
}
}