Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b241b0f954 | |||
| 171dd1dc02 | |||
| af62d969d7 | |||
| c4fd9a66c6 | |||
| d191997a39 |
@@ -2,6 +2,7 @@ import abc
|
|||||||
from typing import Union, Any, List
|
from typing import Union, Any, List
|
||||||
from nakuru.entities.components import Plain, At, Image, BaseMessageComponent
|
from nakuru.entities.components import Plain, At, Image, BaseMessageComponent
|
||||||
from type.astrbot_message import AstrBotMessage
|
from type.astrbot_message import AstrBotMessage
|
||||||
|
from type.command import CommandResult
|
||||||
|
|
||||||
|
|
||||||
class Platform():
|
class Platform():
|
||||||
@@ -24,7 +25,7 @@ class Platform():
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
async def send_msg(self, target: Any, result_message: Union[List[BaseMessageComponent], str]):
|
async def send_msg(self, target: Any, result_message: CommandResult):
|
||||||
'''
|
'''
|
||||||
发送消息(主动)
|
发送消息(主动)
|
||||||
'''
|
'''
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from aiocqhttp.exceptions import ActionFailed
|
|||||||
from . import Platform
|
from . import Platform
|
||||||
from type.astrbot_message import *
|
from type.astrbot_message import *
|
||||||
from type.message_event import *
|
from type.message_event import *
|
||||||
|
from type.command import *
|
||||||
from typing import Union, List, Dict
|
from typing import Union, List, Dict
|
||||||
from nakuru.entities.components import *
|
from nakuru.entities.components import *
|
||||||
from SparkleLogging.utils.core import LogManager
|
from SparkleLogging.utils.core import LogManager
|
||||||
@@ -165,7 +166,7 @@ class AIOCQHTTP(Platform):
|
|||||||
|
|
||||||
await self._reply(message, res)
|
await self._reply(message, res)
|
||||||
|
|
||||||
async def _reply(self, message: AstrBotMessage, message_chain: List[BaseMessageComponent]):
|
async def _reply(self, message: Union[AstrBotMessage, Dict], message_chain: List[BaseMessageComponent]):
|
||||||
if isinstance(message_chain, str):
|
if isinstance(message_chain, str):
|
||||||
message_chain = [Plain(text=message_chain), ]
|
message_chain = [Plain(text=message_chain), ]
|
||||||
|
|
||||||
@@ -179,7 +180,15 @@ class AIOCQHTTP(Platform):
|
|||||||
image_idx.append(idx)
|
image_idx.append(idx)
|
||||||
ret.append(d)
|
ret.append(d)
|
||||||
try:
|
try:
|
||||||
await self.bot.send(message.raw_message, ret)
|
if isinstance(message, AstrBotMessage):
|
||||||
|
await self.bot.send(message.raw_message, ret)
|
||||||
|
if isinstance(message, dict):
|
||||||
|
if 'group_id' in message:
|
||||||
|
await self.bot.send_group_msg(group_id=message['group_id'], message=ret)
|
||||||
|
elif 'user_id' in message:
|
||||||
|
await self.bot.send_private_msg(user_id=message['user_id'], message=ret)
|
||||||
|
else:
|
||||||
|
raise Exception("aiocqhttp: 无法识别的消息来源。仅支持 group_id 和 user_id。")
|
||||||
except ActionFailed as e:
|
except ActionFailed as e:
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
logger.error(f"回复消息失败: {e}")
|
logger.error(f"回复消息失败: {e}")
|
||||||
@@ -196,3 +205,16 @@ class AIOCQHTTP(Platform):
|
|||||||
ret[idx]['data']['file'] = image_url
|
ret[idx]['data']['file'] = image_url
|
||||||
ret[idx]['data']['path'] = image_url
|
ret[idx]['data']['path'] = image_url
|
||||||
await self.bot.send(message.raw_message, ret)
|
await self.bot.send(message.raw_message, ret)
|
||||||
|
|
||||||
|
async def send_msg(self, target: Dict[str, int], result_message: CommandResult):
|
||||||
|
'''
|
||||||
|
以主动的方式给QQ用户、QQ群发送一条消息。
|
||||||
|
|
||||||
|
`target` 接收一个 dict 类型的值引用。
|
||||||
|
|
||||||
|
- 要发给 QQ 下的某个用户,请添加 key `user_id`,值为 int 类型的 qq 号;
|
||||||
|
- 要发给某个群聊,请添加 key `group_id`,值为 int 类型的 qq 群号;
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
await self._reply(target, result_message.message_chain)
|
||||||
@@ -14,6 +14,7 @@ from type.types import Context
|
|||||||
from . import Platform
|
from . import Platform
|
||||||
from type.astrbot_message import *
|
from type.astrbot_message import *
|
||||||
from type.message_event import *
|
from type.message_event import *
|
||||||
|
from type.command import *
|
||||||
from SparkleLogging.utils.core import LogManager
|
from SparkleLogging.utils.core import LogManager
|
||||||
from logging import Logger
|
from logging import Logger
|
||||||
from astrbot.message.handler import MessageHandler
|
from astrbot.message.handler import MessageHandler
|
||||||
@@ -199,7 +200,7 @@ class QQGOCQ(Platform):
|
|||||||
return
|
return
|
||||||
await self.client.sendGroupMessage(group_id, message_chain)
|
await self.client.sendGroupMessage(group_id, message_chain)
|
||||||
|
|
||||||
async def send_msg(self, target: Dict[str, int], result_message: Union[List[BaseMessageComponent], str]):
|
async def send_msg(self, target: Dict[str, int], result_message: CommandResult):
|
||||||
'''
|
'''
|
||||||
以主动的方式给用户、群或者频道发送一条消息。
|
以主动的方式给用户、群或者频道发送一条消息。
|
||||||
|
|
||||||
@@ -211,7 +212,7 @@ class QQGOCQ(Platform):
|
|||||||
|
|
||||||
guild_id 不是频道号。
|
guild_id 不是频道号。
|
||||||
'''
|
'''
|
||||||
await self._reply(target, result_message)
|
await self._reply(target, result_message.message_chain)
|
||||||
|
|
||||||
def convert_message(self, message: Union[GroupMessage, FriendMessage, GuildMessage]) -> AstrBotMessage:
|
def convert_message(self, message: Union[GroupMessage, FriendMessage, GuildMessage]) -> AstrBotMessage:
|
||||||
abm = AstrBotMessage()
|
abm = AstrBotMessage()
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from util.io import save_temp_img, download_image_by_url
|
|||||||
from . import Platform
|
from . import Platform
|
||||||
from type.astrbot_message import *
|
from type.astrbot_message import *
|
||||||
from type.message_event import *
|
from type.message_event import *
|
||||||
|
from type.command import *
|
||||||
from typing import Union, List, Dict
|
from typing import Union, List, Dict
|
||||||
from nakuru.entities.components import *
|
from nakuru.entities.components import *
|
||||||
from SparkleLogging.utils.core import LogManager
|
from SparkleLogging.utils.core import LogManager
|
||||||
@@ -43,10 +44,15 @@ class botClient(Client):
|
|||||||
abm = self.platform._parse_from_qqofficial(message, MessageType.FRIEND_MESSAGE)
|
abm = self.platform._parse_from_qqofficial(message, MessageType.FRIEND_MESSAGE)
|
||||||
await self.platform.handle_msg(abm)
|
await self.platform.handle_msg(abm)
|
||||||
|
|
||||||
|
# 收到 C2C 消息
|
||||||
|
async def on_c2c_message_create(self, message: botpy.message.C2CMessage):
|
||||||
|
abm = self.platform._parse_from_qqofficial(message, MessageType.FRIEND_MESSAGE)
|
||||||
|
await self.platform.handle_msg(abm)
|
||||||
|
|
||||||
|
|
||||||
class QQOfficial(Platform):
|
class QQOfficial(Platform):
|
||||||
|
|
||||||
def __init__(self, context: Context, message_handler: MessageHandler) -> None:
|
def __init__(self, context: Context, message_handler: MessageHandler, test_mode = False) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.loop = asyncio.new_event_loop()
|
self.loop = asyncio.new_event_loop()
|
||||||
asyncio.set_event_loop(self.loop)
|
asyncio.set_event_loop(self.loop)
|
||||||
@@ -80,6 +86,8 @@ class QQOfficial(Platform):
|
|||||||
|
|
||||||
self.client.set_platform(self)
|
self.client.set_platform(self)
|
||||||
|
|
||||||
|
self.test_mode = test_mode
|
||||||
|
|
||||||
async def _parse_to_qqofficial(self, message: List[BaseMessageComponent], is_group: bool = False):
|
async def _parse_to_qqofficial(self, message: List[BaseMessageComponent], is_group: bool = False):
|
||||||
plain_text = ""
|
plain_text = ""
|
||||||
image_path = None # only one img supported
|
image_path = None # only one img supported
|
||||||
@@ -107,11 +115,17 @@ class QQOfficial(Platform):
|
|||||||
abm.tag = "qqchan"
|
abm.tag = "qqchan"
|
||||||
msg: List[BaseMessageComponent] = []
|
msg: List[BaseMessageComponent] = []
|
||||||
|
|
||||||
if message_type == MessageType.GROUP_MESSAGE:
|
if isinstance(message, botpy.message.GroupMessage) or isinstance(message, botpy.message.C2CMessage):
|
||||||
abm.sender = MessageMember(
|
if isinstance(message, botpy.message.GroupMessage):
|
||||||
message.author.member_openid,
|
abm.sender = MessageMember(
|
||||||
""
|
message.author.member_openid,
|
||||||
)
|
""
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
abm.sender = MessageMember(
|
||||||
|
message.author.user_openid,
|
||||||
|
""
|
||||||
|
)
|
||||||
abm.message_str = message.content.strip()
|
abm.message_str = message.content.strip()
|
||||||
abm.self_id = "unknown_selfid"
|
abm.self_id = "unknown_selfid"
|
||||||
|
|
||||||
@@ -126,8 +140,7 @@ class QQOfficial(Platform):
|
|||||||
msg.append(img)
|
msg.append(img)
|
||||||
abm.message = msg
|
abm.message = msg
|
||||||
|
|
||||||
elif message_type == MessageType.GUILD_MESSAGE or message_type == MessageType.FRIEND_MESSAGE:
|
elif isinstance(message, botpy.message.Message) or isinstance(message, botpy.message.DirectMessage):
|
||||||
# 目前对于 FRIEND_MESSAGE 只处理频道私聊
|
|
||||||
try:
|
try:
|
||||||
abm.self_id = str(message.mentions[0].id)
|
abm.self_id = str(message.mentions[0].id)
|
||||||
except:
|
except:
|
||||||
@@ -175,7 +188,7 @@ class QQOfficial(Platform):
|
|||||||
|
|
||||||
async def handle_msg(self, message: AstrBotMessage):
|
async def handle_msg(self, message: AstrBotMessage):
|
||||||
assert isinstance(message.raw_message, (botpy.message.Message,
|
assert isinstance(message.raw_message, (botpy.message.Message,
|
||||||
botpy.message.GroupMessage, botpy.message.DirectMessage))
|
botpy.message.GroupMessage, botpy.message.DirectMessage, botpy.message.C2CMessage))
|
||||||
is_group = message.type != MessageType.FRIEND_MESSAGE
|
is_group = message.type != MessageType.FRIEND_MESSAGE
|
||||||
|
|
||||||
_t = "/私聊" if not is_group else ""
|
_t = "/私聊" if not is_group else ""
|
||||||
@@ -209,7 +222,7 @@ class QQOfficial(Platform):
|
|||||||
if not message_result:
|
if not message_result:
|
||||||
return
|
return
|
||||||
|
|
||||||
await self.reply_msg(message, message_result.result_message)
|
ret = await self.reply_msg(message, message_result.result_message)
|
||||||
if message_result.callback:
|
if message_result.callback:
|
||||||
message_result.callback()
|
message_result.callback()
|
||||||
|
|
||||||
@@ -217,6 +230,8 @@ class QQOfficial(Platform):
|
|||||||
if session_id in self.waiting and self.waiting[session_id] == '':
|
if session_id in self.waiting and self.waiting[session_id] == '':
|
||||||
self.waiting[session_id] = message
|
self.waiting[session_id] = message
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
async def reply_msg(self,
|
async def reply_msg(self,
|
||||||
message: AstrBotMessage,
|
message: AstrBotMessage,
|
||||||
result_message: List[BaseMessageComponent]):
|
result_message: List[BaseMessageComponent]):
|
||||||
@@ -225,7 +240,7 @@ class QQOfficial(Platform):
|
|||||||
'''
|
'''
|
||||||
source = message.raw_message
|
source = message.raw_message
|
||||||
assert isinstance(source, (botpy.message.Message,
|
assert isinstance(source, (botpy.message.Message,
|
||||||
botpy.message.GroupMessage, botpy.message.DirectMessage))
|
botpy.message.GroupMessage, botpy.message.DirectMessage, botpy.message.C2CMessage))
|
||||||
logger.info(
|
logger.info(
|
||||||
f"{message.sender.nickname}({message.sender.user_id}) <- {self.parse_message_outline(result_message)}")
|
f"{message.sender.nickname}({message.sender.user_id}) <- {self.parse_message_outline(result_message)}")
|
||||||
|
|
||||||
@@ -253,12 +268,14 @@ class QQOfficial(Platform):
|
|||||||
'message_reference': msg_ref
|
'message_reference': msg_ref
|
||||||
}
|
}
|
||||||
|
|
||||||
if message.type == MessageType.GROUP_MESSAGE:
|
if isinstance(message.raw_message, botpy.message.GroupMessage):
|
||||||
data['group_openid'] = str(source.group_openid)
|
data['group_openid'] = str(source.group_openid)
|
||||||
elif message.type == MessageType.GUILD_MESSAGE:
|
elif isinstance(message.raw_message, botpy.message.Message):
|
||||||
data['channel_id'] = source.channel_id
|
data['channel_id'] = source.channel_id
|
||||||
elif message.type == MessageType.FRIEND_MESSAGE:
|
elif isinstance(message.raw_message, botpy.message.DirectMessage):
|
||||||
data['guild_id'] = source.guild_id
|
data['guild_id'] = source.guild_id
|
||||||
|
elif isinstance(message.raw_message, botpy.message.C2CMessage):
|
||||||
|
data['openid'] = source.author.user_openid
|
||||||
if image_path:
|
if image_path:
|
||||||
data['file_image'] = image_path
|
data['file_image'] = image_path
|
||||||
if rendered_images:
|
if rendered_images:
|
||||||
@@ -269,14 +286,13 @@ class QQOfficial(Platform):
|
|||||||
_data['message_reference'] = None
|
_data['message_reference'] = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self._reply(**_data)
|
return await self._reply(**_data)
|
||||||
return
|
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
logger.warn(traceback.format_exc())
|
logger.warn(traceback.format_exc())
|
||||||
logger.warn(f"以文本转图片的形式回复消息时发生错误: {e},将尝试默认方式。")
|
logger.warn(f"以文本转图片的形式回复消息时发生错误: {e},将尝试默认方式。")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self._reply(**data)
|
return await self._reply(**data)
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
# 分割过长的消息
|
# 分割过长的消息
|
||||||
@@ -286,28 +302,27 @@ class QQOfficial(Platform):
|
|||||||
split_res.append(plain_text[len(plain_text)//2:])
|
split_res.append(plain_text[len(plain_text)//2:])
|
||||||
for i in split_res:
|
for i in split_res:
|
||||||
data['content'] = i
|
data['content'] = i
|
||||||
await self._reply(**data)
|
return await self._reply(**data)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
# 防止被qq频道过滤消息
|
# 防止被qq频道过滤消息
|
||||||
plain_text = plain_text.replace(".", " . ")
|
plain_text = plain_text.replace(".", " . ")
|
||||||
await self._reply(**data)
|
return await self._reply(**data)
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
try:
|
try:
|
||||||
data['content'] = str.join(" ", plain_text)
|
data['content'] = str.join(" ", plain_text)
|
||||||
await self._reply(**data)
|
return await self._reply(**data)
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
plain_text = re.sub(
|
plain_text = re.sub(
|
||||||
r'(https|http)?:\/\/(\w|\.|\/|\?|\=|\&|\%)*\b', '[被隐藏的链接]', str(e), flags=re.MULTILINE)
|
r'(https|http)?:\/\/(\w|\.|\/|\?|\=|\&|\%)*\b', '[被隐藏的链接]', str(e), flags=re.MULTILINE)
|
||||||
plain_text = plain_text.replace(".", "·")
|
plain_text = plain_text.replace(".", "·")
|
||||||
data['content'] = plain_text
|
data['content'] = plain_text
|
||||||
await self._reply(**data)
|
return await self._reply(**data)
|
||||||
|
|
||||||
async def _reply(self, **kwargs):
|
async def _reply(self, **kwargs):
|
||||||
if 'group_openid' in kwargs:
|
if 'group_openid' in kwargs or 'openid' in kwargs:
|
||||||
# QQ群组消息
|
# QQ群组消息
|
||||||
# qq群组消息需要自行上传,暂时不处理
|
if 'file_image' in kwargs and kwargs['file_image']:
|
||||||
if 'file_image' in kwargs:
|
|
||||||
file_image_path = kwargs['file_image'].replace("file:///", "")
|
file_image_path = kwargs['file_image'].replace("file:///", "")
|
||||||
if file_image_path:
|
if file_image_path:
|
||||||
|
|
||||||
@@ -317,48 +332,61 @@ class QQOfficial(Platform):
|
|||||||
logger.debug(f"上传图片: {file_image_path}")
|
logger.debug(f"上传图片: {file_image_path}")
|
||||||
image_url = await self.context.image_uploader.upload_image(file_image_path)
|
image_url = await self.context.image_uploader.upload_image(file_image_path)
|
||||||
logger.debug(f"上传成功: {image_url}")
|
logger.debug(f"上传成功: {image_url}")
|
||||||
media = await self.client.api.post_group_file(kwargs['group_openid'], 1, image_url)
|
if 'group_openid' in kwargs:
|
||||||
|
media = await self.client.api.post_group_file(kwargs['group_openid'], 1, image_url)
|
||||||
|
elif 'openid' in kwargs:
|
||||||
|
media = await self.client.api.post_c2c_file(kwargs['openid'], 1, image_url)
|
||||||
del kwargs['file_image']
|
del kwargs['file_image']
|
||||||
kwargs['media'] = media
|
kwargs['media'] = media
|
||||||
logger.debug(f"发送群图片: {media}")
|
logger.debug(f"发送群图片: {media}")
|
||||||
kwargs['msg_type'] = 7 # 富媒体
|
kwargs['msg_type'] = 7 # 富媒体
|
||||||
await self.client.api.post_group_message(**kwargs)
|
if self.test_mode:
|
||||||
|
return kwargs
|
||||||
|
if 'group_openid' in kwargs:
|
||||||
|
await self.client.api.post_group_message(**kwargs)
|
||||||
|
elif 'openid' in kwargs:
|
||||||
|
await self.client.api.post_c2c_message(**kwargs)
|
||||||
elif 'channel_id' in kwargs:
|
elif 'channel_id' in kwargs:
|
||||||
# 频道消息
|
# 频道消息
|
||||||
if 'file_image' in kwargs:
|
if 'file_image' in kwargs and kwargs['file_image']:
|
||||||
kwargs['file_image'] = kwargs['file_image'].replace("file:///", "")
|
kwargs['file_image'] = kwargs['file_image'].replace("file:///", "")
|
||||||
# 频道消息发图只支持本地
|
# 频道消息发图只支持本地
|
||||||
if kwargs['file_image'].startswith("http"):
|
if kwargs['file_image'].startswith("http"):
|
||||||
kwargs['file_image'] = await download_image_by_url(kwargs['file_image'])
|
kwargs['file_image'] = await download_image_by_url(kwargs['file_image'])
|
||||||
|
if self.test_mode:
|
||||||
|
return kwargs
|
||||||
await self.client.api.post_message(**kwargs)
|
await self.client.api.post_message(**kwargs)
|
||||||
else:
|
elif 'guild_id' in kwargs:
|
||||||
# 频道私聊消息
|
# 频道私聊消息
|
||||||
if 'file_image' in kwargs:
|
if 'file_image' in kwargs and kwargs['file_image']:
|
||||||
kwargs['file_image'] = kwargs['file_image'].replace("file:///", "")
|
kwargs['file_image'] = kwargs['file_image'].replace("file:///", "")
|
||||||
if kwargs['file_image'].startswith("http"):
|
if kwargs['file_image'].startswith("http"):
|
||||||
kwargs['file_image'] = await download_image_by_url(kwargs['file_image'])
|
kwargs['file_image'] = await download_image_by_url(kwargs['file_image'])
|
||||||
|
if self.test_mode:
|
||||||
|
return kwargs
|
||||||
await self.client.api.post_dms(**kwargs)
|
await self.client.api.post_dms(**kwargs)
|
||||||
|
else:
|
||||||
|
raise ValueError("Unknown target type.")
|
||||||
|
|
||||||
async def send_msg(self, target: Dict[str, str], result_message: Union[List[BaseMessageComponent], str]):
|
async def send_msg(self, target: Dict[str, str], result_message: CommandResult):
|
||||||
'''
|
'''
|
||||||
以主动的方式给用户、群或者频道发送一条消息。
|
以主动的方式给频道用户、群、频道或者消息列表用户(QQ用户)发送一条消息。
|
||||||
|
|
||||||
`target` 接收一个 dict 类型的值引用。
|
`target` 接收一个 dict 类型的值引用。
|
||||||
|
|
||||||
- 如果目标是 QQ 群,请添加 key `group_openid`。
|
- 如果目标是 QQ 群,请添加 key `group_openid`。
|
||||||
- 如果目标是 频道消息,请添加 key `channel_id`。
|
- 如果目标是 频道消息,请添加 key `channel_id`。
|
||||||
- 如果目标是 频道私聊,请添加 key `guild_id`。
|
- 如果目标是 频道私聊,请添加 key `guild_id`。
|
||||||
|
- 如果目标是 QQ 用户,请添加 key `openid`。
|
||||||
'''
|
'''
|
||||||
if isinstance(result_message, list):
|
plain_text, image_path = await self._parse_to_qqofficial(result_message.message_chain)
|
||||||
plain_text, image_path = await self._parse_to_qqofficial(result_message)
|
|
||||||
else:
|
|
||||||
plain_text = result_message
|
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
'content': plain_text,
|
'content': plain_text,
|
||||||
'file_image': image_path,
|
|
||||||
**target
|
**target
|
||||||
}
|
}
|
||||||
|
if image_path:
|
||||||
|
payload['file_image'] = image_path
|
||||||
await self._reply(**payload)
|
await self._reply(**payload)
|
||||||
|
|
||||||
def wait_for_message(self, channel_id: int) -> AstrBotMessage:
|
def wait_for_message(self, channel_id: int) -> AstrBotMessage:
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
VERSION = '3.3.2'
|
VERSION = '3.3.4'
|
||||||
@@ -9,3 +9,4 @@ from model.platform import Platform
|
|||||||
|
|
||||||
from model.platform.qq_nakuru import QQGOCQ
|
from model.platform.qq_nakuru import QQGOCQ
|
||||||
from model.platform.qq_official import QQOfficial
|
from model.platform.qq_official import QQOfficial
|
||||||
|
from model.platform.qq_aiocqhttp import AIOCQHTTP
|
||||||
Reference in New Issue
Block a user