Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bdb7cb17b | ||
|
|
07906aaea1 | ||
|
|
47a33128be | ||
|
|
fdb04149aa | ||
|
|
4805f6cd5d | ||
|
|
ff85c8d485 | ||
|
|
ca0d27a929 | ||
|
|
8ebb689ee7 | ||
|
|
ecc81c5f35 | ||
|
|
96c1fcb17c | ||
|
|
840e17e03a |
Vendored
+10
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"cSpell.words": [
|
||||||
|
"aiofiles",
|
||||||
|
"blurple",
|
||||||
|
"CLANLEADER",
|
||||||
|
"Dakiny",
|
||||||
|
"DISCORDMANAGER",
|
||||||
|
"highcouncil"
|
||||||
|
]
|
||||||
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3.11.9-slim
|
FROM python:3.12.3-slim
|
||||||
|
|
||||||
# install dependencies
|
# install dependencies
|
||||||
RUN apt-get update && apt-get -y upgrade && apt-get -y install git sqlite3
|
RUN apt-get update && apt-get -y upgrade && apt-get -y install git sqlite3
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ class VoteHighcouncilView(nextcord.ui.View):
|
|||||||
)
|
)
|
||||||
for nominee in sorted(
|
for nominee in sorted(
|
||||||
session.scalars(stmt),
|
session.scalars(stmt),
|
||||||
key=lambda nominee: self.guild.get_member(nominee).display_name,
|
key=lambda nominee: self.guild.get_member(nominee).display_name.lower(),
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
options.append(
|
options.append(
|
||||||
@@ -213,7 +213,7 @@ class VoteHighcouncilView(nextcord.ui.View):
|
|||||||
)
|
)
|
||||||
for nominee in sorted(
|
for nominee in sorted(
|
||||||
session.scalars(stmt),
|
session.scalars(stmt),
|
||||||
key=lambda nominee: self.guild.get_member(nominee).display_name,
|
key=lambda nominee: self.guild.get_member(nominee).display_name.lower(),
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
options.append(
|
options.append(
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ class VoteView(nextcord.ui.View):
|
|||||||
)
|
)
|
||||||
for nominee in sorted(
|
for nominee in sorted(
|
||||||
session.scalars(stmt),
|
session.scalars(stmt),
|
||||||
key=lambda nominee: self.guild.get_member(nominee).display_name,
|
key=lambda nominee: self.guild.get_member(nominee).display_name.lower(),
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
options.append(
|
options.append(
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from cogs.commands.remove import RemoveCog
|
|||||||
from cogs.commands.request import RequestCog
|
from cogs.commands.request import RequestCog
|
||||||
from cogs.commands.set import SetCog
|
from cogs.commands.set import SetCog
|
||||||
from cogs.commands.test import TestCog
|
from cogs.commands.test import TestCog
|
||||||
|
from cogs.commands.time import TimeCog
|
||||||
from cogs.commands.view import ViewCog
|
from cogs.commands.view import ViewCog
|
||||||
|
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ def setup(bot: commands.Bot) -> None:
|
|||||||
bot.add_cog(RefreshCog(bot))
|
bot.add_cog(RefreshCog(bot))
|
||||||
bot.add_cog(RemoveCog())
|
bot.add_cog(RemoveCog())
|
||||||
bot.add_cog(RequestCog(bot))
|
bot.add_cog(RequestCog(bot))
|
||||||
bot.add_cog(TestCog(bot))
|
|
||||||
bot.add_cog(ViewCog())
|
|
||||||
bot.add_cog(SetCog())
|
bot.add_cog(SetCog())
|
||||||
|
bot.add_cog(TestCog(bot))
|
||||||
|
bot.add_cog(TimeCog())
|
||||||
|
bot.add_cog(ViewCog())
|
||||||
|
|||||||
+83
-3
@@ -5,7 +5,7 @@ from discord import TextInputStyle
|
|||||||
from nextcord.ext import commands
|
from nextcord.ext import commands
|
||||||
|
|
||||||
from config import DakinyConfig
|
from config import DakinyConfig
|
||||||
from utils import record_event_points
|
from utils import GUILDS, MAX_DISCORD_MESSAGE_LENGTH, record_event_points
|
||||||
|
|
||||||
|
|
||||||
class RecordCog(commands.Cog, name="Record Commands"):
|
class RecordCog(commands.Cog, name="Record Commands"):
|
||||||
@@ -16,7 +16,7 @@ class RecordCog(commands.Cog, name="Record Commands"):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.bot: commands.Bot = bot
|
self.bot: commands.Bot = bot
|
||||||
|
|
||||||
@nextcord.slash_command()
|
@nextcord.slash_command(guild_ids=GUILDS)
|
||||||
async def record(self, interaction: nextcord.Interaction) -> None:
|
async def record(self, interaction: nextcord.Interaction) -> None:
|
||||||
"""Parent slash command for record. Never called."""
|
"""Parent slash command for record. Never called."""
|
||||||
|
|
||||||
@@ -64,6 +64,86 @@ class RecordCog(commands.Cog, name="Record Commands"):
|
|||||||
embed=embed,
|
embed=embed,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@record.subcommand(description="Record a miscellaneous point for a clan member.")
|
||||||
|
async def point( # noqa: PLR0913
|
||||||
|
self,
|
||||||
|
interaction: nextcord.Interaction,
|
||||||
|
activity: str = nextcord.SlashOption(
|
||||||
|
description="The type of activity to receive points for.",
|
||||||
|
required=True,
|
||||||
|
choices=[
|
||||||
|
"Clan chat activity",
|
||||||
|
"Teaching or sharing in-game tips",
|
||||||
|
"Recruiting efforts",
|
||||||
|
"Successful recruitment",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
justification: str = nextcord.SlashOption(
|
||||||
|
required=True,
|
||||||
|
description="Reason to give the recipient a point.",
|
||||||
|
max_length=int(MAX_DISCORD_MESSAGE_LENGTH / 2),
|
||||||
|
),
|
||||||
|
recipient: nextcord.Member = nextcord.SlashOption(
|
||||||
|
required=False,
|
||||||
|
description="Discord user to give point to.",
|
||||||
|
),
|
||||||
|
non_discord_recipient: str = nextcord.SlashOption(
|
||||||
|
required=False, description="Non Discord user to give point to.", max_length=32
|
||||||
|
),
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Gives a point to the provided recipient. Sends recording message to event participation channel.
|
||||||
|
|
||||||
|
Only one recipient should be given point/provided to the command. Fails if not exactly one recipient
|
||||||
|
is given.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
interaction (nextcord.Interaction): interaction with this command.
|
||||||
|
activity (str): Choice list of activities to give point for.
|
||||||
|
justification (str): Reason to give point.
|
||||||
|
recipient (nextcord.Member): Discord member to give point to.
|
||||||
|
non_discord_recipient (str): Non Discord member to give point to.
|
||||||
|
"""
|
||||||
|
# role check
|
||||||
|
if interaction.guild.get_role(self.bot.config.params.clan_member_role_id) not in interaction.user.roles:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
f"You must have the <@&{self.bot.config.params.clan_member_role_id}> role to record points.",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# pylint or whatever seems to think that there are lines of unreachable code. That is false.
|
||||||
|
if recipient and non_discord_recipient:
|
||||||
|
await interaction.response.send_message("Please only provide 1 recipient.", ephemeral=True)
|
||||||
|
return
|
||||||
|
elif recipient is not None: # noqa: RET505
|
||||||
|
record_event_points(
|
||||||
|
self.bot.config,
|
||||||
|
[str(recipient.id)],
|
||||||
|
self.bot.config.params.event_participate_points,
|
||||||
|
is_member=True,
|
||||||
|
)
|
||||||
|
recipient_str = f"<@{recipient.id}>"
|
||||||
|
elif non_discord_recipient is not None:
|
||||||
|
record_event_points(
|
||||||
|
self.bot.config,
|
||||||
|
[non_discord_recipient],
|
||||||
|
self.bot.config.params.event_participate_points,
|
||||||
|
is_member=False,
|
||||||
|
)
|
||||||
|
recipient_str = f'"{non_discord_recipient}"'
|
||||||
|
else:
|
||||||
|
await interaction.response.send_message("You must provide a recipient.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
event_channel: nextcord.TextChannel = interaction.guild.get_channel(
|
||||||
|
self.bot.config.params.event_post_channel_id
|
||||||
|
)
|
||||||
|
|
||||||
|
msg = f"<@{interaction.user.id}> has given {recipient_str} points for the {activity} activity. Justification: {justification}." # noqa: E501
|
||||||
|
await event_channel.send(msg)
|
||||||
|
await interaction.response.send_message("Point has been added.", ephemeral=True)
|
||||||
|
|
||||||
|
|
||||||
class EventView(nextcord.ui.View):
|
class EventView(nextcord.ui.View):
|
||||||
"""
|
"""
|
||||||
@@ -109,8 +189,8 @@ class EventView(nextcord.ui.View):
|
|||||||
record_event_points(
|
record_event_points(
|
||||||
self.config,
|
self.config,
|
||||||
discord_users_list,
|
discord_users_list,
|
||||||
True, # noqa: FBT003
|
|
||||||
self.config.params.event_participate_points,
|
self.config.params.event_participate_points,
|
||||||
|
is_member=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
non_discord_users = interaction.message.embeds[0].fields[1].value
|
non_discord_users = interaction.message.embeds[0].fields[1].value
|
||||||
|
|||||||
@@ -6,14 +6,13 @@ import nextcord
|
|||||||
from nextcord.ext import application_checks, commands
|
from nextcord.ext import application_checks, commands
|
||||||
|
|
||||||
from classes.election_cycle import ElectionCycle
|
from classes.election_cycle import ElectionCycle
|
||||||
from utils import get_current_election_cycle
|
from utils import GUILDS, get_current_election_cycle
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from config import DakinyConfig
|
from config import DakinyConfig
|
||||||
|
|
||||||
|
|
||||||
ME = 206602713511886849
|
ME = 206602713511886849
|
||||||
GUILDS = [1000247361639030804]
|
|
||||||
|
|
||||||
|
|
||||||
def is_me(): # noqa: ANN201
|
def is_me(): # noqa: ANN201
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
"""Contains commands for time commands."""
|
||||||
|
|
||||||
|
from datetime import MAXYEAR, MINYEAR, UTC, datetime
|
||||||
|
|
||||||
|
import nextcord
|
||||||
|
from nextcord.ext import commands
|
||||||
|
|
||||||
|
from utils import GUILDS
|
||||||
|
|
||||||
|
|
||||||
|
class TimeCog(commands.Cog, name="Time Commands"):
|
||||||
|
"""Class holds time commands."""
|
||||||
|
|
||||||
|
@nextcord.slash_command(guild_ids=GUILDS)
|
||||||
|
async def time(self, interaction: nextcord.Interaction) -> None:
|
||||||
|
"""Parent slash command. Never called."""
|
||||||
|
|
||||||
|
@time.subcommand(
|
||||||
|
description="Get a discord timestamp of the given game time. If date information is left blank, uses current day." # noqa: E501
|
||||||
|
)
|
||||||
|
async def at( # noqa: PLR0913
|
||||||
|
self,
|
||||||
|
interaction: nextcord.Interaction,
|
||||||
|
hour: int = nextcord.SlashOption(
|
||||||
|
description="The hour you want a timestamp of. Given in gametime/UTC.",
|
||||||
|
min_value=0,
|
||||||
|
max_value=23,
|
||||||
|
required=True,
|
||||||
|
),
|
||||||
|
minute: int = nextcord.SlashOption(
|
||||||
|
description="The minute you want a timestamp of. Given in gametime/UTC.",
|
||||||
|
min_value=0,
|
||||||
|
max_value=59,
|
||||||
|
required=True,
|
||||||
|
),
|
||||||
|
year: int = nextcord.SlashOption(
|
||||||
|
description="Year to make timestamp of.", required=False, min_value=MINYEAR, max_value=MAXYEAR
|
||||||
|
),
|
||||||
|
month: int = nextcord.SlashOption(
|
||||||
|
description="Month to make timestamp of.", required=False, min_value=1, max_value=12
|
||||||
|
),
|
||||||
|
day: int = nextcord.SlashOption(
|
||||||
|
description="Day to make timestamp of.", required=False, min_value=1, max_value=31
|
||||||
|
),
|
||||||
|
ephemeral: bool = nextcord.SlashOption( # noqa: FBT001
|
||||||
|
description="Whether the replying message will be public or not.", required=False, default=True
|
||||||
|
),
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Replies with a discord timestamp of given UTC time.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
interaction (nextcord.Interaction): Interaction with the command.
|
||||||
|
hour (int): The hour in UTC to make the timestamp of..
|
||||||
|
minute (int): The minute in UTC to make the timestamp of..
|
||||||
|
year (int): The year to make the timestamp of.
|
||||||
|
month (int): The month to make the timestamp of.
|
||||||
|
day (int): The day to make the timestamp of.
|
||||||
|
ephemeral (bool): Whether the response is ephemeral or not.
|
||||||
|
"""
|
||||||
|
gametime_today: datetime = datetime.now(tz=UTC).replace(hour=hour, minute=minute)
|
||||||
|
|
||||||
|
if year is not None:
|
||||||
|
gametime_today = gametime_today.replace(year=year)
|
||||||
|
|
||||||
|
if month is not None and day is not None:
|
||||||
|
try:
|
||||||
|
gametime_today = gametime_today.replace(month=month, day=day)
|
||||||
|
except ValueError:
|
||||||
|
await interaction.response.send_message(f"Day {day} is out of range for month {month}.", ephemeral=True)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
if month is not None:
|
||||||
|
try:
|
||||||
|
gametime_today = gametime_today.replace(month=month)
|
||||||
|
except ValueError:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
f"Today ({gametime_today.day}) is out of range for month {month}.", ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if day is not None:
|
||||||
|
try:
|
||||||
|
gametime_today = gametime_today.replace(day=day)
|
||||||
|
except ValueError:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
f"Day {day} is out of range for this month {gametime_today.month}.", ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
await interaction.response.send_message(content=f"<t:{int(gametime_today.timestamp())}:t>", ephemeral=ephemeral)
|
||||||
|
|
||||||
|
@time.subcommand(description="Get the current time in game.")
|
||||||
|
async def gametime(
|
||||||
|
self,
|
||||||
|
interaction: nextcord.Interaction,
|
||||||
|
ephemeral: bool = nextcord.SlashOption( # noqa: FBT001
|
||||||
|
description="Whether the replying message will be public or not.", required=False, default=True
|
||||||
|
),
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Replies with the current time in game.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
interaction (nextcord.Interaction): Interaction with the command.
|
||||||
|
ephemeral: (bool): Whether the response is ephemeral or not.
|
||||||
|
"""
|
||||||
|
gametime = datetime.now(tz=UTC)
|
||||||
|
await interaction.response.send_message(content=f"{gametime.hour}:{gametime.minute}", ephemeral=ephemeral)
|
||||||
@@ -74,7 +74,7 @@ class ViewCog(commands.Cog, name="View Commands"):
|
|||||||
sorted_members = members.sort_values(
|
sorted_members = members.sort_values(
|
||||||
by="UserId",
|
by="UserId",
|
||||||
key=lambda user_ids: (
|
key=lambda user_ids: (
|
||||||
[interaction.guild.get_member(int(user_id)).display_name for user_id in user_ids]
|
[interaction.guild.get_member(int(user_id)).display_name.lower() for user_id in user_ids]
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
sorted_non_members = non_members.sort_values(by="UserId")
|
sorted_non_members = non_members.sort_values(by="UserId")
|
||||||
|
|||||||
@@ -35,4 +35,4 @@ class BotPresence(commands.Cog, name="Bot Presence"):
|
|||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
presence = session.scalar(select(Presences.Presence).order_by(func.random()))
|
presence = session.scalar(select(Presences.Presence).order_by(func.random()))
|
||||||
|
|
||||||
await self.bot.change_presence(activity=nextcord.CustomActivity(name="Custom Status", state=presence))
|
await self.bot.change_presence(activity=nextcord.CustomActivity(presence))
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ REPO_DIR = "./dakiny-messages"
|
|||||||
MESSAGES_DIR = f"{REPO_DIR}/messages"
|
MESSAGES_DIR = f"{REPO_DIR}/messages"
|
||||||
TEMPLATES_DIR = f"{REPO_DIR}/message_templates"
|
TEMPLATES_DIR = f"{REPO_DIR}/message_templates"
|
||||||
MAX_DISCORD_MESSAGE_LENGTH = 2000
|
MAX_DISCORD_MESSAGE_LENGTH = 2000
|
||||||
|
GUILDS = [1000247361639030804, 239758371358638080]
|
||||||
|
|
||||||
VoteType = StrEnum("VoteType", ["COUNCIL", "DISCORDMANAGER", "CLANLEADER"])
|
VoteType = StrEnum("VoteType", ["COUNCIL", "DISCORDMANAGER", "CLANLEADER"])
|
||||||
|
|
||||||
@@ -136,7 +137,7 @@ def build_votes_str(config: DakinyConfig, guild: nextcord.Guild) -> str:
|
|||||||
Creates a string that contains all nominees and their votes to be sent as a discord message.
|
Creates a string that contains all nominees and their votes to be sent as a discord message.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
config (DakinyConfig): Daking config object
|
config (DakinyConfig): Dakiny config object
|
||||||
guild (nextcord.Guild): The guild object. Used to get display names for sorting.
|
guild (nextcord.Guild): The guild object. Used to get display names for sorting.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -151,7 +152,7 @@ def build_votes_str(config: DakinyConfig, guild: nextcord.Guild) -> str:
|
|||||||
|
|
||||||
for nominee in sorted(
|
for nominee in sorted(
|
||||||
session.scalars(stmt),
|
session.scalars(stmt),
|
||||||
key=lambda nominee: guild.get_member(nominee.MemberId).display_name,
|
key=lambda nominee: guild.get_member(nominee.MemberId).display_name.lower(),
|
||||||
):
|
):
|
||||||
votes[nominee.MemberId] = 0
|
votes[nominee.MemberId] = 0
|
||||||
|
|
||||||
@@ -172,7 +173,7 @@ def build_highcouncil_votes_str(config: DakinyConfig, guild: nextcord.Guild) ->
|
|||||||
Creates a string that contains all high council nominees and their votes to be sent as a discord message.
|
Creates a string that contains all high council nominees and their votes to be sent as a discord message.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
config (DakinyConfig): Daking config object
|
config (DakinyConfig): Dakiny config object
|
||||||
guild (nextcord.Guild): The guild object. Used to get display names for sorting.
|
guild (nextcord.Guild): The guild object. Used to get display names for sorting.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -192,7 +193,7 @@ def build_highcouncil_votes_str(config: DakinyConfig, guild: nextcord.Guild) ->
|
|||||||
|
|
||||||
for nominee in sorted(
|
for nominee in sorted(
|
||||||
session.scalars(stmt),
|
session.scalars(stmt),
|
||||||
key=lambda nominee: guild.get_member(nominee.MemberId).display_name,
|
key=lambda nominee: guild.get_member(nominee.MemberId).display_name.lower(),
|
||||||
):
|
):
|
||||||
cl_votes[nominee.MemberId] = 0
|
cl_votes[nominee.MemberId] = 0
|
||||||
|
|
||||||
@@ -210,7 +211,7 @@ def build_highcouncil_votes_str(config: DakinyConfig, guild: nextcord.Guild) ->
|
|||||||
|
|
||||||
for nominee in sorted(
|
for nominee in sorted(
|
||||||
session.scalars(stmt),
|
session.scalars(stmt),
|
||||||
key=lambda nominee: guild.get_member(nominee.MemberId).display_name,
|
key=lambda nominee: guild.get_member(nominee.MemberId).display_name.lower(),
|
||||||
):
|
):
|
||||||
dm_votes[nominee.MemberId] = 0
|
dm_votes[nominee.MemberId] = 0
|
||||||
|
|
||||||
@@ -373,7 +374,7 @@ def get_allowed_points(session: Session, points_limit: int, user: str, points: i
|
|||||||
add_points = (
|
add_points = (
|
||||||
points
|
points
|
||||||
if limit.Points + points <= points_limit
|
if limit.Points + points <= points_limit
|
||||||
else (points_limit - limit.Points if points_limit - limit.Points > 0 else 0)
|
else (max(0, points_limit - limit.Points))
|
||||||
)
|
)
|
||||||
limit.Points += add_points
|
limit.Points += add_points
|
||||||
|
|
||||||
@@ -511,7 +512,7 @@ async def edit_clan_message(
|
|||||||
|
|
||||||
async def edit_clan_council_info(guild: nextcord.Guild, config: DakinyConfig) -> None:
|
async def edit_clan_council_info(guild: nextcord.Guild, config: DakinyConfig) -> None:
|
||||||
"""
|
"""
|
||||||
Gets the winners of the elections and updates the perminant message that states the current council.
|
Gets the winners of the elections and updates the permanent message that states the current council.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
guild (nextcord.Guild): Guild object that to use to get channel.
|
guild (nextcord.Guild): Guild object that to use to get channel.
|
||||||
@@ -539,8 +540,12 @@ async def edit_clan_council_info(guild: nextcord.Guild, config: DakinyConfig) ->
|
|||||||
.where(CouncilMembers.Elected == true())
|
.where(CouncilMembers.Elected == true())
|
||||||
)
|
)
|
||||||
|
|
||||||
|
council_member_ids = sorted(
|
||||||
|
session.scalars(stmt), key=lambda user_id: guild.get_member(user_id).display_name.lower()
|
||||||
|
)
|
||||||
|
|
||||||
council_members_strs = [
|
council_members_strs = [
|
||||||
f"<@{user_id}>" for user_id in session.scalars(stmt) if user_id not in (discord_manager, clan_leader)
|
f"<@{user_id}>" for user_id in council_member_ids if user_id not in (discord_manager, clan_leader)
|
||||||
]
|
]
|
||||||
|
|
||||||
quarter = get_quarter_name(config)
|
quarter = get_quarter_name(config)
|
||||||
|
|||||||
Reference in New Issue
Block a user