diff --git a/stacks/instagram-poster/modules/instagram-poster/main.tf b/stacks/instagram-poster/modules/instagram-poster/main.tf index d304b96d..7a6181e9 100644 --- a/stacks/instagram-poster/modules/instagram-poster/main.tf +++ b/stacks/instagram-poster/modules/instagram-poster/main.tf @@ -57,31 +57,27 @@ resource "kubernetes_manifest" "external_secret" { data = [ { secretKey = "IMMICH_API_KEY" - remoteRef = { - key = "instagram-poster" - property = "immich_api_key" - } + remoteRef = { key = "instagram-poster", property = "immich_api_key" } }, { secretKey = "POSTIZ_API_TOKEN" - remoteRef = { - key = "instagram-poster" - property = "postiz_api_token" - } + remoteRef = { key = "instagram-poster", property = "postiz_api_token" } }, { secretKey = "IMMICH_TAG_INSTAGRAM" - remoteRef = { - key = "instagram-poster" - property = "immich_tag_instagram" - } + remoteRef = { key = "instagram-poster", property = "immich_tag_instagram" } }, { secretKey = "IMMICH_TAG_POSTED" - remoteRef = { - key = "instagram-poster" - property = "immich_tag_posted" - } + remoteRef = { key = "instagram-poster", property = "immich_tag_posted" } + }, + { + secretKey = "TELEGRAM_BOT_TOKEN" + remoteRef = { key = "instagram-poster", property = "telegram_bot_token" } + }, + { + secretKey = "TELEGRAM_CHAT_ID" + remoteRef = { key = "instagram-poster", property = "telegram_chat_id" } }, ] } @@ -222,10 +218,12 @@ resource "kubernetes_deployment" "instagram_poster" { resources { requests = { cpu = "50m" - memory = "64Mi" + memory = "128Mi" } + # Pillow full-resolution HEIC decode peaks ~600-800Mi for big phone + # photos; 512Mi was OOMKilling on /original requests. limits = { - memory = "512Mi" + memory = "1500Mi" } } } @@ -283,7 +281,7 @@ module "ingress_image_public" { host = "instagram-poster" tls_secret_name = var.tls_secret_name protected = false - ingress_path = ["/image"] + ingress_path = ["/image", "/original"] port = 80 service_name = "instagram-poster" } diff --git a/stacks/instagram-poster/terragrunt.hcl b/stacks/instagram-poster/terragrunt.hcl index b309c258..08349275 100644 --- a/stacks/instagram-poster/terragrunt.hcl +++ b/stacks/instagram-poster/terragrunt.hcl @@ -19,5 +19,5 @@ dependency "external-secrets" { inputs = { # Bump per deploy. Use 8-char git SHA — :latest causes stale pull-through cache. - image_tag = "24935ab4" + image_tag = "25e46efd" } diff --git a/stacks/n8n/workflows/instagram-discover.json b/stacks/n8n/workflows/instagram-discover.json index 055c1f15..3bba2096 100644 --- a/stacks/n8n/workflows/instagram-discover.json +++ b/stacks/n8n/workflows/instagram-discover.json @@ -15,7 +15,7 @@ "type": "n8n-nodes-base.scheduleTrigger", "typeVersion": 1.1, "position": [250, 300], - "notes": "Trigger every 30 minutes. Polling cadence is conservative for a personal pipeline; matches instagram-poster scan rate." + "notes": "Trigger every 30 minutes." }, { "parameters": { @@ -24,7 +24,6 @@ "sendHeaders": true, "headerParameters": { "parameters": [ - {"name": "Authorization", "value": "=Bearer {{ $env.INSTAGRAM_POSTER_TOKEN }}"}, {"name": "Content-Type", "value": "application/json"} ] }, @@ -36,7 +35,7 @@ "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [500, 300], - "notes": "Calls instagram-poster /scan endpoint. Returns {\"new_items\": [\"asset-uuid\", ...]}. Authorization header is optional (internal cluster service); leave INSTAGRAM_POSTER_TOKEN blank if unused." + "notes": "POST /scan returns {\"new_items\": [\"asset-uuid\", ...]}. Internal cluster service, no auth." }, { "parameters": { @@ -48,7 +47,7 @@ "type": "n8n-nodes-base.splitOut", "typeVersion": 1, "position": [750, 300], - "notes": "Fan out one candidate per execution branch so each photo gets its own approval message." + "notes": "Fan out one candidate per execution branch." }, { "parameters": { @@ -60,70 +59,35 @@ "type": "n8n-nodes-base.splitInBatches", "typeVersion": 3, "position": [970, 300], - "notes": "Process one asset at a time so errors on a single asset don't fan out and spam Telegram." - }, - { - "parameters": { - "method": "GET", - "url": "={{ $env.IMMICH_BASE_URL }}/api/assets/{{ $json.new_items }}", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - {"name": "x-api-key", "value": "={{ $env.IMMICH_API_KEY }}"}, - {"name": "Accept", "value": "application/json"} - ] - }, - "options": {"timeout": 30000} - }, - "id": "fetch-asset-meta", - "name": "Fetch Immich asset metadata", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.2, - "position": [1190, 300], - "notes": "Pull asset metadata from Immich for caption preview (filename, fileCreatedAt, EXIF location)." - }, - { - "parameters": { - "jsCode": "const item = $input.first().json;\nconst assetId = item.id || $('Loop one at a time').item.json.new_items;\nconst filename = item.originalFileName || item.originalPath || 'unknown';\nconst createdAt = item.fileCreatedAt || item.localDateTime || '';\nconst exif = item.exifInfo || {};\nconst city = exif.city || '';\nconst country = exif.country || '';\nconst location = [city, country].filter(Boolean).join(', ');\n\nconst dateStr = createdAt ? new Date(createdAt).toISOString().slice(0, 10) : '';\n\nconst lines = [\n 'New Immich candidate',\n '',\n '' + assetId + '',\n '',\n 'File: ' + filename\n];\nif (dateStr) lines.push('Taken: ' + dateStr);\nif (location) lines.push('Where: ' + location);\nlines.push('');\nlines.push('Approve to enqueue for posting, reject to mark seen.');\n\nreturn [{ json: { asset_id: assetId, caption: lines.join('\\n') } }];" - }, - "id": "build-caption", - "name": "Build caption + asset id", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [1410, 300], - "notes": "Compose the caption from Immich metadata. HTML parse_mode is used downstream." + "notes": "Process one asset at a time so a single bad photo doesn't fan out." }, { "parameters": { "method": "POST", - "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendPhoto", + "url": "={{ $env.INSTAGRAM_POSTER_INTERNAL_URL }}/deliver/{{ $json.new_items }}", "sendHeaders": true, "headerParameters": { "parameters": [ {"name": "Content-Type", "value": "application/json"} ] }, - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ chat_id: $env.TELEGRAM_CHAT_ID, photo: $env.PUBLIC_INSTAGRAM_POSTER_URL + '/image/' + $json.asset_id, caption: $json.caption, parse_mode: 'HTML', reply_markup: { inline_keyboard: [[ { text: 'Approve', callback_data: 'approve:' + $json.asset_id }, { text: 'Reject', callback_data: 'reject:' + $json.asset_id } ]] } }) }}", - "options": {"timeout": 30000} + "sendBody": false, + "options": {"timeout": 120000} }, - "id": "telegram-send-photo", - "name": "Telegram sendPhoto with buttons", + "id": "deliver", + "name": "Deliver via Telegram + tag posted", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, - "position": [1630, 300], - "notes": "Telegram needs a public URL for the photo (it fetches the image from outside the cluster). callback_data uses the action:asset_id format consumed by instagram-approval." + "position": [1190, 300], + "notes": "Single-shot endpoint: instagram-poster fetches the original from Immich, converts HEIC->JPEG full-quality, multipart-uploads to Telegram chat, then tags the asset 'posted' in Immich and flips queue row to posted. Telegram URL-fetch fails through Cloudflare for >5MB files, so we push bytes directly from inside the cluster." } ], "connections": { "Every 30 minutes": {"main": [[{"node": "Scan Immich for new candidates", "type": "main", "index": 0}]]}, "Scan Immich for new candidates": {"main": [[{"node": "Split new_items array", "type": "main", "index": 0}]]}, "Split new_items array": {"main": [[{"node": "Loop one at a time", "type": "main", "index": 0}]]}, - "Loop one at a time": {"main": [[{"node": "Fetch Immich asset metadata", "type": "main", "index": 0}]]}, - "Fetch Immich asset metadata": {"main": [[{"node": "Build caption + asset id", "type": "main", "index": 0}]]}, - "Build caption + asset id": {"main": [[{"node": "Telegram sendPhoto with buttons", "type": "main", "index": 0}]]}, - "Telegram sendPhoto with buttons": {"main": [[{"node": "Loop one at a time", "type": "main", "index": 0}]]} + "Loop one at a time": {"main": [[{"node": "Deliver via Telegram + tag posted", "type": "main", "index": 0}]]}, + "Deliver via Telegram + tag posted": {"main": [[{"node": "Loop one at a time", "type": "main", "index": 0}]]} }, "settings": {"executionOrder": "v1", "saveExecutionProgress": false, "saveManualExecutions": true}, "staticData": null,