├── client ├── src │ ├── css │ │ ├── PostStats.scss │ │ ├── PostStatsList.scss │ │ ├── Percentage.scss │ │ ├── Home.scss │ │ ├── FormModal.scss │ │ ├── ProfileList.scss │ │ ├── LoadingBar.scss │ │ └── LoadingSpinner.scss │ ├── react-app-env.d.ts │ ├── helpers │ │ └── constants.ts │ ├── interfaces │ │ ├── IPost.ts │ │ └── IProfile.ts │ ├── index.tsx │ └── components │ │ ├── LoadingBar.tsx │ │ ├── LoadingSpinner.tsx │ │ ├── Percentage.tsx │ │ ├── PostStatsList.tsx │ │ ├── FormModal.tsx │ │ ├── ProfileList.tsx │ │ ├── PostStats.tsx │ │ ├── Home.tsx │ │ ├── PostModal.tsx │ │ └── ProfileModal.tsx ├── public │ ├── favicon.ico │ ├── manifest.json │ └── index.html ├── .env ├── tsconfig.json └── package.json ├── backend ├── nodemon.json ├── app │ ├── public │ │ ├── favicon.ico │ │ ├── manifest.json │ │ ├── precache-manifest.9029fd04bb2e8bbaeb43584b55b39c07.js │ │ ├── asset-manifest.json │ │ ├── service-worker.js │ │ ├── static │ │ │ ├── js │ │ │ │ ├── runtime~main.a8a9905a.js │ │ │ │ ├── runtime~main.a8a9905a.js.map │ │ │ │ ├── main.171b13fe.chunk.js │ │ │ │ └── main.171b13fe.chunk.js.map │ │ │ └── css │ │ │ │ ├── main.49f7432d.chunk.css │ │ │ │ └── main.49f7432d.chunk.css.map │ │ └── index.html │ ├── eventBus.js │ ├── routes │ │ ├── index.js │ │ ├── api-types.js │ │ ├── post-routes.js │ │ └── profile-routes.js │ ├── models │ │ ├── log-model.js │ │ ├── post-history-model.js │ │ ├── profile-history-model.js │ │ ├── post-queue-model.js │ │ ├── profile-queue-model.js │ │ ├── post-model.js │ │ ├── profile-model.js │ │ ├── base-model.js │ │ └── mongodb.js │ ├── logger.js │ ├── server.js │ ├── controller │ │ ├── post-controller.js │ │ └── profile-controller.js │ └── scrapper.js ├── .env ├── package.json ├── index.js └── .eslintrc.js ├── .gitignore ├── LICENSE └── README.md /client/src/css/PostStats.scss: -------------------------------------------------------------------------------- 1 | .nav-link { 2 | cursor: pointer; 3 | } -------------------------------------------------------------------------------- /client/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /client/src/css/PostStatsList.scss: -------------------------------------------------------------------------------- 1 | .postPathCol { 2 | width: 120px; 3 | } -------------------------------------------------------------------------------- /client/src/helpers/constants.ts: -------------------------------------------------------------------------------- 1 | export const apiBackendUrl = process.env.REACT_APP_BACKEND_URL; -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivanvaladares/instagram-scrapper/HEAD/client/public/favicon.ico -------------------------------------------------------------------------------- /backend/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": false, 3 | "ignore": [ 4 | "data/*", 5 | "app/public/*" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /client/src/css/Percentage.scss: -------------------------------------------------------------------------------- 1 | .percUp { 2 | color: forestgreen; 3 | } 4 | 5 | .percDown { 6 | color: red; 7 | } -------------------------------------------------------------------------------- /backend/app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivanvaladares/instagram-scrapper/HEAD/backend/app/public/favicon.ico -------------------------------------------------------------------------------- /client/.env: -------------------------------------------------------------------------------- 1 | REACT_APP_BACKEND_URL=http://localhost:3001/ 2 | REACT_APP_APPLICATION_INSIGHTS_KEY=00000000-0000-0000-0000-000000000000 -------------------------------------------------------------------------------- /backend/app/eventBus.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events'); 2 | const emitter = new EventEmitter(); 3 | 4 | module.exports = emitter; -------------------------------------------------------------------------------- /backend/app/routes/index.js: -------------------------------------------------------------------------------- 1 | exports.init = (app) => { 2 | app.use("/profile", require("./profile-routes")); 3 | app.use("/post", require("./post-routes")); 4 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /backend/node_modules 2 | /client/node_modules 3 | /client/.pnp 4 | /client/.pnp.js 5 | /client/coverage 6 | /client/build 7 | /.vs 8 | package-lock.json 9 | .vscode -------------------------------------------------------------------------------- /backend/app/models/log-model.js: -------------------------------------------------------------------------------- 1 | const baseModel = require('./base-model.js'); 2 | const collection = "is-log"; 3 | 4 | exports.save = (entry) => { 5 | return baseModel.save(collection, entry); 6 | }; -------------------------------------------------------------------------------- /client/src/interfaces/IPost.ts: -------------------------------------------------------------------------------- 1 | export interface IPost { 2 | path: string, 3 | published: number, 4 | likeCount: number, 5 | commentCount: number, 6 | likePercentage: number, 7 | commentPercentage: number 8 | } -------------------------------------------------------------------------------- /client/src/index.tsx: -------------------------------------------------------------------------------- 1 | import 'bootstrap/dist/css/bootstrap.css'; 2 | 3 | import * as React from 'react'; 4 | import * as ReactDOM from 'react-dom'; 5 | 6 | import Home from './components/Home'; 7 | 8 | ReactDOM.render(, document.getElementById('root')); -------------------------------------------------------------------------------- /client/src/css/Home.scss: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 10px; 3 | font-family: Arial, Helvetica, sans-serif 4 | } 5 | 6 | hr { 7 | border-top: 1px dotted #000000 !important; 8 | margin-bottom: 5px !important; 9 | margin-top: 5px !important; 10 | } 11 | -------------------------------------------------------------------------------- /backend/.env: -------------------------------------------------------------------------------- 1 | APPLICATION_INSIGHTS_KEY=00000000-0000-0000-0000-000000000000 2 | MONGODB_URI=mongodb://:@ 3 | MONGODB_DBNAME= 4 | MONGODB_CREATE=true 5 | MAX_PROFILES=3 6 | MAX_DOWNLOADS=50 7 | ISCRAPPER_READONLY=false 8 | ENVIRONMENT=DEV -------------------------------------------------------------------------------- /client/src/components/LoadingBar.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import * as React from 'react'; 4 | import '../css/LoadingBar.scss'; 5 | 6 | 7 | export default class LoadingBar extends React.Component<{}> { 8 | 9 | render() { 10 | 11 | return (
12 |
13 |
); 14 | 15 | } 16 | } -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Instagram Scrapper", 3 | "name": "Instagram Scrapper", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /backend/app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Instagram Scrapper", 3 | "name": "Instagram Scrapper", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /client/src/components/LoadingSpinner.tsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | import * as React from 'react'; 4 | import '../css/LoadingSpinner.scss'; 5 | 6 | 7 | export default class LoadingSpinner extends React.Component<{}> { 8 | 9 | render() { 10 | 11 | return (
12 |
13 |
14 |
15 |
); 16 | 17 | } 18 | } -------------------------------------------------------------------------------- /backend/app/models/post-history-model.js: -------------------------------------------------------------------------------- 1 | const baseModel = require('./base-model.js'); 2 | const collection = "is-post-history"; 3 | 4 | exports.find = (criteria, sortBy, limit) => { 5 | return baseModel.find(collection, criteria, sortBy, limit); 6 | }; 7 | 8 | exports.save = (entry) => { 9 | return baseModel.save(collection, entry); 10 | }; 11 | 12 | exports.remove = (criteria) => { 13 | return baseModel.remove(collection, criteria); 14 | }; -------------------------------------------------------------------------------- /backend/app/models/profile-history-model.js: -------------------------------------------------------------------------------- 1 | const baseModel = require('./base-model.js'); 2 | const collection = "is-profile-history"; 3 | 4 | exports.find = (criteria, sortBy, limit) => { 5 | return baseModel.find(collection, criteria, sortBy, limit); 6 | }; 7 | 8 | exports.save = (entry) => { 9 | return baseModel.save(collection, entry); 10 | }; 11 | 12 | exports.remove = (criteria) => { 13 | return baseModel.remove(collection, criteria); 14 | }; -------------------------------------------------------------------------------- /backend/app/models/post-queue-model.js: -------------------------------------------------------------------------------- 1 | const baseModel = require('./base-model.js'); 2 | const collection = "is-post-queue"; 3 | 4 | exports.find = (criteria, sortBy, limit) => { 5 | return baseModel.find(collection, criteria, sortBy, limit); 6 | }; 7 | 8 | exports.save = (entry) => { 9 | return baseModel.save(collection, entry); 10 | }; 11 | 12 | exports.update = (query, update) => { 13 | return baseModel.update(collection, query, update); 14 | }; 15 | 16 | exports.remove = (criteria) => { 17 | return baseModel.remove(collection, criteria); 18 | }; -------------------------------------------------------------------------------- /backend/app/models/profile-queue-model.js: -------------------------------------------------------------------------------- 1 | const baseModel = require('./base-model.js'); 2 | const collection = "is-profile-queue"; 3 | 4 | exports.find = (criteria, sortBy, limit) => { 5 | return baseModel.find(collection, criteria, sortBy, limit); 6 | }; 7 | 8 | exports.save = (entry) => { 9 | return baseModel.save(collection, entry); 10 | }; 11 | 12 | exports.update = (query, update) => { 13 | return baseModel.update(collection, query, update); 14 | }; 15 | 16 | exports.remove = (criteria) => { 17 | return baseModel.remove(collection, criteria); 18 | }; -------------------------------------------------------------------------------- /client/src/css/FormModal.scss: -------------------------------------------------------------------------------- 1 | 2 | .float{ 3 | position:fixed; 4 | width:60px; 5 | height:60px; 6 | bottom:40px; 7 | right:40px; 8 | background-color:#0C9; 9 | color:#FFF; 10 | border-radius:50px; 11 | text-align:center; 12 | box-shadow: 2px 2px 3px #999; 13 | opacity: 0.7; 14 | transition: opacity 0.2s ease-in-out; 15 | 16 | &:hover { 17 | opacity: 1; 18 | } 19 | } 20 | 21 | .my-float{ 22 | margin-top:22px; 23 | } 24 | 25 | @media only screen and (max-width: 600px) { 26 | .float{ 27 | bottom:20px; 28 | right:20px; 29 | } 30 | } -------------------------------------------------------------------------------- /client/src/css/ProfileList.scss: -------------------------------------------------------------------------------- 1 | .list-group-item { 2 | padding: 10px; 3 | 4 | .progress { 5 | margin-bottom: 0px !important; 6 | } 7 | 8 | .profile-name { 9 | font-size: 16pt; 10 | cursor: pointer; 11 | text-overflow: ellipsis; 12 | overflow: hidden; 13 | 14 | small { 15 | font-size: 10px; 16 | color: grey; 17 | display: block; 18 | margin-top: -5px 19 | } 20 | } 21 | 22 | button { 23 | margin-left: 5px !important 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": false, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "strictNullChecks": false, 15 | "forceConsistentCasingInFileNames": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "preserve" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /client/src/interfaces/IProfile.ts: -------------------------------------------------------------------------------- 1 | export interface IProfile { 2 | techs: string[], 3 | _id: string, 4 | fullName: string, 5 | username: string, 6 | followCount: number, 7 | followedByCount: number, 8 | postsScrapped: number, 9 | likeCount: number, 10 | commentCount: number, 11 | mediaCount: number, 12 | isFixed: boolean, 13 | isPrivate: boolean, 14 | lastScrapDate: Date, 15 | scrapping: boolean, 16 | notFound: boolean, 17 | scanned: boolean, 18 | followPercentage: number, 19 | followedByPercentage: number, 20 | mediaPercentage: number, 21 | likePercentage: number, 22 | commentPercentage: number 23 | } -------------------------------------------------------------------------------- /client/src/css/LoadingBar.scss: -------------------------------------------------------------------------------- 1 | .loaderBar { 2 | height: 4px; 3 | width: 100%; 4 | position: relative; 5 | overflow: hidden; 6 | background-color: #ddd; 7 | 8 | &:before{ 9 | display: block; 10 | position: absolute; 11 | content: ""; 12 | left: -200px; 13 | width: 200px; 14 | height: 4px; 15 | background-color: #2980b9; 16 | animation: loadingBar 2s linear infinite; 17 | } 18 | } 19 | 20 | @keyframes loadingBar { 21 | from {left: -200px; width: 30%;} 22 | 50% {width: 30%;} 23 | 70% {width: 70%;} 24 | 80% { left: 50%;} 25 | 95% {left: 120%;} 26 | to {left: 100%;} 27 | } -------------------------------------------------------------------------------- /backend/app/public/precache-manifest.9029fd04bb2e8bbaeb43584b55b39c07.js: -------------------------------------------------------------------------------- 1 | self.__precacheManifest = (self.__precacheManifest || []).concat([ 2 | { 3 | "revision": "b21c5627d2ca0ceef00e7bfa6516ffea", 4 | "url": "/index.html" 5 | }, 6 | { 7 | "revision": "8db4ff22a782cb946a03", 8 | "url": "/static/css/2.22a7d4ef.chunk.css" 9 | }, 10 | { 11 | "revision": "f0aad53c5c4684a64f17", 12 | "url": "/static/css/main.49f7432d.chunk.css" 13 | }, 14 | { 15 | "revision": "8db4ff22a782cb946a03", 16 | "url": "/static/js/2.0b200c55.chunk.js" 17 | }, 18 | { 19 | "revision": "f0aad53c5c4684a64f17", 20 | "url": "/static/js/main.171b13fe.chunk.js" 21 | }, 22 | { 23 | "revision": "42ac5946195a7306e2a5", 24 | "url": "/static/js/runtime~main.a8a9905a.js" 25 | } 26 | ]); -------------------------------------------------------------------------------- /client/src/components/Percentage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Label } from 'reactstrap'; 3 | import { FaArrowUp, FaArrowDown } from 'react-icons/fa'; 4 | import '../css/Percentage.scss'; 5 | 6 | interface IProps { 7 | value: number 8 | } 9 | 10 | export default class Percentage extends React.Component { 11 | 12 | shouldComponentUpdate(nextProps: any) { 13 | return nextProps.value !== undefined && !isNaN(nextProps.value); 14 | } 15 | 16 | render() { 17 | 18 | return 19 | 20 | {this.props.value >= 0.1 && 21 | } 22 | 23 | {this.props.value <= -0.1 && 24 | } 25 | 26 | 27 | } 28 | } -------------------------------------------------------------------------------- /client/src/css/LoadingSpinner.scss: -------------------------------------------------------------------------------- 1 | .loading { 2 | position: fixed; 3 | top: 0; 4 | right: 0; 5 | bottom: 0; 6 | left: 0; 7 | background: #fff; 8 | 9 | .loader { 10 | left: 50%; 11 | margin-left: -4em; 12 | font-size: 10px; 13 | border: .8em solid rgba(218, 219, 223, 1); 14 | border-left: .8em solid rgba(58, 166, 165, 1); 15 | animation: spin 1.1s infinite linear; 16 | } 17 | 18 | .loader, 19 | .loader:after { 20 | border-radius: 50%; 21 | width: 8em; 22 | height: 8em; 23 | display: block; 24 | position: absolute; 25 | top: 50%; 26 | margin-top: -4.05em; 27 | } 28 | 29 | @keyframes spin { 30 | 0% { 31 | transform: rotate(0deg); 32 | } 33 | 34 | 100% { 35 | transform: rotate(360deg); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /backend/app/public/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "main.css": "/static/css/main.49f7432d.chunk.css", 4 | "main.js": "/static/js/main.171b13fe.chunk.js", 5 | "main.js.map": "/static/js/main.171b13fe.chunk.js.map", 6 | "runtime~main.js": "/static/js/runtime~main.a8a9905a.js", 7 | "runtime~main.js.map": "/static/js/runtime~main.a8a9905a.js.map", 8 | "static/css/2.22a7d4ef.chunk.css": "/static/css/2.22a7d4ef.chunk.css", 9 | "static/js/2.0b200c55.chunk.js": "/static/js/2.0b200c55.chunk.js", 10 | "static/js/2.0b200c55.chunk.js.map": "/static/js/2.0b200c55.chunk.js.map", 11 | "index.html": "/index.html", 12 | "precache-manifest.9029fd04bb2e8bbaeb43584b55b39c07.js": "/precache-manifest.9029fd04bb2e8bbaeb43584b55b39c07.js", 13 | "service-worker.js": "/service-worker.js", 14 | "static/css/2.22a7d4ef.chunk.css.map": "/static/css/2.22a7d4ef.chunk.css.map", 15 | "static/css/main.49f7432d.chunk.css.map": "/static/css/main.49f7432d.chunk.css.map" 16 | } 17 | } -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iscrapper-backend", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "author": "Ivan Valadares - http://ivanvaladares.com", 6 | "dependencies": { 7 | "applicationinsights": "^1.1.0", 8 | "body-parser": "1.18.2", 9 | "cheerio": "^1.0.0-rc.2", 10 | "compression": "^1.7.4", 11 | "cors": "^2.8.4", 12 | "dotenv": "^7.0.0", 13 | "express": "4.16.2", 14 | "express-validator": "^5.3.0", 15 | "jsdom": "^13.2.0", 16 | "moment": "2.20.1", 17 | "express-swagger-generator": "^1.1.14", 18 | "mongodb": "^3.1.13", 19 | "phantom": "^6.0.3", 20 | "request": "^2.88.0", 21 | "uuid": "^3.3.2", 22 | "ws": "^5.2.2" 23 | }, 24 | "scripts": { 25 | "start": "node index.js" 26 | }, 27 | "devDependencies": { 28 | "eslint": "^5.1.0", 29 | "eslint-plugin-node": "^7.0.1", 30 | "eslint-plugin-promise": "^3.8.0", 31 | "nodemon": "^1.18.10" 32 | }, 33 | "engines": { 34 | "node": "~10.14.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/app/models/post-model.js: -------------------------------------------------------------------------------- 1 | const baseModel = require('./base-model.js'); 2 | const collection = "is-post"; 3 | 4 | exports.find = (criteria, sortBy, limit) => { 5 | return baseModel.find(collection, criteria, sortBy, limit); 6 | }; 7 | 8 | exports.save = (entry) => { 9 | return baseModel.save(collection, entry); 10 | }; 11 | 12 | exports.replace = (query, entry) => { 13 | return baseModel.replace(collection, query, entry); 14 | }; 15 | 16 | exports.update = (query, update) => { 17 | return baseModel.update(collection, query, update); 18 | }; 19 | 20 | exports.remove = (criteria) => { 21 | return baseModel.remove(collection, criteria); 22 | }; 23 | 24 | exports.count = (criteria) => { 25 | return baseModel.count(collection, criteria); 26 | }; 27 | 28 | exports.aggregate = (criteria) => { 29 | return baseModel.aggregate(collection, criteria); 30 | }; 31 | 32 | exports.aggregateToStream = (criteria, pageSize, pageNum) => { 33 | return baseModel.aggregateToStream(collection, criteria, pageSize, pageNum); 34 | }; -------------------------------------------------------------------------------- /backend/app/models/profile-model.js: -------------------------------------------------------------------------------- 1 | const baseModel = require('./base-model.js'); 2 | const collection = "is-profile"; 3 | 4 | exports.get = (username) => { 5 | return baseModel.get(collection, {username: username}); 6 | }; 7 | 8 | exports.find = (criteria, sortBy, limit) => { 9 | return baseModel.find(collection, criteria, sortBy, limit); 10 | }; 11 | 12 | exports.save = (entry) => { 13 | return baseModel.save(collection, entry); 14 | }; 15 | 16 | exports.replace = (query, entry) => { 17 | return baseModel.replace(collection, query, entry); 18 | }; 19 | 20 | exports.update = (query, update) => { 21 | return baseModel.update(collection, query, update); 22 | }; 23 | 24 | exports.remove = (criteria) => { 25 | return baseModel.remove(collection, criteria); 26 | }; 27 | 28 | exports.count = (criteria) => { 29 | return baseModel.count(collection, criteria); 30 | }; 31 | 32 | exports.aggregate = (criteria) => { 33 | return baseModel.aggregate(collection, criteria); 34 | }; 35 | 36 | exports.aggregateToStream = (criteria) => { 37 | return baseModel.aggregateToStream(collection, criteria); 38 | }; -------------------------------------------------------------------------------- /backend/app/logger.js: -------------------------------------------------------------------------------- 1 | const util = require("util"); 2 | const eventBus = require("./eventBus.js"); 3 | const logModel = require("./models/log-model"); 4 | 5 | const getDetails = (details) => { 6 | try { 7 | return util.inspect(details, { showHidden: true, depth: null }); 8 | } catch (error) { 9 | return details; 10 | } 11 | }; 12 | 13 | const log = (type, message, details, dontPersist) => { 14 | let date = new Date(); 15 | let fullDetails = ""; 16 | if (details){ 17 | fullDetails = getDetails(details); 18 | } 19 | let log = {type, date, message, details: fullDetails}; 20 | 21 | if (process.env.ENVIRONMENT === "DEV"){ 22 | console.log(type, message, fullDetails); 23 | } 24 | 25 | eventBus.emit("newLog", JSON.stringify(log)); 26 | 27 | if (type !== "info" && !dontPersist){ 28 | logModel.save(log); 29 | } 30 | }; 31 | 32 | exports.error = (message, details, dontPersist) => { 33 | log("error", message, details, dontPersist); 34 | }; 35 | 36 | exports.info = (message, details) => { 37 | log("info", message, details); 38 | }; 39 | 40 | exports.warn = (message, details) => { 41 | log("warn", message, details); 42 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Ivan Valadares 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. -------------------------------------------------------------------------------- /backend/app/routes/api-types.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * @typedef Profile 4 | * @property {string} fullName 5 | * @property {string} username 6 | * @property {number} mediaCount 7 | * @property {boolean} isPrivate 8 | * @property {boolean} notFound 9 | * @property {Array.} history 10 | */ 11 | 12 | /** 13 | * @typedef ProfileHistory 14 | * @property {number} followCount 15 | * @property {number} followedByCount 16 | * @property {number} mediaCount 17 | * @property {number} likeCount 18 | * @property {number} commentCount 19 | * @property {string} date 20 | */ 21 | 22 | /** 23 | * @typedef Post 24 | * @property {string} path 25 | * @property {string} image 26 | * @property {string} description 27 | * @property {Array.} history 28 | */ 29 | 30 | 31 | /** 32 | * @typedef PostHistory 33 | * @property {number} likeCount 34 | * @property {number} commentCount 35 | * @property {string} date 36 | */ 37 | 38 | /** 39 | * @typedef Error 40 | * @property {number} code 41 | * @property {string} message 42 | */ 43 | 44 | /** 45 | * @typedef Error 46 | * @property {number} code 47 | * @property {string} message 48 | */ -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iscrapper-frontend", 3 | "version": "1.0.0", 4 | "author": "Ivan Valadares - http://ivanvaladares.com", 5 | "dependencies": { 6 | "@types/jest": "24.0.12", 7 | "@types/node": "12.0.0", 8 | "@types/react": "16.8.17", 9 | "@types/react-dom": "16.8.4", 10 | "@types/reactstrap": "^8.0.1", 11 | "bootstrap": "^4.3.1", 12 | "highcharts": "^7.1.1", 13 | "highcharts-react-official": "^2.1.3", 14 | "node-sass": "^4.12.0", 15 | "react": "^16.8.6", 16 | "react-dom": "^16.8.6", 17 | "react-icons": "^3.7.0", 18 | "react-scripts": "3.0.1", 19 | "reactstrap": "^8.0.0", 20 | "typescript": "3.4.5" 21 | }, 22 | "scripts": { 23 | "start": "react-scripts start", 24 | "build": "react-scripts build", 25 | "test": "react-scripts test", 26 | "eject": "react-scripts eject" 27 | }, 28 | "eslintConfig": { 29 | "extends": "react-app" 30 | }, 31 | "browserslist": { 32 | "production": [ 33 | ">0.2%", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /backend/app/public/service-worker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Welcome to your Workbox-powered service worker! 3 | * 4 | * You'll need to register this file in your web app and you should 5 | * disable HTTP caching for this file too. 6 | * See https://goo.gl/nhQhGp 7 | * 8 | * The rest of the code is auto-generated. Please don't update this file 9 | * directly; instead, make changes to your Workbox build configuration 10 | * and re-run your build process. 11 | * See https://goo.gl/2aRDsh 12 | */ 13 | 14 | importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js"); 15 | 16 | importScripts( 17 | "/precache-manifest.9029fd04bb2e8bbaeb43584b55b39c07.js" 18 | ); 19 | 20 | self.addEventListener('message', (event) => { 21 | if (event.data && event.data.type === 'SKIP_WAITING') { 22 | self.skipWaiting(); 23 | } 24 | }); 25 | 26 | workbox.core.clientsClaim(); 27 | 28 | /** 29 | * The workboxSW.precacheAndRoute() method efficiently caches and responds to 30 | * requests for URLs in the manifest. 31 | * See https://goo.gl/S9QRab 32 | */ 33 | self.__precacheManifest = [].concat(self.__precacheManifest || []); 34 | workbox.precaching.precacheAndRoute(self.__precacheManifest, {}); 35 | 36 | workbox.routing.registerNavigationRoute(workbox.precaching.getCacheKeyForURL("/index.html"), { 37 | 38 | blacklist: [/^\/_/,/\/[^\/]+\.[^\/]+$/], 39 | }); 40 | -------------------------------------------------------------------------------- /backend/app/public/static/js/runtime~main.a8a9905a.js: -------------------------------------------------------------------------------- 1 | !function(e){function r(r){for(var n,f,i=r[0],l=r[1],a=r[2],c=0,s=[];c { 11 | 12 | render() { 13 | 14 | const postList = this.props.posts.map(post => { 15 | return 16 | 17 | 18 | {post.path} 19 | 20 | 21 |
22 | {post.published === 0 ? "today" : 23 | post.published + (post.published > 1 ? " days " : " day ") + " ago"} 24 | 25 | 26 | Likes: {post.likeCount.toLocaleString('pt-BR')}  27 | 28 |
29 | Comments: {post.commentCount.toLocaleString('pt-BR')}  30 | 31 | 32 | 33 | 34 | }); 35 | 36 | return
37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | {postList} 47 | 48 |
PostStats
49 | 50 |
; 51 | 52 | } 53 | } -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Instagram Scrapper 9 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /backend/index.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | if (!process.env.MONGODB_URI || !process.env.MONGODB_DBNAME){ 4 | throw new Error("MongoDB not specified!"); 5 | } 6 | 7 | let aiClient; 8 | 9 | if (process.env.APPLICATION_INSIGHTS_KEY){ 10 | const appInsights = require('applicationinsights'); 11 | appInsights.setup(process.env.APPLICATION_INSIGHTS_KEY).start(); 12 | aiClient = appInsights.defaultClient; 13 | } 14 | 15 | process.env.TZ = "utc"; 16 | 17 | //############################################################################# 18 | 19 | const eventBus = require('./app/eventBus.js'); 20 | const scrapper = require("./app/scrapper.js"); 21 | const logger = require("./app/logger.js"); 22 | 23 | process.on("uncaughtException", (err) => { 24 | logger.error("uncaughtException", err); 25 | }); 26 | 27 | eventBus.on("newLog", (msg) => { 28 | if (!process.env.APPLICATION_INSIGHTS_KEY) { 29 | return; 30 | } 31 | 32 | let log = JSON.parse(msg); 33 | if (log.type === "error"){ 34 | let err = new Error(log.message); 35 | err.details = log.details; 36 | if (aiClient){ 37 | aiClient.trackException({exception: err }); 38 | } 39 | }else{ 40 | if (aiClient){ 41 | aiClient.trackEvent({name: log.message, properties: {details: log.details}}); 42 | } 43 | } 44 | }); 45 | 46 | //############################################################################# 47 | 48 | const Server = require("./app/server.js").init(); 49 | 50 | Server.open({ port: (process.env.PORT || 3001) }, (err) => { 51 | if (err) { 52 | logger.error("HTTP Server error!!!", err); 53 | } 54 | scrapper.init({maxProfiles: process.env.MAX_PROFILES, maxDownloads: process.env.MAX_DOWNLOADS}); 55 | }); -------------------------------------------------------------------------------- /backend/app/routes/post-routes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const { sanitizeBody } = require('express-validator/filter'); 3 | const postController = require("../controller/post-controller"); 4 | const router = express.Router(); 5 | 6 | const getStats = (req, res) => { 7 | 8 | postController.getStats(req.params.username, req.query.limit).then(result => { 9 | 10 | res.json(result); 11 | 12 | }).catch(err => { 13 | res.status(err.code).json({ "message": err.message }); 14 | }); 15 | 16 | }; 17 | 18 | const getChart = (req, res) => { 19 | 20 | postController.getChart(req.params.path).then(result => { 21 | 22 | res.json(result); 23 | 24 | }).catch(err => { 25 | res.status(err.code).json({ "message": err.message }); 26 | }); 27 | 28 | }; 29 | 30 | 31 | const exportPosts = (req, res) => { 32 | 33 | postController.exportPosts(res, req.params.username, req.query.pageSize, req.query.pageNum).then(() => { 34 | 35 | //ok 36 | 37 | }).catch(err => { 38 | res.status(err.code).json({ "message": err.message }); 39 | }); 40 | 41 | }; 42 | 43 | router.get("/getStats/:username", sanitizeBody('*').trim().escape(), getStats); 44 | router.get("/getChart/:path", sanitizeBody('*').trim().escape(), getChart); 45 | 46 | /** 47 | * @route GET /post/export/{username} 48 | * @group Post 49 | * @param {string} username.path.required 50 | * @param {integer} pageSize.query - use to paginate (optional) 51 | * @param {integer} pageNum.query - use to paginate (optional) 52 | * @produces application/json 53 | * @consumes application/json 54 | * @returns {Array.} Success 55 | * @returns {Error.model} Error 56 | */ 57 | 58 | router.get("/export/:username", sanitizeBody('*').trim().escape(), exportPosts); 59 | 60 | module.exports = router; -------------------------------------------------------------------------------- /client/src/components/FormModal.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Button, Modal, ModalBody, ModalFooter, Form, InputGroup, InputGroupAddon, Input } from 'reactstrap'; 3 | import { FaPlus, FaUserPlus } from 'react-icons/fa'; 4 | import '../css/FormModal.scss'; 5 | 6 | interface IProps { 7 | saveProfile: (username: string) => void 8 | } 9 | 10 | export default class FormModal extends React.Component { 11 | 12 | public state = { 13 | isOpen: false, 14 | username: "" 15 | } 16 | 17 | private updateInputValue (evt: any) { 18 | this.setState({ 19 | username: evt.target.value 20 | }); 21 | } 22 | 23 | private saveProfile (evt: any) { 24 | 25 | if (this.state.username.split(" ").join("") === "") { 26 | return alert("Please type the username"); 27 | } 28 | 29 | this.props.saveProfile(this.state.username); 30 | this.setState({username: "" }); 31 | this.toggle(); 32 | evt.preventDefault(); 33 | } 34 | 35 | private toggle() { 36 | this.setState({ isOpen: !this.state.isOpen }); 37 | } 38 | 39 | render() { 40 | 41 | return { this.toggle(); e.preventDefault(); }}> 42 | 43 | 44 | 45 | 46 | 47 | this.toggle()} isOpen={this.state.isOpen} fade={true} keyboard={true}> 48 | 49 | 50 |
{ this.saveProfile(e); }}> 51 | 52 |   add new @ 53 | this.updateInputValue(evt)} /> 54 | 55 |
56 | 57 |
58 | 59 | 60 | 61 | 62 |
63 | 64 |
; 65 | } 66 | } -------------------------------------------------------------------------------- /backend/app/public/static/css/main.49f7432d.chunk.css: -------------------------------------------------------------------------------- 1 | .list-group-item{padding:10px}.list-group-item .progress{margin-bottom:0!important}.list-group-item .profile-name{font-size:16pt;cursor:pointer;text-overflow:ellipsis;overflow:hidden}.list-group-item .profile-name small{font-size:10px;color:grey;display:block;margin-top:-5px}.list-group-item button{margin-left:5px!important}.percUp{color:#228b22}.percDown{color:red}.loaderBar{height:4px;width:100%;position:relative;overflow:hidden;background-color:#ddd}.loaderBar:before{display:block;position:absolute;content:"";left:-200px;width:200px;height:4px;background-color:#2980b9;-webkit-animation:loadingBar 2s linear infinite;animation:loadingBar 2s linear infinite}@-webkit-keyframes loadingBar{0%{left:-200px;width:30%}50%{width:30%}70%{width:70%}80%{left:50%}95%{left:120%}to{left:100%}}@keyframes loadingBar{0%{left:-200px;width:30%}50%{width:30%}70%{width:70%}80%{left:50%}95%{left:120%}to{left:100%}}.postPathCol{width:120px}.nav-link{cursor:pointer}body{padding:10px;font-family:Arial,Helvetica,sans-serif}hr{border-top:1px dotted #000!important;margin-bottom:5px!important;margin-top:5px!important}.float{position:fixed;width:60px;height:60px;bottom:40px;right:40px;background-color:#0c9;color:#fff;border-radius:50px;text-align:center;box-shadow:2px 2px 3px #999;opacity:.7;-webkit-transition:opacity .2s ease-in-out;transition:opacity .2s ease-in-out}.float:hover{opacity:1}.my-float{margin-top:22px}@media only screen and (max-width:600px){.float{bottom:20px;right:20px}}.loading{position:fixed;top:0;right:0;bottom:0;left:0;background:#fff}.loading .loader{left:50%;margin-left:-4em;font-size:10px;border:.8em solid #dadbdf;border-left-color:#3aa6a5;-webkit-animation:spin 1.1s linear infinite;animation:spin 1.1s linear infinite}.loading .loader,.loading .loader:after{border-radius:50%;width:8em;height:8em;display:block;position:absolute;top:50%;margin-top:-4.05em}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}} 2 | /*# sourceMappingURL=main.49f7432d.chunk.css.map */ -------------------------------------------------------------------------------- /backend/app/routes/profile-routes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const { sanitizeBody } = require('express-validator/filter'); 3 | const profileController = require("../controller/profile-controller"); 4 | const router = express.Router(); 5 | 6 | const listAll = (req, res) => { 7 | 8 | profileController.list({}).then(result => { 9 | 10 | res.json(result); 11 | 12 | }).catch(err => { 13 | res.status(err.code).json({ "message": err.message }); 14 | }); 15 | 16 | }; 17 | 18 | const save = (req, res) => { 19 | 20 | profileController.save(req.body).then(result => { 21 | 22 | res.json(result); 23 | 24 | }).catch(err => { 25 | res.status(err.code).json({ "message": err.message }); 26 | }); 27 | 28 | }; 29 | 30 | const remove = (req, res) => { 31 | 32 | profileController.remove(req.params.username).then(result => { 33 | 34 | res.json(result); 35 | 36 | }).catch(err => { 37 | res.status(err.code).json({ "message": err.message }); 38 | }); 39 | 40 | }; 41 | 42 | const getChart = (req, res) => { 43 | 44 | profileController.getChart(req.params.username).then(result => { 45 | 46 | res.json(result); 47 | 48 | }).catch(err => { 49 | res.status(err.code).json({ "message": err.message }); 50 | }); 51 | 52 | }; 53 | 54 | const exportProfile = (req, res) => { 55 | 56 | profileController.exportProfile(res, req.params.username).then(() => { 57 | 58 | //ok 59 | 60 | }).catch(err => { 61 | res.status(err.code).json({ "message": err.message }); 62 | }); 63 | 64 | }; 65 | 66 | router.get("/listAll", sanitizeBody('*').trim().escape(), listAll); 67 | router.post("/save", sanitizeBody('*').trim().escape(), save); 68 | router.delete("/:username", remove); 69 | router.get("/getChart/:username", sanitizeBody('*').trim().escape(), getChart); 70 | 71 | 72 | /** 73 | * @route GET /profile/export/{username} 74 | * @group Profile 75 | * @param {string} username.path.required 76 | * @produces application/json 77 | * @consumes application/json 78 | * @returns {Profile.model} Success 79 | * @returns {Error.model} Error 80 | */ 81 | 82 | router.get("/export/:username", sanitizeBody('*').trim().escape(), exportProfile); 83 | 84 | module.exports = router; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## instagram-scrapper 2 | Node project to collect Profiles, Posts and Likes, Comments, Follows, Following stats from Instagram profiles without sign-up to their API and without user consent. 3 | 4 | ## Is there a live example? 5 | You can go to http://iscrapper.herokuapp.com/ and register profiles, but please delete them later because this database is limited. There is also an API to export the collected data at: http://iscrapper.herokuapp.com/api-docs 6 | 7 | ## About the app 8 | Actually, there are two separated apps. The Client which serves the FrontEnd (using React), and the Backend (in Node/Express/Mongo). 9 | 10 | ## How to run the Backend 11 | 1. Navigate to the `backend` directory. 12 | 2. Change the `.env` file to your desired configuration. 13 | 3. Open a terminal. 14 | 4. Run `npm install` to install all dependencies. 15 | 5. Run `npm start` to start the app. 16 | 17 | ## How to run the Client 18 | 1. Navigate to the `client` directory. 19 | 2. Change the `.env` file to your desired configuration. 20 | 3. Open a terminal. 21 | 4. Run `npm install` to install all dependencies. 22 | 5. Run `npm start` to start the app. 23 | 24 | ## Check if they are connected 25 | 1. With the two apps running, open your browser in http://localhost:3000/. 26 | 2. Open chrome dev tools to check for backend logs. 27 | 3. Enjoy! 28 | 29 | ## To build the project for deployment 30 | 1. Navigate to the `client` directory. 31 | 2. Open a terminal. 32 | 3. Run `npm run build` to build the client project. 33 | 4. Copy the contents of the `client/build` folder to `backend/app/public` 34 | 5. Deploy the contents of the `backend` folder to your server. (I'm using Heroku and mLab) 35 | 36 | ## Backend .env configuration fields 37 | ``` 38 | APPLICATION_INSIGHTS_KEY # Azure application insights instrumentation key 39 | MONGODB_URI # mongodb://:@ 40 | MONGODB_DBNAME # Database name 41 | MONGODB_CREATE # This option when is true will create all collections and indexes 42 | MAX_PROFILES # How many profiles will be scrapped at the same time 43 | MAX_DOWNLOADS # How many posts download will be placed at the same time 44 | ISCRAPPER_READONLY # This will disable delete and creation of profiles on your instance 45 | ENVIRONMENT # Outputs logs to console when in DEV 46 | ``` 47 | 48 | ## Client .env configuration fields 49 | ``` 50 | REACT_APP_BACKEND_URL # Backend address 51 | REACT_APP_APPLICATION_INSIGHTS_KEY # Azure application insights instrumentation key 52 | ``` 53 | 54 | -------------------------------------------------------------------------------- /backend/app/public/index.html: -------------------------------------------------------------------------------- 1 | Instagram Scrapper
-------------------------------------------------------------------------------- /client/src/components/ProfileList.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ListGroupItem, Button, Label, Row, Col, Progress } from 'reactstrap'; 3 | import '../css/ProfileList.scss'; 4 | import ProfileModal from './ProfileModal'; 5 | import { FaTrash } from 'react-icons/fa'; 6 | import { IProfile } from '../interfaces/IProfile'; 7 | import Percentage from './Percentage'; 8 | 9 | interface IProps { 10 | profile: IProfile, 11 | listProfiles: () => void, 12 | removeProfile: (username: string) => void 13 | } 14 | 15 | export default class ProfileList extends React.Component { 16 | 17 | shouldComponentUpdate(nextProps: any, nextState: any) { 18 | return JSON.stringify(this.props) !== JSON.stringify(nextProps); 19 | } 20 | 21 | render() { 22 | 23 | let profile = this.props.profile; 24 | 25 | let progression: number = (profile.postsScrapped / profile.mediaCount) * 100; 26 | if (progression > 100) { 27 | progression = 100; 28 | } 29 | 30 | let barType: string = "warning"; 31 | if (progression === 100) { 32 | barType = "success"; 33 | } 34 | if (profile.scrapping) { 35 | barType = "info" 36 | } 37 | 38 | return ( 39 | 40 | 41 | 44 | 45 | 46 | 47 | 53 | 54 | 55 | {profile.isPrivate || profile.notFound ? : ""} 56 | 57 | 58 | 59 | Followed:
60 | {profile.followedByCount.toLocaleString('pt-BR')}
61 | 62 | 63 | 64 | Posts:
65 | {profile.mediaCount.toLocaleString('pt-BR')}
66 | 67 | 68 | 69 | Likes:
70 | {profile.likeCount.toLocaleString('pt-BR')}
71 | 72 | 73 | 74 | Comments:
75 | {profile.commentCount.toLocaleString('pt-BR')}
76 | 77 | 78 |
79 | 80 | 81 | 82 |
); 83 | 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /backend/app/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const compression = require('compression'); 3 | const cors = require('cors'); 4 | const expressSwagger = require('express-swagger-generator'); 5 | const WebSocketServer = require('ws').Server; 6 | const bodyParser = require("body-parser"); 7 | const logger = require("./logger.js"); 8 | const eventBus = require('./eventBus.js'); 9 | const path = require("path"); 10 | 11 | class HttpServer { 12 | 13 | constructor () { 14 | 15 | this.app = express(); 16 | this.server = null; 17 | 18 | const swagger = expressSwagger(this.app); 19 | 20 | let options = { 21 | swaggerDefinition: { 22 | info: { 23 | description: '', 24 | title: 'Instagram Scrapper API', 25 | version: '1.0.0' 26 | }, 27 | basePath: '/', 28 | produces: [ 29 | "application/json" 30 | ] 31 | }, 32 | basedir: __dirname, //app absolute path 33 | files: ['./routes/**/*.js'] //Path to the API handle folder 34 | }; 35 | 36 | swagger(options); 37 | 38 | 39 | this.app.use(cors()); 40 | this.app.use(compression()); 41 | this.app.use(bodyParser.urlencoded({limit: "1mb", extended: true})); 42 | this.app.use(bodyParser.json({limit: "1mb"})); 43 | this.app.use(express.static(path.join(__dirname, "public"))); 44 | 45 | this.app.get("/", (req, res) => { 46 | res.sendFile(path.join(__dirname, "/public/index.html")); 47 | }); 48 | 49 | require("./routes/index").init(this.app); 50 | 51 | return this; 52 | } 53 | 54 | open (options, callBack) { 55 | 56 | options = options || {}; 57 | let port = options.port; 58 | 59 | this.server = this.app.listen(port, (err) => { 60 | if (err) { 61 | logger.error("HTTP Server error!", err); 62 | 63 | if (callBack) { 64 | callBack(err); 65 | } 66 | 67 | return; 68 | } 69 | 70 | this.startWebSocketServer(); 71 | 72 | eventBus.on("newMessage", (message) => { 73 | this.broadcast(message); 74 | }); 75 | 76 | eventBus.on("newLog", (message) => { 77 | this.broadcast(message); 78 | }); 79 | 80 | logger.info(`HTTP Server is listening on ${port}`); 81 | 82 | if (callBack) { 83 | callBack(); 84 | } 85 | 86 | }).on("error", (err) => { 87 | logger.error("HTTP Server error!", err); 88 | if (callBack) { 89 | callBack(err); 90 | } 91 | }); 92 | } 93 | 94 | startWebSocketServer () { 95 | this.wss = new WebSocketServer({ 96 | server: this.server 97 | }); 98 | 99 | this.wss.on("connection", ws => { 100 | 101 | ws.isAlive = true; 102 | ws.on('pong', () => { ws.isAlive = true; }); 103 | 104 | ws.send(JSON.stringify({connection: "OK"})); 105 | 106 | }); 107 | 108 | setInterval(() => { 109 | this.wss.clients.forEach((ws) => { 110 | if (ws.isAlive === false) { 111 | return ws.terminate(); 112 | } 113 | ws.isAlive = false; 114 | ws.ping(null); 115 | }); 116 | }, 30000); 117 | } 118 | 119 | broadcast (message) { 120 | this.wss.clients.forEach(client => { 121 | client.send(message); 122 | }); 123 | } 124 | } 125 | 126 | module.exports.init = () => { 127 | return new HttpServer(); 128 | }; -------------------------------------------------------------------------------- /client/src/components/PostStats.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { TabContent, TabPane, Row, Col, Nav, NavItem, NavLink, Label, Alert } from 'reactstrap'; 3 | import { apiBackendUrl } from '../helpers/constants'; 4 | import PostsStatsList from './PostStatsList'; 5 | import { IProfile } from '../interfaces/IProfile'; 6 | import { IPost } from '../interfaces/IPost'; 7 | import '../css/PostStats.scss'; 8 | import LoadingBar from './LoadingBar'; 9 | 10 | interface IProps { 11 | profile: IProfile 12 | } 13 | 14 | export default class PostsStats extends React.Component { 15 | 16 | public state = { 17 | isLoading: true, 18 | hasData: false, 19 | list1: [] as IPost[], 20 | list2: [] as IPost[], 21 | list3: [] as IPost[], 22 | activeTab: '1', 23 | error: false, 24 | errorMessage: "" 25 | } 26 | 27 | private toggle(tab: string) { 28 | this.setState({ activeTab: tab }); 29 | } 30 | 31 | private loadData() { 32 | fetch(`${apiBackendUrl}post/getStats/${this.props.profile.username}?limit=30`).then(response => { 33 | 34 | if (!response.ok) { 35 | if (response.bodyUsed) { 36 | response.json().then(err => { 37 | this.setState({ error: true, errorMessage: err.message }); 38 | }); 39 | } else { 40 | this.setState({ error: true, errorMessage: response.statusText }); 41 | } 42 | return null; 43 | } 44 | 45 | return response.json(); 46 | 47 | }).then(data => { 48 | 49 | if (data === null) { 50 | this.setState({ isLoading: false }); 51 | return; 52 | } 53 | 54 | this.setState({ 55 | isLoading: false, 56 | hasData: data.topInteraction.length > 0 || data.topLikes.length > 0 || data.topComments.length > 0, 57 | list1: data.topInteraction, 58 | list2: data.topLikes, 59 | list3: data.topComments, 60 | activeTab: '1' 61 | }); 62 | 63 | }); 64 | } 65 | 66 | componentDidMount() { 67 | this.loadData(); 68 | } 69 | 70 | render() { 71 | 72 | return
73 | 74 | {this.state.isLoading && !this.state.error && } 75 | 76 | {this.state.error && {this.state.errorMessage}} 77 | 78 | {!this.state.isLoading && !this.state.error && this.state.hasData && 79 |
80 | 81 | 82 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 |
129 | } 130 | 131 |
; 132 | 133 | } 134 | } -------------------------------------------------------------------------------- /backend/app/models/base-model.js: -------------------------------------------------------------------------------- 1 | const mongoDB = require('./mongodb.js'); 2 | 3 | exports.get = (collection, criteria) => { 4 | 5 | return new Promise((resolve, reject) => { 6 | 7 | mongoDB.connectDB((err, connection) => { 8 | 9 | if (err) { 10 | return reject(err); 11 | } 12 | 13 | connection.collection(collection).find(criteria).toArray((err, item) => { 14 | if (err) { 15 | return reject(err); 16 | } 17 | 18 | 19 | resolve(item); 20 | }); 21 | 22 | }); 23 | }); 24 | }; 25 | 26 | exports.find = (collection, criteria, sortBy, limit) => { 27 | 28 | return new Promise((resolve, reject) => { 29 | 30 | mongoDB.connectDB((err, connection) => { 31 | 32 | if (err) { 33 | return reject(err); 34 | } 35 | 36 | connection.collection(collection).find(criteria).limit(limit).sort(sortBy).toArray((err, items) => { 37 | if (err) { 38 | return reject(err); 39 | } 40 | 41 | resolve(items); 42 | }); 43 | 44 | }); 45 | }); 46 | 47 | }; 48 | 49 | exports.save = (collection, entry) => { 50 | 51 | return new Promise((resolve, reject) => { 52 | 53 | mongoDB.connectDB((err, connection) => { 54 | 55 | if (err) { 56 | return reject(err); 57 | } 58 | 59 | connection.collection(collection).insertOne(entry, (err, item) => { 60 | if (err) { 61 | return reject(err); 62 | } 63 | 64 | resolve(item.ops[0]); 65 | }); 66 | }); 67 | 68 | }); 69 | 70 | }; 71 | 72 | exports.replace = (collection, query, entry) => { 73 | 74 | return new Promise((resolve, reject) => { 75 | 76 | mongoDB.connectDB((err, connection) => { 77 | 78 | if (err) { 79 | return reject(err); 80 | } 81 | 82 | connection.collection(collection).replaceOne(query, entry, {upsert: false, multiple: false}, (err, item) => { 83 | if (err) { 84 | return reject(err); 85 | } 86 | 87 | resolve(item); 88 | }); 89 | 90 | }); 91 | }); 92 | 93 | }; 94 | 95 | 96 | exports.update = (collection, query, update) => { 97 | 98 | return new Promise((resolve, reject) => { 99 | 100 | mongoDB.connectDB((err, connection) => { 101 | 102 | if (err) { 103 | return reject(err); 104 | } 105 | 106 | connection.collection(collection).updateMany(query, update, {upsert: false, multiple: true}, (err, item) => { 107 | if (err) { 108 | return reject(err); 109 | } 110 | 111 | resolve(item); 112 | }); 113 | 114 | }); 115 | }); 116 | 117 | }; 118 | 119 | 120 | exports.remove = (collection, criteria) => { 121 | 122 | return new Promise((resolve, reject) => { 123 | 124 | mongoDB.connectDB((err, connection) => { 125 | 126 | if (err) { 127 | return reject(err); 128 | } 129 | 130 | connection.collection(collection).deleteMany(criteria, { multiple: true }, (err, numRemoved) => { 131 | if (err) { 132 | return reject(err); 133 | } 134 | 135 | resolve(numRemoved); 136 | }); 137 | 138 | }); 139 | }); 140 | 141 | }; 142 | 143 | exports.count = (collection, criteria) => { 144 | 145 | return new Promise((resolve, reject) => { 146 | 147 | mongoDB.connectDB((err, connection) => { 148 | 149 | if (err) { 150 | return reject(err); 151 | } 152 | 153 | connection.collection(collection).countDocuments(criteria, (err, count) => { 154 | if (err) { 155 | return reject(err); 156 | } 157 | 158 | resolve(count); 159 | }); 160 | }); 161 | }); 162 | }; 163 | 164 | exports.aggregate = (collection, criteria) => { 165 | 166 | return new Promise((resolve, reject) => { 167 | 168 | mongoDB.connectDB((err, connection) => { 169 | 170 | if (err) { 171 | return reject(err); 172 | } 173 | 174 | connection.collection(collection).aggregate(criteria).toArray((err, items) => { 175 | if (err) { 176 | return reject(err); 177 | } 178 | 179 | resolve(items); 180 | }); 181 | }); 182 | }); 183 | }; 184 | 185 | 186 | exports.aggregateToStream = (collection, criteria, pageSize, pageNum) => { 187 | 188 | return new Promise((resolve, reject) => { 189 | 190 | mongoDB.connectDB((err, connection) => { 191 | 192 | if (err) { 193 | return reject(err); 194 | } 195 | 196 | let cursor; 197 | if (pageSize === undefined || pageNum === undefined){ 198 | cursor = connection.collection(collection).aggregate(criteria).batchSize(100).stream(); 199 | } else { 200 | let intPageSize = parseInt(pageSize, 10); 201 | let skip = intPageSize * (parseInt(pageNum, 10) - 1); 202 | 203 | cursor = connection.collection(collection).aggregate(criteria).skip(skip).limit(intPageSize).batchSize(intPageSize).stream(); 204 | } 205 | 206 | resolve(cursor); 207 | 208 | }); 209 | }); 210 | }; -------------------------------------------------------------------------------- /client/src/components/Home.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ListGroup } from 'reactstrap'; 3 | import ProfileList from './ProfileList'; 4 | import '../css/Home.scss'; 5 | import { apiBackendUrl } from '../helpers/constants'; 6 | import { IProfile } from '../interfaces/IProfile'; 7 | import FormModal from './FormModal'; 8 | import LoadingSpinner from './LoadingSpinner'; 9 | 10 | export default class Home extends React.Component<{}> { 11 | 12 | public state = { 13 | profiles: [] as IProfile[], 14 | loading: true 15 | } 16 | 17 | saveProfile = (username: string) => { 18 | 19 | this.setState({ loading: true }); 20 | 21 | const requestOptions = { 22 | method: 'POST', 23 | headers: { 24 | 'Accept': 'application/json', 25 | 'Content-Type': 'application/json' 26 | }, 27 | body: JSON.stringify({ "username": username }) 28 | }; 29 | 30 | fetch(apiBackendUrl + "profile/save", requestOptions).then(response => { 31 | if (!response.ok) { 32 | response.json().then(function (err) { 33 | alert(err.message); 34 | }); 35 | return; 36 | } 37 | 38 | this.listProfiles(); 39 | }); 40 | } 41 | 42 | removeProfile = (username: string) => { 43 | 44 | if (!window.confirm("Confirm this action?")) { 45 | return; 46 | } 47 | 48 | this.setState({ loading: true }); 49 | 50 | const requestOptions = { 51 | method: 'DELETE' 52 | }; 53 | 54 | fetch(apiBackendUrl + "profile/" + username, requestOptions).then(response => { 55 | if (!response.ok) { 56 | response.json().then(function (err) { 57 | alert(err.message); 58 | }); 59 | return; 60 | } 61 | 62 | this.listProfiles(); 63 | }); 64 | } 65 | 66 | listProfiles = () => { 67 | fetch(`${apiBackendUrl}profile/listAll?_t=${new Date().getTime()}`) 68 | .then(response => response.json()) 69 | .then(profiles => this.setState({ profiles, loading: false })) 70 | .catch(e => e); 71 | } 72 | 73 | openWebSocket = () => { 74 | if ("WebSocket" in window) { 75 | 76 | let protocol = window["location"]["protocol"]; 77 | let address = apiBackendUrl.substring(apiBackendUrl.indexOf(":") + 3); 78 | address = (((protocol + "").toLowerCase().indexOf("https") === 0) ? "wss://" : "ws://") + address; 79 | 80 | let wsSocket = new WebSocket(address); 81 | 82 | wsSocket.onopen = () => { 83 | console.log("Websocket connected!"); 84 | }; 85 | 86 | wsSocket.onmessage = (event) => { 87 | try { 88 | 89 | var jsonMessage = JSON.parse(event.data); 90 | 91 | if (jsonMessage.message) { 92 | if (jsonMessage.message === "new profile" || jsonMessage.message === "removed profile") { 93 | this.listProfiles(); 94 | } 95 | } 96 | 97 | console.log(event.data); 98 | 99 | } catch (error) { 100 | //nothing 101 | } 102 | }; 103 | 104 | wsSocket.onclose = () => { 105 | console.log("Websocket closed!"); 106 | // Try to reconnect in 5 second 107 | setTimeout(this.openWebSocket, 5000); 108 | }; 109 | } 110 | } 111 | 112 | componentDidMount() { 113 | console.log(apiBackendUrl); 114 | this.listProfiles(); 115 | this.openWebSocket(); 116 | setInterval(() => this.listProfiles(), 5000); 117 | } 118 | 119 | shouldComponentUpdate(nextProps: any, nextState: any) { 120 | return JSON.stringify(this.state.profiles) !== JSON.stringify(nextState.profiles) || this.state.loading !== nextState.loading; 121 | } 122 | 123 | render() { 124 | const list = this.state.profiles.map((profile) => { 125 | return 130 | }) 131 | 132 | return ( 133 |
134 | 135 | {this.state.loading && } 136 | 137 | {!this.state.loading && {list}} 138 | 139 | {!this.state.loading && } 140 | 141 | {!this.state.loading &&
142 | 143 | Developed by Ivan Valadares
144 | Check the API docs
145 | Get the source code at github.com/ivanvaladares/instagram-scrapper 146 |
147 |
} 148 | 149 | 150 |
); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /backend/app/public/static/css/main.49f7432d.chunk.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["C:\\Users\\Ivan Valadares\\Desktop\\iscrapper-react/src\\css\\ProfileList.scss","C:\\Users\\Ivan Valadares\\Desktop\\iscrapper-react/src\\css\\Percentage.scss","C:\\Users\\Ivan Valadares\\Desktop\\iscrapper-react/src\\css\\LoadingBar.scss","C:\\Users\\Ivan Valadares\\Desktop\\iscrapper-react/src\\css\\PostStatsList.scss","C:\\Users\\Ivan Valadares\\Desktop\\iscrapper-react/src\\css\\PostStats.scss","C:\\Users\\Ivan Valadares\\Desktop\\iscrapper-react/src\\css\\Home.scss","C:\\Users\\Ivan Valadares\\Desktop\\iscrapper-react/src\\css\\FormModal.scss","C:\\Users\\Ivan Valadares\\Desktop\\iscrapper-react/src\\css\\LoadingSpinner.scss"],"names":[],"mappings":"AAAA,iBACI,YAAa,CADjB,2BAIQ,yBAA6B,CAJrC,+BAQQ,cAAe,CACf,cAAe,CACf,sBAAuB,CACvB,eAAgB,CAXxB,qCAcY,cAAe,CACf,UAAW,CACX,aAAc,CACd,eACJ,CAlBR,wBAsBQ,yBACJ,CCvBJ,QACI,aAAkB,CAGtB,UACI,SAAU,CCLd,WACI,UAAW,CACX,UAAW,CACX,iBAAkB,CAClB,eAAgB,CAChB,qBAAsB,CAL1B,kBAQQ,aAAc,CACd,iBAAkB,CAClB,UAAW,CACX,WAAY,CACZ,WAAY,CACZ,UAAW,CACX,wBAAyB,CACzB,+CAAwC,CAAxC,uCAAwC,CAI9C,8BACI,GAAM,WAAY,CAAE,SAAU,CAC9B,IAAK,SAAU,CACf,IAAK,SAAU,CACf,IAAM,QAAS,CACf,IAAK,SAAU,CACf,GAAI,SAAU,CAAA,CANlB,sBACI,GAAM,WAAY,CAAE,SAAU,CAC9B,IAAK,SAAU,CACf,IAAK,SAAU,CACf,IAAM,QAAS,CACf,IAAK,SAAU,CACf,GAAI,SAAU,CAAA,CCzBpB,aACI,WAAY,CCDhB,UACI,cAAe,CCDnB,KACI,YAAa,CACb,sCACJ,CAEA,GACI,oCAAyC,CACzC,2BAA6B,CAC7B,wBAA0B,CCP9B,OACC,cAAc,CACd,UAAU,CACV,WAAW,CACX,WAAW,CACX,UAAU,CACV,qBAAqB,CACrB,UAAU,CACV,kBAAkB,CAClB,iBAAiB,CACd,2BAA4B,CAC5B,UAAY,CACZ,0CAAoC,CAApC,kCAAoC,CAZxC,aAeQ,SAAU,CAIlB,UACC,eAAe,CAGhB,yCACC,OACC,WAAW,CACX,UAAU,CACV,CC5BF,SACI,cAAe,CACf,KAAM,CACN,OAAQ,CACR,QAAS,CACT,MAAO,CACP,eAAgB,CANpB,iBASQ,QAAS,CACT,gBAAiB,CACjB,cAAe,CAEf,yBAA6C,CAA7C,yBAA6C,CAC7C,2CAAoC,CAApC,mCAAoC,CAd5C,wCAmBQ,iBAAkB,CAClB,SAAU,CACV,UAAW,CACX,aAAc,CACd,iBAAkB,CAClB,OAAQ,CACR,kBAAmB,CAGvB,wBACI,GACI,8BAAuB,CAAvB,sBAAuB,CAG3B,GACI,+BAAyB,CAAzB,uBAAyB,CAAA,CANjC,gBACI,GACI,8BAAuB,CAAvB,sBAAuB,CAG3B,GACI,+BAAyB,CAAzB,uBAAyB,CAAA","file":"main.49f7432d.chunk.css","sourcesContent":[".list-group-item {\r\n padding: 10px;\r\n\r\n .progress {\r\n margin-bottom: 0px !important;\r\n }\r\n \r\n .profile-name {\r\n font-size: 16pt;\r\n cursor: pointer;\r\n text-overflow: ellipsis;\r\n overflow: hidden;\r\n\r\n small {\r\n font-size: 10px;\r\n color: grey;\r\n display: block;\r\n margin-top: -5px\r\n }\r\n }\r\n \r\n button {\r\n margin-left: 5px !important\r\n }\r\n\r\n}",".percUp {\r\n color: forestgreen;\r\n}\r\n\r\n.percDown {\r\n color: red;\r\n}",".loaderBar {\r\n height: 4px;\r\n width: 100%;\r\n position: relative;\r\n overflow: hidden;\r\n background-color: #ddd;\r\n\r\n &:before{\r\n display: block;\r\n position: absolute;\r\n content: \"\";\r\n left: -200px;\r\n width: 200px;\r\n height: 4px;\r\n background-color: #2980b9;\r\n animation: loadingBar 2s linear infinite;\r\n }\r\n }\r\n\r\n @keyframes loadingBar {\r\n from {left: -200px; width: 30%;}\r\n 50% {width: 30%;}\r\n 70% {width: 70%;}\r\n 80% { left: 50%;}\r\n 95% {left: 120%;}\r\n to {left: 100%;}\r\n }",".postPathCol {\r\n width: 120px;\r\n}",".nav-link {\r\n cursor: pointer;\r\n}","body {\r\n padding: 10px;\r\n font-family: Arial, Helvetica, sans-serif\r\n}\r\n\r\nhr {\r\n border-top: 1px dotted #000000 !important;\r\n margin-bottom: 5px !important;\r\n margin-top: 5px !important;\r\n}\r\n","\r\n.float{\r\n\tposition:fixed;\r\n\twidth:60px;\r\n\theight:60px;\r\n\tbottom:40px;\r\n\tright:40px;\r\n\tbackground-color:#0C9;\r\n\tcolor:#FFF;\r\n\tborder-radius:50px;\r\n\ttext-align:center;\r\n box-shadow: 2px 2px 3px #999;\r\n opacity: 0.7;\r\n transition: opacity 0.2s ease-in-out;\r\n\r\n &:hover {\r\n opacity: 1;\r\n }\r\n}\r\n\r\n.my-float{\r\n\tmargin-top:22px;\r\n}\r\n\r\n@media only screen and (max-width: 600px) {\r\n\t.float{\r\n\t\tbottom:20px;\r\n\t\tright:20px;\r\n\t}\r\n }",".loading {\r\n position: fixed;\r\n top: 0;\r\n right: 0;\r\n bottom: 0;\r\n left: 0;\r\n background: #fff;\r\n \r\n .loader {\r\n left: 50%;\r\n margin-left: -4em;\r\n font-size: 10px;\r\n border: .8em solid rgba(218, 219, 223, 1);\r\n border-left: .8em solid rgba(58, 166, 165, 1);\r\n animation: spin 1.1s infinite linear;\r\n }\r\n\r\n .loader,\r\n .loader:after {\r\n border-radius: 50%;\r\n width: 8em;\r\n height: 8em;\r\n display: block;\r\n position: absolute;\r\n top: 50%;\r\n margin-top: -4.05em;\r\n }\r\n\r\n @keyframes spin {\r\n 0% {\r\n transform: rotate(0deg);\r\n }\r\n\r\n 100% {\r\n transform: rotate(360deg);\r\n }\r\n }\r\n}\r\n"]} -------------------------------------------------------------------------------- /client/src/components/PostModal.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Alert, Button, Modal, ModalHeader, ModalBody, ModalFooter, Label } from 'reactstrap'; 3 | import { FaExternalLinkAlt } from 'react-icons/fa'; 4 | import Highcharts from 'highcharts'; 5 | import highchartsSeriesLabel from "highcharts/modules/series-label"; 6 | import HighchartsReact from 'highcharts-react-official'; 7 | import { apiBackendUrl } from '../helpers/constants'; 8 | import { IPost } from '../interfaces/IPost'; 9 | import LoadingBar from './LoadingBar'; 10 | 11 | 12 | highchartsSeriesLabel(Highcharts); 13 | 14 | interface IProps { 15 | post: IPost 16 | } 17 | 18 | export default class PostModal extends React.Component { 19 | 20 | public state = { 21 | isLoading: true, 22 | hasData: false, 23 | isOpen: false, 24 | chartOptionsAndData: {}, 25 | error: false, 26 | errorMessage: "" 27 | } 28 | 29 | private toggle() { 30 | if (!this.state.isOpen) { 31 | this.loadChart(); 32 | } 33 | 34 | this.setState({ isOpen: !this.state.isOpen }); 35 | } 36 | 37 | private loadChart() { 38 | fetch(`${apiBackendUrl}post/getChart/${this.props.post.path}`).then(response => { 39 | 40 | if (!response.ok) { 41 | if (response.bodyUsed) { 42 | response.json().then(err => { 43 | this.setState({ error: true, errorMessage: err.message }); 44 | }); 45 | } else { 46 | this.setState({ error: true, errorMessage: response.statusText }); 47 | } 48 | return null; 49 | } 50 | 51 | return response.json(); 52 | 53 | }).then(data => { 54 | 55 | if (data === null) { 56 | this.setState({ isLoading: false }); 57 | return; 58 | } 59 | 60 | let chartOptionsAndData: Highcharts.Options = { 61 | chart: { 62 | type: 'spline', 63 | zoomType: 'x', 64 | panning: true, 65 | panKey: 'shift', 66 | resetZoomButton: { 67 | position: { 68 | align: 'left', 69 | verticalAlign: 'top', 70 | x: 0, 71 | y: -10 72 | } 73 | } 74 | }, 75 | yAxis: [{ 76 | labels: { 77 | style: { 78 | color: Highcharts.getOptions().colors[7] 79 | } 80 | }, 81 | title: { 82 | text: 'Likes', 83 | style: { 84 | color: Highcharts.getOptions().colors[7] 85 | } 86 | } 87 | }, { 88 | labels: { 89 | style: { 90 | color: Highcharts.getOptions().colors[9] 91 | } 92 | }, 93 | title: { 94 | text: 'Comments', 95 | style: { 96 | color: Highcharts.getOptions().colors[9] 97 | } 98 | }, 99 | opposite: true 100 | }], 101 | xAxis: { 102 | type: 'datetime', 103 | showFirstLabel: true 104 | }, 105 | series: [{ 106 | name: 'Likes', 107 | type: 'spline', 108 | data: data.likesArr, 109 | color: Highcharts.getOptions().colors[1] 110 | }, { 111 | name: 'Comments', 112 | type: 'spline', 113 | data: data.commentsArr, 114 | yAxis: 1, 115 | color: Highcharts.getOptions().colors[9] 116 | }], 117 | plotOptions: { spline: { marker: { enabled: false } } }, 118 | credits: { enabled: false }, 119 | exporting: { enabled: false }, 120 | title: { text: null }, 121 | subtitle: { text: null } 122 | } 123 | 124 | this.setState({ 125 | isLoading: false, 126 | hasData: data.likesArr.length > 0 || 127 | data.commentsArr.length > 0, 128 | chartOptionsAndData 129 | }); 130 | 131 | 132 | }); 133 | } 134 | 135 | render() { 136 | 137 | return { this.toggle(); e.preventDefault(); }}> 138 | 139 | {this.props.children} 140 | 141 | this.toggle()} isOpen={this.state.isOpen} fade={true} keyboard={true}> 142 | this.toggle()}> 143 | {this.props.post.path} 144 | 145 | 146 | 147 | {this.state.isLoading && !this.state.error && } 148 | 149 | {!this.state.isLoading && !this.state.hasData && } 150 | 151 | {this.state.error && {this.state.errorMessage}} 152 | 153 | {!this.state.isLoading && !this.state.error && this.state.hasData && 154 | { 158 | if (!this.state.isLoading && this.state.hasData && window.innerWidth < 800){ 159 | for (var i = 0; i < chart.yAxis.length; i++) { 160 | chart.yAxis[i].update({ 161 | visible: false 162 | }); 163 | } 164 | } 165 | }} 166 | /> 167 | } 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | ; 176 | } 177 | } -------------------------------------------------------------------------------- /backend/app/models/mongodb.js: -------------------------------------------------------------------------------- 1 | const logger = require("../logger.js"); 2 | const MongoClient = require('mongodb').MongoClient; 3 | const uri = process.env.MONGODB_URI; 4 | const dbName = process.env.MONGODB_DBNAME; 5 | 6 | let isCreating = false; 7 | let isConnecting = false; 8 | 9 | let _connection = null; 10 | 11 | const createCollection = (dbase, colName) => { 12 | 13 | return new Promise((resolve, reject) => { 14 | 15 | dbase.createCollection(colName, err => { 16 | if (err) return reject(err); 17 | 18 | resolve(); 19 | }); 20 | 21 | }); 22 | }; 23 | 24 | const createIndex = (collection, index, options) => { 25 | 26 | return new Promise((resolve, reject) => { 27 | 28 | collection.createIndex(index, options, err => { 29 | if (err) return reject(err); 30 | 31 | resolve(); 32 | }); 33 | 34 | }); 35 | }; 36 | 37 | const createDatabase = async (connection) => { 38 | 39 | var colls = [ 40 | 'is-log', 41 | 'is-post', 42 | 'is-post-history', 43 | 'is-post-queue', 44 | 'is-profile', 45 | 'is-profile-history', 46 | 'is-profile-queue' 47 | ]; 48 | 49 | let indexes = [ 50 | { 51 | collection: 'is-post', 52 | fields: { "scrapped": 1, "uploadDate": 1, "lastScrapDate": 1, "removed": 1, "notFoundCount": 1 }, 53 | options: { "unique": false, "name": "ix-post" } 54 | }, 55 | { 56 | collection: 'is-post', 57 | fields: { "username": 1, "likePercentage": -1, "commentPercentage": -1 }, 58 | options: { "unique": false, "name": "ix-post-stats" } 59 | }, 60 | { 61 | collection: 'is-post', 62 | fields: { "username": 1 }, 63 | options: { "unique": false, "name": "ix-post-username" } 64 | }, 65 | { 66 | collection: 'is-post', 67 | fields: { "path": 1 }, 68 | options: { "unique": true, "name": "ix-post-path" } 69 | }, 70 | { 71 | collection: 'is-post-history', 72 | fields: { "path": 1, "date": 1 }, 73 | options: { "unique": false, "name": "ix-post-history" } 74 | }, 75 | { 76 | collection: 'is-post-queue', 77 | fields: { "path": 1 }, 78 | options: { "unique": true, "name": "ix-post-queue" } 79 | }, 80 | { 81 | collection: 'is-profile', 82 | fields: { "username": 1 }, 83 | options: { "unique": true, "name": "ix-profile-username" } 84 | }, 85 | { 86 | collection: 'is-profile-history', 87 | fields: { "username": 1 }, 88 | options: { "unique": false, "name": "ix-profile-history-username" } 89 | }, 90 | { 91 | collection: 'is-profile-history', 92 | fields: { "username": 1, "date": 1 }, 93 | options: { "unique": true, "name": "ix-profile-history" } 94 | }, 95 | { 96 | collection: 'is-profile-queue', 97 | fields: { "username": 1 }, 98 | options: { "unique": true, "name": "ix-profile-queue-username" } 99 | } 100 | ]; 101 | 102 | for await (const col of colls) { 103 | //console.log(`Creating collection ${col}...`); 104 | await createCollection(connection, col).then(() => { 105 | //console.log(`Created!`); 106 | }).catch(err => { 107 | logger.error(err); 108 | }); 109 | } 110 | 111 | for await (const col of colls) { 112 | 113 | for await (const index of indexes) { 114 | 115 | if (index.collection === col) { 116 | 117 | let collection = await connection.collection(index.collection); 118 | 119 | //console.log(`Creating index on ${index.collection}...`); 120 | await createIndex(collection, index.fields, index.options).then(() => { 121 | //console.log(`Created!`); 122 | }).catch(err => { 123 | logger.error(err); 124 | }); 125 | 126 | } 127 | } 128 | } 129 | }; 130 | 131 | const connectDB = (callback) => { 132 | try { 133 | 134 | if (isCreating || isConnecting) { 135 | setTimeout(connectDB, 500, callback); 136 | return; 137 | } 138 | 139 | if (_connection) { 140 | return callback(null, _connection); 141 | } 142 | 143 | logger.info("Connectingto MongoDb"); 144 | 145 | isConnecting = true; 146 | 147 | MongoClient.connect(uri, { useNewUrlParser: true, poolSize: 10 }, async (err, connection) => { 148 | if (err) { 149 | logger.error("Could not connect to MongoDb", err); 150 | isCreating = false; 151 | isConnecting = false; 152 | return callback(err, null); 153 | } 154 | 155 | _connection = connection.db(dbName); 156 | 157 | if (process.env.MONGODB_CREATE !== undefined && process.env.MONGODB_CREATE.toLowerCase() === "true" && !isCreating) { 158 | isCreating = true; 159 | await createDatabase(_connection); 160 | isCreating = false; 161 | } 162 | 163 | isConnecting = false; 164 | return callback(null, _connection); 165 | }); 166 | 167 | } catch (err) { 168 | logger.error("Could not connect to mongo", err); 169 | isCreating = false; 170 | isConnecting = false; 171 | return callback(err, null); 172 | } 173 | }; 174 | 175 | exports.connectDB = connectDB; -------------------------------------------------------------------------------- /backend/app/controller/post-controller.js: -------------------------------------------------------------------------------- 1 | const moment = require("moment"); 2 | const logger = require("../logger.js"); 3 | const postModel = require("../models/post-model"); 4 | const postHistoryModel = require("../models/post-history-model"); 5 | 6 | const projectPost = post => { 7 | return { 8 | path: post.path, 9 | published: moment(new Date()).diff(moment(post.uploadDate), 'days'), 10 | likeCount: post.likeCount, 11 | likePercentage: post.likePercentage, 12 | commentCount: post.commentCount, 13 | commentPercentage: post.commentPercentage 14 | }; 15 | }; 16 | 17 | const removeDuplicated = (arr, key) => { 18 | const map = new Map(); 19 | arr.map(el => { 20 | if (el !== undefined && !map.has(el[key])) { 21 | map.set(el[key], el); 22 | } 23 | }); 24 | return [...map.values()]; 25 | }; 26 | 27 | exports.getStats = (username, limit) => { 28 | 29 | if (!limit || isNaN(limit)) { 30 | limit = 10; 31 | } 32 | 33 | limit = parseInt(limit, 10); 34 | 35 | return new Promise(async (resolve, reject) => { 36 | 37 | try { 38 | 39 | let arrTopLikesPercentage = await postModel.find({ username }, { likePercentage: -1 }, limit).then(posts => { 40 | return posts.map(post => { 41 | if (post.likePercentage >= 0.1 || post.commentPercentage >= 0.1) { 42 | return projectPost(post); 43 | } 44 | }); 45 | }); 46 | 47 | let arrTopCommentsPercentage = await postModel.find({ username }, { commentPercentage: -1 }, limit).then(posts => { 48 | return posts.map(post => { 49 | if (post.likePercentage >= 0.1 || post.commentPercentage >= 0.1) { 50 | return projectPost(post); 51 | } 52 | }); 53 | }); 54 | 55 | let arrTopLikesCount = await postModel.find({ username }, { likeCount: -1 }, limit).then(posts => { 56 | return posts.map(post => { 57 | return projectPost(post); 58 | }); 59 | }); 60 | 61 | let arrTopCommentsCount = await postModel.find({ username }, { commentCount: -1 }, limit).then(posts => { 62 | return posts.map(post => { 63 | return projectPost(post); 64 | }); 65 | }); 66 | 67 | 68 | let joinedArrays = arrTopCommentsPercentage.concat(arrTopLikesPercentage); 69 | let arrTopInteraction = removeDuplicated(joinedArrays, "path"); 70 | 71 | let responseObj = { 72 | topInteraction: arrTopInteraction.sort( 73 | (a, b) => { 74 | return a.likePercentage > b.likePercentage || a.commentPercentage > b.commentPercentage ? -1 : 1; 75 | } 76 | ).splice(0, limit), 77 | topLikes: arrTopLikesCount, 78 | topComments: arrTopCommentsCount 79 | }; 80 | 81 | resolve(responseObj); 82 | 83 | } catch (err) { 84 | logger.error("Error on getChart", err); 85 | reject({ code: 500, "message": err.message }); 86 | } 87 | 88 | }); 89 | }; 90 | 91 | exports.getChart = (path) => { 92 | 93 | return new Promise((resolve, reject) => { 94 | 95 | postHistoryModel.find({ path }, { date: 1 }, 0).then(items => { 96 | 97 | let likesArr = []; 98 | let commentsArr = []; 99 | 100 | items.forEach(data => { 101 | 102 | let dtTicks = data.date.getTime(); 103 | 104 | likesArr.push([dtTicks, data.likeCount]); 105 | commentsArr.push([dtTicks, data.commentCount]); 106 | 107 | }); 108 | 109 | let responseObj = { 110 | likesArr, 111 | commentsArr 112 | }; 113 | 114 | resolve(responseObj); 115 | 116 | }).catch(err => { 117 | logger.error("Error on getChart", err); 118 | reject({ code: 500, "message": err.message }); 119 | }); 120 | 121 | }); 122 | }; 123 | 124 | exports.exportPosts = (res, username, pageSize, pageNum) => { 125 | 126 | return new Promise((resolve, reject) => { 127 | 128 | if (!username) { 129 | return reject({ code: 400, "message": "Username is a required field!" }); 130 | } 131 | 132 | if (pageSize !== undefined || pageNum !== undefined){ 133 | if (isNaN(pageSize) || pageSize <= 0) { 134 | return reject({ code: 401, "message": "PageSize must be a number greater than 0" }); 135 | } 136 | if (isNaN(pageNum) || pageNum <= 0) { 137 | return reject({ code: 401, "message": "PageNum must be a number greater than 0" }); 138 | } 139 | } 140 | 141 | let criteria = [ 142 | { 143 | "$match": 144 | { 145 | "username": username 146 | } 147 | }, 148 | { 149 | "$lookup": { 150 | "from": "is-post-history", 151 | "localField": "path", 152 | "foreignField": "path", 153 | "as": "history" 154 | } 155 | }, 156 | { 157 | "$project": { 158 | "_id": 0, 159 | "path": 1, 160 | "image": 1, 161 | "description": 1, 162 | "history.likeCount": 1, 163 | "history.commentCount": 1, 164 | "history.date": 1 165 | } 166 | } 167 | ]; 168 | 169 | return postModel.aggregateToStream(criteria, pageSize, pageNum).then(cursor => { 170 | 171 | let hasError = false; 172 | let hasResults = false; 173 | 174 | res.setHeader('Content-type', 'text/json'); 175 | 176 | cursor.on('error', err => { 177 | hasError = true; 178 | reject({ code: 500, "message": err.message }); 179 | }); 180 | 181 | cursor.on('data', doc => { 182 | if (!hasResults) { 183 | res.write("["); 184 | }else{ 185 | res.write(","); 186 | } 187 | 188 | res.write(JSON.stringify(doc)); 189 | res.flush(); 190 | 191 | hasResults = true; 192 | }); 193 | 194 | cursor.once('end', () => { 195 | if (!hasError) { 196 | resolve(); 197 | if (!hasResults) { 198 | res.write("["); 199 | } 200 | res.end("]"); 201 | } 202 | }); 203 | 204 | }).catch(err => { 205 | logger.error("Error on export posts", err); 206 | reject({ code: 500, "message": err.message }); 207 | }); 208 | 209 | }); 210 | 211 | }; -------------------------------------------------------------------------------- /backend/app/public/static/js/runtime~main.a8a9905a.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["../webpack/bootstrap"],"names":["webpackJsonpCallback","data","moduleId","chunkId","chunkIds","moreModules","executeModules","i","resolves","length","installedChunks","push","Object","prototype","hasOwnProperty","call","modules","parentJsonpFunction","shift","deferredModules","apply","checkDeferredModules","result","deferredModule","fulfilled","j","depId","splice","__webpack_require__","s","installedModules","1","exports","module","l","m","c","d","name","getter","o","defineProperty","enumerable","get","r","Symbol","toStringTag","value","t","mode","__esModule","ns","create","key","bind","n","object","property","p","jsonpArray","window","oldJsonpFunction","slice"],"mappings":"aACA,SAAAA,EAAAC,GAQA,IAPA,IAMAC,EAAAC,EANAC,EAAAH,EAAA,GACAI,EAAAJ,EAAA,GACAK,EAAAL,EAAA,GAIAM,EAAA,EAAAC,EAAA,GACQD,EAAAH,EAAAK,OAAoBF,IAC5BJ,EAAAC,EAAAG,GACAG,EAAAP,IACAK,EAAAG,KAAAD,EAAAP,GAAA,IAEAO,EAAAP,GAAA,EAEA,IAAAD,KAAAG,EACAO,OAAAC,UAAAC,eAAAC,KAAAV,EAAAH,KACAc,EAAAd,GAAAG,EAAAH,IAKA,IAFAe,KAAAhB,GAEAO,EAAAC,QACAD,EAAAU,OAAAV,GAOA,OAHAW,EAAAR,KAAAS,MAAAD,EAAAb,GAAA,IAGAe,IAEA,SAAAA,IAEA,IADA,IAAAC,EACAf,EAAA,EAAiBA,EAAAY,EAAAV,OAA4BF,IAAA,CAG7C,IAFA,IAAAgB,EAAAJ,EAAAZ,GACAiB,GAAA,EACAC,EAAA,EAAkBA,EAAAF,EAAAd,OAA2BgB,IAAA,CAC7C,IAAAC,EAAAH,EAAAE,GACA,IAAAf,EAAAgB,KAAAF,GAAA,GAEAA,IACAL,EAAAQ,OAAApB,IAAA,GACAe,EAAAM,IAAAC,EAAAN,EAAA,KAGA,OAAAD,EAIA,IAAAQ,EAAA,GAKApB,EAAA,CACAqB,EAAA,GAGAZ,EAAA,GAGA,SAAAS,EAAA1B,GAGA,GAAA4B,EAAA5B,GACA,OAAA4B,EAAA5B,GAAA8B,QAGA,IAAAC,EAAAH,EAAA5B,GAAA,CACAK,EAAAL,EACAgC,GAAA,EACAF,QAAA,IAUA,OANAhB,EAAAd,GAAAa,KAAAkB,EAAAD,QAAAC,IAAAD,QAAAJ,GAGAK,EAAAC,GAAA,EAGAD,EAAAD,QAKAJ,EAAAO,EAAAnB,EAGAY,EAAAQ,EAAAN,EAGAF,EAAAS,EAAA,SAAAL,EAAAM,EAAAC,GACAX,EAAAY,EAAAR,EAAAM,IACA1B,OAAA6B,eAAAT,EAAAM,EAAA,CAA0CI,YAAA,EAAAC,IAAAJ,KAK1CX,EAAAgB,EAAA,SAAAZ,GACA,qBAAAa,eAAAC,aACAlC,OAAA6B,eAAAT,EAAAa,OAAAC,YAAA,CAAwDC,MAAA,WAExDnC,OAAA6B,eAAAT,EAAA,cAAiDe,OAAA,KAQjDnB,EAAAoB,EAAA,SAAAD,EAAAE,GAEA,GADA,EAAAA,IAAAF,EAAAnB,EAAAmB,IACA,EAAAE,EAAA,OAAAF,EACA,KAAAE,GAAA,kBAAAF,QAAAG,WAAA,OAAAH,EACA,IAAAI,EAAAvC,OAAAwC,OAAA,MAGA,GAFAxB,EAAAgB,EAAAO,GACAvC,OAAA6B,eAAAU,EAAA,WAAyCT,YAAA,EAAAK,UACzC,EAAAE,GAAA,iBAAAF,EAAA,QAAAM,KAAAN,EAAAnB,EAAAS,EAAAc,EAAAE,EAAA,SAAAA,GAAgH,OAAAN,EAAAM,IAAqBC,KAAA,KAAAD,IACrI,OAAAF,GAIAvB,EAAA2B,EAAA,SAAAtB,GACA,IAAAM,EAAAN,KAAAiB,WACA,WAA2B,OAAAjB,EAAA,SAC3B,WAAiC,OAAAA,GAEjC,OADAL,EAAAS,EAAAE,EAAA,IAAAA,GACAA,GAIAX,EAAAY,EAAA,SAAAgB,EAAAC,GAAsD,OAAA7C,OAAAC,UAAAC,eAAAC,KAAAyC,EAAAC,IAGtD7B,EAAA8B,EAAA,IAEA,IAAAC,EAAAC,OAAA,aAAAA,OAAA,iBACAC,EAAAF,EAAAhD,KAAA2C,KAAAK,GACAA,EAAAhD,KAAAX,EACA2D,IAAAG,QACA,QAAAvD,EAAA,EAAgBA,EAAAoD,EAAAlD,OAAuBF,IAAAP,EAAA2D,EAAApD,IACvC,IAAAU,EAAA4C,EAIAxC","file":"static/js/runtime~main.a8a9905a.js","sourcesContent":[" \t// install a JSONP callback for chunk loading\n \tfunction webpackJsonpCallback(data) {\n \t\tvar chunkIds = data[0];\n \t\tvar moreModules = data[1];\n \t\tvar executeModules = data[2];\n\n \t\t// add \"moreModules\" to the modules object,\n \t\t// then flag all \"chunkIds\" as loaded and fire callback\n \t\tvar moduleId, chunkId, i = 0, resolves = [];\n \t\tfor(;i < chunkIds.length; i++) {\n \t\t\tchunkId = chunkIds[i];\n \t\t\tif(installedChunks[chunkId]) {\n \t\t\t\tresolves.push(installedChunks[chunkId][0]);\n \t\t\t}\n \t\t\tinstalledChunks[chunkId] = 0;\n \t\t}\n \t\tfor(moduleId in moreModules) {\n \t\t\tif(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {\n \t\t\t\tmodules[moduleId] = moreModules[moduleId];\n \t\t\t}\n \t\t}\n \t\tif(parentJsonpFunction) parentJsonpFunction(data);\n\n \t\twhile(resolves.length) {\n \t\t\tresolves.shift()();\n \t\t}\n\n \t\t// add entry modules from loaded chunk to deferred list\n \t\tdeferredModules.push.apply(deferredModules, executeModules || []);\n\n \t\t// run deferred modules when all chunks ready\n \t\treturn checkDeferredModules();\n \t};\n \tfunction checkDeferredModules() {\n \t\tvar result;\n \t\tfor(var i = 0; i < deferredModules.length; i++) {\n \t\t\tvar deferredModule = deferredModules[i];\n \t\t\tvar fulfilled = true;\n \t\t\tfor(var j = 1; j < deferredModule.length; j++) {\n \t\t\t\tvar depId = deferredModule[j];\n \t\t\t\tif(installedChunks[depId] !== 0) fulfilled = false;\n \t\t\t}\n \t\t\tif(fulfilled) {\n \t\t\t\tdeferredModules.splice(i--, 1);\n \t\t\t\tresult = __webpack_require__(__webpack_require__.s = deferredModule[0]);\n \t\t\t}\n \t\t}\n \t\treturn result;\n \t}\n\n \t// The module cache\n \tvar installedModules = {};\n\n \t// object to store loaded and loading chunks\n \t// undefined = chunk not loaded, null = chunk preloaded/prefetched\n \t// Promise = chunk loading, 0 = chunk loaded\n \tvar installedChunks = {\n \t\t1: 0\n \t};\n\n \tvar deferredModules = [];\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"/\";\n\n \tvar jsonpArray = window[\"webpackJsonp\"] = window[\"webpackJsonp\"] || [];\n \tvar oldJsonpFunction = jsonpArray.push.bind(jsonpArray);\n \tjsonpArray.push = webpackJsonpCallback;\n \tjsonpArray = jsonpArray.slice();\n \tfor(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);\n \tvar parentJsonpFunction = oldJsonpFunction;\n\n\n \t// run deferred modules from other chunks\n \tcheckDeferredModules();\n"],"sourceRoot":""} -------------------------------------------------------------------------------- /client/src/components/ProfileModal.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Alert, Button, Modal, ModalHeader, ModalBody, ModalFooter, Label } from 'reactstrap'; 3 | import { FaExternalLinkAlt } from 'react-icons/fa'; 4 | import Highcharts from 'highcharts'; 5 | import highchartsSeriesLabel from "highcharts/modules/series-label"; 6 | import HighchartsReact from 'highcharts-react-official'; 7 | import { IProfile } from '../interfaces/IProfile'; 8 | import { apiBackendUrl } from '../helpers/constants'; 9 | import PostStats from './PostStats'; 10 | import LoadingBar from './LoadingBar'; 11 | 12 | highchartsSeriesLabel(Highcharts); 13 | 14 | interface IProps { 15 | profile: IProfile 16 | } 17 | 18 | export default class ProfileModal extends React.Component { 19 | 20 | public state = { 21 | isLoading: true, 22 | hasData: false, 23 | isOpen: false, 24 | chartOptionsAndData: {}, 25 | error: false, 26 | errorMessage: "" 27 | } 28 | 29 | private toggle() { 30 | if (!this.state.isOpen) { 31 | this.loadChart(); 32 | } 33 | 34 | this.setState({ isOpen: !this.state.isOpen }); 35 | } 36 | 37 | private loadChart() { 38 | fetch(`${apiBackendUrl}profile/getChart/${this.props.profile.username}`).then(response => { 39 | 40 | if (!response.ok) { 41 | if (response.bodyUsed) { 42 | response.json().then(err => { 43 | this.setState({ error: true, errorMessage: err.message }); 44 | }); 45 | } else { 46 | this.setState({ error: true, errorMessage: response.statusText }); 47 | } 48 | return null; 49 | } 50 | 51 | return response.json(); 52 | 53 | }).then(data => { 54 | 55 | if (data === null) { 56 | this.setState({ isLoading: false }); 57 | return; 58 | } 59 | 60 | let chartOptionsAndData: Highcharts.Options = { 61 | chart: { 62 | type: 'spline', 63 | zoomType: 'x', 64 | panning: true, 65 | panKey: 'shift', 66 | resetZoomButton: { 67 | position: { 68 | align: 'left', 69 | verticalAlign: 'top', 70 | x: 0, 71 | y: -10 72 | } 73 | } 74 | }, 75 | yAxis: [{ 76 | labels: { 77 | style: { 78 | color: Highcharts.getOptions().colors[1] 79 | } 80 | }, 81 | title: { 82 | text: 'Following', 83 | style: { 84 | color: Highcharts.getOptions().colors[1] 85 | } 86 | }, 87 | opposite: true 88 | }, { 89 | labels: { 90 | style: { 91 | color: Highcharts.getOptions().colors[3] 92 | } 93 | }, 94 | title: { 95 | text: 'Followed', 96 | style: { 97 | color: Highcharts.getOptions().colors[3] 98 | } 99 | }, 100 | opposite: true 101 | }, { 102 | labels: { 103 | style: { 104 | color: Highcharts.getOptions().colors[5] 105 | } 106 | }, 107 | title: { 108 | text: 'Posts', 109 | style: { 110 | color: Highcharts.getOptions().colors[5] 111 | } 112 | }, 113 | opposite: true 114 | }, { 115 | labels: { 116 | style: { 117 | color: Highcharts.getOptions().colors[7] 118 | } 119 | }, 120 | title: { 121 | text: 'Likes', 122 | style: { 123 | color: Highcharts.getOptions().colors[7] 124 | } 125 | } 126 | }, { 127 | labels: { 128 | style: { 129 | color: Highcharts.getOptions().colors[9] 130 | } 131 | }, 132 | title: { 133 | text: 'Comments', 134 | style: { 135 | color: Highcharts.getOptions().colors[9] 136 | } 137 | }, 138 | opposite: true 139 | }], 140 | xAxis: { 141 | type: 'datetime', 142 | showFirstLabel: true 143 | }, 144 | series: [{ 145 | name: 'Following', 146 | type: 'spline', 147 | data: data.followArr, 148 | visible: false, 149 | color: Highcharts.getOptions().colors[1] 150 | }, { 151 | name: 'Followed', 152 | type: 'spline', 153 | data: data.followedArr, 154 | yAxis: 1, 155 | color: Highcharts.getOptions().colors[3] 156 | }, { 157 | name: 'Posts', 158 | type: 'spline', 159 | data: data.mediasArr, 160 | yAxis: 2, 161 | color: Highcharts.getOptions().colors[5] 162 | }, { 163 | name: 'Likes', 164 | type: 'spline', 165 | data: data.likesArr, 166 | yAxis: 3, 167 | color: Highcharts.getOptions().colors[7] 168 | }, { 169 | name: 'Comments', 170 | type: 'spline', 171 | data: data.commentsArr, 172 | yAxis: 4, 173 | color: Highcharts.getOptions().colors[9] 174 | }], 175 | plotOptions: { 176 | spline: { 177 | marker: { enabled: false } 178 | } 179 | }, 180 | credits: { enabled: false }, 181 | exporting: { enabled: false }, 182 | title: { text: null }, 183 | subtitle: { text: null } 184 | } 185 | 186 | this.setState({ 187 | isLoading: false, 188 | hasData: data.followArr.length > 0 || 189 | data.followedArr.length > 0 || 190 | data.mediasArr.length > 0 || 191 | data.likesArr.length > 0 || 192 | data.commentsArr.length > 0, 193 | chartOptionsAndData 194 | }); 195 | 196 | }); 197 | } 198 | 199 | render() { 200 | 201 | return
{ this.toggle(); e.preventDefault(); }}> 202 | 203 | {this.props.children} 204 | 205 | this.toggle()} isOpen={this.state.isOpen} fade={true} keyboard={true}> 206 | this.toggle()}> 207 | {this.props.profile.fullName} 208 | 209 | 210 | 211 | {this.state.isLoading && !this.state.error && } 212 | 213 | {!this.state.isLoading && !this.state.hasData && } 214 | 215 | {this.state.error && {this.state.errorMessage}} 216 | 217 | {!this.state.isLoading && !this.state.error && this.state.hasData && 218 | { 222 | if (!this.state.isLoading && this.state.hasData && window.innerWidth < 800){ 223 | for (var i = 0; i < chart.yAxis.length; i++) { 224 | chart.yAxis[i].update({ 225 | visible: false 226 | }); 227 | } 228 | } 229 | }} 230 | /> 231 | } 232 | 233 | {!this.state.isLoading && this.state.hasData && 234 | } 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 |
; 243 | } 244 | } -------------------------------------------------------------------------------- /backend/app/controller/profile-controller.js: -------------------------------------------------------------------------------- 1 | const logger = require("../logger.js"); 2 | const eventBus = require("../eventBus"); 3 | const profileModel = require("../models/profile-model"); 4 | const profileHistoryModel = require("../models/profile-history-model.js"); 5 | const postModel = require("../models/post-model"); 6 | const postHistoryModel = require("../models/post-history-model"); 7 | 8 | exports.list = () => { 9 | 10 | return new Promise((resolve, reject) => { 11 | 12 | profileModel.find({}, { fullName: 1, username: 1 }, 0).then(profiles => { 13 | resolve(profiles); 14 | }).catch(err => { 15 | logger.error("Error on list profiles", err); 16 | reject({ code: 500, "message": err.message }); 17 | }); 18 | 19 | }); 20 | }; 21 | 22 | exports.save = (entry) => { 23 | 24 | return new Promise((resolve, reject) => { 25 | 26 | if (entry === undefined || entry.username === undefined) { 27 | return reject({ code: 400, "message": "Username is a required field!" }); 28 | } 29 | 30 | if (process.env.ISCRAPPER_READONLY !== undefined && process.env.ISCRAPPER_READONLY.toLowerCase() === "true") { 31 | return reject({ code: 401, "message": "This function is currently disabled on this server." }); 32 | } 33 | 34 | let reg = /^([A-Za-z0-9_](?:(?:[A-Za-z0-9_]|(?:\.(?!\.))){0,28}(?:[A-Za-z0-9_]))?)$/g; 35 | 36 | if (!reg.test(entry.username)) { 37 | return reject({ code: 400, "message": "Usernames can only use letters, numbers, underscores and periods!" }); 38 | } 39 | 40 | profileModel.get(entry.username).then(profile_exists => { 41 | 42 | if (profile_exists.length > 0) { 43 | throw new Error("Profile already exists!"); 44 | } 45 | 46 | let profile = { 47 | fullName: "", 48 | username: entry.username, 49 | followCount: 0, 50 | followedByCount: 0, 51 | postsScrapped: 0, 52 | likeCount: 0, 53 | commentCount: 0, 54 | mediaCount: 0, 55 | isPrivate: false, 56 | lastScrapDate: null, 57 | scrapping: false, 58 | notFound: false, 59 | scanned: false 60 | }; 61 | 62 | return profileModel.save(profile).then(savedProfile => { 63 | eventBus.emit("newMessage", JSON.stringify({ message: "new profile" })); 64 | resolve(savedProfile); 65 | }); 66 | 67 | }).catch(err => { 68 | logger.error("Error on save profile", err); 69 | reject({ code: 500, "message": err.message }); 70 | }); 71 | 72 | }); 73 | }; 74 | 75 | exports.remove = (username) => { 76 | 77 | return new Promise((resolve, reject) => { 78 | 79 | if (process.env.ISCRAPPER_READONLY !== undefined && process.env.ISCRAPPER_READONLY.toLowerCase() === "true") { 80 | return reject({ code: 401, "message": "This function is currently disabled on this server." }); 81 | } 82 | 83 | profileModel.get(username).then(profile_exists => { 84 | 85 | if (profile_exists.length <= 0) { 86 | return reject({ code: 404, "message": "Profile not found!" }); 87 | } 88 | 89 | if (profile_exists[0].isFixed) { 90 | return reject({ code: 401, "message": "This profile is fixed!" }); 91 | } 92 | 93 | return profileModel.remove({ username: username }).then(async numRemoved => { 94 | 95 | if (numRemoved === null || numRemoved === 0) { 96 | return reject({ code: 404, "message": "Profile not found!" }); 97 | } 98 | 99 | await profileHistoryModel.remove({ username: username }).then(() => { 100 | return true; 101 | }); 102 | 103 | let postsPathArray = await postModel.find({ username: username }, {}, 0).then(posts => { 104 | return posts.map(post => { 105 | return post.path; 106 | }); 107 | }).catch(() => { 108 | return []; 109 | }); 110 | 111 | await postModel.remove({ path: { $in: postsPathArray } }).then(() => { 112 | return true; 113 | }); 114 | 115 | await postHistoryModel.remove({ path: { $in: postsPathArray } }).then(() => { 116 | eventBus.emit("newMessage", JSON.stringify({ message: "removed profile" })); 117 | resolve("OK"); 118 | }); 119 | 120 | }); 121 | 122 | }).catch(err => { 123 | logger.error("Error on remove Profile", err); 124 | reject({ code: 500, "message": "Please try again!" }); 125 | }); 126 | 127 | }); 128 | }; 129 | 130 | exports.getChart = (username) => { 131 | 132 | return new Promise((resolve, reject) => { 133 | 134 | profileHistoryModel.find({ username }, { date: 1 }, 0).then(items => { 135 | 136 | let followArr = []; 137 | let followedArr = []; 138 | let mediasArr = []; 139 | let likesArr = []; 140 | let commentsArr = []; 141 | 142 | items.forEach(data => { 143 | 144 | let dtTicks = data.date.getTime(); 145 | 146 | followArr.push([dtTicks, data.followCount]); 147 | followedArr.push([dtTicks, data.followedByCount]); 148 | mediasArr.push([dtTicks, data.mediaCount]); 149 | likesArr.push([dtTicks, data.likeCount]); 150 | commentsArr.push([dtTicks, data.commentCount]); 151 | 152 | }); 153 | 154 | resolve({ followArr, followedArr, mediasArr, likesArr, commentsArr }); 155 | 156 | }).catch(err => { 157 | logger.error("Error on getChart", err); 158 | reject({ code: 500, "message": err.message }); 159 | }); 160 | 161 | }); 162 | }; 163 | 164 | 165 | exports.exportProfile = (res, username, skip, limit) => { 166 | 167 | return new Promise((resolve, reject) => { 168 | 169 | if (!username) { 170 | return reject({ code: 400, "message": "Username is a required field!" }); 171 | } 172 | 173 | let criteria = [ 174 | { 175 | "$match": 176 | { 177 | "username": username 178 | } 179 | }, 180 | { 181 | "$lookup": { 182 | "from": "is-profile-history", 183 | "localField": "username", 184 | "foreignField": "username", 185 | "as": "profileHistory" 186 | } 187 | }, 188 | { 189 | "$project": 190 | { 191 | "_id": 0, 192 | "fullName": 1, 193 | "username": 1, 194 | "mediaCount": 1, 195 | "isPrivate": 1, 196 | "notFound": 1, 197 | "profileHistory.followCount": 1, 198 | "profileHistory.followedByCount": 1, 199 | "profileHistory.mediaCount": 1, 200 | "profileHistory.likeCount": 1, 201 | "profileHistory.commentCount": 1, 202 | "profileHistory.date": 1 203 | } 204 | } 205 | ]; 206 | 207 | return profileModel.aggregateToStream(criteria, skip, limit).then(cursor => { 208 | 209 | let hasError = false; 210 | let hasResults = false; 211 | 212 | res.setHeader('Content-type', 'text/json'); 213 | 214 | cursor.on('error', err => { 215 | hasError = true; 216 | reject({ code: 500, "message": err.message }); 217 | }); 218 | 219 | cursor.on('data', doc => { 220 | hasResults = true; 221 | res.write(JSON.stringify(doc)); 222 | res.flush(); 223 | }); 224 | 225 | cursor.once('end', () => { 226 | if (!hasError) { 227 | 228 | if (hasResults) { 229 | resolve(); 230 | res.end(); 231 | }else{ 232 | reject({ code: 404, "message": "Profile not found" }); 233 | } 234 | } 235 | }); 236 | 237 | }).catch(err => { 238 | logger.error("Error on export profiles", err); 239 | reject({ code: 500, "message": err.message }); 240 | }); 241 | 242 | }); 243 | 244 | }; -------------------------------------------------------------------------------- /backend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "es6": true, 4 | "node": true, 5 | "jquery": true, 6 | "browser": true 7 | }, 8 | "extends": ["eslint:recommended", "plugin:node/recommended"], 9 | "parserOptions": { 10 | "sourceType": "module" 11 | }, 12 | "plugins": [ 13 | "promise", 14 | "node" 15 | ], 16 | "rules": { 17 | "node/no-unpublished-require": "off", 18 | "node/no-unsupported-features/es-syntax": "off", 19 | "promise/always-return": "off", 20 | "promise/no-return-wrap": "error", 21 | "promise/param-names": "error", 22 | "promise/catch-or-return": "error", 23 | "promise/no-native": "off", 24 | "promise/no-nesting": "off", 25 | "promise/no-promise-in-callback": "warn", 26 | "promise/no-callback-in-promise": "warn", 27 | "promise/avoid-new": "off", 28 | "promise/no-return-in-finally": "warn", 29 | "accessor-pairs": "error", 30 | "array-bracket-newline": "off", 31 | "array-bracket-spacing": [ 32 | "error", 33 | "never" 34 | ], 35 | "array-callback-return": "off", 36 | "array-element-newline": "off", 37 | "arrow-body-style": "off", 38 | "arrow-parens": "off", 39 | "arrow-spacing": [ 40 | "error", 41 | { 42 | "after": true, 43 | "before": true 44 | } 45 | ], 46 | "block-scoped-var": "error", 47 | "block-spacing": [ 48 | "error", 49 | "always" 50 | ], 51 | "brace-style": "off", 52 | "callback-return": "error", 53 | "camelcase": "off", 54 | "capitalized-comments": "off", 55 | "class-methods-use-this": "off", 56 | "comma-dangle": "error", 57 | "comma-spacing": [ 58 | "error", 59 | { 60 | "after": true, 61 | "before": false 62 | } 63 | ], 64 | "comma-style": [ 65 | "error", 66 | "last" 67 | ], 68 | "complexity": "error", 69 | "computed-property-spacing": [ 70 | "error", 71 | "never" 72 | ], 73 | "consistent-return": "off", 74 | "consistent-this": "error", 75 | "curly": "off", 76 | "default-case": "error", 77 | "dot-location": [ 78 | "error", 79 | "property" 80 | ], 81 | "dot-notation": "off", 82 | "eol-last": [ 83 | "error", 84 | "never" 85 | ], 86 | "eqeqeq": "off", 87 | "for-direction": "error", 88 | "func-call-spacing": "error", 89 | "func-name-matching": "error", 90 | "func-names": [ 91 | "error", 92 | "never" 93 | ], 94 | "func-style": "error", 95 | "generator-star-spacing": "error", 96 | "getter-return": "error", 97 | "global-require": "off", 98 | "guard-for-in": "off", 99 | "handle-callback-err": "off", 100 | "id-blacklist": "error", 101 | "id-length": "off", 102 | "id-match": "error", 103 | "indent": "off", 104 | "indent-legacy": "off", 105 | "init-declarations": "off", 106 | "jsx-quotes": "error", 107 | "key-spacing": "error", 108 | "keyword-spacing": "off", 109 | "line-comment-position": "off", 110 | "linebreak-style": ["off"], 111 | "lines-around-comment": "error", 112 | "lines-around-directive": "off", 113 | "max-depth": "off", 114 | "max-len": "off", 115 | "max-lines": "off", 116 | "max-nested-callbacks": "error", 117 | "max-params": "off", 118 | "max-statements": "off", 119 | "max-statements-per-line": "off", 120 | "new-cap": "error", 121 | "new-parens": "error", 122 | "newline-after-var": "off", 123 | "newline-before-return": "off", 124 | "newline-per-chained-call": "off", 125 | "no-alert": "error", 126 | "no-array-constructor": "off", 127 | "no-await-in-loop": "error", 128 | "no-bitwise": "error", 129 | "no-buffer-constructor": "error", 130 | "no-caller": "error", 131 | "no-catch-shadow": "off", 132 | "no-confusing-arrow": "error", 133 | "no-continue": "off", 134 | "no-div-regex": "error", 135 | "no-duplicate-imports": "error", 136 | "no-else-return": "off", 137 | "no-empty-function": "error", 138 | "no-eq-null": "off", 139 | "no-eval": "error", 140 | "no-console": "off", 141 | "no-extend-native": "off", 142 | "no-extra-bind": "error", 143 | "no-extra-label": "error", 144 | "no-extra-parens": "off", 145 | "no-floating-decimal": "error", 146 | "no-implicit-coercion": "error", 147 | "no-implicit-globals": "error", 148 | "no-implied-eval": "error", 149 | "no-inline-comments": "off", 150 | "no-invalid-this": "off", 151 | "no-iterator": "error", 152 | "no-label-var": "error", 153 | "no-labels": "error", 154 | "no-lone-blocks": "error", 155 | "no-lonely-if": "off", 156 | "no-loop-func": "off", 157 | "no-magic-numbers": "off", 158 | "no-mixed-operators": "error", 159 | "no-mixed-requires": "error", 160 | "no-multi-assign": "error", 161 | "no-multi-spaces": "off", 162 | "no-multi-str": "error", 163 | "no-multiple-empty-lines": "error", 164 | "no-native-reassign": "error", 165 | "no-negated-condition": "off", 166 | "no-negated-in-lhs": "error", 167 | "no-nested-ternary": "error", 168 | "no-new": "error", 169 | "no-new-func": "error", 170 | "no-new-object": "error", 171 | "no-new-require": "error", 172 | "no-new-wrappers": "error", 173 | "no-octal-escape": "error", 174 | "no-param-reassign": "off", 175 | "no-path-concat": "error", 176 | "no-plusplus": "off", 177 | "no-process-env": "off", 178 | "no-process-exit": "error", 179 | "no-proto": "error", 180 | "no-prototype-builtins": "error", 181 | "no-restricted-globals": "error", 182 | "no-restricted-imports": "error", 183 | "no-restricted-modules": "error", 184 | "no-restricted-properties": "error", 185 | "no-restricted-syntax": "error", 186 | "no-return-assign": "error", 187 | "no-return-await": "error", 188 | "no-script-url": "error", 189 | "no-self-compare": "error", 190 | "no-sequences": "error", 191 | "no-shadow": "off", 192 | "no-shadow-restricted-names": "error", 193 | "no-spaced-func": "error", 194 | "no-sync": "error", 195 | "no-tabs": "off", 196 | "no-template-curly-in-string": "error", 197 | "no-ternary": "off", 198 | "no-throw-literal": "error", 199 | "no-trailing-spaces": "off", 200 | "no-undef-init": "error", 201 | "no-undefined": "off", 202 | "no-underscore-dangle": "off", 203 | "no-unmodified-loop-condition": "error", 204 | "no-unneeded-ternary": "error", 205 | "no-unused-expressions": "error", 206 | "no-use-before-define": "error", 207 | "no-useless-call": "error", 208 | "no-useless-computed-key": "error", 209 | "no-useless-concat": "error", 210 | "no-useless-constructor": "error", 211 | "no-useless-rename": "error", 212 | "no-useless-return": "error", 213 | "no-var": "off", 214 | "no-void": "error", 215 | "no-warning-comments": "off", 216 | "no-whitespace-before-property": "error", 217 | "no-with": "error", 218 | "nonblock-statement-body-position": "error", 219 | "object-curly-newline": "error", 220 | "object-curly-spacing": "off", 221 | "object-property-newline": [ 222 | "error", 223 | { 224 | "allowMultiplePropertiesPerLine": true 225 | } 226 | ], 227 | "object-shorthand": "off", 228 | "one-var": "off", 229 | "one-var-declaration-per-line": "off", 230 | "operator-assignment": [ 231 | "error", 232 | "always" 233 | ], 234 | "operator-linebreak": "error", 235 | "padded-blocks": "off", 236 | "padding-line-between-statements": "error", 237 | "prefer-arrow-callback": "off", 238 | "prefer-const": "off", 239 | "prefer-destructuring": "off", 240 | "prefer-numeric-literals": "error", 241 | "prefer-promise-reject-errors": "off", 242 | "prefer-reflect": "off", 243 | "prefer-rest-params": "error", 244 | "prefer-spread": "off", 245 | "prefer-template": "off", 246 | "quote-props": "off", 247 | "quotes": "off", 248 | "radix": "error", 249 | "require-await": "error", 250 | "require-jsdoc": "error", 251 | "rest-spread-spacing": "error", 252 | "semi": "error", 253 | "semi-spacing": [ 254 | "error", 255 | { 256 | "after": true, 257 | "before": false 258 | } 259 | ], 260 | "semi-style": [ 261 | "error", 262 | "last" 263 | ], 264 | "sort-imports": "error", 265 | "sort-keys": "off", 266 | "sort-vars": "error", 267 | "space-before-blocks": "off", 268 | "space-before-function-paren": "error", 269 | "space-in-parens": [ 270 | "error", 271 | "never" 272 | ], 273 | "space-infix-ops": "error", 274 | "space-unary-ops": [ 275 | "error", 276 | { 277 | "nonwords": false, 278 | "words": false 279 | } 280 | ], 281 | "spaced-comment": "off", 282 | "strict": "off", 283 | "switch-colon-spacing": "error", 284 | "symbol-description": "error", 285 | "template-curly-spacing": [ 286 | "error", 287 | "never" 288 | ], 289 | "template-tag-spacing": "error", 290 | "unicode-bom": [ 291 | "error", 292 | "never" 293 | ], 294 | "valid-jsdoc": "off", 295 | "vars-on-top": "error", 296 | "wrap-iife": "error", 297 | "wrap-regex": "error", 298 | "yield-star-spacing": "error", 299 | "yoda": [ 300 | "error", 301 | "never" 302 | ] 303 | } 304 | }; -------------------------------------------------------------------------------- /backend/app/public/static/js/main.171b13fe.chunk.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[0],{32:function(e,t,a){e.exports=a(55)},38:function(e,t,a){},39:function(e,t,a){},42:function(e,t,a){},50:function(e,t,a){},51:function(e,t,a){},52:function(e,t,a){},53:function(e,t,a){},54:function(e,t,a){},55:function(e,t,a){"use strict";a.r(t);a(33);var n=a(1),l=a(18),s=a(8),r=a(9),o=a(11),i=a(10),c=a(12),u=a(74),p=a(69),m=a(61),h=a(56),f=a(67),g=a(68),d=a(70),E=(a(38),a(76)),v=a(57),b=a(58),y=a(59),O=a(60),k=a(14),j=a(6),C=a.n(j),w=a(20),x=a.n(w),S=a(21),A=a.n(S),L="http://localhost:3001/",P=a(62),D=a(63),N=a(64),T=a(65),M=a(66),B=(a(39),function(e){function t(){return Object(s.a)(this,t),Object(o.a)(this,Object(i.a)(t).apply(this,arguments))}return Object(c.a)(t,e),Object(r.a)(t,[{key:"shouldComponentUpdate",value:function(e){return void 0!==e.value&&!isNaN(e.value)}},{key:"render",value:function(){return n.createElement("span",null,this.props.value>=.1&&n.createElement(h.a,{className:"percUp"},n.createElement(k.b,{size:"0.8em"}),this.props.value.toFixed(1),"%"),this.props.value<=-.1&&n.createElement(h.a,{className:"percDown"},n.createElement(k.a,{size:"0.8em"}),this.props.value.toFixed(1),"%"))}}]),t}(n.Component)),F=(a(42),function(e){function t(){return Object(s.a)(this,t),Object(o.a)(this,Object(i.a)(t).apply(this,arguments))}return Object(c.a)(t,e),Object(r.a)(t,[{key:"render",value:function(){return n.createElement("div",null,n.createElement("div",{className:"loaderBar"}))}}]),t}(n.Component));x()(C.a);var I=function(e){function t(){var e,a;Object(s.a)(this,t);for(var n=arguments.length,l=new Array(n),r=0;r0||t.commentsArr.length>0,chartOptionsAndData:a})}else e.setState({isLoading:!1})})}},{key:"render",value:function(){var e=this;return n.createElement("span",{onClick:function(t){e.toggle(),t.preventDefault()}},this.props.children,n.createElement(E.a,{size:"lg",toggle:function(){return e.toggle()},isOpen:this.state.isOpen,fade:!0,keyboard:!0},n.createElement(v.a,{toggle:function(){return e.toggle()}},n.createElement("a",{href:"https://instagram.com/p/"+this.props.post.path,target:"_new"},this.props.post.path," ",n.createElement(k.c,{size:"0.8em"}))),n.createElement(b.a,null,this.state.isLoading&&!this.state.error&&n.createElement(F,null),!this.state.isLoading&&!this.state.hasData&&n.createElement(h.a,null,"No data available"),this.state.error&&n.createElement(y.a,{color:"danger"},this.state.errorMessage),!this.state.isLoading&&!this.state.error&&this.state.hasData&&n.createElement(A.a,{highcharts:C.a,options:this.state.chartOptionsAndData,callback:function(t){if(!e.state.isLoading&&e.state.hasData&&window.innerWidth<800)for(var a=0;a1?" days ":" day ")+" ago")),n.createElement("td",null,"Likes: ",e.likeCount.toLocaleString("pt-BR"),"\xa0",n.createElement(B,{value:e.likePercentage}),n.createElement("br",null),"Comments: ",e.commentCount.toLocaleString("pt-BR"),"\xa0",n.createElement(B,{value:e.commentPercentage})))});return n.createElement("div",null,n.createElement("table",{className:"table table-bordered"},n.createElement("thead",null,n.createElement("tr",null,n.createElement("td",{className:"postPathCol"},"Post"),n.createElement("td",null,"Stats"))),n.createElement("tbody",null,e)))}}]),t}(n.Component)),W=(a(51),function(e){function t(){var e,a;Object(s.a)(this,t);for(var n=arguments.length,l=new Array(n),r=0;r0||t.topLikes.length>0||t.topComments.length>0,list1:t.topInteraction,list2:t.topLikes,list3:t.topComments,activeTab:"1"}):e.setState({isLoading:!1})})}},{key:"componentDidMount",value:function(){this.loadData()}},{key:"render",value:function(){var e=this;return n.createElement("div",null,this.state.isLoading&&!this.state.error&&n.createElement(F,null),this.state.error&&n.createElement(y.a,{color:"danger"},this.state.errorMessage),!this.state.isLoading&&!this.state.error&&this.state.hasData&&n.createElement("div",null,n.createElement(h.a,null,"Top 30 interactions"),n.createElement(P.a,{tabs:!0},n.createElement(D.a,null,n.createElement(N.a,{className:"1"===this.state.activeTab?"active":"",onClick:function(){e.toggle("1")}},"Highlights")),n.createElement(D.a,null,n.createElement(N.a,{className:"2"===this.state.activeTab?"active":"",onClick:function(){e.toggle("2")}},"Likes")),n.createElement(D.a,null,n.createElement(N.a,{className:"3"===this.state.activeTab?"active":"",onClick:function(){e.toggle("3")}},"Comments"))),n.createElement(T.a,{activeTab:this.state.activeTab},n.createElement(M.a,{tabId:"1"},n.createElement(f.a,null,n.createElement(g.a,{sm:"12"},n.createElement(z,{posts:this.state.list1})))),n.createElement(M.a,{tabId:"2"},n.createElement(f.a,null,n.createElement(g.a,{sm:"12"},n.createElement(z,{posts:this.state.list2})))),n.createElement(M.a,{tabId:"3"},n.createElement(f.a,null,n.createElement(g.a,{sm:"12"},n.createElement(z,{posts:this.state.list3})))))))}}]),t}(n.Component));x()(C.a);var J=function(e){function t(){var e,a;Object(s.a)(this,t);for(var n=arguments.length,l=new Array(n),r=0;r0||t.followedArr.length>0||t.mediasArr.length>0||t.likesArr.length>0||t.commentsArr.length>0,chartOptionsAndData:a})}else e.setState({isLoading:!1})})}},{key:"render",value:function(){var e=this;return n.createElement("div",{onClick:function(t){e.toggle(),t.preventDefault()}},this.props.children,n.createElement(E.a,{size:"lg",toggle:function(){return e.toggle()},isOpen:this.state.isOpen,fade:!0,keyboard:!0},n.createElement(v.a,{toggle:function(){return e.toggle()}},n.createElement("a",{href:"https://instagram.com/"+this.props.profile.username,target:"_new"},this.props.profile.fullName," ",n.createElement(k.c,{size:"0.8em"}))),n.createElement(b.a,null,this.state.isLoading&&!this.state.error&&n.createElement(F,null),!this.state.isLoading&&!this.state.hasData&&n.createElement(h.a,null,"No data available"),this.state.error&&n.createElement(y.a,{color:"danger"},this.state.errorMessage),!this.state.isLoading&&!this.state.error&&this.state.hasData&&n.createElement(A.a,{highcharts:C.a,options:this.state.chartOptionsAndData,callback:function(t){if(!e.state.isLoading&&e.state.hasData&&window.innerWidth<800)for(var a=0;a100&&(a=100);var l="warning";return 100===a&&(l="success"),t.scrapping&&(l="info"),n.createElement(p.a,{color:t.isPrivate||t.notFound?"danger":""},n.createElement("span",{className:"float-right"},n.createElement(m.a,{outline:!0,color:"danger",size:"sm",disabled:this.props.profile.isFixed,onClick:function(){return e.props.removeProfile(e.props.profile.username)}},n.createElement(k.e,null))),n.createElement(J,{profile:t},n.createElement("div",{className:"profile-name"},n.createElement("a",{href:"#"+t.username},t.fullName?t.fullName:"loading..."),n.createElement("small",null,"@",t.username))),t.isPrivate||t.notFound?n.createElement(h.a,{color:"danger"},"(not found or private)"):"",n.createElement(f.a,null,n.createElement(g.a,{xs:"6",sm:"3"},n.createElement("small",null,"Followed:"),n.createElement("br",null),t.followedByCount.toLocaleString("pt-BR")," ",n.createElement("br",null),n.createElement(B,{value:t.followedByPercentage})),n.createElement(g.a,{xs:"6",sm:"3"},n.createElement("small",null,"Posts:"),n.createElement("br",null),t.mediaCount.toLocaleString("pt-BR")," ",n.createElement("br",null),n.createElement(B,{value:t.mediaPercentage})),n.createElement(g.a,{xs:"6",sm:"3"},n.createElement("small",null,"Likes:"),n.createElement("br",null),t.likeCount.toLocaleString("pt-BR")," ",n.createElement("br",null),n.createElement(B,{value:t.likePercentage})),n.createElement(g.a,{xs:"6",sm:"3"},n.createElement("small",null,"Comments:"),n.createElement("br",null),t.commentCount.toLocaleString("pt-BR")," ",n.createElement("br",null),n.createElement(B,{value:t.commentPercentage}))),n.createElement(d.a,{value:a,color:l,striped:t.scrapping,animated:t.scrapping}))}}]),t}(n.Component),R=(a(52),a(71)),_=a(72),V=a(75),K=a(73),Z=(a(53),function(e){function t(){var e,a;Object(s.a)(this,t);for(var n=arguments.length,l=new Array(n),r=0;r { 26 | try { 27 | return vOld > 0 ? ((vNew - vOld) / vOld) * 100 : 0; 28 | } catch (error) { 29 | return 0; 30 | } 31 | }; 32 | 33 | const extractTextFromSource = (html, lookFor) => { 34 | let res = html.match(lookFor); 35 | if (res && res.length > 0){ 36 | return res[1]; 37 | } 38 | return ""; 39 | }; 40 | 41 | const extractNumberFromSource = (html, lookFor) => { 42 | let res = html.match(lookFor); 43 | if (res && res.length > 0){ 44 | return parseInt(res[1], 10); 45 | } 46 | return 0; 47 | }; 48 | 49 | const addPostHistoryData = (post) => { 50 | 51 | return new Promise(resolve => { 52 | 53 | let data = { 54 | path: post.path, 55 | likeCount: post.likeCount, 56 | commentCount: post.commentCount, 57 | date: new Date() 58 | }; 59 | 60 | dbPostHistory.find({ path: post.path }, { date: -1 }, 1).then(previousHistory => { 61 | 62 | if (previousHistory.length === 0 || 63 | ((previousHistory[0].likeCount !== data.likeCount || 64 | previousHistory[0].commentCount !== data.commentCount) && 65 | moment.duration(moment(new Date()).diff(moment(previousHistory[0].date))).asMinutes() > 60) 66 | ) { 67 | 68 | return dbPostHistory.save(data).then(() => { 69 | 70 | return dbPostHistory.find({ path: post.path, date: { $gt: moment().add(historyCalcDays, "day").toDate() } }, { date: 1 }, 1).then(hist => { 71 | 72 | if (hist.length > 0) { 73 | post.likePercentage = calcPercentage(hist[0].likeCount, data.likeCount); 74 | post.commentPercentage = calcPercentage(hist[0].commentCount, data.commentCount); 75 | } 76 | 77 | resolve(post); 78 | }); 79 | 80 | }); 81 | 82 | } else { 83 | resolve(post); 84 | } 85 | 86 | }).catch(err => { 87 | resolve(post); 88 | logger.error("Error on addPostHistoryData", { path: post.path, err }); 89 | }); 90 | 91 | }); 92 | 93 | }; 94 | 95 | const addProfileHistoryData = (profile) => { 96 | 97 | return new Promise(resolve => { 98 | 99 | if (!profile.scanned) { 100 | return resolve(profile); 101 | } 102 | 103 | let data = { 104 | followCount: profile.followCount, 105 | followedByCount: profile.followedByCount, 106 | mediaCount: profile.mediaCount, 107 | likeCount: profile.likeCount, 108 | commentCount: profile.commentCount, 109 | username: profile.username, 110 | date: new Date() 111 | }; 112 | 113 | dbProfileHistory.find({ username: profile.username }, { date: -1 }, 1).then(lastHistory => { 114 | 115 | if (lastHistory.length === 0 || 116 | ((lastHistory[0].followCount !== data.followCount || 117 | lastHistory[0].followedByCount !== data.followedByCount || 118 | lastHistory[0].mediaCount !== data.mediaCount || 119 | lastHistory[0].likeCount !== data.likeCount || 120 | lastHistory[0].commentCount !== data.commentCount) && 121 | moment.duration(moment(new Date()).diff(moment(lastHistory[0].date))).asMinutes() > 60) 122 | ) { 123 | 124 | return dbProfileHistory.save(data).then(() => { 125 | 126 | return dbProfileHistory.find({ username: profile.username, date: { $gt: moment().add(historyCalcDays, "day").toDate() } }, { date: 1 }, 1).then(hist => { 127 | 128 | if (hist.length > 0) { 129 | profile.followPercentage = calcPercentage(hist[0].followCount, data.followCount); 130 | profile.followedByPercentage = calcPercentage(hist[0].followedByCount, data.followedByCount); 131 | profile.mediaPercentage = calcPercentage(hist[0].mediaCount, data.mediaCount); 132 | profile.likePercentage = calcPercentage(hist[0].likeCount, data.likeCount); 133 | profile.commentPercentage = calcPercentage(hist[0].commentCount, data.commentCount); 134 | } 135 | 136 | resolve(profile); 137 | }); 138 | 139 | }); 140 | 141 | } else { 142 | resolve(profile); 143 | } 144 | 145 | }).catch(err => { 146 | resolve(profile); 147 | logger.error("Error on addProfileHistoryData", { username: profile.username, err }); 148 | }); 149 | 150 | }); 151 | 152 | }; 153 | 154 | const saveProfile = (profile) => { 155 | return new Promise((resolve, reject) => { 156 | 157 | dbProfile.find({ username: profile.username }, {}, 1).then(savedProfile => { 158 | if (savedProfile.length > 0) { 159 | return dbProfile.replace({ username: profile.username }, profile).then(() => { 160 | resolve(); 161 | }); 162 | } else { 163 | resolve(); 164 | } 165 | }).catch(err => { 166 | reject(err); 167 | }); 168 | }); 169 | }; 170 | 171 | const refreshProfilesState = (profile) => { 172 | return new Promise(resolve => { 173 | let criteria = [ 174 | { 175 | $match: { 176 | "username": profile.username, 177 | "scrapped": true, 178 | "likeCount": { $gte: 0 }, 179 | "commentCount": { $gte: 0 } 180 | } 181 | }, 182 | { 183 | $group: { 184 | _id: "$username", 185 | likes: { 186 | $sum: "$likeCount" 187 | }, 188 | comments: { 189 | $sum: "$commentCount" 190 | }, 191 | scrapped: { 192 | $sum: 1 193 | } 194 | } 195 | } 196 | ]; 197 | 198 | return dbPost.aggregate(criteria).then(totals => { 199 | 200 | if (totals.length > 0) { 201 | profile.likeCount = totals[0].likes; 202 | profile.commentCount = totals[0].comments; 203 | profile.postsScrapped = totals[0].scrapped; 204 | } 205 | 206 | return addProfileHistoryData(profile).then(profile => { 207 | 208 | return saveProfile(profile).then(() => { 209 | resolve(); 210 | }); 211 | 212 | }); 213 | 214 | }); 215 | 216 | }).catch(err => { 217 | logger.error("Error on refreshProfilesState", { username: profile.username, err }); 218 | }); 219 | }; 220 | 221 | const insertPost = (post) => { 222 | return new Promise(resolve => { 223 | 224 | dbPost.save(post).then(() => { 225 | return resolve(1); 226 | }).catch(() => { 227 | return resolve(0); 228 | }); 229 | 230 | }); 231 | }; 232 | 233 | const scrapPostHtml = (post, html) => { 234 | return new Promise(resolve => { 235 | 236 | try { 237 | let $ = cheerio.load(html); 238 | 239 | let title = $('title').text(); 240 | let image = $('meta[property="og:image"]').attr('content'); 241 | let description = $('meta[property="og:description"]').attr('content'); 242 | 243 | let likeCount = extractNumberFromSource(html, /edge_media_preview_like"[^0-9]+([0-9]+)/); 244 | let commentCount = extractNumberFromSource(html, /edge_media_to_comment"[^0-9]+([0-9]+)/); 245 | 246 | if (commentCount === 0) { 247 | commentCount = extractNumberFromSource(html, /edge_media_to_parent_comment"[^0-9]+([0-9]+)/); 248 | } 249 | 250 | let uploadDate = extractNumberFromSource(html, /taken_at_timestamp"[^0-9]+([0-9]+)/); 251 | 252 | if (!isNaN(likeCount) && !isNaN(commentCount) && !isNaN(uploadDate)) { 253 | 254 | post.image = image; 255 | post.likeCount = likeCount; 256 | post.commentCount = commentCount; 257 | post.description = description; 258 | post.lastScrapDate = new Date(); 259 | post.scrapped = true; 260 | post.removed = false; 261 | post.notFoundCount = 0; 262 | post.uploadDate = moment(new Date(uploadDate * 1000)).toDate(); 263 | 264 | return addPostHistoryData(post).then(post => { 265 | 266 | return dbPost.replace({ _id: post._id }, post).then(() => { 267 | resolve(); 268 | }); 269 | 270 | }); 271 | 272 | } else { 273 | 274 | if (title.toLowerCase().indexOf("restricted") >= 0) { 275 | 276 | post.lastScrapDate = new Date(); 277 | post.notFoundCount = post.notFoundCount ? post.notFoundCount + 1 : 1; 278 | post.scrapped = true; 279 | post.removed = true; 280 | 281 | return dbPost.replace({ _id: post._id }, post).then(() => { 282 | resolve(); 283 | }); 284 | 285 | } else { 286 | logger.error("Could not parse post HTML", { path: post.path, html }, true); 287 | resolve(); 288 | } 289 | 290 | } 291 | 292 | } catch (err) { 293 | logger.error("Error during post scrapp", { path: post.path, html, err }, true); 294 | resolve(); 295 | } 296 | }); 297 | }; 298 | 299 | const downloadPostHtml = (post) => { 300 | return new Promise(resolve => { 301 | 302 | const request = require('request'); 303 | 304 | let options = { 305 | url: 'https://www.instagram.com/p/' + post.path + "/", 306 | method: 'GET', 307 | timeout: 10000, 308 | followRedirect: false 309 | }; 310 | 311 | request(options, (err, response, html) => { 312 | 313 | if (err) { 314 | return resolve(); 315 | } 316 | 317 | dbPostQueue.remove({ path: post.path }).catch(err => { 318 | logger.error("Error removing post from queue", err); 319 | }); 320 | 321 | if (response.statusCode === 200) { 322 | 323 | return scrapPostHtml(post, html).then(() => { 324 | resolve(); 325 | }); 326 | 327 | } else { 328 | 329 | post.notFoundCount = post.notFoundCount ? post.notFoundCount + 1 : 1; 330 | post.lastScrapDate = new Date(); 331 | post.removed = true; 332 | 333 | return dbPost.replace({ path: post.path }, post).then(() => { 334 | resolve(); 335 | }); 336 | 337 | } 338 | 339 | }).catch(err => { 340 | logger.error("Error on downloadPostHtml", { username: post.username, path: post.path, err }, true); 341 | resolve(); 342 | }); 343 | 344 | }); 345 | }; 346 | 347 | const scrappPosts = () => { 348 | 349 | dbPostQueue.find({ instanceId }, { date: 1 }, maxDownloads).then(queue => { 350 | 351 | if (queue.length <= 0) { 352 | setTimeout(scrappPosts, 1000); 353 | return; 354 | } 355 | 356 | let arrPaths = queue.map(item => { 357 | return item.path; 358 | }); 359 | 360 | return dbPost.find({ path: { $in: arrPaths } }, {}, maxDownloads).then(posts => { 361 | 362 | if (posts.length > 0) { 363 | 364 | logger.info("Downloading posts", posts.length); 365 | 366 | let arrInsertPostPromises = posts.map(post => { 367 | return downloadPostHtml(post); 368 | }); 369 | 370 | return Promise.all(arrInsertPostPromises).then(() => { 371 | setTimeout(scrappPosts, 5000); 372 | }); 373 | 374 | } else { 375 | setTimeout(scrappPosts, 5000); 376 | } 377 | 378 | }); 379 | 380 | }).catch(() => { 381 | setTimeout(scrappPosts, 10000); 382 | }); 383 | 384 | }; 385 | 386 | const scrapProfile = async (state) => { 387 | 388 | state.startDateScrappingProfile = state.startDateScrappingProfile || new Date(); 389 | state.listPostsScrapped = state.listPostsScrapped || {}; 390 | state.stuckCount = state.stuckCount || 0; 391 | state.rounds = state.rounds || 0; 392 | state.rounds++; 393 | 394 | let arrInsertPostPromises = []; 395 | let arrPostsScrapped = []; 396 | 397 | let arrPaths = await state.pageInstance.evaluate(function () { 398 | var postsLinks = window.document.querySelectorAll("a"); 399 | var arrPaths = []; 400 | var i; 401 | 402 | for (i = 0; i < postsLinks.length; i++) { 403 | if (postsLinks[i].pathname.indexOf("/p/") === 0) { 404 | arrPaths.push(postsLinks[i].pathname.substring(3).replace(/\//gi, "")); 405 | } 406 | } 407 | 408 | return arrPaths; 409 | }); 410 | 411 | if (arrPaths !== null) { 412 | arrInsertPostPromises = arrPaths.map(path => { 413 | arrPostsScrapped.push(path); 414 | return insertPost({ 415 | username: state.profile.username, 416 | path: path, 417 | image: "", 418 | description: "", 419 | likeCount: 0, 420 | commentCount: 0, 421 | uploadDate: null, 422 | lastScrapDate: null, 423 | scrapped: false, 424 | removed: false 425 | }); 426 | }); 427 | } 428 | 429 | Promise.all(arrInsertPostPromises).then(arrInsertPostPromiseResult => { 430 | let capturedPosts = false; 431 | let savedPosts = false; 432 | 433 | state.pageInstance.evaluate(function () { 434 | window.scrollTo(0, 0); 435 | setTimeout(window.scrollTo, 500, 0, document.body.scrollHeight); 436 | }); 437 | 438 | arrPostsScrapped.forEach(path => { 439 | if (state.listPostsScrapped[path] === undefined) { 440 | state.listPostsScrapped[path] = 1; 441 | capturedPosts = true; 442 | } 443 | }); 444 | 445 | if (!capturedPosts) { 446 | state.stuckCount++; 447 | } else { 448 | state.stuckCount = 0; 449 | } 450 | 451 | if (arrInsertPostPromiseResult.length > 0) { 452 | let countSaved = arrInsertPostPromiseResult.reduce((accumulator, currentValue) => accumulator + currentValue); 453 | savedPosts = countSaved > 0; 454 | } 455 | 456 | return dbPost.count({ username: state.profile.username, removed: false, scrapped: true }).then(totalPostsSaved => { 457 | 458 | return dbProfile.get(state.profile.username).then(profileExist => { 459 | 460 | if (profileExist.length === 0) { 461 | setTimeout(() => state.phantomInstance.exit(), 1000); 462 | return; 463 | } 464 | 465 | let postsSavedPercentage = (totalPostsSaved / state.profile.mediaCount); 466 | let closeProfile = state.profile.scanned && (capturedPosts && !savedPosts); 467 | 468 | if (totalPostsSaved >= state.profile.mediaCount) { 469 | if (!state.profile.scanned){ 470 | state.profile.scanned = true; 471 | } 472 | closeProfile = true; 473 | } 474 | 475 | if (moment.duration(moment(new Date()).diff(moment(state.startDateScrappingProfile))).asMinutes() > maxMinutesToScrapProfile) { 476 | logger.warn("Took too long to capture profile", state.profile.username); 477 | closeProfile = true; 478 | 479 | if (postsSavedPercentage >= 0.99 && !state.profile.scanned) { 480 | state.profile.scanned = true; 481 | } 482 | } 483 | 484 | if (closeProfile) { 485 | setTimeout(() => state.phantomInstance.exit(), 1000); 486 | return; 487 | } 488 | 489 | if (state.stuckCount > 5) { 490 | state.stuckCount = 0; 491 | setTimeout(scrapProfile, 60000, state); 492 | logger.info("Scrapper is stuck", state.profile.username); 493 | return; 494 | } 495 | 496 | return refreshProfilesState(state.profile).then(() => { 497 | setTimeout(scrapProfile, 1000, state); 498 | }); 499 | 500 | }); 501 | 502 | }); 503 | 504 | }).catch(err => { 505 | logger.error("Error on scrapProfile", err); 506 | setTimeout(scrapProfile, 10000, state); 507 | }); 508 | 509 | }; 510 | 511 | const openProfile = async () => { 512 | 513 | if (openedProfiles >= maxProfiles) { 514 | return; 515 | } 516 | 517 | let username = await dbProfileQueue.find({ instanceId }, { date: 1 }, 1).then(queue => { 518 | return (queue.length > 0) ? queue[0].username : null; 519 | }).catch(err => { 520 | logger.error("Error at openProfile", { username, err }); 521 | }); 522 | 523 | if (!username) { 524 | return; 525 | } 526 | 527 | let profile = await dbProfile.find({ username, scrapping: false }, {}, 1).then(profiles => { 528 | return profiles.length > 0 ? profiles[0] : null; 529 | }).catch(err => { 530 | logger.error("Error at openProfile", { username, err }); 531 | }); 532 | 533 | if (!profile) { 534 | return; 535 | } 536 | 537 | profile.lastScrapDate = new Date(); 538 | profile.scrapping = true; 539 | 540 | let canProceed = true; 541 | 542 | await saveProfile(profile).then(() => { 543 | logger.info("Scrapping profile", profile.username); 544 | }).catch(err => { 545 | canProceed = false; 546 | logger.error("Error updating lastScrapDate", { username: profile.username, err }); 547 | }); 548 | 549 | if (!canProceed) { 550 | return; 551 | } 552 | 553 | const pageURL = 'https://www.instagram.com/' + profile.username + "?t=" + new Date().getTime(); 554 | 555 | const phantomInstance = await phantom.create(); 556 | const pageInstance = await phantomInstance.createPage(); 557 | pageInstance.setting('userAgent', "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.97 Safari/537.11"); 558 | const status = await pageInstance.open(pageURL); 559 | 560 | dbProfileQueue.remove({ username: profile.username }).catch(err => { 561 | logger.error("Error removing profile from queue", { username: profile.username, err }); 562 | }); 563 | 564 | await pageInstance.on('onClosing', () => { 565 | 566 | profile.lastScrapDate = new Date(); 567 | profile.scrapping = false; 568 | 569 | refreshProfilesState(profile).then(() => { 570 | logger.info("Profile saved", profile.username); 571 | }).catch(err => { 572 | logger.error("Error on saveProfile", { username: profile.username, err }); 573 | }); 574 | 575 | if (openedProfiles > 0) { 576 | openedProfiles--; 577 | } 578 | }); 579 | 580 | openedProfiles++; 581 | 582 | if (status !== "success") { 583 | setTimeout(() => phantomInstance.exit(), 1000); 584 | logger.error("Error on open page", { pageURL, status }); 585 | return; 586 | } 587 | 588 | await pageInstance.evaluate(function () { 589 | window.scrollTo(100, 0); 590 | }); 591 | 592 | await sleep(1000); 593 | 594 | await pageInstance.evaluate(function () { 595 | window.scrollTo(0, 0); 596 | }); 597 | 598 | await sleep(1000); 599 | 600 | const pageContent = await pageInstance.property('content'); 601 | const pageDOM = new JSDOM(pageContent, { runScripts: "dangerously" }); 602 | 603 | if (pageDOM.window._sharedData === undefined) { 604 | setTimeout(() => phantomInstance.exit(), 1000); 605 | 606 | if (!profile.scanned && pageContent.indexOf("Page Not Found") > 0) { 607 | profile.notFound = true; 608 | logger.error("Could not find profile", { pageURL }); 609 | } 610 | return; 611 | } 612 | 613 | let followCount = extractNumberFromSource(pageContent, /edge_follow"[^0-9]+([0-9]+)/); 614 | let followedByCount = extractNumberFromSource(pageContent, /edge_followed_by"[^0-9]+([0-9]+)/); 615 | let mediaCount = extractNumberFromSource(pageContent, /edge_owner_to_timeline_media"[^0-9]+([0-9]+)/); 616 | 617 | if (followCount === 0 && followedByCount === 0 && mediaCount === 0) { 618 | logger.info("Could not capture profile page", { username: profile.username, pageContent }); 619 | setTimeout(() => phantomInstance.exit(), 1000); 620 | return; 621 | } 622 | 623 | profile.fullName = pageDOM.window.document.title.match(/([^(]+)/)[1]; 624 | profile.followCount = followCount; 625 | profile.followedByCount = followedByCount; 626 | profile.mediaCount = mediaCount; 627 | profile.isPrivate = extractTextFromSource(pageContent, /is_private":([^,]+)/) === "true"; 628 | profile.notFound = false; 629 | 630 | if (profile.isPrivate) { 631 | logger.info("private profile", { username: profile.username }); 632 | setTimeout(() => phantomInstance.exit(), 1000); 633 | return; 634 | } 635 | 636 | setTimeout(scrapProfile, 1000, { pageInstance, phantomInstance, profile }); 637 | 638 | }; 639 | 640 | const queuePosts = async () => { 641 | 642 | let arrQueue = await dbPostQueue.find({}, {}, 0).then(queue => { 643 | if (queue.length > 0) { 644 | return queue.map(item => { 645 | return item.path; 646 | }); 647 | } 648 | return []; 649 | }); 650 | 651 | let limit = maxDownloads * 10; 652 | limit = limit > 500 ? 500 : limit; 653 | 654 | dbPost.find( 655 | { 656 | $or: 657 | [ 658 | { 659 | scrapped: false, 660 | removed: false 661 | }, 662 | { 663 | uploadDate: { $gt: moment().add(-1, "day").toDate() }, 664 | lastScrapDate: { $lt: moment().add(-60, "minutes").toDate() }, 665 | removed: false 666 | }, 667 | { 668 | uploadDate: { $gt: moment().add(-5, "day").toDate() }, 669 | lastScrapDate: { $lt: moment().add(-120, "minutes").toDate() }, 670 | removed: false 671 | }, 672 | { 673 | uploadDate: { $gt: moment().add(-10, "day").toDate() }, 674 | lastScrapDate: { $lt: moment().add(-180, "minutes").toDate() }, 675 | removed: false 676 | }, 677 | { 678 | uploadDate: { $gt: moment().add(-20, "day").toDate() }, 679 | lastScrapDate: { $lt: moment().add(-240, "minutes").toDate() }, 680 | removed: false 681 | }, 682 | { 683 | uploadDate: { $gt: moment().add(-30, "day").toDate() }, 684 | lastScrapDate: { $lt: moment().add(-300, "minutes").toDate() }, 685 | removed: false 686 | }, 687 | { 688 | lastScrapDate: { $lt: moment().add(-720, "minutes").toDate() }, 689 | removed: false 690 | }, 691 | { 692 | removed: true, 693 | lastScrapDate: { $lt: moment().add(-300, "minutes").toDate() }, 694 | notFoundCount: { $lt: notFoundCountLimit } 695 | } 696 | ] 697 | }, 698 | { scrapped: 1, lastScrapDate: 1 }, limit).then(async posts => { 699 | 700 | let postsQueued = 0; 701 | 702 | for await (const post of posts) { 703 | if (postsQueued < maxDownloads && arrQueue.indexOf(post.path) < 0) { 704 | postsQueued += await dbPostQueue.save({ instanceId, path: post.path, date: new Date() }) 705 | .then(() => { 706 | return 1; 707 | }) 708 | .catch(() => { 709 | return 0; 710 | }); 711 | } 712 | if (postsQueued >= maxDownloads) { 713 | break; 714 | } 715 | } 716 | 717 | setTimeout(queuePosts, postsQueued > 0 ? 5000 : 2000); 718 | 719 | }).catch(err => { 720 | logger.error("Error on queuePosts", err); 721 | setTimeout(queuePosts, 5000); 722 | }); 723 | 724 | }; 725 | 726 | const queueProfiles = async () => { 727 | 728 | if (openedProfiles >= maxProfiles) { 729 | setTimeout(queueProfiles, 5000); 730 | return; 731 | } 732 | 733 | let arrQueue = await dbProfileQueue.find({}, {}, 0).then(queue => { 734 | if (queue.length > 0) { 735 | return queue.map(item => { 736 | return item.username; 737 | }); 738 | } 739 | return []; 740 | }); 741 | 742 | dbProfile.find({ 743 | $or: [ 744 | { lastScrapDate: null, scrapping: false }, 745 | { lastScrapDate: { $lt: moment().add(-10, "minutes").toDate() }, scrapping: false } 746 | ] 747 | }, { lastScrapDate: 1 }, 0).then(async profiles => { 748 | 749 | let profileQueue = false; 750 | 751 | for await (const profile of profiles) { 752 | if (arrQueue.indexOf(profile.username) < 0 && !profileQueue) { 753 | profileQueue = await dbProfileQueue.save({ instanceId, username: profile.username, date: new Date() }).then(() => { 754 | return true; 755 | }).catch(() => { 756 | return false; 757 | }); 758 | } 759 | if (profileQueue) { 760 | break; 761 | } 762 | } 763 | 764 | setTimeout(queueProfiles, profileQueue ? 10000 : 5000); 765 | 766 | }).catch(err => { 767 | logger.error("Error on queueProfiles", err); 768 | setTimeout(queueProfiles, 5000); 769 | }); 770 | 771 | }; 772 | 773 | const cleanQueues = () => { 774 | 775 | dbProfileQueue.remove({ date: { $lt: moment().add(-1, "minutes").toDate() } }).then(numRemoved => { 776 | if (numRemoved > 0) { 777 | logger.info("Cleaning profile queue"); 778 | } 779 | }).catch(err => { 780 | logger.error("Error cleaning profile queue", err); 781 | }); 782 | 783 | dbPostQueue.remove({ date: { $lt: moment().add(-1, "minutes").toDate() } }).then(numRemoved => { 784 | if (numRemoved > 0) { 785 | logger.info("Cleaning post queue"); 786 | } 787 | }).catch(err => { 788 | logger.error("Error cleaning post queue", err); 789 | }); 790 | 791 | dbProfile.update({ lastScrapDate: { $lt: moment().add(((maxMinutesToScrapProfile + 10) * -1), "minutes").toDate() } }, 792 | { $set: { scrapping: false } }).then(() => { 793 | //ignore 794 | }).catch(err => { 795 | logger.error("Error reseting scrapping flag", err); 796 | }); 797 | 798 | }; 799 | 800 | const init = (options) => { 801 | 802 | options = options || {}; 803 | 804 | maxProfiles = options.maxProfiles || 3; 805 | maxDownloads = options.maxDownloads || 30; 806 | 807 | maxProfiles = parseInt(maxProfiles, 10); 808 | maxDownloads = parseInt(maxDownloads, 10); 809 | 810 | logger.warn("Starting scrapper!", { instanceId }); 811 | 812 | queueProfiles(); 813 | queuePosts(); 814 | 815 | setInterval(cleanQueues, 10000); 816 | setInterval(openProfile, 10000); 817 | 818 | scrappPosts(); 819 | 820 | }; 821 | 822 | exports.init = init; -------------------------------------------------------------------------------- /backend/app/public/static/js/main.171b13fe.chunk.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["helpers/constants.ts","components/Percentage.tsx","components/LoadingBar.tsx","components/PostModal.tsx","components/PostStatsList.tsx","components/PostStats.tsx","components/ProfileModal.tsx","components/ProfileList.tsx","components/FormModal.tsx","components/LoadingSpinner.tsx","components/Home.tsx","index.tsx"],"names":["apiBackendUrl","process","Percentage","nextProps","undefined","value","isNaN","react","this","props","Label","className","index_esm","size","toFixed","React","LoadingBar","highchartsSeriesLabel","Highcharts","PostModal","state","isLoading","hasData","isOpen","chartOptionsAndData","error","errorMessage","loadChart","setState","_this2","fetch","concat","post","path","then","response","ok","json","bodyUsed","err","message","statusText","data","chart","type","zoomType","panning","panKey","resetZoomButton","position","align","verticalAlign","x","y","yAxis","labels","style","color","getOptions","colors","title","text","opposite","xAxis","showFirstLabel","series","name","likesArr","commentsArr","plotOptions","spline","marker","enabled","credits","exporting","subtitle","length","_this3","onClick","e","toggle","preventDefault","children","Modal","fade","keyboard","ModalHeader","href","target","ModalBody","LoadingBar_LoadingBar","Alert","highcharts_react_min_default","a","highcharts","options","callback","window","innerWidth","i","update","visible","ModalFooter","Button","PostsStatsList","postList","posts","map","key","PostModal_PostModal","published","likeCount","toLocaleString","Percentage_Percentage","likePercentage","commentCount","commentPercentage","PostsStats","list1","list2","list3","activeTab","tab","profile","username","topInteraction","topLikes","topComments","loadData","Nav","tabs","NavItem","NavLink","TabContent","TabPane","tabId","Row","Col","sm","PostStatsList_PostsStatsList","ProfileModal","followArr","followedArr","mediasArr","fullName","PostStats_PostsStats","ProfileList","nextState","JSON","stringify","_this","progression","postsScrapped","mediaCount","barType","scrapping","ListGroupItem","isPrivate","notFound","outline","disabled","isFixed","removeProfile","ProfileModal_ProfileModal","xs","followedByCount","followedByPercentage","mediaPercentage","Progress","striped","animated","FormModal","evt","split","join","alert","saveProfile","Form","onSubmit","InputGroup","InputGroupAddon","addonType","Input","placeholder","onChange","updateInputValue","LoadingSpinner","Home","profiles","loading","requestOptions","method","headers","Accept","Content-Type","body","listProfiles","confirm","Date","getTime","catch","openWebSocket","protocol","address","substring","indexOf","toLowerCase","wsSocket","WebSocket","onopen","console","log","onmessage","event","jsonMessage","parse","onclose","setTimeout","setInterval","list","ProfileList_ProfileList","_id","LoadingSpinner_LoadingSpinner","ListGroup","FormModal_FormModal","ReactDOM","Home_Home","document","getElementById"],"mappings":"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","file":"static/js/main.171b13fe.chunk.js","sourcesContent":["export const apiBackendUrl = process.env.REACT_APP_BACKEND_URL;","import * as React from 'react';\r\nimport { Label } from 'reactstrap';\r\nimport { FaArrowUp, FaArrowDown } from 'react-icons/fa';\r\nimport '../css/Percentage.scss';\r\n\r\ninterface IProps {\r\n value: number\r\n}\r\n\r\nexport default class Percentage extends React.Component {\r\n\r\n shouldComponentUpdate(nextProps: any) {\r\n return nextProps.value !== undefined && !isNaN(nextProps.value);\r\n }\r\n\r\n render() {\r\n\r\n return \r\n\r\n {this.props.value >= 0.1 &&\r\n }\r\n\r\n {this.props.value <= -0.1 &&\r\n }\r\n\r\n \r\n }\r\n}","\r\n\r\nimport * as React from 'react';\r\nimport '../css/LoadingBar.scss';\r\n\r\n\r\nexport default class LoadingBar extends React.Component<{}> {\r\n\r\n render() {\r\n\r\n return (
\r\n
\r\n
);\r\n \r\n }\r\n}","import * as React from 'react';\r\nimport { Alert, Button, Modal, ModalHeader, ModalBody, ModalFooter, Label } from 'reactstrap';\r\nimport { FaExternalLinkAlt } from 'react-icons/fa';\r\nimport Highcharts from 'highcharts';\r\nimport highchartsSeriesLabel from \"highcharts/modules/series-label\";\r\nimport HighchartsReact from 'highcharts-react-official';\r\nimport { apiBackendUrl } from '../helpers/constants';\r\nimport { IPost } from '../interfaces/IPost';\r\nimport LoadingBar from './LoadingBar';\r\n\r\n\r\nhighchartsSeriesLabel(Highcharts);\r\n\r\ninterface IProps {\r\n post: IPost\r\n}\r\n\r\nexport default class PostModal extends React.Component {\r\n\r\n public state = {\r\n isLoading: true,\r\n hasData: false,\r\n isOpen: false,\r\n chartOptionsAndData: {},\r\n error: false,\r\n errorMessage: \"\"\r\n }\r\n\r\n private toggle() {\r\n if (!this.state.isOpen) {\r\n this.loadChart();\r\n }\r\n\r\n this.setState({ isOpen: !this.state.isOpen });\r\n }\r\n\r\n private loadChart() {\r\n fetch(`${apiBackendUrl}post/getChart/${this.props.post.path}`).then(response => {\r\n\r\n if (!response.ok) {\r\n if (response.bodyUsed) {\r\n response.json().then(err => {\r\n this.setState({ error: true, errorMessage: err.message });\r\n });\r\n } else {\r\n this.setState({ error: true, errorMessage: response.statusText });\r\n }\r\n return null;\r\n }\r\n\r\n return response.json();\r\n\r\n }).then(data => {\r\n\r\n if (data === null) {\r\n this.setState({ isLoading: false });\r\n return;\r\n }\r\n\r\n let chartOptionsAndData: Highcharts.Options = {\r\n chart: {\r\n type: 'spline',\r\n zoomType: 'x',\r\n panning: true,\r\n panKey: 'shift',\r\n resetZoomButton: {\r\n position: {\r\n align: 'left',\r\n verticalAlign: 'top',\r\n x: 0,\r\n y: -10\r\n }\r\n }\r\n },\r\n yAxis: [{\r\n labels: {\r\n style: {\r\n color: Highcharts.getOptions().colors[7]\r\n }\r\n },\r\n title: {\r\n text: 'Likes',\r\n style: {\r\n color: Highcharts.getOptions().colors[7]\r\n }\r\n }\r\n }, {\r\n labels: {\r\n style: {\r\n color: Highcharts.getOptions().colors[9]\r\n }\r\n },\r\n title: {\r\n text: 'Comments',\r\n style: {\r\n color: Highcharts.getOptions().colors[9]\r\n }\r\n },\r\n opposite: true\r\n }],\r\n xAxis: {\r\n type: 'datetime',\r\n showFirstLabel: true\r\n },\r\n series: [{\r\n name: 'Likes',\r\n type: 'spline',\r\n data: data.likesArr,\r\n color: Highcharts.getOptions().colors[1]\r\n }, {\r\n name: 'Comments',\r\n type: 'spline',\r\n data: data.commentsArr,\r\n yAxis: 1,\r\n color: Highcharts.getOptions().colors[9]\r\n }],\r\n plotOptions: { spline: { marker: { enabled: false } } },\r\n credits: { enabled: false },\r\n exporting: { enabled: false },\r\n title: { text: null },\r\n subtitle: { text: null }\r\n }\r\n\r\n this.setState({\r\n isLoading: false,\r\n hasData: data.likesArr.length > 0 ||\r\n data.commentsArr.length > 0,\r\n chartOptionsAndData\r\n });\r\n\r\n\r\n });\r\n }\r\n\r\n render() {\r\n\r\n return { this.toggle(); e.preventDefault(); }}>\r\n\r\n {this.props.children}\r\n\r\n this.toggle()} isOpen={this.state.isOpen} fade={true} keyboard={true}>\r\n this.toggle()}>\r\n {this.props.post.path} \r\n \r\n \r\n\r\n {this.state.isLoading && !this.state.error && }\r\n\r\n {!this.state.isLoading && !this.state.hasData && }\r\n\r\n {this.state.error && {this.state.errorMessage}}\r\n\r\n {!this.state.isLoading && !this.state.error && this.state.hasData &&\r\n {\r\n if (!this.state.isLoading && this.state.hasData && window.innerWidth < 800){\r\n for (var i = 0; i < chart.yAxis.length; i++) {\r\n chart.yAxis[i].update({\r\n visible: false\r\n });\r\n }\r\n }\r\n }}\r\n />\r\n }\r\n\r\n \r\n \r\n \r\n \r\n \r\n\r\n ;\r\n }\r\n}","import * as React from 'react';\r\nimport Percentage from './Percentage';\r\nimport { IPost } from '../interfaces/IPost';\r\nimport PostModal from './PostModal';\r\nimport '../css/PostStatsList.scss';\r\n\r\ninterface IProps {\r\n posts: IPost[]\r\n}\r\nexport default class PostsStatsList extends React.Component {\r\n\r\n render() {\r\n\r\n const postList = this.props.posts.map(post => {\r\n return \r\n \r\n \r\n {post.path}\r\n \r\n \r\n
\r\n {post.published === 0 ? \"today\" :\r\n post.published + (post.published > 1 ? \" days \" : \" day \") + \" ago\"}\r\n \r\n \r\n Likes: {post.likeCount.toLocaleString('pt-BR')} \r\n \r\n
\r\n Comments: {post.commentCount.toLocaleString('pt-BR')} \r\n \r\n \r\n\r\n \r\n });\r\n\r\n return
\r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n {postList}\r\n \r\n
PostStats
\r\n\r\n
;\r\n\r\n }\r\n}","import * as React from 'react';\r\nimport { TabContent, TabPane, Row, Col, Nav, NavItem, NavLink, Label, Alert } from 'reactstrap';\r\nimport { apiBackendUrl } from '../helpers/constants';\r\nimport PostsStatsList from './PostStatsList';\r\nimport { IProfile } from '../interfaces/IProfile';\r\nimport { IPost } from '../interfaces/IPost';\r\nimport '../css/PostStats.scss';\r\nimport LoadingBar from './LoadingBar';\r\n\r\ninterface IProps {\r\n profile: IProfile\r\n}\r\n\r\nexport default class PostsStats extends React.Component {\r\n\r\n public state = {\r\n isLoading: true,\r\n hasData: false,\r\n list1: [] as IPost[],\r\n list2: [] as IPost[],\r\n list3: [] as IPost[],\r\n activeTab: '1',\r\n error: false,\r\n errorMessage: \"\"\r\n }\r\n\r\n private toggle(tab: string) {\r\n this.setState({ activeTab: tab });\r\n }\r\n\r\n private loadData() {\r\n fetch(`${apiBackendUrl}post/getStats/${this.props.profile.username}?limit=30`).then(response => {\r\n\r\n if (!response.ok) {\r\n if (response.bodyUsed) {\r\n response.json().then(err => {\r\n this.setState({ error: true, errorMessage: err.message });\r\n });\r\n } else {\r\n this.setState({ error: true, errorMessage: response.statusText });\r\n }\r\n return null;\r\n }\r\n\r\n return response.json();\r\n\r\n }).then(data => {\r\n\r\n if (data === null) {\r\n this.setState({ isLoading: false });\r\n return;\r\n }\r\n\r\n this.setState({\r\n isLoading: false,\r\n hasData: data.topInteraction.length > 0 || data.topLikes.length > 0 || data.topComments.length > 0,\r\n list1: data.topInteraction,\r\n list2: data.topLikes,\r\n list3: data.topComments,\r\n activeTab: '1'\r\n });\r\n\r\n });\r\n }\r\n\r\n componentDidMount() {\r\n this.loadData();\r\n }\r\n\r\n render() {\r\n\r\n return
\r\n\r\n {this.state.isLoading && !this.state.error && }\r\n\r\n {this.state.error && {this.state.errorMessage}}\r\n\r\n {!this.state.isLoading && !this.state.error && this.state.hasData &&\r\n
\r\n \r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n }\r\n\r\n
;\r\n\r\n }\r\n}","import * as React from 'react';\r\nimport { Alert, Button, Modal, ModalHeader, ModalBody, ModalFooter, Label } from 'reactstrap';\r\nimport { FaExternalLinkAlt } from 'react-icons/fa';\r\nimport Highcharts from 'highcharts';\r\nimport highchartsSeriesLabel from \"highcharts/modules/series-label\";\r\nimport HighchartsReact from 'highcharts-react-official';\r\nimport { IProfile } from '../interfaces/IProfile';\r\nimport { apiBackendUrl } from '../helpers/constants';\r\nimport PostStats from './PostStats';\r\nimport LoadingBar from './LoadingBar';\r\n\r\nhighchartsSeriesLabel(Highcharts);\r\n\r\ninterface IProps {\r\n profile: IProfile\r\n}\r\n\r\nexport default class ProfileModal extends React.Component {\r\n\r\n public state = {\r\n isLoading: true,\r\n hasData: false,\r\n isOpen: false,\r\n chartOptionsAndData: {},\r\n error: false,\r\n errorMessage: \"\"\r\n }\r\n\r\n private toggle() {\r\n if (!this.state.isOpen) {\r\n this.loadChart();\r\n }\r\n\r\n this.setState({ isOpen: !this.state.isOpen });\r\n }\r\n\r\n private loadChart() {\r\n fetch(`${apiBackendUrl}profile/getChart/${this.props.profile.username}`).then(response => {\r\n\r\n if (!response.ok) {\r\n if (response.bodyUsed) {\r\n response.json().then(err => {\r\n this.setState({ error: true, errorMessage: err.message });\r\n });\r\n } else {\r\n this.setState({ error: true, errorMessage: response.statusText });\r\n }\r\n return null;\r\n }\r\n\r\n return response.json();\r\n\r\n }).then(data => {\r\n\r\n if (data === null) {\r\n this.setState({ isLoading: false });\r\n return;\r\n }\r\n\r\n let chartOptionsAndData: Highcharts.Options = {\r\n chart: {\r\n type: 'spline',\r\n zoomType: 'x',\r\n panning: true,\r\n panKey: 'shift',\r\n resetZoomButton: {\r\n position: {\r\n align: 'left',\r\n verticalAlign: 'top',\r\n x: 0,\r\n y: -10\r\n }\r\n }\r\n },\r\n yAxis: [{\r\n labels: {\r\n style: {\r\n color: Highcharts.getOptions().colors[1]\r\n }\r\n },\r\n title: {\r\n text: 'Following',\r\n style: {\r\n color: Highcharts.getOptions().colors[1]\r\n }\r\n },\r\n opposite: true\r\n }, {\r\n labels: {\r\n style: {\r\n color: Highcharts.getOptions().colors[3]\r\n }\r\n },\r\n title: {\r\n text: 'Followed',\r\n style: {\r\n color: Highcharts.getOptions().colors[3]\r\n }\r\n },\r\n opposite: true\r\n }, {\r\n labels: {\r\n style: {\r\n color: Highcharts.getOptions().colors[5]\r\n }\r\n },\r\n title: {\r\n text: 'Posts',\r\n style: {\r\n color: Highcharts.getOptions().colors[5]\r\n }\r\n },\r\n opposite: true\r\n }, {\r\n labels: {\r\n style: {\r\n color: Highcharts.getOptions().colors[7]\r\n }\r\n },\r\n title: {\r\n text: 'Likes',\r\n style: {\r\n color: Highcharts.getOptions().colors[7]\r\n }\r\n }\r\n }, {\r\n labels: {\r\n style: {\r\n color: Highcharts.getOptions().colors[9]\r\n }\r\n },\r\n title: {\r\n text: 'Comments',\r\n style: {\r\n color: Highcharts.getOptions().colors[9]\r\n }\r\n },\r\n opposite: true\r\n }],\r\n xAxis: {\r\n type: 'datetime',\r\n showFirstLabel: true\r\n },\r\n series: [{\r\n name: 'Following',\r\n type: 'spline',\r\n data: data.followArr,\r\n visible: false,\r\n color: Highcharts.getOptions().colors[1]\r\n }, {\r\n name: 'Followed',\r\n type: 'spline',\r\n data: data.followedArr,\r\n yAxis: 1,\r\n color: Highcharts.getOptions().colors[3]\r\n }, {\r\n name: 'Posts',\r\n type: 'spline',\r\n data: data.mediasArr,\r\n yAxis: 2,\r\n color: Highcharts.getOptions().colors[5]\r\n }, {\r\n name: 'Likes',\r\n type: 'spline',\r\n data: data.likesArr,\r\n yAxis: 3,\r\n color: Highcharts.getOptions().colors[7]\r\n }, {\r\n name: 'Comments',\r\n type: 'spline',\r\n data: data.commentsArr,\r\n yAxis: 4,\r\n color: Highcharts.getOptions().colors[9]\r\n }],\r\n plotOptions: { \r\n spline: { \r\n marker: { enabled: false } \r\n }\r\n },\r\n credits: { enabled: false },\r\n exporting: { enabled: false },\r\n title: { text: null },\r\n subtitle: { text: null }\r\n }\r\n\r\n this.setState({\r\n isLoading: false,\r\n hasData: data.followArr.length > 0 ||\r\n data.followedArr.length > 0 ||\r\n data.mediasArr.length > 0 ||\r\n data.likesArr.length > 0 ||\r\n data.commentsArr.length > 0,\r\n chartOptionsAndData\r\n });\r\n\r\n });\r\n }\r\n\r\n render() {\r\n\r\n return
{ this.toggle(); e.preventDefault(); }}>\r\n\r\n {this.props.children}\r\n\r\n this.toggle()} isOpen={this.state.isOpen} fade={true} keyboard={true}>\r\n this.toggle()}>\r\n {this.props.profile.fullName} \r\n \r\n \r\n\r\n {this.state.isLoading && !this.state.error && }\r\n\r\n {!this.state.isLoading && !this.state.hasData && }\r\n\r\n {this.state.error && {this.state.errorMessage}}\r\n\r\n {!this.state.isLoading && !this.state.error && this.state.hasData &&\r\n {\r\n if (!this.state.isLoading && this.state.hasData && window.innerWidth < 800){\r\n for (var i = 0; i < chart.yAxis.length; i++) {\r\n chart.yAxis[i].update({\r\n visible: false\r\n });\r\n }\r\n }\r\n }}\r\n />\r\n }\r\n\r\n {!this.state.isLoading && this.state.hasData &&\r\n }\r\n\r\n \r\n \r\n \r\n \r\n \r\n\r\n
;\r\n }\r\n}","import * as React from 'react';\r\nimport { ListGroupItem, Button, Label, Row, Col, Progress } from 'reactstrap';\r\nimport '../css/ProfileList.scss';\r\nimport ProfileModal from './ProfileModal';\r\nimport { FaTrash } from 'react-icons/fa';\r\nimport { IProfile } from '../interfaces/IProfile';\r\nimport Percentage from './Percentage';\r\n\r\ninterface IProps {\r\n profile: IProfile,\r\n listProfiles: () => void,\r\n removeProfile: (username: string) => void\r\n}\r\n\r\nexport default class ProfileList extends React.Component {\r\n\r\n shouldComponentUpdate(nextProps: any, nextState: any) {\r\n return JSON.stringify(this.props) !== JSON.stringify(nextProps);\r\n }\r\n\r\n render() {\r\n\r\n let profile = this.props.profile;\r\n\r\n let progression: number = (profile.postsScrapped / profile.mediaCount) * 100;\r\n if (progression > 100) {\r\n progression = 100;\r\n }\r\n\r\n let barType: string = \"warning\";\r\n if (progression === 100) {\r\n barType = \"success\";\r\n }\r\n if (profile.scrapping) {\r\n barType = \"info\"\r\n }\r\n\r\n return (\r\n\r\n \r\n \r\n \r\n\r\n \r\n \r\n \r\n\r\n {profile.isPrivate || profile.notFound ? : \"\"}\r\n\r\n \r\n \r\n Followed:
\r\n {profile.followedByCount.toLocaleString('pt-BR')}
\r\n \r\n \r\n \r\n Posts:
\r\n {profile.mediaCount.toLocaleString('pt-BR')}
\r\n \r\n \r\n \r\n Likes:
\r\n {profile.likeCount.toLocaleString('pt-BR')}
\r\n \r\n \r\n \r\n Comments:
\r\n {profile.commentCount.toLocaleString('pt-BR')}
\r\n \r\n \r\n
\r\n\r\n \r\n\r\n
);\r\n\r\n }\r\n}\r\n","import * as React from 'react';\r\nimport { Button, Modal, ModalBody, ModalFooter, Form, InputGroup, InputGroupAddon, Input } from 'reactstrap';\r\nimport { FaPlus, FaUserPlus } from 'react-icons/fa';\r\nimport '../css/FormModal.scss';\r\n\r\ninterface IProps {\r\n saveProfile: (username: string) => void\r\n}\r\n\r\nexport default class FormModal extends React.Component {\r\n\r\n public state = {\r\n isOpen: false,\r\n username: \"\"\r\n }\r\n\r\n private updateInputValue (evt: any) {\r\n this.setState({\r\n username: evt.target.value\r\n });\r\n }\r\n\r\n private saveProfile (evt: any) {\r\n\r\n if (this.state.username.split(\" \").join(\"\") === \"\") {\r\n return alert(\"Please type the username\");\r\n }\r\n\r\n this.props.saveProfile(this.state.username); \r\n this.setState({username: \"\" });\r\n this.toggle();\r\n evt.preventDefault();\r\n }\r\n\r\n private toggle() {\r\n this.setState({ isOpen: !this.state.isOpen });\r\n }\r\n\r\n render() {\r\n\r\n return { this.toggle(); e.preventDefault(); }}>\r\n\r\n \r\n \r\n \r\n\r\n this.toggle()} isOpen={this.state.isOpen} fade={true} keyboard={true}>\r\n \r\n\r\n
{ this.saveProfile(e); }}>\r\n \r\n   add new @\r\n this.updateInputValue(evt)} />\r\n \r\n
\r\n\r\n
\r\n \r\n \r\n \r\n \r\n
\r\n\r\n
;\r\n }\r\n}","\r\n\r\nimport * as React from 'react';\r\nimport '../css/LoadingSpinner.scss';\r\n\r\n\r\nexport default class LoadingSpinner extends React.Component<{}> {\r\n\r\n render() {\r\n\r\n return (
\r\n
\r\n
\r\n
\r\n
);\r\n \r\n }\r\n}","import * as React from 'react';\r\nimport { ListGroup } from 'reactstrap';\r\nimport ProfileList from './ProfileList';\r\nimport '../css/Home.scss';\r\nimport { apiBackendUrl } from '../helpers/constants';\r\nimport { IProfile } from '../interfaces/IProfile';\r\nimport FormModal from './FormModal';\r\nimport LoadingSpinner from './LoadingSpinner';\r\n\r\nexport default class Home extends React.Component<{}> {\r\n\r\n public state = {\r\n profiles: [] as IProfile[],\r\n loading: true\r\n }\r\n\r\n saveProfile = (username: string) => {\r\n\r\n this.setState({ loading: true });\r\n\r\n const requestOptions = {\r\n method: 'POST',\r\n headers: {\r\n 'Accept': 'application/json',\r\n 'Content-Type': 'application/json'\r\n },\r\n body: JSON.stringify({ \"username\": username })\r\n };\r\n\r\n fetch(apiBackendUrl + \"profile/save\", requestOptions).then(response => {\r\n if (!response.ok) {\r\n response.json().then(function (err) {\r\n alert(err.message);\r\n });\r\n return;\r\n }\r\n\r\n this.listProfiles();\r\n });\r\n }\r\n\r\n removeProfile = (username: string) => {\r\n\r\n if (!window.confirm(\"Confirm this action?\")) {\r\n return;\r\n }\r\n\r\n this.setState({ loading: true });\r\n\r\n const requestOptions = {\r\n method: 'DELETE'\r\n };\r\n\r\n fetch(apiBackendUrl + \"profile/\" + username, requestOptions).then(response => {\r\n if (!response.ok) {\r\n response.json().then(function (err) {\r\n alert(err.message);\r\n });\r\n return;\r\n }\r\n\r\n this.listProfiles();\r\n });\r\n }\r\n\r\n listProfiles = () => {\r\n fetch(`${apiBackendUrl}profile/listAll?_t=${new Date().getTime()}`)\r\n .then(response => response.json())\r\n .then(profiles => this.setState({ profiles, loading: false }))\r\n .catch(e => e);\r\n }\r\n\r\n openWebSocket = () => {\r\n if (\"WebSocket\" in window) {\r\n\r\n let protocol = window[\"location\"][\"protocol\"];\r\n let address = apiBackendUrl.substring(apiBackendUrl.indexOf(\":\") + 3);\r\n address = (((protocol + \"\").toLowerCase().indexOf(\"https\") === 0) ? \"wss://\" : \"ws://\") + address;\r\n\r\n let wsSocket = new WebSocket(address);\r\n\r\n wsSocket.onopen = () => {\r\n console.log(\"Websocket connected!\");\r\n };\r\n\r\n wsSocket.onmessage = (event) => {\r\n try {\r\n\r\n var jsonMessage = JSON.parse(event.data);\r\n\r\n if (jsonMessage.message) {\r\n if (jsonMessage.message === \"new profile\" || jsonMessage.message === \"removed profile\") {\r\n this.listProfiles();\r\n }\r\n }\r\n\r\n console.log(event.data);\r\n\r\n } catch (error) {\r\n //nothing\r\n }\r\n };\r\n\r\n wsSocket.onclose = () => {\r\n console.log(\"Websocket closed!\");\r\n // Try to reconnect in 5 second\r\n setTimeout(this.openWebSocket, 5000);\r\n };\r\n }\r\n }\r\n\r\n componentDidMount() {\r\n console.log(apiBackendUrl);\r\n this.listProfiles();\r\n this.openWebSocket();\r\n setInterval(() => this.listProfiles(), 5000);\r\n }\r\n\r\n shouldComponentUpdate(nextProps: any, nextState: any) {\r\n return JSON.stringify(this.state.profiles) !== JSON.stringify(nextState.profiles) || this.state.loading !== nextState.loading;\r\n }\r\n\r\n render() {\r\n const list = this.state.profiles.map((profile) => {\r\n return \r\n })\r\n\r\n return (\r\n
\r\n\r\n {this.state.loading && }\r\n\r\n {!this.state.loading && {list}}\r\n\r\n {!this.state.loading && }\r\n\r\n {!this.state.loading &&
\r\n \r\n Developed by Ivan Valadares
\r\n Check the API docs
\r\n Get the source code at github.com/ivanvaladares/instagram-scrapper\r\n
\r\n
}\r\n\r\n\r\n
);\r\n }\r\n}\r\n","import 'bootstrap/dist/css/bootstrap.css';\r\n\r\nimport * as React from 'react';\r\nimport * as ReactDOM from 'react-dom';\r\n\r\nimport Home from './components/Home';\r\n\r\nReactDOM.render(, document.getElementById('root'));"],"sourceRoot":""} --------------------------------------------------------------------------------