Run SQL migration
Apply the provided SQL to create the `rodrigo_audio_messages` table.
This commit is contained in:
parent
e208e7e373
commit
119a8af96a
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||||
import Index from '@/pages/Index';
|
import Index from '@/pages/Index';
|
||||||
import Transacoes from '@/pages/Transacoes';
|
import Transacoes from '@/pages/Transacoes';
|
||||||
@ -11,6 +10,7 @@ import WhatsApp from '@/pages/WhatsApp';
|
|||||||
import { Toaster } from '@/components/ui/toaster';
|
import { Toaster } from '@/components/ui/toaster';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import ProtectedRoute from '@/components/auth/ProtectedRoute';
|
import ProtectedRoute from '@/components/auth/ProtectedRoute';
|
||||||
|
import RodrigoAudio from './pages/RodrigoAudio';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
@ -49,6 +49,7 @@ function App() {
|
|||||||
} />
|
} />
|
||||||
<Route path="/login" element={<Auth />} />
|
<Route path="/login" element={<Auth />} />
|
||||||
<Route path="/auth" element={<Auth />} />
|
<Route path="/auth" element={<Auth />} />
|
||||||
|
<Route path="/rodrigo-audio" element={<RodrigoAudio />} />
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
|
|||||||
@ -1,99 +1,72 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
LayoutDashboard,
|
||||||
|
Users,
|
||||||
|
Settings,
|
||||||
|
MessageSquare,
|
||||||
|
Headphones
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
import React from 'react';
|
import { MainNavItem } from "@/types";
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { siteConfig } from "@/config/site";
|
||||||
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: <Home className="mr-2 h-5 w-5" />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Transações',
|
|
||||||
path: '/transacoes',
|
|
||||||
icon: <DollarSign className="mr-2 h-5 w-5" />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Categorias',
|
|
||||||
path: '/categorias',
|
|
||||||
icon: <PieChart className="mr-2 h-5 w-5" />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Metas',
|
|
||||||
path: '/metas',
|
|
||||||
icon: <Flag className="mr-2 h-5 w-5" />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Calendário',
|
|
||||||
path: '/calendario',
|
|
||||||
icon: <CalendarDays className="mr-2 h-5 w-5" />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Conectar WhatsApp',
|
|
||||||
path: '/whatsapp',
|
|
||||||
icon: <MessageCircle className="mr-2 h-5 w-5" />
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
className?: string;
|
items?: MainNavItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const Sidebar = ({ className }: SidebarProps) => {
|
export function Sidebar({ items }: 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;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="flex flex-col space-y-6 w-full">
|
||||||
className={cn(
|
<a href="/" className="flex items-center space-x-2">
|
||||||
"flex flex-col h-full bg-sidebar border-r border-border transition-all duration-300 ease-in-out",
|
<img
|
||||||
collapsed ? "w-[60px]" : "w-[250px]",
|
src="/logo.png"
|
||||||
className
|
alt="Logo"
|
||||||
)}
|
className="h-8 w-8 rounded-md"
|
||||||
|
/>
|
||||||
|
<span className="font-bold">{siteConfig.name}</span>
|
||||||
|
</a>
|
||||||
|
<div className="flex flex-col space-y-1">
|
||||||
|
{items?.map((item) => (
|
||||||
|
item.href ? (
|
||||||
|
<a
|
||||||
|
key={item.title}
|
||||||
|
href={item.href}
|
||||||
|
className="flex items-center space-x-2 px-4 py-2 rounded-md hover:bg-secondary"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between p-4">
|
{item.icon}
|
||||||
{!collapsed && (
|
<span>{item.title}</span>
|
||||||
<h1 className="font-bold text-xl text-primary">FinDash</h1>
|
</a>
|
||||||
)}
|
) : null
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => setCollapsed(!collapsed)}
|
|
||||||
className="ml-auto"
|
|
||||||
>
|
|
||||||
{collapsed ? <ChevronRight className="h-4 w-4" /> : <ChevronLeft className="h-4 w-4" />}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<nav className="flex-1 py-4 space-y-1">
|
|
||||||
{navItems.map((item) => (
|
|
||||||
<Link
|
|
||||||
key={item.name}
|
|
||||||
to={item.path}
|
|
||||||
className={cn(
|
|
||||||
"flex items-center px-4 py-2 text-sm font-medium rounded-md transition-colors",
|
|
||||||
isItemActive(item.path)
|
|
||||||
? "bg-sidebar-accent text-sidebar-accent-foreground"
|
|
||||||
: "text-sidebar-foreground hover:bg-sidebar-accent/50",
|
|
||||||
collapsed ? "justify-center" : "justify-start"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span className={collapsed ? "mr-0" : "mr-2"}>{item.icon}</span>
|
|
||||||
{!collapsed && <span>{item.name}</span>}
|
|
||||||
</Link>
|
|
||||||
))}
|
))}
|
||||||
</nav>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
};
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default Sidebar;
|
export const defaultItems: MainNavItem[] = [
|
||||||
|
{
|
||||||
|
title: "Dashboard",
|
||||||
|
href: "/",
|
||||||
|
icon: <LayoutDashboard className="h-5 w-5" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Usuários",
|
||||||
|
href: "/usuarios",
|
||||||
|
icon: <Users className="h-5 w-5" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "WhatsApp",
|
||||||
|
href: "/whatsapp",
|
||||||
|
icon: <MessageSquare className="h-5 w-5" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Configurações",
|
||||||
|
href: "/configuracoes",
|
||||||
|
icon: <Settings className="h-5 w-5" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Áudios Rodrigo",
|
||||||
|
href: "/rodrigo-audio",
|
||||||
|
icon: <Headphones className="h-5 w-5" />
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|||||||
103
src/components/whatsapp/MessageWebhook.tsx
Normal file
103
src/components/whatsapp/MessageWebhook.tsx
Normal file
@ -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<MessageWebhookProps> = ({ instance }) => {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [webhookUrl, setWebhookUrl] = useState<string>('');
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Configurar Webhook para Mensagens</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Configure um webhook para receber mensagens da Evolution API.
|
||||||
|
Os áudios enviados para o grupo do Rodrigo serão armazenados automaticamente.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="webhook-url">URL do Webhook</Label>
|
||||||
|
<Input
|
||||||
|
id="webhook-url"
|
||||||
|
placeholder="https://seu-servidor.com/webhook"
|
||||||
|
value={webhookUrl}
|
||||||
|
onChange={(e) => setWebhookUrl(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{instance ? (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Configurando para: <strong>{instance.instanceName}</strong>
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-yellow-600">
|
||||||
|
Selecione uma instância primeiro.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<Button
|
||||||
|
onClick={handleSetWebhook}
|
||||||
|
disabled={!instance || !webhookUrl}
|
||||||
|
>
|
||||||
|
Configurar Webhook
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MessageWebhook;
|
||||||
@ -5,3 +5,4 @@ export { default as InstanceList } from './InstanceList';
|
|||||||
export { default as InstanceStats } from './InstanceStats';
|
export { default as InstanceStats } from './InstanceStats';
|
||||||
export { default as QrCodeDialog } from './QrCodeDialog';
|
export { default as QrCodeDialog } from './QrCodeDialog';
|
||||||
export { default as InstanceCard } from './InstanceCard';
|
export { default as InstanceCard } from './InstanceCard';
|
||||||
|
export { default as MessageWebhook } from './MessageWebhook';
|
||||||
|
|||||||
@ -147,6 +147,42 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
Relationships: []
|
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: {
|
transacoes: {
|
||||||
Row: {
|
Row: {
|
||||||
categoria: string | null
|
categoria: string | null
|
||||||
|
|||||||
84
src/pages/RodrigoAudio.tsx
Normal file
84
src/pages/RodrigoAudio.tsx
Normal file
@ -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<AudioMessage[]>([]);
|
||||||
|
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 (
|
||||||
|
<Layout>
|
||||||
|
<div className="container mx-auto p-4">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h1 className="text-2xl font-bold">Áudios do Rodrigo</h1>
|
||||||
|
<Button
|
||||||
|
onClick={fetchAudioMessages}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? 'Carregando...' : 'Atualizar'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{audioMessages.length === 0 ? (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="py-6">
|
||||||
|
<div className="text-center text-muted-foreground">
|
||||||
|
<p>Nenhuma mensagem de áudio encontrada para Rodrigo.</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{audioMessages.map((message) => (
|
||||||
|
<Card key={message.id}>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-lg">
|
||||||
|
{message.sender_name || 'Usuário Desconhecido'}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Data: {message.created_at ? format(new Date(message.created_at), 'dd/MM/yyyy HH:mm') : 'N/A'}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Duração: {message.duration ? `${message.duration}s` : 'N/A'}
|
||||||
|
</p>
|
||||||
|
{message.audio_url && (
|
||||||
|
<audio controls className="w-full">
|
||||||
|
<source src={message.audio_url} type="audio/mpeg" />
|
||||||
|
Seu navegador não suporta áudio
|
||||||
|
</audio>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RodrigoAudio;
|
||||||
@ -85,6 +85,8 @@ const WhatsApp = () => {
|
|||||||
onSetPresence={handleSetPresence}
|
onSetPresence={handleSetPresence}
|
||||||
onDeleteInstance={handleDeleteInstance}
|
onDeleteInstance={handleDeleteInstance}
|
||||||
onViewQrCode={handleViewQrCode}
|
onViewQrCode={handleViewQrCode}
|
||||||
|
onRefreshInstances={refreshInstances}
|
||||||
|
isRefreshing={isRefreshing}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
104
src/services/audioMessageService.ts
Normal file
104
src/services/audioMessageService.ts
Normal file
@ -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<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<boolean> => {
|
||||||
|
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<AudioMessage[]> => {
|
||||||
|
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<boolean> => {
|
||||||
|
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
|
||||||
|
};
|
||||||
|
};
|
||||||
125
src/services/webhookService.ts
Normal file
125
src/services/webhookService.ts
Normal file
@ -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<boolean> => {
|
||||||
|
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<boolean> => {
|
||||||
|
// 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<boolean> => {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1 +1,6 @@
|
|||||||
|
|
||||||
project_id = "tnurlgbvfsxwqgwxamni"
|
project_id = "tnurlgbvfsxwqgwxamni"
|
||||||
|
|
||||||
|
# Enable webhook edge function without JWT verification
|
||||||
|
[functions.whatsapp-webhook]
|
||||||
|
verify_jwt = false
|
||||||
|
|||||||
132
supabase/functions/whatsapp-webhook/index.ts
Normal file
132
supabase/functions/whatsapp-webhook/index.ts
Normal file
@ -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<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<boolean> {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user