Add generic ticket activity filters

This commit is contained in:
Drechsler, Meik
2026-07-23 11:18:42 +02:00
parent d8022567af
commit 8f66923952
15 changed files with 502 additions and 181 deletions

View File

@@ -2,8 +2,9 @@
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Numerics;
using System.Reflection;
using System.Security.Principal;
@@ -31,14 +32,17 @@ using static C4IT.Matrix42.WebClient.cM42WebClient;
namespace C4IT.DataHistoryProvider
{
public class cDataHistoryCollectorM42Wpm : cDataHistoryCollectorModule, ISearchResultRelationProvider
public class cDataHistoryCollectorM42Wpm : cDataHistoryCollectorModule, ISearchResultRelationProvider, ITicketActivityProvider
{
public const string constConnectorName = "Matrix42 WPM conntector";
public const string constLicenseId = "9CE1A6BE-6A0C-4A27-94A5-44AB997B8E62";
private const string constUrlIsAlive = "m42Services/api/c4itf4sdwebapi/isalive";
private const string constUrlGetTickets = "m42Services/api/c4itf4sdwebapi/getticketlist?sid={0}&hours={1}&queueoption={2}";
private const string constUrlGetTicketsQueuesParam = "&queues={0}";
private const string constUrlGetTickets = "m42Services/api/c4itf4sdwebapi/getticketlist?sid={0}&hours={1}&queueoption={2}";
private const string constUrlGetTicketsQueuesParam = "&queues={0}";
private const string constUrlGetTicketsV2 = "m42Services/api/c4itf4sdwebapi/v2/getticketlist";
private const string constUrlGetTicketOverviewCountsV2 = "m42Services/api/c4itf4sdwebapi/v2/getticketoverviewcounts";
private const string constUrlGetTicketOverviewRelationsV2 = "m42Services/api/c4itf4sdwebapi/v2/getticketoverviewrelations";
private const string constUrlGetTicketDetails = "m42Services/api/c4itf4sdwebapi/getticketdetails?objectid={0}";
private const string constUrlGetTicketHistory = "m42Services/api/c4itf4sdwebapi/gettickethistory?objectid={0}";
private const string constUrlGetCreateTicket = "m42Services/api/c4itf4sdwebapi/getdirectlinkcreateticket?sid={0}{1}";
@@ -687,24 +691,33 @@ namespace C4IT.DataHistoryProvider
if (!await CheckOnline())
return null;
var objSid = new SecurityIdentifier(UserId.sid, 0);
var strSid = objSid.ToString();
var queueOption = (int)Collector.InfrastructureConfig.M42Wpm.ActivityQueueFilterOption;
var strUrl = string.Format(constUrlGetTickets,
strSid,
Collector.InfrastructureConfig.M42Wpm.ClosedTicketHistory,
queueOption);
if (queueOption != 0)
{
var encodedQueues = GetEncodedActivityQueues();
if (!string.IsNullOrWhiteSpace(encodedQueues))
strUrl += string.Format(constUrlGetTicketsQueuesParam, encodedQueues);
}
var _wc = await GetWebClient(requestInfo, Token);
var res = await _wc.HttpEnh.GetAsync(strUrl);
if (res.IsSuccessStatusCode)
var objSid = new SecurityIdentifier(UserId.sid, 0);
var strSid = objSid.ToString();
var _wc = await GetWebClient(requestInfo, Token);
var request = new cTicketActivityListRequest
{
Sid = strSid,
Hours = Collector.InfrastructureConfig.M42Wpm.ClosedTicketHistory,
Filters = GetActivityFilters()
};
var res = await PostJsonAsync(_wc, constUrlGetTicketsV2, request, Token);
if (res?.StatusCode == HttpStatusCode.NotFound)
{
if (!TryGetLegacyQueueFilter(out var queueOption, out var encodedQueues))
{
LogEntry("The activity filter policy requires the v2 ticket API; legacy fallback was rejected (fail closed).", LogLevels.Warning);
return null;
}
var strUrl = string.Format(
constUrlGetTickets,
strSid,
Collector.InfrastructureConfig.M42Wpm.ClosedTicketHistory,
queueOption);
if (!string.IsNullOrWhiteSpace(encodedQueues))
strUrl += string.Format(constUrlGetTicketsQueuesParam, encodedQueues);
res = await _wc.HttpEnh.GetAsync(strUrl);
}
if (res?.IsSuccessStatusCode == true)
{
var strJson = await res.Content.ReadAsStringAsync();
if (!string.IsNullOrEmpty(strJson))
@@ -1093,12 +1106,76 @@ namespace C4IT.DataHistoryProvider
return filtered;
}
private int GetActivityQueueOption()
{
return (int)Collector.InfrastructureConfig.M42Wpm.ActivityQueueFilterOption;
}
private string GetEncodedActivityQueues()
private int GetActivityQueueOption()
{
return (int)Collector.InfrastructureConfig.M42Wpm.ActivityQueueFilterOption;
}
private cActivityFilterPolicy GetActivityFilters()
{
return Collector?.GetGlobalConfig()?.TicketConfiguration?.ActivityFilters
?? new cActivityFilterPolicy();
}
private bool TryGetLegacyQueueFilter(out int queueOption, out string encodedQueues)
{
queueOption = 0;
encodedQueues = null;
var activeFilters = (GetActivityFilters()?.Filters ?? new List<cActivityFilter>())
.Where(entry => entry?.Enabled == true)
.ToList();
if (activeFilters.Count == 0)
return true;
if (activeFilters.Count != 1)
return false;
var activityFilter = activeFilters[0];
if (!string.Equals(activityFilter.Provider, "Matrix42", StringComparison.OrdinalIgnoreCase)
|| !string.Equals(activityFilter.Field, "Queue", StringComparison.OrdinalIgnoreCase)
|| activityFilter.Match != enumActivityFilterMatch.include
|| activityFilter.EmptyHandling == enumActivityFilterEmptyHandling.only)
{
return false;
}
queueOption = activityFilter.EmptyHandling == enumActivityFilterEmptyHandling.include ? 1 : 2;
var values = (activityFilter.Values ?? new List<cActivityFilterValue>())
.Where(value => value != null)
.Select(value =>
{
if (Guid.TryParse(value.ID, out var id) && id != Guid.Empty)
return ":" + HttpUtility.UrlEncode(id.ToString("D"));
return string.IsNullOrWhiteSpace(value.Name)
? null
: HttpUtility.UrlEncode(value.Name.Trim());
})
.Where(value => !string.IsNullOrWhiteSpace(value))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
if (values.Count == 0)
return false;
encodedQueues = string.Join("|", values);
return true;
}
private static Task<HttpResponseMessage> PostJsonAsync(
cM42WebClient webClient,
string url,
object request,
CancellationToken token)
{
var json = JsonConvert.SerializeObject(
request,
Formatting.None,
new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
return webClient.HttpEnh.PostAsync(
url,
new StringContent(json, Encoding.UTF8, "application/json"),
token);
}
private string GetEncodedActivityQueues()
{
var queues = Collector.InfrastructureConfig.M42Wpm.ActivityQueues;
if (queues == null || queues.Count == 0)
@@ -1140,28 +1217,36 @@ namespace C4IT.DataHistoryProvider
string scope,
IReadOnlyCollection<string> requestedKeys,
cF4sdWebRequestInfo requestInfo,
CancellationToken token)
{
try
{
var urlBuilder = new StringBuilder(constUrlGetTicketOverviewCounts);
urlBuilder.Append("?sid=").Append(HttpUtility.UrlEncode(sid));
urlBuilder.Append("&scope=").Append(HttpUtility.UrlEncode(scope ?? string.Empty));
if (requestedKeys != null && requestedKeys.Count > 0)
{
urlBuilder.Append("&keys=").Append(HttpUtility.UrlEncode(string.Join(",", requestedKeys)));
}
var queueOption = GetActivityQueueOption();
var encodedQueues = queueOption != 0 ? GetEncodedActivityQueues() : null;
AppendQueueFilterQuery(urlBuilder, queueOption, encodedQueues);
var url = urlBuilder.ToString();
var wc = await GetWebClient(requestInfo, token);
var res = await wc.HttpEnh.GetAsync(url, token);
if (token.IsCancellationRequested)
return new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
CancellationToken token)
{
try
{
var wc = await GetWebClient(requestInfo, token);
var request = new cTicketOverviewFilterRequest
{
Sid = sid,
Scope = scope,
Keys = requestedKeys?.ToList() ?? new List<string>(),
Filters = GetActivityFilters()
};
var res = await PostJsonAsync(wc, constUrlGetTicketOverviewCountsV2, request, token);
if (res?.StatusCode == HttpStatusCode.NotFound)
{
if (!TryGetLegacyQueueFilter(out var queueOption, out var encodedQueues))
{
LogEntry("The activity filter policy requires the v2 overview API; legacy fallback was rejected (fail closed).", LogLevels.Warning);
return new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
}
var urlBuilder = new StringBuilder(constUrlGetTicketOverviewCounts);
urlBuilder.Append("?sid=").Append(HttpUtility.UrlEncode(sid));
urlBuilder.Append("&scope=").Append(HttpUtility.UrlEncode(scope ?? string.Empty));
if (requestedKeys != null && requestedKeys.Count > 0)
urlBuilder.Append("&keys=").Append(HttpUtility.UrlEncode(string.Join(",", requestedKeys)));
AppendQueueFilterQuery(urlBuilder, queueOption, encodedQueues);
res = await wc.HttpEnh.GetAsync(urlBuilder.ToString(), token);
}
if (token.IsCancellationRequested)
return new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
if (res?.IsSuccessStatusCode == true)
{
@@ -1227,64 +1312,12 @@ namespace C4IT.DataHistoryProvider
if (string.IsNullOrWhiteSpace(sid))
return new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
var normalizedKeys = (keys ?? Enumerable.Empty<string>())
.Where(k => !string.IsNullOrWhiteSpace(k))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
var now = DateTime.UtcNow;
var ttl = GetTicketOverviewCacheDuration(useRoleScope);
if (!useRoleScope)
{
// process thr personal scope
if (TryGetTicketOverviewPersonalCache(sid, now, out var cachedPersonal))
return FilterTicketOverviewCounts(cachedPersonal, normalizedKeys);
var counts = await FetchTicketOverviewCountsAsync(sid, "personal", normalizedKeys, requestInfo, Token);
if (Token.IsCancellationRequested)
return new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
SetTicketOverviewPersonalCache(sid, counts, now.Add(ttl));
return FilterTicketOverviewCounts(counts, normalizedKeys);
}
// process the role scope
var roleIds = await GetTicketOverviewRoleIdsAsync(sid, now, ttl, Token);
if (roleIds == null || roleIds.Count == 0)
return new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
var countsByRole = new Dictionary<Guid, Dictionary<string, int>>();
var missingRoles = new List<Guid>();
foreach (var roleId in roleIds)
{
if (TryGetTicketOverviewRoleCache(roleId, now, out var cachedRole))
{
countsByRole[roleId] = cachedRole;
}
else
{
missingRoles.Add(roleId);
}
}
if (missingRoles.Count > 0)
{
var fetched = await FetchTicketOverviewCountsByRolesAsync(missingRoles, normalizedKeys, requestInfo, LogDeep + 1, Token);
var expiresAt = DateTime.UtcNow.Add(ttl);
foreach (var roleId in missingRoles)
{
if (!fetched.TryGetValue(roleId, out var roleCounts))
roleCounts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
SetTicketOverviewRoleCache(roleId, roleCounts, expiresAt);
countsByRole[roleId] = roleCounts;
}
}
var summed = SumTicketOverviewCounts(countsByRole.Values);
return FilterTicketOverviewCounts(summed, normalizedKeys);
var normalizedKeys = (keys ?? Enumerable.Empty<string>())
.Where(k => !string.IsNullOrWhiteSpace(k))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
var scope = useRoleScope ? "role" : "personal";
return await FetchTicketOverviewCountsAsync(sid, scope, normalizedKeys, requestInfo, Token);
}
catch (Exception E)
{
@@ -1314,26 +1347,37 @@ namespace C4IT.DataHistoryProvider
return new List<cF4sdApiSearchResultRelation>();
var sid = requestInfo?.userInfo?.AdSid;
if (string.IsNullOrWhiteSpace(sid))
return new List<cF4sdApiSearchResultRelation>();
var scope = useRoleScope ? "role" : "personal";
var urlBuilder = new StringBuilder(constUrlGetTicketOverviewRelations);
urlBuilder.Append("?sid=").Append(HttpUtility.UrlEncode(sid));
urlBuilder.Append("&scope=").Append(HttpUtility.UrlEncode(scope));
urlBuilder.Append("&key=").Append(HttpUtility.UrlEncode(key));
urlBuilder.Append("&count=").Append(Math.Max(0, count));
var queueOption = GetActivityQueueOption();
var encodedQueues = queueOption != 0 ? GetEncodedActivityQueues() : null;
AppendQueueFilterQuery(urlBuilder, queueOption, encodedQueues);
var url = urlBuilder.ToString();
var wc = await GetWebClient(requestInfo, Token);
var res = await wc.HttpEnh.GetAsync(url, Token);
if (Token.IsCancellationRequested)
return new List<cF4sdApiSearchResultRelation>();
if (string.IsNullOrWhiteSpace(sid))
return new List<cF4sdApiSearchResultRelation>();
var scope = useRoleScope ? "role" : "personal";
var wc = await GetWebClient(requestInfo, Token);
var request = new cTicketOverviewFilterRequest
{
Sid = sid,
Scope = scope,
Key = key,
Count = Math.Max(0, count),
Filters = GetActivityFilters()
};
var res = await PostJsonAsync(wc, constUrlGetTicketOverviewRelationsV2, request, Token);
if (res?.StatusCode == HttpStatusCode.NotFound)
{
if (!TryGetLegacyQueueFilter(out var queueOption, out var encodedQueues))
{
LogEntry("The activity filter policy requires the v2 relation API; legacy fallback was rejected (fail closed).", LogLevels.Warning);
return new List<cF4sdApiSearchResultRelation>();
}
var urlBuilder = new StringBuilder(constUrlGetTicketOverviewRelations);
urlBuilder.Append("?sid=").Append(HttpUtility.UrlEncode(sid));
urlBuilder.Append("&scope=").Append(HttpUtility.UrlEncode(scope));
urlBuilder.Append("&key=").Append(HttpUtility.UrlEncode(key));
urlBuilder.Append("&count=").Append(Math.Max(0, count));
AppendQueueFilterQuery(urlBuilder, queueOption, encodedQueues);
res = await wc.HttpEnh.GetAsync(urlBuilder.ToString(), Token);
}
if (Token.IsCancellationRequested)
return new List<cF4sdApiSearchResultRelation>();
if (res?.IsSuccessStatusCode == true)
{