Add 电子吧唧
All checks were successful
Build and Deploy Backend / build-and-deploy (push) Successful in 7m15s
All checks were successful
Build and Deploy Backend / build-and-deploy (push) Successful in 7m15s
This commit is contained in:
parent
51a673e814
commit
fe0dcb78c3
0
apps/badge/__init__.py
Normal file
0
apps/badge/__init__.py
Normal file
10
apps/badge/admin.py
Normal file
10
apps/badge/admin.py
Normal file
@ -0,0 +1,10 @@
|
||||
from django.contrib import admin
|
||||
from .models import BadgeImage
|
||||
|
||||
|
||||
@admin.register(BadgeImage)
|
||||
class BadgeImageAdmin(admin.ModelAdmin):
|
||||
list_display = ['id', 'user', 'source', 'style', 'generation_status', 'created_at']
|
||||
list_filter = ['source', 'generation_status', 'style']
|
||||
search_fields = ['prompt', 'user__nickname']
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
7
apps/badge/apps.py
Normal file
7
apps/badge/apps.py
Normal file
@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class BadgeConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.badge'
|
||||
verbose_name = '电子吧唧'
|
||||
35
apps/badge/migrations/0001_initial.py
Normal file
35
apps/badge/migrations/0001_initial.py
Normal file
@ -0,0 +1,35 @@
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BadgeImage',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('prompt', models.TextField(blank=True, default='')),
|
||||
('style', models.CharField(blank=True, default='', max_length=32)),
|
||||
('source', models.CharField(choices=[('t2i', '文生图'), ('i2i', '图生图'), ('upload', '用户上传')], default='t2i', max_length=10)),
|
||||
('image_url', models.URLField(blank=True, default='', max_length=500)),
|
||||
('reference_image_url', models.URLField(blank=True, default='', max_length=500)),
|
||||
('strength', models.FloatField(default=0.7)),
|
||||
('generation_status', models.CharField(choices=[('pending', '待生成'), ('generating', '生成中'), ('completed', '已完成'), ('failed', '失败')], default='pending', max_length=20)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='badge_images', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'db_table': 'badge_image',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
]
|
||||
0
apps/badge/migrations/__init__.py
Normal file
0
apps/badge/migrations/__init__.py
Normal file
42
apps/badge/models.py
Normal file
42
apps/badge/models.py
Normal file
@ -0,0 +1,42 @@
|
||||
"""
|
||||
电子吧唧 - 数据模型
|
||||
"""
|
||||
from django.db import models
|
||||
from apps.users.models import User
|
||||
|
||||
|
||||
class BadgeImage(models.Model):
|
||||
"""吧唧图片记录"""
|
||||
|
||||
SOURCE_CHOICES = [
|
||||
('t2i', '文生图'),
|
||||
('i2i', '图生图'),
|
||||
('upload', '用户上传'),
|
||||
]
|
||||
|
||||
STATUS_CHOICES = [
|
||||
('pending', '待生成'),
|
||||
('generating', '生成中'),
|
||||
('completed', '已完成'),
|
||||
('failed', '失败'),
|
||||
]
|
||||
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='badge_images')
|
||||
prompt = models.TextField(blank=True, default='')
|
||||
style = models.CharField(max_length=32, blank=True, default='')
|
||||
source = models.CharField(max_length=10, choices=SOURCE_CHOICES, default='t2i')
|
||||
image_url = models.URLField(max_length=500, blank=True, default='')
|
||||
reference_image_url = models.URLField(max_length=500, blank=True, default='')
|
||||
strength = models.FloatField(default=0.7)
|
||||
generation_status = models.CharField(
|
||||
max_length=20, choices=STATUS_CHOICES, default='pending'
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'badge_image'
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f'BadgeImage#{self.id} [{self.source}] {self.prompt[:30]}'
|
||||
31
apps/badge/serializers.py
Normal file
31
apps/badge/serializers.py
Normal file
@ -0,0 +1,31 @@
|
||||
"""
|
||||
电子吧唧 - 序列化器
|
||||
"""
|
||||
from rest_framework import serializers
|
||||
from .models import BadgeImage
|
||||
|
||||
VALID_STYLES = [
|
||||
'anime', 'realistic', 'pixel', 'watercolor',
|
||||
'cyberpunk', 'cute', 'ink', 'comic',
|
||||
]
|
||||
|
||||
|
||||
class BadgeImageSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = BadgeImage
|
||||
fields = [
|
||||
'id', 'prompt', 'style', 'source', 'image_url',
|
||||
'generation_status', 'created_at',
|
||||
]
|
||||
|
||||
|
||||
class Text2ImageSerializer(serializers.Serializer):
|
||||
prompt = serializers.CharField(max_length=500)
|
||||
style = serializers.ChoiceField(choices=VALID_STYLES, required=False)
|
||||
|
||||
|
||||
class Image2ImageSerializer(serializers.Serializer):
|
||||
image = serializers.ImageField()
|
||||
prompt = serializers.CharField(max_length=500, required=False, allow_blank=True, default='')
|
||||
style = serializers.ChoiceField(choices=VALID_STYLES, required=False)
|
||||
strength = serializers.FloatField(default=0.7, min_value=0.1, max_value=1.0, required=False)
|
||||
0
apps/badge/services/__init__.py
Normal file
0
apps/badge/services/__init__.py
Normal file
244
apps/badge/services/badge_image_service.py
Normal file
244
apps/badge/services/badge_image_service.py
Normal file
@ -0,0 +1,244 @@
|
||||
"""
|
||||
电子吧唧 - AI 图片生成服务
|
||||
使用火山引擎豆包 Seedream 文生图模型,与故事封面生成共用同一模型。
|
||||
"""
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
import requests as req_lib
|
||||
from django.conf import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 风格 → 提示词后缀映射
|
||||
STYLE_PROMPT_MAP = {
|
||||
'anime': '日系动漫风格,精致细腻,色彩鲜明',
|
||||
'realistic': '超写实风格,高清摄影质感,细节丰富',
|
||||
'pixel': '像素艺术风格,复古游戏画面,8-bit色彩',
|
||||
'watercolor': '水彩画风格,淡雅柔和,笔触自然晕染',
|
||||
'cyberpunk': '赛博朋克风格,霓虹灯光,暗色调科幻感',
|
||||
'cute': '可爱卡通风格,Q版萌系,圆润造型,柔和配色',
|
||||
'ink': '中国水墨画风格,黑白灰韵,留白意境',
|
||||
'comic': '漫画风格,粗线条,强对比,夸张表现力',
|
||||
}
|
||||
|
||||
|
||||
def sse_event(data: dict) -> str:
|
||||
"""格式化 SSE data 行"""
|
||||
return f"data: {json.dumps(data, ensure_ascii=False)}\n\n"
|
||||
|
||||
|
||||
def generate_t2i_stream(user, badge_image, prompt, style=None, width=1920, height=1920):
|
||||
"""
|
||||
文生图 SSE 流。
|
||||
使用豆包 Seedream 模型生成正方形图片,上传 OSS,返回 URL。
|
||||
"""
|
||||
config = settings.LLM_CONFIG
|
||||
|
||||
if not config.get('API_KEY'):
|
||||
badge_image.generation_status = 'failed'
|
||||
badge_image.save(update_fields=['generation_status'])
|
||||
yield sse_event({'stage': 'error', 'message': 'AI 服务未配置'})
|
||||
return
|
||||
|
||||
try:
|
||||
from volcenginesdkarkruntime import Ark
|
||||
except ImportError:
|
||||
badge_image.generation_status = 'failed'
|
||||
badge_image.save(update_fields=['generation_status'])
|
||||
yield sse_event({'stage': 'error', 'message': 'AI SDK 未安装'})
|
||||
return
|
||||
|
||||
# ── Stage 1: 生成中 ──
|
||||
yield sse_event({
|
||||
'stage': 'generating', 'progress': 20,
|
||||
'message': '正在生成图片...',
|
||||
})
|
||||
|
||||
try:
|
||||
client = Ark(api_key=config['API_KEY'])
|
||||
|
||||
# 构建提示词
|
||||
full_prompt = _build_prompt(prompt, style)
|
||||
image_model = config.get('IMAGE_MODEL_NAME', 'doubao-seedream-4-5-251128')
|
||||
image_size = f'{width}x{height}'
|
||||
|
||||
result = client.images.generate(
|
||||
model=image_model,
|
||||
prompt=full_prompt,
|
||||
size=image_size,
|
||||
response_format='url',
|
||||
watermark=False,
|
||||
)
|
||||
|
||||
temp_url = result.data[0].url
|
||||
|
||||
# ── Stage 2: 处理中 ──
|
||||
yield sse_event({
|
||||
'stage': 'processing', 'progress': 70,
|
||||
'message': '正在处理图片...',
|
||||
})
|
||||
|
||||
# 下载临时图片并上传到 OSS
|
||||
image_url = _download_and_upload(temp_url)
|
||||
|
||||
# 更新记录
|
||||
badge_image.image_url = image_url
|
||||
badge_image.generation_status = 'completed'
|
||||
badge_image.save(update_fields=['image_url', 'generation_status'])
|
||||
|
||||
# ── Stage 3: 完成 ──
|
||||
yield sse_event({
|
||||
'stage': 'done', 'progress': 100,
|
||||
'message': '生成完成!',
|
||||
'image_url': image_url,
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Badge t2i generation failed: {e}')
|
||||
badge_image.generation_status = 'failed'
|
||||
badge_image.save(update_fields=['generation_status'])
|
||||
yield sse_event({
|
||||
'stage': 'error', 'progress': 0,
|
||||
'message': f'生成失败: {str(e)}',
|
||||
})
|
||||
|
||||
|
||||
def generate_i2i_stream(user, badge_image, image_bytes, prompt='', style=None,
|
||||
strength=0.7, width=1920, height=1920):
|
||||
"""
|
||||
图生图 SSE 流。
|
||||
将用户上传的参考图 + 提示词发给豆包模型,生成正方形新图。
|
||||
"""
|
||||
config = settings.LLM_CONFIG
|
||||
|
||||
if not config.get('API_KEY'):
|
||||
badge_image.generation_status = 'failed'
|
||||
badge_image.save(update_fields=['generation_status'])
|
||||
yield sse_event({'stage': 'error', 'message': 'AI 服务未配置'})
|
||||
return
|
||||
|
||||
try:
|
||||
from volcenginesdkarkruntime import Ark
|
||||
except ImportError:
|
||||
badge_image.generation_status = 'failed'
|
||||
badge_image.save(update_fields=['generation_status'])
|
||||
yield sse_event({'stage': 'error', 'message': 'AI SDK 未安装'})
|
||||
return
|
||||
|
||||
# ── Stage 1: 生成中 ──
|
||||
yield sse_event({
|
||||
'stage': 'generating', 'progress': 20,
|
||||
'message': '正在根据参考图生成...',
|
||||
})
|
||||
|
||||
try:
|
||||
client = Ark(api_key=config['API_KEY'])
|
||||
|
||||
# 先上传参考图到 OSS 获取 URL
|
||||
ref_url = _upload_reference_image(image_bytes)
|
||||
badge_image.reference_image_url = ref_url
|
||||
badge_image.save(update_fields=['reference_image_url'])
|
||||
|
||||
# 构建提示词
|
||||
full_prompt = _build_prompt(prompt or '基于参考图生成类似风格的图片', style)
|
||||
image_model = config.get('IMAGE_MODEL_NAME', 'doubao-seedream-4-5-251128')
|
||||
image_size = f'{width}x{height}'
|
||||
|
||||
# 尝试带参考图的生成,若 SDK 不支持则回退到纯文生图
|
||||
try:
|
||||
result = client.images.generate(
|
||||
model=image_model,
|
||||
prompt=full_prompt,
|
||||
size=image_size,
|
||||
response_format='url',
|
||||
watermark=False,
|
||||
reference_image=ref_url,
|
||||
reference_strength=strength,
|
||||
)
|
||||
except TypeError:
|
||||
logger.warning('Seedream SDK does not support reference_image, falling back to t2i')
|
||||
result = client.images.generate(
|
||||
model=image_model,
|
||||
prompt=full_prompt,
|
||||
size=image_size,
|
||||
response_format='url',
|
||||
watermark=False,
|
||||
)
|
||||
|
||||
temp_url = result.data[0].url
|
||||
|
||||
# ── Stage 2: 处理中 ──
|
||||
yield sse_event({
|
||||
'stage': 'processing', 'progress': 70,
|
||||
'message': '正在处理图片...',
|
||||
})
|
||||
|
||||
image_url = _download_and_upload(temp_url)
|
||||
|
||||
badge_image.image_url = image_url
|
||||
badge_image.generation_status = 'completed'
|
||||
badge_image.save(update_fields=['image_url', 'generation_status'])
|
||||
|
||||
# ── Stage 3: 完成 ──
|
||||
yield sse_event({
|
||||
'stage': 'done', 'progress': 100,
|
||||
'message': '生成完成!',
|
||||
'image_url': image_url,
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Badge i2i generation failed: {e}')
|
||||
badge_image.generation_status = 'failed'
|
||||
badge_image.save(update_fields=['generation_status'])
|
||||
yield sse_event({
|
||||
'stage': 'error', 'progress': 0,
|
||||
'message': f'生成失败: {str(e)}',
|
||||
})
|
||||
|
||||
|
||||
def _build_prompt(prompt, style=None):
|
||||
"""构建完整提示词:用户描述 + 风格后缀 + 正方形构图提示"""
|
||||
parts = [prompt]
|
||||
if style and style in STYLE_PROMPT_MAP:
|
||||
parts.append(STYLE_PROMPT_MAP[style])
|
||||
parts.append('正方形构图,居中主体,适合圆形裁切展示')
|
||||
return ','.join(parts)
|
||||
|
||||
|
||||
def _download_and_upload(temp_url):
|
||||
"""从临时 URL 下载图片,上传到 OSS,返回持久化 URL"""
|
||||
resp = req_lib.get(temp_url, timeout=60)
|
||||
resp.raise_for_status()
|
||||
|
||||
from utils.oss import get_oss_client
|
||||
oss_client = get_oss_client()
|
||||
key = f"badge/generated/{datetime.now().strftime('%Y%m%d')}/{uuid.uuid4().hex}.jpg"
|
||||
oss_client.bucket.put_object(
|
||||
key, resp.content,
|
||||
headers={'Content-Type': 'image/jpeg'},
|
||||
)
|
||||
|
||||
oss_config = settings.ALIYUN_OSS
|
||||
if oss_config.get('CUSTOM_DOMAIN'):
|
||||
return f"https://{oss_config['CUSTOM_DOMAIN']}/{key}"
|
||||
return f"https://{oss_config['BUCKET_NAME']}.{oss_config['ENDPOINT']}/{key}"
|
||||
|
||||
|
||||
def _upload_reference_image(image_bytes):
|
||||
"""上传参考图到 OSS,返回 URL"""
|
||||
from utils.oss import get_oss_client
|
||||
oss_client = get_oss_client()
|
||||
key = f"badge/reference/{datetime.now().strftime('%Y%m%d')}/{uuid.uuid4().hex}.jpg"
|
||||
oss_client.bucket.put_object(
|
||||
key, image_bytes,
|
||||
headers={'Content-Type': 'image/jpeg'},
|
||||
)
|
||||
|
||||
oss_config = settings.ALIYUN_OSS
|
||||
if oss_config.get('CUSTOM_DOMAIN'):
|
||||
return f"https://{oss_config['CUSTOM_DOMAIN']}/{key}"
|
||||
return f"https://{oss_config['BUCKET_NAME']}.{oss_config['ENDPOINT']}/{key}"
|
||||
13
apps/badge/urls.py
Normal file
13
apps/badge/urls.py
Normal file
@ -0,0 +1,13 @@
|
||||
"""
|
||||
电子吧唧模块URL配置
|
||||
"""
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import BadgeViewSet
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register('', BadgeViewSet, basename='badge')
|
||||
|
||||
urlpatterns = [
|
||||
path('badge/', include(router.urls)),
|
||||
]
|
||||
108
apps/badge/views.py
Normal file
108
apps/badge/views.py
Normal file
@ -0,0 +1,108 @@
|
||||
"""
|
||||
电子吧唧 - 视图(App端)
|
||||
"""
|
||||
from django.http import StreamingHttpResponse
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from drf_spectacular.utils import extend_schema
|
||||
|
||||
from utils.response import success, error
|
||||
from apps.admins.authentication import AppJWTAuthentication
|
||||
from .models import BadgeImage
|
||||
from .serializers import BadgeImageSerializer, Text2ImageSerializer, Image2ImageSerializer
|
||||
|
||||
|
||||
@extend_schema(tags=['电子吧唧'])
|
||||
class BadgeViewSet(viewsets.ViewSet):
|
||||
"""电子吧唧视图集(App端)"""
|
||||
|
||||
authentication_classes = [AppJWTAuthentication]
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@action(detail=False, methods=['get'], url_path='history')
|
||||
def history(self, request):
|
||||
"""
|
||||
获取生成历史
|
||||
GET /api/v1/badge/history/
|
||||
"""
|
||||
images = BadgeImage.objects.filter(user=request.user).order_by('-created_at')[:50]
|
||||
serializer = BadgeImageSerializer(images, many=True)
|
||||
return success(data={'images': serializer.data})
|
||||
|
||||
@action(detail=False, methods=['post'], url_path='generate/t2i')
|
||||
def generate_t2i(self, request):
|
||||
"""
|
||||
文生图 (SSE 流式)
|
||||
POST /api/v1/badge/generate/t2i/
|
||||
"""
|
||||
serializer = Text2ImageSerializer(data=request.data)
|
||||
if not serializer.is_valid():
|
||||
return error(message=str(serializer.errors))
|
||||
|
||||
data = serializer.validated_data
|
||||
user = request.user
|
||||
|
||||
badge_image = BadgeImage.objects.create(
|
||||
user=user,
|
||||
prompt=data['prompt'],
|
||||
style=data.get('style', ''),
|
||||
source='t2i',
|
||||
generation_status='generating',
|
||||
)
|
||||
|
||||
from .services.badge_image_service import generate_t2i_stream
|
||||
|
||||
response = StreamingHttpResponse(
|
||||
generate_t2i_stream(
|
||||
user=user,
|
||||
badge_image=badge_image,
|
||||
prompt=data['prompt'],
|
||||
style=data.get('style'),
|
||||
),
|
||||
content_type='text/event-stream',
|
||||
)
|
||||
response['Cache-Control'] = 'no-cache'
|
||||
response['X-Accel-Buffering'] = 'no'
|
||||
return response
|
||||
|
||||
@action(detail=False, methods=['post'], url_path='generate/i2i')
|
||||
def generate_i2i(self, request):
|
||||
"""
|
||||
图生图 (SSE 流式)
|
||||
POST /api/v1/badge/generate/i2i/
|
||||
"""
|
||||
serializer = Image2ImageSerializer(data=request.data, context={'request': request})
|
||||
if not serializer.is_valid():
|
||||
return error(message=str(serializer.errors))
|
||||
|
||||
data = serializer.validated_data
|
||||
user = request.user
|
||||
image_file = data['image']
|
||||
image_bytes = image_file.read()
|
||||
|
||||
badge_image = BadgeImage.objects.create(
|
||||
user=user,
|
||||
prompt=data.get('prompt', ''),
|
||||
style=data.get('style', ''),
|
||||
source='i2i',
|
||||
strength=data.get('strength', 0.7),
|
||||
generation_status='generating',
|
||||
)
|
||||
|
||||
from .services.badge_image_service import generate_i2i_stream
|
||||
|
||||
response = StreamingHttpResponse(
|
||||
generate_i2i_stream(
|
||||
user=user,
|
||||
badge_image=badge_image,
|
||||
image_bytes=image_bytes,
|
||||
prompt=data.get('prompt', ''),
|
||||
style=data.get('style'),
|
||||
strength=data.get('strength', 0.7),
|
||||
),
|
||||
content_type='text/event-stream',
|
||||
)
|
||||
response['Cache-Control'] = 'no-cache'
|
||||
response['X-Accel-Buffering'] = 'no'
|
||||
return response
|
||||
@ -43,6 +43,7 @@ INSTALLED_APPS = [
|
||||
'apps.admins',
|
||||
'apps.stories',
|
||||
'apps.music',
|
||||
'apps.badge',
|
||||
'apps.notifications',
|
||||
'apps.system',
|
||||
]
|
||||
@ -220,6 +221,7 @@ SPECTACULAR_SETTINGS = {
|
||||
{'name': '智能体', 'description': 'AI 智能体 CRUD 和绑定'},
|
||||
{'name': '故事', 'description': '故事列表、书架管理、生成'},
|
||||
{'name': '音乐', 'description': '音乐播放列表、收藏、生成'},
|
||||
{'name': '电子吧唧', 'description': '电子吧唧 AI 生图、图片管理'},
|
||||
{'name': '通知', 'description': '通知列表、已读、删除'},
|
||||
{'name': '系统', 'description': '意见反馈、版本检查'},
|
||||
{'name': '管理员-认证', 'description': '管理员登录和个人信息'},
|
||||
|
||||
@ -21,6 +21,7 @@ app_api_patterns = [
|
||||
path('', include('apps.devices.urls')),
|
||||
path('', include('apps.stories.urls')),
|
||||
path('', include('apps.music.urls')),
|
||||
path('', include('apps.badge.urls')),
|
||||
path('', include('apps.notifications.urls')),
|
||||
path('', include('apps.system.urls')),
|
||||
]
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# 故事音频预转码方案 — MP3 → Opus 预处理
|
||||
|
||||
> 创建时间:2026-03-03
|
||||
> 状态:待实施
|
||||
> 创建时间:2026-03-07
|
||||
> 状态:已实施
|
||||
|
||||
## Context
|
||||
|
||||
|
||||
@ -133,6 +133,10 @@ class ErrorCode:
|
||||
MUSIC_GENERATING = 702
|
||||
MUSIC_DEFAULT_UNDELETABLE = 703
|
||||
|
||||
# 吧唧模块 750-799
|
||||
BADGE_IMAGE_NOT_FOUND = 750
|
||||
BADGE_GENERATE_FAILED = 751
|
||||
|
||||
# 通知模块 800-899
|
||||
NOTIFICATION_NOT_FOUND = 800
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user