Unipile pkg files

/*Project Structure: ├── adapter.go ├── auth.go ├── client.go ├── email_webhook_handler.go ├── helper.go ├── jobs_webhook.go ├── models │ ├── account.go │ ├── calendar.go │ ├── common.go │ ├── email.go │ ├── linkedin.go │ ├── messaging.go │ ├── page.go │ ├── post_types.go │ └── webhooks.go ├── pg_store.go ├── services │ ├── account_service.go │ ├── calendar_service.go │ ├── email_service.go │ ├── linkedin_service.go │ ├── messaging_service.go │ ├── page_service.go │ ├── post_service.go │ └── webhook_service.go ├── sync_job.go ├── webhook_registration.go

*/

// File: adapter.go

//pkg/providers/unipile/adapter.go

package unipile

import ( "bytes" "context" "database/sql" "encoding/json" "errors" "fmt" "log" "log/slog" "net/http" "net/url" "os" "strings" "sync" "time"

"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
_ "github.com/lib/pq"
"github.com/mitchellh/mapstructure"
core "gitlab.com/webigniter/slop-server/internal/core"
"gitlab.com/webigniter/slop-server/internal/jobmanager"
"gitlab.com/webigniter/slop-server/internal/providers" // Ensure this is the correct path for providers.Store
"gitlab.com/webigniter/slop-server/internal/pubsub"
"gitlab.com/webigniter/slop-server/internal/resources"
"gitlab.com/webigniter/slop-server/pkg/providers/unipile/models"
"gitlab.com/webigniter/slop-server/pkg/providers/unipile/services"

)

const ( ProviderID = "unipile" // Define provider ID constant unipileAccountSyncProcessID = "unipile-account-sync" // Define the process ID for the sync job )

func init() { log.Println("DEBUG: unipile init() registering factory") // Register the factory function directly. The registry will call this. if err := providers.RegisterFactory(ProviderID, newUnipileAdapterFactory); err != nil { // Use log.Fatalf or panic in init() for critical registration errors log.Fatalf("CRITICAL: Failed to register provider factory for '%s': %v", ProviderID, err) } log.Printf("DEBUG: Provider factory for '%s' registration submitted.", ProviderID) }

// newUnipileAdapterFactory is the factory function called by the registry. func newUnipileAdapterFactory(deps providers.ProviderDependencies) (providers.Provider, error) { log.Printf("DEBUG: Factory called for provider: %s", ProviderID)

// Check for required dependencies
if deps.CoreStore == nil {
    return nil, errors.New("factory for unipile requires CoreStore dependency")
}
if deps.EncryptionKey == "" {
    return nil, errors.New("factory for unipile requires EncryptionKey dependency")
}
if deps.PgxPool == nil {
    return nil, errors.New("factory for unipile requires PgxPool dependency")
}
// Check for new dependencies
if deps.ResourceManager == nil {
    return nil, errors.New("factory for unipile requires ResourceManager dependency")
}
if deps.Publisher == nil { // Use the new Publisher field
    return nil, errors.New("factory for unipile requires Publisher dependency")
}

// Call the constructor with all dependencies
adapter, err := NewUnipileAdapter(deps.CoreStore, deps.EncryptionKey, deps.PgxPool, deps.ResourceManager, deps.Publisher, deps.JobManager)
if err != nil {
    return nil, fmt.Errorf("failed to create unipile adapter via factory: %w", err)
}

log.Printf("DEBUG: Instance created for provider: %s", ProviderID)
return adapter, nil

}

// UnipileAdapter implements the providers.Provider interface for Unipile type UnipileAdapter struct { config map[string]interface{} initialized bool slopAdapter providers.SlopAdapter accounts map[string][]models.AccountInfo // Map user_id to their accounts store UnipileStoreInterface // Changed to interface type accountMutex sync.RWMutex coreStore core.PgStore encryptionKey string dbPool *pgxpool.Pool

// Dependencies injected via Initialize
StoreDB     *sql.DB // Added for consistency if needed, though dbPool might suffice
resourceMgr resources.Manager
notifier    pubsub.Publisher
jobMgr      jobmanager.Manager // Corrected field name from jobManager to jobMgr

// Configuration loaded in Initialize
baseURL      *url.URL
apiKey       string
clientSecret string
client       *UnipileClient // Reverted back to custom client type

}

// Compile-time check to ensure UnipileAdapter implements SchemaInitializer var _ providers.SchemaInitializer = (*UnipileAdapter)(nil)

// Compile-time check to ensure UnipileAdapter implements EventInfoProvider var _ providers.EventInfoProvider = (*UnipileAdapter)(nil)

// NewUnipileAdapter creates a new Unipile adapter // Added resourceMgr and notifier (Publisher) parameters func NewUnipileAdapter( coreStore core.PgStore, encryptionKey string, pool pgxpool.Pool, resourceMgr resources.Manager, // Added notifier pubsub.Publisher, // Added (using notifier internally) jobMgr jobmanager.Manager, // Added job manager dependency here ) (*UnipileAdapter, error) { if pool == nil { return nil, errors.New("database pool cannot be nil for UnipileAdapter") } if resourceMgr == nil { return nil, errors.New("resource manager cannot be nil for UnipileAdapter") } if notifier == nil { return nil, errors.New("notifier (publisher) cannot be nil for UnipileAdapter") } if jobMgr == nil { // Add check for job manager return nil, errors.New("job manager cannot be nil for UnipileAdapter") }

// Create the store using the provided pool
store := NewUnipileStoreWithPool(pool)

// Initialize schema here after creating the store
initCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := store.InitializeDatabase(initCtx); err != nil {
    store.Close()
    return nil, fmt.Errorf("failed to initialize unipile database schema: %w", err)
}

return &UnipileAdapter{
    store:         store,
    coreStore:     coreStore,
    encryptionKey: encryptionKey,
    dbPool:        pool,
    resourceMgr:   resourceMgr, // Store the resource manager
    notifier:      notifier,    // Store the notifier (publisher)
    jobMgr:        jobMgr,      // Store the job manager
}, nil

}

// InitializeSchema implements the providers.SchemaInitializer interface func (a UnipileAdapter) InitializeSchema(ctx context.Context, pool pgxpool.Pool) error { // The schema is now initialized in NewUnipileAdapter when the store is created. // This method fulfills the interface but doesn't need to do anything extra // if the store was already initialized. // We could add a check here if needed, but assuming NewUnipileAdapter is called first. log.Printf("Unipile schema initialization check (already done in NewUnipileAdapter)...") // We could potentially re-run initialization or verify here if needed, // but for now, we assume it's done. // err := a.store.InitializeDatabase(ctx) // Re-running might be okay or cause issues depending on idempotency // if err != nil { // log.Printf("ERROR: Failed to re-initialize Unipile database schema: %v", err) // return fmt.Errorf("failed to re-initialize unipile schema: %w", err) // } return nil }

// Add a Shutdown method to close the store func (a *UnipileAdapter) Shutdown() error { if a.store != nil { a.store.Close() } return nil }

func (a *UnipileAdapter) GetRoutes() []providers.Route { return []providers.Route{ { Path: "/webhooks/unipile/auth-link", Method: "POST", Handler: http.HandlerFunc(fiberHandlerToHTTP(a.GenerateAuthLinkHandler())), Description: "Generate Unipile auth link", Auth: true, }, { Path: "/webhooks/unipile/auth-callback", Method: "POST", Handler: http.HandlerFunc(fiberHandlerToHTTP(a.AuthCallbackHandler())), Description: "Receive Unipile auth callback", Auth: false, // No auth for webhooks }, } }

// GetID returns the provider ID func (a *UnipileAdapter) GetID() string { return "unipile" }

// GetName returns the human-readable name func (a *UnipileAdapter) GetName() string { return "Unipile Integration" }

// GetDescription returns the provider description func (a *UnipileAdapter) GetDescription() string { return "Provides access to Unipile communication features." }

// GetCompactDescription returns the compact description of this provider func (a *UnipileAdapter) GetCompactDescription() string { return "Unipile integration for communication data." }

// Initialize configures the adapter based on the provided configuration map. // This method now conforms to the providers.Provider interface. func (a *UnipileAdapter) Initialize(config map[string]interface{}) error { ctx := context.Background() // Use a background context for initialization steps slog.InfoContext(ctx, "Initializing UnipileAdapter with config")

// Store the config
a.config = config

// Verify dependencies assigned in constructor are present
if a.store == nil || a.resourceMgr == nil || a.notifier == nil || a.jobMgr == nil {
    return errors.New("UnipileAdapter Initialize failed: core dependencies (store, resourceMgr, notifier, jobMgr) are nil")
}

// Add debug log to see keys passed in config map
keys := make([]string, 0, len(config))
for k := range config {
    keys = append(keys, k)
}
slog.DebugContext(ctx, "Config keys received in Initialize", "keys", keys)

// --- Configure Base URL (from config or env) ---
baseURLStr := ""
baseURLVal, ok := config["base_url"].(string)
if ok && baseURLVal != "" {
    baseURLStr = baseURLVal
    slog.InfoContext(ctx, "Unipile base_url loaded from config map.")
} else {
    slog.WarnContext(ctx, "Unipile base_url not found or empty in config map, checking environment variable UNIPILE_BASE_URL...")
    baseURLStr = os.Getenv("UNIPILE_BASE_URL")
    if baseURLStr != "" {
        slog.InfoContext(ctx, "Unipile base_url loaded from environment variable UNIPILE_BASE_URL.")
    } else {
        slog.ErrorContext(ctx, "Unipile base_url missing in both config map and environment variable UNIPILE_BASE_URL")
        return fmt.Errorf("unipile base URL is required but not configured")
    }
}
parsedBaseURL, err := url.ParseRequestURI(baseURLStr)
if err != nil {
    slog.ErrorContext(ctx, "Failed to parse Unipile base URL", "url", baseURLStr, "error", err)
    return fmt.Errorf("invalid Unipile base URL '%s': %w", baseURLStr, err)
}
a.baseURL = parsedBaseURL
slog.InfoContext(ctx, "Unipile base URL configured", "url", a.baseURL.String())

// --- Configure API Key (from config or env) ---
apiKeyStr := ""
apiKeyVal, ok := config["api_key"].(string)
if ok && apiKeyVal != "" {
    apiKeyStr = apiKeyVal
    slog.InfoContext(ctx, "Unipile api_key loaded from config map.")
} else {
    slog.WarnContext(ctx, "Unipile api_key not found or empty in config map, checking environment variable UNIPILE_API_KEY...")
    apiKeyStr = os.Getenv("UNIPILE_API_KEY")
    if apiKeyStr != "" {
        slog.InfoContext(ctx, "Unipile api_key loaded from environment variable UNIPILE_API_KEY.")
    } else {
        slog.ErrorContext(ctx, "Unipile api_key missing in both config map and environment variable UNIPILE_API_KEY")
        return fmt.Errorf("unipile API key is required but not configured")
    }
}
if len(apiKeyStr) < 10 { // Basic sanity check
    slog.WarnContext(ctx, "Unipile API key seems short, please verify.")
}
a.apiKey = apiKeyStr
slog.InfoContext(ctx, "Unipile API key configured (length check passed).")

// --- Configure Client Secret (from config or env) ---
clientSecretStr := ""
clientSecretVal, ok := config["client_secret"].(string)
if ok && clientSecretVal != "" {
    clientSecretStr = clientSecretVal
    slog.InfoContext(ctx, "Unipile client_secret loaded from config map.")
} else {
    slog.WarnContext(ctx, "Unipile client_secret not found or empty in config map, checking environment variable UNIPILE_CLIENT_SECRET...")
    clientSecretStr = os.Getenv("UNIPILE_CLIENT_SECRET")
    if clientSecretStr != "" {
        slog.InfoContext(ctx, "Unipile client_secret loaded from environment variable UNIPILE_CLIENT_SECRET.")
    } else {
        slog.WarnContext(ctx, "Unipile client_secret missing in both config map and environment variable UNIPILE_CLIENT_SECRET")
    }
}
a.clientSecret = clientSecretStr
if clientSecretStr != "" {
    slog.InfoContext(ctx, "Unipile client secret configured.")
} else {
    slog.WarnContext(ctx, "Unipile client secret is not configured.")
}

// --- Initialize Unipile Client ---
a.client = NewUnipileClient(baseURLStr, a.apiKey)
a.client.InitServices()
slog.InfoContext(ctx, "Unipile client initialized and services registered.")

// --- Create SLOP adapter ---
a.slopAdapter = providers.NewSlopAdapter(a.client.Registry)
slog.InfoContext(ctx, "SLOP adapter created for Unipile.")

// --- Schedule Account Sync Job (if enabled) ---
enableSyncJob, _ := config["enable_account_sync"].(bool)
if enableSyncJob {
    syncIntervalStr, _ := config["account_sync_interval"].(string)
    if syncIntervalStr == "" {
        syncIntervalStr = "1h" // Default sync interval
        slog.WarnContext(ctx, "account_sync_interval not set in config, defaulting to 1h")
    }

    _, err := time.ParseDuration(syncIntervalStr) // Validate duration format
    if err != nil {
        slog.ErrorContext(ctx, "Invalid account_sync_interval format", "interval", syncIntervalStr, "error", err)
        return fmt.Errorf("invalid account_sync_interval '%s': %w", syncIntervalStr, err)
    }

    slog.InfoContext(ctx, "Account synchronization job is enabled", "schedule", syncIntervalStr)
    err = a.scheduleAccountSyncJob(ctx, syncIntervalStr)
    if err != nil {
        slog.ErrorContext(ctx, "Failed to schedule initial account sync job", "error", err)
        // Decide if this should be a fatal error for initialization
        return fmt.Errorf("failed to schedule account sync job: %w", err)
    }
} else {
    slog.InfoContext(ctx, "Account synchronization job is disabled.")
}

a.initialized = true // Mark as initialized
slog.InfoContext(ctx, "UnipileAdapter initialized successfully")
return nil

}

// GetProviderTools returns all tools provided by this provider func (a *UnipileAdapter) GetProviderTools() []providers.ProviderTool { if !a.initialized { return nil }

// Get all services from the registry
services := a.client.Registry.GetAll()

var tools []providers.ProviderTool
providerID := a.GetID() // Get provider ID ("unipile")

for serviceName, service := range services {
    // Get capabilities for this service
    for _, capability := range service.GetCapabilities() {
        tool := providers.ProviderTool{
            // Set ID to provider_service_toolname format
            ID:          fmt.Sprintf("%s_%s_%s", providerID, serviceName, capability.Name),
            Name:        capability.Name, // Keep simple name for display
            Scope:       "",              // Explicitly empty for local providers
            Description: capability.Description,
            Parameters:  capability.Parameters,
            Examples:    capability.Examples,
        }

        tools = append(tools, tool)
    }
}

return tools

}

// ExecuteTool executes a specific tool with the given parameters func (a *UnipileAdapter) ExecuteTool(ctx context.Context, toolID string, params map[string]interface{}) (interface{}, error) { if !a.initialized { return nil, errors.New("adapter not initialized") }

// toolID should now be in format "unipile_service_capability"
parts := strings.SplitN(toolID, "_", 3) // Split into 3 parts
if len(parts) != 3 || parts[0] != a.GetID() {
    return nil, fmt.Errorf("invalid or mismatched tool ID format: %s", toolID)
}

// providerName := parts[0] // We know this is "unipile"
serviceName := parts[1]
capabilityName := parts[2]

// Execute the capability based on service type
switch serviceName {
case "linkedin":
    return a.executeLinkedInCapability(ctx, capabilityName, params)
case "email":
    return a.executeEmailCapability(ctx, capabilityName, params)
case "messaging":
    return a.executeMessagingCapability(ctx, capabilityName, params)
case "calendar":
    return a.executeCalendarCapability(ctx, capabilityName, params)
case "page":
    return a.executePageCapability(ctx, capabilityName, params)
case "post":
    return a.executePostCapability(ctx, capabilityName, params)
case "account":
    return a.executeAccountCapability(ctx, capabilityName, params)
case "webhook": // Added case for webhook service
    return a.executeWebhookCapability(ctx, capabilityName, params)
default:
    // Attempt to execute via the generic service registry if the switch misses something
    log.Printf("WARN: Service '%s' not explicitly handled in ExecuteTool switch, trying generic registry lookup.", serviceName)
    service, exists := a.client.Registry.Get(serviceName)
    if !exists {
        return nil, fmt.Errorf("unsupported service: %s", serviceName)
    }
    // Check if the capability exists within the service (optional but good practice)
    foundCapability := false
    for _, cap := range service.GetCapabilities() {
        if cap.Name == capabilityName {
            foundCapability = true
            break
        }
    }
    if !foundCapability {
        return nil, fmt.Errorf("capability '%s' not found within service '%s'", capabilityName, serviceName)
    }

    // Execute using the generic service interface
    return service.Execute(ctx, params)

}

}

// ProcessWebhook handles incoming webhook events for Unipile func (a *UnipileAdapter) ProcessWebhook(ctx context.Context, webhookID string, payload map[string]interface{}) (interface{}, error) { if !a.initialized { return nil, errors.New("adapter not initialized") }

log.Printf("Received webhook for ID '%s'", webhookID)

// Delegate based on webhookID
switch webhookID {
case "emails":
    log.Println("Delegating to email webhook processing...")
    var emailPayload models.WebhookPayload
    // Use mapstructure to decode the generic map into the specific struct
    // We will need to add 'mapstructure:\"...\"' tags to the models if not present
    // and handle potential 'time.Time' decoding issues.
    config := &mapstructure.DecoderConfig{
        Result: &emailPayload,
        // Add hook for time.Time decoding from string
        DecodeHook:       mapstructure.StringToTimeHookFunc(time.RFC3339Nano), // Use RFC3339Nano to support milliseconds
        WeaklyTypedInput: true,                                                // Allows some flexibility in types
        Squash:           true,                                                // Allows embedding struct fields
    }
    decoder, err := mapstructure.NewDecoder(config)
    if err != nil {
        log.Printf("Error creating mapstructure decoder for email: %v", err)
        return nil, fmt.Errorf("internal configuration error processing email webhook")
    }
    if err := decoder.Decode(payload); err != nil {
        log.Printf("Error decoding email webhook payload: %v. Payload: %v", err, payload)
        // Return a specific error? Or just log and acknowledge?
        // Let's acknowledge receipt but log the error.
        return map[string]string{"status": "webhook received, payload decoding error"}, nil
    }

    // Log the decoded struct to verify mapstructure results
    log.Printf("Decoded emailPayload: %+v", emailPayload)

    // --- Start: Logic from processEmailWebhook ---
    log.Printf("Processing Email webhook: Subject='%s' Event='%s'", emailPayload.Subject, emailPayload.FromAttendee.DisplayName)
    // Process based on event type
    switch emailPayload.Event {
    case "email_received":
        log.Printf("New email received")
        // Additional processing for received emails
    case "email_sent":
        log.Printf("Email sent")
        // Additional processing for sent emails
    case "email_read":
        log.Printf("Email marked as read")
        // Process read events
    case "email_deleted":
        log.Printf("Email deleted")
        // Process deletion events
    case "email_draft":
        log.Printf("Email draft saved")
        // Process draft events
    default:
        log.Printf("Unknown email event type: %s", emailPayload.Event)
    }

    // Save to database
    err = a.store.SaveEmail(ctx, &emailPayload)
    if err != nil {
        log.Printf("Error saving email webhook from adapter: %v", err)
        // Don't return error here as processing is best-effort after acknowledgement
    }
    // a.processEmailNotifications(&emailPayload) // Call notification logic if needed
    // --- End: Logic from processEmailWebhook ---
    return map[string]string{"status": "email webhook processed"}, nil // Indicate processing is done

case "messages":
    log.Println("Delegating to message webhook processing...")
    var messagePayload models.MessageWebhookPayload
    config := &mapstructure.DecoderConfig{
        Result:           &messagePayload,
        WeaklyTypedInput: true,
        Squash:           true,
        // Add hook for time.Time decoding (assuming Timestamp field is similar)
        DecodeHook: mapstructure.StringToTimeHookFunc(time.RFC3339Nano),
    }
    decoder, err := mapstructure.NewDecoder(config)
    if err != nil {
        log.Printf("Error creating mapstructure decoder for message: %v", err)
        return nil, fmt.Errorf("internal configuration error processing message webhook")
    }
    if err := decoder.Decode(payload); err != nil {
        log.Printf("Error decoding message webhook payload: %v. Payload: %v", err, payload)
        return map[string]string{"status": "webhook received, payload decoding error"}, nil
    }

    // Log the decoded struct to verify mapstructure results
    log.Printf("Decoded messagePayload: %+v", messagePayload)

    // --- Start: Logic from processMessageWebhook ---
    log.Printf("Processing Message webhook: ChatID='%s' Event='%s'", messagePayload.ChatID, messagePayload.Event)
    // Check if the sender is the connected user
    isSentByConnectedUser := messagePayload.AccountInfo.UserID == messagePayload.Sender.AttendeeProviderID
    if isSentByConnectedUser {
        log.Printf("Message sent by the connected user")
    } else {
        log.Printf("Message received from another user")
    }
    // Process based on event type
    switch messagePayload.Event {
    case "message_received":
        log.Printf("New message received")
        // Additional processing for received messages
    case "message_reaction":
        log.Printf("Reaction to message: %s by %s",
            messagePayload.Reaction, messagePayload.ReactionSender.AttendeeName)
        // Process reaction events
    case "message_read":
        log.Printf("Message marked as read")
        // Process read events
    case "message_edited":
        log.Printf("Message edited")
        // Process edit events
    case "message_deleted":
        log.Printf("Message deleted")
        // Process deletion events
    default:
        log.Printf("Unknown message event type: %s", messagePayload.Event)
    }
    // Save to database
    err = a.store.SaveMessage(ctx, &messagePayload)
    if err != nil {
        log.Printf("Error saving message webhook from adapter: %v", err)
        // Don't return error here as processing is best-effort after acknowledgement
    }
    // --- End: Logic from processMessageWebhook ---
    return map[string]string{"status": "message webhook processed"}, nil

case "account-status":
    log.Println("Delegating to account status webhook processing...")
    // The simulation payload uses a "data" key, not "AccountStatus"
    statusData, ok := payload["data"].(map[string]interface{}) // Changed from payload["AccountStatus"]
    if !ok {
        log.Printf("Error: 'data' field not found or not a map in account status payload: %v", payload)
        return map[string]string{"status": "webhook received, invalid account status payload structure"}, nil
    }

    var accountStatusPayload models.AccountStatus
    config := &mapstructure.DecoderConfig{
        Result:           &accountStatusPayload,
        WeaklyTypedInput: true,
        Squash:           true,
        // No time.Time field in AccountStatus, hook not needed here
    }
    decoder, err := mapstructure.NewDecoder(config)
    if err != nil {
        log.Printf("Error creating mapstructure decoder for account status: %v", err)
        return nil, fmt.Errorf("internal configuration error processing account status webhook")
    }

    // Decode the nested map
    if err := decoder.Decode(statusData); err != nil {
        log.Printf("Error decoding account status webhook payload: %v. Payload: %v", err, statusData)
        return map[string]string{"status": "webhook received, payload decoding error"}, nil
    }

    // Log the decoded struct to verify mapstructure results
    log.Printf("Decoded accountStatusPayload: %+v", accountStatusPayload)

    // --- Start: Logic from processAccountStatusWebhook ---
    log.Printf("Processing Account Status webhook: AccountID='%s' Status='%s'", accountStatusPayload.AccountID, accountStatusPayload.Message)
    // Save to database
    err = a.store.SaveAccountStatus(ctx, &accountStatusPayload)
    if err != nil {
        log.Printf("Error saving account status webhook: %v", err)
        // return // Decide if we should stop processing on save error
    }
    // Update account status in the adapter's state
    err = a.store.UpdateAccountStatus(ctx, accountStatusPayload.AccountID, accountStatusPayload.Message)
    if err != nil {
        log.Printf("Error updating account status: %v", err)
    }
    // Handle specific statuses
    a.handleAccountStatus(&accountStatusPayload) // Call helper method (to be added below)
    // --- End: Logic from processAccountStatusWebhook ---
    return map[string]string{"status": "account status webhook processed"}, nil

default:
    log.Printf("Received webhook for unknown or unhandled ID '%s'", webhookID)
    // Return a success message acknowledging receipt for unhandled IDs for now
    return map[string]string{"status": fmt.Sprintf("webhook for ID '%s' received by adapter, no specific handler", webhookID)}, nil
}

}

// ListWebhooks returns all registered webhooks (STUB) func (a *UnipileAdapter) ListWebhooks(ctx context.Context) ([]providers.WebhookInfo, error) { // TODO: Implement actual listing logic by potentially querying Unipile API log.Println("ListWebhooks called on UnipileAdapter (Not Implemented)") // For now, return an empty list or an error return nil, errors.New("ListWebhooks not implemented for Unipile provider") }

// CreateWebhook creates a new webhook (STUB) func (a *UnipileAdapter) CreateWebhook(ctx context.Context, definition providers.WebhookDefinition) (providers.WebhookInfo, error) { // TODO: Implement actual creation logic by calling Unipile API log.Printf("CreateWebhook called on UnipileAdapter (Not Implemented) with definition: %+v", definition) // For now, return an empty info struct and an error return providers.WebhookInfo{}, errors.New("CreateWebhook not implemented for Unipile provider") }

// DeleteWebhook deletes a webhook (STUB) func (a *UnipileAdapter) DeleteWebhook(ctx context.Context, webhookID string) error { // TODO: Implement actual deletion logic by calling Unipile API log.Printf("DeleteWebhook called on UnipileAdapter (Not Implemented) for ID: %s", webhookID) // For now, return an error return errors.New("DeleteWebhook not implemented for Unipile provider") }

// Implementation of service-specific tool execution methods

func (a *UnipileAdapter) executeLinkedInCapability(ctx context.Context, capability string, params map[string]interface{}) (interface{}, error) { // Get the user ID from context // userID, ok := ctx.Value("user_id").(string) // if !ok { // return nil, errors.New("user ID not found in context") // }

// Get the account ID from params or use a default
accountID, _ := params["account_id"].(string)
if accountID == "" {
    // In a real implementation, you might look up the user's LinkedIn account
    accountID = "default_linkedin_account"
}

// Get the LinkedIn service
// service, exists := a.client.Registry.Get("linkedin")
// if !exists {
//  return nil, errors.New("LinkedIn service not available")
// }

// Cast to the specific service type
linkedInService := services.NewLinkedInService(a.client)

// Execute the capability
switch capability {
case "get_profile":
    profileID, _ := params["profile_id"].(string)
    if profileID == "" {
        return nil, errors.New("profile_id is required")
    }
    return linkedInService.GetProfile(ctx, accountID, profileID)

case "search_profiles":
    // Convert params to search request
    searchRequest := &models.LinkedInSearchRequest{}
    // Populate searchRequest from params
    return linkedInService.SearchProfiles(ctx, accountID, searchRequest)

case "get_company":
    companyID, _ := params["company_id"].(string)
    if companyID == "" {
        return nil, errors.New("company_id is required")
    }
    return linkedInService.GetCompany(ctx, accountID, companyID)

case "send_invitation":
    recipientID, _ := params["recipient_id"].(string)
    message, _ := params["message"].(string)
    if recipientID == "" {
        return nil, errors.New("recipient_id is required")
    }
    return linkedInService.SendInvitation(ctx, accountID, recipientID, message)

case "send_message":
    recipientID, _ := params["recipient_id"].(string)
    message, _ := params["message"].(string)
    if recipientID == "" || message == "" {
        return nil, errors.New("recipient_id and message are required")
    }
    return linkedInService.SendMessage(ctx, accountID, recipientID, message)

case "create_post":
    content, _ := params["content"].(string)
    if content == "" {
        return nil, errors.New("content is required")
    }
    // Create a post draft from params
    postDraft := &models.LinkedInPostDraft{
        Content: content,
    }
    // Add other fields from params
    return linkedInService.CreatePost(ctx, accountID, postDraft)

case "search_companies":
    query, _ := params["query"].(string)
    industry, _ := params["industry"].(string)
    limit := 10 // Default
    if limitParam, ok := params["limit"].(float64); ok {
        limit = int(limitParam)
    }
    cursor, _ := params["cursor"].(string)
    return linkedInService.SearchCompanies(ctx, accountID, query, industry, limit, cursor)

case "list_invitations":
    status, _ := params["status"].(string) // Optional
    limit := 10                            // Default
    if limitParam, ok := params["limit"].(float64); ok {
        limit = int(limitParam)
    }
    cursor, _ := params["cursor"].(string)
    return linkedInService.ListInvitations(ctx, accountID, status, limit, cursor)

case "get_invitation":
    invitationID, _ := params["invitation_id"].(string)
    if invitationID == "" {
        return nil, errors.New("invitation_id parameter is required")
    }
    return linkedInService.GetInvitation(ctx, accountID, invitationID)

default:
    return nil, fmt.Errorf("unsupported LinkedIn capability: %s", capability)
}

}

func (a *UnipileAdapter) executeEmailCapability(ctx context.Context, capability string, params map[string]interface{}) (interface{}, error) { // Get the user ID from context // userID, ok := ctx.Value("user_id").(string) // if !ok { // return nil, errors.New("user ID not found in context") // }

// Get the account ID from params or use a default
accountID, _ := params["account_id"].(string)
if accountID == "" {
    // In a real implementation, you might look up the user's email account
    accountID = "default_email_account"
}

// Get the Email service
// service, exists := a.client.Registry.Get("email")
// if !exists {
//  return nil, errors.New("Email service not available")
// }

// Cast to the specific service type
emailService := services.NewEmailService(a.client)

// Execute the capability
switch capability {
case "send_email":
    // Validate required parameters
    to, ok := params["to"].([]interface{})
    if !ok || len(to) == 0 {
        return nil, errors.New("to recipients are required")
    }
    subject, _ := params["subject"].(string)
    if subject == "" {
        return nil, errors.New("subject is required")
    }
    body, _ := params["body"].(string)
    if body == "" {
        return nil, errors.New("body is required")
    }

    // Convert to string slices
    toAddresses := make([]string, len(to))
    for i, addr := range to {
        toAddresses[i] = addr.(string)
    }

    // Handle optional parameters
    var ccAddresses []string
    cc, ok := params["cc"].([]interface{})
    if ok {
        ccAddresses = make([]string, len(cc))
        for i, addr := range cc {
            ccAddresses[i] = addr.(string)
        }
    }

    var bccAddresses []string
    bcc, ok := params["bcc"].([]interface{})
    if ok {
        bccAddresses = make([]string, len(bcc))
        for i, addr := range bcc {
            bccAddresses[i] = addr.(string)
        }
    }

    // Create email draft
    draft := &models.EmailDraft{
        AccountID: accountID,
        To:        toAddresses,
        Cc:        ccAddresses,
        Bcc:       bccAddresses,
        Subject:   subject,
        Body:      body,
    }

    // Handle thread ID if provided
    threadID, _ := params["thread_id"].(string)
    if threadID != "" {
        draft.ThreadID = threadID
    }

    // Send the email
    return emailService.SendEmail(ctx, draft)

case "list_emails":
    // Create query options
    options := models.QueryOptions{}
    limit, ok := params["limit"].(float64)
    if ok {
        options.Limit = int(limit)
    }
    cursor, _ := params["cursor"].(string)
    options.Cursor = cursor

    return emailService.ListEmails(ctx, options)

case "get_email":
    emailID, _ := params["email_id"].(string)
    if emailID == "" {
        return nil, errors.New("email_id is required")
    }
    return emailService.GetEmail(ctx, emailID)

case "delete_email":
    emailID, _ := params["email_id"].(string)
    if emailID == "" {
        return nil, errors.New("email_id is required")
    }
    return emailService.DeleteEmail(ctx, emailID)

case "search_emails":
    // Convert params to search request
    searchRequest := &models.EmailSearchRequest{}
    // Populate search request from params
    query, _ := params["query"].(string)
    searchRequest.Query = query
    // Add other search parameters...

    return emailService.SearchEmails(ctx, searchRequest)

case "list_email_threads":
    options := models.QueryOptions{}
    if limit, ok := params["limit"].(float64); ok { // JSON numbers are often float64
        options.Limit = int(limit)
    }
    options.Cursor, _ = params["cursor"].(string)
    return emailService.ListEmailThreads(ctx, options)

case "get_email_thread":
    threadID, _ := params["thread_id"].(string)
    if threadID == "" {
        return nil, errors.New("thread_id parameter is required")
    }
    return emailService.GetEmailThread(ctx, threadID)

case "modify_email":
    emailID, _ := params["email_id"].(string)
    if emailID == "" {
        return nil, errors.New("email_id parameter is required")
    }
    // Create modifications map, only including provided fields
    modifications := make(map[string]interface{})
    if isRead, ok := params["is_read"].(bool); ok {
        modifications["is_read"] = isRead
    }
    if isArchived, ok := params["is_archived"].(bool); ok {
        modifications["is_archived"] = isArchived
    }
    if len(modifications) == 0 {
        return nil, errors.New("at least one modification (is_read or is_archived) is required")
    }
    return emailService.ModifyEmail(ctx, emailID, modifications)

case "upload_attachment":
    accountID, _ := params["account_id"].(string)
    if accountID == "" {
        // Maybe default account ID can be retrieved from context or config?
        return nil, errors.New("account_id parameter is required")
    }
    filename, _ := params["filename"].(string)
    if filename == "" {
        return nil, errors.New("filename parameter is required")
    }
    contentType, _ := params["content_type"].(string)
    if contentType == "" {
        return nil, errors.New("content_type parameter is required")
    }
    contentBase64, _ := params["content_base64"].(string)
    if contentBase64 == "" {
        return nil, errors.New("content_base64 parameter is required")
    }
    attachment := &models.AttachmentContent{
        Filename:    filename,
        ContentType: contentType,
        Data:        contentBase64,
    }
    return emailService.UploadAttachment(ctx, accountID, attachment)

case "create_draft":
    // Note: The EmailService.CreateDraft has a fake implementation currently
    // This case assumes the params match the *service* method signature, not capability definition fully
    threadID, _ := params["thread_id"].(string) // From capability definition
    draftText, _ := params["body"].(string)     // Using 'body' from capability definition for draft text

    // Extract emails (to, cc, bcc) - combine them for the fake implementation?
    var allEmails []string
    if to, ok := params["to"].([]interface{}); ok {
        for _, e := range to {
            if emailStr, ok := e.(string); ok {
                allEmails = append(allEmails, emailStr)
            }
        }
    }
    // Add cc and bcc if needed for the real implementation later

    // Call the fake implementation
    log.Printf("WARN: Calling fake CreateDraft implementation in EmailService for thread %s", threadID)
    return emailService.CreateDraft(ctx, threadID, draftText, allEmails)

case "tag_thread":
    // Note: The EmailService.TagThread has a fake implementation currently
    threadID, _ := params["thread_id"].(string)
    if threadID == "" {
        return nil, errors.New("thread_id parameter is required")
    }
    tagName, _ := params["tag_name"].(string)
    if tagName == "" {
        return nil, errors.New("tag_name parameter is required")
    }
    log.Printf("WARN: Calling fake TagThread implementation in EmailService for thread %s with tag %s", threadID, tagName)
    return emailService.TagThread(ctx, threadID, tagName)

case "untag_thread":
    // Note: The EmailService.UntagThread has a fake implementation currently
    threadID, _ := params["thread_id"].(string)
    if threadID == "" {
        return nil, errors.New("thread_id parameter is required")
    }
    tagName, _ := params["tag_name"].(string)
    if tagName == "" {
        return nil, errors.New("tag_name parameter is required")
    }
    log.Printf("WARN: Calling fake UntagThread implementation in EmailService for thread %s with tag %s", threadID, tagName)
    return emailService.UntagThread(ctx, threadID, tagName)

default:
    return nil, fmt.Errorf("unsupported Email capability: %s", capability)
}

}

func (a *UnipileAdapter) executeMessagingCapability(ctx context.Context, capability string, params map[string]interface{}) (interface{}, error) { msgService := services.NewMessagingService(a.client) options := models.QueryOptions{} if limitParam, ok := params["limit"].(float64); ok { options.Limit = int(limitParam) } options.Cursor, _ = params["cursor"].(string)

switch capability {
case "list_chats":
    return msgService.ListChats(ctx, options)

case "get_chat":
    chatID, _ := params["chat_id"].(string)
    if chatID == "" {
        return nil, errors.New("chat_id parameter is required")
    }
    return msgService.GetChat(ctx, chatID)

case "create_chat":
    var participantIDs []string
    if pIDs, ok := params["participant_ids"].([]interface{}); ok {
        for _, id := range pIDs {
            if idStr, ok := id.(string); ok {
                participantIDs = append(participantIDs, idStr)
            }
        }
    }
    if len(participantIDs) == 0 {
        return nil, errors.New("participant_ids parameter is required")
    }
    name, _ := params["name"].(string)
    description, _ := params["description"].(string)
    return msgService.CreateChat(ctx, participantIDs, name, description)

case "list_messages":
    chatID, _ := params["chat_id"].(string)
    if chatID == "" {
        return nil, errors.New("chat_id parameter is required")
    }
    return msgService.ListMessages(ctx, chatID, options)

case "get_message":
    messageID, _ := params["message_id"].(string)
    if messageID == "" {
        return nil, errors.New("message_id parameter is required")
    }
    return msgService.GetMessage(ctx, messageID)

case "send_message":
    chatID, _ := params["chat_id"].(string)
    if chatID == "" {
        return nil, errors.New("chat_id parameter is required")
    }
    text, _ := params["text"].(string)
    if text == "" {
        return nil, errors.New("text parameter is required")
    }
    // TODO: Handle attachments and other draft fields if added
    draft := &models.MessageDraft{Text: text}
    return msgService.SendMessage(ctx, chatID, draft)

case "list_chat_participants":
    chatID, _ := params["chat_id"].(string)
    if chatID == "" {
        return nil, errors.New("chat_id parameter is required")
    }
    return msgService.ListChatParticipants(ctx, chatID, options)

case "add_chat_participant":
    chatID, _ := params["chat_id"].(string)
    if chatID == "" {
        return nil, errors.New("chat_id parameter is required")
    }
    accountID, _ := params["account_id"].(string)
    if accountID == "" {
        return nil, errors.New("account_id parameter is required")
    }
    role, _ := params["role"].(string)
    return msgService.AddChatParticipant(ctx, chatID, accountID, role)

case "remove_chat_participant":
    chatID, _ := params["chat_id"].(string)
    if chatID == "" {
        return nil, errors.New("chat_id parameter is required")
    }
    participantID, _ := params["participant_id"].(string)
    if participantID == "" {
        return nil, errors.New("participant_id parameter is required")
    }
    return msgService.RemoveChatParticipant(ctx, chatID, participantID)

case "update_message":
    messageID, _ := params["message_id"].(string)
    if messageID == "" {
        return nil, errors.New("message_id parameter is required")
    }
    text, _ := params["text"].(string)
    if text == "" {
        return nil, errors.New("text parameter is required")
    }
    return msgService.UpdateMessage(ctx, messageID, text)

case "delete_message":
    messageID, _ := params["message_id"].(string)
    if messageID == "" {
        return nil, errors.New("message_id parameter is required")
    }
    return msgService.DeleteMessage(ctx, messageID)

case "search_chats":
    var searchRequest models.ChatSearchRequest
    if err := mapstructure.Decode(params, &searchRequest); err != nil {
        return nil, fmt.Errorf("error decoding search_chats params: %w", err)
    }
    return msgService.SearchChats(ctx, &searchRequest)

case "search_messages":
    var searchRequest models.MessageSearchRequest
    if err := mapstructure.Decode(params, &searchRequest); err != nil {
        return nil, fmt.Errorf("error decoding search_messages params: %w", err)
    }
    return msgService.SearchMessages(ctx, &searchRequest)

default:
    return nil, fmt.Errorf("unsupported Messaging capability: %s", capability)
}

}

func (a *UnipileAdapter) executeCalendarCapability(ctx context.Context, capability string, params map[string]interface{}) (interface{}, error) { calService := services.NewCalendarService(a.client) options := models.QueryOptions{} if limitParam, ok := params["limit"].(float64); ok { options.Limit = int(limitParam) } options.Cursor, _ = params["cursor"].(string)

eventID, _ := params["event_id"].(string)

switch capability {
case "list_events":
    return calService.ListEvents(ctx, options)

case "get_event":
    if eventID == "" {
        return nil, errors.New("event_id parameter is required")
    }
    return calService.GetEvent(ctx, eventID)

case "create_event":
    var eventDraft models.CalendarEventDraft
    if eventData, ok := params["event_data"].(map[string]interface{}); ok {
        if err := mapstructure.Decode(eventData, &eventDraft); err != nil {
            return nil, fmt.Errorf("error decoding event_data: %w", err)
        }
    } else {
        return nil, errors.New("event_data parameter is required and must be an object")
    }
    return calService.CreateEvent(ctx, &eventDraft)

case "update_event":
    if eventID == "" {
        return nil, errors.New("event_id parameter is required")
    }
    updates, ok := params["updates"].(map[string]interface{})
    if !ok || len(updates) == 0 {
        return nil, errors.New("updates parameter is required and must be a non-empty object")
    }
    return calService.UpdateEvent(ctx, eventID, updates)

case "delete_event":
    if eventID == "" {
        return nil, errors.New("event_id parameter is required")
    }
    return calService.DeleteEvent(ctx, eventID)

case "respond_to_event":
    if eventID == "" {
        return nil, errors.New("event_id parameter is required")
    }
    var rsvp models.CalendarEventRSVP
    if responseData, ok := params["response"].(map[string]interface{}); ok {
        if err := mapstructure.Decode(responseData, &rsvp); err != nil {
            return nil, fmt.Errorf("error decoding response data: %w", err)
        }
    } else {
        return nil, errors.New("response parameter is required and must be an object")
    }
    return calService.RespondToEvent(ctx, eventID, &rsvp)

case "search_events":
    var searchRequest models.CalendarSearchRequest
    if criteria, ok := params["search_criteria"].(map[string]interface{}); ok {
        if err := mapstructure.Decode(criteria, &searchRequest); err != nil {
            return nil, fmt.Errorf("error decoding search_criteria: %w", err)
        }
    } else {
        return nil, errors.New("search_criteria parameter is required and must be an object")
    }
    return calService.SearchEvents(ctx, &searchRequest)

default:
    return nil, fmt.Errorf("unsupported Calendar capability: %s", capability)
}

}

func (a *UnipileAdapter) executePageCapability(ctx context.Context, capability string, params map[string]interface{}) (interface{}, error) { pageService := services.NewPageService(a.client) options := models.QueryOptions{} if limitParam, ok := params["limit"].(float64); ok { options.Limit = int(limitParam) } options.Cursor, _ = params["cursor"].(string)

pageID, _ := params["page_id"].(string)
websiteID, _ := params["website_id"].(string)

switch capability {
case "list_pages":
    return pageService.ListPages(ctx, options)

case "get_page":
    if pageID == "" {
        return nil, errors.New("page_id parameter is required")
    }
    return pageService.GetPage(ctx, pageID)

case "create_page":
    var pageDraft models.PageDraft
    if draftData, ok := params["pageDraft"].(map[string]interface{}); ok {
        if err := mapstructure.Decode(draftData, &pageDraft); err != nil {
            return nil, fmt.Errorf("error decoding pageDraft: %w", err)
        }
    } else {
        return nil, errors.New("pageDraft parameter is required and must be an object")
    }
    return pageService.CreatePage(ctx, &pageDraft)

case "update_page":
    if pageID == "" {
        return nil, errors.New("page_id parameter is required")
    }
    updates, ok := params["updates"].(map[string]interface{})
    if !ok || len(updates) == 0 {
        return nil, errors.New("updates parameter is required and must be a non-empty object")
    }
    return pageService.UpdatePage(ctx, pageID, updates)

case "delete_page":
    if pageID == "" {
        return nil, errors.New("page_id parameter is required")
    }
    return pageService.DeletePage(ctx, pageID)

case "list_websites":
    return pageService.ListWebsites(ctx, options)

case "get_website":
    if websiteID == "" {
        return nil, errors.New("website_id parameter is required")
    }
    return pageService.GetWebsite(ctx, websiteID)

case "get_menu":
    if websiteID == "" {
        return nil, errors.New("website_id parameter is required")
    }
    return pageService.GetMenu(ctx, websiteID)

case "search_pages":
    var searchRequest models.PageSearchRequest
    if criteria, ok := params["search_criteria"].(map[string]interface{}); ok {
        if err := mapstructure.Decode(criteria, &searchRequest); err != nil {
            return nil, fmt.Errorf("error decoding search_criteria: %w", err)
        }
    } else {
        // Allow search without specific criteria? Check API behavior.
        // For now, assume criteria object might be optional but structure needed if present.
        if params["search_criteria"] != nil {
            return nil, errors.New("search_criteria parameter must be an object if provided")
        }
        // If nil, proceed with empty request
    }
    return pageService.SearchPages(ctx, &searchRequest)

default:
    return nil, fmt.Errorf("unsupported Page capability: %s", capability)
}

}

func (a *UnipileAdapter) executePostCapability(ctx context.Context, capability string, params map[string]interface{}) (interface{}, error) { postService := services.NewPostService(a.client) options := models.QueryOptions{} if limitParam, ok := params["limit"].(float64); ok { options.Limit = int(limitParam) } options.Cursor, _ = params["cursor"].(string)

postID, _ := params["post_id"].(string)
commentID, _ := params["comment_id"].(string)
reactionID, _ := params["reaction_id"].(string)
reactionType, _ := params["reaction_type"].(string)

switch capability {
case "list_posts":
    return postService.ListPosts(ctx, options)

case "get_post":
    if postID == "" {
        return nil, errors.New("post_id parameter is required")
    }
    return postService.GetPost(ctx, postID)

case "create_post":
    var postDraft models.PostDraft
    if draftData, ok := params["postDraft"].(map[string]interface{}); ok { // Assuming param name matches capability def
        if err := mapstructure.Decode(draftData, &postDraft); err != nil {
            return nil, fmt.Errorf("error decoding postDraft: %w", err)
        }
    } else {
        // Maybe extract individual fields if postDraft is not passed directly
        return nil, errors.New("postDraft parameter is required and must be an object")
    }
    return postService.CreatePost(ctx, &postDraft)

case "delete_post":
    if postID == "" {
        return nil, errors.New("post_id parameter is required")
    }
    return postService.DeletePost(ctx, postID)

case "list_comments":
    if postID == "" {
        return nil, errors.New("post_id parameter is required")
    }
    return postService.ListComments(ctx, postID, options)

case "get_comment":
    if commentID == "" {
        return nil, errors.New("comment_id parameter is required")
    }
    return postService.GetComment(ctx, commentID)

case "create_comment":
    if postID == "" {
        return nil, errors.New("post_id parameter is required")
    }
    var commentDraft models.PostCommentDraft
    if draftData, ok := params["commentDraft"].(map[string]interface{}); ok { // Assuming param name
        if err := mapstructure.Decode(draftData, &commentDraft); err != nil {
            return nil, fmt.Errorf("error decoding commentDraft: %w", err)
        }
    } else {
        // Extract text directly? Assume 'text' param for now
        text, ok := params["text"].(string)
        if !ok || text == "" {
            return nil, errors.New("comment text parameter is required")
        }
        commentDraft.Content = text // Corrected field name from Text to Content
    }
    return postService.CreateComment(ctx, postID, &commentDraft)

case "delete_comment":
    if commentID == "" {
        return nil, errors.New("comment_id parameter is required")
    }
    return postService.DeleteComment(ctx, commentID)

case "list_reactions":
    if postID == "" {
        return nil, errors.New("post_id parameter is required")
    }
    return postService.ListReactions(ctx, postID, options)

case "add_reaction":
    if postID == "" {
        return nil, errors.New("post_id parameter is required")
    }
    if reactionType == "" {
        return nil, errors.New("reaction_type parameter is required")
    }
    return postService.AddReaction(ctx, postID, reactionType)

case "delete_reaction":
    if reactionID == "" {
        return nil, errors.New("reaction_id parameter is required")
    }
    return postService.DeleteReaction(ctx, reactionID)

case "search_posts":
    var searchRequest models.PostSearchRequest
    if criteria, ok := params["search_criteria"].(map[string]interface{}); ok {
        if err := mapstructure.Decode(criteria, &searchRequest); err != nil {
            return nil, fmt.Errorf("error decoding search_criteria: %w", err)
        }
    } else {
        // Allow search without specific criteria? Check API behavior.
        if params["search_criteria"] != nil {
            return nil, errors.New("search_criteria parameter must be an object if provided")
        }
        // If nil, proceed with empty request
    }
    return postService.SearchPosts(ctx, &searchRequest)

default:
    return nil, fmt.Errorf("unsupported Post capability: %s", capability)
}

}

func (a *UnipileAdapter) executeAccountCapability(ctx context.Context, capability string, params map[string]interface{}) (interface{}, error) { accService := services.NewAccountService(a.client) options := models.QueryOptions{} if limitParam, ok := params["limit"].(float64); ok { options.Limit = int(limitParam) } options.Cursor, _ = params["cursor"].(string)

accountID, _ := params["account_id"].(string)
connectionID, _ := params["connection_id"].(string)

switch capability {
case "list_accounts":
    return accService.ListAccounts(ctx, options)

case "get_account":
    if accountID == "" {
        return nil, errors.New("account_id parameter is required")
    }
    return accService.GetAccount(ctx, accountID)

case "update_account":
    if accountID == "" {
        return nil, errors.New("account_id parameter is required")
    }
    var updateRequest models.AccountUpdate
    // Use mapstructure to decode directly from params to the struct
    if err := mapstructure.Decode(params, &updateRequest); err != nil {
        return nil, fmt.Errorf("error decoding update_account params: %w", err)
    }
    // Ensure AccountID is not part of the update payload itself if it's in path
    // updateRequest.AccountID = "" // Clear if necessary based on API
    return accService.UpdateAccount(ctx, accountID, &updateRequest)

case "list_connections":
    if accountID == "" {
        return nil, errors.New("account_id parameter is required")
    }
    return accService.ListConnections(ctx, accountID, options)

case "get_connection":
    if accountID == "" {
        return nil, errors.New("account_id parameter is required")
    }
    if connectionID == "" {
        return nil, errors.New("connection_id parameter is required")
    }
    return accService.GetConnection(ctx, accountID, connectionID)

case "create_connection_request":
    if accountID == "" {
        return nil, errors.New("account_id parameter is required")
    }
    var connRequest models.AccountConnectionRequest
    // Use mapstructure to decode directly from params to the struct
    if err := mapstructure.Decode(params, &connRequest); err != nil {
        return nil, fmt.Errorf("error decoding create_connection_request params: %w", err)
    }
    // Ensure required fields from capability definition are present
    if connRequest.Provider == "" {
        return nil, errors.New("provider parameter is required")
    }
    if connRequest.Type == "" {
        return nil, errors.New("type parameter is required")
    }
    return accService.CreateConnectionRequest(ctx, accountID, &connRequest)

case "refresh_oauth":
    if accountID == "" {
        return nil, errors.New("account_id parameter is required")
    }
    return accService.RefreshOAuth(ctx, accountID)

case "search_accounts":
    var searchRequest models.AccountSearchRequest
    if err := mapstructure.Decode(params, &searchRequest); err != nil {
        return nil, fmt.Errorf("error decoding search_accounts params: %w", err)
    }
    return accService.SearchAccounts(ctx, &searchRequest)

default:
    return nil, fmt.Errorf("unsupported Account capability: %s", capability)
}

}

// GetServiceRegistry returns the underlying service registry func (a UnipileAdapter) GetServiceRegistry() providers.ServiceRegistry { return a.client.Registry }

// GenerateAuthLink generates a hosted auth link func (a *UnipileAdapter) GenerateAuthLink(ctx context.Context, userID string, providers []string, linkType string, accountID string) (string, error) { if !a.initialized { return "", errors.New("adapter not initialized") }

// Create expiration time (e.g., 1 hour from now)
expiresOn := time.Now().Add(1 * time.Hour).Format(time.RFC3339)

// Prepare request body
requestBody := map[string]interface{}{
    "type":      linkType,
    "providers": providers,
    "api_url":   a.config["base_url"],
    "expiresOn": expiresOn,
    "name":      userID, // This will be passed back in the notification
}

// Add notify_url from config
if notifyURL, ok := a.config["notify_url"].(string); ok && notifyURL != "" {
    requestBody["notify_url"] = notifyURL
}

// Add success_redirect_url and failure_redirect_url if available
if successURL, ok := a.config["success_redirect_url"].(string); ok && successURL != "" {
    requestBody["success_redirect_url"] = successURL
}
if failureURL, ok := a.config["failure_redirect_url"].(string); ok && failureURL != "" {
    requestBody["failure_redirect_url"] = failureURL
}

// For reconnect, add the account_id
if linkType == "reconnect" && accountID != "" {
    requestBody["reconnect_account"] = accountID
}

// Make request to Unipile API
endpoint := "/api/v1/hosted/accounts/link"
var response struct {
    Object string `json:"object"`
    URL    string `json:"url"`
}

err := a.client.Request(ctx, http.MethodPost, endpoint, requestBody, &response)
if err != nil {
    return "", fmt.Errorf("error generating auth link: %w", err)
}

return response.URL, nil

}

// GetUserAccounts returns all accounts for a user from the store func (a UnipileAdapter) GetUserAccounts(ctx context.Context, userID string) ([]models.AccountInfo, error) { // Use the store to get accounts accounts, err := a.store.GetUserAccounts(ctx, userID) if err != nil { log.Printf("Error getting user accounts for %s from store: %v", userID, err) // Return an empty slice on error, or handle differently if needed return []*models.AccountInfo{}, fmt.Errorf("failed to get user accounts: %w", err) } return accounts, nil

// Remove reliance on in-memory map
// a.accountMutex.RLock()
// defer a.accountMutex.RUnlock()
// if accounts, ok := a.accounts[userID]; ok {
//  return accounts
// }
// return []models.AccountInfo{}

}

// GetAccountByID returns a specific account by ID from the store func (a UnipileAdapter) GetAccountByID(ctx context.Context, accountID string) (models.AccountInfo, error) { // Use the store to get the account account, err := a.store.GetAccount(ctx, accountID) if err != nil { log.Printf("Error getting account %s from store: %v", accountID, err) // Handle not found specifically? pgx might return pgx.ErrNoRows if errors.Is(err, pgx.ErrNoRows) { return nil, nil // Return nil, nil to indicate not found } return nil, fmt.Errorf("failed to get account by ID: %w", err) } return account, nil

// Remove reliance on in-memory map
// a.accountMutex.RLock()
// defer a.accountMutex.RUnlock()
// for _, accounts := range a.accounts {
//  for _, account := range accounts {
//      if account.AccountID == accountID {
//          // Need to return a pointer if changing signature
//          accCopy := account // Make a copy to return pointer safely
//          return &accCopy, true
//      }
//  }
// }
// return nil, false

}

// HandleChat implements the providers.Provider interface. // Unipile itself doesn't provide a direct chat interface; specific // messaging actions are handled via ExecuteTool. func (a UnipileAdapter) HandleChat(ctx context.Context, messages []providers.ChatMessage, threadID string) (providers.ChatResponse, error) { return nil, errors.New("direct chat interaction is not supported by the Unipile provider; use specific tools instead") }

// handleAccountStatus handles different account statuses func (a UnipileAdapter) handleAccountStatus(status models.AccountStatus) { switch status.Message { case "OK": log.Printf("✅ Account %s is functioning normally", status.AccountID) case "ERROR", "STOPPED": log.Printf("❌ Account %s encountered an error and has stopped: %s", status.AccountID, status.Message) // TODO: Notify team or user about the error case "CREDENTIALS": log.Printf("🔑 Account %s has credential issues", status.AccountID) // TODO: Notify user to update credentials case "CONNECTING": log.Printf("🔄 Account %s is connecting", status.AccountID) case "DELETED": log.Printf("🗑️ Account %s has been deleted", status.AccountID) case "CREATION_SUCCESS": log.Printf("➕ Account %s has been created successfully", status.AccountID) case "RECONNECTED": log.Printf("🔄 Account %s has been reconnected successfully", status.AccountID) case "SYNC_SUCCESS": log.Printf("🔄 Account %s synchronization successful", status.AccountID) default: log.Printf("ℹ️ Unknown status for account %s: %s", status.AccountID, status.Message) } }

// GetAvailableServices returns the list of services offered by the Unipile provider. func (a *UnipileAdapter) GetAvailableServices() []providers.ServiceDefinition { if !a.initialized || a.client == nil || a.client.Registry == nil { log.Println("Warning: UnipileAdapter not initialized or client/registry is nil when calling GetAvailableServices") return nil }

registeredServices := a.client.Registry.GetAll() // Get map[string]Service
definitions := make([]providers.ServiceDefinition, 0, len(registeredServices))

providerID := a.GetID()

for serviceName, serviceInstance := range registeredServices {
    // We need a way to get the *definition* (name, description, schemas)
    // Currently, the Service interface only gives GetCapabilities and Execute.
    // Assuming capabilities somehow relate to a service definition or can be derived.
    // For now, create a basic definition based on the registered name and capabilities.
    capabilities := serviceInstance.GetCapabilities()
    firstCapabilityName := ""
    if len(capabilities) > 0 {
        firstCapabilityName = capabilities[0].Name // Use first capability name as a proxy
    }

    def := providers.ServiceDefinition{
        ID:          fmt.Sprintf("%s.%s", providerID, serviceName), // e.g., "unipile.email"
        Name:        strings.Title(serviceName),                    // e.g., "Email"
        Description: fmt.Sprintf("Provides %s services via Unipile", serviceName),
        // InputSchema and OutputSchema would ideally come from the service definition
        // or be derived more intelligently from capabilities.
        InputSchema:  map[string]interface{}{"capability_name": firstCapabilityName}, // Placeholder
        OutputSchema: map[string]interface{}{},                                       // Placeholder
    }
    definitions = append(definitions, def)
}

return definitions

}

// GetVersion returns the version of the Unipile provider adapter. func (a *UnipileAdapter) GetVersion() string { // TODO: Define a proper version, possibly as a constant return "0.1.0" }

// GetService returns a specific service instance by its ID. func (a *UnipileAdapter) GetService(serviceID string) (providers.Service, error) { // Implementation for getting a specific service instance return nil, fmt.Errorf("service retrieval not implemented for Unipile provider") }

// --- New Event Handling Logic ---

// HandleReceivedEmailEvent is called by the central event listener when a 'unipile.email.received' event occurs. // It expects the eventPayload to contain webhookPayloadResourceID and userID (framework user ID). // It fetches the full thread, creates a new resource for it (associated with userID), // and publishes an event containing the necessary info for the job creator. func (a *UnipileAdapter) HandleReceivedEmailEvent(ctx context.Context, eventPayload map[string]interface{}) error {

// Extract required info from the event payload
webhookPayloadResourceID, _ := eventPayload["webhookPayloadResourceId"].(string)
userID, _ := eventPayload["userID"].(string) // Expecting framework userID from the publisher

if webhookPayloadResourceID == "" {
    log.Printf("ERROR: HandleReceivedEmailEvent missing webhookPayloadResourceId in event payload")
    return errors.New("missing webhookPayloadResourceId in event payload")
}
if userID == "" {
    // If the userID is critical for fetching/creating resources, we must have it.
    log.Printf("ERROR: HandleReceivedEmailEvent missing userID in event payload for resource %s", webhookPayloadResourceID)
    return errors.New("missing userID in event payload")
}

log.Printf("HandleReceivedEmailEvent triggered for webhook payload resource: %s, User: %s", webhookPayloadResourceID, userID)

// 1. Check Dependencies (Store is needed now for account check)
if a.resourceMgr == nil || a.jobMgr == nil || a.client == nil || a.store == nil || a.notifier == nil {
    errMsg := "cannot handle email event: missing dependencies (ResourceManager, JobManager, UnipileClient, UnipileStore, or Notifier)"
    log.Printf("ERROR: %s", errMsg)
    return errors.New(errMsg)
}

// 2. Fetch the original webhook payload resource using userID
payloadReader, _, err := a.resourceMgr.GetResourceContent(ctx, userID, webhookPayloadResourceID)
if err != nil {
    log.Printf("ERROR fetching webhook payload resource %s for user %s: %v", webhookPayloadResourceID, userID, err)
    return fmt.Errorf("failed to fetch webhook payload resource: %w", err)
}
defer payloadReader.Close()

// 3. Unmarshal the webhook payload
var webhookPayload models.WebhookPayload
if err := json.NewDecoder(payloadReader).Decode(&webhookPayload); err != nil {
    log.Printf("ERROR decoding webhook payload resource %s: %v", webhookPayloadResourceID, err)
    return fmt.Errorf("failed to decode webhook payload JSON: %w", err)
}

// Extract necessary IDs (redundant with eventPayload, but safer to use payload)
accountID := webhookPayload.AccountID
messageID := webhookPayload.MessageID // Use MessageID from payload if available, might differ from EmailID
if accountID == "" {
    log.Printf("ERROR: Missing accountID in webhook payload from resource %s", webhookPayloadResourceID)
    return errors.New("invalid webhook payload: missing accountID")
}
// Use EmailID as fallback for messageID if needed by downstream?
if messageID == "" {
    messageID = webhookPayload.EmailID
}
if messageID == "" {
    log.Printf("ERROR: Missing messageID/emailID in webhook payload from resource %s", webhookPayloadResourceID)
    return errors.New("invalid webhook payload: missing messageID/emailID")
}

// 4. **Verify Account Mapping** (Crucial Step)
accountInfo, err := a.store.GetAccount(ctx, accountID)
if err != nil {
    log.Printf("ERROR checking account mapping for %s: %v. Cannot proceed.", accountID, err)
    return fmt.Errorf("failed to check account mapping for %s: %w", accountID, err)
}
if accountInfo == nil || accountInfo.UserID == "" {
    log.Printf("WARN: Unipile account %s is not mapped to an internal user in store. Skipping further processing.", accountID)
    // It's important *not* to proceed if the mapping is missing/invalid.
    return nil // Or return an error if this situation requires intervention
}
// Double-check consistency: Does the mapped UserID match the one from the event?
if accountInfo.UserID != userID {
    log.Printf("CRITICAL: UserID mismatch for account %s! Event UserID: '%s', Stored UserID: '%s'. Aborting.", accountID, userID, accountInfo.UserID)
    return fmt.Errorf("user ID mismatch for account %s", accountID)
}
// --- Mapping Confirmed --- > Proceed with userID

// 5. Fetch the full thread details using the internal client
log.Printf("Fetching full thread details for account=%s, message=%s...", accountID, messageID)
fullThread, err := a.GetEmailThreadForMessage(ctx, accountID, messageID) // Use the real implementation now
if err != nil {
    log.Printf("ERROR fetching full thread for account=%s, message=%s: %v", accountID, messageID, err)
    // Don't create resource or event if thread fetch fails
    return fmt.Errorf("failed to fetch full thread: %w", err)
}
if fullThread == nil || len(fullThread.Emails) == 0 {
    log.Printf("WARN: Fetched thread (Account: %s, Message: %s) is nil or empty. Skipping resource/event creation.", accountID, messageID)
    return nil // Not an error, just nothing to process
}
actualThreadID := fullThread.ID // Get the real ThreadID

// 6. Marshal full thread data to JSON
threadJSON, err := json.Marshal(fullThread)
if err != nil {
    log.Printf("ERROR marshaling full thread %s to JSON: %v", actualThreadID, err)
    return fmt.Errorf("failed to marshal full thread JSON: %w", err)
}

// 7. Create a *new* Resource for the raw full thread data, associated with the userID
resourceTitle := fmt.Sprintf("Raw Unipile Email Thread: %s", fullThread.Subject)
resourceType := "raw_email_thread_unipile" // Use the type expected by the preparator
contentType := "application/json"
// Attempt to get source event from payload, handle if not string
sourceEvent := "unknown"
if se, ok := eventPayload["source_event"].(string); ok {
    sourceEvent = se
}
resourceMetadata := map[string]string{
    "unipile_account_id":         accountID,
    "unipile_thread_id":          actualThreadID,
    "unipile_message_id":         messageID, // Include the triggering message ID
    "original_subject":           fullThread.Subject,
    "source_event":               sourceEvent,              // Use extracted source event
    "source_payload_resource_id": webhookPayloadResourceID, // Link back
}
log.Printf("Creating resource '%s' for full thread %s (User: %s)...", resourceType, actualThreadID, userID)

// Pass pointer to userID, ensuring it's not empty
var userIDPtr *string
if userID != "" {
    userIDPtr = &userID
}

createdResource, err := a.resourceMgr.CreateResource(ctx, bytes.NewReader(threadJSON), resourceTitle, resourceType, contentType, nil /* tags */, resourceMetadata, userIDPtr)
if err != nil {
    log.Printf("ERROR creating resource for full thread %s for user %s: %v", actualThreadID, userID, err)
    return fmt.Errorf("failed to create resource for full thread: %w", err)
}
log.Printf("Successfully created full thread resource %s for thread %s (User: %s)", createdResource.ID, actualThreadID, userID)

// 8. Publish Event: Notify listeners that a raw thread resource is ready
const eventChannel = "unipile.email.thread.created" // Define a channel name
eventPayloadOut := map[string]interface{}{
    "rawThreadResourceId":     createdResource.ID,
    "unipileAccountId":        accountID,
    "unipileThreadId":         actualThreadID,
    "unipileMessageId":        messageID,
    "userID":                  userID, // Include the verified framework User ID
    "sourcePayloadResourceId": webhookPayloadResourceID,
    // Include other potentially useful info from fullThread or webhookPayload if needed
}
log.Printf("Publishing event '%s' for resource %s (Thread: %s, User: %s)...", eventChannel, createdResource.ID, actualThreadID, userID)
if err := a.notifier.Publish(ctx, eventChannel, eventPayloadOut); err != nil {
    // Log the error, but potentially don't fail the entire handler?
    // Depends on whether notification is critical path.
    log.Printf("ERROR publishing event '%s' for resource %s: %v", eventChannel, createdResource.ID, err)
    // Consider returning the error if publishing is critical
    // return fmt.Errorf("failed to publish event '%s': %w", eventChannel, err)
}

log.Printf("HandleReceivedEmailEvent completed for webhook payload resource %s. Published event for thread resource %s.", webhookPayloadResourceID, createdResource.ID)
return nil // Indicate successful handling of the event

}

// GetEmailThreadForMessage fetches the full thread details for a given message ID. func (a UnipileAdapter) GetEmailThreadForMessage(ctx context.Context, accountID, messageID string) (models.EmailThread, error) { log.Printf("GetEmailThreadForMessage: Fetching details for Account=%s, Message=%s", accountID, messageID)

// 1. Get the EmailService instance from the registry
if a.client == nil || a.client.Registry == nil {
    return nil, errors.New("Unipile client or service registry not initialized")
}
service, exists := a.client.Registry.Get("email")
if !exists {
    return nil, errors.New("email service not found in Unipile client registry")
}
emailService, ok := service.(*services.EmailService) // Type assertion
if !ok {
    return nil, errors.New("failed to cast to EmailService type")
}

// 2. Fetch the email details using the messageID
log.Printf("Fetching email %s to get thread ID...", messageID)
emailDetails, err := emailService.GetEmail(ctx, messageID)
if err != nil {
    // Handle potential errors like Not Found vs API error
    log.Printf("Error fetching email %s: %v", messageID, err)
    return nil, fmt.Errorf("failed to fetch email details for message %s: %w", messageID, err)
}
if emailDetails == nil {
    log.Printf("Email %s not found.", messageID)
    return nil, fmt.Errorf("email with ID %s not found", messageID)
}

// 3. Extract the ThreadID
threadID := emailDetails.ThreadID
if threadID == "" {
    log.Printf("WARN: Email %s does not have an associated ThreadID. Cannot fetch full thread.", messageID)
    // What should happen here? Return an error? Or maybe construct a minimal thread?
    // For now, return an error as the draft preparator expects a thread.
    return nil, fmt.Errorf("email %s has no ThreadID", messageID)
}
log.Printf("Found ThreadID %s for message %s. Fetching full thread...", threadID, messageID)

// 4. Fetch the full thread details using the threadID
fullThread, err := emailService.GetEmailThread(ctx, threadID)
if err != nil {
    log.Printf("Error fetching email thread %s: %v", threadID, err)
    return nil, fmt.Errorf("failed to fetch email thread %s: %w", threadID, err)
}
if fullThread == nil {
    // Should be unlikely if GetEmail succeeded, but handle defensively
    log.Printf("Email thread %s not found after fetching email %s.", threadID, messageID)
    return nil, fmt.Errorf("email thread %s not found", threadID)
}

log.Printf("Successfully fetched email thread %s (Subject: %s)", threadID, fullThread.Subject)
return fullThread, nil

}

// GetDetailedEventInfo returns detailed information about all registered Unipile notification events. func (a UnipileAdapter) GetDetailedEventInfo() []providers.DetailedEventInfo { // Note: Event patterns like "email." cover multiple specific events (e.g., email_received, email_sent). // Descriptions might need refinement based on actual Unipile docs for each sub-event. return []providers.DetailedEventInfo{ { EventPattern: "unipile.email.", // Use a provider-specific prefix if desired SourceProvider: a.GetID(), Description: "Notifications related to email activities (received, sent, read, deleted, etc.). Triggered by Unipile webhooks.", PayloadIDs: []string{"webhookPayloadResourceId", "accountId", "messageId", "emailId", "subject", "from"}, // Based on HandleReceivedEmailEvent publication RetrievalNotes: "The 'webhookPayloadResourceId' points to the raw webhook payload. Use 'accountId' and 'emailId'/'messageId' with Unipile EmailService (e.g., GetEmail or GetEmailThreadForMessage) to fetch full details.", }, { EventPattern: "unipile.message.", SourceProvider: a.GetID(), Description: "Notifications for chat message activities (received, read, reaction, edited, deleted). Triggered by Unipile webhooks.", PayloadIDs: []string{"AccountID", "ChatID", "MessageID", "Event"}, // From webhook payload struct RetrievalNotes: "Webhook payload is light. Use 'AccountID' and 'MessageID' (or 'ChatID') with the Unipile MessagingService (e.g., GetMessage or GetChat) to fetch the full message or chat details.", }, { EventPattern: "unipile.account.*", SourceProvider: a.GetID(), Description: "Notifications about changes in linked account status (OK, ERROR, CREDENTIALS, etc.). Triggered by Unipile webhooks.", PayloadIDs: []string{"AccountID", "AccountType", "Message"}, // From webhook payload struct RetrievalNotes: "The payload contains the core status ('Message'). To get full account details associated with the 'AccountID', use the Unipile AccountService (e.g., GetAccount).", }, } }

// Implementation for webhook service tool execution func (a *UnipileAdapter) executeWebhookCapability(ctx context.Context, capability string, params map[string]interface{}) (interface{}, error) { whService := services.NewWebhookService(a.client) options := models.QueryOptions{} if limitParam, ok := params["limit"].(float64); ok { options.Limit = int(limitParam) } options.Cursor, _ = params["cursor"].(string)

webhookID, _ := params["webhook_id"].(string)
eventID, _ := params["event_id"].(string)
deliveryID, _ := params["delivery_id"].(string)

switch capability {
case "list_webhooks":
    return whService.ListWebhooks(ctx, options)

case "get_webhook":
    if webhookID == "" {
        return nil, errors.New("webhook_id parameter is required")
    }
    return whService.GetWebhook(ctx, webhookID)

case "create_webhook":
    var whDraft models.WebhookDraft
    // Use mapstructure to decode directly from params to the struct
    if err := mapstructure.Decode(params, &whDraft); err != nil {
        return nil, fmt.Errorf("error decoding create_webhook params: %w", err)
    }
    // Ensure required fields from capability definition are present
    if whDraft.URL == "" {
        return nil, errors.New("url parameter is required")
    }
    if len(whDraft.Events) == 0 {
        return nil, errors.New("events parameter is required")
    }
    return whService.CreateWebhook(ctx, &whDraft)

case "update_webhook":
    if webhookID == "" {
        return nil, errors.New("webhook_id parameter is required")
    }
    var whUpdate models.WebhookUpdate
    // Use mapstructure to decode directly from params to the struct
    if err := mapstructure.Decode(params, &whUpdate); err != nil {
        return nil, fmt.Errorf("error decoding update_webhook params: %w", err)
    }
    return whService.UpdateWebhook(ctx, webhookID, &whUpdate)

case "delete_webhook":
    if webhookID == "" {
        return nil, errors.New("webhook_id parameter is required")
    }
    return whService.DeleteWebhook(ctx, webhookID)

case "list_webhook_events":
    if webhookID == "" {
        return nil, errors.New("webhook_id parameter is required")
    }
    return whService.ListWebhookEvents(ctx, webhookID, options)

case "get_webhook_event":
    if eventID == "" {
        return nil, errors.New("event_id parameter is required")
    }
    return whService.GetWebhookEvent(ctx, eventID)

case "list_webhook_event_deliveries":
    if eventID == "" {
        return nil, errors.New("event_id parameter is required")
    }
    return whService.ListWebhookEventDeliveries(ctx, eventID, options)

case "resend_webhook_delivery":
    if deliveryID == "" {
        return nil, errors.New("delivery_id parameter is required")
    }
    return whService.ResendWebhookDelivery(ctx, deliveryID)

default:
    return nil, fmt.Errorf("unsupported Webhook capability: %s", capability)
}

}

// SyncUserAccounts reconciles the status of locally stored Unipile accounts // for a given framework user with the actual status from the Unipile API. func (a *UnipileAdapter) SyncUserAccounts(ctx context.Context, userID string) error { if !a.initialized { return errors.New("adapter not initialized") } if a.store == nil { return errors.New("unipile store not available") } if a.client == nil { return errors.New("unipile client not available") }

log.Printf("Starting Unipile account sync for user %s", userID)

// 1. Get locally stored accounts for the user
localAccounts, err := a.store.GetUserAccounts(ctx, userID)
if err != nil {
    log.Printf("Error fetching local Unipile accounts for user %s: %v", userID, err)
    return fmt.Errorf("failed to get local accounts for sync: %w", err)
}

if len(localAccounts) == 0 {
    log.Printf("No local Unipile accounts found for user %s to sync.", userID)
    return nil // Nothing to sync
}

// Get Unipile Account Service
accService := services.NewAccountService(a.client)
syncedCount := 0
updatedCount := 0
notFoundCount := 0
errorCount := 0

// 2. Iterate and check each account against Unipile API
for _, localAccount := range localAccounts {
    accountID := localAccount.AccountID
    log.Printf("Syncing account %s for user %s...", accountID, userID)

    // 3. Fetch current info from Unipile API
    apiCtx, cancel := context.WithTimeout(ctx, 15*time.Second)
    unipileAccount, err := accService.GetAccount(apiCtx, accountID) // Returns *models.Account
    cancel()

    if err != nil {
        // TODO: Refine error checking based on actual client implementation for 404.
        // For now, we log the error and assume it might be a 404 if GetAccount returns error.
        // A more robust check would inspect the error type or code.
        log.Printf("Error fetching account %s from Unipile API: %v. Assuming it might be deleted/unlinked.", accountID, err)
        // Attempt to update status to deleted_externally
        notFoundCount++
        newStatus := "deleted_externally"
        if localAccount.Status != newStatus {
            if updateErr := a.store.UpdateAccountStatus(ctx, accountID, newStatus); updateErr != nil {
                log.Printf("ERROR updating local status for account %s (presumed not found externally): %v", accountID, updateErr)
                errorCount++
            } else {
                updatedCount++
            }
        } else {
            log.Printf("Local status for account %s already reflects external deletion.", accountID)
        }
        continue // Move to the next account
    }

    if unipileAccount == nil {
        // Unipile GetAccount returned nil error and nil account info - should not happen
        log.Printf("WARN: Unipile API returned nil info and nil error for account %s. Skipping.", accountID)
        errorCount++
        continue // Move to the next account
    }

    // --- DEBUG: Log the received account info as JSON ---
    accountJSON, jsonErr := json.MarshalIndent(unipileAccount, "", "  ") // Indent for readability
    if jsonErr != nil {
        log.Printf("DEBUG: Failed to marshal account info %s to JSON for logging: %v", accountID, jsonErr)
    } else {
        log.Printf("DEBUG: Received account info for %s from API:\n%s", accountID, string(accountJSON))
    }
    // --- END DEBUG ---

    // 4. Account found, process it
    syncedCount++
    apiStatus := unipileAccount.Status // Status from models.Account
    if apiStatus == "" {
        apiStatus = "active" // Default if status is empty
        log.Printf("WARN: Unipile API returned account %s without an explicit status. Assuming 'active'.", accountID)
    }

    if localAccount.Status != apiStatus {
        log.Printf("Status mismatch for account %s. Local: '%s', API: '%s'. Updating local status.", accountID, localAccount.Status, apiStatus)
        // Update local status only
        if updateErr := a.store.UpdateAccountStatus(ctx, accountID, apiStatus); updateErr != nil {
            log.Printf("ERROR updating local status for account %s: %v", accountID, updateErr)
            errorCount++
        } else {
            updatedCount++
        }
    } else {
        // Status matches, refresh metadata and timestamp by calling StoreAccount
        log.Printf("Status for account %s matches ('%s'). Refreshing metadata/timestamp.", accountID, apiStatus)

        // Create a temporary map for StoreAccount, passing the marshaled bytes
        // StoreAccount expects map[string]interface{}, but we pass marshaled bytes which it handles internally.
        // Correction: StoreAccount actually expects map[string]interface{} and marshals *itself*.
        // We should pass the map directly.
        if updateErr := a.store.StoreAccount(ctx, userID, accountID, unipileAccount.Provider, apiStatus, unipileAccount.Metadata /* pass map directly */); updateErr != nil {
            log.Printf("ERROR refreshing local metadata/timestamp for account %s: %v", accountID, updateErr)
            errorCount++
        }
    }
}

log.Printf("Unipile account sync completed for user %s. Total Checked: %d, Synced (API Found): %d, API Not Found/Error: %d, Updated Locally: %d, Errors During Update: %d",
    userID, len(localAccounts), syncedCount, notFoundCount, updatedCount, errorCount)

if errorCount > 0 {
    return fmt.Errorf("sync encountered %d errors during update/refresh", errorCount)
}

return nil

}

// TODO: Refine error checking for API Not Found based on how the unipile client signals it. // TODO: Verify the structure of models.Account returned by accService.GetAccount, especially Metadata field.

// scheduleAccountSyncJob schedules the recurring job to sync Unipile accounts. // This is a simplified placeholder implementation. func (a *UnipileAdapter) scheduleAccountSyncJob(ctx context.Context, interval string) error { slog.InfoContext(ctx, "Attempting to schedule Unipile account sync job", "interval", interval)

if a.jobMgr == nil {
    return fmt.Errorf("cannot schedule sync job: JobManager is not initialized")
}

// Check if a job with this process ID and a similar name already exists (optional, prevents duplicates)
// listOpts := &jobmanager.ListJobsOptions{ ProcessID: &unipileAccountSyncProcessID } // Assuming ProcessID is filterable
// existingJobs, err := a.jobMgr.ListJobs(ctx, listOpts)
// if err != nil { ... handle error ... }
// if len(existingJobs) > 0 { ... job already exists ... }

job := &jobmanager.Job{
    Name:       "Unipile Account Sync",
    ProcessID:  unipileAccountSyncProcessID, // Ensure this constant is defined
    Status:     jobmanager.JobStatusPending,
    Schedule:   &interval, // Use pointer to string for schedule
    MaxRetries: 3,
    // Params: could include batch size, etc. if needed
}

createdJob, err := a.jobMgr.CreateJob(ctx, job)
if err != nil {
    slog.ErrorContext(ctx, "Failed to create Unipile account sync job", "error", err)
    return fmt.Errorf("failed to create account sync job: %w", err)
}

slog.InfoContext(ctx, "Successfully created Unipile account sync job", "job_id", createdJob.ID, "schedule", *createdJob.Schedule)
return nil

}

// --- End of adapter.go ---

// File: auth.go

//pkg/providers/unipile/auth.go

package unipile

import ( "context" "fmt" "log" "net/http" "time"

"github.com/gofiber/fiber/v2"
"gitlab.com/webigniter/slop-server/internal/auth"

)

// GenerateAuthLinkHandler creates a handler for generating Unipile auth links func (a UnipileAdapter) GenerateAuthLinkHandler() fiber.Handler { return func(c fiber.Ctx) error { // Get user ID from JWT token userID, err := auth.GetUserIDFromToken(c) if err != nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ "error": "Unauthorized", "message": "Invalid token", }) }

    // Parse request
    var req struct {
        Providers []string `json:"providers"`
        Type      string   `json:"type"`                 // create or reconnect
        AccountID string   `json:"account_id,omitempty"` // for reconnect
    }

    if err := c.BodyParser(&req); err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error":   "Bad Request",
            "message": "Invalid request format",
        })
    }

    // Validate request
    if req.Type == "" {
        req.Type = "create" // Default to create
    }

    if req.Type != "create" && req.Type != "reconnect" {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error":   "Bad Request",
            "message": "Type must be 'create' or 'reconnect'",
        })
    }

    if req.Type == "reconnect" && req.AccountID == "" {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error":   "Bad Request",
            "message": "Account ID is required for reconnect",
        })
    }

    // Default to all providers if none specified
    if len(req.Providers) == 0 {
        req.Providers = []string{"*"}
    }

    // Generate auth link
    link, err := a.GenerateAuthLink(c.Context(), userID, req.Providers, req.Type, req.AccountID)
    if err != nil {
        return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
            "error":   "Internal Server Error",
            "message": err.Error(),
        })
    }

    return c.JSON(fiber.Map{
        "url": link,
    })
}

}

// AuthCallbackHandler handles callbacks from Unipile auth func (a UnipileAdapter) AuthCallbackHandler() fiber.Handler { return func(c fiber.Ctx) error { // Parse notification payload var notification struct { Status string json:"status" AccountID string json:"account_id" Name string json:"name" // This is the userID passed in the auth link request }

    if err := c.BodyParser(&notification); err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error":   "Bad Request",
            "message": "Invalid notification format",
        })
    }

    // Validate notification
    if notification.AccountID == "" {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error":   "Bad Request",
            "message": "Account ID is required",
        })
    }

    if notification.Name == "" {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error":   "Bad Request",
            "message": "User ID (name) is required",
        })
    }

    // Process the notification
    err := a.ProcessAuthCallback(c.Context(), notification.Name, notification.AccountID, notification.Status)
    if err != nil {
        return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
            "error":   "Internal Server Error",
            "message": err.Error(),
        })
    }

    return c.JSON(fiber.Map{
        "status": "success",
    })
}

}

// ProcessAuthCallback handles callbacks from Unipile auth func (a *UnipileAdapter) ProcessAuthCallback(ctx context.Context, userID, accountID, status string) error { // Get account type from Unipile API var accountInfo struct { Provider string json:"provider" }

// Use a context with timeout for the API call
apiCtx, cancel := context.WithTimeout(ctx, 15*time.Second) // Use incoming ctx, add timeout
defer cancel()

err := a.client.Request(apiCtx, http.MethodGet, fmt.Sprintf("/accounts/%s", accountID), nil, &accountInfo)
if err != nil {
    return fmt.Errorf("error getting account info for %s: %w", accountID, err)
}

// Store in database
err = a.store.StoreAccount(ctx, userID, accountID, accountInfo.Provider, status, nil) // Use incoming ctx
if err != nil {
    return fmt.Errorf("error storing account %s for user %s: %w", accountID, userID, err)
}

log.Printf("Successfully stored account %s for user %s with status %s", accountID, userID, status)
return nil

}

// --- End of auth.go ---

// File: client.go

//pkg/providers/unipile/client.go

package unipile

import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "time"

"gitlab.com/webigniter/slop-server/internal/providers"
"gitlab.com/webigniter/slop-server/pkg/providers/unipile/services"

)

// UnipileClient is an HTTP client for the Unipile API type UnipileClient struct { BaseURL string APIKey string HTTPClient http.Client Registry providers.ServiceRegistry }

// NewUnipileClient creates a new Unipile client func NewUnipileClient(baseURL, apiKey string) *UnipileClient { client := &UnipileClient{ BaseURL: baseURL, APIKey: apiKey, HTTPClient: &http.Client{ Timeout: 30 * time.Second, }, }

// Create service registry
client.Registry = providers.NewServiceRegistry()

return client

}

// Request sends a request to the Unipile API func (c *UnipileClient) Request(ctx context.Context, method, path string, body interface{}, response interface{}) error { url := c.BaseURL + path

var reqBody io.Reader
if body != nil {
    jsonBody, err := json.Marshal(body)
    if err != nil {
        return fmt.Errorf("error serializing request body: %w", err)
    }
    reqBody = bytes.NewReader(jsonBody)
}

req, err := http.NewRequestWithContext(ctx, method, url, reqBody)
if err != nil {
    return fmt.Errorf("error creating request: %w", err)
}

// Add headers
req.Header.Set("X-API-KEY", c.APIKey)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")

// Send request
resp, err := c.HTTPClient.Do(req)
if err != nil {
    return fmt.Errorf("error executing request: %w", err)
}
defer resp.Body.Close()

// Read response body
respBody, err := io.ReadAll(resp.Body)
if err != nil {
    return fmt.Errorf("error reading response: %w", err)
}

// Check status code
if resp.StatusCode >= 400 {
    var errorResp ErrorResponse
    if err := json.Unmarshal(respBody, &errorResp); err != nil {
        // If error response can't be parsed, return raw error
        return fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(respBody))
    }
    return fmt.Errorf("API error (%d): %s - %s", errorResp.Status, errorResp.Title, errorResp.Detail)
}

// Parse response
if response != nil {
    if err := json.Unmarshal(respBody, response); err != nil {
        return fmt.Errorf("error parsing response: %w", err)
    }
}

return nil

}

// ErrorResponse represents an error response from the Unipile API type ErrorResponse struct { Title string json:"title" Detail string json:"detail" Status int json:"status" Type string json:"type,omitempty" Instance string json:"instance,omitempty" }

// InitServices initializes all available services in the registry func (c *UnipileClient) InitServices() { // Create and register services linkedInService := services.NewLinkedInService(c) emailService := services.NewEmailService(c) messagingService := services.NewMessagingService(c) webhookService := services.NewWebhookService(c) accountService := services.NewAccountService(c) calendarService := services.NewCalendarService(c) pageService := services.NewPageService(c) postService := services.NewPostService(c)

// Register services with the registry
c.Registry.Register("linkedin", linkedInService)
c.Registry.Register("email", emailService)
c.Registry.Register("messaging", messagingService)
c.Registry.Register("webhook", webhookService)
c.Registry.Register("account", accountService)
c.Registry.Register("calendar", calendarService)
c.Registry.Register("page", pageService)
c.Registry.Register("post", postService)

}

// GetSlopAdapter returns a new SLOP adapter for this client func (c UnipileClient) GetSlopAdapter() providers.SlopAdapter { return providers.NewSlopAdapter(c.Registry) }

// SetTimeout sets the timeout for the HTTP client func (c *UnipileClient) SetTimeout(timeout time.Duration) { c.HTTPClient.Timeout = timeout }

// --- End of client.go ---

// File: email_webhook_handler.go

//pkg/providers/unipile/email_webhook_handler.go

package unipile

import ( "bytes" "context" "encoding/json" "fmt" "log" "strings"

"gitlab.com/webigniter/slop-server/pkg/providers/unipile/models"

)

const ( // Define constants for resource type and event channel name resourceTypeWebhookPayload = "unipile_email_webhook_payload" eventEmailReceived = "unipile.email.received" )

// processEmailWebhook processes the email webhook payload func (a UnipileAdapter) processEmailWebhook(ctx context.Context, payload models.WebhookPayload) { log.Printf("Email webhook received: Event='%s', Subject='%s', From='%s', AccountID='%s', MessageID='%s'", payload.Event, payload.Subject, payload.FromAttendee.Identifier, payload.AccountID, payload.MessageID)

// Save the raw webhook payload to unipile's own store first
if err := a.store.SaveEmail(ctx, payload); err != nil {
    log.Printf("ERROR saving email webhook payload (MessageID: %s): %v", payload.MessageID, err)
    // Decide if we should stop or continue if save fails. For now, continue.
}

// Process based on event type
switch payload.Event {
case "email_received":
    log.Printf("Processing 'email_received' for message %s, account %s", payload.MessageID, payload.AccountID)

    // Ensure dependencies are available
    if a.resourceMgr == nil || a.notifier == nil {
        log.Printf("ERROR: ResourceManager or Notifier not initialized in UnipileAdapter. Cannot process event for message %s.", payload.MessageID)
        return
    }

    // 1. Marshal the raw webhook payload to JSON
    payloadJSON, err := json.Marshal(payload)
    if err != nil {
        log.Printf("ERROR marshaling webhook payload for message %s: %v", payload.MessageID, err)
        return // Cannot proceed without JSON data
    }

    // 2. Look up framework UserID from AccountID
    accountInfo, err := a.store.GetAccount(ctx, payload.AccountID)
    if err != nil {
        log.Printf("ERROR looking up account mapping for %s: %v. Cannot process webhook resource/event.", payload.AccountID, err)
        return
    }
    if accountInfo == nil || accountInfo.UserID == "" {
        log.Printf("WARN: Unipile account %s is not mapped to a framework user. Cannot process webhook resource/event.", payload.AccountID)
        return // Skip resource creation if no user mapping
    }
    userID := accountInfo.UserID // Framework user ID

    // 3. Create a Resource for the raw webhook payload, associated with the user
    resourceTitle := fmt.Sprintf("Unipile Email Webhook Payload: %s", payload.Subject)
    contentType := "application/json"
    resourceMetadata := map[string]string{
        "unipile_account_id": payload.AccountID,
        "unipile_message_id": payload.MessageID,
        "unipile_email_id":   payload.EmailID,
        "webhook_event":      payload.Event,
    }
    log.Printf("Creating resource '%s' for message %s (User: %s)...", resourceTypeWebhookPayload, payload.MessageID, userID)

    // Pass nil for tags, and pointer to userID
    createdResource, err := a.resourceMgr.CreateResource(ctx, bytes.NewReader(payloadJSON), resourceTitle, resourceTypeWebhookPayload, contentType, nil /* tags */, resourceMetadata, &userID)
    if err != nil {
        log.Printf("ERROR creating resource for webhook payload (MessageID: %s, User: %s): %v", payload.MessageID, userID, err)
        return // Cannot publish event without resource ID
    }
    log.Printf("Successfully created resource %s for webhook payload (MessageID: %s, User: %s)", createdResource.ID, payload.MessageID, userID)

    // 4. Publish an event with the resource ID and framework userID
    eventPayload := map[string]interface{}{
        "webhookPayloadResourceId": createdResource.ID,
        "accountId":                payload.AccountID,
        "messageId":                payload.MessageID,
        "emailId":                  payload.EmailID,
        "subject":                  payload.Subject,
        "from":                     payload.FromAttendee.Identifier,
        "userID":                   userID, // Include framework user ID
    }
    log.Printf("Publishing event '%s' for resource %s (MessageID: %s, User: %s)...", eventEmailReceived, createdResource.ID, payload.MessageID, userID)
    if err := a.notifier.Publish(ctx, eventEmailReceived, eventPayload); err != nil {
        // Log the error, but maybe don't stop processing other webhook types?
        log.Printf("ERROR publishing event '%s' for resource %s (MessageID: %s, User: %s): %v", eventEmailReceived, createdResource.ID, payload.MessageID, userID, err)
    }

// Handle other event types if needed...
case "email_sent":
    log.Printf("Email sent for message %s", payload.MessageID)
case "email_read":
    log.Printf("Email read for message %s", payload.MessageID)
case "email_deleted":
    log.Printf("Email deleted: %s", payload.EmailID)
case "email_draft":
    log.Printf("Email draft saved: %s", payload.EmailID)
default:
    log.Printf("Unknown email event type '%s' for message %s", payload.Event, payload.MessageID)
}

// Run general notifications (original logic)
a.processEmailNotifications(payload)

}

// processEmailNotifications handles notifications based on email content func (a UnipileAdapter) processEmailNotifications(payload models.WebhookPayload) { // Implement notification logic based on email content or metadata // For example: high priority emails, specific senders, keyword matching, etc.

// Example: Check for high priority or specific subjects
if payload.Subject != "" {
    if contains(payload.Subject, "urgent") || contains(payload.Subject, "important") {
        log.Printf("High priority email detected: %s", payload.Subject)
        // TODO: Send notification or create alert
    }
}

}

// Helper function to check if a string contains a substring (case-insensitive) func contains(str, substr string) bool { s, sub := strings.ToLower(str), strings.ToLower(substr) return strings.Contains(s, sub) }

// --- End of email_webhook_handler.go ---

// File: helper.go

//pkg/providers/unipile/helper.go

package unipile

import ( "bytes" "io" "net/http" "net/http/httptest" "strings"

"github.com/gofiber/fiber/v2"

)

// fiberHandlerToHTTP converts a Fiber handler to a standard Go http.HandlerFunc. // It achieves this by creating a temporary Fiber app, running the request through it using app.Test, // and then writing the response back to the http.ResponseWriter. func fiberHandlerToHTTP(fiberHandler fiber.Handler) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Create a new Fiber app instance for this request app := fiber.New()

    // Register the specific handler for all methods and paths for this temporary app
    app.Use(fiberHandler) // Use .Use to catch all paths and methods

    // Perform the request against the Fiber app
    resp, err := app.Test(r)
    if err != nil {
        // Handle potential errors during the test request
        http.Error(w, "Internal Server Error during handler conversion", http.StatusInternalServerError)
        // Log the error for debugging purposes
        // log.Printf("Error testing Fiber handler: %v", err)
        return
    }
    defer resp.Body.Close()

    // Copy the status code from Fiber response to http.ResponseWriter
    w.WriteHeader(resp.StatusCode)

    // Copy headers from Fiber response to http.ResponseWriter
    for k, vv := range resp.Header {
        for _, v := range vv {
            w.Header().Add(k, v)
        }
    }

    // Copy the body from Fiber response to http.ResponseWriter
    if _, err := io.Copy(w, resp.Body); err != nil {
        // Log error during body copy
        // log.Printf("Error copying response body: %v", err)
        // Avoid writing header again if already written
        if w.Header().Get("Content-Type") == "" {
            // Maybe send an error status if nothing has been sent yet
        }
    }
}

}

// CreateFiberHandler converts a standard Go http.Handler to a Fiber handler. func CreateFiberHandler(handler http.Handler) fiber.Handler { return func(c *fiber.Ctx) error { // Create a response recorder w := httptest.NewRecorder()

    // Create a request from Fiber context
    req, err := http.NewRequest(
        c.Method(),
        c.OriginalURL(),
        bytes.NewReader(c.Body()),
    )
    if err != nil {
        return err
    }

    // Copy headers
    for k, v := range c.GetReqHeaders() {
        req.Header.Set(k, strings.Join(v, ","))
    }

    // Serve the request with the original handler
    handler.ServeHTTP(w, req)

    // Copy status code
    c.Status(w.Code)

    // Copy headers
    for k, v := range w.Header() {
        c.Set(k, v[0]) // Note: This might lose multiple values for the same header key
    }

    // Write the response body
    return c.Send(w.Body.Bytes())
}

}

// --- End of helper.go ---

// File: jobs_webhook.go

//pkg/providers/unipile/jobs_webhook.go

package unipile

import ( "context" "fmt" "net/http" "time"

"gitlab.com/webigniter/slop-server/internal/pubsub"

)

// Channel name for new email events const NewEmailChannel = "emails.new"

// NewEmailPayload defines the structure of the event payload type NewEmailPayload struct { EmailID string json:"emailId" // Add other relevant fields like UserID, AccountID, etc. }

// Assume 'pubsubManager' is an initialized instance of pubsub.Publisher passed or accessible here var pubsubMgr pubsub.Publisher

func HandleEmailWebhook(w http.ResponseWriter, r *http.Request) { ctx := r.Context()

if pubsubMgr == nil {
    fmt.Println("Error: PubSub Manager not initialized in Unipile webhook handler")
    http.Error(w, "Internal Server Error", http.StatusInternalServerError)
    return
}

// 1. Process webhook payload
// ... parse request ...

// 2. Store the email data in your database
newEmailID, err := storeReceivedEmail(ctx /* email data */)
if err != nil {
    // Log error
    fmt.Printf("Error storing received email: %v\n", err)
    http.Error(w, "Failed to store email", http.StatusInternalServerError)
    return
}
fmt.Printf("Stored new email with ID: %s\n", newEmailID)

// 3. Define the event payload
payload := NewEmailPayload{
    EmailID: newEmailID,
    // Populate other fields if added to the struct
}

// 4. Publish the event using the PubSub manager
err = pubsubMgr.Publish(ctx, NewEmailChannel, payload)
if err != nil {
    // Log error - Job creation will not be triggered if publish fails
    fmt.Printf("Error publishing NewEmail event for email %s: %v\n", newEmailID, err)
    // Decide how to handle - failure here means the job won't be triggered
    http.Error(w, "Failed processing event", http.StatusInternalServerError) // Example
    return
}

fmt.Printf("Successfully published event for new email %s to channel %s\n", newEmailID, NewEmailChannel)

// 5. Respond to the webhook source
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "Email received and event published.")

}

func storeReceivedEmail(ctx context.Context, data ...any) (string, error) { // Placeholder: implement your DB logic to store the email emailID := fmt.Sprintf("email-%d", time.Now().UnixNano()) fmt.Printf("Placeholder: Storing email, generated ID: %s\n", emailID) return emailID, nil }

// SetupPubSub needs to be called during app initialization to inject the publisher func SetupPubSub(publisher pubsub.Publisher) { if publisher == nil { // Handle error - perhaps panic or log fatal if pubsub is essential panic("PubSub publisher cannot be nil") } pubsubMgr = publisher fmt.Println("Unipile PubSub publisher initialized.") }

// --- End of jobs_webhook.go ---

// File: models/account.go

//pkg/providers/unipile/models/account.go

package models

import "time"

// Account représente un compte utilisateur dans l'API Unipile type Account struct { ID string json:"id" Provider string json:"provider" Type string json:"type,omitempty" // personal, business, organization Status string json:"status,omitempty" // active, disabled, pending Email string json:"email,omitempty" Username string json:"username,omitempty" Name string json:"name,omitempty" FirstName string json:"first_name,omitempty" LastName string json:"last_name,omitempty" DisplayName string json:"display_name,omitempty" AvatarURL string json:"avatar_url,omitempty" URL string json:"url,omitempty" Bio string json:"bio,omitempty" Location string json:"location,omitempty" Language string json:"language,omitempty" Timezone string json:"timezone,omitempty" IsVerified bool json:"is_verified,omitempty" CreatedAt time.Time json:"created_at,omitempty" UpdatedAt time.Time json:"updated_at,omitempty" LastLoginAt time.Time json:"last_login_at,omitempty" Capabilities []string json:"capabilities,omitempty" Metadata map[string]interface{} json:"metadata,omitempty" }

// AccountInfo represents stored account information for Unipile type AccountInfo struct { ID int64 json:"id" UserID string json:"user_id" AccountID string json:"account_id" Provider string json:"provider" // LINKEDIN, GMAIL, etc. Status string json:"status" // OK, CREDENTIALS, etc. Metadata []byte json:"metadata,omitempty" LastUpdate time.Time json:"last_update" CreatedAt time.Time json:"created_at" }

// AccountList représente une liste paginée de comptes type AccountList struct { Object string json:"object" Accounts []Account json:"accounts" Paging Paging json:"paging,omitempty" }

// AccountConnection représente une connexion entre comptes (suivi, abonnement, etc.) type AccountConnection struct { ID string json:"id,omitempty" Provider string json:"provider" SourceID string json:"source_id" // ID du compte qui suit TargetID string json:"target_id" // ID du compte suivi Type string json:"type,omitempty" // follow, friend, connection Status string json:"status,omitempty" // pending, accepted, blocked CreatedAt time.Time json:"created_at,omitempty" AcceptedAt time.Time json:"accepted_at,omitempty" Source Account json:"source,omitempty" Target Account json:"target,omitempty" }

// AccountConnectionList représente une liste paginée de connexions type AccountConnectionList struct { Object string json:"object" Connections []AccountConnection json:"connections" Paging Paging json:"paging,omitempty" }

// AccountConnectionRequest représente une demande de connexion entre comptes type AccountConnectionRequest struct { Provider string json:"provider" SourceID string json:"source_id,omitempty" // ID du compte qui demande TargetID string json:"target_id" // ID du compte ciblé Type string json:"type,omitempty" // follow, friend, connection Message string json:"message,omitempty" }

// AccountConnectionResponse représente la réponse après une demande de connexion type AccountConnectionResponse struct { ID string json:"id,omitempty" Provider string json:"provider" SourceID string json:"source_id" TargetID string json:"target_id" Status string json:"status" // pending, accepted, rejected, failed CreatedAt time.Time json:"created_at,omitempty" Connection AccountConnection json:"connection,omitempty" }

// AccountUpdate représente une mise à jour de compte type AccountUpdate struct { Name string json:"name,omitempty" FirstName string json:"first_name,omitempty" LastName string json:"last_name,omitempty" DisplayName string json:"display_name,omitempty" Email string json:"email,omitempty" Username string json:"username,omitempty" AvatarURL string json:"avatar_url,omitempty" Bio string json:"bio,omitempty" Location string json:"location,omitempty" Language string json:"language,omitempty" Timezone string json:"timezone,omitempty" Metadata map[string]interface{} json:"metadata,omitempty" }

// AccountUpdateResponse représente la réponse après une mise à jour de compte type AccountUpdateResponse struct { ID string json:"id" Provider string json:"provider" Status string json:"status" // updated, failed UpdatedAt time.Time json:"updated_at" Changes map[string]interface{} json:"changes,omitempty" Account Account json:"account,omitempty" }

// OAuth représente les informations d'authentification OAuth type OAuth struct { Provider string json:"provider" AccessToken string json:"access_token" RefreshToken string json:"refresh_token,omitempty" ExpiresAt time.Time json:"expires_at,omitempty" TokenType string json:"token_type,omitempty" Scope string json:"scope,omitempty" Account Account json:"account,omitempty" }

// OAuthRefreshResponse représente la réponse après un rafraîchissement de token type OAuthRefreshResponse struct { Provider string json:"provider" Status string json:"status" // success, failed AccessToken string json:"access_token,omitempty" RefreshToken string json:"refresh_token,omitempty" ExpiresAt time.Time json:"expires_at,omitempty" TokenType string json:"token_type,omitempty" Scope string json:"scope,omitempty" }

// AccountSearchRequest représente une requête de recherche de comptes type AccountSearchRequest struct { Provider string json:"provider,omitempty" Query string json:"query" Type string json:"type,omitempty" Status string json:"status,omitempty" Keywords []string json:"keywords,omitempty" Location string json:"location,omitempty" Distance int json:"distance,omitempty" Limit int json:"limit,omitempty" Cursor string json:"cursor,omitempty" }

// --- End of models/account.go ---

// File: models/calendar.go

//pkg/providers/unipile/models/calendar.go

package models

import "time"

// CalendarEvent représente un événement de calendrier dans l'API Unipile type CalendarEvent struct { ID string json:"id" Provider string json:"provider" AccountID string json:"account_id,omitempty" Title string json:"title" Description string json:"description,omitempty" Location string json:"location,omitempty" StartAt time.Time json:"start_at" EndAt time.Time json:"end_at" TimeZone string json:"time_zone,omitempty" IsAllDay bool json:"is_all_day,omitempty" IsRecurring bool json:"is_recurring,omitempty" RecurrenceRule string json:"recurrence_rule,omitempty" Status string json:"status,omitempty" // confirmed, tentative, cancelled Visibility string json:"visibility,omitempty" // public, private, default Organizer Attendee json:"organizer,omitempty" Attendees []Attendee json:"attendees,omitempty" Attachments []Attachment json:"attachments,omitempty" URL string json:"url,omitempty" CreatedAt time.Time json:"created_at,omitempty" UpdatedAt time.Time json:"updated_at,omitempty" Metadata map[string]interface{} json:"metadata,omitempty" }

// Attendee représente un participant à un événement type Attendee struct { ID string json:"id,omitempty" Name string json:"name,omitempty" Email string json:"email" Status string json:"status,omitempty" // accepted, declined, tentative, needs_action Optional bool json:"optional,omitempty" Comment string json:"comment,omitempty" }

// CalendarEventList représente une liste paginée d'événements type CalendarEventList struct { Object string json:"object" Events []CalendarEvent json:"events" Paging Paging json:"paging,omitempty" }

// CalendarEventDraft représente un brouillon d'événement à créer type CalendarEventDraft struct { AccountID string json:"account_id,omitempty" Title string json:"title" Description string json:"description,omitempty" Location string json:"location,omitempty" StartAt string json:"start_at" // ISO 8601 date EndAt string json:"end_at" // ISO 8601 date TimeZone string json:"time_zone,omitempty" IsAllDay bool json:"is_all_day,omitempty" IsRecurring bool json:"is_recurring,omitempty" RecurrenceRule string json:"recurrence_rule,omitempty" Status string json:"status,omitempty" // confirmed, tentative, cancelled Visibility string json:"visibility,omitempty" // public, private, default Attendees []Attendee json:"attendees,omitempty" Attachments []Attachment json:"attachments,omitempty" }

// CalendarEventCreated représente la réponse après la création d'un événement type CalendarEventCreated struct { ID string json:"id" Provider string json:"provider" AccountID string json:"account_id" Status string json:"status" // created, failed CreatedAt time.Time json:"created_at" Event CalendarEvent json:"event,omitempty" }

// CalendarEventUpdated représente la réponse après la mise à jour d'un événement type CalendarEventUpdated struct { ID string json:"id" Provider string json:"provider" AccountID string json:"account_id" Status string json:"status" // updated, failed UpdatedAt time.Time json:"updated_at" Changes map[string]interface{} json:"changes,omitempty" Event CalendarEvent json:"event,omitempty" }

// CalendarEventDeleted représente la réponse après la suppression d'un événement type CalendarEventDeleted struct { ID string json:"id" Provider string json:"provider" AccountID string json:"account_id" Status string json:"status" // deleted, failed }

// CalendarEventRSVP représente la réponse à une invitation à un événement type CalendarEventRSVP struct { EventID string json:"event_id" Provider string json:"provider" AccountID string json:"account_id,omitempty" Status string json:"status" // accepted, declined, tentative Comment string json:"comment,omitempty" }

// CalendarEventRSVPResponse représente la réponse après l'envoi d'un RSVP type CalendarEventRSVPResponse struct { EventID string json:"event_id" Provider string json:"provider" AccountID string json:"account_id,omitempty" Status string json:"status" // success, failed UpdatedAt time.Time json:"updated_at,omitempty" }

// CalendarSearchRequest représente une requête de recherche d'événements type CalendarSearchRequest struct { AccountID string json:"account_id,omitempty" Query string json:"query,omitempty" StartAfter string json:"start_after,omitempty" // ISO 8601 date EndBefore string json:"end_before,omitempty" // ISO 8601 date Status string json:"status,omitempty" Limit int json:"limit,omitempty" Cursor string json:"cursor,omitempty" }

// --- End of models/calendar.go ---

// File: models/common.go

//pkg/providers/unipile/models/common.go

package models

import ( "context" )

// Types communs pour le package unipile

// UnipileRequester définit l'interface pour effectuer des requêtes à l'API Unipile type UnipileRequester interface { // Request envoie une requête HTTP à l'API Unipile et décode la réponse JSON dans l'objet response Request(ctx context.Context, method, path string, body interface{}, response interface{}) error }

// QueryOptions représente les options de pagination pour les requêtes d'API type QueryOptions struct { Limit int json:"limit,omitempty" Cursor string json:"cursor,omitempty" }

// Paging contient les informations de pagination type Paging struct { Start int json:"start,omitempty" PageCount int json:"page_count" TotalCount int json:"total_count,omitempty" Cursor string json:"cursor,omitempty" }

// Author représente l'auteur d'un contenu (utilisé par différents types) type Author struct { ID string json:"id" Name string json:"name" Username string json:"username,omitempty" Email string json:"email,omitempty" Avatar string json:"avatar,omitempty" PublicIdentifier string json:"public_identifier,omitempty" IsCompany bool json:"is_company,omitempty" Headline string json:"headline,omitempty" Type string json:"type,omitempty" ProfileURL string json:"profile_url,omitempty" NetworkDistance string json:"network_distance,omitempty" }

// Attachment représente une pièce jointe à différentes ressources type Attachment struct { Type string json:"type" URL string json:"url" PreviewURL string json:"preview_url,omitempty" Title string json:"title,omitempty" Description string json:"description,omitempty" Size int64 json:"size,omitempty" URLExpiresAt string json:"url_expires_at,omitempty" }

// AttachmentContent représente le contenu d'une pièce jointe type AttachmentContent struct { Type string json:"type" Data string json:"data" Filename string json:"filename,omitempty" ContentType string json:"content_type,omitempty" }

// Poll représente un sondage type Poll struct { ID string json:"id" TotalVotesCount int json:"total_votes_count" Question string json:"question" IsOpen bool json:"is_open" Options []struct { ID string json:"id" Text string json:"text" Win bool json:"win" VotesCount int json:"votes_count" } json:"options" }

// Group représente un groupe type Group struct { ID string json:"id" Name string json:"name" Private bool json:"private" }

// ErrorResponse représente une réponse d'erreur détaillée de l'API Unipile type ErrorResponse struct { Title string json:"title" Detail string json:"detail" Instance string json:"instance" Type string json:"type" Status int json:"status" ConnectionParams struct { IMAPHost string json:"imap_host,omitempty" IMAPEncryption string json:"imap_encryption,omitempty" IMAPPort int json:"imap_port,omitempty" IMAPUser string json:"imap_user,omitempty" SMTPHost string json:"smtp_host,omitempty" SMTPPort int json:"smtp_port,omitempty" SMTPUser string json:"smtp_user,omitempty" } json:"connectionParams,omitempty" }

// --- End of models/common.go ---

// File: models/email.go

//pkg/providers/unipile/models/email.go

package models

import "time"

// Email représente un email dans l'API Unipile type Email struct { ID string json:"id" Provider string json:"provider" AccountID string json:"account_id" ThreadID string json:"thread_id,omitempty" From EmailAddress json:"from" To []EmailAddress json:"to" Cc []EmailAddress json:"cc,omitempty" Bcc []EmailAddress json:"bcc,omitempty" ReplyTo []EmailAddress json:"reply_to,omitempty" Subject string json:"subject" Body string json:"body" BodyType string json:"body_type,omitempty" // html, text Attachments []Attachment json:"attachments,omitempty" Date string json:"date" ReceivedAt time.Time json:"received_at,omitempty" SentAt time.Time json:"sent_at,omitempty" Status string json:"status,omitempty" // draft, sent, received IsRead bool json:"is_read,omitempty" IsFlagged bool json:"is_flagged,omitempty" IsDeleted bool json:"is_deleted,omitempty" Labels []string json:"labels,omitempty" Folders []string json:"folders,omitempty" Flags []string json:"flags,omitempty" }

// EmailAddress représente une adresse email type EmailAddress struct { Name string json:"name,omitempty" Address string json:"address" }

// EmailList représente une liste paginée d'emails type EmailList struct { Object string json:"object" Emails []Email json:"emails" Paging Paging json:"paging,omitempty" }

// EmailThread représente un fil de discussion d'emails type EmailThread struct { ID string json:"id" Provider string json:"provider" AccountID string json:"account_id,omitempty" Subject string json:"subject" Snippet string json:"snippet,omitempty" Emails []Email json:"emails,omitempty" IsRead bool json:"is_read,omitempty" Participants []EmailAddress json:"participants,omitempty" LastMessageAt time.Time json:"last_message_at,omitempty" MessageCount int json:"message_count,omitempty" Labels []string json:"labels,omitempty" Folders []string json:"folders,omitempty" }

// EmailThreadList représente une liste paginée de fils de discussion type EmailThreadList struct { Object string json:"object" Threads []EmailThread json:"threads" Paging Paging json:"paging,omitempty" }

// EmailDraft représente un brouillon d'email à envoyer type EmailDraft struct { AccountID string json:"account_id,omitempty" ThreadID string json:"thread_id,omitempty" To []string json:"to" Cc []string json:"cc,omitempty" Bcc []string json:"bcc,omitempty" Subject string json:"subject" Body string json:"body" BodyType string json:"body_type,omitempty" // html, text Attachments []Attachment json:"attachments,omitempty" ReplyToID string json:"reply_to_id,omitempty" }

// EmailSent représente la réponse après l'envoi d'un email type EmailSent struct { ID string json:"id" Provider string json:"provider" AccountID string json:"account_id" ThreadID string json:"thread_id,omitempty" Status string json:"status" // sent, failed SentAt time.Time json:"sent_at,omitempty" Email Email json:"email,omitempty" }

// EmailDeleted représente la réponse après la suppression d'un email type EmailDeleted struct { ID string json:"id" Provider string json:"provider" AccountID string json:"account_id" Status string json:"status" // deleted, failed }

// EmailModified représente la réponse après la modification d'un email type EmailModified struct { ID string json:"id" Provider string json:"provider" AccountID string json:"account_id" Status string json:"status" // modified, failed Changes map[string]interface{} json:"changes,omitempty" }

// EmailAttachmentUploaded représente la réponse après le téléversement d'une pièce jointe type EmailAttachmentUploaded struct { ID string json:"id" Provider string json:"provider" AccountID string json:"account_id" Name string json:"name" Size int64 json:"size" Type string json:"type" URL string json:"url,omitempty" UploadedAt time.Time json:"uploaded_at" }

// EmailSearchRequest représente une requête de recherche d'emails type EmailSearchRequest struct { AccountID string json:"account_id,omitempty" Query string json:"query" Folder string json:"folder,omitempty" Labels []string json:"labels,omitempty" From string json:"from,omitempty" To string json:"to,omitempty" Subject string json:"subject,omitempty" HasAttachment bool json:"has_attachment,omitempty" After string json:"after,omitempty" // ISO 8601 date Before string json:"before,omitempty" // ISO 8601 date IsRead *bool json:"is_read,omitempty" Limit int json:"limit,omitempty" Cursor string json:"cursor,omitempty" }

// --- End of models/email.go ---

// File: models/linkedin.go

//pkg/providers/unipile/models/linkedin.go

package models

// LinkedInProfile représente un profil LinkedIn type LinkedInProfile struct { ID string json:"id" Provider string json:"provider" Name string json:"name" Headline string json:"headline,omitempty" Summary string json:"summary,omitempty" PublicIdentifier string json:"public_identifier,omitempty" Industry string json:"industry,omitempty" Location string json:"location,omitempty" Country string json:"country,omitempty" ProfileURL string json:"profile_url,omitempty" PictureURL string json:"picture_url,omitempty" NetworkDistance string json:"network_distance,omitempty" Email string json:"email,omitempty" Phone string json:"phone,omitempty" Positions []Position json:"positions,omitempty" Education []Education json:"education,omitempty" Skills []Skill json:"skills,omitempty" Languages []Language json:"languages,omitempty" Certifications []Certification json:"certifications,omitempty" }

// Position représente une expérience professionnelle sur LinkedIn type Position struct { Title string json:"title" CompanyName string json:"company_name" CompanyID string json:"company_id,omitempty" Location string json:"location,omitempty" StartDate string json:"start_date,omitempty" EndDate string json:"end_date,omitempty" Description string json:"description,omitempty" Current bool json:"current,omitempty" CompanyPictureURL string json:"company_picture_url,omitempty" }

// Education représente une formation sur LinkedIn type Education struct { SchoolName string json:"school_name" SchoolID string json:"school_id,omitempty" Degree string json:"degree,omitempty" FieldOfStudy string json:"field_of_study,omitempty" StartDate string json:"start_date,omitempty" EndDate string json:"end_date,omitempty" Description string json:"description,omitempty" Activities string json:"activities,omitempty" SchoolPictureURL string json:"school_picture_url,omitempty" }

// Skill représente une compétence LinkedIn type Skill struct { Name string json:"name" Endorsements int json:"endorsements,omitempty" }

// Language représente une langue parlée sur LinkedIn type Language struct { Name string json:"name" Proficiency string json:"proficiency,omitempty" }

// Certification représente une certification sur LinkedIn type Certification struct { Name string json:"name" Organization string json:"organization,omitempty" IssueDate string json:"issue_date,omitempty" ExpirationDate string json:"expiration_date,omitempty" CredentialID string json:"credential_id,omitempty" CredentialURL string json:"credential_url,omitempty" }

// LinkedInProfileList représente une liste paginée de profils LinkedIn type LinkedInProfileList struct { Object string json:"object" Profiles []LinkedInProfile json:"profiles" Paging Paging json:"paging,omitempty" }

// LinkedInCompany représente une entreprise LinkedIn type LinkedInCompany struct { ID string json:"id" Provider string json:"provider" Name string json:"name" Description string json:"description,omitempty" Industry string json:"industry,omitempty" Size string json:"size,omitempty" Location string json:"location,omitempty" Website string json:"website,omitempty" LogoURL string json:"logo_url,omitempty" BannerURL string json:"banner_url,omitempty" Founded string json:"founded,omitempty" Specialties []string json:"specialties,omitempty" Type string json:"type,omitempty" FollowerCount int json:"follower_count,omitempty" EmployeeCount int json:"employee_count,omitempty" CompanyURL string json:"company_url,omitempty" PublicIdentifier string json:"public_identifier,omitempty" }

// LinkedInCompanyList représente une liste paginée d'entreprises LinkedIn type LinkedInCompanyList struct { Object string json:"object" Companies []LinkedInCompany json:"companies" Paging Paging json:"paging,omitempty" }

// LinkedInInvitation représente une invitation de connexion LinkedIn type LinkedInInvitation struct { ID string json:"id" Provider string json:"provider" Type string json:"type" // sent, received Status string json:"status" // pending, accepted, ignored CreatedAt string json:"created_at" Message string json:"message,omitempty" SenderID string json:"sender_id,omitempty" SenderName string json:"sender_name,omitempty" RecipientID string json:"recipient_id,omitempty" RecipientName string json:"recipient_name,omitempty" }

// LinkedInInvitationList représente une liste paginée d'invitations LinkedIn type LinkedInInvitationList struct { Object string json:"object" Invitations []LinkedInInvitation json:"invitations" Paging Paging json:"paging,omitempty" }

// LinkedInInvitationSent représente la réponse après l'envoi d'une invitation type LinkedInInvitationSent struct { ID string json:"id" Provider string json:"provider" SenderID string json:"sender_id" RecipientID string json:"recipient_id" Message string json:"message,omitempty" Status string json:"status" CreatedAt string json:"created_at" }

// LinkedInSearchRequest représente une requête de recherche LinkedIn type LinkedInSearchRequest struct { Query string json:"query,omitempty" Keywords []string json:"keywords,omitempty" Location string json:"location,omitempty" Distance int json:"distance,omitempty" CurrentCompany string json:"current_company,omitempty" PastCompany string json:"past_company,omitempty" School string json:"school,omitempty" Industry string json:"industry,omitempty" ConnectionDegree int json:"connection_degree,omitempty" // 1, 2, 3 Limit int json:"limit,omitempty" Cursor string json:"cursor,omitempty" }

// LinkedInMessageSent représente la réponse après l'envoi d'un message LinkedIn type LinkedInMessageSent struct { ID string json:"id" Provider string json:"provider" SenderID string json:"sender_id" RecipientID string json:"recipient_id" Message string json:"message" Status string json:"status" // sent, delivered, read CreatedAt string json:"created_at" ThreadID string json:"thread_id,omitempty" }

// LinkedInPostDraft représente un brouillon de publication LinkedIn type LinkedInPostDraft struct { Content string json:"content" // Texte de la publication Visibility string json:"visibility,omitempty" // connections, public, mpany ContentType string json:"content_type,omitempty" // text, article, image, video Media []Attachment json:"media,omitempty" // Images, vidéos à joindre ShareURL string json:"share_url,omitempty" // URL à partager Title string json:"title,omitempty" // Pour les articles PublishAt string json:"publish_at,omitempty" // Pour planifier une publication }

// LinkedInPostCreated représente la réponse après la création d'une publication LinkedIn type LinkedInPostCreated struct { ID string json:"id" Provider string json:"provider" AccountID string json:"account_id" Status string json:"status" // published, scheduled, failed PostURL string json:"post_url,omitempty" // URL de la publication CreatedAt string json:"created_at" PublishedAt string json:"published_at,omitempty" }

// --- End of models/linkedin.go ---

// File: models/messaging.go

//pkg/providers/unipile/models/messaging.go

package models

import "time"

// Chat représente une conversation entre plusieurs utilisateurs type Chat struct { ID string json:"id" Provider string json:"provider" Name string json:"name,omitempty" Description string json:"description,omitempty" Type string json:"type,omitempty" // "direct", "group", etc. CreatedAt time.Time json:"created_at" UpdatedAt time.Time json:"updated_at,omitempty" LastMessage *Message json:"last_message,omitempty" UnreadCount int json:"unread_count,omitempty" Attendees int json:"attendees,omitempty" CreatorID string json:"creator_id,omitempty" Metadata map[string]interface{} json:"metadata,omitempty" }

// ChatList représente une liste paginée de chats type ChatList struct { Object string json:"object" Paging Paging json:"paging" Results []*Chat json:"results" }

// ChatCreated représente la réponse après création d'un chat type ChatCreated struct { Object string json:"object" Chat *Chat json:"chat" }

// Message représente un message dans un chat type Message struct { ID string json:"id" Provider string json:"provider" ChatID string json:"chat_id" SenderID string json:"sender_id" ReplyToID string json:"reply_to_id,omitempty" Text string json:"text" CreatedAt time.Time json:"created_at" UpdatedAt time.Time json:"updated_at,omitempty" ReadBy []string json:"read_by,omitempty" Sender Account json:"sender,omitempty" ReplyTo Message json:"reply_to,omitempty" Attachments []Attachment json:"attachments,omitempty" Metadata map[string]interface{} json:"metadata,omitempty" }

// MessageList représente une liste paginée de messages type MessageList struct { Object string json:"object" Paging Paging json:"paging" Results []*Message json:"results" }

// MessageSent représente la réponse après envoi d'un message type MessageSent struct { Object string json:"object" Message *Message json:"message" }

// MessageDraft représente un brouillon de message à envoyer type MessageDraft struct { Text string json:"text" ReplyToID string json:"reply_to_id,omitempty" Attachments []AttachmentContent json:"attachments,omitempty" Metadata map[string]interface{} json:"metadata,omitempty" }

// MessageDeleted représente la réponse après suppression d'un message type MessageDeleted struct { Object string json:"object" ID string json:"id" Provider string json:"provider" Deleted bool json:"deleted" DeletedAt time.Time json:"deleted_at" }

// MessageUpdated représente la réponse après mise à jour d'un message type MessageUpdated struct { Object string json:"object" Message *Message json:"message" UpdatedAt time.Time json:"updated_at" }

// ChatParticipant représente un participant à un chat type ChatParticipant struct { ID string json:"id" Provider string json:"provider" ChatID string json:"chat_id" AccountID string json:"account_id" Role string json:"role,omitempty" // "admin", "member", etc. JoinedAt time.Time json:"joined_at" LastReadAt time.Time json:"last_read_at,omitempty" Account *Account json:"account,omitempty" Metadata map[string]interface{} json:"metadata,omitempty" }

// ChatParticipantList représente une liste paginée de participants type ChatParticipantList struct { Object string json:"object" Paging Paging json:"paging" Results []*ChatParticipant json:"results" }

// ChatParticipantAdded représente la réponse après ajout d'un participant type ChatParticipantAdded struct { Object string json:"object" Participant *ChatParticipant json:"participant" }

// ChatParticipantRemoved représente la réponse après suppression d'un participant type ChatParticipantRemoved struct { Object string json:"object" ID string json:"id" Provider string json:"provider" ChatID string json:"chat_id" AccountID string json:"account_id" Removed bool json:"removed" RemovedAt time.Time json:"removed_at" }

// MessageSearchRequest représente une requête de recherche de messages type MessageSearchRequest struct { Query string json:"query,omitempty" ChatID string json:"chat_id,omitempty" SenderID string json:"sender_id,omitempty" StartDate time.Time json:"start_date,omitempty" EndDate time.Time json:"end_date,omitempty" HasAttachments bool json:"has_attachments,omitempty" Limit int json:"limit,omitempty" Cursor string json:"cursor,omitempty" }

// ChatSearchRequest représente une requête de recherche de chats type ChatSearchRequest struct { Query string json:"query,omitempty" AttendeeID string json:"attendee_id,omitempty" Type string json:"type,omitempty" StartDate time.Time json:"start_date,omitempty" EndDate time.Time json:"end_date,omitempty" Limit int json:"limit,omitempty" Cursor string json:"cursor,omitempty" }

// --- End of models/messaging.go ---

// File: models/page.go

//pkg/providers/unipile/models/page.go

package models

import "time"

// Page représente une page web ou un article dans l'API Unipile type Page struct { ID string json:"id" Provider string json:"provider" AccountID string json:"account_id,omitempty" Title string json:"title" Content string json:"content" ContentType string json:"content_type,omitempty" // html, markdown, text Excerpt string json:"excerpt,omitempty" URL string json:"url,omitempty" Slug string json:"slug,omitempty" Author Author json:"author,omitempty" Featured bool json:"featured,omitempty" Status string json:"status,omitempty" // draft, published, scheduled CreatedAt time.Time json:"created_at" UpdatedAt time.Time json:"updated_at,omitempty" PublishedAt time.Time json:"published_at,omitempty" Language string json:"language,omitempty" Tags []string json:"tags,omitempty" Categories []string json:"categories,omitempty" FeaturedImage string json:"featured_image,omitempty" Images []string json:"images,omitempty" Attachments []Attachment json:"attachments,omitempty" Metadata map[string]interface{} json:"metadata,omitempty" }

// PageList représente une liste paginée de pages type PageList struct { Object string json:"object" Pages []Page json:"pages" Paging Paging json:"paging,omitempty" }

// PageDraft représente un brouillon de page à créer type PageDraft struct { AccountID string json:"account_id,omitempty" Title string json:"title" Content string json:"content" ContentType string json:"content_type,omitempty" // html, markdown, text Excerpt string json:"excerpt,omitempty" Slug string json:"slug,omitempty" Featured bool json:"featured,omitempty" Status string json:"status,omitempty" // draft, published, scheduled PublishAt string json:"publish_at,omitempty" // ISO 8601 date Language string json:"language,omitempty" Tags []string json:"tags,omitempty" Categories []string json:"categories,omitempty" FeaturedImage string json:"featured_image,omitempty" Images []string json:"images,omitempty" Attachments []Attachment json:"attachments,omitempty" Metadata map[string]interface{} json:"metadata,omitempty" }

// PageCreated représente la réponse après la création d'une page type PageCreated struct { ID string json:"id" Provider string json:"provider" AccountID string json:"account_id" Status string json:"status" // created, scheduled, failed CreatedAt time.Time json:"created_at" Page Page json:"page,omitempty" }

// PageUpdated représente la réponse après la mise à jour d'une page type PageUpdated struct { ID string json:"id" Provider string json:"provider" AccountID string json:"account_id" Status string json:"status" // updated, failed UpdatedAt time.Time json:"updated_at" Changes map[string]interface{} json:"changes,omitempty" Page Page json:"page,omitempty" }

// PageDeleted représente la réponse après la suppression d'une page type PageDeleted struct { ID string json:"id" Provider string json:"provider" AccountID string json:"account_id,omitempty" Status string json:"status" // deleted, failed }

// Website représente un site web dans l'API Unipile type Website struct { ID string json:"id" Provider string json:"provider" AccountID string json:"account_id,omitempty" Name string json:"name" Description string json:"description,omitempty" URL string json:"url" Domain string json:"domain,omitempty" Status string json:"status,omitempty" // active, inactive, maintenance Platform string json:"platform,omitempty" // wordpress, shopify, wix, etc. CreatedAt time.Time json:"created_at" UpdatedAt time.Time json:"updated_at,omitempty" Metadata map[string]interface{} json:"metadata,omitempty" }

// WebsiteList représente une liste paginée de sites web type WebsiteList struct { Object string json:"object" Websites []Website json:"websites" Paging Paging json:"paging,omitempty" }

// Menu représente un menu de navigation sur un site web type Menu struct { ID string json:"id" Provider string json:"provider" WebsiteID string json:"website_id" Name string json:"name" Location string json:"location,omitempty" // header, footer, sidebar, etc. Items []MenuItem json:"items" }

// MenuItem représente un élément de menu type MenuItem struct { ID string json:"id,omitempty" Title string json:"title" URL string json:"url,omitempty" Target string json:"target,omitempty" // _blank, _self, etc. Order int json:"order,omitempty" Parent string json:"parent,omitempty" // ID du parent si c'est un sous-menu Type string json:"type,omitempty" // page, category, custom, etc. Children []MenuItem json:"children,omitempty" }

// PageSearchRequest représente une requête de recherche de pages type PageSearchRequest struct { AccountID string json:"account_id,omitempty" Query string json:"query" Author string json:"author,omitempty" Tags []string json:"tags,omitempty" Categories []string json:"categories,omitempty" Status string json:"status,omitempty" After string json:"after,omitempty" // ISO 8601 date Before string json:"before,omitempty" // ISO 8601 date Limit int json:"limit,omitempty" Cursor string json:"cursor,omitempty" }

// --- End of models/page.go ---

// File: models/post_types.go

//pkg/providers/unipile/models/post_types.go

package models

import "time"

// Post représente une publication dans l'API Unipile type Post struct { ID string json:"id" Provider string json:"provider" AccountID string json:"account_id,omitempty" Content string json:"content" ContentType string json:"content_type,omitempty" // text, html URL string json:"url,omitempty" Author Author json:"author" CreatedAt time.Time json:"created_at" UpdatedAt time.Time json:"updated_at,omitempty" PublishedAt time.Time json:"published_at,omitempty" Status string json:"status,omitempty" // draft, published, scheduled Visibility string json:"visibility,omitempty" // public, private, connections Likes int json:"likes,omitempty" Comments int json:"comments,omitempty" Shares int json:"shares,omitempty" Views int json:"views,omitempty" Type string json:"type,omitempty" // article, status, image Attachments []Attachment json:"attachments,omitempty" Tags []string json:"tags,omitempty" Categories []string json:"categories,omitempty" Metadata map[string]interface{} json:"metadata,omitempty" }

// PostList représente une liste paginée de publications type PostList struct { Object string json:"object" Posts []Post json:"posts" Paging Paging json:"paging,omitempty" }

// PostComment représente un commentaire sur une publication type PostComment struct { ID string json:"id" Provider string json:"provider" PostID string json:"post_id" ParentID string json:"parent_id,omitempty" // ID du commentaire parent si c'est une réponse Content string json:"content" Author Author json:"author" CreatedAt time.Time json:"created_at" UpdatedAt time.Time json:"updated_at,omitempty" Likes int json:"likes,omitempty" Replies int json:"replies,omitempty" Attachments []Attachment json:"attachments,omitempty" }

// PostCommentList représente une liste paginée de commentaires type PostCommentList struct { Object string json:"object" Comments []PostComment json:"comments" Paging Paging json:"paging,omitempty" }

// PostReaction représente une réaction à une publication type PostReaction struct { ID string json:"id,omitempty" Provider string json:"provider" PostID string json:"post_id" Type string json:"type" // like, love, support, etc. CreatedAt time.Time json:"created_at,omitempty" User Author json:"user,omitempty" }

// PostReactionList représente une liste paginée de réactions type PostReactionList struct { Object string json:"object" Reactions []PostReaction json:"reactions" Paging Paging json:"paging,omitempty" }

// PostDraft représente un brouillon de publication à créer type PostDraft struct { AccountID string json:"account_id,omitempty" Content string json:"content" ContentType string json:"content_type,omitempty" // text, html Visibility string json:"visibility,omitempty" // public, private, connections PublishAt string json:"publish_at,omitempty" // ISO 8601 date Attachments []Attachment json:"attachments,omitempty" Tags []string json:"tags,omitempty" Categories []string json:"categories,omitempty" Metadata map[string]interface{} json:"metadata,omitempty" }

// PostCreated représente la réponse après la création d'une publication type PostCreated struct { ID string json:"id" Provider string json:"provider" AccountID string json:"account_id" Status string json:"status" // created, scheduled, failed CreatedAt time.Time json:"created_at" Post Post json:"post,omitempty" }

// PostDeleted représente la réponse après la suppression d'une publication type PostDeleted struct { ID string json:"id" Provider string json:"provider" AccountID string json:"account_id,omitempty" Status string json:"status" // deleted, failed }

// PostCommentDraft représente un brouillon de commentaire à créer type PostCommentDraft struct { AccountID string json:"account_id,omitempty" PostID string json:"post_id" ParentID string json:"parent_id,omitempty" // ID du commentaire parent si c'est une réponse Content string json:"content" Attachments []Attachment json:"attachments,omitempty" }

// PostCommentCreated représente la réponse après la création d'un commentaire type PostCommentCreated struct { ID string json:"id" Provider string json:"provider" AccountID string json:"account_id,omitempty" PostID string json:"post_id" Status string json:"status" // created, failed CreatedAt time.Time json:"created_at" Comment PostComment json:"comment,omitempty" }

// PostCommentDeleted représente la réponse après la suppression d'un commentaire type PostCommentDeleted struct { ID string json:"id" Provider string json:"provider" AccountID string json:"account_id,omitempty" PostID string json:"post_id" Status string json:"status" // deleted, failed }

// PostReactionCreated représente la réponse après la création d'une réaction type PostReactionCreated struct { ID string json:"id,omitempty" Provider string json:"provider" AccountID string json:"account_id,omitempty" PostID string json:"post_id" Type string json:"type" Status string json:"status" // created, failed CreatedAt time.Time json:"created_at,omitempty" Reaction PostReaction json:"reaction,omitempty" }

// PostReactionDeleted représente la réponse après la suppression d'une réaction type PostReactionDeleted struct { ID string json:"id,omitempty" Provider string json:"provider" AccountID string json:"account_id,omitempty" PostID string json:"post_id" Type string json:"type,omitempty" Status string json:"status" // deleted, failed }

// PostSearchRequest représente une requête de recherche de publications type PostSearchRequest struct { AccountID string json:"account_id,omitempty" Query string json:"query" Author string json:"author,omitempty" Tags []string json:"tags,omitempty" Categories []string json:"categories,omitempty" Status string json:"status,omitempty" Visibility string json:"visibility,omitempty" After string json:"after,omitempty" // ISO 8601 date Before string json:"before,omitempty" // ISO 8601 date Limit int json:"limit,omitempty" Cursor string json:"cursor,omitempty" }

// --- End of models/post_types.go ---

// File: models/webhooks.go

//pkg/providers/unipile/models/webhooks.go

package models

import "time"

// Webhook represents a webhook configured to receive events type Webhook struct { ID string json:"id" Provider string json:"provider" URL string json:"url" Secret string json:"secret,omitempty" Description string json:"description,omitempty" Events []string json:"events" Status string json:"status" CreatedAt time.Time json:"created_at" UpdatedAt time.Time json:"updated_at,omitempty" Metadata map[string]interface{} json:"metadata,omitempty" }

// WebhookList represents a paginated list of webhooks type WebhookList struct { Object string json:"object" Paging Paging json:"paging" Results []*Webhook json:"results" }

// WebhookCreated represents the response after creating a webhook type WebhookCreated struct { Object string json:"object" Webhook *Webhook json:"webhook" }

// WebhookUpdated represents the response after updating a webhook type WebhookUpdated struct { Object string json:"object" Webhook *Webhook json:"webhook" }

// WebhookDeleted represents the response after deleting a webhook type WebhookDeleted struct { Object string json:"object" ID string json:"id" Provider string json:"provider" Deleted bool json:"deleted" DeletedAt time.Time json:"deleted_at" }

// WebhookEvent represents an event that triggered a webhook type WebhookEvent struct { ID string json:"id" Provider string json:"provider" WebhookID string json:"webhook_id" Type string json:"type" CreatedAt time.Time json:"created_at" ResourceType string json:"resource_type" ResourceID string json:"resource_id" Data map[string]interface{} json:"data" }

// WebhookEventList represents a paginated list of webhook events type WebhookEventList struct { Object string json:"object" Paging Paging json:"paging" Results []*WebhookEvent json:"results" }

// WebhookEventDelivery represents an attempt to deliver a webhook event type WebhookEventDelivery struct { ID string json:"id" Provider string json:"provider" EventID string json:"event_id" URL string json:"url" Status string json:"status" // "success", "failed", "pending" CreatedAt time.Time json:"created_at" SentAt time.Time json:"sent_at,omitempty" Response *WebhookDeliveryResponse json:"response,omitempty" }

// WebhookDeliveryResponse represents the response received when delivering a webhook type WebhookDeliveryResponse struct { StatusCode int json:"status_code" Headers map[string]string json:"headers,omitempty" Body string json:"body,omitempty" Timestamp time.Time json:"timestamp" }

// WebhookEventDeliveryList represents a paginated list of webhook event deliveries type WebhookEventDeliveryList struct { Object string json:"object" Paging Paging json:"paging" Results []*WebhookEventDelivery json:"results" }

// WebhookResend represents the response after requesting to resend a webhook type WebhookResend struct { Object string json:"object" Delivery *WebhookEventDelivery json:"delivery" }

// WebhookDraft represents the data for creating a new webhook type WebhookDraft struct { URL string json:"url" Secret string json:"secret,omitempty" Description string json:"description,omitempty" Events []string json:"events" Metadata map[string]interface{} json:"metadata,omitempty" }

// WebhookUpdate represents the data for updating an existing webhook type WebhookUpdate struct { URL string json:"url,omitempty" Secret string json:"secret,omitempty" Description string json:"description,omitempty" Events []string json:"events,omitempty" Status string json:"status,omitempty" Metadata map[string]interface{} json:"metadata,omitempty" }

// EmailAttendee represents an attendee in an email webhook payload type EmailAttendee struct { DisplayName string json:"display_name" mapstructure:"display_name" Identifier string json:"identifier" mapstructure:"identifier" IdentifierType string json:"identifier_type" mapstructure:"identifier_type" // Add other relevant fields if they exist in the actual payload }

// WebhookPayload represents the payload for email webhooks type WebhookPayload struct { EmailID string json:"email_id" mapstructure:"email_id" AccountID string json:"account_id" mapstructure:"account_id" Event string json:"event" mapstructure:"event" WebhookName string json:"webhook_name" mapstructure:"webhook_name" Date time.Time json:"date" mapstructure:"date" FromAttendee EmailAttendee json:"from_attendee" mapstructure:"from_attendee" ToAttendees []EmailAttendee json:"to_attendees" mapstructure:"to_attendees" BccAttendees []EmailAttendee json:"bcc_attendees" mapstructure:"bcc_attendees" CcAttendees []EmailAttendee json:"cc_attendees" mapstructure:"cc_attendees" ReplyToAttendees []EmailAttendee json:"reply_to_attendees" mapstructure:"reply_to_attendees" ProviderID string json:"provider_id" mapstructure:"provider_id" MessageID string json:"message_id" mapstructure:"message_id" HasAttachments bool json:"has_attachments" mapstructure:"has_attachments" Subject string json:"subject" mapstructure:"subject" Body string json:"body" mapstructure:"body" BodyPlain string json:"body_plain" mapstructure:"body_plain" Attachments []any json:"attachments" mapstructure:"attachments" Folders []string json:"folders" mapstructure:"folders" Role string json:"role" mapstructure:"role" ReadDate *time.Time json:"read_date" mapstructure:"read_date" IsComplete bool json:"is_complete" mapstructure:"is_complete" Origin string json:"origin" mapstructure:"origin" }

// AttendeeInfo represents an attendee in a chat type AttendeeInfo struct { AttendeeID string json:"attendee_id" AttendeeName string json:"attendee_name" AttendeeProviderID string json:"attendee_provider_id" AttendeeProfileURL string json:"attendee_profile_url" }

// MessageWebhookPayload represents a message webhook payload type MessageWebhookPayload struct { AccountID string json:"account_id" AccountType string json:"account_type" AccountInfo AccountInfo json:"account_info" Event string json:"event" ChatID string json:"chat_id" Timestamp time.Time json:"timestamp" WebhookName string json:"webhook_name" MessageID string json:"message_id" Message string json:"message" Sender AttendeeInfo json:"sender" Attendees []AttendeeInfo json:"attendees" Attachments Attachment json:"attachments,omitempty" Reaction string json:"reaction,omitempty" ReactionSender AttendeeInfo json:"reaction_sender,omitempty" }

// AccountStatus represents an account status update type AccountStatus struct { AccountID string json:"account_id" AccountType string json:"account_type" Message string json:"message" }

// --- End of models/webhooks.go ---

// File: pg_store.go

//pkg/providers/unipile/pg_store.go

package unipile

import ( "context" "encoding/json" "errors" "fmt"

"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"gitlab.com/webigniter/slop-server/pkg/providers/unipile/models"

)

// UnipileStoreInterface defines the interface for storage operations type UnipileStoreInterface interface { // Account methods StoreAccount(ctx context.Context, userID, accountID, provider, status string, metadata map[string]interface{}) error GetAccount(ctx context.Context, accountID string) (models.AccountInfo, error) GetUserAccounts(ctx context.Context, userID string) ([]models.AccountInfo, error) UpdateAccountStatus(ctx context.Context, accountID, status string) error DeleteAccount(ctx context.Context, accountID string) error GetDistinctUserIDsWithAccounts(ctx context.Context) ([]string, error) GetAllAccountIDs(ctx context.Context) ([]string, error)

// Email webhook methods
SaveEmail(ctx context.Context, payload *models.WebhookPayload) error

// Message webhook methods
SaveMessage(ctx context.Context, payload *models.MessageWebhookPayload) error

// Account status webhook methods
SaveAccountStatus(ctx context.Context, status *models.AccountStatus) error

// Close database connection
Close()

}

// UnipileStore handles persistence of Unipile account information type UnipileStore struct { pool *pgxpool.Pool }

// NewUnipileStore creates a new PostgreSQL store for Unipile accounts by connecting // Deprecated: InitializeDatabase should be called separately via the Adapter. func NewUnipileStore(connString string) (*UnipileStore, error) { // Connect to PostgreSQL pool, err := pgxpool.New(context.Background(), connString) if err != nil { return nil, err }

// Create store
store := &UnipileStore{
    pool: pool,
}

// DO NOT initialize schema here anymore.
// if err := store.InitializeDatabase(context.Background()); err != nil {
//  pool.Close()
//  return nil, err
// }

return store, nil

}

// NewUnipileStoreWithPool creates a new PostgreSQL store using an existing pool. // Does not initialize the database schema. func NewUnipileStoreWithPool(pool pgxpool.Pool) UnipileStore { return &UnipileStore{ pool: pool, } }

// InitializeDatabase creates necessary tables if they don't exist (made public) func (s *UnipileStore) InitializeDatabase(ctx context.Context) error { // Create schema if it doesn't exist _, err := s.pool.Exec(ctx, CREATE SCHEMA IF NOT EXISTS providers_unipile;) if err != nil { return err }

_, err = s.pool.Exec(ctx, `
    CREATE TABLE IF NOT EXISTS providers_unipile.accounts (
        id SERIAL PRIMARY KEY,
        user_id TEXT NOT NULL,
        account_id TEXT NOT NULL UNIQUE,
        provider TEXT NOT NULL,
        status TEXT NOT NULL,
        metadata JSONB,
        last_update TIMESTAMPTZ NOT NULL DEFAULT NOW(),
        created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
    );

    CREATE INDEX IF NOT EXISTS idx_unipile_user ON providers_unipile.accounts(user_id);
    CREATE INDEX IF NOT EXISTS idx_unipile_provider ON providers_unipile.accounts(provider);
    CREATE INDEX IF NOT EXISTS idx_unipile_status ON providers_unipile.accounts(status);
`)

if err != nil {
    return err
}

// Create webhook tables
if err = s.createEmailsTable(ctx); err != nil {
    return fmt.Errorf("failed to create emails table: %w", err)
}

if err = s.createMessagesTable(ctx); err != nil {
    return fmt.Errorf("failed to create messages table: %w", err)
}

if err = s.createAccountStatusTable(ctx); err != nil {
    return fmt.Errorf("failed to create account_status table: %w", err)
}

return nil

}

// createEmailsTable creates the table for email webhooks func (s *UnipileStore) createEmailsTable(ctx context.Context) error { query := CREATE TABLE IF NOT EXISTS providers_unipile.emails ( id SERIAL PRIMARY KEY, email_id VARCHAR(255) UNIQUE NOT NULL, account_id VARCHAR(255) NOT NULL, event VARCHAR(255) NOT NULL, date TIMESTAMP NOT NULL, from_name VARCHAR(255), from_email VARCHAR(255), subject TEXT, body_html TEXT, body_plain TEXT, has_attachments BOOLEAN, message_id VARCHAR(255), provider_id VARCHAR(255), to_recipients JSONB, cc_recipients JSONB, bcc_recipients JSONB, received_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )

_, err := s.pool.Exec(ctx, query)
return err

}

// createMessagesTable creates the table for message webhooks func (s *UnipileStore) createMessagesTable(ctx context.Context) error { query := CREATE TABLE IF NOT EXISTS providers_unipile.messages ( id SERIAL PRIMARY KEY, message_id VARCHAR(255) UNIQUE NOT NULL, account_id VARCHAR(255) NOT NULL, account_type VARCHAR(255) NOT NULL, event VARCHAR(255) NOT NULL, chat_id VARCHAR(255) NOT NULL, timestamp TIMESTAMP NOT NULL, message TEXT, sender_id VARCHAR(255), sender_name VARCHAR(255), sender_provider_id VARCHAR(255), sender_profile_url TEXT, attendees JSONB, attachments JSONB, reaction VARCHAR(255), reaction_sender JSONB, received_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP )

_, err := s.pool.Exec(ctx, query)
return err

}

// createAccountStatusTable creates the table for account status webhooks func (s *UnipileStore) createAccountStatusTable(ctx context.Context) error { query := CREATE TABLE IF NOT EXISTS providers_unipile.account_status ( id SERIAL PRIMARY KEY, account_id VARCHAR(255) NOT NULL, account_type VARCHAR(255) NOT NULL, message VARCHAR(255) NOT NULL, received_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(account_id, message, received_at) )

_, err := s.pool.Exec(ctx, query)
return err

}

// StoreAccount creates or updates an account func (s *UnipileStore) StoreAccount(ctx context.Context, userID, accountID, provider, status string, metadata map[string]interface{}) error { // Convert metadata to JSON var metadataBytes []byte var err error if metadata != nil { metadataBytes, err = json.Marshal(metadata) if err != nil { return fmt.Errorf("error serializing metadata: %w", err) } }

// Insert or update account
_, err = s.pool.Exec(ctx, `
    INSERT INTO providers_unipile.accounts 
        (user_id, account_id, provider, status, metadata, last_update, created_at)
    VALUES ($1, $2, $3, $4, $5, NOW(), NOW())
    ON CONFLICT (account_id) 
    DO UPDATE SET 
        user_id = $1,
        provider = $3, 
        status = $4, 
        metadata = $5,
        last_update = NOW()
`, userID, accountID, provider, status, metadataBytes)

return err

}

// GetAccount retrieves an account by its ID func (s UnipileStore) GetAccount(ctx context.Context, accountID string) (models.AccountInfo, error) { var account models.AccountInfo var metadataBytes []byte

err := s.pool.QueryRow(ctx, `
    SELECT id, user_id, account_id, provider, status, metadata, last_update, created_at
    FROM providers_unipile.accounts 
    WHERE account_id = $1
`, accountID).Scan(
    &account.ID,
    &account.UserID,
    &account.AccountID,
    &account.Provider,
    &account.Status,
    &metadataBytes,
    &account.LastUpdate,
    &account.CreatedAt,
)

if err != nil {
    if err == pgx.ErrNoRows {
        return nil, errors.New("account not found")
    }
    return nil, err
}

account.Metadata = metadataBytes
return &account, nil

}

// GetUserAccounts retrieves all accounts for a user func (s UnipileStore) GetUserAccounts(ctx context.Context, userID string) ([]models.AccountInfo, error) { rows, err := s.pool.Query(ctx, SELECT id, user_id, account_id, provider, status, metadata, last_update, created_at FROM providers_unipile.accounts WHERE user_id = $1 ORDER BY created_at DESC, userID) if err != nil { return nil, err } defer rows.Close()

var accounts []*models.AccountInfo
for rows.Next() {
    var account models.AccountInfo
    var metadataBytes []byte

    if err := rows.Scan(
        &account.ID,
        &account.UserID,
        &account.AccountID,
        &account.Provider,
        &account.Status,
        &metadataBytes,
        &account.LastUpdate,
        &account.CreatedAt,
    ); err != nil {
        return nil, err
    }

    account.Metadata = metadataBytes
    accounts = append(accounts, &account)
}

if err := rows.Err(); err != nil {
    return nil, err
}

return accounts, nil

}

// UpdateAccountStatus updates the status of an account func (s *UnipileStore) UpdateAccountStatus(ctx context.Context, accountID, status string) error { commandTag, err := s.pool.Exec(ctx, UPDATE providers_unipile.accounts SET status = $1, last_update = NOW() WHERE account_id = $2, status, accountID)

if err != nil {
    return err
}

if commandTag.RowsAffected() == 0 {
    return errors.New("account not found")
}

return nil

}

// DeleteAccount removes an account func (s *UnipileStore) DeleteAccount(ctx context.Context, accountID string) error { commandTag, err := s.pool.Exec(ctx, DELETE FROM providers_unipile.accounts WHERE account_id = $1, accountID)

if err != nil {
    return err
}

if commandTag.RowsAffected() == 0 {
    return errors.New("account not found")
}

return nil

}

// GetDistinctUserIDsWithAccounts retrieves a list of unique framework user IDs // that have at least one account in the providers_unipile.accounts table. func (s *UnipileStore) GetDistinctUserIDsWithAccounts(ctx context.Context) ([]string, error) { rows, err := s.pool.Query(ctx, SELECT DISTINCT user_id FROM providers_unipile.accounts) if err != nil { return nil, fmt.Errorf("error querying distinct user IDs: %w", err) } defer rows.Close()

var userIDs []string
for rows.Next() {
    var userID string
    if err := rows.Scan(&userID); err != nil {
        return nil, fmt.Errorf("error scanning distinct user ID: %w", err)
    }
    userIDs = append(userIDs, userID)
}

if err := rows.Err(); err != nil {
    return nil, fmt.Errorf("error iterating distinct user IDs: %w", err)
}

return userIDs, nil

}

// GetAllAccountIDs retrieves all account IDs stored locally. func (s *UnipileStore) GetAllAccountIDs(ctx context.Context) ([]string, error) { rows, err := s.pool.Query(ctx, SELECT account_id FROM providers_unipile.accounts) if err != nil { return nil, fmt.Errorf("error querying all account IDs: %w", err) } defer rows.Close()

var accountIDs []string
for rows.Next() {
    var accountID string
    if err := rows.Scan(&accountID); err != nil {
        return nil, fmt.Errorf("error scanning account ID: %w", err)
    }
    accountIDs = append(accountIDs, accountID)
}

if err := rows.Err(); err != nil {
    return nil, fmt.Errorf("error iterating account IDs: %w", err)
}

return accountIDs, nil

}

// SaveEmail saves an email webhook to the database func (s UnipileStore) SaveEmail(ctx context.Context, payload models.WebhookPayload) error { // Convert to/cc/bcc to JSON toJSON, err := json.Marshal(payload.ToAttendees) if err != nil { return err }

ccJSON, err := json.Marshal(payload.CcAttendees)
if err != nil {
    return err
}

bccJSON, err := json.Marshal(payload.BccAttendees)
if err != nil {
    return err
}

// Insert into database
query := `
INSERT INTO providers_unipile.emails (
    email_id, account_id, event, date, from_name, from_email,
    subject, body_html, body_plain, has_attachments, message_id,
    provider_id, to_recipients, cc_recipients, bcc_recipients
) VALUES (
    $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15
) ON CONFLICT (email_id) DO UPDATE SET 
    account_id = $2,
    event = $3,
    date = $4,
    from_name = $5,
    from_email = $6,
    subject = $7,
    body_html = $8,
    body_plain = $9,
    has_attachments = $10,
    message_id = $11,
    provider_id = $12,
    to_recipients = $13,
    cc_recipients = $14,
    bcc_recipients = $15,
    received_at = CURRENT_TIMESTAMP
RETURNING id`

var id int
err = s.pool.QueryRow(ctx,
    query,
    payload.EmailID,
    payload.AccountID,
    payload.Event,
    payload.Date,
    payload.FromAttendee.DisplayName,
    payload.FromAttendee.Identifier,
    payload.Subject,
    payload.Body,
    payload.BodyPlain,
    payload.HasAttachments,
    payload.MessageID,
    payload.ProviderID,
    toJSON,
    ccJSON,
    bccJSON,
).Scan(&id)

if err != nil {
    return fmt.Errorf("failed to save email: %w", err)
}

return nil

}

// SaveMessage saves a message webhook to the database func (s UnipileStore) SaveMessage(ctx context.Context, payload models.MessageWebhookPayload) error { // Convert attendees and attachments to JSON attendeesJSON, err := json.Marshal(payload.Attendees) if err != nil { return err }

var attachmentsJSON []byte
if payload.Attachments != (models.Attachment{}) {
    attachmentsJSON, err = json.Marshal(payload.Attachments)
    if err != nil {
        return err
    }
}

var reactionSenderJSON []byte
if payload.Event == "message_reaction" && payload.ReactionSender != (models.AttendeeInfo{}) {
    reactionSenderJSON, err = json.Marshal(payload.ReactionSender)
    if err != nil {
        return err
    }
}

// Insert into database
query := `
INSERT INTO providers_unipile.messages (
    message_id, account_id, account_type, event, chat_id, timestamp, 
    message, sender_id, sender_name, sender_provider_id, sender_profile_url, 
    attendees, attachments, reaction, reaction_sender
) VALUES (
    $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15
) ON CONFLICT (message_id) DO UPDATE SET 
    account_id = $2,
    account_type = $3,
    event = $4,
    chat_id = $5,
    timestamp = $6,
    message = $7,
    sender_id = $8,
    sender_name = $9,
    sender_provider_id = $10,
    sender_profile_url = $11,
    attendees = $12,
    attachments = $13,
    reaction = $14,
    reaction_sender = $15,
    received_at = CURRENT_TIMESTAMP
RETURNING id`

var id int
err = s.pool.QueryRow(ctx,
    query,
    payload.MessageID,
    payload.AccountID,
    payload.AccountType,
    payload.Event,
    payload.ChatID,
    payload.Timestamp,
    payload.Message,
    payload.Sender.AttendeeID,
    payload.Sender.AttendeeName,
    payload.Sender.AttendeeProviderID,
    payload.Sender.AttendeeProfileURL,
    attendeesJSON,
    attachmentsJSON,
    payload.Reaction,
    reactionSenderJSON,
).Scan(&id)

if err != nil {
    return fmt.Errorf("failed to save message: %w", err)
}

return nil

}

// SaveAccountStatus saves an account status webhook to the database func (s UnipileStore) SaveAccountStatus(ctx context.Context, status models.AccountStatus) error { query := INSERT INTO providers_unipile.account_status ( account_id, account_type, message ) VALUES ( $1, $2, $3 ) RETURNING id

var id int
err := s.pool.QueryRow(ctx,
    query,
    status.AccountID,
    status.AccountType,
    status.Message,
).Scan(&id)

if err != nil {
    return fmt.Errorf("failed to save account status: %w", err)
}

return nil

}

// Close closes the database connection func (s *UnipileStore) Close() { s.pool.Close() }

// --- End of pg_store.go ---

// File: services/account_service.go

//pkg/providers/unipile/services/account_service.go

package services

import ( "context" "fmt" "net/http" "net/url" "strconv"

providers "gitlab.com/webigniter/slop-server/internal/providers"
"gitlab.com/webigniter/slop-server/pkg/providers/unipile/models"

)

// AccountService implémente les fonctionnalités pour gérer les comptes utilisateurs type AccountService struct { client models.UnipileRequester }

// NewAccountService crée une nouvelle instance du service de comptes func NewAccountService(client models.UnipileRequester) *AccountService { return &AccountService{ client: client, } }

// GetCapabilities retourne les capacités de ce service func (s *AccountService) GetCapabilities() []providers.ProviderCapability { return []providers.ProviderCapability{ { Name: "list_accounts", Description: "Liste les comptes disponibles", Category: "Account Management", Parameters: map[string]providers.Parameter{ "limit": { Type: "integer", Description: "Nombre maximum de comptes à retourner.", Required: false, Category: "query", }, "cursor": { Type: "string", Description: "Curseur pour la pagination.", Required: false, Category: "query", }, }, Examples: []providers.ToolExample{ { Description: "Lister les 10 premiers comptes", Parameters: map[string]interface{}{ "limit": 10, }, }, }, }, { Name: "get_account", Description: "Récupère les détails d'un compte spécifique", Category: "Account Management", Parameters: map[string]providers.Parameter{ "account_id": { Type: "string", Description: "ID du compte à récupérer.", Required: true, Category: "path", }, }, Examples: []providers.ToolExample{ { Description: "Récupérer le compte avec l'ID 'acc_123'", Parameters: map[string]interface{}{ "account_id": "acc_123", }, }, }, }, { Name: "update_account", Description: "Met à jour les informations d'un compte", Category: "Account Management", Parameters: map[string]providers.Parameter{ "account_id": { Type: "string", Description: "ID du compte à mettre à jour.", Required: true, Category: "path", }, "name": { Type: "string", Description: "Nouveau nom pour le compte.", Required: false, Category: "body", }, "status": { Type: "string", Description: "Nouveau statut pour le compte.", Required: false, Category: "body", }, // Add other fields from models.AccountUpdate as needed }, Examples: []providers.ToolExample{ { Description: "Renommer le compte 'acc_123' en 'Nouveau Nom'", Parameters: map[string]interface{}{ "account_id": "acc_123", "name": "Nouveau Nom", }, }, }, }, { Name: "list_connections", Description: "Liste les connexions d'un compte", Category: "Connection Management", Parameters: map[string]providers.Parameter{ "account_id": { Type: "string", Description: "ID du compte pour lequel lister les connexions.", Required: true, Category: "path", }, "limit": { Type: "integer", Description: "Nombre maximum de connexions à retourner.", Required: false, Category: "query", }, "cursor": { Type: "string", Description: "Curseur pour la pagination.", Required: false, Category: "query", }, }, Examples: []providers.ToolExample{ { Description: "Lister les 5 premières connexions du compte 'acc_123'", Parameters: map[string]interface{}{ "account_id": "acc_123", "limit": 5, }, }, }, }, { Name: "get_connection", Description: "Récupère les détails d'une connexion spécifique", Category: "Connection Management", Parameters: map[string]providers.Parameter{ "account_id": { Type: "string", Description: "ID du compte.", Required: true, Category: "path", }, "connection_id": { Type: "string", Description: "ID de la connexion à récupérer.", Required: true, Category: "path", }, }, Examples: []providers.ToolExample{ { Description: "Récupérer la connexion 'con_456' du compte 'acc_123'", Parameters: map[string]interface{}{ "account_id": "acc_123", "connection_id": "con_456", }, }, }, }, { Name: "create_connection_request", Description: "Crée une demande de connexion", Category: "Connection Management", Parameters: map[string]providers.Parameter{ "account_id": { Type: "string", Description: "ID du compte pour lequel créer la demande.", Required: true, Category: "path", }, "provider": { Type: "string", Description: "Fournisseur de la connexion (ex: 'google', 'microsoft').", Required: true, Category: "body", }, "type": { Type: "string", Description: "Type de connexion (ex: 'email', 'calendar').", Required: true, Category: "body", }, "success_redirect_uri": { Type: "string", Description: "URI de redirection en cas de succès.", Required: false, Category: "body", }, "failure_redirect_uri": { Type: "string", Description: "URI de redirection en cas d'échec.", Required: false, Category: "body", }, // Add other fields from models.AccountConnectionRequest as needed }, Examples: []providers.ToolExample{ { Description: "Créer une demande de connexion Google Email pour le compte 'acc_123'", Parameters: map[string]interface{}{ "account_id": "acc_123", "provider": "google", "type": "email", }, }, }, }, { Name: "refresh_oauth", Description: "Rafraîchit les tokens OAuth d'un compte", Category: "Authentication", Parameters: map[string]providers.Parameter{ "account_id": { Type: "string", Description: "ID du compte pour lequel rafraîchir les tokens.", Required: true, Category: "path", }, }, Examples: []providers.ToolExample{ { Description: "Rafraîchir les tokens OAuth du compte 'acc_123'", Parameters: map[string]interface{}{ "account_id": "acc_123", }, }, }, }, { Name: "search_accounts", Description: "Recherche dans les comptes selon différents critères", Category: "Account Management", Parameters: map[string]providers.Parameter{ "limit": { Type: "integer", Description: "Nombre maximum de comptes à retourner.", Required: false, Category: "body", }, "cursor": { Type: "string", Description: "Curseur pour la pagination.", Required: false, Category: "body", }, "name": { Type: "string", Description: "Filtrer par nom de compte.", Required: false, Category: "body", }, "status": { Type: "string", Description: "Filtrer par statut de compte.", Required: false, Category: "body", }, "email": { Type: "string", Description: "Filtrer par email associé.", Required: false, Category: "body", }, // Add other fields from models.AccountSearchRequest as needed }, Examples: []providers.ToolExample{ { Description: "Rechercher les comptes actifs contenant 'Test'", Parameters: map[string]interface{}{ "name": "Test", "status": "active", "limit": 20, }, }, }, }, } }

// Execute implements the providers.Service interface. // Placeholder implementation. func (s *AccountService) Execute(ctx context.Context, params map[string]interface{}) (result interface{}, err error) { return nil, fmt.Errorf("Execute method not implemented for AccountService") }

// ListAccounts récupère la liste des comptes disponibles func (s AccountService) ListAccounts(ctx context.Context, options models.QueryOptions) (models.AccountList, error) { queryParams := url.Values{} if options.Limit > 0 { queryParams.Set("limit", strconv.Itoa(options.Limit)) } if options.Cursor != "" { queryParams.Set("cursor", options.Cursor) }

path := "/accounts"
if len(queryParams) > 0 {
    path += "?" + queryParams.Encode()
}

var result models.AccountList
err := s.client.Request(ctx, http.MethodGet, path, nil, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la récupération des comptes: %w", err)
}

return &result, nil

}

// GetAccount récupère les détails d'un compte spécifique func (s AccountService) GetAccount(ctx context.Context, accountID string) (models.Account, error) { if accountID == "" { return nil, fmt.Errorf("account ID est requis") }

path := fmt.Sprintf("/accounts/%s", accountID)

var result models.Account
err := s.client.Request(ctx, http.MethodGet, path, nil, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la récupération du compte %s: %w", accountID, err)
}

return &result, nil

}

// UpdateAccount met à jour les informations d'un compte func (s AccountService) UpdateAccount(ctx context.Context, accountID string, updateRequest models.AccountUpdate) (*models.AccountUpdateResponse, error) { if accountID == "" { return nil, fmt.Errorf("account ID est requis") }

if updateRequest == nil {
    return nil, fmt.Errorf("données de mise à jour requises")
}

path := fmt.Sprintf("/accounts/%s", accountID)

var result models.AccountUpdateResponse
err := s.client.Request(ctx, http.MethodPatch, path, updateRequest, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la mise à jour du compte %s: %w", accountID, err)
}

return &result, nil

}

// ListConnections récupère la liste des connexions d'un compte func (s AccountService) ListConnections(ctx context.Context, accountID string, options models.QueryOptions) (models.AccountConnectionList, error) { if accountID == "" { return nil, fmt.Errorf("account ID est requis") }

queryParams := url.Values{}
if options.Limit > 0 {
    queryParams.Set("limit", strconv.Itoa(options.Limit))
}
if options.Cursor != "" {
    queryParams.Set("cursor", options.Cursor)
}

path := fmt.Sprintf("/accounts/%s/connections", accountID)
if len(queryParams) > 0 {
    path += "?" + queryParams.Encode()
}

var result models.AccountConnectionList
err := s.client.Request(ctx, http.MethodGet, path, nil, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la récupération des connexions du compte %s: %w", accountID, err)
}

return &result, nil

}

// GetConnection récupère les détails d'une connexion spécifique func (s AccountService) GetConnection(ctx context.Context, accountID string, connectionID string) (models.AccountConnection, error) { if accountID == "" { return nil, fmt.Errorf("account ID est requis") }

if connectionID == "" {
    return nil, fmt.Errorf("connection ID est requis")
}

path := fmt.Sprintf("/accounts/%s/connections/%s", accountID, connectionID)

var result models.AccountConnection
err := s.client.Request(ctx, http.MethodGet, path, nil, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la récupération de la connexion %s du compte %s: %w", connectionID, accountID, err)
}

return &result, nil

}

// CreateConnectionRequest crée une demande de connexion func (s AccountService) CreateConnectionRequest(ctx context.Context, accountID string, request models.AccountConnectionRequest) (*models.AccountConnectionResponse, error) { if accountID == "" { return nil, fmt.Errorf("account ID est requis") }

if request == nil {
    return nil, fmt.Errorf("données de demande de connexion requises")
}

path := fmt.Sprintf("/accounts/%s/connection-requests", accountID)

var result models.AccountConnectionResponse
err := s.client.Request(ctx, http.MethodPost, path, request, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la création de la demande de connexion pour le compte %s: %w", accountID, err)
}

return &result, nil

}

// RefreshOAuth rafraîchit les tokens OAuth d'un compte func (s AccountService) RefreshOAuth(ctx context.Context, accountID string) (models.OAuthRefreshResponse, error) { if accountID == "" { return nil, fmt.Errorf("account ID est requis") }

path := fmt.Sprintf("/accounts/%s/oauth/refresh", accountID)

var result models.OAuthRefreshResponse
err := s.client.Request(ctx, http.MethodPost, path, nil, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors du rafraîchissement OAuth pour le compte %s: %w", accountID, err)
}

return &result, nil

}

// SearchAccounts recherche dans les comptes selon différents critères func (s AccountService) SearchAccounts(ctx context.Context, searchRequest models.AccountSearchRequest) (*models.AccountList, error) { if searchRequest == nil { return nil, fmt.Errorf("critères de recherche requis") }

var result models.AccountList
err := s.client.Request(ctx, http.MethodPost, "/accounts/search", searchRequest, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la recherche de comptes: %w", err)
}

return &result, nil

}

// --- End of services/account_service.go ---

// File: services/calendar_service.go

//pkg/providers/unipile/services/calendar_service.go

package services

import ( "context" "fmt" "net/http" "net/url" "strconv"

providers "gitlab.com/webigniter/slop-server/internal/providers"
"gitlab.com/webigniter/slop-server/pkg/providers/unipile/models"

)

// CalendarService implémente les fonctionnalités pour gérer les événements du calendrier type CalendarService struct { client models.UnipileRequester }

// NewCalendarService crée une nouvelle instance du service de calendrier func NewCalendarService(client models.UnipileRequester) *CalendarService { return &CalendarService{ client: client, } }

// GetCapabilities retourne les capacités de ce service func (s *CalendarService) GetCapabilities() []providers.ProviderCapability { calendarCategory := "calendar" return []providers.ProviderCapability{ { Name: "list_events", Description: "Liste les événements du calendrier", Category: calendarCategory, Parameters: map[string]providers.Parameter{ "limit": { Type: "integer", Description: "Nombre maximum d'événements à retourner.", Required: false, Category: "query", }, "cursor": { Type: "string", Description: "Curseur pour la pagination.", Required: false, Category: "query", }, }, Examples: []providers.ToolExample{}, }, { Name: "get_event", Description: "Récupère les détails d'un événement spécifique", Category: calendarCategory, Parameters: map[string]providers.Parameter{ "event_id": { Type: "string", Description: "ID de l'événement à récupérer.", Required: true, Category: "path", }, }, Examples: []providers.ToolExample{}, }, { Name: "create_event", Description: "Crée un nouvel événement dans le calendrier", Category: calendarCategory, Parameters: map[string]providers.Parameter{ "event_data": { Type: "object", Description: "Données de l'événement à créer (basé sur models.CalendarEventDraft).", Required: true, Category: "body", }, }, Examples: []providers.ToolExample{}, }, { Name: "update_event", Description: "Met à jour les informations d'un événement", Category: calendarCategory, Parameters: map[string]providers.Parameter{ "event_id": { Type: "string", Description: "ID de l'événement à mettre à jour.", Required: true, Category: "path", }, "updates": { Type: "object", Description: "Champs de l'événement à mettre à jour (map[string]interface{}).", Required: true, Category: "body", }, }, Examples: []providers.ToolExample{}, }, { Name: "delete_event", Description: "Supprime un événement du calendrier", Category: calendarCategory, Parameters: map[string]providers.Parameter{ "event_id": { Type: "string", Description: "ID de l'événement à supprimer.", Required: true, Category: "path", }, }, Examples: []providers.ToolExample{}, }, { Name: "respond_to_event", Description: "Répond à une invitation à un événement (participer, décliner, etc.)", Category: calendarCategory, Parameters: map[string]providers.Parameter{ "event_id": { Type: "string", Description: "ID de l'événement auquel répondre.", Required: true, Category: "path", }, "response": { Type: "object", Description: "Données de la réponse (basé sur models.CalendarEventRSVP).", Required: true, Category: "body", }, }, Examples: []providers.ToolExample{}, }, { Name: "search_events", Description: "Recherche des événements selon différents critères", Category: calendarCategory, Parameters: map[string]providers.Parameter{ "search_criteria": { Type: "object", Description: "Critères de recherche (basé sur models.CalendarSearchRequest).", Required: true, Category: "body", }, }, Examples: []providers.ToolExample{}, }, } }

// Execute implements the providers.Service interface. // Placeholder implementation. func (s *CalendarService) Execute(ctx context.Context, params map[string]interface{}) (result interface{}, err error) { return nil, fmt.Errorf("Execute method not implemented for CalendarService") }

// ListEvents récupère la liste des événements du calendrier func (s CalendarService) ListEvents(ctx context.Context, options models.QueryOptions) (models.CalendarEventList, error) { queryParams := url.Values{} if options.Limit > 0 { queryParams.Set("limit", strconv.Itoa(options.Limit)) } if options.Cursor != "" { queryParams.Set("cursor", options.Cursor) }

path := "/calendar/events"
if len(queryParams) > 0 {
    path += "?" + queryParams.Encode()
}

var result models.CalendarEventList
err := s.client.Request(ctx, http.MethodGet, path, nil, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la récupération des événements: %w", err)
}

return &result, nil

}

// GetEvent récupère les détails d'un événement spécifique func (s CalendarService) GetEvent(ctx context.Context, eventID string) (models.CalendarEvent, error) { if eventID == "" { return nil, fmt.Errorf("event ID est requis") }

path := fmt.Sprintf("/calendar/events/%s", eventID)

var result models.CalendarEvent
err := s.client.Request(ctx, http.MethodGet, path, nil, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la récupération de l'événement %s: %w", eventID, err)
}

return &result, nil

}

// CreateEvent crée un nouvel événement dans le calendrier func (s CalendarService) CreateEvent(ctx context.Context, eventDraft models.CalendarEventDraft) (*models.CalendarEventCreated, error) { if eventDraft == nil { return nil, fmt.Errorf("données d'événement requises") }

var result models.CalendarEventCreated
err := s.client.Request(ctx, http.MethodPost, "/calendar/events", eventDraft, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la création de l'événement: %w", err)
}

return &result, nil

}

// UpdateEvent met à jour les informations d'un événement func (s CalendarService) UpdateEvent(ctx context.Context, eventID string, updates map[string]interface{}) (models.CalendarEventUpdated, error) { if eventID == "" { return nil, fmt.Errorf("event ID est requis") }

if updates == nil || len(updates) == 0 {
    return nil, fmt.Errorf("au moins une mise à jour est requise")
}

path := fmt.Sprintf("/calendar/events/%s", eventID)

var result models.CalendarEventUpdated
err := s.client.Request(ctx, http.MethodPatch, path, updates, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la mise à jour de l'événement %s: %w", eventID, err)
}

return &result, nil

}

// DeleteEvent supprime un événement du calendrier func (s CalendarService) DeleteEvent(ctx context.Context, eventID string) (models.CalendarEventDeleted, error) { if eventID == "" { return nil, fmt.Errorf("event ID est requis") }

path := fmt.Sprintf("/calendar/events/%s", eventID)

var result models.CalendarEventDeleted
err := s.client.Request(ctx, http.MethodDelete, path, nil, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la suppression de l'événement %s: %w", eventID, err)
}

return &result, nil

}

// RespondToEvent répond à une invitation à un événement func (s CalendarService) RespondToEvent(ctx context.Context, eventID string, response models.CalendarEventRSVP) (*models.CalendarEventRSVPResponse, error) { if eventID == "" { return nil, fmt.Errorf("event ID est requis") }

if response == nil {
    return nil, fmt.Errorf("données de réponse requises")
}

path := fmt.Sprintf("/calendar/events/%s/respond", eventID)

var result models.CalendarEventRSVPResponse
err := s.client.Request(ctx, http.MethodPost, path, response, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la réponse à l'événement %s: %w", eventID, err)
}

return &result, nil

}

// SearchEvents recherche des événements selon différents critères func (s CalendarService) SearchEvents(ctx context.Context, searchRequest models.CalendarSearchRequest) (*models.CalendarEventList, error) { if searchRequest == nil { return nil, fmt.Errorf("critères de recherche requis") }

var result models.CalendarEventList
err := s.client.Request(ctx, http.MethodPost, "/calendar/events/search", searchRequest, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la recherche d'événements: %w", err)
}

return &result, nil

}

// --- End of services/calendar_service.go ---

// File: services/email_service.go

//pkg/providers/unipile/services/email_service.go

package services

import ( "context" "fmt" "log" "net/http" "net/url" "strconv"

providers "gitlab.com/webigniter/slop-server/internal/providers"
"gitlab.com/webigniter/slop-server/pkg/providers/unipile/models"

)

// EmailService implémente les fonctionnalités pour gérer les emails type EmailService struct { client models.UnipileRequester }

// NewEmailService crée une nouvelle instance du service d'email func NewEmailService(client models.UnipileRequester) *EmailService { return &EmailService{ client: client, } }

// GetCapabilities retourne les capacités de ce service func (s *EmailService) GetCapabilities() []providers.ProviderCapability { return []providers.ProviderCapability{ { Name: "list_emails", Description: "Liste les emails disponibles via Unipile", Category: "Emails", Parameters: map[string]providers.Parameter{ "limit": { Type: "integer", Description: "Nombre maximum d'emails à retourner", Required: false, Category: "body", }, "cursor": { Type: "string", Description: "Curseur pour la pagination", Required: false, Category: "body", }, }, Examples: []providers.ToolExample{ { Description: "Lister les 20 premiers emails", Parameters: map[string]interface{}{ "limit": 20, }, // Returns omitted for brevity }, }, }, { Name: "get_email", Description: "Récupère les détails d'un email spécifique via Unipile", Category: "Emails", Parameters: map[string]providers.Parameter{ "email_id": { Type: "string", Description: "ID de l'email à récupérer", Required: true, Category: "body", }, }, Examples: []providers.ToolExample{ { Description: "Récupérer l'email avec l'ID 'eml_12345'", Parameters: map[string]interface{}{ "email_id": "eml_12345", }, // Returns omitted for brevity }, }, }, { Name: "list_email_threads", Description: "Liste les fils de discussion d'emails via Unipile", Category: "Threads", Parameters: map[string]providers.Parameter{ "limit": { Type: "integer", Description: "Nombre maximum de fils à retourner", Required: false, Category: "body", }, "cursor": { Type: "string", Description: "Curseur pour la pagination", Required: false, Category: "body", }, }, Examples: []providers.ToolExample{ { Description: "Lister les 10 premiers fils de discussion", Parameters: map[string]interface{}{ "limit": 10, }, }, }, }, { Name: "get_email_thread", Description: "Récupère les détails d'un fil de discussion spécifique via Unipile", Category: "Threads", Parameters: map[string]providers.Parameter{ "thread_id": { Type: "string", Description: "ID du fil de discussion à récupérer", Required: true, Category: "body", }, }, Examples: []providers.ToolExample{ { Description: "Récupérer le fil de discussion avec l'ID 'thr_67890'", Parameters: map[string]interface{}{ "thread_id": "thr_67890", }, }, }, }, { Name: "send_email", Description: "Envoie un nouvel email via Unipile", Category: "Emails", Parameters: map[string]providers.Parameter{ "to": { Type: "array", Description: "Adresse(s) email du destinataire", Required: true, Category: "body", Items: &providers.ParameterItems{Type: "string"}, }, "cc": { Type: "array", Description: "Adresse(s) email en copie carbone (optionnel)", Required: false, Category: "body", Items: &providers.ParameterItems{Type: "string"}, }, "bcc": { Type: "array", Description: "Adresse(s) email en copie carbone invisible (optionnel)", Required: false, Category: "body", Items: &providers.ParameterItems{Type: "string"}, }, "subject": { Type: "string", Description: "Sujet de l'email", Required: true, Category: "body", }, "body": { Type: "string", Description: "Contenu HTML ou texte de l'email", Required: true, Category: "body", }, "thread_id": { Type: "string", Description: "ID du fil de discussion auquel répondre (optionnel)", Required: false, Category: "body", }, "attachment_ids": { Type: "array", Description: "Liste des IDs des pièces jointes à inclure (obtenus via upload_attachment, optionnel)", Required: false, Category: "body", Items: &providers.ParameterItems{Type: "string"}, }, }, Examples: []providers.ToolExample{ { Description: "Envoyer un email simple", Parameters: map[string]interface{}{ "to": []string{"recipient@example.com"}, "subject": "Sujet de l'email", "body": "Contenu de l'email.", }, }, { Description: "Répondre à un fil de discussion", Parameters: map[string]interface{}{ "thread_id": "thr_abcde", "to": []string{"sender@example.com"}, "subject": "Re: Sujet Original", "body": "Merci pour votre email.", }, }, }, }, { Name: "delete_email", Description: "Supprime un email via Unipile", Category: "Emails", Parameters: map[string]providers.Parameter{ "email_id": { Type: "string", Description: "ID de l'email à supprimer", Required: true, Category: "body", }, }, Examples: []providers.ToolExample{ { Description: "Supprimer l'email 'eml_abcde'", Parameters: map[string]interface{}{ "email_id": "eml_abcde", }, }, }, }, { Name: "modify_email", Description: "Modifie les attributs d'un email via Unipile (ex: lu/non lu, archivé)", Category: "Emails", Parameters: map[string]providers.Parameter{ "email_id": { Type: "string", Description: "ID de l'email à modifier", Required: true, Category: "body", }, "is_read": { Type: "boolean", Description: "Marquer l'email comme lu (true) ou non lu (false)", Required: false, Category: "body", }, "is_archived": { Type: "boolean", Description: "Archiver (true) ou désarchiver (false) l'email", Required: false, Category: "body", }, }, Examples: []providers.ToolExample{ { Description: "Marquer l'email 'eml_fghij' comme lu", Parameters: map[string]interface{}{ "email_id": "eml_fghij", "is_read": true, }, }, { Description: "Archiver l'email 'eml_klmno'", Parameters: map[string]interface{}{ "email_id": "eml_klmno", "is_archived": true, }, }, }, }, { Name: "upload_attachment", Description: "Télécharge une pièce jointe pour préparer un envoi d'email via Unipile", Category: "Attachments", Parameters: map[string]providers.Parameter{ "account_id": { Type: "string", Description: "ID du compte Unipile associé (nécessaire pour l'upload)", Required: true, Category: "body", }, "filename": { Type: "string", Description: "Nom du fichier de la pièce jointe", Required: true, Category: "body", }, "content_type": { Type: "string", Description: "Type MIME du fichier (ex: 'application/pdf', 'image/png')", Required: true, Category: "body", }, "content_base64": { Type: "string", Description: "Contenu du fichier encodé en Base64", Required: true, Category: "body", }, }, Examples: []providers.ToolExample{ { Description: "Uploader un fichier 'report.pdf' pour le compte 'acc_123'", Parameters: map[string]interface{}{ "account_id": "acc_123", "filename": "report.pdf", "content_type": "application/pdf", "content_base64": "BASE64_ENCODED_CONTENT...", }, // Example return could include the attachment ID needed for send_email }, }, }, { Name: "search_emails", Description: "Recherche des emails via Unipile selon des critères spécifiés", Category: "Emails", Parameters: map[string]providers.Parameter{ "query": { Type: "string", Description: "Terme de recherche (ex: 'sujet:important de:paul@example.com')", Required: false, Category: "body", }, "from_email": { Type: "string", Description: "Filtrer par adresse email de l'expéditeur", Required: false, Category: "body", }, "to_email": { Type: "string", Description: "Filtrer par adresse email du destinataire", Required: false, Category: "body", }, "subject": { Type: "string", Description: "Filtrer par sujet de l'email", Required: false, Category: "body", }, "limit": { Type: "integer", Description: "Nombre maximum d'emails à retourner", Required: false, Category: "body", }, "cursor": { Type: "string", Description: "Curseur pour la pagination", Required: false, Category: "body", }, }, Examples: []providers.ToolExample{ { Description: "Rechercher les emails importants de Paul", Parameters: map[string]interface{}{ "query": "important", "from_email": "paul@example.com", }, }, { Description: "Rechercher les emails avec le sujet 'Facture'", Parameters: map[string]interface{}{ "subject": "Facture", "limit": 50, }, }, }, }, { Name: "create_draft", Description: "Crée un brouillon d'email dans Unipile", Category: "Drafts", Parameters: map[string]providers.Parameter{ "thread_id": { Type: "string", Description: "ID du fil de discussion existant pour le brouillon (optionnel)", Required: false, Category: "body", }, "to": { Type: "array", Description: "Destinataire(s)", Required: false, Category: "body", Items: &providers.ParameterItems{Type: "string"}, }, "cc": { Type: "array", Description: "Copie carbone (optionnel)", Required: false, Category: "body", Items: &providers.ParameterItems{Type: "string"}, }, "bcc": { Type: "array", Description: "Copie cachée (optionnel)", Required: false, Category: "body", Items: &providers.ParameterItems{Type: "string"}, }, "subject": { Type: "string", Description: "Sujet (optionnel)", Required: false, Category: "body", }, "body": { Type: "string", Description: "Contenu du brouillon (optionnel)", Required: false, Category: "body", }, "attachment_ids": { Type: "array", Description: "Pièces jointes (optionnel)", Required: false, Category: "body", Items: &providers.ParameterItems{Type: "string"}, }, }, Examples: []providers.ToolExample{ { Description: "Créer un nouveau brouillon", Parameters: map[string]interface{}{ "subject": "Idée de projet", }, }, }, }, { Name: "tag_thread", Description: "Applique un tag à un fil de discussion d'emails via Unipile", Category: "Tags", Parameters: map[string]providers.Parameter{ "thread_id": { Type: "string", Description: "ID du fil de discussion à taguer", Required: true, Category: "body", }, "tag_name": { Type: "string", Description: "Nom du tag à appliquer (ex: 'important', 'suivi')", Required: true, Category: "body", }, }, Examples: []providers.ToolExample{ { Description: "Appliquer le tag 'urgent' au thread 'thr_xyz'", Parameters: map[string]interface{}{ "thread_id": "thr_xyz", "tag_name": "urgent", }, }, }, }, { Name: "untag_thread", Description: "Supprime un tag d'un fil de discussion d'emails via Unipile", Category: "Tags", Parameters: map[string]providers.Parameter{ "thread_id": { Type: "string", Description: "ID du fil de discussion", Required: true, Category: "body", }, "tag_name": { Type: "string", Description: "Nom du tag à supprimer", Required: true, Category: "body", }, }, Examples: []providers.ToolExample{ { Description: "Supprimer le tag 'urgent' du thread 'thr_xyz'", Parameters: map[string]interface{}{ "thread_id": "thr_xyz", "tag_name": "urgent", }, }, }, }, } }

// Execute implements the providers.Service interface. // Placeholder implementation. func (s *EmailService) Execute(ctx context.Context, params map[string]interface{}) (result interface{}, err error) { return nil, fmt.Errorf("Execute method not implemented for EmailService") }

// ListEmails récupère la liste des emails disponibles func (s EmailService) ListEmails(ctx context.Context, options models.QueryOptions) (models.EmailList, error) { queryParams := url.Values{} if options.Limit > 0 { queryParams.Set("limit", strconv.Itoa(options.Limit)) } if options.Cursor != "" { queryParams.Set("cursor", options.Cursor) }

path := "/emails"
if len(queryParams) > 0 {
    path += "?" + queryParams.Encode()
}

var result models.EmailList
err := s.client.Request(ctx, http.MethodGet, path, nil, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la récupération des emails: %w", err)
}

return &result, nil

}

// GetEmail récupère les détails d'un email spécifique func (s EmailService) GetEmail(ctx context.Context, emailID string) (models.Email, error) { if emailID == "" { return nil, fmt.Errorf("email ID est requis") }

path := fmt.Sprintf("/emails/%s", emailID)

var result models.Email
err := s.client.Request(ctx, http.MethodGet, path, nil, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la récupération de l'email %s: %w", emailID, err)
}

return &result, nil

}

// ListEmailThreads récupère la liste des fils de discussion d'emails func (s EmailService) ListEmailThreads(ctx context.Context, options models.QueryOptions) (models.EmailThreadList, error) { queryParams := url.Values{} if options.Limit > 0 { queryParams.Set("limit", strconv.Itoa(options.Limit)) } if options.Cursor != "" { queryParams.Set("cursor", options.Cursor) }

path := "/email-threads"
if len(queryParams) > 0 {
    path += "?" + queryParams.Encode()
}

var result models.EmailThreadList
err := s.client.Request(ctx, http.MethodGet, path, nil, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la récupération des fils de discussion: %w", err)
}

return &result, nil

}

// GetEmailThread récupère les détails d'un fil de discussion spécifique func (s EmailService) GetEmailThread(ctx context.Context, threadID string) (models.EmailThread, error) { if threadID == "" { return nil, fmt.Errorf("thread ID est requis") }

path := fmt.Sprintf("/email-threads/%s", threadID)

var result models.EmailThread
err := s.client.Request(ctx, http.MethodGet, path, nil, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la récupération du fil de discussion %s: %w", threadID, err)
}

return &result, nil

}

// SendEmail envoie un nouvel email func (s EmailService) SendEmail(ctx context.Context, draft models.EmailDraft) (*models.EmailSent, error) { if draft == nil { return nil, fmt.Errorf("données d'email requises") }

var result models.EmailSent
err := s.client.Request(ctx, http.MethodPost, "/emails", draft, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de l'envoi de l'email: %w", err)
}

return &result, nil

}

// DeleteEmail supprime un email func (s EmailService) DeleteEmail(ctx context.Context, emailID string) (models.EmailDeleted, error) { if emailID == "" { return nil, fmt.Errorf("email ID est requis") }

path := fmt.Sprintf("/emails/%s", emailID)

var result models.EmailDeleted
err := s.client.Request(ctx, http.MethodDelete, path, nil, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la suppression de l'email %s: %w", emailID, err)
}

return &result, nil

}

// ModifyEmail modifie les attributs d'un email func (s EmailService) ModifyEmail(ctx context.Context, emailID string, modifications map[string]interface{}) (models.EmailModified, error) { if emailID == "" { return nil, fmt.Errorf("email ID est requis") }

if modifications == nil || len(modifications) == 0 {
    return nil, fmt.Errorf("au moins une modification est requise")
}

path := fmt.Sprintf("/emails/%s", emailID)

var result models.EmailModified
err := s.client.Request(ctx, http.MethodPatch, path, modifications, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la modification de l'email %s: %w", emailID, err)
}

return &result, nil

}

// UploadAttachment télécharge une pièce jointe pour un email func (s EmailService) UploadAttachment(ctx context.Context, accountID string, attachment models.AttachmentContent) (*models.EmailAttachmentUploaded, error) { if accountID == "" { return nil, fmt.Errorf("account ID est requis") }

if attachment == nil {
    return nil, fmt.Errorf("données de pièce jointe requises")
}

path := fmt.Sprintf("/accounts/%s/attachments", accountID)

var result models.EmailAttachmentUploaded
err := s.client.Request(ctx, http.MethodPost, path, attachment, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors du téléchargement de la pièce jointe: %w", err)
}

return &result, nil

}

// SearchEmails recherche dans les emails selon différents critères func (s EmailService) SearchEmails(ctx context.Context, searchRequest models.EmailSearchRequest) (*models.EmailList, error) { if searchRequest == nil { return nil, fmt.Errorf("critères de recherche requis") }

var result models.EmailList
err := s.client.Request(ctx, http.MethodPost, "/emails/search", searchRequest, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la recherche d'emails: %w", err)
}

return &result, nil

}

// CreateDraft prépare un brouillon de réponse 'Répondre à tous' // NOTE: Fake implementation - logs parameters and returns nil func (s *EmailService) CreateDraft(ctx context.Context, threadID string, draftText string, emails []string) (interface{}, error) { log.Printf("CreateDraft called with threadID: %s, draftText: %s", threadID, draftText) if threadID == "" { return nil, fmt.Errorf("thread ID est requis") } // Fake implementation - In a real scenario, this would interact with the Unipile API // path := fmt.Sprintf("/email-threads/%s/drafts", threadID) // payload := map[string]string{"text": draftText} // Example payload structure // var result interface{} // Define appropriate result struct // err := s.client.Request(ctx, http.MethodPost, path, payload, &result) // if err != nil { // return nil, fmt.Errorf("erreur lors de la création du brouillon pour le thread %s: %w", threadID, err) // } return nil, nil // Placeholder return }

// TagThread applique un tag à un fil de discussion // NOTE: Fake implementation - logs parameters and returns nil func (s *EmailService) TagThread(ctx context.Context, threadID string, tagName string) (interface{}, error) { log.Printf("TagThread called with threadID: %s, tagName: %s", threadID, tagName) if threadID == "" { return nil, fmt.Errorf("thread ID est requis") } if tagName == "" { return nil, fmt.Errorf("tag expression est requise") } // Fake implementation // path := fmt.Sprintf("/email-threads/%s/tags", threadID) // payload := map[string]string{"tag": tagExpression} // Example payload structure // var result interface{} // Define appropriate result struct // err := s.client.Request(ctx, http.MethodPost, path, payload, &result) // if err != nil { // return nil, fmt.Errorf("erreur lors de l'ajout du tag '%s' au thread %s: %w", tagExpression, threadID, err) // } return nil, nil // Placeholder return }

// UntagThread supprime un tag d'un fil de discussion // NOTE: Fake implementation - logs parameters and returns nil func (s *EmailService) UntagThread(ctx context.Context, threadID string, tagName string) (interface{}, error) { log.Printf("UntagThread called with threadID: %s, tagName: %s", threadID, tagName) if threadID == "" { return nil, fmt.Errorf("thread ID est requis") } if tagName == "" { return nil, fmt.Errorf("tag expression est requise") } // Fake implementation // path := fmt.Sprintf("/email-threads/%s/tags", threadID) // payload := map[string]string{"tag": tagExpression} // Assuming tag to remove is passed in body or params // var result interface{} // Define appropriate result struct // err := s.client.Request(ctx, http.MethodDelete, path, payload, &result) // Or potentially no payload if tag is a path/query param // if err != nil { // return nil, fmt.Errorf("erreur lors de la suppression du tag '%s' du thread %s: %w", tagExpression, threadID, err) // } return nil, nil // Placeholder return }

// --- End of services/email_service.go ---

// File: services/linkedin_service.go

//pkg/providers/unipile/services/linkedin_service.go

package services

import ( "context" "fmt" "net/http" "net/url" "strconv"

providers "gitlab.com/webigniter/slop-server/internal/providers"
"gitlab.com/webigniter/slop-server/pkg/providers/unipile/models"

)

// LinkedInService implémente les fonctionnalités pour interagir avec l'API LinkedIn via Unipile type LinkedInService struct { client models.UnipileRequester }

// NewLinkedInService crée une nouvelle instance du service LinkedIn func NewLinkedInService(client models.UnipileRequester) *LinkedInService { return &LinkedInService{ client: client, } }

// GetCapabilities retourne les capacités de ce service func (s *LinkedInService) GetCapabilities() []providers.ProviderCapability { return []providers.ProviderCapability{ { Name: "get_profile", Description: "Récupère les détails d'un profil LinkedIn", Category: "profiles", Parameters: map[string]providers.Parameter{ "account_id": {Type: "string", Description: "ID du compte LinkedIn (dans l'URL)", Required: true, Category: "path"}, "profile_id": {Type: "string", Description: "ID du profil LinkedIn URN (dans l'URL)", Required: true, Category: "path"}, }, Examples: []providers.ToolExample{ { Description: "Exemple de récupération d'un profil", Parameters: map[string]interface{}{"account_id": "acc_xyz789", "profile_id": "urn:li:profile:123456789"}, }, }, }, { Name: "search_profiles", Description: "Recherche des profils LinkedIn selon différents critères", Category: "profiles", Parameters: map[string]providers.Parameter{ "account_id": {Type: "string", Description: "ID du compte LinkedIn (dans l'URL)", Required: true, Category: "path"}, // Le corps de la requête contient les critères de recherche. La structure exacte dépend de l'API Unipile. // On définit un paramètre générique pour le corps pour l'instant. "requestBody": {Type: "object", Description: "Corps de la requête contenant les critères (query, location, industry, limit, cursor...)", Required: true, Category: "body"}, }, Examples: []providers.ToolExample{ { Description: "Exemple de recherche de profils d'ingénieurs à Paris", Parameters: map[string]interface{}{ "account_id": "acc_xyz789", "requestBody": map[string]interface{}{"query": "Software Engineer", "location": "Paris", "limit": 10}, }, }, }, }, { Name: "get_company", Description: "Récupère les détails d'une entreprise LinkedIn", Category: "companies", Parameters: map[string]providers.Parameter{ "account_id": {Type: "string", Description: "ID du compte LinkedIn (dans l'URL)", Required: true, Category: "path"}, "company_id": {Type: "string", Description: "ID de l'entreprise LinkedIn URN (dans l'URL)", Required: true, Category: "path"}, }, Examples: []providers.ToolExample{ { Description: "Exemple de récupération d'une entreprise", Parameters: map[string]interface{}{"account_id": "acc_xyz789", "company_id": "urn:li:company:98765"}, }, }, }, { Name: "search_companies", Description: "Recherche des entreprises LinkedIn selon différents critères", Category: "companies", Parameters: map[string]providers.Parameter{ "account_id": {Type: "string", Description: "ID du compte LinkedIn (dans l'URL)", Required: true, Category: "path"}, "query": {Type: "string", Description: "Terme de recherche (paramètre de requête)", Required: false, Category: "query"}, "industry": {Type: "string", Description: "Secteur d'activité (paramètre de requête)", Required: false, Category: "query"}, "limit": {Type: "integer", Description: "Nombre maximum de résultats (paramètre de requête)", Required: false, Category: "query", Default: 10}, "cursor": {Type: "string", Description: "Curseur pour la pagination (paramètre de requête)", Required: false, Category: "query"}, }, Examples: []providers.ToolExample{ { Description: "Exemple de recherche d'entreprises tech, limité à 20 résultats", Parameters: map[string]interface{}{"account_id": "acc_xyz789", "query": "Technology", "limit": 20}, }, }, }, { Name: "send_invitation", Description: "Envoie une invitation de connexion à un utilisateur LinkedIn", Category: "networking", Parameters: map[string]providers.Parameter{ "account_id": {Type: "string", Description: "ID du compte LinkedIn (dans l'URL)", Required: true, Category: "path"}, "recipient_id": {Type: "string", Description: "ID du profil LinkedIn du destinataire URN (dans le corps)", Required: true, Category: "body"}, "message": {Type: "string", Description: "Message personnalisé (optionnel, dans le corps)", Required: false, Category: "body"}, }, Examples: []providers.ToolExample{ { Description: "Exemple d'envoi d'une invitation avec message", Parameters: map[string]interface{}{ "account_id": "acc_xyz789", "recipient_id": "urn:li:profile:123456789", "message": "Bonjour, j'aimerais me connecter avec vous.", }, }, }, }, { Name: "list_invitations", Description: "Liste les invitations de connexion d'un compte LinkedIn", Category: "networking", Parameters: map[string]providers.Parameter{ "account_id": {Type: "string", Description: "ID du compte LinkedIn (dans l'URL)", Required: true, Category: "path"}, "status": {Type: "string", Description: "Statut des invitations (pending, accepted, ignored) (paramètre de requête)", Required: false, Category: "query", Enum: []string{"pending", "accepted", "ignored"}}, "limit": {Type: "integer", Description: "Nombre maximum de résultats (paramètre de requête)", Required: false, Category: "query", Default: 10}, "cursor": {Type: "string", Description: "Curseur pour la pagination (paramètre de requête)", Required: false, Category: "query"}, }, Examples: []providers.ToolExample{ { Description: "Exemple de listage des invitations en attente", Parameters: map[string]interface{}{"account_id": "acc_xyz789", "status": "pending", "limit": 50}, }, }, }, { Name: "get_invitation", Description: "Récupère les détails d'une invitation de connexion spécifique", Category: "networking", Parameters: map[string]providers.Parameter{ "account_id": {Type: "string", Description: "ID du compte LinkedIn (dans l'URL)", Required: true, Category: "path"}, "invitation_id": {Type: "string", Description: "ID de l'invitation (dans l'URL)", Required: true, Category: "path"}, }, Examples: []providers.ToolExample{ { Description: "Exemple de récupération d'une invitation spécifique", Parameters: map[string]interface{}{"account_id": "acc_xyz789", "invitation_id": "inv_abcdef123"}, }, }, }, { Name: "send_message", Description: "Envoie un message à une connexion LinkedIn", Category: "messaging", Parameters: map[string]providers.Parameter{ "account_id": {Type: "string", Description: "ID du compte LinkedIn (dans l'URL)", Required: true, Category: "path"}, "recipient_id": {Type: "string", Description: "ID du profil LinkedIn du destinataire URN (dans le corps)", Required: true, Category: "body"}, "message": {Type: "string", Description: "Contenu du message (dans le corps)", Required: true, Category: "body"}, }, Examples: []providers.ToolExample{ { Description: "Exemple d'envoi d'un message de suivi", Parameters: map[string]interface{}{ "account_id": "acc_xyz789", "recipient_id": "urn:li:profile:123456789", "message": "Juste pour faire suite à notre conversation.", }, }, }, }, { Name: "create_post", Description: "Crée une publication LinkedIn", Category: "content", Parameters: map[string]providers.Parameter{ "account_id": {Type: "string", Description: "ID du compte LinkedIn (dans l'URL)", Required: true, Category: "path"}, // Le corps de la requête contient les détails de la publication. La structure exacte dépend de l'API Unipile. "requestBody": {Type: "object", Description: "Corps de la requête contenant les détails de la publication (text, visibility...)", Required: true, Category: "body"}, }, Examples: []providers.ToolExample{ { Description: "Exemple de création d'une publication texte simple", Parameters: map[string]interface{}{ "account_id": "acc_xyz789", "requestBody": map[string]interface{}{"text": "Je partage une nouvelle mise à jour passionnante !", "visibility": "CONNECTIONS"}, }, }, }, }, } }

// Execute implements the providers.Service interface. // Placeholder implementation. func (s *LinkedInService) Execute(ctx context.Context, params map[string]interface{}) (result interface{}, err error) { return nil, fmt.Errorf("Execute method not implemented for LinkedInService") }

// GetProfile récupère les détails d'un profil LinkedIn // accountID: ID du compte LinkedIn (obligatoire) // profileID: ID du profil à récupérer (obligatoire) // Retourne: Le profil LinkedIn demandé ou une erreur func (s LinkedInService) GetProfile(ctx context.Context, accountID string, profileID string) (models.LinkedInProfile, error) { if accountID == "" { return nil, fmt.Errorf("account ID est requis") }

if profileID == "" {
    return nil, fmt.Errorf("profile ID est requis")
}

path := fmt.Sprintf("/accounts/%s/linkedin/profiles/%s", accountID, profileID)

var result models.LinkedInProfile
err := s.client.Request(ctx, http.MethodGet, path, nil, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la récupération du profil %s: %w", profileID, err)
}

return &result, nil

}

// SearchProfiles recherche des profils LinkedIn selon différents critères // accountID: ID du compte LinkedIn (obligatoire) // searchRequest: Critères de recherche (obligatoire) // Retourne: Liste de profils correspondant aux critères ou une erreur func (s LinkedInService) SearchProfiles(ctx context.Context, accountID string, searchRequest models.LinkedInSearchRequest) (*models.LinkedInProfileList, error) { if accountID == "" { return nil, fmt.Errorf("account ID est requis") }

if searchRequest == nil {
    return nil, fmt.Errorf("critères de recherche requis")
}

path := fmt.Sprintf("/accounts/%s/linkedin/profiles/search", accountID)

var result models.LinkedInProfileList
err := s.client.Request(ctx, http.MethodPost, path, searchRequest, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la recherche de profils: %w", err)
}

return &result, nil

}

// GetCompany récupère les détails d'une entreprise LinkedIn // accountID: ID du compte LinkedIn (obligatoire) // companyID: ID de l'entreprise à récupérer (obligatoire) // Retourne: L'entreprise LinkedIn demandée ou une erreur func (s LinkedInService) GetCompany(ctx context.Context, accountID string, companyID string) (models.LinkedInCompany, error) { if accountID == "" { return nil, fmt.Errorf("account ID est requis") }

if companyID == "" {
    return nil, fmt.Errorf("company ID est requis")
}

path := fmt.Sprintf("/accounts/%s/linkedin/companies/%s", accountID, companyID)

var result models.LinkedInCompany
err := s.client.Request(ctx, http.MethodGet, path, nil, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la récupération de l'entreprise %s: %w", companyID, err)
}

return &result, nil

}

// SearchCompanies recherche des entreprises LinkedIn selon différents critères // accountID: ID du compte LinkedIn (obligatoire) // query: Terme de recherche (optionnel) // industry: Industrie spécifique (optionnel) // limit: Nombre maximum de résultats (optionnel) // cursor: Curseur pour la pagination (optionnel) // Retourne: Liste d'entreprises correspondant aux critères ou une erreur func (s LinkedInService) SearchCompanies(ctx context.Context, accountID string, query string, industry string, limit int, cursor string) (models.LinkedInCompanyList, error) { if accountID == "" { return nil, fmt.Errorf("account ID est requis") }

queryParams := url.Values{}
if query != "" {
    queryParams.Set("query", query)
}
if industry != "" {
    queryParams.Set("industry", industry)
}
if limit > 0 {
    queryParams.Set("limit", strconv.Itoa(limit))
}
if cursor != "" {
    queryParams.Set("cursor", cursor)
}

path := fmt.Sprintf("/accounts/%s/linkedin/companies/search", accountID)
if len(queryParams) > 0 {
    path += "?" + queryParams.Encode()
}

var result models.LinkedInCompanyList
err := s.client.Request(ctx, http.MethodGet, path, nil, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la recherche d'entreprises: %w", err)
}

return &result, nil

}

// SendInvitation envoie une invitation de connexion à un utilisateur LinkedIn // accountID: ID du compte LinkedIn (obligatoire) // recipientID: ID du destinataire de l'invitation (obligatoire) // message: Message personnalisé pour l'invitation (optionnel) // Retourne: Confirmation de l'envoi de l'invitation ou une erreur func (s LinkedInService) SendInvitation(ctx context.Context, accountID string, recipientID string, message string) (models.LinkedInInvitationSent, error) { if accountID == "" { return nil, fmt.Errorf("account ID est requis") }

if recipientID == "" {
    return nil, fmt.Errorf("recipient ID est requis")
}

requestBody := map[string]string{
    "recipient_id": recipientID,
}

if message != "" {
    requestBody["message"] = message
}

path := fmt.Sprintf("/accounts/%s/linkedin/invitations", accountID)

var result models.LinkedInInvitationSent
err := s.client.Request(ctx, http.MethodPost, path, requestBody, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de l'envoi de l'invitation: %w", err)
}

return &result, nil

}

// ListInvitations liste les invitations de connexion d'un compte LinkedIn // accountID: ID du compte LinkedIn (obligatoire) // status: Statut des invitations à récupérer (pending, accepted, ignored) (optionnel) // limit: Nombre maximum de résultats (optionnel) // cursor: Curseur pour la pagination (optionnel) // Retourne: Liste des invitations correspondant aux critères ou une erreur func (s LinkedInService) ListInvitations(ctx context.Context, accountID string, status string, limit int, cursor string) (models.LinkedInInvitationList, error) { if accountID == "" { return nil, fmt.Errorf("account ID est requis") }

queryParams := url.Values{}
if status != "" {
    queryParams.Set("status", status)
}
if limit > 0 {
    queryParams.Set("limit", strconv.Itoa(limit))
}
if cursor != "" {
    queryParams.Set("cursor", cursor)
}

path := fmt.Sprintf("/accounts/%s/linkedin/invitations", accountID)
if len(queryParams) > 0 {
    path += "?" + queryParams.Encode()
}

var result models.LinkedInInvitationList
err := s.client.Request(ctx, http.MethodGet, path, nil, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la récupération des invitations: %w", err)
}

return &result, nil

}

// GetInvitation récupère les détails d'une invitation de connexion spécifique // accountID: ID du compte LinkedIn (obligatoire) // invitationID: ID de l'invitation à récupérer (obligatoire) // Retourne: Les détails de l'invitation demandée ou une erreur func (s LinkedInService) GetInvitation(ctx context.Context, accountID string, invitationID string) (models.LinkedInInvitation, error) { if accountID == "" { return nil, fmt.Errorf("account ID est requis") }

if invitationID == "" {
    return nil, fmt.Errorf("invitation ID est requis")
}

path := fmt.Sprintf("/accounts/%s/linkedin/invitations/%s", accountID, invitationID)

var result models.LinkedInInvitation
err := s.client.Request(ctx, http.MethodGet, path, nil, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la récupération de l'invitation %s: %w", invitationID, err)
}

return &result, nil

}

// SendMessage envoie un message à une connexion LinkedIn // accountID: ID du compte LinkedIn (obligatoire) // recipientID: ID du destinataire du message (obligatoire) // message: Contenu du message (obligatoire) // Retourne: Confirmation de l'envoi du message ou une erreur func (s LinkedInService) SendMessage(ctx context.Context, accountID string, recipientID string, message string) (models.LinkedInMessageSent, error) { if accountID == "" { return nil, fmt.Errorf("account ID est requis") }

if recipientID == "" {
    return nil, fmt.Errorf("recipient ID est requis")
}

if message == "" {
    return nil, fmt.Errorf("message est requis")
}

requestBody := map[string]string{
    "recipient_id": recipientID,
    "message":      message,
}

path := fmt.Sprintf("/accounts/%s/linkedin/messages", accountID)

var result models.LinkedInMessageSent
err := s.client.Request(ctx, http.MethodPost, path, requestBody, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de l'envoi du message: %w", err)
}

return &result, nil

}

// CreatePost crée une publication LinkedIn // accountID: ID du compte LinkedIn (obligatoire) // postDraft: Brouillon de la publication (obligatoire) // Retourne: Confirmation de la création de la publication ou une erreur func (s LinkedInService) CreatePost(ctx context.Context, accountID string, postDraft models.LinkedInPostDraft) (*models.LinkedInPostCreated, error) { if accountID == "" { return nil, fmt.Errorf("account ID est requis") }

if postDraft == nil {
    return nil, fmt.Errorf("post draft est requis")
}

path := fmt.Sprintf("/accounts/%s/linkedin/posts", accountID)

var result models.LinkedInPostCreated
err := s.client.Request(ctx, http.MethodPost, path, postDraft, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la création de la publication: %w", err)
}

return &result, nil

}

// --- End of services/linkedin_service.go ---

// File: services/messaging_service.go

//pkg/providers/unipile/services/messaging_service.go

package services

import ( "context" "fmt" "net/http" "net/url" "strconv"

providers "gitlab.com/webigniter/slop-server/internal/providers"
"gitlab.com/webigniter/slop-server/pkg/providers/unipile/models"

)

// MessagingService implémente les fonctionnalités pour gérer les chats et messages type MessagingService struct { client models.UnipileRequester }

// NewMessagingService crée une nouvelle instance du service de messagerie func NewMessagingService(client models.UnipileRequester) *MessagingService { return &MessagingService{ client: client, } }

// GetCapabilities retourne les capacités de ce service func (s *MessagingService) GetCapabilities() []providers.ProviderCapability { return []providers.ProviderCapability{ { Name: "list_chats", Description: "Liste les chats/conversations disponibles", Category: "messaging", Parameters: map[string]providers.Parameter{ "limit": { Type: "integer", Description: "Maximum number of chats to return.", Required: false, Category: "query", }, "cursor": { Type: "string", Description: "Cursor for pagination.", Required: false, Category: "query", }, }, }, { Name: "get_chat", Description: "Récupère les détails d'un chat spécifique", Category: "messaging", Parameters: map[string]providers.Parameter{ "chat_id": { Type: "string", Description: "The ID of the chat to retrieve.", Required: true, Category: "path", }, }, }, { Name: "create_chat", Description: "Crée un nouveau chat/conversation", Category: "messaging", Parameters: map[string]providers.Parameter{ "participant_ids": { Type: "array", // Assuming array of strings Description: "List of account IDs for participants.", Required: true, Category: "body", Items: &providers.ParameterItems{Type: "string"}, }, "name": { Type: "string", Description: "Optional name for the chat.", Required: false, Category: "body", }, "description": { Type: "string", Description: "Optional description for the chat.", Required: false, Category: "body", }, }, }, { Name: "list_messages", Description: "Liste les messages d'un chat", Category: "messaging", Parameters: map[string]providers.Parameter{ "chat_id": { Type: "string", Description: "The ID of the chat.", Required: true, Category: "path", }, "limit": { Type: "integer", Description: "Maximum number of messages to return.", Required: false, Category: "query", }, "cursor": { Type: "string", Description: "Cursor for pagination.", Required: false, Category: "query", }, }, }, { Name: "get_message", Description: "Récupère les détails d'un message spécifique", Category: "messaging", Parameters: map[string]providers.Parameter{ "message_id": { Type: "string", Description: "The ID of the message to retrieve.", Required: true, Category: "path", }, }, }, { Name: "send_message", Description: "Envoie un message dans un chat", Category: "messaging", Parameters: map[string]providers.Parameter{ "chat_id": { Type: "string", Description: "The ID of the chat to send the message to.", Required: true, Category: "path", }, "text": { // Assuming MessageDraft has at least 'text' Type: "string", Description: "The content of the message.", Required: true, Category: "body", }, // Add other potential fields from models.MessageDraft if known // "attachments": { Type: "array", ... } }, }, { Name: "list_chat_participants", Description: "Liste les participants d'un chat", Category: "messaging", Parameters: map[string]providers.Parameter{ "chat_id": { Type: "string", Description: "The ID of the chat.", Required: true, Category: "path", }, "limit": { Type: "integer", Description: "Maximum number of participants to return.", Required: false, Category: "query", }, "cursor": { Type: "string", Description: "Cursor for pagination.", Required: false, Category: "query", }, }, }, { Name: "add_chat_participant", Description: "Ajoute un participant à un chat", Category: "messaging", Parameters: map[string]providers.Parameter{ "chat_id": { Type: "string", Description: "The ID of the chat.", Required: true, Category: "path", }, "account_id": { Type: "string", Description: "The account ID of the participant to add.", Required: true, Category: "body", }, "role": { Type: "string", Description: "Optional role for the participant.", Required: false, Category: "body", }, }, }, { Name: "remove_chat_participant", Description: "Supprime un participant d'un chat", Category: "messaging", Parameters: map[string]providers.Parameter{ "chat_id": { Type: "string", Description: "The ID of the chat.", Required: true, Category: "path", }, "participant_id": { Type: "string", Description: "The ID of the participant to remove.", Required: true, Category: "path", }, }, }, { Name: "update_message", Description: "Met à jour un message existant", Category: "messaging", Parameters: map[string]providers.Parameter{ "message_id": { Type: "string", Description: "The ID of the message to update.", Required: true, Category: "path", }, "text": { Type: "string", Description: "The new text content for the message.", Required: true, Category: "body", }, }, }, { Name: "delete_message", Description: "Supprime un message", Category: "messaging", Parameters: map[string]providers.Parameter{ "message_id": { Type: "string", Description: "The ID of the message to delete.", Required: true, Category: "path", }, }, }, { Name: "search_chats", Description: "Recherche de chats selon différents critères", Category: "messaging", Parameters: map[string]providers.Parameter{ // Parameters depend on models.ChatSearchRequest definition // Placeholder: "query": { Type: "string", Description: "Search query string.", Required: true, // Assuming at least some criteria required Category: "body", }, // Add other potential fields from models.ChatSearchRequest }, }, { Name: "search_messages", Description: "Recherche de messages selon différents critères", Category: "messaging", Parameters: map[string]providers.Parameter{ // Parameters depend on models.MessageSearchRequest definition // Placeholder: "query": { Type: "string", Description: "Search query string.", Required: true, // Assuming at least some criteria required Category: "body", }, "chat_id": { Type: "string", Description: "Optional chat ID to limit search.", Required: false, Category: "body", }, // Add other potential fields from models.MessageSearchRequest }, }, } }

// Execute implements the providers.Service interface. // Placeholder implementation. func (s *MessagingService) Execute(ctx context.Context, params map[string]interface{}) (result interface{}, err error) { return nil, fmt.Errorf("Execute method not implemented for MessagingService") }

// ListChats récupère la liste des chats/conversations disponibles func (s MessagingService) ListChats(ctx context.Context, options models.QueryOptions) (models.ChatList, error) { queryParams := url.Values{} if options.Limit > 0 { queryParams.Set("limit", strconv.Itoa(options.Limit)) } if options.Cursor != "" { queryParams.Set("cursor", options.Cursor) }

path := "/chats"
if len(queryParams) > 0 {
    path += "?" + queryParams.Encode()
}

var result models.ChatList
err := s.client.Request(ctx, http.MethodGet, path, nil, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la récupération des chats: %w", err)
}

return &result, nil

}

// GetChat récupère les détails d'un chat spécifique func (s MessagingService) GetChat(ctx context.Context, chatID string) (models.Chat, error) { if chatID == "" { return nil, fmt.Errorf("chat ID est requis") }

path := fmt.Sprintf("/chats/%s", chatID)

var result models.Chat
err := s.client.Request(ctx, http.MethodGet, path, nil, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la récupération du chat %s: %w", chatID, err)
}

return &result, nil

}

// CreateChat crée un nouveau chat/conversation func (s MessagingService) CreateChat(ctx context.Context, participantIDs []string, name string, description string) (models.ChatCreated, error) { if len(participantIDs) == 0 { return nil, fmt.Errorf("au moins un participant est requis") }

requestBody := map[string]interface{}{
    "participant_ids": participantIDs,
}

if name != "" {
    requestBody["name"] = name
}

if description != "" {
    requestBody["description"] = description
}

var result models.ChatCreated
err := s.client.Request(ctx, http.MethodPost, "/chats", requestBody, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la création du chat: %w", err)
}

return &result, nil

}

// ListMessages récupère la liste des messages d'un chat func (s MessagingService) ListMessages(ctx context.Context, chatID string, options models.QueryOptions) (models.MessageList, error) { if chatID == "" { return nil, fmt.Errorf("chat ID est requis") }

queryParams := url.Values{}
if options.Limit > 0 {
    queryParams.Set("limit", strconv.Itoa(options.Limit))
}
if options.Cursor != "" {
    queryParams.Set("cursor", options.Cursor)
}

path := fmt.Sprintf("/chats/%s/messages", chatID)
if len(queryParams) > 0 {
    path += "?" + queryParams.Encode()
}

var result models.MessageList
err := s.client.Request(ctx, http.MethodGet, path, nil, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la récupération des messages du chat %s: %w", chatID, err)
}

return &result, nil

}

// GetMessage récupère les détails d'un message spécifique func (s MessagingService) GetMessage(ctx context.Context, messageID string) (models.Message, error) { if messageID == "" { return nil, fmt.Errorf("message ID est requis") }

path := fmt.Sprintf("/messages/%s", messageID)

var result models.Message
err := s.client.Request(ctx, http.MethodGet, path, nil, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la récupération du message %s: %w", messageID, err)
}

return &result, nil

}

// SendMessage envoie un message dans un chat func (s MessagingService) SendMessage(ctx context.Context, chatID string, draft models.MessageDraft) (*models.MessageSent, error) { if chatID == "" { return nil, fmt.Errorf("chat ID est requis") }

if draft == nil || draft.Text == "" {
    return nil, fmt.Errorf("le contenu du message est requis")
}

path := fmt.Sprintf("/chats/%s/messages", chatID)

var result models.MessageSent
err := s.client.Request(ctx, http.MethodPost, path, draft, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de l'envoi du message dans le chat %s: %w", chatID, err)
}

return &result, nil

}

// UpdateMessage met à jour un message existant func (s MessagingService) UpdateMessage(ctx context.Context, messageID string, text string) (models.MessageUpdated, error) { if messageID == "" { return nil, fmt.Errorf("message ID est requis") }

if text == "" {
    return nil, fmt.Errorf("le nouveau texte du message est requis")
}

requestBody := map[string]interface{}{
    "text": text,
}

path := fmt.Sprintf("/messages/%s", messageID)

var result models.MessageUpdated
err := s.client.Request(ctx, http.MethodPatch, path, requestBody, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la mise à jour du message %s: %w", messageID, err)
}

return &result, nil

}

// DeleteMessage supprime un message func (s MessagingService) DeleteMessage(ctx context.Context, messageID string) (models.MessageDeleted, error) { if messageID == "" { return nil, fmt.Errorf("message ID est requis") }

path := fmt.Sprintf("/messages/%s", messageID)

var result models.MessageDeleted
err := s.client.Request(ctx, http.MethodDelete, path, nil, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la suppression du message %s: %w", messageID, err)
}

return &result, nil

}

// ListChatParticipants récupère la liste des participants d'un chat func (s MessagingService) ListChatParticipants(ctx context.Context, chatID string, options models.QueryOptions) (models.ChatParticipantList, error) { if chatID == "" { return nil, fmt.Errorf("chat ID est requis") }

queryParams := url.Values{}
if options.Limit > 0 {
    queryParams.Set("limit", strconv.Itoa(options.Limit))
}
if options.Cursor != "" {
    queryParams.Set("cursor", options.Cursor)
}

path := fmt.Sprintf("/chats/%s/participants", chatID)
if len(queryParams) > 0 {
    path += "?" + queryParams.Encode()
}

var result models.ChatParticipantList
err := s.client.Request(ctx, http.MethodGet, path, nil, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la récupération des participants du chat %s: %w", chatID, err)
}

return &result, nil

}

// AddChatParticipant ajoute un participant à un chat func (s MessagingService) AddChatParticipant(ctx context.Context, chatID string, accountID string, role string) (models.ChatParticipantAdded, error) { if chatID == "" { return nil, fmt.Errorf("chat ID est requis") }

if accountID == "" {
    return nil, fmt.Errorf("account ID est requis")
}

requestBody := map[string]interface{}{
    "account_id": accountID,
}

if role != "" {
    requestBody["role"] = role
}

path := fmt.Sprintf("/chats/%s/participants", chatID)

var result models.ChatParticipantAdded
err := s.client.Request(ctx, http.MethodPost, path, requestBody, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de l'ajout du participant au chat %s: %w", chatID, err)
}

return &result, nil

}

// RemoveChatParticipant supprime un participant d'un chat func (s MessagingService) RemoveChatParticipant(ctx context.Context, chatID string, participantID string) (models.ChatParticipantRemoved, error) { if chatID == "" { return nil, fmt.Errorf("chat ID est requis") }

if participantID == "" {
    return nil, fmt.Errorf("participant ID est requis")
}

path := fmt.Sprintf("/chats/%s/participants/%s", chatID, participantID)

var result models.ChatParticipantRemoved
err := s.client.Request(ctx, http.MethodDelete, path, nil, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la suppression du participant %s du chat %s: %w", participantID, chatID, err)
}

return &result, nil

}

// SearchChats recherche des chats selon différents critères func (s MessagingService) SearchChats(ctx context.Context, searchRequest models.ChatSearchRequest) (*models.ChatList, error) { if searchRequest == nil { return nil, fmt.Errorf("critères de recherche requis") }

var result models.ChatList
err := s.client.Request(ctx, http.MethodPost, "/chats/search", searchRequest, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la recherche de chats: %w", err)
}

return &result, nil

}

// SearchMessages recherche des messages selon différents critères func (s MessagingService) SearchMessages(ctx context.Context, searchRequest models.MessageSearchRequest) (*models.MessageList, error) { if searchRequest == nil { return nil, fmt.Errorf("critères de recherche requis") }

var result models.MessageList
err := s.client.Request(ctx, http.MethodPost, "/messages/search", searchRequest, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la recherche de messages: %w", err)
}

return &result, nil

}

// --- End of services/messaging_service.go ---

// File: services/page_service.go

//pkg/providers/unipile/services/page_service.go

package services

import ( "context" "fmt" "net/http" "net/url" "strconv"

providers "gitlab.com/webigniter/slop-server/internal/providers"
"gitlab.com/webigniter/slop-server/pkg/providers/unipile/models"

)

// PageService implémente les fonctionnalités de gestion des pages type PageService struct { client models.UnipileRequester }

// NewPageService crée une nouvelle instance de PageService func NewPageService(client models.UnipileRequester) *PageService { return &PageService{ client: client, } }

// GetCapabilities retourne les capacités du service de pages func (s *PageService) GetCapabilities() []providers.ProviderCapability { return []providers.ProviderCapability{ { Name: "list_pages", Description: "Liste les pages avec pagination", Category: "Page Management", Parameters: map[string]providers.Parameter{ "limit": { Type: "integer", Description: "Maximum number of pages to return", Required: false, Category: "query", }, "cursor": { Type: "string", Description: "Cursor for pagination", Required: false, Category: "query", }, }, Examples: []providers.ToolExample{ { Description: "List the first 10 pages", Parameters: map[string]interface{}{ "limit": 10, }, }, }, }, { Name: "get_page", Description: "Récupère les détails d'une page spécifique", Category: "Page Management", Parameters: map[string]providers.Parameter{ "page_id": { Type: "string", Description: "ID of the page to retrieve", Required: true, Category: "path", }, }, Examples: []providers.ToolExample{ { Description: "Get page with ID 'page-123'", Parameters: map[string]interface{}{ "page_id": "page-123", }, }, }, }, { Name: "create_page", Description: "Crée une nouvelle page", Category: "Page Management", Parameters: map[string]providers.Parameter{ "pageDraft": { Type: "object", // Assuming models.PageDraft maps to a JSON object Description: "Page data for creation (structure defined by models.PageDraft)", Required: true, Category: "body", }, }, Examples: []providers.ToolExample{ { Description: "Create a new page with a title", Parameters: map[string]interface{}{ "pageDraft": map[string]interface{}{ "title": "New Page Title", // Add other fields from models.PageDraft as needed }, }, }, }, }, { Name: "update_page", Description: "Met à jour une page existante", Category: "Page Management", Parameters: map[string]providers.Parameter{ "page_id": { Type: "string", Description: "ID of the page to update", Required: true, Category: "path", }, "updates": { Type: "object", Description: "Fields to update (key-value pairs)", Required: true, Category: "body", }, }, Examples: []providers.ToolExample{ { Description: "Update the title of page 'page-123'", Parameters: map[string]interface{}{ "page_id": "page-123", "updates": map[string]interface{}{ "title": "Updated Page Title", }, }, }, }, }, { Name: "delete_page", Description: "Supprime une page existante", Category: "Page Management", Parameters: map[string]providers.Parameter{ "page_id": { Type: "string", Description: "ID of the page to delete", Required: true, Category: "path", }, }, Examples: []providers.ToolExample{ { Description: "Delete page with ID 'page-456'", Parameters: map[string]interface{}{ "page_id": "page-456", }, }, }, }, { Name: "list_websites", Description: "Liste les sites web avec pagination", Category: "Website Management", Parameters: map[string]providers.Parameter{ "limit": { Type: "integer", Description: "Maximum number of websites to return", Required: false, Category: "query", }, "cursor": { Type: "string", Description: "Cursor for pagination", Required: false, Category: "query", }, }, Examples: []providers.ToolExample{ { Description: "List the first 5 websites", Parameters: map[string]interface{}{ "limit": 5, }, }, }, }, { Name: "get_website", Description: "Récupère les détails d'un site web spécifique", Category: "Website Management", Parameters: map[string]providers.Parameter{ "website_id": { Type: "string", Description: "ID of the website to retrieve", Required: true, Category: "path", }, }, Examples: []providers.ToolExample{ { Description: "Get website with ID 'site-abc'", Parameters: map[string]interface{}{ "website_id": "site-abc", }, }, }, }, { Name: "get_menu", Description: "Récupère le menu d'un site web", Category: "Website Management", Parameters: map[string]providers.Parameter{ "website_id": { Type: "string", Description: "ID of the website whose menu to retrieve", Required: true, Category: "path", }, }, Examples: []providers.ToolExample{ { Description: "Get the menu for website 'site-xyz'", Parameters: map[string]interface{}{ "website_id": "site-xyz", }, }, }, }, { Name: "search_pages", Description: "Recherche des pages basé sur des critères spécifiques", Category: "Page Management", Parameters: map[string]providers.Parameter{ "searchRequest": { Type: "object", // Assuming models.PageSearchRequest maps to a JSON object Description: "Search criteria (structure defined by models.PageSearchRequest)", Required: true, Category: "body", }, }, Examples: []providers.ToolExample{ { Description: "Search for pages containing the term 'example'", Parameters: map[string]interface{}{ "searchRequest": map[string]interface{}{ "query": "example", // Add other fields from models.PageSearchRequest as needed }, }, }, }, }, } }

// Execute implements the providers.Service interface. // Placeholder implementation. func (s *PageService) Execute(ctx context.Context, params map[string]interface{}) (result interface{}, err error) { return nil, fmt.Errorf("Execute method not implemented for PageService") }

// ListPages liste les pages avec pagination func (s PageService) ListPages(ctx context.Context, options models.QueryOptions) (models.PageList, error) { endpoint := "/pages"

// Créer les paramètres de requête pour pagination
queryParams := url.Values{}
if options.Limit > 0 {
    queryParams.Set("limit", strconv.Itoa(options.Limit))
}
if options.Cursor != "" {
    queryParams.Set("cursor", options.Cursor)
}

if len(queryParams) > 0 {
    endpoint += "?" + queryParams.Encode()
}

var pageList models.PageList
if err := s.client.Request(ctx, http.MethodGet, endpoint, nil, &pageList); err != nil {
    return nil, fmt.Errorf("error listing pages: %w", err)
}

return &pageList, nil

}

// GetPage récupère les détails d'une page spécifique func (s PageService) GetPage(ctx context.Context, pageID string) (models.Page, error) { if pageID == "" { return nil, fmt.Errorf("page ID is required") }

endpoint := fmt.Sprintf("/pages/%s", pageID)

var page models.Page
if err := s.client.Request(ctx, http.MethodGet, endpoint, nil, &page); err != nil {
    return nil, fmt.Errorf("error getting page: %w", err)
}

return &page, nil

}

// CreatePage crée une nouvelle page func (s PageService) CreatePage(ctx context.Context, pageDraft models.PageDraft) (*models.PageCreated, error) { if pageDraft == nil { return nil, fmt.Errorf("page draft is required") }

endpoint := "/pages"

var pageCreated models.PageCreated
if err := s.client.Request(ctx, http.MethodPost, endpoint, pageDraft, &pageCreated); err != nil {
    return nil, fmt.Errorf("error creating page: %w", err)
}

return &pageCreated, nil

}

// UpdatePage met à jour une page existante func (s PageService) UpdatePage(ctx context.Context, pageID string, updates map[string]interface{}) (models.PageUpdated, error) { if pageID == "" { return nil, fmt.Errorf("page ID is required") } if updates == nil { return nil, fmt.Errorf("updates are required") }

endpoint := fmt.Sprintf("/pages/%s", pageID)

var pageUpdated models.PageUpdated
if err := s.client.Request(ctx, http.MethodPatch, endpoint, updates, &pageUpdated); err != nil {
    return nil, fmt.Errorf("error updating page: %w", err)
}

return &pageUpdated, nil

}

// DeletePage supprime une page existante func (s PageService) DeletePage(ctx context.Context, pageID string) (models.PageDeleted, error) { if pageID == "" { return nil, fmt.Errorf("page ID is required") }

endpoint := fmt.Sprintf("/pages/%s", pageID)

var pageDeleted models.PageDeleted
if err := s.client.Request(ctx, http.MethodDelete, endpoint, nil, &pageDeleted); err != nil {
    return nil, fmt.Errorf("error deleting page: %w", err)
}

return &pageDeleted, nil

}

// ListWebsites liste les sites web avec pagination func (s PageService) ListWebsites(ctx context.Context, options models.QueryOptions) (models.WebsiteList, error) { endpoint := "/websites"

// Créer les paramètres de requête pour pagination
queryParams := url.Values{}
if options.Limit > 0 {
    queryParams.Set("limit", strconv.Itoa(options.Limit))
}
if options.Cursor != "" {
    queryParams.Set("cursor", options.Cursor)
}

if len(queryParams) > 0 {
    endpoint += "?" + queryParams.Encode()
}

var websiteList models.WebsiteList
if err := s.client.Request(ctx, http.MethodGet, endpoint, nil, &websiteList); err != nil {
    return nil, fmt.Errorf("error listing websites: %w", err)
}

return &websiteList, nil

}

// GetWebsite récupère les détails d'un site web spécifique func (s PageService) GetWebsite(ctx context.Context, websiteID string) (models.Website, error) { if websiteID == "" { return nil, fmt.Errorf("website ID is required") }

endpoint := fmt.Sprintf("/websites/%s", websiteID)

var website models.Website
if err := s.client.Request(ctx, http.MethodGet, endpoint, nil, &website); err != nil {
    return nil, fmt.Errorf("error getting website: %w", err)
}

return &website, nil

}

// GetMenu récupère le menu d'un site web func (s PageService) GetMenu(ctx context.Context, websiteID string) (models.Menu, error) { if websiteID == "" { return nil, fmt.Errorf("website ID is required") }

endpoint := fmt.Sprintf("/websites/%s/menu", websiteID)

var menu models.Menu
if err := s.client.Request(ctx, http.MethodGet, endpoint, nil, &menu); err != nil {
    return nil, fmt.Errorf("error getting menu: %w", err)
}

return &menu, nil

}

// SearchPages recherche des pages basé sur des critères spécifiques func (s PageService) SearchPages(ctx context.Context, searchRequest models.PageSearchRequest) (*models.PageList, error) { if searchRequest == nil { return nil, fmt.Errorf("search request is required") }

endpoint := "/pages/search"

var pageList models.PageList
if err := s.client.Request(ctx, http.MethodPost, endpoint, searchRequest, &pageList); err != nil {
    return nil, fmt.Errorf("error searching pages: %w", err)
}

return &pageList, nil

}

// --- End of services/page_service.go ---

// File: services/post_service.go

//pkg/providers/unipile/services/post_service.go

package services

import ( "context" "fmt" "net/http" "net/url" "strconv"

providers "gitlab.com/webigniter/slop-server/internal/providers"
"gitlab.com/webigniter/slop-server/pkg/providers/unipile/models"

)

// PostService implémente les fonctionnalités de gestion des publications type PostService struct { client models.UnipileRequester }

// NewPostService crée une nouvelle instance de PostService func NewPostService(client models.UnipileRequester) *PostService { return &PostService{ client: client, } }

// GetCapabilities retourne les capacités du service de publications func (s *PostService) GetCapabilities() []providers.ProviderCapability { return []providers.ProviderCapability{ { Name: "list_posts", Description: "Liste les publications avec pagination", }, { Name: "get_post", Description: "Récupère les détails d'une publication spécifique", }, { Name: "create_post", Description: "Crée une nouvelle publication", }, { Name: "delete_post", Description: "Supprime une publication existante", }, { Name: "list_comments", Description: "Liste les commentaires d'une publication", }, { Name: "get_comment", Description: "Récupère les détails d'un commentaire spécifique", }, { Name: "create_comment", Description: "Crée un nouveau commentaire sur une publication", }, { Name: "delete_comment", Description: "Supprime un commentaire existant", }, { Name: "list_reactions", Description: "Liste les réactions d'une publication", }, { Name: "add_reaction", Description: "Ajoute une réaction à une publication", }, { Name: "delete_reaction", Description: "Supprime une réaction existante", }, { Name: "search_posts", Description: "Recherche des publications selon des critères spécifiques", }, } }

// Execute implements the providers.Service interface. // Placeholder implementation. func (s *PostService) Execute(ctx context.Context, params map[string]interface{}) (result interface{}, err error) { return nil, fmt.Errorf("Execute method not implemented for PostService") }

// ListPosts liste les publications avec pagination func (s PostService) ListPosts(ctx context.Context, options models.QueryOptions) (models.PostList, error) { endpoint := "/posts"

// Créer les paramètres de requête pour pagination
queryParams := url.Values{}
if options.Limit > 0 {
    queryParams.Set("limit", strconv.Itoa(options.Limit))
}
if options.Cursor != "" {
    queryParams.Set("cursor", options.Cursor)
}

if len(queryParams) > 0 {
    endpoint += "?" + queryParams.Encode()
}

var postList models.PostList
if err := s.client.Request(ctx, http.MethodGet, endpoint, nil, &postList); err != nil {
    return nil, fmt.Errorf("error listing posts: %w", err)
}

return &postList, nil

}

// GetPost récupère les détails d'une publication spécifique func (s PostService) GetPost(ctx context.Context, postID string) (models.Post, error) { if postID == "" { return nil, fmt.Errorf("post ID is required") }

endpoint := fmt.Sprintf("/posts/%s", postID)

var post models.Post
if err := s.client.Request(ctx, http.MethodGet, endpoint, nil, &post); err != nil {
    return nil, fmt.Errorf("error getting post: %w", err)
}

return &post, nil

}

// CreatePost crée une nouvelle publication func (s PostService) CreatePost(ctx context.Context, postDraft models.PostDraft) (*models.PostCreated, error) { if postDraft == nil { return nil, fmt.Errorf("post draft is required") }

endpoint := "/posts"

var postCreated models.PostCreated
if err := s.client.Request(ctx, http.MethodPost, endpoint, postDraft, &postCreated); err != nil {
    return nil, fmt.Errorf("error creating post: %w", err)
}

return &postCreated, nil

}

// DeletePost supprime une publication existante func (s PostService) DeletePost(ctx context.Context, postID string) (models.PostDeleted, error) { if postID == "" { return nil, fmt.Errorf("post ID is required") }

endpoint := fmt.Sprintf("/posts/%s", postID)

var postDeleted models.PostDeleted
if err := s.client.Request(ctx, http.MethodDelete, endpoint, nil, &postDeleted); err != nil {
    return nil, fmt.Errorf("error deleting post: %w", err)
}

return &postDeleted, nil

}

// ListComments liste les commentaires d'une publication avec pagination func (s PostService) ListComments(ctx context.Context, postID string, options models.QueryOptions) (models.PostCommentList, error) { if postID == "" { return nil, fmt.Errorf("post ID is required") }

endpoint := fmt.Sprintf("/posts/%s/comments", postID)

// Créer les paramètres de requête pour pagination
queryParams := url.Values{}
if options.Limit > 0 {
    queryParams.Set("limit", strconv.Itoa(options.Limit))
}
if options.Cursor != "" {
    queryParams.Set("cursor", options.Cursor)
}

if len(queryParams) > 0 {
    endpoint += "?" + queryParams.Encode()
}

var commentList models.PostCommentList
if err := s.client.Request(ctx, http.MethodGet, endpoint, nil, &commentList); err != nil {
    return nil, fmt.Errorf("error listing comments: %w", err)
}

return &commentList, nil

}

// GetComment récupère les détails d'un commentaire spécifique func (s PostService) GetComment(ctx context.Context, commentID string) (models.PostComment, error) { if commentID == "" { return nil, fmt.Errorf("comment ID is required") }

endpoint := fmt.Sprintf("/comments/%s", commentID)

var comment models.PostComment
if err := s.client.Request(ctx, http.MethodGet, endpoint, nil, &comment); err != nil {
    return nil, fmt.Errorf("error getting comment: %w", err)
}

return &comment, nil

}

// CreateComment crée un nouveau commentaire sur une publication func (s PostService) CreateComment(ctx context.Context, postID string, commentDraft models.PostCommentDraft) (*models.PostCommentCreated, error) { if postID == "" { return nil, fmt.Errorf("post ID is required") } if commentDraft == nil { return nil, fmt.Errorf("comment draft is required") }

endpoint := fmt.Sprintf("/posts/%s/comments", postID)

var commentCreated models.PostCommentCreated
if err := s.client.Request(ctx, http.MethodPost, endpoint, commentDraft, &commentCreated); err != nil {
    return nil, fmt.Errorf("error creating comment: %w", err)
}

return &commentCreated, nil

}

// DeleteComment supprime un commentaire existant func (s PostService) DeleteComment(ctx context.Context, commentID string) (models.PostCommentDeleted, error) { if commentID == "" { return nil, fmt.Errorf("comment ID is required") }

endpoint := fmt.Sprintf("/comments/%s", commentID)

var commentDeleted models.PostCommentDeleted
if err := s.client.Request(ctx, http.MethodDelete, endpoint, nil, &commentDeleted); err != nil {
    return nil, fmt.Errorf("error deleting comment: %w", err)
}

return &commentDeleted, nil

}

// ListReactions liste les réactions d'une publication avec pagination func (s PostService) ListReactions(ctx context.Context, postID string, options models.QueryOptions) (models.PostReactionList, error) { if postID == "" { return nil, fmt.Errorf("post ID is required") }

endpoint := fmt.Sprintf("/posts/%s/reactions", postID)

// Créer les paramètres de requête pour pagination
queryParams := url.Values{}
if options.Limit > 0 {
    queryParams.Set("limit", strconv.Itoa(options.Limit))
}
if options.Cursor != "" {
    queryParams.Set("cursor", options.Cursor)
}

if len(queryParams) > 0 {
    endpoint += "?" + queryParams.Encode()
}

var reactionList models.PostReactionList
if err := s.client.Request(ctx, http.MethodGet, endpoint, nil, &reactionList); err != nil {
    return nil, fmt.Errorf("error listing reactions: %w", err)
}

return &reactionList, nil

}

// AddReaction ajoute une réaction à une publication func (s PostService) AddReaction(ctx context.Context, postID string, reactionType string) (models.PostReactionCreated, error) { if postID == "" { return nil, fmt.Errorf("post ID is required") } if reactionType == "" { return nil, fmt.Errorf("reaction type is required") }

endpoint := fmt.Sprintf("/posts/%s/reactions", postID)

payload := map[string]string{
    "type": reactionType,
}

var reactionCreated models.PostReactionCreated
if err := s.client.Request(ctx, http.MethodPost, endpoint, payload, &reactionCreated); err != nil {
    return nil, fmt.Errorf("error adding reaction: %w", err)
}

return &reactionCreated, nil

}

// DeleteReaction supprime une réaction existante func (s PostService) DeleteReaction(ctx context.Context, reactionID string) (models.PostReactionDeleted, error) { if reactionID == "" { return nil, fmt.Errorf("reaction ID is required") }

endpoint := fmt.Sprintf("/reactions/%s", reactionID)

var reactionDeleted models.PostReactionDeleted
if err := s.client.Request(ctx, http.MethodDelete, endpoint, nil, &reactionDeleted); err != nil {
    return nil, fmt.Errorf("error deleting reaction: %w", err)
}

return &reactionDeleted, nil

}

// SearchPosts recherche des publications basé sur des critères spécifiques func (s PostService) SearchPosts(ctx context.Context, searchRequest models.PostSearchRequest) (*models.PostList, error) { if searchRequest == nil { return nil, fmt.Errorf("search request is required") }

endpoint := "/posts/search"

var postList models.PostList
if err := s.client.Request(ctx, http.MethodPost, endpoint, searchRequest, &postList); err != nil {
    return nil, fmt.Errorf("error searching posts: %w", err)
}

return &postList, nil

}

// --- End of services/post_service.go ---

// File: services/webhook_service.go

//pkg/providers/unipile/services/webhook_service.go

package services

import ( "context" "fmt" "net/http" "net/url" "strconv"

providers "gitlab.com/webigniter/slop-server/internal/providers"
"gitlab.com/webigniter/slop-server/pkg/providers/unipile/models"

)

// WebhookService implémente les fonctionnalités pour gérer les webhooks type WebhookService struct { client models.UnipileRequester }

// NewWebhookService crée une nouvelle instance du service de webhook func NewWebhookService(client models.UnipileRequester) *WebhookService { return &WebhookService{ client: client, } }

// GetCapabilities retourne les capacités de ce service func (s *WebhookService) GetCapabilities() []providers.ProviderCapability { return []providers.ProviderCapability{ { Name: "list_webhooks", Description: "Liste les webhooks configurés", Category: "Webhooks", Parameters: map[string]providers.Parameter{ "limit": { Type: "integer", Description: "Nombre maximum de webhooks à retourner.", Required: false, Category: "query", }, "cursor": { Type: "string", Description: "Curseur pour la pagination.", Required: false, Category: "query", }, }, Examples: []providers.ToolExample{ { Description: "Lister les 10 premiers webhooks", Parameters: map[string]interface{}{ "limit": 10, }, }, }, }, { Name: "get_webhook", Description: "Récupère les détails d'un webhook spécifique", Category: "Webhooks", Parameters: map[string]providers.Parameter{ "webhook_id": { Type: "string", Description: "L'ID du webhook à récupérer.", Required: true, Category: "path", }, }, Examples: []providers.ToolExample{ { Description: "Récupérer le webhook avec l'ID 'wh_123'", Parameters: map[string]interface{}{ "webhook_id": "wh_123", }, }, }, }, { Name: "create_webhook", Description: "Crée un nouveau webhook", Category: "Webhooks", Parameters: map[string]providers.Parameter{ "url": { Type: "string", Description: "L'URL de destination pour les événements webhook.", Required: true, Category: "body", }, "events": { Type: "array", Description: "Liste des types d'événements auxquels s'abonner.", Required: true, Category: "body", Items: &providers.ParameterItems{Type: "string"}, }, "secret": { Type: "string", Description: "Clé secrète pour la vérification de la signature.", Required: false, Category: "body", }, "status": { Type: "string", Description: "Statut initial du webhook.", Required: false, Enum: []string{"active", "inactive"}, Default: "active", Category: "body", }, "metadata": { Type: "object", Description: "Métadonnées personnalisées associées au webhook.", Required: false, Category: "body", }, }, Examples: []providers.ToolExample{ { Description: "Créer un webhook pour les événements 'message.created'", Parameters: map[string]interface{}{ "url": "https://example.com/webhook", "events": []string{"message.created"}, }, }, }, }, { Name: "update_webhook", Description: "Met à jour un webhook existant", Category: "Webhooks", Parameters: map[string]providers.Parameter{ "webhook_id": { Type: "string", Description: "L'ID du webhook à mettre à jour.", Required: true, Category: "path", }, "url": { Type: "string", Description: "La nouvelle URL de destination pour les événements webhook.", Required: false, Category: "body", }, "events": { Type: "array", Description: "La nouvelle liste des types d'événements auxquels s'abonner.", Required: false, Category: "body", Items: &providers.ParameterItems{Type: "string"}, }, "secret": { Type: "string", Description: "La nouvelle clé secrète pour la vérification de la signature.", Required: false, Category: "body", }, "status": { Type: "string", Description: "Le nouveau statut du webhook.", Required: false, Enum: []string{"active", "inactive"}, Category: "body", }, "metadata": { Type: "object", Description: "Les nouvelles métadonnées personnalisées associées au webhook.", Required: false, Category: "body", }, }, Examples: []providers.ToolExample{ { Description: "Désactiver le webhook 'wh_123'", Parameters: map[string]interface{}{ "webhook_id": "wh_123", "status": "inactive", }, }, }, }, { Name: "delete_webhook", Description: "Supprime un webhook", Category: "Webhooks", Parameters: map[string]providers.Parameter{ "webhook_id": { Type: "string", Description: "L'ID du webhook à supprimer.", Required: true, Category: "path", }, }, Examples: []providers.ToolExample{ { Description: "Supprimer le webhook 'wh_123'", Parameters: map[string]interface{}{ "webhook_id": "wh_123", }, }, }, }, { Name: "list_webhook_events", Description: "Liste les événements d'un webhook", Category: "Webhook Events", Parameters: map[string]providers.Parameter{ "webhook_id": { Type: "string", Description: "L'ID du webhook pour lequel lister les événements.", Required: true, Category: "path", }, "limit": { Type: "integer", Description: "Nombre maximum d'événements à retourner.", Required: false, Category: "query", }, "cursor": { Type: "string", Description: "Curseur pour la pagination.", Required: false, Category: "query", }, }, Examples: []providers.ToolExample{ { Description: "Lister les événements pour le webhook 'wh_123'", Parameters: map[string]interface{}{ "webhook_id": "wh_123", }, }, }, }, { Name: "get_webhook_event", Description: "Récupère les détails d'un événement webhook spécifique",

        Category: "Webhook Events",
        Parameters: map[string]providers.Parameter{
            "event_id": {
                Type:        "string",
                Description: "L'ID de l'événement webhook à récupérer.",
                Required:    true,
                Category:    "path",
            },
        },
        Examples: []providers.ToolExample{
            {
                Description: "Récupérer l'événement webhook 'evt_abc'",
                Parameters: map[string]interface{}{
                    "event_id": "evt_abc",
                },
            },
        },
    },
    {
        Name:        "list_webhook_event_deliveries",
        Description: "Liste les tentatives de livraison d'un événement webhook",

        Category: "Webhook Deliveries",
        Parameters: map[string]providers.Parameter{
            "event_id": {
                Type:        "string",
                Description: "L'ID de l'événement webhook pour lequel lister les livraisons.",
                Required:    true,
                Category:    "path",
            },
            "limit": {
                Type:        "integer",
                Description: "Nombre maximum de livraisons à retourner.",
                Required:    false,
                Category:    "query",
            },
            "cursor": {
                Type:        "string",
                Description: "Curseur pour la pagination.",
                Required:    false,
                Category:    "query",
            },
        },
        Examples: []providers.ToolExample{
            {
                Description: "Lister les livraisons pour l'événement 'evt_abc'",
                Parameters: map[string]interface{}{
                    "event_id": "evt_abc",
                },
            },
        },
    },
    {
        Name:        "resend_webhook_delivery",
        Description: "Renvoie une livraison de webhook",

        Category: "Webhook Deliveries",
        Parameters: map[string]providers.Parameter{
            "delivery_id": {
                Type:        "string",
                Description: "L'ID de la livraison webhook à renvoyer.",
                Required:    true,
                Category:    "path",
            },
        },
        Examples: []providers.ToolExample{
            {
                Description: "Renvoyer la livraison 'del_xyz'",
                Parameters: map[string]interface{}{
                    "delivery_id": "del_xyz",
                },
            },
        },
    },
}

}

// Execute implements the providers.Service interface. // For WebhookService, direct execution might not be applicable as capabilities // map to specific API calls handled by dedicated methods (e.g., CreateWebhook). func (s *WebhookService) Execute(ctx context.Context, params map[string]interface{}) (result interface{}, err error) { // Determine the capability/action requested from params, if applicable // Example: action, ok := params["action"].(string) // For now, return not implemented, as specific methods should be called directly. return nil, fmt.Errorf("Execute method not implemented for WebhookService; use specific methods like CreateWebhook") }

// ListWebhooks récupère la liste des webhooks configurés func (s WebhookService) ListWebhooks(ctx context.Context, options models.QueryOptions) (models.WebhookList, error) { queryParams := url.Values{} if options.Limit > 0 { queryParams.Set("limit", strconv.Itoa(options.Limit)) } if options.Cursor != "" { queryParams.Set("cursor", options.Cursor) }

path := "/webhooks"
if len(queryParams) > 0 {
    path += "?" + queryParams.Encode()
}

var result models.WebhookList
err := s.client.Request(ctx, http.MethodGet, path, nil, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la récupération des webhooks: %w", err)
}

return &result, nil

}

// GetWebhook récupère les détails d'un webhook spécifique func (s WebhookService) GetWebhook(ctx context.Context, webhookID string) (models.Webhook, error) { if webhookID == "" { return nil, fmt.Errorf("webhook ID est requis") }

path := fmt.Sprintf("/webhooks/%s", webhookID)

var result models.Webhook
err := s.client.Request(ctx, http.MethodGet, path, nil, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la récupération du webhook %s: %w", webhookID, err)
}

return &result, nil

}

// CreateWebhook crée un nouveau webhook func (s WebhookService) CreateWebhook(ctx context.Context, webhook models.WebhookDraft) (*models.WebhookCreated, error) { if webhook == nil { return nil, fmt.Errorf("données de webhook requises") }

if webhook.URL == "" {
    return nil, fmt.Errorf("URL du webhook est requise")
}

if len(webhook.Events) == 0 {
    return nil, fmt.Errorf("au moins un événement à surveiller est requis")
}

var result models.WebhookCreated
err := s.client.Request(ctx, http.MethodPost, "/webhooks", webhook, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la création du webhook: %w", err)
}

return &result, nil

}

// UpdateWebhook met à jour un webhook existant func (s WebhookService) UpdateWebhook(ctx context.Context, webhookID string, webhook models.WebhookUpdate) (*models.WebhookUpdated, error) { if webhookID == "" { return nil, fmt.Errorf("webhook ID est requis") }

if webhook == nil {
    return nil, fmt.Errorf("données de mise à jour requises")
}

path := fmt.Sprintf("/webhooks/%s", webhookID)

var result models.WebhookUpdated
err := s.client.Request(ctx, http.MethodPatch, path, webhook, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la mise à jour du webhook %s: %w", webhookID, err)
}

return &result, nil

}

// DeleteWebhook supprime un webhook func (s WebhookService) DeleteWebhook(ctx context.Context, webhookID string) (models.WebhookDeleted, error) { if webhookID == "" { return nil, fmt.Errorf("webhook ID est requis") }

path := fmt.Sprintf("/webhooks/%s", webhookID)

var result models.WebhookDeleted
err := s.client.Request(ctx, http.MethodDelete, path, nil, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la suppression du webhook %s: %w", webhookID, err)
}

return &result, nil

}

// ListWebhookEvents récupère la liste des événements d'un webhook func (s WebhookService) ListWebhookEvents(ctx context.Context, webhookID string, options models.QueryOptions) (models.WebhookEventList, error) { if webhookID == "" { return nil, fmt.Errorf("webhook ID est requis") }

queryParams := url.Values{}
if options.Limit > 0 {
    queryParams.Set("limit", strconv.Itoa(options.Limit))
}
if options.Cursor != "" {
    queryParams.Set("cursor", options.Cursor)
}

path := fmt.Sprintf("/webhooks/%s/events", webhookID)
if len(queryParams) > 0 {
    path += "?" + queryParams.Encode()
}

var result models.WebhookEventList
err := s.client.Request(ctx, http.MethodGet, path, nil, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la récupération des événements du webhook %s: %w", webhookID, err)
}

return &result, nil

}

// GetWebhookEvent récupère les détails d'un événement webhook spécifique func (s WebhookService) GetWebhookEvent(ctx context.Context, eventID string) (models.WebhookEvent, error) { if eventID == "" { return nil, fmt.Errorf("event ID est requis") }

path := fmt.Sprintf("/webhook-events/%s", eventID)

var result models.WebhookEvent
err := s.client.Request(ctx, http.MethodGet, path, nil, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la récupération de l'événement %s: %w", eventID, err)
}

return &result, nil

}

// ListWebhookEventDeliveries récupère la liste des tentatives de livraison d'un événement webhook func (s WebhookService) ListWebhookEventDeliveries(ctx context.Context, eventID string, options models.QueryOptions) (models.WebhookEventDeliveryList, error) { if eventID == "" { return nil, fmt.Errorf("event ID est requis") }

queryParams := url.Values{}
if options.Limit > 0 {
    queryParams.Set("limit", strconv.Itoa(options.Limit))
}
if options.Cursor != "" {
    queryParams.Set("cursor", options.Cursor)
}

path := fmt.Sprintf("/webhook-events/%s/deliveries", eventID)
if len(queryParams) > 0 {
    path += "?" + queryParams.Encode()
}

var result models.WebhookEventDeliveryList
err := s.client.Request(ctx, http.MethodGet, path, nil, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors de la récupération des livraisons de l'événement %s: %w", eventID, err)
}

return &result, nil

}

// ResendWebhookDelivery renvoie une livraison de webhook func (s WebhookService) ResendWebhookDelivery(ctx context.Context, deliveryID string) (models.WebhookResend, error) { if deliveryID == "" { return nil, fmt.Errorf("delivery ID est requis") }

path := fmt.Sprintf("/webhook-deliveries/%s/resend", deliveryID)

var result models.WebhookResend
err := s.client.Request(ctx, http.MethodPost, path, nil, &result)
if err != nil {
    return nil, fmt.Errorf("erreur lors du renvoi de la livraison %s: %w", deliveryID, err)
}

return &result, nil

}

// --- End of services/webhook_service.go ---

// File: sync_job.go

//pkg/providers/unipile/sync_job.go

package unipile

import ( "context" "errors" "fmt" "log" "time"

"gitlab.com/webigniter/slop-server/internal/core"
"gitlab.com/webigniter/slop-server/internal/jobmanager"
"gitlab.com/webigniter/slop-server/internal/providers"
"gitlab.com/webigniter/slop-server/pkg/providers/unipile/models"
"gitlab.com/webigniter/slop-server/pkg/providers/unipile/services"

)

// UnipileAccountSyncProcessID is the unique identifier for this job type. const UnipileAccountSyncProcessID = "unipile-account-sync"

// UnipileAccountSyncProcessor handles the job of syncing all user accounts. type UnipileAccountSyncProcessor struct { adapter UnipileAdapter // Need access to SyncUserAccounts coreStore core.PgStore // Need access to fetch all user IDs }

// NewUnipileAccountSyncProcessor creates a new sync processor instance. func NewUnipileAccountSyncProcessor(adapter UnipileAdapter, coreStore core.PgStore) jobmanager.ExecutableProcess { if adapter == nil { log.Fatalf("CRITICAL: UnipileAdapter is nil for NewUnipileAccountSyncProcessor") } if coreStore == nil { log.Fatalf("CRITICAL: CoreStore is nil for NewUnipileAccountSyncProcessor") } return &UnipileAccountSyncProcessor{ adapter: adapter, coreStore: coreStore, } }

// GetCapabilities describes the capabilities (none specific for this background job). func (p *UnipileAccountSyncProcessor) GetCapabilities() []providers.ProviderCapability { // This processor doesn't expose direct capabilities like tools or events. return []providers.ProviderCapability{} }

// Execute performs the synchronization based on accounts fetched from the Unipile API. func (p *UnipileAccountSyncProcessor) Execute(ctx context.Context, msg jobmanager.Message) (jobmanager.ExecuteResult, error) { jobID := msg.Metadata["job_id"].(string) // Assuming job_id is in metadata log.Printf("Starting %s Job ID: %s", UnipileAccountSyncProcessID, jobID)

// Check dependencies are available
if p.adapter == nil || p.adapter.client == nil || p.adapter.store == nil || p.coreStore == nil {
    errMsg := "missing dependencies for sync job (adapter, client, store, or coreStore)"
    log.Printf("Job %s: ERROR - %s", jobID, errMsg)
    return jobmanager.ExecuteResult{}, errors.New(errMsg)
}

// --- 1. Fetch ALL accounts from Unipile API ---
log.Printf("Job %s: Fetching all accounts from Unipile API...", jobID)
accService := services.NewAccountService(p.adapter.client)
allApiAccounts := make([]models.Account, 0)
apiAccountIDs := make(map[string]bool) // Set to track IDs seen from API
var nextCursor string
apiFetchErrors := 0

for {
    options := models.QueryOptions{
        Limit:  100, // Fetch in batches
        Cursor: nextCursor,
    }
    apiCtx, cancel := context.WithTimeout(ctx, 30*time.Second) // Timeout for API call
    accountList, err := accService.ListAccounts(apiCtx, options)
    cancel()

    if err != nil {
        log.Printf("Job %s: ERROR fetching accounts from Unipile API (cursor: '%s'): %v", jobID, nextCursor, err)
        apiFetchErrors++
        // Decide whether to bail out completely or try to process what we have?
        // Let's bail for now if fetching fails.
        return jobmanager.ExecuteResult{}, fmt.Errorf("failed to fetch accounts from Unipile API: %w", err)
    }

    if accountList != nil && len(accountList.Accounts) > 0 {
        allApiAccounts = append(allApiAccounts, accountList.Accounts...)
        for _, acc := range accountList.Accounts {
            apiAccountIDs[acc.ID] = true // Track ID
        }
        log.Printf("Job %s: Fetched %d accounts from API (total so far: %d)", jobID, len(accountList.Accounts), len(allApiAccounts))
    } else {
        log.Printf("Job %s: No accounts returned from API for cursor '%s'.", jobID, nextCursor)
    }

    // Check pagination
    if accountList != nil && accountList.Paging.Cursor != "" {
        nextCursor = accountList.Paging.Cursor
        log.Printf("Job %s: Moving to next page with cursor: %s", jobID, nextCursor)
    } else {
        break // No next page cursor or accountList was nil
    }
}
log.Printf("Job %s: Finished fetching from Unipile API. Total accounts retrieved: %d", jobID, len(allApiAccounts))

// --- 2. Process Each Account from API ---
log.Printf("Job %s: Processing %d accounts retrieved from API...", jobID, len(allApiAccounts))
processedCount := 0
mappedCount := 0
updateErrors := 0

for _, apiAccount := range allApiAccounts {
    processedCount++
    accountID := apiAccount.ID
    email := apiAccount.Email // Use email as the primary lookup key

    if email == "" {
        log.Printf("Job %s: WARN - Skipping account %s: Missing email address for framework user lookup.", jobID, accountID)
        continue
    }

    // --- 3. Attempt Framework User Lookup ---
    // TODO: Implement coreStore.GetUserByEmail(ctx, email) string
    // It should return the framework userID if found, or an empty string/error if not.
    // frameworkUser, err := p.coreStore.GetUserByEmail(ctx, email)
    frameworkUserID := ""                                                               // Placeholder
    var userLookupErr error = errors.New("GetUserByEmail not implemented")              // Placeholder error
    log.Printf("Job %s: TODO - Implement lookup for user with email: %s", jobID, email) // Log placeholder action

    if userLookupErr != nil {
        // Handle lookup errors (e.g., DB connection issue)
        log.Printf("Job %s: ERROR looking up framework user for email %s (Account: %s): %v", jobID, email, accountID, userLookupErr)
        updateErrors++
        continue // Skip this account if user lookup fails
    }

    if frameworkUserID == "" {
        // No matching framework user found for this email
        log.Printf("Job %s: INFO - Unipile account %s (Email: %s) does not match any known framework user. Skipping mapping.", jobID, accountID, email)
        continue
    }

    // --- 4. Store/Update Mapping in Local DB ---
    log.Printf("Job %s: Found framework user %s for Unipile account %s (Email: %s). Storing/updating mapping...", jobID, frameworkUserID, accountID, email)
    mappedCount++

    // Get required fields from apiAccount
    provider := apiAccount.Provider
    status := apiAccount.Status
    if status == "" {
        status = "active"
    } // Default status
    metadata := apiAccount.Metadata // Already map[string]interface{}

    if err := p.adapter.store.StoreAccount(ctx, frameworkUserID, accountID, provider, status, metadata); err != nil {
        log.Printf("Job %s: ERROR storing/updating account mapping for AccountID %s, UserID %s: %v", jobID, accountID, frameworkUserID, err)
        updateErrors++
    }
}

// --- 5. Detect and Mark Deletions ---
log.Printf("Job %s: Checking for accounts deleted from Unipile...", jobID)
localAccountIDs, err := p.adapter.store.GetAllAccountIDs(ctx)
if err != nil {
    log.Printf("Job %s: ERROR fetching local account IDs for deletion check: %v", jobID, err)
    // Proceed without deletion check if this fails? Or report error?
    updateErrors++ // Count this as an error
} else {
    deletedCount := 0
    for _, localID := range localAccountIDs {
        if !apiAccountIDs[localID] { // Check if local ID was seen in API results
            log.Printf("Job %s: Local account %s not found in API results. Marking as deleted_externally.", jobID, localID)

            // Check current status first to avoid unnecessary updates
            localAccountInfo, getErr := p.adapter.store.GetAccount(ctx, localID)
            if getErr != nil {
                log.Printf("Job %s: ERROR fetching local account %s status before marking deleted: %v", jobID, localID, getErr)
                updateErrors++
                continue
            }

            if localAccountInfo.Status != "deleted_externally" {
                if err := p.adapter.store.UpdateAccountStatus(ctx, localID, "deleted_externally"); err != nil {
                    log.Printf("Job %s: ERROR marking local account %s as deleted_externally: %v", jobID, localID, err)
                    updateErrors++
                } else {
                    deletedCount++
                }
            } else {
                log.Printf("Job %s: Local account %s already marked as deleted_externally.", jobID, localID)
            }
        }
    }
    log.Printf("Job %s: Finished deletion check. Marked %d accounts as deleted_externally.", jobID, deletedCount)
}

// --- 6. Report Results ---
log.Printf("Job %s: Finished Unipile sync. API Accounts Processed: %d, Mapped/Updated: %d, Update/Check Errors: %d",
    jobID, processedCount, mappedCount, updateErrors)

resultOutputs := map[string]any{
    "status":                  "completed",
    "api_accounts_processed":  processedCount,
    "accounts_mapped_updated": mappedCount,
    "sync_errors":             updateErrors,
    "api_fetch_errors":        apiFetchErrors, // Report API fetch errors separately if needed
}

if updateErrors > 0 || apiFetchErrors > 0 {
    return jobmanager.ExecuteResult{Output: jobmanager.ExecuteResultMessage{Outputs: resultOutputs}},
        fmt.Errorf("sync job completed with %d processing errors and %d API fetch errors", updateErrors, apiFetchErrors)
}

return jobmanager.ExecuteResult{
    Output: jobmanager.ExecuteResultMessage{
        Outputs: resultOutputs,
    },
}, nil

}

// --- End of sync_job.go ---

// File: webhook_registration.go

//pkg/providers/unipile/webhook_registration.go

package unipile

import ( "context" "errors" "fmt" "log"

"gitlab.com/webigniter/slop-server/pkg/providers/unipile/models"
"gitlab.com/webigniter/slop-server/pkg/providers/unipile/services"

)

// RegisterWebhooks registers our webhook endpoints with Unipile func (a *UnipileAdapter) RegisterWebhooks(ctx context.Context) error { // Get base URL for webhooks from config baseURL, ok := a.config["webhook_base_url"].(string) if !ok || baseURL == "" { return errors.New("webhook_base_url not configured") }

// Get webhook service
service, found := a.client.Registry.Get("webhook")
if !found {
    return errors.New("webhook service not found in registry")
}
webhookService, ok := service.(*services.WebhookService)
if !ok {
    return errors.New("retrieved service is not a WebhookService")
}

// Register email webhook
emailWebhook := &models.WebhookDraft{
    URL:         fmt.Sprintf("%s/webhook/unipile/emails", baseURL),
    Description: "Email notifications webhook",
    Events:      []string{"email.*"},
}
_, err := webhookService.CreateWebhook(ctx, emailWebhook)
if err != nil {
    return fmt.Errorf("failed to register email webhook: %w", err)
}

// Register message webhook
messageWebhook := &models.WebhookDraft{
    URL:         fmt.Sprintf("%s/webhook/unipile/messages", baseURL),
    Description: "Message notifications webhook",
    Events:      []string{"message.*"},
}
_, err = webhookService.CreateWebhook(ctx, messageWebhook)
if err != nil {
    return fmt.Errorf("failed to register message webhook: %w", err)
}

// Register account status webhook
accountStatusWebhook := &models.WebhookDraft{
    URL:         fmt.Sprintf("%s/webhook/unipile/account-status", baseURL),
    Description: "Account status webhook",
    Events:      []string{"account.*"},
}
_, err = webhookService.CreateWebhook(ctx, accountStatusWebhook)
if err != nil {
    return fmt.Errorf("failed to register account status webhook: %w", err)
}

log.Printf("Successfully registered all Unipile webhooks")
return nil

}

// --- End of webhook_registration.go ---