Update files

This commit is contained in:
glpshchn 2025-12-01 17:26:18 +03:00
parent 01f1e1ae94
commit bc2d103e50
15 changed files with 128 additions and 34 deletions

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2025 NakamaSpace
Copyright (c) 2025 Nakama
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -35,11 +35,17 @@ const ensureUserSettings = async (user) => {
}
if (!user.settings.whitelist) {
user.settings.whitelist = { noNSFW: true };
updated = true;
} else if (user.settings.whitelist.noNSFW === undefined) {
user.settings.whitelist.noNSFW = true;
user.settings.whitelist = { noNSFW: true, noHomo: true };
updated = true;
} else {
if (user.settings.whitelist.noNSFW === undefined) {
user.settings.whitelist.noNSFW = true;
updated = true;
}
if (user.settings.whitelist.noHomo === undefined) {
user.settings.whitelist.noHomo = true;
updated = true;
}
}
if (updated) {

View File

@ -50,6 +50,12 @@ const PostSchema = new mongoose.Schema({
type: Boolean,
default: false
},
// Отдельный флаг для гомосексуального контента
// Может отсутствовать у старых постов, поэтому фильтры должны учитывать isHomo === true
isHomo: {
type: Boolean,
default: false
},
likes: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'User'

View File

@ -35,7 +35,10 @@ const UserSchema = new mongoose.Schema({
whitelist: {
noFurry: { type: Boolean, default: false },
onlyAnime: { type: Boolean, default: false },
noNSFW: { type: Boolean, default: false }
// Скрыть контент 18+
noNSFW: { type: Boolean, default: false },
// Скрыть гомосексуальный контент
noHomo: { type: Boolean, default: true }
},
searchPreference: {
type: String,

View File

@ -23,6 +23,7 @@ const normalizeUserSettings = (settings = {}) => {
...plainSettings,
whitelist: {
noNSFW: whitelist?.noNSFW ?? true,
noHomo: whitelist?.noHomo ?? true,
...whitelist
},
searchPreference: ALLOWED_SEARCH_PREFERENCES.includes(plainSettings.searchPreference)
@ -192,7 +193,7 @@ router.post('/oauth', strictAuthLimiter, async (req, res) => {
}
if (telegramUser.first_name) {
user.firstName = telegramUser.first_name;
user.firstName = telegramUser.first_name;
}
if (telegramUser.last_name !== undefined) {
@ -201,7 +202,7 @@ router.post('/oauth', strictAuthLimiter, async (req, res) => {
// Обновлять аватарку только если есть новая
if (telegramUser.photo_url) {
user.photoUrl = telegramUser.photo_url;
user.photoUrl = telegramUser.photo_url;
}
await user.save();

View File

@ -21,6 +21,9 @@ router.get('/', authenticate, searchLimiter, async (req, res) => {
if (req.user.settings.whitelist.noNSFW) {
searchQuery.isNSFW = false;
}
if (req.user.settings.whitelist.noHomo) {
searchQuery.isHomo = { $ne: true };
}
// Поиск по хэштегу
if (hashtag) {
@ -97,6 +100,9 @@ router.get('/hashtag/:tag', authenticate, async (req, res) => {
if (req.user.settings.whitelist.noNSFW) {
query.isNSFW = false;
}
if (req.user.settings.whitelist.noHomo) {
query.isHomo = { $ne: true };
}
const posts = await Post.find(query)
.populate('author', 'username firstName lastName photoUrl')

View File

@ -38,6 +38,11 @@ router.get('/', authenticate, async (req, res) => {
if (req.user.settings.whitelist.noNSFW) {
query.isNSFW = false;
}
if (req.user.settings.whitelist.noHomo) {
// Скрывать только посты, помеченные как гомосексуальные.
// Посты без флага (старые) остаются видимыми.
query.isHomo = { $ne: true };
}
let posts = await Post.find(query)
.populate('author', 'username firstName lastName photoUrl')
@ -67,7 +72,7 @@ router.get('/', authenticate, async (req, res) => {
// Создать пост
router.post('/', authenticate, strictPostLimiter, postCreationLimiter, fileUploadLimiter, uploadPostImages, async (req, res) => {
try {
const { content, tags, mentionedUsers, isNSFW, externalImages } = req.body;
const { content, tags, mentionedUsers, isNSFW, isHomo, externalImages } = req.body;
// Валидация контента
if (content && !validatePostContent(content)) {
@ -139,7 +144,10 @@ router.post('/', authenticate, strictPostLimiter, postCreationLimiter, fileUploa
tags: parsedTags,
hashtags,
mentionedUsers: mentionedUsers ? JSON.parse(mentionedUsers) : [],
isNSFW: isNSFW === 'true'
isNSFW: isNSFW === 'true',
// Флаг гомосексуального контента - полный аналог NSFW по логике,
// но управляется отдельно
isHomo: isHomo === 'true' || isHomo === true
});
await post.save();

View File

@ -91,8 +91,8 @@ router.get('/proxy/:encodedUrl', proxyLimiter, async (req, res) => {
// Если это e621, добавляем авторизацию (если есть учетные данные)
if (urlObj.hostname.includes('e621.net') && config.e621Username && config.e621ApiKey) {
try {
const auth = Buffer.from(`${config.e621Username}:${config.e621ApiKey}`).toString('base64');
headers['Authorization'] = `Basic ${auth}`;
const auth = Buffer.from(`${config.e621Username}:${config.e621ApiKey}`).toString('base64');
headers['Authorization'] = `Basic ${auth}`;
} catch (error) {
console.warn('⚠️ Ошибка создания Basic auth для e621:', error.message);
// Продолжаем без авторизации
@ -220,14 +220,14 @@ router.get('/furry', authenticate, async (req, res) => {
const posts = postsData
.filter(post => post && post.file && post.file.url) // Фильтруем посты без URL
.map(post => ({
id: post.id,
url: createProxyUrl(post.file.url),
id: post.id,
url: createProxyUrl(post.file.url),
preview: post.preview && post.preview.url ? createProxyUrl(post.preview.url) : null,
tags: post.tags && post.tags.general ? post.tags.general : [],
rating: post.rating || 'q',
score: post.score && post.score.total ? post.score.total : 0,
source: 'e621'
}));
source: 'e621'
}));
const payload = { posts };
setCache(cacheKey, payload);

View File

@ -46,15 +46,24 @@ router.get('/:id', authenticate, async (req, res) => {
router.get('/:id/posts', authenticate, async (req, res) => {
try {
const { page = 1, limit = 20 } = req.query;
const posts = await Post.find({ author: req.params.id })
const query = { author: req.params.id };
// Применить фильтры текущего пользователя
if (req.user.settings?.whitelist?.noNSFW) {
query.isNSFW = false;
}
if (req.user.settings?.whitelist?.noHomo) {
query.isHomo = { $ne: true };
}
const posts = await Post.find(query)
.populate('author', 'username firstName lastName photoUrl')
.sort({ createdAt: -1 })
.limit(limit * 1)
.skip((page - 1) * limit)
.exec();
const count = await Post.countDocuments({ author: req.params.id });
const count = await Post.countDocuments(query);
res.json({
posts,
@ -148,9 +157,14 @@ router.put('/profile', authenticate, async (req, res) => {
}
if (!req.user.settings.whitelist) {
req.user.settings.whitelist = { noNSFW: true };
} else if (req.user.settings.whitelist.noNSFW === undefined) {
req.user.settings.whitelist.noNSFW = true;
req.user.settings.whitelist = { noNSFW: true, noHomo: true };
} else {
if (req.user.settings.whitelist.noNSFW === undefined) {
req.user.settings.whitelist.noNSFW = true;
}
if (req.user.settings.whitelist.noHomo === undefined) {
req.user.settings.whitelist.noHomo = true;
}
}
req.user.markModified('settings');

View File

@ -17,6 +17,7 @@ export default function CreatePostModal({ user, onClose, onPostCreated, initialI
const [imagePreviews, setImagePreviews] = useState(initialImage ? [initialImage] : [])
const [externalImages, setExternalImages] = useState(initialImage ? [initialImage] : [])
const [isNSFW, setIsNSFW] = useState(false)
const [isHomo, setIsHomo] = useState(false)
const [loading, setLoading] = useState(false)
const [showUserSearch, setShowUserSearch] = useState(false)
const [userSearchQuery, setUserSearchQuery] = useState('')
@ -107,6 +108,7 @@ export default function CreatePostModal({ user, onClose, onPostCreated, initialI
formData.append('content', content)
formData.append('tags', JSON.stringify(selectedTags))
formData.append('isNSFW', isNSFW)
formData.append('isHomo', isHomo)
// Добавить загруженные файлы
images.forEach((image, index) => {
@ -223,6 +225,18 @@ export default function CreatePostModal({ user, onClose, onPostCreated, initialI
<span>Отметить как NSFW</span>
</label>
</div>
{/* Homo переключатель */}
<div className="nsfw-toggle">
<label>
<input
type="checkbox"
checked={isHomo}
onChange={e => setIsHomo(e.target.checked)}
/>
<span>Отметить как Homo</span>
</label>
</div>
</div>
{/* Футер с действиями */}

View File

@ -13,7 +13,9 @@ const normalizeSearchPreference = (value) =>
const DEFAULT_SETTINGS = {
whitelist: {
noNSFW: true
noNSFW: true,
// Скрыть гомосексуальный контент
noHomo: true
},
searchPreference: 'furry'
}
@ -209,6 +211,21 @@ export default function Profile({ user, setUser }) {
<span className="toggle-slider" />
</label>
</div>
<div className="setting-item card">
<div>
<div className="setting-name">Скрыть Homo</div>
<div className="setting-desc">Не показывать посты с гомосексуальным контентом</div>
</div>
<label className="toggle">
<input
type="checkbox"
checked={settings.whitelist.noHomo}
onChange={(e) => updateWhitelistSetting('noHomo', e.target.checked)}
/>
<span className="toggle-slider" />
</label>
</div>
</div>
{/* Модальное окно редактирования bio */}
@ -276,6 +293,21 @@ export default function Profile({ user, setUser }) {
<span className="toggle-slider" />
</label>
</div>
<div className="setting-row">
<div>
<div className="setting-name">Скрыть Homo</div>
<div className="setting-desc">Убрать гомосексуальный контент из ленты и поиска</div>
</div>
<label className="toggle">
<input
type="checkbox"
checked={settings.whitelist.noHomo}
onChange={(e) => updateWhitelistSetting('noHomo', e.target.checked)}
/>
<span className="toggle-slider" />
</label>
</div>
</div>
<div className="settings-section">

View File

@ -291,19 +291,23 @@ export default function App() {
}
const API_URL = import.meta.env.VITE_API_URL || (
import.meta.env.PROD ? window.location.origin : 'http://localhost:3000'
import.meta.env.PROD ? '/api' : 'http://localhost:3000/api'
);
// Для WebSocket убираем "/api" из base URL, т.к. socket.io слушает на корне
const socketBase = API_URL.replace(/\/?api\/?$/, '');
console.log('[Chat] Инициализация чата');
console.log('[Chat] WS base URL:', socketBase);
console.log('[Chat] User данные:', {
username: user.username,
telegramId: user.telegramId,
hasUsername: !!user.username,
hasTelegramId: !!user.telegramId
});
console.log('[Chat] Подключение к:', `${API_URL}/mod-chat`);
console.log('[Chat] Подключение к:', `${socketBase}/mod-chat`);
const socket = io(`${API_URL}/mod-chat`, {
const socket = io(`${socketBase}/mod-chat`, {
transports: ['websocket', 'polling'],
reconnection: true,
reconnectionDelay: 1000,
@ -677,16 +681,16 @@ export default function App() {
{report.post.author && (
<button className="btn warn" onClick={() => handleBanAuthor(report.post.id)}>
<Ban size={16} />
Забанить автора
Забанить автора (срок)
</button>
)}
</>
)}
<button className="btn" onClick={() => handleReportStatus(report.id, 'resolved')}>
Решено
Закрыть как решённый
</button>
<button className="btn" onClick={() => handleReportStatus(report.id, 'dismissed')}>
Отклонить репорт
Пропустить
</button>
</div>
</div>

View File

@ -1,7 +1,7 @@
{
"name": "nakama-space",
"version": "1.0.0",
"description": "NakamaSpace - Telegram Mini App социальная сеть",
"description": "Nakama - Telegram Mini App социальная сеть",
"main": "backend/server.js",
"scripts": {
"dev": "concurrently \"npm run server\" \"npm run client\"",

View File

@ -1,8 +1,8 @@
#!/bin/bash
# NakamaSpace - Скрипт быстрого запуска
# Nakama - Скрипт быстрого запуска
echo "🚀 Запуск NakamaSpace..."
echo "🚀 Запуск Nakama..."
# Проверка MongoDB
if ! pgrep -x "mongod" > /dev/null; then

View File

@ -1,9 +1,9 @@
#!/bin/bash
# Скрипт обновления NakamaSpace на сервере
# Скрипт обновления Nakama на сервере
# Использование: ./update-server.sh
echo "🚀 Обновление NakamaSpace..."
echo "🚀 Обновление Nakama..."
# 1. Перейти в директорию проекта
cd /var/www/nakama || exit 1