增加飞书模块
This commit is contained in:
parent
93a86d218f
commit
5eab14304c
@ -398,8 +398,8 @@ class CampaignViewSet(viewsets.ModelViewSet):
|
|||||||
"name": creator.name,
|
"name": creator.name,
|
||||||
"category": creator.category,
|
"category": creator.category,
|
||||||
"followers": f"{int(creator.followers / 1000)}k" if creator.followers else "0",
|
"followers": f"{int(creator.followers / 1000)}k" if creator.followers else "0",
|
||||||
"GMV Generated": f"${creator.gmv}k" if creator.gmv else "$0",
|
"GMV Achieved": f"${creator.gmv}k" if creator.gmv else "$0",
|
||||||
"Views Generated": f"{int(creator.avg_video_views / 1000)}k" if creator.avg_video_views else "0",
|
"Views Achieved": f"{int(creator.avg_video_views / 1000)}k" if creator.avg_video_views else "0",
|
||||||
"Pricing": f"${creator.pricing}" if creator.pricing else "$0",
|
"Pricing": f"${creator.pricing}" if creator.pricing else "$0",
|
||||||
"Status": cc.status
|
"Status": cc.status
|
||||||
}
|
}
|
||||||
@ -554,8 +554,8 @@ class CampaignViewSet(viewsets.ModelViewSet):
|
|||||||
"creator_name": creator.name,
|
"creator_name": creator.name,
|
||||||
"category": creator.category,
|
"category": creator.category,
|
||||||
"followers": f"{int(creator.followers / 1000)}k" if creator.followers else "0",
|
"followers": f"{int(creator.followers / 1000)}k" if creator.followers else "0",
|
||||||
"gmv_generated": f"${creator.gmv}k" if creator.gmv else "$0",
|
"gmv_achieved": f"${creator.gmv}k" if creator.gmv else "$0",
|
||||||
"views_generated": f"{int(creator.avg_video_views / 1000)}k" if creator.avg_video_views else "0",
|
"views_achieved": f"{int(creator.avg_video_views / 1000)}k" if creator.avg_video_views else "0",
|
||||||
"pricing": f"${creator.pricing}" if creator.pricing else "$0",
|
"pricing": f"${creator.pricing}" if creator.pricing else "$0",
|
||||||
"status": status
|
"status": status
|
||||||
}
|
}
|
||||||
@ -622,8 +622,8 @@ class CampaignViewSet(viewsets.ModelViewSet):
|
|||||||
"creator_name": creator.name,
|
"creator_name": creator.name,
|
||||||
"category": creator.category,
|
"category": creator.category,
|
||||||
"followers": f"{int(creator.followers / 1000)}k" if creator.followers else "0",
|
"followers": f"{int(creator.followers / 1000)}k" if creator.followers else "0",
|
||||||
"gmv_generated": f"${creator.gmv}k" if creator.gmv else "$0",
|
"gmv_achieved": f"${creator.gmv}k" if creator.gmv else "$0",
|
||||||
"views_generated": f"{int(creator.avg_video_views / 1000)}k" if creator.avg_video_views else "0",
|
"views_achieved": f"{int(creator.avg_video_views / 1000)}k" if creator.avg_video_views else "0",
|
||||||
"pricing": f"${creator.pricing}" if creator.pricing else "$0",
|
"pricing": f"${creator.pricing}" if creator.pricing else "$0",
|
||||||
"status": status
|
"status": status
|
||||||
}
|
}
|
||||||
@ -681,8 +681,8 @@ class CampaignViewSet(viewsets.ModelViewSet):
|
|||||||
"creator_name": creator.name,
|
"creator_name": creator.name,
|
||||||
"category": creator.category,
|
"category": creator.category,
|
||||||
"followers": f"{int(creator.followers / 1000)}k" if creator.followers else "0",
|
"followers": f"{int(creator.followers / 1000)}k" if creator.followers else "0",
|
||||||
"gmv_generated": f"${creator.gmv}k" if creator.gmv else "$0",
|
"gmv_achieved": f"${creator.gmv}k" if creator.gmv else "$0",
|
||||||
"views_generated": f"{int(creator.avg_video_views / 1000)}k" if creator.avg_video_views else "0",
|
"views_achieved": f"{int(creator.avg_video_views / 1000)}k" if creator.avg_video_views else "0",
|
||||||
"pricing": f"${creator.pricing}" if creator.pricing else "$0",
|
"pricing": f"${creator.pricing}" if creator.pricing else "$0",
|
||||||
"status": status
|
"status": status
|
||||||
}
|
}
|
||||||
|
@ -78,7 +78,7 @@ Authorization: Token <your_token>
|
|||||||
"avatar": "头像URL",
|
"avatar": "头像URL",
|
||||||
"email": "邮箱",
|
"email": "邮箱",
|
||||||
"social_user": {
|
"social_user": {
|
||||||
"instagram": "@username",
|
"instagram": "@name",
|
||||||
"tiktok": "tiktok链接"
|
"tiktok": "tiktok链接"
|
||||||
},
|
},
|
||||||
"location": "地理位置",
|
"location": "地理位置",
|
||||||
@ -127,7 +127,7 @@ Authorization: Token <your_token>
|
|||||||
{
|
{
|
||||||
"creator_id": 123, // 必填
|
"creator_id": 123, // 必填
|
||||||
"email": "新邮箱",
|
"email": "新邮箱",
|
||||||
"instagram": "@new_username",
|
"instagram": "@new_name",
|
||||||
"tiktok_link": "新TikTok链接",
|
"tiktok_link": "新TikTok链接",
|
||||||
"location": "新地理位置",
|
"location": "新地理位置",
|
||||||
"live_schedule": "新直播时间安排",
|
"live_schedule": "新直播时间安排",
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.1 on 2025-05-29 02:21
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('daren_detail', '0007_remove_creatorprofile_tiktok_link_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='creatorprofile',
|
||||||
|
old_name='homepage_link',
|
||||||
|
new_name='tiktok_link',
|
||||||
|
),
|
||||||
|
]
|
@ -147,7 +147,7 @@ Authorization: Token your_token_here
|
|||||||
"items_sold": 500,
|
"items_sold": 500,
|
||||||
"avg_video_views": 25000,
|
"avg_video_views": 25000,
|
||||||
"has_ecommerce": true,
|
"has_ecommerce": true,
|
||||||
"tiktok_url": "https://tiktok.com/@username",
|
"tiktok_url": "https://tiktok.com/@name",
|
||||||
"hashtags": "#sport#fitness#outdoor",
|
"hashtags": "#sport#fitness#outdoor",
|
||||||
"trends": "summer fitness",
|
"trends": "summer fitness",
|
||||||
"profile": "tiktok"
|
"profile": "tiktok"
|
||||||
@ -227,7 +227,7 @@ Authorization: Token your_token_here
|
|||||||
"items_sold": 500,
|
"items_sold": 500,
|
||||||
"avg_video_views": 25000,
|
"avg_video_views": 25000,
|
||||||
"has_ecommerce": true,
|
"has_ecommerce": true,
|
||||||
"tiktok_url": "https://tiktok.com/@username",
|
"tiktok_url": "https://tiktok.com/@name",
|
||||||
"hashtags": "#sport#fitness#outdoor",
|
"hashtags": "#sport#fitness#outdoor",
|
||||||
"trends": "summer fitness",
|
"trends": "summer fitness",
|
||||||
"profile": "tiktok"
|
"profile": "tiktok"
|
||||||
@ -341,7 +341,7 @@ Authorization: Token your_token_here
|
|||||||
"items_sold": 500,
|
"items_sold": 500,
|
||||||
"avg_video_views": 25000,
|
"avg_video_views": 25000,
|
||||||
"has_ecommerce": true,
|
"has_ecommerce": true,
|
||||||
"tiktok_url": "https://tiktok.com/@username",
|
"tiktok_url": "https://tiktok.com/@name",
|
||||||
"hashtags": "#sport#fitness#outdoor",
|
"hashtags": "#sport#fitness#outdoor",
|
||||||
"trends": "summer fitness",
|
"trends": "summer fitness",
|
||||||
"profile": "tiktok"
|
"profile": "tiktok"
|
||||||
@ -375,7 +375,7 @@ Authorization: Token your_token_here
|
|||||||
"items_sold": 500,
|
"items_sold": 500,
|
||||||
"avg_video_views": 25000,
|
"avg_video_views": 25000,
|
||||||
"has_ecommerce": true,
|
"has_ecommerce": true,
|
||||||
"tiktok_url": "https://tiktok.com/@username",
|
"tiktok_url": "https://tiktok.com/@name",
|
||||||
"hashtags": "#sport#fitness#outdoor",
|
"hashtags": "#sport#fitness#outdoor",
|
||||||
"trends": "summer fitness",
|
"trends": "summer fitness",
|
||||||
"profile": "tiktok"
|
"profile": "tiktok"
|
||||||
@ -428,7 +428,7 @@ Authorization: Token your_token_here
|
|||||||
"items_sold": 500,
|
"items_sold": 500,
|
||||||
"avg_video_views": 25000,
|
"avg_video_views": 25000,
|
||||||
"has_ecommerce": true,
|
"has_ecommerce": true,
|
||||||
"tiktok_url": "https://tiktok.com/@username",
|
"tiktok_url": "https://tiktok.com/@name",
|
||||||
"hashtags": "#sport#fitness#outdoor",
|
"hashtags": "#sport#fitness#outdoor",
|
||||||
"trends": "summer fitness",
|
"trends": "summer fitness",
|
||||||
"profile": "tiktok"
|
"profile": "tiktok"
|
||||||
@ -479,7 +479,7 @@ Authorization: Token your_token_here
|
|||||||
"items_sold": 500,
|
"items_sold": 500,
|
||||||
"avg_video_views": 25000,
|
"avg_video_views": 25000,
|
||||||
"has_ecommerce": true,
|
"has_ecommerce": true,
|
||||||
"tiktok_url": "https://tiktok.com/@username",
|
"tiktok_url": "https://tiktok.com/@name",
|
||||||
"hashtags": "#sport#fitness#outdoor",
|
"hashtags": "#sport#fitness#outdoor",
|
||||||
"trends": "summer fitness",
|
"trends": "summer fitness",
|
||||||
"profile": "tiktok"
|
"profile": "tiktok"
|
||||||
@ -529,7 +529,7 @@ Authorization: Token your_token_here
|
|||||||
"items_sold": 800,
|
"items_sold": 800,
|
||||||
"avg_video_views": 45000,
|
"avg_video_views": 45000,
|
||||||
"has_ecommerce": true,
|
"has_ecommerce": true,
|
||||||
"tiktok_url": "https://tiktok.com/@username",
|
"tiktok_url": "https://tiktok.com/@name",
|
||||||
"hashtags": "#sport#fitness#outdoor#review",
|
"hashtags": "#sport#fitness#outdoor#review",
|
||||||
"trends": "fitness equipment review",
|
"trends": "fitness equipment review",
|
||||||
"profile": "tiktok"
|
"profile": "tiktok"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.2.1 on 2025-05-28 08:33
|
# Generated by Django 5.2 on 2025-05-07 03:43
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
@ -46,24 +46,7 @@ class Migration(migrations.Migration):
|
|||||||
options={
|
options={
|
||||||
'verbose_name': '创作者数据',
|
'verbose_name': '创作者数据',
|
||||||
'verbose_name_plural': '创作者数据',
|
'verbose_name_plural': '创作者数据',
|
||||||
},
|
'db_table': 'feishu_creators',
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='FeishuTableMapping',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
|
||||||
('app_token', models.CharField(max_length=100, verbose_name='应用令牌')),
|
|
||||||
('table_id', models.CharField(max_length=100, verbose_name='表格ID')),
|
|
||||||
('table_url', models.TextField(verbose_name='表格URL')),
|
|
||||||
('table_name', models.CharField(max_length=100, verbose_name='数据库表名')),
|
|
||||||
('feishu_table_name', models.CharField(blank=True, max_length=255, null=True, verbose_name='飞书表格名称')),
|
|
||||||
('last_sync_time', models.DateTimeField(auto_now=True, verbose_name='最后同步时间')),
|
|
||||||
('total_records', models.IntegerField(default=0, verbose_name='总记录数')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': '飞书表格映射',
|
|
||||||
'verbose_name_plural': '飞书表格映射',
|
|
||||||
'unique_together': {('app_token', 'table_id')},
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
39
apps/feishu/migrations/0002_feishuauth.py
Normal file
39
apps/feishu/migrations/0002_feishuauth.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# Generated by Django 5.2 on 2025-05-14 04:30
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('feishu', '0001_initial'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='FeishuAuth',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('open_id', models.CharField(blank=True, max_length=100, null=True, verbose_name='飞书Open ID')),
|
||||||
|
('union_id', models.CharField(blank=True, max_length=100, null=True, verbose_name='飞书Union ID')),
|
||||||
|
('access_token', models.CharField(max_length=512, verbose_name='访问令牌')),
|
||||||
|
('refresh_token', models.CharField(max_length=512, verbose_name='刷新令牌')),
|
||||||
|
('expires_at', models.DateTimeField(verbose_name='令牌过期时间')),
|
||||||
|
('is_active', models.BooleanField(default=True, verbose_name='是否有效')),
|
||||||
|
('scopes', models.JSONField(blank=True, default=list, verbose_name='授权范围')),
|
||||||
|
('metadata', models.JSONField(blank=True, default=dict, verbose_name='授权元数据')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feishu_auths', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '飞书授权',
|
||||||
|
'verbose_name_plural': '飞书授权',
|
||||||
|
'db_table': 'feishu_auths',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,35 @@
|
|||||||
|
# Generated by Django 5.2 on 2025-05-14 09:10
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('feishu', '0002_feishuauth'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='FeishuTableMapping',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||||
|
('app_token', models.CharField(max_length=100, verbose_name='应用令牌')),
|
||||||
|
('table_id', models.CharField(max_length=100, verbose_name='表格ID')),
|
||||||
|
('table_url', models.TextField(verbose_name='表格URL')),
|
||||||
|
('table_name', models.CharField(max_length=100, verbose_name='数据库表名')),
|
||||||
|
('feishu_table_name', models.CharField(blank=True, max_length=255, null=True, verbose_name='飞书表格名称')),
|
||||||
|
('last_sync_time', models.DateTimeField(auto_now=True, verbose_name='最后同步时间')),
|
||||||
|
('total_records', models.IntegerField(default=0, verbose_name='总记录数')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '飞书表格映射',
|
||||||
|
'verbose_name_plural': '飞书表格映射',
|
||||||
|
'db_table': 'feishu_table_mapping',
|
||||||
|
'unique_together': {('app_token', 'table_id')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='FeishuAuth',
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,21 @@
|
|||||||
|
# Generated by Django 5.2 on 2025-05-21 04:30
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('feishu', '0003_feishutablemapping_delete_feishuauth'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelTable(
|
||||||
|
name='feishucreator',
|
||||||
|
table=None,
|
||||||
|
),
|
||||||
|
migrations.AlterModelTable(
|
||||||
|
name='feishutablemapping',
|
||||||
|
table=None,
|
||||||
|
),
|
||||||
|
]
|
@ -14,13 +14,13 @@ from .services.data_sync_service import DataSyncService
|
|||||||
from .services.gmail_extraction_service import GmailExtractionService
|
from .services.gmail_extraction_service import GmailExtractionService
|
||||||
from .services.auto_gmail_conversation_service import AutoGmailConversationService
|
from .services.auto_gmail_conversation_service import AutoGmailConversationService
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from apps.gmail.models import GmailCredential, GmailConversation, AutoReplyConfig
|
from apps.gmail.models import GmailCredential, GmailConversation, AutoReplyConfig
|
||||||
from apps.gmail.services.gmail_service import GmailService
|
from apps.gmail.services.gmail_service import GmailService
|
||||||
from apps.gmail.serializers import AutoReplyConfigSerializer
|
from apps.gmail.serializers import AutoReplyConfigSerializer
|
||||||
from apps.gmail.services.goal_service import get_or_create_goal, get_conversation_summary
|
from apps.gmail.services.goal_service import get_or_create_goal, get_conversation_summary
|
||||||
from apps.chat.models import ChatHistory
|
from apps.chat.models import ChatHistory
|
||||||
from apps.knowledge_base.models import KnowledgeBase
|
from apps.knowledge_base.models import KnowledgeBase
|
||||||
|
from apps.user.authentication import CustomTokenAuthentication
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -383,6 +383,7 @@ class AutoGmailConversationView(APIView):
|
|||||||
自动Gmail对话API,支持自动发送消息并实时接收和回复达人消息
|
自动Gmail对话API,支持自动发送消息并实时接收和回复达人消息
|
||||||
"""
|
"""
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
authentication_classes = [CustomTokenAuthentication]
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -761,9 +761,9 @@ class GmailPubSubView(APIView):
|
|||||||
credentials = request.user.gmail_credentials.filter(is_valid=True)
|
credentials = request.user.gmail_credentials.filter(is_valid=True)
|
||||||
|
|
||||||
# 构建响应数据
|
# 构建响应数据
|
||||||
accounts = []
|
user = []
|
||||||
for cred in credentials:
|
for cred in credentials:
|
||||||
accounts.append({
|
user.append({
|
||||||
'id': cred.id,
|
'id': cred.id,
|
||||||
'email': cred.email,
|
'email': cred.email,
|
||||||
'is_default': cred.is_default
|
'is_default': cred.is_default
|
||||||
@ -772,7 +772,7 @@ class GmailPubSubView(APIView):
|
|||||||
return Response({
|
return Response({
|
||||||
'code': 200,
|
'code': 200,
|
||||||
'message': '获取账户列表成功',
|
'message': '获取账户列表成功',
|
||||||
'data': {'accounts': accounts}
|
'data': {'user': user}
|
||||||
}, status=status.HTTP_200_OK)
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
@ -915,9 +915,9 @@ class GmailSendEmailView(APIView):
|
|||||||
credentials = request.user.gmail_credentials.filter(is_valid=True)
|
credentials = request.user.gmail_credentials.filter(is_valid=True)
|
||||||
|
|
||||||
# 构建响应数据
|
# 构建响应数据
|
||||||
accounts = []
|
user = []
|
||||||
for cred in credentials:
|
for cred in credentials:
|
||||||
accounts.append({
|
user.append({
|
||||||
'id': cred.id,
|
'id': cred.id,
|
||||||
'email': cred.email,
|
'email': cred.email,
|
||||||
'is_default': cred.is_default
|
'is_default': cred.is_default
|
||||||
@ -926,7 +926,7 @@ class GmailSendEmailView(APIView):
|
|||||||
return Response({
|
return Response({
|
||||||
'code': 200,
|
'code': 200,
|
||||||
'message': '获取账户列表成功',
|
'message': '获取账户列表成功',
|
||||||
'data': {'accounts': accounts}
|
'data': {'user': user}
|
||||||
}, status=status.HTTP_200_OK)
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ def format_user_response(user, include_is_active=False):
|
|||||||
"""
|
"""
|
||||||
response = {
|
response = {
|
||||||
"id": str(user.id),
|
"id": str(user.id),
|
||||||
"username": user.name,
|
"name": user.name,
|
||||||
"email": user.email,
|
"email": user.email,
|
||||||
"name": user.name,
|
"name": user.name,
|
||||||
"role": user.role,
|
"role": user.role,
|
||||||
|
@ -78,7 +78,7 @@ Authorization: Token <your_token>
|
|||||||
"avatar": "头像URL",
|
"avatar": "头像URL",
|
||||||
"email": "邮箱",
|
"email": "邮箱",
|
||||||
"social_user": {
|
"social_user": {
|
||||||
"instagram": "@username",
|
"instagram": "@name",
|
||||||
"tiktok": "tiktok链接"
|
"tiktok": "tiktok链接"
|
||||||
},
|
},
|
||||||
"location": "地理位置",
|
"location": "地理位置",
|
||||||
@ -127,7 +127,7 @@ Authorization: Token <your_token>
|
|||||||
{
|
{
|
||||||
"creator_id": 123, // 必填
|
"creator_id": 123, // 必填
|
||||||
"email": "新邮箱",
|
"email": "新邮箱",
|
||||||
"instagram": "@new_username",
|
"instagram": "@new_name",
|
||||||
"tiktok_link": "新TikTok链接",
|
"tiktok_link": "新TikTok链接",
|
||||||
"location": "新地理位置",
|
"location": "新地理位置",
|
||||||
"live_schedule": "新直播时间安排",
|
"live_schedule": "新直播时间安排",
|
||||||
|
Loading…
Reference in New Issue
Block a user