mirror of
https://github.com/wabbajack-tools/wabbajack.git
synced 2024-08-30 18:42:17 +00:00
Merge pull request #487 from wabbajack-tools/beth-net-better-login
Better Bethesda.NET login
This commit is contained in:
@ -6,6 +6,7 @@
|
|||||||
* Adding `matchAll=<archive-name>` to a *mods's* `meta.ini` file will result in unconditional patching for all unmatching files or BSAs in
|
* Adding `matchAll=<archive-name>` to a *mods's* `meta.ini` file will result in unconditional patching for all unmatching files or BSAs in
|
||||||
that mod (issue #465)
|
that mod (issue #465)
|
||||||
* Added support for non-premium Nexus downloads via manual downloading through the in-app browser.
|
* Added support for non-premium Nexus downloads via manual downloading through the in-app browser.
|
||||||
|
* Downloads from Bethesda.NET are now supported. Can login via SkyrimSE or Fallout 4.
|
||||||
|
|
||||||
=======
|
=======
|
||||||
|
|
||||||
|
@ -87,6 +87,8 @@ namespace Wabbajack.Common
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsInstalled => GameLocation() != null;
|
||||||
|
|
||||||
public string MainExecutable { get; internal set; }
|
public string MainExecutable { get; internal set; }
|
||||||
|
|
||||||
public string GameLocation()
|
public string GameLocation()
|
||||||
|
Binary file not shown.
@ -7,16 +7,18 @@ import frida
|
|||||||
import sys
|
import sys
|
||||||
from subprocess import Popen, PIPE
|
from subprocess import Popen, PIPE
|
||||||
import psutil, time, json
|
import psutil, time, json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
known_headers = {}
|
known_headers = {}
|
||||||
shutdown = False
|
shutdown = False
|
||||||
|
|
||||||
|
|
||||||
def on_message(message, data):
|
def on_message(message, data):
|
||||||
msg_type, msg_data = message["payload"]
|
msg_type, msg_data = message["payload"]
|
||||||
if msg_type == "header":
|
if msg_type == "header":
|
||||||
header, value = msg_data.split(": ");
|
header, value = msg_data.split(": ")
|
||||||
if header not in known_headers:
|
if header not in known_headers:
|
||||||
known_headers[header] = value;
|
known_headers[header] = value
|
||||||
if msg_type == "data":
|
if msg_type == "data":
|
||||||
try:
|
try:
|
||||||
data = json.loads(msg_data)
|
data = json.loads(msg_data)
|
||||||
@ -25,6 +27,7 @@ def on_message(message, data):
|
|||||||
except:
|
except:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
def main(target_process):
|
def main(target_process):
|
||||||
session = frida.attach(target_process)
|
session = frida.attach(target_process)
|
||||||
|
|
||||||
@ -65,17 +68,35 @@ def main(target_process):
|
|||||||
script.on('message', on_message)
|
script.on('message', on_message)
|
||||||
script.load()
|
script.load()
|
||||||
|
|
||||||
while not shutdown:
|
while not shutdown and psutil.pid_exists(target_process):
|
||||||
time.sleep(0.5);
|
time.sleep(0.5)
|
||||||
|
|
||||||
session.detach()
|
session.detach()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
def wait_for_game(name):
|
|
||||||
|
def wait_for_game(started, name):
|
||||||
|
no_exe = 0
|
||||||
|
parent_path = Path(started).parent
|
||||||
while True:
|
while True:
|
||||||
time.sleep(1);
|
found = False
|
||||||
|
time.sleep(1)
|
||||||
for proc in psutil.process_iter():
|
for proc in psutil.process_iter():
|
||||||
|
try:
|
||||||
|
if Path(proc.exe()).parent == parent_path:
|
||||||
|
no_exe = 0
|
||||||
|
found = True
|
||||||
|
except:
|
||||||
|
pass
|
||||||
if proc.name() == name:
|
if proc.name() == name:
|
||||||
return proc.pid;
|
return proc.pid
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
print("Not Found " + str(no_exe))
|
||||||
|
no_exe += 1
|
||||||
|
if no_exe == 3:
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def shutdown_and_print(data):
|
def shutdown_and_print(data):
|
||||||
global shutdown
|
global shutdown
|
||||||
@ -85,19 +106,18 @@ def shutdown_and_print(data):
|
|||||||
|
|
||||||
for proc in psutil.process_iter():
|
for proc in psutil.process_iter():
|
||||||
if proc.pid == pid:
|
if proc.pid == pid:
|
||||||
proc.kill();
|
proc.kill()
|
||||||
break
|
break
|
||||||
|
|
||||||
shutdown = True;
|
shutdown = True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
start = """C:\Steam\steamapps\common\Skyrim Special Edition\SkyrimSE.exe"""
|
start = """C:\Steam\steamapps\common\Skyrim Special Edition\SkyrimSE.exe"""
|
||||||
wait_for = "SkyrimSE.exe"
|
wait_for = "SkyrimSE.exe"
|
||||||
if len(sys.argv) == 3:
|
if len(sys.argv) == 3:
|
||||||
start = sys.argv[1];
|
start = sys.argv[1]
|
||||||
wait_for = sys.argv[2]
|
wait_for = sys.argv[2]
|
||||||
target_process = Popen([start])
|
target_process = Popen([start])
|
||||||
pid = wait_for_game(wait_for);
|
pid = wait_for_game(start, wait_for)
|
||||||
main(pid)
|
main(pid)
|
@ -63,13 +63,14 @@ namespace Wabbajack.Lib.Downloaders
|
|||||||
await Utils.Log(new RequestBethesdaNetLogin()).Task;
|
await Utils.Log(new RequestBethesdaNetLogin()).Task;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<BethesdaNetData> Login()
|
public static async Task<BethesdaNetData> Login(Game game)
|
||||||
{
|
{
|
||||||
var game = Path.Combine(Game.SkyrimSpecialEdition.MetaData().GameLocation(), "SkyrimSE.exe");
|
var metadata = game.MetaData();
|
||||||
|
var gamePath = Path.Combine(metadata.GameLocation(), metadata.MainExecutable);
|
||||||
var info = new ProcessStartInfo
|
var info = new ProcessStartInfo
|
||||||
{
|
{
|
||||||
FileName = @"Downloaders\BethesdaNet\bethnetlogin.exe",
|
FileName = @"Downloaders\BethesdaNet\bethnetlogin.exe",
|
||||||
Arguments = $"\"{game}\" SkyrimSE.exe",
|
Arguments = $"\"{gamePath}\" {metadata.MainExecutable}",
|
||||||
RedirectStandardError = true,
|
RedirectStandardError = true,
|
||||||
RedirectStandardInput = true,
|
RedirectStandardInput = true,
|
||||||
RedirectStandardOutput = true,
|
RedirectStandardOutput = true,
|
||||||
@ -87,9 +88,16 @@ namespace Wabbajack.Lib.Downloaders
|
|||||||
last_line = line;
|
last_line = line;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = last_line.FromJSONString<BethesdaNetData>();
|
try
|
||||||
result.ToEcryptedJson(DataName);
|
{
|
||||||
return result;
|
var result = last_line.FromJSONString<BethesdaNetData>();
|
||||||
|
result.ToEcryptedJson(DataName);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception _)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public AbstractDownloadState GetDownloaderState(string url)
|
public AbstractDownloadState GetDownloaderState(string url)
|
||||||
|
BIN
Wabbajack/Resources/GameGridIcons/Fallout4.png
Normal file
BIN
Wabbajack/Resources/GameGridIcons/Fallout4.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 714 KiB |
BIN
Wabbajack/Resources/GameGridIcons/SkyrimSpecialEdition.png
Normal file
BIN
Wabbajack/Resources/GameGridIcons/SkyrimSpecialEdition.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 642 KiB |
64
Wabbajack/View Models/UserIntervention/BethesdaNetLoginVM.cs
Normal file
64
Wabbajack/View Models/UserIntervention/BethesdaNetLoginVM.cs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
using System.Reactive;
|
||||||
|
using System.Reactive.Linq;
|
||||||
|
using System.Reactive.Subjects;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using CefSharp;
|
||||||
|
using CefSharp.Wpf;
|
||||||
|
using ReactiveUI;
|
||||||
|
using ReactiveUI.Fody.Helpers;
|
||||||
|
using Wabbajack.Common;
|
||||||
|
using Wabbajack.Lib;
|
||||||
|
using Wabbajack.Lib.Downloaders;
|
||||||
|
using Wabbajack.Lib.WebAutomation;
|
||||||
|
|
||||||
|
namespace Wabbajack
|
||||||
|
{
|
||||||
|
public class BethesdaNetLoginVM : ViewModel, IBackNavigatingVM
|
||||||
|
{
|
||||||
|
[Reactive]
|
||||||
|
public string Instructions { get; set; }
|
||||||
|
|
||||||
|
[Reactive]
|
||||||
|
public ViewModel NavigateBackTarget { get; set; }
|
||||||
|
|
||||||
|
[Reactive]
|
||||||
|
public ReactiveCommand<Unit, Unit> BackCommand { get; set; }
|
||||||
|
|
||||||
|
public ReactiveCommand<Unit, Unit> LoginViaSkyrimSE { get; }
|
||||||
|
public ReactiveCommand<Unit, Unit> LoginViaFallout4 { get; }
|
||||||
|
|
||||||
|
private Subject<bool> LoggingIn = new Subject<bool>();
|
||||||
|
|
||||||
|
private BethesdaNetLoginVM()
|
||||||
|
{
|
||||||
|
Instructions = "Login to Bethesda.NET in-game...";
|
||||||
|
LoginViaSkyrimSE = ReactiveCommand.CreateFromTask(async () =>
|
||||||
|
{
|
||||||
|
LoggingIn.OnNext(true);
|
||||||
|
Instructions = "Starting Skyrim Special Edition...";
|
||||||
|
await BethesdaNetDownloader.Login(Game.SkyrimSpecialEdition);
|
||||||
|
LoggingIn.OnNext(false);
|
||||||
|
await BackCommand.Execute();
|
||||||
|
}, Game.SkyrimSpecialEdition.MetaData().IsInstalled
|
||||||
|
? LoggingIn.Select(e => !e).StartWith(true)
|
||||||
|
: Observable.Return(false));
|
||||||
|
|
||||||
|
LoginViaFallout4 = ReactiveCommand.CreateFromTask(async () =>
|
||||||
|
{
|
||||||
|
LoggingIn.OnNext(true);
|
||||||
|
Instructions = "Starting Fallout 4...";
|
||||||
|
await BethesdaNetDownloader.Login(Game.Fallout4);
|
||||||
|
LoggingIn.OnNext(false);
|
||||||
|
await BackCommand.Execute();
|
||||||
|
}, Game.Fallout4.MetaData().IsInstalled
|
||||||
|
? LoggingIn.Select(e => !e).StartWith(true)
|
||||||
|
: Observable.Return(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<BethesdaNetLoginVM> GetNew()
|
||||||
|
{
|
||||||
|
// Make sure libraries are extracted first
|
||||||
|
return new BethesdaNetLoginVM();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -55,6 +55,21 @@ namespace Wabbajack
|
|||||||
MainWindow.NavigateTo(oldPane);
|
MainWindow.NavigateTo(oldPane);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task WrapBethesdaNetLogin(IUserIntervention intervention)
|
||||||
|
{
|
||||||
|
CancellationTokenSource cancel = new CancellationTokenSource();
|
||||||
|
var oldPane = MainWindow.ActivePane;
|
||||||
|
var vm = await BethesdaNetLoginVM.GetNew();
|
||||||
|
MainWindow.NavigateTo(vm);
|
||||||
|
vm.BackCommand = ReactiveCommand.Create(() =>
|
||||||
|
{
|
||||||
|
cancel.Cancel();
|
||||||
|
MainWindow.NavigateTo(oldPane);
|
||||||
|
intervention.Cancel();
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public async Task Handle(IUserIntervention msg)
|
public async Task Handle(IUserIntervention msg)
|
||||||
{
|
{
|
||||||
switch (msg)
|
switch (msg)
|
||||||
@ -71,8 +86,7 @@ namespace Wabbajack
|
|||||||
await WrapBrowserJob(msg, (vm, cancel) => HandleManualNexusDownload(vm, cancel, c));
|
await WrapBrowserJob(msg, (vm, cancel) => HandleManualNexusDownload(vm, cancel, c));
|
||||||
break;
|
break;
|
||||||
case RequestBethesdaNetLogin c:
|
case RequestBethesdaNetLogin c:
|
||||||
var data = await BethesdaNetDownloader.Login();
|
await WrapBethesdaNetLogin(c);
|
||||||
c.Resume(data);
|
|
||||||
break;
|
break;
|
||||||
case AbstractNeedsLoginDownloader.RequestSiteLogin c:
|
case AbstractNeedsLoginDownloader.RequestSiteLogin c:
|
||||||
await WrapBrowserJob(msg, async (vm, cancel) =>
|
await WrapBrowserJob(msg, async (vm, cancel) =>
|
||||||
|
105
Wabbajack/Views/Interventions/BethesdaNetLoginView.xaml
Normal file
105
Wabbajack/Views/Interventions/BethesdaNetLoginView.xaml
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<UserControl x:Class="Wabbajack.BethesdaNetLoginView"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks"
|
||||||
|
xmlns:wabbajack="clr-namespace:Wabbajack"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignHeight="450" d:DesignWidth="800">
|
||||||
|
<UserControl.Resources>
|
||||||
|
<Color x:Key="TextBackgroundFill">#92000000</Color>
|
||||||
|
<SolidColorBrush x:Key="TextBackgroundFillBrush" Color="{StaticResource TextBackgroundFill}" />
|
||||||
|
<Color x:Key="TextBackgroundHoverFill">#DF000000</Color>
|
||||||
|
<Style x:Key="BackgroundBlurStyle" TargetType="TextBlock">
|
||||||
|
<Setter Property="Background" Value="{StaticResource TextBackgroundFillBrush}" />
|
||||||
|
<Setter Property="Foreground" Value="Transparent" />
|
||||||
|
<Setter Property="Visibility" Value="Visible" />
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding IsMouseOver, RelativeSource={RelativeSource AncestorType={x:Type Button}}}" Value="True">
|
||||||
|
<DataTrigger.EnterActions>
|
||||||
|
<BeginStoryboard>
|
||||||
|
<Storyboard>
|
||||||
|
<ColorAnimation
|
||||||
|
Storyboard.TargetProperty="(TextBlock.Background).(SolidColorBrush.Color)"
|
||||||
|
To="{StaticResource TextBackgroundHoverFill}"
|
||||||
|
Duration="0:0:0.06" />
|
||||||
|
</Storyboard>
|
||||||
|
</BeginStoryboard>
|
||||||
|
</DataTrigger.EnterActions>
|
||||||
|
<DataTrigger.ExitActions>
|
||||||
|
<BeginStoryboard>
|
||||||
|
<Storyboard>
|
||||||
|
<ColorAnimation
|
||||||
|
Storyboard.TargetProperty="(TextBlock.Background).(SolidColorBrush.Color)"
|
||||||
|
To="{StaticResource TextBackgroundFill}"
|
||||||
|
Duration="0:0:0.06" />
|
||||||
|
</Storyboard>
|
||||||
|
</BeginStoryboard>
|
||||||
|
</DataTrigger.ExitActions>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</UserControl.Resources>
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="47" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<wabbajack:TopProgressView
|
||||||
|
Title="{Binding Instructions}"
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.RowSpan="2"
|
||||||
|
ShadowMargin="False" />
|
||||||
|
<Button
|
||||||
|
x:Name="BackButton"
|
||||||
|
Grid.Row="0"
|
||||||
|
Width="30"
|
||||||
|
Height="30"
|
||||||
|
Margin="7,5,0,0"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Command="{Binding BackCommand}"
|
||||||
|
Style="{StaticResource IconCircleButtonStyle}"
|
||||||
|
ToolTip="Back to main menu">
|
||||||
|
<iconPacks:PackIconMaterial Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}" Kind="ArrowLeft" />
|
||||||
|
</Button>
|
||||||
|
<Grid Grid.Row="1">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="100"></RowDefinition>
|
||||||
|
<RowDefinition></RowDefinition>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition></ColumnDefinition>
|
||||||
|
<ColumnDefinition></ColumnDefinition>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock TextWrapping="Wrap" Margin = "10" Grid.Row="0" Grid.ColumnSpan="2" FontSize="20" Text="Login with your game of choice. Clicking the button will launch the game. Once at the main menu, load the `Mods` menu. The game will close once you've logged into Bethesda.NET"></TextBlock>
|
||||||
|
<Button Grid.Row="1" Grid.Column="0" Command="{Binding LoginViaSkyrimSE}">
|
||||||
|
<Image Source="../../Resources/GameGridIcons/SkyrimSpecialEdition.png">
|
||||||
|
<Image.Style>
|
||||||
|
<Style TargetType="Image">
|
||||||
|
<Style.Triggers>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.5" />
|
||||||
|
</Trigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</Image.Style>
|
||||||
|
</Image>
|
||||||
|
</Button>
|
||||||
|
<Button Grid.Row="1" Grid.Column="1" Command="{Binding LoginViaFallout4}">
|
||||||
|
<Image Source="../../Resources/GameGridIcons/Fallout4.png">
|
||||||
|
<Image.Style>
|
||||||
|
<Style TargetType="Image">
|
||||||
|
<Style.Triggers>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.5" />
|
||||||
|
</Trigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</Image.Style>
|
||||||
|
</Image>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
13
Wabbajack/Views/Interventions/BethesdaNetLoginView.xaml.cs
Normal file
13
Wabbajack/Views/Interventions/BethesdaNetLoginView.xaml.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using System.Windows.Controls;
|
||||||
|
|
||||||
|
namespace Wabbajack
|
||||||
|
{
|
||||||
|
public partial class BethesdaNetLoginView : UserControl
|
||||||
|
{
|
||||||
|
public BethesdaNetLoginView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -37,6 +37,9 @@
|
|||||||
<DataTemplate DataType="{x:Type local:WebBrowserVM}">
|
<DataTemplate DataType="{x:Type local:WebBrowserVM}">
|
||||||
<local:WebBrowserView />
|
<local:WebBrowserView />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
<DataTemplate DataType="{x:Type local:BethesdaNetLoginVM}">
|
||||||
|
<local:BethesdaNetLoginView />
|
||||||
|
</DataTemplate>
|
||||||
<DataTemplate DataType="{x:Type local:SettingsVM}">
|
<DataTemplate DataType="{x:Type local:SettingsVM}">
|
||||||
<local:SettingsView ViewModel="{Binding}" />
|
<local:SettingsView ViewModel="{Binding}" />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
@ -39,6 +39,8 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Remove="Readme.md" />
|
<None Remove="Readme.md" />
|
||||||
|
<None Remove="Resources\GameGridIcons\Fallout4.png" />
|
||||||
|
<None Remove="Resources\GameGridIcons\SkyrimSpecialEdition.png" />
|
||||||
<None Remove="Resources\MO2Button.png" />
|
<None Remove="Resources\MO2Button.png" />
|
||||||
<None Remove="Resources\VortexButton.png" />
|
<None Remove="Resources\VortexButton.png" />
|
||||||
<None Remove="Resources\Wabba_Ded.png" />
|
<None Remove="Resources\Wabba_Ded.png" />
|
||||||
@ -78,11 +80,14 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Resource Include="Resources\GameGridIcons\Fallout4.png" />
|
||||||
|
<Resource Include="Resources\GameGridIcons\SkyrimSpecialEdition.png" />
|
||||||
<Resource Include="Resources\MO2Button.png" />
|
<Resource Include="Resources\MO2Button.png" />
|
||||||
<Resource Include="Resources\VortexButton.png" />
|
<Resource Include="Resources\VortexButton.png" />
|
||||||
<Resource Include="Resources\Wabba_Ded.png" />
|
<Resource Include="Resources\Wabba_Ded.png" />
|
||||||
<Resource Include="Resources\Wabba_Mouth.png" />
|
<Resource Include="Resources\Wabba_Mouth.png" />
|
||||||
<Resource Include="Resources\Wabba_Mouth_No_Text.png" />
|
<Resource Include="Resources\Wabba_Mouth_No_Text.png" />
|
||||||
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
Reference in New Issue
Block a user