uhhhh. Update sized update
This commit is contained in:
@@ -1,20 +1,20 @@
|
|||||||
### High priority:
|
### High priority:
|
||||||
- [x] ~~Embed creation tool~~
|
<!-- - [x] ~~Embed creation tool~~ -->
|
||||||
- [ ] Reddit bot that scrapes images with tag "Original Art" and posts them in Discord server
|
- [ ] Reddit bot that scrapes images with tag "Original Art" and posts them in Discord server
|
||||||
- [x] ~~Discord bot /bk_help command~~
|
<!-- - [x] ~~Discord bot /bk_help command~~ -->
|
||||||
- [x] ~~Scrape the data~~
|
<!-- - [x] ~~Scrape the data~~ -->
|
||||||
- [x] ~~Put it in a JSON~~
|
<!-- - [x] ~~Put it in a JSON~~ -->
|
||||||
- [x] ~~Multithread so it can run both Discord and Reddit bot!!!~~
|
<!-- - [x] ~~Multithread so it can run both Discord and Reddit bot!!!~~ -->
|
||||||
- [ ] Allow updating the data autonomously and via manual commands.
|
- [ ] Allow updating the data autonomously and via manual commands.
|
||||||
- [ ] Manually add posts
|
- [ ] Manually add posts
|
||||||
- [ ] via `u/[bot] add`
|
- [ ] via `u/[bot] add`
|
||||||
- [x] ~~via `/bk_week_add [url]`~~
|
<!-- - [x] ~~via `/bk_week_add [url]`~~ -->
|
||||||
- [ ] Manually remove posts via `/bk_week_remove [url]`
|
- [ ] Manually remove posts via `/bk_week_remove [url]`
|
||||||
- [x] ~~Manually approve posts via `/bk_week_approve [url]`~~
|
<!-- - [x] ~~Manually approve posts via `/bk_week_approve [url]`~~ -->
|
||||||
- [ ] Manually un-approve posts via `/bk_week_disapprove [url]`
|
- [ ] Manually un-approve posts via `/bk_week_disapprove [url]`
|
||||||
- [x] ~~Automatically add scraped posts to JSON~~
|
<!-- - [x] ~~Automatically add scraped posts to JSON~~ -->
|
||||||
- [ ] Automatically remove posts older than 7 days from JSON
|
- [ ] Automatically remove posts older than 7 days from JSON
|
||||||
- [x] ~~Function~~
|
<!-- - [x] ~~Function~~ -->
|
||||||
- [ ] Automate
|
- [ ] Automate
|
||||||
- [ ] Automatically approve posts that dont get caught by reverse image search (ris)
|
- [ ] Automatically approve posts that dont get caught by reverse image search (ris)
|
||||||
- [ ] Log all posts in a Discord thread
|
- [ ] Log all posts in a Discord thread
|
||||||
@@ -24,10 +24,10 @@
|
|||||||
- [ ] Remove post if its `"removed": true` in data
|
- [ ] Remove post if its `"removed": true` in data
|
||||||
- [ ] Add posts to data from channel
|
- [ ] Add posts to data from channel
|
||||||
- [ ] `/bk_week_update` to forcefully trigger this ^
|
- [ ] `/bk_week_update` to forcefully trigger this ^
|
||||||
- [x] ~~`/bk_week_get [url]` get the data of a single post from the data~~
|
<!-- - [x] ~~`/bk_week_get [url]` get the data of a single post from the data~~ -->
|
||||||
|
|
||||||
### Medium priority:
|
### Medium priority:
|
||||||
- [x] ~~JSON -> Rules list~~
|
<!-- - [x] ~~JSON -> Rules list~~ -->
|
||||||
- [ ] View single rule (/rule {rulename})
|
- [ ] View single rule (/rule {rulename})
|
||||||
- [ ] Postfix calculator
|
- [ ] Postfix calculator
|
||||||
- [ ] Postfic generator
|
- [ ] Postfic generator
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
- [ ] BPS args -> JSON
|
- [ ] BPS args -> JSON
|
||||||
- [ ] Random tip (from ByteDice.net/data/loadingScreenTips.json)
|
- [ ] Random tip (from ByteDice.net/data/loadingScreenTips.json)
|
||||||
- [ ] A command that just sends my socials
|
- [ ] A command that just sends my socials
|
||||||
- [x] ~~Magic 8 ball~~
|
<!-- - [x] ~~Magic 8 ball~~ -->
|
||||||
|
|
||||||
### Low priority:
|
### Low priority:
|
||||||
- [ ] Particle of the week
|
- [ ] Particle of the week
|
||||||
|
|||||||
+29
-7
@@ -46,7 +46,8 @@ pub async fn bk_week_help(
|
|||||||
pub async fn bk_week_get(
|
pub async fn bk_week_get(
|
||||||
ctx: Context<'_>,
|
ctx: Context<'_>,
|
||||||
#[description = "The post URL"] url: String
|
#[description = "The post URL"] url: String
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error>
|
||||||
|
{
|
||||||
data::update_re_data(ctx.data()).await;
|
data::update_re_data(ctx.data()).await;
|
||||||
|
|
||||||
let reddit_data = get_reddit_data(ctx).await?;
|
let reddit_data = get_reddit_data(ctx).await?;
|
||||||
@@ -167,12 +168,17 @@ pub async fn bk_week_add(
|
|||||||
#[description = "Wether to approve it after adding it"] approve: Option<bool>
|
#[description = "Wether to approve it after adding it"] approve: Option<bool>
|
||||||
) -> Result<(), Error>
|
) -> Result<(), Error>
|
||||||
{
|
{
|
||||||
// TODO: auto approve
|
|
||||||
data::update_re_data(ctx.data()).await;
|
data::update_re_data(ctx.data()).await;
|
||||||
let reddit_data = get_reddit_data(ctx).await.unwrap();
|
let reddit_data = get_reddit_data(ctx).await.unwrap();
|
||||||
|
|
||||||
if let Some(bk_week) = reddit_data.get(BK_WEEK) {
|
if let Some(bk_week) = reddit_data.get(BK_WEEK) {
|
||||||
websocket::send_cmd_json("add_post_url", json!([&url])).await;
|
let a = approve.unwrap_or_else(|| false);
|
||||||
|
let r = websocket::send_cmd_json("add_post_url", json!([&url, a])).await.unwrap();
|
||||||
|
|
||||||
|
if !r["value"].as_bool().unwrap() {
|
||||||
|
send_msg(ctx, "Unknown error!\nError trace: `bk_week_cmds.rs -> bk_week_add() -> Unknown error`.".to_string(), true, true).await;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(post) = bk_week.get(&url) {
|
if let Some(post) = bk_week.get(&url) {
|
||||||
if post.get("removed").is_some() {
|
if post.get("removed").is_some() {
|
||||||
@@ -182,6 +188,13 @@ pub async fn bk_week_add(
|
|||||||
send_updated_msg(ctx, &url).await;
|
send_updated_msg(ctx, &url).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
send_msg(ctx, format!("Added post with URL \"<{}>\"!", &url), true, true).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
if a {
|
||||||
|
send_msg(ctx, "Also approved it!".to_string(), true, true).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -221,12 +234,19 @@ pub async fn bk_week_approve(
|
|||||||
data::update_re_data(ctx.data()).await;
|
data::update_re_data(ctx.data()).await;
|
||||||
let reddit_data = get_reddit_data(ctx).await.unwrap();
|
let reddit_data = get_reddit_data(ctx).await.unwrap();
|
||||||
|
|
||||||
|
approve_cmd(ctx, &url, &reddit_data, true).await;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async fn approve_cmd(ctx: Context<'_>, url: &str, reddit_data: &Value, approve: bool) {
|
||||||
if let Some(post) = reddit_data.get(BK_WEEK).unwrap().get(&url) {
|
if let Some(post) = reddit_data.get(BK_WEEK).unwrap().get(&url) {
|
||||||
if post.get("removed").is_some() {
|
if post.get("removed").is_some() {
|
||||||
send_post_removed_message(ctx, &url, post.get("removed_by").unwrap().as_str().unwrap()).await;
|
send_post_removed_message(ctx, &url, post.get("removed_by").unwrap().as_str().unwrap()).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
let r = websocket::send_cmd_json("set_approve_post", json!([true, &url])).await;
|
let r = websocket::send_cmd_json("set_approve_post", json!([approve, &url])).await;
|
||||||
if let Some(v) = r.unwrap().get("value") {
|
if let Some(v) = r.unwrap().get("value") {
|
||||||
send_msg(ctx, format!("Successfully flagged URL \"<{}>\" as `approved:by_human`!", &url), true, true).await;
|
send_msg(ctx, format!("Successfully flagged URL \"<{}>\" as `approved:by_human`!", &url), true, true).await;
|
||||||
}
|
}
|
||||||
@@ -237,18 +257,20 @@ pub async fn bk_week_approve(
|
|||||||
else {
|
else {
|
||||||
send_post_not_found_message(ctx, &url).await;
|
send_post_not_found_message(ctx, &url).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[poise::command(slash_command, prefix_command)]
|
#[poise::command(slash_command, prefix_command)]
|
||||||
pub async fn bk_week_disapprove(
|
pub async fn bk_week_disapprove(
|
||||||
ctx: Context<'_>,
|
ctx: Context<'_>,
|
||||||
#[description = "The post URL"] url: String
|
#[description = "The post URL"] url: String
|
||||||
) -> Result<(), Error>
|
) -> Result<(), Error>
|
||||||
{
|
{
|
||||||
|
data::update_re_data(ctx.data()).await;
|
||||||
|
let reddit_data = get_reddit_data(ctx).await.unwrap();
|
||||||
|
|
||||||
|
approve_cmd(ctx, &url, &reddit_data, false).await;
|
||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
+8
-4
@@ -3,7 +3,7 @@ use std::path::Path;
|
|||||||
|
|
||||||
use serde_json::{self, Value, json};
|
use serde_json::{self, Value, json};
|
||||||
|
|
||||||
use crate::{Data, BK_WEEK};
|
use crate::{Data, BK_WEEK, rs_println};
|
||||||
use crate::websocket::send_cmd_json;
|
use crate::websocket::send_cmd_json;
|
||||||
|
|
||||||
|
|
||||||
@@ -57,8 +57,12 @@ pub fn write_dc_data(data: &Data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn read_re_data(data: &Data) {
|
pub fn read_re_data(data: &Data, wipe: bool) {
|
||||||
if !Path::new(DATA_PATH_RE).exists() {
|
if !Path::new(DATA_PATH_RE).exists() || wipe {
|
||||||
|
rs_println!(
|
||||||
|
"{} creating new from preset...",
|
||||||
|
if !wipe { "reddit_data.json not found," } else { "[WIPE] (reddit_data.json)" }
|
||||||
|
);
|
||||||
generate_re_data();
|
generate_re_data();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +91,7 @@ fn generate_re_data() {
|
|||||||
|
|
||||||
pub async fn update_re_data(data: &Data) {
|
pub async fn update_re_data(data: &Data) {
|
||||||
send_cmd_json("update_data_file", json!([])).await;
|
send_cmd_json("update_data_file", json!([])).await;
|
||||||
read_re_data(data);
|
read_re_data(data, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+5
-3
@@ -32,7 +32,9 @@ struct Args {
|
|||||||
#[arg(long, help = "Runs only the Rust part of the program.")]
|
#[arg(long, help = "Runs only the Rust part of the program.")]
|
||||||
rs: bool,
|
rs: bool,
|
||||||
#[arg(short = 'd', long, help = "Enables dev mode. Dev mode shows more debug info and turns of certain security measures.")]
|
#[arg(short = 'd', long, help = "Enables dev mode. Dev mode shows more debug info and turns of certain security measures.")]
|
||||||
dev: bool
|
dev: bool,
|
||||||
|
#[arg(short = 'w', long, help = "Wipes all data before running the program.")]
|
||||||
|
wipe: bool
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Data {
|
struct Data {
|
||||||
@@ -111,11 +113,11 @@ fn gen_data(args: Args) -> Data {
|
|||||||
byte_dice_id: 697149665166229614,
|
byte_dice_id: 697149665166229614,
|
||||||
reddit_data: None.into(),
|
reddit_data: None.into(),
|
||||||
discord_data: None.into(),
|
discord_data: None.into(),
|
||||||
args
|
args: args.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
data::read_dc_data(&data);
|
data::read_dc_data(&data);
|
||||||
data::read_re_data(&data);
|
data::read_re_data(&data, args.clone().wipe);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-3
@@ -57,14 +57,17 @@ class PostData:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def read_data(bot: botPy.Bot):
|
def read_data(bot: botPy.Bot) -> bool:
|
||||||
# Intentionally unreadable >:]
|
# Intentionally unreadable >:]
|
||||||
data_path = os.path.abspath(os.path.join(os.path.join(os.getcwd(), "data")))
|
data_path = os.path.abspath(os.path.join(os.path.join(os.getcwd(), "data")))
|
||||||
|
|
||||||
try:
|
if os.path.isfile(data_path + "\\reddit_data.json"):
|
||||||
bot.data_f = open(data_path + "\\reddit_data.json", "r+")
|
bot.data_f = open(data_path + "\\reddit_data.json", "r+")
|
||||||
|
|
||||||
except FileNotFoundError:
|
else:
|
||||||
|
if not bot.args["py"]:
|
||||||
|
return False
|
||||||
|
|
||||||
py_print("reddit_data.json not found, creating new from preset...")
|
py_print("reddit_data.json not found, creating new from preset...")
|
||||||
with open(data_path + "\\reddit_data_preset.json", "r") as f:
|
with open(data_path + "\\reddit_data_preset.json", "r") as f:
|
||||||
data_preset_json = json.load(f)
|
data_preset_json = json.load(f)
|
||||||
@@ -84,6 +87,8 @@ def read_data(bot: botPy.Bot):
|
|||||||
if not bot.data["file_created_correctly"]:
|
if not bot.data["file_created_correctly"]:
|
||||||
raise Exception("reddit_data.json file wasn't created properly. Delete the file and retry.")
|
raise Exception("reddit_data.json file wasn't created properly. Delete the file and retry.")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def write_data(bot: botPy.Bot) -> bool:
|
def write_data(bot: botPy.Bot) -> bool:
|
||||||
bot.data_f.seek(0)
|
bot.data_f.seek(0)
|
||||||
|
|||||||
+16
-9
@@ -27,21 +27,28 @@ async def main():
|
|||||||
|
|
||||||
if bot.args["dev"]:
|
if bot.args["dev"]:
|
||||||
py_print("ARGS:", str(bot.args))
|
py_print("ARGS:", str(bot.args))
|
||||||
|
|
||||||
py_print("Reading data...")
|
py_print("Reading data...")
|
||||||
data.read_data(bot)
|
dr = data.read_data(bot)
|
||||||
|
data_retries = 0
|
||||||
|
|
||||||
|
while not dr:
|
||||||
|
data_retries += 1
|
||||||
|
time.sleep(1)
|
||||||
|
py_print(f"Failed to read data: File doesn't exist yet. Retrying (#{data_retries}/5)...")
|
||||||
|
dr = data.read_data(bot)
|
||||||
|
|
||||||
|
if data_retries == 5 and not dr:
|
||||||
|
raise Exception("Couldn't read reddit_data.json: File doesn't exist")
|
||||||
|
|
||||||
|
py_print("Successfully read data!")
|
||||||
|
|
||||||
if not bot.args["py"]:
|
if not bot.args["py"]:
|
||||||
py_print("Connecting to local websocket...")
|
py_print("Connecting to local websocket...")
|
||||||
await py_websocket.websocket_client(bot)
|
await py_websocket.websocket_client(bot)
|
||||||
""" ws_thread = threading.Thread(target=py_websocket.run_thread, args=(bot,))
|
|
||||||
ws_thread.start() """
|
|
||||||
|
|
||||||
while not py_websocket.is_connected:
|
# ws_thread = threading.Thread(target=py_websocket.run_thread, args=(bot,))
|
||||||
py_print("Awaiting connection...")
|
# ws_thread.start()
|
||||||
time.sleep(1)
|
|
||||||
continue
|
|
||||||
|
|
||||||
await py_websocket.send_message("[Connection test] Hello from Python!")
|
|
||||||
|
|
||||||
await bot.stop()
|
await bot.stop()
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -43,7 +43,7 @@ async def add_new_posts(bot: botPy.Bot):
|
|||||||
if post_added: added_posts += 1
|
if post_added: added_posts += 1
|
||||||
else: not_added += 1
|
else: not_added += 1
|
||||||
|
|
||||||
py_print(f"Sucessfully fetched {len(posts)} posts.\n" +
|
py_print(f"Successfully fetched {len(posts)} posts.\n" +
|
||||||
f" Out of which were {added_posts} added.\n" +
|
f" Out of which were {added_posts} added.\n" +
|
||||||
f" {without_media} had no media, " +
|
f" {without_media} had no media, " +
|
||||||
f"and {not_added} weren't added because they are removed or already existed")
|
f"and {not_added} weren't added because they are removed or already existed")
|
||||||
@@ -114,11 +114,12 @@ def get_post_details(post: models.Submission) -> data.PostData:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def add_post_url(bot, url: str) -> bool:
|
async def add_post_url(bot, url: str, approve: bool) -> bool:
|
||||||
result, post = await from_url(bot, url)
|
result, post = await from_url(bot, url)
|
||||||
|
|
||||||
if not result:
|
if not result:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
post_data = get_post_details(post)
|
post_data = get_post_details(post)
|
||||||
|
post_data.approved_by_human = approve
|
||||||
return data.add_post_to_data(bot, post_data, True)
|
return data.add_post_to_data(bot, post_data, True)
|
||||||
@@ -2,7 +2,7 @@ import websockets
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from macros import py_print
|
from macros import *
|
||||||
import bot as botPy
|
import bot as botPy
|
||||||
import data
|
import data
|
||||||
import posts
|
import posts
|
||||||
@@ -18,10 +18,13 @@ async def send_message(message: str):
|
|||||||
|
|
||||||
async def websocket_client(bot: botPy.Bot):
|
async def websocket_client(bot: botPy.Bot):
|
||||||
global ws_global, is_connected
|
global ws_global, is_connected
|
||||||
async with websockets.connect(f"ws://127.0.0.1:{bot.args["port"]}") as ws:
|
port = bot.args["port"]
|
||||||
|
async with websockets.connect(f"ws://127.0.0.1:{port}") as ws:
|
||||||
ws_global = ws
|
ws_global = ws
|
||||||
is_connected = True
|
is_connected = True
|
||||||
py_print(f"Connected webSocket server on ws://127.0.0.1:{bot.args["port"]}")
|
py_print(f"Connected webSocket server on ws://127.0.0.1:{port}")
|
||||||
|
|
||||||
|
await send_message("[Connection test] Hello from Python!")
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
response = await ws.recv()
|
response = await ws.recv()
|
||||||
@@ -52,7 +55,8 @@ async def json_to_func(v: dict, bot: botPy.Bot) -> dict:
|
|||||||
if bot.args["dev"]: py_print("JSON is not a dictionary or does not include \"type\" and \"value\" keys.")
|
if bot.args["dev"]: py_print("JSON is not a dictionary or does not include \"type\" and \"value\" keys.")
|
||||||
return
|
return
|
||||||
if v["type"] != "function":
|
if v["type"] != "function":
|
||||||
if bot.args["dev"]: py_print(f"Type \"{v['type']}\" is not supported.")
|
v_type = v["type"]
|
||||||
|
if bot.args["dev"]: py_print(f"Type \"{v_type}\" is not supported.")
|
||||||
return
|
return
|
||||||
|
|
||||||
value_supported = True
|
value_supported = True
|
||||||
@@ -62,11 +66,15 @@ async def json_to_func(v: dict, bot: botPy.Bot) -> dict:
|
|||||||
case "update_data_file": result = result_json(data.write_data(bot))
|
case "update_data_file": result = result_json(data.write_data(bot))
|
||||||
case "add_post_url": result = result_json(await posts.add_post_url(bot, *v["args"]))
|
case "add_post_url": result = result_json(await posts.add_post_url(bot, *v["args"]))
|
||||||
case "set_approve_post": result = result_json(data.set_approve_post(bot, *v["args"]))
|
case "set_approve_post": result = result_json(data.set_approve_post(bot, *v["args"]))
|
||||||
case "stop_praw": result = result_json(bot.stop())
|
case "stop_praw": result = result_json(await bot.stop())
|
||||||
case _: value_supported = False
|
case _: value_supported = False
|
||||||
|
|
||||||
|
if not isinstance(result.get("value"), bool):
|
||||||
|
py_print("Result JSON is invalid:", str(result))
|
||||||
|
|
||||||
if bot.args["dev"] and not value_supported:
|
if bot.args["dev"] and not value_supported:
|
||||||
py_print(f"Value {v['value']} is not supported")
|
val = v["value"]
|
||||||
|
py_print(f"Value \"{val}\" is not supported")
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ async fn set_receiver(receiver: Receiver) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[allow(static_mut_refs)]
|
||||||
pub async fn send_msg(msg: &str) {
|
pub async fn send_msg(msg: &str) {
|
||||||
unsafe {
|
unsafe {
|
||||||
if let Some(sender) = &GLOBAL_SENDER {
|
if let Some(sender) = &GLOBAL_SENDER {
|
||||||
@@ -43,6 +44,7 @@ pub async fn send_msg(msg: &str) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[allow(static_mut_refs)]
|
||||||
pub async fn send_cmd_json(func_name: &str, func_args: Value) -> Option<Value> {
|
pub async fn send_cmd_json(func_name: &str, func_args: Value) -> Option<Value> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let Some(sender) = &GLOBAL_SENDER else { return None };
|
let Some(sender) = &GLOBAL_SENDER else { return None };
|
||||||
@@ -65,6 +67,7 @@ pub async fn send_cmd_json(func_name: &str, func_args: Value) -> Option<Value> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[allow(static_mut_refs)]
|
||||||
async fn receive_response() -> Option<Value> {
|
async fn receive_response() -> Option<Value> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let Some(receiver) = &GLOBAL_RECEIVER else { return None };
|
let Some(receiver) = &GLOBAL_RECEIVER else { return None };
|
||||||
|
|||||||
Reference in New Issue
Block a user