import { v4 as uuidv4 } from 'uuid'; import amqp from 'amqplib/callback_api.js'; import User from '../models/community/user.js'; import Room from '../models/chat/room.js'; import UserParam from '../models/community/user_param.js'; import UserParamType from '../models/type/user_param.js'; const RABBITMQ_URL = process.env.AMQP_URL || 'amqp://localhost'; const QUEUE = 'oneToOne_messages'; class ChatService { constructor() { this.messages = []; this.searchQueue = []; this.users = []; this.randomChats = []; this.oneToOneChats = []; this.channel = null; this.amqpAvailable = false; this.initRabbitMq(); } initRabbitMq() { amqp.connect(RABBITMQ_URL, (err, connection) => { if (err) { console.warn(`[chatService] RabbitMQ nicht erreichbar (${RABBITMQ_URL}) - fallback ohne Queue wird verwendet.`); return; } connection.on('error', (connectionError) => { console.warn('[chatService] RabbitMQ-Verbindung fehlerhaft:', connectionError.message); this.channel = null; this.amqpAvailable = false; }); connection.on('close', () => { console.warn('[chatService] RabbitMQ-Verbindung geschlossen.'); this.channel = null; this.amqpAvailable = false; }); connection.createChannel((channelError, channel) => { if (channelError) { console.warn('[chatService] RabbitMQ-Channel konnte nicht erstellt werden:', channelError.message); return; } this.channel = channel; this.amqpAvailable = true; channel.assertQueue(QUEUE, { durable: false }); }); }); } getMessages(toId, fromId) { const userChats = this.randomChats.filter(chat => chat.includes(toId) && chat.includes(fromId)); if (userChats.length === 0) { fromId = ''; } const userMessages = this.messages.filter(message => message.to === toId && ["system", fromId].includes(message.from)); this.messages = this.messages.filter(message => message.to === toId && ["system", fromId].includes(message.from)); return userMessages; } async addMessage(from, to, text) { const userChats = this.randomChats.filter(chat => chat.includes(from) && chat.includes(to)); if (userChats.length === 0) { return; } this.messages.push({ from: from, to: to, text: text }); return { text: text }; } findMatch(genders, age, id) { const currentUsersChat = this.randomChats.filter(chat => chat.includes(id)); if (currentUsersChat.length > 0) { return this.findUser(currentUsersChat[0][0] === id ? currentUsersChat[0][1] : currentUsersChat[0][0]); } let filteredSearchQueue = this.users.filter(user => this.searchQueue.some(sq => sq.id === user.id) && user.id !== id && this.randomChats.filter(chat => chat.includes(user.id)).length === 0 ).sort(() => Math.random() - 0.5); for (let i = 0; i < filteredSearchQueue.length; i++) { const user = filteredSearchQueue[i]; const ageMatch = user.age >= age.min && user.age <= age.max; const genderMatch = genders.includes(user.gender); if (ageMatch && genderMatch) { for (let j = this.searchQueue.length - 1; j >= 0; j--) { if ([id, user.id].includes(this.searchQueue[j].id)) { this.searchQueue.splice(j, 1); } } this.randomChats.push([user.id, id]); return user; } } if (!this.searchQueue.find(user => user.id === id)) { this.searchQueue.push({ id, genders, age }); } return null; } findUser(id) { return this.users.find(user => user.id === id); } async registerUser(gender, age) { const id = uuidv4(); this.users.push({ gender, age, id }); return id; } async removeUser(id) { this.searchQueue = this.searchQueue.filter(user => user.id !== id); this.users = this.users.filter(user => user.id !== id); this.randomChats = this.randomChats.filter(pair => pair[0] === id || pair[1] === id); this.messages = this.messages.filter(message => message.from === id || message.to === id); } async endChat(userId) { this.randomChats = this.randomChats.filter(chat => !chat.includes(userId)); this.messages.push({ to: userId, from: 'system', activity: 'otheruserleft' }); } async initOneToOne(user1HashId, user2HashId) { const chat = this.searchOneToOneChat(user1HashId, user2HashId); if (!chat) { this.oneToOneChats.push({ user1Id: user1HashId, user2Id: user2HashId, history: [] }); } } async sendOneToOneMessage(user1HashId, user2HashId, message) { const messageBundle = { timestamp: Date.now(), sender: user1HashId, recipient: user2HashId, message: message, }; const chat = this.searchOneToOneChat(user1HashId, user2HashId); if (chat) { chat.history.push(messageBundle); } else { this.oneToOneChats.push({ user1Id: user1HashId, user2Id: user2HashId, history: [messageBundle], }); } if (this.channel && this.amqpAvailable) { try { this.channel.sendToQueue(QUEUE, Buffer.from(JSON.stringify(messageBundle))); } catch (error) { console.warn('[chatService] sendToQueue fehlgeschlagen, Queue-Bridge vorübergehend deaktiviert:', error.message); this.channel = null; this.amqpAvailable = false; } } } async getOneToOneMessageHistory(user1HashId, user2HashId) { const chat = this.searchOneToOneChat(user1HashId, user2HashId); return chat ? chat.history : []; } searchOneToOneChat(user1HashId, user2HashId) { return this.oneToOneChats.find(chat => (chat.user1Id === user1HashId && chat.user2Id === user2HashId) || (chat.user1Id === user2HashId && chat.user2Id === user1HashId) ); } calculateAge(birthdate) { const birthDate = new Date(birthdate); const ageDifMs = Date.now() - birthDate.getTime(); const ageDate = new Date(ageDifMs); return Math.abs(ageDate.getUTCFullYear() - 1970); } normalizeAdultVerificationStatus(value) { if (!value) return 'none'; const normalized = String(value).trim().toLowerCase(); return ['pending', 'approved', 'rejected'].includes(normalized) ? normalized : 'none'; } async getAdultAccessState(hashedUserId) { if (!hashedUserId) { return { isAdult: false, adultVerificationStatus: 'none', adultAccessEnabled: false }; } const user = await User.findOne({ where: { hashedId: hashedUserId }, attributes: ['id'] }); if (!user) { throw new Error('user_not_found'); } const params = await UserParam.findAll({ where: { userId: user.id }, include: [{ model: UserParamType, as: 'paramType', where: { description: ['birthdate', 'adult_verification_status'] } }] }); const birthdateParam = params.find(param => param.paramType?.description === 'birthdate'); const statusParam = params.find(param => param.paramType?.description === 'adult_verification_status'); const age = birthdateParam ? this.calculateAge(birthdateParam.value) : 0; const adultVerificationStatus = this.normalizeAdultVerificationStatus(statusParam?.value); return { isAdult: age >= 18, adultVerificationStatus, adultAccessEnabled: age >= 18 && adultVerificationStatus === 'approved' }; } async getRoomList(hashedUserId, { adultOnly = false } = {}) { // Nur öffentliche Räume, keine sensiblen Felder const { default: Room } = await import('../models/chat/room.js'); const { default: RoomType } = await import('../models/chat/room_type.js'); const where = { isPublic: true, isAdultOnly: Boolean(adultOnly) }; if (adultOnly) { const adultAccess = await this.getAdultAccessState(hashedUserId); if (!adultAccess.adultAccessEnabled) { return []; } } return Room.findAll({ attributes: [ 'id', 'title', 'roomTypeId', 'isPublic', 'isAdultOnly', 'genderRestrictionId', 'minAge', 'maxAge', 'friendsOfOwnerOnly', 'requiredUserRightId' ], where, include: [ { model: RoomType, as: 'roomType' } ] }); } async getRoomCreateOptions() { const { default: UserRightType } = await import('../models/type/user_right.js'); const { default: InterestType } = await import('../models/type/interest.js'); const [rights, interests] = await Promise.all([ UserRightType.findAll({ attributes: ['id', 'title'], order: [['id', 'ASC']] }), InterestType.findAll({ attributes: ['id', 'name'], order: [['id', 'ASC']] }) ]); return { rights: rights.map((r) => ({ id: r.id, title: r.title })), roomTypes: interests.map((i) => ({ id: i.id, name: i.name })) }; } async getOwnRooms(hashedUserId) { const user = await User.findOne({ where: { hashedId: hashedUserId }, attributes: ['id'] }); if (!user) { throw new Error('user_not_found'); } return Room.findAll({ where: { ownerId: user.id }, attributes: ['id', 'title', 'isPublic', 'isAdultOnly', 'roomTypeId', 'ownerId'], order: [['title', 'ASC']] }); } async deleteOwnRoom(hashedUserId, roomId) { const user = await User.findOne({ where: { hashedId: hashedUserId }, attributes: ['id'] }); if (!user) { throw new Error('user_not_found'); } const deleted = await Room.destroy({ where: { id: roomId, ownerId: user.id } }); if (!deleted) { throw new Error('room_not_found_or_not_owner'); } return true; } } export default new ChatService();