recal
This commit is contained in:
87
src/brsoa_build.py
Normal file
87
src/brsoa_build.py
Normal file
@@ -0,0 +1,87 @@
|
||||
# Copyright (C) 2025 Bellande Robotics Sensors Research Innovation Center, Ronaldson Bellande
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
from bellande_parser.bellande_parser import Bellande_Format
|
||||
|
||||
def load_config():
|
||||
bellande_parser = Bellande_Format()
|
||||
raw_content = bellande_parser.parse_bellande("project_config.bellande")
|
||||
|
||||
def build_cpp(package_dir, output_dir, config):
|
||||
src_file = os.path.join(package_dir, f"{os.path.basename(package_dir)}.cpp")
|
||||
output_file = os.path.join(output_dir, os.path.basename(package_dir))
|
||||
compiler = config['build_settings']['cpp']['compiler']
|
||||
flags = ' '.join(config['build_settings']['cpp']['flags'])
|
||||
|
||||
cmd = f"{compiler} {flags} -I build/common_msgs {src_file} -o {output_file}"
|
||||
subprocess.run(cmd, shell=True, check=True)
|
||||
|
||||
def build_python(package_dir, output_dir, config):
|
||||
src_file = os.path.join(package_dir, f"{os.path.basename(package_dir)}.py")
|
||||
output_file = os.path.join(output_dir, os.path.basename(src_file))
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
os.symlink(src_file, output_file)
|
||||
|
||||
def build_java(package_dir, output_dir, config):
|
||||
src_file = os.path.join(package_dir, f"{os.path.basename(package_dir)}.java")
|
||||
output_dir = os.path.join(output_dir, "classes")
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
cmd = f"javac -d {output_dir} -cp build/common_msgs {src_file}"
|
||||
subprocess.run(cmd, shell=True, check=True)
|
||||
|
||||
def build_rust(package_dir, output_dir, config):
|
||||
cmd = f"cargo build --release --manifest-path {os.path.join(package_dir, 'Cargo.toml')}"
|
||||
subprocess.run(cmd, shell=True, check=True)
|
||||
|
||||
binary_name = os.path.basename(package_dir)
|
||||
src = os.path.join(package_dir, "target", "release", binary_name)
|
||||
dst = os.path.join(output_dir, binary_name)
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
os.rename(src, dst)
|
||||
|
||||
def build_go(package_dir, output_dir, config):
|
||||
cmd = f"go build -o {output_dir} {os.path.join(package_dir, '*.go')}"
|
||||
subprocess.run(cmd, shell=True, check=True)
|
||||
|
||||
def main():
|
||||
config = load_config()
|
||||
|
||||
# Generate common messages
|
||||
subprocess.run(["python", "generate_msgs.py"], check=True)
|
||||
|
||||
build_functions = {
|
||||
'cpp': build_cpp,
|
||||
'python': build_python,
|
||||
'java': build_java,
|
||||
'rust': build_rust,
|
||||
'go': build_go,
|
||||
}
|
||||
|
||||
for package in config['packages']:
|
||||
package_dir = os.path.join('src', package['name'])
|
||||
output_dir = os.path.join('build', package['name'])
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
lang = package['language']
|
||||
if lang in build_functions:
|
||||
build_functions[lang](package_dir, output_dir, config)
|
||||
else:
|
||||
print(f"Warning: No build function for language '{lang}'")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
187
src/brsoa_create_package.py
Normal file
187
src/brsoa_create_package.py
Normal file
@@ -0,0 +1,187 @@
|
||||
# Copyright (C) 2025 Bellande Robotics Sensors Research Innovation Center, Ronaldson Bellande
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import argparse
|
||||
from bellande_parser.bellande_parser import Bellande_Format
|
||||
|
||||
TEMPLATE_DIR = "templates"
|
||||
|
||||
def create_package_bellande(package_name, language, dependencies):
|
||||
content = {
|
||||
"name": package_name,
|
||||
"language": language,
|
||||
"dependencies": dependencies
|
||||
}
|
||||
return yaml.dump(content, default_flow_style=False)
|
||||
|
||||
def create_cpp_source(package_name):
|
||||
return f"""#include "common_msgs.hpp"
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
class {package_name.capitalize()} {{
|
||||
public:
|
||||
void run() {{
|
||||
while (true) {{
|
||||
// TODO: Implement node logic
|
||||
std::cout << "Running {package_name}" << std::endl;
|
||||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||
}}
|
||||
}}
|
||||
}};
|
||||
|
||||
int main(int argc, char** argv) {{
|
||||
{package_name.capitalize()} node;
|
||||
node.run();
|
||||
return 0;
|
||||
}}
|
||||
"""
|
||||
|
||||
def create_python_source(package_name):
|
||||
return f"""from common_msgs import *
|
||||
import time
|
||||
|
||||
class {package_name.capitalize()}:
|
||||
def run(self):
|
||||
while True:
|
||||
# TODO: Implement node logic
|
||||
print(f"Running {package_name}")
|
||||
time.sleep(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
node = {package_name.capitalize()}()
|
||||
node.run()
|
||||
"""
|
||||
|
||||
def create_java_source(package_name):
|
||||
return f"""import common_msgs.Messages.*;
|
||||
|
||||
public class {package_name.capitalize()} {{
|
||||
public void run() {{
|
||||
while (true) {{
|
||||
// TODO: Implement node logic
|
||||
System.out.println("Running {package_name}");
|
||||
try {{
|
||||
Thread.sleep(1000);
|
||||
}} catch (InterruptedException e) {{
|
||||
e.printStackTrace();
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
|
||||
public static void main(String[] args) {{
|
||||
{package_name.capitalize()} node = new {package_name.capitalize()}();
|
||||
node.run();
|
||||
}}
|
||||
}}
|
||||
"""
|
||||
|
||||
def create_rust_source(package_name):
|
||||
return f"""use common_msgs::*;
|
||||
use std::{{thread, time}};
|
||||
|
||||
struct {package_name.capitalize()};
|
||||
|
||||
impl {package_name.capitalize()} {{
|
||||
fn run(&self) {{
|
||||
loop {{
|
||||
// TODO: Implement node logic
|
||||
println!("Running {package_name}");
|
||||
thread::sleep(time::Duration::from_secs(1));
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
|
||||
fn main() {{
|
||||
let node = {package_name.capitalize()}{{}};
|
||||
node.run();
|
||||
}}
|
||||
"""
|
||||
|
||||
def create_go_source(package_name):
|
||||
return f"""package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"common_msgs"
|
||||
)
|
||||
|
||||
type {package_name.capitalize()} struct{{}}
|
||||
|
||||
func (n *{package_name.capitalize()}) Run() {{
|
||||
for {{
|
||||
// TODO: Implement node logic
|
||||
fmt.Println("Running {package_name}")
|
||||
time.Sleep(1 * time.Second)
|
||||
}}
|
||||
}}
|
||||
|
||||
func main() {{
|
||||
node := &{package_name.capitalize()}{{}}
|
||||
node.Run()
|
||||
}}
|
||||
"""
|
||||
|
||||
def create_package(package_name, language):
|
||||
package_dir = os.path.join("src", package_name)
|
||||
os.makedirs(package_dir, exist_ok=True)
|
||||
|
||||
# Create package.bellande
|
||||
with open(os.path.join(package_dir, "package.bellande"), "w") as f:
|
||||
f.write(create_package_bellande(package_name, language, ["common_msgs"]))
|
||||
|
||||
# Create source file
|
||||
source_creators = {
|
||||
"cpp": create_cpp_source,
|
||||
"python": create_python_source,
|
||||
"java": create_java_source,
|
||||
"rust": create_rust_source,
|
||||
"go": create_go_source
|
||||
}
|
||||
|
||||
if language in source_creators:
|
||||
source_content = source_creators[language](package_name)
|
||||
source_filename = f"{package_name}.{language}"
|
||||
if language == "cpp":
|
||||
source_filename = f"{package_name}.cpp"
|
||||
elif language == "python":
|
||||
source_filename = f"{package_name}.py"
|
||||
elif language == "java":
|
||||
source_filename = f"{package_name.capitalize()}.java"
|
||||
elif language == "rust":
|
||||
source_filename = "main.rs"
|
||||
os.makedirs(os.path.join(package_dir, "src"), exist_ok=True)
|
||||
with open(os.path.join(package_dir, "Cargo.toml"), "w") as f:
|
||||
f.write(f"[package]\nname = \"{package_name}\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[dependencies]\ncommon_msgs = {{ path = \"../../build/common_msgs\" }}\n")
|
||||
elif language == "go":
|
||||
source_filename = f"{package_name}.go"
|
||||
|
||||
with open(os.path.join(package_dir, source_filename), "w") as f:
|
||||
f.write(source_content)
|
||||
else:
|
||||
print(f"Unsupported language: {language}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Create a new package for the robot architecture.")
|
||||
parser.add_argument("package_name", help="Name of the package to create")
|
||||
parser.add_argument("language", choices=["cpp", "python", "java", "rust", "go"], help="Programming language for the package")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
create_package(args.package_name, args.language)
|
||||
print(f"Created package {args.package_name} using {args.language}")
|
131
src/brsoa_create_system_config.py
Normal file
131
src/brsoa_create_system_config.py
Normal file
@@ -0,0 +1,131 @@
|
||||
# Copyright (C) 2025 Bellande Robotics Sensors Research Innovation Center, Ronaldson Bellande
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Will later be programming bellronos
|
||||
def create_example_lua_config_file(filename="brsoa_system_config.lua"):
|
||||
"""
|
||||
Creates an example Lua configuration file for a robot system with generic settings.
|
||||
|
||||
Args:
|
||||
filename (str): The name of the file to create
|
||||
|
||||
Returns:
|
||||
bool: True if file creation was successful
|
||||
"""
|
||||
# Configuration content with generic example values
|
||||
config_content = """-- brsoa_system_config.lua
|
||||
-- Global configuration
|
||||
global_config = {
|
||||
max_nodes = 50,
|
||||
discovery_method = "broadcast",
|
||||
system_name = "example_system",
|
||||
log_level = "debug"
|
||||
}
|
||||
|
||||
-- Define nodes to be launched
|
||||
nodes = {
|
||||
{
|
||||
name = "example_node_cpp",
|
||||
package = "example_package_cpp",
|
||||
executable = "example_driver_cpp",
|
||||
language = "cpp",
|
||||
args = {"--fps=30"},
|
||||
env = {SENSOR_TYPE = "lidar"}
|
||||
},
|
||||
{
|
||||
name = "example_node_py",
|
||||
package = "example_package_py",
|
||||
executable = "example_package_py.py",
|
||||
language = "python",
|
||||
args = {"--algorithm=detection"},
|
||||
env = {PYTHONPATH = "${WORKSPACE}/lib:${WORKSPACE}/include"}
|
||||
},
|
||||
{
|
||||
name = "example_node_java",
|
||||
package = "example_ui_package_java",
|
||||
executable = "ExampleDisplayAppJava",
|
||||
language = "java",
|
||||
args = {"--resolution=720p"},
|
||||
env = {JAVA_OPTS = "-Xmx1g"}
|
||||
},
|
||||
{
|
||||
name = "example_node_rust",
|
||||
package = "example_package_rust",
|
||||
executable = "example_package_rust",
|
||||
language = "rust",
|
||||
args = {"--mode=manual"},
|
||||
env = {RUST_BACKTRACE = "1"}
|
||||
},
|
||||
{
|
||||
name = "example_node_bridge_go",
|
||||
package = "example_package_go",
|
||||
executable = "example_package_go",
|
||||
language = "go",
|
||||
args = {"--port=9090"},
|
||||
env = {GOMAXPROCS = "2"}
|
||||
}
|
||||
}
|
||||
|
||||
-- Define communication setup
|
||||
topics = {
|
||||
{
|
||||
name = "/example/example",
|
||||
type = "Example",
|
||||
queue_size = 5,
|
||||
publishers = {"example_node"},
|
||||
subscribers = {"example1", "example2"}
|
||||
}
|
||||
}
|
||||
|
||||
-- Define services
|
||||
services = {
|
||||
{
|
||||
name = "/example/example",
|
||||
type = "Example",
|
||||
server = "example_server",
|
||||
clients = {"example_client"}
|
||||
}
|
||||
}
|
||||
|
||||
-- Define parameters
|
||||
parameters = {
|
||||
{
|
||||
name = "/example/example",
|
||||
type = "string",
|
||||
value = global_config.system_name
|
||||
}
|
||||
}"""
|
||||
|
||||
try:
|
||||
# Write the configuration to the file
|
||||
with open(filename, 'w') as file:
|
||||
file.write(config_content)
|
||||
print(f"Successfully created example file {filename}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Error creating file: {e}")
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Create the example Lua configuration file
|
||||
success = create_example_lua_config_file()
|
||||
|
||||
if success:
|
||||
print("Example configuration file creation completed.")
|
||||
else:
|
||||
print("Failed to create example configuration file.")
|
192
src/brsoa_generate_msgs.py
Normal file
192
src/brsoa_generate_msgs.py
Normal file
@@ -0,0 +1,192 @@
|
||||
# Copyright (C) 2025 Bellande Robotics Sensors Research Innovation Center, Ronaldson Bellande
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
from bellande_parser.bellande_parser import Bellande_Format
|
||||
|
||||
def parse_def_file(filename):
|
||||
bellande_parser = Bellande_Format()
|
||||
content = bellande_parser.parse_bellande(filename)
|
||||
|
||||
structures = {}
|
||||
enums = {}
|
||||
current_struct = None
|
||||
current_enum = None
|
||||
|
||||
for line in content.split('\n'):
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
|
||||
if line.startswith('struct'):
|
||||
current_struct = line.split()[1]
|
||||
structures[current_struct] = []
|
||||
current_enum = None
|
||||
elif line.startswith('enum'):
|
||||
current_enum = line.split()[1]
|
||||
enums[current_enum] = []
|
||||
current_struct = None
|
||||
elif current_struct:
|
||||
field_type, field_name = line.split()
|
||||
structures[current_struct].append((field_type, field_name))
|
||||
elif current_enum:
|
||||
enums[current_enum].append(line)
|
||||
|
||||
return structures, enums
|
||||
|
||||
def generate_cpp(structures, enums):
|
||||
code = "#pragma once\n\n#include <string>\n#include <vector>\n#include <chrono>\n\nnamespace common_msgs {\n\n"
|
||||
|
||||
for enum_name, enum_values in enums.items():
|
||||
code += f"enum class {enum_name} {{\n"
|
||||
code += ',\n'.join(f" {value}" for value in enum_values)
|
||||
code += "\n};\n\n"
|
||||
|
||||
for struct_name, fields in structures.items():
|
||||
code += f"struct {struct_name} {{\n"
|
||||
for field_type, field_name in fields:
|
||||
if field_type == 'timestamp':
|
||||
code += f" std::chrono::system_clock::time_point {field_name};\n"
|
||||
elif field_type.startswith('list<'):
|
||||
inner_type = field_type[5:-1]
|
||||
code += f" std::vector<{inner_type}> {field_name};\n"
|
||||
else:
|
||||
code += f" {field_type} {field_name};\n"
|
||||
code += "};\n\n"
|
||||
|
||||
code += "} // namespace common_msgs\n"
|
||||
return code
|
||||
|
||||
def generate_python(structures, enums):
|
||||
code = "from dataclasses import dataclass\nfrom typing import List\nimport datetime\n\n"
|
||||
|
||||
for enum_name, enum_values in enums.items():
|
||||
code += f"class {enum_name}:\n"
|
||||
for i, value in enumerate(enum_values):
|
||||
code += f" {value} = {i}\n"
|
||||
code += "\n"
|
||||
|
||||
for struct_name, fields in structures.items():
|
||||
code += f"@dataclass\nclass {struct_name}:\n"
|
||||
for field_type, field_name in fields:
|
||||
if field_type == 'timestamp':
|
||||
code += f" {field_name}: datetime.datetime\n"
|
||||
elif field_type.startswith('list<'):
|
||||
inner_type = field_type[5:-1]
|
||||
code += f" {field_name}: List[{inner_type}]\n"
|
||||
else:
|
||||
code += f" {field_name}: {field_type}\n"
|
||||
code += "\n"
|
||||
|
||||
return code
|
||||
|
||||
def generate_java(structures, enums):
|
||||
code = "package common_msgs;\n\nimport java.time.Instant;\nimport java.util.List;\n\npublic class Messages {\n\n"
|
||||
|
||||
for enum_name, enum_values in enums.items():
|
||||
code += f" public enum {enum_name} {{\n"
|
||||
code += ',\n'.join(f" {value}" for value in enum_values)
|
||||
code += "\n }\n\n"
|
||||
|
||||
for struct_name, fields in structures.items():
|
||||
code += f" public static class {struct_name} {{\n"
|
||||
for field_type, field_name in fields:
|
||||
if field_type == 'timestamp':
|
||||
code += f" public Instant {field_name};\n"
|
||||
elif field_type.startswith('list<'):
|
||||
inner_type = field_type[5:-1]
|
||||
code += f" public List<{inner_type}> {field_name};\n"
|
||||
else:
|
||||
code += f" public {field_type} {field_name};\n"
|
||||
code += " }\n\n"
|
||||
|
||||
code += "}\n"
|
||||
return code
|
||||
|
||||
def generate_rust(structures, enums):
|
||||
code = "use chrono::DateTime;\nuse chrono::Utc;\n\n"
|
||||
|
||||
for enum_name, enum_values in enums.items():
|
||||
code += f"pub enum {enum_name} {{\n"
|
||||
code += ',\n'.join(f" {value}" for value in enum_values)
|
||||
code += "\n}\n\n"
|
||||
|
||||
for struct_name, fields in structures.items():
|
||||
code += f"pub struct {struct_name} {{\n"
|
||||
for field_type, field_name in fields:
|
||||
if field_type == 'timestamp':
|
||||
code += f" pub {field_name}: DateTime<Utc>,\n"
|
||||
elif field_type.startswith('list<'):
|
||||
inner_type = field_type[5:-1]
|
||||
code += f" pub {field_name}: Vec<{inner_type}>,\n"
|
||||
elif field_type == 'string':
|
||||
code += f" pub {field_name}: String,\n"
|
||||
else:
|
||||
code += f" pub {field_name}: {field_type},\n"
|
||||
code += "}\n\n"
|
||||
|
||||
return code
|
||||
|
||||
def generate_go(structures, enums):
|
||||
code = "package common_msgs\n\nimport (\n\t\"time\"\n)\n\n"
|
||||
|
||||
for enum_name, enum_values in enums.items():
|
||||
code += f"type {enum_name} int\n\nconst (\n"
|
||||
for i, value in enumerate(enum_values):
|
||||
code += f" {value} {enum_name} = iota\n"
|
||||
code += ")\n\n"
|
||||
|
||||
for struct_name, fields in structures.items():
|
||||
code += f"type {struct_name} struct {{\n"
|
||||
for field_type, field_name in fields:
|
||||
if field_type == 'timestamp':
|
||||
code += f" {field_name.capitalize()} time.Time\n"
|
||||
elif field_type.startswith('list<'):
|
||||
inner_type = field_type[5:-1]
|
||||
code += f" {field_name.capitalize()} []{inner_type}\n"
|
||||
elif field_type == 'string':
|
||||
code += f" {field_name.capitalize()} string\n"
|
||||
else:
|
||||
code += f" {field_name.capitalize()} {field_type}\n"
|
||||
code += "}\n\n"
|
||||
|
||||
return code
|
||||
|
||||
def main():
|
||||
bellande_parser = Bellande_Format()
|
||||
|
||||
config = bellande_parser.parse_bellande("project_config.bellande")
|
||||
structures, enums = parse_def_file('common_msgs.bellande')
|
||||
|
||||
os.makedirs('build/common_msgs', exist_ok=True)
|
||||
|
||||
generators = {
|
||||
'cpp': ('common_msgs.hpp', generate_cpp),
|
||||
'python': ('common_msgs.py', generate_python),
|
||||
'java': ('common_msgs.java', generate_java),
|
||||
'rust': ('common_msgs.rs', generate_rust),
|
||||
'go': ('common_msgs.go', generate_go),
|
||||
}
|
||||
|
||||
for lang in config['languages']:
|
||||
if lang in generators:
|
||||
filename, generator = generators[lang]
|
||||
with open(f'build/common_msgs/{filename}', 'w') as f:
|
||||
f.write(generator(structures, enums))
|
||||
else:
|
||||
print(f"Warning: No generator for language '{lang}'")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
Reference in New Issue
Block a user