test: Update 19 files
All checks were successful
Build-Test-Publish / build (linux/amd64) (push) Successful in 34s
Build-Test-Publish / build (linux/arm64) (push) Successful in 44s
Build-Test-Publish / test-install-from-scratch (linux/amd64) (push) Successful in 8s
Build-Test-Publish / test-install-from-scratch (linux/arm64) (push) Successful in 8s
All checks were successful
Build-Test-Publish / build (linux/amd64) (push) Successful in 34s
Build-Test-Publish / build (linux/arm64) (push) Successful in 44s
Build-Test-Publish / test-install-from-scratch (linux/amd64) (push) Successful in 8s
Build-Test-Publish / test-install-from-scratch (linux/arm64) (push) Successful in 8s
This commit is contained in:
1
dehydrate/src/.gitkeep
Normal file
1
dehydrate/src/.gitkeep
Normal file
@ -0,0 +1 @@
|
||||
# This file ensures the src directory is tracked by git.
|
45
dehydrate/src/argparse.cpp
Normal file
45
dehydrate/src/argparse.cpp
Normal file
@ -0,0 +1,45 @@
|
||||
#include "argparse.hpp"
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
static const std::string HELP_TEXT = R"(
|
||||
Converts existing files to C++ source code which can be used to recreate the original files.
|
||||
|
||||
Usage: dehydrate [OPTIONS] SOURCE DEST
|
||||
|
||||
Options:
|
||||
-s Silent mode (no output)
|
||||
-u Update dehydrate to the latest version
|
||||
|
||||
Examples:
|
||||
dehydrate file.txt output/ Creates _file.txt.cpp and _file.txt.hpp in output/
|
||||
dehydrate src/ output/ Creates _src.cpp and _src.hpp in output/
|
||||
dehydrate -u Updates dehydrate to the latest version
|
||||
)";
|
||||
|
||||
Args parse_args(int argc, char* argv[]) {
|
||||
Args args;
|
||||
int idx = 1;
|
||||
|
||||
// Check for silent flag
|
||||
if (idx < argc && std::string(argv[idx]) == "-s") {
|
||||
args.silent = true;
|
||||
idx++;
|
||||
}
|
||||
|
||||
// Check for update flag
|
||||
if (idx < argc && std::string(argv[idx]) == "-u") {
|
||||
args.update = true;
|
||||
idx++;
|
||||
return args; // No need for source and dest parameters when updating
|
||||
}
|
||||
|
||||
// Require source and dest parameters for normal operation
|
||||
if (argc - idx != 2) {
|
||||
throw std::runtime_error(HELP_TEXT);
|
||||
}
|
||||
|
||||
args.source = argv[idx];
|
||||
args.dest = argv[idx + 1];
|
||||
return args;
|
||||
}
|
372
dehydrate/src/generator.cpp
Normal file
372
dehydrate/src/generator.cpp
Normal file
@ -0,0 +1,372 @@
|
||||
#include "generator.hpp"
|
||||
#include "../include/hash.hpp"
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
#include "xxhash.hpp"
|
||||
#include <sys/stat.h> // For file permissions
|
||||
#include <cstring> // For strlen
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
static std::string sanitize(const std::string& name) {
|
||||
std::string out = name;
|
||||
for (char& c : out) if (!isalnum(c)) c = '_';
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
static uint64_t fnv1a_64(const void* data, size_t len) {
|
||||
const uint8_t* p = static_cast<const uint8_t*>(data);
|
||||
uint64_t h = 0xcbf29ce484222325ULL;
|
||||
for (size_t i = 0; i < len; ++i)
|
||||
h = (h ^ p[i]) * 0x100000001b3ULL;
|
||||
return h;
|
||||
}
|
||||
|
||||
// Base64 encoding function - no dependencies
|
||||
static std::string base64_encode(const unsigned char* data, size_t len) {
|
||||
const char* base64_chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
std::string result;
|
||||
result.reserve((len + 2) / 3 * 4); // Reserve space for the full encoded size
|
||||
|
||||
int val = 0, valb = -6;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
val = (val << 8) + data[i];
|
||||
valb += 8;
|
||||
while (valb >= 0) {
|
||||
result.push_back(base64_chars[(val >> valb) & 0x3F]);
|
||||
valb -= 6;
|
||||
}
|
||||
}
|
||||
|
||||
if (valb > -6) {
|
||||
result.push_back(base64_chars[((val << 8) >> (valb + 8)) & 0x3F]);
|
||||
}
|
||||
|
||||
// Add padding
|
||||
while (result.size() % 4) {
|
||||
result.push_back('=');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Helper function to output the _recreate_file_ utility function and Base64 decoder
|
||||
static void output_recreate_file_utility(std::ofstream& cpp) {
|
||||
cpp << R"cpp(
|
||||
// Base64 decoding function - no dependencies
|
||||
static void base64_decode(const char* encoded_data, size_t encoded_len, unsigned char* output, size_t* output_len) {
|
||||
const char* base64_chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
size_t out_pos = 0;
|
||||
int val = 0, valb = -8;
|
||||
|
||||
for (size_t i = 0; i < encoded_len; i++) {
|
||||
char c = encoded_data[i];
|
||||
if (c == '=') break;
|
||||
|
||||
// Find position in base64_chars
|
||||
const char* pos = strchr(base64_chars, c);
|
||||
if (pos == nullptr) continue; // Skip invalid characters
|
||||
|
||||
val = (val << 6) + static_cast<int>(pos - base64_chars);
|
||||
valb += 6;
|
||||
if (valb >= 0) {
|
||||
output[out_pos++] = static_cast<unsigned char>((val >> valb) & 0xFF);
|
||||
valb -= 8;
|
||||
}
|
||||
}
|
||||
|
||||
*output_len = out_pos;
|
||||
}
|
||||
|
||||
// Utility function to recreate a file with proper permissions
|
||||
static bool _recreate_file_(const std::filesystem::path& outpath, uint64_t file_hash, std::filesystem::perms file_perms, const unsigned char* filedata, size_t filedata_len) {
|
||||
namespace fs = std::filesystem;
|
||||
bool needs_write = false;
|
||||
|
||||
// Check if file exists and has correct hash
|
||||
if (fs::exists(outpath)) {
|
||||
// Check content hash
|
||||
std::ifstream in(outpath, std::ios::binary);
|
||||
std::ostringstream oss;
|
||||
oss << in.rdbuf();
|
||||
std::string data = oss.str();
|
||||
uint64_t existing_hash = fnv1a_64(data.data(), data.size());
|
||||
needs_write = existing_hash != file_hash;
|
||||
} else {
|
||||
needs_write = true; // File doesn't exist, need to create it
|
||||
}
|
||||
|
||||
bool needs_permission_update = true;
|
||||
if (!needs_write) { // we always update permissions if the file is written or changed. Othewise we check.
|
||||
fs::perms current_perms = fs::status(outpath).permissions();
|
||||
needs_permission_update = current_perms != file_perms;
|
||||
}
|
||||
|
||||
if (needs_write) {
|
||||
bool existed = fs::exists(outpath);
|
||||
|
||||
fs::create_directories(outpath.parent_path());
|
||||
std::ofstream out(outpath, std::ios::binary);
|
||||
out.write(reinterpret_cast<const char*>(filedata), filedata_len);
|
||||
out.close();
|
||||
// Set the file permissions
|
||||
fs::permissions(outpath, file_perms);
|
||||
|
||||
if (!existed) {
|
||||
std::cout << "[dehydrate] " << outpath.filename() << ": created\n";
|
||||
} else {
|
||||
std::cout << "[dehydrate] " << outpath.filename() << ": updated (hash changed)\n";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (needs_permission_update) {
|
||||
// Update only permissions
|
||||
fs::permissions(outpath, file_perms);
|
||||
std::cout << "[dehydrate] " << outpath.filename() << ": updated (permissions changed)\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
)cpp";
|
||||
}
|
||||
|
||||
|
||||
void generate_file_code(const std::string& source, const std::string& destfolder, bool silent) {
|
||||
fs::path src(source);
|
||||
fs::path dest(destfolder);
|
||||
std::string ns = "recreate_" + sanitize(src.stem().string());
|
||||
std::string cppname = "_" + src.stem().string() + ".cpp";
|
||||
std::string hppname = "_" + src.stem().string() + ".hpp";
|
||||
std::string bothname = "_" + src.stem().string() + ".{cpp,hpp}";
|
||||
fs::create_directories(dest);
|
||||
std::ifstream in(source, std::ios::binary);
|
||||
std::ostringstream oss;
|
||||
oss << in.rdbuf();
|
||||
std::string filedata = oss.str();
|
||||
uint64_t hash = fnv1a_64(filedata.data(), filedata.size());
|
||||
|
||||
// Get source file permissions
|
||||
fs::perms src_perms = fs::status(src).permissions();
|
||||
|
||||
// Write HPP
|
||||
std::ofstream hpp(dest / hppname);
|
||||
hpp << "#pragma once\n#include <string>\nnamespace " << ns << " {\nbool recreate_file(std::string destination_folder);\n}\n";
|
||||
// Write CPP
|
||||
std::ofstream cpp(dest / cppname);
|
||||
cpp << R"cpp(#include <fstream>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
#include <cstring>
|
||||
|
||||
// Tiny dependency-free FNV-1a 64-bit hash
|
||||
static uint64_t fnv1a_64(const void* data, size_t len) {
|
||||
const uint8_t* p = static_cast<const uint8_t*>(data);
|
||||
uint64_t h = 0xcbf29ce484222325ULL;
|
||||
for (size_t i = 0; i < len; ++i)
|
||||
h = (h ^ p[i]) * 0x100000001b3ULL;
|
||||
return h;
|
||||
}
|
||||
)cpp";
|
||||
cpp << "#include \"" << hppname << "\"\n";
|
||||
cpp << "namespace " << ns << " {\n";
|
||||
|
||||
// Output the recreate_file utility function
|
||||
output_recreate_file_utility(cpp);
|
||||
|
||||
// Write recreate_file function with embedded file data
|
||||
cpp << R"cpp(
|
||||
bool recreate_file(std::string destination_folder) {
|
||||
namespace fs = std::filesystem;
|
||||
fs::path outpath = fs::path(destination_folder) / ")cpp" << src.filename().string() << R"cpp(";
|
||||
|
||||
// File data embedded as Base64
|
||||
static const char filedata_base64[] = )cpp";
|
||||
|
||||
// Encode the file data to Base64
|
||||
std::string base64 = base64_encode(reinterpret_cast<const unsigned char*>(filedata.data()), filedata.size());
|
||||
|
||||
// Split into 76-character chunks for readability
|
||||
const size_t line_length = 76;
|
||||
for (size_t i = 0; i < base64.length(); i += line_length) {
|
||||
if (i > 0) cpp << "\n ";
|
||||
cpp << "\"" << base64.substr(i, std::min(line_length, base64.length() - i)) << "\"";
|
||||
if (i + line_length < base64.length()) cpp << "\\";
|
||||
}
|
||||
cpp << ";\n\n";
|
||||
|
||||
// Decode Base64 at runtime
|
||||
cpp << " // Decode Base64 data\n";
|
||||
cpp << " size_t decoded_size = (strlen(filedata_base64) * 3) / 4;\n";
|
||||
cpp << " unsigned char* decoded_data = new unsigned char[decoded_size];\n";
|
||||
cpp << " size_t actual_size;\n";
|
||||
cpp << " base64_decode(filedata_base64, strlen(filedata_base64), decoded_data, &actual_size);\n\n";
|
||||
|
||||
// Call _recreate_file_ with the decoded data
|
||||
cpp << " bool result = _recreate_file_(outpath, " << hash << "ULL, "
|
||||
<< "std::filesystem::perms(" << static_cast<unsigned>(src_perms) << "), "
|
||||
<< "decoded_data, actual_size);\n";
|
||||
|
||||
// Clean up
|
||||
cpp << " delete[] decoded_data;\n";
|
||||
cpp << " return result;\n";
|
||||
cpp << "}\n";
|
||||
cpp << "}\n";
|
||||
|
||||
if (!silent) {
|
||||
std::cout << "[dehydrate] Generated: " << (dest / bothname) << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to recursively collect all files in a directory
|
||||
template<typename F>
|
||||
void walk_dir(const fs::path& dir, F&& f) {
|
||||
for (auto& p : fs::recursive_directory_iterator(dir)) {
|
||||
if (fs::is_regular_file(p)) f(p.path());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void generate_folder_code(const std::string& source, const std::string& destfolder, bool silent) {
|
||||
fs::path src(source);
|
||||
fs::path dest(destfolder);
|
||||
std::string ns = "recreate_" + sanitize(src.stem().string());
|
||||
std::string cppname = "_" + src.stem().string() + ".cpp";
|
||||
std::string hppname = "_" + src.stem().string() + ".hpp";
|
||||
std::string bothname = "_" + src.stem().string() + ".{cpp,hpp}";
|
||||
fs::create_directories(dest);
|
||||
// Collect all files
|
||||
std::vector<fs::path> files;
|
||||
walk_dir(src, [&](const fs::path& p) { files.push_back(p); });
|
||||
// Write HPP
|
||||
std::ofstream hpp(dest / hppname);
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Generate HPP
|
||||
hpp << R"hpp(
|
||||
#pragma once
|
||||
|
||||
/*
|
||||
|
||||
THIS FILE IS AUTO-GENERATED BY DEHYDRATE.
|
||||
DO NOT EDIT THIS FILE.
|
||||
|
||||
*/
|
||||
|
||||
|
||||
#include <string>
|
||||
namespace )hpp" << ns << R"hpp( {
|
||||
bool recreate_tree(std::string destination_folder);
|
||||
}
|
||||
)hpp";
|
||||
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Write CPP
|
||||
std::ofstream cpp(dest / cppname);
|
||||
cpp << R"cpp(#include <fstream>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
#include <cstring>
|
||||
|
||||
/*
|
||||
|
||||
THIS FILE IS AUTO-GENERATED BY DEHYDRATE.
|
||||
DO NOT EDIT THIS FILE.
|
||||
|
||||
*/
|
||||
|
||||
|
||||
)cpp";
|
||||
cpp << "#include \"" << hppname << "\"\n";
|
||||
cpp << "namespace " << ns << " {\n";
|
||||
|
||||
cpp << R"cpp(
|
||||
|
||||
// Tiny dependency-free FNV-1a 64-bit hash
|
||||
static uint64_t fnv1a_64(const void* data, size_t len) {
|
||||
const uint8_t* p = static_cast<const uint8_t*>(data);
|
||||
uint64_t h = 0xcbf29ce484222325ULL;
|
||||
for (size_t i = 0; i < len; ++i)
|
||||
h = (h ^ p[i]) * 0x100000001b3ULL;
|
||||
return h;
|
||||
}
|
||||
|
||||
)cpp";
|
||||
|
||||
// Output the recreate_file utility function
|
||||
output_recreate_file_utility(cpp);
|
||||
|
||||
// Start writing recreate_tree - we'll embed file data directly in function
|
||||
cpp << R"cpp(
|
||||
bool recreate_tree(std::string destination_folder) {
|
||||
namespace fs = std::filesystem;
|
||||
bool any_written = false;
|
||||
)cpp";
|
||||
|
||||
// Process each file
|
||||
for (const auto& file : files) {
|
||||
std::ifstream in(file, std::ios::binary);
|
||||
std::ostringstream oss;
|
||||
oss << in.rdbuf();
|
||||
std::string filedata = oss.str();
|
||||
uint64_t hash = fnv1a_64(filedata.data(), filedata.size());
|
||||
fs::perms file_perms = fs::status(file).permissions();
|
||||
std::string rel = fs::relative(file, src).string();
|
||||
std::string var = sanitize(rel);
|
||||
|
||||
// Start a scope to limit data's lifetime
|
||||
cpp << " {\n";
|
||||
cpp << " // File: " << rel << "\n";
|
||||
cpp << " fs::path outpath = fs::path(destination_folder) / \"" << rel << "\";\n";
|
||||
|
||||
// Embed file data as Base64
|
||||
cpp << " static const char filedata_base64[] = ";
|
||||
|
||||
// Encode the file data to Base64
|
||||
std::string base64 = base64_encode(reinterpret_cast<const unsigned char*>(filedata.data()), filedata.size());
|
||||
|
||||
// Split into 76-character chunks for readability
|
||||
const size_t line_length = 76;
|
||||
for (size_t i = 0; i < base64.length(); i += line_length) {
|
||||
if (i > 0) cpp << "\n ";
|
||||
cpp << "\"" << base64.substr(i, std::min(line_length, base64.length() - i)) << "\"";
|
||||
if (i + line_length < base64.length()) cpp << "\\";
|
||||
}
|
||||
cpp << ";\n\n";
|
||||
|
||||
// Decode Base64 at runtime
|
||||
cpp << " // Decode Base64 data\n";
|
||||
cpp << " size_t decoded_size = (strlen(filedata_base64) * 3) / 4;\n";
|
||||
cpp << " unsigned char* decoded_data = new unsigned char[decoded_size];\n";
|
||||
cpp << " size_t actual_size;\n";
|
||||
cpp << " base64_decode(filedata_base64, strlen(filedata_base64), decoded_data, &actual_size);\n\n";
|
||||
|
||||
// Call _recreate_file_ with the decoded data
|
||||
cpp << " bool file_written = _recreate_file_(outpath, "
|
||||
<< hash << "ULL, std::filesystem::perms(" << static_cast<unsigned>(file_perms) << "), "
|
||||
<< "decoded_data, actual_size);\n";
|
||||
|
||||
// Clean up and update flag
|
||||
cpp << " delete[] decoded_data;\n";
|
||||
cpp << " any_written = any_written || file_written;\n";
|
||||
cpp << " }\n"; // Close scope to free memory
|
||||
}
|
||||
|
||||
cpp << " return any_written;\n";
|
||||
cpp << "}\n";
|
||||
cpp << "}\n";
|
||||
|
||||
if (!silent) {
|
||||
std::cout << "[dehydrate] Generated: " << (dest / bothname) << std::endl;
|
||||
}
|
||||
}
|
33
dehydrate/src/hash.cpp
Normal file
33
dehydrate/src/hash.cpp
Normal file
@ -0,0 +1,33 @@
|
||||
#include "hash.hpp"
|
||||
#define XXH_INLINE_ALL
|
||||
#include "xxhash.hpp"
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
|
||||
static std::string to_hex64(uint64_t value) {
|
||||
std::ostringstream oss;
|
||||
oss << std::hex << std::setw(16) << std::setfill('0') << value;
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
std::string hash_data(const std::string& data) {
|
||||
uint64_t h = XXH3_64bits(data.data(), data.size());
|
||||
return to_hex64(h);
|
||||
}
|
||||
|
||||
std::string hash_file(const std::string& path) {
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file) return "";
|
||||
XXH64_state_t* state = XXH64_createState();
|
||||
XXH64_reset(state, 0);
|
||||
char buf[4096];
|
||||
while (file) {
|
||||
file.read(buf, sizeof(buf));
|
||||
std::streamsize n = file.gcount();
|
||||
if (n > 0) XXH64_update(state, buf, static_cast<size_t>(n));
|
||||
}
|
||||
uint64_t h = XXH64_digest(state);
|
||||
XXH64_freeState(state);
|
||||
return to_hex64(h);
|
||||
}
|
86
dehydrate/src/main.cpp
Normal file
86
dehydrate/src/main.cpp
Normal file
@ -0,0 +1,86 @@
|
||||
#include <iostream>
|
||||
#include <filesystem>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "argparse.hpp"
|
||||
#include "generator.hpp"
|
||||
#include "version.hpp"
|
||||
|
||||
|
||||
std::string get_arch()
|
||||
{
|
||||
// determine the architecture of the system
|
||||
std::string arch;
|
||||
#ifdef __aarch64__
|
||||
arch = "arm64";
|
||||
#elif __x86_64__
|
||||
arch = "amd64";
|
||||
#endif
|
||||
return arch;
|
||||
}
|
||||
|
||||
int update()
|
||||
{
|
||||
// determine path to this executable
|
||||
std::filesystem::path exepath = std::filesystem::canonical("/proc/self/exe");
|
||||
std::filesystem::path parent_path = exepath.parent_path();
|
||||
std::string project_name = exepath.filename().string();
|
||||
|
||||
// determine the architecture of the system
|
||||
std::string arch = get_arch();
|
||||
|
||||
std::string url = "https://gitea.jde.nz/public/"+project_name+"/releases/download/latest/"+project_name+"." + arch;
|
||||
|
||||
// download new version, preserve permissions and ownership
|
||||
std::string bash_script;
|
||||
bash_script += "docker run --rm -v "+parent_path.string()+":/target";
|
||||
bash_script += " gitea.jde.nz/public/debian-curl:latest";
|
||||
bash_script += " sh -c \"";
|
||||
bash_script += " curl -fsSL " + url + " -o /target/"+project_name+"_temp &&";
|
||||
bash_script += " chmod --reference=/target/"+project_name+" /target/"+project_name+"_temp &&";
|
||||
bash_script += " chown --reference=/target/"+project_name+" /target/"+project_name+"_temp &&";
|
||||
bash_script += " mv /target/"+project_name+"_temp /target/"+project_name;
|
||||
bash_script += "\"";
|
||||
|
||||
std::cout << "Updating " << exepath << " to the latest " << arch << " version." << std::endl;
|
||||
|
||||
// std::cout << "bash_script: " << std::endl
|
||||
// << bash_script << std::endl;
|
||||
|
||||
// run the bash script
|
||||
execlp("bash", "bash", "-c", bash_script.c_str(), (char *)nullptr);
|
||||
std::cerr << "Failed to execute command." << std::endl;
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
try {
|
||||
std::cout << "Dehydrate version " << VERSION << std::endl;
|
||||
Args args = parse_args(argc, argv);
|
||||
|
||||
// Handle update request
|
||||
if (args.update) {
|
||||
return update();
|
||||
}
|
||||
|
||||
std::filesystem::path src(args.source);
|
||||
if (!std::filesystem::exists(src)) {
|
||||
std::cerr << "Source does not exist: " << args.source << std::endl;
|
||||
return 1;
|
||||
}
|
||||
if (std::filesystem::is_regular_file(src)) {
|
||||
generate_file_code(args.source, args.dest, args.silent);
|
||||
} else if (std::filesystem::is_directory(src)) {
|
||||
generate_folder_code(args.source, args.dest, args.silent);
|
||||
} else {
|
||||
std::cerr << "Source is neither a file nor a directory: " << args.source << std::endl;
|
||||
return 1;
|
||||
}
|
||||
} catch (const std::exception& ex) {
|
||||
std::cerr << ex.what() << std::endl;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
1
dehydrate/src/version.hpp.in
Normal file
1
dehydrate/src/version.hpp.in
Normal file
@ -0,0 +1 @@
|
||||
static const char *VERSION = "@PROJECT_VERSION@";
|
Reference in New Issue
Block a user