mirror of
https://github.com/wabbajack-tools/wabbajack.git
synced 2024-08-30 18:42:17 +00:00
commit
390e34c76c
@ -1,5 +1,14 @@
|
|||||||
### Changelog
|
### Changelog
|
||||||
|
|
||||||
|
#### Version - 3.0.0.1 - 8/19/2020
|
||||||
|
* Fix the utterly broken build pipeline, app actually runs now
|
||||||
|
* Trigger login pages for sites if the user doesn't log in before installing
|
||||||
|
|
||||||
|
#### Version - 3.0.0.0 - 8/18/2022
|
||||||
|
* Completely new codebase
|
||||||
|
* Based on WebView2, .NET 6
|
||||||
|
* Probably lots of new bugs, please test
|
||||||
|
|
||||||
#### Version - 2.5.3.27 - 8/7/2022
|
#### Version - 2.5.3.27 - 8/7/2022
|
||||||
* A few fixes for VectorPlexis and LL, you may need to log out and back in from these sites
|
* A few fixes for VectorPlexis and LL, you may need to log out and back in from these sites
|
||||||
|
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
using Wabbajack.Downloaders.Interfaces;
|
||||||
|
|
||||||
namespace Wabbajack.LoginManagers;
|
namespace Wabbajack.LoginManagers;
|
||||||
|
|
||||||
@ -11,4 +14,11 @@ public interface INeedsLogin
|
|||||||
ICommand TriggerLogin { get; set; }
|
ICommand TriggerLogin { get; set; }
|
||||||
ICommand ClearLogin { get; set; }
|
ICommand ClearLogin { get; set; }
|
||||||
ImageSource Icon { get; set; }
|
ImageSource Icon { get; set; }
|
||||||
|
Type LoginFor();
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ILoginFor<T> : INeedsLogin
|
||||||
|
where T : IDownloader
|
||||||
|
{
|
||||||
|
|
||||||
}
|
}
|
@ -7,11 +7,13 @@ using System.Windows.Controls;
|
|||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using System.Windows.Media.Imaging;
|
using System.Windows.Media.Imaging;
|
||||||
|
using System.Windows.Threading;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using ReactiveUI.Fody.Helpers;
|
using ReactiveUI.Fody.Helpers;
|
||||||
using Wabbajack.Common;
|
using Wabbajack.Common;
|
||||||
|
using Wabbajack.Downloaders.IPS4OAuth2Downloader;
|
||||||
using Wabbajack.DTOs.Interventions;
|
using Wabbajack.DTOs.Interventions;
|
||||||
using Wabbajack.DTOs.Logins;
|
using Wabbajack.DTOs.Logins;
|
||||||
using Wabbajack.Messages;
|
using Wabbajack.Messages;
|
||||||
@ -20,7 +22,7 @@ using Wabbajack.UserIntervention;
|
|||||||
|
|
||||||
namespace Wabbajack.LoginManagers;
|
namespace Wabbajack.LoginManagers;
|
||||||
|
|
||||||
public class LoversLabLoginManager : ViewModel, INeedsLogin
|
public class LoversLabLoginManager : ViewModel, ILoginFor<LoversLabDownloader>
|
||||||
{
|
{
|
||||||
private readonly ILogger<LoversLabLoginManager> _logger;
|
private readonly ILogger<LoversLabLoginManager> _logger;
|
||||||
private readonly ITokenProvider<LoversLabLoginState> _token;
|
private readonly ITokenProvider<LoversLabLoginState> _token;
|
||||||
@ -32,6 +34,10 @@ public class LoversLabLoginManager : ViewModel, INeedsLogin
|
|||||||
public ICommand ClearLogin { get; set; }
|
public ICommand ClearLogin { get; set; }
|
||||||
|
|
||||||
public ImageSource Icon { get; set; }
|
public ImageSource Icon { get; set; }
|
||||||
|
public Type LoginFor()
|
||||||
|
{
|
||||||
|
return typeof(LoversLabDownloader);
|
||||||
|
}
|
||||||
|
|
||||||
[Reactive]
|
[Reactive]
|
||||||
public bool HaveLogin { get; set; }
|
public bool HaveLogin { get; set; }
|
||||||
|
@ -8,6 +8,7 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using ReactiveUI.Fody.Helpers;
|
using ReactiveUI.Fody.Helpers;
|
||||||
|
using Wabbajack.Downloaders;
|
||||||
using Wabbajack.DTOs.Interventions;
|
using Wabbajack.DTOs.Interventions;
|
||||||
using Wabbajack.DTOs.Logins;
|
using Wabbajack.DTOs.Logins;
|
||||||
using Wabbajack.Messages;
|
using Wabbajack.Messages;
|
||||||
@ -16,7 +17,7 @@ using Wabbajack.UserIntervention;
|
|||||||
|
|
||||||
namespace Wabbajack.LoginManagers;
|
namespace Wabbajack.LoginManagers;
|
||||||
|
|
||||||
public class NexusLoginManager : ViewModel, INeedsLogin
|
public class NexusLoginManager : ViewModel, ILoginFor<NexusDownloader>
|
||||||
{
|
{
|
||||||
private readonly ILogger<NexusLoginManager> _logger;
|
private readonly ILogger<NexusLoginManager> _logger;
|
||||||
private readonly ITokenProvider<NexusApiState> _token;
|
private readonly ITokenProvider<NexusApiState> _token;
|
||||||
@ -28,6 +29,10 @@ public class NexusLoginManager : ViewModel, INeedsLogin
|
|||||||
public ICommand ClearLogin { get; set; }
|
public ICommand ClearLogin { get; set; }
|
||||||
|
|
||||||
public ImageSource Icon { get; set; }
|
public ImageSource Icon { get; set; }
|
||||||
|
public Type LoginFor()
|
||||||
|
{
|
||||||
|
return typeof(NexusDownloader);
|
||||||
|
}
|
||||||
|
|
||||||
[Reactive]
|
[Reactive]
|
||||||
public bool HaveLogin { get; set; }
|
public bool HaveLogin { get; set; }
|
||||||
|
@ -12,6 +12,7 @@ using Microsoft.Extensions.Logging;
|
|||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using ReactiveUI.Fody.Helpers;
|
using ReactiveUI.Fody.Helpers;
|
||||||
using Wabbajack.Common;
|
using Wabbajack.Common;
|
||||||
|
using Wabbajack.Downloaders.IPS4OAuth2Downloader;
|
||||||
using Wabbajack.DTOs.Interventions;
|
using Wabbajack.DTOs.Interventions;
|
||||||
using Wabbajack.DTOs.Logins;
|
using Wabbajack.DTOs.Logins;
|
||||||
using Wabbajack.Messages;
|
using Wabbajack.Messages;
|
||||||
@ -20,7 +21,7 @@ using Wabbajack.UserIntervention;
|
|||||||
|
|
||||||
namespace Wabbajack.LoginManagers;
|
namespace Wabbajack.LoginManagers;
|
||||||
|
|
||||||
public class VectorPlexusLoginManager : ViewModel, INeedsLogin
|
public class VectorPlexusLoginManager : ViewModel, ILoginFor<LoversLabDownloader>
|
||||||
{
|
{
|
||||||
private readonly ILogger<VectorPlexusLoginManager> _logger;
|
private readonly ILogger<VectorPlexusLoginManager> _logger;
|
||||||
private readonly ITokenProvider<VectorPlexusLoginState> _token;
|
private readonly ITokenProvider<VectorPlexusLoginState> _token;
|
||||||
@ -32,6 +33,10 @@ public class VectorPlexusLoginManager : ViewModel, INeedsLogin
|
|||||||
public ICommand ClearLogin { get; set; }
|
public ICommand ClearLogin { get; set; }
|
||||||
|
|
||||||
public ImageSource Icon { get; set; }
|
public ImageSource Icon { get; set; }
|
||||||
|
public Type LoginFor()
|
||||||
|
{
|
||||||
|
return typeof(LoversLabDownloader);
|
||||||
|
}
|
||||||
|
|
||||||
[Reactive]
|
[Reactive]
|
||||||
public bool HaveLogin { get; set; }
|
public bool HaveLogin { get; set; }
|
||||||
|
@ -235,6 +235,7 @@ namespace Wabbajack
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
||||||
await SaveSettingsFile();
|
await SaveSettingsFile();
|
||||||
var token = CancellationToken.None;
|
var token = CancellationToken.None;
|
||||||
State = CompilerState.Compiling;
|
State = CompilerState.Compiling;
|
||||||
|
@ -15,15 +15,18 @@ using System.Text.Json;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Shell;
|
using System.Windows.Shell;
|
||||||
|
using System.Windows.Threading;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.WindowsAPICodePack.Dialogs;
|
using Microsoft.WindowsAPICodePack.Dialogs;
|
||||||
using Wabbajack.Common;
|
using Wabbajack.Common;
|
||||||
|
using Wabbajack.Downloaders;
|
||||||
using Wabbajack.Downloaders.GameFile;
|
using Wabbajack.Downloaders.GameFile;
|
||||||
using Wabbajack.DTOs;
|
using Wabbajack.DTOs;
|
||||||
using Wabbajack.DTOs.DownloadStates;
|
using Wabbajack.DTOs.DownloadStates;
|
||||||
using Wabbajack.DTOs.JsonConverters;
|
using Wabbajack.DTOs.JsonConverters;
|
||||||
using Wabbajack.Hashing.xxHash64;
|
using Wabbajack.Hashing.xxHash64;
|
||||||
using Wabbajack.Installer;
|
using Wabbajack.Installer;
|
||||||
|
using Wabbajack.LoginManagers;
|
||||||
using Wabbajack.Messages;
|
using Wabbajack.Messages;
|
||||||
using Wabbajack.Models;
|
using Wabbajack.Models;
|
||||||
using Wabbajack.Paths;
|
using Wabbajack.Paths;
|
||||||
@ -116,6 +119,8 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM
|
|||||||
private readonly ResourceMonitor _resourceMonitor;
|
private readonly ResourceMonitor _resourceMonitor;
|
||||||
private readonly Services.OSIntegrated.Configuration _configuration;
|
private readonly Services.OSIntegrated.Configuration _configuration;
|
||||||
private readonly HttpClient _client;
|
private readonly HttpClient _client;
|
||||||
|
private readonly DownloadDispatcher _downloadDispatcher;
|
||||||
|
private readonly IEnumerable<INeedsLogin> _logins;
|
||||||
public ReadOnlyObservableCollection<CPUDisplayVM> StatusList => _resourceMonitor.Tasks;
|
public ReadOnlyObservableCollection<CPUDisplayVM> StatusList => _resourceMonitor.Tasks;
|
||||||
|
|
||||||
[Reactive]
|
[Reactive]
|
||||||
@ -145,7 +150,7 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM
|
|||||||
|
|
||||||
public InstallerVM(ILogger<InstallerVM> logger, DTOSerializer dtos, SettingsManager settingsManager, IServiceProvider serviceProvider,
|
public InstallerVM(ILogger<InstallerVM> logger, DTOSerializer dtos, SettingsManager settingsManager, IServiceProvider serviceProvider,
|
||||||
SystemParametersConstructor parametersConstructor, IGameLocator gameLocator, LogStream loggerProvider, ResourceMonitor resourceMonitor,
|
SystemParametersConstructor parametersConstructor, IGameLocator gameLocator, LogStream loggerProvider, ResourceMonitor resourceMonitor,
|
||||||
Wabbajack.Services.OSIntegrated.Configuration configuration, HttpClient client) : base(logger)
|
Wabbajack.Services.OSIntegrated.Configuration configuration, HttpClient client, DownloadDispatcher dispatcher, IEnumerable<INeedsLogin> logins) : base(logger)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
@ -157,6 +162,8 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM
|
|||||||
_gameLocator = gameLocator;
|
_gameLocator = gameLocator;
|
||||||
_resourceMonitor = resourceMonitor;
|
_resourceMonitor = resourceMonitor;
|
||||||
_client = client;
|
_client = client;
|
||||||
|
_downloadDispatcher = dispatcher;
|
||||||
|
_logins = logins;
|
||||||
|
|
||||||
Installer = new MO2InstallerVM(this);
|
Installer = new MO2InstallerVM(this);
|
||||||
|
|
||||||
@ -344,6 +351,38 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM
|
|||||||
await Task.Run(async () =>
|
await Task.Run(async () =>
|
||||||
{
|
{
|
||||||
InstallState = InstallState.Installing;
|
InstallState = InstallState.Installing;
|
||||||
|
|
||||||
|
foreach (var downloader in await _downloadDispatcher.AllDownloaders(ModList.Archives.Select(a => a.State)))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Preparing {Name}", downloader.GetType().Name);
|
||||||
|
if (await downloader.Prepare())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
|
||||||
|
var manager = _logins
|
||||||
|
.FirstOrDefault(l => l.LoginFor() == downloader.GetType());
|
||||||
|
if (manager == null)
|
||||||
|
{
|
||||||
|
_logger.LogError("Cannot install, could not prepare {Name} for downloading",
|
||||||
|
downloader.GetType().Name);
|
||||||
|
throw new Exception($"No way to prepare {downloader}");
|
||||||
|
}
|
||||||
|
|
||||||
|
RxApp.MainThreadScheduler.Schedule(manager, (_, _) =>
|
||||||
|
{
|
||||||
|
manager.TriggerLogin.Execute(null);
|
||||||
|
return Disposable.Empty;
|
||||||
|
});
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if (await downloader.Prepare())
|
||||||
|
break;
|
||||||
|
await Task.Delay(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
var postfix = (await ModListLocation.TargetPath.ToString().Hash()).ToHex();
|
var postfix = (await ModListLocation.TargetPath.ToString().Hash()).ToHex();
|
||||||
await _settingsManager.Save(InstallSettingsPrefix + postfix, new SavedInstallSettings
|
await _settingsManager.Save(InstallSettingsPrefix + postfix, new SavedInstallSettings
|
||||||
{
|
{
|
||||||
|
@ -1,18 +1,6 @@
|
|||||||
using System;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Drawing;
|
|
||||||
using System.Drawing.Imaging;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reactive;
|
|
||||||
using System.Reactive.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows.Media;
|
|
||||||
using System.Windows.Media.Imaging;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using ReactiveUI;
|
|
||||||
using ReactiveUI.Fody.Helpers;
|
|
||||||
using Wabbajack;
|
|
||||||
using Wabbajack.LoginManagers;
|
using Wabbajack.LoginManagers;
|
||||||
|
|
||||||
namespace Wabbajack
|
namespace Wabbajack
|
||||||
|
@ -12,9 +12,8 @@
|
|||||||
<Copyright>Copyright © 2019-2022</Copyright>
|
<Copyright>Copyright © 2019-2022</Copyright>
|
||||||
<Description>An automated ModList installer</Description>
|
<Description>An automated ModList installer</Description>
|
||||||
<PublishReadyToRun>true</PublishReadyToRun>
|
<PublishReadyToRun>true</PublishReadyToRun>
|
||||||
<StartupObject></StartupObject>
|
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
<IncludeSymbolsInSingleFile>true</IncludeSymbolsInSingleFile>
|
<!-- <IncludeSymbolsInSingleFile>true</IncludeSymbolsInSingleFile> -->
|
||||||
<AssemblyName>Wabbajack</AssemblyName>
|
<AssemblyName>Wabbajack</AssemblyName>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
@ -7,6 +7,6 @@ public class VectorPlexusLoginState : OAuth2LoginState
|
|||||||
public override string SiteName => "Vector Plexus";
|
public override string SiteName => "Vector Plexus";
|
||||||
public override string[] Scopes => new[] {"profile", "get_downloads"};
|
public override string[] Scopes => new[] {"profile", "get_downloads"};
|
||||||
public override string ClientID => "45c6d3c9867903a7daa6ded0a38cedf8";
|
public override string ClientID => "45c6d3c9867903a7daa6ded0a38cedf8";
|
||||||
public override Uri AuthorizationEndpoint => new("https://vectorplexus.com/oauth/authorize/");
|
public override Uri AuthorizationEndpoint => new("https://vectorplexis.com/oauth/authorize/");
|
||||||
public override Uri TokenEndpoint => new("https://vectorplexus.com/oauth/token/");
|
public override Uri TokenEndpoint => new("https://vectorplexis.com/oauth/token/");
|
||||||
}
|
}
|
@ -292,10 +292,9 @@ public class DownloadDispatcher
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task PrepareAll(IEnumerable<IDownloadState> downloadStates)
|
public async Task<IEnumerable<IDownloader>> AllDownloaders(IEnumerable<IDownloadState> downloadStates)
|
||||||
{
|
{
|
||||||
foreach (var d in downloadStates.Select(d => Downloader(new Archive {State = d})).Distinct())
|
return downloadStates.Select(d => Downloader(new Archive {State = d})).Distinct();
|
||||||
await d.Prepare();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Matches(Archive archive, ServerAllowList mirrorAllowList)
|
public bool Matches(Archive archive, ServerAllowList mirrorAllowList)
|
||||||
|
@ -163,9 +163,9 @@ public class AIPS4OAuth2Downloader<TDownloader, TLogin, TState> : ADownloader<TS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task<bool> Prepare()
|
public override async Task<bool> Prepare()
|
||||||
{
|
{
|
||||||
return Task.FromResult(true);
|
return _loginInfo.HaveToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool IsAllowed(ServerAllowList allowList, IDownloadState state)
|
public override bool IsAllowed(ServerAllowList allowList, IDownloadState state)
|
||||||
|
@ -48,7 +48,7 @@ public class NexusDownloader : ADownloader<Nexus>, IUrlDownloader
|
|||||||
|
|
||||||
public override async Task<bool> Prepare()
|
public override async Task<bool> Prepare()
|
||||||
{
|
{
|
||||||
return true;
|
return _api.ApiKey.HaveToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool IsAllowed(ServerAllowList allowList, IDownloadState state)
|
public override bool IsAllowed(ServerAllowList allowList, IDownloadState state)
|
||||||
|
@ -29,7 +29,7 @@ public class NexusApi
|
|||||||
private readonly JsonSerializerOptions _jsonOptions;
|
private readonly JsonSerializerOptions _jsonOptions;
|
||||||
private readonly IResource<HttpClient> _limiter;
|
private readonly IResource<HttpClient> _limiter;
|
||||||
private readonly ILogger<NexusApi> _logger;
|
private readonly ILogger<NexusApi> _logger;
|
||||||
protected readonly ITokenProvider<NexusApiState> ApiKey;
|
public readonly ITokenProvider<NexusApiState> ApiKey;
|
||||||
private DateTime _lastValidated;
|
private DateTime _lastValidated;
|
||||||
private (ValidateInfo info, ResponseMetadata header) _lastValidatedInfo;
|
private (ValidateInfo info, ResponseMetadata header) _lastValidatedInfo;
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ mkdir c:\tmp\publish-wj
|
|||||||
|
|
||||||
dotnet clean
|
dotnet clean
|
||||||
dotnet restore
|
dotnet restore
|
||||||
dotnet publish Wabbajack.App.Wpf\Wabbajack.App.Wpf.csproj --runtime win10-x64 --configuration Release /p:Platform=x64 -o c:\tmp\publish-wj\app --self-contained
|
dotnet publish Wabbajack.App.Wpf\Wabbajack.App.Wpf.csproj --runtime win10-x64 --configuration Release /p:Platform=x64 -o c:\tmp\publish-wj\app /p:PublishedTrimmed=true /p:PublishReadyToRun=true /p:PublishSingleFile=true /p:IncludeNativeLibrariesForSelfExtract=true --self-contained
|
||||||
dotnet publish Wabbajack.Launcher\Wabbajack.Launcher.csproj --runtime win10-x64 --configuration Release /p:Platform=x64 -o c:\tmp\publish-wj\launcher /p:PublishedTrimmed=true /p:PublishReadyToRun=true /p:PublishSingleFile=true /p:IncludeNativeLibrariesForSelfExtract=true --self-contained
|
dotnet publish Wabbajack.Launcher\Wabbajack.Launcher.csproj --runtime win10-x64 --configuration Release /p:Platform=x64 -o c:\tmp\publish-wj\launcher /p:PublishedTrimmed=true /p:PublishReadyToRun=true /p:PublishSingleFile=true /p:IncludeNativeLibrariesForSelfExtract=true --self-contained
|
||||||
dotnet publish c:\oss\Wabbajack\Wabbajack.CLI\Wabbajack.CLI.csproj --runtime win10-x64 --configuration Release /p:Platform=x64 -o c:\tmp\publish-wj\app --self-contained
|
dotnet publish c:\oss\Wabbajack\Wabbajack.CLI\Wabbajack.CLI.csproj --runtime win10-x64 --configuration Release /p:Platform=x64 -o c:\tmp\publish-wj\app --self-contained
|
||||||
"C:\Program Files (x86)\Windows Kits\10\bin\10.0.19041.0\x64\signtool.exe" sign /t http://timestamp.sectigo.com c:\tmp\publish-wj\app\Wabbajack.exe
|
"C:\Program Files (x86)\Windows Kits\10\bin\10.0.19041.0\x64\signtool.exe" sign /t http://timestamp.sectigo.com c:\tmp\publish-wj\app\Wabbajack.exe
|
||||||
|
Loading…
Reference in New Issue
Block a user