Update files
This commit is contained in:
parent
01f1e1ae94
commit
bc2d103e50
2
LICENSE
2
LICENSE
|
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2025 NakamaSpace
|
Copyright (c) 2025 Nakama
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
|
||||||
|
|
@ -35,12 +35,18 @@ const ensureUserSettings = async (user) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user.settings.whitelist) {
|
if (!user.settings.whitelist) {
|
||||||
user.settings.whitelist = { noNSFW: true };
|
user.settings.whitelist = { noNSFW: true, noHomo: true };
|
||||||
updated = true;
|
updated = true;
|
||||||
} else if (user.settings.whitelist.noNSFW === undefined) {
|
} else {
|
||||||
|
if (user.settings.whitelist.noNSFW === undefined) {
|
||||||
user.settings.whitelist.noNSFW = true;
|
user.settings.whitelist.noNSFW = true;
|
||||||
updated = true;
|
updated = true;
|
||||||
}
|
}
|
||||||
|
if (user.settings.whitelist.noHomo === undefined) {
|
||||||
|
user.settings.whitelist.noHomo = true;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (updated) {
|
if (updated) {
|
||||||
user.markModified('settings');
|
user.markModified('settings');
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,12 @@ const PostSchema = new mongoose.Schema({
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
// Отдельный флаг для гомосексуального контента
|
||||||
|
// Может отсутствовать у старых постов, поэтому фильтры должны учитывать isHomo === true
|
||||||
|
isHomo: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
likes: [{
|
likes: [{
|
||||||
type: mongoose.Schema.Types.ObjectId,
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
ref: 'User'
|
ref: 'User'
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,10 @@ const UserSchema = new mongoose.Schema({
|
||||||
whitelist: {
|
whitelist: {
|
||||||
noFurry: { type: Boolean, default: false },
|
noFurry: { type: Boolean, default: false },
|
||||||
onlyAnime: { 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: {
|
searchPreference: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ const normalizeUserSettings = (settings = {}) => {
|
||||||
...plainSettings,
|
...plainSettings,
|
||||||
whitelist: {
|
whitelist: {
|
||||||
noNSFW: whitelist?.noNSFW ?? true,
|
noNSFW: whitelist?.noNSFW ?? true,
|
||||||
|
noHomo: whitelist?.noHomo ?? true,
|
||||||
...whitelist
|
...whitelist
|
||||||
},
|
},
|
||||||
searchPreference: ALLOWED_SEARCH_PREFERENCES.includes(plainSettings.searchPreference)
|
searchPreference: ALLOWED_SEARCH_PREFERENCES.includes(plainSettings.searchPreference)
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,9 @@ router.get('/', authenticate, searchLimiter, async (req, res) => {
|
||||||
if (req.user.settings.whitelist.noNSFW) {
|
if (req.user.settings.whitelist.noNSFW) {
|
||||||
searchQuery.isNSFW = false;
|
searchQuery.isNSFW = false;
|
||||||
}
|
}
|
||||||
|
if (req.user.settings.whitelist.noHomo) {
|
||||||
|
searchQuery.isHomo = { $ne: true };
|
||||||
|
}
|
||||||
|
|
||||||
// Поиск по хэштегу
|
// Поиск по хэштегу
|
||||||
if (hashtag) {
|
if (hashtag) {
|
||||||
|
|
@ -97,6 +100,9 @@ router.get('/hashtag/:tag', authenticate, async (req, res) => {
|
||||||
if (req.user.settings.whitelist.noNSFW) {
|
if (req.user.settings.whitelist.noNSFW) {
|
||||||
query.isNSFW = false;
|
query.isNSFW = false;
|
||||||
}
|
}
|
||||||
|
if (req.user.settings.whitelist.noHomo) {
|
||||||
|
query.isHomo = { $ne: true };
|
||||||
|
}
|
||||||
|
|
||||||
const posts = await Post.find(query)
|
const posts = await Post.find(query)
|
||||||
.populate('author', 'username firstName lastName photoUrl')
|
.populate('author', 'username firstName lastName photoUrl')
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,11 @@ router.get('/', authenticate, async (req, res) => {
|
||||||
if (req.user.settings.whitelist.noNSFW) {
|
if (req.user.settings.whitelist.noNSFW) {
|
||||||
query.isNSFW = false;
|
query.isNSFW = false;
|
||||||
}
|
}
|
||||||
|
if (req.user.settings.whitelist.noHomo) {
|
||||||
|
// Скрывать только посты, помеченные как гомосексуальные.
|
||||||
|
// Посты без флага (старые) остаются видимыми.
|
||||||
|
query.isHomo = { $ne: true };
|
||||||
|
}
|
||||||
|
|
||||||
let posts = await Post.find(query)
|
let posts = await Post.find(query)
|
||||||
.populate('author', 'username firstName lastName photoUrl')
|
.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) => {
|
router.post('/', authenticate, strictPostLimiter, postCreationLimiter, fileUploadLimiter, uploadPostImages, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { content, tags, mentionedUsers, isNSFW, externalImages } = req.body;
|
const { content, tags, mentionedUsers, isNSFW, isHomo, externalImages } = req.body;
|
||||||
|
|
||||||
// Валидация контента
|
// Валидация контента
|
||||||
if (content && !validatePostContent(content)) {
|
if (content && !validatePostContent(content)) {
|
||||||
|
|
@ -139,7 +144,10 @@ router.post('/', authenticate, strictPostLimiter, postCreationLimiter, fileUploa
|
||||||
tags: parsedTags,
|
tags: parsedTags,
|
||||||
hashtags,
|
hashtags,
|
||||||
mentionedUsers: mentionedUsers ? JSON.parse(mentionedUsers) : [],
|
mentionedUsers: mentionedUsers ? JSON.parse(mentionedUsers) : [],
|
||||||
isNSFW: isNSFW === 'true'
|
isNSFW: isNSFW === 'true',
|
||||||
|
// Флаг гомосексуального контента - полный аналог NSFW по логике,
|
||||||
|
// но управляется отдельно
|
||||||
|
isHomo: isHomo === 'true' || isHomo === true
|
||||||
});
|
});
|
||||||
|
|
||||||
await post.save();
|
await post.save();
|
||||||
|
|
|
||||||
|
|
@ -46,15 +46,24 @@ router.get('/:id', authenticate, async (req, res) => {
|
||||||
router.get('/:id/posts', authenticate, async (req, res) => {
|
router.get('/:id/posts', authenticate, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { page = 1, limit = 20 } = req.query;
|
const { page = 1, limit = 20 } = req.query;
|
||||||
|
const query = { author: req.params.id };
|
||||||
|
|
||||||
const posts = await Post.find({ 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')
|
.populate('author', 'username firstName lastName photoUrl')
|
||||||
.sort({ createdAt: -1 })
|
.sort({ createdAt: -1 })
|
||||||
.limit(limit * 1)
|
.limit(limit * 1)
|
||||||
.skip((page - 1) * limit)
|
.skip((page - 1) * limit)
|
||||||
.exec();
|
.exec();
|
||||||
|
|
||||||
const count = await Post.countDocuments({ author: req.params.id });
|
const count = await Post.countDocuments(query);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
posts,
|
posts,
|
||||||
|
|
@ -148,10 +157,15 @@ router.put('/profile', authenticate, async (req, res) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!req.user.settings.whitelist) {
|
if (!req.user.settings.whitelist) {
|
||||||
req.user.settings.whitelist = { noNSFW: true };
|
req.user.settings.whitelist = { noNSFW: true, noHomo: true };
|
||||||
} else if (req.user.settings.whitelist.noNSFW === undefined) {
|
} else {
|
||||||
|
if (req.user.settings.whitelist.noNSFW === undefined) {
|
||||||
req.user.settings.whitelist.noNSFW = true;
|
req.user.settings.whitelist.noNSFW = true;
|
||||||
}
|
}
|
||||||
|
if (req.user.settings.whitelist.noHomo === undefined) {
|
||||||
|
req.user.settings.whitelist.noHomo = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
req.user.markModified('settings');
|
req.user.markModified('settings');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ export default function CreatePostModal({ user, onClose, onPostCreated, initialI
|
||||||
const [imagePreviews, setImagePreviews] = useState(initialImage ? [initialImage] : [])
|
const [imagePreviews, setImagePreviews] = useState(initialImage ? [initialImage] : [])
|
||||||
const [externalImages, setExternalImages] = useState(initialImage ? [initialImage] : [])
|
const [externalImages, setExternalImages] = useState(initialImage ? [initialImage] : [])
|
||||||
const [isNSFW, setIsNSFW] = useState(false)
|
const [isNSFW, setIsNSFW] = useState(false)
|
||||||
|
const [isHomo, setIsHomo] = useState(false)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [showUserSearch, setShowUserSearch] = useState(false)
|
const [showUserSearch, setShowUserSearch] = useState(false)
|
||||||
const [userSearchQuery, setUserSearchQuery] = useState('')
|
const [userSearchQuery, setUserSearchQuery] = useState('')
|
||||||
|
|
@ -107,6 +108,7 @@ export default function CreatePostModal({ user, onClose, onPostCreated, initialI
|
||||||
formData.append('content', content)
|
formData.append('content', content)
|
||||||
formData.append('tags', JSON.stringify(selectedTags))
|
formData.append('tags', JSON.stringify(selectedTags))
|
||||||
formData.append('isNSFW', isNSFW)
|
formData.append('isNSFW', isNSFW)
|
||||||
|
formData.append('isHomo', isHomo)
|
||||||
|
|
||||||
// Добавить загруженные файлы
|
// Добавить загруженные файлы
|
||||||
images.forEach((image, index) => {
|
images.forEach((image, index) => {
|
||||||
|
|
@ -223,6 +225,18 @@ export default function CreatePostModal({ user, onClose, onPostCreated, initialI
|
||||||
<span>Отметить как NSFW</span>
|
<span>Отметить как NSFW</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Homo переключатель */}
|
||||||
|
<div className="nsfw-toggle">
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isHomo}
|
||||||
|
onChange={e => setIsHomo(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<span>Отметить как Homo</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Футер с действиями */}
|
{/* Футер с действиями */}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,9 @@ const normalizeSearchPreference = (value) =>
|
||||||
|
|
||||||
const DEFAULT_SETTINGS = {
|
const DEFAULT_SETTINGS = {
|
||||||
whitelist: {
|
whitelist: {
|
||||||
noNSFW: true
|
noNSFW: true,
|
||||||
|
// Скрыть гомосексуальный контент
|
||||||
|
noHomo: true
|
||||||
},
|
},
|
||||||
searchPreference: 'furry'
|
searchPreference: 'furry'
|
||||||
}
|
}
|
||||||
|
|
@ -209,6 +211,21 @@ export default function Profile({ user, setUser }) {
|
||||||
<span className="toggle-slider" />
|
<span className="toggle-slider" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Модальное окно редактирования bio */}
|
{/* Модальное окно редактирования bio */}
|
||||||
|
|
@ -276,6 +293,21 @@ export default function Profile({ user, setUser }) {
|
||||||
<span className="toggle-slider" />
|
<span className="toggle-slider" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div className="settings-section">
|
<div className="settings-section">
|
||||||
|
|
|
||||||
|
|
@ -291,19 +291,23 @@ export default function App() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const API_URL = import.meta.env.VITE_API_URL || (
|
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] Инициализация чата');
|
||||||
|
console.log('[Chat] WS base URL:', socketBase);
|
||||||
console.log('[Chat] User данные:', {
|
console.log('[Chat] User данные:', {
|
||||||
username: user.username,
|
username: user.username,
|
||||||
telegramId: user.telegramId,
|
telegramId: user.telegramId,
|
||||||
hasUsername: !!user.username,
|
hasUsername: !!user.username,
|
||||||
hasTelegramId: !!user.telegramId
|
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'],
|
transports: ['websocket', 'polling'],
|
||||||
reconnection: true,
|
reconnection: true,
|
||||||
reconnectionDelay: 1000,
|
reconnectionDelay: 1000,
|
||||||
|
|
@ -677,16 +681,16 @@ export default function App() {
|
||||||
{report.post.author && (
|
{report.post.author && (
|
||||||
<button className="btn warn" onClick={() => handleBanAuthor(report.post.id)}>
|
<button className="btn warn" onClick={() => handleBanAuthor(report.post.id)}>
|
||||||
<Ban size={16} />
|
<Ban size={16} />
|
||||||
Забанить автора
|
Забанить автора (срок)
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<button className="btn" onClick={() => handleReportStatus(report.id, 'resolved')}>
|
<button className="btn" onClick={() => handleReportStatus(report.id, 'resolved')}>
|
||||||
Решено
|
Закрыть как решённый
|
||||||
</button>
|
</button>
|
||||||
<button className="btn" onClick={() => handleReportStatus(report.id, 'dismissed')}>
|
<button className="btn" onClick={() => handleReportStatus(report.id, 'dismissed')}>
|
||||||
Отклонить репорт
|
Пропустить
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "nakama-space",
|
"name": "nakama-space",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "NakamaSpace - Telegram Mini App социальная сеть",
|
"description": "Nakama - Telegram Mini App социальная сеть",
|
||||||
"main": "backend/server.js",
|
"main": "backend/server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently \"npm run server\" \"npm run client\"",
|
"dev": "concurrently \"npm run server\" \"npm run client\"",
|
||||||
|
|
|
||||||
4
start.sh
4
start.sh
|
|
@ -1,8 +1,8 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# NakamaSpace - Скрипт быстрого запуска
|
# Nakama - Скрипт быстрого запуска
|
||||||
|
|
||||||
echo "🚀 Запуск NakamaSpace..."
|
echo "🚀 Запуск Nakama..."
|
||||||
|
|
||||||
# Проверка MongoDB
|
# Проверка MongoDB
|
||||||
if ! pgrep -x "mongod" > /dev/null; then
|
if ! pgrep -x "mongod" > /dev/null; then
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Скрипт обновления NakamaSpace на сервере
|
# Скрипт обновления Nakama на сервере
|
||||||
# Использование: ./update-server.sh
|
# Использование: ./update-server.sh
|
||||||
|
|
||||||
echo "🚀 Обновление NakamaSpace..."
|
echo "🚀 Обновление Nakama..."
|
||||||
|
|
||||||
# 1. Перейти в директорию проекта
|
# 1. Перейти в директорию проекта
|
||||||
cd /var/www/nakama || exit 1
|
cd /var/www/nakama || exit 1
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue