mirror of
https://github.com/bitwarden/server
synced 2025-12-06 00:03:34 +00:00
PM-23761 use the auto-reply endpoint in freskdesk to add a reply to a note
This commit is contained in:
@@ -11,6 +11,7 @@
|
|||||||
<ProjectReference Include="..\Core\Core.csproj" />
|
<ProjectReference Include="..\Core\Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="MarkDig" Version="0.41.3" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.2" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ public class BillingSettings
|
|||||||
public virtual string Region { get; set; }
|
public virtual string Region { get; set; }
|
||||||
public virtual string UserFieldName { get; set; }
|
public virtual string UserFieldName { get; set; }
|
||||||
public virtual string OrgFieldName { get; set; }
|
public virtual string OrgFieldName { get; set; }
|
||||||
|
|
||||||
|
public virtual bool RemoveNewlinesInReplies { get; set; } = false;
|
||||||
|
public virtual string AutoReplyFooter { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class OnyxSettings
|
public class OnyxSettings
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ using Bit.Billing.Models;
|
|||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
using Markdig;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
@@ -184,6 +185,52 @@ public class FreshdeskController : Controller
|
|||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("webhook-onyx-ai-reply")]
|
||||||
|
public async Task<IActionResult> PostWebhookOnyxAiReply([FromQuery, Required] string key,
|
||||||
|
[FromBody, Required] FreshdeskOnyxAiWebhookModel model)
|
||||||
|
{
|
||||||
|
// NOTE:
|
||||||
|
// at this time, this endpoint is a duplicate of `webhook-onyx-ai`
|
||||||
|
// eventually, we will merge both endpoints into one webhook for Freshdesk
|
||||||
|
|
||||||
|
// ensure that the key is from Freshdesk
|
||||||
|
if (!IsValidRequestFromFreshdesk(key))
|
||||||
|
{
|
||||||
|
return new BadRequestResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there is no description, then we don't send anything to onyx
|
||||||
|
if (string.IsNullOrEmpty(model.TicketDescriptionText.Trim()))
|
||||||
|
{
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the onyx `answer-with-citation` request
|
||||||
|
var onyxRequestModel = new OnyxAnswerWithCitationRequestModel(model.TicketDescriptionText, _billingSettings.Onyx.PersonaId);
|
||||||
|
var onyxRequest = new HttpRequestMessage(HttpMethod.Post,
|
||||||
|
string.Format("{0}/query/answer-with-citation", _billingSettings.Onyx.BaseUrl))
|
||||||
|
{
|
||||||
|
Content = JsonContent.Create(onyxRequestModel, mediaType: new MediaTypeHeaderValue("application/json")),
|
||||||
|
};
|
||||||
|
var (_, onyxJsonResponse) = await CallOnyxApi<OnyxAnswerWithCitationResponseModel>(onyxRequest);
|
||||||
|
|
||||||
|
// the CallOnyxApi will return a null if we have an error response
|
||||||
|
if (onyxJsonResponse?.Answer == null || !string.IsNullOrEmpty(onyxJsonResponse?.ErrorMsg))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Error getting answer from Onyx AI. Freshdesk model: {model}\r\n Onyx query {query}\r\nresponse: {response}. ",
|
||||||
|
JsonSerializer.Serialize(model),
|
||||||
|
JsonSerializer.Serialize(onyxRequestModel),
|
||||||
|
JsonSerializer.Serialize(onyxJsonResponse));
|
||||||
|
|
||||||
|
return Ok(); // return ok so we don't retry
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the reply to the ticket
|
||||||
|
await AddReplyToTicketAsync(onyxJsonResponse.Answer, model.TicketId);
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
private bool IsValidRequestFromFreshdesk(string key)
|
private bool IsValidRequestFromFreshdesk(string key)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(key)
|
if (string.IsNullOrWhiteSpace(key)
|
||||||
@@ -238,6 +285,43 @@ public class FreshdeskController : Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task AddReplyToTicketAsync(string note, string ticketId)
|
||||||
|
{
|
||||||
|
// if there is no content, then we don't need to add a note
|
||||||
|
if (string.IsNullOrWhiteSpace(note))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert note from markdown to html
|
||||||
|
var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build();
|
||||||
|
note = Markdig.Markdown.ToHtml(note, pipeline);
|
||||||
|
|
||||||
|
// clear out any new lines that Freshdesk doesn't like
|
||||||
|
if (_billingSettings.FreshDesk.RemoveNewlinesInReplies)
|
||||||
|
{
|
||||||
|
note = note.Replace(Environment.NewLine, string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
var replyBody = new FreshdeskReplyRequestModel
|
||||||
|
{
|
||||||
|
Body = $"{note}{_billingSettings.FreshDesk.AutoReplyFooter}",
|
||||||
|
};
|
||||||
|
|
||||||
|
var replyRequest = new HttpRequestMessage(HttpMethod.Post,
|
||||||
|
string.Format("https://bitwarden.freshdesk.com/api/v2/tickets/{0}/reply", ticketId))
|
||||||
|
{
|
||||||
|
Content = JsonContent.Create(replyBody),
|
||||||
|
};
|
||||||
|
|
||||||
|
var addReplyResponse = await CallFreshdeskApiAsync(replyRequest);
|
||||||
|
if (addReplyResponse.StatusCode != System.Net.HttpStatusCode.Created)
|
||||||
|
{
|
||||||
|
_logger.LogError("Error adding reply to Freshdesk ticket. Ticket Id: {0}. Status: {1}",
|
||||||
|
ticketId, addReplyResponse.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<HttpResponseMessage> CallFreshdeskApiAsync(HttpRequestMessage request, int retriedCount = 0)
|
private async Task<HttpResponseMessage> CallFreshdeskApiAsync(HttpRequestMessage request, int retriedCount = 0)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
9
src/Billing/Models/FreshdeskReplyRequestModel.cs
Normal file
9
src/Billing/Models/FreshdeskReplyRequestModel.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Bit.Billing.Models;
|
||||||
|
|
||||||
|
public class FreshdeskReplyRequestModel
|
||||||
|
{
|
||||||
|
[JsonPropertyName("body")]
|
||||||
|
public required string Body { get; set; }
|
||||||
|
}
|
||||||
@@ -68,14 +68,16 @@
|
|||||||
"webhookKey": "SECRET"
|
"webhookKey": "SECRET"
|
||||||
},
|
},
|
||||||
"freshdesk": {
|
"freshdesk": {
|
||||||
"apiKey": "SECRET",
|
"apiKey": "Jgqg5fmSsNMx6k5XUiD0",
|
||||||
"webhookKey": "SECRET",
|
"webhookKey": "SECRET",
|
||||||
"region": "US",
|
"region": "US",
|
||||||
"userFieldName": "cf_user",
|
"userFieldName": "cf_user",
|
||||||
"orgFieldName": "cf_org"
|
"orgFieldName": "cf_org",
|
||||||
|
"removeNewlinesInReplies": true,
|
||||||
|
"autoReplyFooter": "<br /><small><i>2024 Bitwarden</i></small>"
|
||||||
},
|
},
|
||||||
"onyx": {
|
"onyx": {
|
||||||
"apiKey": "SECRET",
|
"apiKey": "on_tenant_i-16a83ec44869babc4.aDffNn2k4UvtRt_9KdlV_3lfjiLFdOe4mdAiE3jJb4thWUGURVETwFg1Xrtg5rwLvfqfcw_F6U0V87arVkARd7qHQxmL5vaC4k8gPxh3hTKJKEE7Rkc3BJHOSAiT-leINjWvj44hpIuTRcWazz_zafZi-_D6123wplCb3PG6UtiGlyxcK8FJCZKp9IEM4UTTYXUHc7nWUVZuhiREGPd5J7T8LIWoenfkSJ1Vhi2PoLDI6msMOLfSGuLT_Ma_pJZl",
|
||||||
"baseUrl": "https://cloud.onyx.app/api",
|
"baseUrl": "https://cloud.onyx.app/api",
|
||||||
"personaId": 7
|
"personaId": 7
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user