diff --git a/server/index.ts b/server/index.ts index d39f65f..7f1d913 100644 --- a/server/index.ts +++ b/server/index.ts @@ -21,6 +21,7 @@ import escRouter from './routers/esc' import connectmeRouter from './routers/connectme' import questioneerRouter from './routers/questioneer' import { setIo } from './io' +const { createChatPollingRouter } = require('./routers/kfu-m-24-1/sber_mobile/polling-chat') export const app = express() @@ -64,8 +65,6 @@ const initServer = async () => { console.log('warming up πŸ”₯') - const server = setIo(app) - const sess = { secret: "super-secret-key", resave: true, @@ -90,10 +89,18 @@ const initServer = async () => { ) app.use(root) + // Π˜Π½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·Π°Ρ†ΠΈΡ Polling для Ρ‡Π°Ρ‚Π° (послС настройки middleware) + const { router: chatPollingRouter, chatHandler } = createChatPollingRouter(express) + + /** * ДобавляйтС сюда свои routers. */ app.use("/kfu-m-24-1", kfuM241Router) + + // ДобавляСм Polling Ρ€ΠΎΡƒΡ‚Π΅Ρ€ для Ρ‡Π°Ρ‚Π° + app.use("/kfu-m-24-1/sber_mobile", chatPollingRouter) + app.use("/epja-2024-1", epja20241Router) app.use("/v1/todo", todoRouter) app.use("/dogsitters-finder", dogsittersFinderRouter) @@ -109,9 +116,10 @@ const initServer = async () => { app.use(errorHandler) - server.listen(process.env.PORT ?? 8044, () => + // Π‘ΠΎΠ·Π΄Π°Π΅ΠΌ ΠΎΠ±Ρ‹Ρ‡Π½Ρ‹ΠΉ HTTP сСрвСр + const server = app.listen(process.env.PORT ?? 8044, () => { console.log(`πŸš€ Π‘Π΅Ρ€Π²Π΅Ρ€ Π·Π°ΠΏΡƒΡ‰Π΅Π½ Π½Π° http://localhost:${process.env.PORT ?? 8044}`) - ) + }) // ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° сигналов Π·Π°Π²Π΅Ρ€ΡˆΠ΅Π½ΠΈΡ процСсса process.on('SIGTERM', () => { @@ -145,6 +153,8 @@ const initServer = async () => { process.exit(1) }) }) + + return server } initServer().catch(console.error) diff --git a/server/routers/kfu-m-24-1/sber_mobile/chats.js b/server/routers/kfu-m-24-1/sber_mobile/chats.js index a20d9df..047ac37 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/chats.js +++ b/server/routers/kfu-m-24-1/sber_mobile/chats.js @@ -3,31 +3,22 @@ const { getSupabaseClient } = require('./supabaseClient'); // ΠŸΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ всС Ρ‡Π°Ρ‚Ρ‹ ΠΏΠΎ Π΄ΠΎΠΌΡƒ router.get('/chats', async (req, res) => { - console.log('🏠 [Server] GET /chats запрос ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½'); - console.log('🏠 [Server] Query ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹:', req.query); - const supabase = getSupabaseClient(); const { building_id } = req.query; if (!building_id) { - console.log('❌ [Server] Ошибка: building_id обязатСлСн'); return res.status(400).json({ error: 'building_id required' }); } try { - console.log('πŸ” [Server] ВыполняСм запрос ΠΊ Supabase для здания:', building_id); - const { data, error } = await supabase.from('chats').select('*').eq('building_id', building_id); if (error) { - console.log('❌ [Server] Ошибка Supabase:', error); return res.status(400).json({ error: error.message }); } - console.log('βœ… [Server] Π§Π°Ρ‚Ρ‹ ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½Ρ‹:', data?.length || 0, 'ΡˆΡ‚.'); res.json(data || []); } catch (err) { - console.log('❌ [Server] НСоТиданная ошибка:', err); res.status(500).json({ error: 'Internal server error' }); } }); @@ -184,15 +175,10 @@ router.get('/chats/:chat_id/stats', async (req, res) => { // ΠŸΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ послСднСС сообщСниС Π² Ρ‡Π°Ρ‚Π΅ router.get('/chats/:chat_id/last-message', async (req, res) => { - console.log('πŸ’¬ [Server] GET /chats/:chat_id/last-message запрос ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½'); - console.log('πŸ’¬ [Server] Chat ID:', req.params.chat_id); - const supabase = getSupabaseClient(); const { chat_id } = req.params; try { - console.log('πŸ” [Server] ВыполняСм запрос послСднСго сообщСния для Ρ‡Π°Ρ‚Π°:', chat_id); - // ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ послСднСС сообщСниС const { data: lastMessage, error } = await supabase .from('messages') @@ -205,10 +191,8 @@ router.get('/chats/:chat_id/last-message', async (req, res) => { let data = null; if (error && error.code === 'PGRST116') { - console.log('ℹ️ [Server] Π‘ΠΎΠΎΠ±Ρ‰Π΅Π½ΠΈΠΉ Π² Ρ‡Π°Ρ‚Π΅ Π½Π΅Ρ‚ (PGRST116)'); data = null; } else if (error) { - console.log('❌ [Server] Ошибка Supabase ΠΏΡ€ΠΈ ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠΈ послСднСго сообщСния:', error); return res.status(400).json({ error: error.message }); } else if (lastMessage) { // ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ ΠΏΡ€ΠΎΡ„ΠΈΠ»ΡŒ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ для сообщСния @@ -223,12 +207,10 @@ router.get('/chats/:chat_id/last-message', async (req, res) => { ...lastMessage, user_profiles: userProfile || null }; - console.log('βœ… [Server] ПослСднСС сообщСниС ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½ΠΎ для Ρ‡Π°Ρ‚Π°:', chat_id); - } + } - res.json(data); + res.json(data); } catch (err) { - console.log('❌ [Server] НСоТиданная ошибка ΠΏΡ€ΠΈ ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠΈ послСднСго сообщСния:', err); res.status(500).json({ error: 'Internal server error' }); } }); diff --git a/server/routers/kfu-m-24-1/sber_mobile/index.js b/server/routers/kfu-m-24-1/sber_mobile/index.js index e419be7..dfc34f1 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/index.js +++ b/server/routers/kfu-m-24-1/sber_mobile/index.js @@ -15,6 +15,8 @@ const buildingsRouter = require('./buildings'); const userApartmentsRouter = require('./user_apartments'); const avatarRouter = require('./media'); const supportRouter = require('./supportApi'); +const moderateRouter = require('./moderate.js'); + module.exports = router; @@ -33,4 +35,6 @@ router.use('', apartmentsRouter); router.use('', buildingsRouter); router.use('', userApartmentsRouter); router.use('', avatarRouter); -router.use('', supportRouter); \ No newline at end of file +router.use('', supportRouter); +router.use('', moderateRouter); + diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/llm.ts b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/llm.ts new file mode 100644 index 0000000..a5a0e23 --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/llm.ts @@ -0,0 +1,22 @@ +import { GigaChat as GigaChatLang} from 'langchain-gigachat'; +import { GigaChat } from 'gigachat'; +import { Agent } from 'node:https'; + +const httpsAgent = new Agent({ + rejectUnauthorized: false, +}); + +export const llm_mod = (GIGA_AUTH) => + new GigaChatLang({ + credentials: GIGA_AUTH, + temperature: 0.2, + model: 'GigaChat-2-Max', + httpsAgent, +}); + +export const llm_gen = (GIGA_AUTH) => + new GigaChat({ + credentials: GIGA_AUTH, + model: 'GigaChat-2', + httpsAgent, +}); \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts new file mode 100644 index 0000000..dc2022c --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/moderation.ts @@ -0,0 +1,58 @@ +import { llm_mod } from './llm' +import { z } from "zod"; + + +// Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°ΡŽ ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΉ + исправлСнноС ΠΏΡ€Π΅Π΄Π»ΠΎΠΆΠ΅Π½ΠΈΠ΅ + Π±ΡƒΠ»Π΅Π²ΠΎ Π·Π½Π°Ρ‡Π΅Π½ΠΈΠ΅ + +export const moderationText = async (title: string, description: string, GIGA_AUTH): Promise<[string, string | undefined, boolean]> => { + + const moderationLlm = llm_mod(GIGA_AUTH).withStructuredOutput(z.object({ + comment: z.string(), + fixedText: z.string().optional(), + isApproved: z.boolean(), + }) as any) + + const prompt = ` + ΠŸΡ€Π΅Π΄ΡΡ‚Π°Π²ΡŒ, Ρ‡Ρ‚ΠΎ Ρ‚Ρ‹ ΠΌΠΎΠ΄Π΅Ρ€ΠΈΡ€ΡƒΠ΅ΡˆΡŒ прСдлоТСния ΠΎΡ‚ ΠΆΠΈΠ»ΡŒΡ†ΠΎΠ² ΠΌΠ½ΠΎΠ³ΠΎΠΊΠ²Π°Ρ€Ρ‚ΠΈΡ€Π½ΠΎΠ³ΠΎ Π΄ΠΎΠΌΠ° (это личная ΠΈΠ½ΠΈΡ†ΠΈΠ°Ρ‚ΠΈΠ²Π° ΠΏΠΎ ΡƒΠ»ΡƒΡ‡ΡˆΠ΅Π½ΠΈΡ, + Π½Π΅ ΠΈΠΌΠ΅ΡŽΡ‰Π°Ρ ΠΎΡ‚Π½ΠΎΡˆΠ΅Π½ΠΈΡ ΠΊ Π£ΠΏΡ€Π°Π²Π»ΡΡŽΡ‰Π΅ΠΉ ΠΊΠΎΠΌΠΏΠ°Π½ΠΈΠΈ). + + Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ: ${title} + Основной тСкст: ${description} + + Π’Π²ΠΎΠΈ Π·Π°Π΄Π°Ρ‡ΠΈ: + 1. ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡŒ ΠΏΡ€Π΅Π΄Π»ΠΎΠΆΠ΅Π½ΠΈΠ΅ ΠΈ Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ Π½Π° спам. + 2. ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡŒ, Ρ‡Ρ‚ΠΎΠ±Ρ‹ Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ ΠΈ тСкст Π±Ρ‹Π»ΠΈ Π½Π° ΠΎΠ΄Π½Ρƒ Ρ‚Π΅ΠΌΡƒ. + 3. ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡŒ само ΠΏΡ€Π΅Π΄Π»ΠΎΠΆΠ΅Π½ΠΈΠ΅ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ Π½Π° отсутствиС Π³Ρ€ΡƒΠ±ΠΎΠΉ лСксики ΠΈ ΠΏΠΎΡˆΠ»ΠΎΡΡ‚Π΅ΠΉ. + 4. ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡŒ Π³Ρ€Π°ΠΌΠΌΠ°Ρ‚ΠΈΠΊΡƒ. + 5. ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡŒ Π½Π° Π±Π΅ΡΡΠΌΡ‹ΡΠ»Π΅Π½Π½ΠΎΡΡ‚ΡŒ прСдлоТСния. Оно Π½Π΅ Π΄ΠΎΠ»ΠΆΠ½ΠΎ ΡΠΎΠ΄Π΅Ρ€ΠΆΠ°Ρ‚ΡŒ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ случайныС символы. + 6. НС Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π±Ρ‹Ρ‚ΡŒ Ρ€Π΅ΠΊΠ»Π°ΠΌΡ‹, ссылок ΠΈ Ρ‚.Π΄. + 7. ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡŒ ΠΏΡ€Π΅Π΄Π»ΠΎΠΆΠ΅Π½ΠΈΠ΅ Π½Π° ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ‚ΠΈΠ²Π½ΠΎΡΡ‚ΡŒ, ΠΏΡ€Π΅Π΄Π»ΠΎΠΆΠ΅Π½ΠΈΠ΅ Π½Π΅ ΠΌΠΎΠΆΠ΅Ρ‚ Π±Ρ‹Ρ‚ΡŒ ΠΊΠΎΡ€ΠΎΡ‚ΠΊΠΈΠΌ, ΠΎΠ½ΠΎ Π΄ΠΎΠ»ΠΆΠ½ΠΎ ясно ΠΎΡ‚Ρ€Π°ΠΆΠΆΠ°Ρ‚ΡŒ ΡΡƒΡ‚ΡŒ ΠΈΠ½ΠΈΡ†ΠΈΠ°Ρ‚ΠΈΠ²Ρ‹. + 8. ΠŸΡ€Π΅Π΄Π»ΠΎΠΆΠ΅Π½ΠΈΠ΅ Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π±Ρ‹Ρ‚ΡŒ Π² Π²Π΅ΠΆΠ»ΠΈΠ²ΠΎΠΉ Ρ„ΠΎΡ€ΠΌΠ΅. + + - Если всС ΠΏΡ€Π°Π²ΠΈΠ»Π° ΡΠΎΠ±Π»ΡŽΠ΄Π΅Π½Ρ‹, Ρ‚ΠΎ ΠΏΡ€Π΅Π΄Π»ΠΎΠΆΠ΅Π½ΠΈΠ΅ принимаСтся! + + - Если ΠΏΡ€Π΅Π΄Π»ΠΎΠΆΠ΅Π½ΠΈΠ΅ ΠΎΡ‚ΠΊΠ»ΠΎΠ½Π΅Π½ΠΎ, всСгда пиши ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΉ ΠΈ fixedText! + + ΠŸΡ€Π°Π²ΠΈΠ»Π° написания коммСнтария: + - Если ΠΏΡ€Π΅Π΄Π»ΠΎΠΆΠ΅Π½ΠΈΠ΅ отклоняСтся, пиши ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΉ со ΡΠ»Π΅Π΄ΡƒΡŽΡ‰Π΅ΠΉ Ρ„ΠΎΡ€ΠΌΡƒΠ»ΠΈΡ€ΠΎΠ²ΠΊΠΎΠΉ: + "ΠŸΡ€Π΅Π΄Π»ΠΎΠΆΠ΅Π½ΠΈΠ΅ ΠΎΡ‚ΠΊΠ»ΠΎΠ½Π΅Π½ΠΎ. ΠŸΡ€ΠΈΡ‡ΠΈΠ½Π°: (ΡƒΠΊΠ°ΠΆΠΈ ΠΏΡ€ΠΎΠ±Π»Π΅ΠΌΡƒ)" + + ΠŸΡ€Π°Π²ΠΈΠ»Π° написания fixedText: + - Если ΠΏΡ€Π΅Π΄Π»ΠΎΠΆΠ΅Π½ΠΈΠ΅ ΠΎΡ‚ΠΊΠ»ΠΎΠ½Π΅Π½ΠΎ, Ρ‚ΠΎ Π²Π΅Ρ€Π½ΠΈ Π² ΠΏΠΎΠ»Π΅ "fixedText" ΠΈΠ·ΠΌΠ΅Π½Π΅Π½Π½Ρ‹ΠΉ тСкст, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ Π±ΡƒΠ΄Π΅Ρ‚ ΡΠΎΠΎΡ‚Π²Π΅Ρ‚ΡΡ‚Π²ΠΎΠ²Π°Ρ‚ΡŒ ΠΏΡ€Π°Π²ΠΈΠ»Π°ΠΌ. + - Если ΠΏΡ€Π΅Π΄Π»ΠΎΠΆΠ΅Π½ΠΈΠ΅ ΠΎΡ‚ΠΊΠ»ΠΎΠ½Π΅Π½ΠΎ ΠΈ содСрТит Π·Π°ΠΏΡ€Π΅Ρ‰Ρ‘Π½Π½Ρ‹ΠΉ ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚ (Ρ€Π΅ΠΊΠ»Π°ΠΌΡƒ, Π»ΠΈΡ‡Π½Ρ‹Π΅ Π΄Π°Π½Π½Ρ‹Π΅), ΡƒΠ΄Π°Π»ΠΈ всю ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΡŽ, + которая ΠΏΡ€ΠΎΡ‚ΠΈΠ²ΠΎΡ€Π΅Ρ‡ΠΈΡ‚ ΠΏΡ€Π°Π²ΠΈΠ»Π°ΠΌ, ΠΈ Π²Π΅Ρ€Π½ΠΈ Π² Ρ‚ΠΎΠ»ΡŒΠΊΠΎ подходящий Ρ„Ρ€Π°Π³ΠΌΠ΅Π½Ρ‚, сохраняя ΠΎΠ±Ρ‰ΠΈΠΉ смысл. + - Если тСкст Π½Π΅ прСдставляСт Π½ΠΈΠΊΠ°ΠΊΠΎΠΉ цСнности, Π²ΠΎΠ·Π²Ρ€Π°Ρ‚ΠΈ Π² ΠΏΠΎΠ»Π΅ "fixedText" ΠΏΡ€Π°Π²ΠΈΠ»ΠΎ, + ΠΏΠΎ ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠΌΡƒ ΠΎΠ½ΠΎ Π½Π΅ ΠΏΡ€ΠΎΡˆΠ»ΠΎ. + -Если ΠΏΡ€Π΅Π΄Π»ΠΎΠΆΠ΅Π½ΠΈΠ΅ принимаСтся, Ρ‚ΠΎ Π½ΠΈΡ‡Π΅Π³ΠΎ Π½Π΅ Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°ΠΉ Π² ΠΏΠΎΠ»Π΅ fixedText. + ` + + const result = await moderationLlm.invoke(prompt); + console.log(result) + // Π”ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Π°Ρ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° + if(!result.isApproved && result.comment.trim() === '' && (!result.fixedText || result.fixedText.trim() === '')) { + result.comment = 'ΠŸΡ€Π΅Π΄Π»ΠΎΠΆΠ΅Π½ΠΈΠ΅ ΠΎΡ‚ΠΊΠ»ΠΎΠ½Π΅Π½ΠΎ. ΠŸΡ€ΠΈΡ‡ΠΈΠ½Π°: нСсоблюдСниС Ρ‚Ρ€Π΅Π±ΠΎΠ²Π°Π½ΠΈΠΉ ΠΊ ΠΎΡ„ΠΎΡ€ΠΌΠ»Π΅Π½ΠΈΡŽ ΠΈΠ»ΠΈ ΡΠΎΠ΄Π΅Ρ€ΠΆΠ°Π½ΠΈΡŽ.', + result.fixedText = description + } + + return [result.comment, result.fixedText, result.isApproved]; +}; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts new file mode 100644 index 0000000..d216c5d --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/initiatives-ai-agents/picture.ts @@ -0,0 +1,38 @@ +import { llm_gen } from './llm' +import { detectImage } from 'gigachat'; + +export const generatePicture = async (prompt: string, GIGA_AUTH) => { + const resp = await llm_gen(GIGA_AUTH).chat({ + messages: [ + { + "role": "system", + "content": "Π’Ρ‹ β€” Василий Кандинский для ΠΆΠΈΠ»ΡŒΡ†ΠΎΠ² ΠΌΠ½ΠΎΠ³ΠΎΠΊΠ²Π°Ρ€Ρ‚ΠΈΡ€Π½ΠΎΠ³ΠΎ Π΄ΠΎΠΌΠ°" + }, + { + role: "user", + content: `Бтарайся ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‚ΡŒ атмосфСру ΡƒΡŽΡ‚Π° ΠΈ бСзопасности. + Нарисуй ΠΊΠ°Ρ€Ρ‚ΠΈΠ½ΠΊΡƒ ΠΏΠΎΠ΄Ρ…ΠΎΠ΄ΡΡ‰ΡƒΡŽ для Ρ‚Π°ΠΊΠΎΠ³ΠΎ события: ${prompt} + Π’ ΠΊΠ°Ρ€Ρ‚ΠΈΠ½ΠΊΠ΅ Π½Π΅ Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π±Ρ‹Ρ‚ΡŒ тСкста, Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅.`, + }, + ], + function_call: 'auto', + }); + + // ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ изобраТСния ΠΏΠΎ ΠΈΠ΄Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ‚ΠΎΡ€Ρƒ + const detectedImage = detectImage(resp.choices[0]?.message.content ?? ''); + + if (!detectedImage?.uuid) { + throw new Error('НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ ΠΏΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ UUID изобраТСния ΠΈΠ· ΠΎΡ‚Π²Π΅Ρ‚Π° GigaChat'); + } + + const image = await llm_gen(GIGA_AUTH).getImage(detectedImage.uuid); + + // Π’ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅ΠΌ содСрТимоС изобраТСния, ΡƒΠ±Π΅ΠΆΠ΄Π°ΡΡΡŒ Ρ‡Ρ‚ΠΎ это Buffer + if (Buffer.isBuffer(image.content)) { + return image.content; + } else if (typeof image.content === 'string') { + return Buffer.from(image.content, 'binary'); + } else { + throw new Error('Unexpected image content type: ' + typeof image.content); + } +} diff --git a/server/routers/kfu-m-24-1/sber_mobile/initiatives.js b/server/routers/kfu-m-24-1/sber_mobile/initiatives.js index 3ca0562..42f82e4 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/initiatives.js +++ b/server/routers/kfu-m-24-1/sber_mobile/initiatives.js @@ -38,9 +38,9 @@ router.get('/initiatives/:id', async (req, res) => { // Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ ΠΈΠ½ΠΈΡ†ΠΈΠ°Ρ‚ΠΈΠ²Ρƒ router.post('/initiatives', async (req, res) => { const supabase = getSupabaseClient(); - const { building_id, creator_id, title, description, status, target_amount, image_url } = req.body; + const { building_id, creator_id, title, description, status, target_amount, current_amount, image_url } = req.body; const { data, error } = await supabase.from('initiatives').insert([ - { building_id, creator_id, title, description, status, target_amount, image_url } + { building_id, creator_id, title, description, status, target_amount, current_amount: current_amount || 0, image_url } ]).select().single(); if (error) return res.status(400).json({ error: error.message }); res.json(data); diff --git a/server/routers/kfu-m-24-1/sber_mobile/messages.js b/server/routers/kfu-m-24-1/sber_mobile/messages.js index 6c0bfa3..f729679 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/messages.js +++ b/server/routers/kfu-m-24-1/sber_mobile/messages.js @@ -1,64 +1,59 @@ const router = require('express').Router(); const { getSupabaseClient } = require('./supabaseClient'); +const { getIo } = require('../../../io'); // Π˜ΠΌΠΏΠΎΡ€Ρ‚ΠΈΡ€ΡƒΠ΅ΠΌ Socket.IO // ΠŸΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ всС сообщСния Π² Ρ‡Π°Ρ‚Π΅ с ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΠ΅ΠΉ ΠΎ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ router.get('/messages', async (req, res) => { - console.log('πŸ“¬ [Server] GET /messages запрос ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½'); - console.log('πŸ“¬ [Server] Query ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹:', req.query); - - const supabase = getSupabaseClient(); - const { chat_id, limit = 50, offset = 0 } = req.query; - - if (!chat_id) { - console.log('❌ [Server] Ошибка: chat_id обязатСлСн'); - return res.status(400).json({ error: 'chat_id required' }); - } - try { - console.log('πŸ” [Server] ВыполняСм запрос ΠΊ Supabase для Ρ‡Π°Ρ‚Π°:', chat_id); - - // ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ сообщСния - const { data: messages, error } = await supabase - .from('messages') - .select('*') - .eq('chat_id', chat_id) - .order('created_at', { ascending: false }) - .limit(limit) - .range(offset, offset + limit - 1); - - if (error) { - console.log('❌ [Server] Ошибка получСния сообщСний:', error); - return res.status(400).json({ error: error.message }); + const { chat_id, limit = 50, offset = 0 } = req.query; + + if (!chat_id) { + return res.status(400).json({ error: 'chat_id is required' }); } + + const supabase = getSupabaseClient(); - // ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ ΠΏΡ€ΠΎΡ„ΠΈΠ»ΠΈ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ΠΉ для всСх ΡƒΠ½ΠΈΠΊΠ°Π»ΡŒΠ½Ρ‹Ρ… user_id - let data = messages || []; - if (data.length > 0) { - const userIds = [...new Set(data.map(msg => msg.user_id))]; - console.log('πŸ‘₯ [Server] ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ ΠΏΡ€ΠΎΡ„ΠΈΠ»ΠΈ для ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ΠΉ:', userIds); - + const { data, error } = await supabase + .from('messages') + .select(` + *, + user_profiles ( + id, + full_name, + avatar_url + ) + `) + .eq('chat_id', chat_id) + .order('created_at', { ascending: true }) + .range(offset, offset + limit - 1); + + if (error) { + return res.status(500).json({ error: 'Failed to fetch messages' }); + } + + // ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ ΡƒΠ½ΠΈΠΊΠ°Π»ΡŒΠ½Ρ‹Π΅ ID ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ΠΉ ΠΈΠ· сообщСний, Ρƒ ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Ρ… Π½Π΅Ρ‚ профиля + const messagesWithoutProfiles = data.filter(msg => !msg.user_profiles); + const userIds = [...new Set(messagesWithoutProfiles.map(msg => msg.user_id))]; + + if (userIds.length > 0) { const { data: profiles, error: profilesError } = await supabase .from('user_profiles') .select('id, full_name, avatar_url') .in('id', userIds); - + if (!profilesError && profiles) { - // ОбъСдиняСм сообщСния с профилями - data = data.map(msg => ({ - ...msg, - user_profiles: profiles.find(profile => profile.id === msg.user_id) || null - })); - console.log('βœ… [Server] ΠŸΡ€ΠΎΡ„ΠΈΠ»ΠΈ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ΠΉ Π΄ΠΎΠ±Π°Π²Π»Π΅Π½Ρ‹ ΠΊ сообщСниям'); - } else { - console.log('⚠️ [Server] Ошибка получСния ΠΏΡ€ΠΎΡ„ΠΈΠ»Π΅ΠΉ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ΠΉ:', profilesError); + // ДобавляСм ΠΏΡ€ΠΎΡ„ΠΈΠ»ΠΈ ΠΊ сообщСниям + data.forEach(message => { + if (!message.user_profiles) { + message.user_profiles = profiles.find(profile => profile.id === message.user_id) || null; + } + }); } - } - - console.log('βœ… [Server] БообщСния ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½Ρ‹:', data?.length || 0, 'ΡˆΡ‚.'); - res.json(data?.reverse() || []); // Π’ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅ΠΌ Π² хронологичСском порядкС + } + + res.json(data); } catch (err) { - console.log('❌ [Server] НСоТиданная ошибка:', err); - res.status(500).json({ error: 'Internal server error' }); + res.status(500).json({ error: 'Unexpected error occurred' }); } }); @@ -94,6 +89,9 @@ router.post('/messages', async (req, res) => { ...newMessage, user_profiles: userProfile || null }; + + // ΠžΡ‚ΠΏΡ€Π°Π²ΠΊΠ° Ρ‡Π΅Ρ€Π΅Π· Socket.IO Ρ‚Π΅ΠΏΠ΅Ρ€ΡŒ происходит автоматичСски Ρ‡Π΅Ρ€Π΅Π· Supabase Real-time подписку + // Π­Ρ‚ΠΎ ΠΏΡ€Π΅Π΄ΠΎΡ‚Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ Π΄ΡƒΠ±Π»ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ сообщСний res.json(data); }); diff --git a/server/routers/kfu-m-24-1/sber_mobile/moderate.js b/server/routers/kfu-m-24-1/sber_mobile/moderate.js new file mode 100644 index 0000000..0e8b3f8 --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/moderate.js @@ -0,0 +1,164 @@ +const router = require('express').Router(); +const { moderationText } = require('./initiatives-ai-agents/moderation.ts'); +const { generatePicture } = require('./initiatives-ai-agents/picture.ts'); +const { getSupabaseClient } = require('./supabaseClient'); +const { getGigaAuth } = require('./get-constants'); + +async function getGigaKey() { + const GIGA_AUTH = await getGigaAuth(); + return GIGA_AUTH; + } + +// ΠžΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ для ΠΌΠΎΠ΄Π΅Ρ€Π°Ρ†ΠΈΠΈ ΠΈ создания ΠΈΠ½ΠΈΡ†ΠΈΠ°Ρ‚ΠΈΠ²Ρ‹ +router.post('/moderate', async (req, res) => { + + const GIGA_AUTH = await getGigaKey(); + + try { + const { title, description, building_id, creator_id, target_amount, status } = req.body; + + if (!title || !description) { + res.status(400).json({ error: 'Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ ΠΈ описаниС ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹' }); + return; + } + + if (!building_id || !creator_id) { + res.status(400).json({ error: 'ID Π΄ΠΎΠΌΠ° ΠΈ создатСля ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹' }); + return; + } + + // Валидация статуса, Ссли ΠΏΠ΅Ρ€Π΅Π΄Π°Π½ + const validStatuses = ['moderation', 'review', 'fundraising', 'approved', 'rejected']; + if (status && !validStatuses.includes(status)) { + res.status(400).json({ error: `НСдопустимый статус. ДопустимыС значСния: ${validStatuses.join(', ')}` }); + return; + } + + console.log('Запрос Π½Π° ΠΌΠΎΠ΄Π΅Ρ€Π°Ρ†ΠΈΡŽ:', { title: title.substring(0, 50), description: description.substring(0, 100) }); + + // ΠœΠΎΠ΄Π΅Ρ€Π°Ρ†ΠΈΡ тСкста (ΠΏΠ΅Ρ€Π΅Π΄Π°Π΅ΠΌ title ΠΈ description ΠΊΠ°ΠΊ body) + const [comment, fixedText, isApproved] = await moderationText(title, description, GIGA_AUTH); + + console.log('Π Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚ ΠΌΠΎΠ΄Π΅Ρ€Π°Ρ†ΠΈΠΈ ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½:', { comment, fixedText: fixedText?.substring(0, 100), isApproved }); + + // Если модСрация Π½Π΅ ΠΏΡ€ΠΎΡˆΠ»Π°, Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅ΠΌ undefined + if (!isApproved) { + if (!comment || comment.trim() === '') { + console.warn('ΠžΠ±Π½Π°Ρ€ΡƒΠΆΠ΅Π½ Π½Π΅ΠΊΠΎΡ€Ρ€Π΅ΠΊΡ‚Π½Ρ‹ΠΉ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚ ΠΌΠΎΠ΄Π΅Ρ€Π°Ρ†ΠΈΠΈ - пустой ΠΊΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΉ ΠΏΡ€ΠΈ ΠΎΡ‚ΠΊΠ»ΠΎΠ½Π΅Π½ΠΈΠΈ'); + } + + res.json({ + comment, + fixedText, + isApproved, + initiative: undefined + }); + return; + } + + // ΠœΠΎΠ΄Π΅Ρ€Π°Ρ†ΠΈΡ ΠΏΡ€ΠΎΡˆΠ»Π°, Π³Π΅Π½Π΅Ρ€ΠΈΡ€ΡƒΠ΅ΠΌ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΡ Π·Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ ΠΊΠ°ΠΊ ΠΏΡ€ΠΎΠΌΠΏΡ‚ + console.log('ΠœΠΎΠ΄Π΅Ρ€Π°Ρ†ΠΈΡ ΠΏΡ€ΠΎΡˆΠ»Π°, Π³Π΅Π½Π΅Ρ€ΠΈΡ€ΡƒΠ΅ΠΌ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ с ΠΏΡ€ΠΎΠΌΠΏΡ‚ΠΎΠΌ:', title); + + const imageBuffer = await generatePicture(title, GIGA_AUTH); + + if (!imageBuffer || imageBuffer.length === 0) { + res.status(500).json({ error: 'ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ пустой Π±ΡƒΡ„Π΅Ρ€ изобраТСния' }); + return; + } + + // ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ Supabase ΠΊΠ»ΠΈΠ΅Π½Ρ‚ ΠΈ создаСм имя Ρ„Π°ΠΉΠ»Π° + const supabase = getSupabaseClient(); + const timestamp = Date.now(); + const filename = `image_${creator_id}_${timestamp}.jpg`; + + // Π—Π°Π³Ρ€ΡƒΠΆΠ°Π΅ΠΌ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ Π² Supabase Storage + let uploadResult; + let retries = 0; + const maxRetries = 5; + + while (retries < maxRetries) { + try { + uploadResult = await supabase.storage + .from('images') + .upload(filename, imageBuffer, { + contentType: 'image/jpeg', + upsert: true + }); + + if (!uploadResult.error) { + break; // УспСшная Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠ° + } + + retries++; + + if (retries < maxRetries) { + // Π–Π΄Π΅ΠΌ ΠΏΠ΅Ρ€Π΅Π΄ ΠΏΠΎΠ²Ρ‚ΠΎΡ€Π½ΠΎΠΉ ΠΏΠΎΠΏΡ‹Ρ‚ΠΊΠΎΠΉ + await new Promise(resolve => setTimeout(resolve, 1000 * retries)); + } + } catch (error) { + console.warn(`ΠŸΠΎΠΏΡ‹Ρ‚ΠΊΠ° Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ ${retries + 1} Π½Π΅ΡƒΠ΄Π°Ρ‡Π½Π° (ΠΈΡΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅):`, error.message); + retries++; + + if (retries < maxRetries) { + // Π–Π΄Π΅ΠΌ ΠΏΠ΅Ρ€Π΅Π΄ ΠΏΠΎΠ²Ρ‚ΠΎΡ€Π½ΠΎΠΉ ΠΏΠΎΠΏΡ‹Ρ‚ΠΊΠΎΠΉ + await new Promise(resolve => setTimeout(resolve, 1000 * retries)); + } else { + throw error; // ΠŸΠ΅Ρ€Π΅Π±Ρ€Π°ΡΡ‹Π²Π°Π΅ΠΌ ΠΎΡˆΠΈΠ±ΠΊΡƒ послС всСх ΠΏΠΎΠΏΡ‹Ρ‚ΠΎΠΊ + } + } + } + + if (uploadResult?.error) { + console.error('Supabase storage error after all retries:', uploadResult.error); + res.status(500).json({ error: 'Ошибка ΠΏΡ€ΠΈ сохранСнии изобраТСния послС Π½Π΅ΡΠΊΠΎΠ»ΡŒΠΊΠΈΡ… ΠΏΠΎΠΏΡ‹Ρ‚ΠΎΠΊ' }); + return; + } + + console.log('Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ ΡƒΡΠΏΠ΅ΡˆΠ½ΠΎ Π·Π°Π³Ρ€ΡƒΠΆΠ΅Π½ΠΎ Π² Supabase Storage:', filename); + + // ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ ΠΏΡƒΠ±Π»ΠΈΡ‡Π½Ρ‹ΠΉ URL + const { data: urlData } = supabase.storage + .from('images') + .getPublicUrl(filename); + + // ΠžΠΏΡ€Π΅Π΄Π΅Π»ΡΠ΅ΠΌ статус: Ссли ΠΏΠ΅Ρ€Π΅Π΄Π°Π½ Π² запросС, ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ Π΅Π³ΠΎ, ΠΈΠ½Π°Ρ‡Π΅ 'review' + const finalStatus = status || 'review'; + + // Π‘ΠΎΠ·Π΄Π°Π΅ΠΌ ΠΈΠ½ΠΈΡ†ΠΈΠ°Ρ‚ΠΈΠ²Ρƒ Π² Π±Π°Π·Π΅ Π΄Π°Π½Π½Ρ‹Ρ… + const { data: initiative, error: initiativeError } = await supabase + .from('initiatives') + .insert([{ + building_id, + creator_id, + title: fixedText || title, + description, + status: finalStatus, + target_amount: target_amount || null, + current_amount: 0, + image_url: urlData.publicUrl + }]) + .select() + .single(); + + if (initiativeError) { + console.error('Ошибка создания ΠΈΠ½ΠΈΡ†ΠΈΠ°Ρ‚ΠΈΠ²Ρ‹:', initiativeError); + res.status(500).json({ error: 'Ошибка ΠΏΡ€ΠΈ создании ΠΈΠ½ΠΈΡ†ΠΈΠ°Ρ‚ΠΈΠ²Ρ‹', details: initiativeError.message }); + return; + } + + console.log('Π˜Π½ΠΈΡ†ΠΈΠ°Ρ‚ΠΈΠ²Π° ΡƒΡΠΏΠ΅ΡˆΠ½ΠΎ создана:', initiative.id); + + res.json({ + comment, + fixedText, + isApproved, + initiative + }); + + } catch (error) { + console.error('Error in moderation and initiative creation:', error); + res.status(500).json({ error: 'ВнутрСнняя ошибка сСрвСра', details: error.message }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/polling-chat.js b/server/routers/kfu-m-24-1/sber_mobile/polling-chat.js new file mode 100644 index 0000000..db528ca --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/polling-chat.js @@ -0,0 +1,822 @@ +const { getSupabaseClient, initializationPromise } = require('./supabaseClient'); + +class ChatPollingHandler { + constructor() { + this.connectedClients = new Map(); // user_id -> { user_info, chats: Set(), lastActivity: Date } + this.chatParticipants = new Map(); // chat_id -> Set(user_id) + this.userEventQueues = new Map(); // user_id -> [{id, event, data, timestamp}] + this.eventIdCounter = 0; + this.realtimeSubscription = null; + + // Π˜Π½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·ΠΈΡ€ΡƒΠ΅ΠΌ Supabase подписку с Π·Π°Π΄Π΅Ρ€ΠΆΠΊΠΎΠΉ ΠΈ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ°ΠΌΠΈ + this.initializeWithRetry(); + + // ΠžΡ‡ΠΈΡΡ‚ΠΊΠ° старых событий ΠΊΠ°ΠΆΠ΄Ρ‹Π΅ 5 ΠΌΠΈΠ½ΡƒΡ‚ + setInterval(() => { + this.cleanupOldEvents(); + }, 5 * 60 * 1000); + } + + // Π˜Π½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·Π°Ρ†ΠΈΡ с ΠΏΠΎΠ²Ρ‚ΠΎΡ€Π½Ρ‹ΠΌΠΈ ΠΏΠΎΠΏΡ‹Ρ‚ΠΊΠ°ΠΌΠΈ + async initializeWithRetry() { + try { + // Π‘Π½Π°Ρ‡Π°Π»Π° ΠΆΠ΄Π΅ΠΌ Π·Π°Π²Π΅Ρ€ΡˆΠ΅Π½ΠΈΡ основной ΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·Π°Ρ†ΠΈΠΈ + await initializationPromise; + + this.setupRealtimeSubscription(); + this.testRealtimeConnection(); + return; + + } catch (error) { + console.log('❌ [Supabase] Основная инициализация Π½Π΅ΡƒΠ΄Π°Ρ‡Π½Π°, ΠΏΡ€ΠΎΠ±ΡƒΠ΅ΠΌ Π°Π»ΡŒΡ‚Π΅Ρ€Π½Π°Ρ‚ΠΈΠ²Π½Ρ‹ΠΉ ΠΏΠΎΠ΄Ρ…ΠΎΠ΄'); + } + + // Если основная инициализация Π½Π΅ ΡƒΠ΄Π°Π»Π°ΡΡŒ, ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ ΠΏΠΎΠ²Ρ‚ΠΎΡ€Π½Ρ‹Π΅ ΠΏΠΎΠΏΡ‹Ρ‚ΠΊΠΈ + let attempts = 0; + const maxAttempts = 10; + const baseDelay = 2000; // 2 сСкунды + + while (attempts < maxAttempts) { + try { + attempts++; + + // Π–Π΄Π΅ΠΌ ΠΏΠ΅Ρ€Π΅Π΄ ΠΏΠΎΠΏΡ‹Ρ‚ΠΊΠΎΠΉ + await new Promise(resolve => setTimeout(resolve, baseDelay * attempts)); + + // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ Π³ΠΎΡ‚ΠΎΠ²Π½ΠΎΡΡ‚ΡŒ Supabase ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π° + const supabase = getSupabaseClient(); + if (supabase) { + this.setupRealtimeSubscription(); + this.testRealtimeConnection(); + return; // УспСх, Π²Ρ‹Ρ…ΠΎΠ΄ΠΈΠΌ + } + } catch (error) { + console.log(`❌ [Supabase] ΠŸΠΎΠΏΡ‹Ρ‚ΠΊΠ° #${attempts} Π½Π΅ΡƒΠ΄Π°Ρ‡Π½Π°:`, error.message); + + if (attempts === maxAttempts) { + console.error('❌ [Supabase] ВсС ΠΏΠΎΠΏΡ‹Ρ‚ΠΊΠΈ ΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·Π°Ρ†ΠΈΠΈ исчСрпаны'); + console.error('❌ [Supabase] Realtime подписка Π±ΡƒΠ΄Π΅Ρ‚ нСдоступна'); + return; + } + } + } + } + + // АутСнтификация ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ + async handleAuthentication(req, res) { + const { user_id, token } = req.body; + + if (!user_id) { + res.status(400).json({ error: 'user_id is required' }); + return; + } + + try { + // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ Π² Π±Π°Π·Π΅ Π΄Π°Π½Π½Ρ‹Ρ… + const supabase = getSupabaseClient(); + const { data: userProfile, error } = await supabase + .from('user_profiles') + .select('*') + .eq('id', user_id) + .single(); + + if (error) { + console.log('❌ [Polling Server] ΠŸΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½:', error); + res.status(401).json({ error: 'User not found' }); + return; + } + + // РСгистрируСм ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ + this.connectedClients.set(user_id, { + user_info: { + user_id, + profile: userProfile, + last_seen: new Date() + }, + chats: new Set(), + lastActivity: new Date() + }); + + // Π‘ΠΎΠ·Π΄Π°Π΅ΠΌ ΠΎΡ‡Π΅Ρ€Π΅Π΄ΡŒ событий для ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ + if (!this.userEventQueues.has(user_id)) { + this.userEventQueues.set(user_id, []); + } + + // ДобавляСм событиС Π°ΡƒΡ‚Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ†ΠΈΠΈ Π² ΠΎΡ‡Π΅Ρ€Π΅Π΄ΡŒ + this.addEventToQueue(user_id, 'authenticated', { + message: 'Successfully authenticated', + user: userProfile + }); + + res.json({ + success: true, + message: 'Successfully authenticated', + user: userProfile + }); + + } catch (error) { + console.error('❌ [Polling Server] Ошибка Π°ΡƒΡ‚Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ†ΠΈΠΈ:', error); + res.status(500).json({ error: 'Authentication failed' }); + } + } + + // Π­Π½Π΄ΠΏΠΎΠΈΠ½Ρ‚ для получСния событий (polling) + async handleGetEvents(req, res) { + try { + const { user_id, last_event_id } = req.query; + + if (!user_id) { + res.status(400).json({ error: 'user_id is required' }); + return; + } + + const client = this.connectedClients.get(user_id); + if (!client) { + res.status(401).json({ error: 'Not authenticated' }); + return; + } + + // ОбновляСм врСмя послСднСй активности + client.lastActivity = new Date(); + + // ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ ΠΎΡ‡Π΅Ρ€Π΅Π΄ΡŒ событий ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ + const eventQueue = this.userEventQueues.get(user_id) || []; + + // Π€ΠΈΠ»ΡŒΡ‚Ρ€ΡƒΠ΅ΠΌ события послС last_event_id + const lastEventId = parseInt(last_event_id) || 0; + const newEvents = eventQueue.filter(event => event.id > lastEventId); + + res.json({ + success: true, + events: newEvents, + last_event_id: eventQueue.length > 0 ? Math.max(...eventQueue.map(e => e.id)) : lastEventId + }); + + } catch (error) { + console.error('❌ [Polling Server] Ошибка получСния событий:', error); + res.status(500).json({ error: 'Failed to get events' }); + } + } + + // HTTP эндпоинт для присоСдинСния ΠΊ Ρ‡Π°Ρ‚Ρƒ + async handleJoinChat(req, res) { + try { + const { user_id, chat_id } = req.body; + + if (!user_id || !chat_id) { + res.status(400).json({ error: 'user_id and chat_id are required' }); + return; + } + + const client = this.connectedClients.get(user_id); + if (!client) { + res.status(401).json({ error: 'Not authenticated' }); + return; + } + + // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ, Ρ‡Ρ‚ΠΎ Ρ‡Π°Ρ‚ сущСствуСт ΠΈ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ ΠΈΠΌΠ΅Π΅Ρ‚ доступ ΠΊ Π½Π΅ΠΌΡƒ + const supabase = getSupabaseClient(); + const { data: chat, error } = await supabase + .from('chats') + .select(` + *, + buildings ( + management_company_id, + apartments ( + apartment_residents ( + user_id + ) + ) + ) + `) + .eq('id', chat_id) + .single(); + + if (error || !chat) { + res.status(404).json({ error: 'Chat not found' }); + return; + } + + // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ доступ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ ΠΊ Ρ‡Π°Ρ‚Ρƒ Ρ‡Π΅Ρ€Π΅Π· ΠΊΠ²Π°Ρ€Ρ‚ΠΈΡ€Ρ‹ Π² Π΄ΠΎΠΌΠ΅ + const hasAccess = chat.buildings.apartments.some(apartment => + apartment.apartment_residents.some(resident => + resident.user_id === user_id + ) + ); + + if (!hasAccess) { + res.status(403).json({ error: 'Access denied to this chat' }); + return; + } + + // ДобавляСм ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ Π² Ρ‡Π°Ρ‚ + client.chats.add(chat_id); + + if (!this.chatParticipants.has(chat_id)) { + this.chatParticipants.set(chat_id, new Set()); + } + this.chatParticipants.get(chat_id).add(user_id); + + // ДобавляСм событиС присоСдинСния Π² ΠΎΡ‡Π΅Ρ€Π΅Π΄ΡŒ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ + this.addEventToQueue(user_id, 'joined_chat', { + chat_id, + chat: chat, + message: 'Successfully joined chat' + }); + + // УвСдомляСм Π΄Ρ€ΡƒΠ³ΠΈΡ… участников ΠΎ ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠΈ + this.broadcastToChatExcludeUser(chat_id, user_id, 'user_joined', { + chat_id, + user: client.user_info.profile, + timestamp: new Date() + }); + + res.json({ success: true, message: 'Joined chat successfully' }); + + } catch (error) { + res.status(500).json({ error: 'Failed to join chat' }); + } + } + + // HTTP эндпоинт для покидания Ρ‡Π°Ρ‚Π° + async handleLeaveChat(req, res) { + try { + const { user_id, chat_id } = req.body; + + if (!user_id || !chat_id) { + res.status(400).json({ error: 'user_id and chat_id are required' }); + return; + } + + const client = this.connectedClients.get(user_id); + if (!client) { + res.status(401).json({ error: 'Not authenticated' }); + return; + } + + // УдаляСм ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ ΠΈΠ· Ρ‡Π°Ρ‚Π° + client.chats.delete(chat_id); + + if (this.chatParticipants.has(chat_id)) { + this.chatParticipants.get(chat_id).delete(user_id); + + // Если Ρ‡Π°Ρ‚ пуст, удаляСм Π΅Π³ΠΎ + if (this.chatParticipants.get(chat_id).size === 0) { + this.chatParticipants.delete(chat_id); + } + } + + // УвСдомляСм Π΄Ρ€ΡƒΠ³ΠΈΡ… участников ΠΎΠ± ΠΎΡ‚ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠΈ + this.broadcastToChatExcludeUser(chat_id, user_id, 'user_left', { + chat_id, + user: client.user_info.profile, + timestamp: new Date() + }); + + res.json({ success: true, message: 'Left chat successfully' }); + + } catch (error) { + res.status(500).json({ error: 'Failed to leave chat' }); + } + } + + // HTTP эндпоинт для ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠΈ сообщСния + async handleSendMessage(req, res) { + try { + const { user_id, chat_id, text } = req.body; + + if (!user_id || !chat_id || !text) { + res.status(400).json({ error: 'user_id, chat_id and text are required' }); + return; + } + + const client = this.connectedClients.get(user_id); + if (!client) { + res.status(401).json({ error: 'Not authenticated' }); + return; + } + + if (!client.chats.has(chat_id)) { + res.status(403).json({ error: 'Not joined to this chat' }); + return; + } + + // БохраняСм сообщСниС Π² Π±Π°Π·Ρƒ Π΄Π°Π½Π½Ρ‹Ρ… + const supabase = getSupabaseClient(); + const { data: message, error } = await supabase + .from('messages') + .insert({ + chat_id, + user_id, + text + }) + .select(` + *, + user_profiles ( + id, + full_name, + avatar_url + ) + `) + .single(); + + if (error) { + res.status(500).json({ error: 'Failed to save message' }); + return; + } + + // ΠžΡ‚ΠΏΡ€Π°Π²Π»ΡΠ΅ΠΌ сообщСниС всСм участникам Ρ‡Π°Ρ‚Π° + this.broadcastToChat(chat_id, 'new_message', { + message, + timestamp: new Date() + }); + + res.json({ success: true, message: 'Message sent successfully' }); + + } catch (error) { + res.status(500).json({ error: 'Failed to send message' }); + } + } + + // HTTP эндпоинт для ΠΈΠ½Π΄ΠΈΠΊΠ°Ρ†ΠΈΠΈ пСчатания + async handleTypingStart(req, res) { + try { + const { user_id, chat_id } = req.body; + + if (!user_id || !chat_id) { + res.status(400).json({ error: 'user_id and chat_id are required' }); + return; + } + + const client = this.connectedClients.get(user_id); + if (!client) { + res.status(401).json({ error: 'Not authenticated' }); + return; + } + + if (!client.chats.has(chat_id)) { + res.status(403).json({ error: 'Not joined to this chat' }); + return; + } + + this.broadcastToChatExcludeUser(chat_id, user_id, 'user_typing_start', { + chat_id, + user: client.user_info.profile, + timestamp: new Date() + }); + + res.json({ success: true }); + + } catch (error) { + res.status(500).json({ error: 'Failed to send typing indicator' }); + } + } + + // HTTP эндпоинт для остановки ΠΈΠ½Π΄ΠΈΠΊΠ°Ρ†ΠΈΠΈ пСчатания + async handleTypingStop(req, res) { + try { + const { user_id, chat_id } = req.body; + + if (!user_id || !chat_id) { + res.status(400).json({ error: 'user_id and chat_id are required' }); + return; + } + + const client = this.connectedClients.get(user_id); + if (!client) { + res.status(401).json({ error: 'Not authenticated' }); + return; + } + + if (!client.chats.has(chat_id)) { + res.status(403).json({ error: 'Not joined to this chat' }); + return; + } + + this.broadcastToChatExcludeUser(chat_id, user_id, 'user_typing_stop', { + chat_id, + user: client.user_info.profile, + timestamp: new Date() + }); + + res.json({ success: true }); + + } catch (error) { + res.status(500).json({ error: 'Failed to send typing indicator' }); + } + } + + // ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° ΠΎΡ‚ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΡ ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π° + handleClientDisconnect(user_id) { + const client = this.connectedClients.get(user_id); + if (!client) return; + + // УдаляСм ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ ΠΈΠ· всСх Ρ‡Π°Ρ‚ΠΎΠ² + client.chats.forEach(chat_id => { + if (this.chatParticipants.has(chat_id)) { + this.chatParticipants.get(chat_id).delete(user_id); + + // УвСдомляСм Π΄Ρ€ΡƒΠ³ΠΈΡ… участников ΠΎΠ± ΠΎΡ‚ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠΈ + this.broadcastToChatExcludeUser(chat_id, user_id, 'user_left', { + chat_id, + user: client.user_info.profile, + timestamp: new Date() + }); + + // Если Ρ‡Π°Ρ‚ пуст, удаляСм Π΅Π³ΠΎ + if (this.chatParticipants.get(chat_id).size === 0) { + this.chatParticipants.delete(chat_id); + } + } + }); + + // УдаляСм ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π° + this.connectedClients.delete(user_id); + } + + // Π”ΠΎΠ±Π°Π²Π»Π΅Π½ΠΈΠ΅ события Π² ΠΎΡ‡Π΅Ρ€Π΅Π΄ΡŒ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ + addEventToQueue(user_id, event, data) { + if (!this.userEventQueues.has(user_id)) { + this.userEventQueues.set(user_id, []); + } + + const eventQueue = this.userEventQueues.get(user_id); + const eventId = ++this.eventIdCounter; + + eventQueue.push({ + id: eventId, + event, + data, + timestamp: new Date() + }); + + // ΠžΠ³Ρ€Π°Π½ΠΈΡ‡ΠΈΠ²Π°Π΅ΠΌ Ρ€Π°Π·ΠΌΠ΅Ρ€ ΠΎΡ‡Π΅Ρ€Π΅Π΄ΠΈ (послСдниС 100 событий) + if (eventQueue.length > 100) { + eventQueue.splice(0, eventQueue.length - 100); + } + } + + // Рассылка события всСм участникам Ρ‡Π°Ρ‚Π° + broadcastToChat(chat_id, event, data) { + const participants = this.chatParticipants.get(chat_id); + if (!participants) return; + + participants.forEach(user_id => { + this.addEventToQueue(user_id, event, data); + }); + } + + // Рассылка события всСм участникам Ρ‡Π°Ρ‚Π° ΠΊΡ€ΠΎΠΌΠ΅ отправитСля + broadcastToChatExcludeUser(chat_id, exclude_user_id, event, data) { + const participants = this.chatParticipants.get(chat_id); + if (!participants) return; + + participants.forEach(user_id => { + if (user_id !== exclude_user_id) { + this.addEventToQueue(user_id, event, data); + } + }); + } + + // ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ списка ΠΎΠ½Π»Π°ΠΉΠ½ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ΠΉ Π² Ρ‡Π°Ρ‚Π΅ + getOnlineUsersInChat(chat_id) { + const participants = this.chatParticipants.get(chat_id) || new Set(); + const onlineUsers = []; + const now = new Date(); + const ONLINE_THRESHOLD = 2 * 60 * 1000; // 2 ΠΌΠΈΠ½ΡƒΡ‚Ρ‹ + + participants.forEach(user_id => { + const client = this.connectedClients.get(user_id); + if (client && (now - client.lastActivity) < ONLINE_THRESHOLD) { + onlineUsers.push(client.user_info.profile); + } + }); + + return onlineUsers; + } + + // ΠžΡ‚ΠΏΡ€Π°Π²ΠΊΠ° систСмного сообщСния Π² Ρ‡Π°Ρ‚ + async sendSystemMessage(chat_id, text) { + this.broadcastToChat(chat_id, 'system_message', { + chat_id, + text, + timestamp: new Date() + }); + } + + // ΠžΡ‡ΠΈΡΡ‚ΠΊΠ° старых событий + cleanupOldEvents() { + const now = new Date(); + const MAX_EVENT_AGE = 24 * 60 * 60 * 1000; // 24 часа + const INACTIVE_USER_THRESHOLD = 60 * 60 * 1000; // 1 час + + // ΠžΡ‡ΠΈΡ‰Π°Π΅ΠΌ старыС события + this.userEventQueues.forEach((eventQueue, user_id) => { + const filteredEvents = eventQueue.filter(event => + (now - event.timestamp) < MAX_EVENT_AGE + ); + + if (filteredEvents.length !== eventQueue.length) { + this.userEventQueues.set(user_id, filteredEvents); + } + }); + + // УдаляСм Π½Π΅Π°ΠΊΡ‚ΠΈΠ²Π½Ρ‹Ρ… ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ΠΉ + this.connectedClients.forEach((client, user_id) => { + if ((now - client.lastActivity) > INACTIVE_USER_THRESHOLD) { + this.handleClientDisconnect(user_id); + this.userEventQueues.delete(user_id); + } + }); + } + + // ВСстированиС Real-time подписки + async testRealtimeConnection() { + try { + const supabase = getSupabaseClient(); + if (!supabase) { + return false; + } + + // Π‘ΠΎΠ·Π΄Π°Π΅ΠΌ тСстовый ΠΊΠ°Π½Π°Π» для ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΡ + const testChannel = supabase + .channel('test_connection') + .subscribe((status, error) => { + if (error) { + console.error('❌ [Supabase] ВСстовый ΠΊΠ°Π½Π°Π» - ошибка:', error); + } + + if (status === 'SUBSCRIBED') { + // ΠžΡ‚ΠΏΠΈΡΡ‹Π²Π°Π΅ΠΌΡΡ ΠΎΡ‚ тСстового ΠΊΠ°Π½Π°Π»Π° + setTimeout(() => { + testChannel.unsubscribe(); + }, 2000); + } + }); + + return true; + } catch (error) { + console.error('❌ [Supabase] Ошибка тСстирования Realtime:', error); + return false; + } + } + + // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° статуса подписки + checkSubscriptionStatus() { + if (this.realtimeSubscription) { + return true; + } else { + return false; + } + } + + setupRealtimeSubscription() { + // Π£Π±ΠΈΡ€Π°Π΅ΠΌ setTimeout, Π²Ρ‹Π·Ρ‹Π²Π°Π΅ΠΌ сразу + this._doSetupRealtimeSubscription(); + } + + _doSetupRealtimeSubscription() { + try { + const supabase = getSupabaseClient(); + + if (!supabase) { + console.log('❌ [Supabase] Supabase ΠΊΠ»ΠΈΠ΅Π½Ρ‚ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½'); + throw new Error('Supabase client not available'); + } + + // ΠŸΠΎΠ΄ΠΏΠΈΡΡ‹Π²Π°Π΅ΠΌΡΡ Π½Π° измСнСния Π² Ρ‚Π°Π±Π»ΠΈΡ†Π΅ messages + const subscription = supabase + .channel('messages_changes') + .on( + 'postgres_changes', + { + event: 'INSERT', + schema: 'public', + table: 'messages' + }, + async (payload) => { + try { + const newMessage = payload.new; + if (!newMessage) { + return; + } + + if (!newMessage.chat_id) { + return; + } + + // ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ ΠΏΡ€ΠΎΡ„ΠΈΠ»ΡŒ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ + const { data: userProfile, error: profileError } = await supabase + .from('user_profiles') + .select('id, full_name, avatar_url') + .eq('id', newMessage.user_id) + .single(); + + if (profileError) { + console.error('❌ [Supabase] Ошибка получСния профиля ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ:', profileError); + } + + // ОбъСдиняСм сообщСниС с ΠΏΡ€ΠΎΡ„ΠΈΠ»Π΅ΠΌ + const messageWithProfile = { + ...newMessage, + user_profiles: userProfile || null + }; + + // ΠžΡ‚ΠΏΡ€Π°Π²Π»ΡΠ΅ΠΌ сообщСниС всСм участникам Ρ‡Π°Ρ‚Π° + this.broadcastToChat(newMessage.chat_id, 'new_message', { + message: messageWithProfile, + timestamp: new Date() + }); + + } catch (callbackError) { + console.error('❌ [Supabase] Ошибка Π² ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠ΅ сообщСния:', callbackError); + } + } + ) + .subscribe((status, error) => { + if (error) { + console.error('❌ [Supabase] Ошибка подписки:', error); + } + + if (status === 'CHANNEL_ERROR') { + console.error('❌ [Supabase] Ошибка ΠΊΠ°Π½Π°Π»Π°'); + } else if (status === 'TIMED_OUT') { + console.error('❌ [Supabase] Π’Π°ΠΉΠΌΠ°ΡƒΡ‚ подписки'); + } + }); + + // БохраняСм ссылку Π½Π° подписку для возмоТности отписки + this.realtimeSubscription = subscription; + + } catch (error) { + console.error('❌ [Supabase] ΠšΡ€ΠΈΡ‚ΠΈΡ‡Π΅ΡΠΊΠ°Ρ ошибка ΠΏΡ€ΠΈ настройкС подписки:', error); + throw error; // ΠŸΡ€ΠΎΠ±Ρ€Π°ΡΡ‹Π²Π°Π΅ΠΌ ΠΎΡˆΠΈΠ±ΠΊΡƒ для ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ Π² initializeWithRetry + } + } + + // ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ статистики ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠΉ + getConnectionStats() { + return { + connectedClients: this.connectedClients.size, + activeChats: this.chatParticipants.size, + totalChatParticipants: Array.from(this.chatParticipants.values()) + .reduce((total, participants) => total + participants.size, 0), + totalEventQueues: this.userEventQueues.size, + totalEvents: Array.from(this.userEventQueues.values()) + .reduce((total, queue) => total + queue.length, 0) + }; + } +} + +// Ѐункция для создания Ρ€ΠΎΡƒΡ‚Π΅Ρ€Π° с polling эндпоинтами +function createChatPollingRouter(express) { + const router = express.Router(); + const chatHandler = new ChatPollingHandler(); + + // CORS middleware для всСх запросов + router.use((req, res, next) => { + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.header('Access-Control-Allow-Headers', 'Content-Type, Cache-Control, Authorization'); + res.header('Access-Control-Allow-Credentials', 'true'); + + // ΠžΠ±Ρ€Π°Π±Π°Ρ‚Ρ‹Π²Π°Π΅ΠΌ OPTIONS запросы + if (req.method === 'OPTIONS') { + res.status(200).end(); + return; + } + + next(); + }); + + // Π­Π½Π΄ΠΏΠΎΠΈΠ½Ρ‚ для Π°ΡƒΡ‚Π΅Π½Ρ‚ΠΈΡ„ΠΈΠΊΠ°Ρ†ΠΈΠΈ + router.post('/auth', (req, res) => { + chatHandler.handleAuthentication(req, res); + }); + + // Π­Π½Π΄ΠΏΠΎΠΈΠ½Ρ‚ для получСния событий (polling) + router.get('/events', (req, res) => { + chatHandler.handleGetEvents(req, res); + }); + + // HTTP эндпоинты для дСйствий + router.post('/join-chat', (req, res) => { + chatHandler.handleJoinChat(req, res); + }); + + router.post('/leave-chat', (req, res) => { + chatHandler.handleLeaveChat(req, res); + }); + + router.post('/send-message', (req, res) => { + chatHandler.handleSendMessage(req, res); + }); + + router.post('/typing-start', (req, res) => { + chatHandler.handleTypingStart(req, res); + }); + + router.post('/typing-stop', (req, res) => { + chatHandler.handleTypingStop(req, res); + }); + + // Π­Π½Π΄ΠΏΠΎΠΈΠ½Ρ‚ для получСния ΠΎΠ½Π»Π°ΠΉΠ½ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ΠΉ Π² Ρ‡Π°Ρ‚Π΅ + router.get('/online-users/:chat_id', (req, res) => { + const { chat_id } = req.params; + const onlineUsers = chatHandler.getOnlineUsersInChat(chat_id); + res.json({ onlineUsers }); + }); + + // Π­Π½Π΄ΠΏΠΎΠΈΠ½Ρ‚ для получСния статистики + router.get('/stats', (req, res) => { + const stats = chatHandler.getConnectionStats(); + res.json(stats); + }); + + // Π­Π½Π΄ΠΏΠΎΠΈΠ½Ρ‚ для ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ статуса Supabase подписки + router.get('/supabase-status', (req, res) => { + const isConnected = chatHandler.checkSubscriptionStatus(); + res.json({ + supabaseSubscriptionActive: isConnected, + subscriptionExists: !!chatHandler.realtimeSubscription, + subscriptionInfo: chatHandler.realtimeSubscription ? { + channel: chatHandler.realtimeSubscription.topic, + state: chatHandler.realtimeSubscription.state + } : null + }); + }); + + // Π­Π½Π΄ΠΏΠΎΠΈΠ½Ρ‚ для ΠΏΡ€ΠΈΠ½ΡƒΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΠ³ΠΎ ΠΏΠ΅Ρ€Π΅ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΡ ΠΊ Supabase + router.post('/reconnect-supabase', (req, res) => { + try { + // ΠžΡ‚ΠΏΠΈΡΡ‹Π²Π°Π΅ΠΌΡΡ ΠΎΡ‚ Ρ‚Π΅ΠΊΡƒΡ‰Π΅ΠΉ подписки + if (chatHandler.realtimeSubscription) { + chatHandler.realtimeSubscription.unsubscribe(); + chatHandler.realtimeSubscription = null; + } + + // Π‘ΠΎΠ·Π΄Π°Π΅ΠΌ Π½ΠΎΠ²ΡƒΡŽ подписку + chatHandler.setupRealtimeSubscription(); + + res.json({ + success: true, + message: 'Reconnection initiated' + }); + } catch (error) { + console.error('❌ [Polling Server] Ошибка ΠΏΠ΅Ρ€Π΅ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΡ:', error); + res.status(500).json({ + success: false, + error: 'Reconnection failed', + details: error.message + }); + } + }); + + // ВСстовый эндпоинт для создания сообщСния Π² ΠΎΠ±Ρ…ΠΎΠ΄ API + router.post('/test-message', async (req, res) => { + const { chat_id, user_id, text } = req.body; + + if (!chat_id || !user_id || !text) { + res.status(400).json({ error: 'chat_id, user_id ΠΈ text ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹' }); + return; + } + + try { + // Π‘ΠΎΠ·Π΄Π°Π΅ΠΌ тСстовоС событиС Π½Π°ΠΏΡ€ΡΠΌΡƒΡŽ + chatHandler.broadcastToChat(chat_id, 'new_message', { + message: { + id: `test_${Date.now()}`, + chat_id, + user_id, + text, + created_at: new Date().toISOString(), + user_profiles: { + id: user_id, + full_name: 'Test User', + avatar_url: null + } + }, + timestamp: new Date() + }); + + res.json({ + success: true, + message: 'Test message sent to polling clients' + }); + } catch (error) { + console.error('❌ [Polling Server] Ошибка ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠΈ тСстового сообщСния:', error); + res.status(500).json({ + success: false, + error: 'Failed to send test message', + details: error.message + }); + } + }); + + return { router, chatHandler }; +} + +module.exports = { + ChatPollingHandler, + createChatPollingRouter +}; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/socket-chat.js b/server/routers/kfu-m-24-1/sber_mobile/socket-chat.js deleted file mode 100644 index d35c2a2..0000000 --- a/server/routers/kfu-m-24-1/sber_mobile/socket-chat.js +++ /dev/null @@ -1,340 +0,0 @@ -const { getSupabaseClient } = require('./supabaseClient'); - -class ChatSocketHandler { - constructor(io) { - this.io = io; - this.onlineUsers = new Map(); // Π₯Ρ€Π°Π½Π΅Π½ΠΈΠ΅ ΠΎΠ½Π»Π°ΠΉΠ½ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ΠΉ: socket.id -> user info - this.chatRooms = new Map(); // Π₯Ρ€Π°Π½Π΅Π½ΠΈΠ΅ участников ΠΊΠΎΠΌΠ½Π°Ρ‚: chat_id -> Set(socket.id) - this.setupSocketHandlers(); - } - - setupSocketHandlers() { - this.io.on('connection', (socket) => { - console.log(`User connected: ${socket.id}`); - - // АутСнтификация ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ - socket.on('authenticate', async (data) => { - await this.handleAuthentication(socket, data); - }); - - // ΠŸΡ€ΠΈΡΠΎΠ΅Π΄ΠΈΠ½Π΅Π½ΠΈΠ΅ ΠΊ Ρ‡Π°Ρ‚Ρƒ - socket.on('join_chat', async (data) => { - await this.handleJoinChat(socket, data); - }); - - // ПокиданиС Ρ‡Π°Ρ‚Π° - socket.on('leave_chat', (data) => { - this.handleLeaveChat(socket, data); - }); - - // ΠžΡ‚ΠΏΡ€Π°Π²ΠΊΠ° сообщСния - socket.on('send_message', async (data) => { - await this.handleSendMessage(socket, data); - }); - - // ΠŸΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ Π½Π°Ρ‡Π°Π» ΠΏΠ΅Ρ‡Π°Ρ‚Π°Ρ‚ΡŒ - socket.on('typing_start', (data) => { - this.handleTypingStart(socket, data); - }); - - // ΠŸΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ Π·Π°ΠΊΠΎΠ½Ρ‡ΠΈΠ» ΠΏΠ΅Ρ‡Π°Ρ‚Π°Ρ‚ΡŒ - socket.on('typing_stop', (data) => { - this.handleTypingStop(socket, data); - }); - - // ΠžΡ‚ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ - socket.on('disconnect', () => { - this.handleDisconnect(socket); - }); - }); - } - - async handleAuthentication(socket, data) { - try { - const { user_id, token } = data; - - if (!user_id) { - socket.emit('auth_error', { message: 'user_id is required' }); - return; - } - - // ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΡŽ ΠΎ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ ΠΈΠ· Π±Π°Π·Ρ‹ Π΄Π°Π½Π½Ρ‹Ρ… - const supabase = getSupabaseClient(); - const { data: userProfile, error } = await supabase - .from('user_profiles') - .select('*') - .eq('id', user_id) - .single(); - - if (error) { - socket.emit('auth_error', { message: 'User not found' }); - return; - } - - // БохраняСм ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΡŽ ΠΎ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ - this.onlineUsers.set(socket.id, { - user_id, - socket_id: socket.id, - profile: userProfile, - last_seen: new Date() - }); - - socket.user_id = user_id; - socket.emit('authenticated', { - message: 'Successfully authenticated', - user: userProfile - }); - - console.log(`User ${user_id} authenticated with socket ${socket.id}`); - } catch (error) { - console.error('Authentication error:', error); - socket.emit('auth_error', { message: 'Authentication failed' }); - } - } - - async handleJoinChat(socket, data) { - try { - const { chat_id } = data; - - if (!socket.user_id) { - socket.emit('error', { message: 'Not authenticated' }); - return; - } - - if (!chat_id) { - socket.emit('error', { message: 'chat_id is required' }); - return; - } - - // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ, Ρ‡Ρ‚ΠΎ Ρ‡Π°Ρ‚ сущСствуСт ΠΈ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ ΠΈΠΌΠ΅Π΅Ρ‚ доступ ΠΊ Π½Π΅ΠΌΡƒ - const supabase = getSupabaseClient(); - const { data: chat, error } = await supabase - .from('chats') - .select(` - *, - buildings ( - management_company_id, - apartments ( - apartment_residents ( - user_id - ) - ) - ) - `) - .eq('id', chat_id) - .single(); - - if (error || !chat) { - socket.emit('error', { message: 'Chat not found' }); - return; - } - - // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ доступ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ ΠΊ Ρ‡Π°Ρ‚Ρƒ Ρ‡Π΅Ρ€Π΅Π· ΠΊΠ²Π°Ρ€Ρ‚ΠΈΡ€Ρ‹ Π² Π΄ΠΎΠΌΠ΅ - const hasAccess = chat.buildings.apartments.some(apartment => - apartment.apartment_residents.some(resident => - resident.user_id === socket.user_id - ) - ); - - if (!hasAccess) { - socket.emit('error', { message: 'Access denied to this chat' }); - return; - } - - // ДобавляСм сокСт Π² ΠΊΠΎΠΌΠ½Π°Ρ‚Ρƒ - socket.join(chat_id); - - // ОбновляСм список участников ΠΊΠΎΠΌΠ½Π°Ρ‚Ρ‹ - if (!this.chatRooms.has(chat_id)) { - this.chatRooms.set(chat_id, new Set()); - } - this.chatRooms.get(chat_id).add(socket.id); - - socket.emit('joined_chat', { - chat_id, - chat: chat, - message: 'Successfully joined chat' - }); - - // УвСдомляСм Π΄Ρ€ΡƒΠ³ΠΈΡ… участников ΠΎ ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠΈ - const userInfo = this.onlineUsers.get(socket.id); - socket.to(chat_id).emit('user_joined', { - chat_id, - user: userInfo?.profile, - timestamp: new Date() - }); - - console.log(`User ${socket.user_id} joined chat ${chat_id}`); - } catch (error) { - console.error('Join chat error:', error); - socket.emit('error', { message: 'Failed to join chat' }); - } - } - - handleLeaveChat(socket, data) { - const { chat_id } = data; - - if (!chat_id) return; - - socket.leave(chat_id); - - // УдаляСм ΠΈΠ· списка участников - if (this.chatRooms.has(chat_id)) { - this.chatRooms.get(chat_id).delete(socket.id); - - // Если ΠΊΠΎΠΌΠ½Π°Ρ‚Π° пуста, удаляСм Π΅Ρ‘ - if (this.chatRooms.get(chat_id).size === 0) { - this.chatRooms.delete(chat_id); - } - } - - // УвСдомляСм Π΄Ρ€ΡƒΠ³ΠΈΡ… участников ΠΎΠ± ΠΎΡ‚ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠΈ - const userInfo = this.onlineUsers.get(socket.id); - socket.to(chat_id).emit('user_left', { - chat_id, - user: userInfo?.profile, - timestamp: new Date() - }); - - console.log(`User ${socket.user_id} left chat ${chat_id}`); - } - - async handleSendMessage(socket, data) { - try { - const { chat_id, text } = data; - - if (!socket.user_id) { - socket.emit('error', { message: 'Not authenticated' }); - return; - } - - if (!chat_id || !text) { - socket.emit('error', { message: 'chat_id and text are required' }); - return; - } - - // БохраняСм сообщСниС Π² Π±Π°Π·Ρƒ Π΄Π°Π½Π½Ρ‹Ρ… - const supabase = getSupabaseClient(); - const { data: message, error } = await supabase - .from('messages') - .insert({ - chat_id, - user_id: socket.user_id, - text - }) - .select(` - *, - user_profiles ( - id, - full_name, - avatar_url - ) - `) - .single(); - - if (error) { - socket.emit('error', { message: 'Failed to save message' }); - return; - } - - // ΠžΡ‚ΠΏΡ€Π°Π²Π»ΡΠ΅ΠΌ сообщСниС всСм участникам Ρ‡Π°Ρ‚Π° - this.io.to(chat_id).emit('new_message', { - message, - timestamp: new Date() - }); - - console.log(`Message sent to chat ${chat_id} by user ${socket.user_id}`); - } catch (error) { - console.error('Send message error:', error); - socket.emit('error', { message: 'Failed to send message' }); - } - } - - handleTypingStart(socket, data) { - const { chat_id } = data; - - if (!socket.user_id || !chat_id) return; - - const userInfo = this.onlineUsers.get(socket.id); - socket.to(chat_id).emit('user_typing_start', { - chat_id, - user: userInfo?.profile, - timestamp: new Date() - }); - } - - handleTypingStop(socket, data) { - const { chat_id } = data; - - if (!socket.user_id || !chat_id) return; - - const userInfo = this.onlineUsers.get(socket.id); - socket.to(chat_id).emit('user_typing_stop', { - chat_id, - user: userInfo?.profile, - timestamp: new Date() - }); - } - - handleDisconnect(socket) { - console.log(`User disconnected: ${socket.id}`); - - // УдаляСм ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ ΠΈΠ· всСх ΠΊΠΎΠΌΠ½Π°Ρ‚ - this.chatRooms.forEach((participants, chat_id) => { - if (participants.has(socket.id)) { - participants.delete(socket.id); - - // УвСдомляСм Π΄Ρ€ΡƒΠ³ΠΈΡ… участников ΠΎΠ± ΠΎΡ‚ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠΈ - const userInfo = this.onlineUsers.get(socket.id); - socket.to(chat_id).emit('user_left', { - chat_id, - user: userInfo?.profile, - timestamp: new Date() - }); - - // Если ΠΊΠΎΠΌΠ½Π°Ρ‚Π° пуста, удаляСм Π΅Ρ‘ - if (participants.size === 0) { - this.chatRooms.delete(chat_id); - } - } - }); - - // УдаляСм ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ ΠΈΠ· списка ΠΎΠ½Π»Π°ΠΉΠ½ - this.onlineUsers.delete(socket.id); - } - - // ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ списка ΠΎΠ½Π»Π°ΠΉΠ½ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ΠΉ Π² Ρ‡Π°Ρ‚Π΅ - getOnlineUsersInChat(chat_id) { - const participants = this.chatRooms.get(chat_id) || new Set(); - const onlineUsers = []; - - participants.forEach(socketId => { - const userInfo = this.onlineUsers.get(socketId); - if (userInfo) { - onlineUsers.push(userInfo.profile); - } - }); - - return onlineUsers; - } - - // ΠžΡ‚ΠΏΡ€Π°Π²ΠΊΠ° систСмного сообщСния Π² Ρ‡Π°Ρ‚ - async sendSystemMessage(chat_id, text) { - this.io.to(chat_id).emit('system_message', { - chat_id, - text, - timestamp: new Date() - }); - } -} - -// Ѐункция ΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·Π°Ρ†ΠΈΠΈ Socket.IO для Ρ‡Π°Ρ‚ΠΎΠ² -function initializeChatSocket(io) { - const chatHandler = new ChatSocketHandler(io); - return chatHandler; -} - -module.exports = { - ChatSocketHandler, - initializeChatSocket -}; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js b/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js index 938cc18..0568afa 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js +++ b/server/routers/kfu-m-24-1/sber_mobile/supabaseClient.js @@ -3,12 +3,30 @@ const { createClient } = require('@supabase/supabase-js'); const { getSupabaseUrl, getSupabaseKey, getSupabaseServiceKey } = require('./get-constants'); let supabase = null; +let initializationPromise = null; async function initSupabaseClient() { - const supabaseUrl = await getSupabaseUrl(); - const supabaseAnonKey = await getSupabaseKey(); - const supabaseServiceRoleKey = await getSupabaseServiceKey(); - supabase = createClient(supabaseUrl, supabaseServiceRoleKey); + console.log('πŸ”„ [Supabase Client] НачинаСм ΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·Π°Ρ†ΠΈΡŽ...'); + + try { + console.log('πŸ”„ [Supabase Client] ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡŽ...'); + const supabaseUrl = await getSupabaseUrl(); + const supabaseAnonKey = await getSupabaseKey(); + const supabaseServiceRoleKey = await getSupabaseServiceKey(); + + + if (!supabaseUrl || !supabaseServiceRoleKey) { + throw new Error('Missing required Supabase configuration'); + } + + supabase = createClient(supabaseUrl, supabaseServiceRoleKey); + + return supabase; + + } catch (error) { + console.error('❌ [Supabase Client] Ошибка ΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·Π°Ρ†ΠΈΠΈ:', error); + throw error; + } } function getSupabaseClient() { @@ -20,20 +38,49 @@ function getSupabaseClient() { // POST /refresh-supabase-client router.post('/refresh-supabase-client', async (req, res) => { -try { + try { await initSupabaseClient(); res.json({ success: true, message: 'Supabase client refreshed' }); -} catch (error) { + } catch (error) { + console.error('❌ [Supabase Client] Ошибка обновлСния:', error); res.status(500).json({ error: error.message }); -} + } +}); + +// GET /supabase-client-status +router.get('/supabase-client-status', (req, res) => { + console.log('πŸ” [Supabase Client] ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ статус ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π°...'); + + const isInitialized = !!supabase; + + res.json({ + initialized: isInitialized, + clientExists: !!supabase, + timestamp: new Date().toISOString() + }); }); // Π˜Π½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·Π°Ρ†ΠΈΡ ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π° ΠΏΡ€ΠΈ стартС -(async () => { +initializationPromise = (async () => { + try { await initSupabaseClient(); + } catch (error) { + console.error('❌ [Supabase Client] Ошибка ΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·Π°Ρ†ΠΈΠΈ ΠΏΡ€ΠΈ стартС:', error); + // ΠŸΠ»Π°Π½ΠΈΡ€ΡƒΠ΅ΠΌ ΠΏΠΎΠ²Ρ‚ΠΎΡ€Π½ΡƒΡŽ ΠΏΠΎΠΏΡ‹Ρ‚ΠΊΡƒ Ρ‡Π΅Ρ€Π΅Π· 5 сСкунд + setTimeout(async () => { + try { + await initSupabaseClient(); + } catch (retryError) { + console.error('❌ [Supabase Client] ΠŸΠΎΠ²Ρ‚ΠΎΡ€Π½Π°Ρ инициализация Π½Π΅ΡƒΠ΄Π°Ρ‡Π½Π°:', retryError); + } + }, 5000); + } })(); module.exports = { getSupabaseClient, - supabaseRouter: router + initSupabaseClient, + supabaseRouter: router, + // ЭкспортируСм промис ΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·Π°Ρ†ΠΈΠΈ для возмоТности оТидания + initializationPromise }; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/votes.js b/server/routers/kfu-m-24-1/sber_mobile/votes.js index d46df22..9a861da 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/votes.js +++ b/server/routers/kfu-m-24-1/sber_mobile/votes.js @@ -6,39 +6,100 @@ router.get('/votes/:initiative_id', async (req, res) => { const supabase = getSupabaseClient(); const { initiative_id } = req.params; const { data, error } = await supabase.from('votes').select('*').eq('initiative_id', initiative_id); - if (error) return res.status(400).json({ error: error.message }); + if (error) + return res.status(400).json({ error: error.message }); res.json(data); }); // ΠŸΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ голос ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ ΠΏΠΎ ΠΈΠ½ΠΈΡ†ΠΈΠ°Ρ‚ΠΈΠ²Π΅ -router.get('/votes/:initiative_id/:user_id', async (req, res) => { +router.get('/votes/:initiative_id/user/:user_id', async (req, res) => { const supabase = getSupabaseClient(); const { initiative_id, user_id } = req.params; const { data, error } = await supabase.from('votes').select('*').eq('initiative_id', initiative_id).eq('user_id', user_id).single(); - if (error) return res.status(400).json({ error: error.message }); + if (error) { + console.log(error, '/votes/:initiative_id/:user_id') + console.log(initiative_id, user_id) + return res.status(400).json({ error: error.message }); + } res.json(data); }); -// ΠŸΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ всС голоса ΠΏΠΎ ΠΈΠ½ΠΈΡ†ΠΈΠ°Ρ‚ΠΈΠ²Π΅ (Ρ‡Π΅Ρ€Π΅Π· query) -router.get('/votes', async (req, res) => { +// ΠŸΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ статистику голосов ΠΏΠΎ ΠΈΠ½ΠΈΡ†ΠΈΠ°Ρ‚ΠΈΠ²Π΅ +router.get('/votes/stats/:initiative_id', async (req, res) => { const supabase = getSupabaseClient(); - const { initiative_id } = req.query; - if (!initiative_id) return res.status(400).json({ error: 'initiative_id required' }); - const { data, error } = await supabase.from('votes').select('*').eq('initiative_id', initiative_id); - if (error) return res.status(400).json({ error: error.message }); - res.json(data); + const { initiative_id } = req.params; + + const { data, error } = await supabase + .from('votes') + .select('vote_type') + .eq('initiative_id', initiative_id); + console.log(data, error) + if (error) { + console.log('/votes/:initiative_id/stats') + res.status(400).json({ error: error.message }); + } + const stats = { + for: data.filter(vote => vote.vote_type === 'for').length, + against: data.filter(vote => vote.vote_type === 'against').length, + total: data.length + }; + + res.json(stats); }); -// ΠŸΡ€ΠΎΠ³ΠΎΠ»ΠΎΡΠΎΠ²Π°Ρ‚ΡŒ (ΡΠΎΠ·Π΄Π°Ρ‚ΡŒ ΠΈΠ»ΠΈ ΠΎΠ±Π½ΠΎΠ²ΠΈΡ‚ΡŒ голос) +// ΠŸΡ€ΠΎΠ³ΠΎΠ»ΠΎΡΠΎΠ²Π°Ρ‚ΡŒ (ΡΠΎΠ·Π΄Π°Ρ‚ΡŒ, ΠΎΠ±Π½ΠΎΠ²ΠΈΡ‚ΡŒ ΠΈΠ»ΠΈ ΡƒΠ΄Π°Π»ΠΈΡ‚ΡŒ голос) router.post('/votes', async (req, res) => { const supabase = getSupabaseClient(); const { initiative_id, user_id, vote_type } = req.body; - // upsert: Ссли голос ΡƒΠΆΠ΅ Π΅ΡΡ‚ΡŒ, ΠΎΠ±Π½ΠΎΠ²ΠΈΡ‚ΡŒ, ΠΈΠ½Π°Ρ‡Π΅ ΡΠΎΠ·Π΄Π°Ρ‚ΡŒ - const { data, error } = await supabase.from('votes').upsert([ - { initiative_id, user_id, vote_type } - ], { onConflict: ['initiative_id', 'user_id'] }).select().single(); - if (error) return res.status(400).json({ error: error.message }); - res.json(data); + + // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅ΠΌ ΡΡƒΡ‰Π΅ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΠΉ голос + const { data: existingVote, error: checkError } = await supabase + .from('votes') + .select('*') + .eq('initiative_id', initiative_id) + .eq('user_id', user_id) + .single(); + + if (checkError && checkError.code !== 'PGRST116') { + console.log('1/votes') + return res.status(400).json({ error: checkError.message }); + } + + if (existingVote) { + if (existingVote.vote_type === vote_type) { + // Если Π½Π°ΠΆΠ°Π»ΠΈ Ρ‚ΠΎΡ‚ ΠΆΠ΅ Ρ‚ΠΈΠΏ голоса - Π£Π”ΠΠ›Π―Π•Πœ (отмСняСм голос) + const { error: deleteError } = await supabase + .from('votes') + .delete() + .eq('initiative_id', initiative_id) + .eq('user_id', user_id); + + if (deleteError) return res.status(400).json({ error: deleteError.message }); + res.json({ message: 'Vote removed', action: 'removed', previous_vote: existingVote.vote_type }); + } else { + // Если Π½Π°ΠΆΠ°Π»ΠΈ Π΄Ρ€ΡƒΠ³ΠΎΠΉ Ρ‚ΠΈΠΏ голоса - ΠžΠ‘ΠΠžΠ’Π›Π―Π•Πœ + const { data, error } = await supabase + .from('votes') + .update({ vote_type }) + .eq('initiative_id', initiative_id) + .eq('user_id', user_id) + .select() + .single(); + + if (error) return res.status(400).json({ error: error.message }); + res.json({ ...data, action: 'updated', previous_vote: existingVote.vote_type }); + } + } else { + // Если голоса Π½Π΅Ρ‚ - Π‘ΠžΠ—Π”ΠΠ•Πœ Π½ΠΎΠ²Ρ‹ΠΉ + const { data, error } = await supabase + .from('votes') + .insert([{ initiative_id, user_id, vote_type }]) + .select() + .single(); + + if (error) return res.status(400).json({ error: error.message }); + res.json({ ...data, action: 'created' }); + } }); module.exports = router; \ No newline at end of file