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 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() {
|
||||
} />
|
||||
<Route path="/login" element={<Auth />} />
|
||||
<Route path="/auth" element={<Auth />} />
|
||||
<Route path="/rodrigo-audio" element={<RodrigoAudio />} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
<Toaster />
|
||||
|
||||
@ -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: <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" />
|
||||
},
|
||||
];
|
||||
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 (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col h-full bg-sidebar border-r border-border transition-all duration-300 ease-in-out",
|
||||
collapsed ? "w-[60px]" : "w-[250px]",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between p-4">
|
||||
{!collapsed && (
|
||||
<h1 className="font-bold text-xl text-primary">FinDash</h1>
|
||||
)}
|
||||
<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>
|
||||
<div className="flex flex-col space-y-6 w-full">
|
||||
<a href="/" className="flex items-center space-x-2">
|
||||
<img
|
||||
src="/logo.png"
|
||||
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"
|
||||
>
|
||||
{item.icon}
|
||||
<span>{item.title}</span>
|
||||
</a>
|
||||
) : null
|
||||
))}
|
||||
</nav>
|
||||
</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 QrCodeDialog } from './QrCodeDialog';
|
||||
export { default as InstanceCard } from './InstanceCard';
|
||||
export { default as MessageWebhook } from './MessageWebhook';
|
||||
|
||||
@ -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
|
||||
|
||||
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}
|
||||
onDeleteInstance={handleDeleteInstance}
|
||||
onViewQrCode={handleViewQrCode}
|
||||
onRefreshInstances={refreshInstances}
|
||||
isRefreshing={isRefreshing}
|
||||
/>
|
||||
</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