├── .gitignore ├── content-script.js ├── img └── download.png ├── interceptor.js ├── manifest.json └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /content-script.js: -------------------------------------------------------------------------------- 1 | const insertScript = (pathOrCode, isPath = true) => { 2 | let newScript = window.document.createElement('script'); 3 | if (isPath) newScript.src = chrome.extension.getURL(pathOrCode); 4 | if (!isPath) newScript.innerText = pathOrCode; 5 | newScript.onload = function () { 6 | window.document.body.removeChild(newScript); 7 | }; 8 | let doc = window.document.body || window.document.head || window.document.documentElement; 9 | doc.appendChild(newScript); 10 | } 11 | 12 | insertScript('./interceptor.js'); 13 | 14 | (() => { 15 | const downloadIconUrl = chrome.extension.getURL("img/download.png"); 16 | let interval = setInterval(() => { 17 | const controlsContainer = document.querySelector('.miomc0xe.pmk7jnqg.cgat1ltu.n7fi1qx3.j83agx80'); 18 | if (!controlsContainer) return; 19 | if (controlsContainer.querySelector('.injected-controls')) return; 20 | const button = document.createElement('div'); 21 | button.className = 'tojvnm2t a6sixzi8 k5wvi7nf q3lfd5jv pk4s997a bipmatt0 cebpdrjk qowsmv63 owwhemhu dp1hu0rb dhp61c6y l9j0dhe7 iyyx5f41 a8s20v7p injected-controls'; 22 | button.innerHTML = `
23 |
24 | 25 |
26 |
`; 27 | button.onclick = () => { 28 | const e = new CustomEvent('request-download-story', {}); 29 | window.dispatchEvent(e); 30 | }; 31 | controlsContainer.prepend(button); 32 | }, 100); 33 | })() 34 | -------------------------------------------------------------------------------- /img/download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monokaijs/fb-story-downloader/1f3e8883acc3f4cdfa63ddf3a3ea92a668f17073/img/download.png -------------------------------------------------------------------------------- /interceptor.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | console.log("Interceptor loaded..."); 3 | const serialize = function(obj, prefix) { 4 | let str = [], 5 | p; 6 | for (p in obj) { 7 | if (obj.hasOwnProperty(p)) { 8 | let k = prefix ? prefix + "[" + p + "]" : p, 9 | v = obj[p]; 10 | str.push((v !== null && typeof v === "object") ? 11 | serialize(v, k) : 12 | encodeURIComponent(k) + "=" + encodeURIComponent(v)); 13 | } 14 | } 15 | return str.join("&"); 16 | } 17 | const getQueryValue = (name, query) => { 18 | let raw = query.includes('?') ? query.split('?')[1] : query; 19 | let parts = raw.split('&'); 20 | let parsedObject = {}; 21 | parts.forEach(part => { 22 | if (part.includes('=')) parsedObject[part.split('=')[0]] = part.split('=')[1]; 23 | }); 24 | // console.log(parsedObject); 25 | return parsedObject[name]; 26 | } 27 | 28 | let lastStorySeenRequestData = null; 29 | 30 | (function (xhr) { 31 | let 32 | proto = xhr.prototype, 33 | _send = proto.send, 34 | _open = proto.open; 35 | 36 | // overload open() to access url and request method 37 | proto.open = function () { 38 | // store type and url to use in other methods 39 | this._method = arguments[0]; 40 | this._url = arguments[1]; 41 | if (this._method.toLowerCase() === 'post' && this._url === '/api/graphql/') { 42 | this._isGraphQL = true; 43 | } 44 | _open.apply(this, arguments); 45 | }; 46 | 47 | proto.send = function () { 48 | if (this._isGraphQL) { 49 | const friendlyName = getQueryValue('fb_api_req_friendly_name', arguments[0]); 50 | if (friendlyName === 'storiesUpdateSeenStateMutation') lastStorySeenRequestData = arguments[0]; 51 | } 52 | _send.apply(this, arguments); 53 | }; 54 | })(XMLHttpRequest); 55 | 56 | window.addEventListener('request-download-story', e => { 57 | const variables = getQueryValue('variables', lastStorySeenRequestData); 58 | if (variables) { 59 | const object = JSON.parse(decodeURIComponent(variables)); 60 | const bucketId = object.input['bucket_id']; 61 | const storyId = object.input['story_id']; 62 | fetch('https://www.facebook.com/api/graphql/', { 63 | method: 'POST', 64 | headers: { 65 | 'content-type': 'application/x-www-form-urlencoded' 66 | }, 67 | body: serialize({ 68 | doc_id: '2913003758722672', 69 | variables: JSON.stringify({ 70 | 'bucketIDs': [bucketId], 71 | 'scale': 1, 72 | 'prefetchPhotoUri': false, 73 | }), 74 | fb_dtsg: require('DTSGInitialData').token, 75 | server_timestamps: true, 76 | }), 77 | }).then(r => r.json()).then(response => { 78 | if (response && response.data && response.data.nodes && response.data.nodes[0]) { 79 | const bucket = response.data.nodes[0]; 80 | const stories = bucket['unified_stories']['edges']; 81 | const storyToDownload = stories.find(story => { 82 | return story.node['id'] === storyId 83 | }); 84 | if (storyToDownload) { 85 | const story = storyToDownload.node; 86 | const attachments = story['attachments']; 87 | attachments.forEach(attachment => { 88 | let url; 89 | if (attachment.media['__typename'] === "Photo") { 90 | url = attachment.media['image']['uri']; 91 | } else if (attachment.media['__typename'] === "Video") { 92 | url = attachment.media['playable_url_quality_hd'] || attachment.media['playable_url']; 93 | } 94 | const anchor = document.createElement('a'); 95 | anchor.href = url; 96 | anchor.download = url.split('#').shift().split('?').shift().split('/').pop(); 97 | anchor.target = "_blank"; 98 | document.body.appendChild(anchor); 99 | anchor.click(); 100 | document.body.removeChild(anchor); 101 | }); 102 | } else { 103 | console.log("Failed to find your wanted story.") 104 | } 105 | } 106 | }); 107 | } 108 | }); 109 | 110 | })(); 111 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "FB Story Downloader", 3 | "version": "1.0", 4 | "description": "Download your story on ease.", 5 | "content_scripts": [ 6 | { 7 | "matches": [ 8 | "https://*.facebook.com/*" 9 | ], 10 | "js": [ 11 | "content-script.js" 12 | ] 13 | } 14 | ], 15 | "web_accessible_resources": [ 16 | "img/*.png", 17 | "interceptor.js" 18 | ], 19 | "permissions": [ 20 | "webRequest" 21 | ], 22 | "manifest_version": 2 23 | } 24 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Story Downloader for FB Users 2 | 3 | This extension helps you easily download any Facebook Story. 4 | 5 | --- 6 | 7 | --------------------------------------------------------------------------------