An open API service indexing awesome lists of open source software.

https://github.com/slgfire/ezgramwatch

Discord webhook bot that mirrors Instagram Business/Creator posts and Reels to Discord via the official Meta Graph API.
https://github.com/slgfire/ezgramwatch

Last synced: about 1 month ago
JSON representation

Discord webhook bot that mirrors Instagram Business/Creator posts and Reels to Discord via the official Meta Graph API.

Awesome Lists containing this project

README

          


ezgramwatch

[![Build](https://img.shields.io/github/actions/workflow/status/slgfire/ezgramwatch/build.yml?branch=main&label=build&style=flat-square)](https://github.com/slgfire/ezgramwatch/actions/workflows/build.yml)
[![Docker](https://img.shields.io/github/actions/workflow/status/slgfire/ezgramwatch/docker.yml?branch=main&label=docker&style=flat-square)](https://github.com/slgfire/ezgramwatch/actions/workflows/docker.yml)
[![Release](https://img.shields.io/github/v/release/slgfire/ezgramwatch?include_prereleases&style=flat-square)](https://github.com/slgfire/ezgramwatch/releases)
[![License](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](LICENSE)

**๐Ÿ”” Automatically forwards your Instagram Business posts and Reels to Discord via the official Meta Graph API โ€” no scraping, fully ToS-compliant**

---

> **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.

## Features

- ๐Ÿ“ธ Detects new **posts, videos, and Reels** on a configurable polling interval
- ๐Ÿ–ผ๏ธ **Carousel albums** are sent as a multi-image Discord gallery (up to 10 images per message)
- ๐Ÿ”‡ **Silent first run** โ€” existing posts are imported without flooding Discord
- ๐Ÿ”‘ **Automatic token refresh** 7 days before expiry (requires Meta App credentials)
- ๐Ÿ—ƒ๏ธ SQLite-backed deduplication โ€” no duplicate posts after restarts
- ๐Ÿณ Single Docker Compose command to run

## Prerequisites

Before you start, you'll need:

- A **Meta Developer account** โ€” [developers.facebook.com](https://developers.facebook.com)
- An **Instagram Business or Creator account** linked to a Facebook Page
- A **Discord server** where you have permission to create Webhooks
- **Docker** and **Docker Compose** installed on your server

---

## Setup Guide

### Step 1 โ€” Create a Discord Webhook

1. Open your Discord server and go to the channel where posts should appear.
2. Click the **gear icon โš™๏ธ** next to the channel name โ†’ **Integrations** โ†’ **Webhooks**.
3. Click **New Webhook**, give it a name (e.g. `ezgramwatch`), and optionally set an avatar.
4. Click **Copy Webhook URL**.

Save this URL โ€” you'll need it as `DISCORD_WEBHOOK_URL`.

---

### Step 2 โ€” Create a Meta Developer App

1. Go to [developers.facebook.com](https://developers.facebook.com) and sign in.
2. Click **My Apps** โ†’ **Create App**.
3. Choose **Business** as the app type, give it a name, and confirm.
4. On the App Dashboard, click **Add Product** and add **Instagram Graph API**.

> **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.

---

### Step 3 โ€” Add yourself as Developer and connect your Instagram account

#### 3a. Add your Facebook account as Developer

1. In the App Dashboard, go to **App Roles** (in the left sidebar).
2. Click **Add People**, search for your Facebook account, and assign the **Developer** role.

> **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.

#### 3b. Connect your Instagram account as Tester

1. In the App Dashboard, go to **Instagram Graph API โ†’ Settings**.
2. Click **Add Instagram Tester** and enter your Instagram username.
3. Open Instagram on your phone โ†’ **Settings โ†’ Apps and Websites โ†’ Tester Invites** โ†’ Accept the invitation.

Your Instagram account is now linked to the app.

---

### Step 4 โ€” Get a Long-Lived Access Token

#### 4a. Generate a short-lived token in Graph API Explorer

1. Open the [Graph API Explorer](https://developers.facebook.com/tools/explorer).
2. Select your app from the top-right dropdown.
3. Click **Generate Access Token**.
4. In the permission dialog, make sure at least one of these is checked:
- `instagram_business_basic` *(newer apps โ€” recommended)*
- `instagram_basic` + `pages_read_engagement` *(older app setup)*
5. Click **Generate Access Token** and confirm in the popup. Copy the token shown.

#### Option A: Dashboard token (newer apps)

Newer 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.

**Get the exact expiry timestamp for `.env`:**
1. Open the [Token Debugger](https://developers.facebook.com/tools/debug/accesstoken)
2. Paste your token โ†’ click **Debug**
3. Find the row **"Ablaufdatum"** โ€” copy the number before the parenthesis (e.g. `1784580703`)
4. Add it to `.env`: `INSTAGRAM_TOKEN_EXPIRES_AT=1784580703`

> **Tip:** The bot auto-refreshes the token automatically 7 days before expiry โ€” no extra credentials needed.

---

### Step 5 โ€” Find your Instagram User ID

You need the numeric ID of your Instagram account (not the username).

**Easiest way:** Call the API directly in your browser or with curl:

```
https://graph.instagram.com/me?fields=id,username&access_token=YOUR_TOKEN
```

The `id` in the response is your Instagram User ID.

Use it in `INSTAGRAM_ACCOUNTS` like this:
```
INSTAGRAM_ACCOUNTS=17841400000000001:mychannel
```

The `:mychannel` part is an optional display alias shown in the logs. If omitted, the bot fetches your actual username from the API.

---

### Step 6 โ€” Configure and run

```bash
git clone https://github.com/slgfire/ezgramwatch
cd ezgramwatch
cp .env.example .env
```

Open `.env` and fill in the required values:

```env
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/...
INSTAGRAM_ACCESS_TOKEN=EAABsb...
INSTAGRAM_ACCOUNTS=17841400000000001:myaccount
```

Then start the bot:

```bash
mkdir -p data
docker compose up -d
docker compose logs -f
```

On first start you'll see something like:

```
{"msg":"poll.account","fetched":12,"new":12,"posted":0}
```

All existing posts are silently imported. Only new posts from this point on will be forwarded to Discord.

---

## Configuration Reference

| Variable | Required | Default | Description |
|---|---|---|---|
| `DISCORD_WEBHOOK_URL` | โœ… | โ€” | Full Discord Webhook URL |
| `INSTAGRAM_ACCESS_TOKEN` | โœ… | โ€” | Long-lived User Access Token (`instagram_business_basic`) |
| `INSTAGRAM_ACCOUNTS` | โœ… | โ€” | `[:],โ€ฆ` comma-separated |
| `INSTAGRAM_TOKEN_EXPIRES_AT` | โ€” | โ€” | Unix timestamp of token expiry (from Token Debugger โ†’ "Ablaufdatum") โ€” improves auto-refresh accuracy |
| `POLL_INTERVAL_SECONDS` | โ€” | `300` | How often to check for new posts (seconds) |
| `POST_EXISTING_ON_FIRST_RUN` | โ€” | `false` | Set `true` to post up to `FIRST_RUN_POST_LIMIT` existing posts on first start |
| `FIRST_RUN_POST_LIMIT` | โ€” | `10` | Max posts sent when `POST_EXISTING_ON_FIRST_RUN=true` |
| `CAPTION_PREVIEW_CHARS` | โ€” | `300` | Characters of caption shown in the embed |
| `MEDIA_FETCH_LIMIT` | โ€” | `25` | Items fetched per poll (max 100) |
| `LOG_LEVEL` | โ€” | `info` | Log level: `trace` `debug` `info` `warn` `error` `fatal` |
| `LOG_PRETTY` | โ€” | `true`ยน | Human-readable logs; set `false` for JSON (e.g. log aggregation) |
| `DATABASE_PATH` | โ€” | `/data/bot.sqlite` | SQLite path inside the container |
| `GRAPH_API_VERSION` | โ€” | `v21.0` | Meta Graph API version |

ยน Default `true` set in `compose.yaml`; override with `LOG_PRETTY=false` in `.env` for JSON output.

### Monitoring multiple accounts

Separate accounts with commas:

```env
INSTAGRAM_ACCOUNTS=17841400000000001:brand,17841400000000002:personal
```

---

## Volume & Permissions

The container runs as the `node` user (uid=1000). Make sure the `./data` directory is writable:

```bash
sudo chown -R 1000:1000 ./data
```

The SQLite database in `./data` persists across container restarts and image updates.

---

## Updating

```bash
docker compose pull
docker compose up -d
```

---

## Troubleshooting

**"Developer role insufficient" (Entwickler-Rolle nicht ausreichend)**
Your 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).

**"Tester Invites" not visible in Instagram settings**
Make 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**.

**Bot logs a 401 or auth error**
Re-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.

**`Fatal: unable to open database file`**
The container cannot write to the `./data` directory. Either fix ownership (`sudo chown -R 1000:1000 ./data`) or set `user: "root"` in `compose.yaml`.

---

## Known Limitations

- **Own accounts only** โ€” the Graph API does not provide access to third-party profiles.
- **Stories not supported** โ€” they use a separate endpoint with a 24-hour lifetime and are out of scope.
- **60-day token expiry** โ€” the bot renews automatically via `ig_refresh_token` 7 days before expiry.
- **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.

See [`.ai/API_LIMITATIONS.md`](.ai/API_LIMITATIONS.md) for the full list.

---

## License

MIT โ€” see [LICENSE](LICENSE).