From be38f711b7fb88d6db6838db7a47f3188dae697d Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 20 Jun 2025 23:16:32 +0200 Subject: [PATCH] Refactored the code --- immich-souvenirs/immich-souvenirs.go | 648 +++++------------- immich-souvenirs/internals/immich/immich.go | 169 +++++ .../internals/whatsapp/whatsapp.go | 84 +++ immich-souvenirs/test.sh | 2 +- 4 files changed, 437 insertions(+), 466 deletions(-) create mode 100644 immich-souvenirs/internals/immich/immich.go create mode 100644 immich-souvenirs/internals/whatsapp/whatsapp.go diff --git a/immich-souvenirs/immich-souvenirs.go b/immich-souvenirs/immich-souvenirs.go index 4dc0a20..a7743f2 100644 --- a/immich-souvenirs/immich-souvenirs.go +++ b/immich-souvenirs/immich-souvenirs.go @@ -1,11 +1,7 @@ package main import ( - "bytes" - "context" - "encoding/json" "fmt" - "io/ioutil" "math" "net/http" "os" @@ -13,507 +9,229 @@ import ( "strconv" "strings" "time" - "github.com/mdp/qrterminal/v3" - _ "github.com/mattn/go-sqlite3" - "google.golang.org/protobuf/proto" - "go.mau.fi/whatsmeow" - "go.mau.fi/whatsmeow/types" - "go.mau.fi/whatsmeow/store/sqlstore" - waProto "go.mau.fi/whatsmeow/binary/proto" - waLog "go.mau.fi/whatsmeow/util/log" + + "github.com/napnap75/multiarch-docker-files/immich-souvenirs/internals/immich" + "github.com/napnap75/multiarch-docker-files/immich-souvenirs/internals/whatsapp" ) type Parameters struct { - ImmichURL string - ImmichKey string - WhatsappSessionFile string - WhatsappGroup string - TimeToRun string - DevelopmentMode string - HealthchecksURL string + ImmichURL string + ImmichKey string + WhatsappSessionFile string + WhatsappGroup string + TimeToRun string + DevelopmentMode string + HealthchecksURL string } -type Album struct { - ID string `json:"id"` - Name string `json:"albumName"` - Description string `json:"description"` - Shared bool `json:"shared"` - HasSharedLink bool `json:"hasSharedLink"` - StartDate time.Time `json:"startDate"` - CreatedAt time.Time `json:"createdAt"` - AlbumThumbnailAssetId string `json:"albumThumbnailAssetId"` +func parseTime(timeStr string) (int, int, error) { + parts := strings.Split(timeStr, ":") + if len(parts) != 2 { + return 0, 0, fmt.Errorf("invalid format: %s", timeStr) + } + hours, err := strconv.Atoi(parts[0]) + if err != nil { + return 0, 0, err + } + minutes, err := strconv.Atoi(parts[1]) + if err != nil { + return 0, 0, err + } + return hours, minutes, nil } -type Key struct { - ID string `json:"id"` - Key string `json:"key"` - Album *Album `json:"album"` -} - -func connect(param Parameters) (*whatsmeow.Client, error) { - dbLog := waLog.Stdout("Database", "ERROR", true) - container, err := sqlstore.New(context.Background(), "sqlite3", "file:" + param.WhatsappSessionFile + "?_foreign_keys=on", dbLog) - if err != nil { - return nil, err - } - deviceStore, err := container.GetFirstDevice(context.Background()) - if err != nil { - return nil, err - } - clientLog := waLog.Stdout("Client", "ERROR", true) - client := whatsmeow.NewClient(deviceStore, clientLog) - - if client.Store.ID == nil { - // No ID stored, new login - qrChan, _ := client.GetQRChannel(context.Background()) - err = client.Connect() - if err != nil { - return nil, err - } - for evt := range qrChan { - if evt.Event == "code" { - qrterminal.GenerateHalfBlock(evt.Code, qrterminal.L, os.Stdout) - fmt.Println("QR code:", evt.Code) - } else { - fmt.Println("Login event:", evt.Event) - } - } - } else { - // Already logged in, just connect - err = client.Connect() - if err != nil { - return nil, err - } - } - - return client, nil -} - -func sendMessage(client *whatsmeow.Client, group string, title string, description string, message string, url string, thumbnail []byte) error { - jid, err := types.ParseJID(group) - if err != nil { - return fmt.Errorf("Incorrect group identifier '%s': %v", group, err) - } - - msg := &waProto.Message{ - ExtendedTextMessage: &waProto.ExtendedTextMessage{ - Title: proto.String(title), - Description: proto.String(description), - Text: proto.String(message), - MatchedText: proto.String(url), - JPEGThumbnail: thumbnail, - }, - } - ts, err := client.SendMessage(context.Background(), jid, msg) - if err != nil { - return fmt.Errorf("Error sending message with title '%s': %v", title, err) - } - - fmt.Fprintf(os.Stdout, "Message with title '%s' sent (timestamp: %s)\n", title, ts) - return nil -} - -func testConnexions(param Parameters) error { - // Create new WhatsApp connection and connect - client, err := connect(param) - if err != nil { - return fmt.Errorf("Error connecting to WhatsApp: %v", err) - } - <-time.After(3 * time.Second) - defer client.Disconnect() - - // Prints the available groups if none provided - if param.WhatsappGroup == "" { - fmt.Fprintf(os.Stdout, "No WhatsApp group provided, showing all available groups\n") - groups, err := client.GetJoinedGroups() - if err != nil { - return fmt.Errorf("Error getting groups list: %v", err) - } - for _, groupInfo := range groups { - fmt.Fprintf(os.Stdout, "%s | %s\n", groupInfo.JID, groupInfo.GroupName) - } - - return fmt.Errorf("No WhatsApp group provided") - } else { - jid, err := types.ParseJID(param.WhatsappGroup) - if err != nil { - return fmt.Errorf("Incorrect group identifier '%s': %v", param.WhatsappGroup, err) - } - _, err = client.GetGroupInfo(jid) - if err != nil { - return fmt.Errorf("Unknown WhatsApp group %s", param.WhatsappGroup) - } - } - - // Connects to Immich and load albums - spaceClient := http.Client{ - Timeout: time.Second * 10, - } - req, err := http.NewRequest(http.MethodGet, param.ImmichURL + "/api/albums", nil) - if err != nil { - return fmt.Errorf("Error connecting to Immich with URL '%s': %v", param.ImmichURL, err) - } - req.Header.Set("x-api-key", param.ImmichKey) - req.Header.Set("Accept", "application/json") - - res, err := spaceClient.Do(req) - if err != nil { - return fmt.Errorf("Error connecting to Immich with URL '%s': %v", param.ImmichURL, err) - } - if res.Body != nil { - defer res.Body.Close() - } - if res.StatusCode != 200 { - return fmt.Errorf("Error connecting to Immich with URL '%s': Status code %d", param.ImmichURL, res.StatusCode) - } - body, err := ioutil.ReadAll(res.Body) - if err != nil { - return fmt.Errorf("Error connecting to Immich with URL '%s': %v", param.ImmichURL, err) - } - var albums []Album - err = json.Unmarshal([]byte(body), &albums) - if err != nil { - return fmt.Errorf("Error connecting to Immich with URL '%s': %v", param.ImmichURL, err) - } - - return nil -} - -func getSharingKey(album Album, param Parameters) (string, error) { - spaceClient := http.Client{ - Timeout: time.Second * 10, - } - if (album.HasSharedLink) { - // Retrieve the existing key - req, err := http.NewRequest(http.MethodGet, param.ImmichURL + "/api/shared-links", nil) - if err != nil { - return "", fmt.Errorf("Error retrieving sharing key for album '%s': %v", album.Name, err) - } - req.Header.Set("x-api-key", param.ImmichKey) - req.Header.Set("Accept", "application/json") - res, err := spaceClient.Do(req) - if err != nil { - return "", fmt.Errorf("Error retrieving sharing key for album '%s': %v", album.Name, err) - } - if res.Body != nil { - defer res.Body.Close() - } - if res.StatusCode != 200 { - return "", fmt.Errorf("Error retrieving sharing key for album '%s': Status code %d", album.Name, res.StatusCode) - } - body, err := ioutil.ReadAll(res.Body) - if err != nil { - return "", fmt.Errorf("Error retrieving sharing key for album '%s': %v", album.Name, err) - } - var keys []Key - err = json.Unmarshal([]byte(body), &keys) - if err != nil { - return "", fmt.Errorf("Error retrieving sharing key for album '%s': %v", album.Name, err) - } - for _, key := range keys { - if (key.Album != nil && album.ID == key.Album.ID) { - return key.Key, nil - } - } - - return "", fmt.Errorf("Error retrieving sharing key for album '%s': no key found for this albume", album.Name) - } else { - // Create the missing key - var jsonData = []byte(`{ - "albumId": "` + album.ID + `", - "allowDownload": true, - "allowUpload": false, - "showMetadata": true, - "type": "ALBUM" - }`) - req, err := http.NewRequest(http.MethodPost, param.ImmichURL + "/api/shared-links", bytes.NewBuffer(jsonData)) - if err != nil { - return "", fmt.Errorf("Error creating missing key for album '%s': %v", album.Name, err) - } - req.Header.Set("x-api-key", param.ImmichKey) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Accept", "application/octet-stream") - res, err := spaceClient.Do(req) - if err != nil { - return "", fmt.Errorf("Error creating missing key for album '%s': %v", album.Name, err) - } - if res.Body != nil { - defer res.Body.Close() - } - if res.StatusCode != 201 { - return "", fmt.Errorf("Error creating missing key for album '%s': Status code %d", album.Name, res.StatusCode) - } - body, err := ioutil.ReadAll(res.Body) - if err != nil { - return "", fmt.Errorf("Error creating missing key for album '%s': %v", album.Name, err) - } - var key Key - err = json.Unmarshal([]byte(body), &key) - if err != nil { - return "", fmt.Errorf("Error creating missing key for album '%s': %v", album.Name, err) - } - return key.Key, nil - } -} - -func getThumbnail(album Album, param Parameters) ([]byte, error) { - spaceClient := http.Client{ - Timeout: time.Second * 10, - } - - req, err := http.NewRequest(http.MethodGet, param.ImmichURL + "/api/assets/" + album.AlbumThumbnailAssetId + "/thumbnail?size=preview", nil) - if err != nil { - return nil, fmt.Errorf("Error retrieving thumbnail for album '%s': %v", album.Name, err) - } - req.Header.Set("x-api-key", param.ImmichKey) - req.Header.Set("Accept", "application/octet-stream") - res, err := spaceClient.Do(req) - if err != nil { - return nil, fmt.Errorf("Error retrieving thumbnail for album '%s': %v", album.Name, err) - } - if res.Body != nil { - defer res.Body.Close() - } - if res.StatusCode != 200 { - return nil, fmt.Errorf("Error retrieving thumbnail for album '%s': Status code %d", album.Name, res.StatusCode) - } - thumbnail, err := ioutil.ReadAll(res.Body) - if err != nil { - return nil, fmt.Errorf("Error retrieving thumbnail for album '%s': %v", album.Name, err) - } - - return thumbnail, nil -} - -func eventHandler(evt interface{}) { - fmt.Println("Received a message!", evt) -} - -func listen(param Parameters) error { - // Create new WhatsApp connection and connect - client, err := connect(param) - if err != nil { - return fmt.Errorf("Error creating connection to WhatsApp: %v", err) - } - <-time.After(3 * time.Second) - defer client.Disconnect() - - client.AddEventHandler(eventHandler) - +func main() { + // Capture interrupt signal for graceful shutdown c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt) - <-c + go func() { + <-c + fmt.Println("Got interrupt signal. Exiting...") + os.Exit(0) + }() + // Load parameters from environment variables + param := &Parameters{ + ImmichURL: os.Getenv("IMMICH-URL"), + ImmichKey: os.Getenv("IMMICH-KEY"), + WhatsappSessionFile: os.Getenv("WHATSAPP-SESSION-FILE"), + WhatsappGroup: os.Getenv("WHATSAPP-GROUP"), + TimeToRun: os.Getenv("TIME-TO-RUN"), + DevelopmentMode: os.Getenv("DEVELOPMENT-MODE"), + HealthchecksURL: os.Getenv("HEALTHCHECKS-URL"), + } + + // Initialize WhatsApp connection + wac, err := whatsapp.New(param.WhatsappSessionFile) + if err != nil { + fmt.Fprintf(os.Stderr, "Error initializing WhatsApp: %v\n", err) + return + } + + // Initialize Immich client + immichClient := immich.New(param.ImmichURL, param.ImmichKey) + + switch param.DevelopmentMode { + case "run-once", "run-last": + if err := runLoop(wac, immichClient, param); err != nil { + fmt.Fprintf(os.Stderr, "Error in runLoop: %v\n", err) + } + case "listen": + if err := listen(wac); err != nil { + fmt.Fprintf(os.Stderr, "Error in listen: %v\n", err) + } + default: + // Test connection on startup + if err := testConnections(wac, immichClient, param); err != nil { + fmt.Fprintf(os.Stderr, "Connection test failed: %v\n", err) + return + } + // Main scheduled loop + for { + hours, minutes, err := parseTime(param.TimeToRun) + if err != nil { + fmt.Fprintf(os.Stderr, "Invalid time format: %v\n", err) + return + } + now := time.Now() + target := time.Date(now.Year(), now.Month(), now.Day(), hours, minutes, 0, 0, now.Location()) + if target.Before(now) { + target = target.Add(24 * time.Hour) + } + sleepDuration := target.Sub(now) + fmt.Printf("Sleeping for: %v\n", sleepDuration) + time.Sleep(sleepDuration) + + // Retry logic + for i := 0; i < 3; i++ { + err := runLoop(wac, immichClient, param) + if err == nil { + break + } + fmt.Fprintf(os.Stderr, "Attempt %d failed: %v\n", i+1, err) + time.Sleep(time.Duration(math.Pow(2, float64(i))) * 30 * time.Second) + } + if err != nil { + fmt.Fprintf(os.Stderr, "Error after retries: %v\n", err) + } else if param.HealthchecksURL != "" { + client := &http.Client{Timeout: 10 * time.Second} + _, err := client.Head(param.HealthchecksURL) + if err != nil { + fmt.Fprintf(os.Stderr, "Healthcheck error: %v\n", err) + } + } + } + } +} + +// Fonction pour tester la connexion à WhatsApp et Immich +func testConnections(wac *whatsapp.WhatsAppClient, immichClient *immich.ImmichClient, param *Parameters) error { + fmt.Println("Testing connections...") + // Test WhatsApp + if err := wac.Client.Connect(); err != nil { + return fmt.Errorf("WhatsApp connection failed: %v", err) + } + defer wac.Client.Disconnect() + fmt.Println("WhatsApp connected.") + + // Test Immich + albums, err := immichClient.FetchAlbums() + if err != nil { + return fmt.Errorf("Immich connection failed: %v", err) + } + fmt.Printf("Fetched %d albums from Immich.\n", len(albums)) return nil } +// Fonction principale pour exécuter la logique +func runLoop(wac *whatsapp.WhatsAppClient, immichClient *immich.ImmichClient, param *Parameters) error { + // Connecter WhatsApp si nécessaire + if !wac.Client.IsConnected() { + if err := wac.Client.Connect(); err != nil { + return err + } + } + defer wac.Client.Disconnect() -func runLoop(param Parameters) error { - // Create new WhatsApp connection and connect - client, err := connect(param) + // Charger albums depuis Immich + albums, err := immichClient.FetchAlbums() if err != nil { - return fmt.Errorf("Error creating connection to WhatsApp: %v", err) - } - <-time.After(3 * time.Second) - defer client.Disconnect() - - - // Connects to Immich and load albums - spaceClient := http.Client{ - Timeout: time.Second * 10, - } - req, err := http.NewRequest(http.MethodGet, param.ImmichURL + "/api/albums", nil) - if err != nil { - return fmt.Errorf("Error connecting to Immich with URL '%s': %v", param.ImmichURL, err) - } - req.Header.Set("x-api-key", param.ImmichKey) - req.Header.Set("Accept", "application/json") - - res, err := spaceClient.Do(req) - if err != nil { - return fmt.Errorf("Error connecting to Immich with URL '%s': %v", param.ImmichURL, err) - } - if res.Body != nil { - defer res.Body.Close() - } - if res.StatusCode != 200 { - return fmt.Errorf("Error connecting to Immich with URL '%s': Status code %d", param.ImmichURL, res.StatusCode) - } - body, err := ioutil.ReadAll(res.Body) - if err != nil { - return fmt.Errorf("Error connecting to Immich with URL '%s': %v", param.ImmichURL, err) - } - var albums []Album - err = json.Unmarshal([]byte(body), &albums) - if err != nil { - return fmt.Errorf("Error connecting to Immich with URL '%s': %v", param.ImmichURL, err) + return err } for _, album := range albums { if album.Shared { - // Get albums from x years ago - if param.DevelopmentMode == "run-last" || (album.StartDate.Month() == time.Now().Month()) && (album.StartDate.Day() == time.Now().Day()) { - // Retrieve the sharing key - sharingKey, err := getSharingKey(album, param) + // Récupérer les albums anniversaire + if param.DevelopmentMode == "run-last" || (album.StartDate.Month() == time.Now().Month() && album.StartDate.Day() == time.Now().Day()) { + // Obtenir la clé de partage + sharingKey, err := immichClient.GetSharingKey(album) if err != nil { - return fmt.Errorf("Error retrieving the sharing key for album '%s': %v", album.Name, err) - } - - // Retrieve the thumbnail - thumbnail, err := getThumbnail(album, param) - if err != nil { - return fmt.Errorf("Error retrieving thumbnail for album '%s': %v", album.Name, err) - } - - // Send the message - link := param.ImmichURL + "/share/" + sharingKey - err = sendMessage(client, param.WhatsappGroup, album.Name, album.Description, fmt.Sprintf("Il y a %d an(s) : %s", time.Now().Year()-album.StartDate.Year(), link), link, thumbnail) - if err != nil { - fmt.Fprintf(os.Stderr, "Error sending message to WhatsApp for album '%s': %v\n", album.Name, err) + fmt.Fprintf(os.Stderr, "Erreur récupération clé partage: %v\n", err) continue } + link := param.ImmichURL + "/share/" + sharingKey + + // Récupérer la miniature + thumbnail, err := immichClient.GetThumbnail(album.AlbumThumbnailAssetId) + if err != nil { + fmt.Fprintf(os.Stderr, "Erreur miniature: %v\n", err) + continue + } + + // Envoyer le message + err = wac.SendMessage(param.WhatsappGroup, album.Name, album.Description, fmt.Sprintf("Il y a %d an(s) : %s", time.Now().Year()-album.StartDate.Year(), link), link, thumbnail) + if err != nil { + fmt.Fprintf(os.Stderr, "Erreur envoi WhatsApp: %v\n", err) + } } if param.DevelopmentMode == "run-last" { return nil } - // Get albums created yesterday + // Récupérer les albums de la veille if album.CreatedAt.Year() == time.Now().AddDate(0, 0, -1).Year() && (album.CreatedAt.Month() == time.Now().AddDate(0, 0, -1).Month()) && (album.CreatedAt.Day() == time.Now().AddDate(0, 0, -1).Day()) { - // Retrieve the sharing key - sharingKey, err := getSharingKey(album, param) + // Obtenir la clé de partage + sharingKey, err := immichClient.GetSharingKey(album) if err != nil { - return fmt.Errorf("Error retrieving the sharing key for album '%s': %v", album.Name, err) - } - - // Retrieve the thumbnail - thumbnail, err := getThumbnail(album, param) - if err != nil { - return fmt.Errorf("Error retrieving thumbnail for album '%s': %v", album.Name, err) - } - - // Send the message - link := param.ImmichURL + "/share/" + sharingKey - err = sendMessage(client, param.WhatsappGroup, album.Name, album.Description, fmt.Sprintf("Nouvel album : %s", link), link, thumbnail) - if err != nil { - fmt.Fprintf(os.Stderr, "Error sending message to WhatsApp for album '%s': %v\n", album.Name, err) + fmt.Fprintf(os.Stderr, "Erreur récupération clé partage: %v\n", err) continue } + link := param.ImmichURL + "/share/" + sharingKey + + // Récupérer la miniature + thumbnail, err := immichClient.GetThumbnail(album.AlbumThumbnailAssetId) + if err != nil { + fmt.Fprintf(os.Stderr, "Erreur miniature: %v\n", err) + continue + } + + // Envoyer le message + err = wac.SendMessage(param.WhatsappGroup, album.Name, album.Description, fmt.Sprintf("Nouvel album : %s", link), link, thumbnail) + if err != nil { + fmt.Fprintf(os.Stderr, "Erreur envoi WhatsApp: %v\n", err) + } } } } - return nil } -func parseTime(timeStr string) (int, int, error) { - // Split the string into hours and minutes - parts := strings.Split(timeStr, ":") - if len(parts) != 2 { - return 0, 0, fmt.Errorf("invalid format: %s", timeStr) +// Fonction pour écouter les événements WhatsApp +func listen(wac *whatsapp.WhatsAppClient) error { + if err := wac.Client.Connect(); err != nil { + return err } + defer wac.Client.Disconnect() - // Convert the parts to integers - hours, err := strconv.Atoi(parts[0]) - if err != nil { - return 0, 0, fmt.Errorf("error converting hours: %v", err) - } + wac.Client.AddEventHandler(func(evt interface{}) { + fmt.Println("Réception d'un message:", evt) + }) - minutes, err := strconv.Atoi(parts[1]) - if err != nil { - return 0, 0, fmt.Errorf("error converting minutes: %v", err) - } - - return hours, minutes, nil -} - -func main() { - // Handle interrupts to clean properly - c := make(chan os.Signal) + // Attendre une interruption + c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt) - go func() { - select { - case sig := <-c: - fmt.Printf("Got %s signal. Aborting...\n", sig) - os.Exit(1) - } - }() - - // Load the parameters - param := new(Parameters) - param.ImmichURL = os.Getenv("IMMICH-URL") - param.ImmichKey = os.Getenv("IMMICH-KEY") - param.WhatsappSessionFile = os.Getenv("WHATSAPP-SESSION-FILE") - param.WhatsappGroup = os.Getenv("WHATSAPP-GROUP") - param.TimeToRun = os.Getenv("TIME-TO-RUN") - param.HealthchecksURL = os.Getenv("HEALTHCHECKS-URL") - param.DevelopmentMode = os.Getenv("DEVELOPMENT-MODE") - - if param.DevelopmentMode == "run-once" || param.DevelopmentMode == "run-last" { - err := runLoop(*param) - if err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - } - } else if param.DevelopmentMode == "listen" { - err := listen(*param) - if err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - } - } else { - // Test the connexion on startup - err := testConnexions(*param) - if err != nil { - fmt.Fprintf(os.Stderr, "Unable to connect: %v\n", err) - return - } - - // Run the loop everyday - for { - // First, wait for the appropriate time - hours, minutes, err := parseTime(param.TimeToRun) - if err != nil { - fmt.Fprintf(os.Stderr, "Unable to read the time provided: %v\n", err) - return - } - - t := time.Now() - n := time.Date(t.Year(), t.Month(), t.Day(), hours, minutes, 0, 0, t.Location()) - d := n.Sub(t) - if d < 0 { - n = n.Add(24 * time.Hour) - d = n.Sub(t) - } - fmt.Fprintf(os.Stderr, "Sleeping for: %s\n", d) - time.Sleep(d) - - // Then try to run it 3 times in case of error - for i := 0; i < 3; i++ { - err = runLoop(*param) - if err == nil { - break - } - - // Print the error and attempt number - fmt.Fprintf(os.Stderr, "Attempt %d failed: %v\n", i+1, err) - // Wait before the next attempt - time.Sleep(time.Duration(math.Pow(2, float64(i))) * 30 * time.Second) - } - - // Manage the error - if err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - continue - } else { - if param.HealthchecksURL != "" { - client := http.Client{ - Timeout: time.Second * 10, - } - _, err := client.Head(param.HealthchecksURL) - if err != nil { - fmt.Fprintf(os.Stderr, "%s", err) - } - } - } - } - } + <-c + fmt.Println("Arrêt demandé.") + return nil } diff --git a/immich-souvenirs/internals/immich/immich.go b/immich-souvenirs/internals/immich/immich.go new file mode 100644 index 0000000..8735275 --- /dev/null +++ b/immich-souvenirs/internals/immich/immich.go @@ -0,0 +1,169 @@ +package immich + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "time" +) + +type Album struct { + ID string `json:"id"` + Name string `json:"albumName"` + Description string `json:"description"` + Shared bool `json:"shared"` + HasSharedLink bool `json:"hasSharedLink"` + StartDate time.Time `json:"startDate"` + CreatedAt time.Time `json:"createdAt"` + AlbumThumbnailAssetId string `json:"albumThumbnailAssetId"` +} + +type Key struct { + ID string `json:"id"` + Key string `json:"key"` + Album *Album `json:"album"` +} + +type ImmichClient struct { + BaseURL string + APIKey string +} + +// Fonction pour créer une instance d'ImmichClient +func New(baseURL string, apiKey string) *ImmichClient { + return &ImmichClient{ + BaseURL: baseURL, + APIKey: apiKey, + } +} + +// Récupérer la liste des albums +func (ic *ImmichClient) FetchAlbums() ([]Album, error) { + client := &http.Client{Timeout: 10 * time.Second} + req, err := http.NewRequest(http.MethodGet, ic.BaseURL+"/api/albums", nil) + if err != nil { + return nil, err + } + req.Header.Set("x-api-key", ic.APIKey) + req.Header.Set("Accept", "application/json") + + res, err := client.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + + if res.StatusCode != 200 { + return nil, fmt.Errorf("Status code %d", res.StatusCode) + } + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, err + } + + var albums []Album + if err := json.Unmarshal(body, &albums); err != nil { + return nil, err + } + return albums, nil +} + +// Obtenir la clé de partage pour un album +func (ic *ImmichClient) GetSharingKey(album Album) (string, error) { + client := &http.Client{Timeout: 10 * time.Second} + if album.HasSharedLink { + // Récupérer la clé existante + req, err := http.NewRequest(http.MethodGet, ic.BaseURL+"/api/shared-links", nil) + if err != nil { + return "", err + } + req.Header.Set("x-api-key", ic.APIKey) + req.Header.Set("Accept", "application/json") + res, err := client.Do(req) + if err != nil { + return "", err + } + defer res.Body.Close() + + if res.StatusCode != 200 { + return "", fmt.Errorf("Status code %d", res.StatusCode) + } + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return "", err + } + + var keys []Key + if err := json.Unmarshal(body, &keys); err != nil { + return "", err + } + for _, key := range keys { + if key.Album != nil && key.Album.ID == album.ID { + return key.Key, nil + } + } + return "", fmt.Errorf("No sharing key found for album '%s'", album.Name) + } else { + // Créer un nouveau lien partagé + jsonData := []byte(`{ + "albumId": "` + album.ID + `", + "allowDownload": true, + "allowUpload": false, + "showMetadata": true, + "type": "ALBUM" + }`) + req, err := http.NewRequest(http.MethodPost, ic.BaseURL+"/api/shared-links", bytes.NewBuffer(jsonData)) + if err != nil { + return "", err + } + req.Header.Set("x-api-key", ic.APIKey) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/octet-stream") + res, err := client.Do(req) + if err != nil { + return "", err + } + defer res.Body.Close() + + if res.StatusCode != 201 { + return "", fmt.Errorf("Status code %d", res.StatusCode) + } + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return "", err + } + + var key Key + if err := json.Unmarshal(body, &key); err != nil { + return "", err + } + return key.Key, nil + } +} + +// Récupérer la miniature d'un album +func (ic *ImmichClient) GetThumbnail(assetID string) ([]byte, error) { + client := &http.Client{Timeout: 10 * time.Second} + req, err := http.NewRequest(http.MethodGet, ic.BaseURL+"/api/assets/"+assetID+"/thumbnail?size=preview", nil) + if err != nil { + return nil, err + } + req.Header.Set("x-api-key", ic.APIKey) + req.Header.Set("Accept", "application/octet-stream") + res, err := client.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + + if res.StatusCode != 200 { + return nil, fmt.Errorf("Status code %d", res.StatusCode) + } + + return ioutil.ReadAll(res.Body) +} diff --git a/immich-souvenirs/internals/whatsapp/whatsapp.go b/immich-souvenirs/internals/whatsapp/whatsapp.go new file mode 100644 index 0000000..5e58662 --- /dev/null +++ b/immich-souvenirs/internals/whatsapp/whatsapp.go @@ -0,0 +1,84 @@ +package whatsapp + +import ( + "context" + "fmt" + "os" + + _ "github.com/mattn/go-sqlite3" + "github.com/mdp/qrterminal/v3" + "go.mau.fi/whatsmeow" + "go.mau.fi/whatsmeow/store/sqlstore" + waLog "go.mau.fi/whatsmeow/util/log" + "go.mau.fi/whatsmeow/types" + waProto "go.mau.fi/whatsmeow/binary/proto" + "google.golang.org/protobuf/proto" +) + +type WhatsAppClient struct { + Client *whatsmeow.Client +} + +// Fonction pour initialiser la connexion et retourner une instance de WhatsAppClient +func New(sessionFile string) (*WhatsAppClient, error) { + dbLog := waLog.Stdout("Database", "ERROR", true) + container, err := sqlstore.New(context.Background(), "sqlite3", "file:"+sessionFile+"?_foreign_keys=on", dbLog) + if err != nil { + return nil, err + } + deviceStore, err := container.GetFirstDevice(context.Background()) + if err != nil { + return nil, err + } + clientLog := waLog.Stdout("Client", "ERROR", true) + client := whatsmeow.NewClient(deviceStore, clientLog) + + if client.Store.ID == nil { + // No ID stored, nouvelle connexion + qrChan, _ := client.GetQRChannel(context.Background()) + err = client.Connect() + if err != nil { + return nil, err + } + for evt := range qrChan { + if evt.Event == "code" { + qrterminal.GenerateHalfBlock(evt.Code, qrterminal.L, os.Stdout) + fmt.Println("QR code:", evt.Code) + } else { + fmt.Println("Event:", evt.Event) + } + } + } else { + // Connexion existante + err = client.Connect() + if err != nil { + return nil, err + } + } + + return &WhatsAppClient{Client: client}, nil +} + +// Méthode pour envoyer un message +func (wac *WhatsAppClient) SendMessage(group string, title string, description string, message string, url string, thumbnail []byte) error { + jid, err := types.ParseJID(group) + if err != nil { + return fmt.Errorf("Incorrect group identifier '%s': %v", group, err) + } + + msg := &waProto.Message{ + ExtendedTextMessage: &waProto.ExtendedTextMessage{ + Title: proto.String(title), + Description: proto.String(description), + Text: proto.String(message), + MatchedText: proto.String(url), + JPEGThumbnail: thumbnail, + }, + } + ts, err := wac.Client.SendMessage(context.Background(), jid, msg) + if err != nil { + return fmt.Errorf("Error sending message with title '%s': %v", title, err) + } + fmt.Printf("Message with title '%s' sent (timestamp: %s)\n", title, ts) + return nil +} diff --git a/immich-souvenirs/test.sh b/immich-souvenirs/test.sh index 81812fa..175536b 100755 --- a/immich-souvenirs/test.sh +++ b/immich-souvenirs/test.sh @@ -7,4 +7,4 @@ echo "go mod tidy" echo "env CGO_ENABLED=1 env DEVELOPMENT-MODE=run-once go run immich-souvenirs.go" echo "-----------------------------------------------------------------------" -docker run -it -v $(pwd)/immich-souvenirs.go:/app/immich-souvenirs.go -w /app -v immich-souvenirs_config:/config --env-file test.env --rm golang:1.23-alpine /bin/sh +docker run -it -v $(pwd)/immich-souvenirs.go:/app/immich-souvenirs.go -v $(pwd)/internals:/app/internals -w /app -v immich-souvenirs_config:/config --env-file test.env --rm golang:1.23-alpine /bin/sh