├── .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 |  
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 | 
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 |
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 |
19 | `;
20 |
21 | exports[`#render without limit return html 1`] = `
22 |
33 | `;
34 |
--------------------------------------------------------------------------------
/src/components/__snapshots__/Media.test.js.snap:
--------------------------------------------------------------------------------
1 | exports[`#render returns html 1`] = `
2 |
6 |
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 |
--------------------------------------------------------------------------------