├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── CHANGELOG.md ├── README.md ├── dist ├── components │ ├── Feed.js │ └── Media.js ├── index.js └── lib │ └── Instagram.js ├── docs └── screenshot.png ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── index.html └── src ├── components ├── Feed.js ├── Feed.test.js ├── Media.js ├── Media.test.js └── __snapshots__ │ ├── Feed.test.js.snap │ └── Media.test.js.snap ├── index.js └── lib ├── Instagram.js └── Instagram.test.js /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-16.04 6 | strategy: 7 | matrix: 8 | node: ["8", "12"] 9 | name: Node ${{ matrix.node }} 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Setup node 13 | uses: actions/setup-node@v1 14 | with: 15 | node-version: ${{ matrix.node }} 16 | - run: npm install 17 | - run: npm test 18 | - run: npm run build 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | coverage 4 | node_modules 5 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 2.0.1 4 | 5 | - Adding retry to fetch in response to failures. 6 | 7 | ## 2.0.0 8 | 9 | - Instagram CORs breaking changes workaround taken from [here](https://github.com/jsanahuja/InstagramFeed/commit/3fcb4bf7d8e56fc56fc8efe2a3b7d467ab3bcd5c#diff-0eb547304658805aad788d320f10bf1f292797b5e6d745a3bf617584da017051R319). 10 | 11 | ## 1.0.0 12 | 13 | - Initial release to establish API and usage. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Instagram Authless Feed 2 | 3 | ![ci](https://github.com/jamesmoriarty/react-instagram-authless-feed/workflows/ci/badge.svg) ![GitHub package.json version](https://img.shields.io/github/package-json/v/jamesmoriarty/react-instagram-authless-feed) 4 | 5 | ## Notice 6 | 7 | 2021/03 Cross-Origin Resource Sharing (CORS) security improvements have broken the way this library extracts user feed data from Instagram. As a result, I am archiving the project. 8 | 9 | ## Examples 10 | 11 | - [repl.it](https://repl.it/@jamesmoriarty1/SizzlingNonstopCallbacks) 12 | - [jamesmoriarty.xyz](http://www.jamesmoriarty.xyz/react-instagram-authless-feed/) 13 | 14 | ## Screenshots 15 | 16 | ![Screenshot](docs/screenshot.png) 17 | 18 | ## Install 19 | 20 | ``` 21 | npm install jamesmoriarty/react-instagram-authless-feed#v2.0.0 22 | ``` 23 | 24 | ## Props 25 | 26 | | Name | Description | Required | 27 | | ---------------- | ---------------------------- | -------- | 28 | | userName | Instagram user name. | true | 29 | | className | Container css class. | false | 30 | | classNameLoading | Container loading css class. | false | 31 | | limit | Limit media returned. | false | 32 | 33 | ## Usage 34 | 35 | _Please use with caution_ - Instagram's been blocking the workarounds this solution depends on more regularly. 36 | 37 | ```javascript 38 | import Feed from "react-instagram-authless-feed" 39 | ... 40 | ReactDOM.render( 41 | , 42 | document.getElementById('root') 43 | ); 44 | ``` 45 | 46 | It's recommended to wrap the component in an [Error Boundary](https://reactjs.org/docs/error-boundaries.html) because of Instagram's rate limiting. _See [#12](https://github.com/jamesmoriarty/react-instagram-authless-feed/issues/12)_. 47 | 48 | ## Development 49 | 50 | ``` 51 | npm start 52 | ``` 53 | 54 | ## Test 55 | 56 | ``` 57 | npm test 58 | ``` 59 | 60 | ## Release 61 | 62 | ``` 63 | npm run dist 64 | ``` 65 | 66 | ## Build App 67 | 68 | ``` 69 | npm run build 70 | ``` 71 | 72 | ## Deploy App 73 | 74 | ``` 75 | npm run deploy 76 | ``` 77 | -------------------------------------------------------------------------------- /dist/components/Feed.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports["default"] = void 0; 7 | 8 | var _react = _interopRequireWildcard(require("react")); 9 | 10 | var _Instagram = _interopRequireDefault(require("./../lib/Instagram")); 11 | 12 | var _Media = _interopRequireDefault(require("./Media")); 13 | 14 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } 15 | 16 | function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; } 17 | 18 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } 19 | 20 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 21 | 22 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 23 | 24 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 25 | 26 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 27 | 28 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } 29 | 30 | function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } 31 | 32 | function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; } 33 | 34 | function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); } 35 | 36 | function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } 37 | 38 | function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } } 39 | 40 | function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } 41 | 42 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 43 | 44 | var Feed = /*#__PURE__*/function (_Component) { 45 | _inherits(Feed, _Component); 46 | 47 | var _super = _createSuper(Feed); 48 | 49 | function Feed(props) { 50 | var _this; 51 | 52 | _classCallCheck(this, Feed); 53 | 54 | _this = _super.call(this, props); 55 | _this.state = { 56 | loading: true, 57 | media: [] 58 | }; 59 | return _this; 60 | } 61 | 62 | _createClass(Feed, [{ 63 | key: "componentDidMount", 64 | value: function componentDidMount() { 65 | var _this2 = this; 66 | 67 | this.props.getFeedFn(this.props.userName).then(function (media) { 68 | return _this2.setState({ 69 | loading: false, 70 | media: media.slice(0, _this2.props.limit) 71 | }); 72 | })["catch"](function (error) { 73 | return _this2.setState({ 74 | error: error 75 | }); 76 | }); 77 | } 78 | }, { 79 | key: "render", 80 | value: function render() { 81 | if (this.state.error) throw this.state.error; 82 | var className = this.state.loading ? [this.props.className, this.props.classNameLoading].join(" ") : this.props.className; 83 | return /*#__PURE__*/_react["default"].createElement("div", { 84 | className: className 85 | }, this.state.media.map(function (media, index) { 86 | return /*#__PURE__*/_react["default"].createElement(_Media["default"], { 87 | key: index, 88 | src: media.src, 89 | url: media.url, 90 | alt: media.alt 91 | }); 92 | })); 93 | } 94 | }]); 95 | 96 | return Feed; 97 | }(_react.Component); 98 | 99 | _defineProperty(Feed, "defaultProps", { 100 | className: "", 101 | classNameLoading: "", 102 | getFeedFn: _Instagram["default"].getFeed, 103 | limit: 12 104 | }); 105 | 106 | var _default = Feed; 107 | exports["default"] = _default; -------------------------------------------------------------------------------- /dist/components/Media.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports["default"] = void 0; 7 | 8 | var _react = _interopRequireWildcard(require("react")); 9 | 10 | function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; } 11 | 12 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } 13 | 14 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 15 | 16 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 17 | 18 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 19 | 20 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 21 | 22 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } 23 | 24 | function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } 25 | 26 | function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; } 27 | 28 | function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); } 29 | 30 | function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } 31 | 32 | function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } } 33 | 34 | function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } 35 | 36 | var Media = /*#__PURE__*/function (_Component) { 37 | _inherits(Media, _Component); 38 | 39 | var _super = _createSuper(Media); 40 | 41 | function Media() { 42 | _classCallCheck(this, Media); 43 | 44 | return _super.apply(this, arguments); 45 | } 46 | 47 | _createClass(Media, [{ 48 | key: "render", 49 | value: function render() { 50 | return /*#__PURE__*/_react["default"].createElement("a", { 51 | href: this.props.url, 52 | rel: "noopener", 53 | target: "_blank" 54 | }, /*#__PURE__*/_react["default"].createElement("img", { 55 | src: this.props.src, 56 | alt: this.props.alt 57 | })); 58 | } 59 | }]); 60 | 61 | return Media; 62 | }(_react.Component); 63 | 64 | var _default = Media; 65 | exports["default"] = _default; -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _react = _interopRequireDefault(require("react")); 4 | 5 | var _reactDom = _interopRequireDefault(require("react-dom")); 6 | 7 | var _Feed = _interopRequireDefault(require("./components/Feed")); 8 | 9 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } 10 | 11 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 12 | 13 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 14 | 15 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 16 | 17 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 18 | 19 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } 20 | 21 | function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } 22 | 23 | function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; } 24 | 25 | function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); } 26 | 27 | function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } 28 | 29 | function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } } 30 | 31 | function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } 32 | 33 | var ErrorBoundary = /*#__PURE__*/function (_React$Component) { 34 | _inherits(ErrorBoundary, _React$Component); 35 | 36 | var _super = _createSuper(ErrorBoundary); 37 | 38 | function ErrorBoundary(props) { 39 | var _this; 40 | 41 | _classCallCheck(this, ErrorBoundary); 42 | 43 | _this = _super.call(this, props); 44 | _this.state = { 45 | hasError: false 46 | }; 47 | return _this; 48 | } 49 | 50 | _createClass(ErrorBoundary, [{ 51 | key: "componentDidCatch", 52 | value: function componentDidCatch(error, errorInfo) {// You can also log the error to an error reporting service 53 | } 54 | }, { 55 | key: "render", 56 | value: function render() { 57 | if (this.state.hasError) { 58 | return /*#__PURE__*/_react["default"].createElement("div", null, /*#__PURE__*/_react["default"].createElement("h1", null, "Something went wrong."), /*#__PURE__*/_react["default"].createElement("p", null, "N.B. VPN/NAT source ip addresses can trigger Instagram rate limits.")); 59 | } 60 | 61 | return this.props.children; 62 | } 63 | }], [{ 64 | key: "getDerivedStateFromError", 65 | value: function getDerivedStateFromError(error) { 66 | return { 67 | hasError: true 68 | }; 69 | } 70 | }]); 71 | 72 | return ErrorBoundary; 73 | }(_react["default"].Component); 74 | 75 | _reactDom["default"].render( /*#__PURE__*/_react["default"].createElement(ErrorBoundary, null, /*#__PURE__*/_react["default"].createElement(_Feed["default"], { 76 | userName: "jamespaulmoriarty", 77 | className: "Feed", 78 | classNameLoading: "Loading", 79 | limit: "8" 80 | })), document.getElementById("root")); -------------------------------------------------------------------------------- /dist/lib/Instagram.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports["default"] = void 0; 7 | 8 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 9 | 10 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 11 | 12 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 13 | 14 | var Instagram = /*#__PURE__*/function () { 15 | function Instagram() { 16 | _classCallCheck(this, Instagram); 17 | } 18 | 19 | _createClass(Instagram, null, [{ 20 | key: "getFeed", 21 | value: function getFeed(userName) { 22 | var mapMedia = function mapMedia(json) { 23 | try { 24 | var thumbnailIndex = function thumbnailIndex(node) { 25 | node.thumbnail_resources.forEach(function (item, index) { 26 | if (item.config_width === 640) { 27 | return index; 28 | } 29 | }); 30 | return 4; // MAGIC 31 | }; 32 | 33 | var _url = function _url(node) { 34 | return "https://www.instagram.com/p/" + node.shortcode; 35 | }; 36 | 37 | var src = function src(node) { 38 | switch (node.__typename) { 39 | case "GraphVideo": 40 | return node.thumbnail_src; 41 | 42 | case "GraphSidecar": 43 | default: 44 | return node.thumbnail_resources[thumbnailIndex(node)].src; 45 | } 46 | }; 47 | 48 | var alt = function alt(node) { 49 | if (node.edge_media_to_caption.edges[0] && node.edge_media_to_caption.edges[0].node) { 50 | return node.edge_media_to_caption.edges[0].node.text; 51 | } else if (node.accessibility_caption) { 52 | return node.accessibility_caption; 53 | } else { 54 | return ""; 55 | } 56 | }; 57 | 58 | var edges = json.entry_data.ProfilePage[0].graphql.user.edge_owner_to_timeline_media.edges; 59 | return edges.map(function (edge) { 60 | return { 61 | alt: alt(edge.node), 62 | url: _url(edge.node), 63 | src: src(edge.node) 64 | }; 65 | }); 66 | } catch (err) { 67 | throw Error("cannot map media array"); 68 | } 69 | }; 70 | 71 | var getJSON = function getJSON(body) { 72 | try { 73 | var data = body.split("window._sharedData = ")[1].split("")[0]; 74 | return JSON.parse(data.substr(0, data.length - 1)); 75 | } catch (err) { 76 | throw Error("cannot parse response body"); 77 | } 78 | }; 79 | 80 | var url = function url() { 81 | return "https://images" + ~~(Math.random() * 3333) + "-focus-opensocial.googleusercontent.com/gadgets/proxy?container=none&url=https://www.instagram.com/" + userName + "/"; 82 | }; 83 | 84 | var fetchWithRetry = function fetchWithRetry(n, err) { 85 | if (n <= 1) throw err; 86 | return fetch(url()).then(function (resp) { 87 | return resp.text(); 88 | }).then(function (body) { 89 | return getJSON(body); 90 | }).then(function (json) { 91 | return mapMedia(json); 92 | })["catch"](function (err) { 93 | return fetchWithRetry(n - 1, err); 94 | }); 95 | }; 96 | 97 | return fetchWithRetry(5); 98 | } 99 | }]); 100 | 101 | return Instagram; 102 | }(); 103 | 104 | var _default = Instagram; 105 | exports["default"] = _default; -------------------------------------------------------------------------------- /docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesmoriarty/react-instagram-authless-feed/00c339c1d700971c458e89d9d36ebf9da8024fb9/docs/screenshot.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-instagram-authless-feed", 3 | "homepage": "https://jamesmoriarty.github.io/react-instagram-authless-feed", 4 | "version": "2.0.1", 5 | "main": "dist/components/Feed.js", 6 | "private": true, 7 | "dependencies": {}, 8 | "devDependencies": { 9 | "@babel/cli": "^7.10.5", 10 | "@babel/core": "^7.11.0", 11 | "@babel/plugin-proposal-class-properties": "^7.10.4", 12 | "@babel/preset-env": "^7.11.0", 13 | "@babel/preset-react": "^7.10.4", 14 | "gh-pages": "^3.1.0", 15 | "prettier": "2.0.5", 16 | "react": "^16.13.1", 17 | "react-dom": "^16.13.1", 18 | "react-scripts": "0.9.5", 19 | "react-test-renderer": "^16.13.1" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test --env=jsdom --coverage=true", 25 | "eject": "react-scripts eject", 26 | "predeploy": "npm run build", 27 | "deploy": "gh-pages --add -d build", 28 | "dist": "npx babel --presets @babel/preset-env --presets @babel/preset-react src/ --plugins @babel/plugin-proposal-class-properties --ignore 'src/**/*.test.js' --out-dir=dist/", 29 | "pretty": "npx prettier --write ." 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesmoriarty/react-instagram-authless-feed/00c339c1d700971c458e89d9d36ebf9da8024fb9/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 | React Instagram Authless Feed 17 | 69 | 70 | 71 |
72 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /src/components/Feed.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import Instagram from "./../lib/Instagram"; 3 | import Media from "./Media"; 4 | 5 | class Feed extends Component { 6 | static defaultProps = { 7 | className: "", 8 | classNameLoading: "", 9 | getFeedFn: Instagram.getFeed, 10 | limit: 12, 11 | }; 12 | 13 | constructor(props) { 14 | super(props); 15 | 16 | this.state = { loading: true, media: [] }; 17 | } 18 | 19 | componentDidMount() { 20 | this.props 21 | .getFeedFn(this.props.userName) 22 | .then((media) => 23 | this.setState({ 24 | loading: false, 25 | media: media.slice(0, this.props.limit), 26 | }) 27 | ) 28 | .catch((error) => this.setState({ error })); 29 | } 30 | 31 | render() { 32 | if (this.state.error) throw this.state.error; 33 | 34 | const className = this.state.loading 35 | ? [this.props.className, this.props.classNameLoading].join(" ") 36 | : this.props.className; 37 | 38 | return ( 39 |
40 | {this.state.media.map((media, index) => ( 41 | 42 | ))} 43 |
44 | ); 45 | } 46 | } 47 | 48 | export default Feed; 49 | -------------------------------------------------------------------------------- /src/components/Feed.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { create, act } from "react-test-renderer"; 3 | import Feed from "./Feed"; 4 | 5 | describe("#render", async () => { 6 | describe("without limit", async () => { 7 | it("return html", async () => { 8 | const getFeedFn = (userName) => 9 | Promise.resolve([ 10 | { 11 | url: "https://placeholder.com/640", 12 | src: "https://via.placeholder.com/640", 13 | alt: "640x640px image from placeholder.com", 14 | }, 15 | ]); 16 | 17 | let component; 18 | 19 | await act(async () => { 20 | component = create( 21 | 26 | ); 27 | }); 28 | 29 | expect(component.toJSON()).toMatchSnapshot(); 30 | }); 31 | }); 32 | 33 | describe("with limit", async () => { 34 | it("return html", async () => { 35 | const getFeedFn = (userName) => 36 | Promise.resolve([ 37 | { 38 | url: "https://placeholder.com/640", 39 | src: "https://via.placeholder.com/640", 40 | alt: "640x640px image from placeholder.com", 41 | }, 42 | { 43 | url: "https://placeholder.com/640", 44 | src: "https://via.placeholder.com/640", 45 | alt: "640x640px image from placeholder.com", 46 | }, 47 | ]); 48 | 49 | let component; 50 | 51 | await act(async () => { 52 | component = create( 53 | 59 | ); 60 | }); 61 | 62 | expect(component.toJSON()).toMatchSnapshot(); 63 | }); 64 | }); 65 | 66 | it("returns error html", async () => { 67 | class ErrorBoundary extends React.Component { 68 | constructor(props) { 69 | super(props); 70 | 71 | this.state = { hasError: false }; 72 | } 73 | 74 | static getDerivedStateFromError(error) { 75 | return { hasError: true }; 76 | } 77 | 78 | componentDidCatch(error, errorInfo) { 79 | // You can also log the error to an error reporting service 80 | } 81 | 82 | render() { 83 | if (this.state.hasError) { 84 | return
Something went wrong.
; 85 | } 86 | 87 | return this.props.children; 88 | } 89 | } 90 | 91 | const getFeedFn = (userName) => Promise.reject(new Error("fail")); 92 | 93 | let component; 94 | 95 | await act(async () => { 96 | component = create( 97 | 98 | 103 | 104 | ); 105 | }); 106 | 107 | expect(component.toJSON()).toMatchSnapshot(); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /src/components/Media.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | 3 | class Media extends Component { 4 | render() { 5 | return ( 6 | 7 | {this.props.alt} 8 | 9 | ); 10 | } 11 | } 12 | 13 | export default Media; 14 | -------------------------------------------------------------------------------- /src/components/Media.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Media from "./Media"; 3 | import { create } from "react-test-renderer"; 4 | 5 | describe("#render", () => { 6 | it("returns html", () => { 7 | const component = create( 8 | 13 | ); 14 | 15 | expect(component.toJSON()).toMatchSnapshot(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/components/__snapshots__/Feed.test.js.snap: -------------------------------------------------------------------------------- 1 | exports[`#render returns error html 1`] = ` 2 |
3 | Something went wrong. 4 |
5 | `; 6 | 7 | exports[`#render with limit return html 1`] = ` 8 |
10 | 14 | 640x640px image from placeholder.com 17 | 18 |
19 | `; 20 | 21 | exports[`#render without limit return html 1`] = ` 22 |
24 | 28 | 640x640px image from placeholder.com 31 | 32 |
33 | `; 34 | -------------------------------------------------------------------------------- /src/components/__snapshots__/Media.test.js.snap: -------------------------------------------------------------------------------- 1 | exports[`#render returns html 1`] = ` 2 | 6 | 640x640px image from placeholder.com 9 | 10 | `; 11 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import Feed from "./components/Feed"; 4 | 5 | class ErrorBoundary extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.state = { hasError: false }; 10 | } 11 | 12 | static getDerivedStateFromError(error) { 13 | return { hasError: true }; 14 | } 15 | 16 | componentDidCatch(error, errorInfo) { 17 | // You can also log the error to an error reporting service 18 | } 19 | 20 | render() { 21 | if (this.state.hasError) { 22 | return ( 23 |
24 |

Something went wrong.

25 |

26 | N.B. VPN/NAT source ip addresses can trigger Instagram rate limits. 27 |

28 |
29 | ); 30 | } 31 | 32 | return this.props.children; 33 | } 34 | } 35 | 36 | ReactDOM.render( 37 | 38 | 44 | , 45 | document.getElementById("root") 46 | ); 47 | -------------------------------------------------------------------------------- /src/lib/Instagram.js: -------------------------------------------------------------------------------- 1 | class Instagram { 2 | static getFeed(userName) { 3 | const proxyUrl = (url) => { 4 | return ( 5 | "https://images" + 6 | ~~(Math.random() * 3333) + 7 | "-focus-opensocial.googleusercontent.com/gadgets/proxy?container=fb&url=" + 8 | url 9 | ); 10 | }; 11 | 12 | const mapMedia = (json) => { 13 | try { 14 | const thumbnailIndex = (node) => { 15 | node.thumbnail_resources.forEach((item, index) => { 16 | if (item.config_width === 640) { 17 | return index; 18 | } 19 | }); 20 | 21 | return 4; // MAGIC 22 | }; 23 | 24 | const url = (node) => { 25 | return "https://www.instagram.com/p/" + node.shortcode; 26 | }; 27 | 28 | const src = (node) => { 29 | switch (node.__typename) { 30 | case "GraphVideo": 31 | return node.thumbnail_src; 32 | case "GraphSidecar": 33 | default: 34 | return node.thumbnail_resources[thumbnailIndex(node)].src; 35 | } 36 | }; 37 | 38 | const alt = (node) => { 39 | if ( 40 | node.edge_media_to_caption.edges[0] && 41 | node.edge_media_to_caption.edges[0].node 42 | ) { 43 | return node.edge_media_to_caption.edges[0].node.text; 44 | } else if (node.accessibility_caption) { 45 | return node.accessibility_caption; 46 | } else { 47 | return ""; 48 | } 49 | }; 50 | 51 | const edges = 52 | json.entry_data.ProfilePage[0].graphql.user 53 | .edge_owner_to_timeline_media.edges; 54 | 55 | return edges.map((edge) => { 56 | return { 57 | alt: alt(edge.node), 58 | url: url(edge.node), 59 | src: src(edge.node), 60 | }; 61 | }); 62 | } catch (err) { 63 | throw Error("cannot map media array"); 64 | } 65 | }; 66 | 67 | const getJSON = (body) => { 68 | try { 69 | const data = body 70 | .split("window._sharedData = ")[1] 71 | .split("")[0]; 72 | return JSON.parse(data.substr(0, data.length - 1)); 73 | } catch (err) { 74 | throw Error("cannot parse response body"); 75 | } 76 | }; 77 | 78 | return fetch(proxyUrl("https://www.instagram.com/" + userName + "/")) 79 | .then((resp) => resp.text()) 80 | .then((body) => getJSON(body)) 81 | .then((json) => mapMedia(json)); 82 | } 83 | } 84 | 85 | export default Instagram; 86 | -------------------------------------------------------------------------------- /src/lib/Instagram.test.js: -------------------------------------------------------------------------------- 1 | import Instagram from "./Instagram"; 2 | 3 | const edge = 4 | '{"node":{"__typename":"GraphImage","id":"2369795963156206899","shortcode":"CDjNN9uFJUz","dimensions":{"height":1080,"width":1080},"display_url":"https://instagram.fmel8-1.fna.fbcdn.net/v/t51.2885-15/e35/116908564_318537832724621_6986842012601040626_n.jpg?_nc_ht=instagram.fmel8-1.fna.fbcdn.net&_nc_cat=105&_nc_ohc=1jfXjgFWWrgAX_72g2z&oh=326cae3299db6d02d0211fa079e00020&oe=5F577A75","edge_media_to_tagged_user":{"edges":[]},"fact_check_overall_rating":null,"fact_check_information":null,"gating_info":null,"media_overlay_info":null,"media_preview":"ACoqvbacFqXbS4HSt7mNiILTgtS7adtqbjsRBadtqULS7aVx2MGG9mj5lDOPTH/1qjnumdt0aEHryv5c1ZDt25+lO8zHXP5Vze0fb8TWxSF1OTu+ZS2MkDrjp2q4NSkAwUJIxyB1/wAKesnvR5nOOaXtX2/r7h2HNqT7SPLbceMryB79KoG5uTz8/wCv+FXmfacfz/xoDse1L2j7fiFihuUHg/l9euffp+NPWbacHLD39h+dSBQEOAOh/mKikAEgx6f0NLfQBTOvZTgc9McZ65p5kJOQMgY6478D/wCvSSnDgDpz/KnMBkf8B/kanQQhcYOeMcnH4jj16YqLcnqfzP8AjU90AFGOOR/OqFOKuDP/2Q==","owner":{"id":"9186429","username":"jamespaulmoriarty"},"is_video":false,"accessibility_caption":"Photo by James Moriarty in St Kilda, Victoria. Image may contain: sky, grass, flower, outdoor and nature","edge_media_to_caption":{"edges":[]},"edge_media_to_comment":{"count":0},"comments_disabled":false,"taken_at_timestamp":1596721714,"edge_liked_by":{"count":2},"edge_media_preview_like":{"count":2},"location":{"id":"753558458","has_public_page":true,"name":"St Kilda, Victoria","slug":"st-kilda-victoria"},"thumbnail_src":"https://instagram.fmel8-1.fna.fbcdn.net/v/t51.2885-15/sh0.08/e35/s640x640/116908564_318537832724621_6986842012601040626_n.jpg?_nc_ht=instagram.fmel8-1.fna.fbcdn.net&_nc_cat=105&_nc_ohc=1jfXjgFWWrgAX_72g2z&oh=c18698cd5f2f95150127b4c1c62aa571&oe=5F595ACF","thumbnail_resources":[{"src":"https://instagram.fmel8-1.fna.fbcdn.net/v/t51.2885-15/e35/s150x150/116908564_318537832724621_6986842012601040626_n.jpg?_nc_ht=instagram.fmel8-1.fna.fbcdn.net&_nc_cat=105&_nc_ohc=1jfXjgFWWrgAX_72g2z&oh=b144f6475375ebf3712868d4bc2c869f&oe=5F5B31CC","config_width":150,"config_height":150},{"src":"https://instagram.fmel8-1.fna.fbcdn.net/v/t51.2885-15/e35/s240x240/116908564_318537832724621_6986842012601040626_n.jpg?_nc_ht=instagram.fmel8-1.fna.fbcdn.net&_nc_cat=105&_nc_ohc=1jfXjgFWWrgAX_72g2z&oh=83d7fc28316aeb828cac4b645c4a462a&oe=5F5862CE","config_width":240,"config_height":240},{"src":"https://instagram.fmel8-1.fna.fbcdn.net/v/t51.2885-15/e35/s320x320/116908564_318537832724621_6986842012601040626_n.jpg?_nc_ht=instagram.fmel8-1.fna.fbcdn.net&_nc_cat=105&_nc_ohc=1jfXjgFWWrgAX_72g2z&oh=83842b641e7a64d29d7d03dccacf92cd&oe=5F588934","config_width":320,"config_height":320},{"src":"https://instagram.fmel8-1.fna.fbcdn.net/v/t51.2885-15/e35/s480x480/116908564_318537832724621_6986842012601040626_n.jpg?_nc_ht=instagram.fmel8-1.fna.fbcdn.net&_nc_cat=105&_nc_ohc=1jfXjgFWWrgAX_72g2z&oh=b92ade96bd3791fafc362c6d6a3139b4&oe=5F58BDF5","config_width":480,"config_height":480},{"src":"https://instagram.fmel8-1.fna.fbcdn.net/v/t51.2885-15/sh0.08/e35/s640x640/116908564_318537832724621_6986842012601040626_n.jpg?_nc_ht=instagram.fmel8-1.fna.fbcdn.net&_nc_cat=105&_nc_ohc=1jfXjgFWWrgAX_72g2z&oh=c18698cd5f2f95150127b4c1c62aa571&oe=5F595ACF","config_width":640,"config_height":640}]}}', 5 | body = 6 | 'window._sharedData = {"entry_data":{"ProfilePage":[{"graphql":{"user":{"edge_owner_to_timeline_media":{"edges":[' + 7 | edge + 8 | "]}}}}]}}}"; 9 | 10 | describe("#getFeed", async () => { 11 | it("throw error with invalid response body", async () => { 12 | global.fetch = (url) => { 13 | return Promise.resolve({ 14 | text: () => { 15 | return "bad response"; 16 | }, 17 | }); 18 | }; 19 | 20 | try { 21 | await Instagram.getFeed("foo"); 22 | } catch (e) { 23 | expect(e.message).toBe("cannot parse response body"); 24 | } 25 | }); 26 | 27 | it("throw error with invalid response body embed json", async () => { 28 | global.fetch = (url) => { 29 | return Promise.resolve({ 30 | text: () => { 31 | return 'window._sharedData = {"entry_data":{"ProfilePage":[{"graphql":{"user":{"edge_owner_to_timeline_media":{"edges":[{}]}}}}]}}}'; 32 | }, 33 | }); 34 | }; 35 | 36 | try { 37 | await Instagram.getFeed("foo"); 38 | } catch (e) { 39 | expect(e.message).toBe("cannot map media array"); 40 | } 41 | }); 42 | 43 | it("returns media array", async () => { 44 | global.fetch = (url) => { 45 | return Promise.resolve({ 46 | text: () => { 47 | return body; 48 | }, 49 | }); 50 | }; 51 | 52 | expect(await Instagram.getFeed("foo")).toEqual([ 53 | { 54 | alt: 55 | "Photo by James Moriarty in St Kilda, Victoria. Image may contain: sky, grass, flower, outdoor and nature", 56 | src: 57 | "https://instagram.fmel8-1.fna.fbcdn.net/v/t51.2885-15/sh0.08/e35/s640x640/116908564_318537832724621_6986842012601040626_n.jpg?_nc_ht=instagram.fmel8-1.fna.fbcdn.net&_nc_cat=105&_nc_ohc=1jfXjgFWWrgAX_72g2z&oh=c18698cd5f2f95150127b4c1c62aa571&oe=5F595ACF", 58 | url: "https://www.instagram.com/p/CDjNN9uFJUz", 59 | }, 60 | ]); 61 | }); 62 | }); 63 | --------------------------------------------------------------------------------