1
0
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:
voommen-livefront
2025-09-11 09:39:51 -05:00
parent e60564253e
commit bd6c34b394
5 changed files with 102 additions and 3 deletions

View File

@@ -11,6 +11,7 @@
<ProjectReference Include="..\Core\Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MarkDig" Version="0.41.3" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.2" />
</ItemGroup>

View File

@@ -34,6 +34,9 @@ public class BillingSettings
public virtual string Region { get; set; }
public virtual string UserFieldName { 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

View File

@@ -11,6 +11,7 @@ using Bit.Billing.Models;
using Bit.Core.Repositories;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Markdig;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
@@ -184,6 +185,52 @@ public class FreshdeskController : Controller
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)
{
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)
{
try

View 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; }
}

View File

@@ -68,14 +68,16 @@
"webhookKey": "SECRET"
},
"freshdesk": {
"apiKey": "SECRET",
"apiKey": "Jgqg5fmSsNMx6k5XUiD0",
"webhookKey": "SECRET",
"region": "US",
"userFieldName": "cf_user",
"orgFieldName": "cf_org"
"orgFieldName": "cf_org",
"removeNewlinesInReplies": true,
"autoReplyFooter": "<br /><small><i>2024 Bitwarden</i></small>"
},
"onyx": {
"apiKey": "SECRET",
"apiKey": "on_tenant_i-16a83ec44869babc4.aDffNn2k4UvtRt_9KdlV_3lfjiLFdOe4mdAiE3jJb4thWUGURVETwFg1Xrtg5rwLvfqfcw_F6U0V87arVkARd7qHQxmL5vaC4k8gPxh3hTKJKEE7Rkc3BJHOSAiT-leINjWvj44hpIuTRcWazz_zafZi-_D6123wplCb3PG6UtiGlyxcK8FJCZKp9IEM4UTTYXUHc7nWUVZuhiREGPd5J7T8LIWoenfkSJ1Vhi2PoLDI6msMOLfSGuLT_Ma_pJZl",
"baseUrl": "https://cloud.onyx.app/api",
"personaId": 7
}