81 lines
4.5 KiB
C#
81 lines
4.5 KiB
C#
using System.Net.Http.Headers;
|
|
using System.Text.Json;
|
|
using JobTrackerApi.Models;
|
|
using Microsoft.Extensions.Caching.Memory;
|
|
|
|
namespace JobTrackerApi.Services;
|
|
|
|
public interface IJobDiscoverySearchService
|
|
{
|
|
Task<IReadOnlyList<DiscoveredJob>> SearchAsync(string? query, string? location, CancellationToken cancellationToken);
|
|
}
|
|
|
|
public sealed class NavJobDiscoverySearchService(
|
|
IHttpClientFactory clients,
|
|
IConfiguration configuration,
|
|
IMemoryCache cache,
|
|
TimeProvider timeProvider) : IJobDiscoverySearchService
|
|
{
|
|
private const string BaseUrl = "https://pam-stilling-feed.nav.no";
|
|
|
|
public async Task<IReadOnlyList<DiscoveredJob>> SearchAsync(string? query, string? location, CancellationToken cancellationToken)
|
|
{
|
|
var retrievedAt = timeProvider.GetUtcNow();
|
|
var token = await GetTokenAsync(cancellationToken);
|
|
var client = clients.CreateClient();
|
|
var entries = new Dictionary<string, DiscoveredJob>(StringComparer.OrdinalIgnoreCase);
|
|
var next = "/api/v1/feed";
|
|
for (var page = 0; page < 20 && !string.IsNullOrWhiteSpace(next); page++)
|
|
{
|
|
using var request = new HttpRequestMessage(HttpMethod.Get, BaseUrl + next);
|
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
|
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
|
if (page == 0) request.Headers.IfModifiedSince = timeProvider.GetUtcNow().AddDays(-14);
|
|
using var response = await client.SendAsync(request, cancellationToken);
|
|
response.EnsureSuccessStatusCode();
|
|
using var json = JsonDocument.Parse(await response.Content.ReadAsStreamAsync(cancellationToken));
|
|
next = json.RootElement.TryGetProperty("next_url", out var nextElement) ? nextElement.GetString() ?? "" : "";
|
|
foreach (var item in json.RootElement.GetProperty("items").EnumerateArray())
|
|
{
|
|
var feed = item.GetProperty("_feed_entry");
|
|
var jobId = feed.GetProperty("uuid").GetString();
|
|
if (string.IsNullOrWhiteSpace(jobId)) continue;
|
|
var status = feed.TryGetProperty("status", out var statusElement) ? statusElement.GetString() : null;
|
|
if (!string.Equals(status, "ACTIVE", StringComparison.OrdinalIgnoreCase)) { entries.Remove(jobId); continue; }
|
|
entries[jobId] = new DiscoveredJob(jobId,
|
|
feed.TryGetProperty("title", out var title) ? title.GetString() ?? "" : "",
|
|
feed.TryGetProperty("businessName", out var company) ? company.GetString() : null,
|
|
feed.TryGetProperty("municipal", out var municipal) ? municipal.GetString() : null,
|
|
item.TryGetProperty("date_modified", out var modified) && modified.TryGetDateTimeOffset(out var date) ? date : null,
|
|
feed.TryGetProperty("applicationDue", out var due) && due.TryGetDateTimeOffset(out var deadline) ? deadline : null,
|
|
$"https://arbeidsplassen.nav.no/stillinger/stilling/{jobId}", "nav", "NAV Arbeidsplassen", "searched", retrievedAt, "NO");
|
|
}
|
|
}
|
|
|
|
var term = (query ?? "").Trim();
|
|
var place = (location ?? "").Trim();
|
|
return entries.Values.Where(job => Contains(job.Title, term) || Contains(job.Company, term))
|
|
.Where(job => Contains(job.Location, place)).OrderByDescending(job => job.ModifiedAt).Take(100).ToList();
|
|
}
|
|
|
|
private async Task<string> GetTokenAsync(CancellationToken cancellationToken)
|
|
{
|
|
var configured = configuration["NavJobs:Token"]?.Trim();
|
|
if (!string.IsNullOrWhiteSpace(configured)) return configured;
|
|
|
|
return await cache.GetOrCreateAsync("nav-jobs-public-token", async entry =>
|
|
{
|
|
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30);
|
|
var text = await clients.CreateClient().GetStringAsync(BaseUrl + "/api/publicToken", cancellationToken);
|
|
var start = text.IndexOf("eyJ", StringComparison.Ordinal);
|
|
if (start < 0) throw new InvalidOperationException("NAV public token was not returned.");
|
|
var token = text[start..].Trim();
|
|
var end = token.IndexOfAny(['\r', '\n', ' ', '\t']);
|
|
return end < 0 ? token : token[..end];
|
|
}) ?? throw new InvalidOperationException("NAV public token was not returned.");
|
|
}
|
|
|
|
private static bool Contains(string? value, string filter) =>
|
|
filter.Length == 0 || (value?.Contains(filter, StringComparison.OrdinalIgnoreCase) ?? false);
|
|
}
|