├── .dockerignore ├── .tool-versions ├── .eslintignore ├── src ├── types.ts ├── features │ ├── index.ts │ ├── timeline.ts │ ├── hashtag.ts │ ├── utils.ts │ └── story.ts ├── index.ts ├── streams │ ├── utils.ts │ └── like.ts ├── core │ ├── utils.ts │ ├── logging.ts │ ├── store.ts │ └── config.ts ├── main.ts ├── cli.js └── session.ts ├── Dockerfile ├── .babelrc ├── .github └── workflows │ ├── deploy.yml │ └── tests.yml ├── CHANGELOG.md ├── index.ts ├── .eslintrc.yml ├── LICENSE ├── .gitignore ├── package.json ├── README.md └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 12.11.1 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | README.* 2 | LICENSE 3 | .DS_Store 4 | package-lock.json 5 | node_modules 6 | dist 7 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | interface User { 2 | pk: number; 3 | username: string; 4 | } 5 | 6 | export { 7 | User, 8 | }; 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12 2 | 3 | WORKDIR /usr/app 4 | 5 | COPY . . 6 | 7 | RUN npm install && npm run build 8 | 9 | EXPOSE 80 10 | 11 | CMD ["npm", "start"] 12 | -------------------------------------------------------------------------------- /src/features/index.ts: -------------------------------------------------------------------------------- 1 | import timeline from './timeline'; 2 | import hashtag from './hashtag'; 3 | import { storyView, storyMassView } from './story'; 4 | 5 | export { 6 | timeline, 7 | hashtag, 8 | storyView, 9 | storyMassView, 10 | }; 11 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ "@babel/preset-env", { "targets": { "node": "8" } } ], 4 | "@babel/typescript" 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-proposal-class-properties", 8 | "@babel/plugin-proposal-object-rest-spread", 9 | "@babel/plugin-transform-runtime" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import setup from './main'; 2 | import { Config } from './core/config'; 3 | import { 4 | timeline, 5 | hashtag, 6 | storyMassView, 7 | } from './features'; 8 | 9 | export default setup; 10 | 11 | export { 12 | setup, 13 | Config, 14 | 15 | timeline, 16 | hashtag, 17 | storyMassView, 18 | }; 19 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@master 14 | - name: build and install 15 | run: | 16 | npm install 17 | npm run test 18 | npm run build 19 | - name: publish npm package 20 | uses: epeli/npm-release@master 21 | with: 22 | type: stable 23 | token: ${{ secrets.NPM_TOKEN }} 24 | 25 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [Unreleased] 4 | 5 | ## [0.0.36] - 2019-11-05 6 | 7 | ### Added 8 | - chunking users for story mass view with random delay 9 | 10 | ## [0.0.35] - 2019-11-05 11 | 12 | ### Added 13 | - story mass view 14 | - changelog file 15 | 16 | ### Changed 17 | 18 | - catch exception when session file is invalid 19 | - advanced configuration setup 20 | 21 | ## [0.0.34] - 2019-11-01 22 | 23 | ### Added 24 | 25 | - like by timeline 26 | - like by hashtag 27 | - advanced configuration 28 | - easy start via cli 29 | - proxy support 30 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [8.x, 10.x, 12.x] 13 | 14 | steps: 15 | - uses: actions/checkout@master 16 | - name: use nodejs version ${{ matrix.node-version }} 17 | uses: actions/setup-node@master 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: npm install, build, and test 21 | run: | 22 | npm install 23 | npm run build 24 | npm test 25 | -------------------------------------------------------------------------------- /src/streams/utils.ts: -------------------------------------------------------------------------------- 1 | import logger from '../core/logging'; 2 | 3 | export function blacklistFilter(text: string, blacklist: string[]): boolean { 4 | if (!text) return true; 5 | 6 | for (const key of blacklist) { 7 | if (text.includes(key)) { 8 | logger.info('description is matching blacklisted word: %s', key); 9 | return false; 10 | } 11 | } 12 | 13 | return true; 14 | } 15 | 16 | export function interestRate(text: string, keywords: string[], base: number, inc: number): number { 17 | if (!text) return base; 18 | let interest = base; 19 | 20 | for (const key of keywords) { 21 | if (text.includes(key)) interest += inc; 22 | } 23 | 24 | return interest; 25 | } 26 | -------------------------------------------------------------------------------- /src/features/timeline.ts: -------------------------------------------------------------------------------- 1 | import { IgApiClient } from 'instagram-private-api'; 2 | import { Config } from '../core/config'; 3 | import { mediaFeed } from './utils'; 4 | import { store } from '../core/store'; 5 | import logger from '../core/logging'; 6 | 7 | async function timeline(client: IgApiClient, config: Config): Promise { 8 | // exit when like limit is reached 9 | if (config.likeLimit > 0) { 10 | // setup process exit when like limit reached 11 | store.pluck('imageLikes').subscribe(likes => { 12 | if (likes >= config.likeLimit) { 13 | logger.info('like limit reached. exiting process.'); 14 | process.exit(0); 15 | } 16 | }); 17 | } 18 | 19 | logger.info('starting with timeline feed'); 20 | return await mediaFeed(client, config, client.feed.timeline('pagination')); 21 | } 22 | 23 | export default timeline; 24 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | setup, 3 | Config, 4 | hashtag, 5 | timeline, 6 | storyMassView, 7 | } from './src'; 8 | 9 | require('dotenv').config(); 10 | 11 | async function main(): Promise { 12 | const workspace = './workspace'; 13 | 14 | const { IG_USERNAME, IG_PASSWORD } = process.env; 15 | const config = new Config( 16 | IG_USERNAME, 17 | IG_PASSWORD, 18 | workspace, 19 | ); 20 | 21 | const massview = false; 22 | //config.tags = ['vegan', 'world']; 23 | //config.likeLimit = 10; 24 | 25 | const client = await setup(config); 26 | 27 | if (massview) { 28 | await storyMassView(client, config); 29 | } 30 | 31 | if (config.tags.length) { 32 | // run hashtag feed 33 | await hashtag(client, config); 34 | } else { 35 | // run timeline feed 36 | await timeline(client, config); 37 | } 38 | } 39 | 40 | main(); 41 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | node: true 3 | es6: true 4 | extends: 5 | - 'eslint:recommended' 6 | - 'plugin:@typescript-eslint/recommended' 7 | parser: '@typescript-eslint/parser' 8 | parserOptions: 9 | ecmaVersion: 2018 10 | sourceType: module 11 | plugins: 12 | - '@typescript-eslint' 13 | rules: 14 | indent: 15 | - error 16 | - tab 17 | linebreak-style: 18 | - error 19 | - unix 20 | quotes: 21 | - error 22 | - single 23 | semi: 24 | - error 25 | - always 26 | 'no-console': 0 27 | 'no-undef': 0 28 | 'no-unused-vars': 29 | - warn 30 | 'require-atomic-updates': 0 31 | '@typescript-eslint/indent': 32 | - error 33 | - tab 34 | '@typescript-eslint/class-name-casing': 0 35 | '@typescript-eslint/interface-name-prefix': 0 36 | '@typescript-eslint/camelcase': 0 37 | '@typescript-eslint/no-non-null-assertion': 0 38 | '@typescript-eslint/ban-ts-ignore': 0 39 | '@typescript-eslint/no-explicit-any': 0 40 | -------------------------------------------------------------------------------- /src/core/utils.ts: -------------------------------------------------------------------------------- 1 | import bigInt from 'big-integer'; 2 | 3 | const sleep = (seconds: number): Promise => new Promise(resolve => setTimeout(resolve, seconds * 1000)); 4 | const chance = (percentage: number): boolean => Math.random() < percentage; 5 | 6 | function convertIDtoPost(mediaID: string): string { 7 | let id = bigInt(mediaID.split('_', 1)[0]); 8 | const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; 9 | let shortcode = ''; 10 | 11 | while (id.greater(0)) { 12 | const division = id.divmod(64); 13 | id = division.quotient; 14 | shortcode = `${alphabet.charAt(Number(division.remainder))}${shortcode}`; 15 | } 16 | 17 | return 'https://instagram.com/p/' + shortcode; 18 | } 19 | 20 | // returns a random number between (lowerBound, upperBound). upperBound is not included 21 | const random = (lowerBound: number, upperBound: number): number => 22 | lowerBound + Math.floor(Math.random()*(upperBound - lowerBound)); 23 | 24 | 25 | export { 26 | sleep, 27 | chance, 28 | convertIDtoPost, 29 | random, 30 | }; 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Felix Breuer 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 | -------------------------------------------------------------------------------- /src/core/logging.ts: -------------------------------------------------------------------------------- 1 | import winston from 'winston'; 2 | import { format } from 'winston'; 3 | import DailyRotateFile from 'winston-daily-rotate-file'; 4 | import path from 'path'; 5 | 6 | const fileLogFormat = winston.format.printf(({ level, message, timestamp }) => { 7 | return `${timestamp} ${level}: ${message}`; 8 | }); 9 | 10 | const logger = winston.createLogger({ 11 | levels: winston.config.npm.levels, 12 | transports: [ 13 | new winston.transports.Console({ 14 | format: format.combine( 15 | winston.format.splat(), 16 | winston.format.cli(), 17 | winston.format.align(), 18 | ), 19 | level: 'info', 20 | }), 21 | ], 22 | }); 23 | 24 | function addLogRotate(workspace: string): void { 25 | const logRotate = new DailyRotateFile({ 26 | format: format.combine(winston.format.splat(), winston.format.timestamp(), winston.format.padLevels(), fileLogFormat), 27 | level: 'debug', 28 | filename: path.resolve(workspace, 'logs', 'jinsta-%DATE%.log'), 29 | datePattern: 'YYYY-MM-DD', 30 | zippedArchive: true, 31 | maxSize: '20m', 32 | maxFiles: '7d', 33 | }); 34 | 35 | logger.add(logRotate); 36 | } 37 | 38 | export { 39 | addLogRotate, 40 | }; 41 | 42 | export default logger; 43 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { Config } from './core/config'; 2 | import login from './session'; 3 | import { addLogRotate } from './core/logging'; 4 | import { IgApiClient } from 'instagram-private-api'; 5 | import fs from 'fs'; 6 | import { liked$ } from './streams/like'; 7 | import { store } from './core/store'; 8 | 9 | function setupClient(config: Config): IgApiClient { 10 | // must be the first thing in the application start 11 | addLogRotate(config.workspacePath); 12 | 13 | // TODO check if config is valid 14 | if (!fs.existsSync(config.workspacePath)) fs.mkdirSync(config.workspacePath); 15 | // use username as seed as default 16 | if (!config.seed) config.seed = config.username; 17 | 18 | const client = new IgApiClient(); 19 | if (config.proxy) client.state.proxyUrl = config.proxy; 20 | 21 | return client; 22 | } 23 | 24 | async function setup(config: Config): Promise { 25 | const client = setupClient(config); 26 | 27 | await login(client, config); 28 | 29 | // push to store 30 | store.setState({ config, client }); 31 | 32 | // trigger the like pipeline 33 | // TODO make it hot per default 34 | liked$.subscribe(); 35 | 36 | return client; 37 | } 38 | 39 | export default setup; 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | .env 3 | 4 | workspace 5 | session.json 6 | data.json 7 | ts-dist 8 | dist 9 | 10 | # Logs 11 | logs 12 | *.log 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # TypeScript v1 declaration files 49 | typings/ 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional REPL history 58 | .node_repl_history 59 | 60 | # Output of 'npm pack' 61 | *.tgz 62 | 63 | # Yarn Integrity file 64 | .yarn-integrity 65 | 66 | # dotenv environment variables file 67 | .env 68 | 69 | # next.js build output 70 | .next 71 | -------------------------------------------------------------------------------- /src/core/store.ts: -------------------------------------------------------------------------------- 1 | import createStore from 'unistore'; 2 | import { IgApiClient } from 'instagram-private-api'; 3 | import { Observable } from 'rxjs'; 4 | import { 5 | publishReplay, 6 | pluck, 7 | distinctUntilChanged, 8 | } from 'rxjs/operators'; 9 | import { Config } from './config'; 10 | 11 | interface State { 12 | imageLikes: number; 13 | serverCalls: number; // TODO add a limit for this 14 | 15 | config: Config; 16 | client: IgApiClient; 17 | } 18 | 19 | type changeFunction = (state: State) => Partial; 20 | 21 | interface UniStoreObservable extends Observable { 22 | setState: (state: Partial, overwrite?: boolean) => void; 23 | getState: () => State; 24 | connect: () => void; 25 | change: (fn: changeFunction) => void; 26 | pluck: (key: string) => Observable; 27 | } 28 | 29 | // TODO without unistore 30 | const org_store = createStore(); 31 | const store: UniStoreObservable = Observable.create((observer: any) => { 32 | org_store.subscribe((state: any) => { 33 | observer.next(state); 34 | }); 35 | }).pipe(publishReplay(1)); 36 | 37 | store.connect(); // make it a hot observable 38 | 39 | // define store functions 40 | store.setState = (newState: Partial): void => org_store.setState(newState); 41 | store.getState = (): State => org_store.getState() as State; 42 | store.change = (fn: changeFunction): void => org_store.setState(fn(org_store.getState() as State)); 43 | store.pluck = (key: string): Observable => store.pipe(pluck(key), distinctUntilChanged()); 44 | 45 | const initState: Partial = { 46 | imageLikes: 0, 47 | serverCalls: 0, 48 | }; 49 | 50 | store.setState(initState); 51 | 52 | // useful functions 53 | const addServerCalls = (amount = 1): void => store.change(({ serverCalls }) => ({ serverCalls: serverCalls + amount })); 54 | 55 | export { 56 | store, 57 | State, 58 | UniStoreObservable, 59 | addServerCalls, 60 | }; 61 | -------------------------------------------------------------------------------- /src/core/config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { User } from '../types'; 3 | 4 | class Config { 5 | public username: string; 6 | public password: string; 7 | public proxy: string; 8 | 9 | // only for session restor 10 | public reset = false; 11 | public restore = false; 12 | public seed: string; 13 | public cookie: any; 14 | public user: User; 15 | 16 | // paths 17 | public workspacePath: string; 18 | public sessionPath: string; 19 | 20 | // application config 21 | public baseInterest = .2; 22 | public interestInc = .3; // should be adjusted if there are more / less keywords 23 | 24 | public mediaDelay = 3; 25 | 26 | // all these chances are getting multiplied by the base interest 27 | // example: chance to like a picture is => 28 | // (baseInterest + (number of matching keywords * interestInc)) * likeChance 29 | public likeChance = .5; 30 | public nestedFeedChance = .2; 31 | public dropFeedChance = .2; 32 | 33 | // program will exit when reached 34 | public likeLimit = 35; // 0 for disable 35 | 36 | public tags: string[] = []; 37 | 38 | public keywords = [ 39 | 'climate', 'sport', 'vegan', 'world', 'animal', 40 | 'vegetarian', 'savetheworld', 41 | ]; 42 | 43 | public blacklist = [ 44 | 'porn', 'naked', 'sex', 'vagina', 'penis', 'nude', 45 | 'tits', 'boobs', 'like4like', 'nsfw', 'sexy', 'drugs', 46 | 'babe', 'binary', 'bitcoin', 'crypto', 'forex', 'dick', 47 | 'squirt', 'gay', 'homo', 'nazi', 'jew', 'judaism', 48 | 'muslim', 'islam', 'hijab', 'niqab', 'farright', 49 | 'rightwing', 'conservative', 'death', 'racist', 'cbd', 50 | ]; 51 | 52 | constructor( 53 | username: string, 54 | password: string, 55 | workspace: string, 56 | ) { 57 | this.username = username; 58 | this.password = password; 59 | this.workspacePath = path.resolve(path.normalize(workspace)); 60 | this.sessionPath = path.resolve(this.workspacePath, 'session.json'); 61 | } 62 | } 63 | 64 | export { 65 | Config, 66 | }; 67 | -------------------------------------------------------------------------------- /src/features/hashtag.ts: -------------------------------------------------------------------------------- 1 | import { IgApiClient } from 'instagram-private-api'; 2 | import { Config } from '../core/config'; 3 | import { mediaFeed, likesForTags } from './utils'; 4 | import { store } from '../core/store'; 5 | import logger from '../core/logging'; 6 | 7 | async function hashtag(client: IgApiClient, config: Config): Promise { 8 | if (config.likeLimit <= 0) { 9 | logger.error('like limit has to be set for like by hashtag!'); 10 | return; 11 | } 12 | 13 | if (!config.tags) { 14 | logger.error('there are no tags given!'); 15 | return; 16 | } 17 | 18 | const likeBoundsForTags = likesForTags(config); 19 | let currentTagIndex = 0; 20 | 21 | let hashtagRunning = true; 22 | let tagRunning = false; 23 | 24 | // setup process exit when like limit reached 25 | store.pluck('imageLikes').subscribe(likes => { 26 | if (likes >= config.likeLimit) { 27 | logger.info('like limit reached for hashtag feed'); 28 | hashtagRunning = false; 29 | tagRunning = false; 30 | return; 31 | } 32 | 33 | // if it's the last tag then keep running untill likes >= imageLikes 34 | if (currentTagIndex == config.tags.length - 1) return; 35 | 36 | // the like limit for a tag of index i is equal to the sum of all the single tag limit between 0 and i. 37 | // if I've reached that limit simply go to the next tag. 38 | const currentTagLimit = likeBoundsForTags.reduce( 39 | (acc, val, index) => acc += index <= currentTagIndex ? val : 0 40 | ); 41 | 42 | if (likes >= currentTagLimit) tagRunning = false; 43 | }); 44 | 45 | for (const tag of config.tags) { 46 | // like limit is reached 47 | if (!hashtagRunning) break; 48 | 49 | tagRunning = true; 50 | logger.info( 51 | 'starting hashtag feed for tag: %s. randomized number of likes: %i.', 52 | tag, 53 | likeBoundsForTags[currentTagIndex], 54 | ); 55 | await mediaFeed(client, config, client.feed.tags(tag, 'recent'), () => tagRunning); 56 | currentTagIndex++; 57 | } 58 | } 59 | 60 | export default hashtag; 61 | -------------------------------------------------------------------------------- /src/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* eslint-disable */ 4 | 5 | var args = require('yargs') 6 | .scriptName('jinsta') 7 | .usage('Usage: $0 [options]') 8 | .options({ 9 | 'u': { 10 | alias: 'username', 11 | demandOption: true, 12 | type: 'string', 13 | describe: 'Instagram Login Username', 14 | implies: 'password', 15 | }, 16 | 'p': { 17 | alias: 'password', 18 | demandOption: true, 19 | type: 'string', 20 | describe: 'Instagram Login Password', 21 | implies: 'username', 22 | }, 23 | 'w': { 24 | alias: 'workspace', 25 | demandOption: true, 26 | type: 'string', 27 | normalize: true, 28 | describe: 'Folder where permanent data is stored', 29 | }, 30 | 's': { 31 | alias: 'seed', 32 | type: 'string', 33 | default: null, 34 | describe: 'Seed for generating the Device ID', 35 | }, 36 | 'r': { 37 | alias: 'reset', 38 | type: 'boolean', 39 | default: false, 40 | describe: 'Force to reset the Session', 41 | }, 42 | 't': { 43 | alias: [ 'tags', 'tag' ], 44 | type: 'array', 45 | default: [], 46 | describe: 'Uses given Tags for like by Hashtag', 47 | }, 48 | 'likeLimit': { 49 | alias: [ 'likelimit', 'like-limit' ], 50 | type: 'number', 51 | default: null, 52 | describe: 'Like limit when the bot should exit', 53 | }, 54 | 'storyMassView': { 55 | alias: [ 'storymassview', 'story-mass-view', 'smv' ], 56 | type: 'boolean', 57 | default: false, 58 | describe: 'All your not seen stories will instantly get seen', 59 | }, 60 | 'proxy': { 61 | type: 'string', 62 | default: null, 63 | describe: 'Proxy URL', 64 | }, 65 | }) 66 | .help('h') 67 | .alias('h', 'help') 68 | .alias('v', 'version') 69 | .showHelpOnFail(false, 'whoops, something went wrong! run with --help') 70 | .argv; 71 | 72 | var jinsta = require('jinsta'); 73 | 74 | var setup = jinsta.setup; 75 | var Config = jinsta.Config; 76 | var timeline = jinsta.timeline; 77 | var hashtag = jinsta.hashtag; 78 | var storyMassView = jinsta.storyMassView; 79 | 80 | var config = new Config( 81 | args.username, 82 | args.password, 83 | args.workspace, 84 | ); 85 | 86 | config.reset = args.reset; 87 | config.seed = args.seed; 88 | config.proxy = args.proxy; 89 | 90 | config.tags = args.tags; 91 | if (args.likeLimit) config.likeLimit = args.likeLimit; 92 | 93 | setup(config).then(function(client) { 94 | if (args.storrymassview) { 95 | storyMassView(client, config); 96 | } 97 | 98 | if (config.tags.length) { 99 | // run hashtag feed 100 | hashtag(client, config); 101 | } else { 102 | // run timeline feed 103 | timeline(client, config); 104 | } 105 | }); 106 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jinsta", 3 | "version": "0.0.38", 4 | "description": "javascript + instagram + flow", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "bin": { 8 | "jinsta": "dist/cli.js" 9 | }, 10 | "scripts": { 11 | "start": "npm run prod", 12 | "build": "babel src -d dist --extensions \".ts,.js\" --ignore '**/*.test.*' --ignore '**/__test__/**/*'", 13 | "prod": "node dist/index.js", 14 | "dev": "babel-node index.ts --extensions \".ts,.js\"", 15 | "dev:cli": "babel-node src/cli.ts --extensions \".ts,.js\"", 16 | "watch": "nodemon --ext js,graphql --watch src --exec npm run dev", 17 | "watch:docker": "npm run watch -- --legacy-watch", 18 | "test": "npm run lint && npm run type-check && npm run build", 19 | "lint": "eslint --config .eslintrc.yml --ext ts,js src", 20 | "lint:fix": "npm run lint -- --fix", 21 | "build:types": "tsc --emitDeclarationOnly", 22 | "type-check": "tsc --noEmit", 23 | "type-check:watch": "npm run type-check -- --watch", 24 | "clean:cache": "rm -rf ./node_modules/.cache/@babel", 25 | "deploy": "np --no-release-draft --no-publish", 26 | "deploy:beta": "np prerelease --tag=beta --no-release-draft --no-publish --any-branch" 27 | }, 28 | "pre-commit": [ 29 | "lint:fix" 30 | ], 31 | "repository": "github:breuerfelix/jinsta", 32 | "keywords": [ 33 | "instagram", 34 | "automation", 35 | "typescript", 36 | "flow", 37 | "bot" 38 | ], 39 | "author": "Felix Breuer", 40 | "license": "MIT", 41 | "bugs": { 42 | "url": "https://github.com/breuerfelix/jinsta/issues" 43 | }, 44 | "files": [ 45 | "dist/" 46 | ], 47 | "homepage": "https://github.com/breuerfelix/jinsta#readme", 48 | "devDependencies": { 49 | "@babel/cli": "^7.6.4", 50 | "@babel/core": "^7.6.4", 51 | "@babel/node": "^7.6.3", 52 | "@babel/plugin-proposal-class-properties": "^7.5.5", 53 | "@babel/plugin-proposal-object-rest-spread": "^7.6.2", 54 | "@babel/plugin-transform-runtime": "^7.6.2", 55 | "@babel/preset-env": "^7.6.3", 56 | "@babel/preset-typescript": "^7.6.0", 57 | "@types/dotenv": "^6.1.1", 58 | "@types/inquirer": "^6.5.0", 59 | "@types/node": "^12.7.12", 60 | "@types/yargs": "^13.0.3", 61 | "@typescript-eslint/eslint-plugin": "^2.3.3", 62 | "@typescript-eslint/parser": "^2.3.3", 63 | "babel-eslint": "^10.0.3", 64 | "dotenv": "^8.2.0", 65 | "eslint": "^6.5.1", 66 | "nodemon": "^1.19.3", 67 | "np": "^5.1.1", 68 | "pre-commit": "^1.2.2", 69 | "typescript": "^3.6.4" 70 | }, 71 | "dependencies": { 72 | "@babel/runtime": "^7.6.3", 73 | "big-integer": "^1.6.47", 74 | "inquirer": "^7.0.0", 75 | "instagram-private-api": "^1.31.0", 76 | "rxjs": "^6.5.3", 77 | "unistore": "^3.5.0", 78 | "winston": "^3.2.1", 79 | "winston-daily-rotate-file": "^4.2.1", 80 | "yargs": "^14.2.0" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/streams/like.ts: -------------------------------------------------------------------------------- 1 | import { Subject } from 'rxjs'; 2 | import { TimelineFeedResponseMedia_or_ad } from 'instagram-private-api/dist/responses'; 3 | import { chance, convertIDtoPost } from '../core/utils'; 4 | import { store, addServerCalls } from '../core/store'; 5 | import { 6 | withLatestFrom, 7 | filter, 8 | flatMap, 9 | share, 10 | tap, 11 | map, 12 | } from 'rxjs/operators'; 13 | import { blacklistFilter, interestRate } from './utils'; 14 | import logger from '../core/logging'; 15 | 16 | export const media$ = new Subject(); 17 | 18 | export const like$ = media$.pipe( 19 | filter(media => { 20 | // TODO just a test, could be removed if this seems to be true 21 | // detecting ads and filter them out 22 | if (media.ad_id || media.link) { 23 | logger.warn('media was an ad with id: %s / link: %s', media.ad_id, media.link); 24 | return false; 25 | } 26 | 27 | return true; 28 | }), 29 | filter(media => !media.has_liked), 30 | withLatestFrom(store.pluck('config')), 31 | filter(([media, { blacklist }]) => { 32 | if (!media.caption) return true; 33 | const { text } = media.caption; 34 | return blacklistFilter(text, blacklist); 35 | }), 36 | filter(([media, { keywords, baseInterest, interestInc }]) => { 37 | if (!media.caption) return true; 38 | const { text } = media.caption; 39 | return chance(interestRate(text, keywords, baseInterest, interestInc)); 40 | }), 41 | share(), 42 | ); 43 | 44 | export const liked$ = like$.pipe( 45 | withLatestFrom(store.pluck('client')), 46 | map(([[media, config], client]) => ([ media, config, client ])), 47 | flatMap(async ([media, config, client]) => { 48 | const { user } = config; 49 | 50 | let response: any = null; 51 | 52 | try { 53 | response = await client.media.like({ 54 | mediaId: media.id, 55 | moduleInfo: { 56 | module_name: 'profile', 57 | user_id: user.pk, 58 | username: user.username, 59 | }, 60 | // d means like by double tap (1), you cant unlike posts with double tap 61 | d: chance(.5) ? 0 : 1, 62 | }); 63 | } catch (e) { 64 | if (e.message.includes('deleted')) { 65 | response.status = 'not okay'; 66 | response.error = e; 67 | } else { throw e; } // throw the error 68 | } 69 | 70 | return { media, response, config }; 71 | }), 72 | filter(({ media, response}) => { 73 | if (response.status == 'ok') return true; 74 | 75 | logger.error('unable to like media: %o - response: %o', convertIDtoPost(media.id), response); 76 | return false; 77 | }), 78 | 79 | // get current likes from store 80 | withLatestFrom(store.pluck('imageLikes')), 81 | map(([{ media, response, config }, imageLikes]) => ({ media, response, config, imageLikes })), 82 | 83 | tap(({ media, response, config, imageLikes }) => { 84 | logger.info('liked %d / %d - media: %s - response: %o', imageLikes + 1, config.likeLimit, convertIDtoPost(media.id), response); 85 | // increment image likes 86 | store.setState({ imageLikes: imageLikes + 1 }); 87 | }), 88 | 89 | // increment server calls for the like call 90 | tap(() => addServerCalls(1)), 91 | 92 | share(), 93 | ); 94 | -------------------------------------------------------------------------------- /src/features/utils.ts: -------------------------------------------------------------------------------- 1 | import { IgApiClient } from 'instagram-private-api'; 2 | import { Feed } from 'instagram-private-api/dist/core/feed'; 3 | import { of } from 'rxjs'; 4 | import { concatMap, delay } from 'rxjs/operators'; 5 | import { Config } from '../core/config'; 6 | import { media$ } from '../streams/like'; 7 | import { sleep, random } from '../core/utils'; 8 | import logger from '../core/logging'; 9 | import { addServerCalls } from '../core/store'; 10 | import { User } from '../types'; 11 | 12 | export async function mediaFeed( 13 | client: IgApiClient, 14 | config: Config, 15 | feed: Feed, 16 | cb = (): boolean => true, 17 | ): Promise { 18 | const allMediaIDs: string[] = []; 19 | let running = true; 20 | let progress = 1; 21 | 22 | while (running) { 23 | const items = await feed.items(); 24 | addServerCalls(1); 25 | 26 | // filter out old items 27 | const newItems = items.filter(item => !allMediaIDs.includes(item.id)); 28 | allMediaIDs.push(...newItems.map(item => item.id)); 29 | 30 | logger.info( 31 | 'got %d more media for user \'%s\'', 32 | newItems.length, 33 | config.username, 34 | ); 35 | 36 | // exit when no new items are there 37 | if (!newItems.length) running = false; 38 | 39 | for (const item of newItems) { 40 | logger.info( 41 | 'current progress: %d / %d', 42 | progress, 43 | allMediaIDs.length, 44 | ); 45 | 46 | media$.next(item); 47 | 48 | progress++; 49 | await sleep(config.mediaDelay); 50 | 51 | // break out when callback returns true 52 | if (!cb()) { 53 | running = false; 54 | break; 55 | } 56 | } 57 | } 58 | } 59 | 60 | /** 61 | Used to calculate how many likes to give in each tag without exceding the maximum like number. 62 | Input: config with likeLimit and tags. 63 | Return: an array of length tags.length and as value integer number which together will sum up to be (approximately) likeLimit. 64 | */ 65 | export function likesForTags(config: Config): Array { 66 | const likeNumber = config.likeLimit; 67 | const tagsNumber = config.tags ? config.tags.length : 0; 68 | if (!likeNumber || !tagsNumber) return []; 69 | 70 | let sum = 0; 71 | const array = []; 72 | 73 | for (let i = 0; i < tagsNumber; i++) { 74 | const current = Math.random() * 100; 75 | sum += current; 76 | array.push(current); 77 | } 78 | 79 | return array.map(i => Math.round((i / sum) * likeNumber)); 80 | } 81 | 82 | 83 | export async function getFollowers( 84 | client: IgApiClient, 85 | username: string 86 | ): Promise { 87 | const id = await client.user.getIdByUsername(username); 88 | const userInfo = await client.user.info(id); 89 | const followersFeed = client.feed.accountFollowers(id); 90 | const followerList: object[] = []; 91 | let progress = 0; 92 | 93 | logger.info('starting to get follower list from %s. Total followers: %s', 94 | username, 95 | userInfo.follower_count 96 | ); 97 | 98 | return new Promise((resolve, reject) => 99 | followersFeed.items$ 100 | .pipe( 101 | concatMap(x => of(x) 102 | .pipe( 103 | delay(random(2000, 5000))) 104 | ) 105 | ) 106 | .subscribe( 107 | followers => { 108 | progress += followers.length; 109 | 110 | logger.info( 111 | 'current progress: %d / %d', 112 | progress, 113 | userInfo.follower_count 114 | ); 115 | 116 | followerList.push(followers.map((el: User) => el.pk)); 117 | }, 118 | error => reject(error), 119 | () => resolve([].concat(...followerList)) 120 | ) 121 | ); 122 | } 123 | -------------------------------------------------------------------------------- /src/features/story.ts: -------------------------------------------------------------------------------- 1 | import { IgApiClient } from 'instagram-private-api'; 2 | import { Config } from '../core/config'; 3 | import logger from '../core/logging'; 4 | import { sleep, random } from '../core/utils'; 5 | 6 | /** 7 | This function will attempt to see all the stories of a given user set. 8 | Input: 9 | - client: client to use 10 | - userIds: array of user's pk to see 11 | - lastSeenPerUser: 12 | (optional) obj of the form {"user1.pk":"user1.seen","user2.pk":"user2.seen"} 13 | from all the retrieved stories of an user only those taken after the specified `seen` attribute will be actually viewed. 14 | For example this is used by storyMassView to avoid viewing multiple time stories. 15 | */ 16 | async function storyView( 17 | client: IgApiClient, 18 | userIds: number[], 19 | lastSeenPerUser?: any, 20 | ): Promise { 21 | const NAMESPACE = 'STORY'; 22 | 23 | //get all the stories from the users picked above 24 | if(!userIds || userIds.length == 0) { 25 | logger.error(`[${NAMESPACE}] tried to view story without passing an array of user ids`); 26 | return; 27 | } 28 | 29 | let currentIndex = 0; 30 | let viewedStories = 0; 31 | while (currentIndex < userIds.length) { 32 | const topIndex = random(currentIndex + 1, Math.min(currentIndex + 6, userIds.length)); 33 | 34 | const storyMediaFeed = client.feed.reelsMedia({ 35 | userIds: userIds.slice(currentIndex, topIndex), 36 | }); 37 | 38 | let stories = await storyMediaFeed.items(); 39 | if (!stories.length) { 40 | logger.info(`[${NAMESPACE}] no stories for given users available`); 41 | return; 42 | } 43 | 44 | if (lastSeenPerUser) { 45 | //from all the stories of the user only get those how i've not yet seen. 46 | stories = stories.filter( 47 | ({ taken_at, user: { pk } }) => 48 | !lastSeenPerUser[pk] || taken_at > lastSeenPerUser[pk] 49 | ); 50 | } 51 | 52 | if (!stories.length) { 53 | logger.info(`[${NAMESPACE}] no new stories to view`); 54 | return; 55 | } 56 | 57 | let logString = `[${NAMESPACE}] stories already viewed: ${viewedStories}. now viewing: ${stories.length}. `; 58 | logString += `still ${Math.max(userIds.length - topIndex, 0)} users to fetch.`; 59 | logger.info(logString); 60 | 61 | await sleep(random(5, 10)); 62 | 63 | // view stories 64 | await client.story.seen(stories); 65 | 66 | viewedStories += stories.length; 67 | currentIndex = topIndex + 1; 68 | } 69 | } 70 | 71 | /** 72 | This function will attemp to view all the new (not yet seen) stories showed in the top of the timeline 73 | */ 74 | async function storyMassView( 75 | client: IgApiClient, 76 | config: Config 77 | ): Promise { 78 | const NAMESPACE = 'STORY MASS VIEW'; 79 | 80 | logger.info(`[${NAMESPACE}] starting viewing stories from personal feed`); 81 | 82 | // get users who have stories to see in instagram tray (top bar) 83 | const storyTrayFeed = client.feed.reelsTray('cold_start'); 84 | let storyTrayItems = await storyTrayFeed.items(); 85 | storyTrayItems = storyTrayItems.filter( 86 | (item: any) => 87 | item.seen < item.latest_reel_media && 88 | item.user.username != config.username 89 | ); 90 | 91 | if (!storyTrayItems.length) { 92 | logger.info(`[${NAMESPACE}] no stories left to view!`); 93 | return; 94 | } 95 | 96 | const userIds: number[] = []; 97 | const lastSeenPerUser: any = {}; 98 | 99 | storyTrayItems.forEach(({ seen, user: { pk } }) => { 100 | lastSeenPerUser[pk] = seen; 101 | userIds.push(pk); 102 | }); 103 | 104 | // view stories 105 | await storyView(client, userIds, lastSeenPerUser); 106 | } 107 | 108 | export { storyView, storyMassView }; 109 | -------------------------------------------------------------------------------- /src/session.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IgApiClient, 3 | IgCheckpointError, 4 | IgLoginTwoFactorRequiredError, 5 | } from 'instagram-private-api'; 6 | import { Config } from './core/config'; 7 | import { User } from './types'; 8 | import inquirer from 'inquirer'; 9 | import fs from 'fs'; 10 | import logger from './core/logging'; 11 | 12 | interface sessionFile { 13 | cookie: any; 14 | user: any; 15 | seed: string; 16 | restore: boolean; 17 | } 18 | 19 | function parseSession(filepath: string): sessionFile { 20 | let cookie = null; 21 | let user = null; 22 | let seed = null; 23 | let restore = false; 24 | 25 | if (fs.existsSync(filepath)) { 26 | const content = fs.readFileSync(filepath, 'utf-8'); 27 | const data = JSON.parse(content); 28 | 29 | cookie = data.cookie; 30 | seed = data.seed; 31 | user = data.user; 32 | restore = true; 33 | } 34 | 35 | const config = { 36 | cookie, 37 | user, 38 | seed, 39 | restore, 40 | }; 41 | 42 | return config; 43 | } 44 | 45 | function restore(config: Config): void { 46 | // try to parse session from file 47 | const additionalConfig = parseSession(config.sessionPath); 48 | config.restore = additionalConfig.restore; 49 | config.cookie = additionalConfig.cookie; 50 | config.seed = additionalConfig.seed || config.seed; 51 | config.user = additionalConfig.user; 52 | } 53 | 54 | async function solveChallenge(client: IgApiClient): Promise { 55 | await client.challenge.auto(true); // Requesting sms-code or click "It was me" button 56 | const { code } = await inquirer.prompt([{ 57 | type: 'input', 58 | name: 'code', 59 | message: 'Enter sms / email code:', 60 | }]); 61 | 62 | const res = await client.challenge.sendSecurityCode(code); 63 | return res.logged_in_user!; 64 | } 65 | 66 | async function twoFactorLogin(client: IgApiClient, config: Config, err: any): Promise { 67 | const twoFactorIdentifier = err.response.body.two_factor_info.two_factor_identifier; 68 | if (!twoFactorIdentifier) { 69 | throw 'Unable to login, no 2fa identifier found'; 70 | } 71 | // At this point a code should have been received via SMS 72 | // Get SMS code from stdin 73 | const { code } = await inquirer.prompt([ 74 | { 75 | type: 'input', 76 | name: 'code', 77 | message: 'Enter sms code:', 78 | }, 79 | ]); 80 | 81 | // Use the code to finish the login process 82 | return await client.account.twoFactorLogin({ 83 | username: config.username, 84 | verificationCode: code, 85 | twoFactorIdentifier, 86 | verificationMethod: '1', // '1' = SMS (default), '0' = OTP 87 | trustThisDevice: '1', // Can be omitted as '1' is used by default 88 | }); 89 | } 90 | 91 | // TODO when event system is implemented, save session on exit 92 | async function saveSession(client: IgApiClient, config: Config): Promise { 93 | const cookie = await client.state.serializeCookieJar(); 94 | const { sessionPath, user, seed } = config; 95 | fs.writeFile( 96 | sessionPath, 97 | JSON.stringify({ cookie, seed, user }), 98 | 'utf-8', err => err ? logger.error(err) : void 0, 99 | ); 100 | 101 | logger.info('session saved'); 102 | } 103 | 104 | async function login(client: IgApiClient, config: Config): Promise { 105 | if (!config.reset) restore(config); 106 | 107 | client.state.generateDevice(config.seed); 108 | 109 | if (config.restore) { 110 | await client.state.deserializeCookieJar(config.cookie); 111 | 112 | try { 113 | // check if session is still valid 114 | const tl = client.feed.timeline('warm_start_fetch'); 115 | await tl.items(); 116 | logger.info('session restored'); 117 | return config.user; 118 | } catch { 119 | logger.info('session expired, going for relogin'); 120 | } 121 | } 122 | 123 | logger.info(`logging in with user: ${config.username} ...`); 124 | await client.simulate.preLoginFlow(); 125 | let user = null; 126 | 127 | try { 128 | user = await client.account.login(config.username, config.password); 129 | } catch (e) { 130 | if (e instanceof IgCheckpointError) { 131 | user = await solveChallenge(client); 132 | } else if (e instanceof IgLoginTwoFactorRequiredError) { 133 | user = await twoFactorLogin(client, config, e); 134 | } 135 | } 136 | 137 | await client.simulate.postLoginFlow(); 138 | 139 | config.user = user; 140 | logger.info(`user: ${config.username} logged in successfully`); 141 | 142 | await saveSession(client, config); 143 | 144 | return user!; 145 | } 146 | 147 | export default login; 148 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jinsta 2 | 3 | special thanks to [@timgrossmann](https://github.com/timgrossmann) for creating [instapy](https://github.com/timgrossmann/instapy) !! (: 4 | 5 | check out our [discord channel](https://discord.gg/FDETsht) and chat me there to get an invite for the channel :) 6 | 7 | **this bot is experimental!** 8 | unfortunately this approach doesnt seem to lower the blocks and bans of instagram bots. dont use this bot if you search for a stable one. 9 | 10 | ## getting started basic 11 | 12 | - install [nodejs](https://nodejs.org) 13 | - open the terminal 14 | - `npm install -g jinsta` 15 | - `jinsta -u instagram_username -p instagram_password -w workspace_path` 16 | - the `-w` parameter could be `./jinsta_data` for example 17 | - jinsta will save log files and session data in this folder 18 | - run `jinsta --help` for additional information 19 | 20 | ## update 21 | 22 | - `npm update -g jinsta` 23 | 24 | ## advanced configuration 25 | 26 | ### example index.js 27 | 28 | - install [nodejs](https://nodejs.org) 29 | - open the terminal and create a new folder 30 | - the name the folder must be different than 'jinsta' 31 | - `npm init -y` 32 | - `npm install jinsta` 33 | - create a new file `index.js` 34 | 35 | #### plain javascript 36 | 37 | ```js 38 | var jinsta = require('jinsta'); 39 | 40 | var setup = jinsta.default; 41 | var Config = jinsta.Config; 42 | var timeline = jinsta.timeline; 43 | var hashtag = jinsta.hashtag; 44 | var storyMassView = jinsta.storyMassView; 45 | 46 | var config = new Config( 47 | 'instagram_username', 48 | 'instagram_password', 49 | 'workspace_path' // like './jinsta_data' 50 | ); 51 | 52 | var massview = true; 53 | 54 | // have a look at https://github.com/breuerfelix/jinsta/blob/master/src/core/config.ts for an example 55 | config.likeLimit = 30; 56 | // you can edit every property you want 57 | // just do it like we change the keywords here 58 | config.keywords = [ 'vegan', 'climate', 'sports' ]; 59 | 60 | setup(config).then(function(client) { 61 | if (massview) { 62 | storyMassView(client, config); 63 | } 64 | 65 | if (config.tags.length) { 66 | // run hashtag feed 67 | hashtag(client, config); 68 | } else { 69 | // run timeline feed 70 | timeline(client, config); 71 | } 72 | }); 73 | ``` 74 | 75 | - `node index.js` to start the bot 76 | 77 | #### es6 javascript 78 | 79 | ```js 80 | import { 81 | setup, 82 | Config, 83 | hashtag, 84 | timeline, 85 | storyMassView, 86 | } from 'jinsta'; 87 | 88 | async function main() { 89 | const workspace = './workspace'; 90 | 91 | const { IG_USERNAME, IG_PASSWORD } = process.env; 92 | const config = new Config( 93 | IG_USERNAME, 94 | IG_PASSWORD, 95 | workspace, 96 | ); 97 | 98 | const massview = false; 99 | //config.tags = ['vegan', 'world']; 100 | //config.likeLimit = 10; 101 | 102 | const client = await setup(config); 103 | 104 | if (massview) { 105 | await storyMassView(client, config); 106 | } 107 | 108 | if (config.tags.length) { 109 | // run hashtag feed 110 | await hashtag(client, config); 111 | } else { 112 | // run timeline feed 113 | await timeline(client, config); 114 | } 115 | } 116 | 117 | main(); 118 | ``` 119 | 120 | ### proxy 121 | 122 | if you're running jinsta on a server in the internet or in a cloud environment it could be really helpful to use a proxy, so it is not that easy for instagram to catch you up. if you are running jinsta from home this may not be needed. 123 | 124 | there are two ways to achieve this: 125 | 1. append `--proxy ip.ip.ip.ip:port` on the commandline 126 | 2. set following configuration in the advanced configuration: `config.proxy = 'ip.ip.ip.ip:port'` 127 | 128 | ### like by hashtag 129 | 130 | the bot will go through all `tags` and split the like limit randomly between given tags. 131 | 132 | **simple:** `jinsta --tags climate vegan sport --likeLimit 10` 133 | 134 | **advanced:** 135 | ```js 136 | // the bot will like 10 images with the hashtag vegan 137 | // and then 10 images with the hashtag climate 138 | config.likeLimit = 10; 139 | config.tags = ['vegan', 'climate', 'sport']; 140 | ``` 141 | 142 | ## contribute 143 | 144 | - clone this repo 145 | - `npm install` 146 | - create a file named `.env` in the root folder 147 | 148 | ```env 149 | IG_USERNAME=instagram_username 150 | IG_PASSWORD=instagram_password 151 | ``` 152 | 153 | - `npm run dev` to start the bot 154 | 155 | ## additional information / helpful another projects 156 | 157 | - [jinsta_starter](https://github.com/demaya/jinsta_starter/): helpful for scheduling jinsta, e.g. on a raspberry pi at home 158 | 159 | --- 160 | 161 | _we love lowercase_ 162 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | // "lib": [], /* Specify library files to be included in the compilation. */ 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 12 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 13 | // "outFile": "./", /* Concatenate and emit output to single file. */ 14 | "outDir": "./dist", /* Redirect output structure to the directory. */ 15 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 16 | // "composite": true, /* Enable project compilation */ 17 | // "incremental": true, /* Enable incremental compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | "strictNullChecks": false, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | "typeRoots": ["node_modules/@types"], 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | 52 | /* Source Map Options */ 53 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 54 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 55 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 56 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 57 | 58 | /* Experimental Options */ 59 | "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */ 60 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 61 | }, 62 | "include": [ 63 | "src/**/*.ts" 64 | ], 65 | "exclude": [ 66 | "node_modules" 67 | ] 68 | } 69 | --------------------------------------------------------------------------------