/*
* Copyright (c) 2025 Proton AG
*
* This file is part of ProtonVPN.
*
* ProtonVPN is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonVPN is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonVPN. If not, see .
*/
using ProtonVPN.Api.Contracts;
using ProtonVPN.Api.Contracts.Geographical;
using ProtonVPN.Client.EventMessaging.Contracts;
using ProtonVPN.Client.Localization.Contracts;
using ProtonVPN.Client.Localization.Contracts.Messages;
using ProtonVPN.Client.Logic.Servers.Cache;
using ProtonVPN.Client.Logic.Servers.Contracts.Messages;
using ProtonVPN.Client.Logic.Servers.Contracts.Models;
using ProtonVPN.Client.Logic.Servers.Files;
using ProtonVPN.Client.Settings.Contracts;
using ProtonVPN.Common.Core.Extensions;
using ProtonVPN.Logging.Contracts;
using ProtonVPN.Logging.Contracts.Events.ApiLogs;
using ProtonVPN.Logging.Contracts.Events.AppLogs;
namespace ProtonVPN.Client.Logic.Servers;
public class LocationNamesProvider :
IEventMessageReceiver,
IEventMessageReceiver
{
private static readonly TimeSpan _cacheExpiry = TimeSpan.FromDays(7);
private readonly IApiClient _apiClient;
private readonly IServersCache _serversCache;
private readonly IEventMessageSender _eventMessageSender;
private readonly ILogger _logger;
private readonly ILocationNamesFileReaderWriter _fileReaderWriter;
private readonly ILocationLocalizationSetter _locationLocalizationSetter;
private readonly IGlobalSettings _globalSettings;
private LocationNamesFile _cacheFile = new();
public LocationNamesProvider(
IApiClient apiClient,
IServersCache serversCache,
IEventMessageSender eventMessageSender,
ILogger logger,
ILocationNamesFileReaderWriter fileReaderWriter,
ILocationLocalizationSetter locationLocalizationSetter,
IGlobalSettings globalSettings)
{
_apiClient = apiClient;
_serversCache = serversCache;
_eventMessageSender = eventMessageSender;
_logger = logger;
_fileReaderWriter = fileReaderWriter;
_locationLocalizationSetter = locationLocalizationSetter;
_globalSettings = globalSettings;
}
private async Task RefreshAsync(CancellationToken cancellationToken = default)
{
if (_serversCache.Servers.Count == 0)
{
_logger.Info("Server list is not available yet. Skipping location names refresh.");
return;
}
string currentLanguage = _globalSettings.Language;
if (_cacheFile.Languages.Count == 0)
{
LoadCacheFile();
}
bool hasCachedLanguage = _cacheFile.Languages.TryGetValue(currentLanguage, out LocationNamesCache? languageCache);
bool cacheNeedsRefresh = !hasCachedLanguage || DateTime.UtcNow - languageCache!.LastUpdatedUtc > _cacheExpiry;
if (cacheNeedsRefresh)
{
_logger.Info($"Cache refresh needed for language '{currentLanguage}'. Has cache: {hasCachedLanguage}, Expired: {cacheNeedsRefresh}");
await FetchFromApiAsync(currentLanguage, cancellationToken);
}
else
{
_logger.Info($"Using cached location names for language '{currentLanguage}'.");
SetActiveLanguage(languageCache!);
}
}
public void Receive(LanguageChangedMessage message)
{
RefreshAsync().FireAndForget();
}
public void Receive(ServerListChangedMessage message)
{
RefreshAsync().FireAndForget();
}
private void LoadCacheFile()
{
_cacheFile = _fileReaderWriter.Read();
}
private void SetActiveLanguage(LocationNamesCache languageCache)
{
_locationLocalizationSetter.SetCityNames(languageCache.Cities);
_locationLocalizationSetter.SetStateNames(languageCache.States);
_eventMessageSender.Send();
}
private async Task FetchFromApiAsync(string requestedLanguage, CancellationToken cancellationToken = default)
{
try
{
_logger.Info($"Fetching localized location names from API for language '{requestedLanguage}'.");
ApiResponseResult response = await _apiClient.GetCityNamesAsync(cancellationToken);
if (response.Success)
{
LocationNamesCache newCache = new()
{
LastUpdatedUtc = DateTime.UtcNow,
Cities = response.Value.Cities ?? [],
States = response.Value.States ?? []
};
_cacheFile.Languages[requestedLanguage] = newCache;
_fileReaderWriter.Save(_cacheFile);
_logger.Info($"Using fetched location names for language '{requestedLanguage}'. Location names have been cached for future usage.");
SetActiveLanguage(newCache);
}
else
{
_logger.Error($"Failed to fetch name translations: {response.Error}");
if (_cacheFile.Languages.TryGetValue(requestedLanguage, out LocationNamesCache? staleCache))
{
_logger.Info($"Using stale cached location names for language '{requestedLanguage}' due to API failure.");
SetActiveLanguage(staleCache);;
}
}
}
catch (Exception ex)
{
_logger.Error("Error fetching name translations", ex);
}
}
}