{"id":50547416,"url":"https://github.com/slgfire/ezgramwatch","last_synced_at":"2026-06-04T00:02:05.652Z","repository":{"id":358544465,"uuid":"1241805822","full_name":"slgfire/ezgramwatch","owner":"slgfire","description":"Discord webhook bot that mirrors Instagram Business/Creator posts and Reels to Discord via the official Meta Graph API.","archived":false,"fork":false,"pushed_at":"2026-05-17T23:06:39.000Z","size":214,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-17T23:15:46.829Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/slgfire.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-17T20:51:06.000Z","updated_at":"2026-05-17T23:06:43.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/slgfire/ezgramwatch","commit_stats":null,"previous_names":["slgfire/ezgramwatch"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/slgfire/ezgramwatch","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/slgfire%2Fezgramwatch","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/slgfire%2Fezgramwatch/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/slgfire%2Fezgramwatch/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/slgfire%2Fezgramwatch/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/slgfire","download_url":"https://codeload.github.com/slgfire/ezgramwatch/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/slgfire%2Fezgramwatch/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33884734,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-03T02:00:06.370Z","response_time":59,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-04T00:02:04.696Z","updated_at":"2026-06-04T00:02:05.647Z","avatar_url":"https://github.com/slgfire.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n  \u003cimg src=\"logo.png\" alt=\"ezgramwatch\" width=\"500\"/\u003e\n\n  [![Build](https://img.shields.io/github/actions/workflow/status/slgfire/ezgramwatch/build.yml?branch=main\u0026label=build\u0026style=flat-square)](https://github.com/slgfire/ezgramwatch/actions/workflows/build.yml)\n  [![Docker](https://img.shields.io/github/actions/workflow/status/slgfire/ezgramwatch/docker.yml?branch=main\u0026label=docker\u0026style=flat-square)](https://github.com/slgfire/ezgramwatch/actions/workflows/docker.yml)\n  [![Release](https://img.shields.io/github/v/release/slgfire/ezgramwatch?include_prereleases\u0026style=flat-square)](https://github.com/slgfire/ezgramwatch/releases)\n  [![License](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](LICENSE)\n\n  **🔔 Automatically forwards your Instagram Business posts and Reels to Discord via the official Meta Graph API — no scraping, fully ToS-compliant**\n\u003c/div\u003e\n\n---\n\n\u003e **Heads up:** This bot only works with Instagram accounts **you own** and have connected to a Meta Developer App. Monitoring other people's profiles is not possible through the official API.\n\n## Features\n\n- 📸 Detects new **posts, videos, and Reels** on a configurable polling interval\n- 🖼️ **Carousel albums** are sent as a multi-image Discord gallery (up to 10 images per message)\n- 🔇 **Silent first run** — existing posts are imported without flooding Discord\n- 🔑 **Automatic token refresh** 7 days before expiry (requires Meta App credentials)\n- 🗃️ SQLite-backed deduplication — no duplicate posts after restarts\n- 🐳 Single Docker Compose command to run\n\n## Prerequisites\n\nBefore you start, you'll need:\n\n- A **Meta Developer account** — [developers.facebook.com](https://developers.facebook.com)\n- An **Instagram Business or Creator account** linked to a Facebook Page\n- A **Discord server** where you have permission to create Webhooks\n- **Docker** and **Docker Compose** installed on your server\n\n---\n\n## Setup Guide\n\n### Step 1 — Create a Discord Webhook\n\n1. Open your Discord server and go to the channel where posts should appear.\n2. Click the **gear icon ⚙️** next to the channel name → **Integrations** → **Webhooks**.\n3. Click **New Webhook**, give it a name (e.g. `ezgramwatch`), and optionally set an avatar.\n4. Click **Copy Webhook URL**.\n\nSave this URL — you'll need it as `DISCORD_WEBHOOK_URL`.\n\n---\n\n### Step 2 — Create a Meta Developer App\n\n1. Go to [developers.facebook.com](https://developers.facebook.com) and sign in.\n2. Click **My Apps** → **Create App**.\n3. Choose **Business** as the app type, give it a name, and confirm.\n4. On the App Dashboard, click **Add Product** and add **Instagram Graph API**.\n\n\u003e **App Mode:** Your app starts in Development mode. This is fine for personal use — you can connect your own account as a Tester without going through App Review.\n\n---\n\n### Step 3 — Add yourself as Developer and connect your Instagram account\n\n#### 3a. Add your Facebook account as Developer\n\n1. In the App Dashboard, go to **App Roles** (in the left sidebar).\n2. Click **Add People**, search for your Facebook account, and assign the **Developer** role.\n\n\u003e **Why?** The App starts with you as Admin, but connected Facebook accounts (e.g. a second account you use for Instagram) need to be explicitly added as Developer. Without this you'll see *\"Entwickler-Rolle nicht ausreichend\"* (Developer role insufficient) when connecting Instagram.\n\n#### 3b. Connect your Instagram account as Tester\n\n1. In the App Dashboard, go to **Instagram Graph API → Settings**.\n2. Click **Add Instagram Tester** and enter your Instagram username.\n3. Open Instagram on your phone → **Settings → Apps and Websites → Tester Invites** → Accept the invitation.\n\nYour Instagram account is now linked to the app.\n\n---\n\n### Step 4 — Get a Long-Lived Access Token\n\n#### 4a. Generate a short-lived token in Graph API Explorer\n\n1. Open the [Graph API Explorer](https://developers.facebook.com/tools/explorer).\n2. Select your app from the top-right dropdown.\n3. Click **Generate Access Token**.\n4. In the permission dialog, make sure at least one of these is checked:\n   - `instagram_business_basic` *(newer apps — recommended)*\n   - `instagram_basic` + `pages_read_engagement` *(older app setup)*\n5. Click **Generate Access Token** and confirm in the popup. Copy the token shown.\n\n#### Option A: Dashboard token (newer apps)\n\nNewer Meta apps offer a token generator directly in the App Dashboard under **Instagram Graph API → API Setup → Generate Token**. This produces a **long-lived token (~60 days) directly** — no exchange needed.\n\n**Get the exact expiry timestamp for `.env`:**\n1. Open the [Token Debugger](https://developers.facebook.com/tools/debug/accesstoken)\n2. Paste your token → click **Debug**\n3. Find the row **\"Ablaufdatum\"** — copy the number before the parenthesis (e.g. `1784580703`)\n4. Add it to `.env`: `INSTAGRAM_TOKEN_EXPIRES_AT=1784580703`\n\n\n\u003e **Tip:** The bot auto-refreshes the token automatically 7 days before expiry — no extra credentials needed.\n\n---\n\n### Step 5 — Find your Instagram User ID\n\nYou need the numeric ID of your Instagram account (not the username).\n\n**Easiest way:** Call the API directly in your browser or with curl:\n\n```\nhttps://graph.instagram.com/me?fields=id,username\u0026access_token=YOUR_TOKEN\n```\n\nThe `id` in the response is your Instagram User ID.\n\nUse it in `INSTAGRAM_ACCOUNTS` like this:\n```\nINSTAGRAM_ACCOUNTS=17841400000000001:mychannel\n```\n\nThe `:mychannel` part is an optional display alias shown in the logs. If omitted, the bot fetches your actual username from the API.\n\n---\n\n### Step 6 — Configure and run\n\n```bash\ngit clone https://github.com/slgfire/ezgramwatch\ncd ezgramwatch\ncp .env.example .env\n```\n\nOpen `.env` and fill in the required values:\n\n```env\nDISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/...\nINSTAGRAM_ACCESS_TOKEN=EAABsb...\nINSTAGRAM_ACCOUNTS=17841400000000001:myaccount\n```\n\nThen start the bot:\n\n```bash\nmkdir -p data\ndocker compose up -d\ndocker compose logs -f\n```\n\nOn first start you'll see something like:\n\n```\n{\"msg\":\"poll.account\",\"fetched\":12,\"new\":12,\"posted\":0}\n```\n\nAll existing posts are silently imported. Only new posts from this point on will be forwarded to Discord.\n\n---\n\n## Configuration Reference\n\n| Variable | Required | Default | Description |\n|---|---|---|---|\n| `DISCORD_WEBHOOK_URL` | ✅ | — | Full Discord Webhook URL |\n| `INSTAGRAM_ACCESS_TOKEN` | ✅ | — | Long-lived User Access Token (`instagram_business_basic`) |\n| `INSTAGRAM_ACCOUNTS` | ✅ | — | `\u003cig_user_id\u003e[:\u003calias\u003e],…` comma-separated |\n| `INSTAGRAM_TOKEN_EXPIRES_AT` | — | — | Unix timestamp of token expiry (from Token Debugger → \"Ablaufdatum\") — improves auto-refresh accuracy |\n| `POLL_INTERVAL_SECONDS` | — | `300` | How often to check for new posts (seconds) |\n| `POST_EXISTING_ON_FIRST_RUN` | — | `false` | Set `true` to post up to `FIRST_RUN_POST_LIMIT` existing posts on first start |\n| `FIRST_RUN_POST_LIMIT` | — | `10` | Max posts sent when `POST_EXISTING_ON_FIRST_RUN=true` |\n| `CAPTION_PREVIEW_CHARS` | — | `300` | Characters of caption shown in the embed |\n| `MEDIA_FETCH_LIMIT` | — | `25` | Items fetched per poll (max 100) |\n| `LOG_LEVEL` | — | `info` | Log level: `trace` `debug` `info` `warn` `error` `fatal` |\n| `LOG_PRETTY` | — | `true`¹ | Human-readable logs; set `false` for JSON (e.g. log aggregation) |\n| `DATABASE_PATH` | — | `/data/bot.sqlite` | SQLite path inside the container |\n| `GRAPH_API_VERSION` | — | `v21.0` | Meta Graph API version |\n\n¹ Default `true` set in `compose.yaml`; override with `LOG_PRETTY=false` in `.env` for JSON output.\n\n### Monitoring multiple accounts\n\nSeparate accounts with commas:\n\n```env\nINSTAGRAM_ACCOUNTS=17841400000000001:brand,17841400000000002:personal\n```\n\n---\n\n## Volume \u0026 Permissions\n\nThe container runs as the `node` user (uid=1000). Make sure the `./data` directory is writable:\n\n```bash\nsudo chown -R 1000:1000 ./data\n```\n\nThe SQLite database in `./data` persists across container restarts and image updates.\n\n---\n\n## Updating\n\n```bash\ndocker compose pull\ndocker compose up -d\n```\n\n---\n\n## Troubleshooting\n\n**\"Developer role insufficient\" (Entwickler-Rolle nicht ausreichend)**\nYour Facebook account is not listed as Developer in the App. Go to **App Roles** in the left sidebar of the App Dashboard and add it there (see Step 3a).\n\n**\"Tester Invites\" not visible in Instagram settings**\nMake sure your Instagram account is set to **Business** or **Creator** mode. Personal accounts cannot be connected to the Graph API. Change this in the Instagram app under **Settings → Account → Switch to Professional Account**.\n\n**Bot logs a 401 or auth error**\nRe-generate the token in the App Dashboard under **Instagram Graph API → API Setup** and make sure `instagram_business_basic` is included. Paste the new token into `INSTAGRAM_ACCESS_TOKEN` in your `.env` and restart the container.\n\n**`Fatal: unable to open database file`**\nThe container cannot write to the `./data` directory. Either fix ownership (`sudo chown -R 1000:1000 ./data`) or set `user: \"root\"` in `compose.yaml`.\n\n---\n\n## Known Limitations\n\n- **Own accounts only** — the Graph API does not provide access to third-party profiles.\n- **Stories not supported** — they use a separate endpoint with a 24-hour lifetime and are out of scope.\n- **60-day token expiry** — the bot renews automatically via `ig_refresh_token` 7 days before expiry.\n- **Rate limits** — 200 API calls/hour (Standard tier). With the default `POLL_INTERVAL_SECONDS=300` and a small number of accounts this is not an issue.\n\nSee [`.ai/API_LIMITATIONS.md`](.ai/API_LIMITATIONS.md) for the full list.\n\n---\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fslgfire%2Fezgramwatch","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fslgfire%2Fezgramwatch","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fslgfire%2Fezgramwatch/lists"}