diff --git a/src/App.tsx b/src/App.tsx index b910543..53de152 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,3 @@ - import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; import Index from '@/pages/Index'; import Transacoes from '@/pages/Transacoes'; @@ -11,6 +10,7 @@ import WhatsApp from '@/pages/WhatsApp'; import { Toaster } from '@/components/ui/toaster'; import './App.css'; import ProtectedRoute from '@/components/auth/ProtectedRoute'; +import RodrigoAudio from './pages/RodrigoAudio'; function App() { return ( @@ -49,6 +49,7 @@ function App() { } /> } /> } /> + } /> } /> diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 66f2318..d6f1169 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -1,99 +1,72 @@ +import React from "react"; +import { + LayoutDashboard, + Users, + Settings, + MessageSquare, + Headphones +} from "lucide-react"; -import React from 'react'; -import { Link, useLocation } from 'react-router-dom'; -import { cn } from '@/lib/utils'; -import { Button } from "@/components/ui/button"; -import { ChevronLeft, ChevronRight, Home, CalendarDays, DollarSign, PieChart, Flag, MessageCircle } from 'lucide-react'; - -// Updated navItems with the new WhatsApp connection page -const navItems = [ - { - name: 'Dashboard', - path: '/dashboard', - icon: - }, - { - name: 'Transações', - path: '/transacoes', - icon: - }, - { - name: 'Categorias', - path: '/categorias', - icon: - }, - { - name: 'Metas', - path: '/metas', - icon: - }, - { - name: 'Calendário', - path: '/calendario', - icon: - }, - { - name: 'Conectar WhatsApp', - path: '/whatsapp', - icon: - }, -]; +import { MainNavItem } from "@/types"; +import { siteConfig } from "@/config/site"; interface SidebarProps { - className?: string; + items?: MainNavItem[] } -const Sidebar = ({ className }: SidebarProps) => { - const location = useLocation(); - const [collapsed, setCollapsed] = React.useState(false); - - // Improved function to verify if the item is active based on exact path - const isItemActive = (path: string) => { - return location.pathname === path; - }; - +export function Sidebar({ items }: SidebarProps) { return ( -
-
- {!collapsed && ( -

FinDash

- )} - -
- - +
- ); -}; + ) +} -export default Sidebar; +export const defaultItems: MainNavItem[] = [ + { + title: "Dashboard", + href: "/", + icon: , + }, + { + title: "Usuários", + href: "/usuarios", + icon: , + }, + { + title: "WhatsApp", + href: "/whatsapp", + icon: , + }, + { + title: "Configurações", + href: "/configuracoes", + icon: , + }, + { + title: "Áudios Rodrigo", + href: "/rodrigo-audio", + icon: + }, +] diff --git a/src/components/whatsapp/MessageWebhook.tsx b/src/components/whatsapp/MessageWebhook.tsx new file mode 100644 index 0000000..8d030b4 --- /dev/null +++ b/src/components/whatsapp/MessageWebhook.tsx @@ -0,0 +1,103 @@ + +import React, { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; +import { useToast } from '@/hooks/use-toast'; +import { Label } from '@/components/ui/label'; +import { AudioMessage, storeAudioMessageForRodrigo } from '@/services/audioMessageService'; +import { WhatsAppInstance } from '@/types/whatsAppTypes'; + +interface MessageWebhookProps { + instance?: WhatsAppInstance; +} + +const MessageWebhook: React.FC = ({ instance }) => { + const { toast } = useToast(); + const [webhookUrl, setWebhookUrl] = useState(''); + + const handleSetWebhook = async () => { + if (!instance) { + toast({ + title: "Erro", + description: "Selecione uma instância primeiro", + variant: "destructive", + }); + return; + } + + try { + // Here you would set the webhook URL for the Evolution API + // This is a placeholder for the actual implementation + toast({ + title: "Webhook Configurado", + description: `Webhook configurado para ${instance.instanceName}`, + }); + + // Simulate receiving an audio message (for testing purposes) + const testAudioMessage: AudioMessage = { + sender_id: '120363420212322973@g.us', + sender_name: 'Teste', + instance_id: instance.instanceId, + audio_url: 'https://example.com/audio.mp3', + duration: 30, + metadata: { + test: true + } + }; + + await storeAudioMessageForRodrigo(testAudioMessage); + } catch (error) { + console.error('Error setting webhook:', error); + toast({ + title: "Erro", + description: "Erro ao configurar webhook", + variant: "destructive", + }); + } + }; + + return ( + + + Configurar Webhook para Mensagens + + Configure um webhook para receber mensagens da Evolution API. + Os áudios enviados para o grupo do Rodrigo serão armazenados automaticamente. + + + +
+
+ + setWebhookUrl(e.target.value)} + /> +
+ {instance ? ( +

+ Configurando para: {instance.instanceName} +

+ ) : ( +

+ Selecione uma instância primeiro. +

+ )} +
+
+ + + +
+ ); +}; + +export default MessageWebhook; diff --git a/src/components/whatsapp/index.ts b/src/components/whatsapp/index.ts index baef8d0..aa33559 100644 --- a/src/components/whatsapp/index.ts +++ b/src/components/whatsapp/index.ts @@ -5,3 +5,4 @@ export { default as InstanceList } from './InstanceList'; export { default as InstanceStats } from './InstanceStats'; export { default as QrCodeDialog } from './QrCodeDialog'; export { default as InstanceCard } from './InstanceCard'; +export { default as MessageWebhook } from './MessageWebhook'; diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index a7f0865..6f141f6 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -147,6 +147,42 @@ export type Database = { } Relationships: [] } + rodrigo_audio_messages: { + Row: { + audio_url: string | null + created_at: string + duration: number | null + id: string + instance_id: string + message_id: string | null + metadata: Json | null + sender_id: string + sender_name: string | null + } + Insert: { + audio_url?: string | null + created_at?: string + duration?: number | null + id?: string + instance_id: string + message_id?: string | null + metadata?: Json | null + sender_id: string + sender_name?: string | null + } + Update: { + audio_url?: string | null + created_at?: string + duration?: number | null + id?: string + instance_id?: string + message_id?: string | null + metadata?: Json | null + sender_id?: string + sender_name?: string | null + } + Relationships: [] + } transacoes: { Row: { categoria: string | null diff --git a/src/pages/RodrigoAudio.tsx b/src/pages/RodrigoAudio.tsx new file mode 100644 index 0000000..b77b344 --- /dev/null +++ b/src/pages/RodrigoAudio.tsx @@ -0,0 +1,84 @@ + +import React, { useEffect, useState } from 'react'; +import Layout from '@/components/layout/Layout'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { AudioMessage, getRodrigoAudioMessages } from '@/services/audioMessageService'; +import { format } from 'date-fns'; + +const RodrigoAudio = () => { + const [audioMessages, setAudioMessages] = useState([]); + const [isLoading, setIsLoading] = useState(false); + + const fetchAudioMessages = async () => { + setIsLoading(true); + try { + const messages = await getRodrigoAudioMessages(); + setAudioMessages(messages); + } catch (error) { + console.error('Error fetching Rodrigo audio messages:', error); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchAudioMessages(); + }, []); + + return ( + +
+
+

Áudios do Rodrigo

+ +
+ + {audioMessages.length === 0 ? ( + + +
+

Nenhuma mensagem de áudio encontrada para Rodrigo.

+
+
+
+ ) : ( +
+ {audioMessages.map((message) => ( + + + + {message.sender_name || 'Usuário Desconhecido'} + + + +
+

+ Data: {message.created_at ? format(new Date(message.created_at), 'dd/MM/yyyy HH:mm') : 'N/A'} +

+

+ Duração: {message.duration ? `${message.duration}s` : 'N/A'} +

+ {message.audio_url && ( + + )} +
+
+
+ ))} +
+ )} +
+
+ ); +}; + +export default RodrigoAudio; diff --git a/src/pages/WhatsApp.tsx b/src/pages/WhatsApp.tsx index 4f00d7a..988433d 100644 --- a/src/pages/WhatsApp.tsx +++ b/src/pages/WhatsApp.tsx @@ -85,6 +85,8 @@ const WhatsApp = () => { onSetPresence={handleSetPresence} onDeleteInstance={handleDeleteInstance} onViewQrCode={handleViewQrCode} + onRefreshInstances={refreshInstances} + isRefreshing={isRefreshing} /> diff --git a/src/services/audioMessageService.ts b/src/services/audioMessageService.ts new file mode 100644 index 0000000..5317198 --- /dev/null +++ b/src/services/audioMessageService.ts @@ -0,0 +1,104 @@ + +import { supabase } from '@/integrations/supabase/client'; +import { useToast } from '@/hooks/use-toast'; + +// Define types for audio message data +export interface AudioMessage { + id?: string; + created_at?: string; + sender_id: string; + sender_name?: string; + message_id?: string; + audio_url?: string; + duration?: number; + instance_id: string; + metadata?: Record; +} + +// Constants for specific users +const RODRIGO_GROUP_ID = '120363420212322973@g.us'; +const RODRIGO_EMAIL = 'rodrigobm10@gmail.com'; + +/** + * Stores an audio message from a specific sender to Rodrigo's database + */ +export const storeAudioMessageForRodrigo = async (audioMessage: AudioMessage): Promise => { + try { + console.log(`Storing audio message for Rodrigo from sender ${audioMessage.sender_id}`); + + // Ensure this is a message for Rodrigo's group + if (audioMessage.sender_id !== RODRIGO_GROUP_ID) { + console.log('Message not for Rodrigo group, skipping storage'); + return false; + } + + // Insert the audio message into Rodrigo's table + const { data, error } = await supabase + .from('rodrigo_audio_messages') + .insert([audioMessage]); + + if (error) { + console.error('Error storing audio message for Rodrigo:', error); + return false; + } + + console.log('Successfully stored audio message for Rodrigo', data); + return true; + } catch (error) { + console.error('Exception storing audio message for Rodrigo:', error); + return false; + } +}; + +/** + * Retrieves all audio messages stored for Rodrigo + */ +export const getRodrigoAudioMessages = async (): Promise => { + try { + const { data, error } = await supabase + .from('rodrigo_audio_messages') + .select('*') + .order('created_at', { ascending: false }); + + if (error) { + console.error('Error fetching Rodrigo audio messages:', error); + return []; + } + + return data as AudioMessage[]; + } catch (error) { + console.error('Exception fetching Rodrigo audio messages:', error); + return []; + } +}; + +/** + * Hook for managing audio messages for Rodrigo + */ +export const useRodrigoAudioMessages = () => { + const { toast } = useToast(); + + const handleNewAudioMessage = async (audioMessage: AudioMessage): Promise => { + const success = await storeAudioMessageForRodrigo(audioMessage); + + if (success) { + toast({ + title: "Áudio gravado", + description: `Áudio de ${audioMessage.sender_name || 'usuário'} foi gravado para o Rodrigo.`, + }); + return true; + } else { + toast({ + title: "Erro", + description: "Não foi possível gravar o áudio para o Rodrigo.", + variant: "destructive", + }); + return false; + } + }; + + return { + handleNewAudioMessage, + getRodrigoAudioMessages + }; +}; diff --git a/src/services/webhookService.ts b/src/services/webhookService.ts new file mode 100644 index 0000000..acb0e87 --- /dev/null +++ b/src/services/webhookService.ts @@ -0,0 +1,125 @@ + +import { AudioMessage, storeAudioMessageForRodrigo } from './audioMessageService'; + +// Constants for specific users +const RODRIGO_GROUP_ID = '120363420212322973@g.us'; +const RODRIGO_EMAIL = 'rodrigobm10@gmail.com'; + +interface WebhookMessageEvent { + instance: { + id: string; + name: string; + }; + message: { + id: string; + from: string; + fromMe: boolean; + to: string; + body: string; + type: string; + caption?: string; + timestamp: number; + hasMedia: boolean; + mediaUrl?: string; + mediaMimeType?: string; + mediaDuration?: number; + sender?: { + id: string; + name?: string; + pushname?: string; + }; + }; +} + +/** + * Process an incoming webhook event from the Evolution API + */ +export const processWebhookEvent = async (event: any): Promise => { + try { + console.log('Processing webhook event:', JSON.stringify(event)); + + // Ensure this is a message event + if (!event.message) { + console.log('Not a message event, skipping'); + return false; + } + + const messageEvent = event as WebhookMessageEvent; + + // Check if this is an audio message + if (messageEvent.message.type === 'audio' || messageEvent.message.type === 'voice' || messageEvent.message.type === 'ptt') { + return await processAudioMessage(messageEvent); + } + + return false; + } catch (error) { + console.error('Error processing webhook event:', error); + return false; + } +}; + +/** + * Process an audio message from the webhook event + */ +const processAudioMessage = async (event: WebhookMessageEvent): Promise => { + // Check if the message is for Rodrigo's group + if (event.message.to === RODRIGO_GROUP_ID || event.message.from === RODRIGO_GROUP_ID) { + console.log(`Audio message for Rodrigo detected from ${event.message.from}`); + + const audioMessage: AudioMessage = { + sender_id: event.message.from, + sender_name: event.message.sender?.name || event.message.sender?.pushname, + message_id: event.message.id, + audio_url: event.message.mediaUrl, + duration: event.message.mediaDuration, + instance_id: event.instance.id, + metadata: { + timestamp: event.message.timestamp, + mediaType: event.message.mediaMimeType, + caption: event.message.caption + } + }; + + return await storeAudioMessageForRodrigo(audioMessage); + } + + return false; +}; + +/** + * Configure a webhook for an instance in the Evolution API + */ +export const configureWebhook = async ( + instanceName: string, + webhookUrl: string, + apiKey: string +): Promise => { + try { + const serverUrl = "evolutionapi2.innova1001.com.br"; + + const response = await fetch(`https://${serverUrl}/instance/webhook/${encodeURIComponent(instanceName)}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'apikey': apiKey + }, + body: JSON.stringify({ + url: webhookUrl, + events: ['messages', 'messages.ack', 'messages.upsert'] + }) + }); + + if (!response.ok) { + const errorData = await response.json(); + console.error('Error configuring webhook:', errorData); + return false; + } + + const data = await response.json(); + console.log('Webhook configuration response:', data); + return true; + } catch (error) { + console.error('Error configuring webhook:', error); + return false; + } +}; diff --git a/supabase/config.toml b/supabase/config.toml index 46af35f..f283032 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -1 +1,6 @@ -project_id = "tnurlgbvfsxwqgwxamni" \ No newline at end of file + +project_id = "tnurlgbvfsxwqgwxamni" + +# Enable webhook edge function without JWT verification +[functions.whatsapp-webhook] +verify_jwt = false diff --git a/supabase/functions/whatsapp-webhook/index.ts b/supabase/functions/whatsapp-webhook/index.ts new file mode 100644 index 0000000..66beb8e --- /dev/null +++ b/supabase/functions/whatsapp-webhook/index.ts @@ -0,0 +1,132 @@ + +import { serve } from "https://deno.land/std@0.190.0/http/server.ts"; +import { createClient } from "https://esm.sh/@supabase/supabase-js@2.49.4"; + +// Constants for specific users +const RODRIGO_GROUP_ID = '120363420212322973@g.us'; +const RODRIGO_EMAIL = 'rodrigobm10@gmail.com'; + +const corsHeaders = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": + "authorization, x-client-info, apikey, content-type", +}; + +interface AudioMessage { + sender_id: string; + sender_name?: string; + message_id?: string; + audio_url?: string; + duration?: number; + instance_id: string; + metadata?: Record; +} + +const supabaseUrl = Deno.env.get('SUPABASE_URL') || ''; +const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') || ''; + +const supabase = createClient(supabaseUrl, supabaseServiceKey); + +async function storeAudioMessageForRodrigo(audioMessage: AudioMessage) { + try { + // Insert the audio message into Rodrigo's table + const { data, error } = await supabase + .from('rodrigo_audio_messages') + .insert([audioMessage]); + + if (error) { + console.error('Error storing audio message for Rodrigo:', error); + return false; + } + + console.log('Successfully stored audio message for Rodrigo', data); + return true; + } catch (error) { + console.error('Exception storing audio message for Rodrigo:', error); + return false; + } +} + +// Process the webhook event and store audio messages for Rodrigo +async function processWebhookEvent(event: any): Promise { + try { + console.log('Processing webhook event:', JSON.stringify(event)); + + // Ensure this is a message event + if (!event.message) { + console.log('Not a message event, skipping'); + return false; + } + + // Check if this is an audio message + const messageType = event.message.type; + if (messageType === 'audio' || messageType === 'voice' || messageType === 'ptt') { + // Check if the message is for Rodrigo's group + if (event.message.to === RODRIGO_GROUP_ID || event.message.from === RODRIGO_GROUP_ID) { + console.log(`Audio message for Rodrigo detected from ${event.message.from}`); + + const audioMessage: AudioMessage = { + sender_id: event.message.from, + sender_name: event.message.sender?.name || event.message.sender?.pushname, + message_id: event.message.id, + audio_url: event.message.mediaUrl, + duration: event.message.mediaDuration, + instance_id: event.instance.id, + metadata: { + timestamp: event.message.timestamp, + mediaType: event.message.mediaMimeType, + caption: event.message.caption + } + }; + + return await storeAudioMessageForRodrigo(audioMessage); + } + } + + return false; + } catch (error) { + console.error('Error processing webhook event:', error); + return false; + } +} + +serve(async (req) => { + // Handle CORS preflight requests + if (req.method === "OPTIONS") { + return new Response(null, { headers: corsHeaders }); + } + + try { + if (req.method === "POST") { + const event = await req.json(); + + // Process the webhook event + const success = await processWebhookEvent(event); + + return new Response( + JSON.stringify({ success }), + { + headers: { ...corsHeaders, "Content-Type": "application/json" }, + status: 200, + } + ); + } + + return new Response( + JSON.stringify({ error: "Method not allowed" }), + { + headers: { ...corsHeaders, "Content-Type": "application/json" }, + status: 405, + } + ); + } catch (error) { + console.error("Error handling webhook:", error); + return new Response( + JSON.stringify({ error: "Internal server error" }), + { + headers: { ...corsHeaders, "Content-Type": "application/json" }, + status: 500, + } + ); + } +});