mirror of
https://github.com/D1GQ/BetterAmongUs.git
synced 2026-10-20 01:46:19 +00:00
9 files changed
+57
-221
No files matched your search
@@ -0,0 +1,34 @@
|
|||||||
|
name: Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, dev ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-mod:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup .NET 6
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: '6.0.x'
|
||||||
|
|
||||||
|
- name: Add BepInEx NuGet source
|
||||||
|
run: |
|
||||||
|
# Add the BepInEx NuGet feed
|
||||||
|
dotnet nuget add source --name bepinex "https://nuget.bepinex.dev/v3/index.json"
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: dotnet build src/*.csproj --configuration Release
|
||||||
|
|
||||||
|
- name: Archive DLL file
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: BetterAmongUs.dll
|
||||||
|
path: src/bin/Release/net6.0/BetterAmongUs.dll
|
||||||
@@ -96,7 +96,6 @@ internal class BAUPlugin : BasePlugin
|
|||||||
Translator.Init();
|
Translator.Init();
|
||||||
Harmony.PatchAll();
|
Harmony.PatchAll();
|
||||||
GameSettingMenuPatch.SetupSettings(true);
|
GameSettingMenuPatch.SetupSettings(true);
|
||||||
FileChecker.Initialize();
|
|
||||||
InstanceAttribute.RegisterAll();
|
InstanceAttribute.RegisterAll();
|
||||||
OutfitData.Init();
|
OutfitData.Init();
|
||||||
|
|
||||||
|
|||||||
+20
-20
@@ -7,9 +7,26 @@
|
|||||||
<langVersion>preview</langVersion>
|
<langVersion>preview</langVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<!-- Build Events -->
|
||||||
<None Include="..\.editorconfig" Link=".editorconfig" />
|
<PropertyGroup>
|
||||||
</ItemGroup>
|
<IsCI Condition="'$(CI)' == 'true' or '$(GITHUB_ACTIONS)' == 'true' or '$(TF_BUILD)' == 'true'">true</IsCI>
|
||||||
|
<IsCI Condition="'$(IsCI)' == ''">false</IsCI>
|
||||||
|
<ModPathFile>build\mod_folder_path</ModPathFile>
|
||||||
|
<GamePath Condition="Exists('$(ModPathFile)') and '$(IsCI)' != 'true'">$([System.IO.File]::ReadAllText('$(ModPathFile)').Trim())</GamePath>
|
||||||
|
<ModsPath>$(GamePath)</ModsPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<Target Name="CopyModToGame" AfterTargets="PostBuildEvent"
|
||||||
|
Condition="'$(GamePath)' != '' and Exists('$(GamePath)') and '$(IsCI)' != 'true'">
|
||||||
|
<Message Text="Copying mod to $(GamePath)" Importance="high" />
|
||||||
|
<Copy SourceFiles="$(TargetDir)$(AssemblyName).dll" DestinationFolder="$(GamePath)" SkipUnchangedFiles="true" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
<Target Name="WarnIfNoGamePath" AfterTargets="PostBuildEvent"
|
||||||
|
Condition="'$(GamePath)' == '' and '$(IsCI)' != 'true'">
|
||||||
|
<Warning Text="Game path not found. Set game path in build\mod_folder_path." />
|
||||||
|
<Message Text="Mod was NOT copied to game folder!" Importance="high" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AmongUs.GameLibs.Steam" Version="2025.11.18" />
|
<PackageReference Include="AmongUs.GameLibs.Steam" Version="2025.11.18" />
|
||||||
@@ -24,21 +41,4 @@
|
|||||||
<!-- Embed all files in the Resources folder -->
|
<!-- Embed all files in the Resources folder -->
|
||||||
<EmbeddedResource Include="Resources\**\*" />
|
<EmbeddedResource Include="Resources\**\*" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Build Events -->
|
|
||||||
<PropertyGroup>
|
|
||||||
<ModPathFile>build\mod_folder_path</ModPathFile>
|
|
||||||
<GamePath Condition="Exists('$(ModPathFile)')">$([System.IO.File]::ReadAllText('$(ModPathFile)').Trim())</GamePath>
|
|
||||||
<ModsPath>$(GamePath)\Mods</ModsPath>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<Target Name="CopyModToGame" AfterTargets="PostBuildEvent" Condition="Exists('$(ModsPath)')">
|
|
||||||
<Message Text="Copying mod to $(ModsPath)" Importance="high" />
|
|
||||||
<Copy SourceFiles="$(TargetDir)$(AssemblyName).dll" DestinationFolder="$(ModsPath)" SkipUnchangedFiles="true" />
|
|
||||||
</Target>
|
|
||||||
|
|
||||||
<Target Name="WarnIfNoGamePath" AfterTargets="PostBuildEvent" Condition="!Exists('$(ModsPath)')">
|
|
||||||
<Warning Text="Game path not found. Set game path in mod_folder_path." />
|
|
||||||
<Message Text="Mod was NOT copied to game folder!" Importance="high" />
|
|
||||||
</Target>
|
|
||||||
</Project>
|
</Project>
|
||||||
+1
-1
@@ -9,7 +9,7 @@ internal static class ModInfo
|
|||||||
internal const string PluginName = "BetterAmongUs";
|
internal const string PluginName = "BetterAmongUs";
|
||||||
internal const string PluginGuid = "com.d1gq.betteramongus";
|
internal const string PluginGuid = "com.d1gq.betteramongus";
|
||||||
internal const string PluginVersion = "1.3.1";
|
internal const string PluginVersion = "1.3.1";
|
||||||
internal const string ReleaseDate = "01.08.2026"; // mm/dd/yyyy
|
internal const string ReleaseDate = "01.09.2026"; // mm/dd/yyyy
|
||||||
internal const string Github = "https://github.com/D1GQ/BetterAmongUs-Public";
|
internal const string Github = "https://github.com/D1GQ/BetterAmongUs-Public";
|
||||||
internal const string Discord = "https://discord.gg/vjYrXpzNAn";
|
internal const string Discord = "https://discord.gg/vjYrXpzNAn";
|
||||||
}
|
}
|
||||||
@@ -124,7 +124,7 @@ internal static class BetterAntiCheat
|
|||||||
internal static void CheckRPC(PlayerControl player, byte callId, MessageReader oldReader)
|
internal static void CheckRPC(PlayerControl player, byte callId, MessageReader oldReader)
|
||||||
{
|
{
|
||||||
if (player == null || player?.Data == null) return;
|
if (player == null || player?.Data == null) return;
|
||||||
if (!IsEnabled || !BAUPlugin.AntiCheat.Value || (GameState.IsBetterHostLobby && !GameState.IsHost) || !BetterGameSettings.DetectInvalidRPCs.GetBool()) return;
|
if (!IsEnabled || !BAUPlugin.AntiCheat.Value || !BetterGameSettings.DetectInvalidRPCs.GetBool()) return;
|
||||||
if (player.IsLocalPlayer() && player.IsHost()) return;
|
if (player.IsLocalPlayer() && player.IsHost()) return;
|
||||||
|
|
||||||
MessageReader reader = MessageReader.Get(oldReader);
|
MessageReader reader = MessageReader.Get(oldReader);
|
||||||
@@ -138,7 +138,7 @@ internal static class BetterAntiCheat
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (player == null || player?.Data == null) return true;
|
if (player == null || player?.Data == null) return true;
|
||||||
if (!IsEnabled || !BAUPlugin.AntiCheat.Value || (GameState.IsBetterHostLobby && !GameState.IsHost) || !BetterGameSettings.DetectInvalidRPCs.GetBool()) return true;
|
if (!IsEnabled || !BAUPlugin.AntiCheat.Value || !BetterGameSettings.DetectInvalidRPCs.GetBool()) return true;
|
||||||
if (player.IsLocalPlayer() && player.IsHost()) return true;
|
if (player.IsLocalPlayer() && player.IsHost()) return true;
|
||||||
|
|
||||||
MessageReader reader = MessageReader.Get(oldReader);
|
MessageReader reader = MessageReader.Get(oldReader);
|
||||||
|
|||||||
@@ -1,159 +0,0 @@
|
|||||||
using BetterAmongUs.Helpers;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
namespace BetterAmongUs.Modules;
|
|
||||||
|
|
||||||
internal static class FileChecker
|
|
||||||
{
|
|
||||||
private static bool enabled = false;
|
|
||||||
private static FileSystemWatcher? fileWatcher;
|
|
||||||
private static bool hasUnauthorizedFileOrMod = false;
|
|
||||||
internal static string WarningMsg { get; private set; } = string.Empty;
|
|
||||||
internal static bool HasShownWarning { get; set; } = false;
|
|
||||||
internal static bool HasUnauthorizedFileOrMod => hasUnauthorizedFileOrMod;
|
|
||||||
|
|
||||||
private static readonly ReadOnlyCollection<string> TrustedNamespaces = new(new List<string>
|
|
||||||
{
|
|
||||||
"System", "Unity", "Harmony", "BepInEx", "Microsoft", "Il2Cpp", "Hazel", "AppleAuth", "QRCoder",
|
|
||||||
"MonoMod", "netstandard", "mscorlib", "AssetRipper", "Cpp2IL", "AsmResolver", "Iced", "CsvHelper",
|
|
||||||
"SemanticVersioning", "Mono.Cecil", "Assembly-CSharp", "StableNameDotNet", "AmongUsCaching",
|
|
||||||
"Disarm", "Gee.External.Capstone", "Rewired_Core", "AddressablesPlayAssetDelivery", "Newtonsoft.Json", "Purchasing.Common",
|
|
||||||
"Assembly-CSharp-firstpass", "Sentry", "Rewired_Windows", "__Generated", "XGamingRuntime",
|
|
||||||
"BetterAmongUs", "MCI", "CrowdedMod", "Mini.RegionInstall", "Unlock", "Skin"
|
|
||||||
});
|
|
||||||
|
|
||||||
private static readonly ReadOnlyCollection<string> UntrustedNamespaces = new(new List<string>
|
|
||||||
{
|
|
||||||
"Sicko", "AmongUsMenu", "Malum", "GoatNetClient", "SM", "AUM", "GNC", "MM", "Cheat", "Menu", "Hack", "Exploit", "Bypass", "Crack", "Spoof"
|
|
||||||
});
|
|
||||||
|
|
||||||
private static readonly string[] TrustedPaths =
|
|
||||||
{
|
|
||||||
Environment.CurrentDirectory,
|
|
||||||
Path.Combine(Environment.CurrentDirectory, "BepInEx"),
|
|
||||||
};
|
|
||||||
|
|
||||||
private static readonly string[] UnauthorizedFiles = [
|
|
||||||
"version.dll",
|
|
||||||
"sicko-settings.json",
|
|
||||||
"sicko-log.txt",
|
|
||||||
"sicko-prev-log.txt",
|
|
||||||
"sicko-config",
|
|
||||||
"settings.json",
|
|
||||||
"aum-log.txt",
|
|
||||||
"aum-prev-log.txt"
|
|
||||||
];
|
|
||||||
|
|
||||||
internal static void Initialize()
|
|
||||||
{
|
|
||||||
if (enabled) return;
|
|
||||||
enabled = true;
|
|
||||||
|
|
||||||
AppDomain.CurrentDomain.AssemblyLoad += (sender, args) =>
|
|
||||||
{
|
|
||||||
CheckUnauthorizedAssemblies();
|
|
||||||
CheckUnauthorizedFiles();
|
|
||||||
};
|
|
||||||
|
|
||||||
fileWatcher = new FileSystemWatcher(Environment.CurrentDirectory)
|
|
||||||
{
|
|
||||||
NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.DirectoryName,
|
|
||||||
IncludeSubdirectories = true
|
|
||||||
};
|
|
||||||
|
|
||||||
fileWatcher.Created += (sender, args) => CheckUnauthorizedFiles();
|
|
||||||
fileWatcher.Deleted += (sender, args) => CheckUnauthorizedFiles();
|
|
||||||
|
|
||||||
fileWatcher.EnableRaisingEvents = true;
|
|
||||||
|
|
||||||
CheckUnauthorizedFiles();
|
|
||||||
CheckUnauthorizedAssemblies();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void CheckUnauthorizedFiles()
|
|
||||||
{
|
|
||||||
if (hasUnauthorizedFileOrMod) return;
|
|
||||||
|
|
||||||
foreach (var fileName in UnauthorizedFiles)
|
|
||||||
{
|
|
||||||
if (File.Exists(Path.Combine(Environment.CurrentDirectory, fileName)))
|
|
||||||
{
|
|
||||||
WarningMsg = "<#D20200>Unauthorized File Detected</color>\n<#9D9D9D><size=70%>Look in logs for further information!</size></color>";
|
|
||||||
Logger_.Warning($"Unauthorized File: {Path.Combine(Environment.CurrentDirectory, fileName)}");
|
|
||||||
if (GameState.IsInGame)
|
|
||||||
{
|
|
||||||
SceneChanger.ChangeScene("MainMenu");
|
|
||||||
}
|
|
||||||
hasUnauthorizedFileOrMod = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static readonly HashSet<Assembly> _processedAssemblies = [];
|
|
||||||
|
|
||||||
private static void CheckUnauthorizedAssemblies()
|
|
||||||
{
|
|
||||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
|
||||||
{
|
|
||||||
if (_processedAssemblies.Contains(assembly) || assembly.IsDynamic)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
_processedAssemblies.Add(assembly);
|
|
||||||
|
|
||||||
if (UntrustedNamespaces.Any(ns => assembly.FullName.Contains(ns, StringComparison.OrdinalIgnoreCase)))
|
|
||||||
{
|
|
||||||
if (TrustedNamespaces.Any(ns => assembly.FullName.Contains(ns, StringComparison.OrdinalIgnoreCase)))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
WarningMsg = "<#D20200>Untrusted Assembly Detected</color>\n<#9D9D9D><size=70%>Look in logs for further information!</size></color>";
|
|
||||||
Logger_.Warning($"Unauthorized Assembly: {assembly.FullName} (Untrusted Namespace)");
|
|
||||||
hasUnauthorizedFileOrMod = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TrustedNamespaces.Any(ns => assembly.FullName.Contains(ns, StringComparison.OrdinalIgnoreCase)))
|
|
||||||
{
|
|
||||||
WarningMsg = "<#D20200>Unregistered Assembly Detected</color>\n<#9D9D9D><size=70%>Look in logs for further information!</size></color>";
|
|
||||||
Logger_.Warning($"Unauthorized Assembly: {assembly.FullName} (Unregistered Namespace)");
|
|
||||||
hasUnauthorizedFileOrMod = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!IsSafeLocation(assembly))
|
|
||||||
{
|
|
||||||
WarningMsg = "<#D20200>Unregistered Assembly Location Detected</color>\n<#9D9D9D><size=70%>Look in logs for further information!</size></color>";
|
|
||||||
Logger_.Warning($"Unauthorized Assembly: {assembly.FullName} (Unregistered Location: {assembly.Location})");
|
|
||||||
hasUnauthorizedFileOrMod = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static void SetHasUnauthorizedFileOrMod()
|
|
||||||
{
|
|
||||||
hasUnauthorizedFileOrMod = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static void SetWarningMsg(string msg)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(WarningMsg))
|
|
||||||
{
|
|
||||||
WarningMsg = msg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsSafeLocation(Assembly assembly)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(assembly.Location))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return TrustedPaths.Any(path => assembly.Location.StartsWith(path, StringComparison.OrdinalIgnoreCase));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
using AmongUs.GameOptions;
|
using AmongUs.GameOptions;
|
||||||
using BetterAmongUs.Helpers;
|
|
||||||
using BetterAmongUs.Mono;
|
|
||||||
|
|
||||||
namespace BetterAmongUs.Modules;
|
namespace BetterAmongUs.Modules;
|
||||||
|
|
||||||
@@ -105,7 +103,6 @@ internal static class GameState
|
|||||||
internal static bool IsShip => ShipStatus.Instance != null;
|
internal static bool IsShip => ShipStatus.Instance != null;
|
||||||
internal static bool IsHost => AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost;
|
internal static bool IsHost => AmongUsClient.Instance != null && AmongUsClient.Instance.AmHost;
|
||||||
internal static bool IsPrivateOnlyLobby => (BAUPlugin.PrivateOnlyLobby.Value || AmongUsClient.Instance.AmLocalHost) && IsHost;
|
internal static bool IsPrivateOnlyLobby => (BAUPlugin.PrivateOnlyLobby.Value || AmongUsClient.Instance.AmLocalHost) && IsHost;
|
||||||
internal static bool IsBetterHostLobby => PlayerControl.LocalPlayer?.IsHost() == true || BAUPlugin.AllPlayerControls.Any(pc => pc?.BetterData() != null && pc.IsHost() && pc.BetterData().IsBetterUser && pc.BetterData().IsVerifiedBetterUser);
|
|
||||||
internal static bool IsCanMove => PlayerControl.LocalPlayer?.CanMove is true;
|
internal static bool IsCanMove => PlayerControl.LocalPlayer?.CanMove is true;
|
||||||
internal static bool IsDead => PlayerControl.LocalPlayer?.Data?.IsDead is true;
|
internal static bool IsDead => PlayerControl.LocalPlayer?.Data?.IsDead is true;
|
||||||
}
|
}
|
||||||
@@ -35,13 +35,6 @@ internal static class ClientPatch
|
|||||||
[HarmonyPrefix]
|
[HarmonyPrefix]
|
||||||
private static bool SetOnline_Prefix(SignInStatusComponent __instance)
|
private static bool SetOnline_Prefix(SignInStatusComponent __instance)
|
||||||
{
|
{
|
||||||
var lines = "<color=#ebbd34>----------------------------------------------------------------------------------------------</color>";
|
|
||||||
if (!FileChecker.HasShownWarning && FileChecker.HasUnauthorizedFileOrMod)
|
|
||||||
{
|
|
||||||
Utils.ShowPopUp($"{lines}\n<b><size=200%><#0DFF00>{Translator.GetString("BetterAmongUs")}</color></size></b>\n<color=#757575><u><size=150%>{FileChecker.WarningMsg}</size></u>\n{lines}");
|
|
||||||
FileChecker.HasShownWarning = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var varSupportedVersions = BAUPlugin.SupportedAmongUsVersions;
|
var varSupportedVersions = BAUPlugin.SupportedAmongUsVersions;
|
||||||
Version currentVersion = new(BAUPlugin.AppVersion);
|
Version currentVersion = new(BAUPlugin.AppVersion);
|
||||||
Version firstSupportedVersion = new(varSupportedVersions.First());
|
Version firstSupportedVersion = new(varSupportedVersions.First());
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
using BetterAmongUs.Helpers;
|
using BetterAmongUs.Helpers;
|
||||||
using BetterAmongUs.Managers;
|
using BetterAmongUs.Managers;
|
||||||
using BetterAmongUs.Modules;
|
|
||||||
using HarmonyLib;
|
using HarmonyLib;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.SceneManagement;
|
|
||||||
|
|
||||||
namespace BetterAmongUs.Patches.Client;
|
namespace BetterAmongUs.Patches.Client;
|
||||||
|
|
||||||
@@ -17,32 +15,6 @@ internal static class MainMenuPatch
|
|||||||
[HarmonyPostfix]
|
[HarmonyPostfix]
|
||||||
private static void LateUpdate_Postfix(MainMenuManager __instance)
|
private static void LateUpdate_Postfix(MainMenuManager __instance)
|
||||||
{
|
{
|
||||||
if (FileChecker.HasUnauthorizedFileOrMod)
|
|
||||||
{
|
|
||||||
__instance.playButton.enabled = false;
|
|
||||||
sprite ??= __instance.playButton.transform.Find("Inactive").GetComponent<SpriteRenderer>();
|
|
||||||
if (sprite != null)
|
|
||||||
{
|
|
||||||
sprite.color = new Color(0.7f, 0.7f, 0.7f);
|
|
||||||
ObjectHelper.AddColor(sprite);
|
|
||||||
}
|
|
||||||
|
|
||||||
SceneManager.s_AllowLoadScene = false;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
__instance.playButton.enabled = true;
|
|
||||||
sprite ??= __instance.playButton.transform.Find("Inactive").GetComponent<SpriteRenderer>();
|
|
||||||
if (sprite != null)
|
|
||||||
{
|
|
||||||
sprite.color = Color.white;
|
|
||||||
ObjectHelper.AddColor(sprite);
|
|
||||||
}
|
|
||||||
|
|
||||||
SceneManager.s_AllowLoadScene = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
List<PassiveButton> buttons = [__instance.playButton, __instance.inventoryButton, __instance.shopButton, __instance.playLocalButton, __instance.PlayOnlineButton, __instance.backButtonOnline,
|
List<PassiveButton> buttons = [__instance.playButton, __instance.inventoryButton, __instance.shopButton, __instance.playLocalButton, __instance.PlayOnlineButton, __instance.backButtonOnline,
|
||||||
__instance.newsButton, __instance.myAccountButton, __instance.settingsButton, __instance.howToPlayButton, __instance.freePlayButton, __instance.accountCTAButton, __instance.accountStatsButton];
|
__instance.newsButton, __instance.myAccountButton, __instance.settingsButton, __instance.howToPlayButton, __instance.freePlayButton, __instance.accountCTAButton, __instance.accountStatsButton];
|
||||||
foreach (var button in buttons)
|
foreach (var button in buttons)
|
||||||
|
|||||||
Reference in new issue
Block a user