[
  {
    "path": ".editorconfig",
    "content": "[*.{js,md}]\nend_of_line = lf\nindent_style = space\nindent_size = 4\ncharset = utf-8\ninsert_final_newline = true\n\n[*.js]\ntrim_trailing_whitespace = true\n\n[*.md]\ntrim_trailing_whitespace = false"
  },
  {
    "path": "LICENSE.md",
    "content": "# MIT License\n\nCopyright (c) 2020-2024 Marcus <m@rcus.dev>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# OnlyFans Cookie Helper\n\nAn extension made to make it easier to copy the correct `config.json` values when using [datawhores/OF-Scraper](https://github.com/datawhores/OF-Scraper) or [DIGITALCRIMINALS/OnlyFans](https://github.com/DIGITALCRIMINALS/OnlyFans).\n\n## How to install\n\nExtension is only available on the Firefox addon store, but not on the Chrome web store. For Chromium-based browsers, alternatively installation methods are necessary.  \nOne of these days I might explore putting it on the Chrome web store, but for a few different reasons I bit am hesitant to so (one of them being the paywall Google has to publish extensions).  \n\n### Firefox\n\n#### Option 1 (Recommended)\n\nInstall it from the [Firefox Addon Store (AMO)](https://addons.mozilla.org/en-US/firefox/addon/onlyfans/)\n\n**NOTE**: Mozilla disabled the addon on February 27th 2024 citing the reason:  \n> Acceptable Use, specifically Sexual content: This content contains sexual or pornographic content that violates Mozilla’s Acceptable Use Policy.\n\nI have replied to them to appeal, since the addon doesn't contain (or link to) any sexual content, but still awaiting any further reply from Mozilla.  \nIf you need to \\[re-\\]install the addon, please use option 2 or 3 for the time being.\n\n#### Option 2\n\nGo to [Releases](https://github.com/M-rcus/OnlyFans-Cookie-Helper/releases), download the `.xpi` file and install it by typing `about:addons` into your URL bar, pressing `CTRL+Shift+A` or clicking the \"hamburger menu\" top-right of the Firefox window and then \"Addons\".\n\n![Screenshot on how to do Firefox install option 1](https://i.marcus.pw/ss/2021-04-10_vOzkx1.png)\n\n#### Option 3\n\nFollow the [Trying it out](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Your_first_WebExtension#Trying_it_out) steps on their developer website.\n\n### Chrome / Chromium\n\nThese steps MAY work on other Chromium-based browsers, such as: Brave, Microsoft Edge, Vivaldi and Opera (to name a few).  \nNo guarantees though, I only do simple tests on a basic Chromium install, as my primary browser is Firefox.\n\n#### Option 1\n\nThis option is only available as of v2.2.0. This is **VERY unofficial way** of installing the extension and you might a few warnings about it being unsafe (which is _generally_ true).  \n<ins>If you are not comfortable with that, you can either choose to use Firefox instead or try option 2 below.</ins>\n\n1. Go to [Releases](https://github.com/M-rcus/OnlyFans-Cookie-Helper/releases) and click the `.crx` file. Your browser might prompt you to install the extension. You can then just click 'Add extension'.\n    - From my testing, Google Chrome and Brave give you an error when doing this. It does seem to work on \"Ungoogled Chromium\". Besides that I am not sure whether it will work or not.\n\nIf it _does not_ prompt you to install the extension, you can try the following:\n\n1. Right-click on the `.crx` download link and click \"Save link as...\"\n    - As I mentioned, it will likely ask you (sometimes multiple times) if you want to keep the file as it can be malicious. You want to keep the file.\n2. In your Chromium-browser, go to your URL bar and hit enter after typing in `chrome://extensions`\n3. Find the `.crx` file that you just downloaded.\n4. Click and drag the `.crx` file into your Chromium browser window, where `chrome://extensions` is open.\n5. It should prompt you to add the extension.\n\n#### Option 2\n\n1. Download the ZIP file of the version - `Source code (.zip)`\n2. Extract the ZIP into a folder.\n3. In your Chromium-browser, go to your URL bar and hit enter after typing in `chrome://extensions`\n4. Click on \"Load unpacked\". **Select** the extracted folder and click \"Open\".\n\n## How to use\n\nMake sure you're logged into the OnlyFans website normally.\n\nAfter installing the extension, click the cookie icon.  \nA popup should show up (see [preview](#preview)) with a JSON-formatted text.  \nThere's a a \"Copy to clipboard\" button at the bottom of the popup that should copy the text to your clipboard.  \nIf it does not work, you can just copy the text manually by selecting it.\n\nOnce you've copied the text to clipboard, you can paste it into the `auth.json` file in your profiles folder.  \nThe default `auth.json` file should be located in `<OnlyFans-Software-Folder>/.profiles/OnlyFans/default/auth.json`, but may not show up until you've started up the OnlyFans software at least once.\n\nYou can also create a new folder and a separate `auth.json` file, which is useful if you have multiple accounts.  \nFor example:\n- `<OnlyFans-Software-Folder>/.profiles/OnlyFans/my-personal-account/auth.json`\n- `<OnlyFans-Software-Folder>/.profiles/OnlyFans/my-secret-account/auth.json`\n\n### Preview\n\nScreenshot as of extension version v1.0.3, which means it's slightly outdated.  \nA few things to note:\n- `auth_hash`, `auth_uniq_`, `email` and `password` are _typically empty_. Don't panic if they don't have any values, as it's completely normal.\n- The `username` field is by default set to \"u\" plus the same number as `auth_id`. It _does not_ need to be your actual OnlyFans username.\n\n![Preview of extension](https://i.marcus.pw/ss/2021-05-20_5hI4rK.png)\n\n## Permissions\n\nOverview of permissions and why they're required.\n\n- `cookies`\n    - Values such as `auth_id` and `sess` are contained within cookies.\n    - Keep in mind that the `cookies` permission only applies for `onlyfans.com` and no other websites.\n- `clipboardWrite`\n    - To copy the `auth.json` values into your clipboard\n- `storage`\n    - This is specifically just to \"synchronize\" the `x_bc` value to the popup (so it can be copied).\n    - `x_bc` isn't available via the regular `cookies` permission, so we need a workaround (which utilizes the `storage` permission).\n- `contextualIdentities`\n    - On Firefox, it's used to support multi-account containers.\n    - ~~On Chromium-based browsers (Google Chrome, Brave, Microsoft Edge, Vivaldi, Opera etc.) it does nothing. However, it may give a warning. The extension should still work even with this warning.~~ - This should no longer happen as of v2.2.0.\n\n## LICENSE\n\n[MIT License](./LICENSE.md)\n\n## Mirrors\n\nThis project is currently mirrored to three different providers:\n\n- [GitHub](https://github.com/M-rcus/OnlyFans-Cookie-Helper)\n- [GitLab](https://gitlab.com/Maarcus/OnlyFans-Cookie-Helper)\n- [GitGud.io](https://gitgud.io/Maarcus/OnlyFans-Cookie-Helper)\n\nThose are the only 'official' sources for this extension.  \nAnyone else can of course freely mirror the project as they see fit.\n\n## Sellout (Tips)\n\nIf you find the extension useful and would like to send me a tip, then I'll gladly take some crypto <3\n\n- Bitcoin: `bc1qps35rpadgmpf2a7vmuq45xnt7qscymtlnny6mx`\n- Dogecoin: `DAjtoHdXFFhRc3qJq8sqCWpQLLDB8t3L6n`\n- Litecoin: `LbX5iqVfYoRz7kPAPQoEKdqiN7Y9PRxsAg`\n\nAlternatively, PayPal, though crypto is preferred: https://paypal.me/maaaarcus"
  },
  {
    "path": "background/background.js",
    "content": "const ls = chrome.storage.local;\n\n/**\n * Helper for storing the new bcTokens object\n */\nfunction storeBcTokens(bcTokens)\n{\n    ls.set({'bcTokens': bcTokens});\n}\n\n/**\n * Retrieve the stored bcTokens object\n * If none, return a fresh object\n */\nasync function getStoredBcTokens()\n{\n    return new Promise((resolve, reject) => {\n        ls.get(['bcTokens'], function(data) {\n            if (!data.bcTokens) {\n                storeBcTokens({});\n                resolve({});\n                return;\n            }\n\n            resolve(data.bcTokens);\n        });\n    });\n}\n\nasync function handleBcToken(data)\n{\n    const { bcTokenSha, id } = data;\n\n    const bcTokens = await getStoredBcTokens();\n    bcTokens[id] = bcTokenSha;\n    storeBcTokens(bcTokens);\n\n    return true;\n}\n\nchrome.runtime.onMessage.addListener(handleBcToken);\n"
  },
  {
    "path": "content_scripts/bcToken.js",
    "content": "async function getBcToken()\n{\n    const ls = window.localStorage;\n    if (!ls.bcTokenSha) {\n        return;\n    }\n\n    const bcToken = ls.bcTokenSha;\n\n    /**\n     * We don't have access to all cookies here, so instead we use a workaround\n     * with the few cookie values we _do_ have access to.\n     */\n    const match = document.cookie.match(/st=(\\w{64})/);\n    const id = match[1];\n\n    try {\n        const message = await chrome.runtime.sendMessage({\n            bcTokenSha: bcToken,\n            id: id,\n        });\n    }\n    catch (err) {\n        console.error('Error occurred when trying to send bcToken to background script', err);\n    }\n}\n\n// Handle changes/updates to localStorage\nwindow.addEventListener('storage', function() {\n    const ls = window.localStorage;\n\n    if (ls.bcTokenSha) {\n        getBcToken();\n    }\n});\n\ngetBcToken();\n"
  },
  {
    "path": "create_firefox_zip.sh",
    "content": "#!/bin/bash\nversion=\"$(jq -r .version ./manifest_v2.json)\";\nfilename=\"../OnlyFans-Cookie-Helper_v${version}.zip\";\nrm \"${filename}\";\nzip -x '*.git*' -x '*.sh' -r \"${filename}\" *;\nzip -d \"${filename}\" \"manifest.json\";\nprintf \"@ manifest_v2.json\\n@=manifest.json\\n\" | zipnote -w \"${filename}\";"
  },
  {
    "path": "manifest.json",
    "content": "{\n    \"manifest_version\": 3,\n    \"name\": \"OnlyFans Cookie Helper\",\n    \"version\": \"2.3.0\",\n    \"description\": \"Helper extension that makes it easier to copy config.json values for the DIGITALCRIMINALS/OnlyFans scraper\",\n    \"icons\": {\n        \"48\": \"icons/cookie.png\"\n    },\n    \"background\": {\n        \"service_worker\": \"background/background.js\"\n    },\n    \"permissions\": [\n        \"cookies\",\n        \"clipboardWrite\",\n        \"storage\"\n    ],\n    \"host_permissions\": [\n        \"*://*.onlyfans.com/\"\n    ],\n    \"action\": {\n        \"browser_style\": true,\n        \"default_icon\": {\n            \"48\": \"icons/cookie.png\"\n        },\n        \"default_title\": \"OnlyFans Cookie Helper\",\n        \"default_popup\": \"popup/cookies.html\"\n    },\n    \"content_scripts\": [\n        {\n            \"matches\": [\n                \"*://*.onlyfans.com/*\",\n                \"*://*.onlyfans.com/\"\n            ],\n            \"js\": [\n                \"content_scripts/bcToken.js\"\n            ]\n        }\n    ]\n}"
  },
  {
    "path": "manifest_v2.json",
    "content": "{\n    \"manifest_version\": 2,\n    \"name\": \"OnlyFans Cookie Helper\",\n    \"version\": \"2.3.0\",\n    \"description\": \"Helper extension that makes it easier to copy config.json values for the DIGITALCRIMINALS/OnlyFans scraper\",\n    \"icons\": {\n        \"48\": \"icons/cookie.png\"\n    },\n    \"background\": {\n        \"scripts\": [\"background/background.js\"]\n    },\n    \"permissions\": [\n        \"*://*.onlyfans.com/\",\n        \"cookies\",\n        \"clipboardWrite\",\n        \"storage\",\n        \"contextualIdentities\"\n    ],\n    \"browser_action\": {\n        \"browser_style\": true,\n        \"default_icon\": {\n            \"48\": \"icons/cookie.png\"\n        },\n        \"default_title\": \"OnlyFans Cookie Helper\",\n        \"default_popup\": \"popup/cookies.html\"\n    },\n    \"content_scripts\": [\n        {\n            \"matches\": [\"*://*.onlyfans.com/*\", \"*://*.onlyfans.com/\"],\n            \"js\": [\"content_scripts/bcToken.js\"]\n        }\n    ]\n}\n"
  },
  {
    "path": "popup/cookies.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <style type=\"text/css\">\n        .hidden {\n            display: none;\n        }\n\n        .red {\n            color: red;\n        }\n\n        body {\n            margin: 1em;\n        }\n\n        #container-list {\n            margin-top: 0.5em;\n        }\n    </style>\n</head>\n<body>\n    <div>\n        <code><pre id=\"json\"></pre></code>\n        <span id=\"errorMessage\" class=\"hidden red\"></span>\n        <br><br>\n        <button id=\"copy-to-clipboard\">Copy to clipboard</button>\n    </div>\n\n    <div id=\"container-list\" class=\"hidden\">\n        Select container tab:\n        <select>\n            <option value=\"\">Default (no container)</option>\n        </select>\n    </div>\n</body>\n<script type=\"text/javascript\" src=\"../lib/browser-polyfill.min.js\"></script>\n<script type=\"text/javascript\" src=\"cookies.js\"></script>\n</html>"
  },
  {
    "path": "popup/cookies.js",
    "content": "/**\n * Shamelessly copied from: https://techoverflow.net/2018/03/30/copying-strings-to-the-clipboard-using-pure-javascript/\n *\n * Only used as a fallback if for some reason the Clipboard API\n * does not exist... heh.\n */\nasync function copyStringToClipboard (str) {\n    // Create new element\n    var el = document.createElement('textarea');\n    // Set value (string to be copied)\n    el.value = str;\n    // Set non-editable to avoid focus and move outside of view\n    el.setAttribute('readonly', '');\n    el.style = {position: 'absolute', left: '-9999px'};\n    document.body.appendChild(el);\n    // Select text inside element\n    el.select();\n    // Copy text to clipboard\n    document.execCommand('copy');\n    // Remove temporary element\n    document.body.removeChild(el);\n}\n\nconst containerNames = {};\nconst containersEnabled = browser.contextualIdentities !== undefined;\n\n/**\n * Get the correct bcToken from storage\n */\nasync function getBcTokenSha(id)\n{\n    return new Promise((resolve, reject) => {\n        chrome.storage.local.get(['bcTokens'], function(data) {\n            const bcTokens = data.bcTokens || {};\n\n            if (bcTokens[id]) {\n                resolve(bcTokens[id]);\n                return;\n            }\n\n            resolve(null);\n        });\n    });\n}\n\nasync function getContainers()\n{\n    /**\n     * Prefill popup with \"no container\" cookies\n     */\n    grabCookies();\n\n    /**\n     * Non-Firefox browser or containers not enabled.\n     */\n    if (!containersEnabled) {\n        return;\n    }\n\n    /**\n     * Containers are enabled, but none found.\n     */\n    let containers = await browser.contextualIdentities.query({});\n    if (containers.length < 1) {\n        return;\n    }\n\n    // Sort container list by name.\n    containers.sort(function(a, b) {\n        const nameA = a.name.toLowerCase();\n        const nameB = b.name.toLowerCase();\n\n        if (nameA < nameB) {\n            return -1;\n        }\n\n        if (nameA > nameB) {\n            return 1;\n        }\n\n        return 0;\n    });\n\n    const containerSection = document.querySelector('#container-list');\n    containerSection.classList.remove('hidden');\n\n    const optionList = containerSection.querySelector('select');\n\n    for (const container of containers)\n    {\n        const storeId = container.cookieStoreId;\n        const { name } = container;\n\n        containerNames[storeId] = name;\n\n        const option = document.createElement('option');\n        option.setAttribute('value', storeId);\n        option.textContent = name;\n\n        optionList.insertAdjacentElement('beforeend', option);\n    }\n\n    optionList.addEventListener('change', function(event) {\n        const storeId = event.target.value;\n\n        if (!storeId || storeId.length < 1) {\n            grabCookies(null);\n            return;\n        }\n\n        grabCookies(storeId);\n    });\n}\n\nasync function grabCookies(cookieStoreId) {\n    /**\n     * Grab the cookies from the browser...\n     */\n    const cookieOpts = {\n        domain: '.onlyfans.com',\n    };\n\n    /**\n     * Container tabs\n     */\n    if (cookieStoreId) {\n        cookieOpts.storeId = cookieStoreId;\n    }\n\n    const cookies = await browser.cookies.getAll(cookieOpts);\n\n    /**\n     * We only care about `name` and `value` in each cookie entry.\n     */\n    const mappedCookies = {};\n    for (const cookie of cookies)\n    {\n        mappedCookies[cookie.name] = cookie.value;\n    }\n\n    /**\n     * Define and check if `authId` exists\n     * if not, return and call it a day...\n     *\n     * Also define the other elements.\n     */\n    const authId = mappedCookies.auth_id;\n    const sess = mappedCookies.sess;\n    const copyBtn = document.querySelector('#copy-to-clipboard');\n    const jsonElement = document.querySelector('#json');\n    const errorElement = document.querySelector('#errorMessage');\n\n    /**\n     * If authId isn't specified, user is not logged into\n     * OnlyFans... or at least we assume so.\n     */\n    if (!authId || !sess) {\n        let errorMessage = 'Could not find valid cookie values, make sure you are logged into OnlyFans.';\n        if (containersEnabled) {\n            const containerName = containerNames[cookieStoreId] || 'Default (no container)';\n            errorMessage = `Could not find valid cookie values in container: <strong>${containerName}</strong><br>Make sure you are logged into OnlyFans.`;\n        }\n\n        errorElement.innerHTML = errorMessage;\n        errorElement.classList.remove('hidden');\n\n        if (!copyBtn.classList.contains('hidden')) {\n            copyBtn.classList.add('hidden');\n            jsonElement.classList.add('hidden');\n        }\n\n        return;\n    }\n\n    // See `background/background.js` as to why we use `st` here\n    const st = mappedCookies.st;\n    const bcToken = await getBcTokenSha(st);\n    if (!bcToken) {\n        let errorMessage = 'Could not find valid x_bc value. Please open OnlyFans.com once and make sure it fully loads. If you are not logged in, please log in and <i>refresh the page</i>.';\n        if (containersEnabled) {\n            const containerName = containerNames[cookieStoreId] || 'Default (no container)';\n            errorMessage = `Could not find valid x_bc value. Please open OnlyFans.com once in container: <strong>${containerName}</strong><br>Make sure it fully loads. If you are not logged in, please log in and <i>refresh the page</i>.`;\n        }\n\n        errorElement.innerHTML = errorMessage;\n        errorElement.classList.remove('hidden');\n\n        if (!copyBtn.classList.contains('hidden')) {\n            copyBtn.classList.add('hidden');\n            jsonElement.classList.add('hidden');\n        }\n\n        return;\n    }\n\n    copyBtn.classList.remove('hidden');\n    jsonElement.classList.remove('hidden');\n    errorElement.classList.add('hidden');\n\n    /**\n     * Fill out the object that OnlyFans excepts\n     */\n    const config = {\n        username: 'u' + authId,\n        cookie: `auth_id=${authId}; sess=${sess}; auth_hash=; auth_uniq_${authId}=; auth_uid_${authId}=;`,\n        // TODO: Still need to handle this better...\n        user_agent: navigator.userAgent,\n        x_bc: bcToken,\n        support_2fa: true,\n        active: true,\n        email: \"\",\n        password: \"\",\n        hashed: false,\n    };\n\n    /**\n     * Then we print it to the popup :)\n     *\n     * Third parameter to JSON.stringify() is for spacing the indentation.\n     */\n    const authConfig = {\n        auth: config,\n    };\n\n    const cookieJson = JSON.stringify(authConfig, null, 2);\n    jsonElement.textContent = cookieJson;\n\n    /**\n     * Use yee yee ghetto ass method as a fallback\n     * method for copying to clipboard.\n     */\n    const clipboardWriteText = browser.clipboard.writeText || copyStringToClipboard;\n    const oldBtnText = copyBtn.innerHTML;\n    copyBtn.addEventListener('click', async () => {\n        try {\n            await clipboardWriteText(cookieJson);\n\n            copyBtn.textContent = 'Copied to clipboard!';\n            copyBtn.setAttribute('disabled', '1');\n        }\n        catch (err) {\n            console.error(err);\n        }\n\n        setTimeout(() => {\n            copyBtn.textContent = oldBtnText;\n            copyBtn.removeAttribute('disabled');\n        }, 2500);\n    });\n}\n\ndocument.addEventListener('DOMContentLoaded', async () => {\n    await getContainers();\n});\n"
  }
]