Update files
This commit is contained in:
parent
e9afda5e16
commit
af063ecc7d
|
|
@ -0,0 +1,199 @@
|
|||
const axios = require('axios');
|
||||
const config = require('../config');
|
||||
const { log, logError } = require('../middleware/logger');
|
||||
const User = require('../models/User');
|
||||
|
||||
const BOT_TOKEN = config.telegramBotToken;
|
||||
const TELEGRAM_API = BOT_TOKEN ? `https://api.telegram.org/bot${BOT_TOKEN}` : null;
|
||||
|
||||
let isPolling = false;
|
||||
let offset = 0;
|
||||
|
||||
const sendMessage = async (chatId, text, options = {}) => {
|
||||
if (!TELEGRAM_API) {
|
||||
log('warn', 'TELEGRAM_BOT_TOKEN не установлен, отправка сообщения невозможна');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post(`${TELEGRAM_API}/sendMessage`, {
|
||||
chat_id: chatId,
|
||||
text,
|
||||
parse_mode: 'HTML',
|
||||
...options
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
logError('Ошибка отправки сообщения', error, { chatId, text: text.substring(0, 50) });
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const sendMessageToAllUsers = async (messageText) => {
|
||||
if (!TELEGRAM_API) {
|
||||
throw new Error('TELEGRAM_BOT_TOKEN не установлен');
|
||||
}
|
||||
|
||||
try {
|
||||
const users = await User.find({ banned: { $ne: true } }).select('telegramId');
|
||||
let sent = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (const user of users) {
|
||||
try {
|
||||
await sendMessage(user.telegramId, messageText);
|
||||
sent++;
|
||||
// Небольшая задержка, чтобы не превысить лимиты API
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
} catch (error) {
|
||||
failed++;
|
||||
logError('Ошибка отправки сообщения пользователю', error, { telegramId: user.telegramId });
|
||||
}
|
||||
}
|
||||
|
||||
return { sent, failed, total: users.length };
|
||||
} catch (error) {
|
||||
logError('Ошибка массовой отправки сообщений', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const getStartMessage = () => {
|
||||
return `👋 <b>Добро пожаловать в Nakama!</b>
|
||||
|
||||
📱 <b>Nakama</b> — социальная сеть для фурри и аниме сообщества.
|
||||
|
||||
<b>Основные возможности:</b>
|
||||
• Создание постов с текстом и изображениями
|
||||
• Поиск контента через e621 и Gelbooru
|
||||
• Комментарии и лайки
|
||||
• Подписки на пользователей
|
||||
• Система уведомлений
|
||||
• Фильтры и теги
|
||||
|
||||
<b>Как начать:</b>
|
||||
1. Нажмите кнопку "Войти" ниже, чтобы запустить приложение
|
||||
2. Создайте свой первый пост
|
||||
3. Подписывайтесь на интересных пользователей
|
||||
|
||||
<b>Поддержка:</b>
|
||||
Если возникли проблемы, напишите @NakamaReportbot
|
||||
|
||||
Приятного использования!`;
|
||||
};
|
||||
|
||||
const handleCommand = async (message) => {
|
||||
const chatId = message.chat.id;
|
||||
const text = (message.text || '').trim();
|
||||
const args = text.split(/\s+/);
|
||||
const command = args[0].toLowerCase();
|
||||
|
||||
if (command === '/start') {
|
||||
const startParam = message.text.split(' ')[1] || '';
|
||||
|
||||
// Если есть start_param (например, post_12345 или ref_ABC123)
|
||||
// Это обрабатывается при открытии миниаппа, здесь просто показываем инструкцию
|
||||
const startMessage = getStartMessage();
|
||||
|
||||
// Добавить кнопку для открытия миниаппа
|
||||
let botUsername = 'NakamaSpaceBot';
|
||||
if (config.telegramBotToken) {
|
||||
try {
|
||||
const botInfo = await axios.get(`${TELEGRAM_API}/getMe`);
|
||||
botUsername = botInfo.data.result.username || 'NakamaSpaceBot';
|
||||
} catch (error) {
|
||||
log('warn', 'Не удалось получить имя бота', { error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
await sendMessage(chatId, startMessage, {
|
||||
reply_markup: {
|
||||
inline_keyboard: [[
|
||||
{
|
||||
text: '🚀 Открыть Nakama',
|
||||
web_app: {
|
||||
url: `https://t.me/${botUsername}`
|
||||
}
|
||||
}
|
||||
]]
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Игнорируем неизвестные команды
|
||||
};
|
||||
|
||||
const processUpdate = async (update) => {
|
||||
const message = update.message || update.edited_message;
|
||||
if (!message || !message.text) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await handleCommand(message);
|
||||
} catch (error) {
|
||||
logError('Ошибка обработки команды основного бота', error, {
|
||||
chatId: message.chat.id,
|
||||
text: message.text?.substring(0, 50)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const pollUpdates = async () => {
|
||||
if (!TELEGRAM_API) {
|
||||
log('warn', 'Основной бот не запущен: TELEGRAM_BOT_TOKEN не установлен');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPolling) {
|
||||
return;
|
||||
}
|
||||
|
||||
isPolling = true;
|
||||
log('info', 'Основной бот запущен, опрос обновлений...');
|
||||
|
||||
const poll = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${TELEGRAM_API}/getUpdates`, {
|
||||
params: {
|
||||
offset,
|
||||
timeout: 30,
|
||||
allowed_updates: ['message']
|
||||
}
|
||||
});
|
||||
|
||||
const updates = response.data.result || [];
|
||||
|
||||
for (const update of updates) {
|
||||
offset = update.update_id + 1;
|
||||
await processUpdate(update);
|
||||
}
|
||||
|
||||
// Продолжить опрос
|
||||
setTimeout(poll, 1000);
|
||||
} catch (error) {
|
||||
logError('Ошибка опроса Telegram для основного бота', error);
|
||||
// Переподключиться через 5 секунд
|
||||
setTimeout(poll, 5000);
|
||||
}
|
||||
};
|
||||
|
||||
poll();
|
||||
};
|
||||
|
||||
const startMainBot = () => {
|
||||
if (!BOT_TOKEN) {
|
||||
log('warn', 'Основной бот не запущен: TELEGRAM_BOT_TOKEN не установлен');
|
||||
return;
|
||||
}
|
||||
|
||||
pollUpdates();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
startMainBot,
|
||||
sendMessageToAllUsers,
|
||||
sendMessage
|
||||
};
|
||||
|
||||
|
|
@ -159,6 +159,7 @@ const authenticate = async (req, res, next) => {
|
|||
}
|
||||
|
||||
const telegramUser = payload.user;
|
||||
const startParam = payload.start_param || payload.startParam;
|
||||
|
||||
if (!validateTelegramId(telegramUser.id)) {
|
||||
logSecurityEvent('INVALID_TELEGRAM_ID', req, { telegramId: telegramUser.id });
|
||||
|
|
@ -171,14 +172,28 @@ const authenticate = async (req, res, next) => {
|
|||
let user = await User.findOne({ telegramId: normalizedUser.id.toString() });
|
||||
|
||||
if (!user) {
|
||||
// Обработка реферального кода из start_param
|
||||
let referredBy = null;
|
||||
if (startParam && startParam.startsWith('ref_')) {
|
||||
const referralCode = startParam;
|
||||
const referrer = await User.findOne({ referralCode });
|
||||
if (referrer) {
|
||||
referredBy = referrer._id;
|
||||
}
|
||||
}
|
||||
|
||||
user = new User({
|
||||
telegramId: normalizedUser.id.toString(),
|
||||
username: normalizedUser.username || normalizedUser.firstName || 'user',
|
||||
firstName: normalizedUser.firstName,
|
||||
lastName: normalizedUser.lastName,
|
||||
photoUrl: normalizedUser.photoUrl
|
||||
photoUrl: normalizedUser.photoUrl,
|
||||
referredBy: referredBy
|
||||
});
|
||||
await user.save();
|
||||
|
||||
// Счетчик рефералов увеличивается только когда пользователь создаст первый пост
|
||||
// (см. routes/posts.js)
|
||||
} else {
|
||||
// Обновлять только если есть новые данные, не перезаписывать существующие пустыми значениями
|
||||
if (normalizedUser.username) {
|
||||
|
|
|
|||
|
|
@ -55,11 +55,35 @@ const UserSchema = new mongoose.Schema({
|
|||
default: false
|
||||
},
|
||||
bannedUntil: Date,
|
||||
// Реферальная система
|
||||
referralCode: {
|
||||
type: String,
|
||||
unique: true,
|
||||
sparse: true
|
||||
},
|
||||
referredBy: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User'
|
||||
},
|
||||
referralsCount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
});
|
||||
|
||||
// Генерировать реферальный код перед сохранением
|
||||
UserSchema.pre('save', async function(next) {
|
||||
if (!this.referralCode) {
|
||||
// Генерировать уникальный код на основе telegramId
|
||||
const code = `ref_${this.telegramId.slice(-8)}${Math.random().toString(36).substring(2, 6)}`.toUpperCase();
|
||||
this.referralCode = code;
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
module.exports = mongoose.model('User', UserSchema);
|
||||
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@ const respondWithUser = async (user, res) => {
|
|||
role: populatedUser.role,
|
||||
followersCount: populatedUser.followers.length,
|
||||
followingCount: populatedUser.following.length,
|
||||
referralCode: populatedUser.referralCode,
|
||||
referralsCount: populatedUser.referralsCount || 0,
|
||||
settings,
|
||||
banned: populatedUser.banned
|
||||
}
|
||||
|
|
@ -232,6 +234,8 @@ router.post('/oauth', strictAuthLimiter, async (req, res) => {
|
|||
role: populatedUser.role,
|
||||
followersCount: populatedUser.followers.length,
|
||||
followingCount: populatedUser.following.length,
|
||||
referralCode: populatedUser.referralCode,
|
||||
referralsCount: populatedUser.referralsCount || 0,
|
||||
settings,
|
||||
banned: populatedUser.banned
|
||||
}
|
||||
|
|
@ -277,6 +281,8 @@ router.post('/session', async (req, res) => {
|
|||
{ path: 'following', select: 'username firstName lastName photoUrl' }
|
||||
]);
|
||||
|
||||
const settings = normalizeUserSettings(populatedUser.settings);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
user: {
|
||||
|
|
@ -290,6 +296,8 @@ router.post('/session', async (req, res) => {
|
|||
role: populatedUser.role,
|
||||
followersCount: populatedUser.followers.length,
|
||||
followingCount: populatedUser.following.length,
|
||||
referralCode: populatedUser.referralCode,
|
||||
referralsCount: populatedUser.referralsCount || 0,
|
||||
settings,
|
||||
banned: populatedUser.banned
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,5 +57,36 @@ router.post('/send-photos', authenticate, async (req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
// Отправить сообщение всем пользователям (только админы)
|
||||
router.post('/broadcast', authenticate, async (req, res) => {
|
||||
try {
|
||||
// Проверка прав админа
|
||||
if (req.user.role !== 'admin') {
|
||||
return res.status(403).json({ error: 'Требуются права администратора' });
|
||||
}
|
||||
|
||||
const { message } = req.body;
|
||||
|
||||
if (!message || !message.trim()) {
|
||||
return res.status(400).json({ error: 'Сообщение обязательно' });
|
||||
}
|
||||
|
||||
const { sendMessageToAllUsers } = require('../bots/mainBot');
|
||||
const result = await sendMessageToAllUsers(message);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Сообщение отправлено ${result.sent} пользователям`,
|
||||
result
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Ошибка рассылки:', error);
|
||||
res.status(500).json({
|
||||
error: 'Ошибка отправки сообщений',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
|
|
|
|||
|
|
@ -58,7 +58,8 @@ const serializeUser = (user) => ({
|
|||
banned: user.banned,
|
||||
bannedUntil: user.bannedUntil,
|
||||
lastActiveAt: user.lastActiveAt,
|
||||
createdAt: user.createdAt
|
||||
createdAt: user.createdAt,
|
||||
referralsCount: user.referralsCount || 0
|
||||
});
|
||||
|
||||
router.post('/auth/verify', authenticateModeration, requireModerationAccess, async (req, res) => {
|
||||
|
|
|
|||
|
|
@ -153,6 +153,17 @@ router.post('/', authenticate, strictPostLimiter, postCreationLimiter, fileUploa
|
|||
await post.save();
|
||||
await post.populate('author', 'username firstName lastName photoUrl');
|
||||
|
||||
// Проверка первого поста для реферальной системы
|
||||
// Счетчик рефералов увеличивается только когда приглашенный пользователь создал первый пост
|
||||
const userPostsCount = await Post.countDocuments({ author: req.user._id });
|
||||
if (userPostsCount === 1 && req.user.referredBy) {
|
||||
// Это первый пост пользователя, который был приглашен по реферальной ссылке
|
||||
const User = require('../models/User');
|
||||
await User.findByIdAndUpdate(req.user.referredBy, {
|
||||
$inc: { referralsCount: 1 }
|
||||
});
|
||||
}
|
||||
|
||||
// Создать уведомления для упомянутых пользователей
|
||||
if (post.mentionedUsers.length > 0) {
|
||||
const notifications = post.mentionedUsers.map(userId => ({
|
||||
|
|
|
|||
|
|
@ -262,6 +262,8 @@ initWebSocket(server);
|
|||
// Автообновление аватарок отключено - обновление происходит только при перезаходе
|
||||
// scheduleAvatarUpdates();
|
||||
startServerMonitorBot();
|
||||
const { startMainBot } = require('./bots/mainBot');
|
||||
startMainBot();
|
||||
|
||||
// Обработка необработанных ошибок
|
||||
process.on('uncaughtException', (error) => {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Heart, MessageCircle, MoreVertical, ChevronLeft, ChevronRight, Download, Send, X, ZoomIn } from 'lucide-react'
|
||||
import { Heart, MessageCircle, MoreVertical, ChevronLeft, ChevronRight, Download, Send, X, ZoomIn, Share2 } from 'lucide-react'
|
||||
import { likePost, deletePost, sendPhotoToTelegram } from '../utils/api'
|
||||
import { hapticFeedback, showConfirm } from '../utils/telegram'
|
||||
import { hapticFeedback, showConfirm, openTelegramLink } from '../utils/telegram'
|
||||
import './PostCard.css'
|
||||
|
||||
const TAG_COLORS = {
|
||||
|
|
@ -114,6 +114,27 @@ export default function PostCard({ post, currentUser, onUpdate }) {
|
|||
}
|
||||
}
|
||||
|
||||
const handleRepost = () => {
|
||||
try {
|
||||
hapticFeedback('light')
|
||||
|
||||
// Получить имя бота из переменных окружения или использовать дефолтное
|
||||
const botName = import.meta.env.VITE_TELEGRAM_BOT_NAME || 'NakamaSpaceBot'
|
||||
|
||||
// Создать deeplink для открытия поста в миниапп
|
||||
const deeplink = `https://t.me/${botName}?startapp=post_${post._id}`
|
||||
|
||||
// Открыть нативное окно "Поделиться" в Telegram
|
||||
const shareUrl = `https://t.me/share/url?url=${encodeURIComponent(deeplink)}&text=${encodeURIComponent('Смотри пост в Nakama!')}`
|
||||
|
||||
openTelegramLink(shareUrl)
|
||||
hapticFeedback('success')
|
||||
} catch (error) {
|
||||
console.error('Ошибка репоста:', error)
|
||||
hapticFeedback('error')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="post-card card fade-in">
|
||||
{/* Хедер поста */}
|
||||
|
|
@ -236,6 +257,17 @@ export default function PostCard({ post, currentUser, onUpdate }) {
|
|||
<span>{post.comments.length}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="action-btn"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleRepost()
|
||||
}}
|
||||
title="Поделиться постом"
|
||||
>
|
||||
<Share2 size={20} stroke="currentColor" />
|
||||
</button>
|
||||
|
||||
{images.length > 0 && (
|
||||
<button
|
||||
className="action-btn"
|
||||
|
|
|
|||
|
|
@ -182,6 +182,96 @@
|
|||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* Реферальная карточка */
|
||||
.referral-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.referral-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.referral-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 12px;
|
||||
background: rgba(52, 199, 89, 0.12);
|
||||
color: #34C759;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.referral-text h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.referral-text p {
|
||||
margin: 4px 0 8px;
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.referral-stats {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.referral-stats strong {
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.referral-link-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.referral-link {
|
||||
padding: 12px;
|
||||
background: var(--bg-primary);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--divider-color);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.referral-link code {
|
||||
font-size: 12px;
|
||||
color: var(--text-primary);
|
||||
font-family: 'SF Mono', 'Monaco', 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.referral-copy-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 10px 18px;
|
||||
border-radius: 12px;
|
||||
background: var(--button-accent);
|
||||
color: white;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.referral-copy-btn:active {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.search-switch {
|
||||
display: flex;
|
||||
background: var(--bg-primary);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useState } from 'react'
|
||||
import { Settings, Heart, Edit2, Shield } from 'lucide-react'
|
||||
import { Settings, Heart, Edit2, Shield, Copy, Users } from 'lucide-react'
|
||||
import { updateProfile } from '../utils/api'
|
||||
import { hapticFeedback } from '../utils/telegram'
|
||||
import { hapticFeedback, showAlert } from '../utils/telegram'
|
||||
import ThemeToggle from '../components/ThemeToggle'
|
||||
import './Profile.css'
|
||||
|
||||
|
|
@ -181,6 +181,62 @@ export default function Profile({ user, setUser }) {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
{/* Реферальная ссылка */}
|
||||
{user.referralCode && (
|
||||
<div className="referral-card card">
|
||||
<div className="referral-content">
|
||||
<div className="referral-icon">
|
||||
<Users size={20} />
|
||||
</div>
|
||||
<div className="referral-text">
|
||||
<h3>Пригласи друзей</h3>
|
||||
<p>Получи +1 к счетчику, когда приглашенный создаст первый пост</p>
|
||||
<div className="referral-stats">
|
||||
Приглашено: <strong>{user.referralsCount || 0}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="referral-link-section">
|
||||
<div className="referral-link">
|
||||
<code>{`https://t.me/${import.meta.env.VITE_TELEGRAM_BOT_NAME || 'NakamaSpaceBot'}?startapp=${user.referralCode}`}</code>
|
||||
</div>
|
||||
<button
|
||||
className="referral-copy-btn"
|
||||
onClick={async () => {
|
||||
try {
|
||||
hapticFeedback('light')
|
||||
const botName = import.meta.env.VITE_TELEGRAM_BOT_NAME || 'NakamaSpaceBot'
|
||||
const referralLink = `https://t.me/${botName}?startapp=${user.referralCode}`
|
||||
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
await navigator.clipboard.writeText(referralLink)
|
||||
hapticFeedback('success')
|
||||
showAlert('✅ Ссылка скопирована!')
|
||||
} else {
|
||||
const textArea = document.createElement('textarea')
|
||||
textArea.value = referralLink
|
||||
textArea.style.position = 'fixed'
|
||||
textArea.style.opacity = '0'
|
||||
document.body.appendChild(textArea)
|
||||
textArea.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(textArea)
|
||||
hapticFeedback('success')
|
||||
showAlert('✅ Ссылка скопирована!')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Ошибка копирования:', error)
|
||||
hapticFeedback('error')
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Copy size={18} />
|
||||
<span>Копировать</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="profile-powered">
|
||||
Powered by glpshcn \\ RBach \\ E621 \\ GelBooru
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -496,6 +496,7 @@ export default function App() {
|
|||
<div className="list-item-meta">
|
||||
<span>Роль: {u.role}</span>
|
||||
<span>Активность: {formatDate(u.lastActiveAt)}</span>
|
||||
{u.referralsCount > 0 && <span className="badge badge-info">Рефералов: {u.referralsCount}</span>}
|
||||
{u.banned && <span className="badge badge-danger">Бан до {formatDate(u.bannedUntil)}</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue