mirror of
https://github.com/Nathanwoodburn/FireWallet.git
synced 2025-12-06 08:33:00 +11:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
23cbace1ea
|
|||
|
6894e9c079
|
|||
|
95d0498672
|
@@ -12,7 +12,7 @@
|
||||
<PackageIcon>HSDBatcher.png</PackageIcon>
|
||||
<RepositoryUrl>https://github.com/Nathanwoodburn/FireWallet</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<Version>3.0</Version>
|
||||
<Version>3.1</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -30,7 +30,6 @@
|
||||
<PackageReference Include="DnsClient" Version="1.7.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="QRCoder" Version="1.4.3" />
|
||||
<PackageReference Include="Yubico.YubiKey" Version="1.7.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
48
FireWallet/MainForm.Designer.cs
generated
48
FireWallet/MainForm.Designer.cs
generated
@@ -95,7 +95,6 @@ namespace FireWallet
|
||||
textBoxReceiveAddress = new TextBox();
|
||||
labelReceive1 = new Label();
|
||||
panelDomains = new Panel();
|
||||
labelDomainSort = new Label();
|
||||
comboBoxDomainSort = new ComboBox();
|
||||
buttonExportDomains = new Button();
|
||||
groupBoxDomains = new GroupBox();
|
||||
@@ -104,7 +103,6 @@ namespace FireWallet
|
||||
textBoxDomainSearch = new TextBox();
|
||||
panelSettings = new Panel();
|
||||
groupBoxSettingsWallet = new GroupBox();
|
||||
buttonSettingsYubikey = new Button();
|
||||
buttonSettingsRescan = new Button();
|
||||
buttonSeed = new Button();
|
||||
groupBoxSettingsMisc = new GroupBox();
|
||||
@@ -125,6 +123,7 @@ namespace FireWallet
|
||||
textBoxExAddr = new TextBox();
|
||||
labelSettings4 = new Label();
|
||||
textBoxExTX = new TextBox();
|
||||
labelDomainSort = new Label();
|
||||
statusStripmain.SuspendLayout();
|
||||
panelaccount.SuspendLayout();
|
||||
groupBoxaccount.SuspendLayout();
|
||||
@@ -242,7 +241,7 @@ namespace FireWallet
|
||||
//
|
||||
panelaccount.BackColor = Color.Transparent;
|
||||
panelaccount.Controls.Add(groupBoxaccount);
|
||||
panelaccount.Location = new Point(132, 30);
|
||||
panelaccount.Location = new Point(1082, 211);
|
||||
panelaccount.Name = "panelaccount";
|
||||
panelaccount.Size = new Size(1074, 642);
|
||||
panelaccount.TabIndex = 1;
|
||||
@@ -575,7 +574,7 @@ namespace FireWallet
|
||||
panelSend.Controls.Add(labelSendingTo);
|
||||
panelSend.Controls.Add(labelSendPrompt);
|
||||
panelSend.Controls.Add(labelHIPArrow);
|
||||
panelSend.Location = new Point(880, 441);
|
||||
panelSend.Location = new Point(138, 33);
|
||||
panelSend.Name = "panelSend";
|
||||
panelSend.Size = new Size(974, 521);
|
||||
panelSend.TabIndex = 2;
|
||||
@@ -793,22 +792,12 @@ namespace FireWallet
|
||||
panelDomains.Controls.Add(groupBoxDomains);
|
||||
panelDomains.Controls.Add(labelDomainSearch);
|
||||
panelDomains.Controls.Add(textBoxDomainSearch);
|
||||
panelDomains.Location = new Point(861, 364);
|
||||
panelDomains.Location = new Point(120, 48);
|
||||
panelDomains.Name = "panelDomains";
|
||||
panelDomains.Size = new Size(920, 536);
|
||||
panelDomains.TabIndex = 18;
|
||||
panelDomains.Visible = false;
|
||||
//
|
||||
// labelDomainSort
|
||||
//
|
||||
labelDomainSort.AutoSize = true;
|
||||
labelDomainSort.Font = new Font("Segoe UI", 12F, FontStyle.Regular, GraphicsUnit.Point);
|
||||
labelDomainSort.Location = new Point(638, 15);
|
||||
labelDomainSort.Name = "labelDomainSort";
|
||||
labelDomainSort.Size = new Size(42, 21);
|
||||
labelDomainSort.TabIndex = 12;
|
||||
labelDomainSort.Text = "Sort:";
|
||||
//
|
||||
// comboBoxDomainSort
|
||||
//
|
||||
comboBoxDomainSort.DropDownStyle = ComboBoxStyle.DropDownList;
|
||||
@@ -882,7 +871,7 @@ namespace FireWallet
|
||||
panelSettings.Controls.Add(buttonSettingsSave);
|
||||
panelSettings.Controls.Add(groupBoxSettingsExplorer);
|
||||
panelSettings.Font = new Font("Segoe UI", 12F, FontStyle.Regular, GraphicsUnit.Point);
|
||||
panelSettings.Location = new Point(848, 306);
|
||||
panelSettings.Location = new Point(1065, 51);
|
||||
panelSettings.Name = "panelSettings";
|
||||
panelSettings.Size = new Size(930, 550);
|
||||
panelSettings.TabIndex = 19;
|
||||
@@ -890,7 +879,6 @@ namespace FireWallet
|
||||
//
|
||||
// groupBoxSettingsWallet
|
||||
//
|
||||
groupBoxSettingsWallet.Controls.Add(buttonSettingsYubikey);
|
||||
groupBoxSettingsWallet.Controls.Add(buttonSettingsRescan);
|
||||
groupBoxSettingsWallet.Controls.Add(buttonSeed);
|
||||
groupBoxSettingsWallet.Location = new Point(507, 16);
|
||||
@@ -900,17 +888,6 @@ namespace FireWallet
|
||||
groupBoxSettingsWallet.TabStop = false;
|
||||
groupBoxSettingsWallet.Text = "Wallet Controls";
|
||||
//
|
||||
// buttonSettingsYubikey
|
||||
//
|
||||
buttonSettingsYubikey.FlatStyle = FlatStyle.Flat;
|
||||
buttonSettingsYubikey.Location = new Point(6, 133);
|
||||
buttonSettingsYubikey.Name = "buttonSettingsYubikey";
|
||||
buttonSettingsYubikey.Size = new Size(98, 50);
|
||||
buttonSettingsYubikey.TabIndex = 9;
|
||||
buttonSettingsYubikey.Text = "YubiKey";
|
||||
buttonSettingsYubikey.UseVisualStyleBackColor = true;
|
||||
buttonSettingsYubikey.Click += buttonSettingsYubikey_Click;
|
||||
//
|
||||
// buttonSettingsRescan
|
||||
//
|
||||
buttonSettingsRescan.FlatStyle = FlatStyle.Flat;
|
||||
@@ -1104,15 +1081,25 @@ namespace FireWallet
|
||||
textBoxExTX.Size = new Size(307, 29);
|
||||
textBoxExTX.TabIndex = 1;
|
||||
//
|
||||
// labelDomainSort
|
||||
//
|
||||
labelDomainSort.AutoSize = true;
|
||||
labelDomainSort.Font = new Font("Segoe UI", 12F, FontStyle.Regular, GraphicsUnit.Point);
|
||||
labelDomainSort.Location = new Point(638, 15);
|
||||
labelDomainSort.Name = "labelDomainSort";
|
||||
labelDomainSort.Size = new Size(42, 21);
|
||||
labelDomainSort.TabIndex = 12;
|
||||
labelDomainSort.Text = "Sort:";
|
||||
//
|
||||
// MainForm
|
||||
//
|
||||
AutoScaleDimensions = new SizeF(7F, 15F);
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
ClientSize = new Size(1152, 575);
|
||||
Controls.Add(panelaccount);
|
||||
Controls.Add(panelSettings);
|
||||
Controls.Add(panelDomains);
|
||||
Controls.Add(panelSend);
|
||||
Controls.Add(panelSettings);
|
||||
Controls.Add(panelaccount);
|
||||
Controls.Add(panelPortfolio);
|
||||
Controls.Add(panelRecieve);
|
||||
Controls.Add(panelNav);
|
||||
@@ -1247,6 +1234,5 @@ namespace FireWallet
|
||||
private Label labelSendingHIPAddress;
|
||||
private ComboBox comboBoxDomainSort;
|
||||
private Label labelDomainSort;
|
||||
private Button buttonSettingsYubikey;
|
||||
}
|
||||
}
|
||||
@@ -7,14 +7,13 @@ using QRCoder;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Security.Policy;
|
||||
using System.Windows.Forms;
|
||||
using System.Net;
|
||||
using DnsClient;
|
||||
using DnsClient.Protocol;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Net.Security;
|
||||
// Used to use Yubikey to login
|
||||
using Yubico.YubiKey;
|
||||
using Yubico.YubiKey.Piv;
|
||||
|
||||
namespace FireWallet
|
||||
{
|
||||
@@ -222,6 +221,28 @@ namespace FireWallet
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else
|
||||
{
|
||||
if (!Directory.Exists(dir + "hsd"))
|
||||
{
|
||||
NotifyForm Notifyinstall = new NotifyForm("Installing hsd\nThis may take a few minutes\nDo not close FireWallet", false);
|
||||
Notifyinstall.Show();
|
||||
// Wait for the notification to show
|
||||
await Task.Delay(1000);
|
||||
|
||||
string repositoryUrl = "https://github.com/handshake-org/hsd.git";
|
||||
string destinationPath = dir + "hsd";
|
||||
CloneRepository(repositoryUrl, destinationPath);
|
||||
|
||||
Notifyinstall.CloseNotification();
|
||||
Notifyinstall.Dispose();
|
||||
}
|
||||
if (!Directory.Exists(dir + "hsd\\node_modules"))
|
||||
{
|
||||
AddLog("HSD install failed");
|
||||
this.Close();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -701,27 +722,7 @@ namespace FireWallet
|
||||
}
|
||||
|
||||
account = comboBoxaccount.Text;
|
||||
|
||||
if (textBoxaccountpassword.Text == "")
|
||||
{
|
||||
if (File.Exists(dir + account + ".yubikey"))
|
||||
{
|
||||
// Check if yubikey is plugged in
|
||||
var devices = YubiKeyDevice.FindAll();
|
||||
if (devices.Count() > 0)
|
||||
{
|
||||
// Get key from yubikey
|
||||
password = YubiUnlock();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} else password = textBoxaccountpassword.Text;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
password = textBoxaccountpassword.Text;
|
||||
bool loggedin = await Login();
|
||||
if (loggedin)
|
||||
{
|
||||
@@ -1437,27 +1438,6 @@ namespace FireWallet
|
||||
return hex.ToString();
|
||||
}
|
||||
|
||||
private static string GetHash(HashAlgorithm hashAlgorithm, byte[] input)
|
||||
{
|
||||
|
||||
// Convert the input string to a byte array and compute the hash.
|
||||
byte[] data = hashAlgorithm.ComputeHash(input);
|
||||
|
||||
// Create a new Stringbuilder to collect the bytes
|
||||
// and create a string.
|
||||
var sBuilder = new StringBuilder();
|
||||
|
||||
// Loop through each byte of the hashed data
|
||||
// and format each one as a hexadecimal string.
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
sBuilder.Append(data[i].ToString("x2"));
|
||||
}
|
||||
|
||||
// Return the hexadecimal string.
|
||||
return sBuilder.ToString();
|
||||
}
|
||||
|
||||
private void textBoxSendingAmount_Leave(object sender, EventArgs e)
|
||||
{
|
||||
decimal amount = 0;
|
||||
@@ -2052,6 +2032,43 @@ namespace FireWallet
|
||||
domainSearch = Regex.Replace(textBoxDomainSearch.Text, "[^a-zA-Z0-9-_]", "");
|
||||
textBoxDomainSearch.Text = domainSearch;
|
||||
}
|
||||
private void export_Click(object sender, EventArgs e)
|
||||
{
|
||||
SaveFileDialog saveFileDialog = new SaveFileDialog();
|
||||
saveFileDialog.Filter = "CSV file (*.csv)|*.csv";
|
||||
saveFileDialog.Title = "Export";
|
||||
if (saveFileDialog.ShowDialog() == DialogResult.OK)
|
||||
{
|
||||
StreamWriter sw = new StreamWriter(saveFileDialog.FileName);
|
||||
foreach (string domain in DomainsRenewable)
|
||||
{
|
||||
if (domain == null) break;
|
||||
sw.WriteLine(domain);
|
||||
}
|
||||
sw.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void buttonRenewAll_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (DomainsRenewable == null)
|
||||
{
|
||||
NotifyForm notifyForm = new NotifyForm("No renewable domains found");
|
||||
notifyForm.ShowDialog();
|
||||
notifyForm.Dispose();
|
||||
return;
|
||||
}
|
||||
foreach (string domain in DomainsRenewable)
|
||||
{
|
||||
if (domain == null) break;
|
||||
AddBatch(domain, "RENEW");
|
||||
}
|
||||
}
|
||||
private void comboBoxDomainSort_DropDownClosed(object sender, EventArgs e)
|
||||
{
|
||||
UpdateDomains();
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
#region Batching
|
||||
@@ -2199,39 +2216,7 @@ namespace FireWallet
|
||||
}
|
||||
#endregion
|
||||
|
||||
private void export_Click(object sender, EventArgs e)
|
||||
{
|
||||
SaveFileDialog saveFileDialog = new SaveFileDialog();
|
||||
saveFileDialog.Filter = "CSV file (*.csv)|*.csv";
|
||||
saveFileDialog.Title = "Export";
|
||||
if (saveFileDialog.ShowDialog() == DialogResult.OK)
|
||||
{
|
||||
StreamWriter sw = new StreamWriter(saveFileDialog.FileName);
|
||||
foreach (string domain in DomainsRenewable)
|
||||
{
|
||||
if (domain == null) break;
|
||||
sw.WriteLine(domain);
|
||||
}
|
||||
sw.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void buttonRenewAll_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (DomainsRenewable == null)
|
||||
{
|
||||
NotifyForm notifyForm = new NotifyForm("No renewable domains found");
|
||||
notifyForm.ShowDialog();
|
||||
notifyForm.Dispose();
|
||||
return;
|
||||
}
|
||||
foreach (string domain in DomainsRenewable)
|
||||
{
|
||||
if (domain == null) break;
|
||||
AddBatch(domain, "RENEW");
|
||||
}
|
||||
}
|
||||
|
||||
#region Help Menu
|
||||
private void githubToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
// Open the GitHub page
|
||||
@@ -2262,166 +2247,6 @@ namespace FireWallet
|
||||
};
|
||||
Process.Start(psi);
|
||||
}
|
||||
|
||||
private void comboBoxDomainSort_DropDownClosed(object sender, EventArgs e)
|
||||
{
|
||||
UpdateDomains();
|
||||
}
|
||||
#region yubikey
|
||||
static bool PinSubmitter(KeyEntryData pin)
|
||||
{
|
||||
string s = "123456";
|
||||
var s_b = Encoding.UTF8.GetBytes(s);
|
||||
pin.SubmitValue(s_b);
|
||||
return true;
|
||||
}
|
||||
private void buttonSettingsYubikey_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (password.Length < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
NotifyForm notifyForm = new NotifyForm("Insert Yubikey\nThis will use your yubikey to encrypt your account password.");
|
||||
notifyForm.ShowDialog();
|
||||
notifyForm.Dispose();
|
||||
var devices = YubiKeyDevice.FindAll();
|
||||
if (devices.Count() != 1)
|
||||
{
|
||||
NotifyForm notifyForm2 = new NotifyForm("Please insert your yubikey and try again.");
|
||||
notifyForm2.ShowDialog();
|
||||
notifyForm2.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
NotifyForm yubiLoadingForm = new NotifyForm("Encrypting. . .", false);
|
||||
yubiLoadingForm.Show();
|
||||
// Wait for the form to load
|
||||
Application.DoEvents();
|
||||
|
||||
try
|
||||
{
|
||||
//Assumes there is exactly one yubikey connected and it has a RSA2048 certificate in slot 9d
|
||||
//PIV PIN is assumed to be 123456
|
||||
|
||||
var ykDevice = devices.First();
|
||||
PivSession piv = new(ykDevice);
|
||||
|
||||
piv.KeyCollector += PinSubmitter;
|
||||
piv.VerifyPin();
|
||||
|
||||
var slot = PivSlot.KeyManagement;
|
||||
|
||||
X509Certificate2 cert = piv.GetCertificate(slot);
|
||||
if (cert.SignatureAlgorithm.FriendlyName != "sha256RSA")
|
||||
throw new CryptographicException("Certificate must be RSA with SHA256");
|
||||
|
||||
var publicKey = cert.GetRSAPublicKey() ?? throw new CryptographicException("Couldn't get public key from certificate.");
|
||||
|
||||
Aes aesFirst = Aes.Create();
|
||||
|
||||
var encryptedKey = publicKey.Encrypt(aesFirst.Key, RSAEncryptionPadding.Pkcs1);
|
||||
var decryptedKey = piv.Decrypt(slot, encryptedKey);
|
||||
|
||||
//MessageBox.Show($"aesFirst.Key.Length: {aesFirst.Key.Length}");
|
||||
//MessageBox.Show($"encryptedKey.Length: {encryptedKey.Length}");
|
||||
//MessageBox.Show($"decryptedKey.Length: {decryptedKey.Length}");
|
||||
|
||||
// split the message into blocks of 128 bytes
|
||||
|
||||
string message = password;
|
||||
int blockSize = 128;
|
||||
int blockCount = (int)Math.Ceiling((double)message.Length / blockSize);
|
||||
|
||||
string[] strings = new string[blockCount];
|
||||
FileStream sw = new FileStream(dir + account + ".yubikey", FileMode.Create, FileAccess.Write);
|
||||
|
||||
for (int i = 0; i < blockCount; i++)
|
||||
{
|
||||
int size = Math.Min(blockSize, message.Length - i * blockSize);
|
||||
strings[i] = message.Substring(i * blockSize, size);
|
||||
|
||||
byte[] bytes = Encoding.ASCII.GetBytes(strings[i]);
|
||||
var encryptedBytes = publicKey.Encrypt(bytes, RSAEncryptionPadding.Pkcs1);
|
||||
sw.Write(encryptedBytes, 0, encryptedBytes.Length);
|
||||
}
|
||||
sw.Close();
|
||||
sw.Dispose();
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AddLog(ex.Message);
|
||||
}
|
||||
|
||||
yubiLoadingForm.CloseNotification();
|
||||
}
|
||||
private string YubiUnlock()
|
||||
{
|
||||
NotifyForm yubiLoadingForm = new NotifyForm("Decrypting. . .", false);
|
||||
yubiLoadingForm.Show();
|
||||
// Wait for the form to load
|
||||
Application.DoEvents();
|
||||
try
|
||||
{
|
||||
|
||||
//Assumes there is exactly one yubikey connected and it has a RSA2048 certificate in slot 9d
|
||||
//PIV PIN is assumed to be 123456
|
||||
var devices = YubiKeyDevice.FindAll();
|
||||
var ykDevice = devices.First();
|
||||
PivSession piv = new(ykDevice);
|
||||
|
||||
piv.KeyCollector += PinSubmitter;
|
||||
piv.VerifyPin();
|
||||
|
||||
var slot = PivSlot.KeyManagement;
|
||||
|
||||
X509Certificate2 cert = piv.GetCertificate(slot);
|
||||
if (cert.SignatureAlgorithm.FriendlyName != "sha256RSA")
|
||||
throw new CryptographicException("Certificate must be RSA with SHA256");
|
||||
|
||||
var publicKey = cert.GetRSAPublicKey() ?? throw new CryptographicException("Couldn't get public key from certificate.");
|
||||
|
||||
Aes aesFirst = Aes.Create();
|
||||
|
||||
var encryptedKey = publicKey.Encrypt(aesFirst.Key, RSAEncryptionPadding.Pkcs1);
|
||||
var decryptedKey = piv.Decrypt(slot, encryptedKey);
|
||||
|
||||
byte[] input = File.ReadAllBytes(dir + account + ".yubikey");
|
||||
|
||||
// decrypt the input
|
||||
|
||||
int blockSize = 256;
|
||||
int blockCount = (int)Math.Ceiling((double)input.Length / blockSize);
|
||||
|
||||
byte[][] blocks = new byte[blockCount][];
|
||||
byte[] decripted = new byte[blockCount * blockSize];
|
||||
string output = "";
|
||||
for (int i = 0; i < blockCount; i++)
|
||||
{
|
||||
int size = Math.Min(blockSize, input.Length - i * blockSize);
|
||||
blocks[i] = new byte[size];
|
||||
Array.Copy(input, i * blockSize, blocks[i], 0, size);
|
||||
var paddedDecryptedBytes = piv.Decrypt(slot, blocks[i]);
|
||||
byte[] decryptedBytes;
|
||||
bool couldParse = Yubico.YubiKey.Cryptography.RsaFormat.TryParsePkcs1Decrypt(paddedDecryptedBytes, out decryptedBytes);
|
||||
Array.Copy(decryptedBytes, 0, decripted, i * blockSize, decryptedBytes.Length);
|
||||
|
||||
output += Encoding.ASCII.GetString(decryptedBytes);
|
||||
}
|
||||
yubiLoadingForm.CloseNotification();
|
||||
return output;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AddLog(ex.Message);
|
||||
yubiLoadingForm.CloseNotification();
|
||||
return "";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -224,15 +224,15 @@
|
||||
{
|
||||
"Name" = "8:Microsoft Visual Studio"
|
||||
"ProductName" = "8:FireWallet"
|
||||
"ProductCode" = "8:{007B0A5E-57B9-4DB7-AABE-5A3631A89BEB}"
|
||||
"PackageCode" = "8:{546D4209-3E58-4144-A9DC-E659A2482DD4}"
|
||||
"ProductCode" = "8:{460D8F86-4FE9-4547-9B17-7E01ACBF9194}"
|
||||
"PackageCode" = "8:{A6678F97-9CE8-4005-82AC-AB2D09A9DA27}"
|
||||
"UpgradeCode" = "8:{0C86F725-6B01-4173-AA05-3F0EDF481362}"
|
||||
"AspNetVersion" = "8:"
|
||||
"RestartWWWService" = "11:FALSE"
|
||||
"RemovePreviousVersions" = "11:TRUE"
|
||||
"DetectNewerInstalledVersion" = "11:TRUE"
|
||||
"InstallAllUsers" = "11:FALSE"
|
||||
"ProductVersion" = "8:3.0"
|
||||
"ProductVersion" = "8:3.1"
|
||||
"Manufacturer" = "8:Nathan.Woodburn/"
|
||||
"ARPHELPTELEPHONE" = "8:"
|
||||
"ARPHELPLINK" = "8:https://l.woodburn.au/discord"
|
||||
|
||||
Reference in New Issue
Block a user