From 14fd8a359b7c847e49d05faf14b811cbf52dd095 Mon Sep 17 00:00:00 2001 From: napnap75 Date: Wed, 25 Sep 2024 10:17:48 +0200 Subject: [PATCH 01/18] Corrected the build --- chrooted-sshd/entrypoint.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 chrooted-sshd/entrypoint.sh diff --git a/chrooted-sshd/entrypoint.sh b/chrooted-sshd/entrypoint.sh old mode 100644 new mode 100755 From d4926240abb96ee3f5fe701d2e14daa7cbfa7e8e Mon Sep 17 00:00:00 2001 From: napnap75 Date: Wed, 25 Sep 2024 17:45:43 +0200 Subject: [PATCH 02/18] Now send the logs to a file --- chrooted-sshd/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chrooted-sshd/Dockerfile b/chrooted-sshd/Dockerfile index ebe86a5..5406d56 100644 --- a/chrooted-sshd/Dockerfile +++ b/chrooted-sshd/Dockerfile @@ -6,4 +6,4 @@ COPY entrypoint.sh /usr/local/bin/entrypoint.sh ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] -CMD ["/usr/sbin/sshd", "-D", "-e"] +CMD ["/usr/sbin/sshd", "-D", "-E", "/var/log/sshd.log"] From dafa9726611d9bf3023ac9b6c519b5550cac9e80 Mon Sep 17 00:00:00 2001 From: napnap75 Date: Wed, 2 Oct 2024 08:20:20 +0200 Subject: [PATCH 03/18] Updated the GO version --- immich-souvenirs/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/immich-souvenirs/Dockerfile b/immich-souvenirs/Dockerfile index 6596a45..1af4e81 100644 --- a/immich-souvenirs/Dockerfile +++ b/immich-souvenirs/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.21-alpine AS builder +FROM golang:1.22-alpine AS builder WORKDIR $GOPATH/src/napnap75/immich-souvenirs/ COPY immich-souvenirs.go . From 3ec4250f245c5a9bcc81fe41a7d6fb144217b3c2 Mon Sep 17 00:00:00 2001 From: napnap75 Date: Wed, 2 Oct 2024 18:02:22 +0200 Subject: [PATCH 04/18] Removed the unused images --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 93ba9c3..a50041a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,7 +10,7 @@ jobs: strategy: fail-fast: false matrix: - repository: [chrooted-sshd, immich-souvenirs, dnsupdater, rsync-server, sshd, webhook, gandi, http-tunnel, restic-auto, restic-rest, shairport-sync, telegraf] + repository: [chrooted-sshd, immich-souvenirs, dnsupdater, webhook, restic-auto, shairport-sync] steps: - name: Checkout From aa87ef296992091d1e783a7fd5c27894fa459eec Mon Sep 17 00:00:00 2001 From: napnap75 Date: Sun, 6 Oct 2024 15:20:35 +0200 Subject: [PATCH 05/18] Added runitor to handle HC pings --- restic-auto/Dockerfile | 7 +++++-- restic-auto/docker-entrypoint.sh | 14 ++++++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/restic-auto/Dockerfile b/restic-auto/Dockerfile index 983ecba..e044f0d 100644 --- a/restic-auto/Dockerfile +++ b/restic-auto/Dockerfile @@ -7,11 +7,14 @@ RUN DOWNLOAD_ARCH=$(echo ${TARGETPLATFORM} | cut -d"/" -f 2) \ && DOWNLOAD_URL=$(curl -s https://api.github.com/repos/restic/restic/releases/latest | grep "browser_download_url" | grep "linux_"${DOWNLOAD_ARCH}"\." | cut -d\" -f4) \ && curl --retry 3 -L -s -o restic.bz2 ${DOWNLOAD_URL} \ && bunzip2 restic.bz2 \ - && chmod +x restic + && chmod +x restic \ + && DOWNLOAD_URL=$(curl -s https://api.github.com/repos/bdd/runitor/releases/latest | grep "browser_download_url" | grep "linux-"${DOWNLOAD_ARCH} | cut -d\" -f4) \ + && curl --retry 3 -L -s -o runitor ${DOWNLOAD_URL} \ + && chmod +x runitor FROM alpine:latest -COPY --from=builder restic /usr/bin/ +COPY --from=builder restic runitor /usr/bin/ RUN apk add --no-cache bash curl jq openssh-client dcron tzdata docker diff --git a/restic-auto/docker-entrypoint.sh b/restic-auto/docker-entrypoint.sh index 54aac7d..a83befc 100755 --- a/restic-auto/docker-entrypoint.sh +++ b/restic-auto/docker-entrypoint.sh @@ -37,7 +37,10 @@ else else echo -n "0 4 * * *" >> /tmp/crontab fi - echo -n " restic-auto >> /var/log/cron.log" >> /tmp/crontab + if [[ "$HC_PING_KEY" ]] ; then + echo -n " runitor -slug ${HOSTNAME}-restic-backup --" >> /tmp/crontab + fi + echo -n " restic-auto >> /var/log/cron.log" >> /tmp/crontab if [[ "$POST_BACKUP_COMMAND" ]] ; then echo -n " && $POST_BACKUP_COMMAND" >> /tmp/crontab fi @@ -47,7 +50,14 @@ else else echo -n "0 1 * * 0" >> /tmp/crontab fi - echo -n " restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --keep-yearly 2 --prune >> /var/log/cron.log && restic check >> /var/log/cron.log" >> /tmp/crontab + if [[ "$HC_PING_KEY" ]] ; then + echo -n " runitor -slug ${HOSTNAME}-restic-forget --" >> /tmp/crontab + fi + echo -n " restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --keep-yearly 2 --prune >> /var/log/cron.log &&" >> /tmp/crontab + if [[ "$HC_PING_KEY" ]] ; then + echo -n " runitor -slug ${HOSTNAME}-restic-check --" >> /tmp/crontab + fi + echo -n " restic check >> /var/log/cron.log" >> /tmp/crontab if [[ "$POST_MAINTENANCE_COMMAND" ]] ; then echo -n " && $POST_MAINTENANCE_COMMAND" >> /tmp/crontab fi From 56037ebe0f2f128dde59bf84f79dc5387dde2fd8 Mon Sep 17 00:00:00 2001 From: napnap75 Date: Thu, 10 Oct 2024 09:06:52 +0200 Subject: [PATCH 06/18] Delete immich-souvenirs/README.md --- immich-souvenirs/README.md | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 immich-souvenirs/README.md diff --git a/immich-souvenirs/README.md b/immich-souvenirs/README.md deleted file mode 100644 index b8df464..0000000 --- a/immich-souvenirs/README.md +++ /dev/null @@ -1,7 +0,0 @@ -## Development env -docker run --rm -it -v $PWD:/go/src/napnap75/immich-souvenirs -v immich-souvenirs_config:/config golang:1.21-alpine /bin/sh -apk add --no-cache git gcc musl-dev nano -cd src/napnap75/immich-souvenirs/ -go mod init github.com/napnap75/multiarch-docker-files/immich-souvenirs -go get -d -v -env IMMICH-URL="https://immich.nappez.com" env IMMICH-KEY="wRJWYUGlEfWGQ3a5lckHivLCU40s7ldEZSmikpmsE30" env WHATSAPP-SESSION-FILE="/config/ws.gob" env WHATSAPP-GROUP="120363288639885954@g.us" env RUN-ONCE=true go run immich-souvenirs.go From 163ae5a8bf41ad36bf4ce931b69d9cd0e0b67c32 Mon Sep 17 00:00:00 2001 From: napnap75 Date: Fri, 11 Oct 2024 22:05:11 +0200 Subject: [PATCH 07/18] Simplified the script by adding Runitor --- dnsupdater/Dockerfile | 16 +++++- dnsupdater/dnsupdater.sh | 105 +++++++++++++++++++-------------------- 2 files changed, 66 insertions(+), 55 deletions(-) diff --git a/dnsupdater/Dockerfile b/dnsupdater/Dockerfile index 72f755d..5935a54 100644 --- a/dnsupdater/Dockerfile +++ b/dnsupdater/Dockerfile @@ -1,8 +1,20 @@ +FROM alpine:latest AS builder + +ARG TARGETPLATFORM + +RUN DOWNLOAD_ARCH=$(echo ${TARGETPLATFORM} | cut -d"/" -f 2) \ + && apk add --no-cache curl \ + && DOWNLOAD_URL=$(curl -s https://api.github.com/repos/bdd/runitor/releases/latest | grep "browser_download_url" | grep "linux-"${DOWNLOAD_ARCH} | cut -d\" -f4) \ + && curl --retry 3 -L -s -o runitor ${DOWNLOAD_URL} \ + && chmod +x runitor + FROM alpine:latest -ADD dnsupdater.sh /usr/bin/dnsupdater.sh +COPY --from=builder runitor /usr/bin/ + +ADD dnsupdater.sh docker-command.sh /usr/bin/ RUN apk add --no-cache bash curl jq bind-tools \ && chmod +x /usr/bin/dnsupdater.sh -CMD /usr/bin/dnsupdater.sh +CMD ["/usr/bin/docker-command.sh"] diff --git a/dnsupdater/dnsupdater.sh b/dnsupdater/dnsupdater.sh index c4983fa..e27dec5 100755 --- a/dnsupdater/dnsupdater.sh +++ b/dnsupdater/dnsupdater.sh @@ -1,59 +1,58 @@ #!/bin/bash -while true ; do - # Get my current IP - my_ip=$(curl -s https://api.ipify.org) +# Get my IP +my_ip=$(curl -s https://api.ipify.org) +if [[ ! ("$my_ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$) ]] ; then + echo "Got incorrect IP: $my_ip" + exit 1 +fi - # Get my currently registered IP - current_ip= - if [[ "$GANDI_API_KEY" != "" ]] ; then - current_ip=$(curl -s -H"X-Api-Key: $GANDI_API_KEY" https://dns.api.gandi.net/api/v5/domains/$DNS_DOMAIN/records | jq -r '.[] | select(.rrset_name == "'$DNS_HOST'") | select(.rrset_type == "A") | .rrset_values[0]') - elif [[ "$OVH_USERNAME" != "" ]] ; then - master_server=$(dig +short -t NS $DNS_DOMAIN | head -n 1) - current_ip=$(dig +short @$master_server $DNS_HOST.$DNS_DOMAIN) +# Get my registered IP +current_ip= +if [[ "$GANDI_API_KEY" != "" ]] ; then + current_ip=$(curl -s -H"X-Api-Key: $GANDI_API_KEY" https://dns.api.gandi.net/api/v5/domains/$DNS_DOMAIN/records | jq -r '.[] | select(.rrset_name == "'$DNS_HOST'") | select(.rrset_type == "A") | .rrset_values[0]') +elif [[ "$OVH_USERNAME" != "" ]] ; then + master_server=$(dig +short -t NS $DNS_DOMAIN | head -n 1) + current_ip=$(dig +short @$master_server $DNS_HOST.$DNS_DOMAIN) +else + echo "No DNS provider configured" + exit 2 +fi +if [[ ! ("$current_ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$) ]] ; then + echo "Incorrect registered IP: $current_ip" + exit 3 +fi + +# If they match, do nothing +if [[ "$my_ip" == "$current_ip" ]]; then + echo "$DNS_HOST.$DNS_DOMAIN record already up-to-date with IP $my_ip" + exit 0 +fi + +# Update the DNS record +echo "Updating $DNS_HOST.$DNS_DOMAIN record with IP $my_ip" +if [[ "$GANDI_API_KEY" != "" ]] ; then + current_record=$(curl -s -H"X-Api-Key: $GANDI_API_KEY" https://dns.api.gandi.net/api/v5/domains/$DNS_DOMAIN/records | jq -c '.[] | select(.rrset_name == "'$DNS_HOST'") | select(.rrset_type == "A")') + current_ttl=$(echo $current_record | jq -r '.rrset_ttl') + curl -s -X PUT -H "Content-Type: application/json" -H "X-Api-Key: $GANDI_API_KEY" -d '{"rrset_ttl": '$current_ttl', "rrset_values":["'$my_ip'"]}' https://dns.api.gandi.net/api/v5/domains/$DNS_DOMAIN/records/$DNS_HOST/A + if [[ $? != 0 ]] ; then + echo "Unable to update GANDI record" + exit 4 fi - - # Check if both IP addresses are correct - if [[ "$my_ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ && "$current_ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]] ; then - # If they do not match, change it (and keep the TTL and TYPE) - if [[ "$my_ip" != "$current_ip" ]]; then - result=1 - echo "Updating $DNS_HOST.$DNS_DOMAIN record with IP $my_ip" - if [[ "$GANDI_API_KEY" != "" ]] ; then - current_record=$(curl -s -H"X-Api-Key: $GANDI_API_KEY" https://dns.api.gandi.net/api/v5/domains/$DNS_DOMAIN/records | jq -c '.[] | select(.rrset_name == "'$DNS_HOST'") | select(.rrset_type == "A")') - current_ttl=$(echo $current_record | jq -r '.rrset_ttl') - curl -s -X PUT -H "Content-Type: application/json" -H "X-Api-Key: $GANDI_API_KEY" -d '{"rrset_ttl": '$current_ttl', "rrset_values":["'$my_ip'"]}' https://dns.api.gandi.net/api/v5/domains/$DNS_DOMAIN/records/$DNS_HOST/A - result=$? - elif [[ "$OVH_USERNAME" != "" ]] ; then - curl -s --user "$OVH_USERNAME:$OVH_PASSWORD" "http://www.ovh.com/nic/update?system=dyndns&hostname=$DNS_HOST.$DNS_DOMAIN&myip=$my_ip" - result=$? - fi - - # If the update was OK - if [[ $result == 0 ]] ; then - # Send a notification to Slack - if [[ "$SLACK_URL" != "" ]] ; then - curl -o /dev/null -s -m 10 --retry 5 -X POST -d "payload={\"username\": \"gandi\", \"icon_emoji\": \":dart:\", \"text\": \"New IP $my_ip for host $DNS_HOST.$DNS_DOMAIN\"}" $SLACK_URL - fi - - # Send a notification to Gotify - if [[ "$GOTIFY_URL" != "" ]] ; then - curl -o /dev/null -s -m 10 --retry 5 -X POST -H "accept: application/json" -H "Content-Type: application/json" -d "{\"priority\": 5, \"title\": \"New IP for host $DNS_HOST.$DNS_DOMAIN\", \"message\": \"New IP $my_ip for host $DNS_HOST.$DNS_DOMAIN, the old IP was $current_ip\"}" $GOTIFY_URL - fi - - # Send a notification to Healthchecks - if [[ "$HEALTHCHECKS_URL" != "" ]] ; then - curl -o /dev/null -s -m 10 --retry 5 $HEALTHCHECKS_URL - fi - fi - else - # Send a notification to Healthchecks - if [[ "$HEALTHCHECKS_URL" != "" ]] ; then - curl -o /dev/null -s -m 10 --retry 5 $HEALTHCHECKS_URL - fi - fi +elif [[ "$OVH_USERNAME" != "" ]] ; then + curl -s --user "$OVH_USERNAME:$OVH_PASSWORD" "http://www.ovh.com/nic/update?system=dyndns&hostname=$DNS_HOST.$DNS_DOMAIN&myip=$my_ip" + if [[ $? != 0 ]] ; then + echo "Unable to update OVH record" + exit 5 fi +fi - # Wait 5 minutes - sleep 300 -done +# Send a notification to Slack +if [[ "$SLACK_URL" != "" ]] ; then + curl -o /dev/null -s -m 10 --retry 5 -X POST -d "payload={\"username\": \"gandi\", \"icon_emoji\": \":dart:\", \"text\": \"New IP $my_ip for host $DNS_HOST.$DNS_DOMAIN\"}" $SLACK_URL +fi + +# Send a notification to Gotify +if [[ "$GOTIFY_URL" != "" ]] ; then + curl -o /dev/null -s -m 10 --retry 5 -X POST -H "accept: application/json" -H "Content-Type: application/json" -d "{\"priority\": 5, \"title\": \"New IP for host $DNS_HOST.$DNS_DOMAIN\", \"message\": \"New IP $my_ip for host $DNS_HOST.$DNS_DOMAIN, the old IP was $current_ip\"}" $GOTIFY_URL +fi From 0cd35234385e5032c5ecf333403bcaebcbf5fb50 Mon Sep 17 00:00:00 2001 From: napnap75 Date: Fri, 11 Oct 2024 22:07:15 +0200 Subject: [PATCH 08/18] Simplified the script by adding Runitor --- dnsupdater/docker-command.sh | 3 +++ 1 file changed, 3 insertions(+) create mode 100755 dnsupdater/docker-command.sh diff --git a/dnsupdater/docker-command.sh b/dnsupdater/docker-command.sh new file mode 100755 index 0000000..4d0918a --- /dev/null +++ b/dnsupdater/docker-command.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +/usr/bin/runitor -every 5m -slug ${HOSTNAME}-dnsupdater -- /usr/bin/dnsupdater.sh From fe23e93e4acf06e11b86de6e7965f517041703a3 Mon Sep 17 00:00:00 2001 From: napnap75 Date: Mon, 21 Oct 2024 22:51:40 +0200 Subject: [PATCH 09/18] Ouputs logs to stdout --- chrooted-sshd/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chrooted-sshd/Dockerfile b/chrooted-sshd/Dockerfile index 5406d56..ebe86a5 100644 --- a/chrooted-sshd/Dockerfile +++ b/chrooted-sshd/Dockerfile @@ -6,4 +6,4 @@ COPY entrypoint.sh /usr/local/bin/entrypoint.sh ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] -CMD ["/usr/sbin/sshd", "-D", "-E", "/var/log/sshd.log"] +CMD ["/usr/sbin/sshd", "-D", "-e"] From 6299e4e01a44aee903b66febed18ea498dc6149b Mon Sep 17 00:00:00 2001 From: napnap75 Date: Mon, 16 Jun 2025 20:17:54 +0200 Subject: [PATCH 10/18] Update GO version --- immich-souvenirs/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/immich-souvenirs/Dockerfile b/immich-souvenirs/Dockerfile index 1af4e81..fa5d7e6 100644 --- a/immich-souvenirs/Dockerfile +++ b/immich-souvenirs/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.22-alpine AS builder +FROM golang:1.23-alpine AS builder WORKDIR $GOPATH/src/napnap75/immich-souvenirs/ COPY immich-souvenirs.go . From 99c016dba94866aecb72769c480d1e2e15ddb0a4 Mon Sep 17 00:00:00 2001 From: napnap75 Date: Tue, 17 Jun 2025 00:06:45 +0200 Subject: [PATCH 11/18] Corrected the script --- immich-souvenirs/immich-souvenirs.go | 29 ++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/immich-souvenirs/immich-souvenirs.go b/immich-souvenirs/immich-souvenirs.go index ab45794..d934702 100644 --- a/immich-souvenirs/immich-souvenirs.go +++ b/immich-souvenirs/immich-souvenirs.go @@ -51,11 +51,11 @@ type Key struct { func connect(param Parameters) (*whatsmeow.Client, error) { dbLog := waLog.Stdout("Database", "ERROR", true) - container, err := sqlstore.New("sqlite3", "file:" + param.WhatsappSessionFile + "?_foreign_keys=on", dbLog) + container, err := sqlstore.New(context.Background(), "sqlite3", "file:" + param.WhatsappSessionFile + "?_foreign_keys=on", dbLog) if err != nil { return nil, err } - deviceStore, err := container.GetFirstDevice() + deviceStore, err := container.GetFirstDevice(context.Background()) if err != nil { return nil, err } @@ -94,14 +94,23 @@ func sendMessage(client *whatsmeow.Client, group string, message string, url str return fmt.Errorf("Incorrect group identifier '%s': %v", group, err) } - msg := &waProto.Message{ExtendedTextMessage: &waProto.ExtendedTextMessage{ - Text: proto.String(message), - Title: proto.String(title), - Description: proto.String(title), - CanonicalURL: proto.String(url), - MatchedText: proto.String(url), - JPEGThumbnail: thumbnail, - }} +// msg := &waProto.Message{ExtendedTextMessage: &waProto.ExtendedTextMessage{ +// Text: proto.String(message), +// Title: proto.String(title), +// Description: proto.String(title), +// CanonicalUrl: proto.String(url), +// MatchedText: proto.String(url), +// JPEGThumbnail: thumbnail, +// }} + msg := &waProto.Message{ + ExtendedTextMessage: &waProto.ExtendedTextMessage{ + Text: proto.String(fmt.Sprintf("%s\n%s\n%s", title, message, url)), + Title: proto.String(title), + Description: proto.String(message), +// PreviewType: proto.Uint32(0), // 0 pour un lien standard + 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) From c8f24c08ecec709cecf3c3180c7f3e3fca81e84a Mon Sep 17 00:00:00 2001 From: Thomas Date: Thu, 19 Jun 2025 21:32:13 +0200 Subject: [PATCH 12/18] Corrrected the script --- immich-souvenirs/immich-souvenirs.go | 134 ++++++++++++++++----------- immich-souvenirs/test.sh | 10 ++ 2 files changed, 90 insertions(+), 54 deletions(-) create mode 100755 immich-souvenirs/test.sh diff --git a/immich-souvenirs/immich-souvenirs.go b/immich-souvenirs/immich-souvenirs.go index d934702..4dc0a20 100644 --- a/immich-souvenirs/immich-souvenirs.go +++ b/immich-souvenirs/immich-souvenirs.go @@ -29,13 +29,14 @@ type Parameters struct { WhatsappSessionFile string WhatsappGroup string TimeToRun string - RunOnce bool + 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"` @@ -88,26 +89,18 @@ func connect(param Parameters) (*whatsmeow.Client, error) { return client, nil } -func sendMessage(client *whatsmeow.Client, group string, message string, url string, title string, thumbnail []byte) error { +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{ -// Text: proto.String(message), -// Title: proto.String(title), -// Description: proto.String(title), -// CanonicalUrl: proto.String(url), -// MatchedText: proto.String(url), -// JPEGThumbnail: thumbnail, -// }} msg := &waProto.Message{ ExtendedTextMessage: &waProto.ExtendedTextMessage{ - Text: proto.String(fmt.Sprintf("%s\n%s\n%s", title, message, url)), Title: proto.String(title), - Description: proto.String(message), -// PreviewType: proto.Uint32(0), // 0 pour un lien standard + Description: proto.String(description), + Text: proto.String(message), + MatchedText: proto.String(url), JPEGThumbnail: thumbnail, }, } @@ -218,7 +211,7 @@ func getSharingKey(album Album, param Parameters) (string, error) { return "", fmt.Errorf("Error retrieving sharing key for album '%s': %v", album.Name, err) } for _, key := range keys { - if (album.ID == key.Album.ID) { + if (key.Album != nil && album.ID == key.Album.ID) { return key.Key, nil } } @@ -292,6 +285,29 @@ func getThumbnail(album Album, param Parameters) ([]byte, error) { 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) + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + <-c + + return nil +} + + func runLoop(param Parameters) error { // Create new WhatsApp connection and connect client, err := connect(param) @@ -334,51 +350,56 @@ func runLoop(param Parameters) error { } for _, album := range albums { - // Get albums from x years ago - if album.Shared && (album.StartDate.Month() == time.Now().Month()) && (album.StartDate.Day() == time.Now().Day()) { - // Retrieve the sharing key - sharingKey, err := getSharingKey(album, param) - if err != nil { - return fmt.Errorf("Error retrieving the sharing key for album '%s': %v", album.Name, err) + 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) + 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) + continue + } } - // Retrieve the thumbnail - thumbnail, err := getThumbnail(album, param) - if err != nil { - return fmt.Errorf("Error retrieving thumbnail for album '%s': %v", album.Name, err) + if param.DevelopmentMode == "run-last" { + return nil } - // Send the message - link := param.ImmichURL + "/share/" + sharingKey - err = sendMessage(client, param.WhatsappGroup, fmt.Sprintf("Il y a %d an(s) : %s", time.Now().Year()-album.StartDate.Year(), link), link, album.Name, thumbnail) - if err != nil { - fmt.Fprintf(os.Stderr, "Error sending message to WhatsApp for album '%s': %v\n", album.Name, err) - continue - } - } + // Get albums created yesterday + 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) + if err != nil { + return fmt.Errorf("Error retrieving the sharing key for album '%s': %v", album.Name, err) + } - // Get albums created yesterday - if album.Shared && (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) - 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) + } - // 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) + continue + } } - - // Send the message - link := param.ImmichURL + "/share/" + sharingKey - err = sendMessage(client, param.WhatsappGroup, fmt.Sprintf("Nouvel album : %s", link), link, album.Name, thumbnail) - if err != nil { - fmt.Fprintf(os.Stderr, "Error sending message to WhatsApp for album '%s': %v\n", album.Name, err) - continue - } - } } @@ -426,13 +447,18 @@ func main() { param.WhatsappGroup = os.Getenv("WHATSAPP-GROUP") param.TimeToRun = os.Getenv("TIME-TO-RUN") param.HealthchecksURL = os.Getenv("HEALTHCHECKS-URL") - _, param.RunOnce = os.LookupEnv("RUN-ONCE") + param.DevelopmentMode = os.Getenv("DEVELOPMENT-MODE") - if param.RunOnce { + 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) diff --git a/immich-souvenirs/test.sh b/immich-souvenirs/test.sh new file mode 100755 index 0000000..81812fa --- /dev/null +++ b/immich-souvenirs/test.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +echo "-----------------------------------------------------------------------" +echo "apk add --no-cache git gcc musl-dev" +echo "go mod init github.com/napnap75/multiarch-docker-files/immich-souvenirs" +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 From 7ea5713bf507319ad17a97948eb6f79200089064 Mon Sep 17 00:00:00 2001 From: Thomas Date: Thu, 19 Jun 2025 23:05:06 +0200 Subject: [PATCH 13/18] Updated GO version to correct the build --- webhook/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webhook/Dockerfile b/webhook/Dockerfile index ccccac1..b8a129f 100644 --- a/webhook/Dockerfile +++ b/webhook/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:alpine3.11 AS build +FROM golang:1-alpine AS build WORKDIR /go/src/github.com/adnanh/webhook RUN apk add --update -t build-deps curl libc-dev gcc libgcc jq \ && DOWNLOAD_URL=$(curl -s https://api.github.com/repos/adnanh/webhook/releases/latest | jq -r '.tarball_url') \ @@ -7,7 +7,7 @@ RUN apk add --update -t build-deps curl libc-dev gcc libgcc jq \ && go get -d \ && go build -o /usr/local/bin/webhook -FROM alpine:3.11 +FROM alpine:latest RUN apk add --no-cache openssl expect COPY --from=build /usr/local/bin/webhook /usr/local/bin/webhook WORKDIR /etc/webhook From be38f711b7fb88d6db6838db7a47f3188dae697d Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 20 Jun 2025 23:16:32 +0200 Subject: [PATCH 14/18] 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 From 83240a5bc4fe2f215bf6e2fec40a019dfcc429a2 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sun, 6 Jul 2025 13:07:31 +0200 Subject: [PATCH 15/18] Added a check not to backup the root directory --- restic-auto/restic-auto | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/restic-auto/restic-auto b/restic-auto/restic-auto index 2061e90..27f94b9 100755 --- a/restic-auto/restic-auto +++ b/restic-auto/restic-auto @@ -3,7 +3,10 @@ # Backup one directory function backup_dir { # Check if the dir to backup is mounted as a subdirectory of /root inside this container - if [ -d "/root_fs$1" ]; then + if [ "$1" = "" ]; then + echo "[ERROR] Cannot backup the root directory. Have you correctly configured the volumes to backup for this container ?" + return -1 + elif [ -d "/root_fs$1" ]; then restic backup $RESTIC_BACKUP_FLAGS /root_fs$1 return $? else From a659f3c0843d3eb04553ff5232377e95947e76c1 Mon Sep 17 00:00:00 2001 From: napnap75 Date: Sun, 17 Aug 2025 14:40:30 +0300 Subject: [PATCH 16/18] Revert to Alpine 3.21 --- sshd/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sshd/Dockerfile b/sshd/Dockerfile index 8e41a35..ceeda61 100644 --- a/sshd/Dockerfile +++ b/sshd/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:latest AS builder +FROM alpine:3.21 AS builder ARG TARGETPLATFORM @@ -12,7 +12,7 @@ RUN apk add --no-cache curl jq \ && curl --retry 3 -L -s -o /tmp/s6-overlay.tar.xz $S6_DOWNLOAD_URL \ && tar -xf /tmp/s6-overlay.tar.xz -C /tmp/s6-overlay -FROM alpine:latest +FROM alpine:3:21 RUN apk add --no-cache bash curl openssh-server rsync rrsync borgbackup COPY --from=builder /tmp/s6-overlay / From 08cc0db5ecc518c7f56c392fad4530072c00c97f Mon Sep 17 00:00:00 2001 From: napnap75 Date: Sun, 17 Aug 2025 14:54:46 +0300 Subject: [PATCH 17/18] Revert to alpine 3.21 --- chrooted-sshd/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chrooted-sshd/Dockerfile b/chrooted-sshd/Dockerfile index ebe86a5..96c0cd9 100644 --- a/chrooted-sshd/Dockerfile +++ b/chrooted-sshd/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:latest +FROM alpine:3.21 RUN apk add --no-cache openssh From 2f34fbac5f74c55b306e73f3153e230689052ac5 Mon Sep 17 00:00:00 2001 From: napnap75 Date: Sun, 31 Aug 2025 22:11:05 +0200 Subject: [PATCH 18/18] Removed ssh-chrooted --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a50041a..78a665f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,7 +10,7 @@ jobs: strategy: fail-fast: false matrix: - repository: [chrooted-sshd, immich-souvenirs, dnsupdater, webhook, restic-auto, shairport-sync] + repository: [immich-souvenirs, dnsupdater, webhook, restic-auto, shairport-sync] steps: - name: Checkout