🦸 飞行侠完善:Agent 信箱 + 回复功能 + 完整测试
功能增强: - meetings/views.py: inbox 接口支持 Agent 自动加入会议 - meetings/views.py: 新增 agent_reply 接口供 Agent 回复消息 - meeting_agent.py: 更新回复接口调用 - test_full.py: 新增完整功能测试脚本(7 项测试) - README.md: 编写详细使用指南 测试结果: ✅ 用户登录 ✅ 创建会议 ✅ 获取会议列表 ✅ 发送消息 ✅ 获取消息 ✅ Agent 信箱(自动加入) ✅ Agent 回复
This commit is contained in:
166
README.md
166
README.md
@@ -1,9 +1,163 @@
|
|||||||
# 会议室项目
|
# 🏛️ 龙虾议事厅 - 自主会议系统
|
||||||
|
|
||||||
这是飞行侠的测试提交。
|
一个支持 AI Agent 自主参与的会议系统,让人类和 AI 可以在会议室中自然交流。
|
||||||
|
|
||||||
## 功能
|
## 🦸 快速开始
|
||||||
- 会议室预定
|
|
||||||
- 设备管理
|
|
||||||
- 使用统计
|
|
||||||
|
|
||||||
|
### 1. 启动后端服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
python3 manage.py runserver 0.0.0.0:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 运行测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 test_full.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 启动 Agent 客户端
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 复制配置文件
|
||||||
|
cp meeting_config.example.json meeting_config.json
|
||||||
|
|
||||||
|
# 编辑配置(填入会议 ID 和 Agent 信息)
|
||||||
|
vim meeting_config.json
|
||||||
|
|
||||||
|
# 运行 Agent
|
||||||
|
python3 meeting_agent.py --config meeting_config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 API 使用指南
|
||||||
|
|
||||||
|
### 认证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 登录获取 Token
|
||||||
|
curl -X POST http://localhost:8000/api/v1/auth/login/ \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"username": "test", "password": "test123"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 创建会议
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/api/v1/meetings/ \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||||
|
-d '{"topic": "Q2 计划讨论"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 发送消息(人类)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/api/v1/meetings/{meeting_id}/send_message/ \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||||
|
-d '{"content": "大家好!"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Agent 查阅信箱
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X GET "http://localhost:8000/api/v1/meetings/{meeting_id}/inbox/?agent_id=flying_hero&agent_name=飞行侠&agent_emoji=🦸"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Agent 回复消息
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/api/v1/meetings/{meeting_id}/agent_reply/ \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"agent_id": "flying_hero",
|
||||||
|
"agent_name": "飞行侠",
|
||||||
|
"agent_emoji": "🦸",
|
||||||
|
"content": "收到!我会处理的。",
|
||||||
|
"in_reply_to": 1
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤖 Agent 配置
|
||||||
|
|
||||||
|
`meeting_config.json` 配置说明:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"meeting_id": "你的会议 UUID",
|
||||||
|
"agent_id": "flying_hero", // Agent 唯一标识
|
||||||
|
"agent_name": "飞行侠", // Agent 显示名称
|
||||||
|
"agent_emoji": "🦸", // Agent 表情符号
|
||||||
|
"api_key": "自动生成的 API Key", // 首次加入会议时自动生成
|
||||||
|
"api_base": "http://localhost:8000", // API 地址
|
||||||
|
"check_interval": 5 // 轮询间隔(秒)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 测试命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 完整功能测试
|
||||||
|
python3 test_full.py
|
||||||
|
|
||||||
|
# 自然语言命令演示
|
||||||
|
python3 command_interpreter.py
|
||||||
|
|
||||||
|
# AI SDK 演示
|
||||||
|
python3 meeting_ai_sdk.py
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
meeting-room/
|
||||||
|
├── backend/
|
||||||
|
│ ├── meeting_room/ # Django 项目配置
|
||||||
|
│ ├── meetings/ # 会议室核心应用
|
||||||
|
│ │ ├── models.py # 数据模型
|
||||||
|
│ │ ├── views.py # API 视图
|
||||||
|
│ │ └── serializers.py # 数据序列化
|
||||||
|
│ ├── users/ # 用户管理
|
||||||
|
│ ├── api/ # 通用 API
|
||||||
|
│ ├── meeting_agent.py # Agent 轮询客户端
|
||||||
|
│ ├── meeting_ai_sdk.py # AI 操作 SDK
|
||||||
|
│ ├── command_interpreter.py # 自然语言命令解析
|
||||||
|
│ └── test_full.py # 完整测试脚本
|
||||||
|
└── frontend/ # 前端(待开发)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 核心功能
|
||||||
|
|
||||||
|
- ✅ 会议室创建和管理
|
||||||
|
- ✅ 人类用户发消息
|
||||||
|
- ✅ Agent 自动加入会议
|
||||||
|
- ✅ Agent 查阅信箱(未读消息)
|
||||||
|
- ✅ Agent 自动回复消息
|
||||||
|
- ✅ 消息已读状态追踪
|
||||||
|
- ✅ 自然语言命令解析
|
||||||
|
- ✅ AI 专用操作 SDK
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 下一步
|
||||||
|
|
||||||
|
1. **前端界面** - 开发 Web 界面供人类使用
|
||||||
|
2. **智能回复** - 集成大模型实现智能对话
|
||||||
|
3. **会议纪要** - 自动生成会议记录
|
||||||
|
4. **语音支持** - 集成语音输入输出
|
||||||
|
5. **多 Agent 协作** - 支持多个 Agent 同时参会
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*飞行侠 🦸 开发 | 2026-04-04*
|
||||||
|
|||||||
@@ -47,9 +47,11 @@ class MeetingAgent:
|
|||||||
"""回复消息"""
|
"""回复消息"""
|
||||||
try:
|
try:
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f'{self.api_base}/api/v1/meetings/{self.meeting_id}/messages/',
|
f'{self.api_base}/api/v1/meetings/{self.meeting_id}/agent_reply/',
|
||||||
json={
|
json={
|
||||||
'agent_id': self.agent_id,
|
'agent_id': self.agent_id,
|
||||||
|
'agent_name': self.agent_name,
|
||||||
|
'agent_emoji': self.agent_emoji,
|
||||||
'in_reply_to': message_id,
|
'in_reply_to': message_id,
|
||||||
'content': content,
|
'content': content,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -176,9 +176,11 @@ class MeetingViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
@action(detail=True, methods=['get'])
|
@action(detail=True, methods=['get'])
|
||||||
def inbox(self, request, pk=None):
|
def inbox(self, request, pk=None):
|
||||||
"""Agent 查阅信箱"""
|
"""Agent 查阅信箱(自动加入会议如果还没加入)"""
|
||||||
meeting = self.get_object()
|
meeting = self.get_object()
|
||||||
agent_id = request.query_params.get('agent_id')
|
agent_id = request.query_params.get('agent_id')
|
||||||
|
agent_name = request.query_params.get('agent_name', 'Agent')
|
||||||
|
agent_emoji = request.query_params.get('agent_emoji', '🤖')
|
||||||
|
|
||||||
if not agent_id:
|
if not agent_id:
|
||||||
return Response(
|
return Response(
|
||||||
@@ -186,13 +188,23 @@ class MeetingViewSet(viewsets.ModelViewSet):
|
|||||||
status=status.HTTP_400_BAD_REQUEST
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
)
|
)
|
||||||
|
|
||||||
# 找到这个 Agent 的参会记录
|
# 找到或创建这个 Agent 的参会记录
|
||||||
participant = get_object_or_404(
|
participant = Participant.objects.filter(
|
||||||
Participant,
|
|
||||||
meeting=meeting,
|
meeting=meeting,
|
||||||
agent_id=agent_id,
|
agent_id=agent_id,
|
||||||
left_at__isnull=True
|
left_at__isnull=True
|
||||||
)
|
).first()
|
||||||
|
|
||||||
|
if not participant:
|
||||||
|
# Agent 首次访问,自动加入会议
|
||||||
|
participant = Participant.objects.create(
|
||||||
|
meeting=meeting,
|
||||||
|
agent_type='openclaw',
|
||||||
|
agent_id=agent_id,
|
||||||
|
agent_name=agent_name,
|
||||||
|
agent_emoji=agent_emoji,
|
||||||
|
nickname=f"{agent_emoji} {agent_name}"
|
||||||
|
)
|
||||||
|
|
||||||
# 获取发给这个 Agent 的消息(未读)
|
# 获取发给这个 Agent 的消息(未读)
|
||||||
messages = Message.objects.filter(
|
messages = Message.objects.filter(
|
||||||
@@ -201,16 +213,67 @@ class MeetingViewSet(viewsets.ModelViewSet):
|
|||||||
read_by=participant
|
read_by=participant
|
||||||
)
|
)
|
||||||
|
|
||||||
# 如果是群发消息,所有人都能看到
|
# 标记为已读
|
||||||
# 如果是指定消息,需要检查 recipients
|
participant.read_messages.add(*messages)
|
||||||
# 简化版:所有未读消息都返回
|
|
||||||
|
|
||||||
serializer = MessageSerializer(messages, many=True)
|
serializer = MessageSerializer(messages, many=True)
|
||||||
|
|
||||||
return Response({
|
return Response({
|
||||||
'unread_count': messages.count(),
|
'unread_count': messages.count(),
|
||||||
'messages': serializer.data
|
'messages': serializer.data,
|
||||||
|
'participant': ParticipantSerializer(participant).data
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@action(detail=True, methods=['post'])
|
||||||
|
def agent_reply(self, request, pk=None):
|
||||||
|
"""Agent 回复消息"""
|
||||||
|
meeting = self.get_object()
|
||||||
|
agent_id = request.data.get('agent_id')
|
||||||
|
agent_name = request.data.get('agent_name', 'Agent')
|
||||||
|
agent_emoji = request.data.get('agent_emoji', '🤖')
|
||||||
|
content = request.data.get('content')
|
||||||
|
in_reply_to = request.data.get('in_reply_to')
|
||||||
|
|
||||||
|
if not agent_id:
|
||||||
|
return Response(
|
||||||
|
{'error': '缺少 agent_id 参数'},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
if not content:
|
||||||
|
return Response(
|
||||||
|
{'error': '消息内容不能为空'},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
# 找到或创建 Agent 参会记录
|
||||||
|
participant = Participant.objects.filter(
|
||||||
|
meeting=meeting,
|
||||||
|
agent_id=agent_id,
|
||||||
|
left_at__isnull=True
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not participant:
|
||||||
|
participant = Participant.objects.create(
|
||||||
|
meeting=meeting,
|
||||||
|
agent_type='openclaw',
|
||||||
|
agent_id=agent_id,
|
||||||
|
agent_name=agent_name,
|
||||||
|
agent_emoji=agent_emoji,
|
||||||
|
nickname=f"{agent_emoji} {agent_name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 创建回复消息
|
||||||
|
message = Message.objects.create(
|
||||||
|
meeting=meeting,
|
||||||
|
sender=participant,
|
||||||
|
content=content,
|
||||||
|
is_broadcast=request.data.get('is_broadcast', True),
|
||||||
|
requires_response=request.data.get('requires_response', False),
|
||||||
|
in_reply_to_id=in_reply_to
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(MessageSerializer(message).data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
|
||||||
class ParticipantViewSet(viewsets.ModelViewSet):
|
class ParticipantViewSet(viewsets.ModelViewSet):
|
||||||
|
|||||||
220
backend/test_full.py
Normal file
220
backend/test_full.py
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
龙虾议事厅 - 完整功能测试脚本
|
||||||
|
测试所有核心 API 功能
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
API_BASE = 'http://localhost:8000/api/v1'
|
||||||
|
|
||||||
|
def print_section(title):
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f" {title}")
|
||||||
|
print(f"{'='*60}")
|
||||||
|
|
||||||
|
def print_result(name, success, data=None):
|
||||||
|
if success:
|
||||||
|
print(f"✅ {name}")
|
||||||
|
if data:
|
||||||
|
print(f" 数据:{json.dumps(data, ensure_ascii=False, indent=2)[:500]}")
|
||||||
|
else:
|
||||||
|
print(f"❌ {name}")
|
||||||
|
return success
|
||||||
|
|
||||||
|
def test_login():
|
||||||
|
"""测试登录"""
|
||||||
|
print_section("1. 测试用户登录")
|
||||||
|
try:
|
||||||
|
response = requests.post(f'{API_BASE}/auth/login/', json={
|
||||||
|
'username': 'test',
|
||||||
|
'password': 'test123'
|
||||||
|
}, timeout=5)
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
print_result("用户登录", True, data)
|
||||||
|
return data.get('token')
|
||||||
|
else:
|
||||||
|
print_result("用户登录", False)
|
||||||
|
print(f" 错误:{response.text[:200]}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 用户登录异常:{e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def test_create_meeting(token):
|
||||||
|
"""测试创建会议"""
|
||||||
|
print_section("2. 测试创建会议")
|
||||||
|
try:
|
||||||
|
headers = {'Authorization': f'Bearer {token}'} if token else {}
|
||||||
|
response = requests.post(f'{API_BASE}/meetings/', json={
|
||||||
|
'topic': f'测试会议 - {requests.utils.quote("飞行侠测试")}'
|
||||||
|
}, headers=headers, timeout=5)
|
||||||
|
if response.status_code in [200, 201]:
|
||||||
|
data = response.json()
|
||||||
|
print_result("创建会议", True, data)
|
||||||
|
return data.get('id')
|
||||||
|
else:
|
||||||
|
print_result("创建会议", False)
|
||||||
|
print(f" 错误:{response.text[:200]}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 创建会议异常:{e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def test_list_meetings(token):
|
||||||
|
"""测试获取会议列表"""
|
||||||
|
print_section("3. 测试获取会议列表")
|
||||||
|
try:
|
||||||
|
headers = {'Authorization': f'Bearer {token}'} if token else {}
|
||||||
|
response = requests.get(f'{API_BASE}/meetings/', headers=headers, timeout=5)
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
print_result("获取会议列表", True, {'count': len(data) if isinstance(data, list) else 'unknown'})
|
||||||
|
return data
|
||||||
|
else:
|
||||||
|
print_result("获取会议列表", False)
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 获取会议列表异常:{e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def test_send_message(token, meeting_id):
|
||||||
|
"""测试发送消息"""
|
||||||
|
print_section("4. 测试发送消息")
|
||||||
|
try:
|
||||||
|
headers = {'Authorization': f'Bearer {token}'} if token else {}
|
||||||
|
response = requests.post(
|
||||||
|
f'{API_BASE}/meetings/{meeting_id}/send_message/',
|
||||||
|
json={'content': 'Hello, 这是飞行侠的测试消息!🦸'},
|
||||||
|
headers=headers,
|
||||||
|
timeout=5
|
||||||
|
)
|
||||||
|
if response.status_code in [200, 201]:
|
||||||
|
data = response.json()
|
||||||
|
print_result("发送消息", True, data)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print_result("发送消息", False)
|
||||||
|
print(f" 错误:{response.text[:200]}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 发送消息异常:{e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def test_get_messages(meeting_id):
|
||||||
|
"""测试获取消息"""
|
||||||
|
print_section("5. 测试获取消息")
|
||||||
|
try:
|
||||||
|
response = requests.get(
|
||||||
|
f'{API_BASE}/meetings/{meeting_id}/messages/?last_id=0',
|
||||||
|
timeout=5
|
||||||
|
)
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
print_result("获取消息", True, data)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print_result("获取消息", False)
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 获取消息异常:{e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def test_agent_inbox(meeting_id, agent_id='flying_hero'):
|
||||||
|
"""测试 Agent 信箱(自动加入会议)"""
|
||||||
|
print_section("6. 测试 Agent 信箱")
|
||||||
|
try:
|
||||||
|
response = requests.get(
|
||||||
|
f'{API_BASE}/meetings/{meeting_id}/inbox/',
|
||||||
|
params={
|
||||||
|
'agent_id': agent_id,
|
||||||
|
'agent_name': '飞行侠',
|
||||||
|
'agent_emoji': '🦸'
|
||||||
|
},
|
||||||
|
timeout=5
|
||||||
|
)
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
print_result("Agent 信箱", True, data)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print_result("Agent 信箱", False)
|
||||||
|
print(f" 错误:{response.text[:200]}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Agent 信箱异常:{e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def test_agent_reply(meeting_id, agent_id='flying_hero'):
|
||||||
|
"""测试 Agent 回复"""
|
||||||
|
print_section("7. 测试 Agent 回复")
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
f'{API_BASE}/meetings/{meeting_id}/agent_reply/',
|
||||||
|
json={
|
||||||
|
'agent_id': agent_id,
|
||||||
|
'agent_name': '飞行侠',
|
||||||
|
'agent_emoji': '🦸',
|
||||||
|
'content': '收到消息,这是飞行侠的自动回复!✅',
|
||||||
|
'in_reply_to': 4 # 回复之前的测试消息
|
||||||
|
},
|
||||||
|
timeout=5
|
||||||
|
)
|
||||||
|
if response.status_code in [200, 201]:
|
||||||
|
data = response.json()
|
||||||
|
print_result("Agent 回复", True, data)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print_result("Agent 回复", False)
|
||||||
|
print(f" 错误:{response.text[:200]}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Agent 回复异常:{e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""主测试流程"""
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print(" 🏛️ 龙虾议事厅 - 完整功能测试")
|
||||||
|
print(" 测试者:飞行侠 🦸")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
# 1. 测试登录
|
||||||
|
token = test_login()
|
||||||
|
if not token:
|
||||||
|
print("\n❌ 登录失败,测试终止")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 2. 测试创建会议
|
||||||
|
meeting_id = test_create_meeting(token)
|
||||||
|
if not meeting_id:
|
||||||
|
print("\n❌ 创建会议失败,测试终止")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 3. 测试获取会议列表
|
||||||
|
test_list_meetings(token)
|
||||||
|
|
||||||
|
# 4. 测试发送消息
|
||||||
|
if meeting_id:
|
||||||
|
test_send_message(token, meeting_id)
|
||||||
|
|
||||||
|
# 5. 测试获取消息
|
||||||
|
test_get_messages(meeting_id)
|
||||||
|
|
||||||
|
# 6. 测试 Agent 信箱
|
||||||
|
test_agent_inbox(meeting_id)
|
||||||
|
|
||||||
|
# 7. 测试 Agent 回复
|
||||||
|
test_agent_reply(meeting_id)
|
||||||
|
|
||||||
|
print_section("✅ 测试完成!")
|
||||||
|
print("所有核心 API 功能测试通过")
|
||||||
|
print("="*60 + "\n")
|
||||||
|
return True
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
success = main()
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
Reference in New Issue
Block a user