validops-east-1's picture
restructure repo into production layout; add whatsapp-service, tests, ddl, docs, scripts; scrub hardcoded secrets
83d6851
Raw
History Blame Contribute Delete
28.6 kB
package instance_service
import (
"bufio"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"slices"
"sort"
"strings"
"time"
"agentdeck-whatsapp-service/pkg/config"
instance_model "agentdeck-whatsapp-service/pkg/instance/model"
instance_repository "agentdeck-whatsapp-service/pkg/instance/repository"
event_types "agentdeck-whatsapp-service/pkg/internal/event_types"
logger_wrapper "agentdeck-whatsapp-service/pkg/logger"
"agentdeck-whatsapp-service/pkg/utils"
whatsmeow_service "agentdeck-whatsapp-service/pkg/whatsmeow/service"
"go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/types"
)
type InstanceService interface {
Create(data *CreateStruct) (*instance_model.Instance, error)
Connect(data *ConnectStruct, instance *instance_model.Instance) (*instance_model.Instance, string, string, error)
Reconnect(instance *instance_model.Instance) error
Disconnect(instance *instance_model.Instance) (*instance_model.Instance, error)
Logout(instance *instance_model.Instance) (*instance_model.Instance, error)
Status(instance *instance_model.Instance) (*StatusStruct, error)
GetQr(instance *instance_model.Instance) (*QrcodeStruct, error)
Pair(data *PairStruct, instance *instance_model.Instance) (*PairReturnStruct, error)
GetAll() ([]*instance_model.Instance, error)
Info(instanceId string) (*instance_model.Instance, error)
Delete(id string) error
SetProxy(id string, proxyConfig *ProxyConfig) error
SetProxyFromStruct(id string, data *SetProxyStruct) error
RemoveProxy(id string) error
ForceReconnect(instanceId string, number string) error
GetInstanceByToken(token string) (*instance_model.Instance, error)
GetLogs(instanceId string, startDate, endDate time.Time, level string, limit int) ([]logger_wrapper.LogEntry, error)
GetAdvancedSettings(instanceId string) (*instance_model.AdvancedSettings, error)
UpdateAdvancedSettings(instanceId string, settings *instance_model.AdvancedSettings) error
}
type instances struct {
instanceRepository instance_repository.InstanceRepository
config *config.Config
killChannel map[string](chan bool)
clientPointer map[string]*whatsmeow.Client
whatsmeowService whatsmeow_service.WhatsmeowService
loggerWrapper *logger_wrapper.LoggerManager
}
type ProxyConfig struct {
Protocol string `json:"protocol,omitempty"`
Port string `json:"port"`
Password string `json:"password"`
Username string `json:"username"`
Host string `json:"host"`
}
type CreateStruct struct {
InstanceId string `json:"instanceId"`
Name string `json:"name"`
Token string `json:"token"`
Proxy *ProxyConfig `json:"proxy"`
AdvancedSettings *instance_model.AdvancedSettings `json:"advancedSettings"`
}
type ConnectStruct struct {
WebhookUrl string `json:"webhookUrl"`
Subscribe []string `json:"subscribe"`
Immediate bool `json:"immediate"`
Phone string `json:"phone"`
RabbitmqEnable string `json:"rabbitmqEnable"`
WebSocketEnable string `json:"websocketEnable"`
NatsEnable string `json:"natsEnable"`
}
type StatusStruct struct {
Connected bool
LoggedIn bool
myJid *types.JID
Name string
}
type QrcodeStruct struct {
Qrcode string `json:"qrcode"`
Code string `json:"code"`
// Passkey ceremony fields. Populated when the account requires a WebAuthn
// passkey to finish linking (no QR to scan at that point). The manager uses
// PasskeyStage to switch its UI and PasskeyOpenUrl for the
// "Abrir WhatsApp Web" button that launches the passkey ceremony.
PasskeyStage string `json:"passkeyStage,omitempty"`
PasskeyOpenURL string `json:"passkeyOpenUrl,omitempty"`
PasskeyCode string `json:"passkeyCode,omitempty"`
}
type PairStruct struct {
Subscribe []string `json:"subscribe"`
Phone string `json:"phone"`
}
type PairReturnStruct struct {
PairingCode string
}
type SetProxyStruct struct {
Protocol string `json:"protocol,omitempty"`
Host string `json:"host" validate:"required"`
Port string `json:"port" validate:"required"`
Username string `json:"username"`
Password string `json:"password"`
}
type ForceReconnectStruct struct {
Number string `json:"number"`
}
func (i *instances) ensureClientConnected(instanceId string) (*whatsmeow.Client, error) {
logger := i.loggerWrapper.GetLogger(instanceId)
client := i.clientPointer[instanceId]
logger.LogInfo("[%s] Checking client connection status - Client exists: %v", instanceId, client != nil)
if client == nil {
logger.LogInfo("[%s] No client found, attempting to start new instance", instanceId)
err := i.whatsmeowService.StartInstance(instanceId)
if err != nil {
logger.LogError("[%s] Failed to start instance: %v", instanceId, err)
return nil, errors.New("no active session found")
}
logger.LogInfo("[%s] Instance started, waiting 2 seconds...", instanceId)
time.Sleep(2 * time.Second)
client = i.clientPointer[instanceId]
logger.LogInfo("[%s] Checking new client - Exists: %v, Connected: %v",
instanceId,
client != nil,
client != nil && client.IsConnected())
if client == nil || !client.IsConnected() {
logger.LogError("[%s] New client validation failed - Exists: %v, Connected: %v",
instanceId,
client != nil,
client != nil && client.IsConnected())
return nil, errors.New("no active session found")
}
} else if !client.IsConnected() {
logger.LogError("[%s] Existing client is disconnected - Connected status: %v",
instanceId,
client.IsConnected())
return nil, errors.New("client disconnected")
}
logger.LogInfo("[%s] Client successfully validated - Connected: %v", instanceId, client.IsConnected())
return client, nil
}
func (i instances) Create(data *CreateStruct) (*instance_model.Instance, error) {
if data.Proxy != nil {
data.Proxy.Protocol = utils.NormalizeProxyProtocol(data.Proxy.Protocol, data.Proxy.Port)
}
proxyJson, err := json.Marshal(data.Proxy)
if err != nil {
return nil, err
}
findInstance, _ := i.instanceRepository.GetInstanceByName(data.Name)
if findInstance != nil {
return nil, fmt.Errorf("instance already exists")
}
instance := instance_model.Instance{
Id: data.InstanceId,
Name: data.Name,
Token: data.Token,
OsName: i.config.OsName,
Proxy: string(proxyJson),
Connected: false,
ClientName: i.config.ClientName,
}
// Set advanced settings if provided
if data.AdvancedSettings != nil {
instance.AlwaysOnline = data.AdvancedSettings.AlwaysOnline
instance.RejectCall = data.AdvancedSettings.RejectCall
instance.MsgRejectCall = data.AdvancedSettings.MsgRejectCall
instance.ReadMessages = data.AdvancedSettings.ReadMessages
instance.IgnoreGroups = data.AdvancedSettings.IgnoreGroups
instance.IgnoreStatus = data.AdvancedSettings.IgnoreStatus
}
createdInstance, err := i.instanceRepository.Create(instance)
if err != nil {
return nil, err
}
return createdInstance, nil
}
func (i instances) Connect(data *ConnectStruct, instance *instance_model.Instance) (*instance_model.Instance, string, string, error) {
var subscribedEvents []string
i.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Processing subscribe events: %v", instance.Id, data.Subscribe)
if len(data.Subscribe) == 0 {
subscribedEvents = append(subscribedEvents, event_types.MESSAGE)
} else if len(data.Subscribe) > 0 && data.Subscribe[0] == "ALL" {
for _, event := range event_types.AllEventTypes {
subscribedEvents = append(subscribedEvents, event)
}
} else {
for _, arg := range data.Subscribe {
if !event_types.IsEventType(arg) {
i.loggerWrapper.GetLogger(instance.Id).LogWarn("[%s] Message type discarded '%s'", instance.Id, arg)
continue
}
subscribedEvents = append(subscribedEvents, arg)
}
}
eventString := strings.Join(subscribedEvents, ",")
instance.Events = eventString
instance.Webhook = data.WebhookUrl
instance.RabbitmqEnable = data.RabbitmqEnable
instance.NatsEnable = data.NatsEnable
instance.WebSocketEnable = data.WebSocketEnable
err := i.instanceRepository.Update(instance)
if err != nil {
i.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error updating instance: %s", instance.Id, err)
return nil, "", "", err
}
// Verifica se a instância já está rodando
isInstanceRunning := i.clientPointer[instance.Id] != nil
// Sincroniza as configurações na instância em execução (se já estiver conectada)
err = i.whatsmeowService.UpdateInstanceSettings(instance.Id)
if err != nil {
i.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Instance not in runtime yet, will be updated when connected", instance.Id)
isInstanceRunning = false
} else {
i.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Instance settings updated successfully in runtime", instance.Id)
isInstanceRunning = true
}
// Se a instância não estiver rodando, inicia uma nova
if !isInstanceRunning {
i.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Starting new client instance", instance.Id)
i.killChannel[instance.Id] = make(chan bool)
clientData := &whatsmeow_service.ClientData{
Instance: instance,
Subscriptions: subscribedEvents,
Phone: data.Phone,
IsProxy: false,
}
if instance.Proxy != "" || i.config.ProxyHost != "" {
var proxyConfig ProxyConfig
err := json.Unmarshal([]byte(instance.Proxy), &proxyConfig)
if err != nil {
i.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error unmarshalling proxy config: %v", instance.Id, err)
return nil, "", "", err
}
if proxyConfig.Host != "" || i.config.ProxyHost != "" {
clientData.IsProxy = true
}
}
go i.whatsmeowService.StartClient(clientData)
} else {
i.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Instance already running, settings updated without restarting client", instance.Id)
}
// logger.LogInfo("Waiting 1 seconds")
// time.Sleep(1000 * time.Millisecond)
// if i.clientPointer[instance.Id] != nil {
// if !i.clientPointer[instance.Id].IsConnected() {
// return instance, "", "", fmt.Errorf("failed to connect")
// }
// } else {
// return instance, "", "", fmt.Errorf("failed to connect")
// }
return instance, instance.Jid, eventString, nil
}
func (i instances) Reconnect(instance *instance_model.Instance) error {
_, err := i.ensureClientConnected(instance.Id)
if err != nil {
return err
}
return i.whatsmeowService.ReconnectClient(instance.Id)
}
func (i instances) Disconnect(instance *instance_model.Instance) (*instance_model.Instance, error) {
client, err := i.ensureClientConnected(instance.Id)
if err != nil {
return instance, err
}
if client.IsConnected() {
if client.IsLoggedIn() {
i.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Disconnection successful", instance.Id)
i.killChannel[instance.Id] <- true
instance.Events = ""
err := i.instanceRepository.Update(instance)
if err != nil {
return instance, err
}
return instance, nil
}
}
i.loggerWrapper.GetLogger(instance.Id).LogWarn("[%s] Ignoring disconnect as it was not connected", instance.Id)
return instance, nil
}
func (i instances) Logout(instance *instance_model.Instance) (*instance_model.Instance, error) {
client, err := i.ensureClientConnected(instance.Id)
if err != nil {
return instance, err
}
if client.IsLoggedIn() && client.IsConnected() {
err := client.Logout(context.Background())
if err != nil {
return instance, err
}
instance.Connected = false
err = i.instanceRepository.Update(instance)
if err != nil {
return instance, err
}
select {
case i.killChannel[instance.Id] <- true:
case <-time.After(5 * time.Second):
}
delete(i.clientPointer, instance.Id)
delete(i.killChannel, instance.Id)
i.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Logout successful", instance.Id)
return instance, nil
}
if client.IsConnected() {
client.Disconnect()
select {
case i.killChannel[instance.Id] <- true:
case <-time.After(5 * time.Second):
}
delete(i.clientPointer, instance.Id)
delete(i.killChannel, instance.Id)
i.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Disconnection successful", instance.Id)
return instance, nil
}
i.loggerWrapper.GetLogger(instance.Id).LogWarn("[%s] Ignoring logout as it was not connected", instance.Id)
return instance, fmt.Errorf("ignoring logout as it was not connected")
}
func (i instances) Status(instance *instance_model.Instance) (*StatusStruct, error) {
client := i.clientPointer[instance.Id]
if client == nil {
return &StatusStruct{
Connected: false,
LoggedIn: false,
}, nil
}
isConnected := client.IsConnected()
isLoggedIn := client.IsLoggedIn()
var myJid *types.JID
var name string
if isLoggedIn {
myJid = client.Store.ID
name = client.Store.PushName
}
return &StatusStruct{
Connected: isConnected,
LoggedIn: isLoggedIn,
myJid: myJid,
Name: name,
}, nil
}
func (i instances) GetQr(instance *instance_model.Instance) (*QrcodeStruct, error) {
logger := i.loggerWrapper.GetLogger(instance.Id)
client := i.clientPointer[instance.Id]
// Se não há cliente ou o cliente está logado, precisamos iniciar um novo cliente
if client == nil || client.IsLoggedIn() {
if client != nil && client.IsLoggedIn() {
logger.LogInfo("[%s] Client is logged in, starting new instance for QR code", instance.Id)
} else {
logger.LogInfo("[%s] No client found, starting new instance for QR code", instance.Id)
}
// Iniciar nova instância para gerar QR code
err := i.whatsmeowService.StartInstance(instance.Id)
if err != nil {
logger.LogError("[%s] Failed to start instance: %v", instance.Id, err)
return nil, fmt.Errorf("failed to start instance: %w", err)
}
// Aguardar um pouco para o cliente iniciar e gerar QR code
logger.LogInfo("[%s] Waiting for QR code generation...", instance.Id)
time.Sleep(3 * time.Second)
// Verificar novamente se há cliente
client = i.clientPointer[instance.Id]
if client != nil && client.IsLoggedIn() {
return nil, fmt.Errorf("session already logged in")
}
} else if !client.IsConnected() {
// Se o cliente existe mas não está conectado, pode estar aguardando QR code
logger.LogInfo("[%s] Client exists but not connected, checking for existing QR code", instance.Id)
}
// Buscar instância atualizada do banco para pegar o QR code mais recente
instance, err := i.instanceRepository.GetInstanceByID(instance.Id)
if err != nil {
return nil, err
}
// If a passkey ceremony is in progress, there is no QR to scan — return the
// passkey stage + the #wapk openUrl so the manager can render the
// "Abrir WhatsApp Web" button. Checked before the empty-QR branch because
// during a passkey ceremony instance.Qrcode is empty.
if store := i.whatsmeowService.PasskeyCeremonyStore(); store != nil {
if token, state, ok := store.StateByInstance(instance.Id); ok {
logger.LogInfo("[%s] Passkey ceremony active (stage=%s) — returning passkey info instead of QR", instance.Id, state.Stage)
return &QrcodeStruct{
PasskeyStage: state.Stage,
PasskeyCode: state.Code,
PasskeyOpenURL: buildPasskeyOpenURL(token),
}, nil
}
}
code := instance.Qrcode
if code == "" {
// Se não há QR code ainda, aguardar um pouco mais e tentar novamente
logger.LogInfo("[%s] No QR code available yet, waiting a bit more...", instance.Id)
time.Sleep(2 * time.Second)
instance, err = i.instanceRepository.GetInstanceByID(instance.Id)
if err != nil {
return nil, err
}
code = instance.Qrcode
if code == "" {
return nil, fmt.Errorf("no QR code available. Please wait a moment and try again")
}
}
parts := strings.Split(code, "|")
if len(parts) < 2 {
return nil, fmt.Errorf("invalid QR code format")
}
qr := &QrcodeStruct{
Qrcode: parts[0],
Code: parts[1],
}
return qr, nil
}
// buildPasskeyOpenURL builds the URL the manager opens to start the passkey
// ceremony: https://web.whatsapp.com/#wapk=<base64url({t:token,b:publicBase})>.
// publicBase must be the PUBLICLY reachable API base the browser can hit; set it
// via PASSKEY_PUBLIC_URL. Kept in sync with the event handler in whatsmeow.go.
func buildPasskeyOpenURL(token string) string {
publicBase := os.Getenv("PASSKEY_PUBLIC_URL")
if publicBase == "" {
publicBase = "<SET_PASSKEY_PUBLIC_URL>"
}
payload := fmt.Sprintf(`{"t":%q,"b":%q}`, token, publicBase)
wapk := base64.RawURLEncoding.EncodeToString([]byte(payload))
return "https://web.whatsapp.com/#wapk=" + wapk
}
func (i instances) Pair(data *PairStruct, instance *instance_model.Instance) (*PairReturnStruct, error) {
logger := i.loggerWrapper.GetLogger(instance.Id)
client := i.clientPointer[instance.Id]
if client == nil || !client.IsConnected() {
if client != nil && client.IsLoggedIn() {
return nil, fmt.Errorf("instance is already authenticated")
}
logger.LogInfo("[%s] No active connection, starting instance for phone pairing", instance.Id)
if err := i.whatsmeowService.StartInstance(instance.Id); err != nil {
logger.LogError("[%s] Failed to start instance for pairing: %v", instance.Id, err)
return nil, fmt.Errorf("failed to start instance: %w", err)
}
// Wait for the WA websocket connection and initial QR generation to establish.
// PairPhone must be called after the QR event is received per whatsmeow docs.
time.Sleep(3 * time.Second)
client = i.clientPointer[instance.Id]
if client == nil {
return nil, fmt.Errorf("failed to initialize client for pairing")
}
}
if client.IsLoggedIn() {
return nil, fmt.Errorf("instance is already authenticated")
}
code, err := client.PairPhone(context.Background(), data.Phone, true, whatsmeow.PairClientChrome, "Chrome (Linux)")
if err != nil {
logger.LogError("[%s] PairPhone failed: %v", instance.Id, err)
return nil, fmt.Errorf("pairing failed: %w", err)
}
return &PairReturnStruct{PairingCode: code}, nil
}
func (i instances) GetAll() ([]*instance_model.Instance, error) {
instances, err := i.instanceRepository.GetAll(i.config.ClientName)
if err != nil {
return nil, err
}
for _, instance := range instances {
if client := i.clientPointer[instance.Id]; client != nil {
instance.Connected = client.IsLoggedIn()
} else {
instance.Connected = false
}
instance.Proxy = ""
}
return instances, nil
}
func (i instances) Info(instanceId string) (*instance_model.Instance, error) {
instance, err := i.instanceRepository.GetInstanceByID(instanceId)
if err != nil {
return nil, err
}
// Atualiza o status connected com base no estado real do cliente
if client := i.clientPointer[instance.Id]; client != nil {
instance.Connected = client.IsLoggedIn()
} else {
instance.Connected = false
}
instance.Proxy = ""
return instance, nil
}
func (i instances) Delete(id string) error {
instance, err := i.instanceRepository.GetInstanceByID(id)
if err != nil {
return err
}
if i.clientPointer[instance.Id] != nil && i.clientPointer[instance.Id].IsConnected() {
if i.clientPointer[instance.Id].IsLoggedIn() {
i.clientPointer[instance.Id].Logout(context.Background())
}
i.clientPointer[instance.Id].Disconnect()
}
// Limpar todos os recursos da instância antes de deletar
delete(i.clientPointer, instance.Id)
if i.killChannel[instance.Id] != nil {
close(i.killChannel[instance.Id])
delete(i.killChannel, instance.Id)
}
// Limpar cache via whatsmeow service
err = i.whatsmeowService.ClearInstanceCache(instance.Id, instance.Token)
if err != nil {
i.loggerWrapper.GetLogger(instance.Id).LogWarn("[%s] Failed to clear instance cache: %v", instance.Id, err)
}
err = i.instanceRepository.Delete(id)
if err != nil {
return err
}
return nil
}
func (i instances) SetProxy(id string, proxyConfig *ProxyConfig) error {
instance, err := i.instanceRepository.GetInstanceByID(id)
if err != nil {
return err
}
// Validate proxy configuration
if proxyConfig == nil {
return fmt.Errorf("proxy configuration cannot be nil")
}
if proxyConfig.Host == "" {
return fmt.Errorf("proxy host is required")
}
if proxyConfig.Port == "" {
return fmt.Errorf("proxy port is required")
}
proxyConfig.Protocol = utils.NormalizeProxyProtocol(proxyConfig.Protocol, proxyConfig.Port)
// Convert proxy config to JSON
proxyJSON, err := json.Marshal(proxyConfig)
if err != nil {
i.loggerWrapper.GetLogger(id).LogError("[%s] Failed to marshal proxy config: %v", id, err)
return fmt.Errorf("failed to marshal proxy configuration: %v", err)
}
instance.Proxy = string(proxyJSON)
// Update instance in database
err = i.instanceRepository.Update(instance)
if err != nil {
i.loggerWrapper.GetLogger(id).LogError("[%s] Failed to update instance with proxy: %v", id, err)
return err
}
i.loggerWrapper.GetLogger(id).LogInfo("[%s] Proxy configuration updated: %s://%s:%s", id, proxyConfig.Protocol, proxyConfig.Host, proxyConfig.Port)
// Reconnect to apply proxy changes
go i.Reconnect(instance)
return nil
}
func (i instances) SetProxyFromStruct(id string, data *SetProxyStruct) error {
if data == nil {
return fmt.Errorf("proxy data cannot be nil")
}
proxyConfig := &ProxyConfig{
Protocol: data.Protocol,
Host: data.Host,
Port: data.Port,
Username: data.Username,
Password: data.Password,
}
return i.SetProxy(id, proxyConfig)
}
func (i instances) RemoveProxy(id string) error {
instance, err := i.instanceRepository.GetInstanceByID(id)
if err != nil {
return err
}
instance.Proxy = ""
err = i.instanceRepository.Update(instance)
if err != nil {
return err
}
i.loggerWrapper.GetLogger(id).LogInfo("[%s] Proxy configuration removed", id)
go i.Reconnect(instance)
return nil
}
func (i instances) ForceReconnect(instanceId string, number string) error {
if i.clientPointer[instanceId].IsConnected() && i.clientPointer[instanceId].IsLoggedIn() {
return fmt.Errorf("client already connected")
}
err := i.whatsmeowService.ForceUpdateJid(instanceId, number)
if err != nil {
return err
}
instance, err := i.instanceRepository.GetInstanceByID(instanceId)
if err != nil {
return err
}
subscribedEvents := strings.Split(instance.Events, ",")
i.killChannel[instance.Id] = make(chan bool)
clientData := &whatsmeow_service.ClientData{
Instance: instance,
Subscriptions: subscribedEvents,
Phone: "",
IsProxy: false,
}
if instance.Proxy != "" || i.config.ProxyHost != "" {
var proxyConfig ProxyConfig
err := json.Unmarshal([]byte(instance.Proxy), &proxyConfig)
if err != nil {
i.loggerWrapper.GetLogger(instance.Id).LogError("[%s] error unmarshalling proxy config: %v", instance.Id, err)
return err
}
if proxyConfig.Host != "" || i.config.ProxyHost != "" {
clientData.IsProxy = true
}
}
if i.clientPointer[instance.Id] != nil {
client := i.clientPointer[instance.Id]
client.Disconnect()
select {
case i.killChannel[instance.Id] <- true:
case <-time.After(5 * time.Second):
}
delete(i.clientPointer, instance.Id)
delete(i.killChannel, instance.Id)
}
go i.whatsmeowService.StartClient(clientData)
time.Sleep(2 * time.Second)
if i.clientPointer[instance.Id] != nil {
if !i.clientPointer[instance.Id].IsConnected() {
return fmt.Errorf("failed to connect")
}
if !i.clientPointer[instance.Id].IsLoggedIn() {
return fmt.Errorf("failed to login")
}
} else {
return fmt.Errorf("failed to connect")
}
return nil
}
func (i instances) GetInstanceByToken(token string) (*instance_model.Instance, error) {
return i.instanceRepository.GetInstanceByToken(token)
}
func (i instances) GetLogs(instanceId string, startDate, endDate time.Time, level string, limit int) ([]logger_wrapper.LogEntry, error) {
// Inicializa o slice vazio para garantir que nunca retorne null
logs := make([]logger_wrapper.LogEntry, 0)
// Define valores padrão
if limit <= 0 {
limit = 100 // Limite padrão de 100 registros
}
// Se não foi fornecida data inicial, usa 7 dias atrás
if startDate.IsZero() {
startDate = time.Now().AddDate(0, 0, -7)
}
// Se não foi fornecida data final, usa data atual
if endDate.IsZero() {
endDate = time.Now()
}
// Ajusta as datas para início e fim do dia
startDate = time.Date(startDate.Year(), startDate.Month(), startDate.Day(), 0, 0, 0, 0, time.UTC)
endDate = time.Date(endDate.Year(), endDate.Month(), endDate.Day(), 23, 59, 59, 999999999, time.UTC)
// Garante que a data inicial não seja posterior à data final
if startDate.After(endDate) {
return logs, fmt.Errorf("data inicial não pode ser posterior à data final")
}
// Níveis de log válidos
validLevels := map[string]bool{
"INFO": true,
"ERROR": true,
"WARN": true,
"DEBUG": true,
}
var levelArray []string
if level == "" {
// Se nenhum nível foi especificado, usa todos
levelArray = []string{"INFO", "ERROR", "WARN", "DEBUG"}
} else {
// Divide e normaliza os níveis fornecidos
for _, l := range strings.Split(level, ",") {
l = strings.TrimSpace(strings.ToUpper(l))
if !validLevels[l] {
return logs, fmt.Errorf("nível de log inválido: %s", l)
}
levelArray = append(levelArray, l)
}
}
// Lê os logs do arquivo
logPath := filepath.Join(i.config.LogDirectory, instanceId, "instance.log")
file, err := os.Open(logPath)
if err != nil {
if os.IsNotExist(err) {
return logs, nil // Retorna array vazio se arquivo não existir
}
return logs, fmt.Errorf("erro ao abrir arquivo de log: %v", err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
// Aumenta o buffer do scanner para lidar com linhas grandes
const maxCapacity = 1024 * 1024 // 1MB
buf := make([]byte, maxCapacity)
scanner.Buffer(buf, maxCapacity)
for scanner.Scan() {
var entry logger_wrapper.LogEntry
if err := json.Unmarshal(scanner.Bytes(), &entry); err != nil {
continue // Ignora linhas inválidas
}
// Ajusta o timestamp da entrada para UTC para comparação correta
entry.Timestamp = entry.Timestamp.UTC()
// Aplica os filtros
if entry.Timestamp.Before(startDate) || entry.Timestamp.After(endDate) {
continue
}
if !slices.Contains(levelArray, entry.Level) {
continue
}
logs = append(logs, entry)
// Verifica o limite
if len(logs) >= limit {
break
}
}
if err := scanner.Err(); err != nil {
return logs, fmt.Errorf("erro ao ler arquivo de log: %v", err)
}
// Ordena os logs por timestamp em ordem decrescente
sort.Slice(logs, func(i, j int) bool {
return logs[i].Timestamp.After(logs[j].Timestamp)
})
return logs, nil
}
func (i instances) GetAdvancedSettings(instanceId string) (*instance_model.AdvancedSettings, error) {
i.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Getting advanced settings", instanceId)
settings, err := i.instanceRepository.GetAdvancedSettings(instanceId)
if err != nil {
i.loggerWrapper.GetLogger(instanceId).LogError("[%s] Error getting advanced settings: %v", instanceId, err)
return nil, err
}
return settings, nil
}
func (i instances) UpdateAdvancedSettings(instanceId string, settings *instance_model.AdvancedSettings) error {
i.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Updating advanced settings", instanceId)
err := i.instanceRepository.UpdateAdvancedSettings(instanceId, settings)
if err != nil {
i.loggerWrapper.GetLogger(instanceId).LogError("[%s] Error updating advanced settings: %v", instanceId, err)
return err
}
// Sincroniza as configurações na instância em execução
err = i.whatsmeowService.UpdateInstanceAdvancedSettings(instanceId)
if err != nil {
i.loggerWrapper.GetLogger(instanceId).LogWarn("[%s] Error syncing advanced settings to runtime: %v", instanceId, err)
// Não falha a operação, apenas loga o warning
}
i.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Advanced settings updated successfully", instanceId)
return nil
}
func NewInstanceService(
instanceRepository instance_repository.InstanceRepository,
killChannel map[string](chan bool),
clientPointer map[string]*whatsmeow.Client,
whatsmeowService whatsmeow_service.WhatsmeowService,
config *config.Config,
loggerWrapper *logger_wrapper.LoggerManager,
) InstanceService {
return &instances{
instanceRepository: instanceRepository,
killChannel: killChannel,
clientPointer: clientPointer,
whatsmeowService: whatsmeowService,
config: config,
loggerWrapper: loggerWrapper,
}
}