├── storyUID.json ├── metadata-story.json ├── .gitignore ├── assets └── Instagram-telegram.png ├── orchestrator.sh ├── .env.example ├── index.js ├── package.json ├── downloader.js ├── LICENSE ├── .all-contributorsrc ├── dispatcher-telegram.js ├── dispatcher-discord.js ├── util.js ├── collector.js ├── frontend └── index.html └── README.md /storyUID.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /metadata-story.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules 3 | images/* 4 | session.json 5 | yarn.lock 6 | videos/* 7 | .DS_Store -------------------------------------------------------------------------------- /assets/Instagram-telegram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2pai/instastory-monitor-telegram/HEAD/assets/Instagram-telegram.png -------------------------------------------------------------------------------- /orchestrator.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Run Collector" 4 | npm run collect 5 | echo "Run Downloader" 6 | npm run download 7 | echo "Run Dispatcher" 8 | npm run dispatch 9 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | #APP 2 | PORT= 3 | 4 | #INSTAGRAM 5 | IG_USERNAME= 6 | IG_PASSWORD= 7 | IG_PROXY= 8 | 9 | #TELEGRAM 10 | CHAT_ID= 11 | TOKEN_TELEGRAM= 12 | 13 | #DISCORD 14 | DC_WEBHOOK_URL= 15 | 16 | TARGET_USERNAME= 17 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | const fs = require('fs') 3 | const express = require('express') 4 | const app = express() 5 | 6 | app.engine('html', require('ejs').renderFile) 7 | app.get('/',(req,res) => { 8 | const metadataStory = JSON.parse(fs.readFileSync('./metadata-story.json', 'utf-8')) // load metadata 9 | res.render(__dirname + "/frontend/index.html", {metadataStory}); 10 | }) 11 | 12 | app.listen(process.env.PORT || 3555, () => { 13 | console.log("OK") 14 | }) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tele-ig", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "collector.js", 6 | "dependencies": { 7 | "dotenv": "^8.2.0", 8 | "ejs": "^3.1.6", 9 | "express": "^4.17.1", 10 | "fs-path": "0.0.25", 11 | "image-downloader": "^4.0.2", 12 | "instagram-private-api": "^1.43.3", 13 | "node-fetch": "^2.6.1", 14 | "node-telegram-bot-api": "^0.51.0", 15 | "webhook-discord": "^3.7.7" 16 | }, 17 | "devDependencies": {}, 18 | "scripts": { 19 | "frontend": "node index.js", 20 | "dispatch": "npm run dispatch-telegram", 21 | "dispatch-telegram": "node dispatcher-telegram.js", 22 | "dispatch-discord": "node dispatcher-discord.js", 23 | "collect": "node collector.js", 24 | "download": "node downloader.js", 25 | "test": "echo \"Error: no test specified\" && exit 1" 26 | }, 27 | "keywords": [], 28 | "author": "", 29 | "license": "ISC" 30 | } 31 | -------------------------------------------------------------------------------- /downloader.js: -------------------------------------------------------------------------------- 1 | const {loadData, storeData, downloadImage, upsertDirectory, downloadVideo} = require('./util') 2 | const story = loadData("metadata-story.json") 3 | const storyDownloaded = story.map(content => { 4 | if(!content.downloaded){ 5 | if(content.mediaType == 1){ // image 6 | const dir = `./images/${content.username}/${content.path}` 7 | const filename = `${content.id}.jpg` 8 | upsertDirectory(dir) 9 | downloadImage(content.url, dir, filename).then(() => { 10 | content.downloaded = true; 11 | }) 12 | content.downloaded = true; 13 | }else if(content.mediaType == 2){ 14 | const dir = `./videos/${content.username}/${content.path}` 15 | const filename = `${content.id}.mp4` 16 | upsertDirectory(dir) 17 | downloadVideo(content.url, dir, filename) 18 | content.downloaded = true; 19 | } 20 | } 21 | return content; 22 | }) 23 | 24 | storeData("metadata-story.json", storyDownloaded) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Iqbal syamil ayasy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "YogaSakti", 10 | "name": "Imperial Owl", 11 | "avatar_url": "https://avatars.githubusercontent.com/u/24309806?v=4", 12 | "profile": "https://github.com/YogaSakti", 13 | "contributions": [ 14 | "code" 15 | ] 16 | }, 17 | { 18 | "login": "2pai", 19 | "name": "Iqbal syamil ayasy", 20 | "avatar_url": "https://avatars.githubusercontent.com/u/22183588?v=4", 21 | "profile": "https://github.com/2pai", 22 | "contributions": [ 23 | "code", 24 | "doc" 25 | ] 26 | }, 27 | { 28 | "login": "fauzan121002", 29 | "name": "Muhammad Fauzan", 30 | "avatar_url": "https://avatars.githubusercontent.com/u/50759463?v=4", 31 | "profile": "http://fauzan.tech", 32 | "contributions": [ 33 | "code" 34 | ] 35 | } 36 | ], 37 | "contributorsPerLine": 7, 38 | "projectName": "instastory-monitor-telegram", 39 | "projectOwner": "2pai", 40 | "repoType": "github", 41 | "repoHost": "https://github.com", 42 | "skipCi": true 43 | } 44 | -------------------------------------------------------------------------------- /dispatcher-telegram.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | const {loadData, updateData} = require('./util') 3 | const TelegramBot = require('node-telegram-bot-api'); 4 | const token = process.env.TOKEN_TELEGRAM; 5 | const bot = new TelegramBot(token); 6 | const chatID = process.env.CHAT_ID; 7 | 8 | const metadataStory = loadData("./metadata-story.json"); 9 | 10 | metadataStory 11 | .map(story => { 12 | if (story.send_telegram) return 13 | if (story.mediaType == 1) { // Image 14 | bot.sendPhoto(chatID, story.url) 15 | .then((data) => { 16 | console.log(`Success send ${story.id} to ${data.chat.title || data.chat.username || data.chat.first_name || data.chat.last_name} (${data.chat.id})`) 17 | updateData("./metadata-story.json", story.id, true, "telegram") 18 | }).catch((error) => { 19 | console.log(`failed to send ${story.id}`) 20 | }) 21 | } else if (story.mediaType == 2) { // Videos 22 | bot.sendVideo(chatID, story.url) 23 | .then((data) => { 24 | console.log(`Success send ${story.id} to ${data.chat.title || data.chat.username || data.chat.first_name || data.chat.last_name} (${data.chat.id})`) 25 | updateData("./metadata-story.json", story.id, true, "telegram") 26 | }).catch((error) => { 27 | console.log(`failed to send ${story.id}`) 28 | }) 29 | } 30 | }) 31 | -------------------------------------------------------------------------------- /dispatcher-discord.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | 3 | const { 4 | loadData, 5 | updateData, 6 | } = require('./util') 7 | const webhook = require("webhook-discord") 8 | const webhookURL = process.env.DC_WEBHOOK_URL; 9 | const Hook = new webhook.Webhook(webhookURL) 10 | 11 | const metadataStory = loadData("./metadata-story.json"); 12 | 13 | metadataStory 14 | .map(story => { 15 | if (story.send_discord) return 16 | if (story.mediaType == 1) { // Image 17 | const msg = new webhook.MessageBuilder() 18 | .setName("SnapgramLord") 19 | .setColor("#aabbcc") 20 | .setImage(story.url) 21 | .setDescription( 22 | `Timestamp : ${story.timestamp}\n` 23 | ) 24 | 25 | Hook.send(msg) 26 | .then((data) => { 27 | updateData("./metadata-story.json", story.id, true, "discord") 28 | }).catch((error) => { 29 | console.log("failed to send: " + story.id) 30 | }) 31 | } else if (story.mediaType == 2) { // Videos 32 | 33 | const msg = new webhook.MessageBuilder() 34 | .setName("SnapgramLord") 35 | .setColor("#aabbcc") 36 | .setURL(story.url) 37 | .setDescription( 38 | `Timestamp : ${story.timestamp}\n` 39 | ) 40 | 41 | Hook.send(msg) 42 | .then((data) => { 43 | updateData("./metadata-story.json", story.id, true, "discord") 44 | }).catch((error) => { 45 | console.log("failed to send: " + story.id) 46 | }) 47 | } 48 | }) 49 | -------------------------------------------------------------------------------- /util.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const fsp = require('fs-path') 3 | const download = require('image-downloader') 4 | const fetch = require('node-fetch') 5 | 6 | const upsertDirectory = (path) => { 7 | if (!fs.existsSync(path)){ 8 | fs.mkdir(path, { recursive: true }, (err) => { 9 | if (err) throw err; 10 | }); 11 | } 12 | } 13 | 14 | const downloadImage = async (url, dir, filename) => { 15 | const opt = { 16 | url, 17 | dest: `${dir}/${filename}`, 18 | extractFilename: false 19 | } 20 | download.image(opt) 21 | .then(({ filename }) => { 22 | return true 23 | }) 24 | .catch((err) => { 25 | console.error(err) 26 | return false 27 | }) 28 | } 29 | const downloadVideo = async (url, dir, filename) => { 30 | const opt = { 31 | url, 32 | dest: `${dir}/${filename}` 33 | } 34 | const response = await fetch(opt.url); 35 | const buffer = await response.buffer(); 36 | 37 | fs.writeFile(opt.dest, buffer, () => true); 38 | 39 | } 40 | const storeData = (path, data) => { 41 | try { 42 | fsp.writeFileSync(path, JSON.stringify(data)) 43 | } catch (err) { 44 | console.error(err) 45 | } 46 | } 47 | const loadData = (path) => { 48 | try { 49 | const data = fs.readFileSync(path, 'utf8') 50 | return JSON.parse(data) 51 | } catch (err) { 52 | console.error(err) 53 | return false 54 | } 55 | } 56 | const updateData = function(path, id, value, mediaName) { 57 | const data = fs.readFileSync(path, 'utf8') 58 | const content = JSON.parse(data) 59 | if(mediaName == "discord"){ 60 | content.forEach((s) => s.id === id && (s.send_discord = value)) 61 | }else if(mediaName == "telegram"){ 62 | content.forEach((s) => s.id === id && (s.send_telegram = value)) 63 | } 64 | 65 | fsp.writeFileSync(path, JSON.stringify(content)) 66 | }; 67 | 68 | module.exports = { 69 | storeData, 70 | loadData, 71 | updateData, 72 | downloadImage, 73 | downloadVideo, 74 | upsertDirectory 75 | } 76 | -------------------------------------------------------------------------------- /collector.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | const { IgApiClient } = require("instagram-private-api"); 3 | const fs = require('fs'); 4 | const { storeData, loadData } = require('./util'); 5 | 6 | function saveCred(data) { 7 | fs.writeFileSync("session.json", JSON.stringify(data)) 8 | return data; 9 | } 10 | 11 | function credExist() { 12 | try { 13 | if (fs.existsSync("session.json")) { 14 | return true; 15 | } 16 | } catch (err) { 17 | console.error(err) 18 | return false; 19 | } 20 | } 21 | 22 | function credLoad() { 23 | const data = fs.readFileSync("session.json") 24 | // here you would load the data 25 | 26 | return JSON.parse(data); 27 | }; 28 | (async () => { 29 | const ig = new IgApiClient(); 30 | ig.state.generateDevice(process.env.IG_USERNAME); 31 | ig.state.proxyUrl = process.env.IG_PROXY; 32 | ig.request.end$.subscribe(async () => { 33 | const serialized = await ig.state.serialize(); 34 | delete serialized.constants; // this deletes the version info, so you'll always use the version provided by the library 35 | saveCred(serialized); 36 | }); 37 | if (credExist()) { 38 | // import state accepts both a string as well as an object 39 | // the string should be a JSON object 40 | await ig.state.deserialize(credLoad()); 41 | } 42 | 43 | await ig.account.login(process.env.IG_USERNAME, process.env.IG_PASSWORD); 44 | 45 | const parsedTarget = process.env.TARGET_USERNAME.split(","); 46 | const targets = await Promise.all(parsedTarget.map((target) => ig.user.searchExact(target))); // getting exact user by username 47 | 48 | targets.forEach(async (targetUser) => { 49 | const reelsFeed = ig.feed.reelsMedia({ // working with reels media feed (stories feed) 50 | userIds: [targetUser.pk], // you can specify multiple user id's, "pk" param is user id 51 | }); 52 | const storyItems = await reelsFeed.items(); // getting reels, see "account-followers.feed.example.ts" if you want to know how to work with feeds 53 | if (storyItems.length === 0) return console.log(`${targetUser.username}'s story is empty`); // we can check items length and find out if the user does have any story to watch 54 | 55 | console.log(`Found ${storyItems.length} stories on ${targetUser.username}`) 56 | const storyMetadata = loadData("./metadata-story.json") 57 | const storyUID = loadData("./storyUID.json") 58 | 59 | Promise.all(storyItems 60 | .filter(stry => !storyUID.includes(stry.id)) // check if story is already exists 61 | .map(r => { 62 | const { taken_at, id, media_type } = r 63 | const dt = new Date(taken_at * 1000) 64 | const storyElementTarget = media_type == 1 ? r.image_versions2.candidates : r.video_versions 65 | storyUID.push(r.id) 66 | storyMetadata.push({ 67 | username: targetUser.username, 68 | id, 69 | url: storyElementTarget[0].url, 70 | mediaType: media_type, // 1 for image, 2 for video 71 | path: dt.toLocaleDateString("eu-ES"), // make format yyyy/M/dd 72 | timestamp: dt.toLocaleString("id-ID"), 73 | send_telegram: false, //is story sended - telegram 74 | send_discord: false, //is story sended - discord 75 | downloaded: false, 76 | }) 77 | })).then(() => { 78 | storeData("./storyUID.json", storyUID) 79 | storeData("./metadata-story.json", storyMetadata) 80 | }) 81 | }); 82 | 83 | })(); -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 🐬 Instagram Story Explorer 8 | 9 | 10 | 11 |
12 |
13 |

🐙 InstaStory Explorer

14 |
15 |
16 |
17 | <% let contentPath =[];metadataStory.forEach(function(content) { %> 18 | <% if(!contentPath.includes(content.path)) { %> 19 |
20 |

🗓 <%= content.path %>

21 |
22 |
23 | <% contentPath.push(content.path)} %> 24 |
25 |
26 | <% if(content.mediaType == 1) { %> 27 | 28 | <% } else if(content.mediaType == 2) { %> 29 |
30 | 33 |
34 | <% } %> 35 |
36 |
Metadata
37 |

38 |

39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 66 | 67 | 68 |
🌟Username<%= content.username %>
🆔ID<%= content.id %>
📷Taken<%= content.timestamp %>
💾Downloaded<%= content.downloaded %>
🚀DispatchedTelegram : <%= content.send_telegram %>Discord : <%= content.send_discord %>
64 | 📂 Open Media 65 |
69 |
70 |

71 |
72 |
73 |
74 | <% }); %> 75 |
76 |
77 |
78 |     
79 | 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Instagram Story Monitor 2 | 3 | [![All Contributors](https://img.shields.io/badge/all_contributors-3-orange.svg?style=flat-square)](#contributors-) 4 | 5 | Monitor story your target instagram account & send to telegram on every update. This app also Inlcuding auto backup locally & frontend for local `metadata-story` explorer. 6 | ## How To Use 📚 7 | 1. Clone this Repository 8 | ``` 9 | git clone https://github.com/2pai/instastory-monitor-telegram 10 | ``` 11 | 2. Fill the .env file based on .env.example 12 | ``` 13 | cp .env.example .env 14 | ``` 15 | > Note: you can add more than one target in TARGET_USERNAME with comma separator without spaces (Example: instagram,facebook,google). for now there is no target limit, but keep in mind that the more targets the longer the process. 16 | 3. Install the dependency 17 | ``` 18 | npm install 19 | ``` 20 | 4. Run the command in order 21 | ```bash 22 | npm run collect # collect instagram story metadata 23 | npm run dispatch-telegram # dispatch / send instagram story to telegram 24 | npm run dispatch-discord # dispatch / send instagram story to discord 25 | npm run download # download content locally 26 | npm run frontend # run the frontend to serve metadata 27 | 28 | ``` 29 | or simply by using `orchestrator.sh` 30 | ```bash 31 | chmod +x ./orchestrator.sh 32 | ./orchestrator.sh 33 | ``` 34 | 35 | You can also run the orchestrator with CronJob to update the metadata & dispatch periodically. 36 | 37 | ## Architecture 🏹 38 | ![image](assets/Instagram-telegram.png) 39 | 40 | 41 | ### `collector.js` 42 | This collector will be collecting instagram story metadata from instagram private api then compare it (with local metadata) & store/append it to `metadata-story.json` 43 | 44 | The metadata structure will be 45 | ```js 46 | { 47 | username, // username target 48 | id, // uniquie id 49 | url, // url content 50 | mediaType, // 1 for image, 2 for video 51 | path, // path to store content locally, format yyyy/M/dd 52 | timestamp, 53 | send, // status dispatcher 54 | downloaded, // status downloader 55 | } 56 | ``` 57 | ### `dispatcher` 58 | This dispatcher will read the `metadata-story.json` and check if the `send` property was false then the dispatcher will send the media (based on media type) to the defined telegram chat/ discord channel. 59 | 60 | If the dispatcher success send the media, it will update the metadata `send` to true. 61 | 62 | > *Don't forget to specify your dispatcher in .env (default = telegram)* 63 | ### `downloader.js` 64 | This downloader will read the `metadata-story.json` and check if the `downloaded` property was false then the downloader will download the media (based on media type) locally to the defined `path` on metadata, the path was using `yyyy/M/dd` to make searching the content more easy (if needed). 65 | 66 | If the downloader success download the media locally, it will update the metadata `downloaded` to true. 67 | ### `index.js` (frontend) 68 | Serve the `metadata-story.json` with simple frontend to make exploring the instagram story more easily. 69 | 70 | ## Contributors ✨ 71 | 72 | Thanks goes to these wonderful people : 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 |

Imperial Owl

💻

Iqbal syamil ayasy

💻 📖

Muhammad Fauzan

💻
84 | 85 | 86 | 87 | 88 | 89 | 90 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! --------------------------------------------------------------------------------