[
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\nlog/\ngeckodriver.log\n\n# production\n/build\n/dist\nOnlySnarf.egg-info\n\n# misc\n.DS_Store\n.env\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# python\n/venv\n__pycache__\n.pytest_cache\n\n# credentials\nOnlySnarf/conf/users/alexdicksdown.conf\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog  \n\n**0.0.1 : 9/25/2018**\n  - code organized\n  **0.0.2 : 10/20/2018**\n  - python package organized\n  **0.0.3 : 1/14/2019**\n  - sync with DBot updates; while loop upload\n  **0.0.4 : 1/21/2019**\n  - upload fix & hashtagging\n  **0.0.5 : 1/29/2019**\n  - demo\n  **0.1.0 : 2/3/2019**\n  - menu\n  - package & setup.py\n  **0.1.1 : 2/4/2019**\n  - menu fixes\n  **0.1.2 : 2/7/2019**\n  - config.py\n  - script names updated\n  - readme updated\n  **2/9/2019**\n  - fuck you PyDrive\n  **0.1.3 : 2/24/2019**\n  - jpeg\n  **0.1.4 : 3/4/2019**\n  - mount path\n  **0.1.5 : 3/7/2019**\n  - updated send_post_button refs\n  **0.1.6 : 3/19/2019**\n  - module separation\n  **0.1.7 : 3/28/2019**\n  - settings.py\n  - user.py\n  **0.1.8 : 3/31/2019**\n  - debugging\n  - Drive API for mp4 downloads\n  **0.2.0 : 4/10/2019**\n  - User: read_chat\n  **0.2.1 : 4/12/2019**\n  - upload performer\n  - upload scene\n  **0.2.2 : 4/15/2019**\n  - settings now actually updates\n  - settings globals -> class\n  **4/16/2019**\n  - fucking default variables\n  **1.0.0 : Production : 4/22/2019**\n  - save image_name instead of path\n  - uploaded to pip\n  **1.0.1**\n  - removed video.mp4\n  **1.0.2**\n  - minor adjustments\n  **1.0.3 : 5/3/2019**\n  - minor bug fixes\n  **1.0.4 : 5/8/2019**\n  - more minor bug fixes\n  **1.1.0 : 5/12/2019**\n  - added: (settings).MOUNT_DRIVE, ROOT_FOLDER, DRIVE_FOLDERS, CREATE_MISSING_FOLDERS\n  - create Google folder structure programmatically\n  - predefine Google root\n  **5/14/2019**\n  - replaced: settings.TYPE - settings.ACTION\n  - added: settings profile -> skeetzo\n  - updated: scenes to include trailer addition\n  **1.1.1 : 5/23/2019**\n  - fixed tweeting bug\n  - todo priority queue\n  **1.1.2 : 5/25/2019**\n  - added: cron.py\n  - |_ needs a menu system to be added\n  **1.1.3 : 6/26/2019**\n  - fixed file & directory uploads\n  - removed config initialization from google.py\n  - updated ReadMe\n  **1.1.4**\n  - fixed MANIFEST and credentials\n  **1.1.5**\n  - removed credentials\n  **1.1.6**\n  - relative imports -> absolute\n  - fixed messaging: input price\n  **1.1.7 : 7/3/2019**\n  - collapsed upload_file & upload_directory into upload_to\n  **1.1.8 : 7/19/2019**\n  - fixed user scrape css\n  **1.1.9 : 8/14/2019**\n  - fixed user scrape css again\n  **1.1.10 : 8/21/2019**\n  - really really fixed user scrape css\n  - removed apiclient from setup.py\n  **1.2.0 : 9/2/2019**\n  - fixed user scrape & cache\n  - added promotions (unfinished)\n  **1.2.1 : 9/4/2019**\n  - removed innate debug profiles and added profile.conf\n  - added google creds to onlysnarf-config\n  - updated readme to reflect creds process\n  **1.2.2 : 9/5/2019**\n  - fixed user scrape & messaging\n  - fixed messaging by username\n  **1.3.0 : 9/8/2019**\n  - finished testing promotions- unworkable w/o email or clipboard utility\n  - finished testing messages & user selection\n  - hidden unworking functions in menu w/o debug\n  **1.3.1 : 9/9/2019**\n  - error messages cleanup in user messaging\n  **1.3.2 : 9/10/2019**\n  - submit button works again\n  - added: way to select google drive file to message\n  **1.3.3 : 9/15/2019**\n  - package install fix & dynamic version in menu\n  **1.3.4 : 9/15/2019**\n  - menu cleanup\n  **1.4.0 : 9/15/2019**\n  - chromedriver binary version set = 77.0.3865.40\n  - added catch for image upload error\n  **1.4.1 : 9/16/2019**\n  - cleaned print & maybePrint outputs\n  - cleaned up settings.py\n  - cron cleanup\n  **1.4.2 : 9/17/2019**\n  - text fix\n  - fixed verbose output\n  **1.4.3 : 9/21/2019**\n  - dbot issue\n  **2.0.0 : 9/25/2019**\n  - added functionality to choose instead of random\n  **2.0.1**\n  - oops\n  **2.1.0 9/29/2019**\n  - discount: all or select users x% for n months\n  - fixed local user load\n  - updated User(mess=mess) -> User(data)\n  **2.1.1**\n  - shameless 2.1.0 fix\n  **2.2.0**\n  - cleaned up & mostly fixed release via selection\n  - cleaned up release via random\n  - nonrandom uploads now confirm entered information\n  - cleaned out user methods from driver -> static\n  - updated download_performers and seperated randomizing selection\n  **2.2.1**\n  - release: keywords and performers can be deleted\n  **2.2.2**\n  - standalone script bug\n  **2.2.3**\n  - removed pointless User cache -> fixed User count bug\n  - removed overwrite-local\n  - added BROWSER.url checks\n  **2.2.4**\n  - user selections -> debug\n  **2.3.0**\n  - added: post\n  **2.4.0**\n  - settings cleanup\n  - release -> upload\n  - onlyfans var cleanup\n  - added local input\n  - added posts beginning -> needs configparser\n  **2.5.0 : 10/5/2019**\n  - configparser -> profile.conf updated\n  - posts & text prompt\n  - functionality to create a post w/ text\n  |_ create multiple basic posts such as \"greetings\" or \"going on holiday\" or a trip, or question of what to post more of?\n  |_ a menu of standardized posts like above\n  |_ a menu of questions|greetings to message to users\n  - added: easier way to select local file to upload\n  **2.5.1**\n  - more verbose cleanup\n  **2.6.0**\n  - config cleanup\n  |_ profile.conf & posts.conf & config.json -> config.conf\n  - added: cron feature for adding, deleting, listing crons\n  - added: Twitter login prompt\n  - added: `local` setting\n  - added check for failed login\n  - added post: \"OnlySnarf Bot commands: !pic | !pic dick | !pic ass\"\n  **2.6.1**\n  - creds cleanup\n  **2.7.0**\n  - menu sort\n  - onlysnarfpy\n  **2.7.1**\n  - driver auth fix\n  - cron fix\n  **2.8.0 : 10/8/2019**\n  - added Expiration\n  - added Schedule\n  - added: Poll\n  - upload a gallery to a message\n  **2.9.0**\n  - a post that advertises custom requests\n  - a post that advertises tipping price for messaging\n  - a post that advertises prices for paid messages for individual photo requests\n  - a post for requesting people to comment or dm me individuals they'd like to see me with\n  - a post thanking followers for being followers\n  **2.9.1**\n  - args fix: keywords, performers, input\n  **2.9.2**\n  - text fix\n  **2.9.3**\n  - add video reduce to somewhere it can impact INPUT files\n  **2.9.4**\n  - input bug?\n  **2.10.0**\n  - discount css update\n  - message image upload fix\n  - added gifs\n  **2.11.0**\n  - OFKEYWORD - specifies random folder\n  **2.11.1**\n  - oops\n  **2.11.2**\n  - oops\n  **2.11.3**\n  - oopsies\n  **2.11.4**\n  - more oopsies\n  **2.12.0**\n  - added setting: skip-backup\n  **2.12.1**\n  - fixed BYKEYWORD bug preventing random upload\n  - fixed messaging folder of images\n  **2.13.0**\n  - fixed: skip-backup\n  - added: skip-delete-google\n  **2.13.1**\n  - fixed fixed: skip-backup\n  - fixed: enter upload\n  **2.13.2**\n  - updated: google mimetypes\n  **2.14.0**\n  - added: NOTKEYWORD for excluding folders by keyword\n  **2.14.1**\n  - updated: upload_to_OnlyFans w/ more error checks in attempt to fix below bug\n  - BUG: does not find \"send_post_button\" from chromebook ubuntu laptop using ChromeDriver 74.0.3729.6 | Google Chrome 74.0.3729.131\n  - cleaned up settings.py comments\n  - added: remember me upon login is checked, does nothing\n  **2.14.2**\n  - added: error_checker for not found elements\n  **2.14.3**\n  - fixed: user scrapes\n  - updated: config.py, unable to test\n  **2.14.4**\n  - class reorg\n  - added 'dynamic' element searching\n  - fixed css elements for major functions\n  - added element.py\n  **2.15.0**\n  - major functionality restored\n  **2.15.1**\n  - fixed menu\n  - added -version flag\n  **2.15.2**\n  - settings options debugging\n  - added: tabbing to inputs\n  - undid tabbing to inputs -> unpredictable odd behavior\n  - minor random debugging (??wtf?)\n  - xmas scripts\n  **2.15.3**\n  - better sorted test scripts\n  - changed error_window:filename fix to just closing window\n  - fixed mimetype upload (sorta)\n  **2.15.4**\n  - updated test scripts output\n  - added setting: verbosest\n  - cleaned up driver&profile elements\n  **2.15.5**\n  - more settings prep\n  - minor fixes to login\n  **2.16.0**\n  - fixed: post\n  - updated menu.md\n  **2.16.1**\n  - fixed: browser closes now\n  **2.16.2**\n  - more Settings integration\n  - updated method of messaging all, recent, favorite\n  **2.16.3**\n  - OnlySnarf classname -> Snarf\n  - updated profile init\n  **2.16.4**\n  - minor bugs\n  **2.16.5**\n  - updated BYKEYWORD and NOTKEYWORD -> str != str\n  **2.16.6**\n  - fixed: message all price submit\n  - fixed: random files now properly downloaded, again\n  **2.16.7**\n  - fixed: messageAll\n  - fixed: go_to_page\n  **2.16.8**\n  - update: go_to_* auths first\n  - added: following_get\n  **2.16.9**\n  - update: users_get, following_get -> speed +, reliability +\n  - mostly functional\n  **2.16.10** debugging pre 2.17.0\n  - upload -> post\n  - Message, File, Google_File, Google_Folder, Video, Image classes\n  - ffmpeg.py\n  - login function cleaned up / spawn_browser\n  - settings -> argsparse\n  - menu cleaned up\n  - file system selection cleaned up\n  - category\n  **2.17.0**\n  - massive spaghetti -> api overhaul\n  - new menu\n  **2.17.1**\n  - fixed packaging\n  **2.17.2**\n  - fixed post/message w/o prompt\n  **2.17.3**\n  - fucking a\n  **2.17.4**\n  - fixed google uploads w/o prompt\n  **2.17.5**\n  - fixed video extensions\n  **2.17.6**\n  - increased UPLOAD_MAX_DURATION to 6 hrs\n  - minor fixes to menu input\n  **2.17.7**\n  - fixed backup pathing\n  - fixed file upload max\n  **2.17.8**\n  - added remote webdriver operations\n  - added firefox\n  - add performers; debugging\n  **2.17.9**\n  - debugged 2.17.8\n  **2.17.10**\n  - removed moviepy\n  - exit(1) when missing driver\n  **2.17.11**\n  - fixed messaging a user\n  - fixed performer operations\n  **2.17.12**\n  - minor fixes to launching firefox\n  **2.17.13**\n  - fixed post uploads with random content\n  - updated onlysnarf-config\n  **2.17.14**\n  - herpderp\n  **2.17.15**\n  - herpaderpaderp\n  - fixedfixed uploading\n  **2.17.16**\n  - herpaderpaderpa\n  - fixed more uploading prompts\n  **2.17.17**\n  - debugged firefox\n  - debugged Profile (a bit)\n  - debugged backup content\n  - args: added username_account to differentiate from twitter username for login\n  - args: added source & destination\n  - added remote ssh dir\n  - added: login source [onlyfans|twitter]\n  **2.17.18**\n  - oops; fixed firefox \"binary\"\n  **2.17.19**\n  - User: following_get, following_write (still needs debugging)\n  - remote: updated connection priorities; auto -> form -> twitter -> google\n  - login: google; needs debugging\n  **2.17.20**\n  - oops, disabled auto_reconnect until debugging\n  **2.17.21**\n  - minor fixes to menu\n  **2.18.0**\n  - menu updates\n  - profile / settings updates\n  - cleaned up menu.md\n  - updated profile: sync from, sync to, backup\n  - updated login methods\n  - added: delete-empty folders; properly remove empty folders that all images have been removed from when backing up / moving files\n  - debugged: google login\n  - debugged: following_write\n  -> remote webserver behavior\n  - added: session_id, session_url\n  - added: remote-chrome, remote-firefox, auto-remote\n  - added: reconnect\n  - added: session_id & session_url -> session.json for reconnecting to existing browser sessions\n  **2.18.1**\n  - debugged: redundant category asking\n  - debugging: local\n  - updated: tests\n  **2.18.2**\n  - debugged: local\n  - create-drive -> create-missing\n  - debugging: remote\n  **2.18.3**\n  - failed expires/poll/schedule ends post\n  - fixed date validator\n  - debugged: schedule, date, time\n  - debugged: post schedule\n  - debugging promotion: updated promotion args\n  - debugged: discount\n  - debugging: promotion (mostly)\n  **2.18.4**\n  - debugged: promotion- free trial (ish)\n  - debugging: promotion- campaign\n  **2.18.5**\n  - debugged: promotion- campaign\n  - debugging: settings\n  **2.19.0**\n  - added: tabs behavior\n  - added: cookies - wow i'm a fucking idiot for not adding this sooner\n  - debugging: bot\n  - properly tested: settings get\n  - properly testedish: settings set\n  **2.20.0**\n  - added: bot functionality - menu prompt, tip parsing\n  **2.20.1**\n  - updated: saving session_id and session_url\n  - more bot debugging\n  - fixed bin/install-firefox.sh: update for processor\n  **2.20.2**\n  - more debugging\n  - fixed: file input\n  - debugging: grandfathered\n  **3.0.0 : Bot Experiments : 9/21/2020**\n  - major updates to browsers debugged\n  - added: grandfather promotion\n  - added: user lists (finally) - favorites, bookmarks, friends, etc\n  - fixed: performer uploads\n  - added: specify inner category for performers via 'category-performer'\n  - added: fetch file by 'sort' - random|ordered\n  -> Bot\n  - bot functionality to check posts for tips\n  - automatically heart / send dick pics to tips in messages\n  **3.0.1**\n  - added argument error catch\n  **3.0.2**\n  - documentation started\n  **3.0.3**\n  - selenium version 3.141.1 -> 3.141.59\n  - bin/install-firefox version 26 -> 29\n  **3.0.4**\n  - jk no selenium bump...\n  **4.0.0 : Flask & React : 3/24/2021**\n  - flask-react integration and folder restructure\n  **4.0.1 : 3/25/2021**\n  - combined args: download_max & upload_max -> image-limit\n  - added arg: delete (from delete_google)\n  - removed: all cron references\n  - changed: output print to log and uppercase to lowercase, except for menu cli\n  **4.0.2 : 4/14/2021**\n  - added test skeletons\n  **4.0.3 : 12/6/2021**\n  - removed react shit...\n  - cleaned up dir structure; needs updates to package links\n  **4.0.4 : 12/8/2021**\n  - cleaned up snarf.py staticness\n  - updated test_snarf\n  **4.1.0 : Beginning Phase Out : 2/19/2022**\n  - removed all the flask stuff that was being added\n  - updated readme\n  - dropped prices to free account\n  - grandfathered everyone currently to a free amount a while ago\n  - removed paid account $ structure\n  - add flask gui for onlysnarf, etc -> submodules -> idea moved to next encompassing project -> ?\n  - review setup / config\n  - removed all email notifications implementations\n  - added easier on off toggle states\n  - checked DD writeup / ended project, elaborated on crypto payments and current market forewarning w/ fans\n  - completely removed cron features\n  - checked / cleaned content folders -> organize for free model funnel\n  - cleaned up social links + snapchat\n  - updated from.package imports to be shorter -> properly add to __init__.py files\n  **4.1.1 : 3/10/2022**\n  - take a look at AVN stars, maybe (re) set up profile, bio, socials etc and integrate ---> nah, too lazy\n  **4.1.2 : 7/15/2022**\n  - added docstring comments for menu.py\n  - moved config baseDir -> \"/HOME/$USER/.onlysnarf\"\n  - removed google & dropbox (finally)\n  - fixed action: Settings -> now sets values again\n  **4.1.3 : 8/23/2022**\n  - begin testing finally yay\n  - moved saving configs & user configs & session id & cookies to .onlysnarf\n  - added method for reading profiles from conf/users / .onlysnarf/users\n  **4.1.4 : 8/29/2022**\n  - finished first login test\n  - removed 'email' from config for fetching username for login\n  **4.1.5 : 8/31/2022**\n  - added 'debug-firefox' to args for enabling trace logging\n  - added 'debug-selenium' to control logging\n  - finished test_users\n  - added temporary fix for boolean bug: using \"True\" and \"False\" strings instead of booleans\n  **4.1.6 : 9/1/2022**\n  - finished debugging test_discount\n  **4.1.7 : 9/5/2022**\n  - updates code and docstrings in messages.py; left off in file.py \n  - added classes for enums\n  - added beginnings of IPFS \n  **4.1.8 : 9/7/2022**\n  - more code cleanup; debugging process for messages & posts uploading files\n  - switched git branch to development to break things less\n  **4.1.9 : 9/8/2022 : God save the Queen**\n  - added xmas test; need to add xmas shnarfs for testing\n  - cleaned up more test code, still not much headway on uplading a file\n  - began updates for rest of old sh test scripts into python test scripts\n  **4.1.10 : 9/9/2022**\n  - updated menu.md, updated removed_args.py\n  - cleaned up args & commands & docs of such\n  - add / ensure all default values to config.conf\n  - cleand up config files and example\n  - cleaned up dir structure references across project\n  **4.2.0 : 9/12/2022**\n    - finished debugging test_message & test_post; uploading files works again\n    - tested changes made from removing / cleaning up args and commands\n    - cleaned up tests (broke again)\n  **4.2.1 : 9/13/2022**\n    - mostly finished debugging (again): test_message\n    - more finishing touches to uploading post & message (and rebroken fixed things)\n    - major updates / fixes to browser creation flow / attempts to fix reconnect bug\n    - fixed issue in lib/driver with media upload popup from multiple of the same file --> updated error window close\n  **4.2.2 : 9/14/2022**\n    - finished testing test_message and test_post (again)\n    - added tests for selenium browser configurations\n    - mostly finished updating expiration, poll, schedule new .get() return dict({})\n    - mostly finished testing: test_discount (again), test_poll, test_schedule\n    - major updates to classes/schedule & util/settings for proper datetime manipulation\n    - added new tests for schedule variables\n  **4.2.3 : 9/15/2022, 9/18/2022**\n    - more updates to debugging schedule & poll\n    - continued finalizing sufficient OK testing responses\n  **4.2.4 : 9/19/2022**\n    - more debugging schedule & poll, reconnect\n    - added tests for trying different browsers, reconnecting, keeping open, remote sessions\n    - schedule tests pass they just don't set the right hour\n    - major snarf tests all OK (minus poll)\n  **4.3.0 : 9/20/2022, 9/21/2022**\n  - updated selenium, google chrome, & firefox geckodriver versions\n  - driver updates to accomodate selenium version changes\n  - changed Driver back to a basic class instead of all static, needs more debugging (again)\n  - more individual tests for messages\n  - mostly OK on basic tests\n  - reconnect works again for chrome\n  **4.3.1 : 9/22/2022**\n  - more test debugging and finalizing basic OKs\n  - browser reconnect reconnects to browser / retains session\n  - debugging cookies somewhat saving login session\n  - finished test for cookies; finished debugging cookies\n  - finished debugging browser reconnect completely (maybe)\n  **4.3.2 : 9/22/2022**\n  - mostly finished debugging schedule\n  - preparing for pypi upload version bump\n  - almost done completely debugging basic snarf functionality\n  **4.3.3 : 9/24/2022**\n  - updated readme\n  - update & test pypi upload process\n  - updated / checked pypi config\n  - mostly finished / updated tests: all OKs\n  - reorganized tests for grouping\n  **4.3.4 : 9/26/2022**\n  - fixed driver: schedule hours not being set now work again\n  - reorganized schedule in prep for individual component testing\n  - finished debugging schedule (date & time)\n  - fixed driver: poll button not being clicked and rest of poll functionality\n  - finished debugging poll\n  - updated cookie process to check if logged in from session data before overwriting existing cookies and re logging in\n  - fixed message price not entering \n  **4.3.5 : 9/27/2022**\n  - added text clear from post to message\n  - added snarf pic to readme\n  - completely finished debugging basic snarf functionality\n  - ran full tests suite before final upload to pypi\n  **4.3.6 : 10/2/2022**\n  - update / check '-help' output; add to readme\n  - ensure docs/menu.md is properly updated\n  **4.3.7 : 10/4/2022**\n  - added subcommands to -help\n  - changed 'questions' to 'poll'\n  - reorganize tests as necessary (none)\n  **4.3.8 : 10/5/2022**\n  - finished cleaning up class/user\n  - cleaned up user class & simplified current methods for selecting user(s) aka removed prompts for now\n  - restructured class/discount and how users are passed via args\n  - updated message for new way of handling users passed via args\n  **4.3.9: 10/6/2022**\n  - added 'min' and 'max' to arg inputs: price, expiration, duration, amount, months, limit\n  - changed 'poll' args back to 'question'\n  - prepared commands for generating previews to record functionality with ala: \"onlysnarf discount -user random\"\n  **4.3.10: 10/7/2022**\n  - finished adding docstrings to classes/user.py\n  - added subcommand for fetching users\n  - reupdated menu.md w/ pruned config & args\n  - updated help.md\n  - more debugging for new subcommand structure\n  **4.3.11: 10/8/2022**\n  - updated method of importing config/args to allow for full subcommand testing via pytest by adding shim for args\n  **4.3.12: 10/10/2022**\n  - fixed tab handling in driver\n  - debugged & tested newly added subcommand structure: discount, message, post --> snarf.py\n  - beginning recordings for behavior previews\n  **4.4.0: 10/11/2022**\n  - fixed args & config overwrite direction\n  - recorded new videos for demos\n  - updated preview gifs of behavior for readme w/ OBS: discount, message, poll, post, schedule, users\n  - cleaned up config files w/ final changes\n  - added previews to readme\n  - updated user config explainer to readme\n  - cleaned up packages\n  - cleaned up classes/files to keep up with gutting google, etc; removed Remote & Bot and saved in notes/old\n  - doublechecked code for missing docstrings... aka finished cleaning up code (wow go me)\n  - double checked / re-enabled performers & tags functionality\n  - updated help.md and menu.md with new text changes\n  - fixed driver & message actually sending... haha and discount applying.... woops\n  - synced with main/master branch\n  - uploaded working changes to pypi\n  **4.4.1: 10/12/2022**\n  - minor text changes\n  - fixed file upload when posting (of course this would still be semi broken after publishing changes)\n  **10/13/2022**\n  - more minor text changes\n  - changed text: bin/google* --> bin/chrome*\n  **4.4.2: 10/14/2022**\n  - fixed args validator for duration's \"min\" \"max\"\n  - debugging project deployment & installer scripts for web browsers\n  **4.4.3: 10/15/2022**\n  - add a way for installation to work for webdrivers for pypi\n  - added: webdriver_manager; cleaned up driver spawn code and packages : https://pypi.org/project/webdriver-manager/\n  - debugging webdriver install processes on rpi4\n  - added browser options to help with debugging on rpi: brave, chromium, ie, edge, and opera\n  - added tests for new browser options\n  **4.4.4: 10/20/2022**\n  - continued debugging attempts for browsers on rpi4\n  - added notes for debugging browsers\n  **4.4.5: 10/20/2022**\n  - added travis.cli config\n  - connected travis to github\n  - more driver debugging for added webmanager autoinstalls\n  **4.4.6: 10/27/2022**\n  - added travisci for testing python versions & os installs\n  - more rpi debugging attempts; added attempt scripts\n  **4.4.7: 3/17/2023**\n  - upgraded selenium to 4.0\n  - prep for project cleanup and python update\n  - pruned prompts\n  - fixed webdriver manager configurations for most browsers: brave, chrome, chromium, and firefox\n  **3/18/2023**\n  - fixed cookies for chrome but not firefox\n  - rpi4 testing and prep for selenium cleanup\n  - added a way for installation to work for webdrivers for pypi\n  **4.4.8: 3/20/2023**\n  - added check for rpi processor for chrome only\n  - finished testing new browser changes\n  - finished debugging web browser on rpi4\n  - checked current instructions for installing from github\n  - updated to python10\n  - updated install scripts and organize by usability by platform; distinguish arm scripts for rpis\n  - finished debugging new webdriver manager system\n  **4.4.9: 3/21/2023**\n  - fixed unknown bug when fetching random user\n  - fixed applying discounts and updated min/max tests for discounts \n  - fixed messaging and posting \n  **3/22/2023**\n  - fixed poll and schedule\n  - pytest bug w/ final arg\n  - updated any webscraping as necessary\n  - added tests for alternate logins (that probably won't work anyways *cough* google)\n  - begin prepping for merging new changes to main and publishing to pypi\n  **4.4.10: 3/23/2023**\n  - completely finished fixing schedule\n  - super duper verified test results\n  - full test coverage\n  - merged w/ main\n  - published changes to pypi\n  **4.4.11**\n  - fixed 'onlysnarf' cmd references\n  - removed nonworking browser references in optional args\n  - fixed discount bug\n  **4.4.12: 3/24/2023**\n  - RPi4 debugging\n  - fixed element bug when posting\n  - fixed users\n  - fixed error message on close\n  **4/15/2023**\n  - cleaned up git repo size / long clone time\n  **4.4.13: 4/17/2023**\n  - Windows compatability testing\n  - updated pathings for Windows\n  - retested google login (remains disabled)\n  **4.4.14 : 5/29/2023**\n  - beginning readd of cli menu\n  - switch from pyinquirer to inquirer\n  **4.4.15 : 6/2/2023**\n  - fixed cookies bug\n  **4.4.16 : 7/5/2023**\n  - update readme and help&menu docs / added personal touchups\n  - fixed get random user for discount test \n  **4.5.0 : 7/11/2023**\n  - added wget functionality to input for when a url is provided\n  - cleaned up bin/test scripts\n  - added basic api setup\n  - added test scripts for flask & api\n  - beginning modifications for receiving api calls\n  **4.5.1 : 7/12/2023**\n  - fixed package req: validators\n  - added modifications for running via api\n  **4.5.2 : 7/16/2023**\n  - relocated api structure for testing\n  - added tests for flask api\n  - updates to tests, code flow for missing config / args values\n  - added individual message funcationality tests \n    **7/17/2023**\n  - continued debugging message tests\n  - fixed random user functionality\n  - updated driver.poll\n  - fixed new message tests\n  - added flask to package reqs\n  - updated install script\n  - updated api scripts to route through snarf\n  **4.5.3,4,5,6 : 7/30/2023**\n  - api debugging\n  **4.5.7,8 : 8/2/2023**\n  - fixed twitter login; added phone number to args&config\n  - more api debugging w/ aws\n  - updated date&time formats\n  - debugged api: /message & /post\n  **4.5.9 : 8/3/2023**\n  - added update script meant to be run by systemd service script\n  - added config script; requires testing\n  **4.5.10 : 8/4/2023**\n  - moved api & menu to cli\n  - updates to config script\n  - tested new config script\n**4.6.0,1 : 8/7/2023**\n  - minor version bump for working api & cli changes\n**4.6.2 : 8-17-2024**\n- adjusted manifest to include config files\n- updated config subcommand to reset base config file via 'Reset'\n\n------------------------------------------------------------------------------------\n\n## TODO\n\n- add cli args for config to autoconfigure more easily\n- update 'snarf config' to interact with main config file and variables\n\n- look into Marshmellow package for class / object cleanup\n\n\n- add smart idea for getting statement information\n- add better version notes to readme's list of \"works on\"\n- re-add stuff for testing on multiple platforms ala mac ;) \n\n- double check how tags & performers are implemented in config and text config and then re-add to docs\n\n- finish updating image/video downloading\n- finish updating cli menu functionality\n- finish updating profile class & menu\n\n- add bypass for 2fa\nhttps://www.geeksforgeeks.org/two-factor-authentication-using-google-authenticator-in-python/\nhttps://stackoverflow.com/questions/55870489/how-to-handle-google-authenticator-with-selenium\nhttps://stackoverflow.com/questions/8529265/google-authenticator-implementation-in-python\n\n(review usability and code first)\n-> OnlyFans: Promos\n- clean up / fix & test\n- add min/max to args & validators\n- re-enable / add promo subcommands and config variables\n\n-> OnlyFans: Profile\n- new - setup - Twitter -> profile, banner; Price and Settings\n- new - advertise\n- new - posts - tweet to advertise new account, tweet to ask about what you should post, etc; recommend what to post\n- need to add 'create' to Profile for asking for profile settings when syncing to\n- add config for profile templates when testing profile features again\n- add tests for profile integration / behavior\n- re-enable / add profile subcommands and config variables\n\n-> OnlyFans\n- add quiz & target interactions (onlyfans buttons)\n- add functionality that follows profiles that are free for a month\n- update schedule, date, and time args to accept strings aka \"1 day\" or \"1 day 2 hours\"\n- update time to accept strings that modify to add to current time aka \"+2\" or \"2 hours\" adds 2 hours to the current time\n\n(once everything else in app works again)\n- run new auth tests w/ appropriately connected accounts\n-> Twitter\n- actually test if tweeting behavior works in driver\n- needs a dummy account to test actual tweeting w/\n- tweet reminders from inlaid config behavior\n- can enter and edit the final text that is tweeted\n- can include media attachments\n-- add checks for previously existing tweets\n-- keep track of tweets (somehow)\n\n-> Tests\n- separate driver functions into individual components ala schedule --> individual steps; for easier testing (and to clean up the giant ass driver file)\n- add tests for newly separated driver files / functions\n- add tests for additional config variables such as browser and image/video options, limits\n- finish adding tests for individual messaging circumstances: all, recent, favorite, renew on\n- finish adding tests for individual message entry parts, individual post entry parts\n- add and finish tests for remote browser testing; requires remote server setup for testing? or test on same device or the rpi; readd references to remote in config files and such\n\n(webdriver)\n- (if necessary) finish integrating edge, ie, and opera\n- figure out how to request specific webdriver versions installs to test v102 for edge\n\n-> CLI Menu\n(probably never)\n- re-add menu system\n- fix any new cli menu errors made while updating major processes\n- re-enable prompting for discount amount&months in Settings or somewhere else (at some point)\n- re-add removed user select code in notes/selectstuff.py (for menu prompts)\n- re-enable menu command\n\n## Fix / Debug\n\n- fix how tabs open and scroll and then the process opens another tab to find the same elements and scroll again ala: find users then discount user\n\n(unlikely to be fixed soon, if ever)\n- google login: unsafe browser warning --> possibly end of usability --> should I just remove this? form login works, twitter login works (i think)\n-- maybe just cut out / leave as is until can debug \"unsafe browser\" issue?\n\n- debug: discover the cause of the super slow web scraping\n-- probably not: debug_delay\n---- possibly improved via recent updated coding? (4.3.10)\n\n- figure out how to suppress the chrome stacktrace debugging messages\n- fix driver.firefox: DeprecationWarning: service_log_path has been deprecated, please pass in a Service object\n\n### Browser Changes\n\nworking: brave, chrome, chromium, firefox \nnot working: edge, ie, opera\n\nexisting browsers: chrome, firefox\nadded new browsers: brave, chromium, ie, edge, and opera\nother potential browsers: phantomjs (requires node), safari (requires python2.7)\n\nhttps://pypi.org/project/webdriver-manager/\nhttps://stackoverflow.com/questions/58686471/how-to-use-edge-chromium-webdriver-unknown-error-cannot-find-msedge-binary\n\nnotes:\n\n#### edge:\nrequires: msedge-selenium-tools\n- might only work for selenium v102\n- might only work on Windows\n\"There are various issues for chromium drivers for browser v103 used by Edge and Google Chrome. These are being addressed in v104, but they are still in beta. Advise that you downgrade for now to v102.\"\nhttps://stackoverflow.com/questions/72773330/when-running-selenium-edge-in-pyton-getting-sedgedriver-exe-unexpectedly-exite\n\n\n#### ie:\n- might only work on Windows\nhttps://stackoverflow.com/questions/49787327/selenium-on-mac-message-chromedriver-executable-may-have-wrong-permissions\n\n#### opera:\n- might have a version limit requirement\n\nupdating permissions didn't work:\nchown -R ubuntu /home/ubuntu/.wdm/drivers\n\nwhat helps in general:\n>> using correct webdriver options generator\n>> specifying binary paths\n>> correct permissions on binary paths\n\n# API\n\nnote: the \n\n# Bugs\n\n**4.1.4**\n  - boolean checks from \"Settings.is_\" functions are failing: replaced with redundant string checks for if == \"True\"\n**4.3.12**\n  - message: drag&drop has decided to occasionally stop working; maybe a selenium version issue?\n**4.4.0**\n  - discount: amount&months still require 2 passes on average to update values correctly\n**4.4.6**\n  - followed instructions here for enabled firefox on ubuntu 22.04: https://www.reddit.com/r/learnpython/comments/umft75/selenium_your_firefox_profile_cannot_be_loaded_it/ -> https://support.mozilla.org/en-US/kb/install-firefox-linux#w_install-firefox-from-mozilla-builds-for-advanced-users\n**4.4.9**\n  - when running pytest, the final arg is mistakenly picked up as an input (and passes validation, because it's a file) and tests therefore have multiple repeat file uploads    \n**4.5.2**\n  - [fixed] message tests come up negative when they're all passing basic functionality\n\n\n\n# Web Browser Versions\n(no longer as relevant as of 4.4.7ish updates)\n\nVersion Check:\nstable => Google Chrome 106.0.5249.40 beta\nbeta => Google Chrome 106.0.5249.40 beta\nbinary => Version: 106.0.5249.21.0\ngeckodriver => geckodriver 0.31.0 (b617178ef491 2022-04-06 11:57 +0000)\n"
  },
  {
    "path": "LICENSE.txt",
    "content": "MIT License\n\nCopyright (c) 2018 Skeetzo\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": "MANIFEST.in",
    "content": "# Config\ninclude OnlySnarf/conf/*\n\nexclude OnlySnarf/bin\nexclude OnlySnarf/build\nexclude OnlySnarf/dist\nexclude OnlySnarf/log\nexclude OnlySnarf/notes\n\n# Include the README and CHANGELOG\ninclude *.md\n\n# Include the license file\ninclude LICENSE.txt"
  },
  {
    "path": "OnlySnarf/__init__.py",
    "content": "from .snarf import Snarf"
  },
  {
    "path": "OnlySnarf/__main__.py",
    "content": "import sys\nimport os\n\ndef main(args=None):\n    \"\"\"The main routine.\"\"\"\n    if args is None:\n        args = sys.argv[1:]\n    os.system(\"python \"+os.path.join(os.path.dirname(os.path.realpath(__file__)),'snarf.py')+\" \"+\" \".join(args))\n\nif __name__ == \"__main__\":\n    try:\n        main()\n    except:\n        print(sys.exc_info()[0])\n        print(\"Shnarf!\")\n    finally:\n        sys.exit(0)"
  },
  {
    "path": "OnlySnarf/classes/__init__.py",
    "content": ""
  },
  {
    "path": "OnlySnarf/classes/discount.py",
    "content": "from ..lib.driver import Driver\nfrom ..util.settings import Settings\nfrom .user import User\n##\nfrom ..util.defaults import DISCOUNT_MAX_AMOUNT, DISCOUNT_MIN_AMOUNT, DISCOUNT_MAX_MONTHS, DISCOUNT_MIN_MONTHS\n\nclass Discount:\n\n    \"\"\"OnlyFans discount class\"\"\"\n\n    def __init__(self, username, amount=None, months=None):\n\n        \"\"\"OnlyFans discount action.\"\"\"\n\n        self.amount = amount\n        self.months = months\n        self.username = username # the recipient username\n\n    def apply(self):\n\n        \"\"\"\n        Applies the discounted amount to the recipient username via Driver.discount_user\n\n        If the targeted username is one of the matching keywords then all of the \n        matching recipients will be discounted. Values are determined by runtime args or prompted\n        for.\n\n        \"\"\"\n\n        Settings.maybe_print(\"discounting: {}\".format(self.username))\n        return Driver.discount_user(self.get())\n\n    def get(self):\n        \"\"\"\n        Get the discount's values in a dict.\n\n        Returns\n        -------\n        dict\n            A dict containing the values of the discount\n\n        \"\"\"\n\n        return dict({\n            \"amount\": self.get_amount(),\n            \"months\": self.get_months(),\n            \"username\": self.get_username()\n        })\n\n    def get_amount(self):\n\n        \"\"\"\n        Populate and get the amount value\n\n        If not found in args and prompt is enabled, ask for value.\n\n        Returns\n        -------\n        int\n            the discounted amount to apply\n\n        \"\"\"\n\n        amount = self.amount or Settings.get_amount()\n        if int(amount) > int(Settings.get_discount_max_amount()):\n            Settings.warn_print(\"discount amount too high, max -> {}%\".format(Settings.get_discount_max_months()))\n            amount = int(Settings.get_discount_max_amount())\n        elif int(amount) < int(Settings.get_discount_min_amount()):\n            Settings.warn_print(\"discount amount too low, min -> {}%\".format(Settings.get_discount_min_months()))\n            amount = int(Settings.get_discount_min_amount())\n        self.amount = amount\n        return self.amount\n\n    def get_months(self):\n\n        \"\"\"\n        Populate and get the months value\n\n        If not found in args and prompt is enabled, ask for value.\n\n        Returns\n        -------\n        int\n            the number of months to discount for\n\n        \"\"\"\n\n        months = self.months or Settings.get_months()\n        # check variable constraints\n        if int(months) > int(Settings.get_discount_max_months()):\n            Settings.warn_print(\"discount months too high, max -> {} months\".format(Settings.get_discount_max_months()))\n            months = int(Settings.get_discount_max_months())\n        elif int(months) < int(Settings.get_discount_min_months()):\n            Settings.warn_print(\"discount months too low, min -> {} months\".format(Settings.get_discount_min_months()))\n            months = int(Settings.get_discount_min_months())\n        self.months = months\n        return self.months\n\n    def get_username(self):\n\n        \"\"\"\n        Populate and get the username value\n\n        If not found in args and prompt is enabled, ask for value.\n\n        Returns\n        -------\n        str\n            the username to discount\n\n        \"\"\"\n\n        # if self.username: return self.username\n        # self.username = Settings.get_user().username\n        return self.username\n\n    def grandfatherer(self, users=[]):\n\n        \"\"\"\n        Executes the 'Grandfather' discount model\n\n        If users is empty it is populated with users from the 'Grandfather' OnlyFans list in \n        the account. All 'Grandfather'ed users are provided with the max discount for the max months.\n\n        Parameters\n        ----------\n        users : list\n            list of users to 'Grandfather'\n\n        \"\"\"\n\n        if len(users) == 0:\n            users = User.get_users_by_list(name=\"grandfathered\")\n        print(\"Discount - Grandfathering: {} users\".format(len(users)))\n        self.months = DISCOUNT_MAX_MONTHS\n        self.amount = DISCOUNT_MAX_AMOUNT\n        # apply discount to all users\n        for user in users:\n            self.username = user.username\n            print(\"Grandfathering: {}\".format(self.username))\n            try:\n                Driver.get_driver().discount_user(discount=self)\n            except Exception as e:\n                print(e)"
  },
  {
    "path": "OnlySnarf/classes/element.py",
    "content": "# for easily interacting with changeable page elements\n\nfrom ..util.settings import Settings\nfrom ..elements.driver import ELEMENTS as driverElements\nfrom ..elements.login import ELEMENTS as loginElements\nfrom ..elements.profile import ELEMENTS as profileElements\n\nONLYFANS_ELEMENTS = []\nONLYFANS_ELEMENTS.extend(driverElements)\nONLYFANS_ELEMENTS.extend(loginElements)\nONLYFANS_ELEMENTS.extend(profileElements)\n\n# represents elements the webdriver sortof looks for\n# this class and the objects in th elements folder act as a half assed method for organizing the onlyfans interaction points\n# it's an attempt to make things easier to parse but should be cleaned up at some point\n\nclass Element:\n    def __init__(self, name=None, classes=[], text=[], id=[]):\n        self.name = name\n        self.classes = classes\n        self.text = text\n        self.id = id\n\n    def getClass(self):\n        if self.classes and len(self.classes) > 0:\n            return self.classes[0]\n        return \"\"\n\n    def getClasses(self):\n        return self.classes\n\n    def getText(self):\n        if self.text and len(self.text) > 0:\n            return self.text[0]\n        return \"\"\n\n    def getTexts(self):\n        return self.text\n\n    def getId(self):\n        if self.id and len(self.id) > 0:\n            return self.id[0]\n\n    def getIds(self):\n        return self.id\n\n    @staticmethod\n    def get_element_by_name(name):\n        Settings.dev_print(\"getting element: {}\".format(name))\n        if name == None:\n            Settings.err_print(\"missing element name\")\n            return None\n        global ONLYFANS_ELEMENTS\n        for element in ONLYFANS_ELEMENTS:\n            if str(element[\"name\"]) == str(name): return Element(name=element[\"name\"], classes=element[\"classes\"], text=element[\"text\"], id=element[\"id\"])\n        Settings.warn_print(\"missing element fetch - {}\".format(name))\n        return None"
  },
  {
    "path": "OnlySnarf/classes/file.py",
    "content": "import os, shutil, random, sys\nfrom os import walk\n##\nfrom ..lib.ffmpeg import ffmpeg\nfrom ..util.settings import Settings\nimport wget\n\n###############################################################\n\nclass File():\n    \"\"\"File class for manipulating files.\"\"\"\n\n    ONE_GIGABYTE = 1000000000\n    ONE_MEGABYTE = 1000000\n    FIFTY_MEGABYTES = 50000000\n    ONE_HUNDRED_KILOBYTES = 100000\n    \n    MIMETYPES_IMAGES = \"(mimeType contains 'image/jpeg' or mimeType contains 'image/jpg' or mimeType contains 'image/png')\"\n    MIMETYPES_VIDEOS = \"(mimeType contains 'video/mp4' or mimeType contains 'video/quicktime' or mimeType contains 'video/x-ms-wmv' or mimeType contains 'video/x-flv')\"\n    MIMETYPES_ALL = \"(mimeType contains 'image/jpeg' or mimeType contains 'image/jpg' or mimeType contains 'image/png' or mimeType contains 'video/mp4' or mimeType contains 'video/quicktime')\"\n    MIMETYPES_IMAGES_LIST = [\"image/jpeg\",\"image/jpg\",\"image/png\"]\n    MIMETYPES_VIDEOS_LIST = [\"video/mp4\",\"video/quicktime\",\"video/x-ms-wmv\",\"video/x-flv\"]\n    MIMETYPES_ALL_LIST = []\n    MIMETYPES_ALL_LIST.extend(MIMETYPES_IMAGES_LIST)\n    MIMETYPES_ALL_LIST.extend(MIMETYPES_VIDEOS_LIST)\n\n    def __init__(self):\n        \"\"\"File object represents local image/video file\"\"\"\n\n        # the path to the file locally\n        self.path = \"\"\n        # the file extension\n        self.ext = \"\"\n        # image|video, default image\n        self.type = \"image\"\n        ##\n        # file title reference\n        self.title = \"\"\n        # file size\n        self.size = 0\n\n    ######################################################################################\n\n    def check_size(self):\n        \"\"\"\n        Check file size.\n\n        Returns\n        -------\n        bool\n            Whether or not the file exists by checking size\n\n        \"\"\"\n\n        size = self.size\n        if not size and not os.path.exists(self.get_path()):\n            return False\n        if size: return True\n        size = os.path.getsize(self.get_path())\n        Settings.maybe_print(\"file size: {}kb - {}mb\".format(size/1000, size/1000000))\n        if size <= File.ONE_HUNDRED_KILOBYTES:\n            Settings.warn_print(\"tiny file size\")\n        elif size <= File.ONE_MEGABYTE:\n            Settings.warn_print(\"small file size\")\n        elif size > 0:\n            Settings.maybe_print(\"normal file size\")\n        else:\n            Settings.err_print(\"empty file size\")\n            return False\n        self.size = size\n        return True\n\n    ##############################\n\n    def download(self):\n        \"\"\"Download a url. An input can only be a valid path or a valid url.\"\"\"\n\n        Settings.maybe_print(\"downloading file...\")\n        filename = wget.download(self.path, out=self.get_tmp())\n        Settings.print(\"\") # resume same line after wget download\n        Settings.maybe_print(\"downloaded: \"+filename)\n        self.path = filename\n\n    def get_ext(self):\n        \"\"\"Get the file's extension\"\"\"\n\n        if self.ext != \"\": return self.ext\n        self.get_title()\n        return self.ext\n\n    def get_path(self):\n        \"\"\"\n        Get the file's path\n        \n        Returns\n        -------\n        str\n            The file path\n\n        \"\"\"\n\n        if self.path == \"\":\n            Settings.err_print(\"missing file path\")\n            return  \"\"\n        return str(self.path)\n\n    def get_title(self):\n        \"\"\"\n        Get the file's title from it's filename\n        \n        Returns\n        -------\n        str\n            The file's title or filename without extension\n\n        \"\"\"\n\n        if self.title != \"\": return self.title\n        path = self.get_path()\n        if str(path) == \"\": \n            Settings.err_print(\"missing file title!\")\n            return \"\"\n        title, ext = os.path.splitext(path)\n        self.ext = ext.replace(\".\",\"\")\n        self.title = \"{}{}\".format(os.path.basename(title), ext)\n        return self.title\n\n    @staticmethod\n    def get_tmp():\n        \"\"\"Creates / gets the default temporary download directory\"\"\"\n\n        download_path = Settings.get_download_path()\n        if not os.path.exists(download_path):\n            os.mkdir(download_path)\n        return download_path\n\n    def get_type(self):\n        \"\"\"\n        Gets the file's type as an inner class of either Image or Video\n        \n        Returns\n        -------\n        Image|Video\n            The file's type as an image or video class\n\n        \"\"\"\n\n        if self.type: return self.type\n        if str(self.get_ext()) in str(File.MIMETYPES_VIDEOS_LIST):\n            self.type = Video()\n        elif str(self.get_ext()) in str(File.MIMETYPES_IMAGES_LIST):\n            self.type = Image()\n        else: Settings.warn_print(\"unable to parse file type\")\n        return self.type\n\n    def prepare(self):\n        \"\"\"\n        Prepares the file for uploading.\n\n        Runs the apppropriate file type method and downloads the file locally if necessary.\n\n        Returns\n        -------\n        bool\n            Whether or not the file is prepared\n\n        \"\"\"\n\n        Settings.maybe_print(\"preparing file: {}\".format(self.get_title()))\n        # self.get_type().prepare()\n        if not self.check_size():\n            self.download()\n        return self.check_size()\n\n    @staticmethod\n    def get_files_by_folder(path):\n        \"\"\"\n        Get local files from the local folder path.\n\n        Parameters\n        ----------\n        path : str\n            Path to folder to get files of\n\n        Returns\n        -------\n        list\n            The files at the path\n\n        \"\"\"\n\n        f = []\n        for (dirpath, dirnames, filenames) in walk(path):\n            f.extend(filenames)\n            break\n        return f\n\n    def get_random_file(self):\n        \"\"\"Get random file from all files\"\"\"\n\n        return random.choice(self.get_files())\n\n    @staticmethod\n    def get_images_of_folder(folder):\n        \"\"\"\n        Get images of folder.\n\n        Parameters\n        ----------\n        folder : str\n            The folder path to get images from\n\n        Returns\n        -------\n        list\n            The discovered image files\n\n        \"\"\"\n\n        Settings.dev_print(\"getting images of folder: {}\".format(folder.get_title()))\n        if not folder: return []\n        imgs = []\n        files = []\n        valid_images = [\".jpg\",\".gif\",\".png\",\".tga\",\".jpeg\"]\n        for f in os.listdir(folder.get_path()):\n            ext = os.path.splitext(f)[1]\n            if ext.lower() not in valid_images:\n                continue\n            file = File()\n            setattr(file, \"path\", os.path.join(folder.get_path(),f))\n            files.append(file)\n            Settings.maybe_print(\"image path: {}\".format(os.path.join(folder.get_path(),f)))\n        return files\n\n    @staticmethod\n    def get_videos_of_folder(folder):\n        \"\"\"\n        Get videos of folder.\n\n        Parameters\n        ----------\n        folder : str\n            The folder path to get videos from\n\n        Returns\n        -------\n        list\n            The discovered video files\n\n        \"\"\"\n\n        Settings.dev_print(\"getting videos of folder: {}\".format(folder.get_title()))\n        if not folder: return []\n        videos = []\n        files = []\n\n        ## TODO: change this to mimetypes\n\n        valid_videos = [\".mp4\",\".mov\"]\n        for f in os.listdir(folder.get_path()):\n            ext = os.path.splitext(f)[1]\n            if ext.lower() not in valid_videos:\n                continue\n            file = File()\n            setattr(file, \"path\", os.path.join(folder.get_path(),f))\n            files.append(file)\n            Settings.maybe_print(\"video path: {}\".format(os.path.join(folder.get_path(),f)))\n        return files\n\n    @staticmethod\n    def get_folders_of_folder(folderPath):\n        \"\"\"\n        Get folders of folder.\n\n        Parameters\n        ----------\n        folderPath : str\n            The folder path to get folders from\n\n        Returns\n        -------\n        list\n            The discovered folders\n\n        \"\"\"\n\n        # os.walk(directory)\n        # will yield a tuple for each subdirectory. Ths first entry in the 3-tuple is a directory name, so\n        # [x[0] for x in os.walk(directory)]\n        # should give you all of the subdirectories, recursively.\n        # Note that the second entry in the tuple is the list of child directories of the entry in the first position, so you could use this instead, but it's not likely to save you much.\n        # However, you could use it just to give you the immediate child directories:\n        Settings.maybe_print(\"local walk: {}\".format(folderPath))\n        folders = []\n        # Settings.print(os.walk(folderPath))\n        for folder in next(os.walk(folderPath))[1]:\n            Settings.maybe_print(\"folder: {}\".format(folder))\n            fol = Folder()\n            setattr(fol, \"path\", os.path.join(folderPath, folder))\n            folders.append(fol)\n        return folders\n\n    @staticmethod\n    def remove_local():\n        \"\"\"\n        Delete all local files.\n\n        \"\"\"\n\n        try:\n            Settings.maybe_print('deleting local files...')\n            # delete /tmp\n            tmp = File.get_tmp()\n            if os.path.exists(tmp):\n                shutil.rmtree(tmp)\n                Settings.maybe_print('local files removed!')\n            else:\n                Settings.maybe_print('no local files found!')\n        except Exception as e:\n            Settings.dev_print(e)\n\n    # def upload(self):\n    #     \"\"\"\n    #     Process ran by a file after it has been uploaded.\n\n    #     Ensures the file has been backed up and then deleted locally.\n        \n    #     Returns\n    #     -------\n    #     bool\n    #         Whether or not the file was properly handled after its upload\n\n    #     \"\"\"\n    #     if not self.prepare():\n    #         Settings.err_print(\"unable to upload file - {}\".format(self.get_title()))\n    #         return False\n    #     return True\n\n######################################################################################################################\n######################################################################################################################\n######################################################################################################################\n\nclass Folder(File):\n    def __init__(self):\n        File.__init__(self)\n        self.files = None\n\n    def check_size(self):\n        \"\"\"\n        Check the size of the files in the folder to check if the folder exists.\n\n        Returns\n        -------\n        bool\n            Whether or not the folder exists\n\n        \"\"\"\n\n        for file in self.get_files():\n            exists = file.check_size()\n            if not exists: return False\n        return True\n\n    # def combine(self):\n    #     if len(self.files) == 0: return\n    #     Settings.dev_print(\"combining files: {}\".format(len(self.files)))\n    #     Settings.dev_print(\"combine path: {}\".format(combinedPath))\n    #     combinedPath = os.path.join(File.get_tmp(), \"{}-combined\".format(self.title))\n    #     for file in files:\n    #         shutil.move(file.get_path(), combinedPath)\n    #         file.path = \"{}/{}\".format(combinedPath, self.title)\n    #     self.combined = ffmpeg.combine(combinedPath)\n\n    ##############################\n\n    def get_files(self):\n        \"\"\"\n        Get files from the folder.\n\n\n        Returns\n        -------\n        list\n            The discovered files\n\n        \"\"\"\n\n        if not self.files and self.path:\n            self.files = []\n            files = File.get_files_by_folder(self.get_path())\n            for file in files:\n                file_ = File()\n                setattr(file_, \"path\", os.path.join(self.get_path(), file))\n                self.files.append(file_)\n                Settings.maybe_print(\"local file found: {}\".format(file_.get_title()))\n        if Settings.get_title():\n            for file in self.files:\n                if str(Settings.get_title()) == str(file.get_title()):\n                    self.files = [file]\n                    break\n        return self.files\n\n    def get_title(self):\n        \"\"\"\n        Get the title of the folder.\n\n        Returns\n        -------\n        str\n            The folder's title\n\n        \"\"\"\n\n        if self.title: return self.title\n        path = self.get_path()\n        if str(path) == \"\": \n            Settings.err_print(\"missing file title\")\n            return \"\"\n        title = os.path.basename(path)\n        self.title = title\n        return self.title\n\n    def prepare():\n        \"\"\"\n        Prepare the files in the folder for handling.\n\n        Returns\n        -------\n        bool\n            Whether or not the folder has been prepared successfully\n\n        \"\"\"\n\n        Settings.maybe_print(\"preparing folder: {}\".format(self.get_title()))\n        prepared = False\n        for file in self.get_files():\n            prepared_ = file.prepare()\n            if prepared_: prepared = prepared_\n        return prepared\n\n######################################################################################################################\n######################################################################################################################\n######################################################################################################################\n\nclass Image(File):\n    def __init__(self):\n        pass\n\n    def prepare(self):\n        \"\"\"\n        Prepare the image.\n\n        Returns\n        -------\n        bool\n            Whether or not the image has been prepared\n\n        \"\"\"\n\n        Settings.maybe_print(\"preparing image: {}\".format(self.get_title()))\n        return super().prepare()\n\n######################################################################################################################\n######################################################################################################################\n######################################################################################################################\n\nclass Video(File):\n    def __init__(self):\n        self.screenshots = []\n        self.trimmed = \"\"\n        self.split = \"\"\n\n    #seconds off front or back\n    def trim(self):\n        \"\"\"Trim the video file.\"\"\"\n\n        path = self.get_path()\n        self.trimmed = ffmpeg.trim(path) \n\n    # into segments (60 sec, 5 min, 10 min)\n    def split(self):\n        \"\"\"Split the video file.\"\"\"\n\n        path = self.get_path()\n        self.split = ffmpeg.split(path)\n\n    # unnecessary, handled by onlyfans\n    # unless this somehow adds like more metadata\n    def watermark(self):\n        pass\n\n    # cleanup & label appropriately (digital watermarking?)\n    # def get_metadata(self):\n        # pass\n\n    # frames for preview gallery\n    def get_frames(self):\n        \"\"\"Get frames from the video as screenshots.\"\"\"\n\n        path = self.get_path()\n        self.screenshots = ffmpeg.frames(path)\n\n    def prepare(self):\n        \"\"\"\n        Prepare the video.\n\n        Returns\n        -------\n        bool\n            Whether or not the video has been prepared\n\n        \"\"\"\n\n        Settings.maybe_print(\"preparing video: {}\".format(self.get_title()))\n        self.reduce()\n        self.repair()\n        self.watermark()\n        return super().prepare()\n\n    def reduce(self):\n        \"\"\"Reduce the video file.\"\"\"\n\n        if not Settings.is_reduce(): \n            Settings.maybe_print(\"skipping: video reduction\")\n            return\n        path = self.get_path()\n        if (int(os.stat(str(path)).st_size) < File.FIFTY_MEGABYTES or str(Settings.is_reduce()) == \"False\"):\n            return\n        Settings.dev_print(\"reduce: {}\".format(self.get_title()))\n        self.path = ffmpeg.reduce(path)\n    \n    # unnecessary\n    # def repair(self):\n    #     \"\"\"Repair the video file.\"\"\"\n        \n    #     if not Settings.is_repair():\n    #         Settings.dev_print(\"skipping: video repair\")\n    #         return\n    #     path = self.get_path()\n    #     if Settings.is_repair():\n    #         return\n    #     Settings.dev_print(\"repair: {}\".format(self.get_title()))\n    #     self.path = ffmpeg.repair(path)\n\n"
  },
  {
    "path": "OnlySnarf/classes/message.py",
    "content": "import re\nfrom datetime import datetime\nfrom decimal import Decimal\nfrom re import sub\n##\nfrom ..lib.driver import Driver\nfrom .file import File, Folder\nfrom .poll import Poll\nfrom .user import User\nfrom ..util.settings import Settings\nfrom .schedule import Schedule\n\nclass Message():\n    \"\"\"OnlyFans message (and post) class\"\"\"\n\n    def __init__(self, users=[]):\n        \"\"\"\n        OnlyFans message and post object\n\n        A post is just a message on a profile with different options made available. So all posts are messages, as all messages are messages.\n            Squares and rectangles.\n\n        \"\"\"\n\n        # universal message variables\n        self.text = \"\"\n        self.files = []\n        self.performers = []\n        self.price = 0 # $3 - $100\n        self.keywords = []\n        self.__initialized__ = False\n\n    def init(self):\n        \"\"\"Initialize.\"\"\"\n\n        if self.__initialized__: return\n        self.get_text()\n        self.get_keywords()\n        self.get_price()\n        self.get_files()\n        self.get_performers()\n        self.__initialized__ = True\n\n    @staticmethod\n    def format_keywords(keywords):\n        \"\"\"\n        Formats the list provided into a combined string with a # in front of each value.\n\n        Parameters\n        ----------\n        keywords : list\n            List of keywords as strings\n\n        Returns\n        -------\n        str\n            The generated keywords into a string\n        \"\"\"\n\n        # ternary: a if condition else b\n        return \"#{}\".format(\" #\".join(keywords)) if len(keywords) > 0 else \"\"\n\n    @staticmethod\n    def format_performers(performers):\n        \"\"\"\n        Formats the list provided into a combined string with an @ in front of each value.\n            A space is added before @ to close performer search modal (???).\n\n        Parameters\n        ----------\n        performers : list\n            List of performers usernames as strings\n\n        Returns\n        -------\n        str\n            The generated performers into a string\n        \"\"\"\n\n        # ternary: a if condition else b\n        return \" @{} \".format(\" @\".join(performers)) if len(performers) > 0 else \"\"\n\n\n    def format_text(self):\n        \"\"\"Formats self.text with the provided keywords and performers\n\n        \n        Returns\n        -------\n        str\n            The generated text into a string. Example:\n            \"This is the text. @name, @name, and @name #keyword0 #keyword1\"\n\n        \"\"\"\n\n        return \"{}{}{}\".format(self.get_text(), Message.format_performers(self.get_performers()), Message.format_keywords(self.get_keywords())).strip()\n\n    @staticmethod\n    def is_tip(text):\n        \"\"\"\n        Checks if the text contains a tip amount.\n\n        Parameters\n        ----------\n        text : str\n            The text to parse\n\n        Returns\n        -------\n        bool\n            Whether the text contains a tip or not\n        int\n            The tip amount contained, default 0\n\n        \"\"\"\n\n        if re.search(r'I sent you a \\$[0-9]*\\.00 tip ♥', text):\n            amount = re.match(r'I sent you a \\$([0-9]*)\\.00 tip ♥', text).group(1)\n            Settings.maybe_print(\"message contains (tip): {}\".format(amount))\n            return True, int(amount)\n        elif re.search(r\"I\\'ve contributed \\$[0-9]*\\.00 to your Campaign\", text):\n            amount = re.match(r'I\\'ve contributed \\$([0-9]*)\\.00 to your Campaign', text).group(1)\n            Settings.maybe_print(\"message contains (campaign): {}\".format(amount))\n            return True, int(amount)\n        return False, 0\n\n    def get_files(self):\n        \"\"\"\n        Gets files from args specified source or prompts as necessary.\n\n        Uses appropriate file select method as specified by runtime args:\n        - remote (server, ipfs)\n        - local\n\n        Parameters\n        ----------\n        again : bool\n            Whether or not it is the script user's first time around.\n\n        Returns\n        -------\n        list\n            Files in a list\n\n        \"\"\"\n\n        if len(self.files) > 0:\n            files_ = []\n            for file in self.files[:int(Settings.get_upload_max())]:\n                if not isinstance(file, File):\n                    file_ = File()\n                    setattr(file_, \"path\", file)\n                    files_.append(file_)\n                else:\n                    files_.append(file)\n            return files_\n            # return self.files[:int(Settings.get_upload_max())]\n        files = Settings.get_input_as_files()\n        if len(files) > 0:\n            Settings.dev_print(\"fetched input files for upload\")\n            self.files = files[:int(Settings.get_upload_max())] # reduce by max\n            # self.files = files\n            # return files\n        return self.files\n        # files = Folder.get_files()\n        # if files is empty this all basically just skips to the end and returns blank \n        # filed = []\n        # for file in files:\n            # turn all folders into their files\n            # if isinstance(file, Folder): filed.extend(file.get_files())\n            # else:\n                # filed.append(file)\n                # TODO\n                # this goes elsewhere\n                # flag that the files include a performer\n                # if hasattr(file, \"performer\"):\n                    # self.performers.append(getattr(\"performer\", file))\n\n    def get_message(self):\n        \"\"\"\n        Gets the message as a serialized JSON object.\n\n\n        Returns\n        -------\n        Object\n            The message as an object.\n\n        \"\"\"\n\n        return dict({\n            \"text\": self.format_text(),\n            \"files\": self.get_files(),\n            \"performers\": self.get_performers(),\n            \"price\": self.get_price(),\n            \"keywords\": self.get_keywords()\n        })\n\n    def get_performers(self):\n        \"\"\"\n        Gets the performers for the text.\n\n        Returns\n        -------\n        list\n            The performers\n\n        \"\"\"\n\n        if len(self.performers) > 0: return self.performers\n        self.performers = Settings.get_performers()\n        return self.performers\n\n    def get_price(self):\n        \"\"\"\n        Gets the price value if not none else sets it from args or prompts.\n\n\n        Returns\n        -------\n        int\n            The price\n\n        \"\"\"\n\n        if self.price: return self.price\n        price = Settings.get_price()\n        if str(price) == \"0\": return 0\n        priceMin = Settings.get_price_minimum()\n        priceMax = Settings.get_price_maximum()\n        if str(price) == \"max\": price = priceMax\n        elif str(price) == \"min\": price = priceMin\n        elif Decimal(sub(r'[^\\d.]', '', str(price))) < Decimal(priceMin):\n            Settings.warn_print(\"price too low: {} < {}\".format(price, priceMin))\n            Settings.maybe_print(\"adjusting price to minimum...\")\n            price = priceMin\n        elif Decimal(sub(r'[^\\d.]', '', str(price))) > Decimal(priceMax):\n            Settings.warn_print(\"price too high: {} < {}\".format(price, priceMax))\n            Settings.maybe_print(\"adjusting price to maximum...\")\n            price = priceMax    \n        self.price = price\n        return self.price\n\n    def get_keywords(self):\n        \"\"\"\n        Gets the keywords for the text.\n\n        Returns\n        -------\n        list\n            The keywords\n\n        \"\"\"\n\n        if len(self.keywords) > 0: return self.keywords\n        self.keywords = Settings.get_keywords()\n        return self.keywords\n\n    def get_text(self, again=True):\n        \"\"\"\n        Gets the text value if not none else sets it from args or prompts.\n\n\n        Parameters\n        ----------\n        again : bool\n            Whether or not it is the script user's first time around.\n\n        Returns\n        -------\n        str\n            The text to enter.\n\n        \"\"\"\n\n        if self.text != \"\": return self.text\n        # retrieve from args and return if exists\n        text = Settings.get_text()\n        if text != \"\": \n            self.text = text\n            return text\n        text = self.get_text_from_filename()\n        if text != \"\":\n            self.text = text\n            return text\n        self.text = text\n        return self.text\n\n    def get_text_from_filename(self):\n        \"\"\"Gets text from this object's file's title\"\"\"\n\n        if not self.get_files(): return \"\"\n        text = self.files[0].get_title()\n        # if \"_\" in str(self.text):\n        if re.match(\"[0-9]_[0-9]\", text) is not None:\n            texttext = self.files[0].get_parent()[\"title\"]\n        else:\n            try: \n                int(text)\n                # is a simple int\n                if int(text) > 20:\n                    text = self.files[0].get_parent()[\"title\"]\n            except Exception as e:\n                # not a simple int\n                # do nothing cause probably set already\n                pass\n        text = text.replace(\"_\", \" \")\n        # redo keyword parsing (unsure if necessary call)\n        text = self.update_keywords(text)\n        return text\n\n    def send(self, username, user_id=None):\n        \"\"\"\n        Sends a message.\n\n\n        Returns\n        -------\n        bool\n            Whether or not sending the message was successful.\n\n        \"\"\"\n\n        self.init()\n        return User.message_user(self.get_message(), username, user_id=user_id)            \n\nclass Post(Message):\n    \"\"\"OnlyFans message (and post) class\"\"\"\n\n    def __init__(self):\n        \"\"\"\n        OnlyFans post object\n\n        A post is just a message on a profile with different options made available. So all posts are messages, as all messages are messages.\n            Squares and rectangles.\n\n        \"\"\"\n\n        super().__init__(self)\n        self.expiration = 0\n        self.poll = None\n        self.schedule = None\n\n    # def __str__(self):\n    #     return \"fooPost\"\n\n    def init(self):\n        \"\"\"Initialize.\"\"\"\n\n        super().init()\n        self.__initialized__ = False\n        self.get_poll()\n        self.get_schedule()\n        self.get_expiration()\n        self.__initialized__ = True\n\n    def get_expiration(self, again=True):\n        \"\"\"\n        Gets the expiration value if not none else sets it from args or prompts.\n        \n        Parameters\n        ----------\n        again : bool\n            Whether or not it is the script user's first time around.\n\n\n        Returns\n        -------\n        int\n            The expiration as an int.\n\n        \"\"\"\n\n        if self.expiration: return self.expiration\n        # retrieve from args and return if exists\n        expiration = Settings.get_expiration() or 0\n        if expiration: \n            self.expiration = expiration\n            return expiration\n        self.expiration = expiration\n        return self.expiration\n\n    def get_poll(self, again=True):\n        \"\"\"\n        Gets the poll value if not none else sets it from args or prompts.\n        \n        Parameters\n        ----------\n        again : bool\n            Whether or not it is the script user's first time around.\n\n\n        Returns\n        -------\n        Poll\n            Poll object with proper values\n\n        \"\"\"\n\n        # check if poll is ready\n        if self.poll: return self.poll\n        self.poll = Poll()\n        return self.poll\n\n    def get_post(self):\n        \"\"\"\n        Gets the message as a serialized JSON object.\n\n\n        Returns\n        -------\n        Object\n            The message as an object.\n\n        \"\"\"\n\n        return dict({\n            \"text\": self.format_text(),\n            \"files\": self.get_files(),\n            \"performers\": self.get_performers(),\n            \"price\": self.get_price(),\n            \"expiration\": self.get_expiration(),\n            \"schedule\": self.get_schedule(),\n            \"poll\": self.get_poll(),\n            \"keywords\": self.get_keywords()\n        })\n\n    def get_schedule(self):\n        \"\"\"\n        Gets the schedule value if not none else sets it from args or prompts.\n\n        Returns\n        -------\n        Schedule\n            Schedule object with proper values.\n\n        \"\"\"\n\n        if self.schedule: return self.schedule\n        self.schedule = Schedule()\n        return self.schedule\n\n    def send(self):\n        \"\"\"\n        Sends a post.\n\n\n        Returns\n        -------\n        bool\n            Whether or not sending the post was successful.\n\n        \"\"\"\n        \n        self.init()\n        Settings.print(\"post > {}\".format(self.get_text()))\n        if not self.get_files() and self.get_text() == \"\":\n            Settings.err_print(\"Missing files and text!\")\n            return False\n        return Driver.post(self.get_post())\n            \n"
  },
  {
    "path": "OnlySnarf/classes/poll.py",
    "content": "import re\nfrom datetime import datetime\nfrom ..lib.driver import Driver\nfrom ..util.settings import Settings\nfrom .user import User\n##\nfrom .file import File, Folder\n\nclass Poll:\n    \"\"\"OnlyFans Poll class\"\"\"\n\n    def __init__(self):\n        \"\"\"OnlyFans Poll object\"\"\"\n\n        # duration of poll\n        self.duration = None\n        # list of strings\n        self.questions = []\n        # prevents double prompts\n        self.gotten = False\n\n    def get(self):\n        \"\"\"\n        Get the poll's values in a dict.\n\n        Returns\n        -------\n        dict\n            A dict containing the values of the poll\n\n        \"\"\"\n\n        return dict({\n            \"duration\": self.get_duration(),\n            \"questions\": self.get_questions()\n        })\n\n    def get_duration(self):\n        \"\"\"\n        Gets the duration value if not none else sets it from args or prompts.\n\n        Returns\n        -------\n        int\n            The duration as an int\n\n        \"\"\"\n\n        if self.duration: return self.duration\n        self.duration = Settings.get_duration()\n        if int(self.duration) > 30: self.duration = \"No limit\"\n        return self.duration\n\n    def get_questions(self):\n        \"\"\"\n        Gets the questions value if not none else sets it from args or prompts.\n\n        Returns\n        -------\n        list\n            The questions as strings in a list\n\n        \"\"\"\n\n        if len(self.questions) > 0: return self.questions\n        self.questions = Settings.get_questions()\n        return self.questions\n\n    def validate(self):\n        \"\"\"\n        Determines whether or not the poll settings are valid.\n\n        Returns\n        -------\n        bool\n            Whether or not the poll is valid\n\n        \"\"\"\n\n        Settings.dev_print(\"validating poll...\")\n        if len(self.get_questions()) > 0 and str(self.get_duration()) != \"0\":\n            Settings.dev_print(\"valid poll!\")\n            return True\n        Settings.dev_print(\"invalid poll!\")\n        return False"
  },
  {
    "path": "OnlySnarf/classes/profile.py",
    "content": "#!/usr/bin/python3\n# Profile Settings\nimport json\nimport inquirer\n##\nfrom ..lib.driver import Driver\nfrom ..util.settings import Settings\nfrom .user import User\n\nclass Profile:\n    TABS = [\"profile\", \"advanced\", \"messaging\", \"notifications\", \"security\", \"story\", \"other\"]\n\n    # profile settings are either:\n    #   enabled or disabled\n    #   display text, variable type, variable name in settings\n    def __init__(self):\n        profile = Profile.fill_data()\n        for key, value in profile.items():\n            setattr(self, str(key), value)\n\n    @staticmethod\n    def ask_action():\n        questions = [\n            inquirer.List('action',\n                message= \"Please select an action:\",\n                choices= ['Back', 'Backup', 'Check', 'Posts', 'Setup', 'Sync']\n            )\n        ]\n        answers = inquirer.prompt(questions)\n        return answers[\"action\"]\n\n    # Backup\n\n    @staticmethod\n    def ask_backup():\n        questions = [\n            inquirer.List('backup',\n                message= \"Please select a backup action:\",\n                choices= ['Back', 'Content', 'Messages']\n            )\n        ]\n        answers = inquirer.prompt(questions)\n        return answers[\"backup\"]\n\n    @staticmethod\n    def backup_menu():\n        action = Profile.ask_backup()\n        if (action == 'Back'): return\n        elif (action == 'Content'): Profile.backup_content()\n        elif (action == 'Messages'): Profile.backup_messages()\n\n    @staticmethod\n    def backup_content():\n        print(\"Backing Up: Content\")\n        driver = Driver.get_driver()\n        driver.download_content()\n        ## TODO\n        # Files.backup()\n        print(\"Backed Up: Content\")\n        return True\n\n    @staticmethod\n    def backup_messages():\n        print(\"Backing Up: Messages\")\n        # TODO: add user select\n        # select user\n        user = \"all\"\n        # user = User.select_user()\n        driver = Driver.get_driver()\n        driver.download_messages(user)\n        print(\"Backed Up: Messages\")\n\n    # new - advertise - tweet to advertise new account, tweet to ask about what you should post\n    def advertise():\n        pass\n\n    def advertise_menu():\n        pass\n\n    # check settings for 'profile completion'\n    # |- subscription price |- calculate recommended price from percentile count, posts numbers etc \n    # |-  Reward for subscriber referrals\n    # |- about, location, website url, wishlist\n    # |- if connected twitter/google\n    # |- welcome message enabled\n    # |- two step authentication\n    # |- watermark enabled & custom text\n    def check():\n        if not Settings.is_debug():\n            print(\"### Not Available ###\")\n            return\n        print(\"Checking Profile Settings\")\n        profile = Profile.read_local() or Profile.create()\n        desiredProfile = {\n            \"subprice\":\"avalue\", # do manually to check price number as int\n            \"about\":\"avalue\",\n            \"location\":\"avalue\",\n            \"websiteURL\":\"avalue\",\n            \"wishlist\":\"avalue\",\n            \"twitter\":\"avalue\",\n            \"google\":\"avalue\",\n            \"welcomeMessage\":\"avalue\",\n            \"twoStepAuth\":True,\n            \"watermark\":True,\n            \"watermarkPhoto\":True,\n            \"watermarkVideo\":True\n        }\n        # get profile settings\n        # check against preferred settings\n        # output message\n        failed = False\n        for key, value in profile.items():\n            for key_, value_ in desiredProfile.items():\n                Settings.dev_print(\"{}: {} = {}\".format(key, value, value_))\n                if value and str(value_) != \"avalue\":\n                    if value != value_:\n                        print(\"Warning: Unrecommended setting - {}\".format(key))\n                        failed = True\n                elif not value or str(value) != str(value_):\n                    print(\"Warning: Unrecommended setting - {}\".format(key))\n                    failed = True\n        if failed:\n            print(\"Error: Profile check failed!\")\n            return False\n        print(\"Success! Profile check completed.\")\n        return True\n\n    # update basic new profile settings w/ profile settings or prompt \n    # get Twitter profile & banner and use to update profile & banner\n    # About, Price, Wishlist\n    # watermark enabled & custom text == username\n    def setup():\n        if not Settings.is_debug():\n            print(\"### Not Available ###\")\n            return\n        print(\"Setting up basic profile settings\")\n        profile = Profile.read_local() or Profile.create()\n        desiredProfile = {\n            \"subprice\":\"avalue\", # do manually to check price number as int\n            \"about\":\"avalue\",\n\n            \"welcomeMessage\":\"avalue\",\n            \"watermark\":True,\n            \"watermarkText\":True\n        }\n        \n        # compare to existing values to ignore already correctly set values\n        for key, value in profile.items():\n            for key_, value_ in desiredProfile.items():\n                if str(key) == \"subprice\":\n                    # do stuff\n                    continue\n\n                Settings.dev_print(\"{}: {} = {}\".format(key, value, value_))\n                setattr(profile, str(key), value_)\n\n        # search for twitter banner\n        twitterBanner = None\n        # search for twitter profile photo\n        twitterProfile = None\n        # update both\n        setattr(profile, \"coverImage\", twitterBanner)\n        setattr(profile, \"profilePhoto\", twitterProfile)\n        Profile.sync_to_profile(profile=profile)\n\n    def posts_menu():\n        if not Settings.is_debug():\n            print(\"### Not Available ###\")\n            return\n        action = Profile.ask_new()\n        if (action == 'back'): return Profile.menu()\n        elif (action == 'advertise'):\n            Profile.advertise()\n        # elif (action == 'new')\n        # elif (action == 'new')\n\n        Profile.menu()\n\n    @staticmethod\n    def menu():\n        action = Profile.ask_action()\n        if (action == 'Back'): return\n        elif (action == 'Backup'): Profile.backup_menu()\n        elif (action == 'Check'): Profile.check()\n        elif (action == 'Posts'): Profile.posts_menu()\n        elif (action == 'Setup'): Profile.setup()\n        elif (action == 'Sync'): Profile.sync_from_profile()\n        # elif (action == 'sync to'): Profile.sync_to_profile()\n        \n    @staticmethod\n    def get_profile():\n        print(\"Getting Profile\")\n        profile = Profile()\n        for tab in Profile.TABS:\n            profile.sync_from_tab(tab)\n        return profile\n\n    @staticmethod\n    def sync_from_profile():\n        # opens every settings tab in the browser from pages or all\n        # gets necessary variables from browser\n        # variables = get_settings_variables()\n        print(\"Syncing from Profile\")\n        profile = Profile()\n        for tab in Profile.TABS:\n            profile.sync_from_tab(tab)\n        print(\"Synced from Profile\")\n        Profile.write_local(profile)\n        return True\n\n    @staticmethod\n    def sync_to_profile(profile=None):\n        # syncs profile settings to onlyfans\n        print(\"Syncing to Profile\")\n        if not profile:\n            profile = Profile.read_local() or Profile.create()\n        for tab in Profile.TABS:\n            profile.sync_to_tab(tab)\n        print(\"Synced to Profile\")\n        return True\n\n    def sync_from_tab(self, tab):\n        # syncs profile settings from the specificed tab\n        Driver.sync_from_settings_page(profile=self, page=tab)\n\n    def sync_to_tab(self, tab):\n        # syncs profile settings to the specificed tab\n        Driver.sync_to_settings_page(profile=self, page=tab)\n\n    @staticmethod\n    def get_country_list():\n        return [\"USA\",\"Canada\"]\n\n    @staticmethod\n    def get_variables_for_page(page):\n        variables = get_settings_variables()\n        vars_ = []\n        for var in variables:\n            if str(var[1]) == str(page):\n                vars_.append(var)\n        return vars_\n\n    @staticmethod\n    def fill_data():\n        prof = {\n            \"coverImage\": None,\n            \"profilePhoto\": None,\n            \"displayName\": \"\",\n            \"subscriptionPrice\": \"4.99\",\n            \"about\": \"\",\n            \"location\": \"\",\n            \"websiteURL\": None,\n            \"wishlist\":None,\n            \"twitter\":None,\n            \"google\":None,\n            \"welcomeMessage\":None,\n            \"twoStepAuth\":False,\n            \"username\": \"\",\n            \"email\": \"\",\n            \"password\": \"\",\n            \"emailNotifs\": False,\n            \"emailNotifsNewReferral\": False,\n            \"emailNotifsNewStream\": False,\n            \"emailNotifsNewSubscriber\": False,\n            \"emailNotifsNewTip\": False,\n            \"emailNotifsRenewal\": False,\n            \"emailNotifsNewLikes\": False,\n            \"emailNotifsNewPosts\": False,\n            \"emailNotifsNewPrivMessages\": False,\n            \"siteNotifs\": False,\n            \"siteNotifsNewComment\": False,\n            \"siteNotifsNewFavorite\": False,\n            \"siteNotifsDiscounts\": False,\n            \"siteNotifsNewSubscriber\": False,\n            \"siteNotifsNewTip\": False,\n            \"toastNotifs\": False,\n            \"toastNotifsNewComment\": False,\n            \"toastNotifsNewFavorite\": False,\n            \"toastNotifsNewSubscriber\": False,\n            \"toastNotifsNewTip\": False,\n            \"fullyPrivate\": False,\n            \"enableComments\": False,\n            \"showFansCount\": False,\n            \"showPostsTip\": False,\n            \"publicFriendsList\": False,\n            \"ipCountry\": Profile.get_country_list(),\n            \"ipIP\": \"\",\n            \"watermark\": True,\n            \"watermarkPhoto\": False,\n            \"watermarkVideo\": False,\n            \"watermarkText\": \"\",\n            \"liveServer\": \"\",\n            \"liveServerKey\": \"\"\n        }\n        if prof.get(\"username\") and str(prof.get(\"username\")) != \"\" and prof.get(\"watermarkText\") == \"\":\n            prof.set(\"watermarkText\", \"OnlyFans.com/{}\".format(prof.get(\"username\")))\n        return prof\n\n    @staticmethod\n    def read_local():\n        Settings.maybe_print(\"Getting Local Profile\")\n        profile = None\n        try:\n            profile_ = {}\n            with open(str(Settings.get_profile_path())) as json_file:  \n                profile_ = json.load(json_file)['profile']\n            Settings.maybe_print(\"Loaded Local Profile\")\n            profile = Profile()\n            for key, value in profile_:\n                setattr(profile, str(key), value)\n        except Exception as e:\n            Settings.dev_print(e)\n        return profile\n\n    @staticmethod\n    def write_local(profile=None):\n        if profile is None:\n            profile = Profile.get_profile()\n        print(\"Saving Profile Locally\")\n        Settings.maybe_print(\"local profile path: \"+str(Settings.get_profile_path()))\n        try:\n            with open(str(Settings.get_profile_path()), 'w') as outfile:  \n                json.dump({\"profile\":profile.__dict__}, outfile, indent=4, sort_keys=True)\n        except FileNotFoundError:\n            print(\"Error: Missing Profile File\")\n        except OSError:\n            print(\"Error: Missing Profile Path\")\n\n    # returns list of settings and their classes\n    # [\"settingVariableName\",\"pageProfile\",\"inputType-text\"]\n    \ndef get_settings_variables():\n    return [\n\n        ### Profile ###\n\n        [\"coverImage\",\"profile\",\"file\"],\n        [\"profilePhoto\",\"profile\",\"file\"],\n        [\"username\",\"profile\",\"text\"],\n        [\"displayName\",\"profile\",\"text\"],\n        [\"subscriptionPrice\",\"profile\",\"text\"],\n        [\"referralReward\",\"dropdown\"],\n        [\"about\",\"profile\",\"text\"],\n        [\"location\",\"profile\",\"text\"],\n        [\"websiteURL\",\"profile\",\"text\"],\n\n        #### Account / Advanced ###\n\n        # id=\"input-email\"\n        [\"email\",\"advanced\",\"text\"],\n        # id=\"old_password_input\"\n        [\"password\",\"advanced\",\"text\"],\n        # id=\"new_password_input\"\n        [\"newPassword\",\"advanced\",\"text\"],\n        # id=\"new_password2_input\"\n        [\"confirmPassword\",\"advanced\",\"checkbox\"],\n\n        ### Chats / Messages ###\n        # welcome message toggle\n        # welcome message text\n        # welcome message file\n        # welcome message record voice, video\n        # welcome message price\n        # welcome message submit\n\n        # hide outgoing message toggle\n        # show full text of message in the notification email\n\n        ### Notifications ###\n\n        # id=\"push-notifications\"\n        [\"emailNotifs\",\"notifications\",\"toggle\"],\n        # id=\"email-notifications\"\n        [\"emailNotifsReferral\",\"notifications\",\"checkbox\"],\n        [\"emailNotifsStream\",\"notifications\",\"toggle\"],\n        [\"emailNotifsSubscriber\",\"notifications\",\"toggle\"],\n        [\"emailNotifsTip\",\"notifications\",\"toggle\"],\n        [\"emailNotifsRenewal\",\"notifications\",\"toggle\"],\n        # this is a dropdown\n        [\"emailNotifsLikes\",\"notifications\",\"dropdown\"],\n        [\"emailNotifsPosts\",\"notifications\",\"toggle\"],\n        [\"emailNotifsPrivMessages\",\"notifications\",\"toggle\"],\n        [\"siteNotifs\",\"notifications\",\"toggle\"],\n        [\"siteNotifsComment\",\"notifications\",\"toggle\"],\n        [\"siteNotifsFavorite\",\"notifications\",\"toggle\"],\n        [\"siteNotifsDiscounts\",\"notifications\",\"toggle\"],\n        [\"siteNotifsSubscriber\",\"notifications\",\"toggle\"],\n        [\"siteNotifsTip\",\"notifications\",\"toggle\"],\n        [\"toastNotifsComment\",\"notifications\",\"toggle\"],\n        [\"toastNotifsFavorite\",\"notifications\",\"toggle\"],\n        [\"toastNotifsSubscriber\",\"notifications\",\"toggle\"],\n        [\"toastNotifsTip\",\"notifications\",\"toggle\"],\n\n        ### Security ###\n\n        [\"fullyPrivate\",\"security\",\"checkbox\"],\n        [\"enableComments\",\"security\",\"toggle\"],\n        [\"showFansCount\",\"security\",\"toggle\"],\n        [\"showPostsTip\",\"security\",\"toggle\"],\n        [\"publicFriendsList\",\"security\",\"toggle\"],\n        [\"ipCountry\",\"security\",\"list\"],\n        # id=\"input-blocked-ips\"\n        [\"ipIP\",\"security\",\"list\"],\n        # id=\"hasWatermarkPhoto\"\n        [\"watermarkPhoto\",\"security\",\"toggle\"],\n        # id=\"hasWatermarkVideo\"\n        [\"watermarkVideo\",\"security\",\"toggle\"],\n        # placeholder=\"Watermark custom text\"\n        [\"watermarkText\",\"security\",\"text\"],\n\n        ### Story ###\n        # allow message replies - nobody\n        # allow message replies - subscribers\n\n        ### Other ###\n\n        [\"liveServer\",\"other\",\"text\"],\n        [\"liveServerKey\",\"other\",\"text\"],\n        [\"welcomeMessageToggle\",\"other\",\"toggle\"],\n        [\"welcomeMessageText\",\"other\",\"text\"],\n\n    ]\n\n"
  },
  {
    "path": "OnlySnarf/classes/promotion.py",
    "content": "import re\nfrom datetime import datetime\n##\n\nfrom ..lib.driver import Driver\nfrom ..util import defaults as DEFAULT\nfrom ..util.settings import Settings\nfrom .file import File, Folder\nfrom .user import User\n\nclass Promotion:\n    \"\"\"Promotion class\"\"\"\n\n    def __init__(self):\n        \"\"\"Promotion object\"\"\"\n\n        # the amount to discount\n        self.amount = None\n        # the number of trials to allow\n        self.limit = None\n        # the expiration of the trial\n        self.expiration = None\n        # the duration of the discount\n        self.duration = None\n        # the user to apply the promotion to\n        self.user = None\n        # the message to provide with the promotion\n        self.message = None\n        # prevents double prompts\n        self.gotten = False\n\n    @staticmethod\n    def apply_to_user():\n        \"\"\"Applies promotion directly to user via their profile page\n        \n           Applying a discount to a user requires:\n           - amount\n           - duration\n           - expiration\n           - message\n           - user\n\n        \"\"\"\n\n        print(\"Promotion - Apply To User\")\n        p = Promotion()\n        # ensure the promotion has non default values, return early if missing\n        # p.get()\n        gotten = p.get_amount()\n        if not gotten: return False\n        gotten = p.get_duration()\n        if not gotten: return False\n        gotten = p.get_expiration()\n        if not gotten: return False\n        gotten = p.get_message()\n        if not gotten: return False\n        gotten = p.get_user()\n        if not gotten: return False\n        # prompt skip\n        from .driver import Driver\n        # get default driver and apply the promotion directly\n        Driver.promotion_user_directly(promotion=p)\n        return True\n\n    @staticmethod\n    def create_campaign():\n        \"\"\"Creates a Promotional Campaign\n\n           A campaign consists of:\n           - amount\n           - duration\n           - expiration\n           - limit\n           - user\n           - text\n\n        \"\"\"\n\n        print(\"Promotion - Creating Campaign\")\n        p = Promotion()\n        # ensure the promotion has non default values, return early if missing\n        # p.get()\n        gotten = p.get_amount()\n        if not gotten: return False\n        gotten = p.get_user()\n        if not gotten: return False\n        gotten = p.get_expiration()\n        if not gotten: return False\n        gotten = p.get_limit()\n        if not gotten: return False\n        gotten = p.get_duration()\n        if not gotten: return False\n        gotten = p.get_message()\n        if not gotten: return False\n        # prompt skip\n        from .driver import Driver\n        # get the default driver and enter the promotion campaign\n        Driver.promotional_campaign(promotion=p)\n        return True\n\n    # requires the copy/paste and email steps\n    @staticmethod\n    def create_trial_link():\n        \"\"\"Creates a Promotional Trial Link\n\n           A trial link consists of:\n           - duration\n           - expiration\n           - limit\n           - message\n           - user\n            \n           Note: this creates a free trial link but does NOT send it to the user\n           because it is incomplete. The copy/paste step to message to a user is nonfunctioning.           \n\n        \"\"\"\n\n        print(\"Promotion - Creating Trial Link\")\n        p = Promotion()\n        # ensure the promotion has non default values, return early if missing\n        # p.get()\n        gotten = p.get_duration()\n        if not gotten: return False\n        gotten = p.get_expiration()\n        if not gotten: return False\n        gotten = p.get_limit()\n        if not gotten: return False\n        gotten = p.get_message()\n        if not gotten: return False\n        gotten = p.get_user()\n        if not gotten: return False\n        # if not self.gotten: return\n        # limit, expiration, months, user\n        from .driver import Driver\n        link = Driver.promotional_trial_link(promotion=p)\n        # text = \"Here's your free trial link!\\n\"+link\n        # Settings.dev_print(\"Link: \"+str(text))\n        # Settings.send_email(email, text)\n        return True\n\n    def get(self):\n        \"\"\"Update the promotion object's default values\"\"\"\n\n        return dict({\n            \"user\": self.get_user(),\n            \"amount\": self.get_amount(),\n            \"expiration\": self.get_expiration(),\n            \"limit\": self.get_limit(),\n            \"duraction\": self.get_duration(),\n            \"message\": self.get_message()\n        })\n\n    def get_amount(self):\n        \"\"\"\n        Gets the amount value if not none else sets it from args or prompts.\n\n        Returns\n        -------\n        int\n            The amount as an int\n\n        \"\"\"\n\n        if self.amount: return self.amount\n        # retrieve from args and return if exists\n        amount = Settings.get_amount() or None\n        if amount: \n            self.amount = amount\n            return amount\n        # prompt skip\n        if not Settings.confirm(amount): return self.get_amount()\n        self.amount = amount\n        return self.amount\n\n    def get_expiration(self):\n        \"\"\"\n        Gets the expiration value if not none else sets it from args or prompts.\n\n        Returns\n        -------\n        int\n            The expiration as an int\n\n        \"\"\"\n\n        if self.expiration: return self.expiration\n        # retrieve from args and return if exists\n        expiration = Settings.get_expiration() or None\n        if expiration: \n            self.expiration = expiration\n            return expiration\n        # prompt skip\n        # confirm expiration\n        if not Settings.confirm(expiration): return self.get_expiration()\n        self.expiration = expiration\n        return self.expiration\n\n    def get_limit(self):\n        \"\"\"\n        Gets the expiration value if not none else sets it from args or prompts.\n\n        Returns\n        -------\n        int\n            The expiration as an int\n\n        \"\"\"\n\n        if self.limit: return self.limit\n        # retrieve from args and return if exists\n        limit = Settings.get_promotion_limit() or None\n        if limit: \n            self.limit = limit\n            return limit\n        # prompt skip\n        # confirm limit\n        if not Settings.confirm(limit): return self.get_limit()\n        self.limit = limit\n        return self.limit\n\n    def get_message(self):\n        \"\"\"\n        Gets the message value if not none else sets it from args or prompts.\n\n        Returns\n        -------\n        str\n            The message as a str\n\n        \"\"\"\n\n        if self.message != None: return self.message\n        # retrieve from args and return if exists\n        message = Settings.get_text() or None\n        if message: \n            self.message = message\n            return message\n        # prompt skip\n        # confirm message\n        if not Settings.confirm(message): return self.get_text()\n        self.message = message\n        return self.message\n\n    def get_duration(self):\n        \"\"\"\n        Gets the duration value if not none else sets it from args or prompts.\n\n        Returns\n        -------\n        int\n            The duration as an int\n\n        \"\"\"\n\n        if self.duration: return self.duration\n        # retrieve from args and return if exists\n        duration = Settings.get_promo_duration() or None\n        if duration: \n            self.duration = duration\n            return duration\n        # duration skip\n        # confirm duration\n        if not Settings.confirm(duration): return self.get_duration()\n        self.duration = duration\n        return self.duration\n\n    def get_user(self):\n        \"\"\"\n        Populate and get the username value\n\n        If not found in args and prompt is enabled, ask for value.\n\n        Returns\n        -------\n        User\n            the user to apply the promotion to\n\n        \"\"\"\n\n        if self.user: return self.user\n        user = User.select_user()\n        self.user = user.username\n        return self.user\n\n    @staticmethod\n    def grandfathered():\n        \"\"\"\n        Executes the 'Grandfather' promotion model\n\n        In groups of 5, existing users will be added to the 'Grandfathered' OnlyFans list and\n        then provided with the max discount for the max months. If the process interrupts, \n        running again will continue to discount users not yet added to the list.\n\n        \"\"\"\n\n        print(\"Promotion - Grandfather\")\n        # prompt skip\n        Settings.maybe_print(\"getting users to grandfather\")\n        # get all users\n        users = User.get_all_users()\n        from .driver import Driver\n        # get all users from logged in user's 'grandfathered' list\n        users_, name, number = Driver.get_list(name=\"grandfathered\")\n        # remove all users that have already been grandfathered from the list of all users\n        # users = [user for user in users if user not in users_] # i guess doesn't work?\n        for i, user in enumerate(users[:]):\n            for user_ in users_:\n                for key, value in user_.items():\n                    if str(key) == \"username\" and str(user.username) == str(value):\n                        users.remove(user)\n\n        def chunks(lst, n):\n            \"\"\"Yield successive n-sized chunks from lst.\"\"\"\n            for i in range(0, len(lst), n):\n                yield lst[i:i + n]\n\n        # get users in groups of 5 to allow performance over interrupts\n        userChunks = chunks(users, 5)\n        num = 1\n        for userChunk in userChunks:\n            print(\"Chunk: {}/{}\".format(num, len(users)/5))\n            num += 1\n            # add users to 'grandfathered' list prior to discounting\n            Settings.maybe_print(\"grandfathering: {}\".format(len(userChunk)))\n            try:\n                successful = Driver.add_users_to_list(users=userChunk, number=number, name=\"grandfathered\")\n                # if successful then discount\n                if not successful: return\n                d = Discount() # discount will fill defaults with promotion values\n                d.grandfatherer(users=userChunk)\n            except Exception as e:\n                Settings.dev_print(e)\n        return True\n"
  },
  {
    "path": "OnlySnarf/classes/schedule.py",
    "content": "from datetime import datetime\n\nfrom ..util import defaults as DEFAULT\nfrom ..util.settings import Settings\n\nclass Schedule:\n\n    def __init__(self):\n        self._initialized_ = False\n        self.date = None\n        self.time = None\n        ##\n        self.hour = \"00\"\n        self.minute = \"00\"\n        self.year = \"0\"\n        self.month = \"0\"\n        self.day = \"0\"\n        self.suffix = \"am\"\n        ##\n        self.init()\n\n    def init(self):\n        \"\"\"Initialize the schedule's settings\"\"\"\n\n        if self._initialized_: return\n        Settings.dev_print(\"initiliazing schedule...\")\n        schedule = Settings.get_schedule()\n        date = datetime.strptime(str(schedule), DEFAULT.SCHEDULE_FORMAT)\n        self.year = date.year\n        self.month = date.strftime(\"%B\")\n        self.day = date.day\n        self.hour = date.hour\n        self.minute = date.minute\n        self.suffix = \"am\"\n        if int(self.hour) > 12:\n            self.suffix = \"pm\"\n            self.hour = int(self.hour) - 12\n        Settings.dev_print(\"year: {}\".format(self.year))\n        Settings.dev_print(\"month: {}\".format(self.month))\n        Settings.dev_print(\"day: {}\".format(self.day))\n        Settings.dev_print(\"hour: {}\".format(self.hour))\n        Settings.dev_print(\"minutes: {}\".format(self.minute))\n        Settings.dev_print(\"suffix: {}\".format(self.suffix))\n        Settings.dev_print(\"initiliazed schedule\")\n        self._initialized_ = True\n\n    def get(self):\n        \"\"\"\n        Get the schedule's values in a dict.\n\n        Returns\n        -------\n        dict\n            A dict containing the values of the schedule\n\n        \"\"\"\n\n        return dict({\n            \"date\": self.get_date(),\n            \"time\": self.get_time(),\n            \"hour\" : self.hour,\n            \"minute\" : self.minute,\n            \"year\" : self.year,\n            \"month\" : self.month,\n            \"day\" : self.day,\n            \"suffix\" : self.suffix\n        })\n\n    def get_date(self):\n        \"\"\"\n        Gets the date value if not none else sets it from args or prompts.\n\n        Returns\n        -------\n        str\n            The date as a valid date string\n\n        \"\"\"\n\n        if self.date: return self.date\n        self.date = Settings.get_date()\n        return self.date\n\n        # prompt skip\n        # confirm date\n        if not Settings.confirm(date): return self.get_date()\n        self.date = date\n        return self.date\n\n    def get_time(self):\n        \"\"\"\n        Gets the time value if not none else sets it from args or prompts.\n\n        Returns\n        -------\n        str\n            The time as a valid time string\n\n        \"\"\"\n\n        if self.time: return self.time\n        # retrieve from args and return if exists\n        self.time = Settings.get_time()\n        return self.time\n\n\n\n        # # if time: \n        # time = datetime.strptime(str(time), DEFAULT.SCHEDULE_FORMAT)\n        # # Settings.dev_print(time)\n        # time = time.strftime(\"%I:%M %p\")\n        # # Settings.dev_print(time)\n        # self.time = time\n        # return self.time\n\n        # retrieve time from schedule args and return if exists\n        schedule = Settings.get_schedule() or None\n        if schedule:\n            time = datetime.strptime(str(schedule), DEFAULT.SCHEDULE_FORMAT)\n            # Settings.dev_print(time)\n            time = time.strftime(\"%I:%M %p\")\n            # Settings.dev_print(time)\n            self.time = time\n            return self.time\n        # prompt skip\n        # confirm time\n        if not Settings.confirm(time): return self.get_time()\n        self.time = time\n        return self.time\n\n    def validate(self):\n        \"\"\"\n        Determines whether or not the schedule settings are valid.\n\n        Returns\n        -------\n        bool\n            Whether or not the schedule is valid\n\n        \"\"\"\n\n        Settings.dev_print(\"validating schedule...\")\n        today = datetime.strptime(str(datetime.now().strftime(DEFAULT.SCHEDULE_FORMAT)), DEFAULT.SCHEDULE_FORMAT)\n        # schedule = datetime.strptime(str(Settings.get_schedule().now().strftime(DEFAULT.SCHEDULE_FORMAT)), DEFAULT.SCHEDULE_FORMAT)\n        schedule = Settings.get_schedule()\n        if not schedule: return False\n        if isinstance(schedule, str):\n            schedule = datetime.strptime(schedule, DEFAULT.SCHEDULE_FORMAT)\n        # should invalidate if all default settings\n        if str(self.get_date()) == DEFAULT.DATE and (str(self.get_time()) == DEFAULT.TIME or str(self.get_time()) == DEFAULT.TIME_NONE):\n            Settings.dev_print(\"invalid schedule! (default date and time)\")\n            return False\n        # cannot post in the past\n        # TODO: possibly add margin of error if necessary\n        elif schedule <= today:\n            Settings.dev_print(\"invalid schedule! (must be in future)\")\n            return False\n        Settings.dev_print(\"valid schedule!\")\n        return True\n"
  },
  {
    "path": "OnlySnarf/classes/user.py",
    "content": "import json\nimport time\nimport os\nimport random\nimport threading\nfrom datetime import datetime, timedelta\n##\nfrom ..util.colorize import colorize\nfrom ..lib.driver import Driver\nfrom ..util.settings import Settings\n\nALREADY_RANDOMIZED_USERS = []\n\nclass User:\n    \"\"\"OnlyFans users.\"\"\"\n\n    def __init__(self, data):\n        \"\"\"User object\"\"\"\n\n        data = json.loads(json.dumps(data))\n        self.name               =   data.get('name')                            or None\n        self.username           =   str(data.get('username')).replace(\"@\",\"\")   or None\n        self.id                 =   data.get('id')                              or None\n\n        self.messages_parsed    =   data.get('messages_parsed')                 or []\n        self.messages_sent      =   data.get('messages_sent')                   or []\n        self.messages_received  =   data.get('messages_received')               or []\n        self.messages           =   data.get('messages')                        or []\n\n        self.sent_files         =   data.get('sent_files')                      or []\n        self.isFavorite         =   data.get('isFavorite')                      or False\n        # self.lists              =   data.get('lists')                           or []\n        self.start_date         =   data.get('started')                         or None\n\n        # BUG: fix empty array\n        if len(self.sent_files) > 0 and self.sent_files[0] == \"\":\n            self.sent_files = []\n\n    def toJSON(self):\n        \"\"\"\n        Dumps relevant user data to JSON.\n        \"\"\"\n\n        return json.dumps({\n            \"name\":str(self.name),\n            \"username\":str(self.username),\n            \"id\":str(self.id),\n            \"messages_parsed\":str(self.messages_parsed),\n            \"messages_sent\":str(self.messages_sent),\n            \"messages_received\":str(self.messages_received),\n            \"messages\":str(self.messages),\n            \"sent_files\":str(self.sent_files),\n            \"isFavorite\":str(self.isFavorite)\n        })\n\n    def equals(self, user):\n        \"\"\"\n        Equals comparison checks usernames and ids.\n\n        Parameters\n        ----------\n        classes.User\n            The user to compare another user object against\n        \"\"\"\n\n        if (str(user.username) != \"None\" and str(user.username) == str(self.username)) or (str(user.id) != \"None\" and str(user.id) == str(self.id)): return True\n        return False\n\n    def get_id(self):\n        \"\"\"\n        Get the provided ID of the User. Searches via username if necessary.\n\n        Returns\n        -------\n        str\n            The user id\n        \"\"\"\n\n        if self.id: return self.id\n        self.id = Driver.user_get_id(self.get_username())\n        return self.id\n\n    def get_username(self):\n        \"\"\"\n        Get the username of the User.\n\n        Returns\n        -------\n        str\n            The username\n        \"\"\"\n\n        if self.username: return self.username\n        self.username = Driver.get_username(self.get_id())\n        return self.username\n\n    def message(self, message):\n        \"\"\"\n        Message the user by their available username or id with the provided message.\n\n        Parameters\n        ----------\n        message : Object\n            The message to send as a serialized Message object from get_message.\n\n        Returns\n        -------\n        bool\n            Whether or not the message was successful\n        \"\"\"\n\n        if not self.get_username() and not self.get_id(): return Settings.err_print(\"missing user identifiers!\")\n        if self.id:\n            Settings.print(\"messaging user (id): {} ({}) - \\\"{}\\\"\".format(self.username, self.id, message[\"text\"]))\n        else:\n            Settings.print(\"messaging user: {} - \\\"{}\\\"\".format(self.username, message[\"text\"]))\n        if not Driver.message(self.username, user_id=self.id): return False\n        return self.message_send(message)\n\n    def messages_read(self):\n        \"\"\"\n        Read the chat of the user.\n        \"\"\"\n\n        Settings.print(\"reading user chat: {} ({})\".format(self.username, self.id))\n        # messages, messages_received, messages_sent = Driver.read_user_messages(self.username, user_id=self.id)\n        # self.messages = messages\n        # self.messages_received = messages_received\n        # self.messages_sent = messages_sent\n        self.messages, self.messages_received, self.messages_sent = Driver.read_user_messages(self.username, user_id=self.id)\n        # self.messages_and_timestamps = messages[1]\n        Settings.maybe_print(\"chat read!\")\n\n    def message_send(self, message):\n        \"\"\"\n        Complete the various components of sending a message to a user.\n        \n        Parameters\n        ----------\n        message : Object\n            The message to send as a serialized Message object from get_message.\n\n        Returns\n        -------\n        bool\n            Whether or not the message was successful\n        \"\"\"\n\n        Settings.print(\"entering message: (${}) {}\".format(message[\"price\"], message[\"text\"]))\n        try:\n            driver = Driver.get_driver()\n            def confirm_message(): return driver.message_confirm()\n            # enter the text of the message\n            def enter_text(text): return driver.message_text(text)\n            # enter the price to send the message to the user\n            def enter_price(price):\n                if not price: return True\n                return driver.message_price(price)\n            def enter_files(files):\n                # TODO: requires proper debugging\n                # for file in files:\n                    # enter files by filepath while checking for already sent files\n                    # file_name = file.get_title()\n                    # if str(file_name) in self.sent_files:\n                    #     Settings.warn_print(\"file already sent to user: {} <-- {}\".format(self.username, file_name))\n                    #     Settings.maybe_print(\"skipping...\")\n                    #     continue\n                    # self.sent_files.append(file_name)\n                return driver.upload_files(files)\n            if all([enter_text(message[\"text\"]), enter_price(message[\"price\"]), enter_files(message[\"files\"])]): return confirm_message()\n        except Exception as e:\n            Settings.err_print(\"message failed!\")\n            Settings.dev_print(e)\n        Settings.err_print(\"message somehow failed!\")\n        return False\n\n    def update(self, user):\n        for key, value in json.loads(user.toJSON()).items():\n            # Settings.print(\"updating: {} = {}\".format(key, value))\n            setattr(self, str(key), value)\n\n    #############\n    ## Statics ##\n    #############\n\n    # TODO: update with more accurate \"active\"ness\n    # gets users from local or refreshes from onlyfans.com\n    @staticmethod\n    def get_active_users():\n        \"\"\"\n        Get active users.\n\n        Returns\n        -------\n        list\n            The active users\n\n        \"\"\"\n\n        Settings.dev_print(\"getting active users...\")\n        active_users = []\n        for user in User.get_all_users():\n            if not User.skipUserCheck(user): continue\n            active_users.append(user)\n        Settings.maybe_print(\"active users: {}\".format(len(active_users)))\n        return active_users\n    \n    @staticmethod\n    def get_all_users():\n        \"\"\"\n        Get all users.\n\n        Returns\n        -------\n        list\n            The users\n\n        \"\"\"\n\n        Settings.dev_print(\"getting all users...\")\n        users = []\n        if Settings.is_prefer_local():\n            users = User.read_users_local()\n        if len(users) == 0:\n            for user in Driver.users_get():\n                if user is None: continue\n                users.append(User(user))\n        Settings.maybe_print(\"users: {}\".format(len(users)))\n        User.write_users_local(users=users)\n        Settings.set_prefer_local(True)\n        return users\n\n    ## TODO\n    # make this actually do something\n    @staticmethod\n    def get_favorite_users():\n        \"\"\"\n        Get all favorite users.\n\n        Returns\n        -------\n        list\n            The favorite users\n\n        \"\"\"\n\n        Settings.dev_print(\"getting favorite users...\")\n        users = []\n        for user in User.get_all_users():\n            if user.isFavorite:\n                Settings.maybe_print(\"fav user: {}\".format(user.username))\n                users.append(user)\n        return users\n\n    @staticmethod\n    def get_following():\n        \"\"\"\n        Get all following.\n\n        Returns\n        -------\n        list\n            The users being followed\n\n        \"\"\"\n\n        Settings.dev_print(\"getting following...\")\n        if Settings.is_prefer_local():\n            users = User.read_following_local()\n            if len(users) > 0: return users\n        users = []\n        for user in Driver.following_get():\n            user = User(user)\n            users.append(user)\n        Settings.maybe_print(\"following: {}\".format(len(users)))\n        User.write_following_local(users=users)\n        Settings.set_prefer_local(True)\n        return users\n\n    @staticmethod\n    def get_never_messaged_users():\n        \"\"\"\n        Get all users that have never been messaged before.\n\n        Returns\n        -------\n        list\n            The users that have not been messaged\n\n        \"\"\"\n\n        Settings.dev_print(\"getting users that have never been messaged...\")\n        users = []\n        for user in User.get_all_users():\n            if len(user.messages_received) == 0:\n                Settings.maybe_print(\"never messaged user: {}\".format(user.username))\n                users.append(user)\n        return users\n\n    @staticmethod\n    def get_new_users():\n        \"\"\"\n        Get all new users.\n\n        Returns\n        -------\n        list\n            The users that are new\n\n        \"\"\"\n\n        Settings.dev_print(\"getting new users...\")\n        newUsers = []\n        date_ = datetime.today() - timedelta(days=10)\n        for user in User.get_all_users():\n            if not user.start_date: continue\n            started = datetime.strptime(str(user.start_date),\"%b %d, %Y\")\n            # Settings.maybe_print(\"date: \"+str(date_)+\" - \"+str(started))\n            if started < date_: continue\n            Settings.maybe_print(\"new user: {}\".format(user.username))\n            newUsers.append(user)\n        return newUsers\n\n    @staticmethod\n    def get_random_user():\n        \"\"\"\n        Get a random user.\n\n        Returns\n        -------\n        classes.User\n            A random user\n\n        \"\"\"\n\n        Settings.dev_print(\"getting random user...\")\n\n        users = User.get_all_users_usernames()\n\n        randomUser = None\n        randomizedUsers = User.get_already_randomized_users()\n\n        while randomUser not in randomizedUsers:\n            randomUser = random.choice(users)\n            if randomUser not in randomizedUsers:\n                User.add_to_randomized_users(randomUser, users=randomizedUsers)\n                randomizedUsers.append(randomUser)\n\n        Settings.dev_print(\"random user: {}\".format(randomUser))\n\n        users = User.get_all_users()\n        for user in users:\n            if str(user.username) == str(randomUser):\n                return user\n        return User({\"username\":randomUser})\n\n    @staticmethod\n    def get_all_users_usernames():\n        users = User.get_all_users()\n        usernames = []\n        for user in users:\n            usernames.append(user.username)\n        return usernames\n\n    # return from json file \n    @staticmethod\n    def get_already_randomized_users():\n        Settings.dev_print(\"getting already randomized users...\")\n        users = []\n        try:\n            with open(str(Settings.get_users_path().replace(\"users.json\",\"random_users.json\"))) as json_file:  \n                for user in json.load(json_file)['randomized_users']:\n                    users.append(user)\n            Settings.maybe_print(\"loaded randomized users\")\n        except Exception as e:\n            Settings.dev_print(e)\n        return users\n\n    # add to json file\n    @staticmethod\n    def add_to_randomized_users(newUser, users=[]):\n        data = {}\n        data['randomized_users'] = []\n        for user in users:\n            data['randomized_users'].append(user)\n        data['randomized_users'].append(newUser)\n        try:\n            with open(str(Settings.get_users_path().replace(\"users.json\",\"random_users.json\")), 'w') as outfile:  \n                json.dump(data, outfile, indent=4, sort_keys=True)\n        except FileNotFoundError:\n            Settings.err_print(\"missing random users!\")\n        except OSError:\n            Settings.err_print(\"missing random users path!\")\n\n    @staticmethod\n    def get_recent_messagers():\n        \"\"\"\n        Get users that have recently sent messages.\n\n        Returns\n        -------\n        list\n            The users that have recently sent messages\n\n        \"\"\"\n        Settings.dev_print(\"getting recent users from messages...\")\n        users = []\n        for user in Driver.messages_scan():\n            users.append(User({\"id\":user}))\n        return users\n\n    ## TODO: maybe update this so it actually works?\n    @staticmethod\n    def get_recent_users():\n        \"\"\"\n        Get recent users.\n\n        Returns\n        -------\n        list\n            The recent users\n\n        \"\"\"\n        Settings.dev_print(\"getting recent users...\")\n        i = 0\n        users = []\n        for user in User.get_all_users():\n            Settings.maybe_print(\"recent user: {}\".format(user.username))\n            users.append(user)\n            i += 1\n            if i == int(Settings.get_recent_user_count()): break\n        return users\n\n    @staticmethod\n    def get_user_by_id(userid):\n        \"\"\"\n        Get user by id.\n\n        Returns\n        -------\n        int\n            The user id\n\n        \"\"\"\n        if not userid or userid == None:\n            Settings.err_print(\"missing user id\")\n            return None\n        for user in User.get_all_users():\n            if str(user.id) == \"@u\"+str(userid) or str(user.id) == \"@\"+str(userid) or str(user.id) == str(userid):\n                Settings.maybe_print(\"found user id: {}\".format(userid))\n                return user\n        Settings.err_print(\"missing user by user id - {}\".format(userid))\n        return None\n\n    @staticmethod\n    def get_user_by_username(username):\n        \"\"\"\n        Get user by username.\n\n        Returns\n        -------\n        classes.User\n            The user with the provided username\n\n        \"\"\"\n        if not username or str(username) == \"None\":\n            Settings.err_print(\"missing username!\")\n            return None\n        for user in User.get_all_users():\n            if str(user.username) == \"@u\"+str(username) or str(user.username) == \"@\"+str(username) or str(user.username) == str(username):\n                Settings.maybe_print(\"found username: {}\".format(username))\n                return user\n        Settings.err_print(\"missing user by username - {}\".format(username))\n        return None\n\n    @staticmethod\n    def get_users_by_list(number=None, name=None, ):\n        \"\"\"\n        Get users by custom list.\n\n        Returns\n        -------\n        list\n            The users on the list\n\n        \"\"\"\n        Settings.maybe_print(\"getting users by list: {} - {}\".format(number, name))\n        listUsers = []\n        for user in Driver.get_list(number=number, name=name):\n            Settings.maybe_print(\"user: {}\".format(user.username))\n            listUsers.append(user)\n        return listUsers\n\n    @staticmethod\n    def message_user(message, username, user_id=None):\n\n        \"\"\"\n        Message the user by their available username or id with the provided message data.\n\n        Parameters\n        ----------\n        message : Object\n            The message to send as a serialized Message object from get_message.\n        \"\"\"\n\n        if str(username).lower() == \"random\":\n            return User.get_random_user().message(message)\n        else:\n            return User({\"username\":username,\"id\":user_id}).message(message)\n\n    @staticmethod\n    def read_following_local():\n        \"\"\"\n        Read the locally saved following file.\n\n        Returns\n        -------\n        list\n            The locally saved followers\n\n        \"\"\"\n        Settings.dev_print(\"getting local following...\")\n        users = []\n        try:\n            with open(str(Settings.get_users_path().replace(\"users.json\", \"following.json\"))) as json_file:  \n                for user in json.load(json_file)['users']:\n                    users.append(User(json.loads(user)))\n            Settings.maybe_print(\"loaded local following\")\n        except Exception as e:\n            Settings.dev_print(e)\n        return users\n\n    @staticmethod\n    def read_users_local():\n        \"\"\"\n        Read the locally saved users file.\n\n        Returns\n        -------\n        list\n            The locally saved users\n\n        \"\"\"\n        Settings.dev_print(\"getting local users...\")\n        users = []\n        try:\n            with open(str(Settings.get_users_path())) as json_file:  \n                for user in json.load(json_file)['users']:\n                    users.append(User(json.loads(user)))\n            Settings.maybe_print(\"loaded local users\")\n        except Exception as e:\n            Settings.dev_print(e)\n        return users\n\n    @staticmethod\n    def read_users_messages(users=[]):\n        \"\"\"\n        Read all the users messages.\n\n        Parameters\n        ----------\n        classes.User\n            A list of users to read the messages of.\n\n        \"\"\"\n\n        if len(users) == 0: users = User.get_all_users()\n        Settings.print(\"updating chat logs: {}\".format(len(users)))\n        for user in users: user.messages_read()\n        # User.write_users_local(users=users)\n        return users\n \n    @staticmethod\n    def skipUserCheck(user):\n        \"\"\"\n        Skip user if meets flags.\n\n        Returns\n        -------\n        classes.User\n            The same user provided (if not skipped)\n\n        \"\"\"\n        if str(user.id).lower() in Settings.get_skipped_users() or str(user.username).lower() in Settings.get_skipped_users():\n            Settings.maybe_print(\"skipping: {}\".format(user.username))\n            return None\n        return user\n\n    @staticmethod\n    def write_users_local(users=None):\n        \"\"\"\n        Write to local users file.\n\n        \"\"\"\n        if users is None:\n            users = User.get_all_users()\n        if len(users) == 0:\n            Settings.maybe_print(\"skipping: local users save - empty\")\n            return\n        Settings.print(\"saving users...\")\n        Settings.dev_print(\"local users path: \"+str(Settings.get_users_path()))\n        # merge with existing user data\n        existingUsers = User.read_users_local()\n        for user in users:\n            for u in existingUsers:\n                if user.equals(u):\n                    user.update(u)\n        data = {}\n        data['users'] = []\n        for user in users:\n            data['users'].append(user.toJSON())\n        try:\n            with open(str(Settings.get_users_path()), 'w') as outfile:  \n                json.dump(data, outfile, indent=4, sort_keys=True)\n        except FileNotFoundError:\n            Settings.err_print(\"missing local users!\")\n        except OSError:\n            Settings.err_print(\"missing local path!\")\n\n    @staticmethod\n    def write_following_local(users=None):\n        \"\"\"\n        Write to local followers file.\n\n        \"\"\"\n        if users is None:\n            users = User.get_following()\n        if len(users) == 0:\n            Settings.maybe_print(\"skipping: local following save - empty following\")\n            return\n        Settings.print(\"saving following...\")\n        Settings.dev_print(\"local users path: \"+str(Settings.get_users_path().replace(\"users.json\", \"following.json\")))\n        data = {}\n        data['users'] = []\n        for user in users:\n            data['users'].append(user.toJSON())\n        try:\n            with open(str(Settings.get_users_path().replace(\"users.json\", \"following.json\")), 'w') as outfile:  \n                json.dump(data, outfile, indent=4, sort_keys=True)\n        except FileNotFoundError:\n            Settings.err_print(\"missing local following\")\n        except OSError:\n            Settings.err_print(\"missing local path\")"
  },
  {
    "path": "OnlySnarf/conf/config.conf",
    "content": "[ARGS]\n\n## General ##\n# auto, onlyfans, or twitter\n#login = auto\n#prefer_local = True\n#save_users = False\n#upload_max = 10\n#upload_max_duration = 60 \n\n## OnlySnarf ##\n#amount = 0\n#months = 0\n#price = \n#date = \n#duration = 0\n#expiration = 0 \n#questions = \n#schedule = \n#tags = \n#text = \n#time = \n#tweeting = False\n#user =  \n#users =  \n## fetches credentials from user config with provided username\n#username = \n#phone =\n\n## Selenium ##\n# auto, brave, chrome, chromium, firefox; reconnect[-firefox, chrome, etc], remote[-firefox, chrome, etc]\n#browser = auto\n#cookies = True\n#keep = False\n#session_id = \n#session_url = \n## show browser window\n#show = False\n\n## Debugging ##\n#debug = False\n#debug_cookies = False\n#debug_delay = False\n#debug_firefox = False\n#debug_google = False\n#debug_selenium = False\n#force_upload = False\n#recent_users_count = 10\n#skip_upload = False\n#skipped_users = \n#users_read = 10\n#reduce = False\n#verbose = 1\n\n[PATH]\n#mount = $HOME/onlysnarf\n#download = $HOME/onlysnarf/downloads \n#profile = $HOME/onlysnarf/profiles/$username\n#users = $HOME/.onlysnarf/users.json\n\n[POSTS]\n#welcome = \"Welcome to my OnlyFans!\"\n#ask = \"What would you like to see me post more of? Comment below!\"\n#content_request = \"Are there any specific content requests?\"\n#giggle = \"teehee\"\n#no_customs = \"I don\\'t do customs, sorry!\"\n#social_media = \"Be sure to check out my social medias!\"\n#thank_you = \"Thank you all for your support!\"\n#reminder = \"Reminder that you\\'re awesome for being a subscriber!\"\n#commands = \"OnlySnarf Bot Commands:\\n\\n!pic | !pic dick | !pic ass\"\n#more_soon = \"Thanks for being followers! More coming soon!\"\n#customs = \"I am currently accepting customs! Have a request? Message me directly! Prices vary :)\"\n#messages_tip = \"I am 100%% more likely to notice your message and respond with a dick pic if you\\'ve randomly tipped me\"\n#performers = \"Be sure to comment below or message me directly which other performers you'd like to see me with most!\"\n#requests = \"Be sure to message me w/ any requests for pics! Feet / Dick / etc. $5 for any quick pic, $10 if it requires setup\"\n#dick_pics = \"Message me any particular dick pics you\\'d like me to send! Tip for tip ;)\"\n\n[REMOTE]\n\n## Selenium ##\n#browser_host = \n#browser_port = 4444"
  },
  {
    "path": "OnlySnarf/conf/test-config.conf",
    "content": "[ARGS]\n\n## General ##\n# auto, onlyfans, or twitter\nlogin = auto\nprefer_local = True\nsave_users = False\nupload_max = 10\nupload_max_duration = 60 \n\n## OnlySnarf ##\namount = 0\nmonths = 0\nprice = 0\ndate = None\nduration = 0\nexpiration = 0 \nquestions = []\nschedule = None\ntags = []\ntext = None\ntime = None\ntweeting = False\nuser = None\nusers = None\n## fetches credentials from user config with provided username\n#username = alexdicksdown\n#phone = \n\n## Selenium ##\n# auto, brave, chrome, chromium, firefox, ie, edge, opera; reconnect[-firefox, chrome, etc], remote[-firefox, chrome, etc]\nbrowser = auto\ncookies = True\nkeep = True\nsession_id = None\nsession_url = None\n## show browser window\nshow = True\n\n## Debugging ##\ndebug = True\ndebug_cookies = False\ndebug_delay = True\ndebug_firefox = False\ndebug_google = False\ndebug_selenium = False\nforce_upload = False\nrecent_users_count = 10\nskip_download = False\nskip_upload = False\nskipped_users = \nusers_read = 10\nreduce = False\nverbose = 3\n\n[PATH]\n#mount = $HOME/onlysnarf\n#download = $HOME/onlysnarf/downloads \n#profile = $HOME/onlysnarf/profiles/$username\n#users = $HOME/.onlysnarf/users.json\n\n[POSTS]\n## these were all ideas that were never used, fyi\n#welcome = \"Welcome to my OnlyFans!\"\n#ask = \"What would you like to see me post more of? Comment below!\"\n#content_request = \"Are there any specific content requests?\"\n#giggle = \"teehee\"\n#no_customs = \"I don\\'t do customs, sorry!\"\n#social_media = \"Be sure to check out my social medias!\"\n#thank_you = \"Thank you all for your support!\"\n#reminder = \"Reminder that you\\'re awesome for being a subscriber!\"\n#commands = \"OnlySnarf Bot Commands:\\n\\n!pic | !pic dick | !pic ass\"\n#more_soon = \"Thanks for being followers! More coming soon!\"\n#customs = \"I am currently accepting customs! Have a request? Message me directly! Prices vary :)\"\n#messages_tip = \"I am 100%% more likely to notice your message and respond with a dick pic if you\\'ve randomly tipped me\"\n#performers = \"Be sure to comment below or message me directly which other performers you'd like to see me with most!\"\n#requests = \"Be sure to message me w/ any requests for pics! Feet / Dick / etc. $5 for any quick pic, $10 if it requires setup\"\n#dick_pics = \"Message me any particular dick pics you\\'d like me to send! Tip for tip ;)\"\n\n[REMOTE]\n\n## Selenium ##\n#browser_host = \n#browser_port = 4444"
  },
  {
    "path": "OnlySnarf/conf/users/example-user.conf",
    "content": "[ONLYFANS]\nusername = $USERNAME\npassword = $PASSWORD\n\n# not working\n[GOOGLE]\nusername = $UGOOGLE\npassword = $PGOOGLE\n\n[TWITTER]\nusername = $UTWITTER\npassword = $PTWITTER"
  },
  {
    "path": "OnlySnarf/elements/__init__.py",
    "content": ""
  },
  {
    "path": "OnlySnarf/elements/driver.py",
    "content": "# general driver elements\n\nELEMENTS = [\n\n    {\n        \"name\": \"sendButton\",\n        \"classes\": [\"g-btn.m-rounded\"],\n        \"text\": [],\n        \"id\": []\n    },\n    \n    {\n        \"name\": \"enterText\",\n        \"classes\": [],\n        \"text\": [],\n        \"id\": [\"new_post_text_input\"]\n    },\n\n    {\n        \"name\": \"enterMessage\",\n        \"classes\": [\"b-chat__message__text\"],\n        \"text\": [],\n        \"id\": []\n    },\n\n    {\n        \"name\": \"discountUserPromotion\",\n        \"classes\": [\"g-btn.m-rounded.m-border.m-sm\"],\n        \"text\": [],\n        \"id\": []\n    },\n\n    {\n        \"name\": \"tweet\",\n        \"xpath\": [\"//label[@for='new_post_tweet_send']\"],\n        \"classes\": [],\n        \"text\": [],\n        \"id\": []\n    },\n\n    {\n        \"name\": \"pollInput\",\n        \"xpath\": [\"//input[@class='form-control']\"],\n        \"classes\": [],\n        \"text\": [],\n        \"id\": []\n    },\n\n    {\n        \"name\": \"new_message\",\n        \"classes\": [\"g-btn.m-rounded.b-chat__btn-submit\"],\n        \"text\": [\"Send\"],\n        \"id\": []\n    },\n\n    ### upload\n    # send\n    {\n        \"name\": \"new_post\",\n        \"classes\": [\"g-btn.m-rounded\", \"button.g-btn.m-rounded\"],\n        \"text\": [\"Post\"],\n        \"id\": []\n    },\n\n    # record voice\n    {\n        \"name\": \"recordVoice\",\n        \"classes\": [None],\n        \"text\": [],\n        \"id\": []\n    },\n    # post price\n    {\n        \"name\": \"post_price\",\n        \"classes\": [None],\n        \"text\": [],\n        \"id\": []\n    },\n    # post price cancel\n    {\n        \"name\": \"post_price_cancel\",\n        \"classes\": [None],\n        \"text\": [],\n        \"id\": []\n    },\n    # post price save\n    {\n        \"name\": \"post_price_save\",\n        \"classes\": [None],\n        \"text\": [],\n        \"id\": []\n    },\n    # go live\n    {\n        \"name\": \"go_live\",\n        \"classes\": [None],\n        \"text\": [],\n        \"id\": []\n    },\n    # upload image file\n    {\n        \"name\": \"image_upload\",\n        \"classes\": [\"attach_file\"],\n        \"text\": [],\n        \"id\": [\"attach_file_photo\"]\n    },\n    # show more options # unnecessary w/ tabbing\n    {\n        \"name\": \"moreOptions\",\n        \"classes\": [\"button.g-btn.m-flat.b-make-post__more-btn\"],\n        \"text\": [],\n        \"id\": []\n    },\n    \n    # poll\n    {\n        \"name\": \"poll\",\n        \"classes\": [\"g-btn.m-flat.m-gray.m-with-round-hover.m-size-md-hover.m-default-icon-size.m-reset-width.has-tooltip\", \"g-btn.m-flat.b-make-post__voting-btn\", \"g-btn.m-flat.b-make-post__voting-btn.has-tooltip\", \"button.g-btn.m-flat.b-make-post__voting-btn\", \"button.g-btn.m-flat.b-make-post__voting-btn.has-tooltip\"],\n        \"text\": [],\n        \"id\": [\"icon-quiz\"]\n    },\n    # expire add\n    {\n        \"name\": \"expiresAdd\",\n        \"classes\": [\"b-make-post__expire-period-btn\"],\n        \"text\": [\"Save\"],\n        \"id\": []\n    },\n    {\n        \"name\": \"expiresPeriods\",\n        \"classes\": [\"b-tabs__nav__text\", \"b-make-post__expire__label\"],\n        \"text\": [],\n        \"id\": []\n    },\n    {\n        \"name\": \"listSingleSave\",\n        \"classes\": [\"g-btn.m-transparent-bg\"],\n        \"text\": [\"Close\"],\n        \"id\": []\n    },\n    {\n        \"name\": \"expiresSave\",\n        \"classes\": [\"g-btn.m-transparent-bg\", \"g-btn.m-rounded\"],\n        \"text\": [\"Save\"],\n        \"id\": []\n    },\n    {\n        \"name\": \"expiresCancel\",\n        \"classes\": [\"g-btn.m-flat.m-btn-gaps.m-reset-width\", \"g-btn.m-transparent-bg\", \"g-btn.m-rounded.m-border\"],\n        \"text\": [\"Cancel\"],\n        \"id\": []\n    },\n    # poll cancel\n    {\n        \"name\": \"pollCancel\",\n        \"classes\": [\"b-dropzone__preview__delete\"],\n        \"text\": [\"Cancel\"],\n        \"id\": []\n    },\n    # poll duration\n    {\n        \"name\": \"pollDuration\",\n        \"classes\": [\"g-btn.m-flat.b-make-post__voting__duration\", \"button.g-btn.m-flat.b-make-post__voting__duration\", \"g-btn.m-rounded.js-make-post-poll-duration-save\", \"button.g-btn.m-rounded.js-make-post-poll-duration-save\"],\n        \"text\": [],\n        \"id\": []\n    },\n    # duration tabs\n    {\n        \"name\": \"pollDurations\",\n        \"classes\": [\"b-make-post__expire__label\"],\n        \"text\": [],\n        \"id\": []\n    },\n    # poll save duration\n    {\n        \"name\": \"pollSave\",\n        \"classes\": [\"g-btn.m-flat.m-btn-gaps.m-reset-width\"],\n        \"text\": [\"Save\"],\n        \"id\": []\n    },\n    # poll add question\n    {\n        \"name\": \"pollQuestionAdd\",\n        \"classes\": [\"g-btn.m-flat.new_vote_add_option\", \"button.g-btn.m-flat.new_vote_add_option\"],\n        \"text\": [],\n        \"id\": []\n    },\n\n    # expiration\n    {\n        \"name\": \"expirationAdd\",\n        \"classes\": [\"g-btn.m-flat.b-make-post__expire-period-btn\", \"button.g-btn.m-flat.b-make-post__expire-period-btn\"],\n        \"text\": [],\n        \"id\": []\n    },\n    # expiration periods (same for duration)\n    {\n        \"name\": \"expirationPeriods\",\n        \"classes\": [\"b-make-post__expire__label\", \"button.b-make-post__expire__label\"],\n        \"text\": [],\n        \"id\": []\n    },\n    # expiration save\n    {\n        \"name\": \"expirationSave\",\n        \"classes\": [\"g-btn.m-rounded\", \"button.g-btn.m-rounded\", \"button.g-btn.m-rounded.js-make-post-poll-duration-save\", \"g-btn.m-rounded.js-make-post-poll-duration-save\"],\n        \"text\": [\"Save\"],\n        \"id\": []\n    },\n    # expiration cancel\n    {\n        \"name\": \"expirationCancel\",\n        \"classes\": [\"g-btn.m-rounded.m-border\", \"button.g-btn.m-rounded.m-border\"],\n        \"text\": [\"Cancel\"],\n        \"id\": []\n    },\n    \n    {\n        \"name\": \"listSave\",\n        \"classes\": [\"g-btn.m-rounded.m-sm-width\"],\n        \"text\": [\"Add\"],\n        \"id\": []\n    },\n\n    ## price\n    # price add\n    {\n        \"name\": \"priceClick\",\n        \"classes\": [\"g-btn.m-flat.has-tooltip\", \"g-btn.m-flat.m-gray.m-with-round-hover.m-size-md-hover.m-default-icon-size.m-reset-width\"],\n        \"text\": [],\n        \"id\": []\n    },\n    {\n        \"name\": \"priceSave\",\n        \"classes\": [\"g-btn.m-flat.m-btn-gaps.m-reset-width\", \"g-btn.m-transparent-bg\", \"g-btn.m-rounded\"], # \"b-chat__btn-set-price\", \"button.g-btn.m-rounded\"\n        \"text\": [\"Save\"],\n        \"id\": []\n    },\n    # price enter (adds .00)\n    {\n        \"name\": \"priceEnter\",\n        \"classes\": [\"form-control.g-input\", \".form-control.g-input\", \"input.form-control.g-input\", \"input.form-control.g-input\"],\n        \"text\": [\"Free\"],\n        \"id\": []\n    },\n\n    # schedule add\n    {\n        \"name\": \"scheduleAdd\",\n        \"classes\": [\"g-btn.m-flat.b-make-post__datepicker-btn\", \"button.g-btn.m-flat.b-make-post__datepicker-btn\"],\n        \"text\": [],\n        \"id\": []\n    },\n    # schedule next month\n    {\n        \"name\": \"scheduleNextMonth\",\n        \"classes\": [\"vdatetime-calendar__navigation--next\", \"button.vdatetime-calendar__navigation--next\"],\n        \"text\": [],\n        \"id\": []\n    },\n    # schedule date\n    {\n        \"name\": \"scheduleDate\",\n        \"classes\": [\"vdatetime-calendar__current--month\", \"div.vdatetime-calendar__navigation > div.vdatetime-calendar__current--month\", \".vdatetime-calendar__current--month\", \"div.vdatetime-calendar__current--month\", \"vdatetime-popup__date\", \"div.vdatetime-popup__date\"],\n        \"text\": [],\n        \"id\": []\n    },\n    # schedule minutes\n    {\n        \"name\": \"scheduleMinutes\",\n        \"classes\": [\"vdatetime-time-picker__item\", \"button.vdatetime-time-picker__item\", \"vdatetime-time-picker__item.vdatetime-time-picker__item--selected\"],\n        \"text\": [],\n        \"id\": []\n    },\n    # schedule hours\n    {\n        \"name\": \"scheduleHours\",  \n        \"classes\": [\"vdatetime-time-picker__list--hours\", \"vdatetime-time-picker__item.vdatetime-time-picker__item\", \"button.vdatetime-time-picker__item.vdatetime-time-picker__item\"],\n        \"text\": [],\n        \"id\": []\n    },\n    # schedule days\n    {\n        \"name\": \"scheduleDays\",\n        \"classes\": [\"vdatetime-calendar__month__day\", \"button.vdatetime-calendar__month__day\"],\n        \"text\": [],\n        \"id\": []\n    },\n    # schedule next\n    {\n        \"name\": \"scheduleNext\",\n        \"classes\": [\"g-btn.m-flat.m-reset-width.m-btn-gaps\", \"g-btn.m-transparent-bg\", \"g-btn.m-transparent-bg.m-no-uppercase\", \"g-btn.m-rounded\", \"button.g-btn.m-rounded\"],\n        \"text\": [\"Next\"],\n        \"id\": []\n    },\n    # schedule save\n    {\n        \"name\": \"scheduleSave\",\n        \"classes\": [\"g-btn.m-transparent-bg\", \"g-btn.m-rounded\", \"button.g-btn.m-rounded\"],\n        \"text\": [\"OK\"],\n        \"id\": []\n    },\n    # schedule cancel\n    {\n        \"name\": \"scheduleCancel\",\n        \"classes\": [\"g-btn.m-transparent-bg\", \"custom-datepicker-button-cancel\", \"button.g-btn.m-rounded\"],\n        \"text\": [\"Cancel\"],\n        \"id\": []\n    },\n    # schedule am/pm\n    {\n        \"name\": \"scheduleAMPM\",\n        \"classes\": [\"vdatetime-time-picker__item.vdatetime-time-picker__item--selected\"],\n        \"text\": [],\n        \"id\": []\n    },\n\n    ### message\n    # message enter text\n    {\n        \"name\": \"messageText\",\n        \"classes\": [\"form-control.b-make-post__text-input\", \".form-control.b-chat__message-input\"],\n        \"text\": [],\n        \"id\": []\n    },\n    # message upload image\n    {\n        \"name\": \"uploadMessageConfirm\",\n        \"classes\": [\"g-btn.m-rounded.b-chat__btn-submit\"],\n        \"text\": [],\n        \"id\": [\"fileupload_photo\"]\n    },\n\n    # upload error window close\n    # tab probably closes error windows...\n    {\n        \"name\": \"errorUpload\",\n        \"classes\": [\"g-btn.m-flat.m-btn-gaps.m-reset-width\", \"g-btn.m-transparent-bg\", \"g-btn.m-rounded.m-border\", \"button.g-btn.m-rounded.m-border\"],\n        \"text\": [\"Close\"],\n        \"id\": []\n    },\n    # messages all\n    {\n        \"name\": \"messagesAll\",\n        \"classes\": [\"b-chat__message__text\"],\n        \"text\": [],\n        \"id\": []\n    },\n    # messages from user\n    {\n        \"name\": \"messagesFrom\",\n        \"classes\": [\"m-from-me\",\"b-chat__message.m-from-me\"],\n        \"text\": [],\n        \"id\": []\n    },\n\n    ## Users\n    {\n        \"name\": \"usersUsernames\",\n        \"classes\": [\"g-user-username\"],\n        \"text\": [],\n        \"id\": []\n    },\n    # users\n    {\n        \"name\": \"usersUsers\",\n        \"classes\": [\"g-user-name__wrapper\", \"b-username\"],\n        \"text\": [],\n        \"id\": [\"profileUrl\"]\n    },\n    # users started dates\n    {\n        \"name\": \"usersStarteds\",\n        \"classes\": [\"b-fans__item__list__item\"],\n        \"text\": [],\n        \"id\": []\n    },\n    # users ids\n    {\n        \"name\": \"usersIds\",\n        \"classes\": [\"a.g-btn.m-rounded.m-border.m-sm\", \"a.g-button.m-rounded.m-border.m-profile.m-with-icon.m-message-btn\"],\n        \"text\": [],\n        \"id\": []\n    },\n    # users count\n    {\n        \"name\": \"usersCount\",\n        \"classes\": [\"l-sidebar__user-data__item__count\", \"b-tabs__nav__item.m-current\"],\n        \"text\": [],\n        \"id\": []\n    },\n    {\n        \"name\": \"followingCount\",\n        \"classes\": [\"b-tabs__nav__item.m-current\"],\n        \"text\": [],\n        \"id\": []\n    },\n    # users discount buttons\n    {\n        \"name\": \"discountUserButtons\",\n        \"classes\": [\"g-btn.m-rounded.m-border.m-sm\"],\n        \"text\": [],\n        \"id\": []\n    },\n    {\n        \"name\": \"newMessage\",\n        \"classes\": [\"g-page__header__btn.b-chats__btn-new.has-tooltip\"],\n        \"text\": [],\n        \"href\": [\"/my/chats/send\"],\n        \"id\": [],\n    },\n    {\n        \"name\": \"messageAll\",\n        \"classes\": [\"g-btn__text\"],\n        \"text\": [\"Fans\"],\n        \"id\": [],\n    },\n    {\n        \"name\": \"messageRecent\",\n        \"classes\": [\"g-btn__text\"],\n        \"text\": [\"Recent\"],\n        \"id\": [],\n    },\n    {\n        \"name\": \"messageFavorite\",\n        \"classes\": [\"g-btn__text\"],\n        \"text\": [\"FAVORITE\"],\n        \"id\": [],\n    },\n    {\n        \"name\": \"messageRenewers\",\n        \"classes\": [\"g-btn__text\"],\n        \"text\": [\"Renew\"],\n        \"id\": [],\n    },\n    {\n        \"name\": \"promotionalTrial\",\n        \"classes\": [\"g-btn.m-rounded.m-lg.m-flex.m-with-icon.m-uppercase\"],\n        \"text\": [\"create new free trial link\"],\n        \"id\": [],\n    },\n    {\n        \"name\": \"promotionalCampaignAmount\",\n        \"classes\": [\"form-control.b-fans__trial__select\"],\n        \"text\": [\"promo-campaign-discount-percent-select\"],\n        \"id\": [],\n    },\n    {\n        \"name\": \"promotionalCampaign\",\n        \"classes\": [\"g-btn.m-rounded.m-block.m-uppercase\"],\n        \"text\": [\" Add a promotional campaign \"],\n        \"id\": [],\n    },\n    {\n        \"name\": \"promotionalTrialShow\",\n        \"classes\": [\"g-box__header.m-icon-title.m-gray-bg\"],\n        \"text\": [\"Free trial links\"],\n        \"id\": [],\n    },\n    {\n        \"name\": \"promotionalCopy\",\n        \"classes\": [\"g-btn.m-rounded.m-uppercase\"],\n        \"text\": [\"Copy link to profile\"],\n        \"id\": [],\n    },\n    {\n        \"name\": \"promotionalTrialCount\",\n        \"classes\": [\"form-control.b-fans__trial__select\"],\n        \"text\": [],\n        \"id\": [\"trial-count-select\"],\n    },\n    {\n        \"name\": \"promotionalTrialExpiration\",\n        \"classes\": [],\n        \"text\": [],\n        \"id\": [\"trial-expiration-select\"],\n    },\n    {\n        \"name\": \"promotionalTrialMessage\",\n        \"classes\": [\"form-control.g-input\"],\n        \"text\": [\"Type a message to users (optional)\"],\n        \"id\": [],\n    },\n    {\n        \"name\": \"promotionalTrialDuration\",\n        \"classes\": [],\n        \"text\": [],\n        \"id\": [\"promo-campaign-period-select\"],\n    },\n    {\n        \"name\": \"promotionalTrialConfirm\",\n        \"classes\": [\"g-btn.m-rounded\"],\n        \"text\": [\"Create\"],\n        \"id\": [],\n    },\n    {\n        \"name\": \"promotionalTrialCancel\",\n        \"classes\": [\"g-btn.m-rounded.m-border\"],\n        \"text\": [\"Cancel\"],\n        \"id\": [],\n    },\n    {\n        \"name\": \"promotionalTrialLink\",\n        \"classes\": [\"g.btn.m-rounded\"],\n        \"text\": [\"Copy trial link\"],\n        \"id\": [],\n    },\n\n\n    {\n        \"name\": \"postCancel\",\n        \"classes\": [\"m-btn-clear-draft.g-btn.m-border.m-rounded.m-sm-width.m-reset-width\"],\n        \"text\": [\"Clear\"],\n        \"id\": [],\n    },\n\n\n    {\n        \"name\": \"userOptions\",\n        \"classes\": [\"btn.dropdown-toggle.btn-link\"],\n        \"text\": [],\n        \"id\": [\"__BVID__56__BV_toggle_\"],\n    },\n\n    # save discount for user\n    {\n        \"name\": \"discountUserButton\",\n        \"classes\": [\"g-btn.m-flat.m-btn-gaps.m-reset-width\", \"g-btn.m-rounded\"],\n        \"text\": [\"Apply\"],\n        \"id\": []\n    },\n    # discount save for user\n    # {\n    #     \"name\": \"discountUsers\",\n    #     \"classes\": [\"b-users__item.m-fans\"],\n    #     \"text\": [\"Save\"],\n    #     \"id\": []\n    # },\n\n    {\n        \"name\": \"discountUser\",\n        \"classes\": [\"b-tabs__nav__text\"],\n        \"text\": [\"Discount\"],\n        \"id\": [],\n    },\n\n    {\n        \"name\": \"discountUserAmount\",\n        \"classes\": [\"v-select__selection.v-select__selection--comma\"],\n        \"text\": [\"% discount\"],\n        \"id\": [],\n    },\n\n    {\n        \"name\": \"discountUserMonths\",\n        \"classes\": [\"v-select__selection.v-select__selection--comma\"],\n        \"text\": [\" month\"],\n        \"id\": [],\n    },\n\n    {\n        \"name\": \"promotionalTrialExpirationUser\",\n        \"classes\": [],\n        \"text\": [],\n        \"id\": [\"trial-expire-select\"],\n    },\n    {\n        \"name\": \"promotionalTrialDurationUser\",\n        \"classes\": [],\n        \"text\": [],\n        \"id\": [\"trial-period-select\"],\n    },\n    {\n        \"name\": \"promotionalTrialMessageUser\",\n        \"classes\": [\"form-control.g-input\"],\n        \"text\": [],\n        \"id\": [],\n    },\n    {\n        \"name\": \"promotionalTrialApply\",\n        \"classes\": [\"g-btn.m-rounded\"],\n        \"text\": [\"Apply\"],\n        \"id\": [],\n    },\n    {\n        \"name\": \"promotionalTrialCancel\",\n        \"classes\": [\"g-btn.m-rounded.m-border\"],\n        \"text\": [\"Cancel\"],\n        \"id\": [],\n    },\n    {\n        \"name\": \"numberOfPosts\",\n        \"classes\": [\"b-profile__actions__count\"],\n        \"text\": [],\n        \"id\": [],\n    }\n\n\n]"
  },
  {
    "path": "OnlySnarf/elements/login.py",
    "content": "##\n# unused, from Driver\n##\n# LOGIN_FORM = \"b-loginreg__form\"\n# TWITTER_LOGIN0 = \"//a[@class='g-btn m-rounded m-flex m-lg']\"\n# TWITTER_LOGIN1 = \"//a[@class='g-btn m-rounded m-flex m-lg btn-twitter']\"\n# TWITTER_LOGIN2 = \"//a[@class='btn btn-default btn-block btn-lg btn-twitter']\"\n# TWITTER_LOGIN3 = \"//a[@class='g-btn m-rounded m-flex m-lg m-with-icon']\"\n# USERNAME_XPATH = \"//input[@id='username_or_email']\"\n# PASSWORD_XPATH = \"//input[@id='password']\"\n# SEND_BUTTON_XPATH = \"//button[@type='submit' and @class='g-btn m-rounded']\"\n# SEND_BUTTON_CLASS2 = \"button.g-btn.m-rounded\"\n# LIVE_BUTTON_CLASS = \"b-make-post__streaming-link\"\n# DISCOUNT_INPUT = \"form-control.b-fans__trial__select-wrapper\"\n# ONLYFANS_PRICE2 = \"button.b-chat__btn-set-price\"\n\nELEMENTS = [\n\n    ### login\n    {\n        \"name\": \"login\",\n        \"classes\": [],\n        \"text\": [],\n        \"id\": []\n    },\n\n    # username\n    {\n        \"name\": \"loginUsername\",\n        \"classes\": [],\n        \"text\": [],\n        \"id\": []\n    },\n\n    # password\n    {\n        \"name\": \"loginPassword\",\n        \"classes\": [],\n        \"text\": [],\n        \"id\": []\n    },\n\n    {\n        \"name\": \"loginCheck\",\n        \"classes\": [\"b-make-post__streaming-link\"],\n        \"text\": [],\n        \"id\": []\n    },\n\n    {\n        \"name\": \"rememberMe\",\n        \"xpath\": [\"//input[@id='remember']\"],\n        \"classes\": [],\n        \"text\": [],\n        \"id\": []\n    }\n]"
  },
  {
    "path": "OnlySnarf/elements/profile.py",
    "content": "# profile settings elements\n\nELEMENTS = [\n    ## Settings ##\n\n    ## Account\n    # cover image enter\n    {\n        \"name\": \"coverImage\",\n        \"classes\": [\"g-btn.m-rounded.m-sm.m-border\"],\n        \"text\": [\"Upload cover image\"],\n        \"id\": []\n    },\n    # cover image cancel button\n    {\n        \"name\": \"coverImageCancel\",\n        \"classes\": [\"b-user-panel__del-btn.m-cover\"],\n        \"text\": [],\n        \"id\": []\n    },\n    # profile photo\n    {\n        \"name\": \"profilePhoto\",\n        \"classes\": [\"g-btn.m-rounded.m-sm.m-border\"],\n        \"text\": [\"Upload profile photo\"],\n        \"id\": []\n    },\n    # profile photo cancel button\n    {\n        \"name\": \"profilePhotoCancel\",\n        \"classes\": [\"b-user-panel__del-btn.m-avatar\"],\n        \"text\": [],\n        \"id\": []\n    },\n    # username\n    {\n        \"name\": \"username\",\n        \"classes\": [],\n        \"text\": [],\n        \"id\": [\"input-login\"]\n    },\n    # display name\n    {\n        \"name\": \"displayName\",\n        \"classes\": [],\n        \"text\": [],\n        \"id\": [\"input-name\"]\n    },\n    # subscription price\n    {\n        \"name\": \"subscriptionPrice\",\n        \"classes\": [\"form-control.g-input\"],\n        \"text\": [\"Free\"],\n        \"id\": []\n    },\n    # subscription bundle\n    # TODO\n    {\n        \"name\": \"subscriptionBundle\",\n        \"classes\": [None],\n        \"text\": [],\n        \"id\": []\n    },\n    # referral award enabled / disabled\n    # TODO\n    {\n        \"name\": \"referralReward\",\n        \"classes\": [None],\n        \"text\": [],\n        \"id\": []\n    },\n\n    # ADD reward for subscriber referrals\n    # about\n    {\n        \"name\": \"about\",\n        \"classes\": [],\n        \"text\": [],\n        \"id\": [\"input-about\"]\n    },\n    # location\n    {\n        \"name\": \"location\",\n        \"classes\": [],\n        \"text\": [],\n        \"id\": [\"input-location\"]\n    },\n    # website url\n    {\n        \"name\": \"websiteURL\",\n        \"classes\": [],\n        \"text\": [],\n        \"id\": [\"input-website\"]\n    },\n\n    ## Advanced\n    # username\n    # BLANK\n    # username\n    # {\n    #     \"name\": \"username\",\n    #     \"classes\": [\"form-control.g-input\"],\n    #     \"text\": [],\n    #     \"id\": []\n    # },\n    # email\n    {\n        \"name\": \"email\",\n        \"classes\": [\"form-control.g-input\"],\n        \"text\": [],\n        \"id\": []\n    },\n    # connect other onlyfans accounts username enter area\n    # BLANK\n    # password\n    {\n        \"name\": \"password\",\n        \"classes\": [\"form-control.g-input\"],\n        \"text\": [],\n        \"id\": []\n    },\n    # password 2x\n    {\n        \"name\": \"newPassword\",\n        \"classes\": [\"form-control.g-input\"],\n        \"text\": [],\n        \"id\": []\n    },\n    # confirm new password\n    {\n        \"name\": \"confirmPassword\",\n        \"classes\": [\"form-control.g-input\"],\n        \"text\": [],\n        \"id\": []\n    },\n\n    ## Messaging\n    # all TODO\n\n    {\n        \"name\": \"welcomeMessageToggle\",\n        \"classes\": [None],\n        \"text\": [],\n        \"id\": []\n    },\n\n    {\n        \"name\": \"welcomeMessageText\",\n        \"classes\": [None],\n        \"text\": [],\n        \"id\": []\n    },\n\n    {\n        \"name\": \"welcomeMessageUpload\",\n        \"classes\": [None],\n        \"text\": [],\n        \"id\": []\n    },\n\n    {\n        \"name\": \"welcomeMessageVoice\",\n        \"classes\": [None],\n        \"text\": [],\n        \"id\": []\n    },\n\n    {\n        \"name\": \"welcomeMessageVideo\",\n        \"classes\": [None],\n        \"text\": [],\n        \"id\": []\n    },\n\n    {\n        \"name\": \"welcomeMessagePrice\",\n        \"classes\": [None],\n        \"text\": [],\n        \"id\": []\n    },\n\n    {\n        \"name\": \"welcomeMessageSave\",\n        \"classes\": [None],\n        \"text\": [],\n        \"id\": []\n    },\n\n    {\n        \"name\": \"welcomeMessageHideToggle\",\n        \"classes\": [None],\n        \"text\": [],\n        \"id\": []\n    },\n\n    {\n        \"name\": \"showFullTextInEmailToggle\",\n        \"classes\": [None],\n        \"text\": [],\n        \"id\": []\n    },\n\n    ## Notifications\n    # push notifications\n    {\n        \"name\": \"pushNotifs\",\n        \"classes\": [\"g-input__wrapper.m-checkbox__toggle\"],\n        \"text\": [],\n        \"id\": [\"push-notifications\"]\n    },\n    # email notifications\n    {\n        \"name\": \"emailNotifs\",\n        \"classes\": [\"g-input__wrapper.m-checkbox__toggle\"],\n        \"text\": [],\n        \"id\": [\"email-notifications\"]\n    },\n    # new referral email\n    {\n        \"name\": \"emailNotifsReferral\",\n        \"classes\": [\"b-input-radio\"],\n        \"text\": [\"New Referral\"],\n        \"id\": []\n    },\n    # new stream email\n    {\n        \"name\": \"emailNotifsStream\",\n        \"classes\": [\"b-input-radio\"],\n        \"text\": [\"New Stream\"],\n        \"id\": []\n    },\n    # new subscriber email\n    {\n        \"name\": \"emailNotifsSubscriber\",\n        \"classes\": [\"b-input-radio\"],\n        \"text\": [\"New Subscriber\"],\n        \"id\": []\n    },\n    # new tip email\n    {\n        \"name\": \"emailNotifsSubscriber\",\n        \"classes\": [\"b-input-radio\"],\n        \"text\": [\"New Tip\"],\n        \"id\": []\n    },\n    # new renewal email\n    {\n        \"name\": \"emailNotifsSubscriber\",\n        \"classes\": [\"b-input-radio\"],\n        \"text\": [\"Renewal\"],\n        \"id\": []\n    },\n\n    {\n        \"name\": \"emailNotifsTip\",\n        \"classes\": [\"b-input-radio\"],\n        \"text\": [],\n        \"id\": []\n    },\n    #\n    {\n        \"name\": \"emailNotifsRenewal\",\n        \"classes\": [\"b-input-radio\"],\n        \"text\": [],\n        \"id\": []\n    },\n    # new likes summary\n    {\n        \"name\": \"emailNotifsLikes\",\n        \"classes\": [None],\n        \"text\": [],\n        \"id\": []\n    },\n    # new posts summary\n    {\n        \"name\": \"emailNotifsPosts\",\n        \"classes\": [None],\n        \"text\": [],\n        \"id\": []\n    },\n    # new private message summary\n    {\n        \"name\": \"emailNotifsPrivMessages\",\n        \"classes\": [None],\n        \"text\": [],\n        \"id\": []\n    },\n    # telegram bot button\n    # BLANK\n    # site notifications\n    {\n        \"name\": \"siteNotifs\",\n        \"classes\": [None],\n        \"text\": [],\n        \"id\": []\n    },\n    # new comment notification\n    {\n        \"name\": \"siteNotifsComment\",\n        \"classes\": [],\n        \"text\": [\"New comment\"],\n        \"id\": []\n    },\n    # new favorite notification\n    {\n        \"name\": \"siteNotifsFavorite\",\n        \"classes\": [],\n        \"text\": [\"New favorite (like)\"],\n        \"id\": []\n    },\n    # discounts from users i've used to follow notification\n    {\n        \"name\": \"siteNotifsDiscounts\",\n        \"classes\": [],\n        \"text\": [\"Discounts from users I used to follow\"],\n        \"id\": []\n    },\n    # new subscriber notification\n    {\n        \"name\": \"siteNotifsSubscriber\",\n        \"classes\": [],\n        \"text\": [\"New Subscriber\"],\n        \"id\": []\n    },\n    # new tip notification\n    {\n        \"name\": \"siteNotifsTip\",\n        \"classes\": [],\n        \"text\": [\"New Tip\"],\n        \"id\": []\n    },\n    # toast notification new comment\n    {\n        \"name\": \"toastNotifsComment\",\n        \"classes\": [],\n        \"text\": [\"New comment\"],\n        \"id\": []\n    },\n    # toast notification new favorite\n    {\n        \"name\": \"toastNotifsFavorite\",\n        \"classes\": [],\n        \"text\": [\"New favorite (like)\"],\n        \"id\": []\n    },\n    # toast notification new subscriber\n    {\n        \"name\": \"toastNotifsSubscriber\",\n        \"classes\": [],\n        \"text\": [\"New Subscriber\"],\n        \"id\": []\n    },\n    # toast notification new tip\n    {\n        \"name\": \"toastNotifsTip\",\n        \"classes\": [],\n        \"text\": [\"New Tip\"],\n        \"id\": []\n    },\n\n    ## Security\n\n    # two step toggle\n    # BLANK\n    # fully private profile\n    {\n        \"name\": \"fullyPrivate\",\n        \"classes\": [],\n        \"text\": [],\n        \"id\": [\"is_private\"]\n    },\n    # enable comments\n    {\n        \"name\": \"enableComments\",\n        \"classes\": [],\n        \"text\": [],\n        \"id\": [\"is_want_comments\"]\n    },\n    # show fans count on profile\n    {\n        \"name\": \"showFansCount\",\n        \"classes\": [],\n        \"text\": [],\n        \"id\": [\"show_subscribers_count\"]\n    },\n    # show posts tips summary\n    {\n        \"name\": \"showPostsTip\",\n        \"classes\": [],\n        \"text\": [],\n        \"id\": [\"show_posts_tips\"]\n    },\n    # public friends list\n    {\n        \"name\": \"publicFriendsList\",\n        \"classes\": [],\n        \"text\": [],\n        \"id\": [\"show_friends_list\"]\n    },\n    # geo blocking\n    {\n        \"name\": \"ipCountry\",\n        \"classes\": [\"multiselect__input\"],\n        \"text\": [],\n        \"id\": []\n    },\n    # ip blocking\n    {\n        \"name\": \"ipIP\",\n        \"classes\": [],\n        \"text\": [],\n        \"id\": [\"input-blocked-ips\"]\n    },\n    # watermarks photos\n    {\n        \"name\": \"watermarkPhoto\",\n        \"classes\": [],\n        \"text\": [],\n        \"id\": [\"hasWatermarkPhoto\"]\n    },\n    # watermarks video\n    {\n        \"name\": \"watermarkVideo\",\n        \"classes\": [],\n        \"text\": [],\n        \"id\": [\"hasWatermarkVideo\"]\n    },\n    # watermarks text\n    {\n        \"name\": \"watermarkText\",\n        \"classes\": [\"form-control.g-input\"],\n        \"text\": [],\n        \"id\": []\n    },\n    ####### save changes may be the same for each\n    ## Story\n    # allow message replies - nobody\n    {\n        \"name\": \"storyAllowRepliesNobody\",\n        \"classes\": [],\n        \"text\": [],\n        \"id\": [\"allowNobody\"]\n    },\n    # allow message replies - subscribers\n    {\n        \"name\": \"storyAllowRepliesSubscribers\",\n        \"classes\": [],\n        \"text\": [],\n        \"id\": [\"allowSubscribers\"]\n    },\n    ## Other\n    # obs server\n    {\n        \"name\": \"liveServer\",\n        \"classes\": [],\n        \"text\": [],\n        \"id\": [\"obsstreamingserver\"]\n    },\n    # obs key\n    {\n        \"name\": \"liveServerKey\",\n        \"classes\": [],\n        \"text\": [],\n        \"id\": [\"streamingobskey\"]\n    },\n    # welcome chat message toggle\n    {\n        \"name\": \"welcomeMessageToggle\",\n        \"classes\": [],\n        \"text\": [],\n        \"id\": [\"autoMessage\"]\n    },\n    # then same pattern for message enter text or add stuff and price\n    {\n        \"name\": \"welcomeMessageText\",\n        \"classes\": [\"form-control.b-chat__message-input\"],\n        \"text\": [],\n        \"id\": []\n    },\n    # save button for welcome chat message\n    {\n        \"name\": \"welcomeMessageSave\",\n        \"classes\": [\"g-btn.m-rounded.b-chat__btn-submit\"],\n        \"text\": [],\n        \"id\": []\n    },\n    {\n        \"name\": \"profileSave\",\n        \"classes\": [\"g-btn.m-rounded\"],\n        \"text\": [\"Save changes\"],\n        \"id\": [],\n    }\n\n]\n\n\n# # working\n########################\n# username\n# displayName\n# about\n# location\n# websiteURL\n\n## security\n# fullyPrivate\n# enableComments\n# showFansCount\n# showPostsTip\n# publicFriendsList\n# ipCountry\n# ipIP\n# watermarkPhoto\n# watermarkVideo\n# watermarkText\n# welcomeMessageToggle\n## other\n# liveServer\n# liveServerKey\n\n# # sorta working\n########################\n# coverImage\n# profilePhoto\n# password\n# newPassword\n# confirmPassword\n\n# # all the notifs are probably false positives\n# # are all b.input radio should maybe nth one found\n# emailNotifsReferral\n# emailNotifsStream\n# emailNotifsSubscriber\n# emailNotifsTip\n# emailNotifsRenewal\n\n# # not working\n# ########################\n# email\n# emailNotifs\n# emailNotifsPosts\n# emailNotifsPrivMessages\n# siteNotifs\n# siteNotifsComment\n# siteNotifsFavorite\n# siteNotifsDiscounts\n# siteNotifsSubscriber\n# siteNotifsTip\n# toastNotifsComment\n# toastNotifsSubscriber\n# toastNotifsTip\n# welcomeMessageText\n"
  },
  {
    "path": "OnlySnarf/lib/__init__.py",
    "content": ""
  },
  {
    "path": "OnlySnarf/lib/config.py",
    "content": "import os\nimport sys\nimport json\nimport time\nimport shutil\nimport inquirer\nimport fileinput\n# from pathlib import Path\n##\nfrom ..util.settings import Settings\nfrom ..util.colorize import colorize\nfrom pathlib import Path\n\nEMPTY_USER_CONFIG = Path(__file__).parent.joinpath(\"../conf/users/example-user.conf\").resolve()\nBASE_CONFIG = Path(__file__).parent.joinpath(\"../conf/config.conf\").resolve()\n\nclass Config:\n\n    def __init__(self):\n        pass\n\n    def add_user():\n        username = input(\"OnlyFans username: \")\n        # check if user already exists\n        if str(username)+\".conf\" in Config.get_users():\n            Settings.warn_print(\"user already exists!\")\n            return Config.main()\n        Config.reset_user_config(username)\n        Config.update_onlyfans_user(user=username)\n        Config.update_google_user(user=username)\n        Config.update_twitter_user(user=username)\n        Config.main()\n\n\n    def check_config(user):\n        try:\n            if not os.path.isfile(Settings.get_user_config_path(user)):\n                Config.reset_user_config(user)\n        except Exception as e:\n            Config.reset_user_config(user)\n\n    def display_user():\n        Config.list_users()\n        username = Config.list_user_menu()\n        if (username == 'back'): return Config.main()\n        Config.list_user_config(username)\n        Config.main()\n\n    def list_users():\n        # list all user configs in conf/users\n        for (dirpath, dirnames, filenames) in os.walk(os.path.join(Settings.get_base_directory(), \"conf/users\")):\n            for filename in filenames:\n                Settings.print(\"> \"+filename)\n            break\n\n    def list_user_config(user):\n        Settings.print(\"-- OnlySnarf Config --\")\n        Settings.print(colorize(\"Green\", \"green\")+\": configured\")\n        Settings.print(colorize(\"Blue\", \"blue\")+\": system defaults\")\n        Settings.print(colorize(\"Red\", \"red\")+\": missing\")\n        Settings.print(\"------------------------------\")\n        Settings.print(colorize(\"Config File\", 'conf')+\": \"+colorize(user, 'green'))\n        if str(Settings.get_username_onlyfans(user)) != \"None\":\n            color = \"green\"\n            if str(Settings.get_username_onlyfans(user)) == \"$USERNAME\":            \n                color = \"blue\"\n            Settings.print(colorize(\"OnlyFans Username\", 'conf')+\": \"+colorize(Settings.get_username_onlyfans(user), color))\n        else:\n            Settings.print(colorize(\"OnlyFans Username\", 'conf')+\": \"+colorize(\"N/A\", 'red'))\n\n        if str(Settings.get_password(user)) != \"None\":\n            color = \"green\"\n            if str(Settings.get_password(user)) == \"$PASSWORD\":            \n                color = \"blue\"\n            Settings.print(colorize(\"OnlyFans Password\", 'conf')+\": \"+colorize(\"******\", color))\n        else:\n            Settings.print(colorize(\"OnlyFans Password\", 'conf')+\": \"+colorize(\"N/A\", 'red'))\n\n        if str(Settings.get_username_google(user)) != \"None\":\n            color = \"green\"\n            if str(Settings.get_username_google(user)) == \"$UGOOGLE\":            \n                color = \"blue\"\n            Settings.print(colorize(\"Google Username\", 'conf')+\": \"+colorize(Settings.get_username_google(user), color))\n        else:\n            Settings.print(colorize(\"Google Username\", 'conf')+\": \"+colorize(\"N/A\", 'red'))\n\n        if str(Settings.get_password_google(user)) != \"None\":\n            color = \"green\"\n            if str(Settings.get_password_google(user)) == \"$PGOOGLE\":            \n                color = \"blue\"\n            Settings.print(colorize(\"Google Password\", 'conf')+\": \"+colorize(\"******\", color))\n        else:\n            Settings.print(colorize(\"Google Password\", 'conf')+\": \"+colorize(\"N/A\", 'red'))\n\n        if str(Settings.get_username_twitter(user)) != \"None\":\n            color = \"green\"\n            if str(Settings.get_username_twitter(user)) == \"$UTWITTER\":            \n                color = \"blue\"\n            Settings.print(colorize(\"Twitter Username\", 'conf')+\": \"+colorize(Settings.get_username_twitter(user), color))\n        else:\n            Settings.print(colorize(\"Twitter Username\", 'conf')+\": \"+colorize(\"N/A\", 'red'))\n\n        if str(Settings.get_password_twitter(user)) != \"None\":\n            color = \"green\"\n            if str(Settings.get_password_twitter(user)) == \"$PTWITTER\":            \n                color = \"blue\"\n            Settings.print(colorize(\"Twitter Password\", 'conf')+\": \"+colorize(\"******\", color))\n        else:\n            Settings.print(colorize(\"Twitter Password\", 'conf')+\": \"+colorize(\"N/A\", 'red'))\n        Settings.print(\"------------------------------\")\n\n    def list_user_menu():\n        options = [\"back\"]\n        options.extend(Config.get_users())\n        questions = [\n            inquirer.List('list',\n                message= \"Please select a username for more info:\",\n                choices= options,\n            )\n        ]\n        answers = inquirer.prompt(questions)\n        return answers['list']\n\n    def get_users():\n        users = []\n        for (dirpath, dirnames, filenames) in os.walk(os.path.join(Settings.get_base_directory(), \"conf/users\")):\n            users.extend(filenames)\n            break\n        return users\n\n    def ask_username():\n        options = [\"back\"]\n        options.extend(Config.get_users())\n        if \"example-user.conf\" in options:\n            options.remove(\"example-user.conf\") # should not update the example / template file\n        questions = [\n            inquirer.List('username',\n                message= \"Please select a username:\",\n                choices= options,\n            )\n        ]\n        answers = inquirer.prompt(questions)\n        return answers['username']\n\n    def update_menu():\n        username = Config.ask_username()\n        if (username == 'back'): return Config.main()\n        Config.update_user_config(username.replace(\".conf\",\"\"))\n        Config.main()\n\n    def user_header(user=\"default\"):\n        Settings.print(\"User:\")\n        if Settings.get_username_onlyfans(user) != \"\":\n            Settings.print(\" - Email = {}\".format(Settings.get_username_onlyfans(user)))\n        Settings.print(\" - Username = {}\".format(Settings.get_username(user)))\n        pass_ = \"\"\n        if str(Settings.get_password()) != \"\":\n            pass_ = \"******\"\n        Settings.print(\" - Password = {}\".format(pass_))\n        if str(Settings.get_username_twitter(user)) != \"\":\n            Settings.print(\" - Twitter = {}\".format(Settings.get_username_twitter(user)))\n            pass_ = \"\"\n            if str(Settings.get_password_twitter(user)) != \"\":\n                pass_ = \"******\"\n            Settings.print(\" - Password = {}\".format(pass_))\n        Settings.print('\\r')\n\n    def remove_menu():\n        username = Config.ask_username()\n        if (username == 'back'): return Config.main()\n        if input(\"ARE YOU SURE? N/y \") == \"y\":\n            Config.remove_user(user=username)\n        else:\n            Settings.print(\"canceling deletion!\")\n        Config.main()\n\n    def remove_user(user=\"default\"):\n        try:\n            os.remove(Settings.get_user_config_path(user))\n        except Exception as e:\n            pass\n        Settings.print(\"successfully removed {}!\".format(user))\n\n    def menu():\n        questions = [\n            inquirer.List('menu',\n                message= \"Please select an option:\",\n                choices= ['Add', 'Display', 'List', 'Update', 'Remove', 'Reset', 'Exit']\n            )\n        ]\n        answers = inquirer.prompt(questions)\n        return answers['menu']\n\n    def main_menu():\n        action = Config.menu()\n        if (action == 'Add'): Config.add_user()\n        elif (action == 'Display'): Config.display_user()\n        elif (action == 'List'): Config.list_users()\n        elif (action == 'Update'): Config.update_menu()\n        elif (action == 'Remove'): Config.remove_menu()\n        elif (action == 'Reset'): Config.reset_config()\n        else: exit()\n        Config.main()\n\n    def main():\n        time.sleep(1)\n        try:\n            Config.main_menu()\n        except Exception as e:\n            Settings.dev_print(e)\n\n    def prompt_google(user):\n        data = {}\n        data['username'] = Settings.get_username_google(user)\n        data['password'] = Settings.get_password_google(user)\n        Settings.print(\"Username: \"+data['username'])\n        Settings.print(\"Password: \"+data['password'])\n        if data['username'] == \"\" or input(\"Update Google email? N/y \").lower() == \"y\":\n            data['username'] = input('Google Email: ')\n        if data['password'] == \"\" or input(\"Update Google password? N/y \").lower() == \"y\":\n            data['password'] = input('Google Password: ')\n        return data\n\n    def prompt_onlyfans(user):\n        data = {}\n        data['username'] = Settings.get_username_onlyfans(user)\n        data['password'] = Settings.get_password(user)\n        Settings.print(\"Username: \"+data['username'])\n        Settings.print(\"Password: \"+data['password'])\n        if data['username'] == \"\" or input(\"Update OnlyFans email? N/y \").lower() == \"y\":\n            data['username'] = input('OnlyFans Email: ')\n        if data['password'] == \"\" or input(\"Update OnlyFans password? N/y \").lower() == \"y\":\n            data['password'] = input('OnlyFans Password: ')\n        return data\n\n    def prompt_twitter(user):\n        data = {}\n        data['username'] = Settings.get_username_twitter(user)\n        data['password'] = Settings.get_password_twitter(user)\n        Settings.print(\"Username: \"+data['username'])\n        Settings.print(\"Password: \"+data['password'])\n        if data['username'] == \"\" or input(\"Update Twitter username? N/y \").lower() == \"y\":\n            data['username'] = input('Twitter Username: ')\n        if data['password'] == \"\" or input(\"Update Twitter password? N/y \").lower() == \"y\":\n            data['password'] = input('Twitter Password: ')\n        return data\n\n    def reset_config():\n        Settings.print(\"resetting configuration...\")\n        shutil.copyfile(BASE_CONFIG, os.path.join(Settings.get_base_directory(), \"conf\", \"config.conf\"))\n        shutil.rmtree(os.path.join(Settings.get_base_directory(), \"conf/users\"))\n        Path(os.path.join(Settings.get_base_directory(), \"conf/users\")).mkdir(parents=True, exist_ok=True)\n        Settings.print(\"OnlySnarf config reset!\")\n\n    def reset_user_config(user=\"default\"):\n        Settings.dev_print(\"resetting user config files for {}...\".format(user))\n        if os.path.exists(Settings.get_user_config_path(user)):\n            os.remove(Settings.get_user_config_path(user))\n        else:\n            Settings.dev_print(\"no user config exists to reset!\")\n        shutil.copyfile(EMPTY_USER_CONFIG, Settings.get_user_config_path(user))\n        Settings.dev_print(\"successfully reset user config!\")\n\n    def update_user_config(user=\"default\"):\n        # save user settings in variables\n        username = Settings.get_username_onlyfans(user)\n        password = Settings.get_password(user)\n\n        googleU = Settings.get_username_google(user)\n        googleP = Settings.get_password_google(user)\n        \n        twitterU = Settings.get_username_twitter(user)\n        twitterP = Settings.get_password_twitter(user)\n\n        # reset user config\n        Config.reset_user_config(user)\n\n        onlyfans_data = Config.prompt_onlyfans(user)\n        google_data = Config.prompt_google(user)\n        twitter_data = Config.prompt_twitter(user)\n\n        if onlyfans_data[\"username\"] == \"$USERNAME\": onlyfans_data[\"username\"] = username \n        if onlyfans_data[\"password\"] == \"$PASSWORD\": onlyfans_data[\"password\"] = password \n        if google_data[\"username\"] == \"$UGOOGLE\": google_data[\"username\"] = googleU \n        if google_data[\"password\"] == \"$PGOOGLE\": google_data[\"password\"] = googleP \n        if twitter_data[\"username\"] == \"$UTWITTER\": twitter_data[\"username\"] = twitterU \n        if twitter_data[\"password\"] == \"$PTWITTER\": twitter_data[\"password\"] = twitterP \n\n        Config.update_onlyfans_user(onlyfans_data, user)\n        Config.update_google_user(google_data, user)\n        Config.update_twitter_user(twitter_data, user)\n        Settings.print(\"successfully updated user config for {}!\".format(user))\n\n    def update_onlyfans_user(data=None, user=\"default\"):\n        Config.check_config(user)\n        if not data: data = Config.prompt_onlyfans(user)\n        with fileinput.FileInput(Settings.get_user_config_path(user), inplace = True) as f:\n            for line in f: \n                if data['username']:\n                    line = line.replace(\"$USERNAME\", data['username'])\n                if data['password']:\n                    line = line.replace(\"$PASSWORD\", data['password'])\n                print(line, end ='')\n        Settings.print(\"OnlyFans user config updated!\")\n\n    def update_google_user(data=None, user=\"default\"):\n        Config.check_config(user)\n        if not data: data = Config.prompt_google(user)\n        with fileinput.FileInput(Settings.get_user_config_path(user), inplace = True) as f:\n            for line in f: \n                if data['username']:\n                    line = line.replace(\"$UGOOGLE\", data['username'])\n                if data['password']:\n                    line = line.replace(\"$PGOOGLE\", data['password'])\n                print(line, end ='')\n        Settings.print(\"Google user config updated!\")\n\n    def update_twitter_user(data=None, user=\"default\"):\n        Config.check_config(user)\n        if not data: data = Config.prompt_twitter(user)\n        with fileinput.FileInput(Settings.get_user_config_path(user), inplace = True) as f:\n            for line in f: \n                if data['username']:\n                    line = line.replace(\"$UTWITTER\", data['username'])\n                if data['password']:\n                    line = line.replace(\"$PTWITTER\", data['password'])\n                print(line, end ='')\n        Settings.print(\"Twitter user config updated!\")\n"
  },
  {
    "path": "OnlySnarf/lib/driver.py",
    "content": "import re\nimport random\nimport os\nimport shutil\nimport json\nimport pathlib\nimport time\nimport wget\nimport pickle\nfrom datetime import datetime, timedelta\nfrom pathlib import Path\nfrom selenium import webdriver\nfrom selenium.webdriver.common.action_chains import ActionChains\nfrom selenium.webdriver.common.by import By\nfrom selenium.webdriver.common.desired_capabilities import DesiredCapabilities\nfrom selenium.webdriver.common.keys import Keys\nfrom selenium.webdriver.firefox.options import Options as FirefoxOptions\nfrom selenium.webdriver.remote.file_detector import LocalFileDetector\nfrom selenium.webdriver.remote.webdriver import WebDriver\nfrom selenium.webdriver.support import expected_conditions as EC\nfrom selenium.webdriver.support.ui import WebDriverWait\nfrom selenium.common.exceptions import NoSuchElementException\nfrom selenium.common.exceptions import TimeoutException\nfrom selenium.common.exceptions import WebDriverException\n##\nfrom webdriver_manager.chrome import ChromeDriverManager\nfrom selenium.webdriver.chrome.service import Service as BraveService\n# chrome\nfrom selenium.webdriver.chrome.service import Service as ChromeService\nfrom webdriver_manager.chrome import ChromeDriverManager\n# chromium\nfrom webdriver_manager.core.utils import ChromeType\n# brave\n# use ChromeService\n# firefox\nfrom selenium.webdriver.firefox.service import Service as FirefoxService\nfrom webdriver_manager.firefox import GeckoDriverManager\n# ie\nfrom selenium.webdriver.ie.service import Service as IEService\nfrom webdriver_manager.microsoft import IEDriverManager\n# edge\n# from selenium.webdriver.edge.service import Service as EdgeService\n# from webdriver_manager.microsoft import EdgeChromiumDriverManager\n# from msedge.selenium_tools import Edge, EdgeOptions\n# opera\nfrom webdriver_manager.opera import OperaDriverManager\n##\nfrom ..classes.element import Element\nfrom ..util.settings import Settings\n#\nfrom ..classes.file import File\n\n###################\n##### Globals #####\n###################\n\n# Urls\nONLYFANS_HOME_URL = \"https://onlyfans.com\"\nONLYFANS_HOME_URL2 = \"https://onlyfans.com/\"\nONLYFANS_NEW_MESSAGE_URL = \"/my/chats/send\"\nONLYFANS_CHAT_URL = \"/my/chats/chat/\"\nONLYFANS_SETTINGS_URL = \"/my/settings/\"\nONLYFANS_USERS_ACTIVE_URL = \"/my/subscribers/active\"\nONLYFANS_USERS_FOLLOWING_URL = \"/my/subscriptions/active\"\nONLYFANS_LISTS_URL = \"/my/lists/\"\n\nclass Driver:\n    \"\"\"Driver class for Selenium management\"\"\"\n\n    BROWSER = None\n    BROWSERS = []\n    DRIVERS = []\n\n    #\n    DOWNLOADING = True\n    DOWNLOADING_MAX = False\n    DOWNLOAD_MAX_IMAGES = 1000\n    DOWNLOAD_MAX_VIDEOS = 1000\n    #\n    MAX_TABS = 20\n    NOT_INFORMED_KEPT = False # whether or not \"Keep\"ing the Driver.browser session has been printed once upon exit\n    NOT_INFORMED_CLOSED = False # same dumb shit as above\n\n    initialScrollDelay = 0.5\n    scrollDelay = 0.5\n\n    def __init__(self):\n\n        # selenium web driver\n        self.browser = None\n        self.browsers = []\n\n        # browser tabs cache\n        self.tabs = []\n        # OnlyFans discovered lists cache\n        self.lists = []\n        # save login state\n        self.logged_in = False\n        # web browser session id and url for reconnecting\n        self.session_id = None\n        self.session_url = None\n\n        self._initialized_ = False\n\n    def init(self):\n        \"\"\"\n        Initiliaze the web driver aspect.\n\n\n        \"\"\"\n\n        if self._initialized_: return\n        self.browser = self.spawn_browser(Settings.get_browser_type())\n        if self.browser:\n            self.browsers.append(self.browser)\n            ## Cookies\n            if str(Settings.is_cookies()) == \"True\":\n                self.cookies_load()\n            self.tabs.append([self.browser.current_url, self.browser.current_window_handle, 0])\n        self._initialized_ = True\n        Driver.DRIVERS.append(self)\n\n    def auth(self):\n        \"\"\"\n        Authorization check\n\n        Logs in with provided runtime creds if not logged in\n\n        Returns\n        -------\n        bool\n            Whether or not the login attempt was successful\n\n        \"\"\"\n\n        self.init()\n        if not self.login():\n            if str(Settings.is_debug()) == \"True\":\n                return False\n            os._exit(1)\n        if str(Settings.is_cookies()) == \"True\":\n            self.cookies_save()\n        return True\n\n    ###################\n    ##### Cookies #####\n    ###################\n\n    def cookies_load(self):\n        \"\"\"Loads existing web browser cookies from local source\"\"\"\n\n        Settings.maybe_print(\"loading cookies...\")\n        try:\n            if os.path.exists(Settings.get_cookies_path()):\n                # must be at onlyfans.com to load cookies of onlyfans.com\n                self.go_to_home()\n                file = open(Settings.get_cookies_path(), \"rb\")\n                cookies = pickle.load(file)\n                file.close()\n                Settings.dev_print(\"cookies: \")\n                for cookie in cookies:\n                    Settings.dev_print(cookie)\n                    self.browser.add_cookie(cookie)\n                Settings.maybe_print(\"successfully loaded cookies\")\n                self.refresh()\n            else: \n                Settings.maybe_print(\"failed to load cookies, do not exist\")\n        except Exception as e:\n            Settings.print(\"error loading cookies!\")\n            Settings.dev_print(e)\n\n    def cookies_save(self):\n        \"\"\"Saves existing web browser cookies to local source\"\"\"\n\n        Settings.maybe_print(\"saving cookies...\")\n        try:\n            # must be at onlyfans.com to save cookies of onlyfans.com\n            self.go_to_home()\n            Settings.dev_print(self.browser.get_cookies())\n            file = open(Settings.get_cookies_path(), \"wb\")\n            pickle.dump(self.browser.get_cookies(), file) # \"cookies.pkl\"\n            file.close()\n            Settings.maybe_print(\"successfully saved cookies\")\n        except Exception as e:\n            Settings.print(\"failed to save cookies!\")\n            Settings.dev_print(e)\n\n    ####################\n    ##### Discount #####\n    ####################\n\n    @staticmethod\n    def discount_user(discount, reattempt=False):\n        \"\"\"\n        Enter and apply discount to user\n\n        Discount object requires:\n        - duration (in months)\n        - amount\n        - username\n\n        Parameters\n        ----------\n        discount : classes.Discount\n            Discount object that contains or prompts for proper values\n\n        Returns\n        -------\n        bool\n            Whether or not the discount was applied successfully\n\n        \"\"\"\n\n        if not discount:\n            Settings.err_print(\"missing discount\")\n            return False\n\n        # BUG\n        # doesn't want to work with local variables\n        Driver.originalAmount = None\n        Driver.originalMonths = None\n        try:\n            driver = Driver.get_driver()\n            driver.auth()\n            months = int(discount[\"months\"])\n            amount = int(discount[\"amount\"])\n            username = str(discount[\"username\"])\n            Settings.print(\"discounting: {} {} for {} month(s)\".format(username, amount, months))\n            driver.go_to_page(ONLYFANS_USERS_ACTIVE_URL)\n            end_ = True\n            count = 0\n            user_ = None\n            Settings.maybe_print(\"searching for fan...\")\n            # scroll through users on page until user is found\n            attempts = 0\n            while end_:\n                elements = driver.browser.find_elements(By.CLASS_NAME, \"m-fans\")\n                for ele in elements:\n                    username_ = ele.find_element(By.CLASS_NAME, \"g-user-username\").get_attribute(\"innerHTML\").strip()\n                    # if str(username) == str(username_).replace(\"@\",\"\"):\n                    if username in username_:\n                        driver.browser.execute_script(\"arguments[0].scrollIntoView();\", ele)\n                        user_ = ele\n                        end_ = False\n                if not end_: continue\n\n                if len(elements) == int(count):\n                    Driver.scrollDelay += Driver.initialScrollDelay\n                    attempts+=1\n                    if attempts == 5:\n                        break\n\n                Settings.print_same_line(\"({}/{}) scrolling...\".format(count, len(elements)))\n                count = len(elements)\n                driver.browser.execute_script(\"window.scrollTo(0, document.body.scrollHeight);\")\n                time.sleep(Driver.scrollDelay)\n\n            Settings.print(\"\")\n            Settings.dev_print(\"successfully found fans\")\n            if not user_:\n                Settings.err_print(\"unable to find fan - {}\".format(username))\n                if not reattempt:\n                    Settings.maybe_print(\"reattempting fan search...\")\n                    return Driver.discount_user(discount, reattempt=True)\n                return False\n\n            Settings.maybe_print(\"found: {}\".format(username))\n            ActionChains(driver.browser).move_to_element(user_).perform()\n            Settings.dev_print(\"successfully moved to fan\")\n            Settings.dev_print(\"finding discount btn\")\n            buttons = user_.find_elements(By.CLASS_NAME, Element.get_element_by_name(\"discountUser\").getClass())\n            clicked = False\n            for button in buttons:\n                # print(button.get_attribute(\"innerHTML\"))\n                if \"Discount\" in button.get_attribute(\"innerHTML\") and button.is_enabled() and button.is_displayed():\n                    try:\n                        Settings.dev_print(\"clicking discount btn\")\n                        button.click()\n                        Settings.dev_print(\"clicked discount btn\")\n                        clicked = True\n                        break\n                    except Exception as e:\n                        Driver.error_checker(e)\n                        Settings.warn_print(\"unable to click discount btn for: {}\".format(username))\n                        return False\n            if not clicked:\n                Settings.warn_print(\"unable to find discount btn for: {}\".format(username))\n                return False\n            time.sleep(1)\n\n            def apply_discount():\n                Settings.maybe_print(\"attempting discount entry...\")\n                Settings.dev_print(\"finding months and discount amount btns\")\n                ## amount\n                discountEle = driver.browser.find_elements(By.CLASS_NAME, Element.get_element_by_name(\"discountUserAmount\").getClass())[0]\n                discountAmount = int(discountEle.get_attribute(\"innerHTML\").replace(\"% discount\", \"\"))\n                if not Driver.originalAmount: Driver.originalAmount = discountAmount\n                Settings.dev_print(\"amount: {}\".format(discountAmount))\n                Settings.dev_print(\"entering discount amount\")\n                if int(discountAmount) != int(amount):\n                    up_ = int((discountAmount / 5) - 1)\n                    down_ = int((int(amount) / 5) - 1)\n                    Settings.dev_print(\"up: {}\".format(up_))\n                    Settings.dev_print(\"down: {}\".format(down_))\n                    action = ActionChains(driver.browser)\n                    action.click(on_element=discountEle)\n                    action.pause(1)\n                    for n in range(up_):\n                        action.send_keys(Keys.UP)\n                        action.pause(0.5)\n                    for n in range(down_):\n                        action.send_keys(Keys.DOWN)\n                        action.pause(0.5)                \n                    action.send_keys(Keys.TAB)\n                    action.perform()\n                Settings.dev_print(\"successfully entered discount amount\")\n                ## months\n                monthsEle = driver.browser.find_elements(By.CLASS_NAME, Element.get_element_by_name(\"discountUserMonths\").getClass())[1]\n                monthsAmount = int(monthsEle.get_attribute(\"innerHTML\").replace(\" months\", \"\").replace(\" month\", \"\"))\n                if not Driver.originalMonths: Driver.originalMonths = monthsAmount\n                Settings.dev_print(\"months: {}\".format(monthsAmount))\n                Settings.dev_print(\"entering discount months\")\n                if int(monthsAmount) != int(months):\n                    up_ = int(monthsAmount - 1)\n                    down_ = int(int(months) - 1)\n                    Settings.dev_print(\"up: {}\".format(up_))\n                    Settings.dev_print(\"down: {}\".format(down_))\n                    action = ActionChains(driver.browser)\n                    action.click(on_element=monthsEle)\n                    action.pause(1)\n                    for n in range(up_):\n                        action.send_keys(Keys.UP)\n                        action.pause(0.5)\n                    for n in range(down_):\n                        action.send_keys(Keys.DOWN)\n                        action.pause(0.5)\n                    action.send_keys(Keys.TAB)\n                    action.perform()\n                Settings.dev_print(\"successfully entered discount months\")\n                discountEle = driver.browser.find_elements(By.CLASS_NAME, Element.get_element_by_name(\"discountUserAmount\").getClass())[0]\n                discountAmount = int(discountEle.get_attribute(\"innerHTML\").replace(\"% discount\", \"\"))\n                monthsEle = driver.browser.find_elements(By.CLASS_NAME, Element.get_element_by_name(\"discountUserMonths\").getClass())[1]\n                monthsAmount = int(monthsEle.get_attribute(\"innerHTML\").replace(\" months\", \"\").replace(\" month\", \"\"))\n                return discountAmount, monthsAmount\n\n            # discount method is repeated until values are correct because somehow it occasionally messes up...\n            discountAmount, monthsAmount = apply_discount()\n            while int(discountAmount) != int(amount) and int(monthsAmount) != int(months):\n                # Settings.print(\"{} = {}    {} = {}\".format(discountAmount, amount, monthsAmount, months))\n                discountAmount, monthsAmount = apply_discount()\n\n            Settings.debug_delay_check()\n            ## apply\n            Settings.dev_print(\"applying discount\")\n            buttons_ = driver.find_elements_by_name(\"discountUserButton\")\n            for button in buttons_:\n                if not button.is_enabled() and not button.is_displayed(): continue\n                if \"Cancel\" in button.get_attribute(\"innerHTML\") and str(Settings.is_debug()) == \"True\":\n                    Settings.print(\"skipping save discount (debug)\")\n                    button.click()\n                    Settings.dev_print(\"successfully canceled discount\")\n                    Settings.dev_print(\"### Discount Successful ###\")\n                    return True\n                elif \"Cancel\" in button.get_attribute(\"innerHTML\") and int(discountAmount) == int(Driver.originalAmount) and int(monthsAmount) == int(Driver.originalMonths):\n                    Settings.print(\"skipping existing discount\")\n                    button.click()\n                    Settings.dev_print(\"successfully skipped existing discount\")\n                    Settings.dev_print(\"### Discount Successful ###\")\n                    # return True\n                elif \"Apply\" in button.get_attribute(\"innerHTML\"):\n                    button.click()\n                    Settings.print(\"discounted: {}\".format(username))\n                    Settings.dev_print(\"successfully applied discount\")\n                    Settings.dev_print(\"### Discount Successful ###\")\n                    return True\n            Settings.dev_print(\"### Discount Failure ###\")\n            return False\n        except Exception as e:\n            Settings.print(e)\n            Driver.error_checker(e)\n            buttons_ = driver.find_elements_by_name(\"discountUserButton\")\n            for button in buttons_:\n                if \"Cancel\" in button.get_attribute(\"innerHTML\"):\n                    button.click()\n                    Settings.dev_print(\"### Discount Successful Failure ###\")\n                    return False\n            Settings.dev_print(\"### Discount Failure ###\")\n            return False\n\n    def download_content(self):\n        \"\"\"Downloads all content (images and video) from the user's profile page\"\"\"\n\n        Settings.print(\"downloading content...\")\n        def scroll_to_bottom():\n            try:\n                # go to profile page and scroll to bottom\n                self.go_to_profile()\n                # count number of content elements to scroll to bottom\n                num = self.browser.find_element(By.CLASS_NAME, \"b-profile__sections__count\").get_attribute(\"innerHTML\")\n                num = num.replace(\"K\",\"00\").replace(\".\",\"\")\n                Settings.maybe_print(\"content count: {}\".format(num))\n                for n in range(int(int(int(num)/5)+1)):\n                    Settings.print_same_line(\"({}/{}) scrolling...\".format(n,int(int(int(num)/5)+1)))\n                    self.browser.execute_script(\"window.scrollTo(0, document.body.scrollHeight);\")\n                    time.sleep(1)\n                Settings.print(\"\")\n            except Exception as e:\n                Settings.print(e)\n                Settings.err_print(\"failed to find content to scroll\")\n        scroll_to_bottom()\n        imagesDownloaded = self.download_images()\n        videosDownloaded = self.download_videos()\n        Settings.print(\"downloaded content\")\n        Settings.print(\"count: {}\".format(len(imagesDownloaded)+len(videosDownloaded)))\n\n    def download_images(self, destination=None):\n        \"\"\"Downloads all images on the page\"\"\"\n\n        downloaded = []\n        downloadMe = []\n        try:\n            images_ = self.browser.find_elements(By.TAG_NAME, \"img\")\n            images = []\n\n            for image in images_:\n                # print(image)\n                # print(image.get_attribute(\"src\"))\n                if \"thumbs.onlyfans.com\" not in str(image.get_attribute(\"src\")):\n                    # print(image.get_attribute(\"src\"))\n                    images.append(image)\n\n            end = len(images)\n            if len(images) == 0:\n                Settings.warn_print(\"no images found!\")\n                return downloaded\n            if not destination: destination = os.path.join(Settings.get_download_path(), \"images\")\n            Path(destination).mkdir(parents=True, exist_ok=True)\n            i=0\n            for j in range(end):\n                try:\n                    images_ = self.browser.find_elements(By.TAG_NAME, \"img\")\n                    images = []\n\n                    for image in images_:\n                        if \"thumbs.onlyfans.com\" not in str(image.get_attribute(\"src\")):\n                            # print(image.get_attribute(\"src\"))\n                            images.append(image)\n\n                    # click on each image\n                    # download each image via class \"pswp__img\"\n                    successful = self.move_to_then_click_element(images[j])\n\n                    while not successful:\n                        driver.browser.execute_script(\"window.scrollTo(0, document.body.scrollHeight);\")\n                        time.sleep(1)\n                        successful = self.move_to_then_click_element(images[j])\n\n                    time.sleep(1)\n                    hdImages = self.browser.find_elements(By.CLASS_NAME, \"pswp__img\")\n                    for image in hdImages:\n                        downloadMe.append(image.get_attribute(\"src\"))\n                    # print(len(downloadMe))\n                except Exception as err:\n                    Settings.print(\"\")            \n                    Settings.warn_print(err)\n                finally:\n                    ActionChains(self.browser).send_keys(Keys.ESCAPE).perform()\n                    i+=1\n            Settings.print(\"\")\n        except Exception as err:\n            Settings.err_print(err)\n\n        # print(downloadMe)\n        downloadMe = list(set(downloadMe)) # remove duplicates\n        # print(downloadMe)\n\n        i=1\n        for src in downloadMe:\n            # src = \"\"\n            try:\n                # if Driver.DOWNLOADING_MAX and i > Driver.DOWNLOAD_MAX_IMAGES: break\n                # src = str(image.get_attribute(\"src\"))\n                # print(src)\n                if not src or src == \"\" or src == \"None\" or \"/thumbs/\" in src or \"_frame_\" in src or \"http\" not in src: continue\n                Settings.print_same_line(\"downloading image: {}/{}\".format(i, len(images)))\n                # Settings.print(\"Image: {}\".format(src[:src.find(\".jpg\")+4]))\n                # Settings.dev_print(\"image src: {}\".format(src))\n                    # while os.path.isfile(\"{}/{}.jpg\".format(destination, i)):\n                        # i+=1\n\n                # TODO: maybe open image in new tab then download it\n\n                wget.download(src, \"{}/{}.jpg\".format(destination, i), False)\n                downloaded.append(i)\n            except Exception as err:\n                Settings.print(\"\")            \n                Settings.err_print(err)\n                Settings.warn_print(\"skipped image: \"+src)\n            finally:\n                i+=1\n\n        return downloaded\n\n    def download_messages(self, user=\"all\", destination=None):\n        \"\"\"\n        Downloads all content in messages with the user\n\n        Parameters\n        ----------\n        user : str or classes.User\n            The user to download message content from\n\n        \"\"\"\n\n        Settings.print(\"downloading messages: {}\".format(user))\n        try:\n            if str(user) == \"all\":\n                # from OnlySnarf.classes.user import User\n                from ..classes.user import User\n                user = random.choice(User.get_all_users())\n            self.message_user(user.username)\n            time.sleep(1)\n            contentCount = 0\n            while True:\n                self.browser.execute_script(\"document.querySelector('div[id=chatslist]').scrollTop=1e100\")\n                time.sleep(1)\n                self.browser.execute_script(\"document.querySelector('div[id=chatslist]').scrollTop=1e100\")\n                time.sleep(1)\n                self.browser.execute_script(\"document.querySelector('div[id=chatslist]').scrollTop=1e100\")\n                time.sleep(1)\n                images = self.browser.find_elements(By.TAG_NAME, \"img\")\n                videos = self.browser.find_elements(By.TAG_NAME, \"video\")\n                # Settings.print((len(images)+len(videos)))\n                if contentCount == len(images)+len(videos): break\n                contentCount = len(images)+len(videos)\n            # download all images and videos\n\n            # TODO: download into correct user folders by username\n            imagesDownloaded = self.download_images()\n            videosDownloaded = self.download_videos()\n\n            Settings.print(\"downloaded messages\")\n            Settings.print(\"count: {}\".format(len(imagesDownloaded)+len(videosDownloaded)))\n        except Exception as e:\n            Settings.err_print(e)\n\n    def download_videos(self, destination=None):\n        \"\"\"Downloads all videos on the page\"\"\"\n\n        downloaded = []\n        downloadMe = []\n        try:\n            # find all video elements on page\n            # videos = self.browser.find_elements(By.TAG_NAME, \"video\")\n            # videos = self.browser.find_elements(By.CLASS_NAME, \"m-video-item\")\n            playButtons = self.browser.find_elements(By.CLASS_NAME, \"b-photos__item__play-btn\")\n            end = len(playButtons)\n\n            if len(playButtons) == 0:\n                Settings.warn_print(\"no videos found!\")\n                return downloaded\n            if not destination: destination = os.path.join(Settings.get_download_path(), \"videos\")\n            Path(destination).mkdir(parents=True, exist_ok=True)\n            i=0\n            for j in range(end):\n                src = \"\"\n                playButtons = self.browser.find_elements(By.CLASS_NAME, \"b-photos__item__play-btn\")\n\n                try:\n                    # click on play button\n                    # find new and only video ele on page\n                    self.move_to_then_click_element(playButtons[i])\n\n                    time.sleep(2)\n\n                    video = self.browser.find_element(By.CLASS_NAME, \"vjs-tech\")\n                    # try:\n                    # except Exception as e:\n                        # pass\n                        # try:\n                            # video = self.browser.find_element(By.TAG_NAME, \"video\")\n                        # except Exception as e:\n                            # pass\n\n                    # if not video: continue\n\n                    # if Driver.DOWNLOADING_MAX and i > Driver.DOWNLOAD_MAX_VIDEOS: break\n                    src = str(video.get_attribute(\"src\"))\n                    if not src or src == \"\" or src == \"None\" or \"http\" not in src: continue\n                    downloadMe.append(src)\n                except Exception as e:\n                    Settings.warn_print(e)\n                finally:\n                    # self.browser.switch_to.default_content()\n                    ActionChains(self.browser).send_keys(Keys.ESCAPE).perform()\n                    i+=1\n\n            downloadMe = list(set(downloadMe)) # remove duplicates\n\n            i=1\n            for src in downloadMe:\n                try:\n                    Settings.print_same_line(\"downloading video: {}/{}\".format(i, end))\n                    # Settings.print(\"Video: {}\".format(src[:src.find(\".mp4\")+4]))\n                    # Settings.dev_print(\"video src: {}\".format(src))\n                    # while os.path.isfile(\"{}/{}.mp4\".format(destination, i)):\n                        # i+=1\n                    wget.download(src, \"{}/{}.mp4\".format(destination, i), False)\n                    downloaded.append(i)\n                except Exception as e:\n                    Settings.print(\"\")            \n                    Settings.err_print(e)\n                    Settings.warn_print(\"skipped video: \"+src)\n                finally:\n                    ActionChains(self.browser).send_keys(Keys.ESCAPE).perform()\n                    i+=1\n            Settings.print(\"\")\n        except Exception as e:\n            Settings.err_print(e)\n        return downloaded\n\n    @staticmethod\n    def drag_and_drop_file(drop_target, path):\n        \"\"\"\n        Drag and drop the provided file path onto the provided element target.\n\n\n        Parameters\n        ----------\n        drop_target : WebElement\n            The web element to drop the file at path on\n\n        path : str\n            The file path to drag onto the web element\n\n        Returns\n        -------\n        bool\n            Whether or not dragging the file was successful\n\n        \"\"\"\n\n        # https://stackoverflow.com/questions/43382447/python-with-selenium-drag-and-drop-from-file-system-to-webdriver\n        JS_DROP_FILE = \"\"\"\n            var target = arguments[0],\n                offsetX = arguments[1],\n                offsetY = arguments[2],\n                document = target.ownerDocument || document,\n                window = document.defaultView || window;\n\n            var input = document.createElement('INPUT');\n            input.type = 'file';\n            input.onchange = function () {\n              var rect = target.getBoundingClientRect(),\n                  x = rect.left + (offsetX || (rect.width >> 1)),\n                  y = rect.top + (offsetY || (rect.height >> 1)),\n                  dataTransfer = { files: this.files };\n\n              ['dragenter', 'dragover', 'drop'].forEach(function (name) {\n                var evt = document.createEvent('MouseEvent');\n                evt.initMouseEvent(name, !0, !0, window, 0, 0, 0, x, y, !1, !1, !1, !1, 0, null);\n                evt.dataTransfer = dataTransfer;\n                target.dispatchEvent(evt);\n              });\n\n              setTimeout(function () { document.body.removeChild(input); }, 25);\n            };\n            document.body.appendChild(input);\n            return input;\n        \"\"\"\n        try:\n            Settings.maybe_print(\"dragging and dropping...\")\n            Settings.dev_print(\"drop target: {}\".format(drop_target.get_attribute(\"innerHTML\")))\n            # BUG: requires double to register file upload\n            file_input = drop_target.parent.execute_script(JS_DROP_FILE, drop_target, 0, 0)\n            file_input.send_keys(path)\n            file_input = drop_target.parent.execute_script(JS_DROP_FILE, drop_target, 50, 50)\n            file_input.send_keys(path)\n            Settings.debug_delay_check()\n            return True\n        except Exception as e:\n            Settings.err_print(e) \n        return False\n\n    def enter_text(self, text):\n        \"\"\"\n        Enter the provided text into the page's text area\n\n        Must be ran on a page with an OnlyFans text area.\n\n\n        Parameters\n        ----------\n        text : str\n            The text to enter\n\n        Returns\n        -------\n        bool\n            Whether or not entering the text was successful\n\n        \"\"\"\n\n        try:\n            Settings.dev_print(\"entering text: \"+text)\n            # extra redundancy in action chain for getting the text entry area\n            # for clearing text field with action chain:\n            # https://stackoverflow.com/questions/45690688/clear-selenium-action-chains\n            textEntry = self.browser.find_element(By.ID, \"new_post_text_input\")\n            action = ActionChains(self.browser)\n            action.move_to_element(textEntry)\n            action.click(on_element = textEntry)\n            action.double_click()\n            action.click_and_hold()\n            action.send_keys(Keys.CLEAR)\n            action.send_keys(str(text))\n            action.perform()\n            ## TODO\n            ## check text was entered properly\n            ## does not work\n            # print(self.browser.find_element(By.ID, \"new_post_text_input\").get_attribute('innerHTML'))\n            # Settings.debug_delay_check()\n            # print(self.browser.find_element(By.ID, \"new_post_text_input\").get_attribute('innerHTML'))\n            # if self.browser.find_element(By.ID, \"new_post_text_input\").get_attribute('innerHTML') != text:\n                # Settings.dev_print(\"failed to enter text\")\n                # return False  \n            Settings.dev_print(\"successfully entered text\")\n            return True\n        except Exception as e:\n            Settings.dev_print(e)\n        return False\n\n    @staticmethod\n    def error_checker(e):\n        \"\"\"\n        Custom error checker\n\n        Parameters\n        ----------\n        e : str\n            Error text\n\n        \"\"\"\n\n        if \"Unable to locate element\" in str(e):\n            Settings.warn_print(\"onlysnarf may require an update\")\n        if \"Message: \" in str(e): return\n        Settings.dev_print(e)\n\n    def error_window_upload(self):\n        \"\"\"Closes error window that appears during uploads for 'duplicate' files\"\"\"\n\n        try:\n            element = Element.get_element_by_name(\"errorUpload\")\n            error_buttons = self.browser.find_elements(By.CLASS_NAME, element.getClass())\n            Settings.dev_print(\"errors btns: {}\".format(len(error_buttons)))\n            if len(error_buttons) == 0: return True\n            for butt in error_buttons:\n                if butt.get_attribute(\"innerHTML\").strip() == \"Close\" and butt.is_enabled():\n                    Settings.maybe_print(\"upload error message, closing\")\n                    butt.click()\n                    Settings.maybe_print(\"success: upload error message closed\")\n                    time.sleep(0.5)\n                    return True\n            return False\n        except Exception as e:\n            Driver.error_checker(e)\n        return False\n\n    ######################\n    ##### Expiration #####\n    ######################\n\n    def expires(self, expiration):\n        \"\"\"\n        Enters the provided expiration duration for a post\n\n        Must be on home page\n\n        Parameters\n        ----------\n        expiration : int\n            The duration (in days) until the post expires\n        \n        Returns\n        -------\n        bool\n            Whether or not entering the expiration was successful\n\n        \"\"\"\n\n        if str(expiration) == \"0\" or not expiration: return True\n        try:\n            Settings.print(\"Expiration:\")\n            Settings.print(\"- Period: {}\".format(expiration))\n            # if expiration is 'no limit', then there's no expiration and hence no point here\n            if expiration == 999: return True\n\n            def enter_expiration(expires):\n                # enter duration\n                Settings.dev_print(\"entering expiration\")\n                action = ActionChains(self.browser)\n                action.click(on_element=self.find_element_to_click(\"expiresAdd\"))\n                action.pause(int(1))\n                action.send_keys(Keys.TAB)\n                action.send_keys(str(expires))\n                action.pause(int(1))\n                action.key_down(Keys.SHIFT).send_keys(Keys.TAB).key_up(Keys.SHIFT)\n                action.pause(int(1))\n                action.send_keys(Keys.ENTER)\n                action.perform()\n                Settings.dev_print(\"successfully entered expiration\")\n\n            # not really necessary with 'Clear' button\n            def cancel_expiration():\n                #icon-close\n                elements = self.browser.find_elements(By.TAG_NAME, \"use\")\n                element = [elem for elem in elements if '#icon-close' in str(elem.get_attribute('href'))][0]\n                ActionChains(self.browser).move_to_element(element).click().perform()\n\n            enter_expiration(expiration)\n            Settings.debug_delay_check()\n            Settings.dev_print(\"### Expiration Successful ###\")\n            return True\n        except Exception as e:\n            Driver.error_checker(e)\n            Settings.err_print(\"failed to enter expiration\")\n            try:\n                Settings.dev_print(\"canceling expiration\")\n                self.find_element_to_click(\"expiresCancel\").click()\n                Settings.dev_print(\"successfully canceled expiration\")\n                Settings.dev_print(\"### Expiration Successful Failure ###\")\n            except: \n                Settings.dev_print(\"### Expiration Failure Failure\")\n        return False\n\n    ######################################################################\n\n    def find_element_by_name(self, name):\n        \"\"\"\n        Find element on page by name\n\n        Does not auth check or otherwise change the focus\n\n        Parameters\n        ----------\n        name : str\n            The name of the element to reference from its /elements/element name\n\n        Returns\n        -------\n        Selenium.WebDriver.WebElement\n            The located web element if found by id, class name, or css selector\n\n        \"\"\"\n        element = Element.get_element_by_name(name)\n        if not element:\n            Settings.err_print(\"unable to find element reference\")\n            return None\n        # prioritize id over class name\n        eleID = None\n        try: eleID = self.browser.find_element(By.ID, element.getId())\n        except: eleID = None\n        if eleID: return eleID\n        for className in element.getClasses():\n            ele = None\n            eleCSS = None\n            try: ele = self.browser.find_element(By.CLASS_NAME, className)\n            except: ele = None\n            # try: eleCSS = self.browser.find_element(By.CSS_SELECTOR, className)\n            # except: eleCSS = None\n            Settings.dev_print(\"class: {} - {}:css\".format(ele, eleCSS))\n            if ele: return ele\n            # if eleCSS: return eleCSS\n        raise Exception(\"unable to locate element\")\n\n    def find_elements_by_name(self, name):\n        \"\"\"\n        Find elements on page by name. Does not change window focus.\n\n        Parameters\n        ----------\n        name : str\n            The name of the element to reference from its /elements/element name\n\n        Returns\n        -------\n        list\n            A list of the located Selenium.WebDriver.WebElements as found by id, class name, or css selector. \n            Elements must also be displayed\n\n        \"\"\"\n\n        element = Element.get_element_by_name(name)\n        if not element:\n            Settings.err_print(\"unable to find element reference\")\n            return []\n        eles = []\n        for className in element.getClasses():\n            eles_ = []\n            elesCSS_ = []\n            try: eles_ = self.browser.find_elements(By.CLASS_NAME, className)\n            except: eles_ = []\n            # try: elesCSS_ = self.browser.find_elements(By.CSS_SELECTOR, className)\n            # except: elesCSS_ = []\n            Settings.dev_print(\"class: {} - {}:css\".format(len(eles_), len(elesCSS_)))\n            eles.extend(eles_)\n            # eles.extend(elesCSS_)\n        eles_ = []\n        for i in range(len(eles)):\n            # Settings.dev_print(\"ele: {} -> {}\".format(eles[i].get_attribute(\"innerHTML\").strip(), element.getText()))\n            if eles[i].is_displayed():\n                Settings.dev_print(\"found displayed ele: {}\".format(eles[i].get_attribute(\"innerHTML\").strip()))\n                eles_.append(eles[i])\n        if len(eles_) == 0:\n            raise Exception(\"unable to locate elements: {}\".format(name))\n        return eles_\n\n    def find_element_to_click(self, name, useId=False, offset=0):\n        \"\"\"\n        Find element on page by name to click\n\n        Does not auth check or otherwise change the focus. Checks that located element is properly \n        capable of being clicked.\n\n        Parameters\n        ----------\n        name : str\n            The name of the element to click as referenced from its /elements/element name\n\n        Returns\n        -------\n        Selenium.WebDriver.WebElements\n            The located web element that can be clicked\n\n        \"\"\"\n\n        Settings.dev_print(\"finding click: {}\".format(name))\n        element = Element.get_element_by_name(name)\n        if not element:\n            Settings.err_print(\"unable to find element reference - {}\".format(name))\n            return False\n        elements = element.getClasses()\n        if useId: elements = element.getIds()\n        for className in elements:\n            eles = []\n            elesCSS = []\n            try: eles = self.browser.find_elements(By.CLASS_NAME, className)\n            except: eles = []\n            # try: elesCSS = self.browser.find_elements(By.CSS_SELECTOR, className)\n            # except: elesCSS = []\n            Settings.dev_print(\"class: {} - {}:css\".format(len(eles), len(elesCSS)))\n            eles.extend(elesCSS)\n            for i in range(len(eles)):\n                i += offset\n                if i > len(eles): i = len(eles)\n                Settings.dev_print(\"ele: {} -> {}\".format(eles[i].get_attribute(\"innerHTML\").strip(), element.getText()))\n                if (eles[i].is_displayed() and element.getText() and str(element.getText().lower()) == eles[i].get_attribute(\"innerHTML\").strip().lower()) and eles[i].is_enabled():\n                    Settings.dev_print(\"found matching ele\")\n                    # Settings.dev_print(\"found matching ele: {}\".format(eles[i].get_attribute(\"innerHTML\").strip()))\n                    return eles[i]\n                elif (eles[i].is_displayed() and element.getText() and str(element.getText().lower()) in eles[i].get_attribute(\"innerHTML\").strip().lower()) and eles[i].is_enabled():\n                    Settings.dev_print(\"found matching(ish) ele\")\n                    # Settings.dev_print(\"found matching ele: {}\".format(eles[i].get_attribute(\"innerHTML\").strip()))\n                    return eles[i]\n                elif (eles[i].is_displayed() and element.getText() and str(element.getText().lower()) in eles[i].get_attribute(\"innerHTML\").strip().lower()):\n                    Settings.dev_print(\"found text ele\")\n                    # Settings.dev_print(\"found text ele: {}\".format(eles[i].get_attribute(\"innerHTML\").strip()))\n                    return eles[i]\n                elif eles[i].is_displayed() and not element.getText() and eles[i].is_enabled():\n                    Settings.dev_print(\"found enabled ele\")\n                    # Settings.dev_print(\"found enabled ele: {}\".format(eles[i].get_attribute(\"innerHTML\").strip()))\n                    return eles[i]\n            if len(eles) > 0: return eles[offset]\n            Settings.dev_print(\"unable to find element - {}\".format(name))\n        raise Exception(\"unable to locate element - {}\".format(name))\n\n    ######################################################################\n\n    @staticmethod\n    def get_driver():\n        \"\"\"\n        Return an existing driver, if not create one\n\n        Returns\n        -------\n        classes.driver\n            The default driver object.\n\n\n        \"\"\"\n\n        if len(Driver.DRIVERS) > 0:\n            return Driver.DRIVERS[0]\n        return Driver()\n\n    # waits for page load\n    def get_page_load(self):\n        \"\"\"Attempt to generic page load\"\"\"\n\n        time.sleep(2)\n        try: WebDriverWait(self.browser, 60*3, poll_frequency=10).until(EC.visibility_of_element_located((By.CLASS_NAME, \"main-wrapper\")))\n        except Exception as e: Settings.dev_print(e)\n\n    def handle_alert(self):\n        \"\"\"Switch to alert pop up\"\"\"\n\n        try:\n            alert_obj = self.browser.switch_to.alert or None\n            if alert_obj:\n                alert_obj.accept()\n        except: pass\n\n    ##############\n    ### Go Tos ###\n    ##############\n\n    def go_to_home(self, force=False):\n        \"\"\"\n        Go to home page\n\n        If already at home don't go unless forced\n\n        Parameters\n        ----------\n        force : bool\n            Force page goto even if already at url\n\n        \"\"\"\n\n        def goto():\n            Settings.maybe_print(\"goto -> onlyfans.com\")\n            try:\n                self.browser.set_page_load_timeout(10)\n                self.browser.get(ONLYFANS_HOME_URL)\n            except TimeoutException:\n                Settings.dev_print(\"timed out waiting for page to check login element\")\n            except WebDriverException as e:\n                Settings.dev_print(\"error fetching home page\")\n                Settings.err_print(e)\n            # self.open_tab(ONLYFANS_HOME_URL)\n            self.handle_alert()\n            self.get_page_load()\n        if force: return goto()\n        if self.search_for_tab(ONLYFANS_HOME_URL):\n            Settings.maybe_print(\"found -> /\")\n            return\n        Settings.dev_print(\"current url: {}\".format(self.browser.current_url))\n        if str(self.browser.current_url) == str(ONLYFANS_HOME_URL):\n            Settings.maybe_print(\"at -> onlyfans.com\")\n            self.browser.execute_script(\"window.scrollTo(0, 0);\")\n        else: goto()        \n        \n    def go_to_page(self, page):\n        \"\"\"\n        Go to page\n\n        If already at page don't go\n\n        Parameters\n        ----------\n        page : str\n            The url of the OnlyFans 'page' to go to\n\n        \"\"\"\n\n        self.auth()\n        if self.search_for_tab(page):\n            Settings.maybe_print(\"found -> {}\".format(page))\n            return\n        if str(self.browser.current_url) == str(page) or str(page) in str(self.browser.current_url):\n            Settings.maybe_print(\"at -> {}\".format(page))\n            self.browser.execute_script(\"window.scrollTo(0, 0);\")\n        else:\n            Settings.maybe_print(\"goto -> {}\".format(page))\n            self.open_tab(page)\n            self.handle_alert()\n            self.get_page_load()\n\n    def go_to_profile(self):\n        \"\"\"Go to OnlyFans profile page\"\"\"\n\n        self.auth()\n        username = Settings.get_username()\n        if str(username) == \"\":\n            username = self.get_username()\n        page = \"{}/{}\".format(ONLYFANS_HOME_URL, username)\n        if self.search_for_tab(page):\n            Settings.maybe_print(\"found -> /{}\".format(username))\n            return\n        if str(username) in str(self.browser.current_url):\n            Settings.maybe_print(\"at -> {}\".format(username))\n            self.browser.execute_script(\"window.scrollTo(0, 0);\")\n        else:\n            Settings.maybe_print(\"goto -> {}\".format(username))\n            # self.browser.get(\"{}{}\".format(ONLYFANS_HOME_URL, username))\n            self.open_tab(page)\n            # self.handle_alert()\n            # self.get_page_load()\n\n    # onlyfans.com/my/settings\n    def go_to_settings(self, settingsTab):\n        \"\"\"\n        Go to settings tab on settings page\n\n        If already at tab, stay\n\n        Parameters\n        ----------\n        settingsTab : str\n            The name of the Settings tab to go to\n\n        \"\"\"\n\n        self.auth()\n        if self.search_for_tab(\"{}{}\".format(ONLYFANS_SETTINGS_URL, settingsTab)):  \n            Settings.maybe_print(\"found -> settings/{}\".format(settingsTab))\n            return\n        if str(ONLYFANS_SETTINGS_URL) in str(self.browser.current_url) and str(settingsTab) == \"profile\":\n            Settings.maybe_print(\"at -> onlyfans.com/settings/{}\".format(settingsTab))\n            self.browser.execute_script(\"window.scrollTo(0, 0);\")\n        else:\n            if str(settingsTab) == \"profile\": settingsTab = \"\"\n            Settings.maybe_print(\"goto -> onlyfans.com/settings/{}\".format(settingsTab))\n            self.go_to_page(\"{}{}\".format(ONLYFANS_SETTINGS_URL, settingsTab))\n\n    def search_for_tab(self, page):\n        \"\"\"\n        Search for (and goto if exists) tab in Driver.tabs cache\n\n        Parameters\n        ----------\n        page : str\n            The url of the OnlyFans 'page' to go to\n\n        Returns\n        -------\n        bool\n            Whether or not the tab exists\n\n\n        \"\"\"\n\n        original_handle = self.browser.current_window_handle\n        Settings.dev_print(\"searching for page: {}\".format(page))\n        Settings.dev_print(\"tabs: {}\".format(self.tabs))\n        Settings.dev_print(\"handles: {}\".format(self.browser.window_handles))\n        try:\n            Settings.dev_print(\"checking tabs...\")\n            for page_, handle, value in self.tabs:\n                Settings.dev_print(\"{} = {}\".format(page_, page))\n                if str(page_) in str(page):\n                    self.browser.switch_to.window(handle)\n                    value += 1\n                    Settings.dev_print(\"successfully located tab in cache: {}\".format(page))\n                    return True\n            Settings.dev_print(\"checking handles...\")\n            for handle in self.browser.window_handles:\n                Settings.dev_print(handle)\n                self.browser.switch_to.window(handle)\n                if str(page) in str(self.browser.current_url):\n                    Settings.dev_print(\"successfully located tab in handles: {}\".format(page))\n                    return True\n            Settings.dev_print(\"failed to locate tab: {}\".format(page))\n            self.browser.switch_to.window(original_handle)\n        except Exception as e:\n            # print(e)\n            # if \"Unable to locate window\" not in str(e):\n            Settings.dev_print(e)\n        return False\n\n    def open_tab(self, url):\n        \"\"\"\n        Open new tab of url\n\n        Parameters\n        ----------\n        url : str\n            The url to open in a new tab\n\n        Returns\n        -------\n        bool\n            Whether or not the tab was opened successfully\n\n        \"\"\"\n\n        Settings.maybe_print(\"tab -> {}\".format(url))\n        # self.browser.find_element(By.TAG_NAME, 'body').send_keys(Keys.CONTROL + 't')\n        # self.browser.get(url)\n        # https://stackoverflow.com/questions/50844779/how-to-handle-multiple-windows-in-python-selenium-with-firefox-driver\n        windows_before  = self.browser.current_window_handle\n        Settings.dev_print(\"current window handle is : %s\" %windows_before)\n        windows = self.browser.window_handles\n        self.browser.execute_script('''window.open(\"{}\",\"_blank\");'''.format(url))\n        # self.browser.execute_script(\"window.open('{}')\".format(url))\n        self.handle_alert()\n        self.get_page_load()\n        WebDriverWait(self.browser, 10).until(EC.number_of_windows_to_be(len(windows)+1))\n        windows_after = self.browser.window_handles\n        new_window = [x for x in windows_after if x not in windows][0]\n        # self.browser.switch_to.window(new_window) <!---deprecated>\n        self.browser.switch_to.window(new_window)\n        Settings.dev_print(\"page title after tab switching is : %s\" %self.browser.title)\n        Settings.dev_print(\"new window handle is : %s\" %new_window)\n        # if len(self.tabs) >= Driver.MAX_TABS:\n        #     least = self.tabs[0]\n        #     for i, tab in enumerate(self.tabs):\n        #         if int(tab[2]) < int(least[2]):\n        #             least = tab\n        #     self.tabs.remove(least)\n        # self.tabs.append([url, new_window, 0]) # url, window_handle, use count\n    \n    ##################\n    ###### Login #####\n    ##################\n\n    def login(self):\n        \"\"\"\n        Logs into OnlyFans account provided via args and chosen method.\n\n        Checks if already logged in first. Logs in via requested method or tries all available.\n\n        Returns\n        -------\n        bool\n            Whether or not the login was successful\n\n        \"\"\"\n\n        if self.logged_in: return True\n        Settings.print('logging into OnlyFans for {}...'.format(Settings.get_username()))\n\n        def loggedin_check():\n            \"\"\"Check if already logged in before attempting to login again\"\"\"\n\n            self.go_to_home(force=True)\n            try:\n                # ele = self.browser.find_element(By.CLASS_NAME, Element.get_element_by_name(\"loginCheck\").getClass())\n                WebDriverWait(self.browser, 10, poll_frequency=1).until(EC.visibility_of_element_located((By.CLASS_NAME, Element.get_element_by_name(\"loginCheck\").getClass())))\n                # if ele: \n                Settings.print(\"already logged into OnlyFans!\")\n                return True\n            except TimeoutException as te:\n                Settings.dev_print(str(te))\n            except Exception as e:\n                Settings.dev_print(e)\n            return False\n\n        def login_check(which):\n            \"\"\"\n            Check after login attempt for successful home page\n\n            Returns\n            -------\n            bool\n                Whether or not the login check was successful\n\n            \"\"\"\n\n\n            def try_phone():\n                Settings.maybe_print(\"verifying phone number...\")\n                element = self.browser.switch_to.active_element\n                element.send_keys(str(Settings.get_phone_number()))\n                element.send_keys(Keys.ENTER)\n\n            # TODO: requires testing, not successfuly receiving email w/ code to test further\n            def try_email():\n                Settings.print(\"email verification required - please enter the code sent to your email!\")\n                element = self.browser.switch_to.active_element\n                element.send_keys(str(input(\"Enter code: \")))\n                element.send_keys(Keys.SHIFT + Keys.TAB)\n                element.send_keys(Keys.ENTER)\n\n            try:\n                Settings.dev_print(\"waiting for login check...\")\n                WebDriverWait(self.browser, 30, poll_frequency=2).until(EC.visibility_of_element_located((By.CLASS_NAME, Element.get_element_by_name(\"loginCheck\").getClass())))\n                Settings.print(\"OnlyFans login successful!\")\n                Settings.dev_print(\"login successful - {}\".format(which))\n                return True\n            except TimeoutException as te:\n                bodyText = self.browser.find_element(By.TAG_NAME, \"body\").text\n                Settings.dev_print(bodyText)\n                # check for phone number page\n                if \"Verify your identity by entering the phone number associated with your Twitter account.\" in str(bodyText):\n                    try_phone()\n                    login_check(which)\n                # check for email notification\n                elif \"Check your email\" in str(bodyText):\n                    try_email()\n                    login_check(which)\n                else:\n                    # Settings.dev_print(str(te))\n                    Settings.print(\"Login Failure: Timed Out! Please check your credentials.\")\n                    Settings.print(\": If the problem persists, OnlySnarf may require an update.\")\n                    # output page text for debugging\n                return False\n            except Exception as e:\n                Driver.error_checker(e)\n                Settings.print(\"OnlyFans login failure: OnlySnarf may require an update\")\n                return False\n            return True\n        \n        def via_form():\n            \"\"\"\n            Logs in via OnlyFans username & password form\n            \n            Returns\n            -------\n            bool\n                Whether or not the login attempt was successful\n\n            \"\"\"\n\n            try:\n                Settings.maybe_print(\"logging in via form\")\n                username = str(Settings.get_username_onlyfans())\n                password = str(Settings.get_password())\n                if str(username) == \"\" or str(password) == \"\":\n                    Settings.err_print(\"missing onlyfans login info\")\n                    return False\n                self.go_to_home()\n                WAIT = WebDriverWait(self.browser, 10, poll_frequency=2)\n                Settings.dev_print(\"finding username & password\")\n                usernameField = WAIT.until(EC.presence_of_element_located((By.NAME, \"email\")))\n                passwordField = WAIT.until(EC.presence_of_element_located((By.NAME, \"password\")))\n                usernameField.click()\n                usernameField.send_keys(username)\n                Settings.dev_print(\"username entered\")\n                passwordField.click()\n                passwordField.send_keys(password)\n                Settings.dev_print(\"password entered\")\n                passwordField.send_keys(Keys.ENTER)\n                def check_captcha():\n                    try:\n                        time.sleep(10) # wait extra long to make sure it doesn't verify obnoxiously\n                        el = self.browser.find_element(\"name\", \"password\")\n                        if not el: return # likely logged in without captcha\n                        Settings.print(\"waiting for captcha completion by user...\")\n                        # action = webdriver.common.action_chains.ActionChains(self.browser)\n                        action = ActionChains(self.browser)\n                        action.move_to_element_with_offset(el, 40, 100)\n                        action.click()\n                        action.perform()\n                        time.sleep(10)\n                        sub = None\n                        submit = self.browser.find_element(By.CLASS_NAME, \"g-btn.m-rounded.m-flex.m-lg\")\n                        for ele in submit:\n                            if str(ele.get_attribute(\"innerHTML\")) == \"Login\":\n                                sub = ele\n                        if sub and sub.is_enabled():\n                            submit.click()\n                        elif sub and not sub.is_enabled():\n                            Settings.err_print(\"unable to login via form - captcha\")\n                    except Exception as e:\n                        if \"Unable to locate element: [name=\\\"password\\\"]\" not in str(e):\n                            Settings.dev_print(e)\n                check_captcha()\n                return login_check(\"form\")\n            except Exception as e:\n                Settings.dev_print(\"form login failure\")\n                Driver.error_checker(e)\n            return False\n\n        def via_google():\n            \"\"\"\n            Logs in via linked Google account. (doesn't work)\n            \n            Returns\n            -------\n            bool\n                Whether or not the login attempt was successful\n\n            \"\"\"\n\n            try:\n                Settings.maybe_print(\"logging in via google\")\n                username = str(Settings.get_username_google())\n                password = str(Settings.get_password_google())\n                if str(username) == \"\" or str(password) == \"\":\n                    Settings.err_print(\"missing google login info\")\n                    return False\n                # self.go_to_home()\n                elements = self.browser.find_elements(By.TAG_NAME, \"a\")\n                [elem for elem in elements if '/auth/google' in str(elem.get_attribute('href'))][0].click()\n                time.sleep(5)\n                username_ = self.browser.switch_to.active_element\n                # then click username spot\n                username_.send_keys(username)\n                username_.send_keys(Keys.ENTER)\n                Settings.dev_print(\"username entered\")\n                time.sleep(2)\n                password_ = self.browser.switch_to.active_element\n                # fill in password and hit the login button \n                password_.send_keys(password)\n                Settings.dev_print(\"password entered\")\n                password_.send_keys(Keys.ENTER)\n                return login_check(\"google\")\n            except Exception as e:\n                Settings.dev_print(\"google login failure\")\n                Driver.error_checker(e)\n            return False\n\n        def via_twitter():\n            \"\"\"\n            Logs in via linked Twitter account\n            \n            Returns\n            -------\n            bool\n                Whether or not the login attempt was successful\n\n            \"\"\"\n\n            try:\n                Settings.maybe_print(\"logging in via twitter\")\n                username = str(Settings.get_username_twitter())\n                password = str(Settings.get_password_twitter())\n                if str(username) == \"\" or str(password) == \"\":\n                    Settings.err_print(\"missing twitter login info\")\n                    return False\n                # self.go_to_home()\n                # rememberMe checkbox doesn't actually cause login to be remembered\n                # rememberMe = self.browser.find_element_by_xpath(Element.get_element_by_name(\"rememberMe\").getXPath())\n                # if not rememberMe.is_selected():\n                    # rememberMe.click()\n                # if str(Settings.MANUAL) == \"True\":\n                    # Settings.print(\"Please Login\")\n                elements = self.browser.find_elements(By.TAG_NAME, \"a\")\n                [elem for elem in elements if '/twitter/auth' in str(elem.get_attribute('href'))][0].click()\n                self.browser.find_element(\"name\", \"session[username_or_email]\").send_keys(username)\n                Settings.dev_print(\"username entered\")\n                # fill in password and hit the login button \n                password_ = self.browser.find_element(\"name\", \"session[password]\")\n                password_.send_keys(password)\n                Settings.dev_print(\"password entered\")\n                password_.send_keys(Keys.ENTER)\n                return login_check(\"twitter\")\n            except Exception as e:\n                Settings.dev_print(\"twitter login failure\")\n                Driver.error_checker(e)\n            return False\n\n        # this needs to go after them because they reconnect then need to login check\n        # if Settings.get_browser_type() == \"reconnect\" or Settings.get_browser_type() == \"remote\" or \n\n        try:\n            if loggedin_check():\n                self.logged_in = True\n                return True\n        except Exception as e:\n            Settings.err_print(e)\n            return False\n\n        # if str(Settings.is_cookies()) == \"True\":\n        #     self.cookies_load()\n        #     if loggedin_check():\n        #         self.logged_in = True\n        #         return True\n        #     elif str(Settings.is_cookies()) == \"True\" and str(Settings.is_debug(\"cookies\")) == \"True\":\n        #         Settings.err_print(\"failed to login from cookies!\")\n        #         Settings.set_cookies(False)\n        #         return False\n        #     elif str(Settings.is_cookies()) == \"True\":\n        #         Settings.set_cookies(False)\n        #         Settings.maybe_print(\"failed to login from cookies!\")\n\n        Settings.dev_print(\"attempting login...\")\n        successful = False\n        try:\n            if Settings.get_login_method() == \"auto\":\n                successful = via_form()\n                if not successful: successful = via_twitter()\n                if not successful: successful = via_google()\n            elif Settings.get_login_method() == \"onlyfans\":\n                successful = via_form()\n            elif Settings.get_login_method() == \"twitter\":\n                successful = via_twitter()\n            elif Settings.get_login_method() == \"google\":\n                successful = via_google()\n            if successful:\n                self.logged_in = True\n                return True\n        except Exception as e:\n            Driver.error_checker(e)\n        Settings.err_print(\"OnlyFans login failed!\")\n        return False\n\n    ####################\n    ##### Messages #####\n    ####################\n\n    @staticmethod\n    def message(username, user_id=None):\n        \"\"\"\n        Start a message to the username (or group of users) or user_id.\n\n        Parameters\n        ----------\n        username : str\n            The username of the user to message\n        user_id : str\n            The user id of the user to message\n\n        Returns\n        -------\n        bool\n            Whether or not the message was successful\n\n        \"\"\"\n\n        if not username and not user_id:\n            Settings.err_print(\"missing user to message\")\n            return False\n        try:\n            driver = Driver.get_driver()\n            driver.auth()\n            driver.go_to_home(force=True)\n            Settings.dev_print(\"attempting to start message for {}...\".format(username))\n            type__ = None # default\n            # if the username is a key string it will behave differently\n            if str(username).lower() == \"all\": type__ = \"messageAll\"\n            elif str(username).lower() == \"recent\": type__ = \"messageRecent\"\n            elif str(username).lower() == \"favorite\": type__ = \"messageFavorite\"\n            elif str(username).lower() == \"renew on\": type__ = \"messageRenewers\"\n            elif str(username).lower() == \"random\":\n                from ..classes.user import User\n                username = User.get_random_user().username\n            successful = False\n            if type__ != None:\n                driver.go_to_page(ONLYFANS_NEW_MESSAGE_URL)\n                Settings.dev_print(\"clicking message type: {}\".format(username))\n                driver.find_element_to_click(type__).click()\n                successful = True\n            else:\n                successful = driver.message_user(username, user_id=user_id)\n            if successful: Settings.dev_print(\"started message for {}\".format(username))\n            else: Settings.warn_print(\"failed to start message for {}!\".format(username))\n            return successful\n        except Exception as e:\n            Driver.error_checker(e)\n            Settings.err_print(\"failure to message - {}\".format(username))\n        return False\n     \n    def message_clear(self):\n        \"\"\"\n        Enter the provided text into the message on the page\n\n        Parameters\n        ----------\n        text : str\n            The text to enter\n\n        Returns\n        -------\n        bool\n            Whether or not entering the text was successful\n\n        \"\"\"\n\n        def close_icons():\n            try:\n                #icon-close\n                elements = self.browser.find_elements(By.TAG_NAME, \"use\")\n                for element in [elem for elem in elements if '#icon-close' in str(elem.get_attribute('href'))]:\n                    ActionChains(self.browser).move_to_element(element).click().perform()\n            except Exception as e:\n                # Settings.err_print(e)\n                Settings.dev_print(\"unable to click: #icon-close\")\n\n        def clear_text():\n            try:\n                ActionChains(self.browser).move_to_element(self.browser.find_element(By.ID, \"new_post_text_input\")).double_click().click_and_hold().send_keys(Keys.CLEAR).perform()\n            except Exception as e:\n                # Settings.err_print(e)\n                Settings.dev_print(\"unable to clear text\")\n\n        try:\n            Settings.dev_print(\"clearing message\")\n            clearButton = [ele for ele in self.browser.find_elements(By.TAG_NAME, \"button\") if \"Clear\" in ele.get_attribute(\"innerHTML\") and ele.is_enabled()]\n            if len(clearButton) > 0:\n                Settings.dev_print(\"clicking clear button...\")\n                clearButton[0].click()\n            else:\n                Settings.dev_print(\"refreshing page and clearing text...\")\n                self.go_to_home(force=True)\n                clear_text()\n                close_icons()\n            Settings.dev_print(\"successfully cleared message\")\n            # return True\n        except Exception as e:\n            Driver.error_checker(e)\n            Settings.warn_print(\"failure to clear message\")\n        # return False\n\n    def message_confirm(self):\n        \"\"\"\n        Wait for the message open on the page's Confirm button to be clickable and click it\n\n        Returns\n        -------\n        bool\n            Whether or not the message confirmation was successful\n\n        \"\"\"\n\n        try:\n            Settings.dev_print(\"waiting for message confirm to be clickable...\")\n            confirm = WebDriverWait(self.browser, int(Settings.get_upload_max_duration()), poll_frequency=3).until(EC.element_to_be_clickable((By.CLASS_NAME, Element.get_element_by_name(\"new_message\").getClass())))\n            Settings.dev_print(\"message confirm is clickable\")\n            if str(Settings.is_debug()) == \"True\":\n                Settings.debug_delay_check()\n                Settings.print('skipping message (debug)')\n                self.message_clear()\n                return True\n            Settings.dev_print(\"clicking confirm\")\n            confirm.click()\n            Settings.print('OnlyFans message sent!')\n            return True\n        except TimeoutException:\n            Settings.warn_print(\"timed out waiting for message confirm!\")\n        except Exception as e:\n            Driver.error_checker(e)\n            Settings.err_print(\"failure to confirm message!\")\n        self.message_clear()\n        return False\n\n    def message_price(self, price):\n        \"\"\"\n        Enter the provided price into the message on the page\n\n        Parameters\n        ----------\n        price : str\n            The price to enter in dollars\n\n        Returns\n        -------\n        bool\n            Whether or not entering the price was successful\n\n        \"\"\"\n\n        try:\n            if not price or price == None or str(price) == \"None\":\n                Settings.err_print(\"missing price\")\n                return False\n            time.sleep(1) # prevents delay from inputted text preventing buttom from being available to click\n            try:\n                Settings.dev_print(\"clearing any preexisting price...\")\n                self.browser.find_element(By.CLASS_NAME, \"m-btn-remove\").click()\n            except Exception as e:\n                Settings.dev_print(e)\n            Settings.dev_print(\"entering price...\")\n            self.browser.find_element(By.CLASS_NAME, \"b-make-post__actions__btns\").find_elements(By.XPATH, \"./child::*\")[7].click()\n            priceText = WebDriverWait(self.browser, 10, poll_frequency=2).until(EC.element_to_be_clickable(self.browser.find_element(By.ID, \"priceInput_1\")))\n            priceText.click()\n            priceText.send_keys(str(price))\n            Settings.dev_print(\"entered price\")\n            Settings.debug_delay_check()\n            Settings.dev_print(\"saving price...\")\n            self.find_element_to_click(\"priceSave\").click()    \n            Settings.dev_print(\"saved price\")\n            return True\n        except Exception as e:\n            Driver.error_checker(e)\n            Settings.err_print(\"failure to enter price\")\n        return False\n\n    def message_text(self, text):\n        \"\"\"\n        Enter the provided text into the message on the page\n\n        Parameters\n        ----------\n        text : str\n            The text to enter\n\n        Returns\n        -------\n        bool\n            Whether or not entering the text was successful\n\n        \"\"\"\n\n        try:\n            if not text or text == None or str(text) == \"None\" or str(text) == \"\":\n                Settings.err_print(\"missing text for message!\")\n                return False\n            Settings.dev_print(\"entering text\")\n            ActionChains(self.browser).move_to_element(self.browser.find_element(By.ID, \"new_post_text_input\")).double_click().click_and_hold().send_keys(Keys.CLEAR).send_keys(str(text)).perform()\n            Settings.dev_print(\"successfully entered text\")\n            return True\n        except Exception as e:\n            Driver.error_checker(e)\n            Settings.err_print(\"failure to enter message\")\n        return False\n\n    def message_user_by_id(self, user_id=None):\n        \"\"\"\n        Message the provided user id\n\n        Parameters\n        ----------\n        user_id : str\n            The user id of the user to message\n\n        Returns\n        -------\n        bool\n            Whether or not messaging the user was successful\n\n        \"\"\"\n\n        user_id = str(user_id).replace(\"@u\",\"\").replace(\"@\",\"\")\n        if not user_id or user_id == None or str(user_id) == \"None\":\n            Settings.err_print(\"missing user id!\")\n            return False\n        try:\n            self.go_to_page(\"{}{}\".format(ONLYFANS_CHAT_URL, user_id))\n            Settings.dev_print(\"successfully messaging user id: {}\".format(user_id))\n            return True\n        except Exception as e:\n            Driver.error_checker(e)\n            Settings.err_print(\"failed to message user by id!\")\n        return False\n\n    def message_user(self, username, user_id=None):\n        \"\"\"\n        Message the matching username or user id\n\n        Parameters\n        ----------\n        username : str\n            The username of the user to message\n        user_id : str\n            The user id of the user to message\n\n        Returns\n        -------\n        bool\n            Whether or not messaging the user was successful\n\n        \"\"\"\n\n        Settings.dev_print(\"username: {} : {}: user_id\".format(username, user_id))\n        if user_id and str(user_id) != \"None\": return self.message_user_by_id(user_id=user_id)\n        if not username:\n            Settings.err_print(\"missing username to message!\")\n            return False\n        try:\n            self.go_to_page(username)\n            time.sleep(5) # for whatever reason this constantly errors out from load times\n            elements = self.browser.find_elements(By.TAG_NAME, \"a\")\n            ele = [ele for ele in elements if ONLYFANS_CHAT_URL in str(ele.get_attribute(\"href\"))]\n            if len(ele) == 0:\n                Settings.warn_print(\"user cannot be messaged - unable to locate id\")\n                return False\n            ele = ele[0]\n            ele = ele.get_attribute(\"href\").replace(\"https://onlyfans.com\", \"\")\n            # clicking no longer works? just open href in self.browser\n            # Settings.dev_print(\"clicking send message\")\n            # ele.click()\n            # Settings.dev_print(ele.get_attribute(\"href\"))\n            Settings.maybe_print(\"user id found: {}\".format(ele.replace(ONLYFANS_HOME_URL2, \"\")))\n            self.go_to_page(ele)\n            Settings.dev_print(\"successfully messaging username: {}\".format(username))\n            return True\n        except Exception as e:\n            Driver.error_checker(e)\n            Settings.err_print(\"failed to message user\")\n        return False\n\n    @staticmethod\n    def messages_scan(num=0):\n        \"\"\"\n        Scan messages page for recent users\n\n        Parameters\n        ----------\n        num : int\n            The number of users to consider recent (doesn't work)\n\n        Returns\n        -------\n        list\n            The list of users found\n\n        \"\"\"\n\n        # go to /messages page\n        # get top n users\n        Settings.dev_print(\"scanning messages\")\n        users = []\n        try:\n            driver = Driver.get_driver()\n            driver.auth()\n            driver.go_to_page(\"/my/chats\")\n            users_ = driver.browser.find_elements(By.CLASS_NAME, \"g-user-username\")\n            Settings.dev_print(\"users: {}\".format(len(users_)))\n            user_ids = driver.browser.find_elements(By.CLASS_NAME, \"b-chats__item__link\")\n            Settings.dev_print(\"ids: {}\".format(len(user_ids)))\n            for user in user_ids:\n                if not user or not user.get_attribute(\"href\") or str(user.get_attribute(\"href\")) == \"None\": continue\n                users.append(str(user.get_attribute(\"href\")).replace(\"https://onlyfans.com/my/chats/chat/\", \"\"))\n            return users[:10]\n        except Exception as e:\n            Settings.print(e)\n            Driver.error_checker(e)\n            Settings.err_print(\"failed to scan messages\")\n        return users\n\n    def move_to_then_click_element(self, element):\n        \"\"\"\n        Move to then click element.\n        \n        From: https://stackoverflow.com/questions/44777053/selenium-movetargetoutofboundsexception-with-firefox\n\n        Parameters\n        ----------\n        element : Selenium.WebDriver.WebElement\n            The element to move to then click\n\n        \"\"\"\n\n        def scroll_shim(passed_in_driver, object):\n            x = object.location['x']\n            y = object.location['y']\n            scroll_by_coord = 'window.scrollTo(%s,%s);' % (\n                x,\n                y\n            )\n            scroll_nav_out_of_way = 'window.scrollBy(0, -120);'\n            passed_in_driver.execute_script(scroll_by_coord)\n            passed_in_driver.execute_script(scroll_nav_out_of_way)\n        #\n        try:\n            ActionChains(self.browser).move_to_element(element).click().perform()\n            return True\n        except Exception as e:\n            # Settings.dev_print(e)\n            # if 'firefox' in self.browser.capabilities['browserName']:\n            try:\n                scroll_shim(self.browser, element)\n                ActionChains(self.browser).move_to_element(element).click().perform()\n            except Exception as e:\n                pass\n                # Settings.dev_print(e)\n                self.browser.execute_script(\"arguments[0].scrollIntoView();\", element)\n                # try:\n                #     self.browser.find_element(By.TAG_NAME, 'body').send_keys(Keys.CONTROL + Keys.HOME)\n                #     ActionChains(self.browser).move_to_element(element).click().perform()\n                # except Exception as e:\n                #     Settings.dev_print(e)\n        return False\n\n    ####################################################################################################\n    ####################################################################################################\n    ####################################################################################################\n\n    # tries both and throws error for not found element internally\n    def open_more_options(self):\n        \"\"\"\n        Click to open more options on a post.\n\n        Returns\n        -------\n        bool\n            Whether or not opening more options was successful\n\n        \"\"\"\n\n        def option_one():\n            \"\"\"Click on '...' element\"\"\"\n\n            Settings.dev_print(\"opening options (1)\")\n            moreOptions = self.find_element_to_click(\"moreOptions\")\n            if not moreOptions: return False    \n            moreOptions.click()\n            Settings.dev_print(\"successfully opened more options (1)\")\n            return True\n        def option_two():\n            \"\"\"Click in empty space\"\"\"\n\n            Settings.dev_print(\"opening options (2)\")\n            moreOptions = self.browser.find_element(By.ID, Element.get_element_by_name(\"enterText\").getId())\n            if not moreOptions: return False    \n            moreOptions.click()\n            Settings.dev_print(\"successfully opened more options (2)\")\n            return True\n        try:\n            successful = option_one()\n            if not successful: return option_two()\n        except Exception as e:\n            try:\n                return option_two()\n            except Exception as e:    \n                Driver.error_checker(e)\n                raise Exception(\"unable to locate 'More Options' element\")\n\n    ################\n    ##### Poll #####\n    ################\n\n    def poll(self, poll):\n        \"\"\"\n        Enter the Poll object into the current post\n\n        Parameters\n        ----------\n        poll : dict\n            The values of the poll in a dict\n\n        Returns\n        -------\n        bool\n            Whether or not entering the poll was successful\n\n        \"\"\"\n\n        if str(poll) == \"None\" or not poll: return True\n        try:\n            Settings.print(\"Poll:\")\n\n            # open the poll model\n            def open_model():\n                Settings.dev_print(\"adding poll\")\n                elements = self.browser.find_elements(By.TAG_NAME, \"use\")\n                element = [elem for elem in elements if '#icon-poll' in str(elem.get_attribute('href'))][0]\n                ActionChains(self.browser).move_to_element(element).click().perform()\n                time.sleep(1)\n\n            # open the poll duration\n            # can click anywhere near the top label\n            # TODO: finish updating any inserted wait times to be more dynamic\n            def add_duration(duration, wait=1):\n                # self.find_element_to_click(\"pollDuration\").click()\n                Settings.print(\"- Duration: {}\".format(duration))\n                Settings.dev_print(\"setting duration\")\n                action = ActionChains(self.browser)\n                action.click(on_element=self.browser.find_element(By.CLASS_NAME, \"b-post-piece__value\"))\n                action.pause(int(wait))\n                action.send_keys(Keys.TAB)\n                action.send_keys(str(duration))\n                action.perform()\n                # save the duration\n                Settings.dev_print(\"saving duration\")\n                self.find_element_to_click(\"pollSave\").click()\n                Settings.dev_print(\"successfully saved duration\")\n\n            def add_questions(questions):\n                Settings.dev_print(\"configuring question paths...\")\n                questionsElement = self.browser.find_elements(By.CLASS_NAME, \"v-text-field__slot\")\n                # add extra question space\n                OFFSET = 2 # number of preexisting questionsElement\n                if OFFSET + len(questions) > len(questionsElement):\n                    for i in range(OFFSET + len(questions)-len(questionsElement)):\n                        Settings.dev_print(\"adding question\")\n                        question_ = self.find_element_to_click(\"pollQuestionAdd\").click()\n                        Settings.dev_print(\"added question\")\n                # find the question inputs again\n                questionsElement = self.browser.find_elements(By.CLASS_NAME, \"v-text-field__slot\")\n                Settings.dev_print(\"question paths: {}\".format(len(questionsElement)))\n                # enter the questions\n                i = 0\n                Settings.dev_print(\"questions: {}\".format(questions))\n                Settings.print(\"- Questions:\")\n                for question in list(questions):\n                    Settings.print(\"> {}\".format(question))\n                    Settings.dev_print(\"entering question: {}\".format(question))\n                    questionsElement[i].find_elements(By.XPATH, \"./child::*\")[0].send_keys(str(question))\n                    Settings.dev_print(\"entered question\")\n                    time.sleep(1)\n                    i+=1\n                Settings.dev_print(\"successfully entered questions\")\n\n            open_model()\n            Settings.debug_delay_check()\n            add_duration(poll[\"duration\"])\n            Settings.debug_delay_check()\n            add_questions(poll[\"questions\"])\n            Settings.debug_delay_check()\n\n            if str(Settings.is_debug()) == \"True\":\n                Settings.maybe_print(\"skipping poll (debug)\")\n                cancel = self.find_element_to_click(\"pollCancel\")\n                cancel.click()\n            Settings.dev_print(\"### Poll Successful ###\")\n            return True\n        except Exception as e:\n            Driver.error_checker(e)\n            Settings.err_print(\"failed to enter poll!\")\n        return False\n\n    ################\n    ##### Post #####\n    ################\n\n    @staticmethod\n    def post(message):\n        \"\"\"\n        Post the message to OnlyFans.\n\n        Optionally tweet if enabled. A message must contain text and can contain:\n        - files\n        - keywords\n        - performers\n        - expiration\n        - schedule\n        - poll\n\n        Parameters\n        ----------\n        message : dict\n            The message values to be entered into the post \n\n        Returns\n        -------\n        bool\n            Whether or not the post was successful\n\n        \"\"\"\n\n        Settings.dev_print(\"posting...\")\n        driver = Driver.get_driver()\n        driver.auth()\n\n\n        ## TODO\n        # add check for clearing any text or images already in post field\n        driver.message_clear()\n\n        # try:\n        #     driver.find_element_to_click(\"postCancel\").click()\n        # except Exception as e:\n        #     Settings.dev_print(e)\n\n        #################### Formatted Text ####################\n        Settings.print(\"====================\")\n        Settings.print(\"Posting:\")\n        Settings.print(\"- Files: {}\".format(len(message[\"files\"])))\n        Settings.print(\"- Performers: {}\".format(message[\"performers\"]))\n        Settings.print(\"- Keywords: {}\".format(message[\"keywords\"]))\n        Settings.print(\"- Text: {}\".format(message[\"text\"]))\n        Settings.print(\"- Tweeting: {}\".format(Settings.is_tweeting()))\n        ## Expires, Schedule, Poll ##\n        if not driver.expires(message[\"expiration\"]): return False\n        if message[\"schedule\"] and message[\"schedule\"].validate() and not driver.schedule(message[\"schedule\"].get()): return False\n        if message[\"poll\"].validate() and not driver.poll(message[\"poll\"].get()): return False\n        Settings.print(\"====================\")\n        ############################################################\n\n        ## Tweeting ##\n        ## TODO\n        ## test this\n        # if str(Settings.is_tweeting()) == \"True\":\n            # Settings.dev_print(\"tweeting...\")\n            # twitter tweet button is 1st, post is 2nd\n            # ActionChains(driver.browser).move_to_element(driver.browser.find_element(By.CLASS_NAME, \"b-btns-group\").find_elements(By.XPATH, \"./child::*\")[0]).click().perform()\n            # WebDriverWait(driver.browser, 30, poll_frequency=3).until(EC.element_to_be_clickable((By.XPATH, Element.get_element_by_name(\"tweet\").getXPath()))).click()\n        # else: Settings.dev_print(\"not tweeting\")\n\n        ## Upload Files ##\n        try:\n\n            if not driver.enter_text(message[\"text\"]):\n                Settings.err_print(\"unable to post!\")\n                return False\n\n            successful, skipped = driver.upload_files(message[\"files\"])\n            if successful and not skipped:\n                # twitter tweet button is 1st, post is 2nd\n                postButton = [ele for ele in driver.browser.find_elements(By.TAG_NAME, \"button\") if \"Post\" in ele.get_attribute(\"innerHTML\")][0]\n                WebDriverWait(driver.browser, Settings.get_upload_max_duration(), poll_frequency=3).until(EC.element_to_be_clickable(postButton))\n                Settings.dev_print(\"upload complete\")\n\n            if str(Settings.is_debug()) == \"True\":\n                driver.message_clear()\n                Settings.print('skipped post (debug)')\n                Settings.debug_delay_check()\n                return True\n\n            Settings.dev_print(\"uploading post...\")\n            postButton = [ele for ele in driver.browser.find_elements(By.TAG_NAME, \"button\") if \"Post\" in ele.get_attribute(\"innerHTML\")][0]\n            ActionChains(driver.browser).move_to_element(postButton).click().perform()\n            Settings.print('posted to OnlyFans!')\n            return True\n        except TimeoutException:\n            Settings.dev_print(\"timed out waiting for post upload!\")\n        except Exception as e:\n            Settings.dev_print(e)\n            Settings.err_print(\"unable to send post\")\n        driver.go_to_home(force=True)\n        return False\n\n    ######################\n    ##### Promotions #####\n    ######################\n\n    def promotional_campaign(self, promotion=None):\n        \"\"\"\n        Enter the promotion as a campaign.\n\n        Parameters\n        ----------\n        promotion : classes.Promotion\n            The promotion to enter as a campaign\n\n        Returns\n        -------\n        bool\n            Whether or not the promotion was successful\n\n        \"\"\"\n\n        if not promotion:\n            Settings.err_print(\"missing promotion\")\n            return False\n        # go to onlyfans.com/my/subscribers/active\n        try:\n            promotion.get()\n            limit = promotion[\"limit\"]\n            expiration = promotion[\"expiration\"]\n            duration = promotion[\"duration\"]\n            # user = promotion[\"user\"]\n            amount = promotion[\"amount\"]\n            text = promotion[\"message\"]\n            Settings.maybe_print(\"goto -> /my/promotions\")\n            self.go_to_page(\"my/promotions\")\n            Settings.dev_print(\"checking existing promotion\")\n            copies = self.browser.find_elements(By.CLASS_NAME, \"g-btn.m-rounded.m-uppercase\")\n            for copy in copies:\n                if \"copy link to profile\" in str(copy.get_attribute(\"innerHTML\")).lower():\n                # Settings.print(\"{}\".format(copy.get_attribute(\"innerHTML\")))\n                    copy.click()\n                    Settings.dev_print(\"successfully clicked early copy\")\n                    Settings.warn_print(\"a promotion already exists\")\n                    Settings.print(\"Copied existing promotion\")\n                    return True\n            Settings.dev_print(\"clicking promotion campaign\")\n            self.find_element_to_click(\"promotionalCampaign\").click()\n            Settings.dev_print(\"successfully clicked promotion campaign\")\n            # Settings.debug_delay_check()\n            time.sleep(10)\n            # limit dropdown\n            Settings.dev_print(\"setting campaign count\")\n            limitDropwdown = self.find_element_by_name(\"promotionalTrialCount\")\n            for n in range(11): # 11 max subscription limits\n                limitDropwdown.send_keys(str(Keys.UP))\n            Settings.debug_delay_check()\n            if limit:\n                for n in range(int(limit)):\n                    limitDropwdown.send_keys(Keys.DOWN)\n            Settings.dev_print(\"successfully set campaign count\")\n            Settings.debug_delay_check()\n            # expiration dropdown\n            Settings.dev_print(\"settings campaign expiration\")\n            expirationDropdown = self.find_element_by_name(\"promotionalTrialExpiration\")\n            for n in range(11): # 31 max days\n                expirationDropdown.send_keys(str(Keys.UP))\n            Settings.debug_delay_check()\n            if expiration:\n                for n in range(int(expiration)):\n                    expirationDropdown.send_keys(Keys.DOWN)\n            Settings.dev_print(\"successfully set campaign expiration\")\n            Settings.debug_delay_check()\n            # duration dropdown\n            # LIMIT_ALLOWED = [\"1 day\",\"3 days\",\"7 days\",\"14 days\",\"1 month\",\"3 months\",\"6 months\",\"12 months\"]\n            durationDropdown = self.find_element_by_name(\"promotionalCampaignAmount\")\n            Settings.dev_print(\"entering discount amount\")\n            for n in range(11):\n                durationDropdown.send_keys(str(Keys.UP))\n            for n in range(round(int(amount)/5)-1):\n                durationDropdown.send_keys(Keys.DOWN)\n            Settings.dev_print(\"successfully entered discount amount\")\n            # todo: add message to users\n            message = self.find_element_by_name(\"promotionalTrialMessage\")\n            Settings.dev_print(\"found message text\")\n            message.clear()\n            Settings.dev_print(\"sending text\")\n            message.send_keys(str(text))\n            # todo: [] apply to expired subscribers checkbox\n            Settings.debug_delay_check()\n            # find and click promotionalTrialConfirm\n            if str(Settings.is_debug()) == \"True\":\n                Settings.dev_print(\"finding campaign cancel\")\n                self.find_element_to_click(\"promotionalTrialCancel\").click()\n                Settings.maybe_print(\"skipping promotion (debug)\")\n                Settings.dev_print(\"successfully cancelled promotion campaign\")\n                return True\n            Settings.dev_print(\"finding campaign save\")\n            # save_ = self.find_element_to_click(\"promotionalTrialConfirm\")\n            # save_ = self.find_element_to_click(\"promotionalCampaignConfirm\")\n            save_ = self.browser.find_elements(By.CLASS_NAME, \"g-btn.m-rounded\")\n            for save__ in save_:\n                Settings.print(save__.get_attribute(\"innerHTML\"))\n            if len(save_) == 0:\n                Settings.dev_print(\"unable to find promotion 'Create'\")\n                Settings.err_print(\"unable to save promotion\")\n                return False\n            for save__ in save_:\n                if save__.get_attribute(\"innerHTML\").lower().strip() == \"create\":\n                    save_ = save__    \n            Settings.print(save_.get_attribute(\"innerHTML\"))\n            Settings.dev_print(\"saving promotion\")\n            save_.click()\n            Settings.dev_print(\"successfully saved promotion\")\n            Settings.dev_print(\"successful promotion campaign\")\n            # todo: add copy link to profile\n            Settings.debug_delay_check()\n            Settings.dev_print(\"clicking copy\")\n            copies = self.browser.find_elements(By.CLASS_NAME, \"g-btn.m-rounded.m-uppercase\")\n            for copy in copies:\n                Settings.print(\"{}\".format(copy.get_attribute(\"innerHTML\")))\n                if \"copy link to profile\" in str(copy.get_attribute(\"innerHTML\")).lower():\n                    copy.click()\n                    Settings.dev_print(\"successfully clicked copy\")\n            return True\n        except Exception as e:\n            Driver.error_checker(e)\n            Settings.err_print(\"failed to apply promotion\")\n            return None\n\n    # or email\n    def promotional_trial_link(self, promotion=None):\n        \"\"\"\n        Enter the promotion as a trial link\n\n        Parameters\n        ----------\n        promotion : classes.Promotion\n            The promotion to enter as a link\n\n        Returns\n        -------\n        bool\n            Whether or not the promotion was successful\n\n        \"\"\"\n\n        if not promotion:\n            Settings.err_print(\"missing promotion\")\n            return False\n        # go to onlyfans.com/my/subscribers/active\n        try:\n            promotion.get()\n            limit = promotion[\"limit\"]\n            expiration = promotion[\"expiration\"]\n            duration = promotion[\"duration\"]\n            user = promotion[\"user\"]\n            Settings.maybe_print(\"goto -> /my/promotions\")\n            self.go_to_page(\"/my/promotions\")\n            Settings.dev_print(\"showing promotional trial link\")\n            self.find_element_to_click(\"promotionalTrialShow\").click()\n            Settings.dev_print(\"successfully showed promotional trial link\")\n            Settings.dev_print(\"creating promotional trial\")\n            self.find_element_to_click(\"promotionalTrial\").click()\n            Settings.dev_print(\"successfully clicked promotional trial\")\n            # limit dropdown\n            Settings.dev_print(\"setting trial count\")\n            limitDropwdown = self.find_element_by_name(\"promotionalTrialCount\")\n            for n in range(11): # 11 max subscription limits\n                limitDropwdown.send_keys(str(Keys.UP))\n            Settings.debug_delay_check()\n            if limit:\n                for n in range(int(limit)):\n                    limitDropwdown.send_keys(Keys.DOWN)\n            Settings.dev_print(\"successfully set trial count\")\n            Settings.debug_delay_check()\n            # expiration dropdown\n            Settings.dev_print(\"settings trial expiration\")\n            expirationDropdown = self.find_element_by_name(\"promotionalTrialExpiration\")\n            for n in range(11): # 31 max days\n                expirationDropdown.send_keys(str(Keys.UP))\n            Settings.debug_delay_check()\n            if expiration:\n                for n in range(int(expiration)):\n                    expirationDropdown.send_keys(Keys.DOWN)\n            Settings.dev_print(\"successfully set trial expiration\")\n            Settings.debug_delay_check()\n            # duration dropdown\n            # LIMIT_ALLOWED = [\"1 day\",\"3 days\",\"7 days\",\"14 days\",\"1 month\",\"3 months\",\"6 months\",\"12 months\"]\n            Settings.dev_print(\"settings trial duration\")\n            durationDropwdown = self.find_element_by_name(\"promotionalTrialDuration\")\n            for n in range(11):\n                durationDropwdown.send_keys(str(Keys.UP))\n            Settings.debug_delay_check()\n            num = 1\n            if str(duration) == \"1 day\": num = 1\n            if str(duration) == \"3 day\": num = 2\n            if str(duration) == \"7 days\": num = 3\n            if str(duration) == \"14 days\": num = 4\n            if str(duration) == \"1 month\": num = 5\n            if str(duration) == \"3 months\": num = 6\n            if str(duration) == \"6 months\": num = 7\n            if str(duration) == \"12 months\": num = 8\n            for n in range(int(num)-1):\n                durationDropwdown.send_keys(Keys.DOWN)\n            Settings.dev_print(\"successfully set trial duration\")\n            Settings.debug_delay_check()\n            # find and click promotionalTrialConfirm\n            # if Settings.is_debug():\n            #     Settings.dev_print(\"finding trial cancel\")\n            #     self.find_element_to_click(\"promotionalTrialCancel\").click()\n            #     Settings.print(\"skipping: Promotion (debug)\")\n            #     Settings.dev_print(\"successfully cancelled promotion trial\")\n            #     return True\n            Settings.dev_print(\"finding trial save\")\n            save_ = self.find_element_to_click(\"promotionalTrialConfirm\")\n            # \"g-btn.m-rounded\"\n\n            save_ = self.browser.find_elements(By.CLASS_NAME, \"g-btn.m-rounded\")\n            for save__ in save_:\n                Settings.print(save__.get_attribute(\"innerHTML\"))\n            if len(save_) == 0:\n                Settings.dev_print(\"unable to find promotion 'Create'\")\n                Settings.err_print(\"unable to save promotion\")\n                return False\n            for save__ in save_:\n                if save__.get_attribute(\"innerHTML\").lower().strip() == \"create\":\n                    save_ = save__    \n            Settings.print(save_.get_attribute(\"innerHTML\"))\n            Settings.dev_print(\"saving promotion\")\n            save_.click()\n            Settings.dev_print(\"successfully saved promotion\")\n            ## TODO ##\n            # finish this\n            link = \"\"\n            # Settings.dev_print(\"copying trial link\")\n            # self.find_element_by_name(\"promotionalTrialLink\").click()\n            # Settings.dev_print(\"successfully copied trial link\")\n\n            # in order for this to work accurately i need to figure out the number of trial things already on the page\n            # then find the new trial thing\n            # then get the link for the new trial thing\n            # as of now it creates a new trial for the x duration so voila\n\n            # todo maybe probably never:\n            # go to /home\n            # enter copied paste into new post\n            # get text in new post\n            # email link to user\n\n            Settings.dev_print(\"successful promotion trial\")\n            Settings.debug_delay_check()\n            return link\n        except Exception as e:\n            Driver.error_checker(e)\n            Settings.err_print(\"failed to apply promotion\")\n            return None\n\n    def promotion_user_directly(self, promotion=None):\n        \"\"\"\n        Apply the promotion directly to the user.\n\n        Parameters\n        ----------\n        promotion : classes.Promotion\n            The promotion to provide to the user\n\n        Returns\n        -------\n        bool\n            Whether or not the promotion was successful\n\n        \"\"\"\n\n        if not promotion:\n            Settings.err_print(\"missing promotion\")\n            return False\n        # go to onlyfans.com/my/subscribers/active\n        promotion.get()\n        expiration = promotion[\"expiration\"]\n        months = promotion[\"duration\"]\n        user = promotion[\"user\"]\n        message = promotion[\"message\"]\n        if int(expiration) > int(Settings.get_discount_max_amount()):\n            Settings.warn_print(\"discount too high, max -> {}%\".format(Settings.get_discount_max_amount()))\n            discount = Settings.get_discount_max_amount()\n        elif int(expiration) > int(Settings.get_discount_min_amount()):\n            Settings.warn_print(\"discount too low, min -> {}%\".format(Settings.get_discount_min_amount()))\n            discount = Settings.get_discount_min_amount()\n        if int(months) > int(Settings.get_discount_max_months()):\n            Settings.warn_print(\"duration too high, max -> {} days\".format(Settings.get_discount_max_months()))\n            months = Settings.get_discount_max_months()\n        elif int(months) < int(Settings.get_discount_min_months()):\n            Settings.warn_print(\"duration too low, min -> {} days\".format(Settings.get_discount_min_months()))\n            months = Settings.get_discount_min_months()\n        try:\n            Settings.maybe_print(\"goto -> /{}\".format(user))\n            self.go_to_page(user)\n            # click discount button\n            self.find_element_to_click(\"discountUserPromotion\").click()\n            # enter expiration\n            expirations = self.find_element_by_name(\"promotionalTrialExpirationUser\")\n            # enter duration\n            durations = self.find_element_by_name(\"promotionalTrialDurationUser\")\n            # enter message\n            message = self.find_element_by_name(\"promotionalTrialMessageUser\")\n            # save\n            Settings.dev_print(\"entering expiration\")\n            for n in range(11):\n                expirations.send_keys(str(Keys.UP))\n            for n in range(round(int(expiration)/5)-1):\n                expirations.send_keys(Keys.DOWN)\n            Settings.dev_print(\"successfully entered expiration\")\n            Settings.dev_print(\"entering duration\")\n            for n in range(11):\n                durations.send_keys(str(Keys.UP))\n            for n in range(int(months)-1):\n                durations.send_keys(Keys.DOWN)\n            Settings.dev_print(\"successfully entered duration\")\n            Settings.debug_delay_check()\n            Settings.dev_print(\"entering message\")\n            message.clear()\n            message.send_keys(message)\n            Settings.dev_print(\"successfully entered message\")\n            Settings.dev_print(\"applying discount\")\n            save = self.find_element_by_name(\"promotionalTrialApply\")\n            if str(Settings.is_debug()) == \"True\":\n                self.find_element_by_name(\"promotionalTrialCancel\").click()\n                Settings.maybe_print(\"skipping save discount (debug)\")\n                Settings.dev_print(\"successfully canceled discount\")\n                cancel.click()\n                return True\n            save.click()\n            Settings.print(\"discounted: {}\".format(user.username))\n            Settings.dev_print(\"### User Discount Successful ###\")\n            return True\n        except Exception as e:\n            Driver.error_checker(e)\n            try:\n                self.find_element_by_name(\"promotionalTrialCancel\").click()\n                Settings.dev_print(\"### Discount Successful Failure ###\")\n                return False\n            except Exception as e:\n                Driver.error_checker(e)\n            Settings.dev_print(\"### Discount Failure ###\")\n            return False\n\n    ######################################################################\n\n    @staticmethod\n    def read_user_messages(username, user_id=None):\n        \"\"\"\n        Read the messages of the target user by username or user id.\n\n        Parameters\n        ----------\n        username : str\n            The username of the user to read messages of\n        user_id : str\n            The user id of the user to read messages of\n\n        Returns\n        -------\n        list\n            A list containing the messages read\n\n        \"\"\"\n\n        try:\n            driver = Driver.get_driver()\n            # go to onlyfans.com/my/subscribers/active\n            driver.message_user(username, user_id=user_id)\n            messages_sent_ = []\n            try:\n                messages_sent_ = driver.find_elements_by_name(\"messagesFrom\")\n            except Exception as e:\n                if \"Unable to locate elements\" in str(e):\n                    pass\n                else: Settings.dev_print(e)\n            # Settings.print(\"first message: {}\".format(messages_received_[0].get_attribute(\"innerHTML\")))\n            # messages_received_.pop(0) # drop self user at top of page\n            messages_all_ = []\n            try:\n                messages_all_ = driver.find_elements_by_name(\"messagesAll\")\n            except Exception as e:\n                if \"Unable to locate elements\" in str(e):\n                    pass\n                else: Settings.dev_print(e)\n            messages_all = []\n            messages_received = []\n            messages_sent = []\n            # timestamps_ = driver.browser.find_elements(By.CLASS_NAME, \"b-chat__message__time\")\n            # timestamps = []\n            # for timestamp in timestamps_:\n                # Settings.maybe_print(\"timestamp1: {}\".format(timestamp))\n                # timestamp = timestamp[\"data-timestamp\"]\n                # timestamp = timestamp.get_attribute(\"innerHTML\")\n                # Settings.maybe_print(\"timestamp: {}\".format(timestamp))\n                # timestamps.append(timestamp)\n            for message in messages_all_:\n                message = message.get_attribute(\"innerHTML\")\n                message = re.sub(r'<[a-zA-Z0-9=\\\"\\\\/_\\-!&;%@#$\\(\\)\\.:\\+\\s]*>', \"\", message)\n                Settings.maybe_print(\"all: {}\".format(message))\n                messages_all.append(message)\n            messages_and_timestamps = []\n            # messages_and_timestamps = [j for i in zip(timestamps,messages_all) for j in i]\n            # Settings.maybe_print(\"chat log:\")\n            # for f in messages_and_timestamps:\n                # Settings.maybe_print(\": {}\".format(f))\n            for message in messages_sent_:\n                # Settings.maybe_print(\"from1: {}\".format(message.get_attribute(\"innerHTML\")))\n                message = message.find_element(By.CLASS_NAME, Element.get_element_by_name(\"enterMessage\").getClass()).get_attribute(\"innerHTML\")\n                message = re.sub(r'<[a-zA-Z0-9=\\\"\\\\/_\\-!&;%@#$\\(\\)\\.:\\+\\s]*>', \"\", message)\n                Settings.maybe_print(\"sent: {}\".format(message))\n                messages_sent.append(message)\n            i = 0\n\n            # messages_all = list(set(messages_all))\n            # messages_sent = list(set(messages_sent))\n            # i really only want to remove duplicates if they're over a certain str length\n\n            def remove_dupes(list_):\n                \"\"\"Remove duplicates from the list\"\"\"\n\n                for i in range(len(list_)):\n                    for j in range(len(list_)):\n                        # if j >= len(list_): break\n                        if i==j: continue\n                        if str(list_[i]) == str(list_[j]) and len(str(list_[i])) > 10:\n                            del list_[j]\n                            remove_dupes(list_)\n                            return\n                            \n            remove_dupes(messages_all)\n            remove_dupes(messages_sent)\n\n            for message in messages_all:\n                if message not in messages_sent:\n                    messages_received.append(message)\n                i += 1\n            Settings.maybe_print(\"received: {}\".format(messages_received))\n            Settings.maybe_print(\"sent: {}\".format(messages_sent))\n            Settings.maybe_print(\"messages sent: {}\".format(len(messages_sent)))\n            Settings.maybe_print(\"messages received: {}\".format(len(messages_received)))\n            Settings.maybe_print(\"messages all: {}\".format(len(messages_all)))\n            return [messages_all, messages_and_timestamps, messages_received, messages_sent]\n        except Exception as e:\n            Driver.error_checker(e)\n            Settings.err_print(\"failure to read chat - {}\".format(username))\n            return [[],[],[],[]]\n\n    ###################\n    ##### Refresh #####\n    ###################\n\n    def refresh(self):\n        \"\"\"Refresh the web browser\"\"\"\n\n        Settings.dev_print(\"refreshing browser...\")\n        self.browser.refresh()\n\n    #################\n    ##### Reset #####\n    #################\n\n    def reset(self):\n        \"\"\"\n        Reset the web browser to home page\n\n        Returns\n        -------\n        bool\n            Whether or not the browser was reset successfully\n\n        \"\"\"\n\n        if not self.browser:\n            Settings.print('OnlyFans not open, skipping reset')\n            return True\n        try:\n            self.go_to_home()\n            Settings.print('OnlyFans reset')\n            return True\n        except Exception as e:\n            Driver.error_checker(e)\n            Settings.err_print(\"failure resetting onlyfans\")\n            return False\n\n    ####################\n    ##### Schedule #####\n    ####################\n\n    def schedule_open(self):\n        \"\"\"Click schedule\"\"\"\n\n        Settings.dev_print(\"opening schedule\")\n        self.find_element_to_click(\"scheduleAdd\").click()\n        Settings.dev_print(\"successfully opened schedule\")\n\n    def schedule_date(self, month, year):\n        \"\"\"Find and click month w/ correct date\"\"\"\n\n        Settings.dev_print(\"setting date\")\n        while True:\n\n            date = self.browser.find_element(By.CLASS_NAME, \"vdatetime-calendar__current--month\").get_attribute(\"innerHTML\")\n\n            # date = self.find_element_by_name(\"scheduleDate\").get_attribute(\"innerHTML\")\n            Settings.dev_print(\"date: {} - {} {}\".format(date, month, year))\n            if str(month) in str(date) and str(year) in str(date):\n                Settings.dev_print(\"set month and year\")\n                return True\n            else:\n                self.find_element_to_click(\"scheduleNextMonth\").click()\n        return False\n\n    def schedule_day(self, day):\n        \"\"\"Set day in month\"\"\"\n\n        Settings.dev_print(\"setting day\")\n        for ele in self.find_elements_by_name(\"scheduleDays\"):\n            if str(day) in ele.get_attribute(\"innerHTML\").replace(\"<span><span>\",\"\").replace(\"</span></span>\",\"\"):\n                ele.click()\n                Settings.dev_print(\"set day\")\n                return True\n        return False\n\n    def schedule_save_date(self):\n        \"\"\"Save schedule date and move to next view in frame by hitting next\"\"\"\n        \n        self.find_element_to_click(\"scheduleNext\").click()\n        Settings.dev_print(\"successfully saved date\")\n\n    def schedule_hour(self, hour):\n        \"\"\"Set schedule hour\"\"\"\n\n        Settings.dev_print(\"setting hours\")\n        eles = self.browser.find_element(By.CLASS_NAME, \"vdatetime-time-picker__list--hours\").find_elements(By.XPATH, \"./child::*\")\n        for ele in eles:\n            if str(hour) in ele.get_attribute(\"innerHTML\").strip():\n                # ActionChains(self.browser).move_to_element(ele).click().perform()\n                ele.click()\n                Settings.dev_print(\"set hour\")\n                return True\n        return False\n\n    def schedule_minutes(self, minutes):\n        \"\"\"Set schedule minutes\"\"\"\n\n        Settings.dev_print(\"setting minutes\")\n        eles = self.browser.find_element(By.CLASS_NAME, \"vdatetime-time-picker__list--minutes\").find_elements(By.XPATH, \"./child::*\")\n        for ele in eles:\n            if str(minutes) in ele.get_attribute(\"innerHTML\").strip():\n                ele.click()\n                Settings.dev_print(\"set minutes\")\n                return True\n        return False\n\n    def schedule_suffix(self, suffix):\n        \"\"\"Set am/pm suffix\"\"\"\n\n        Settings.dev_print(\"setting suffix\")\n        eles = self.browser.find_element(By.CLASS_NAME, \"vdatetime-time-picker__list--suffix\").find_elements(By.XPATH, \"./child::*\")\n        for ele in eles:\n            if str(suffix).lower() in ele.get_attribute(\"innerHTML\").strip().lower():\n                ele.click()\n                Settings.dev_print(\"set suffix\")\n                return True\n        return False\n\n    def schedule_cancel(self):\n        \"\"\"Cancel schedule by clicking cancel\"\"\"\n\n        self.browser.find_element(By.CLASS_NAME, \"vdatetime-popup__actions__button--cancel\").find_elements(By.XPATH, \"./child::*\")[0].click()\n        Settings.print(\"canceled schedule\")\n        return True\n\n    def schedule_save(self):\n        \"\"\"Save schedule by clicking save\"\"\"\n\n        # self.find_element_to_click(\"scheduleSave\").click()\n        self.browser.find_element(By.CLASS_NAME, \"vdatetime-popup__actions__button--confirm\").find_elements(By.XPATH, \"./child::*\")[0].click()\n        Settings.print(\"saved schedule\")\n        return True\n\n    def schedule(self, schedule):\n        \"\"\"\n        Enter the provided schedule\n\n        Parameters\n        ----------\n        schedule : dict\n            The schedule object containing the values to enter\n\n        Returns\n        -------\n        bool\n            Whether or not the schedule was entered successfully\n\n        \"\"\"\n\n        if str(schedule) == \"None\" or not schedule: return True\n        try:\n            Settings.print(\"Schedule:\")\n            Settings.print(\"- Date: {}\".format(Settings.format_date(schedule[\"date\"])))\n            Settings.print(\"- Time: {}\".format(Settings.format_time(schedule[\"time\"])))\n            # ensure schedule button can be accessed\n            # self.open_more_options()\n\n            # tries twice to solve various bugs\n            try:\n                self.schedule_open()\n            except Exception as e:\n                Settings.dev_print(e)\n                self.go_to_home()\n                self.schedule_open()\n\n            # return self.schedule_cancel()\n\n            # set month, year, and day\n            if not self.schedule_date(schedule[\"month\"], schedule[\"year\"]):\n                Settings.debug_delay_check()\n                raise Exception(\"failed to enter date!\")\n            if not self.schedule_day(schedule[\"day\"]):\n                Settings.debug_delay_check()\n                raise Exception(\"failed to enter day!\")\n            Settings.debug_delay_check()\n            self.schedule_save_date()\n            # set time\n            if not self.schedule_hour(schedule[\"hour\"]):\n                Settings.debug_delay_check()\n                raise Exception(\"failed to enter hour!\")\n            if not self.schedule_minutes(schedule[\"minute\"]):\n                Settings.debug_delay_check()\n                raise Exception(\"failed to enter minutes!\")\n            if not self.schedule_suffix(schedule[\"suffix\"]):\n                Settings.debug_delay_check()\n                raise Exception(\"failed to enter suffix!\")\n            # save time\n            Settings.debug_delay_check()\n            Settings.dev_print(\"saving schedule\")\n            # if str(Settings.is_debug()) == \"True\":\n            #     Settings.print(\"skipping schedule save (debug)\")\n            #     return self.schedule_cancel()\n            # else:\n            return self.schedule_save()\n        except Exception as e:\n            Driver.error_checker(e)\n        # attempt to cancel window\n        return self.schedule_cancel()\n\n    ####################\n    ##### Settings #####\n    ####################\n\n    # gets all settings from whichever page its on\n    # or get a specific setting\n    # probably just way easier and resourceful to do it all at once\n    # though it would be ideal to also be able to update individual settings without risking other settings\n\n    # goes through the settings and get all the values\n    # @staticmethod\n    # def settings_get_all():\n    #     Settings.print(\"Getting All Settings\")\n    #     profile = Profile()\n    #     try:\n    #         pages = Profile.get_pages()\n    #         for page in pages:\n    #             data = self.sync_from_settings_page(page)\n    #             for key, value in data:\n    #                 profile[key] = value\n    #         Settings.dev_print(\"successfully got settings\")\n    #         Settings.print(\"Settings Retrieved\")\n    #     except Exception as e:\n    #         Driver.error_checker(e)\n    #     return profile\n\n    def sync_from_settings_page(self, profile=None, page=None):\n        \"\"\"\n        Sync values from settings page.\n\n        Parameters\n        ----------\n        profile : Profile\n            The profile object to sync from\n        page : str\n            The profile page to sync settings from\n\n        Returns\n        -------\n        bool\n            Whether or not the sync was successful\n\n        \"\"\"\n\n        Settings.print(\"Getting Settings: {}\".format(page))\n        from ..classes.profile import Profile\n        try:\n            variables = Profile.get_variables_for_page(page)\n            Settings.dev_print(\"going to settings page: {}\".format(page))\n            self.go_to_settings(page)\n            Settings.dev_print(\"reached settings: {}\".format(page))\n            if profile == None:\n                profile = Profile()\n            for var in variables:\n                name = var[0]\n                page_ = var[1]\n                type_ = var[2]\n                status = None\n                Settings.dev_print(\"searching: {} - {}\".format(name, type_))\n                try:\n                    element = self.find_element_by_name(name)\n                    Settings.dev_print(\"successful ele: {}\".format(name))\n                except Exception as e:\n                    Driver.error_checker(e)\n                    continue\n                if str(type_) == \"text\":\n                    # get attr text\n                    status = element.get_attribute(\"innerHTML\").strip() or None\n                    status2 = element.get_attribute(\"value\").strip() or None\n                    Settings.print(\"{} - {}\".format(status, status2))\n                    if not status and status2: status = status2\n                elif str(type_) == \"toggle\":\n                    # get state true|false\n                    status = element.is_selected()\n                elif str(type_) == \"dropdown\":\n                    ele = self.find_element_by_name(name)\n                    Select(self.browser.find_element(By.ID, ele.getId()))\n                    status = element.first_selected_option\n                elif str(type_) == \"list\":\n                    status = element.get_attribute(\"innerHTML\")\n                elif str(type_) == \"file\":\n                    Settings.print(\"NEED TO UPDATE THIS\")\n                    # can get file from image above\n                    # can set once found\n                    # status = element.get_attribute(\"innerHTML\")\n                    # pass\n                elif str(type_) == \"checkbox\":\n                    status = element.is_selected()\n                if status is not None: Settings.dev_print(\"successful value: {}\".format(status))\n                Settings.maybe_print(\"{} : {}\".format(name, status))\n                setattr(profile, str(name), status)\n            Settings.dev_print(\"successfully got settings page: {}\".format(page))\n            Settings.print(\"Settings Page Retrieved: {}\".format(page))\n        except Exception as e:\n            Driver.error_checker(e)\n\n    # goes through each page and sets all the values\n    def sync_to_settings_page(self, profile=None, page=None):\n        \"\"\"\n        Sync values to settings page.\n\n        Parameters\n        ----------\n        profile : Profile\n            The profile object to sync to\n        page : str\n            The profile page to sync settings to\n\n        Returns\n        -------\n        bool\n            Whether or not the sync was successful\n\n        \"\"\"\n\n        Settings.print(\"Updating Page Settings: {}\".format(page))\n        from ..classes.profile import Profile\n        try:\n            variables = Profile.get_variables_for_page(page)\n            Settings.dev_print(\"going to settings page: {}\".format(page))\n            self.go_to_settings(page)\n            Settings.dev_print(\"reached settings: {}\".format(page))\n            if profile == None:\n                profile = Profile()\n            for var in variables:\n                name = var[0]\n                page_ = var[1]\n                type_ = var[2]\n                status = None\n                Settings.dev_print(\"searching: {} - {}\".format(name, type_))\n                try:\n                    element = self.find_element_by_name(name)\n                    Settings.dev_print(\"successful ele: {}\".format(name))\n                except Exception as e:\n                    Driver.error_checker(e)\n                    continue\n                if str(type_) == \"text\":\n\n                    element.send_keys(getattr(profile, str(name)))\n                elif str(type_) == \"toggle\":\n                    # somehow set the other toggle state\n                    pass\n                elif str(type_) == \"dropdown\":\n                    ele = self.find_element_by_name(name)\n                    Select(self.browser.find_element(By.ID, ele.getId()))\n                    # go to top\n                    # then go to matching value\n                    pass\n                elif str(type_) == \"list\":\n                    element.send_keys(getattr(profile, str(name)))\n                elif str(type_) == \"file\":\n                    element.send_keys(getattr(profile, str(name)))\n                elif str(type_) == \"checkbox\":\n                    element.click()\n            if str(Settings.is_debug()) == \"True\":\n                Settings.dev_print(\"successfully cancelled settings page: {}\".format(page))\n            else:\n                self.settings_save(page=page)\n                Settings.dev_print(\"successfully set settings page: {}\".format(page))\n            Settings.print(\"Settings Page Updated: {}\".format(page))\n        except Exception as e:\n            Driver.error_checker(e)\n\n    # @staticmethod\n    # def settings_set_all(Profile):\n    #     Settings.print(\"Updating All Settings\")\n    #     try:\n    #         pages = Profile.TABS\n    #         for page in pages:\n    #             self.sync_to_settings_page(Profile, page)\n    #         Settings.dev_print(\"successfully set settings\")\n    #         Settings.print(\"Settings Updated\")\n    #     except Exception as e:\n    #         Driver.error_checker(e)\n\n    # saves the settings page if it is a page that needs to be saved\n        # has save:\n        # profile\n        # account\n        # security\n        ##\n        # doesn't have save:\n        # story\n        # notifications\n        # other\n    def settings_save(self, page=None):\n        \"\"\"\n        Save the provided settings page if it is a page that saves\n\n        Parameters\n        ----------\n        page : str\n            The settings page to check if saves\n        \n        \"\"\"\n\n        if str(page) not in [\"profile\", \"account\", \"security\"]:\n            Settings.dev_print(\"not saving: {}\".format(page))\n            return\n        try:\n            Settings.dev_print(\"saving: {}\".format(page))\n            element = self.find_element_by_name(\"profileSave\")\n            Settings.dev_print(\"derp\")\n            element = self.find_element_to_click(\"profileSave\")\n            Settings.dev_print(\"found page save\")\n            if str(Settings.is_debug()) == \"True\":\n                Settings.print(\"skipping settings save (debug)\")\n            else:\n                Settings.dev_print(\"saving page\")\n                element.click()\n                Settings.dev_print(\"page saved\")\n        except Exception as e:\n            Driver.error_checker(e)\n\n    #################\n    ##### Spawn #####\n    #################\n\n    def spawn_browser(self, browserType):\n        \"\"\"\n        Spawns a browser according to args.\n\n        Browser options can be: auto, chrome, firefox, remote\n\n        Parameters\n        ----------\n        browserType : str\n            The configured browser type to use\n\n        Returns\n        -------\n        Selenium.WebDriver\n            The created browser object\n\n        \"\"\"\n\n        if str(Settings.is_debug(\"selenium\")) == \"False\":\n            import logging\n            from selenium.webdriver.remote.remote_connection import LOGGER as SeleniumLogger\n            SeleniumLogger.setLevel(logging.ERROR)\n            logging.getLogger(\"urllib3\").setLevel(logging.ERROR)\n            logging.getLogger(\"requests\").setLevel(logging.ERROR)\n            logging.getLogger('selenium.webdriver.remote.remote_connection').setLevel(logging.ERROR)\n\n            if int(Settings.get_verbosity()) >= 2:\n                SeleniumLogger.setLevel(logging.WARNING)\n                logging.getLogger(\"urllib3\").setLevel(logging.WARNING)\n                logging.getLogger(\"requests\").setLevel(logging.WARNING)\n                logging.getLogger('selenium.webdriver.remote.remote_connection').setLevel(logging.WARNING)\n\n        browser = None\n        Settings.print(\"spawning web browser...\")\n\n        def add_options(options):\n            if str(Settings.is_show_window()) == \"False\":\n                options.add_argument('--headless')\n            options.add_argument(\"--no-sandbox\") # Bypass OS security model\n            options.add_argument(\"--disable-gpu\")\n            options.add_argument(\"--disable-extensions\")\n            options.add_argument(\"--disable-dev-shm-usage\")\n\n            options.add_argument(\"enable-automation\")\n            options.add_argument(\"--disable-infobars\")\n\n            # if os.name == 'nt':\n                # options.add_argument(r\"--user-data-dir=C:\\Users\\brain\\AppData\\Local\\Google\\Chrome\\User Data\")\n            # else:\n            options.add_argument(\"--user-data-dir=\"+os.path.join(Settings.get_base_directory(),\"tmp\",\"selenium\")) # do not disable, required for cookies to work \n            # options.add_argument(r'--profile-directory=Alex D') #e.g. Profile 3\n            \n            # options.add_argument(\"--allow-insecure-localhost\")            \n            # possibly linux only\n            # options.add_argument('disable-notifications')\n            # https://stackoverflow.com/questions/50642308/webdriverexception-unknown-error-devtoolsactiveport-file-doesnt-exist-while-t\n            # options.add_arguments(\"start-maximized\"); // open Browser in maximized mode\n            # options.add_argument(\"--window-size=1920,1080\")\n            # options.add_argument(\"--disable-crash-reporter\")\n            # options.add_argument(\"--disable-infobars\")\n            # options.add_argument(\"--disable-in-process-stack-traces\")\n            # options.add_argument(\"--disable-logging\")\n            # options.add_argument(\"--log-level=3\")\n            # options.add_argument(\"--output=/dev/null\")\n            # TODO: to be added to list of removed (if not truly needed by then)\n            # options.add_argument('--disable-software-rasterizer')\n            # options.add_argument('--ignore-certificate-errors')\n            # options.add_argument(\"--remote-debugging-address=localhost\")    \n            # options.add_argument(\"--remote-debugging-port=9223\")\n\n        def browser_error(err, browserName):\n            Settings.warn_print(\"unable to launch {}!\".format(browserName))\n            Settings.dev_print(err)\n\n        def attempt_chrome(brave=False, chromium=False, edge=False):\n            browserName = None\n            browserAttempt = None\n            try:\n                options = webdriver.ChromeOptions()\n                add_options(options)\n                if brave:\n                    browserName = \"brave\"\n                    Settings.maybe_print(\"attempting {} web browser...\".format(browserName))\n                    browserAttempt = webdriver.Chrome(service=BraveService(ChromeDriverManager(chrome_type=ChromeType.BRAVE).install()), options=options)\n                    Settings.print(\"browser created - {}\".format(browserName))\n                elif chromium:\n                    browserName = \"chromium\"\n                    Settings.maybe_print(\"attempting {} web browser...\".format(browserName))\n                    browserAttempt = webdriver.Chrome(ChromeDriverManager(chrome_type=ChromeType.CHROMIUM).install(), options=options)\n                    Settings.print(\"browser created - {}\".format(browserName))\n                elif edge:\n                    # doesn't work\n                    browserName = \"edge\"\n                    Settings.maybe_print(\"attempting {} web browser...\".format(browserName))\n                    # options = EdgeOptions()\n                    # options.use_chromium = True\n                    # add_options(options)\n                    # options.binary_location=\"/home/{user}/.wdm/drivers/edgedriver/linux64/111.0.1661/msedgedriver\".format(user=os.getenv('USER'))\n                    # fix any permissions issues\n                    # os.chmod(options.binary_location, 0o755)\n                    # shutil.chown(options.binary_location, user=os.getenv('USER'), group=None)\n                    # browserAttempt = Edge(executable_path=options.binary_location, options=options)\n                    # browserAttempt = webdriver.Edge(service=EdgeService(EdgeChromiumDriverManager().install()))\n                    Settings.print(\"browser created - {}\".format(browserName))\n                else:\n                    browserName = \"chrome\"\n                    # linux = x86_64\n                    # rpi = aarch64\n                    import platform\n                    # raspberrypi arm processors don't work with webdriver manager\n                    if platform.processor() == \"aarch64\":\n                        browserAttempt = webdriver.Chrome('/usr/bin/chromedriver', options=options)\n                    else:\n                        browserAttempt = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install()), options=options)\n                return browserAttempt\n            except Exception as e:\n                browser_error(e, browserName)\n            return None\n\n        def attempt_firefox():\n            Settings.maybe_print(\"attempting firefox web browser...\")\n            # firefox needs non root\n            if os.geteuid() == 0:\n                Settings.print(\"You must run `onlysnarf` as non-root for Firefox to work correctly!\")\n                return False\n            try:\n                options = FirefoxOptions()\n                if str(Settings.is_debug(\"firefox\")) == \"True\":\n                    options.log.level = \"trace\"\n                add_options(options)\n                # options.add_argument(\"--enable-file-cookies\")\n                browserAttempt = webdriver.Firefox(service=FirefoxService(GeckoDriverManager().install()), options=options)\n                return browserAttempt\n            except Exception as e:\n                browser_error(e, \"firefox\")\n            return None\n\n        # doesn't work\n        def attempt_ie():\n            Settings.maybe_print(\"attempting ie web browser...\")\n            try:\n                driver_path = IEDriverManager().install()\n                os.chmod(driver_path, 0o755)\n                # browserAttempt = webdriver.Ie(executable_path=IEService(driver_path))\n                browserAttempt = webdriver.Ie(service=IEService(IEDriverManager().install()))\n                return browserAttempt\n            except Exception as e:\n                browser_error(e, \"ie\")\n            return None\n\n        # doesn't work\n        def attempt_opera():\n            Settings.maybe_print(\"attempting opera web browser...\")\n            try:\n                # options.add_argument('allow-elevated-browser')\n                # options.binary_location = \"C:\\\\Users\\\\USERNAME\\\\FOLDERLOCATION\\\\Opera\\\\VERSION\\\\opera.exe\"\n                browserAttempt = webdriver.Opera(executable_path=OperaDriverManager().install())\n                return browserAttempt\n            except Exception as e:\n                browser_error(e, \"opera\")\n            return None\n\n        def attempt_reconnect():\n            self.read_session_data()\n            if not self.session_id and not self.session_url:\n                Settings.warn_print(\"unable to read session data!\")\n                return None\n            Settings.maybe_print(\"reconnecting to web browser...\")\n            Settings.dev_print(\"reconnect id: {}\".format(self.session_id))\n            Settings.dev_print(\"reconnect url: {}\".format(self.session_url))\n            try:\n                options = webdriver.ChromeOptions()\n                add_options(options)\n                browserAttempt = webdriver.Remote(command_executor=self.session_url, options=options)\n                browserAttempt.close()   # this closes the session's window - it is currently the only one, thus the session itself will be auto-killed, yet:\n                # take the session that's already running\n                browserAttempt.session_id = self.session_id\n                browserAttempt.title # fails check with: 'NoneType' object has no attribute 'title'\n                Settings.print(\"browser reconnected!\")\n                return browserAttempt\n            except Exception as e:\n                Settings.warn_print(\"unable to reconnect!\")\n                Settings.dev_print(e)\n            return None\n\n        def attempt_remote():\n            link = 'http://{}:{}/wd/hub'.format(Settings.get_remote_browser_host(), Settings.get_remote_browser_port())\n            Settings.dev_print(\"remote url: {}\".format(link))\n            def attempt(dc, opts):\n                try:\n                    if str(Settings.is_show_window()) == \"False\":\n                        opts.add_argument('--headless')\n                    Settings.dev_print(\"attempting remote: {}\".format(browserType))\n                    browserAttempt = webdriver.Remote(command_executor=link, desired_capabilities=dc, options=opts)\n                    Settings.print(\"remote browser created - {}\".format(browserType))\n                    return browserAttempt\n                except Exception as e:\n                    Settings.warn_print(\"unable to connect remotely!\")\n                    Settings.dev_print(e)\n                return None\n\n            def brave_options():\n                dC = DesiredCapabilities.BRAVE\n                options = webdriver.BraveOptions()\n                return dC, options\n\n            def chrome_options():\n                dC = DesiredCapabilities.CHROME\n                options = webdriver.ChromeOptions()\n                return dC, options\n\n            def chromium_options():\n                dC = DesiredCapabilities.CHROMIUM\n                options = webdriver.ChromeOptions()\n                return dC, options\n\n            def edge_options():\n                dC = DesiredCapabilities.EDGE\n                options = webdriver.EdgeOptions()\n                return dC, options\n\n            def firefox_options():\n                dC = DesiredCapabilities.FIREFOX\n                options = webdriver.FirefoxOptions()\n                return dC, options\n\n            def ie_options():\n                dC = DesiredCapabilities.IE\n                options = webdriver.ChromeOptions()\n                return dC, options\n\n            def opera_options():\n                dC = DesiredCapabilities.OPERA\n                options = webdriver.OperaOptions()\n                return dC, options\n\n            if \"brave\" in browserType: return attempt(*brave_options())\n            elif \"chrome\" in browserType: return attempt(*chrome_options())\n            elif \"chromium\" in browserType: return attempt(*chromium_options())\n            elif \"edge\" in browserType: return attempt(*edge_options())\n            elif \"firefox\" in browserType: return attempt(*firefox_options())\n            elif \"ie\" in browserType: return attempt(*ie_options())\n            elif \"opera\" in browserType: return attempt(*opera_options())\n            Settings.warn_print(\"unable to connect remotely via {}!\".format(browserType))\n            return None\n\n        ################################################################################################################################################\n        ################################################################################################################################################\n        ################################################################################################################################################\n\n        if \"auto\" in browserType:\n            browser = attempt_reconnect()\n            if not browser: browser = attempt_chrome(brave=True, chromium=False, edge=False)\n            if not browser: browser = attempt_chrome(brave=False, chromium=False, edge=False)\n            if not browser: browser = attempt_chrome(brave=False, chromium=True, edge=False)\n            if not browser: browser = attempt_chrome(brave=False, chromium=False, edge=True)\n            if not browser: browser = attempt_firefox()\n            if not browser: browser = attempt_ie()\n            if not browser: browser = attempt_opera()\n        elif \"remote\" in browserType:\n            browser = attempt_remote()\n        elif \"brave\" in browserType:\n            browser = attempt_chrome(brave=True, chromium=False, edge=False)\n        elif \"chrome\" in browserType:\n            browser = attempt_chrome(brave=False, chromium=False, edge=False)\n        elif \"chromium\" in browserType:\n            browser = attempt_chrome(brave=False, chromium=True, edge=False)\n        elif \"edge\" in browserType:\n            browser = attempt_chrome(brave=False, chromium=False, edge=True)\n        elif \"firefox\" in browserType:\n            browser = attempt_firefox()\n        elif \"ie\" in browserType:\n            browser = attempt_ie()\n        elif \"opera\" in browserType:\n            browser = attempt_opera()\n\n        if browser and str(Settings.is_keep()) == \"True\":\n            self.session_id = browser.session_id\n            self.session_url = browser.command_executor._url\n            self.write_session_data()\n\n        if not browser:\n            Settings.err_print(\"unable to spawn a web browser!\")\n            if os.environ.get(\"ENV\") and str(os.environ.get(\"ENV\")) == \"test\": return False\n            os._exit(1)\n\n        browser.implicitly_wait(30) # seconds\n        browser.set_page_load_timeout(1200)\n        browser.file_detector = LocalFileDetector() # for uploading via remote sessions\n        if str(Settings.is_show_window()) == \"False\":\n            Settings.print(\"browser spawned successfully (headless)\".format(browserType))\n        else:\n            Settings.print(\"browser spawned successfully\".format(browserType))\n        return browser\n\n    ## possibly move these functions elsewhere (again)\n    def read_session_data(self):\n        Settings.maybe_print(\"reading local session\")\n        path_ = os.path.join(Settings.get_base_directory(), \"session.json\")\n        Settings.dev_print(\"local session path: \"+str(path_))\n        try:\n            with open(str(path_)) as json_file:  \n                data = json.load(json_file)\n                self.session_id = data['id']\n                self.session_url = data['url']\n            Settings.maybe_print(\"loaded local users\")\n        except Exception as e:\n            Settings.dev_print(e)\n\n    def write_session_data(self):\n        Settings.maybe_print(\"writing local session\")\n        Settings.dev_print(\"saving session id: {}\".format(self.session_id))        \n        Settings.dev_print(\"saving session url: {}\".format(self.session_url))\n        path_ = os.path.join(Settings.get_base_directory(), \"session.json\")\n        Settings.dev_print(\"local session path: \"+str(path_))\n        data = {}\n        data['id'] = self.session_id\n        data['url'] = self.session_url\n        try:\n            with open(str(path_), 'w') as outfile:  \n                json.dump(data, outfile, indent=4, sort_keys=True)\n            Settings.maybe_print(\"saved session data\")\n        except FileNotFoundError:\n            Settings.err_print(\"Missing Session File\")\n        except OSError:\n            Settings.err_print(\"Missing Session Path\")\n\n    ##################\n    ##### Upload #####\n    ##################\n\n    def upload_files(self, files):\n        \"\"\"\n        Upload the files to a post or message.\n\n        Must be on a post or message.\n\n        Parameters\n        ----------\n        files : list\n            The list of files to upload\n\n        Returns\n        -------\n        bool\n            Whether or not the upload was successful\n\n        \"\"\"\n\n        if str(Settings.is_skip_download()) == \"True\": \n            Settings.print(\"skipping upload (download)\")\n            return True, True\n        elif str(Settings.is_skip_upload()) == \"True\": \n            Settings.print(\"skipping upload (upload)\")\n            return True, True\n        if len(files) == 0:\n            Settings.maybe_print(\"skipping upload (empty file list)\")\n            return True, True\n        if str(Settings.is_skip_upload()) == \"True\":\n            Settings.print(\"skipping upload (disabled)\")\n            return True, True\n        files = files[:int(Settings.get_upload_max())]\n        Settings.print(\"uploading file(s): {}\".format(len(files)))\n\n        ####\n\n        import threading\n        import concurrent.futures\n\n        files_ = []\n\n        def prepare(file):\n            # add a better check for this w/ the new API\n            if not isinstance(file, File):\n                _file = File()\n                setattr(_file, \"path\", file)\n                file = _file\n            uploadable = file.prepare() # downloads if necessary\n            if not uploadable: Settings.err_print(\"unable to upload - {}\".format(file.get_title()))\n            else: files_.append(file)    \n\n        with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:\n            executor.map(prepare, files)\n\n        Settings.dev_print(\"files prepared: {}\".format(len(files_)))\n        if len(files_) == 0:\n            Settings.err_print(\"skipping upload (unable to prepare files)\")\n            return False, True\n\n        ####\n\n        enter_file = self.browser.find_element(By.ID, \"attach_file_photo\")\n        successful = []\n\n        i = 1\n        for file in files_:\n            Settings.print('> {} - {}/{}'.format(file.get_title(), i, len(files)))\n            i += 1\n            successful.append(self.drag_and_drop_file(enter_file , file.get_path()))\n            time.sleep(1)\n            ###\n            def fix_filename(file):\n                # move file to change its name\n                filename = os.path.basename(file.get_path())\n                filename = os.path.splitext(filename)[0]\n                if \"_fixed\" in str(filename): return\n                Settings.dev_print(\"fixing filename...\")\n                filename += \"_fixed\"\n                ext = os.path.splitext(filename)[1].lower()\n                Settings.dev_print(\"{} -> {}.{}\".format(os.path.dirname(file.get_path()), filename, ext))\n                dst = \"{}/{}.{}\".format(os.path.dirname(file), filename, ext)\n                shutil.move(file.get_path(), dst)\n                file.path = dst\n                # add file to end of list so it gets retried\n                files.append(file)\n                # if this doesn't force it then it'll loop forever without a stopper\n            ###\n        # one last final check\n        Settings.debug_delay_check()\n        if all(successful):\n            if self.error_window_upload(): Settings.dev_print(\"files uploaded successfully\")\n            else: Settings.dev_print(\"files probably uploaded succesfully\")\n            time.sleep(1) # bug prevention\n            return True, False\n        Settings.warn_print(\"a file failed to upload!\")\n        return False, False\n\n    #################\n    ##### Users #####\n    #################\n\n    @staticmethod\n    def get_username():\n        \"\"\"\n        Gets the username of the logged in user.\n\n        Returns\n        -------\n        str\n            The username of the logged in user\n\n        \"\"\"\n\n        try:\n            driver = Driver.get_driver()\n            driver.auth()\n            eles = [ele for ele in driver.browser.find_elements(By.TAG_NAME, \"a\") if \"@\" in str(ele.get_attribute(\"innerHTML\")) and \"onlyfans\" not in str(ele.get_attribute(\"innerHTML\"))]\n            Settings.dev_print(\"successfully found users...\")\n            if Settings.is_debug():\n                for ele in eles:\n                    Settings.dev_print(\"{} - {}\".format(ele.get_attribute(\"innerHTML\"), ele.get_attribute(\"href\")))\n            if len(eles) == 0:\n                Settings.err_print(\"unable to find username!\")\n            else:\n                username = str(eles[0].get_attribute(\"href\")).replace(ONLYFANS_HOME_URL2, \"\")\n                Settings.dev_print(\"successfully found active username: {}\".format(username))\n                return username\n        except Exception as e:\n            Driver.error_checker(e)\n            Settings.err_print(\"failed to find username\")\n        return None\n\n    @staticmethod\n    def following_get():\n        \"\"\"\n        Return lists of accounts followed by the logged in user.\n\n        Returns\n        -------\n        list\n            The list of users being followed\n\n        \"\"\"\n\n        users = []\n        try:\n            driver = Driver.get_driver()\n            driver.go_to_page(ONLYFANS_USERS_FOLLOWING_URL)\n            count = 0\n            while True:\n                elements = driver.browser.find_elements(By.CLASS_NAME, \"m-subscriptions\")\n                if len(elements) == count: break\n                Settings.print_same_line(\"({}/{}) scrolling...\".format(count, len(elements)))\n                count = len(elements)\n                driver.browser.execute_script(\"window.scrollTo(0, document.body.scrollHeight);\")\n                time.sleep(2)\n            Settings.print(\"\")\n            elements = driver.browser.find_elements(By.CLASS_NAME, \"m-subscriptions\")\n            Settings.dev_print(\"successfully found subscriptions\")\n            for ele in elements:\n                username = ele.find_element(By.CLASS_NAME, \"g-user-username\").get_attribute(\"innerHTML\").strip()\n                name = ele.find_element(By.CLASS_NAME, \"g-user-name\").get_attribute(\"innerHTML\")\n                name = re.sub(\"<!-*>\", \"\", name)\n                name = re.sub(\"<.*\\\">\", \"\", name)\n                name = re.sub(\"</.*>\", \"\", name).strip()\n                # Settings.print(\"username: {}\".format(username))\n                # Settings.print(\"name: {}\".format(name))\n                users.append({\"name\":name, \"username\":username.replace(\"@\",\"\")}) \n            Settings.maybe_print(\"found: {}\".format(len(users)))\n            for user in users:\n                Settings.dev_print(user)\n        except Exception as e:\n            Driver.error_checker(e)\n            Settings.err_print(\"failed to find subscriptions\")\n        Settings.dev_print(\"successfully found following users\")\n        return users\n\n    @staticmethod\n    def users_get(page=ONLYFANS_USERS_ACTIVE_URL):\n        \"\"\"\n        Return lists of accounts subscribed to the logged in user.\n\n        Returns\n        -------\n        list\n            The list of users subscribed\n\n        \"\"\"\n\n        users = []\n        try:\n            driver = Driver.get_driver()\n            driver.go_to_page(page)\n            # scroll until elements stop spawning\n            thirdTime = 0\n            count = 0\n            while True:\n                elements = driver.browser.find_elements(By.CLASS_NAME, \"m-fans\")\n                if len(elements) == int(count) and thirdTime >= 3: break\n                Settings.print_same_line(\"({}) scrolling...\".format(count))\n                count = len(elements)\n                driver.browser.execute_script(\"window.scrollTo(0, document.body.scrollHeight);\")\n                time.sleep(2)\n                if thirdTime >= 3 and len(elements) == 0: break\n                thirdTime += 1\n            Settings.print(\"\")\n            elements = driver.browser.find_elements(By.CLASS_NAME, \"m-fans\")\n            Settings.dev_print(\"searching fan elements...\")\n            for ele in elements:\n                username = ele.find_element(By.CLASS_NAME, \"g-user-username\").get_attribute(\"innerHTML\").strip()\n                name = ele.find_element(By.CLASS_NAME, \"g-user-name\").get_attribute(\"innerHTML\")\n                name = re.sub(\"<!-*>\", \"\", name)\n                name = re.sub(\"<.*\\\">\", \"\", name)\n                name = re.sub(\"</.*>\", \"\", name).strip()\n                users.append({\"name\":name, \"username\":username.replace(\"@\",\"\")})\n                Settings.dev_print(users[-1])\n            Settings.maybe_print(\"found {} fans\".format(len(users)))\n            Settings.dev_print(\"successfully found fans\")\n        except Exception as e:\n            Settings.print(e)\n            Driver.error_checker(e)\n            Settings.err_print(\"failed to find fans\")\n        return users\n\n    @staticmethod\n    def user_get_id(username):\n        \"\"\"\n        Get the user id of the user by username.\n\n        Parameters\n        ----------\n        username : str\n            The username to find the id of\n\n        Returns\n        -------\n        str\n            The user id of the located user\n\n        \"\"\"\n\n        user_id = None\n        try:\n            driver = Driver.get_driver()\n            driver.go_to_page(username)\n            time.sleep(3) # this should realistically only fail if they're no longer subscribed but it fails often from loading\n            elements = driver.browser.find_elements(By.TAG_NAME, \"a\")\n            ele = [ele.get_attribute(\"href\") for ele in elements\n                    if \"/my/chats/chat/\" in str(ele.get_attribute(\"href\"))]\n            if len(ele) == 0: \n                Settings.warn_print(\"unable to find user id\")\n                return None\n            ele = ele[0]\n            ele = ele.replace(\"https://onlyfans.com/my/chats/chat/\", \"\")\n            user_id = ele\n            Settings.dev_print(\"successfully found user id: {}\".format(user_id))\n        except Exception as e:\n            Settings.dev_print(\"failure to find id: {}\".format(username))\n            Driver.error_checker(e)\n            Settings.err_print(\"failed to find user id\")\n        return user_id\n\n    def search_for_list(self, name=None, number=None):\n        \"\"\"\n        Search for list in Driver.lists cache by name or number.\n\n        Parameters\n        ----------\n        name : str\n            The name of the list to find\n        number : int\n            The number for the list to find\n\n        Returns\n        -------\n        str\n            The located list name\n        str\n            The located list number\n\n        \"\"\"\n\n        Settings.dev_print(\"lists: {}\".format(self.lists))\n        try:\n            for list_ in self.lists:\n                if list_[0] == name or list_[1] == number:\n                    return list_[0], list_[1]\n            Settings.dev_print(\"failed to locate list: {} - {}\".format(name, number))\n        except Exception as e:\n            if \"Unable to locate window\" not in str(e):\n                Settings.dev_print(e)\n        return name, number\n\n    @staticmethod\n    def get_list(name=None, number=None):\n        \"\"\"\n        Search for list by name or number on OnlyFans.\n\n        Parameters\n        ----------\n        name : str\n            The name of the list to find\n        number : int\n            The number for the list to find\n\n        Returns\n        -------\n        list\n            The list of users on the found list\n        str\n            The located list name\n        str\n            The located list number\n\n        \"\"\"\n\n        driver = Driver.get_driver()\n        driver.auth()\n        # gets members from list\n        users = []\n        Settings.maybe_print(\"getting list: {} - {}\".format(name, number))\n        name, number = driver.search_for_list(name=name, number=number)\n        try:\n            if not name or not number:\n                for list_ in driver.get_lists():\n                    if name and str(list_[1]).lower() == str(name).lower():\n                        number = list_[0]\n                    if number and str(list_[0]).lower() == str(number).lower():\n                        name = list_[1]\n            users = Driver.users_get(page=\"/my/lists/{}\".format(number))\n        except Exception as e:\n            Driver.error_checker(e)\n            Settings.err_print(\"failed to find list members\")\n        return users, name, number\n\n    def get_lists(self):\n        \"\"\"\n        Search and return all lists from OnlyFans.\n\n        Returns\n        -------\n        list\n            The list of lists that were found\n\n        \"\"\"\n\n        lists = []\n        try:\n            Settings.maybe_print(\"getting lists\")\n            self.go_to_page(\"/my/lists\")\n\n            elements = self.browser.find_elements(By.CLASS_NAME, \"b-users-lists__item\")\n\n            # find favorites\n            # find bookmarks\n            # find friends\n            # find other lists and their names\n            # each page has the same user boxes that are used in users_get\n\n            # /my/favorites\n            # /my/bookmarks\n            # /my/friends\n            # /my/lists\n            # b-users-lists__item -> href -> /my/lists/#\n            # b-users-lists__item__name -> innerHTML -> list name\n            # b-users-lists__item__count -> innerHTML -> list amount\n\n            for ele in elements:\n                if \"/my/favorites\" in str(ele.get_attribute(\"href\")):\n                    # Settings.print(\"{} - {}\".format(ele.get_attribute(\"innerHTML\"), ele.get_attribute(\"href\")))\n                    count = ele.find_elements(By.CLASS_NAME, \"b-users-lists__item__count\").get_attribute(\"innerHTML\").replace(\"people\", \"\").replace(\"person\", \"\").strip()\n                    if int(count) > 0: lists.append(\"favorites\")\n                elif \"/my/bookmarks\" in str(ele.get_attribute(\"href\")):\n                    # Settings.print(\"{} - {}\".format(ele.get_attribute(\"innerHTML\"), ele.get_attribute(\"href\")))\n                    count = ele.find_elements(By.CLASS_NAME, \"b-users-lists__item__count\").get_attribute(\"innerHTML\").replace(\"people\", \"\").replace(\"person\", \"\").strip()\n                    if int(count) > 0: lists.append(\"bookmarks\")\n                elif \"/my/friends\" in str(ele.get_attribute(\"href\")):\n                    # Settings.print(\"{} - {}\".format(ele.get_attribute(\"innerHTML\"), ele.get_attribute(\"href\")))\n                    count = ele.find_elements(By.CLASS_NAME, \"b-users-lists__item__count\").get_attribute(\"innerHTML\").replace(\"people\", \"\").replace(\"person\", \"\").strip()\n                    if int(count) > 0: lists.append(\"friends\")\n                elif \"/my/lists\" in str(ele.get_attribute(\"href\")):\n                    try:\n                        # Settings.print(\"{} - {}\".format(ele.get_attribute(\"innerHTML\"), ele.get_attribute(\"href\")))\n\n                        # ele = ele.find_elements(By.CLASS_NAME, \"b-users-lists__item__text\")\n                        listNumber = ele.get_attribute(\"href\").replace(\"https://onlyfans.com/my/lists/\", \"\")\n                        listName = ele.find_element(By.CLASS_NAME, \"b-users-lists__item__name\").get_attribute(\"innerHTML\").strip()\n                        count = ele.find_element(By.CLASS_NAME, \"b-users-lists__item__count\").get_attribute(\"innerHTML\").replace(\"people\", \"\").replace(\"person\", \"\").strip()\n                        Settings.dev_print(\"{} - {}: {}\".format(listNumber, listName, count))\n                        lists.append([listNumber, listName])\n                    except Exception as e:\n                        Settings.dev_print(e)\n            Settings.dev_print(\"successfully found lists: {}\".format(len(lists)))\n        except Exception as e:\n            Driver.error_checker(e)\n            Settings.print(e)\n            Settings.err_print(\"failed to find lists\")\n        return lists\n\n    def get_list_members(self, list):\n        \"\"\"\n        Get the members of a list.\n\n        Parameters\n        ----------\n        list : list\n            The list to get members of\n        \n        Returns\n        -------\n        list\n            The list of members that were found\n\n        \"\"\"\n\n        users = []\n        try:\n            users = Driver.users_get(page=\"/my/lists/{}\".format(int(list_)))\n        except Exception as e:\n            Driver.error_checker(e)\n            Settings.err_print(\"failed to find list members\")\n        return users\n\n    def add_user_to_list(self, username=None, listNumber=None):\n        \"\"\"\n        Add user by username to list by number.\n\n        Parameters\n        ----------\n        username : str\n            The username of the user to add to the list\n        listNumber : int\n            The number of the list to add the user to\n\n        Returns\n        -------\n        bool\n            Whether or not the user was added successfully\n\n        \"\"\"\n\n        Settings.print(\"Adding user to list: {} - {}\".format(username, listNumber))\n        if not username:\n            Settings.err_print(\"missing username for list\")\n            return False\n        if not listNumber:\n            Settings.err_print(\"missing list number\")\n            return False\n        users = []\n        try:\n            self.go_to_page(ONLYFANS_USERS_ACTIVE_URL)\n            end_ = True\n            count = 0\n            user_ = None\n            while end_:\n                elements = self.browser.find_elements(By.CLASS_NAME, \"m-fans\")\n                for ele in elements:\n                    username_ = ele.find_element(By.CLASS_NAME, \"g-user-username\").get_attribute(\"innerHTML\").strip()\n                    if str(username) == str(username_).replace(\"@\",\"\"):\n                        self.browser.execute_script(\"arguments[0].scrollIntoView();\", ele)\n                        user_ = ele\n                        end_ = False\n                if not end_: continue\n                if len(elements) == int(count): break\n                Settings.print_same_line(\"({}/{}) scrolling...\".format(count, len(elements)))\n                count = len(elements)\n                self.browser.execute_script(\"window.scrollTo(0, document.body.scrollHeight);\")\n                time.sleep(2)\n            Settings.print(\"\")\n            Settings.dev_print(\"successfully found fans\")\n            if not user_:\n                Settings.err_print(\"unable to find user - {}\".format(username))\n                return False\n            Settings.maybe_print(\"found: {}\".format(username))\n            ActionChains(self.browser).move_to_element(user_).perform()\n            Settings.dev_print(\"finding list add\")\n            listAdds = user_.find_elements(By.CLASS_NAME, \"g-btn.m-add-to-lists\")\n            listAdd_ = None\n            for listAdd in listAdds:\n                if str(\"/my/lists/\"+listNumber) in str(listAdd.get_attribute(\"href\")):\n                    Settings.print(\"skipping: User already on list - {}\".format(listNumber))\n                    return True\n                if \" lists \" in str(listAdd.get_attribute(\"innerHTML\")).lower():\n                    Settings.dev_print(\"found list add\")\n                    listAdd_ = listAdd\n            Settings.dev_print(\"clicking list add\")\n            listAdd_.click()\n            links = self.browser.find_elements(By.CLASS_NAME, \"b-users-lists__item\")\n            for link in links:\n                # Settings.print(\"{} {}\".format(link.get_attribute(\"href\"), link.get_attribute(\"innerHTML\")))\n                if str(\"/my/lists/\"+listNumber) in str(link.get_attribute(\"href\")):\n                    Settings.dev_print(\"clicking list\")\n                    self.move_to_then_click_element(link)\n                    time.sleep(0.5)\n                    Settings.dev_print(\"successfully clicked list\")\n            Settings.dev_print(\"searching for list save\")\n            close = self.find_element_to_click(\"listSingleSave\")\n            Settings.dev_print(\"clicking save list\")\n            close.click()\n            Settings.dev_print(\"successfully added user to list - {}\".format(listNumber))\n            return True\n        except Exception as e:\n            Driver.error_checker(e)\n            Settings.err_print(\"failed to add user to list\")\n        return False\n\n    def add_users_to_list(self, users=[], number=None, name=None):\n        \"\"\"\n        Add the users to the list by name or number.\n\n        Parameters\n        ----------\n        users : list\n            The list of users to add to the list\n        number : int\n            The number for the list to add to\n        name : str\n            The name of the list to add to\n\n        Returns\n        -------\n        bool\n            Whether or not the users were added successfully\n\n        \"\"\"\n\n        try:\n            users = users.copy()\n            users_, name, number = self.get_list(number=number, name=name)\n            # users = [user for user in users if user not in users_]\n            for i, user in enumerate(users[:]):\n                for user_ in users_:\n                    for key, value in user_.items():\n                        if str(key) == \"username\" and str(user.username) == str(value):\n                            users.remove(user)\n            Settings.maybe_print(\"adding users to list: {} - {} - {}\".format(len(users), number, name))\n            try:\n                Settings.dev_print(\"opening toggle options\")\n                toggle = self.browser.find_element(By.CLASS_NAME, \"b-users__list__add-btn\")\n                Settings.dev_print(\"clicking toggle options\")\n                toggle.click()\n                Settings.dev_print(\"toggle options opened\")\n            except Exception as e:\n                Settings.dev_print(\"no options to toggle - users already available\")\n                # Settings.print(\"weird fuckup\")\n                # return self.add_users_to_list(users=users, number=number, name=name)\n            time.sleep(1)\n            original_handle = self.browser.current_window_handle\n            clicked = False\n            Settings.maybe_print(\"searching for users\")\n            while len(users) > 0:\n                # find user thing\n                eles = self.browser.find_elements(By.CLASS_NAME, \"b-chats__available-users__item.m-search\")\n                for ele in eles:\n                    for user in users.copy():\n                        # Settings.print(\"{} - {}\".format(i, user.username))\n                        if str(user.username) in str(ele.get_attribute(\"href\")):\n                            Settings.maybe_print(\"found user: {}\".format(user.username))\n                            # time.sleep(2)\n                            self.move_to_then_click_element(ele)\n                            users.remove(user)\n                            clicked = True\n                Settings.print_same_line(\"({}/{}) scrolling...\".format(len(eles), len(users)))\n                self.browser.execute_script(\"window.scrollTo(0, document.body.scrollHeight);\")\n                if len(eles) > 100:\n                    Settings.maybe_print(\"adding users to list individually\")\n                    for user in users.copy():\n                        successful = self.add_user_to_list(username=user.username, listNumber=number)\n                        if successful: users.remove(user)\n                # if current window has changed, switch back\n                if self.browser.current_window_handle != original_handle:\n                    self.browser.switch_to.window(original_handle)\n            Settings.print(\"\")\n            if not clicked:\n                Settings.print(\"skipping list add (none)\")\n                Settings.dev_print(\"skipping list save\")\n                self.browser.refresh()\n                Settings.dev_print(\"### List Add Successfully Skipped ###\")\n                return True\n            if str(Settings.is_debug()) == \"True\":\n                Settings.print(\"skipping list add (debug)\")\n                Settings.dev_print(\"skipping list save\")\n                self.browser.refresh()\n                Settings.dev_print(\"### List Add Successfully Canceled ###\")\n                return True\n            Settings.dev_print(\"saving list\")\n            save = self.find_element_by_name(\"listSave\")\n            self.move_to_then_click_element(save)\n            Settings.dev_print(\"### successfully added users to list\")\n        except Exception as e:\n            Settings.print(e)\n            Driver.error_checker(e)\n            Settings.err_print(\"failed to add users to list\")\n            return False\n        return True\n\n    ################\n    ##### Exit #####\n    ################\n\n    def exit(self):\n        \"\"\"Save and exit\"\"\"\n\n        if not self.browser: return\n        ## Cookies\n        if str(Settings.is_cookies()) == \"True\":\n            self.cookies_save()\n        if str(Settings.is_save_users()) == \"True\":\n            Settings.print(\"saving and exiting OnlyFans...\")\n            # from OnlySnarf.classes.user import User\n            from ..classes.user import User\n            User.write_users_local()\n        if str(Settings.is_keep()) == \"True\":\n            self.go_to_home()\n            Settings.dev_print(\"reset to home page\")\n            if not Driver.NOT_INFORMED_KEPT:\n                Settings.print(\"kept browser open\")\n            Driver.NOT_INFORMED_KEPT = True\n            # todo: add delay for setting this back to false?\n        else:\n            Settings.print(\"exiting OnlyFans...\")\n            self.browser.quit()\n            Settings.maybe_print(\"browser closed\")\n            self._initialized_ = False\n            Driver.DRIVERS.remove(self)\n\n    @staticmethod\n    def exit_all():\n        \"\"\"Exit all known browsers.\"\"\"\n\n        for driver in Driver.DRIVERS:\n            driver.exit()\n\n##################################################################################\n\ndef parse_users(user_ids, starteds, users, usernames):\n    # usernames.pop(0)\n    # Settings.print(\"My User Id: {}\".format(user_ids[0]))\n    # user_ids.pop(0)\n    Settings.dev_print(\"user_ids: \"+str(len(user_ids)))\n    Settings.dev_print(\"starteds: \"+str(len(starteds)))\n    useridsFailed = False\n    startedsFailed = False\n    if len(user_ids) == 0:\n        Settings.maybe_Settings.warn_print(\"unable to find user ids\")\n        useridsFailed = True\n    if len(starteds) == 0:\n        Settings.maybe_Settings.warn_print(\"unable to find starting dates\")\n        startedsFailed = True\n    users_ = []\n    try:\n        user_ids_ = []\n        starteds_ = []\n        for i in range(len(user_ids)):\n            if user_ids[i].get_attribute(\"href\"):\n                user_ids_.append(user_ids[i].get_attribute(\"href\"))\n        for i in range(len(starteds)):\n            text = starteds[i].get_attribute(\"innerHTML\")\n            match = re.findall(\"Started.*([A-Za-z]{3}\\\\s[0-9]{1,2},\\\\s[0-9]{4})\", text)\n            if len(match) > 0:\n                starteds_.append(match[0])\n        if len(user_ids_) == 0:\n            Settings.maybe_Settings.warn_print(\"unable to find user ids\")\n            useridsFailed = True\n        if len(starteds_) == 0:\n            Settings.maybe_Settings.warn_print(\"unable to find starting dates\")\n            startedsFailed = True\n        # Settings.maybe_print(\"ids vs starteds vs avatars: \"+str(len(user_ids_))+\" - \"+str(len(starteds_))+\" - \"+str(len(avatars)))\n        Settings.maybe_print(\"users vs ids vs starteds vs usernames:\"+str(len(users))+\" - \"+str(len(user_ids_))+\" - \"+str(len(starteds_))+\" - \"+str(len(usernames)))\n        # for user in usernames:\n            # Settings.print(user.get_attribute(\"innerHTML\"))\n        if len(usernames) > 2:\n            # first 2 usernames are self\n            usernames.pop(0)\n            usernames.pop(0)\n        if len(users) > 2:\n            users.pop(0)\n            users.pop(0)\n        for i in range(len(users)): # the first is you and doesn't count towards total\n            try:\n                if not startedsFailed:\n                    start = starteds_[i]\n                else:\n                    start = datetime.now().strftime(\"%b %d, %Y\")\n                if not useridsFailed:\n                    user_id = user_ids_[i][35:] # cuts out initial chars instead of unwieldy regex\n                else:\n                    user_id = None\n                name = users[i]\n                username = usernames[i]\n                name = str(name.get_attribute(\"innerHTML\"))\n                # Settings.print(\"name: \"+name)\n                # if \"<!\" in str(name):\n                name = re.sub(\"<!-*>\", \"\", name)\n                # Settings.print(name)\n                # if \"<\" in str(name) and \">\" in str(name):\n                name = re.sub(\"<.*\\\">\", \"\", name).strip()\n                # Settings.print(name)\n                name = re.sub(\"</.*>\", \"\", name).strip()\n                # Settings.print(name)\n                # name = re.sub(name, \"<.*>\", \"\").strip()\n                # Settings.print(name)\n                # name = re.sub(name, \"<!-*>\", \"\")\n                username = str(username.get_attribute(\"innerHTML\"))\n                # Settings.print(\"username: \"+username)\n                # if \"<!\" in str(username):\n                username = re.sub(\"<!-*>\", \"\", username)\n                # Settings.print(username)\n                # if \"<\" in str(username) and \">\" in str(username):\n                username = re.sub(\"<.*\\\">\", \"\", username).strip()\n                # Settings.print(username)\n                username = re.sub(\"</.*>\", \"\", username).strip()\n                username = username.replace(\"@\",\"\")\n                # Settings.print(username)\n                # username = re.sub(\"<.*>\", \"\", username).strip()\n                # Settings.print(username)\n                # username = re.sub(username, \"<!-*>\", \"\")\n                # Settings.maybe_print(\"name: \"+str(name))\n                # Settings.maybe_print(\"username: \"+str(username))\n                # Settings.maybe_print(\"user_id: \"+str(user_id))\n                # if str(Settings.get_username()).lower() in str(username).lower():\n                #     Settings.maybe_print(\"(): %s = %s\" % (Settings.get_username(), username))\n                #     # first user is always active user but just in case find it in list of users\n                #     Settings.USER_ID = username\n                # else:\n                users_.append({\"name\":name, \"username\":username, \"id\":user_id, \"started\":start})\n            except Exception as e: Settings.dev_print(e)\n    except Exception as e: Driver.error_checker(e)\n    return users_\n\n\n\n\n\n\n\n"
  },
  {
    "path": "OnlySnarf/lib/ffmpeg.py",
    "content": "import ffmpeg\nimport datetime\nimport os\n##\nfrom ..util.settings import Settings\n\n##################\n##### FFMPEG #####\n##################\n\n# all videos in folder into single mp4\ndef combine(folderPath):\n    # if str(Settings.COMBINE) == \"False\":\n    #     Settings.warn_print(\"skipping combine\")\n    #     return False\n    if \".mp4\" not in str(folderPath):\n        Settings.err_print(\"unable to combine\")\n        return False\n    combinePath = str(folderPath).replace(\".mp4\", \"_full.mp4\")\n    try:    \n        ffmpeg.input(str(folderPath), format='concat', safe=0).output(combinePath, c='copy').run()\n    except Exception as e:\n        Settings.dev_print(e)\n        if \"Conversion failed!\" in str(e):\n            Settings.err_print(\"combine failure\")\n            return combinePath                    \n    Settings.print(\"Combine Complete\")\n    return combinePath\n\n\n# images from gallery\ndef gifify(path):\n    # First convert the images to a video:\n    # ffmpeg -f image2 -i image%d.jpg video.avi\n    loglevel = \"quiet\"\n    # if Settings.is_debug():\n        # loglevel = \"debug\"\n    # p = subprocess.call(['ffmpeg', '-loglevel', str(loglevel), '-y', '-i', str(path), '-c', 'copy', '-c:v', 'libx264', '-c:a', 'aac', '-strict', '2', '-crf', '26', '-b:v', str(bitrate), str(reducedPath)])\n    # Then convert the avi to a gif:\n    # ffmpeg -i video.avi -pix_fmt rgb24 -loop_output 0 out.gif\n\n# frames for preview gallery\ndef frames(path):\n    try:\n        Settings.maybe_print(\"capturing frames: {}\".format(path))\n        try:\n            clip = VideoFileClip(str(path))\n            Settings.maybe_print(\"length: {}\".format(clip.duration))\n        except FileNotFoundError:\n            Settings.err_print(\"missing file to capture frames\")\n            return path\n    except: pass\n    screenshots = []\n    # ffmpeg -i test.avi -vcodec png -ss 10 -vframes 1 -an -f rawvideo test.png\n    for i in range(10):\n        output = path.replace(\".mp4\", \"-{}.png\".format(i))\n        p = subprocess.call(['ffmpeg', '-loglevel', str(loglevel), '-y', '-ss', str(int(i)*10), '-i', str(path), '-vcodec', 'png', '-vframes', '1', '-an', '-f', 'rawvideo', output])\n        screenshots.append(output)\n        if int(i)*10 > int(clip.duration): break\n    return screenshots\n\n# this requires a similar video and has no real use so remove?\n# or change to some other repair method for videos\n# def repair(path):\n#     if str(Settings.get_repair()) == \"False\":\n#         Settings.warn_print(\"skipping repair\")\n#         return path\n#     if \".mp4\" not in str(path):\n#         Settings.err_print(\"unable to repair\")\n#         return path\n#     if not os.path.isfile(str(Settings.WORKING_VIDEO)):\n#         Settings.err_print(\"missing working video\")\n#         return path\n#     repairedPath = str(path).replace(\".mp4\", \"_fixed.mp4\")\n#     try:\n#         Settings.print(\"Repairing: {} <-> {}\".format(path, Settings.WORKING_VIDEO))\n#         if Settings.is_debug():\n#             subprocess.call(['untrunc', str(Settings.WORKING_VIDEO), str(path)]).communicate()\n#         else:\n#             subprocess.Popen(['untrunc', str(Settings.WORKING_VIDEO), str(path)],stdin=FNULL,stdout=FNULL)\n#     except AttributeError:\n#         if os.path.isfile(str(path)+\"_fixed.mp4\"):\n#             shutil.move(str(path)+\"_fixed.mp4\", repairedPath)\n#             Settings.print(\"Repair Complete\")\n#     except:\n#         Settings.maybe_print(sys.exc_info()[0])\n#         Settings.warn_print(\"skipping repair\")\n#         return path\n#     Settings.print(\"Repair Successful\")\n#     return str(repairedPath)\n\ndef reduce(path):\n    if not Settings.is_reduce():\n        Settings.warn_print(\"skipping reduction\")\n        return path\n    if \".mp4\" not in str(path):\n        Settings.err_print(\"unable to reduce\")\n        return path\n    reducedPath = str(path).replace(\".mp4\", \"_reduced.mp4\")\n    try:\n        Settings.maybe_print(\"reducing: {}\".format(path))\n        try:\n            clip = VideoFileClip(str(path))\n            Settings.maybe_print(\"length: {}\".format(clip.duration))\n            bitrate = 1000000000 / int(clip.duration)\n            Settings.maybe_print(\"bitrate: {}\".format(bitrate))\n        except FileNotFoundError:\n            Settings.err_print(\"missing file to reduce\")\n            return path\n        loglevel = \"quiet\"\n        if Settings.is_debug():\n            loglevel = \"debug\"\n        p = subprocess.call(['ffmpeg', '-loglevel', str(loglevel), '-err_detect', 'ignore_err', '-y', '-i', str(path), '-c', 'copy', '-c:v', 'libx264', '-c:a', 'aac', '-strict', '2', '-crf', '26', '-b:v', str(bitrate), str(reducedPath)])\n        # p.communicate()\n    except FileNotFoundError:\n        Settings.warn_print(\"ignoring fixed video\")\n        return reduce(str(path).replace(\".mp4\", \"_fixed.mp4\"))\n    except Exception as e:\n        Settings.dev_print(e)\n        if \"Conversion failed!\" in str(e):\n            Settings.err_print(\"conversion failure\")\n            return path                    \n    Settings.print(\"Reduction Complete\")\n    originalSize = os.path.getsize(str(path))\n    newSize = os.path.getsize(str(reducedPath))\n    Settings.print(\"Original Size: {}kb - {}mb\".format(originalSize/1000, originalSize/1000000))\n    Settings.print(\"Reduced Size: {}kb - {}mb\".format(newSize/1000, newSize/1000000))\n    if int(originalSize) < int(newSize):\n        Settings.warn_print(\"original size smaller\")\n        return path\n    if int(newSize) == 0:\n        Settings.err_print(\"missing reduced file\")\n        return path\n    return reducedPath\n\n\n# into segments (60 sec, 5 min, 10 min)\n# segment: minutes\ndef split(path, segment):\n    if not Settings.is_split():\n        Settings.warn_print(\"skipping split\")\n        return path\n    if \".mp4\" not in str(path):\n        Settings.err_print(\"unable to split\")\n        return path\n    splitPaths = []\n    splitPath = str(path).replace(\".mp4\", \"_split$.mp4\")\n    try:\n        Settings.maybe_print(\"splitting: {}\".format(path))\n        try:\n            clip = VideoFileClip(str(path))\n            Settings.maybe_print(\"length: {}\".format(clip.duration))\n        except FileNotFoundError:\n            Settings.err_print(\"missing file to split\")\n            return path\n        i = 0\n        index = 0\n        loglevel = \"quiet\"\n        if Settings.is_debug():\n            loglevel = \"debug\"\n        while True:\n            start = datetime.timedelta(seconds=index)\n            end = datetime.timedelta(seconds=int(index)+(60*int(segment)))\n            out = splitPath.replace(\"$\",i)\n            p = subprocess.call(['ffmpeg', '-loglevel', str(loglevel), '-ss', str(start), '-y', '-i', str(path), '-to', str(end), '-c', 'copy', '-c:v', 'libx264', '-c:a', 'aac', '-strict', '2', str(out)])\n            splitPaths.append(out)\n            index += 60*int(segment)\n            i += 1\n        # p.communicate()\n    except Exception as e:\n        Settings.dev_print(e)\n        if \"Conversion failed!\" in str(e):\n            Settings.err_print(\"split failure\")\n            return splitPaths                    \n    Settings.print(\"Split Complete\")\n    return splitPaths\n\ndef thumbnail_fix(path):\n    # if str(Settings.THUMBNAILING_PREVIEW) == \"False\":\n    #     Settings.warn_print(\"preview thumbnailing disabled\")\n    #     return path\n    try:\n        Settings.print(\"Thumbnailing: {}\".format(path))\n        loglevel = \"quiet\"\n        if Settings.is_debug():\n            loglevel = \"debug\"\n        thumbnail_path = os.path.join(os.path.dirname(str(path)), 'thumbnail.png')\n        Settings.maybe_print(\"thumbnail path: {}\".format(thumbnail_path))\n        p = subprocess.call(['ffmpeg', '-loglevel', str(loglevel), '-i', str(path),'-ss', '00:00:00.000', '-vframes', '1', str(thumbnail_path)])\n        p.communicate()\n        Settings.print(\"Thumbnailing Complete\")\n        return thumbedPath\n    except FileNotFoundError:\n        Settings.warn_print(\"ignoring thumbnail\")\n    except AttributeError:\n        Settings.print(\"Thumbnailing: Captured PNG\")\n    except:\n        Settings.maybe_print(sys.exc_info()[0])\n        Settings.err_print(\"thumbnailing fuckup\")    \n\n#seconds off front or back\ndef trim(path):\n    if not Settings.is_trim():\n        Settings.warn_print(\"skipping trim\")\n        return path\n    if \".mp4\" not in str(path):\n        Settings.err_print(\"unable to trim\")\n        return path\n    reducedPath = str(path).replace(\".mp4\", \"_trimmed.mp4\")\n    try:\n        Settings.maybe_print(\"trimming: {}\".format(path))\n        try:\n            clip = VideoFileClip(str(path))\n            Settings.maybe_print(\"length: {}\".format(clip.duration))\n        except FileNotFoundError:\n            Settings.err_print(\"missing file to reduce\")\n            return path\n        start = datetime.timedelta(seconds=60)\n        end = datetime.timedelta(seconds=clip.duration-60)\n        loglevel = \"quiet\"\n        if Settings.is_debug():\n            loglevel = \"debug\"\n        p = subprocess.call(['ffmpeg', '-loglevel', str(loglevel), '-ss', str(start), '-y', '-i', str(path), '-to', str(end), '-c', 'copy', '-c:v', 'libx264', '-c:a', 'aac', '-strict', '2', str(reducedPath)])\n        # p.communicate()\n    except Exception as e:\n        Settings.dev_print(e)\n        if \"Conversion failed!\" in str(e):\n            Settings.err_print(\"trim failure\")\n            return path                    \n    Settings.print(\"Trim Complete\")\n    return reducedPath\n\n# unnecessary, handled by onlyfans\ndef watermark():\n    pass\n# cleanup & label appropriately (digital watermarking?)\ndef metadata():\n    pass\n"
  },
  {
    "path": "OnlySnarf/lib/menu.py",
    "content": "#!/usr/bin/python3\n\nimport os\nimport time\nimport random\nimport sys\nimport inquirer\n##\nfrom ..lib.driver import Driver\nfrom ..classes.profile import Profile\nfrom ..util.colorize import colorize\nfrom ..util.settings import Settings\n\n# from .util.args import parser\n# print(parser)\n# parser.add_parser('menu', help='> access the cli menu')\n\n####################\n##### CLI Menu #####\n####################\n\nclass Menu:\n\n    ASCII = \"\\n     ________         .__          _________                     _____ \\n \\\n    \\\\_____  \\\\   ____ |  | ___.__./   _____/ ____ _____ ________/ ____\\\\\\n \\\n     /   |   \\\\ /    \\\\|  |<   |  |\\\\_____  \\\\ /    \\\\\\\\__  \\\\\\\\_   _ \\\\   __\\\\ \\n \\\n    /    |    \\\\   |  \\\\  |_\\\\___  |/        \\\\   |  \\\\/ __ \\\\ |  |\\\\/| |   \\n \\\n    \\\\_______  /___|  /____/ ____/_______  /___|  (____  \\\\\\\\__|  |_|   \\n \\\n            \\\\/     \\\\/     \\\\/            \\\\/     \\\\/     \\\\/              \\n\"\n\n    def __init__(self):\n        pass\n\n    def ask_action():\n        \"\"\"\n        Ask action to take\n\n        Returns\n        -------\n        str\n            The action selected\n\n        \"\"\"\n\n        options = [\"back\"]\n        options.extend(Settings.get_actions())\n        questions = [\n            inquirer.List('action',\n                message= \"Please select an action:\",\n                choices= [str(option).title() for option in options],\n            )\n        ]\n        answers = inquirer.prompt(questions)\n        return answers['action'].lower()\n\n    def action_menu():\n        \"\"\"\n        Prompt the action menu. Cycles back to main menu\n\n\n        \"\"\"\n\n        from ..snarf import Snarf\n        action = Menu.ask_action()\n        if (action == 'back'): return Menu.main()\n        elif (action == 'discount'): Snarf.discount()\n        elif (action == 'message'): Snarf.message()\n        elif (action == 'post'): Snarf.post()\n        elif (action == 'profile'): Profile.menu()\n        elif (action == 'promotion'): Snarf.promotion()\n        else: Settings.print(\"Missing Action: {}\".format(colorize(action,\"red\")))\n        Menu.main()\n        \n    def header():\n        \"\"\"\n        Show the header text\n\n\n        \"\"\"\n\n        if not Settings.is_debug(): os.system('clear')\n        print(colorize(Menu.ASCII, 'header'))\n        print(colorize('version {}\\n'.format(Settings.get_version()), 'green'))\n        Menu.user_header()\n        Menu.settings_header()\n\n    def settings_header():\n        \"\"\"\n        Show the settings header text\n\n\n        \"\"\"\n\n        Settings.header()\n\n    def user_header():\n        \"\"\"\n        Show the user header text\n\n\n        \"\"\"\n\n        Settings.print(\"User:\")\n        if Settings.get_username_onlyfans() != \"\":\n            Settings.print(\" - Email = {}\".format(Settings.get_username_onlyfans()))\n        Settings.print(\" - Username = {}\".format(Settings.get_username()))\n        pass_ = \"\"\n        if str(Settings.get_password()) != \"\":\n            pass_ = \"******\"\n        Settings.print(\" - Password = {}\".format(pass_))\n        if str(Settings.get_username_twitter()) != \"\":\n            Settings.print(\" - Twitter = {}\".format(Settings.get_username_twitter()))\n            pass_ = \"\"\n            if str(Settings.get_password_twitter()) != \"\":\n                pass_ = \"******\"\n            Settings.print(\" - Password = {}\".format(pass_))\n        Settings.print('\\r')\n\n    def menu():\n        \"\"\"\n        Prompt the basic menu selection\n\n        Returns\n        -------\n        str\n            The menu option selected\n\n        \"\"\"\n\n        questions = [\n            inquirer.List('menu',\n                message= \"Please select an option:\",\n                choices= ['Action', 'Settings', 'Exit']\n            )\n        ]\n        answers = inquirer.prompt(questions)\n\n        # menu_prompt = {\n        #     'type': 'list',\n        #     'name': 'menu',\n        #     'message': 'Please select an option:',\n        #     'choices': ['Action', 'Profile', 'Settings', 'Exit']\n        # }\n        # answers = prompt(menu_prompt)\n\n        return answers['menu']\n\n    def main_menu():\n        \"\"\"\n        Show the main menu\n\n\n        \"\"\"\n\n        action = Menu.menu()\n        if (action == 'Action'): Menu.action_menu()\n        elif (action == 'Settings'): Settings.menu()\n        else: exit_handler()\n        Menu.main()\n\n    def main():\n        \"\"\"\n        Primary script entry\n\n\n        \"\"\"\n\n        time.sleep(1)\n        try:\n            Menu.header()\n            Menu.settings_header()\n            Menu.main_menu()\n        except Exception as e:\n            Settings.dev_print(e)\n\n#################################################################################################\n\ndef exit_handler():\n    \"\"\"Exit cleanly\"\"\"\n\n    try:\n        Driver.exit_all()\n    except Exception as e:\n        print(e)\n\nimport atexit\natexit.register(exit_handler)\n\n######################################################\n\ndef main():\n    try:\n        Menu.main()\n    except Exception as e:\n        Settings.dev_print(e)\n        Settings.print(\"shnarf??\")\n    finally:\n        Settings.print(\"shnarrf!\")\n        exit_handler()\n\nif __name__ == \"__main__\":\n    main()"
  },
  {
    "path": "OnlySnarf/server/api.py",
    "content": "import os\nimport json\nfrom flask import Flask, request\n\nfrom ..util.config import config\nfrom ..util.settings import Settings\n\ndef create_app():\n    app = Flask(__name__)\n\n    @app.route('/message', methods=['POST'])\n    def message():\n        try:\n            args = json.loads(request.data)\n            Settings.dev_print(args)\n            config[\"text\"] = args[\"text\"]\n            config[\"user\"] = args[\"user\"]\n            try: config[\"input\"] = args[\"input\"].split(\",\")\n            except Exception as e: pass\n            try: config[\"price\"] = args[\"price\"] or 0\n            except Exception as e: pass\n            try: config[\"schedule\"] = args[\"schedule\"]\n            except Exception as e: pass\n            try: config[\"performers\"] = args[\"performers\"]\n            except Exception as e: pass\n            from ..snarf import Snarf\n            Snarf.message()\n            Snarf.close()\n        except Exception as e:\n            Settings.dev_print(e)\n        finally:\n            return \"\", 200\n\n    @app.route('/post', methods=['POST'])\n    def post():\n        try:\n            args = json.loads(request.data)\n            Settings.dev_print(args)\n            config[\"text\"] = args[\"text\"]\n            try: config[\"input\"] = args[\"input\"].split(\",\")\n            except Exception as e: pass\n            try: config[\"performers\"] = args[\"performers\"]\n            except Exception as e: pass\n            try: config[\"schedule\"] = args[\"schedule\"]\n            except Exception as e: pass\n            try: config[\"questions\"] = args[\"questions\"]\n            except Exception as e: pass\n            try: config[\"duration\"] = args[\"duration\"]\n            except Exception as e: pass\n            try: config[\"expires\"] = args[\"expires\"]\n            except Exception as e: pass\n            from ..snarf import Snarf\n            Snarf.post()\n            Snarf.close()\n        except Exception as e:\n            Settings.dev_print(e)\n        finally:\n            return \"\", 200\n\n    return app\n\ndef main():\n    app = create_app()\n    if str(config[\"debug\"]) == \"True\":\n        app.debug = True\n        app.testing = True\n    app.run(host=\"0.0.0.0\", port=5000)\n\nif __name__ == \"__main__\":\n    main()"
  },
  {
    "path": "OnlySnarf/snarf.py",
    "content": "#!/usr/bin/python3\n\nfrom .lib.driver import Driver\nfrom .lib.config import Config\nfrom .lib.menu import Menu\nfrom .util.settings import Settings\nfrom .classes.discount import Discount\nfrom .classes.message import Message, Post\nfrom .classes.profile import Profile\nfrom .classes.promotion import Promotion\nfrom .classes.user import User\nfrom .server import api as API\n\n#################\n##### Snarf #####\n#################\n\nclass Snarf:\n\n    \"\"\"\n    OnlySnarf main class and runtime parser.\n\n    All methods are static and handle the basic runtime operations, \n     importing variables from settings & args.\n\n    \"\"\"\n\n    def __init__(self):\n        \"\"\"Snarf object\"\"\"\n\n        pass\n\n    @staticmethod\n    def close():\n        Driver.exit_all()\n        Settings.print(\"*snarf waves goodbye*\")\n\n    @staticmethod\n    def api():\n        API.main()\n\n    @staticmethod\n    def config():\n        Config.main()\n\n    @staticmethod\n    def menu():\n        Menu.main()\n\n    @staticmethod\n    def discount():\n\n        \"\"\"\n        Applies a discount to users as provided from args / prompts.\n\n\n        \"\"\"\n\n        try:\n            successful = []\n            for user in Settings.get_users():\n                Settings.print(\"> Discounting fan: {}\".format(user.username))\n                discount = Discount(user.username)\n                successful.append(discount.apply())\n            return all(successful)\n        except Exception as e: Settings.dev_print(e)\n        return False\n\n    @staticmethod\n    def message():\n\n        \"\"\"\n        Sends the configured message from args / prompts.\n\n        \n        \"\"\"\n\n        try:\n            successful = []\n            for user in Settings.get_users():\n                Settings.print(\"> Messaging fan: {}\".format(user.username))\n                message = Message(user.username)\n                successful.append(message.send(user.username, user_id=user.id))\n            return all(successful)\n        except Exception as e: Settings.dev_print(e)\n        return False\n                \n    @staticmethod\n    def post():\n\n        \"\"\"\n        Posts the configured text from args / prompts.\n\n        \n        \"\"\"\n\n        post = Post()\n        try: return post.send()\n        except Exception as e: Settings.dev_print(e)\n        return False\n\n    @staticmethod\n    def profile():\n\n        \"\"\"\n        Runs the profile method specified at runtime.\n\n        backup - downloads all content and saves settings\n\n        syncFrom - reads all profile settings and saves locally\n\n        syncTo - updates profile settings with provided profile\n\n        Extended description of function.\n\n        \"\"\"\n\n        profile = Profile()\n        try: \n            # get profile method\n            method = Settings.get_profile_method()\n            if method == \"backup\": return Profile.backup_content()\n            elif method == \"syncfrom\": return Profile.sync_from_profile()\n            elif method == \"syncto\": return Profile.sync_to_profile()\n            else: Settings.err_print(\"Missing Profile Method\")\n        except Exception as e: Settings.dev_print(e)\n        return False\n        \n    @staticmethod\n    def promotion():\n\n        \"\"\"\n        Runs the promotion method specified at runtime.\n\n        campain - creates discount campaign\n\n        trial - creates free trial\n\n        user - applies directly to user\n\n        grandfather - applies discounted price to existing users and adds them all to list\n\n        \"\"\"\n\n        try: \n            # get promotion method\n            method = Settings.get_promotion_method()\n            if method == \"campaign\": return Promotion.create_campaign()\n            elif method == \"trial\": return Promotion.create_trial_link()\n            elif method == \"user\": return Promotion.apply_to_user()\n            elif method == \"grandfather\": return Promotion.grandfathered()\n            else: Settings.err_print(\"Missing Promotion Method\")\n        except Exception as e: Settings.dev_print(e)\n        return False\n\n    @staticmethod\n    def users():\n\n        \"\"\"\n        Scan users.\n\n        \n        \"\"\"\n\n        try:\n            Settings.set_prefer_local(False)\n            User.get_all_users()\n            return True\n        except Exception as e: Settings.dev_print(e)\n        return False\n\n################################################################################################################################################\n\n# import sys\n\ndef exit_handler():\n    \"\"\"Exit cleanly\"\"\"\n\n    try:\n        Driver.exit_all()\n        # sys.exit(0)\n    except Exception as e:\n        print(e)\n\nimport atexit\natexit.register(exit_handler)\n\ndef main():\n    try:\n        # purge local tmp files\n        # from .classes.file import File\n        # File.remove_local()\n        # get the thing, do the thing\n        action = Settings.get_action()\n        Settings.print(\"Running - {}\".format(action))\n        action = getattr(Snarf, action)\n        action()\n    except Exception as e:\n        Settings.dev_print(e)\n        Settings.print(\"shnarf??\")\n    finally:\n        Settings.print(\"shnarrf!\")\n        exit_handler()\n\n################################################################################################################################################\n\nif __name__ == \"__main__\":\n    main()"
  },
  {
    "path": "OnlySnarf/util/__init__.py",
    "content": ""
  },
  {
    "path": "OnlySnarf/util/args.py",
    "content": "import argparse\nfrom typing import Dict, Any\n\nargs: Dict[str, Any] = {}\n\n########################################################################################################\n\n##\n# Argument Parser\n##\n\nparser = argparse.ArgumentParser(prog='snarf', allow_abbrev=False, epilog=\"Shnarrf!\", \n  description=\"No mention of old Shnarf, I notice. Go ahead, just take all the glory, and leave it to Snarf to clean up after you. I don't mind!\", conflict_handler='resolve')\n\n############\n\nimport os\nif os.environ.get(\"ENV\") != \"test\":\n  from .optional_args import apply_subcommand_args\n  apply_subcommand_args(parser)\nelif os.environ.get(\"ENV\") == \"test\":\n  from .optional_args import apply_shim_args\n  apply_shim_args(parser)\n\nfrom .optional_args import apply_args\napply_args(parser)\n\n##\nimport pkg_resources\nparser.version = str(pkg_resources.get_distribution(\"onlysnarf\").version)\nparser.add_argument('-version', action='version')\n\n############################################################################################\n\ntry:\n  parsedargs, unknownargs = parser.parse_known_args()\n  # print(\"unknown args: {}\".format(unknownargs))\n  args.update(vars(parsedargs))\nexcept Exception as e:\n    print(e)\n    print(\"Error: Incorrect arg format\")\n    parser.exit(1)\n\n#############\n# Debugging #\n# import sys\n# print(args)\n# sys.exit(0)"
  },
  {
    "path": "OnlySnarf/util/colorize.py",
    "content": "\ncolors = {\n    'blue': '\\033[94m',\n    'magenta': '\\033[35m',\n\t'header': '\\033[48;1;34m',\n\t'teal': '\\033[96m',\n\t'pink': '\\033[95m',\n\t'green': '\\033[92m',\n\t'yellow': '\\033[93m',\n\t'menu': '\\033[48;1;44m',\n\t'underline': '\\033[4m',\n\t'fail': '\\033[91m',\n\t'bold': '\\033[1m',\n\t'red': '\\033[31m'\n}\n\nclass fg:\n    BLACK   = '\\033[30m'\n    RED     = '\\033[31m'\n    GREEN   = '\\033[32m'\n    YELLOW  = '\\033[33m'\n    BLUE    = '\\033[34m'\n    MAGENTA = '\\033[35m'\n    CYAN    = '\\033[36m'\n    WHITE   = '\\033[37m'\n    RESET   = '\\033[39m'\n\nclass bg:\n    BLACK   = '\\033[40m'\n    RED     = '\\033[41m'\n    GREEN   = '\\033[42m'\n    YELLOW  = '\\033[43m'\n    BLUE    = '\\033[44m'\n    MAGENTA = '\\033[45m'\n    CYAN    = '\\033[46m'\n    WHITE   = '\\033[47m'\n    RESET   = '\\033[49m'\n\nclass style:\n    BRIGHT    = '\\033[1m'\n    DIM       = '\\033[2m'\n    NORMAL    = '\\033[22m'\n    RESET_ALL = '\\033[0m'\n\n###############\n### Classes ###\n###############\n\ndef colorize(string, color):\n\tif not color in colors: return str(string)\n\treturn \"{}{}{}\".format(colors[color], string,'\\033[0m')\n"
  },
  {
    "path": "OnlySnarf/util/config.py",
    "content": "# parse config file while maintaining default values from args\n\nimport configparser\nimport os\n\n## SAME as Settings.get_base_directory ##\nimport getpass\nUSER = getpass.getuser()\n# USER = os.getenv('USER')\nif str(os.getenv('SUDO_USER')) != \"root\" and str(os.getenv('SUDO_USER')) != \"None\":\n    USER = os.getenv('SUDO_USER')\nconfigFile = \"config.conf\"\n\nif not os.environ.get('ENV') or os.environ.get('ENV') == \"test\":\n  configFile = os.path.join(os.getcwd(), \"OnlySnarf/conf\", \"test-config.conf\")\nelif os.path.isfile(os.path.join(\"/home/{}/.onlysnarf/conf\".format(USER), \"config.conf\")):\n  configFile = os.path.join(\"/home/{}/.onlysnarf/conf\".format(USER), \"config.conf\")\nelse:\n  configFile = os.path.join(os.getcwd(), \"OnlySnarf/conf\", \"config.conf\")\n\nconfig_file = configparser.ConfigParser()\nconfig_file.read(configFile)\n\nconfigs = {}\n\n# relabels config for cleaner usage\nfor section in config_file.sections():\n  for key in config_file[section]:\n    if section == \"ARGS\":\n      configs[key] = config_file[section][key]\n      # print(key, config[key])\n    else:\n      configs[section.lower()+\"_\"+key.lower()] = config_file[section][key].strip(\"\\\"\")\n      # print(key, config[section.lower()+\"_\"+key.lower()])\n\nconfig = {}\n\n# continue to overwrite values from config file with args\n# print(args.items())\n# print(os.environ.get('ENV'))\nif str(os.environ.get('ENV')) != \"test\":\n  from .args import args\n  for key, value in args.items():\n    config[key] = value\nfor key, value in configs.items():\n  config[key] = value  \n\n###############\n## Debugging ##\n# import sys\n# print(config)\n# sys.exit(0)"
  },
  {
    "path": "OnlySnarf/util/defaults.py",
    "content": "import os\nfrom datetime import datetime\nfrom pathlib import Path\n\n##\n# Defaults \n##\n\nACTIONS = [ \"Discount\", \"Message\", \"Post\", \"Profile\", \"Promotion\" ]\n\nAMOUNT_NONE = 0\n\nDATE_FORMAT = \"%Y-%m-%d\"\nTIME_FORMAT = \"%H:%M:%S\"\nSCHEDULE_FORMAT = \"{} {}\".format(DATE_FORMAT, TIME_FORMAT)\n\ndate_ = datetime.strptime(str(datetime.now().strftime(SCHEDULE_FORMAT)), SCHEDULE_FORMAT)\n\nDATE = date_.date().strftime(DATE_FORMAT)[:10]\nTIME = date_.time().strftime(TIME_FORMAT)[:9]\nSCHEDULE = date_.strftime(SCHEDULE_FORMAT)\n\nTIME_NONE = \"00:00:00\"\n\nDEFAULT_MESSAGE = \":)\"\nDEFAULT_REFRESHER = \"hi!\"\nDEFAULT_GREETING = \"hi! thanks for subscribing :3 do you have any preferences?\"\n\nDISCOUNT_MAX_AMOUNT = 55\nDISCOUNT_MIN_AMOUNT = 5\nDISCOUNT_MAX_MONTHS = 12\nDISCOUNT_MIN_MONTHS = 1\n\n## note: '99' aka 'No Limit' no longer allowed?\n# DURATION_ALLOWED = [1,3,7,30, 99] # in days\nDURATION_ALLOWED = [1,3,7,30] # in days\nDURATION_NONE = 0\n\nEXPIRATION_MIN = 1\nEXPIRATION_MAX = 30\nEXPIRATION_NONE = 0\n\nIMAGE_LIMIT = 15\n\nLIMIT_ALLOWED = [0,1,2,3,4,5,6,7,8,9,10,20,30,40,50,60,70,80,90,100] # in %\n\nMESSAGE_CHOICES = [\"all\", \"recent\", \"favorite\", \"renew on\"]\n\nPROMOTION_EXPIRATION_ALLOWED = [int(i) for i in range(30)] # in %\nPROMOTION_EXPIRATION_ALLOWED.insert(0,0)\n# number\nPROMOTION_OFFER_LIMIT = [0,1,2,3,4,5,6,7,8,9,10]\n# various datetime\nPROMOTION_DURATION_ALLOWED = [\"1 day\",\"3 days\",\"7 days\",\"14 days\",\"1 month\",\"3 months\",\"6 months\",\"12 months\"]\n\nPRICE_MINIMUM = 3\nPRICE_MAXIMUM = 200\n\n# sftp\n# selenium browser\nREMOTE_BROWSER = \"127.0.0.1\"\nBROWSER_PORT = 4444\n\nREMOTE_HOST = \"127.0.0.1\"\nREMOTE_PORT = 22\n\n#3600 # 1hr in seconds\nUPLOAD_MAX_DURATION = 60 # 1 minute\n\n\nUSER_LIMIT = 10\n\n#########\n# Paths #\n#########\n\nimport getpass\nUSER = getpass.getuser()\n# USER = os.getenv('USER')\nif str(os.getenv('SUDO_USER')) != \"root\" and str(os.getenv('SUDO_USER')) != \"None\":\n    USER = os.getenv('SUDO_USER')\n\n# linux default\nHOME_DIR = \"/home\"\n# check for Windows\nif os.name == 'nt':\n    HOME_DIR = \"C:\\\\Users\"\nROOT_PATH = os.path.join(HOME_DIR, USER, \".onlysnarf\")\n    \nDOWNLOAD_PATH = os.path.join(ROOT_PATH, \"downloads\")\nUPLOAD_PATH = os.path.join(ROOT_PATH, \"uploads\")\nLOG_PATH = os.path.join(ROOT_PATH, \"snarf.log\")\nREMOTE_PATH = ROOT_PATH\nCONFIGS_PATH = os.path.join(ROOT_PATH, \"conf\")\nUSERS_PATH = os.path.join(CONFIGS_PATH, \"users.json\")\nPROFILE_PATH = os.path.join(CONFIGS_PATH, \"profile.json\")\nCONFIG_PATH = os.path.join(CONFIGS_PATH, \"config.conf\")\nif os.environ.get('ENV') == \"test\":\n    # CONFIG_PATH = os.path.join(CONFIGS_PATH, \"test-config.conf\")\n    CONFIG_PATH = os.path.join(os.getcwd(), \"OnlySnarf\", \"conf\", \"test-config.conf\")\nPROFILES_PATH = os.path.join(CONFIGS_PATH, \"users\")\n\nif os.environ.get('ENV') == \"test\":\n    print(\"Paths:\")\n    print(\"root: \"+ROOT_PATH)\n    print(\"configs: \"+CONFIGS_PATH)\n    print(\"config: \"+CONFIG_PATH)\n    print(\"download: \"+DOWNLOAD_PATH)\n    print(\"log: \"+LOG_PATH)\n    print(\"profiles: \"+PROFILES_PATH)\n    print(\"profile: \"+PROFILE_PATH)\n    print(\"remote: \"+REMOTE_PATH)\n    print(\"upload: \"+UPLOAD_PATH)\n    print(\"users: \"+USERS_PATH)\n\nPath(ROOT_PATH).mkdir(parents=True, exist_ok=True)\nPath(DOWNLOAD_PATH).mkdir(parents=True, exist_ok=True)\nPath(UPLOAD_PATH).mkdir(parents=True, exist_ok=True)\nPath(CONFIGS_PATH).mkdir(parents=True, exist_ok=True)\nPath(PROFILES_PATH).mkdir(parents=True, exist_ok=True)\n"
  },
  {
    "path": "OnlySnarf/util/logger.py",
    "content": "import os\nimport logging\nfrom pathlib import Path\nfrom . import defaults as DEFAULT\nfrom .config import config\n\nloglevel = logging.INFO\nif config[\"debug\"]: loglevel = logging.DEBUG\nif int(config[\"verbose\"]) >= 2: loglevel = logging.DEBUG\n\nlogPath = DEFAULT.LOG_PATH\nif os.environ.get('ENV') == \"test\":\n    logPath = os.path.join(os.getcwd(), \"log\", \"snarf.log\")\n\nPath(os.path.dirname(logPath)).mkdir(parents=True, exist_ok=True)\n\n# set up logging to file - see previous section for more details\nlogging.basicConfig(level=loglevel,\n                    format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s',\n                    datefmt='%m-%d %H:%M',\n                    filename=logPath,\n                    filemode='w')\n\n# https://stackoverflow.com/questions/384076/how-can-i-color-python-logging-output\nclass CustomFormatter(logging.Formatter):\n    \"\"\"Logging Formatter to add colors and count warning / errors\"\"\"\n\n    teal = \"\"\n    grey = \"\\x1b[38;21m\"\n    yellow = \"\\x1b[33;21m\"\n    red = \"\\x1b[31;21m\"\n    bold_red = \"\\x1b[31;1m\"\n    reset = \"\\x1b[0m\"\n    # the filename & line isn't helpful when i'm redirecting through Settings.maybe_print & dev_print\n    # format = \"%(asctime)s - %(name)s - %(levelname)s - %(message)s (%(filename)s:%(lineno)d)\"\n    format = \"%(asctime)s - %(name)s - %(levelname)s - %(message)s\"\n\n    FORMATS = {\n        logging.DEBUG: grey + format + reset,\n        logging.INFO: grey + format + reset,\n        logging.WARNING: yellow + format + reset,\n        logging.ERROR: red + format + reset,\n        logging.CRITICAL: bold_red + format + reset\n    }\n\n    def format(self, record):\n        log_fmt = self.FORMATS.get(record.levelno)\n        formatter = logging.Formatter(log_fmt)\n        return formatter.format(record)\n\n# define a Handler which writes INFO messages or higher to the sys.stderr\nconsole = logging.StreamHandler()\n# tell the handler to use this format\nconsole.setFormatter(CustomFormatter())\n# add the handler to the root logger\nlogging.getLogger('').addHandler(console)"
  },
  {
    "path": "OnlySnarf/util/optional_args.py",
    "content": "import argparse\nfrom .validators import valid_amount, valid_price, valid_path\nfrom .validators import valid_date, valid_month, valid_schedule, valid_time\nfrom .validators import valid_duration, valid_expiration, valid_limit\nfrom .validators import valid_promo_duration, valid_promo_expiration\nfrom . import defaults as DEFAULT\n\ndef apply_args(parser):\n\n  #############\n  ## General ##\n  #############\n\n  ##\n  # -browser\n  parser.add_argument('-browser', '-B', type=str, default=\"auto\", choices=[\"auto\",\"brave\",\"chrome\",\"chromium\",\"firefox\",\"remote\"], dest='browser', help='web browser to use')\n  # parser.add_argument('-browser', '-B', type=str, default=\"auto\", choices=[\"auto\",\"brave\",\"chrome\",\"chromium\",\"firefox\",\"ie\",\"edge\",\"opera\",\"remote\"], dest='browser', help='web browser to use')\n  ##\n  # -login\n  # method to prefer when logging in\n  # note: \"google\" is disabled due to updates&testing requirements\n  parser.add_argument('-login', '-L', dest='login', default=\"auto\", choices=[\"auto\",\"onlyfans\",\"twitter\"], help='method of user login to prefer')\n  ##\n  # -reduce\n  # enables file reduction\n  parser.add_argument('-reduce', action='store_true', dest='reduce', help='enable reducing files over 50 MB')\n  ##\n  # --save\n  # saves OnlyFans users upon exit\n  parser.add_argument('-save', '-S', action='store_true', dest='save_users', help='enable saving users locally on exit')\n  ##\n  # -tweet\n  # enabled tweeting\n  parser.add_argument('-tweet', action='store_true', dest='tweeting', help='enable tweeting when posting')\n  ##\n  # --username\n  # OnlyFans username to use\n  parser.add_argument('--username', '--u', type=str, default=\"default\", dest='username', help='OnlyFans username to use')\n  ##\n  # -phone\n  # OnlyFans phone number to use for additional login steps\n  parser.add_argument('-phone', type=str, default=\"\", dest='phone', help='OnlyFans phone number to use')\n\n  ###############\n  ## DEBUGGING ##\n  ###############\n\n  ##\n  # -config\n  # path to config.conf file\n  parser.add_argument('-config', '-C', dest=\"path_config\", type=str, help='path to config.conf', default=DEFAULT.CONFIG_PATH)\n  ##\n  # -cookies\n  # load & save from/to local cookies path\n  parser.add_argument('-cookies', action='store_true', dest='cookies', help=argparse.SUPPRESS)\n  # -debug\n  # debugging - skips uploading and deleting unless otherwise forced\n  parser.add_argument('-debug', '-D', action='store_true', dest='debug', help='enable debugging')\n  ##\n  # -debug-delay\n  # user message delay\n  parser.add_argument('-debug-delay', action='store_true', dest='debug_delay', help=argparse.SUPPRESS)\n  ##\n  # -debug-firefox\n  # enables trace logging for firefox\n  parser.add_argument('-debug-firefox', action='store_true', dest='debug_firefox', help=argparse.SUPPRESS)\n  ##\n  # -debug-google\n  # enables trace logging for google chrome\n  parser.add_argument('-debug-chrome', action='store_true', dest='debug_chrome', help=argparse.SUPPRESS)\n  ##\n  # -debug-selenium\n  # enables selenium logging\n  parser.add_argument('-debug-selenium', action='store_true', dest='debug_selenium', help=argparse.SUPPRESS)\n  ##\n  # -download-max\n  # maximum number of images to download\n  parser.add_argument('-download-max', type=int, default=DEFAULT.IMAGE_LIMIT, dest=\"download_limit\", help=argparse.SUPPRESS)\n  ##\n  # -download-path\n  # the path to the downloaded files\n  parser.add_argument('-download-path', type=str, dest='path_download', default=DEFAULT.DOWNLOAD_PATH, help=argparse.SUPPRESS)\n  ##\n  # -force-upload\n  # ignore upload max wait\n  parser.add_argument('-force-upload', action='store_true', dest='force_upload', help=argparse.SUPPRESS)\n  ##\n  # -keep\n  # keep the browser window open\n  parser.add_argument('-keep', '-K', action='store_true', dest='keep', help='keep browser window open after scripting ends')\n  ##\n  # -prefer-local\n  # prefers local user cache over refreshing first call\n  parser.add_argument('-prefer-local', default=True, action='store_false', dest='prefer_local', help='prefer recently cached data')\n  ##\n  # -show\n  # shows window\n  parser.add_argument('-show', '-SW', dest='show', action='store_true',  help='enable displaying browser window')\n  ##\n  # -skip-download\n  parser.add_argument('-skip-download', action='store_true', dest='skip_download', help=argparse.SUPPRESS)\n  ##\n  # -skip-upload\n  # skips file upload\n  parser.add_argument('-skip-upload', action='store_true', dest='skip_upload', help=argparse.SUPPRESS)\n  ##\n  # -skip-users\n  # list of users to skip\n  parser.add_argument('-skip-users', dest='skipped_users', action='append', help=argparse.SUPPRESS)\n  ##\n  # -upload-max\n  # maximum number of images that can be uploaded\n  parser.add_argument('-upload-max', type=int, default=DEFAULT.IMAGE_LIMIT, dest='upload_max', help=argparse.SUPPRESS)\n  ##\n  # -upload-max-duration\n  # the max number of 10 minute intervals to upload for\n  parser.add_argument('-upload-max-duration', dest='upload_max_duration', default=DEFAULT.UPLOAD_MAX_DURATION, type=int, help=argparse.SUPPRESS)\n  ##\n  # -user-path\n  # the path to the users.json file\n  parser.add_argument('-users-path', type=str, dest='path_users', default=DEFAULT.USERS_PATH, help=argparse.SUPPRESS)\n  ##\n  # -verbose\n  # v, vv, vvv\n  parser.add_argument('-v', '-verbose', dest=\"verbose\", action='count', default=0, help=\"verbosity level (max 3)\")\n\n\ndef apply_subcommand_args(parser):\n\n  subparsers = parser.add_subparsers(help='Include a sub-command to run a corresponding action:', dest=\"action\", required=True)\n\n  #########\n  ## API ##\n  #########\n\n  parser_config = subparsers.add_parser('api', help='> flask server')\n\n  ############\n  ## Config ##\n  ############\n\n  parser_config = subparsers.add_parser('config', help='> configuration options')\n\n  ##############\n  ## Discount ##\n  ##############\n\n  parser_discount = subparsers.add_parser('discount', help='> discount one or more users')\n  userAndUsers = parser_discount.add_mutually_exclusive_group()\n  ##\n  # -amount\n  # action: discount\n  # the amount to discount a user by\n  parser_discount.add_argument('-amount', type=valid_amount, dest='amount', help='amount (%%) to discount by', default=DEFAULT.AMOUNT_NONE)\n  ##\n  # -months\n  # action: discount\n  # the number of months to discount for\n  parser_discount.add_argument('-months', type=valid_month, default=None, dest='months', help='number of months to discount')\n  ##\n  # -user\n  # the user to discount\n  userAndUsers.add_argument('-user', type=str,  default=None, dest='user', help='user to discount')\n  ##\n  # -users\n  # the users to discount\n  userAndUsers.add_argument('-users', dest='users', action='append', default=[], help='users to discount')\n\n  ##########\n  ## Menu ##\n  ##########\n\n  parser_menu = subparsers.add_parser('menu', help='> access the cli menu')\n\n  #############\n  ## Message ##\n  #############\n\n  parser_message = subparsers.add_parser('message', help='> send a message to one or more users')\n  dateAndSchedule = parser_message.add_mutually_exclusive_group()\n  userAndUsers = parser_message.add_mutually_exclusive_group()\n  ##\n  # -date\n  # date in MM-DD-YYYY\n  dateAndSchedule.add_argument('-date', type=valid_date, default=DEFAULT.DATE, dest='date', help='schedule date (MM-DD-YYYY)')\n  ##\n  # -performers\n  # list of performers to tag\n  parser_message.add_argument('-performers', dest='performers', action='append',  default=[], help='performers to reference. adds \\\"@[...performers]\\\"')\n  ##\n  # -price\n  # the price to be set in a message\n  parser_message.add_argument('-price', type=valid_price, help='price to charge ($)', default=0, dest='price')\n  ##\n  # -schedule\n  # the schedule to upload a post for\n  dateAndSchedule.add_argument('-schedule', type=valid_schedule, default=DEFAULT.SCHEDULE, dest='schedule', help='schedule (MM-DD-YYYY:HH:MM:SS)')\n  ##\n  # -time\n  # time in HH:MM\n  parser_message.add_argument('-time', type=valid_time, default=DEFAULT.TIME, dest='time', help='time (HH:MM)')\n  ##\n  # -tags\n  # @[tag]\n  parser_message.add_argument('-tags', dest='tags', action='append', default=[], help='the tags (@[tag])')\n  ##\n  # -text\n  # text for message or upload\n  parser_message.add_argument('-text', default=None, dest='text', help='text to send')\n  ##\n  # -user\n  # the user to message\n  userAndUsers.add_argument('-user', type=str,  default=None, dest='user', help='user to message')\n  ##\n  # -users\n  # the users to message\n  userAndUsers.add_argument('-users', dest='users', action='append', default=[], help='users to message')\n  ##\n  # input\n  parser_message.add_argument('input', default=[], nargs=argparse.REMAINDER, type=valid_path, help='one or more paths to files (or folder) to include in the message')\n\n  ##########\n  ## Post ##\n  ##########\n\n  parser_post = subparsers.add_parser('post', help='> upload a post')\n  dateAndSchedule = parser_post.add_mutually_exclusive_group()\n  durationAndExpiration = parser_post.add_mutually_exclusive_group()\n\n  ##\n  # -date\n  # date in MM-DD-YYYY\n  dateAndSchedule.add_argument('-date', type=valid_date, default=DEFAULT.DATE, dest='date', help='schedule date (MM-DD-YYYY)')\n  ##\n  # -duration\n  # poll duration\n  durationAndExpiration.add_argument('-duration', type=valid_duration, dest='duration',\n    help='duration in days (99 for \\'No Limit\\') for a poll', choices=DEFAULT.DURATION_ALLOWED, default=DEFAULT.DURATION_NONE)\n  ##\n  # -expiration\n  # date of post or poll expiration\n  durationAndExpiration.add_argument('-expiration', type=valid_expiration, dest='expiration', help='expiration in days (999 for \\'No Limit\\')', default=DEFAULT.EXPIRATION_NONE)\n  ##\n  # -performers\n  # list of performers to tag\n  parser_post.add_argument('-performers', dest='performers', action='append',  default=[], help='performers to reference. adds \\\"@[...performers]\\\"')\n  ##\n  # -price\n  # price to be set in a message\n  parser_post.add_argument('-price', type=valid_price, help='price to charge ($)', default=0, dest='price')\n  ##\n  # -schedule\n  # schedule to upload a post for\n  dateAndSchedule.add_argument('-schedule', type=valid_schedule, default=DEFAULT.SCHEDULE, dest='schedule', help='schedule (MM-DD-YYYY:HH:MM:SS)')\n  ##\n  # -time\n  # time in HH:MM\n  parser_post.add_argument('-time', type=valid_time, default=DEFAULT.TIME, dest='time', help='time (HH:MM)')\n  ##\n  # -tags\n  # @[tag]\n  parser_post.add_argument('-tags', dest='tags', action='append', default=[], help='tags (@[tag])')\n  ##\n  # -text\n  # text for message or upload\n  parser_post.add_argument('-text', default=None, dest='text', help='text to send')\n  ##\n  # -question\n  # poll questions\n  parser_post.add_argument('-question', '-Q', dest='questions', action='append', default=[], help='questions to ask')\n  ##\n  # input\n  parser_post.add_argument('input', default=[], nargs=argparse.REMAINDER, type=valid_path, help='one or more paths to files (or folders) to include in the post')\n\n  ##########\n  ## User ##\n  ##########\n\n  parser_user = subparsers.add_parser('users', help='> scan & save users')\n\ndef apply_shim_args(parser):\n  parser.add_argument('-amount', type=valid_amount, dest='amount', help='amount (%%) to discount by', default=DEFAULT.AMOUNT_NONE)\n  parser.add_argument('-months', type=valid_month, default=None, dest='months', help='number of months to discount')\n  parser.add_argument('-user', type=str,  default=None, dest='user', help='user to discount')\n  parser.add_argument('-users', dest='users', action='append', default=[], help='users to discount')\n  parser.add_argument('-date', type=valid_date, default=DEFAULT.DATE, dest='date', help='schedule date (MM-DD-YYYY)')\n  parser.add_argument('-price', type=valid_price, help='price to charge ($)', default=0, dest='price')\n  parser.add_argument('-schedule', type=valid_schedule, default=DEFAULT.SCHEDULE, dest='schedule', help='schedule (MM-DD-YYYY:HH:MM:SS)')\n  parser.add_argument('-time', type=valid_time, default=DEFAULT.TIME, dest='time', help='time (HH:MM)')\n  parser.add_argument('-tags', dest='tags', action='append', default=[], help='the tags (@[tag])')\n  parser.add_argument('-text', default=None, dest='text', help='text to send')\n  parser.add_argument('-duration', type=valid_duration, dest='duration', help='duration in days (99 for \\'No Limit\\') for a poll', choices=DEFAULT.DURATION_ALLOWED, default=DEFAULT.DURATION_NONE)\n  parser.add_argument('-expiration', type=valid_expiration, dest='expiration', help='expiration in days (999 for \\'No Limit\\')', default=DEFAULT.EXPIRATION_NONE)\n  parser.add_argument('-question', '-Q', dest='questions', action='append', default=[], help='questions to ask')\n  parser.add_argument('input', default=[], nargs=argparse.REMAINDER, type=valid_path, help='one or more paths to files (or folder) to include in the message')\n"
  },
  {
    "path": "OnlySnarf/util/settings.py",
    "content": "import pkg_resources\nimport time\nimport os, json, sys\nfrom datetime import datetime\nfrom pathlib import Path\n##\nfrom .colorize import colorize\nfrom .config import config\nfrom . import defaults as DEFAULT\nfrom .validators import valid_schedule, valid_time\nfrom .logger import logging\nlog = logging.getLogger('onlysnarf')\n\nclass Settings:\n    \n    LAST_UPDATED_KEY = None\n    CATEGORY = None\n    FILES = None\n    PERFORMER_CATEGORY = None\n\n    #####################\n    ##### Functions #####\n    #####################\n\n    def debug_delay_check():\n        if str(Settings.is_debug()) == \"True\" and str(Settings.is_debug_delay()) == \"True\":\n            Settings.dev_print(\"napping...\")\n            time.sleep(10)\n\n    ##\n    # Print\n    ##\n\n    def print(text):\n        if int(config[\"verbose\"]) >= 0:\n            log.info(text)\n\n    def print_same_line(text):\n        sys.stdout.write('\\r')\n        sys.stdout.flush()\n        sys.stdout.write(text)\n        sys.stdout.flush()\n\n    def maybe_print(text):\n        if int(config[\"verbose\"]) >= 1 or str(os.environ.get(\"ENV\")) == \"test\":\n            log.debug(text)\n\n    def dev_print(text):\n        if int(config[\"verbose\"]) >= 2 or str(os.environ.get(\"ENV\")) == \"test\":\n            log.debug(text)\n\n    def err_print(error):\n        log.error(error)\n\n    def warn_print(error):\n        log.warning(error)\n\n    def format_date(date):\n        if isinstance(date, str):\n            return datetime.strptime(date, DEFAULT.DATE_FORMAT).strftime(DEFAULT.DATE_FORMAT)\n        else:\n            return date.strftime(DEFAULT.DATE_FORMAT)\n\n    def format_time(time):\n        if isinstance(time, str):\n            return datetime.strptime(time, DEFAULT.TIME_FORMAT).strftime(DEFAULT.TIME_FORMAT)\n        else:\n            return time.strftime(DEFAULT.TIME_FORMAT)\n\n    def header():\n        Settings.print(\"### SETTINGS ###\")\n        Settings.print(\"...\")\n        Settings.print(\"...\")\n        Settings.print(\"...\")\n\n    def menu():\n        Settings.print(\"### SETTINGS MENU ###\")\n        Settings.print(\"...\")\n        Settings.print(\"...\")\n        Settings.print(\"...\")\n\n    def ensure_paths():\n        Path(DEFAULT.ROOT_PATH).mkdir(parents=True, exist_ok=True)\n        Path(DEFAULT.DOWNLOAD_PATH).mkdir(parents=True, exist_ok=True)\n        Path(DEFAULT.UPLOAD_PATH).mkdir(parents=True, exist_ok=True)\n        Path(DEFAULT.CONFIGS_PATH).mkdir(parents=True, exist_ok=True)\n        Path(DEFAULT.PROFILES_PATH).mkdir(parents=True, exist_ok=True)\n\n    ##\n    # Getters\n    ##\n\n    def get_action():\n        return config[\"action\"]\n\n    def get_actions():\n        return DEFAULT.ACTIONS\n\n    def get_amount():\n        return config[\"amount\"]\n\n    def get_base_directory():\n        return DEFAULT.ROOT_PATH\n\n    def get_browser_type():\n        return config[\"browser\"]\n\n    def get_months():\n        return config[\"months\"]\n\n    def get_category():\n        cat = config[\"category\"]\n        if str(cat) == \"image\": cat = \"images\"\n        if str(cat) == \"gallery\": cat = \"galleries\"\n        if str(cat) == \"video\": cat = \"videos\"\n        if str(cat) == \"performer\": cat = \"performers\"\n        return cat or None\n\n    def get_categories():\n        cats = []\n        cats.extend(list(DEFAULT.CATEGORIES))\n        cats.extend(list(config[\"categories\"]))\n        return cats\n\n    def get_cookies_path():\n        # return os.path.join(Settings.get_base_directory(), Settings.get_username(), \"cookies.pkl\")\n        return os.path.join(Settings.get_base_directory(), \"cookies.pkl\")\n\n    def get_price():\n        try: return config[\"price\"]\n        except Exception as e: pass\n        return 0\n\n    def get_price_minimum():\n        return DEFAULT.PRICE_MINIMUM\n\n    def get_price_maximum():\n        return DEFAULT.PRICE_MAXIMUM\n\n    def get_default_greeting():\n        return DEFAULT.GREETING or \"\"\n\n    def get_default_refresher():\n        return DEFAULT.REFRESHER or \"\"\n        \n    def get_discount_max_amount():\n        return DEFAULT.DISCOUNT_MAX_AMOUNT or 0\n        \n    def get_discount_min_amount():\n        return DEFAULT.DISCOUNT_MIN_AMOUNT or 0\n        \n    def get_discount_max_months():\n        return DEFAULT.DISCOUNT_MAX_MONTHS or 0\n        \n    def get_discount_min_months():\n        return DEFAULT.DISCOUNT_MIN_MONTHS or 0\n\n    def get_download_max():\n        return config[\"download_limit\"] or DEFAULT.IMAGE_LIMIT\n\n    def get_duration():\n        try: return config[\"duration\"]\n        except Exception as e: pass\n        return None\n\n    def get_promo_duration():\n        try: return config[\"duration_promo\"]\n        except Exception as e: pass\n        return None\n        \n    def get_duration_allowed():\n        return DEFAULT.DURATION_ALLOWED or []\n        \n    def get_duration_promo_allowed():\n        return DEFAULT.PROMOTION_DURATION_ALLOWED or []\n\n    def get_expiration():\n        try: return config[\"expiration\"]\n        except Exception as e: pass\n        return DEFAULT.EXPIRATION_NONE\n\n    def get_promo_expiration():\n        return config[\"promotion_expiration\"]\n\n    def get_input():\n        # fix pytest bug from 4.4.9\n        files = []\n        for file_path in config[\"input\"]:\n            if \".py\" not in str(file_path):\n                files.append[file_path]\n        return set(files)\n\n    def get_input_as_files():\n        if Settings.FILES: return Settings.FILES\n        from ..classes.file import File\n        files = []\n        try:\n            _input = config[\"input\"]\n            if isinstance(_input, list):\n                for file_path in _input:\n                    file = File()\n                    setattr(file, \"path\", file_path)\n                    files.append(file)\n            else:\n                file = File()\n                setattr(file, \"path\", _input)\n                files.append(file)\n        except Exception as e:\n            pass\n        Settings.FILES = files\n        return files\n        \n    def get_logs_path(process):\n        if process == \"firefox\":\n            path_ = os.path.join(Settings.get_base_directory(), \"log\")\n            Path(path_).mkdir(parents=True, exist_ok=True)\n            return os.path.join(path_, \"geckodriver.log\")\n        elif process == \"google\":\n            path_ = os.path.join(Settings.get_base_directory(), \"log\")\n            Path(path_).mkdir(parents=True, exist_ok=True)\n            return os.path.join(path_, \"chromedriver.log\")\n        return \"\"\n\n    def get_message_choices():\n        return DEFAULT.MESSAGE_CHOICES\n\n    def get_root_path():\n        return DEFAULT.ROOT_PATH\n\n    def get_sort_method():\n        return config[\"sort\"] or \"random\"\n\n    def get_phone_number():\n        try:\n            return config[\"phone\"]\n        except Exception as e:\n            Settings.err_print(\"missing phone number!\")\n        return None\n\n    def get_performers():\n        try:\n            performers = config[\"performers\"] or []\n            performers = [n.strip() for n in performers]\n            return performers\n        except Exception as e: pass\n        return []\n\n    def get_profile_path():\n        try: return config[\"path_profile\"]\n        except Exception as e: pass\n        return DEFAULT.PROFILE_PATH\n\n    def get_recent_user_count():\n        return config[\"recent_users_count\"] or 0\n    \n    def get_promotion_limit():\n        return config[\"promotion_limit\"] or None\n\n    def get_promotion_method():\n        return config[\"promotion_method\"] or None\n\n    def get_password(username=None):\n        try:\n            if not username: username = Settings.get_username()\n            return Settings.get_user_config(username)[\"onlyfans_password\"]\n        except Exception as e: pass\n        return \"\"\n\n    def get_password_google(username=None):\n        try:\n            if not username: username = Settings.get_username()\n            return Settings.get_user_config(username)[\"google_password\"]\n        except Exception as e: pass\n        return \"\"\n\n    def get_password_twitter(username=None):\n        try: \n            if not username: username = Settings.get_username()\n            return Settings.get_user_config(username)[\"twitter_password\"]\n        except Exception as e: pass\n        return \"\"\n\n    def get_download_path():\n        try: return config[\"path_download\"]\n        except Exception as e: pass\n        return DEFAULT.DOWNLOAD_PATH\n\n    def get_users_path():\n        try: return config[\"path_users\"]\n        except Exception as e: pass\n        return DEFAULT.USERS_PATH\n\n    def get_config_path():\n        try: return config[\"path_config\"]   \n        except Exception as e: pass\n        return DEFAULT.CONFIG_PATH\n\n    def get_local_path():\n        localPath = os.path.join(Settings.get_root_path(), Settings.get_username())\n        Path(localPath).mkdir(parents=True, exist_ok=True)\n        for cat in Settings.get_categories():\n            Path(os.path.join(localPath, cat)).mkdir(parents=True, exist_ok=True)\n        return localPath\n\n    def get_reconnect_id():\n        return config[\"session_id\"] or \"\"\n\n    def get_reconnect_url():\n        return config[\"session_url\"] or \"\"\n\n    def get_remote_host():\n        return config[\"remote_host\"] or DEFAULT.REMOTE_HOST\n\n    def get_remote_port():\n        return config[\"remote_port\"] or DEFAULT.REMOTE_PORT\n\n    def get_remote_path():\n        return config[\"remote_path\"] or DEFAULT.REMOTE_PATH\n\n    def get_remote_username():\n        return config[\"remote_username\"] or \"\"\n\n    def get_remote_password():\n        return config[\"remote_password\"] or \"\"\n\n    def get_profile_method():\n        return config[\"profile_method\"] or None\n\n    def get_date():\n        try:\n            config[\"date\"] = Settings.format_date(config[\"date\"])\n            if str(config[\"date\"]) == DEFAULT.DATE and str(config[\"schedule\"]) != DEFAULT.SCHEDULE and str(config[\"schedule\"] != \"None\"):\n                if isinstance(config[\"schedule\"], str):\n                    config[\"date\"] = datetime.strptime(config[\"schedule\"], DEFAULT.SCHEDULE_FORMAT).date().strftime(DEFAULT.DATE_FORMAT)\n                else:\n                    config[\"date\"] = config[\"schedule\"].date().strftime(DEFAULT.DATE_FORMAT)\n                config[\"date\"] = datetime.strptime(str(config[\"date\"]), DEFAULT.DATE_FORMAT)\n            else:\n                config[\"date\"] = datetime.strptime(str(config[\"date\"]), DEFAULT.DATE_FORMAT)\n            config[\"date\"] = config[\"date\"].strftime(DEFAULT.DATE_FORMAT)    \n        except Exception as e:\n            config[\"date\"] = datetime.strptime(DEFAULT.DATE, DEFAULT.DATE_FORMAT)\n        Settings.maybe_print(\"date (settings): {}\".format(str(config[\"date\"])[:10]))\n        return str(config[\"date\"])[:10]\n\n    def get_time():\n        try:\n            config[\"time\"] = Settings.format_time(config[\"time\"])        \n            if (str(config[\"time\"]) == DEFAULT.TIME or str(config[\"time\"]) == DEFAULT.TIME_NONE) and str(config[\"schedule\"]) != DEFAULT.SCHEDULE and str(config[\"schedule\"]) != \"None\":\n                Settings.dev_print(\"time from schedule\")\n                date = datetime.strptime(str(config[\"schedule\"]), DEFAULT.SCHEDULE_FORMAT)\n                config[\"time\"] = datetime.strptime(str(date.time().strftime(DEFAULT.TIME_FORMAT)), DEFAULT.TIME_FORMAT)\n            else:\n                Settings.dev_print(\"time from config\")\n                config[\"time\"] = datetime.strptime(str(config[\"time\"]), DEFAULT.TIME_FORMAT)\n            config[\"time\"] = config[\"time\"].strftime(DEFAULT.TIME_FORMAT)\n        except Exception as e:\n            config[\"time\"] = datetime.strptime(DEFAULT.TIME, DEFAULT.TIME_FORMAT).strftime(DEFAULT.TIME_FORMAT)\n        Settings.maybe_print(\"time (settings): {}\".format(str(config[\"time\"])[:9]))\n        return str(config[\"time\"])[:9]\n\n    def get_schedule():\n        schedule = \"\"\n        try:\n            schedule = config[\"schedule\"]\n            if str(schedule) == \"None\": schedule = DEFAULT.SCHEDULE\n            if str(schedule) == DEFAULT.SCHEDULE:\n                schedule = datetime.strptime(schedule, DEFAULT.SCHEDULE_FORMAT).strftime(DEFAULT.SCHEDULE_FORMAT)\n            elif not isinstance(schedule, str):\n                schedule = schedule.strftime(DEFAULT.SCHEDULE_FORMAT)\n        except Exception as e:\n            schedule = datetime.strptime(\"{} {}\".format(Settings.get_date(), Settings.get_time()), DEFAULT.SCHEDULE_FORMAT).strftime(DEFAULT.SCHEDULE_FORMAT)\n        Settings.maybe_print(\"schedule (settings): {}\".format(schedule))\n        return str(schedule)[:20] # must be less than 19 characters\n\n    def get_keywords():\n        keywords = []\n        try:\n            keywords = config[\"keywords\"]\n            keywords = [n.strip() for n in keywords]\n        except Exception as e:\n            pass\n            # Settings.err_print(e)\n        return keywords\n\n    def get_text():\n        return config[\"text\"] or \"\"\n\n    def get_title():\n        return config[\"title\"] or \"\"\n        \n    def get_skipped_users():\n        return config[\"skipped_users\"] or []\n        \n    def get_questions():\n        try:\n            return config[\"questions\"]\n        except Exception as e:\n            pass\n        return []\n        \n    def get_upload_max():\n        try:\n            return config[\"upload_max\"]\n        except Exception as e:\n            pass\n        return DEFAULT.IMAGE_LIMIT\n        \n    # def get_upload_max_messages():\n        # return config[\"upload_max_messages\"] or UPLOAD_MAX_MESSAGES\n\n    def get_login_method():\n        try: return config[\"login\"]\n        except Exception as e: pass\n        return \"auto\"\n        \n    def get_upload_max_duration():\n        return config[\"upload_max_duration\"] or DEFAULT.UPLOAD_MAX_DURATION # 1 hour\n\n    # comma separated string of usernames\n    def get_users():\n        from ..classes.user import User\n        if str(config[\"user\"]) != \"None\":\n            if str(config[\"user\"]) == \"all\":\n                config[\"users\"].extend([user.username for user in User.get_all_users()])\n            elif str(config[\"user\"]) == \"recent\":\n                config[\"users\"].extend([user.username for user in User.get_recent_users()])\n            elif str(config[\"user\"]) == \"favorite\":\n                config[\"users\"].extend([user.username for user in User.get_favorite_users()])\n            elif str(config[\"user\"]) == \"random\":\n                config[\"users\"] = [User.get_random_user().username]\n            else: config[\"users\"].append(config[\"user\"])\n        users = []\n        for user in [n.strip() for n in config[\"users\"]]:\n            if isinstance(user, User): pass\n            elif isinstance(user, str): user = User({\"username\":user})\n            # BUG (potential): might bug out if the username is for whatever reason all numbers\n            elif isinstance(user, int): user = User({\"id\":user})\n            users.append(user)\n        return users\n\n    def get_user():\n        return Settings.get_users()[0]\n\n    def get_user_configs():\n        # load configs from .onlysnarf or baseDir\n        pass\n\n    def get_user_config(username=\"default\"):\n        import configparser\n        config_file = configparser.ConfigParser()\n        # strip email\n        if \"@\" in username: username = username[0 : username.index(\"@\")]\n        username = username.replace(\".conf\", \"\") # filename formatting\n        Settings.dev_print(\"retrieving user config: {}\".format(username))\n        Settings.dev_print(os.path.join(Settings.get_base_directory(), \"conf/users\", username+\".conf\"))\n        config_file.read(os.path.join(Settings.get_base_directory(), \"conf/users\", username+\".conf\"))\n        userConfig = {}\n        for section in config_file.sections():\n            # Settings.dev_print(section)\n            for key in config_file[section]:\n                # Settings.dev_print(section, key, config_file[section][key].strip(\"\\\"\"))\n                userConfig[section.lower()+\"_\"+key.lower()] = config_file[section][key].strip(\"\\\"\")\n        # Settings.dev_print(userConfig)\n        return userConfig\n\n    def get_user_config_path(username=\"default\"):\n        if \".conf\" not in str(username): username = username+\".conf\"\n        return os.path.join(Settings.get_base_directory(), \"conf/users\", username)\n\n    def get_username():\n        return config[\"username\"] or \"default\"\n\n    def get_username_onlyfans(username=None):\n        try:\n            if not username: username = Settings.get_username()\n            return Settings.get_user_config(username)[\"onlyfans_username\"]\n        except Exception as e: pass\n        return \"\"\n\n    def get_username_google(username=None):\n        try:\n            if not username: username = Settings.get_username()\n            return Settings.get_user_config(username)[\"google_username\"]\n        except Exception as e: pass\n        return \"\"            \n\n    def get_username_twitter(username=None):\n        try:\n            if not username: username = Settings.get_username()\n            return Settings.get_user_config(username)[\"twitter_username\"]\n        except Exception as e: pass\n        return \"\"\n\n    def get_remote_browser_host():\n        return config[\"remote_browser_host\"]\n\n    def get_remote_browser_port():\n        return config[\"remote_browser_port\"]\n\n    ## TODO\n    # add arg -profile\n    # add method for reading config profiles from conf/users\n\n    def get_profile():\n        pass\n\n    def select_profile():\n        pass\n\n    # def get_users_favorite():\n    #     return config[\"users_favorite\"] or []\n        \n    def get_verbosity():\n        return config[\"verbose\"] or 0\n\n    def get_version():\n        return pkg_resources.get_distribution(\"onlysnarf\").version\n\n    def get_user_num():\n        return config[\"users_read\"] or DEFAULT.USER_LIMIT\n\n    # Bools\n\n    def is_confirm():\n        return config[\"confirm\"]\n\n    def is_cookies():\n        return config[\"cookies\"]\n\n    def is_delete():\n        return config[\"delete\"]\n\n    def is_delete_empty():\n        return config[\"delete_empty\"]\n\n    def is_debug(process=None):\n        if process == \"firefox\": return config[\"debug_firefox\"]\n        elif process == \"chrome\": return config[\"debug_chrome\"]\n        elif process == \"selenium\": return config[\"debug_selenium\"]\n        elif process == \"cookies\": return config[\"debug_cookies\"]\n        # elif process == \"tests\": return \n        return config[\"debug\"]\n\n    def is_debug_delay():\n        return config[\"debug_delay\"]\n\n    def is_force_upload():\n        return config[\"force_upload\"]\n\n    def is_keep():\n        return config[\"keep\"]\n\n    def is_prefer_local():\n        return config[\"prefer_local\"]\n        \n    def is_prefer_local_following():\n        return config[\"prefer_local_following\"]\n\n    def is_save_users():\n        return config[\"save_users\"]\n        \n    def is_reduce():\n        return config[\"reduce\"]\n    \n    def is_show_window():\n        return config[\"show\"]\n        \n    def is_tweeting():\n        return config[\"tweeting\"]\n        \n    def is_backup():\n        return config[\"backup\"]\n        \n    def is_skip_download():\n        return config[\"skip_download\"]\n        \n    def is_skip_upload():\n        return config[\"skip_upload\"]\n\n    ##\n    # Setters\n    ##\n\n    def set_bycategory(cat):\n        config[\"bycategory\"] = cat\n\n    def set_category(cat):\n        config[\"category\"] = cat\n\n    def set_cookies(value):\n        config[\"cookies\"] = value\n\n    def set_confirm(value):\n        config[\"confirm\"] = value\n\n    def set_email(email):\n        config[\"email\"] = str(email)\n\n    def set_debug(newValue):\n        if str(newValue) == \"tests\":\n            pass\n            # config[\"confirm\"] = False\n        else:\n            config[\"debug\"] = newValue\n\n    def set_username(username):\n        config[\"username\"] = str(username)\n\n    def set_username_google(username):\n        config[\"username_google\"] = str(username)\n\n    def set_username_twitter(username):\n        config[\"username_twitter\"] = str(username)\n\n    def set_password(password):\n        config[\"password\"] = str(password)\n\n    def set_password_google(password):\n        config[\"password_google\"] = str(password)\n\n    def set_password_twitter(password):\n        config[\"password_twitter\"] = str(password)\n\n    def set_prefer_local(buul):\n        config[\"prefer_local\"] = buul\n    \n    def set_prefer_local_following(buul):\n        config[\"prefer_local_following\"] = buul\n\n###########################################################################\n\n\n\n#     def update_value(self, variable, newValue):\n#         variable = str(variable).upper().replace(\" \",\"_\")\n#         try:\n#             # print(\"Updating: {} = {}\".format(variable, newValue))\n#             setattr(self, variable, newValue)\n#             # print(\"Updated: {} = {}\".format(variable, getattr(self, variable)))\n#         except Exception as e:\n#             maybePrint(e)\n\n# # move this behavior to user\n#     def update_profile_value(self, variable, newValue):\n#         variable = str(variable).upper().replace(\" \",\"_\")\n#         try:\n#             # print(\"Updating: {} = {}\".format(variable, newValue))\n#             Settings.PROFILE.setattr(self, variable, newValue)\n#             # print(\"Updated: {} = {}\".format(variable, getattr(self, variable)))\n#         except Exception as e:\n#             maybePrint(e)\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n#######################################################################################\n\ndef delayForThirty():\n    Settings.maybe_print(\"30...\")\n    time.sleep(10)\n    Settings.maybe_print(\"20...\")\n    time.sleep(10)\n    Settings.maybe_print(\"10...\")\n    time.sleep(7)\n    Settings.maybe_print(\"3...\")\n    time.sleep(1)\n    Settings.maybe_print(\"2...\")\n    time.sleep(1)\n    Settings.maybe_print(\"1...\")\n    time.sleep(1)"
  },
  {
    "path": "OnlySnarf/util/validators.py",
    "content": "import argparse, os\nimport validators\nfrom datetime import datetime\nfrom . import defaults as DEFAULT\n\n# Validators\n\n#\n# Args\n\n# def valid_action(s):\n# \ttry:\n# \t\tif str(s) in DEFAULT.ACTIONS:\n# \t\t\treturn str(s)\n# \texcept ValueError:\n# \t\tmsg = \"Not a valid action: '{0}'.\".format(s)\n# \t\traise argparse.ArgumentTypeError(msg)\n\ndef valid_amount(s):\n\ttry:\n\t\tif str(s) == \"max\": return DEFAULT.DISCOUNT_MAX_AMOUNT\n\t\telif str(s) == \"min\": return DEFAULT.DISCOUNT_MIN_AMOUNT\n\t\telif int(s) >= DEFAULT.DISCOUNT_MIN_AMOUNT and int(s) <= DEFAULT.DISCOUNT_MAX_AMOUNT:\n\t\t\treturn int(s)\n\texcept ValueError:\n\t\tmsg = \"Not a valid discount amount: '{0}'.\".format(s)\n\t\traise argparse.ArgumentTypeError(msg)\n\ndef valid_date(s):\n\ttry: return datetime.strptime(s, DEFAULT.DATE_FORMAT)\n\texcept ValueError:\n\t\tmsg = \"Not a valid date: '{0}'.\".format(s)\n\t\traise argparse.ArgumentTypeError(msg)\n\ndef valid_duration(s):\n\ttry:\n\t\tif str(s) == \"max\": return DEFAULT.DURATION_ALLOWED[-1]\n\t\telif str(s) == \"min\": return DEFAULT.DURATION_ALLOWED[0]\n\t\telif str(s) in DEFAULT.DURATION_ALLOWED: return str(s)\n\texcept ValueError:\n\t\tmsg = \"Not a valid duration: '{0}'.\".format(s)\n\t\traise argparse.ArgumentTypeError(msg)\n\treturn int(s)\n\ndef valid_promo_duration(s):\n\ttry:\n\t\tif str(s) in DEFAULT.PROMOTION_DEFAULT.DURATION_ALLOWED: return str(s)\n\texcept ValueError:\n\t\tmsg = \"Not a valid promo duration: '{0}'.\".format(s)\n\t\traise argparse.ArgumentTypeError(msg)\n\treturn int(s)\n\ndef valid_promo_expiration(s):\n\ttry:\n\t\tif int(s) in DEFAULT.PROMOTION_EXPIRATION_ALLOWED: return int(s)\n\texcept ValueError:\n\t\tmsg = \"Not a valid promo expiration: '{0}'.\".format(s)\n\t\traise argparse.ArgumentTypeError(msg)\n\ndef valid_expiration(s):\n\ttry:\n\t\tif str(s) == \"max\": return DEFAULT.EXPIRATION_MAX\n\t\telif str(s) == \"min\": return DEFAULT.EXPIRATION_MIN\n\t\telif int(s) <= DEFAULT.EXPIRATION_MAX: return int(s)\n\texcept ValueError:\n\t\tmsg = \"Not a valid expiration: '{0}'.\".format(s)\n\t\traise argparse.ArgumentTypeError(msg)\n\ndef valid_limit(s):\n\ttry:\n\t\tif str(s) == \"max\": return DEFAULT.LIMIT_ALLOWED[-1]\n\t\telif str(s) == \"min\": return DEFAULT.LIMIT_ALLOWED[0]\n\t\telif int(s) in DEFAULT.LIMIT_ALLOWED: return int(s)\n\texcept ValueError:\n\t\tmsg = \"Not a valid limit: '{0}'.\".format(s)\n\t\traise argparse.ArgumentTypeError(msg)\n\treturn int(s)\n\ndef valid_month(s):\n\ttry:\n\t\tif str(s) == \"max\": return DEFAULT.DISCOUNT_MAX_MONTHS\n\t\telif str(s) == \"min\": return DEFAULT.DISCOUNT_MIN_MONTHS\n\t\telif int(s) >= DEFAULT.DISCOUNT_MIN_MONTHS and int(s) <= DEFAULT.DISCOUNT_MAX_MONTHS:\n\t\t\treturn int(s)\n\texcept ValueError:\n\t\tmsg = \"Not a valid month number: '{0}'.\".format(s)\n\t\traise argparse.ArgumentTypeError(msg)\n\ndef valid_path(s):\n\ttry:\n\t\tif isinstance(s, list):\n\t\t\tfor f in s: os.stat(s)\n\t\telse: os.stat(s)\n\texcept FileNotFoundError:\n\t\t# check as url\n\t\treturn valid_url(s)\n\t\t# msg = \"Not a valid path: '{0}'.\".format(s)\n\t\t# raise argparse.ArgumentTypeError(msg)\n\treturn s\n\ndef valid_url(s):\n\tif not validators.url(s):\n\t\tmsg = \"Not a valid path or url: '{0}'.\".format(s)\n\t\traise argparse.ArgumentTypeError(msg)\n\treturn s\n\ndef valid_price(s):\n\tif str(s) == \"max\": return DEFAULT.PRICE_MAXIMUM\n\telif str(s) == \"min\": return DEFAULT.PRICE_MINIMUM\n\ttry: return \"{:.2f}\".format(float(s))\n\texcept ValueError:\n\t\tmsg = \"Not a valid price: '{0}'.\".format(s)\n\t\traise argparse.ArgumentTypeError(msg)\n\ndef valid_schedule(s):\n\ttry: return datetime.strptime(s, DEFAULT.SCHEDULE_FORMAT)\n\texcept ValueError:\n\t\tmsg = \"Not a valid schedule: '{0}'.\".format(s)\n\t\traise argparse.ArgumentTypeError(msg)\n\ndef valid_time(s):\n\ttry: return datetime.strptime(s, DEFAULT.TIME_FORMAT)\n\texcept ValueError:\n\t\tmsg = \"Not a valid time: '{0}'.\".format(s)\n\t\traise argparse.ArgumentTypeError(msg)\n\n# check against min/max amounts & months\n# def valid_discount(s):\n  # pass\n\n# def valid_category(s):\n#   if str(s) not in DEFAULT.CATEGORIES_DEFAULT:\n#     msg = \"Not a valid category: '{0}'.\".format(s)\n#     raise argparse.ArgumentTypeError(msg)\n"
  },
  {
    "path": "Pipfile",
    "content": "[[source]]\nurl = \"https://pypi.org/simple\"\nverify_ssl = true\nname = \"pypi\"\n\n[packages]\n\n[dev-packages]\n\n[requires]\npython_version = \"3.10\"\n"
  },
  {
    "path": "README.md",
    "content": "<h1 align=\"center\">OnlySnarf</h1>\n<p align=\"center\"><img src=\"public/images/snarf-missionary.jpg\" alt=\"Shnarf\" width=\"400\"/></p>\n<p align=\"center\">Please refer to the <a href=\"public/docs/menu.md\">Menu</a> for help with the available arguments and config settings.</p> \n\n## Description\nOnlySnarf is a python based automation tool to assist with uploading content to OnlyFans by interacting with the site via web scraping. It does not interact with the API whatsoever. OnlySnarf carries no weapons, but it has been known to use its tail, teeth and claws when improperly configured.\n\nHere are some fuzzy debugging previews of how it looks when everything works:\n- [Discount](//ipfs.io/ipfs/QmboqfpCeAAbbhqGhPQ8cCscqm7CNH4mxTPR42g8Cg7iLW?filename=discount.gif)\n- [Message](//ipfs.io/ipfs/QmXitqxkRuMXb6XnUJw7MHUxLii7UNEXjENc5k4PyfTWfY?filename=message.gif)\n- [Poll](//ipfs.io/ipfs/QmNkE4GpBoiQ3tGLLfxtTGS96jJJJixS4qbkx9fxN9GeYC?filename=poll.gif)\n- [Post](//ipfs.io/ipfs/QmUBjuLK3yh5v4U9SSPmSG3NAGgYaY6rYoYACGi1smZpJ7?filename=post.gif)\n- [Schedule](//ipfs.io/ipfs/QmUd843FXXyMP2eyfkB1d1erZyrKN1hmKchuviruzN8ctD?filename=schedule.gif)\n- [Users](//ipfs.io/ipfs/Qmc9zPytgSKx4EK6V1A8DABNeCpMxBybcRs4hNtAMSKDyi?filename=users.gif)\n\n## Installation\n\nThere are two **different** installation options (that I know of):\n1) via pip for the latest official package: `python3 -m pip install onlysnarf`  \n2) or clone the repo & setup a virtual environment to install locally like in the bash script at [bin/virtualenv.sh](/bin/virtualenv.sh) \n\nHere is an output of the command: [`snarf -h`](/public/docs/help.md/#-h)\n  \nCommand example: `snarf -text \"suck my giant balls\" /path/to/imageOfBalls.jpeg`\n\n## Config\nExample config files are provided. There are two main config files that should be provided to affect runtime behavior as well as one optional method to help distinguish between user logins for multiple accounts.\n1) the config for the general app's behavior: `$HOME/.onlysnarf/config.conf`\n2) one config for each user containing their credentials: `$HOME/.onlysnarf/users/$username.conf`\n3) (optional) one config containing the default user credentials to use: `$HOME/.onlysnarf/users/default.conf`\n\nUser config example: `$HOME/.onlysnarf/users/alexdicksdown.conf`\n\n**Note for Windows**: the user's $HOME path works out to `C:\\Users\\YOUR_USERNAME` so the base directory for config files and such can instead be found at `C:\\Users\\YOUR_USERNAME\\.onlysnarf`\n\n**No**, the user credentials **are not** handled in the safest manner because they are very clearly **stored in plain text** with **no encryption**. Yes, a better way can be figured out. Do I think a better way is necessary for this project? No. So please be careful with your own credentials.\n\n## Dependencies\nSelenium's webdriver manager should install everything it needs automatically. If left unspecified the default browser argument is \"auto\" which will cylce throuch each web driver available and attempt to spawn a working browser. If you are using a Raspberry Pi 4, be sure to run `sudo apt-get install chromium-chromedriver` on your device to be able to launch chrome. \n\n## Platforms\nRuns successfully on:\n- Linux Ubuntu\n- Windows 11\n\nRuns sucessfully on browsers:\n- Chrome\n- Firefox\n\nRuns successfully on devices:\n- Raspberry Pi 4\n\n## Tests\n\nThe test environment uses the config file found at:  [OnlySnarf/conf/test-config.conf](/OnlySnarf/conf/test-config.conf) \n\nBasic unittesting:\n- `python -m unittest tests/snarf/test_discount.py`\n- `python -m unittest tests/snarf/test_post.py`\n- `python -m unittest tests/snarf/test_message.py`\n- `python -m unittest tests/snarf/test_users.py`\n\nPytests available under /tests:\n- `pytest tests/selenium`\n- `pytest tests/snarf`\n\n## Updates\n7/5/2023 : clarifications to readme and menu text...\n4/18/2023 : To further reduce repo size, preview gifs have been relocated to [IPFS](//ipfs.io/ipfs/QmVpjSy9NXy3VUM474hSDoPSsmsb5WVYkN9WN6N7nFxZuj).\n\n<hr>\nFeel free to make use of my <a href=\"//onlyfans.com/?ref=409408\" target=\"_blank\">referral code</a> ;)"
  },
  {
    "path": "bin/aws-setup.sh",
    "content": "#!/bin/bash\n\n# AWS Linux setup steps:\n\n# ssh keys\nssh-keygen -t ed25519 -C \"WebmasterSkeetzo@gmail.com\"\neval \"$(ssh-agent -s)\"\nssh-add ~/.ssh/id_ed25519\ncat ~/.ssh/id_ed25519.pub\n# > add to github\n\n# basic dependencies\nsudo yum -y install git python-pip\n\ngit clone git@github.com:skeetzo/onlysnarf --single-branch\nsudo cp onlysnarf/notes/onlysnarf_api.service /etc/systemd/system\nsudo systemctl start onlysnarf_api.service\nsudo systemctl enable onlysnarf_api.service\n\n# add user\nsudo useradd -m snarf\nsudo passwd snarf\nsu snarf\n\npip install onlysnarf"
  },
  {
    "path": "bin/clean.sh",
    "content": "#!/usr/bin/env bash\n# git filter-branch -f --tree-filter 'rm -rf ./OnlySnarf/google_creds.txt' HEAD\n\nrm -rf dist/ build/ *.egg-info .pytest_cache\n\n# project logs\nrm -rf log/* $HOME/OnlySnarf/snarf.log $HOME/.onlysnarf/log/* \n\n# session data and cookies\nrm -rf $HOME/.onlysnarf/session.json $HOME/.onlysnarf/cookies.pkl\n\n# any remaining files\nrm -rf $HOME/OnlySnarf/downloads/* $HOME/OnlySnarf/uploads/*"
  },
  {
    "path": "bin/demo-scripts.sh",
    "content": "##################\n## Demo Scripts ##\n##################\n\n# Discount\nsnarf -debug discount -user random -amount max -months max\n\n# Message\nsnarf -debug message -user random -text shnarf! -price min ~/Projects/onlysnarf/public/images/snarf-missionary.jpg\n\n# Post\nsnarf -debug post -text \"shnarf\" -tags \"suck\" -tags \"my\" -tags \"balls\" -performers \"yourmom\" -performers  \"yourdad\" ~/Projects/onlysnarf/public/images/snarf-missionary.jpg\n\n# Poll\nsnarf -debug post -text shnarff! -question \"sharnf shnarf?\" -question \"shnarf shhhnarff snarf?\" -duration min\n\n# Schedule\nsnarf -debug post -text shnarff! -schedule \"10/31/2022 16:20:00\"\n\n# User\nsnarf -debug users\n\nsnarf -debug -browser brave users\n\nsnarf post -text \"shnarff?\" -question \"yes\" -question \"maybe?\" -question \"no\" -question \"double shnarf\" -duration \"min\"\n\n# debug remote path upload\nsnarf -debug -debug-delay -verbose -verbose -verbose -show post -text \"shnarrff\" \"https://github.com/skeetzo/onlysnarf/blob/master/public/images/shnarf.jpg?raw=true\""
  },
  {
    "path": "bin/drivers/check-chrome.sh",
    "content": "#!/usr/bin/env bash\necho \"Google Version Check:\"\ngoogle-chrome --version | (echo -n \"stable => \" && cat)\ngoogle-chrome-beta --version | (echo -n \"beta => \" && cat)\npip show chromedriver-binary | grep \"Version: \" | (echo -n \"binary => \" && cat)"
  },
  {
    "path": "bin/drivers/check-firefox.sh",
    "content": "#!/usr/bin/env bash\necho \"Firefox Version Check:\"\ngeckodriver --version | head -n 1 | (echo -n \"geckodriver => \" && cat)"
  },
  {
    "path": "bin/drivers/check.sh",
    "content": "#!/usr/bin/env bash\npython -m build\ntwine check dist/*"
  },
  {
    "path": "bin/drivers/fix-chromedriver.sh",
    "content": "#!/usr/bin/env bash\n\n# didn't work\n\n# https://stackoverflow.com/questions/65617246/issues-running-selenium-with-chromedriver-on-raspberry-pi-4\nsudo apt install chromium-chromedriver\npip3 install selenium \nsudo chmod 755 /usr/lib/chromium-browser/chromedriver\n\nsudo apt purge --remove chromium-browser -y\nsudo apt autoremove && sudo apt autoclean -y\nsudo apt install chromium-chromedriver\n\n# https://serverfault.com/questions/1091926/running-chrome-on-ubuntu-server-how-to-solve-xdg-settings-not-found-using\n sudo apt-get install xdg-utils"
  },
  {
    "path": "bin/drivers/fix-firefox-profile-error.sh",
    "content": "#!/bin/bash\n# https://support.mozilla.org/en-US/kb/install-firefox-linux#w_install-firefox-from-mozilla-builds-for-advanced-users\nwget https://www.mozilla.org/en-US/firefox/download/thanks/ -P ~/Downloads\ncd ~/Downloads \ntar xjf firefox-*.tar.bz2 \nsudo mv firefox /opt \nsudo ln -s /opt/firefox/firefox /usr/local/bin/firefox \nsudo wget https://raw.githubusercontent.com/mozilla/sumo-kb/main/install-firefox-linux/firefox.desktop -P /usr/local/share/applications "
  },
  {
    "path": "bin/drivers/install-chrome.sh",
    "content": "#!/usr/bin/env bash\n\nsudo apt-get remove google-chrome-stable --purge -y\nsudo apt-get remove google-chrome-beta --purge -y\npip uninstall chromedriver-binary -y\n\n#\nwget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add -\n# echo 'deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main' | sudo tee /etc/apt/sources.list.d/google-chrome.list\n# sudo sh -c 'echo \"deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main\" >> /etc/apt/sources.list.d/google.list'\n# sudo sh -c \"echo 'deb http://dl.google.com/linux/chrome/deb/ stable main' >>   /etc/apt/sources.list\"\nsudo apt-get update\n#\n\n# sudo apt-get install -y google-chrome-stable\nsudo apt-get install -y google-chrome-beta\npip install chromedriver-binary\n\n## by version\n# VERSION=\"106.0.5249.21\"\n# sudo apt-get install google-chrome-stable=$VERSION-1 -y\n# sudo apt-get install google-chrome-beta=$VERSION-1 -y\n# pip install chromedriver-binary==$VERSION.0 --force --upgrade\n\n\n\n## didn't work for rpi4\n# or\n# Chrome \t\t\tChromedriver\n# 81.0.4044.129  |  106.0.5249.61\nversion=$(curl -s https://chromedriver.storage.googleapis.com/LATEST_RELEASE)\nwget -qP \"/tmp/\" \"https://chromedriver.storage.googleapis.com/${version}/chromedriver_linux64.zip\"\nsudo apt-get install unzip\nsudo unzip -o /tmp/chromedriver_linux64.zip -d /usr/bin\n\necho \"\n\"\nMYDIR=\"$(dirname \"$(realpath \"$0\")\")\"\n$MYDIR/check-chrome.sh"
  },
  {
    "path": "bin/drivers/install-chromedriver-aws.sh",
    "content": "#!/bin/bash\n\n# install chromedriver on aws ec2\n\ncd tmp\nwget https://chromedriver.storage.googleapis.com/2.37/chromedriver_linux64.zip\nunzip chromedriver_linux64.zip\nsudo mv chromedriver /usr/bin/chromedriver\nsudo yum install -y libX11\nchromedriver --version\n\n# install google chrome\n\ncurl https://intoli.com/install-google-chrome.sh | bash\nsudo mv /usr/bin/google-chrome-stable /usr/bin/google-chrome\ngoogle-chrome --version && which google-chrome\n"
  },
  {
    "path": "bin/drivers/install-chromedriver-rpi.sh",
    "content": "#!/bin/bash\nsudo apt-get install chromium-chromedriver"
  },
  {
    "path": "bin/drivers/install-firefox.sh",
    "content": "#!/usr/bin/env bash\n###############################\nVERSION=\"0.31.0\"\nBIT=\"64\"\n#\nwget \"https://github.com/mozilla/geckodriver/releases/download/v$VERSION/geckodriver-v$VERSION-linux$BIT.tar.gz\" -O /tmp/geckodriver.tar.gz\n# sudo tar -C /opt -xvzf /tmp/geckodriver.tar.gz\nsudo tar -xvzf /tmp/geckodriver*\nsudo chmod +x ./geckodriver\n# sudo chmod 755 ./geckodriver\nsudo mv ./geckodriver /usr/local/bin/"
  },
  {
    "path": "bin/drivers/install-geckodriver-arm.sh",
    "content": "#!/usr/bin/env bash\n# https://raspberrypi.stackexchange.com/questions/63258/selenium-firefox-oserror-errno-8-exec-format-error\nwget \"https://github.com/mozilla/geckodriver/releases/download/v0.19.1/geckodriver-v0.19.1-arm7hf.tar.gz\" -O /tmp/geckodriver.tar.gz\nsudo tar -xvzf /tmp/geckodriver*\nsudo chmod +x ./geckodriver\nsudo mv ./geckodriver /usr/local/bin/"
  },
  {
    "path": "bin/drivers/install-geckodriver-rpi.sh",
    "content": "#!/bin/bash\n# doesn't work\n\nsudo apt-get upgrade\nsudo apt-get update\nsudo pip3 install selenium\nsudo apt-get install iceweasel\ncurl -O https://github.com/mozilla/geckodriver/releases/download/v0.19.1/geckodriver-v0.19.1-arm7hf.tar.gz\ntar -xzvf geckodriver-v0.19.1-arm7hf.tar.gz\nsudo cp geckodriver /usr/local/bin/"
  },
  {
    "path": "bin/drivers/switch-firefox.sh",
    "content": "#!/bin/bash\nsudo snap remove firefox\nsudo add-apt-repository ppa:mozillateam/ppa\necho '\n\tPackage: *\n\tPin: release o=LP-PPA-mozillateam\n\tPin-Priority: 1001\n' | sudo tee /etc/apt/preferences.d/mozilla-firefox\necho 'Unattended-Upgrade::Allowed-Origins:: \"LP-PPA-mozillateam:${distro_codename}\";' | sudo tee /etc/apt/apt.conf.d/51unattended-upgrades-firefox\nsudo apt install firefox"
  },
  {
    "path": "bin/install.sh",
    "content": "#!/bin/bash\nyes | pip install onlysnarf"
  },
  {
    "path": "bin/run-tests.sh",
    "content": "#!/bin/bash\npython setup.py install\npytest tests/selenium\npytest tests/snarf"
  },
  {
    "path": "bin/save.sh",
    "content": "#!/usr/bin/env bash\nsudo bin/clean.sh\nif [ -z \"$1\" ]; then\n\tset \"saved\"\nfi\ngit add . && git commit -m \"$1\" && git push"
  },
  {
    "path": "bin/start-api-dev.sh",
    "content": "#!/bin/sh\npython ./OnlySnarf/api/index.py -debug -verbose -verbose -verbose users"
  },
  {
    "path": "bin/start-api.sh",
    "content": "#!/bin/sh\n# -users arg is provided to squelch requirement from onlysnarf args\npython ./OnlySnarf/api/index.py -verbose users\n\n# from flask example but don't apply here:\n\n# export FLASK_APP=./api/index.py\n# export FLASK_ENV=production\n# pipenv run flask --debug run -h 0.0.0.0\n# FLASK_ENV=production python api/index.py users"
  },
  {
    "path": "bin/test-all.sh",
    "content": "#!/bin/bash\npytest tests/api\npytest tests/selenium\npytest tests/snarf"
  },
  {
    "path": "bin/test-api-remote.sh",
    "content": "#!/bin/bash\n\n# curl -X POST -H \"Content-Type: application/json\" -d '{\n#   \"text\": \"your mom\",\n#   \"input\": \"https://github.com/skeetzo/onlysnarf/blob/master/public/images/shnarf.jpg?raw=true\"\n# }' http://13.48.136.241:5000\n\n# curl -X POST -H \"Content-Type: application/json\" -d '{\n#   \"text\": \"your mom\",\n#   \"user\": \"random\",\n#   \"input\": \"https://github.com/skeetzo/onlysnarf/blob/master/public/images/shnarf.jpg?raw=true\"\n# }' http://13.57.87.181:5000/message\n\n\ncurl -X POST -H \"Content-Type: application/json\" -d '{\n  \"text\": \"your mom\",\n  \"input\": \"https://github.com/skeetzo/onlysnarf/blob/master/public/images/shnarf.jpg?raw=true\"\n}' http://3.101.69.182:5000/post\n\n"
  },
  {
    "path": "bin/test-api.sh",
    "content": "#!/bin/bash\n\n# curl -X POST -H \"Content-Type: application/json\" -d '{\n#   \"text\": \"your mom\",\n#   \"schedule\": \"07/18/2023 16:20:00\",\n#   \"input\": \"https://github.com/skeetzo/onlysnarf/blob/master/public/images/shnarf.jpg?raw=true\"\n# }' http://localhost:5000/post\n\ncurl -X POST -H \"Content-Type: application/json\" -d '{\n  \"text\": \"your mom\",\n  \"price\": 6,\n  \"user\": \"random\",\n  \"input\": \"https://github.com/skeetzo/onlysnarf/blob/master/public/images/shnarf.jpg?raw=true, https://github.com/skeetzo/onlysnarf/blob/master/public/images/snarf.jpg?raw=true\"\n}' http://localhost:5000/message"
  },
  {
    "path": "bin/test.sh",
    "content": "#!/bin/bash\n# list of test scripts and other useful commands\n\ngit clone --depth 1  --branch development git@github.com:skeetzo/onlysnarf\n\npython -m pip install -e .[dev]\n\nsnarf -debug -vvv post -text \"balls\"\n\npytest tests/selenium\npytest tests/selenium/browsers\npytest tests/selenium/reconnect\n\npytest tests/snarf\n\n# does not work for some reason due to imports\npytest tests/api\n\n#############\n# Unit Test #\n#############\n\npython -m unittest tests/selenium/test_browsers.py\npython -m unittest tests/selenium/browsers/test_firefox.py\n\npython -m unittest tests/snarf/auth/test_twitter.py\n\n\npython -m unittest tests/snarf/test_auth.py\npython -m unittest tests/snarf/test_discount.py\npython -m unittest tests/snarf/test_expiration.py\npython -m unittest tests/snarf/test_message.py\npython -m unittest tests/snarf/test_poll.py\npython -m unittest tests/snarf/test_post.py\npython -m unittest tests/snarf/test_schedule.py\npython -m unittest tests/snarf/test_users.py\n\n##########\n# pytest #\n########## \n\n## API ##\npython -m unittest tests/api/test_api.py\npython -m pytest tests/api/test_api.py\n\n## Selenium Processes ##\npytest tests/selenium/test_browsers.py\npytest tests/selenium/test_reconnect.py\npytest tests/selenium/test_remote.py\n\npytest tests/selenium/browsers/test_brave.py\npytest tests/selenium/browsers/test_chrome.py\npytest tests/selenium/browsers/test_chromium.py\npytest tests/selenium/browsers/test_edge.py\npytest tests/selenium/browsers/test_firefox.py\npytest tests/selenium/browsers/test_ie.py\npytest tests/selenium/browsers/test_opera.py\n\npytest tests/selenium/reconnect\npytest tests/selenium/reconnect/...\n\n## Snarf Processes ##\n\n## Authentication ##\npytest tests/snarf/auth/test_onlyfans.py\n\npytest tests/snarf/test_auth.py\npytest tests/snarf/test_discount.py\npytest tests/snarf/test_expiration.py\npytest tests/snarf/test_message.py\npytest tests/snarf/test_poll.py\npytest tests/snarf/test_post.py\npytest tests/snarf/test_schedule.py\npytest tests/snarf/test_users.py\n\n# Unfinished\npytest tests/snarf/auth/test_google.py\npytest tests/snarf/auth/test_twitter.py\npytest tests/snarf/test_profile.py\npytest tests/snarf/test_promotion.py\n"
  },
  {
    "path": "bin/update-and-start.sh",
    "content": "#!/bin/bash\n/usr/bin/pip install -U onlysnarf\n/usr/local/bin/snarf -debug -verbose -verbose -verbose api"
  },
  {
    "path": "bin/upload-test.sh",
    "content": "#!/usr/bin/env bash\nif [ -z \"$1\" ]; then\n\tset \"upload\"\nfi\nbin/save.sh\nwait\npython -m build\ntwine upload --verbose -r testpypi dist/*"
  },
  {
    "path": "bin/upload.sh",
    "content": "#!/usr/bin/env bash\nif [ -z \"$1\" ]; then\n\tset \"upload\"\nfi\npython -m pip freeze\nbin/save.sh\nwait\npython -m build\ntwine upload dist/*"
  },
  {
    "path": "bin/virtualenv.sh",
    "content": "#!/bin/bash\n# basic setup script for python3 virtual environments\nsudo apt-get -y install python3-virtualenv python3-pip python3-venv python3-setuptools \n\n# TODO:\n# are these required still?\n# sudo apt-get -y install libjpeg-dev zlib1g-dev\n\npython3 -m pip install --user virtualenv\nvirtualenv venv\necho \"This script fails to update source automatically so copy and paste or type the following code to update the virtual environment for development:\"\necho \"source venv/bin/activate\"\necho \"python -m pip install --upgrade pip setuptools wheel build twine pytest\"\n\n\n## are all of these handled by `pipenv`???"
  },
  {
    "path": "notes/Self-serving an ARM build",
    "content": "https://firefox-source-docs.mozilla.org/testing/geckodriver/ARM.html\nSelf-serving an ARM build¶\n\nMozilla announced the intent to deprecate ARMv7 HF builds of geckodriver in September 2018. This does not mean you can no longer use geckodriver on ARM systems, and this document explains how you can self-service a build for ARMv7 HF.\n\nAssuming you have already checked out central, the steps to cross-compile ARMv7 from a Linux host system is as follows:\n\n    If you don’t have Rust installed:\n\n    # curl https://sh.rustup.rs -sSf | sh\n\nInstall cross-compiler toolchain:\n\n# apt install gcc-arm-linux-gnueabihf libc6-armhf-cross libc6-dev-armhf-cross\n\nCreate a new shell, or to reuse the existing shell:\n\nsource $HOME/.cargo/env\n\nInstall rustc target toolchain:\n\n% rustup target install armv7-unknown-linux-gnueabihf\n\nPut this in testing/geckodriver/.cargo/config:\n\n[target.armv7-unknown-linux-gnueabihf]\nlinker = \"arm-linux-gnueabihf-gcc\"\n\nBuild geckodriver from testing/geckodriver:\n\n% cd testing/geckodriver\n% cargo build --release --target armv7-unknown-linux-gnueabihf\n"
  },
  {
    "path": "notes/adding-phantomjs.py",
    "content": "# https://stackoverflow.com/questions/13287490/is-there-a-way-to-use-phantomjs-in-python\n\n# The easiest way to use PhantomJS in python is via Selenium. The simplest installation method is\n\n#     Install NodeJS\n#     Using Node's package manager install phantomjs: npm -g install phantomjs-prebuilt\n#     install selenium (in your virtualenv, if you are using that)\n\n# After installation, you may use phantom as simple as:\n\nfrom selenium import webdriver\n\ndriver = webdriver.PhantomJS() # or add to your PATH\ndriver.set_window_size(1024, 768) # optional\ndriver.get('https://google.com/')\ndriver.save_screenshot('screen.png') # save a screenshot to disk\nsbtn = driver.find_element_by_css_selector('button.gbqfba')\nsbtn.click()\n\n# If your system path environment variable isn't set correctly, you'll need to specify the exact path as an argument to webdriver.PhantomJS(). Replace this:\n\ndriver = webdriver.PhantomJS() # or add to your PATH\n\n# ... with the following:\n\ndriver = webdriver.PhantomJS(executable_path='/usr/local/lib/node_modules/phantomjs/lib/phantom/bin/phantomjs')\n"
  },
  {
    "path": "notes/animal.py",
    "content": "class Animal:\n    \"\"\"\n    A class used to represent an Animal\n\n    ...\n\n    Attributes\n    ----------\n    says_str : str\n        a formatted string to print out what the animal says\n    name : str\n        the name of the animal\n    sound : str\n        the sound that the animal makes\n    num_legs : int\n        the number of legs the animal has (default 4)\n\n    Methods\n    -------\n    says(sound=None)\n        Prints the animals name and what sound it makes\n    \"\"\"\n\n    says_str = \"A {name} says {sound}\"\n\n    def __init__(self, name, sound, num_legs=4):\n        \"\"\"\n        Parameters\n        ----------\n        name : str\n            The name of the animal\n        sound : str\n            The sound the animal makes\n        num_legs : int, optional\n            The number of legs the animal (default is 4)\n        \"\"\"\n\n        self.name = name\n        self.sound = sound\n        self.num_legs = num_legs\n\n    def says(self, sound=None):\n        \"\"\"Prints what the animals name is and what sound it makes.\n\n        If the argument `sound` isn't passed in, the default Animal\n        sound is used.\n\n        Parameters\n        ----------\n        sound : str, optional\n            The sound the animal makes (default is None)\n\n        Raises\n        ------\n        NotImplementedError\n            If no sound is set for the animal or passed in as a\n            parameter.\n        \"\"\"\n\n        if self.sound is None and sound is None:\n            raise NotImplementedError(\"Silent Animals are not supported!\")\n\n        out_sound = self.sound if sound is None else sound\n        print(self.says_str.format(name=self.name, sound=out_sound))\n"
  },
  {
    "path": "notes/docstrings.py",
    "content": "\"\"\"\nSummary line.\n\nExtended description of function.\n\nParameters\n----------\narg1 : int\n    Description of arg1\narg2 : str\n    Description of arg2\n\nReturns\n-------\nint\n    Description of return value\n\n\"\"\"\n\n\n\n\ndef my_function(my_arg, my_other_arg):\n    \"\"\"A function just for me.\n\n    :param my_arg: The first of my arguments.\n    :param my_other_arg: The second of my arguments.\n\n    :returns: A message (just for me, of course).\n    \"\"\"\n\n\n\n\nGoogle Docstrings Example\n\n\"\"\"Gets and prints the spreadsheet's header columns\n\nArgs:\n    file_loc (str): The file location of the spreadsheet\n    print_cols (bool): A flag used to print the columns to the console\n        (default is False)\n\nReturns:\n    list: a list of strings representing the header columns\n\"\"\"\n\nreStructured Text Example\n\n\"\"\"Gets and prints the spreadsheet's header columns\n\n:param file_loc: The file location of the spreadsheet\n:type file_loc: str\n:param print_cols: A flag used to print the columns to the console\n    (default is False)\n:type print_cols: bool\n:returns: a list of strings representing the header columns\n:rtype: list\n\"\"\"\n\nNumPy/SciPy Docstrings Example\n\n\"\"\"Gets and prints the spreadsheet's header columns\n\nParameters\n----------\nfile_loc : str\n    The file location of the spreadsheet\nprint_cols : bool, optional\n    A flag used to print the columns to the console (default is False)\n\nReturns\n-------\nlist\n    a list of strings representing the header columns\n\"\"\"\n\nEpytext Example\n\n\"\"\"Gets and prints the spreadsheet's header columns\n\n@type file_loc: str\n@param file_loc: The file location of the spreadsheet\n@type print_cols: bool\n@param print_cols: A flag used to print the columns to the console\n    (default is False)\n@rtype: list\n@returns: a list of strings representing the header columns\n\"\"\"\n\n\n\n\n\n\n\"\"\"This is the summary line\n\nThis is the further elaboration of the docstring. Within this section,\nyou can elaborate further on details as appropriate for the situation.\nNotice that the summary and the elaboration is separated by a blank new\nline.\n\"\"\"\n\n# Notice the blank line above. Code should continue on this line.\n\n\n\"\"\"summary\n\nlong description\n\"\"\"\n\n\n\n\nclass SimpleClass:\n    \"\"\"Class docstrings go here.\"\"\"\n\n    def say_hello(self, name: str):\n        \"\"\"Class method docstrings go here.\"\"\"\n\n        print(f'Hello {name}')\n\n\n\n\n\nPackage and Module Docstrings\n\nPackage docstrings should be placed at the top of the package’s __init__.py file. This docstring should list the modules and sub-packages that are exported by the package.\n\nModule docstrings are similar to class docstrings. Instead of classes and class methods being documented, it’s now the module and any functions found within. Module docstrings are placed at the top of the file even before any imports. Module docstrings should include the following:\n\n    A brief description of the module and its purpose\n    A list of any classes, exception, functions, and any other objects exported by the module\n\nThe docstring for a module function should include the same items as a class method:\n\n    A brief description of what the function is and what it’s used for\n    Any arguments (both required and optional) that are passed including keyword arguments\n    Label any arguments that are considered optional\n    Any side effects that occur when executing the function\n    Any exceptions that are raised\n    Any restrictions on when the function can be called\n\n\n\n\n\n\n\n\nScript Docstrings\n\nScripts are considered to be single file executables run from the console. Docstrings for scripts are placed at the top of the file and should be documented well enough for users to be able to have a sufficient understanding of how to use the script. It should be usable for its “usage” message, when the user incorrectly passes in a parameter or uses the -h option.\n\nIf you use argparse, then you can omit parameter-specific documentation, assuming it’s correctly been documented within the help parameter of the argparser.parser.add_argument function. It is recommended to use the __doc__ for the description parameter within argparse.ArgumentParser’s constructor. Check out our tutorial on Command-Line Parsing Libraries for more details on how to use argparse and other common command line parsers.\n\nFinally, any custom or third-party imports should be listed within the docstrings to allow users to know which packages may be required for running the script. Here’s an example of a script that is used to simply print out the column headers of a spreadsheet:\n\n\"\"\"Spreadsheet Column Printer\n\nThis script allows the user to print to the console all columns in the\nspreadsheet. It is assumed that the first row of the spreadsheet is the\nlocation of the columns.\n\nThis tool accepts comma separated value files (.csv) as well as excel\n(.xls, .xlsx) files.\n\nThis script requires that `pandas` be installed within the Python\nenvironment you are running this script in.\n\nThis file can also be imported as a module and contains the following\nfunctions:\n\n    * get_spreadsheet_cols - returns the column headers of the file\n    * main - the main function of the script\n\"\"\"\n\nimport argparse\n\nimport pandas as pd\n\n\ndef get_spreadsheet_cols(file_loc, print_cols=False):\n    \"\"\"Gets and prints the spreadsheet's header columns\n\n    Parameters\n    ----------\n    file_loc : str\n        The file location of the spreadsheet\n    print_cols : bool, optional\n        A flag used to print the columns to the console (default is\n        False)\n\n    Returns\n    -------\n    list\n        a list of strings used that are the header columns\n    \"\"\"\n\n    file_data = pd.read_excel(file_loc)\n    col_headers = list(file_data.columns.values)\n\n    if print_cols:\n        print(\"\\n\".join(col_headers))\n\n    return col_headers\n\n\ndef main():\n    parser = argparse.ArgumentParser(description=__doc__)\n    parser.add_argument(\n        'input_file',\n        type=str,\n        help=\"The spreadsheet file to pring the columns of\"\n    )\n    args = parser.parse_args()\n    get_spreadsheet_cols(args.input_file, print_cols=True)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "notes/login-state.py",
    "content": "https://stackoverflow.com/questions/35641019/how-do-you-use-credentials-saved-by-the-browser-in-auto-login-script-in-python-2\n\nThis is because selenium doesn't use your default browser instance, it opens a different instance with a temporary (empty) profile.\n\nIf you would like it to load a default profile you need to instruct it to do so.\n\nHere's a chrome example:\n\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\noptions = webdriver.ChromeOptions() \noptions.add_argument(\"user-data-dir=C:\\\\Path\") #Path to your chrome profile\nw = webdriver.Chrome(executable_path=\"C:\\\\Users\\\\chromedriver.exe\", chrome_options=options)\n\nAnd here's a firefox example:\n\nfrom selenium import webdriver\nfrom selenium.webdriver.firefox.webdriver import FirefoxProfile\n\nprofile = FirefoxProfile(\"C:\\\\Path\\\\to\\\\profile\")\ndriver = webdriver.Firefox(profile)\n\nHere we go, just dug up a link to this in the (unofficial) documentation. Firefox Profile and the Chrome driver info is right underneath it.\n\n\n\n\n\n\n\n\nsaving cookies\n\n    import pickle \n    from selenium import webdriver \n    driver = webdriver.Firefox() \n    driver.get('http://www.quora.com') \n    # login code \n    pickle.dump(driver.get_cookies() , open(\"QuoraCookies.pkl\",\"wb\")) \n\nloading cookies\n\n    import pickle \n    from selenium import webdriver \n    driver = webdriver.Firefox() \n    driver.get('http://www.quora.com') \n    for cookie in pickle.load(open(\"QuoraCookies.pkl\", \"rb\")): \n        driver.add_cookie(cookie) \n\n\n\n# https://stackoverflow.com/questions/45651879/using-selenium-how-to-keep-logged-in-after-closing-driver-in-python\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\noptions = Options()\noptions.add_argument(\"user-data-dir=/tmp/tarun\")\ndriver = webdriver.Chrome(chrome_options=options)\n\ndriver.get('https://web.whatsapp.com/')\ndriver.quit()\n\n\n# windows\noptions.add_argument(\"user-data-dir=C:\\\\Users\\\\Username\\\\AppData\\\\Local\\\\Google\\\\Chrome\\\\User Data\")\n\n\n\n\n\n\n\n# https://stackoverflow.com/questions/15058462/how-to-save-and-load-cookies-using-python-selenium-webdriver\n\nimport pickle\nimport selenium.webdriver \n\ndriver = selenium.webdriver.Firefox()\ndriver.get(\"http://www.google.com\")\npickle.dump( driver.get_cookies() , open(\"cookies.pkl\",\"wb\"))\n\nand later to add them back:\n\nimport pickle\nimport selenium.webdriver \n\ndriver = selenium.webdriver.Firefox()\ndriver.get(\"http://www.google.com\")\ncookies = pickle.load(open(\"cookies.pkl\", \"rb\"))\nfor cookie in cookies:\n    driver.add_cookie(cookie)"
  },
  {
    "path": "notes/notes1.py",
    "content": "# https://testdriven.io/blog/distributed-testing-with-selenium-grid/\n\n\nimport time\nimport unittest\n\nfrom selenium import webdriver\nfrom selenium.webdriver.common.keys import Keys\n\n\nclass HackerNewsSearchTest(unittest.TestCase):\n\n    def setUp(self):\n        self.browser = webdriver.Chrome()\n\n    def test_hackernews_search_for_testdrivenio(self):\n        browser = self.browser\n        browser.get('https://news.ycombinator.com')\n        search_box = browser.find_element_by_name('q')\n        search_box.send_keys('testdriven.io')\n        search_box.send_keys(Keys.RETURN)\n        time.sleep(3)  # simulate long running test\n        self.assertIn('testdriven.io', browser.page_source)\n\n    def test_hackernews_search_for_selenium(self):\n        browser = self.browser\n        browser.get('https://news.ycombinator.com')\n        search_box = browser.find_element_by_name('q')\n        search_box.send_keys('selenium')\n        search_box.send_keys(Keys.RETURN)\n        time.sleep(3)  # simulate long running test\n        self.assertIn('selenium', browser.page_source)\n\n    def test_hackernews_search_for_testdriven(self):\n        browser = self.browser\n        browser.get('https://news.ycombinator.com')\n        search_box = browser.find_element_by_name('q')\n        search_box.send_keys('testdriven')\n        search_box.send_keys(Keys.RETURN)\n        time.sleep(3)  # simulate long running test\n        self.assertIn('testdriven', browser.page_source)\n\n    def test_hackernews_search_with_no_results(self):\n        browser = self.browser\n        browser.get('https://news.ycombinator.com')\n        search_box = browser.find_element_by_name('q')\n        search_box.send_keys('?*^^%')\n        search_box.send_keys(Keys.RETURN)\n        time.sleep(3)  # simulate long running test\n        self.assertNotIn('<em>', browser.page_source)\n\n    def tearDown(self):\n        self.browser.quit()  # quit vs close?\n\n\nif __name__ == '__main__':\n    unittest.main()\n"
  },
  {
    "path": "notes/notes2.py",
    "content": "# https://crossbrowsertesting.com/blog/selenium/selenium-design-patterns/\n\n\nimport org.openqa.selenium.WebElement;\nimport org.openqa.selenium.support.CacheLookup;\nimport org.openqa.selenium.support.FindBy;\nimport org.openqa.selenium.support.How;\n\npublic class LoginPage {\n\n@FindBy(how = How.ID, using = \"username\")\n@CacheLookup\nprivate WebElement username;\n\n@FindBy(how = How.ID, using = \"password\")\n@CacheLookup\nprivate WebElement password;\n\n@FindBy(how = How.ID, using = \"login\")\n@CacheLookup\nprivate WebElement login;\n\npublic String GetUsername() {\n\nreturn username.getAttribute(\"value\");>/pre>\n\n}\n\npublic void setUsername(String value) {\n\nusername.clear();\nusername.sendKeys(value);\n\n}\n\npublic String getPassword() {\n\nreturn password.getAttribute(\"value\");\n\n}\n\npublic void setPassword(String value) {\n\npassword.clear();\npassword.sendKeys(value);\n\n}\n\npublic void submitLogin() {\n\nlogin.click();\n\n}\n\n}\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nimport org.junit.Test;\nimport org.openqa.selenium.WebDriver;\nimport org.openqa.selenium.chrome.ChromeDriver;\nimport org.openqa.selenium.support.PageFactory;\n\npublic class LoginTest {\n\n@Test\npublic void testLogin() {\n\nWebDriver driver = new ChromeDriver();\n\ndriver.navigate().to(\"http://www.selenium.academy/Examples/DesignPattern.html\");\n\n// Create a new instance of the login page object\nLoginPage login = PageFactory.initElements(driver, LoginPage.class);\n\n// set the username\nlogin.setUsername(\"daniel\");\n\n// set the password\nlogin.setPassword(\"secret\");\n\n// submit the login\nlogin.submitLogin();\n\ndriver.quit();\n\n}\n\n}"
  },
  {
    "path": "notes/notes3.py",
    "content": "\nbrowsers = [\n   {“platform”: “Windows 7 64-bit”, “browserName”: “Internet Explorer”,”version”: “10”, “name”: “Python Parallel”}, {“platform”: “Windows 8.1”, “browserName”: “Chrome”, “version”: “50”, “name”: “Python Parallel”},\n]\n\n\n\nbrowsers_waiting = [ ]\n\ndef get_browser_and_wait(browser_data):\n   print (\"starting %s\\n\" % browser_data[\"browserName\"])\n   browser = get_browser(browser_data)\n   browser.get(\"http://crossbrowsertesting.com\")\n   browsers_waiting.append({\"data\": browser_data, \"driver\": browser})\n   print (\"%s ready\" % browser_data[\"browserName\"])\n   while len(browsers_waiting) < len(browsers):\n     print (\"working on %s.... please wait\" % browser_data[\"browserName\"])\nbrowser.get(\"http://crossbrowsertesting.com\")\n     time.sleep(3)\n\n\n\n\n\n\nthreads = []\nfor i, browser in enumerate(browsers):\n   thread = Thread(target=get_browser_and_wait, args=[browser])\n   threads.append(thread)\n   thread.start()\n\nfor thread in threads:\n   thread.join()\n\nprint (\"all browsers ready\")\nfor i, b in enumerate(browsers_waiting):\n   print (\"browser %s's title: %s\" % (b[\"data\"][\"name\"], b[\"driver\"].title))\n   b[\"driver\"].quit()"
  },
  {
    "path": "notes/notes4.py",
    "content": "# https://www.gridlastic.com/python-code-example.html\n\npip install pytest\npip install pytest-xdist\npip install pytest-rerunfailures\n\n\n#file test_unittest.py\nimport unittest\nfrom selenium import webdriver\nfrom selenium.webdriver.common.keys import Keys\nfrom selenium.webdriver.common.desired_capabilities import DesiredCapabilities\nimport logging\nlogging.basicConfig(filename=\"log.txt\", level=logging.INFO)\n\nclass TestExamples(unittest.TestCase):\n\n    def setUp(self):\n        self.driver  = webdriver.Remote(\n\t\tcommand_executor=\"https://USERNAME:ACCESS_KEY@HUB_SUBDOMAIN.gridlastic.com/wd/hub\",\n\t\tdesired_capabilities={\n            \"browserName\": \"chrome\",\n            \"browserVersion\": \"latest\",\n            \"video\": \"True\",\n            \"platform\": \"WIN10\",\n            \"platformName\": \"windows\",\n        })\n        self.driver.implicitly_wait(30)\n        self.driver.maximize_window() # Note: driver.maximize_window does not work on Linux, instead set window size and window position like driver.set_window_position(0,0) and driver.set_window_size(1920,1080)\n\n    def test_one(self):\n        try:\n           driver = self.driver\n           driver.get(\"http://www.python.org\")\n           self.assertIn(\"Python\", driver.title)\n           elem = driver.find_element_by_name(\"q\")\n           elem.send_keys(\"documentation\")\n           elem.send_keys(Keys.RETURN)\n           assert \"No results found.\" not in driver.page_source\n        finally:\n           logging.info(\"Test One Video: \" + VIDEO_URL + driver.session_id)\n\t\t   \n    def test_two(self):\n        try:\n           driver = self.driver\n           driver.get(\"http://www.google.com\")\n           elem = driver.find_element_by_name(\"q\")\n           elem.send_keys(\"webdriver\")\n           elem.send_keys(Keys.RETURN)\n        finally:\n           \tlogging.info(\"Test Two Video: \" + VIDEO_URL + driver.session_id)\n\t\t\t\n    def tearDown(self):\n        self.driver.quit()\n\nif __name__ == \"__main__\":\n    unittest.main()"
  },
  {
    "path": "notes/notes5.py",
    "content": "pip install pytest-selenium\npip install pytest-variables\n\n\n JSON config file (capabilities.json) \n{ \"capabilities\": {\n\t\"video\": \"True\",\n\t\"gridlasticUser\": USERNAME,\n\t\"gridlasticKey\": ACCESS_KEY\n\t}\n}\n\n\n\n#file test_pytest_selenium.py\nimport pytest\nfrom selenium.webdriver.common.keys import Keys\nfrom selenium.webdriver.common.desired_capabilities import DesiredCapabilities\nimport logging\nlogging.basicConfig(filename=\"log.txt\", level=logging.INFO)\n\n@pytest.fixture\ndef selenium(selenium):\n    selenium.implicitly_wait(30)\n    selenium.maximize_window()\n    return selenium\n\t\ndef test_one(selenium):\n\ttry:\n\t\tselenium.get(\"http://www.python.org\")\n\t\tassert \"Python\" in selenium.title\n\t\telem = selenium.find_element_by_name(\"q\")\n\t\telem.send_keys(\"documentation\")\n\t\telem.send_keys(Keys.RETURN)\n\t\tassert \"No results found.\" not in selenium.page_source\n\tfinally:\n\t\tlogging.info(\"Test One Video: \" + VIDEO_URL + selenium.session_id)\n\ndef test_two(selenium):\n\ttry:\n\t\tselenium.get(\"http://www.google.com\")\n\t\telem = selenium.find_element_by_name(\"q\")\n\t\telem.send_keys(\"webdriver\")\n\t\telem.send_keys(Keys.RETURN)\n\tfinally:\n\t\tlogging.info(\"Test Two Video: \" + VIDEO_URL + selenium.session_id)"
  },
  {
    "path": "notes/notes6.py",
    "content": "PROXY = \"hub_subdomain.gridlastic.com:8001\"; # hosted Squid proxy on your selenium grid hub\n#PROXY = \"your_gridlastic_connect_subdomain.gridlastic.com:9999\"; # An example Gridlastic Connect endpoint\n\ndesired_capabilities['proxy'] = {\n    \"httpProxy\":PROXY,\n    \"ftpProxy\":PROXY,\n    \"sslProxy\":PROXY,\n    \"noProxy\":None,\n    \"proxyType\":\"MANUAL\",\n    \"class\":\"org.openqa.selenium.Proxy\",\n    \"autodetect\":False\n}"
  },
  {
    "path": "notes/old/bot.py",
    "content": "## unused ##\n\nimport time\nimport threading\nimport concurrent.futures\n##\nfrom OnlySnarf.driver import Driver\nfrom OnlySnarf.actions import Message\nfrom OnlySnarf.user import User\nfrom OnlySnarf.settings import Settings\n\nREFRESH_DURATION = 60*9\n\n# RUN_DURATION_ALL = 60*60\n# RUN_DURATION_RECENT = 60*10\n\n# commands = [\n#   \"0) menu\",\n#   # \"1) dick pic\"\"\n# ]\n\n# COMMANDS_AVAILABLE = \"Commands available:\\n0) menu\\n1) notice me senpai\"\n\nMAX_BROWSERS = 1\n# MAX_THREADS = 5\n\nclass Bot():\n\n    USERS = []\n    i = 0\n    lock = threading.RLock()\n\n    def __init__(self):\n        self.driver = None\n        self.drivers = []\n        self.refreshing = None\n        self.running = None\n        self.lock = threading.RLock()\n        self.locks = []\n\n        ##\n        # self.refresher()\n\n    @staticmethod\n    def parse(user=None):\n        Settings.print(\"Parsing: {} - {}\".format(user.username, user.id))\n        # check user for commands in unchecked messages\n        # run command\n        # Settings.print(\"user: {}\".format(user))\n        # Settings.print(\"parsing: {}\".format(user.username))\n\n        # if not user or not user.username or str(user) == \"None\" or str(user.username) == \"None\": return\n        # commands = [\"0) menu\"]\n\n        user.update_chat_log()\n\n        ## TODO: update to be used here instead of in user class\n        def get_unparsed_messages(self):\n            unparsed_messages = [m for m in self.messages if m not in self.messages_parsed]  \n            Settings.dev_print(\"unparsed messages: {}\\n{}\".format(len(unparsed_messages),\"\\n\".join(unparsed_messages)))\n            return unparsed_messages\n\n        def parse_message(self, message=None):\n            self.messages.remove(str(message))\n            self.messages_parsed.append(str(message))\n\n        unparsed = user.get_unparsed_messages()\n        for message in unparsed:\n            successful = False\n            isTip, amount = Message.is_tip(message)\n            if isTip:\n                Settings.dev_print(0)\n                successful = Bot.tipped(user=user, amount=amount)\n                Settings.dev_print(1)\n            # elif \"0) menu\" in str(message).lower():\n            #   successful = Bot.prompt(user=user)\n            if successful:\n                user.parse_message(message=message.message)\n        Settings.dev_print(\"successfully parsed user: {} - {}\".format(user.username, user.id))\n\n    def parse_messages(self):\n        # if self.running: self.running.stop()\n        if not self.driver:\n            self.driver = Driver()\n            self.driver.init()\n\n        # read all messages\n        # users = Bot.USERS\n        # if len(users) == 0:\n        #   users = User.get_all_users(driver=self.driver)\n        # else:\n        #   users = User.get_recent_messagers(driver=self.driver)\n        #   # users = User.get_recent_messagers(notusers=users, driver=self.driver)\n        # Bot.USERS = users\n\n        users = User.get_recent_messagers(driver=self.driver)\n\n        Settings.print(\"Users to parse: {}\".format(len(users)))\n        # self.running = threading.Timer(RUN_DURATION*len(users), self.run).start()\n        # self.running = threading.Timer(RUN_DURATION, self.run).start()\n\n        # respond to messages\n\n        def threaded():\n            def parse(user):\n                self.lock.acquire()\n                i = int(Bot.get_index())\n                # if i > len(self.locks):\n                    # self.locks.append(threading.RLock())\n                # self.locks[i].acquire()\n                try:\n                    # self.lock.acquire()\n                    if not user.driver or not user.browser:\n                        if len(self.drivers) == 0:\n                            user.driver = self.driver\n                            self.drivers.append(user.driver)\n                            self.driver = None\n                        elif len(self.drivers) >= MAX_BROWSERS:\n                            user.driver = self.drivers[i]\n                        else:\n                            user.driver = Driver()\n                            self.drivers.append(user.driver)\n                    self.lock.release()\n                    Bot.parse(user=user)\n                except Exception as e:\n                    Settings.print(e)\n                    Settings.dev_print(\"failed to parse user: {} - {}\".format(user.id, user.username))\n                # finally:\n                    # self.locks[i].release()\n\n            # for user in users:\n            #   prepare(user)\n\n            # if \"remote\" in str(Settings.get_browser_type()): MAX_THREADS = 10\n            with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_BROWSERS) as executor:\n                executor.map(parse, users)\n\n        def single():\n            for user in users:\n                if not user.driver or not user.browser:\n                    # setattr(user, \"driver\", Driver(browser=None))\n                    # setattr(user, \"browser\", user.driver.spawn())\n                    setattr(user, \"driver\", self.driver)\n                Bot.parse(user=user)\n\n        if \"remote\" in Settings.get_browser_type() or \"reconnect\" in Settings.get_browser_type():\n            single()\n        else:\n            threaded()\n\n        time.sleep(60*10)\n        if not self.driver: self.driver = Driver(browser=None)\n        users = User.get_all_users(driver=self.driver)\n\n        Settings.print(\"Users to parse: {}\".format(len(users)))\n\n        if \"remote\" in Settings.get_browser_type() or \"reconnect\" in Settings.get_browser_type():\n            single()\n        else:\n            threaded()\n\n        time.sleep(60*10)\n        if not self.driver: self.driver = Driver(browser=None)\n        users = User.get_recent_messagers(driver=self.driver)\n\n        Settings.print(\"Users to parse: {}\".format(len(users)))\n\n        if \"remote\" in Settings.get_browser_type() or \"reconnect\" in Settings.get_browser_type():\n            single()\n        else:\n            threaded()\n\n        # self.parse_messages()\n\n    @staticmethod\n    def get_index():\n        # Bot.lock.acquire()\n        i = Bot.i\n        Bot.i += 1\n        if Bot.i == MAX_BROWSERS: Bot.i = 0\n        # Bot.lock.release()\n        return i\n\n    @staticmethod\n    def prompt(user=None):\n        # show list of commands available\n        user.message(message=COMMANDS_AVAILABLE)\n        return True\n\n    # refresh the Driver\n    def refresh(self):\n        self.driver.refresh()\n\n    # handle the timer for refreshing the Driver\n    def refresher(self):\n        if not Settings.is_keep(): return\n        if self.refreshing: self.refreshing.stop()\n        self.refreshing = threading.Timer(REFRESH_DURATION, self.refresh).start()\n\n\n\n    @staticmethod\n    def tipped(user=None, amount=None):\n        # for every $x amountsend 1 dick pic\n        num = int(amount)%5\n        Settings.dev_print(\"tipped num: {}\".format(num))\n        return user.send_dick_pics(num)"
  },
  {
    "path": "notes/old/config.py",
    "content": "#!/usr/bin/python\n# setup & update script for config\n\nimport os\nimport sys\nimport json\nimport shutil\n##\nfrom OnlySnarf.lib import google as Google\nfrom OnlySnarf.lib import driver as OnlySnarf\nfrom OnlySnarf.util import Settings\nfrom OnlySnarf.util import colorize\n\ndef checkBothCreds():\n    checkGoogle()\n    checkOnlyFans()\n\n# checks Google creds access\ndef checkGoogle():\n    Settings.print(\"Checking Google Creds (uploads)\")\n    if not os.path.exists(Settings.get_google_path()):\n        Settings.print(\"Missing Google Creds\")\n        Settings.print()\n        return main()\n    authed = Google.checkAuth()\n    if authed:\n        Settings.print(\"Google Auth Successful\")\n    else: \n        Settings.print(\"Google Auth Failure\")\n\n# checks OnlyFans login process\ndef checkOnlyFans():\n    Settings.print(\"Checking OnlyFans Creds\")\n    if not os.path.exists(Settings.get_config_path()):\n        Settings.print(\"Missing Config Path\")\n        return main()\n    OnlySnarf.auth()\n    OnlySnarf.exit()\n\ndef checkTwitter():\n    Settings.print(\"Checking Twitter Creds\")\n    if not os.path.exists(Settings.get_config_path()):\n        Settings.print(\"Missing Config Path\")\n        return main()\n    OnlySnarf.auth()\n    OnlySnarf.exit()\n\n# function that creates the missing config\ndef createConfig():\n    Settings.print(\"Preparing OnlySnarf Config\")\n    # ensure /opt/onlysnarf exists\n    if not os.path.exists(\"/opt/onlysnarf\"):\n        Settings.print(\"Creating Missing Config Dir\")\n        try:\n            os.makedirs(\"/opt/onlysnarf\")\n            Settings.print(\"Created OnlySnarf Root\")\n        except Exception as e:\n            Settings.print(e)\n            main()\n    if not os.path.exists(\"/opt/onlysnarf/config.conf\"):\n        Settings.print(\"Copying Default Config\")\n        try:\n            shutil.copyfile(os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), \"../config.conf\")), Settings.get_config_path())\n            shutil.chown(Settings.get_config_path(), user=os.environ['USER'], group=os.environ['USER'])\n            Settings.print(\"Created OnlySnarf Config\")\n        except Exception as e:\n            Settings.print(e)\n            main()\n    else:\n        Settings.print(\"OnlySnarf Config Exists\")\n        return True\n    return False\n\n# provides instructions for creating or refreshing google creds\ndef googleInstructions():\n    Settings.print(\"[Google Instructions From README Go Here]\")\n\n# creates the config then prompts for missing credentials\ndef setupConfig():\n    alreadyCreated = createConfig()\n    if not alreadyCreated:\n        updateConfig()\n\n# receives input for Google login\ndef receiveGoogle():\n    data = {}\n    data['username'] = input('Google Username: ')\n    data['password'] = input('Google Password: ')\n    return data\n\ndef receiveOnlyFans():\n    data = {}\n    data['email'] = input('OnlyFans Email: ')\n    data['username'] = input('OnlyFans Username: ')\n    data['password'] = input('OnlyFans Password: ')\n    return data\n\n# receives input for Twitter login\ndef receiveTwitter():\n    data = {}\n    data['username'] = input('Twitter Username: ')\n    data['password'] = input('Twitter Password: ')\n    return data\n\n# refreshes all creds\ndef refreshAll():\n    removeConfig()\n    setupConfig()\n    removeGoogle()\n    googleInstructions()\n\n# removes config.conf\ndef removeConfig():\n    Settings.print(\"Removing Config\")\n    # ensure /opt/onlysnarf exists\n    if os.path.exists(Settings.get_config_path()):\n        os.remove(Settings.get_config_path())\n        Settings.print(\"Removed Config\")\n    else:\n        Settings.err_print(\"failed to find config\")\n\n# removes google creds\ndef removeGoogle():\n    Settings.print(\"Removing Google Creds\")\n    # ensure /opt/onlysnarf exists\n    if os.path.exists(Settings.get_google_path()):\n        os.remove(Settings.get_google_path())\n        Settings.print(\"Removed Google Creds\")\n    else:\n        Settings.err_print(\"failed to find google creds\")\n\n# receives input for twitter login and saves to config.conf\ndef updateConfig():\n    updateOnlyFans()\n    updateGoogle()\n    updateTwitter()\n\ndef updateOnlyFans():\n    data = receiveOnlyFans()\n    # update conf variables username and password\n    # save the conf file\n    import fileinput\n    # Does a list of files, and\n    # redirects STDOUT to the file in question\n    for line in fileinput.input(Settings.get_config_path(), inplace = 1): \n        line.replace(\"email None\", \"username {}\".format(data['email']))\n        line.replace(\"username None\", \"username {}\".format(data['username']))\n        line.replace(\"password None\", \"password {}\".format(data['password']))\n        Settings.print(line)\n    Settings.print(\"OnlyFans Config Updated\")\n\ndef updateGoogle():\n    data = receiveGoogle()\n    # update conf variables username and password\n    # save the conf file\n    import fileinput\n    # Does a list of files, and\n    # redirects STDOUT to the file in question\n    for line in fileinput.input(Settings.get_config_path(), inplace = 1): \n        line.replace(\"username_google None\", \"username {}\".format(data['username']))\n        line.replace(\"password_google None\", \"password {}\".format(data['password']))\n        Settings.print(line)\n    Settings.print(\"Google Config Updated\")\n\ndef updateTwitter():\n    data = receiveTwitter()\n    # update conf variables username and password\n    # save the conf file\n    import fileinput\n    # Does a list of files, and\n    # redirects STDOUT to the file in question\n    for line in fileinput.input(Settings.get_config_path(), inplace = 1): \n        line.replace(\"username_twitter None\", \"username {}\".format(data['username']))\n        line.replace(\"password_twitter None\", \"password {}\".format(data['password']))\n        Settings.print(line)\n    Settings.print(\"Twitter Config Updated\")\n    \n# this script is supposed to have menu options for \n# ) creating the .conf file\n# ) updating the .conf file\n# ) instructions for creating the google creds\n# ) a function for checking the google creds\n# when ran in it should check for the .conf file and google_creds\ndef main():\n    Settings.print(\"-- OnlySnarf Config --\")\n    Settings.print(\"------------------------------\")\n    if os.path.isfile(Settings.get_config_path()):\n        Settings.print(colorize(\"[*] Config File\", 'conf')+\": \"+colorize(\"True\", 'green'))\n        if str(Settings.get_email()) != \"None\":\n            Settings.print(colorize(\"[-] OnlyFans Email\", 'conf')+\": \"+colorize(Settings.get_email(), 'green'))\n        else:\n            Settings.print(colorize(\"[-] OnlyFans Email\", 'conf')+\": \"+colorize(\"\", 'red'))\n        if str(Settings.get_password()) != \"None\":\n            Settings.print(colorize(\"[-] OnlyFans Password\", 'conf')+\": \"+colorize(\"******\", 'green'))\n        else:\n            Settings.print(colorize(\"[-] OnlyFans Password\", 'conf')+\": \"+colorize(\"\", 'red'))\n        if str(Settings.get_username()) != \"None\":\n            Settings.print(colorize(\"[-] OnlyFans Username\", 'conf')+\": \"+colorize(Settings.get_username(), 'green'))\n        else:\n            Settings.print(colorize(\"[-] OnlyFans Username\", 'conf')+\": \"+colorize(\"\", 'red'))\n        if str(Settings.get_username_google()) != \"None\":\n            Settings.print(colorize(\"[-] Google Username\", 'conf')+\": \"+colorize(Settings.get_username_google(), 'green'))\n        else:\n            Settings.print(colorize(\"[-] Google Username\", 'conf')+\": \"+colorize(\"\", 'red'))\n        if str(Settings.get_password_google()) != \"None\":\n            Settings.print(colorize(\"[-] Google Password\", 'conf')+\": \"+colorize(\"******\", 'green'))\n        else:\n            Settings.print(colorize(\"[-] Google Password\", 'conf')+\": \"+colorize(\"\", 'red'))\n        if str(Settings.get_username_twitter()) != \"None\":\n            Settings.print(colorize(\"[-] Twitter Username\", 'conf')+\": \"+colorize(Settings.get_username_google(), 'green'))\n        else:\n            Settings.print(colorize(\"[-] Twitter Username\", 'conf')+\": \"+colorize(\"\", 'red'))\n        if str(Settings.get_password_twitter()) != \"None\":\n            Settings.print(colorize(\"[-] Twitter Password\", 'conf')+\": \"+colorize(\"******\", 'green'))\n        else:\n            Settings.print(colorize(\"[-] Twitter Password\", 'conf')+\": \"+colorize(\"\", 'red'))\n    else:\n        Settings.print(colorize(\"[*] Config File\", 'conf')+\": \"+colorize(\"False\", 'red'))\n    if os.path.isfile(Settings.get_google_path()):\n        Settings.print(colorize(\"[*] Google Creds\", 'conf')+\": \"+colorize(\"True\", 'green'))\n    else:\n        Settings.print(colorize(\"[*] Google Creds\", 'conf')+\": \"+colorize(\"False\", 'red'))\n    Settings.print(\"------------------------------\")\n    Settings.print(colorize(\"Menu:\", 'menu'))\n    Settings.print(colorize(\"[ 0 ]\", 'menu') + \" Config - Create\")\n    Settings.print(colorize(\"[ 1 ]\", 'menu') + \" Config - Update - Google\")\n    Settings.print(colorize(\"[ 2 ]\", 'menu') + \" Config - Update - OnlyFans\")\n    Settings.print(colorize(\"[ 3 ]\", 'menu') + \" Config - Update - Twitter\")\n    Settings.print(colorize(\"[ 4 ]\", 'menu') + \" Config - Remove\")\n    Settings.print(colorize(\"[ 5 ]\", 'menu') + \" Google Creds - Check\")\n    Settings.print(colorize(\"[ 6 ]\", 'menu') + \" Google Creds - Instructions\")\n    Settings.print(colorize(\"[ 7 ]\", 'menu') + \" Google Creds - Remove\")\n    # Settings.print(colorize(\"[ 8 ]\", 'menu') + \" Refresh All\")\n    while True:\n        choice = input(\">> \")\n        try:\n            if int(choice) < 0 or int(choice) >= 9: raise ValueError\n            # elif int(choice) == 2:\n            #     checkBothCreds()\n            if int(choice) == 0:\n                setupConfig()\n            elif int(choice) == 1:\n                updateGoogle()\n            elif int(choice) == 2:\n                updateOnlyFans()\n            elif int(choice) == 3:\n                updateTwitter()\n            elif int(choice) == 4:\n                removeConfig()\n            elif int(choice) == 5:\n                checkGoogle()\n            elif int(choice) == 6:\n                googleInstructions()\n            elif int(choice) == 7:\n                removeGoogle()\n            # elif int(choice) == 8:\n            #     refreshAll()\n        except (ValueError, IndexError, KeyboardInterrupt):\n            Settings.err_print(\"incorrect index\")\n    Settings.print()\n    main()\n\n###########################\n\nif __name__ == \"__main__\":\n    try:\n        Settings.initialize()\n        main()\n    except Exception as e:\n        Settings.maybe_print(e)\n        Settings.print(e)"
  },
  {
    "path": "notes/old/file-old.py",
    "content": "import os, shutil, random, sys\nimport PyInquirer\nfrom PIL import Image\nfrom os import walk\n##\nfrom ..lib import google as Google\nfrom ..lib import remote as Remote\nfrom ..lib.ffmpeg import ffmpeg\nfrom ..util.settings import Settings\n\nONE_GIGABYTE = 1000000000\nONE_MEGABYTE = 1000000\nFIFTY_MEGABYTES = 50000000\nONE_HUNDRED_KILOBYTES = 100000\n\nMIMETYPES_IMAGES = \"(mimeType contains 'image/jpeg' or mimeType contains 'image/jpg' or mimeType contains 'image/png')\"\nMIMETYPES_VIDEOS = \"(mimeType contains 'video/mp4' or mimeType contains 'video/quicktime' or mimeType contains 'video/x-ms-wmv' or mimeType contains 'video/x-flv')\"\nMIMETYPES_ALL = \"(mimeType contains 'image/jpeg' or mimeType contains 'image/jpg' or mimeType contains 'image/png' or mimeType contains 'video/mp4' or mimeType contains 'video/quicktime')\"\n\nMIMETYPES_IMAGES_LIST = [\"image/jpeg\",\"image/jpg\",\"image/png\"]\nMIMETYPES_VIDEOS_LIST = [\"video/mp4\",\"video/quicktime\",\"video/x-ms-wmv\",\"video/x-flv\"]\nMIMETYPES_ALL_LIST = []\nMIMETYPES_ALL_LIST.extend(MIMETYPES_IMAGES_LIST)\nMIMETYPES_ALL_LIST.extend(MIMETYPES_VIDEOS_LIST)\n\n###############################################################\n\nclass File():\n    \"\"\"File class\"\"\"\n\n    FILES = None\n\n    def __init__(self):\n        \"\"\"File object represents local image/video file\"\"\"\n\n        # the path to the file locally\n        self.path = None\n        # the file extension\n        self.ext = None\n        # image|video\n        self.type = None\n        ##\n        # file title reference\n        self.title = None\n        # [image, gallery, video, performer]\n        self.category = None\n        # file size\n        self.size = None\n\n    ######################################################################################\n\n    def backup(self):\n        \"\"\"Backup file to appropriate destination source\"\"\"\n\n        if not File.backup_text(self.get_title()): return\n        if Settings.get_destination() == \"remote\":\n            Remote.upload_file(self)\n        elif Settings.get_destination() == \"google\":\n            Google.upload_file(file=self)\n        else:\n            # move file to local backup location\n            backupPath = os.path.join(Settings.get_local_path(), \"posted\")\n            backupPath = os.path.join(backupPath, self.category, self.get_title())\n            shutil.move(self.get_path(), backupPath)\n\n    @staticmethod\n    def backup_text(title):\n        \"\"\"\n        Print applicable backup text\n\n        Returns\n        -------\n        bool\n            Whether or not the file should be backed up\n\n        \"\"\"\n\n        if Settings.is_skip_download():\n            Settings.warn_print(\"skipping backup, skipped download\")\n            return False\n        if Settings.is_force_backup():\n            Settings.maybe_print(\"backing up (forced): {}\".format(title))\n        elif not Settings.is_backup():\n            Settings.maybe_print(\"skipping backup (disabled): {}\".format(title))\n            return False\n        elif Settings.is_debug():\n            Settings.maybe_print(\"skipping backup (debug): {}\".format(title))\n            return False\n        else:\n            Settings.maybe_print(\"backing up: {}\".format(title))\n        return True\n\n    @staticmethod\n    def backup_files(files=[]):\n        \"\"\"\n        Backup files provided to appropriate destinations\n\n        Returns\n        -------\n        bool\n            Whether or not the files were backed up successfully\n\n        \"\"\"\n\n        if not File.backup_text(self.get_title()): return\n        if Settings.get_destination() == \"remote\":\n            Remote.upload_files(files)\n        elif Settings.get_destination() == \"google\":\n            Google.upload_files(files)\n        else:\n            for file in files:\n                file.backup()\n        return True\n\n    def check_size(self):\n        \"\"\"\n        Check file size.\n\n        Returns\n        -------\n        bool\n            Whether or not the file exists by checking size\n\n        \"\"\"\n\n        if not self.size:\n            if not os.path.exists(self.get_path()): return False\n            size = os.path.getsize(self.get_path())\n        else: size = self.size\n        Settings.maybe_print(\"file size: {}kb - {}mb\".format(size/1000, size/1000000))\n        global ONE_MEGABYTE\n        if size <= ONE_MEGABYTE:\n            Settings.warn_print(\"small file size\")\n        global ONE_HUNDRED_KILOBYTES\n        if size <= ONE_HUNDRED_KILOBYTES:\n            Settings.warn_print(\"tiny file size\")\n        self.size = size\n        if size == 0:\n            Settings.err_print(\"empty file size\")\n            return False\n        return True\n\n    def delete(self):\n        \"\"\"Delete file\"\"\"\n\n        if not File.delete_text(self.get_title()): return\n        try: \n            os.remove(self.get_path())\n            Settings.print('File Deleted: {}'.format(self.get_title()))\n        except Exception as e: Settings.dev_print(e)\n\n    @staticmethod\n    def delete_text(title):\n        \"\"\"\n        Print applicable deletion text\n        \n        Returns\n        -------\n        bool\n            Whether or not the file should be deleted\n\n        \"\"\"\n\n        if Settings.is_skip_download():\n            Settings.warn_print(\"skipping delete, skipped download\")\n            return False\n        if not Settings.is_delete():\n            Settings.maybe_print(\"skipping delete (disabled): {}\".format(title))\n            return False\n        elif Settings.is_debug():\n            Settings.maybe_print(\"skipping delete (debug): {}\".format(title))\n            return False\n        else:\n            Settings.maybe_print(\"deleting: {}\".format(title))\n        return True\n\n    @staticmethod\n    def download_text(title):\n        \"\"\"\n        Print applicable download text.\n        \n        Returns\n        -------\n        bool\n            Whether or not the file should be downloaded\n\n        \"\"\"\n\n        if Settings.is_skip_download():\n            Settings.print(\"Skipping Download (debug)\")\n            return False\n        return True\n\n    ##############################\n\n    def get_ext(self):\n        \"\"\"Get the file's extension\"\"\"\n\n        if self.ext: return self.ext\n        self.get_title()\n\n    def get_path(self):\n        \"\"\"\n        Get the file's path\n        \n        Returns\n        -------\n        str\n            The file path\n\n        \"\"\"\n\n        if not self.path:\n            Settings.err_print(\"missing file path\")\n            return  \"\"\n        return self.path\n\n    def get_title(self):\n        \"\"\"\n        Get the file's title from it's filename\n        \n        Returns\n        -------\n        str\n            The file's title or filename without extension\n\n        \"\"\"\n\n        if self.title: return self.title\n        path = self.get_path()\n        if str(path) == \"\": \n            Settings.err_print(\"missing file title\")\n            return \"\"\n        title, ext = os.path.splitext(path)\n        self.ext = ext\n        self.title = \"{}{}\".format(os.path.basename(title), ext)\n        return self.title\n\n    @staticmethod\n    def get_tmp():\n        \"\"\"Creates / gets the default temporary download directory\"\"\"\n\n        # tmp = os.getcwd()\n        # if Settings.get_download_path() != \"\":\n        #     tmp = os.path.join(Settings.get_download_path(), \"tmp\")\n        # else:\n        #     tmp = os.path.join(tmp, \"tmp\")\n        # if not os.path.exists(str(tmp)):\n        #     os.mkdir(str(tmp))\n        # return tmp\n        download_path = Settings.get_download_path()\n        if not os.path.exists(str(download_path)):\n            os.mkdir(str(download_path))\n        return download_path\n\n    def get_type(self):\n        \"\"\"\n        Gets the file's type as an inner class of either Image or Video\n        \n        Returns\n        -------\n        Image|Video\n            The file's type as an image or video class\n\n        \"\"\"\n\n        if self.type: return self.type\n        if str(self.get_ext()) in str(MIMETYPES_VIDEOS_LIST):\n            self.type = Video()\n        elif str(self.get_ext()) in str(MIMETYPES_IMAGES_LIST):\n            self.type = Image()\n        else: Settings.warn_print(\"unable to parse file type\")\n        return self.type\n\n    def prepare(self):\n        \"\"\"\n        Prepares the file for uploading.\n\n        Runs the apppropriate file type method and downloads the file locally if necessary.\n\n        Returns\n        -------\n        bool\n            Whether or not the file is prepared\n\n        \"\"\"\n\n        # Settings.maybe_print(\"preparing: {}\".format(self.get_title()))\n        self.get_type().prepare()\n        if not self.check_size():\n            return False\n        return True\n\n    @staticmethod\n    def remove_local():\n        \"\"\"\n        Delete all local files.\n\n        \"\"\"\n\n        try:\n            # if str(Settings.SKIP_DELETE) == \"True\":\n                # Settings.maybe_print(\"skipping local remove\")\n                # return\n            # Settings.print('Deleting Local File(s)')\n            # delete /tmp\n            tmp = File.get_tmp()\n            if os.path.exists(tmp):\n                shutil.rmtree(tmp)\n                Settings.print('Local File(s) Removed')\n            else:\n                Settings.print('Local Files Not Found')\n        except Exception as e:\n            Settings.dev_print(e)\n\n    @staticmethod\n    def get_files():\n        \"\"\"\n        Get files from the runtime category folder.\n\n        Returns\n        -------\n        list\n            The files retrieved\n\n        \"\"\"\n\n        if File.FILES: return File.FILES\n        category = Settings.get_category()\n        if not category: category = Settings.select_category()\n        if not category: Settings.warn_print(\"missing category\")\n        files = File.get_files_by_category(category)\n        if Settings.get_title() and str(files) != \"unset\":\n            for file in files:\n                if str(Settings.get_title()) == str(file.get_title()):\n                    files = [file]\n                    break\n        File.FILES = files\n        return files\n\n    @staticmethod\n    def get_files_by_folder(path):\n        \"\"\"\n        Get local files from the local folder path.\n\n        Parameters\n        ----------\n        path : str\n            Path to folder to get files of\n\n        Returns\n        -------\n        list\n            The files at the path\n\n        \"\"\"\n\n        f = []\n        for (dirpath, dirnames, filenames) in walk(path):\n            f.extend(filenames)\n            break\n        return f\n\n    def get_folder_by_name(category, parent=None):\n        \"\"\"\n        Get local folder by category and parent.\n\n        Parameters\n        ----------\n        category : str\n            The category or folder name to get\n        parent : file.Folder\n            The local folder's parent to search within\n\n        Returns\n        -------\n        str\n            The folder path of the found folder\n\n        \"\"\"\n\n        if not parent:\n            parent = Settings.get_local_path()\n        Settings.maybe_print(\"parent: {}\".format(parent))\n        f = []\n        for (dirpath, dirnames, filenames) in walk(parent):\n            Settings.dev_print(\"dirpath: {}\".format(dirpath))\n            for dir_ in dirnames:\n                Settings.dev_print(\"dir: {} = {} :category\".format(dir_, category))\n                if str(dir_) == str(category):\n                    return os.path.join(parent, dir_)\n            break\n        return None\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n    @staticmethod\n    def get_folders_of_folder_by_keywords(categoryFolder):\n        \"\"\"\n        Summary line.\n\n        Extended description of function.\n\n        Parameters\n        ----------\n        arg1 : int\n            Description of arg1\n        arg2 : str\n            Description of arg2\n\n        Returns\n        -------\n        int\n            Description of return value\n\n        \"\"\"\n\n        Settings.dev_print(\"getting keywords of folder: {}\".format(categoryFolder))\n        if categoryFolder == None: return []\n        folders = File.get_folders_of_folder(categoryFolder)\n        folders_ = []\n        for folder in folders:\n            if Settings.get_drive_keyword() and str(folder.get_title()) != str(Settings.get_drive_keyword()):\n                Settings.dev_print(\"{} -> not keyword\".format(folder.get_title()))\n                continue\n            elif Settings.get_drive_ignore() and str(folder.get_title()) == str(Settings.get_drive_ignore()):\n                Settings.dev_print(\"{} -> by not keyword\".format(folder.get_title()))\n                continue\n            elif str(folder.get_title()) == str(Settings.get_drive_keyword):\n                Settings.dev_print(\"{} -> by keyword\".format(folder.get_title()))\n            else:\n                Settings.dev_print(\"{}\".format(folder.get_title()))\n            folders_.append(folder)\n        return folders_\n\n    @staticmethod\n    def get_random_file():\n        \"\"\"Get random file from all files\"\"\"\n\n        return random.choice(File.get_files())\n\n    @staticmethod\n    def get_images_of_folder(folder):\n        \"\"\"\n        Summary line.\n\n        Extended description of function.\n\n        Parameters\n        ----------\n        arg1 : int\n            Description of arg1\n        arg2 : str\n            Description of arg2\n\n        Returns\n        -------\n        int\n            Description of return value\n\n        \"\"\"\n\n        Settings.dev_print(\"getting images of folder: {}\".format(folder.get_title()))\n        if not folder: return []\n        imgs = []\n        files = []\n        valid_images = [\".jpg\",\".gif\",\".png\",\".tga\",\".jpeg\"]\n        for f in os.listdir(folder.get_path()):\n            ext = os.path.splitext(f)[1]\n            if ext.lower() not in valid_images:\n                continue\n            file = File()\n            setattr(file, \"path\", os.path.join(folder.get_path(),f))\n            files.append(file)\n            Settings.maybe_print(\"image path: {}\".format(os.path.join(folder.get_path(),f)))\n        return files\n\n    @staticmethod\n    def get_videos_of_folder(folder):\n        \"\"\"\n        Summary line.\n\n        Extended description of function.\n\n        Parameters\n        ----------\n        arg1 : int\n            Description of arg1\n        arg2 : str\n            Description of arg2\n\n        Returns\n        -------\n        int\n            Description of return value\n\n        \"\"\"\n\n        Settings.dev_print(\"getting videos of folder: {}\".format(folder.get_title()))\n        if not folder: return []\n        videos = []\n        files = []\n        valid_videos = [\".mp4\",\".mov\"]\n        for f in os.listdir(folder.get_path()):\n            ext = os.path.splitext(f)[1]\n            if ext.lower() not in valid_videos:\n                continue\n            file = File()\n            setattr(file, \"path\", os.path.join(folder.get_path(),f))\n            files.append(file)\n            Settings.maybe_print(\"video path: {}\".format(os.path.join(folder.get_path(),f)))\n        return files\n\n    @staticmethod\n    def get_folders_of_folder(folderPath):\n        \"\"\"\n        Summary line.\n\n        Extended description of function.\n\n        Parameters\n        ----------\n        arg1 : int\n            Description of arg1\n        arg2 : str\n            Description of arg2\n\n        Returns\n        -------\n        int\n            Description of return value\n\n        \"\"\"\n\n        # os.walk(directory)\n        # will yield a tuple for each subdirectory. Ths first entry in the 3-tuple is a directory name, so\n        # [x[0] for x in os.walk(directory)]\n        # should give you all of the subdirectories, recursively.\n        # Note that the second entry in the tuple is the list of child directories of the entry in the first position, so you could use this instead, but it's not likely to save you much.\n        # However, you could use it just to give you the immediate child directories:\n        Settings.maybe_print(\"local walk: {}\".format(folderPath))\n        folders = []\n        # Settings.print(os.walk(folderPath))\n        for folder in next(os.walk(folderPath))[1]:\n            Settings.maybe_print(\"folder: {}\".format(folder))\n            fol = Folder()\n            setattr(fol, \"path\", os.path.join(folderPath, folder))\n            folders.append(fol)\n        return folders\n\n    @staticmethod\n    def get_files_by_category(cat, performer=None):\n        \"\"\"\n        Summary line.\n\n        Extended description of function.\n\n        Parameters\n        ----------\n        arg1 : int\n            Description of arg1\n        arg2 : str\n            Description of arg2\n\n        Returns\n        -------\n        int\n            Description of return value\n\n        \"\"\"\n\n        Settings.maybe_print(\"loading local files...\")\n        files = []\n        ##\n        def parse_categories(category, categoryFolder=None):\n            \"\"\"\n            Summary line.\n\n            Extended description of function.\n\n            Parameters\n            ----------\n            arg1 : int\n                Description of arg1\n            arg2 : str\n                Description of arg2\n\n            Returns\n            -------\n            int\n                Description of return value\n\n            \"\"\"\n\n            files = []\n            # return File.get_files_by_category(cat)\n            if \"image\" in str(category):\n                categoryFolder = File.get_folder_by_name(category, parent=categoryFolder)\n                for folder in File.get_folders_of_folder_by_keywords(categoryFolder):\n                    if not folder: continue\n                    for image in File.get_images_of_folder(folder):\n                        file = File()\n                        setattr(file, \"path\", imageget_path())\n                        setattr(file, \"category\", folder.get_title())\n                        files.append(file)\n            elif \"video\" in str(category):\n                categoryFolder = File.get_folder_by_name(category, parent=categoryFolder)\n                for folder in File.get_folders_of_folder_by_keywords(categoryFolder):\n                    if not folder: continue\n                    videos = File.get_videos_of_folder(folder)\n                    # if len(videos) > 0:\n                        # files.append(folder)\n                    for video in videos:\n                        file = File()\n                        setattr(file, \"path\", video.get_path())\n                        setattr(file, \"category\", folder.get_title())\n                        files.append(file)\n            elif \"performer\" in str(category):\n                categoryFolder = File.get_folder_by_name(category, parent=categoryFolder)\n                for performer_ in File.get_folders_of_folder_by_keywords(categoryFolder):\n                    # for performer in File.get_folders_of_folder(folder):\n                    if not performer_: continue\n                    p = Folder()\n                    setattr(p, \"path\", performer_.get_path())\n                    setattr(p, \"category\", categoryFolder.get_title())\n                    files.append(p)\n            # elif \"galler\" in str(category):\n            else:\n                categoryFolder = File.get_folder_by_name(category, parent=categoryFolder)\n                for folder in File.get_folders_of_folder_by_keywords(categoryFolder):\n                    if not folder: continue\n                    galleries = File.get_folders_of_folder(folder)\n                    if len(galleries) > 0:\n                        files.append(folder)\n                    for gallery in galleries:\n                        file = Folder()\n                        setattr(file, \"path\", galleryget_path())\n                        setattr(file, \"category\", folder.get_title())\n                        files.append(file)\n            return files\n        ##\n        if performer:\n            categoryFolder = File.get_folder_by_name(\"performers\")\n            for performerFolder in File.get_folders_of_folder_by_keywords(categoryFolder):\n                if str(performer) == str(performerFolder.get_title()):\n                    return parse_categories(cat, categoryFolder=performerFolder)\n        return parse_categories(cat)\n\n    @staticmethod\n    def select_file(category, performer=None):\n        \"\"\"\n        Summary line.\n\n        Extended description of function.\n\n        Parameters\n        ----------\n        arg1 : int\n            Description of arg1\n        arg2 : str\n            Description of arg2\n\n        Returns\n        -------\n        int\n            Description of return value\n\n        \"\"\"\n\n        files = File.get_files_by_category(category, performer=performer)\n        files_ = []\n        for file in files:\n            if isinstance(file, str):\n                files_.append(PyInquirer.Separator())\n                continue\n            file.category = category\n            file_ = {\n                \"name\": file.get_title(),\n                \"value\": file,\n            }\n            files_.append(file_)\n        if len(files_) == 0:\n            Settings.print(\"Missing Files\")\n            return\n        files_.append({\n            \"name\": 'Back',\n            \"value\": None,\n        })\n        question = {\n            'type': 'list',\n            'name': 'file',\n            'message': 'File Path:',\n            'choices': files_,\n            # 'filter': lambda file: file.lower()\n        }\n        answer = PyInquirer.prompt(question)\n        if not answer: return File.select_files()\n        file = answer[\"file\"]\n        if not Settings.confirm(file.get_path()): return None\n        return file\n\n    @staticmethod\n    def select_files():\n        \"\"\"\n        Summary line.\n\n        Extended description of function.\n\n        Parameters\n        ----------\n        arg1 : int\n            Description of arg1\n        arg2 : str\n            Description of arg2\n\n        Returns\n        -------\n        int\n            Description of return value\n\n        \"\"\"\n\n        if not Settings.is_prompt(): return [File.get_random_file()]\n        category = Settings.select_category()\n        if not category: return File.select_file_upload_method()\n        # if not Settings.confirm(category): return File.select_files()\n        Settings.print(\"Select Files or a Folder\")\n        files = []\n        while True:\n            file = File.select_file(category)\n            if not file: break\n            ##\n            if \"performer\" in str(category):\n                cat = Settings.select_category([cat for cat in Settings.get_categories() if \"performer\" not in cat])\n                performerName = file.get_title()\n                file = File.select_file(cat, performer=performerName)\n                if not file: break\n                setattr(file, \"performer\", performerName)\n                files.append(file)\n                if \"galler\" in str(cat) or \"video\" in str(cat): break\n            ##\n            files.append(file)\n            if \"galler\" in str(category) or \"video\" in str(category): break\n        if str(files[0]) == \"unset\": return files\n        if not Settings.confirm([file.get_title() for file in files]): return File.select_files()\n        return files\n\n    @staticmethod\n    def select_file_upload_method():\n        \"\"\"\n        Menu to select the method to upload a file.\n\n        Returns\n        -------\n        list\n            The appropriately selected files\n\n        \"\"\"\n\n        if not Settings.prompt(\"upload files\"): \n            return \"unset\"\n        Settings.print(\"Select an upload source\")\n        sources = Settings.get_source_options()\n        question = {\n            'type': 'list',\n            'name': 'upload',\n            'message': 'Upload:',\n            'choices': [src.title() for src in sources]\n        }\n        upload = PyInquirer.prompt(question)[\"upload\"]\n\n\n        # everything after this part should be in another function\n        # this should just return the string of the upload source\n\n\n        if str(upload) == \"Local\":\n            return File.select_files()\n        elif str(upload) == \"Google\":\n            return Google_File.select_files()\n        # elif str(upload) == \"Dropbox\":\n            # return Dropbox.select_files()\n        elif str(upload) == \"Remote\":\n            return Remote.select_files()\n        return File.select_files()\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n    def upload(self):\n        \"\"\"\n        Process ran by a file after it has been uploaded.\n\n        Ensures the file has been backed up and then deleted locally.\n        \n        Returns\n        -------\n        bool\n            Whether or not the file was properly handled after its upload\n\n        \"\"\"\n        if not self.prepare():\n            Settings.err_print(\"unable to upload file - {}\".format(self.get_title()))\n            return False\n        self.backup()\n        self.delete()\n        return True\n\n##\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nclass Remote_File(File):\n    def __init__(self):\n        File.__init__(self)\n\n    def backup(self):\n        if not File.backup_text(self.get_title()): return\n        if Settings.get_destination() == \"remote\":\n            Remote.backup_file(self)\n        elif Settings.get_destination() == \"google\":\n            Google.upload_file(file=self)\n        # elif Settings.get_destination() == \"dropbox\":\n        #     Dropbox.upload_file(file=self)\n        else:\n            file = self.download()\n            file.backup()\n            self.delete()\n\n    def delete(self):\n        if not File.delete_text(self.get_title()): return\n        try: \n            Remote.delete_file(self)\n            Settings.print('File Deleted: {}'.format(self.get_title()))\n        except Exception as e: Settings.dev_print(e)\n\n    def download(self):\n        if not File.download_text(self.get_title()): return False\n        file = Remote.download_file(self)\n        if not file: return False\n        ### Finish ###\n        if not file.check_size():\n            Settings.err_print(\"missing downloaded file\")\n            return False\n        Settings.print(\"Downloaded: {}\".format(file.get_title()))\n        return file\n\n##\n\nclass Folder(File):\n    def __init__(self):\n        File.__init__(self)\n        self.files = None\n\n    def backup(self):\n        if File.backup_text(self.get_title()): return\n        # Google.upload_gallery(files=self.files)\n\n    def check_size(self):\n        for file in self.get_files():\n            exists = file.check_size()\n            if not exists: return False\n        return True\n\n    # def combine(self):\n    #     if len(self.files) == 0: return\n    #     Settings.dev_print(\"combining files: {}\".format(len(self.files)))\n    #     Settings.dev_print(\"combine path: {}\".format(combinedPath))\n    #     combinedPath = os.path.join(File.get_tmp(), \"{}-combined\".format(self.title))\n    #     for file in files:\n    #         shutil.move(file.get_path(), combinedPath)\n    #         file.path = \"{}/{}\".format(combinedPath, self.title)\n    #     self.combined = ffmpeg.combine(combinedPath)\n\n    ##############################\n\n\n    def download(self):\n        Settings.print(\"Downloading Folder: {}\".format(self.get_title()))\n        if len(self.files) == 0:\n            file_list = Google.get_files_by_folder_id(self.get_id())\n            self.files = []\n            for file in file_list:\n                file_ = Google_File()\n                setattr(file_, \"id\", file[\"id\"])\n                setattr(file_, \"file\", file)\n                self.files.append(file_)\n        folder_size = len(self.files)\n        Settings.maybe_print(\"folder size: {}\".format(folder_size))\n        Settings.maybe_print(\"upload limit: {}\".format(Settings.get_upload_max()))\n        if int(folder_size) == 0:\n            Settings.err_print(\"empty folder\")\n            return False\n        file_list = self.files\n        random.shuffle(file_list)\n        file_list = file_list[:int(Settings.get_upload_max())]\n        ## video preference\n        videos = []\n        for file in file_list:\n            if str(file.get_mimetype()) in MIMETYPES_VIDEOS_LIST:\n                videos.append(file)\n        if len(videos) > 0: file_list = [random.choice(videos)]\n        ##\n        i = 1\n        for file in sorted(file_list, key = lambda x: x.get_title()):\n            Settings.print(\"Downloading: {} ({}/{})\".format(file.get_title(), i, folder_size))\n            file.download()\n            i+=1\n        Settings.print()\n        Settings.print(\"Downloaded Folder: {}\".format(self.get_title()))\n\n    def get_files(self):\n        if not self.files and self.path:\n            self.files = []\n            files = File.get_files_by_folder(self.get_path())\n            for file in files:\n                file_ = File()\n                setattr(file_, \"path\", os.path.join(self.get_path(), file))\n                self.files.append(file_)\n                Settings.maybe_print(\"local file found: {}\".format(file_.get_title()))\n        if Settings.get_title():\n            for file in self.files:\n                if str(Settings.get_title()) == str(file.get_title()):\n                    self.files = [file]\n                    break\n        return self.files\n\n    def get_title(self):\n        if self.title: return self.title\n        path = self.get_path()\n        if str(path) == \"\": \n            Settings.err_print(\"missing file title\")\n            return \"\"\n        title = os.path.basename(path)\n        self.title = title\n        return self.title\n\n    def prepare():\n        prepared = False\n        for file in self.get_files():\n            prepared_ = file.prepare()\n            if prepared_: prepared = prepared_\n        return prepared\n\n\n###################################################################################\n\nclass Google_File(File):\n\n    def __init__(self):\n        File.__init__(self)\n        self.id = None\n        self.title = None\n        self.file = None\n        self.parent = None\n        self.mimeType = None\n\n    def backup(self):\n        if not File.backup_text(self.get_title()): return\n        Google.backup_file(self)\n\n    def delete(self):\n        if not File.delete_text(self.get_title()): return\n        Google.delete(self)\n\n    @staticmethod\n    def download_files(files=[]):\n        Settings.maybe_print(\"download limit: \"+str(Settings.get_image_download_limit()))\n        random.shuffle(files)\n        files = files[:int(Settings.get_image_download_limit())]\n        Settings.print('Downloading Files: {}'.format(len(files)))\n        i = 1\n        for file in sorted(files, key = lambda x: x['title']):\n            Settings.print('Downloading: {}/{}'.format(i, Settings.get_image_download_limit()))\n            file.download()\n        Settings.print(\"Downloaded: {}\".format(len(files)))\n\n    # Download File\n    def download(self):\n        if not File.download_text(self.get_title()): return False\n        successful = Google.download_file(self)\n        if not successful: return False\n        ### Finish ###\n        if not self.check_size():\n            Settings.err_print(\"missing downloaded file\")\n            return False\n        Settings.print(\"Downloaded: {}\".format(self.get_title()))\n        return True\n\n    def get_ext(self):\n        if self.ext: return self.ext\n        title, ext = os.path.splitext(self.get_file()[\"title\"])\n        if str(ext) == \"\":\n            ext = self.get_file()[\"mimeType\"]\n            mime, ext = str(ext).split(\"/\")\n            ext = \".\"+str(ext)\n        self.ext = ext\n        self.title = title\n        return self.ext\n\n    def get_id(self):\n        if self.id: return self.id\n        if self.file: self.id = self.file[\"id\"]\n        return self.id\n\n    def get_file(self):\n        if self.file: return self.file\n        self.file = Google.get_file(self.get_id())\n        # if not self.check_size(): self.download()\n        return self.file\n\n    @staticmethod\n    def get_files():\n        if File.FILES: return File.FILES\n        category = Settings.get_category()\n        if not category: category = Settings.select_category()\n        if not category: Settings.warn_print(\"missing category\")\n        files = Google_File.get_files_by_category(category)\n        if Settings.get_title():\n            for file in files:\n                if str(Settings.get_title()) == str(file.get_title()):\n                    files = [file]\n                    break\n        File.FILES = files\n        return files\n\n    @staticmethod\n    def get_files_by_category(cat, performer=None):\n        Settings.maybe_print(\"Loading Google Files...\")\n        files = []\n        ##\n        def parse_categories(category, categoryFolder=None):\n            files = []\n            # return Google_File.get_files_by_category(cat)\n            if \"image\" in str(category):\n                categoryFolder = Google.get_folder_by_name(category, parent=categoryFolder)\n                for folder in Google.get_folders_of_folder_by_keywords(categoryFolder):\n                    for image in Google.get_images_of_folder(folder):\n                        file = Google_File()\n                        setattr(file, \"file\", image)\n                        setattr(file, \"parent\", folder)\n                        if performer: setattr(file, \"performer\", performer)\n                        files.append(file)\n            elif \"video\" in str(category):\n                categoryFolder = Google.get_folder_by_name(category, parent=categoryFolder)\n                for folder in Google.get_folders_of_folder_by_keywords(categoryFolder):\n                    videos = Google.get_videos_of_folder(folder)\n                    if len(videos) > 0:\n                        files.append(folder[\"title\"])\n                    for video in videos:\n                        file = Google_File()\n                        setattr(file, \"file\", video)\n                        setattr(file, \"parent\", folder)\n                        if performer: setattr(file, \"performer\", performer)\n                        files.append(file)\n            elif \"galler\" in str(category):\n                categoryFolder = Google.get_folder_by_name(category, parent=categoryFolder)\n                for folder in Google.get_folders_of_folder_by_keywords(categoryFolder):\n                    galleries = Google.get_folders_of_folder(folder)\n                    if len(galleries) > 0:\n                        files.append(folder[\"title\"])\n                    for gallery in galleries:\n                        file = Google_Folder()\n                        setattr(file, \"file\", gallery)\n                        setattr(file, \"parent\", folder)\n                        if performer: setattr(file, \"performer\", performer)\n                        files.append(file)\n            elif \"performer\" in str(category):\n                categoryFolder = Google.get_folder_by_name(category, parent=categoryFolder)\n                for performer_ in Google.get_folders_of_folder_by_keywords(categoryFolder):\n                    # for performer in Google.get_folders_of_folder(folder):\n                    p = Google_Folder()\n                    setattr(p, \"file\", performer_)\n                    setattr(p, \"parent\", categoryFolder)\n                    files.append(p)\n            return files\n        ##\n        if str(cat) == \"performers\":\n            categoryFolder = Google.get_folder_by_name(\"performers\")\n            performerFolders = Google.get_folders_of_folder_by_keywords(categoryFolder)\n            for performerFolder in performerFolders:\n                if performer and str(performer) == str(performerFolder['title']):\n                    return parse_categories(cat, categoryFolder=performerFolder)\n\n            if Settings.get_sort_method() == \"ordered\":\n                sortedPerformers = performerFolders\n                sortedPerformers = sorted(sortedPerformers, key = lambda x: x[\"title\"])\n                for performer_ in sortedPerformers:\n                    performer = performer_[\"title\"]\n                    if Settings.get_category_performer():\n                        maybeFiles = parse_categories(Settings.get_category_performer(), categoryFolder=performer_)\n                        if len(maybeFiles) > 0: \n                            Settings.maybe_print(\"performer: {}\".format(performer))\n                            return maybeFiles\n                        continue\n                    for cat_ in Settings.get_categories().remove(\"performers\"):\n                        maybeFiles = parse_categories(cat_, categoryFolder=performer_)\n                        if len(maybeFiles) > 0: \n                            Settings.maybe_print(\"performer: {}\".format(performer))\n                            return maybeFiles\n            elif Settings.get_sort_method() == \"random\":\n                randomPerformers = performerFolders\n                random.shuffle(randomPerformers)\n                randomCats = Settings.get_categories()\n                randomCats.remove(\"performers\")\n                random.shuffle(randomCats)\n                for performer_ in randomPerformers:\n                    performer = performer_[\"title\"]\n                    if Settings.get_category_performer():\n                        maybeFiles = parse_categories(Settings.get_category_performer(), categoryFolder=performer_)\n                        if len(maybeFiles) > 0: \n                            Settings.maybe_print(\"performer: {}\".format(performer))\n                            return maybeFiles\n                        continue\n                    for cat_ in randomCats:\n                        maybeFiles = parse_categories(cat_, categoryFolder=performer_)\n                        if len(maybeFiles) > 0: \n                            Settings.maybe_print(\"performer: {}\".format(performer))\n                            return maybeFiles\n        return parse_categories(cat)\n\n    @staticmethod\n    def get_random_file():\n        files = Google_File.get_files()\n        files = [file for file in files if not isinstance(file, str)]\n        randomFile = random.choice(files)\n        if Settings.get_sort_method() == \"ordered\":\n            randomFile = sorted(files, key = lambda x: x.get_title())[0]\n        Settings.maybe_print(\"random file: {}\".format(randomFile.get_title()))\n        return randomFile\n\n    def get_mimetype(self):\n        if self.mimeType: return self.mimeType\n        self.mimeType = self.get_file()[\"mimeType\"]\n        return self.mimeType\n\n    def get_parent(self):\n        if self.parent: return self.parent \n        self.parent = Google.get_file_parent(self.get_id())\n        return self.parent\n\n    def get_path(self):\n        if self.path: return self.path\n        # downloads to /tmp/downloads or whatever\n        # if exists, adds 1 to end of name\n        tmp = File.get_tmp()\n        def counterfy():\n            filename_ = str(self.get_title())+\"{}\"+str(self.get_ext())\n            counter = 0\n            while os.path.isfile(os.path.join(tmp, filename_.format(counter))):\n                counter += 1\n            filename_ = filename_.format(counter)\n            Settings.maybe_print(\"filename: {}\".format(filename_))\n            filename_ = os.path.join(tmp, filename_.format(counter))\n            return filename_\n        filename = os.path.join(tmp, \"{}{}\".format(self.get_title(), self.get_ext()))\n        if os.path.isfile(filename) and not Settings.is_prefer_local():\n            filename = counterfy()\n        # tmp = File.get_tmp() # i don't think this should be in file over settings\n        filename = filename.strip('\\'').strip('\\\"').strip()\n        self.path = filename\n        Settings.dev_print(self.path)\n        return self.path\n\n    def get_title(self):\n        ## title would be set when created\n        if self.title: return self.title\n        title, ext = os.path.splitext(self.get_file()[\"title\"])\n        self.ext = ext\n        self.title = title.replace(\" \",\"_\")\n        return self.title\n\n    # files are File references\n    # file references can be GoogleId references which need to download their source\n    # files exist when checked for size\n    def prepare(self):\n        Settings.maybe_print(\"preparing1: {}\".format(self.get_title()))\n        if not self.check_size():\n            self.download()\n        return super()\n\n    @staticmethod\n    def select_file(category, performer=None):\n        if not Settings.is_prompt(): return Google_File.get_random_file()\n        # this is a list of google files to select from\n        files = Google_File.get_files_by_category(category, performer=performer)\n        files_ = []\n        for file in files:\n            if isinstance(file, str):\n                files_.append(PyInquirer.Separator())\n                continue\n            file.category = category\n            file_ = {\n                \"name\": file.file['title'],\n                \"value\": file,\n                \"short\": file.file['id']\n            }\n            files_.append(file_)\n        if len(files_) == 0:\n            Settings.print(\"Missing Files\")\n            return Google_File.select_files()\n        question = {\n            'type': 'list',\n            'name': 'file',\n            'message': 'Google Files:',\n            'choices': files_,\n            # 'filter': lambda file: file.lower()\n        }\n        file = PyInquirer.prompt(question)[\"file\"]\n        if not Settings.confirm(file.get_title()): return Google_File.select_file(category)\n        return file\n\n    @staticmethod\n    def select_files():\n        if not Settings.is_prompt(): return [Google_File.get_random_file()]\n        category = Settings.select_category()\n        if not category: return File.select_file_upload_method()\n        # if not Settings.confirm(category): return Google_File.select_files()\n        Settings.print(\"Select Google Files or a Folder\")\n        files = []\n        while True:\n            file = Google_File.select_file(category)\n            if not file: break\n            ##\n            if \"performer\" in str(category):\n                cat = Settings.select_category([cat for cat in Settings.get_categories() if \"performer\" not in cat])\n                performerName = file.get_title()\n                file = Google_File.select_file(cat, performer=performerName)\n                if not file: break\n                setattr(file, \"performer\", performerName)\n                files.append(file)\n                if \"galler\" in str(cat) or \"video\" in str(cat): break\n            ##\n            files.append(file)\n            if \"galler\" in str(category) or \"video\" in str(category): break\n        if not Settings.confirm([file.file['title'] for file in files]): return Google_File.select_files()\n        return files\n\n##########################################################################################\n\nclass Google_Folder(Google_File):\n    def __init__(self):\n        Google_File.__init__(self)\n        self.files = None\n\n    def backup(self):\n        if File.backup_text(self.get_title()): return\n        Google.upload_gallery(files=self.files)\n\n    def check_size(self):\n        for file in self.get_files():\n            exists = file.check_size()\n            if not exists: return False\n        return True\n\n    def download(self):\n        Settings.print(\"Downloading Folder: {}\".format(self.get_title()))\n        if len(self.files) == 0:\n            file_list = Google.get_files_by_folder_id(self.get_id())\n            self.files = []\n            for file in file_list:\n                file_ = Google_File()\n                setattr(file_, \"id\", file[\"id\"])\n                setattr(file_, \"file\", file)\n                self.files.append(file_)\n        folder_size = len(self.files)\n        Settings.maybe_print(\"folder size: {}\".format(folder_size))\n        Settings.maybe_print(\"upload limit: {}\".format(Settings.get_upload_max()))\n        if int(folder_size) == 0:\n            Settings.err_print(\"empty folder\")\n            return False\n        file_list = self.files\n        random.shuffle(file_list)\n        file_list = file_list[:int(Settings.get_upload_max())]\n        ## video preference\n        videos = []\n        for file in file_list:\n            if str(file.get_mimetype()) in MIMETYPES_VIDEOS_LIST:\n                videos.append(file)\n        if len(videos) > 0: file_list = [random.choice(videos)]\n        ##\n        i = 1\n        for file in sorted(file_list, key = lambda x: x.get_title()):\n            Settings.print(\"Downloading: {} ({}/{})\".format(file.get_title(), i, folder_size))\n            file.download()\n            i+=1\n        Settings.print()\n        Settings.print(\"Downloaded Folder: {}\".format(self.get_title()))\n\n    def get_files(self):\n        if not self.files:\n            self.files = []\n            files = Google.get_files_by_folder_id(self.get_id())\n            for file in files:\n                file_ = Google_File()\n                setattr(file_, \"file\", file)\n                self.files.append(file_)\n        if Settings.get_title():\n            for file in self.files:\n                if str(Settings.get_title()) == str(file.get_title()):\n                    self.files = [file]\n                    break\n        return self.files\n\n    def prepare():\n        prepared = False\n        for file in self.get_files():\n            prepared_ = file.prepare()\n            if prepared_: prepared = prepared_\n        return prepared\n        \n###################################################################################\n\nclass Image(File):\n    def __init__(self):\n        pass\n\n    def prepare(self):\n        Settings.maybe_print(\"preparingi: {}\".format(self.get_title()))\n        return super()\n\n###################################################################################\n\nclass Video(File):\n    def __init__(self):\n        self.screenshots = []\n        self.trimmed = \"\"\n        self.split = \"\"\n\n    #seconds off front or back\n    def trim(self):\n        path = self.get_path()\n        self.trimmed = ffmpeg.trim(path) \n\n    # into segments (60 sec, 5 min, 10 min)\n    def split(self):\n        path = self.get_path()\n        self.split = ffmpeg.split(path)\n\n    # unnecessary, handled by onlyfans\n    # unless this somehow adds like more metadata\n    def watermark(self):\n        pass\n\n    # cleanup & label appropriately (digital watermarking?)\n    def get_metadata(self):\n        pass\n\n    # frames for preview gallery\n    def get_frames(self):\n        path = self.get_path()\n        self.screenshots = ffmpeg.frames(path)\n\n    def prepare(self):\n        Settings.maybe_print(\"preparingv: {}\".format(self.get_title()))\n        self.reduce()\n        self.repair()\n        self.watermark()\n        return super()\n\n    def reduce(self):\n        if not Settings.is_reduce(): \n            Settings.maybe_print(\"skipping: video reduction\")\n            return\n        path = self.get_path()\n        global FIFTY_MEGABYTES\n        if (int(os.stat(str(path)).st_size) < FIFTY_MEGABYTES or str(Settings.is_reduce()) == \"False\"):\n            return\n        Settings.dev_print(\"reduce: {}\".format(self.get_title()))\n        self.path = ffmpeg.reduce(path)\n    \n    # unnecessary\n    def repair(self):\n        if not Settings.is_repair():\n            Settings.dev_print(\"skipping: video repair\")\n            return\n        path = self.get_path()\n        if Settings.is_repair():\n            return\n        Settings.dev_print(\"repair: {}\".format(self.get_title()))\n        self.path = ffmpeg.repair(path)\n\n\n"
  },
  {
    "path": "notes/old/google-old.py",
    "content": "#!/usr/bin/python3\n\n# hide annoying google warning\nimport logging\nlogging.getLogger('googleapicliet.discovery_cache').setLevel(logging.ERROR)\n\nimport os\nimport shutil\nimport datetime\nimport json\nimport sys\nimport subprocess\nimport pathlib\nimport io\nfrom subprocess import PIPE, Popen\nfrom pydrive.auth import GoogleAuth\nfrom pydrive.drive import GoogleDrive\n# from moviepy.editor import VideoFileClip\nfrom apiclient.discovery import build\nfrom httplib2 import Http\nfrom oauth2client import file, client, tools\nfrom apiclient.http import MediaFileUpload,MediaIoBaseDownload\n##\nfrom ..util.settings import Settings\n\n###################\n##### Globals #####\n###################\n\nAUTH = False\nCACHE = []\nDRIVE = None\nPYDRIVE = None\nONE_GIGABYTE = 1000000000\nONE_MEGABYTE = 1000000\nFIFTY_MEGABYTES = 50000000\nONE_HUNDRED_KILOBYTES = 100000\nOnlyFansFolder_ = None\n\n# Video MimeTypes\n# Flash   .flv    video/x-flv\n# MPEG-4  .mp4    video/mp4\n# iPhone Index    .m3u8   application/x-mpegURL\n# iPhone Segment  .ts     video/MP2T\n# 3GP Mobile  .3gp    video/3gpp\n# QuickTime   .mov    video/quicktime\n# A/V Interleave  .avi    video/x-msvideo\n# Windows Media   .wmv    video/x-ms-wmv\nMIMETYPES_IMAGES = \"(mimeType contains 'image/jpeg' or mimeType contains 'image/jpg' or mimeType contains 'image/png')\"\nMIMETYPES_VIDEOS = \"(mimeType contains 'video/mp4' or mimeType contains 'video/quicktime' or mimeType contains 'video/x-ms-wmv' or mimeType contains 'video/x-flv')\"\nMIMETYPES_ALL = \"(mimeType contains 'image/jpeg' or mimeType contains 'image/jpg' or mimeType contains 'image/png' or mimeType contains 'video/mp4' or mimeType contains 'video/quicktime')\"\nMIMETYPE_FOLDER = \"mimeType contains 'application/vnd.google-apps.folder'\"\n\n################\n##### Auth #####\n################\n\ndef authGoogle():\n    \"\"\"Authorizes Google Drive API\"\"\"\n\n    Settings.dev_print(\"authenticating google\")\n    try:\n        # PyDrive\n        gauth = GoogleAuth()\n        if os.path.exists(\"/opt/onlysnarf/settings.yaml\"):\n            gauth = GoogleAuth(settings_file=\"/opt/onlysnarf/settings.yaml\")\n        # Try to load saved client credentials\n        gauth.LoadCredentialsFile(Settings.get_google_path())\n        Settings.dev_print(\"loaded: google credentials\")\n        if gauth.credentials is None:\n            # Authenticate if they're not there\n            gauth.LocalWebserverAuth()\n        elif gauth.access_token_expired:\n            # Refresh them if expired\n            gauth.Refresh()\n        else:\n            # Initialize the saved creds\n            gauth.Authorize()\n        # Save the current credentials to a file\n        gauth.SaveCredentialsFile(Settings.get_google_path())\n        global PYDRIVE\n        PYDRIVE = GoogleDrive(gauth)\n        # Drive v3 API (alternative downloads)\n        SCOPES = 'https://www.googleapis.com/auth/drive'\n        store = file.Storage(Settings.get_google_path())\n        creds = store.get()\n        if not creds or creds.invalid:\n            flow = client.flow_from_clientsecrets(Settings.get_secret_path(), SCOPES)\n            creds = tools.run_flow(flow, store)\n        global DRIVE\n        DRIVE = build('drive', 'v3', http=creds.authorize(Http()))\n    except Exception as e:\n        Settings.dev_print(e)\n        Settings.print('Error: Unable to Authenticate w/ Google')\n        return False\n    Settings.dev_print(\"authentication successful\") \n    return True\n\ndef checkAuth():\n    \"\"\"Check if Google Drive is authorized, if not then authorize\"\"\"\n\n    global AUTH\n    if not AUTH:\n        AUTH = authGoogle()\n    return AUTH\n\n###########################################\n##### Archiving / Creating / Deleting #####\n###########################################\n\ndef backup_file(file):\n    \"\"\"\n    Backs up file to Google Drive in OnlyFans/posted folder\n\n    Parameters\n    ----------\n    file : Google File\n        Google Drive file to backup\n\n    \"\"\"\n\n    try:\n        global PYDRIVE\n        # get backup folder\n        backupTo = get_folder_by_name(\"posted\")\n        # determine category of folder to backup into\n        stri = \"posted/{}\".format(backupTo[\"title\"])\n        if file.category or Settings.get_category():\n            category = file.category or Settings.get_category()\n            # get the folder to back up to\n            backupTo = get_posted_folder_by_name(category)\n            # if performer, get the proper inner category folder\n            if str(category) == \"performers\" and Settings.get_category_performer():\n                backupTo = get_folder_by_name(Settings.get_category_performer(), parent=backupTo)\n            stri = \"posted/{}\".format(backupTo[\"title\"])\n\n        # check posted for folder to backup with existing name\n        file_list = PYDRIVE.ListFile({'q': \"'{}' in parents and trashed=false and title='{}'\".format(backupTo['id'], str(file.get_parent()[\"title\"]))}).GetList()\n        if len(file_list) == 0:\n            parentFolder = PYDRIVE.CreateFile({'title':str(file.get_parent()[\"title\"]), 'parents':[{\"kind\": \"drive#fileLink\", \"id\": str(backupTo['id'])}], 'mimeType':'application/vnd.google-apps.folder'})\n        else:\n            parentFolder = file_list[0]\n\n        parentFolder.Upload()\n        Settings.dev_print(\"moving to: {}\".format(stri))\n        # change parents of file to \"move\" it\n        file.get_file()['parents'] = [{\"kind\": \"drive#fileLink\", \"id\": str(parentFolder['id'])}]\n        file.get_file().Upload()\n        Settings.print(\"File Backed Up: {}\".format(file.get_title()))\n    except Exception as e:\n        Settings.dev_print(e)\n\ndef create_folders():\n    \"\"\"Create OnlySnarf category folders\"\"\"\n\n    auth = checkAuth()\n    if not auth: return\n    Settings.print(\"Creating Folders: {}\".format(Settings.get_drive_path()))\n    # get root OnlySnarf folder in Drive\n    OnlyFansFolder = get_folder_root()\n    if OnlyFansFolder is None:\n        Settings.err_print(\"unable to create category folders\")\n        return\n    file_list = PYDRIVE.ListFile({'q': \"'{}' in parents and trashed=false\".format(OnlyFansFolder['id'])}).GetList()\n    # create each missing folder\n    for folder in Settings.get_categories():\n        found = False\n        for folder_ in file_list:\n            if str(folder) == folder_['title']:\n                Settings.maybe_print(\"found folder: {}\".format(folder))\n                found = True\n        if not found:\n            if not Settings.is_create_missing():\n                Settings.maybe_print(\"skipping: create missing category folder - {}\".format(folder))\n                continue   \n            Settings.maybe_print(\"created folder: {}\".format(folder))\n            contentFolder = PYDRIVE.CreateFile({\"title\": str(folder), \"parents\": [{\"id\": OnlyFansFolder['id']}], \"mimeType\": \"application/vnd.google-apps.folder\"})\n            contentFolder.Upload()\n\ndef delete_folder(folder):\n    \"\"\"\n    Delete folder\n\n    Parameters\n    ----------\n    folder : Google Folder\n        Google Drive folder to delete\n\n    \"\"\"\n\n    if Settings.is_delete():\n        try:\n            Settings.print(\"Deleting folder: {}\".format(folder[\"title\"]))\n            folder.Trash()\n        except Exception as e: Settings.dev_print(e)\n\n#################\n##### Cache #####\n#################\n\ndef cache_add(folders):\n    \"\"\"Add folder to local cache\"\"\"\n\n    global CACHE\n    for folder in folders:\n        CACHE.append([folder['title'], folder])\n\ndef cache_check(folderName):\n    \"\"\"Check local cache for folder by name\"\"\"\n\n    global CACHE\n    for folder in CACHE:\n        if str(CACHE[0]) == str(folderName):\n            return CACHE[1]\n    return False\n\n####################\n##### Download #####\n####################\n\ndef download_file(file):\n    \"\"\"\n    Download file using one of two methods\n\n    1) PyDrive\n    2) Google Drive - shows download %\n\n    Parameters\n    ----------\n    file : Google File\n        Google file to download\n\n    Returns\n    -------\n    bool\n        Whether or not the download was successful\n\n    \"\"\"\n\n    Settings.maybe_print(\"downloading file: {}\".format(file.get_title()))\n    def method_one():\n        try:\n            with open(str(file.get_path()), 'w+b') as output:\n                #Settings.print(\"8\",end=\"\",flush=True)\n                request = DRIVE.files().get_media(fileId=file.get_id())\n                downloader = MediaIoBaseDownload(output, request)\n                #Settings.print(\"=\",end=\"\",flush=True)\n                done = False\n                while done is False:\n                    #Settings.print(\"=\",end=\"\",flush=True)\n                    status, done = downloader.next_chunk()\n                    if int(Settings.get_verbosity()) >= 1:\n                       Settings.print_same_line(\"Downloading: %d%%\\r\" % (status.progress() * 100))\n                #Settings.print(\"D\")\n                Settings.maybe_print(\"download complete (1)\")\n        except Exception as e:\n            Settings.dev_print(e)\n            return False\n        return True \n    def method_two():\n        try:\n            file.get_file().GetContentFile(file.get_path())\n            Settings.maybe_print(\"download complete (2)\")\n        except Exception as e:\n            Settings.dev_print(e)\n            return False\n        return True\n    successful = method_one() or method_two()\n    return successful\n\n###############\n##### Get #####\n###############\n\ndef get_files_by_folder_id(folderID):\n    \"\"\"Get files of folder by id\n\n    Parameters\n    ----------\n    folderID : str\n        The folder id of the Google Folder to get the files of\n\n    Returns\n    -------\n    list\n        A list of Google Files in the matching folder\n\n    \"\"\"\n\n    if not folderID:\n        Settings.err_print(\"missing folder id\")\n        return\n    auth = checkAuth()\n    if not auth: return []\n    return PYDRIVE.ListFile({'q': \"'{}' in parents and trashed=false\".format(folderID)}).GetList()\n    \ndef get_file(id_):\n    \"\"\"Get file by id\n\n    Parameters\n    ----------\n    id_ : str\n        The id of the Google File to locate\n\n    Returns\n    -------\n    Google File\n        The located Google File\n\n    \"\"\"\n\n    auth = checkAuth()\n    if not auth: return\n    myfile = PYDRIVE.CreateFile({'id': id_})\n    # myfile.FetchMetadata()\n    return myfile\n\ndef get_file_parent(id_):\n    \"\"\"\n    Get parent file of file\n\n    Parameters\n    ----------\n    id_ : str\n        The id of the Google File to locate\n\n    Returns\n    -------\n    Google Folder\n        The located parent Google Folder\n\n    \"\"\"\n    # auth = checkAuth()\n    # if not auth: return\n    # Settings.dev_print(\"getting file parent: {}\".format(id_))\n    parent = get_file(id_)[\"parents\"][0]\n    parent = PYDRIVE.CreateFile({'id': parent[\"id\"]})\n    return parent\n\n\ndef get_folder_by_name(folderName, parent=None):\n    \"\"\"\n    Find folder by name with parent\n    \n    Parameters\n    ----------\n    folderName : str\n        Name of folder to find\n    parent : str\n        Optional parent folder to search within \n\n    Returns\n    -------\n    Google Folder\n        The located / created Google Folder\n\n    \"\"\"\n\n    global CACHE\n    if cache_check(folderName): return cache_check(folderName)\n    auth = checkAuth()\n    if not auth: return\n    if str(folderName) in str(Settings.get_categories()) and not parent:\n        parent = get_folder_root()\n    if not parent: parent = get_folder_root()\n    Settings.maybe_print(\"getting folder: {}\".format(folderName))\n    file_list = PYDRIVE.ListFile({'q': \"'{}' in parents and trashed=false\".format(parent['id'])}).GetList()\n    for folder in file_list:\n        if str(folder['title'])==str(folderName):\n            Settings.maybe_print(\"found folder: {}\".format(folderName))\n            cache_add([folder])\n            return folder\n    if not Settings.is_create_missing():\n        Settings.maybe_print(\"skipping: create missing folder - {}\".format(folderName))\n        return None\n    # create if missing\n    folder = PYDRIVE.CreateFile({\"title\": str(folderName), \"mimeType\": \"application/vnd.google-apps.folder\", \"parents\": [{\"kind\": \"drive#fileLink\", \"id\": parent[\"id\"]}]})\n    folder.Upload()\n    Settings.maybe_print(\"created folder: {}\".format(folderName))\n    return folder\n\ndef get_images_of_folder(folder):\n    \"\"\"\n    Get all image files of folder\n\n    Parameters\n    ----------\n    folder : Google Folder\n        The Google Folder to search for images\n\n    Returns\n    -------\n    list\n        The images located within the folder\n\n    \"\"\"\n\n    image_list = PYDRIVE.ListFile({'q': \"'{}' in parents and trashed=false and {}\".format(folder['id'], MIMETYPES_IMAGES)}).GetList()\n    if len(image_list) > 0:\n        Settings.dev_print(\"images: {}\".format(len(image_list)))\n    else:\n        Settings.maybe_print(\"images folder (empty): {}\".format(folder['title']))\n        if Settings.is_delete_empty(): delete_folder(folder)\n    return image_list\n\ndef get_videos_of_folder(folder):\n    \"\"\"\n    Gets all video files of folder\n\n    Parameters\n    ----------\n    folder : Google Folder\n        The Google Folder to search for videos\n\n    Returns\n    -------\n    list\n        The videos located within the folder\n\n    \"\"\"\n\n    video_list = PYDRIVE.ListFile({'q': \"'{}' in parents and trashed=false and {}\".format(folder['id'], MIMETYPES_VIDEOS)}).GetList()\n    if len(video_list) > 0:\n        Settings.dev_print(\"videos: {}\".format(len(video_list)))\n    else:\n        Settings.maybe_print(\"video folder (empty): {}\".format(folder['title']))\n        if Settings.is_delete_empty(): delete_folder(folder)\n    return video_list\n\n# returns first layer of files found\ndef get_folders_of_folder_by_keywords(folder):\n    \"\"\"\n    Gets all folders in folder by the matching keywords\n\n    Parameters\n    ----------\n    folder : Google Folder\n        The Google Folder to search for keywords within\n\n    Returns\n    -------\n    list\n        The matching keyword folders located within folder\n\n    \"\"\"\n\n    auth = checkAuth()\n    if not auth: return []\n    Settings.maybe_print(\"getting keywords in: {}\".format(folder['title']))\n    foundFolders = []\n    folders = PYDRIVE.ListFile({'q': \"'{}' in parents and trashed=false and {}\".format(folder['id'], MIMETYPE_FOLDER)}).GetList()\n    for folder in folders:\n        if Settings.get_drive_keyword() and str(folder['title']) != str(Settings.get_drive_keyword()):\n            Settings.dev_print(\"{} -> not keyword\".format(folder['title']))\n            continue\n        elif Settings.get_drive_ignore() and str(folder['title']) == str(Settings.get_drive_ignore()):\n            Settings.dev_print(\"{} -> by not keyword\".format(folder['title']))\n            continue\n        elif str(folder['title']) == str(Settings.get_drive_keyword):\n            Settings.dev_print(\"{} -> by keyword\".format(folder['title']))\n        else:\n            Settings.dev_print(\"{}\".format(folder['title']))\n        foundFolders.append(folder)\n    if Settings.get_sort_method() == \"ordered\":\n        foundFolders = sorted(foundFolders, key = lambda x: x[\"title\"])\n    return foundFolders\n\ndef get_posted_folder_by_name(folderName, parent=None):\n    \"\"\"\n    Get folder in \"posted\" folders by name.\n\n    Parameters\n    ----------\n    folderName : str\n        Name of the folder to find\n    parent : Google Folder\n        Optional Google Folder to search in\n\n    Returns\n    -------\n    Google Folder\n        The Google Folder that matches the folderName\n\n    \"\"\"\n\n    global CACHE\n    if cache_check(folderName): return cache_check(folderName)\n    auth = checkAuth()\n    if not auth: return\n    Settings.maybe_print(\"getting posted folder: {}\".format(folderName))\n    if parent is None:\n        parent = get_folder_root()\n    posted = None\n    file_list = PYDRIVE.ListFile({'q': \"'{}' in parents and trashed=false\".format(parent['id'])}).GetList()\n    for folder in file_list:\n        if str(folder['title'])==\"posted\":\n            Settings.maybe_print(\"found folder: posted\")\n            cache_add([folder])\n            posted = folder\n    if posted == None:\n        if not Settings.is_create_missing():\n            Settings.maybe_print(\"skipping: create missing folder - {}\".format(\"posted\"))\n            return None        \n        # create if missing\n        posted = PYDRIVE.CreateFile({\"title\": str(\"posted\"), \"mimeType\": \"application/vnd.google-apps.folder\", \"parents\": [{\"kind\": \"drive#fileLink\", \"id\": parent}]})\n        posted.Upload()\n        Settings.maybe_print(\"created folder: {}\".format(\"posted\"))\n    folder= None\n    file_list = PYDRIVE.ListFile({'q': \"'{}' in parents and trashed=false\".format(posted['id'])}).GetList()\n    for folder_ in file_list:\n        if str(folder_['title'])==str(folderName):\n            Settings.maybe_print(\"found folder: {}\".format(folderName))\n            cache_add([folder_])\n            return folder_\n    if not Settings.is_create_missing():\n        Settings.maybe_print(\"skipping: create missing folder - {}\".format(folderName))\n        return None\n    # create if missing\n    folder = PYDRIVE.CreateFile({\"title\": str(folderName), \"mimeType\": \"application/vnd.google-apps.folder\", \"parents\": [{\"kind\": \"drive#fileLink\", \"id\": posted['id']}]})\n    folder.Upload()\n    Settings.maybe_print(\"created folder: {}\".format(folderName))\n    cache_add([folder])\n    return folder\n\ndef get_folders_of_folder(folder):\n    \"\"\"\n    Get folders of folder.\n\n    Parameters\n    ----------\n    folder : Google Folder\n        The folder to get the folders of\n\n    Returns\n    -------\n    list\n        A list of the folders found\n\n    \"\"\"\n\n    global CACHE\n    if cache_check(folder): return cache_check(folder)\n    auth = checkAuth()\n    if not auth: return []\n    Settings.maybe_print(\"getting folders of: {}\".format(folder['title']))\n    folders = []\n    folder_list = PYDRIVE.ListFile({'q': \"'{}' in parents and trashed=false and {}\".format(folder['id'], MIMETYPE_FOLDER)}).GetList()\n    for folder in folder_list:\n        file_list = PYDRIVE.ListFile({'q': \"'{}' in parents and trashed=false\".format(folder['id'])}).GetList()\n        if len(file_list) > 0:\n            Settings.maybe_print(\"found folder: {}\".format(folder['title']))\n            folders.append(folder)\n        else:\n            Settings.maybe_print(\"found folder (empty): {}\".format(folder['title']))\n            if Settings.is_delete_empty(): delete_folder(folder)\n    cache_add(folders)\n    return folders\n\n# Creates the OnlyFans folder structure\ndef get_folder_root():\n    \"\"\"\n    Gets the OnlySnarf root folder.\n\n    Creates the OnlySnarf folder structure if missing.\n\n    Returns\n    -------\n    Google Folder\n        The root OnlySnarf folder\n\n    \"\"\"\n\n    auth = checkAuth()\n    if not auth: return\n    global OnlyFansFolder_\n    if OnlyFansFolder_ is not None:\n        return OnlyFansFolder_\n    OnlyFansFolder = None\n    if Settings.get_drive_path() != \"\":\n        mount_root = \"root\"\n        root_folders = str(Settings.get_drive_path()).split(\"/\")\n        Settings.maybe_print(\"mount folders: {}\".format(\"/\".join(root_folders)))    \n        for folder in root_folders:\n            mount_root = get_folder_by_name(mount_root, parent=folder)\n            if mount_root is None:\n                mount_root = \"root\"\n                Settings.warn_print(\"drive mount folder not found\")\n                break\n        mount_root = get_folder_by_name(mount_root, parent=Settings.get_drive_root())\n        if mount_root is None:\n            mount_root = {\"id\":\"root\"}\n            Settings.warn_print(\"drive mount folder not found\")\n        else:\n            Settings.maybe_print(\"found root (alt): {}/{}\".format(Settings.get_drive_path(), Settings.get_drive_root()))\n        OnlyFansFolder = mount_root\n    else:\n        file_list = PYDRIVE.ListFile({'q': \"'root' in parents and trashed=false\"}).GetList()\n        for folder in file_list:\n            if str(folder['title']) == str(Settings.get_drive_path()):\n                OnlyFansFolder = folder\n                Settings.maybe_print(\"found root: {}\".format(Settings.get_drive_path()))\n    if OnlyFansFolder is None:\n        Settings.print(\"Creating Root: {}\".format(Settings.get_drive_path()))\n        OnlyFansFolder = PYDRIVE.CreateFile({\"title\": str(Settings.get_drive_path()), \"mimeType\": \"application/vnd.google-apps.folder\"})\n        OnlyFansFolder.Upload()\n    OnlyFansFolder_ = OnlyFansFolder\n    return OnlyFansFolder_\n\n##################\n##### Upload #####\n##################\n\ndef upload_file(file=None):\n    \"\"\"\n    Upload file to Google Drive\n\n    Parameters\n    ----------\n    file : Google File\n        Google file to be uploaded\n\n    Returns\n    -------\n    bool\n        Whether or not the upload was successful\n\n    \"\"\"\n\n    auth = checkAuth()\n    if not auth: return False\n    if not file:\n        Settings.err_print(\"missing file\")\n        return False\n    if Settings.is_debug():\n        Settings.print(\"Skipping Google Upload (debug): {}\".format(filename))\n        return False\n    elif not Settings.is_backup():\n        Settings.print('Skipping Google Upload (disabled): {}'.format(filename))\n        return False\n    else:\n        Settings.print('Google Upload (file): {}'.format(filename))\n    uploadedFile = PYDRIVE.CreateFile({'title':str(file.get_title()), 'parents':[{\"kind\": \"drive#fileLink\", \"id\": str(file.get_parent()[\"id\"])}],'mimeType':str(file.get_mimetype())})\n    uploadedFile.SetContentFile(file.get_path())\n    uploadedFile.Upload()\n    return True\n\ndef upload_gallery(files=[]):\n    \"\"\"\n    Upload files to folder.\n\n    Parameters\n    ----------\n    files : list\n        The list of files to upload\n\n    Returns\n    -------\n    bool\n        Whether or not the upload was successful\n\n    \"\"\"\n\n    parent = get_folder_by_name(\"posted\")\n    if not parent:\n        Settings.err_print(\"missing posted folder\")\n        return False\n    if Settings.is_debug():\n        Settings.print(\"Skipping Google Upload (debug): {}\".format(path))\n        return False\n    elif not Settings.is_backup():\n        Settings.print('Skipping Google Upload (disabled): {}'.format(path))\n        return False\n    else:\n        Settings.print('Google Upload: {}'.format(path))\n    tmp_folder = PYDRIVE.CreateFile({'title':str(datetime.datetime.now()), 'parents':[{\"kind\": \"drive#fileLink\", \"id\": str(parent['id'])}],'mimeType':'application/vnd.google-apps.folder'})\n    tmp_folder.Upload()\n    successful = False\n    for file in files:\n        setattr(file, \"parent\", tmp_folder)\n        successful = upload_file(file=file)\n    return successful\n"
  },
  {
    "path": "notes/old/remote.py",
    "content": "import pysftp, os\nimport PyInquirer\nimport random\n##\nfrom ..util.settings import Settings\n\n\ndef auth():\n\t\n\tglobal HOSTNAME\n\tglobal USERNAME\n\tglobal PASSWORD\n\tglobal PORT\n\n\t# https://pysftp.readthedocs.io/en/release_0.2.9/cookbook.html\n\tHOSTNAME = str(Settings.get_remote_host())\n\tUSERNAME = str(Settings.get_remote_username())\n\tPASSWORD = str(Settings.get_remote_password())\n\tPORT = int(Settings.get_remote_port())\n\tcnopts = pysftp.CnOpts(knownhosts='known_hosts')\n\t# cnopts = pysftp.CnOpts(knownhosts='/home/skeetzo/.ssh/known_hosts')\n\tcnopts.hostkeys = None\n\n\tif HOSTNAME == \"\":\n\t\tprint(\"Error: Missing remote host\")\n\t\treturn False\n\tif USERNAME == \"\":\n\t\tprint(\"Error: Missing remote username\")\n\t\treturn False\n\tif PASSWORD == \"\":\n\t\tprint(\"Error: Missing remote password\")\n\t\treturn False\n\tif PORT == \"\":\n\t\tprint(\"Error: Missing remote port\")\n\t\treturn False\n\treturn True\n\ndef backup_file(file):\n\tprint(\"Backing Up File Remotely\")\n\tif not auth(): return\n\ttry:\n\t\twith pysftp.Connection(host=HOSTNAME, username=USERNAME, password=PASSWORD, cnopts=cnopts, port=PORT) as sftp:\n\t\t\tSettings.maybe_print(\"connection succesfully established ... \")\n\n\t\t\t# Define the file that you want to upload from your local directorty\n\t\t\t# or absolute \"C:\\Users\\sdkca\\Desktop\\TUTORIAL2.txt\"\n\t\t\t# localFilePath = './TUTORIAL2.txt'\n\t\t\tlocalFilePath = file.get_path()\n\n\t\t\t# Define the remote path where the file will be uploaded\n\t\t\t# remoteFilePath = '/var/integraweb-db-backups/TUTORIAL2.txt'\n\t\t\tremoteFilePath = os.path.join(Settings.get_local_path(), \"posted\")\n\t\t\tremoteFilePath = os.path.join(remoteFilePath, file.category, file.title)\n\n\t\t\tsftp.put(localFilePath, remoteFilePath)\n\t\t# connection closed automatically at the end of the with-block\n\texcept Exception as e:\n\t\tSettings.dev_print(e)\n\ndef backup_files(files):\n\tprint(\"Backing Up Files Remotely\")\n\tif not auth(): return\n\ttry:\n\t\twith pysftp.Connection(host=HOSTNAME, username=USERNAME, password=PASSWORD, cnopts=cnopts, port=PORT) as sftp:\n\t\t\tSettings.maybe_print(\"connection succesfully established ... \")\n\n\t\t\tfor file in files:\n\t\t\t\t# Define the file that you want to upload from your local directorty\n\t\t\t\t# or absolute \"C:\\Users\\sdkca\\Desktop\\TUTORIAL2.txt\"\n\t\t\t\t# localFilePath = './TUTORIAL2.txt'\n\t\t\t\tlocalFilePath = file.get_path()\n\n\t\t\t\t# Define the remote path where the file will be uploaded\n\t\t\t\t# remoteFilePath = '/var/integraweb-db-backups/TUTORIAL2.txt'\n\t\t\t\tremoteFilePath = os.path.join(Settings.get_local_path(), \"posted\")\n\t\t\t\tremoteFilePath = os.path.join(remoteFilePath, file.category, file.title)\n\n\t\t\t\tsftp.put(localFilePath, remoteFilePath)\n\t\t# connection closed automatically at the end of the with-block\n\texcept Exception as e:\n\t\tSettings.dev_print(e)\n\ndef upload_file(file):\n\tprint(\"Uploading Remote File\")\n\tif not auth(): return\n\ttry:\n\t\twith pysftp.Connection(host=HOSTNAME, username=USERNAME, password=PASSWORD, cnopts=cnopts, port=PORT) as sftp:\n\t\t\tSettings.maybe_print(\"connection succesfully established ... \")\n\n\t\t\t# Define the file that you want to download from the remote directory\n\t\t\t# remoteFilePath = '/var/integraweb-db-backups/TUTORIAL.txt'\n\t\t\tremoteFilePath = file.get_path()\n\n\t\t\t# Define the local path where the file will be saved\n\t\t\t# or absolute \"C:\\Users\\sdkca\\Desktop\\TUTORIAL.txt\"\n\t\t\t# os.path.join(Settings.get_root_path(), Settings.get_username(), file.category, file.get_title())\n\t\t\tlocalFilePath = file.get_path()\n\n\t\t\tsftp.put(localFilePath, remoteFilePath)\n\t\t# connection closed automatically at the end of the with-block\n\texcept Exception as e:\n\t\tSettings.dev_print(e)\n\ndef upload_files(files):\n\tprint(\"Uploading Remote Files\")\n\tif not auth(): return\n\ttry:\n\t\twith pysftp.Connection(host=HOSTNAME, username=USERNAME, password=PASSWORD, cnopts=cnopts, port=PORT) as sftp:\n\t\t\tSettings.maybe_print(\"connection succesfully established ... \")\n\t\t\tfor file in files:\n\t\t\t\tremoteFilePath = file.get_path()\n\t\t\t\tlocalFilePath = file.get_path()\n\t\t\t\tsftp.put(localFilePath, remoteFilePath)\n\texcept Exception as e:\n\t\tSettings.dev_print(e)\n\ndef delete_file(file):\n\tprint(\"Deleting Remote File\")\n\tif not auth(): return\n\ttry:\n\t\tif not file.remote_path:\n\t\t\tprint(\"Error: File missing remote path\")\n\t\t\treturn\n\t\twith pysftp.Connection(host=HOSTNAME, username=USERNAME, password=PASSWORD, cnopts=cnopts, port=PORT) as sftp:\n\t\t\tSettings.maybe_print(\"connection succesfully established ... \")\n\t\t\tsftp.execute('rm {}'.format(file.remote_path))\n\texcept Exception as e:\n\t\tSettings.dev_print(e)\n\n# download\ndef download_file(file):\n\tprint(\"Downloading Remote File\")\n\tif not auth(): return\n\ttry:\n\t\twith pysftp.Connection(host=HOSTNAME, username=USERNAME, password=PASSWORD, cnopts=cnopts, port=PORT) as sftp:\n\t\t\tSettings.maybe_print(\"connection succesfully established ... \")\n\n\t\t\t# Define the file that you want to download from the remote directory\n\t\t\t# remoteFilePath = '/var/integraweb-db-backups/TUTORIAL.txt'\n\t\t\tremoteFilePath = file.get_path()\n\n\t\t\t# Define the local path where the file will be saved\n\t\t\t# or absolute \"C:\\Users\\sdkca\\Desktop\\TUTORIAL.txt\"\n\t\t\t# os.path.join(Settings.get_root_path(), Settings.get_username(), file.category, file.get_title())\n\t\t\tlocalFilePath = file.get_path()\n\n\t\t\tsftp.get(remoteFilePath, localFilePath)\n\t\t\tfile = File()\n\t\t\tsetattr(file, \"path\", localFilePath)\n\t\t\treturn file\n\t\t# connection closed automatically at the end of the with-block\n\texcept Exception as e:\n\t\tSettings.dev_print(e)\n\ndef prepare_dir(sftp=None):\n\tif not sftp: return\n\tif not Settings.is_create_missing():\n\t\tprint(\"Warning: Not creating missing remote category directories\")\n\t\treturn\n\tSettings.maybe_print(\"creating missing remote folders\")\n\tfor cat in Settings.get_categories():\n\t\tsftp.mkdir(os.path.join(Settings.get_root_path(), Settings.get_username(), cat))\n\ndef get_files(category=None, performer=None):\n\tprint(\"Reading Remote Files\")\n\tif not auth(): return\n\tif Settings.get_remote_host() == \"127.0.0.1\" or Settings.get_remote_host() == \"localhost\":\n\t\tprint(\"Please set a remote host\")\n\t\treturn []\n\ttry:\n\t\twith pysftp.Connection(host=HOSTNAME, username=USERNAME, password=PASSWORD, cnopts=cnopts, port=PORT) as sftp:\n\t\t\tSettings.maybe_print(\"connection succesfully established ... \")\n\n\t\t\t# Switch to a remote directory\n\t\t\tpath = os.path.join(Settings.get_root_path(), Settings.get_username())\n\t\t\tif category:\n\t\t\t\tpath = os.path.join(path, category)\n\t\t\tif performer:\n\t\t\t\tpath = os.path.join(path, performer)\n\t\t\tSettings.dev_print(\"remote file path: {}\".format(path))\n\t\t\ttry:\n\t\t\t\tsftp.cwd(path)\n\t\t\texcept Exception as e:\n\t\t\t\tSettings.dev_print(e)\n\t\t\t\tif \"No such file\" in str(e):\n\t\t\t\t\tprepare_dir(sftp)\n\t\t\t\t\ttry:\n\t\t\t\t\t\tsftp.cwd(path)\n\t\t\t\t\texcept Exception as e:\n\t\t\t\t\t\tSettings.dev_print(e)\n\t\t\t\t\t\treturn []\n\n\t\t\tdirectory_structure = sftp.listdir_attr()\n\n\t\t\tfrom .file import Remote_File\n\t\t\tfile = Remote_File()\n\t\t\tfiles = []\n\t\t\tfor attr in directory_structure:\n\t\t\t\tSettings.dev_print(\"{} {}\".format(attr.filename, attr))\n\t\t\t\tsetattr(file, \"title\", attr.filename)\n\t\t\t\tsetattr(file, \"size\", sftp.stat(os.path.join(path, attr.filename)).st_size)\n\t\t\t\tsetattr(file, \"path\", os.path.join(path, attr.filename))\n\t\t\t\tfiles.append(file)\n\t\t\t\t#Settings.print(os.path.join(path, attr.filename))\n\t\t\t\t#Settings.print(sftp.stat(os.path.join(path, attr.filename)).st_size)\n\t\t\treturn files\n\t\t# connection closed automatically at the end of the with-block\n\texcept Exception as e:\n\t\tSettings.dev_print(e)\n\t\treturn []\n\ndef get_random_file(category=None, performer=None):\n\tif not category: category = Settings.get_category()\n\tif not category:\n\t\tprint(\"Error: Missing category\")\n\t\treturn None\n\treturn random.choice(get_files(category=category, performer=performer))\n\ndef select_file(category, performer=None):\n\tfiles = get_files(category=category, performer=performer)\n\tfiles_ = []\n\tfor file in files:\n\t\tif isinstance(file, str):\n\t\t\tfiles_.append(PyInquirer.Separator())\n\t\t\tcontinue\n\t\tfile.category = category\n\t\tfile_ = {\n\t\t\t\"name\": file.get_title(),\n\t\t\t\"value\": file,\n\t\t}\n\t\tfiles_.append(file_)\n\tif len(files_) == 0:\n\t\tprint(\"Warning: Missing Files\")\n\t\treturn select_files()\n\tquestion = {\n\t\t'type': 'list',\n\t\t'name': 'file',\n\t\t'message': 'File:',\n\t\t'choices': files_\n\t}\n\tanswer = PyInquirer.prompt(question)\n\tfile = answer[\"file\"]\n\tif not Settings.confirm(file.get_title()): return None\n\treturn file\n\ndef select_files():\n\tif not Settings.is_prompt(): return [get_random_file()]\n\tfrom .file import File, Remote_File\n\tcategory = Settings.select_category()\n\tif not category: return File.select_file_upload_method()\n\tprint(\"Select Remote File\")\n\tfiles = []\n\twhile True:\n\t\tfile = select_file(category)\n\t\tif not file: break\n\t\t##\n\t\tif \"performer\" in str(category):\n\t\t\tcat = Settings.select_category([cat for cat in Settings.get_categories() if \"performer\" not in cat])\n\t\t\tperformerName = file.get_title()\n\t\t\tfile = select_file(cat, performer=performerName)\n\t\t\tif not file: break\n\t\t\tsetattr(file, \"performer\", performerName)\n\t\t\tfiles.append(file)\n\t\t\tif \"galler\" in str(cat) or \"video\" in str(cat): break\n\t\t##\n\t\tif isinstance(file, Remote_File): files.append(file)\n\t\tif not Settings.prompt(\"another file\"): break\n\tif not Settings.confirm([file.get_path() for file in files]): return []\n\treturn files\n\n\n\n\n\n# sftp = pysftp.Connection('hostname', username='me', password='secret')\n# #\n# # ... do sftp operations\n# #\n# sftp.close()    # close your connection to hostname\n\n\n# with pysftp.Connection('hostname', username='me', password='secret') as sftp:\n#     #\n#     # ... do sftp operations\n#     #\n# # connection closed automatically at the end of the with-block\n"
  },
  {
    "path": "notes/old/removed-args.md",
    "content": "# Removed / Old\n\nThese args / flags have been deleted or have had their behavior's simplified elsewhere:\n\n-action [discount|message|post|promotion]\nThe action to take.\n\n-cron\nFlags the script as a cron operation.\n\n-cron-user\nThe user to run cron script as.\n\n-create-drive\nEnables creating missing folders in Google Drive.\n\n-category image  \nUploads a random image labeled: 'fileName'\n-category gallery  \nUploads a random gallery labeled: 'folderName'\n-category video  \nUploads a random video labeled: 'fileName'\n\n-categories\nThe categories / folders to include in searches.\n\n-delete-google\nEnable deleting Google files after upload (after backup if enabled).\n\n-delete-empty\nDelete empty folders when searching for content.\n\n-drive-path\nThe Google Drive path to the main OnlySnarf content folder.\n\n-keywords\nThe folder by keyword to download & upload.\n\n-mount-path\nThe location of local data.\n\n-bykeyword\nThe keyword to search for content by location.\n\n-notbykeyword\nThe keyword to ignore when searching for content by location.\n\n-performers\nThe performers to specificy for upload.\n\n-profile-backup\nEnabled profile backup w/ 'profile' action.\n\n-profile-syncto\nEnabled profile sync to w/ 'profile' action.\n\n-profile-syncfrom\nEnabled profile sync from w/ 'profile' action.\n\n-google-creds\nThe path to the google_credentials.txt file.\n\n-client-secret\nThe path to the client_secrets.json file.\n\n-users-path\nThe path to the users file.\n\n-profile-path\nThe path to the profile file.\n\n-promotion-expiration\nThe expiration to use for a promotion.\n\n-promotion-limit\nThe max number of subscribers for a promotion.\n\n-promotion-trial\n\n-promotion-user\nEnables user method.\n\n-password_google\nThe password to login for Google with.\n\n-remote-host\nThe remote host to connect to for a reconnect.\n-remote-port\nThe remote port to use for connecting to a reconnect.\n-remote-username\nThe remote host username for accessing remote content.\n-remote-password\nThe remote host password for accessing remote content.\n-remote-host\nThe remote host for accessing remote content.\n-remote-port\nThe remote port for accessing remote content.\n\n-session-id\nThe session id to use for reconnecting.\n-session-url\nThe session url to use for reconnecting.\n\n-drive-root\nThe Google Drive root folder path.\n\n-users-favorite\nThe favorite users by username to specify for favorite operations.\n\n-title\nSpecific title of file to search for remotely.\n\n\n\n\n\n\n\n\n# Removed from Conf file\n\nnotkeyword = \nbykeyword = \nkeywords = \nroot_path = \nprofile_method = \ntitle = \ndelete_empty = False\npassword_google = \n"
  },
  {
    "path": "notes/old/removed-cron.py",
    "content": "from crontab import CronTab\nfrom .settings import Settings\n\n################\n##### Cron #####\n################\n\ndef delete(comment):\n    cron = CronTab(user=str(Settings.get_cron_user()))\n    cron.remove_all(comment=comment)\n    cron.write()\n    print(\"Cron Deleted: {}\".format(comment))\n\ndef deleteAll():\n    cron = CronTab(user=str(Settings.get_cron_user()))\n    cron.remove_all()\n    cron.write()\n    print(\"Crons Deleted\")\n\n# use cron.comment to set cron name and find crons\ndef disable(comment):\n    # find cron by comment\n    cron = find(comment)\n    cron.enable(False)\n    cron.write()\n    print(\"Cron Disabled: {}\".format(comment))\n\ndef enable(comment):\n    # find cron by comment\n    cron = find(comment)\n    cron.enable()\n    cron.write()\n    print(\"Cron Enabled: {}\".format(comment))\n\ndef create(comment, args=[], minute=None, hour=None):\n    print(\"Creating Cron: {}\".format(comment))\n    # if find(comment) is not None:\n        # print(\"Warning: Cron Exists\")\n    cron = CronTab(user=str(Settings.get_cron_user()))\n    cron.remove_all(comment=comment)\n    args = [n.strip() for n in args]\n    newCron = cron.new(command=\"/usr/local/bin/onlysnarfpy -cron {} {} >> /var/log/onlysnarf.log 2>&1\".format(comment, \" \".join(args)), comment=comment);\n    newCron.hour.every(1)\n    if minute is not None:\n        newCron.minute.on(minute)\n    if hour is not None:\n        newCron.hour.on(hour)\n    cron.write()\n    print(\"Created Cron: {}\".format(comment))\n\ndef list():\n    cron = CronTab(user=str(Settings.get_cron_user()))\n    print(\"Crons:\")\n    for job in cron:\n        print(job)\n\ndef find(comment):\n    cron = CronTab(user=str(Settings.get_cron_user()))\n    iter1 = cron.find_comment(str(comment))\n    for item in iter1:\n        return item\n    return False \n\ndef getAll():\n    cron = CronTab(user=str(Settings.get_cron_user()))\n    jobs = []\n    for job in cron:\n        jobs.append(job)\n    return jobs\n\n###################\n##### Special #####\n###################\n\ndef discount(user, amount=None, months=None):\n    args = [\"-action\",\"discount\",\"-user\",str(user)]\n    if amount: \n        args.append([\"-amount\"])\n        args.append([amount])\n    if months:\n        args.append([\"-months\"])\n        args.append([months])\n    create(\"discount-\"+user, args)\n\ndef message(user, text=None, image=None, price=None):\n    args = [\"-action\",\"message\",\"-user\",str(user)]\n    if text: \n        args.append([\"-text\"])\n        args.append([\"\\\"{}\\\"\".format(text)])\n    if image:\n        args.append([\"-image\"])\n        args.append([\"\\\"{}\\\"\".format(image)])\n    if price:\n        args.append([\"-price\"])\n        args.append([\"\\\"{}\\\"\".format(price)])\n    create(\"message-\"+user, args)\n\ndef post(text):\n    args = [\"-action\",\"post\",\"-text\",\"\\\"{}\\\"\".format(text)]\n    create(\"post-\"+user, args)\n\ndef upload(opt):\n    args = [\"-type\",str(opt)]\n    create(\"upload\", args)\n\ndef uploadInput(type_, path):\n    args = [\"-type\",str(type_),\"-method\",\"input\",\"-input\",\"\\\"{}\\\"\".format(path)]\n    create(\"upload-input\"+str(path), args)\n\n# check all scenes folders' data.txt's for \"releaseDate\"\ndef checkScenes():\n    pass\n\n# sends messages to users that were queued for a specific time\ndef sendQueuedMessages():\n    pass\n\n###############\n##### Dev #####\n###############\n\ndef test():\n    create(\"upload-video\", args=[\"-type\",\"video\"])\n    # create(\"upload-balls\", [], \"30\", \"11\")\n    # create(\"check-scenes\")\n    # disable(\"upload-video\")\n    # enable(\"upload-video\")\n    return True\n\n\n# Cron:\n\n# Message:\n# - All, etc\n\n# Upload:\n# - Gallery\n# - Image\n# - Video\n# - Performer\n# - Scene\n\n\n\n\n\n\n\n# ###\n# ### Cron\n# ###\n\n# cronItems = sorted([\n#     [ \"Add\", \"add\" ],\n#     [ \"List\", \"list\" ],\n#     [ \"Delete\", \"delete\" ],\n#     [ \"Delete All\", \"deleteall\" ]\n# ])\n# cronItems.insert(0,[ \"Back\", \"main\"])\n\n\n\n\n\n# def finalizeCron(actionChoice):\n#     for item in cronItems:\n#         print(colorize(\"[\" + str(cronItems.index(item)) + \"] \", 'teal') + list(item)[0])\n#     while True:\n#         cronChoice = input(\">> \")\n#         try:\n#             if int(cronChoice) < 0 or int(cronChoice) >= len(cronItems): raise ValueError\n#             if str(cronItems[int(cronChoice)][1]) == \"main\":\n#                 return action()\n#             cronChoice = list(cronItems[int(cronChoice)])[1]\n#             return performCron(actionChoice, cronChoice)\n#         except (ValueError, IndexError):\n#             print(\"Error: Incorrect Index\")\n#         except Exception as e:\n#             settings.maybePrint(e)\n#             print(\"Error: Missing Method\") \n\n# def performCron(actionChoice, cronChoice):\n#     if str(cronChoice) == \"add\":\n#         print(\"Comment:\")\n#         comment = input(\">> \")\n#         print(\"Args:\")\n#         args = input(\">> \")\n#         args = args.split(\",\")\n#         print(\"Minute:\")\n#         minute = input(\">> \")\n#         print(\"Hours:\")\n#         hour = input(\">> \")\n#         Cron.create(comment, args=args, minute=minute, hour=hour)\n#     elif str(cronChoice) == \"list\":\n#         Cron.list()\n#     elif str(cronChoice) == \"delete\":\n#         jobs = Cron.getAll()\n#         print(colorize(\"[0] \", 'teal') + \"Back\")\n#         jobs_ = []\n#         for job in jobs:\n#             jobs_.append(str(job.comment))\n#             print(colorize(\"[\" + str(jobs.index(job)+1) + \"] \", 'teal') + str(job))\n#         while True:\n#             choice = input(\">> \")\n#             try:\n#                 choice = int(choice)\n#                 if int(choice) < 0 or int(choice) > len(jobs): raise ValueError\n#                 if int(choice) == 0: return finalizeCron(actionChoice)\n#                 Cron.delete(jobs_[int(choice)-1])\n#                 return mainMenu()\n#             except (ValueError, IndexError):\n#                 print(sys.exc_info()[0])\n#                 print(\"Error: Incorrect Index\")\n        \n#     elif str(cronChoice) == \"deleteall\":\n#         Cron.deleteAll()\n#     else:\n#         print(\"Error: Missing Cron Action\")\n#     mainMenu()    \n\n\n\n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "notes/old/removed-readme.md",
    "content": "## Previews\n![preview](https://github.com/skeetzo/onlysnarf/blob/master/images/preview.jpeg)\n[Gallery](https://github.com/skeetzo/onlysnarf/blob/master/images/gallery.gif)\n[Video](https://github.com/skeetzo/onlysnarf/blob/master/images/video.gif)\n[Discount](https://github.com/skeetzo/onlysnarf/blob/master/images/discount-recent.gif)\n[Message](https://github.com/skeetzo/onlysnarf/blob/master/images/message-recent-debug.gif)\n\n\n## Scripts\nFirst run:  \n  * `onlysnarf-config`\nThen from within project's OnlySnarf directory either:  \n  * `onlysnarf [args]`\n  * `onlysnarfpy (-debug) -category image|gallery|video`\n  * or directly via `python3 onlysnarf.py (-debug) -category image|gallery|video`\n\n##### config.conf  \nPath: /opt/onlysnarf/config.conf (previously /etc/onlysnarf/config.conf)\nCreate or update the \"config.conf\" file with the following values:\n  * username -> the Twitter connected to your OnlyFans's username  \n  * password -> the Twitter conencted to your OnlyFans's password  \n\n## args\n\n-debug  \n  `python3 onlysnarf.py -debug`  \nTests configuration. Does not upload or remove from file source.\n\n-category image  \n  `python3 onlysnarf.py -category image`  \nUploads an image labeled: 'imageName - %d%m%y'  \n\n-category gallery  \n  `python3 onlysnarf.py -category gallery`  \nUploads a gallery labeled: 'folderName - %d%m%y'  \n\n-category video  \n  `python3 onlysnarf.py -category video`  \nUploads a video labeled: 'folderName - %d%m%y'  \n\n-text  \n  `python3 onlysnarf.py -category video -text \"your mom\"`  \nUploads a video labeled: 'your mom'  \n\n-show\n  `python3 onlysnarf.py -show`\nShows the browser\n\n**more available in menu**\n\nOr include a 'config.conf' file located at '/opt/onlysnarf/config.conf' to set variables at runtime without using arguments. An example file has been provided. Please be sure to follow the key:value pattern. A starting # denotes a comment.\n\n\n## Google Authentication  \n--------------\nWhen downloading/uploading from a Google Drive account this package requires configuring a Google App with *PyDrive* for access to your Google Drive. The Drive API requires OAuth2.0 for authentication.\n###### from [Auth Quickstart](https://raw.githubusercontent.com/gsuitedevs/PyDrive/master/docs/quickstart.rst)\n1. Go to `APIs Console`_ and make your own project.\n2. Search for 'Google Drive API', select the entry, and click 'Enable'.\n3. Select 'Credentials' from the left menu, click 'Create Credentials', select 'OAuth client ID'.\n4. Now, the product name and consent screen need to be set -> click 'Configure consent screen' and follow the instructions. Once finished:\n\n a. Select 'Application type' to be *Web application*.\n b. Enter an appropriate name.\n c. Input *http://localhost:8080* for 'Authorized JavaScript origins'.\n d. Input *http://localhost:8080/* for 'Authorized redirect URIs'.\n e. Click 'Create'.\n\n5. Click 'Download JSON' on the right side of Client ID to download **client_secret_<really long ID>.json**.\n\n**Rename the file to \"client_secrets.json\" and place it into your installed OnlySnarf directory.**\nTo update your installation with the new file, run `onlysnarf-config`, select 'Update Google Creds', and enter the location of your \"client_secret.json\" file.\n\n\n##### google_creds.txt   \nGenerated by Google Drive's authentication process. Saves Google authentication for repeat access.\n\n##### settings.yaml  \nUsed to facilitate Google Drive's python authentication. Requires generating an app w/ credentials via Google Console. Credentials are authenticated once and then saved to \"google_creds.txt\".\n\n## Example Crons  \n\nUpload a random image once a day at noon:  \n  `* 12 * * * onlysnarfpy -category image`\n\nUpload a random gallery of images every Wednesday at 2:30pm:  \n  `30 14 * * 3 onlysnarfpy -category gallery`\n\nUpload a random video every Friday in the month of June at 6:00pm:  \n  `00 18 * 6 5 onlysnarfpy -category video`\n\nText will be generated if not provided with `-text`\n  `* 12 * * * onlysnarfpy -category image -text \"Your mother is a dirty whore\"`\n\n"
  },
  {
    "path": "notes/old/removed-selenium-options.py",
    "content": "\n# Chrome\noptions.add_argument(\"--disable-setuid-sandbox\")\noptions.add_argument(\"--disable-dev-shm-usage\") # overcome limited resource problems\noptions.add_argument(\"--disable-gpu\") # applicable to windows os only\noptions.add_argument('--disable-smooth-scrolling')\noptions.add_argument(\"--start-maximized\")\noptions.add_argument(\"--window-size=1920,1080\")\noptions.add_argument(\"--user-data-dir=/tmp/\")\noptions.add_argument('--disable-login-animations')\noptions.add_argument('--disable-modal-animations')\noptions.add_argument('--disable-sync')\noptions.add_argument('--disable-background-networking')\noptions.add_argument('--disable-web-resources')\noptions.add_argument('--disable-logging')\noptions.add_argument('--no-experiments')\noptions.add_argument('--incognito')\noptions.add_argument('--user-agent=MozillaYerMomFox')\noptions.add_argument(\"--acceptInsecureCerts\")\noptions.add_experimental_option(\"prefs\", {\n    \"download.default_directory\": str(DOWNLOAD_PATH),\n    \"download.prompt_for_download\": False,\n    \"download.directory_upgrade\": True,\n    \"safebrowsing.enabled\": True\n})\n\n"
  },
  {
    "path": "notes/old/removed_args.py",
    "content": "###################################################################\n##                          DEPRECATED                           ##\n###################################################################\n# these have all been moved from optionalargs to config file only #\n###################################################################\n\n##\n# removed 10/4/2021\n# mostly temporary\n##\n\n##\n# -password\n# the password for OnlyFans\nparser.add_argument('-password', type=str, dest='password',\n  help='the OnlyFans user password for login (used with username)')\n##\n# -password\n# the password for Google\nparser.add_argument('-password-google', type=str, dest='google_password',\n  help='the Google password for login')\n##\n# -password\n# the password for Twitter\nparser.add_argument('-password-twitter', type=str, dest='twitter_password',\n  help='the Twitter password for login')\n\n##\n# -profile-path\n# the path to the profile.json file\nparser.add_argument('-profile-path', type=str, dest='path_profile',\n  help='the path to cache profile locally', default=DEFAULT.PROFILE_PATH)\n\n##\n# -promotion-expiration\n# expiration for a promotion\nparser.add_argument('-promotion-expiration', type=valid_promo_expiration, dest='promotion_expiration',\n  help='the promotions expiration in days)', choices=DEFAULT.PROMOTION_EXPIRATION_ALLOWED, default=None)\n##\n# -promotion-limit\n# maximum number of subscribers for a promotion\nparser.add_argument('-promotion-limit', type=valid_limit, default=None, dest='promotion_limit', choices=DEFAULT.LIMIT_ALLOWED,\n  help='the max number of subscribers allowed for a promotion')\n\n##\n# -username-google\n# the Google username to use\nparser.add_argument('-username-google', type=str, default=\"\", dest='google_username',\n  help='the Google username for login')\n##\n# -username-twitter\n# the Twitter username to use\nparser.add_argument('-username-twitter', type=str, default=\"\", dest='twitter_username',\n  help='the Twitter username for login')\n\n############\n## Remote ##\n############\n\n##\n# -remote-path\n# root remote folder. can be set in profile.conf\nparser.add_argument('-remote-path', type=str, default=DEFAULT.REMOTE_PATH, dest='remote_path',\n  help='the root remote file sharing folder name')\n\n##\n# -remote-host\n# the remote host to connect to\nparser.add_argument('-remote-host', type=str, dest='remote_host',\n  help='the remote host to connect to for file sharing', default=\"127.0.0.1\")\n\n##\n# -remote-host\n# the remote host to connect to\nparser.add_argument('-remote-browser-host', type=str, dest='remote_browser_host',\n  help='the remote host to connect to for remote browser', default=\"127.0.0.1\")\n\n##\n# -remote-port\n# the remote port to connect to\nparser.add_argument('-remote-port', type=int, dest='remote_port',\n  help='the remote port to connect to for file sharing', default=22)\n\n##\n# -remote-browser-port\n# the remote port to connect to\nparser.add_argument('-remote-browser-port', type=int, dest='remote_browser_port',\n  help='the remote port to connect to for remote browser', default=4444)\n\n##\n# -remote-username\n# the remote username to use\nparser.add_argument('-remote-username', type=str, dest='remote_username',\n  help='the remote username to use', default=None)\n\n##\n# -remote-password\n# the remote password to use\nparser.add_argument('-remote-password', type=int, dest='remote_password',\n  help='the remote password to use', default=None)\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n##\n# removed ?/?/2021\n##\n\n##\n# configurable w/ profile.conf\n# OnlySnarf Drive folder list, appends to defaults\nparser.add_argument('-categories', dest='categories',\n  action='append', help='the categories to list in menu (appends to \\'{}\\''.format(\"\\'\".join(CATEGORIES_DEFAULT)), \n  default=[])\n##\n# -create-missing \n# creates missing OnlySnarf folders\nparser.add_argument('-create-missing', action='store_true', dest='create_missing',\n  help='creates missing OnlySnarf folders at target source')\n\n##\n# -cron\n# determines whether script running is a cronjob\nparser.add_argument('-cron', action='store_true', help='toggle cron behavior', dest='cron')\n##\n# -cron-user\n# the user to run OnlySnarf as\nparser.add_argument('-cron-user', type=str, dest='cron_user',\n  help='the user to run OnlySnarf as', default='root')\n\n##\n# -delete-empty\n# delete empty content folders\nparser.add_argument('-delete-empty', action='store_true', dest='delete_empty',\n  help='delete empty content folders')\n\n##\n# download path\nparser.add_argument('-download-path', type=str, dest='download_path',\n  help='the path to download files to locally', default=DOWNLOAD_PATH)\n\n# Combined / Deleted into new args\n\n##\n# -delete-google\n# delete uploaded content instaed of backing it up\nparser.add_argument('-delete-google', action='store_true', dest='delete_google',\n  help='delete file instead of backing up')\n\n\n##\n# removed 9/9/2022\n##\n\n\n\n##\n# -duration-promo\n# promotion duration\nparser.add_argument('-duration-promo', type=valid_promo_duration, dest='duration_promo',\n  help='the duration in days (99 for \\'No Limit\\') for a promotion', choices=DEFAULT.PROMOTION_DURATION_ALLOWED, default=None)\n\n##\n# -email\n# the OnlyFans email to use for login\nparser.add_argument('-email', type=str, default=\"\", dest='email',\n  help='the email for an OnlyFans profile')\n\n##\n# -keywords\n# keywords to # in post\nparser.add_argument('-keywords', dest='keywords', action='append', default=[], \n  help=\"the keywords (#[keyword])\")\n\n##\n# -bykeyword\n# the keyword to search for in folder selection\nparser.add_argument('-bykeyword', dest='bykeyword', default=None, \n  help=\"search for folder by keyword\")\n##\n# -notkeyword\n# the keyword to skip in folder selection\nparser.add_argument('-notkeyword', dest='notkeyword', default=None,\n  help=\"search for folder not by keyword\")\n\n##\n# -repair\n# enables file repair (buggy)\nparser.add_argument('-repair', action='store_true', dest='repair',\n  help='enable repairing videos as appropriate (buggy)')\n\n##\n# -recent-users-count\n# the maximum number of recent users\nparser.add_argument('-recent-users-count', default=3, dest='recent_users_count',\n  type=int, help='the number of users to consider recent')\n\n##\n# -title\n# the title of a file to search for\nparser.add_argument('-title', default=None, dest='title',\n  help='the title of the file to search for')\n\n##\n# -session-id\nparser.add_argument('-session-id', default=None, dest='session_id',\n  help='the session id to use')\n\n# -session-url\nparser.add_argument('-session-url', default=None, dest='session_url',\n  help='the session url to use')\n\n##\n# -thumbnail\n# attempt to fix thumbnail\nparser.add_argument('-thumbnail', action='store_true', dest='thumbnail',\n  help='fix thumbnails when necessary')\n\n##\n# -users-read\n# the number of users read when checking messages\nparser.add_argument('-users-read', type=int, dest='users_read',\n  help='the number of users to read when checking messages', default=10)\n\n## \n# -profile-method\nparser.add_argument('-profile-method', dest=\"profile_method\", default=\"syncfrom\", choices=[\"syncto\",\"syncfrom\"],\n  help='the profile method to use')\n\n##\n# -promotion\n# the promotion method to use\nparser.add_argument('-promotion-method', dest='promotion_method', default=\"campaign\", choices=[\"campaign\",\"trial\",\"grandfather\",\"user\"],\n  help='the promotion method to use')\n\n##\n# -promotion-user\nparser.add_argument('-promotion-user', dest=\"promotion_user\", action='store_true', \n  help=\"uses user method when combined with action=promotion\")\n\n##\n# -root-path\n# the root path for a local directory of OnlyFans config files\nparser.add_argument('-root-path', dest='root_path',\n  help='the local path to OnlySnarf processes')"
  },
  {
    "path": "notes/old/selectstuff.py",
    "content": "if str(username) == \"all\":\n    return User.get_all_users()\nelif str(username) == \"recent\":\n    return User.get_recent_users()\nelif str(username) == \"favorite\":\n    return User.get_favorite_users()\nelif str(username) == \"random\":\n    return User.get_random_user()\n\n\n\n\n@staticmethod\ndef select_user():\n    user = Settings.get_user() or None\n    if user: return user\n    # if user: return User.get_user_by_username(user.username)\n    # if not Settings.prompt(\"user\"): return User.get_random_user()\n    choices = Settings.get_message_choices()\n    choices.append(\"enter username\")\n    choices.append(\"select username\")\n    choices = [str(choice).title() for choice in choices]\n    question = {\n        'type': 'list',\n        'name': 'user',\n        'message': 'User:',\n        'choices': choices,\n        'filter': lambda val: str(val).lower()\n    }\n    answers = PyInquirer.prompt(question)\n    user = answers[\"user\"]\n    if str(user) == \"enter username\":\n        username = input(\"Username: \")\n        return User.get_user_by_username(username)\n    elif str(user) == \"select username\":\n        return User.select_username()\n    elif str(user) == \"favorites\":\n        return User.get_favorite_users()\n    # elif str(user) == \"list\":\n        # return User.list_menu()\n    elif str(user) == \"all\":\n        return User.get_all_users()\n    if not Settings.confirm(user): return User.select_user()\n    return user\n\n@staticmethod\ndef select_users():\n    # if not Settings.prompt(\"users\"): return []\n    users = []\n    while True:\n        user = User.select_user()\n        if not user: break\n        if str(user).lower() == \"all\" or str(user).lower() == \"recent\": return [user]\n        users.append(user)\n        if not Settings.prompt(\"another user\"): break\n    if not Settings.confirm([user.username for user in users]): return User.select_users()\n    return users\n\n@staticmethod\ndef select_username():\n    # returns the list of usernames to select\n    # if not Settings.prompt(\"select username\"): return None\n    users = User.get_all_users()\n    users_ = []\n    for user in users:\n        user_ = {\n            \"name\":user.username.replace(\"@\",\"\"),\n            \"value\":user,\n            \"short\":user.id\n        }\n        users_.append(user_)\n    question = {\n        'type': 'list',\n        'name': 'user',\n        'message': 'Username:',\n        'choices': users_\n    }\n    user = PyInquirer.prompt(question)['user']\n    if not Settings.confirm(user.username): return User.select_username()\n    return user\n\n\n\n\n\n@staticmethod\ndef list_menu():\n    question = {\n        'type': 'list',\n        'name': 'answer',\n        'message': 'User:',\n        'choices': [\"Back\", \"Enter\", \"Select\"]\n    }\n    answer = PyInquirer.prompt(question)[\"answer\"]\n    if str(answer) == \"Back\":\n        Settings.print(0)\n        return User.select_user()\n    elif str(answer) == \"Enter\":\n        question = {\n            'type': 'input',\n            'message': 'Enter List (name or #):',\n            'name': 'list'\n        }\n        list_ = PyInquirer.prompt(question)[\"list\"]\n        theList = None\n        try:\n            theList = int(list_)\n            users = User.get_users_by_list(number=theList)\n        except Exception as e:\n            try:\n                theList = str(list_)\n                return User.get_users_by_list(name=theList)\n            except Exception as e:\n                Settings.err_print(\"unable to find list number\")\n    elif str(answer) == \"Select\":\n        lists_ = Driver.get_driver().get_lists()\n        lists__ = [{\"name\":\"Back\", \"value\":\"back\"}]\n        for list___ in lists_:\n            lists__.append({\n                \"name\":list___[1],\n                \"value\":list___[0],\n            })\n        question = {\n            'type': 'list',\n            'name': 'answer',\n            'message': 'Lists:',\n            'choices': lists_\n        }\n        answer = PyInquirer.prompt(question)[\"answer\"]\n        if str(answer) == \"back\":\n            return User.select_user()\n        else:\n            return Driver.get_driver().get_list_members(answer)\n    return []\n\n\n\n\n\n\n\n\n\n# from selecting / confirming the tags/performers in messages.py\n        # skip prompt\n        if not Settings.prompt(variable): return []\n        question = {\n            'type': 'input',\n            'name': 'keywords',\n            'message': '{}:'.format(variable.camelCase()),\n            'validate': ListValidator\n        }\n        if again: Settings.print(\"are you sure you've done this before, {}? ;)\".format(Settings.get_username()))\n        variables = prompt(question)[variable]\n        variables = [n.strip() for n in variables.split(\",\")]\n        # confirm variables or go in a circle\n        # if not Settings.confirm(variables): return self.get_tags(performers=performers, again=True)\n        dict(self)[variable] = variables\n        return variables"
  },
  {
    "path": "notes/onlysnarf_api.service",
    "content": "[Unit]\nDescription=OnlySnarf API\nAfter=network.target\n\n[Service]\nWorkingDirectory=/home/snarf/\nExecStart=/home/snarf/update-and-start.sh\nRestart=always\nUser=snarf\n\n[Install]\nWantedBy=multi-user.target"
  },
  {
    "path": "notes/scroll-notes.py",
    "content": "SCROLL_PAUSE_TIME = 0.5\n\n# Get scroll height\nlast_height = driver.execute_script(\"return document.body.scrollHeight\")\n\nwhile True:\n    # Scroll down to bottom\n    driver.execute_script(\"window.scrollTo(0, document.body.scrollHeight);\")\n\n    # Wait to load page\n    time.sleep(SCROLL_PAUSE_TIME)\n\n    # Calculate new scroll height and compare with last scroll height\n    new_height = driver.execute_script(\"return document.body.scrollHeight\")\n    if new_height == last_height:\n        break\n    last_height = new_height"
  },
  {
    "path": "notes/selenium-notes.py",
    "content": "def run_browserstack(self,os_name,os_version,browser,browser_version,remote_project_name,remote_build_name):\n    \"Run the test in browser stack when remote flag is 'Y'\"\n    #Get the browser stack credentials from browser stack credentials file\n    USERNAME = remote_credentials.USERNAME\n    PASSWORD = remote_credentials.ACCESS_KEY\n    if browser.lower() == 'ff' or browser.lower() == 'firefox':\n        desired_capabilities = DesiredCapabilities.FIREFOX            \n    elif browser.lower() == 'ie':\n        desired_capabilities = DesiredCapabilities.INTERNETEXPLORER\n    elif browser.lower() == 'chrome':\n        desired_capabilities = DesiredCapabilities.CHROME            \n    elif browser.lower() == 'opera':\n        desired_capabilities = DesiredCapabilities.OPERA        \n    elif browser.lower() == 'safari':\n        desired_capabilities = DesiredCapabilities.SAFARI\n    desired_capabilities['os'] = os_name\n    desired_capabilities['os_version'] = os_version\n    desired_capabilities['browser_version'] = browser_version\n    if remote_project_name is not None:\n        desired_capabilities['project'] = remote_project_name\n    if remote_build_name is not None:\n        desired_capabilities['build'] = remote_build_name+\"_\"+str(datetime.now().strftime(\"%c\"))\n\n    return webdriver.Remote(RemoteConnection(\"http://%s:%s@hub-cloud.browserstack.com/wd/hub\"%(USERNAME,PASSWORD),resolve_ip= False),\n        desired_capabilities=desired_capabilities) \n\n\n\n\n\ndef remote(self) -> None:\n    if not self.debug:\n        log(\"debug mode is turned off, can't reuse old session\")\n        return\n\n    file = open(self.current_session_path, \"r\")\n    content = file.read()\n    lines = content.split(\";\")\n    url = lines[0]\n    session = lines[1]\n\n    self.driver = webdriver.Remote(\n        command_executor=url, desired_capabilities=DesiredCapabilities.CHROME\n    )\n    self.driver.session_id = session\n\n    self.set_config() \n\n\n\n\ndef attach_to_session(executor_url, session_id):\n    original_execute = WebDriver.execute\n\n    def new_command_execute(self, command, params=None):\n        if command == \"newSession\":\n            # Mock the response\n            return {'success': 0, 'value': None, 'sessionId': session_id}\n        else:\n            return original_execute(self, command, params)\n\n    # Patch the function before creating the driver object\n    WebDriver.execute = new_command_execute\n    driver = webdriver.Remote(command_executor=executor_url,\n                              desired_capabilities={})\n    driver.session_id = session_id\n    # Replace the patched function with original function\n    WebDriver.execute = original_execute\n    return driver \n\n\n\n\ndef __init__(self, command_executor = None, session_id = None, previous_read_message = None):\n    chrome_options = Options()\n    chrome_options.add_argument(\"--no-sandbox\")\n\n    chrome_options.add_argument(\"--user-agent='Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.50 Safari/537.36'\")\n\n    chrome_options.add_argument('--verbose')\n    chrome_options.add_argument('--log-path=/tmp/chrome.log')\n\n\n    if command_executor == None:\n        if config.CHROMEDRIVER_LOCATION == 'None': #Launching in docker container\n            chrome_options.add_argument('--headless')\n            self.driver = webdriver.Chrome(chrome_options=chrome_options)\n            self.driver.set_window_size(1920, 1080)\n\n        else:\n            self.driver = webdriver.Chrome(config.CHROMEDRIVER_LOCATION, chrome_options=chrome_options)\n\n        self.driver.get('https://web.whatsapp.com')\n\n        print(self.driver.session_id )\n        print(self.driver.command_executor._url)\n\n        self.find_scan_code()\n\n    else:\n        self.driver = webdriver.Remote(command_executor=command_executor,desired_capabilities={})\n        self.driver.close()\n        self.driver.session_id = session_id\n\n    self.previous_read_message = previous_read_message \n\n\n\n\n\n\n\nfrom selenium import webdriver\n\ndriver = webdriver.Chrome()\nexecutor_url = driver.command_executor._url\nsession_id = driver.session_id\ndriver.get(\"http://tarunlalwani.com\")\n\nprint session_id\nprint executor_url\n\n\ndriver2 = webdriver.Remote(command_executor=executor_url, desired_capabilities={})\ndriver2.session_id = session_id\nprint driver2.current_url\n\n\n\n\n\n\n\n\ndriver = webdriver.Firefox()  #python\n\n# extract to session_id and _url from driver object.\n\nurl = driver.command_executor._url       #\"http://127.0.0.1:60622/hub\"\nsession_id = driver.session_id            #'4e167f26-dc1d-4f51-a207-f761eaf73c31'\n\n# Use these two parameter to connect to your driver.\n\ndriver = webdriver.Remote(command_executor=url,desired_capabilities={})\ndriver.close()   # this prevents the dummy browser\ndriver.session_id = session_id\n\n# And you are connected to your driver again.\n\ndriver.get(\"http://www.mrsmart.in\")\n\n\n\n\n\n\n\n\n\n\n\nadd `destination` to backup functions\n\n-keep -> keep browser open\n-reconnect -> reconnect to existing/previous session\n\ncheck if present @ spawn, determines reconnect behaviour without extra args\n-session $sessionId -> session to reconnect to\n-url $executorURL -> executor_url in conjunct w/ session\n\nif keep: save session&url in mount_path\nif reconnect & no session||url: check for saved session||url in mount_path\n"
  },
  {
    "path": "notes/selenium-notes1.py",
    "content": "# https://stackoverflow.com/questions/43382447/python-with-selenium-drag-and-drop-from-file-system-to-webdriver\n\n\nJS_DROP_FILE = \"\"\"\n    var target = arguments[0],\n        offsetX = arguments[1],\n        offsetY = arguments[2],\n        document = target.ownerDocument || document,\n        window = document.defaultView || window;\n\n    var input = document.createElement('INPUT');\n    input.type = 'file';\n    input.onchange = function () {\n      var rect = target.getBoundingClientRect(),\n          x = rect.left + (offsetX || (rect.width >> 1)),\n          y = rect.top + (offsetY || (rect.height >> 1)),\n          dataTransfer = { files: this.files };\n\n      ['dragenter', 'dragover', 'drop'].forEach(function (name) {\n        var evt = document.createEvent('MouseEvent');\n        evt.initMouseEvent(name, !0, !0, window, 0, 0, 0, x, y, !1, !1, !1, !1, 0, null);\n        evt.dataTransfer = dataTransfer;\n        target.dispatchEvent(evt);\n      });\n\n      setTimeout(function () { document.body.removeChild(input); }, 25);\n    };\n    document.body.appendChild(input);\n    return input;\n\"\"\"\n\ndef drag_and_drop_file(drop_target, path):\n    driver = drop_target.parent\n    file_input = driver.execute_script(JS_DROP_FILE, drop_target, 0, 0)\n    file_input.send_keys(path)"
  },
  {
    "path": "notes/supervisor-api.txt",
    "content": "\nadd to supervisord.conf:\n\n[program:flask_app]\ncommand = FLASK_ENV=production python api/index.py users\ndirectory = /home/zapier/onlysnarf\nautostart = true\nautorestart = true\n\nsudo supervisorctl update\nsudo supervisorctl status"
  },
  {
    "path": "notes/testflask.py",
    "content": "import flask_unittest\nfrom flaskr.db import get_db\n\nclass TestFoo(flask_unittest.AppTestCase):\n\n    def create_app(self):\n        # Return/Yield a `Flask` object here\n        pass\n\n    def setUp(self, app):\n        # Perform set up before each test, using app\n        pass\n\n    def tearDown(self, app):\n        # Perform tear down after each test, using app\n        pass\n\n    '''\n    Note: the setUp and tearDown method don't need to be explicitly declared\n    if they don't do anything (like in here) - this is just an example\n    Only declare the setUp and tearDown methods with a body, same as regular unittest testcases\n    '''\n\n    def test_foo_with_app(self, app):\n        # Use the app here\n        # Example of using test_request_context (on a hypothetical app)\n        with app.test_request_context('/1/update'):\n            self.assertEqual(request.endpoint, 'blog.update')\n\n    def test_bar_with_app(self, app):\n        # Use the app here\n        # Example of using client from app (on a hypothetical app)\n        with app.test_client() as client:\n            rv = client.get('/hello')\n            self.assertInResponse(rv, 'hello world!')\n\n    def test_baz_with_app(self, app):\n        # Use the app here\n        # Example of using app_context (on a hypothetical app)\n        with app.app_context():\n            get_db().execute(\"INSERT INTO user (username, password) VALUES ('test', 'testpass');\")"
  },
  {
    "path": "notes/testflaskclient.py",
    "content": "import flask_unittest\nimport flask.globals\n\nclass TestAPI(flask_unittest.ClientTestCase):\n    # Assign the `Flask` app object\n    app = ...\n\n    def setUp(self, client):\n        # Perform set up before each test, using client\n        pass\n\n    def tearDown(self, client):\n        # Perform tear down after each test, using client\n        pass\n\n    '''\n    Note: the setUp and tearDown method don't need to be explicitly declared\n    if they don't do anything (like in here) - this is just an example\n    Only declare the setUp and tearDown methods with a body, same as regular unittest testcases\n    '''\n\n    def test_foo_with_client(self, client):\n        # Use the client here\n        # Example request to a route returning \"hello world\" (on a hypothetical app)\n        rv = client.get('/hello')\n        self.assertInResponse(rv, 'hello world!')\n\n    def test_bar_with_client(self, client):\n        # Use the client here\n        # Example login request (on a hypothetical app)\n        rv = client.post('/login', {'username': 'pinkerton', 'password': 'secret_key'})\n        # Make sure rv is a redirect request to index page\n        self.assertLocationHeader('http://localhost/')\n        # Make sure session is set\n        self.assertIn('user_id', flask.globals.session)"
  },
  {
    "path": "notes/testrunners.py",
    "content": "Executing Test Runners\n\nThe Python application that executes your test code, checks the assertions, and gives you test results in your console is called the test runner.\n\nAt the bottom of test.py, you added this small snippet of code:\n\nif __name__ == '__main__':\n    unittest.main()\n\nThis is a command line entry point. It means that if you execute the script alone by running python test.py at the command line, it will call unittest.main(). This executes the test runner by discovering all classes in this file that inherit from unittest.TestCase.\n\nThis is one of many ways to execute the unittest test runner. When you have a single test file named test.py, calling python test.py is a great way to get started.\n\nAnother way is using the unittest command line. Try this:\n\n$ python -m unittest test\n\nThis will execute the same test module (called test) via the command line.\n\nYou can provide additional options to change the output. One of those is -v for verbose. Try that next:\n\n$ python -m unittest -v test\ntest_list_int (test.TestSum) ... ok\n\n----------------------------------------------------------------------\nRan 1 tests in 0.000s\n\nThis executed the one test inside test.py and printed the results to the console. Verbose mode listed the names of the tests it executed first, along with the result of each test.\n\nInstead of providing the name of a module containing tests, you can request an auto-discovery using the following:\n\n$ python -m unittest discover\n\nThis will search the current directory for any files named test*.py and attempt to test them.\n\nOnce you have multiple test files, as long as you follow the test*.py naming pattern, you can provide the name of the directory instead by using the -s flag and the name of the directory:\n\n$ python -m unittest discover -s tests\n\nunittest will run all tests in a single test plan and give you the results.\n\nLastly, if your source code is not in the directory root and contained in a subdirectory, for example in a folder called src/, you can tell unittest where to execute the tests so that it can import the modules correctly with the -t flag:\n\n$ python -m unittest discover -s tests -t src\n\nunittest will change to the src/ directory, scan for all test*.py files inside the the tests directory, and execute them."
  },
  {
    "path": "notes/unittest.py",
    "content": "import unittest\n\n\nclass TestSum(unittest.TestCase):\n\n    def test_sum(self):\n        self.assertEqual(sum([1, 2, 3]), 6, \"Should be 6\")\n\n    def test_sum_tuple(self):\n        self.assertEqual(sum((1, 2, 2)), 6, \"Should be 6\")\n\nif __name__ == '__main__':\n    unittest.main()\n\n\n\n\nMethod  Equivalent to\n.assertEqual(a, b)  a == b\n.assertTrue(x)  bool(x) is True\n.assertFalse(x)     bool(x) is False\n.assertIs(a, b)     a is b\n.assertIsNone(x)    x is None\n.assertIn(a, b)     a in b\n.assertIsInstance(a, b)     isinstance(a, b)\n\n.assertIs(), .assertIsNone(), .assertIn(), and .assertIsInstance() all have opposite methods, named .assertIsNot(), and so forth.\n\n\n\n\n\nAn integration test checks that components in your application operate with each other.\nA unit test checks a small component in your application.\n\n\n\nBefore you dive into writing tests, you’ll want to first make a couple of decisions:\n\n    What do you want to test?\n    Are you writing a unit test or an integration test?\n\nThen the structure of a test should loosely follow this workflow:\n\n    Create your inputs\n    Execute the code being tested, capturing the output\n    Compare the output with an expected result\n\n\n\n\n\n# Note: What if your application is a single script?\n\n# You can import any attributes of the script, such as classes, functions, and variables by using the built-in __import__() function. Instead of from my_sum import sum, you can write the following:\n\n# target = __import__(\"my_sum.py\")\n# sum = target.sum\n\n# The benefit of using __import__() is that you don’t have to turn your project folder into a package, and you can specify the file name. This is also useful if your filename collides with any standard library packages. For example, math.py would collide with the math module.\n"
  },
  {
    "path": "notes/unittestskips.py",
    "content": "class MyTestCase(unittest.TestCase):\n\n    @unittest.skip(\"demonstrating skipping\")\n    def test_nothing(self):\n        self.fail(\"shouldn't happen\")\n\n    @unittest.skipIf(mylib.__version__ < (1, 3),\n                     \"not supported in this library version\")\n    def test_format(self):\n        # Tests that work for only a certain version of the library.\n        pass\n\n    @unittest.skipUnless(sys.platform.startswith(\"win\"), \"requires Windows\")\n    def test_windows_support(self):\n        # windows specific testing code\n        pass\n\n    def test_maybe_skipped(self):\n        if not external_resource_available():\n            self.skipTest(\"external resource not available\")\n        # test code that depends on the external resource\n        pass"
  },
  {
    "path": "notes/unittesttestrunners.py",
    "content": "def suite():\n    suite = unittest.TestSuite()\n    suite.addTest(WidgetTestCase('test_default_widget_size'))\n    suite.addTest(WidgetTestCase('test_widget_resize'))\n    return suite\n\nif __name__ == '__main__':\n    runner = unittest.TextTestRunner()\n    runner.run(suite())"
  },
  {
    "path": "notes/windows-python-setup.txt",
    "content": "\nhttps://stackoverflow.com/questions/4822400/register-an-exe-so-you-can-run-it-from-any-command-line-in-windows\n\nC:\\Users\\brain\\AppData\\Local\\Packages\\PythonSoftwareFoundation.Python.3.7_qbz5n2kfra8p0\\LocalCache\\local-packages\\Python37\\Scripts\n\npython3 setup.py install --user"
  },
  {
    "path": "public/docs/help.md",
    "content": "**Note**: General options go in front of the chosen subcommand. Options specific to the subcommand go after the subcommand.  \n**Double Note**: These are all modified help outputs for reading on github from running the help commmand or `-h` for each subcommand.\n\n# -h\n\n`snarf [-h] [-browser {auto,brave,chrome,chromium,firefox,remote}] [-login {auto,onlyfans,twitter}] [-reduce] [-save] [-tweet] [--username USERNAME] [-config PATH_CONFIG] [-debug] [-keep] [-prefer-local] [-show] [-v] [-version] {api,config,discount,message,post,users} ... ` \n\nNo mention of old Shnarf, I notice. Go ahead, just take all the glory, and leave it to Snarf to clean up after you. I don't mind!  \n\npositional arguments: {**discount**,**message**,**post**,**users**}  \n\nInclude a subcommand to run a corresponding action:  \n>   **discount**            > discount one or more users  \n>   **message**             > send a message to one or more users  \n>   **post**                > upload a post  \n>   **users**               > scan & save users  \n\noptions:  \n> -h, --help            show this help message and exit  \n> -browser {auto,brave,chrome,chromium,firefox,remote}, -B {auto,brave,chrome,chromium,firefox,remote}  web browser to use  \n> -login {auto,onlyfans,twitter}, -L {auto,onlyfans,twitter}  method of user login to prefer  \n> -reduce               enable reducing files over 50 MB  \n> -save, -S             enable saving users locally on exit  \n> -tweet                enable tweeting when posting  \n> --username USERNAME, --u USERNAME OnlyFans username to use  \n> -phone PHONE          OnlyFans phone number to use\n> -config PATH_CONFIG, -C PATH_CONFIG path to config.conf  \n> -debug, -D            enable debugging  \n> -keep, -K             keep browser window open after scripting ends  \n> -prefer-local         prefer recently cached data  \n> -show, -SW            enable displaying browser window  \n> -v, -verbose          verbosity level (max 3)  \n> -version              show program's version number and exit  \n\nShnarrf!  \n\n# API\n\n`snarf api`\n\nFlask server API for receiving POST requests to /post and /message. \n\n# Config\n\n`snarf config`\n\nMenu interface for interacting with user config files.\n\n# Discount\n\n`snarf discount [-h] [-amount AMOUNT] [-months MONTHS] [-user USER | -users USERS]`  \n\noptions:  \n> -h, --help      show this help message and exit  \n> -amount AMOUNT  amount (%) to discount by  \n> -months MONTHS  number of months to discount  \n> -user USER      user to discount  \n> -users USERS    users to discount  \n\n# Message\n\n`snarf message [-h] [-date DATE] [-performers PERFORMERS] [-price PRICE] [-schedule SCHEDULE] [-time TIME] [-tags TAGS] [-text TEXT] [-user USER | -users USERS] ... `  \n\npositional arguments:  \n> input                 one or more paths to files (or folder) to include in the message  \n\noptions:  \n> -h, --help            show this help message and exit  \n> -date DATE            schedule date (MM-DD-YYYY)  \n> -performers PERFORMERS  performers to reference. adds \"@[...performers]\"  \n> -price PRICE          price to charge ($)  \n> -schedule SCHEDULE    schedule (MM-DD-YYYY:HH:MM:SS)  \n> -time TIME            time (HH:MM)  \n> -tags TAGS            the tags (@[tag])  \n> -text TEXT            text to send  \n> -user USER            user to message  \n> -users USERS          users to message  \n\n# Post\n\n`snarf post [-h] [-date DATE] [-duration {1,3,7,30,99} | -expiration EXPIRATION] [-performers PERFORMERS] [-price PRICE] [-schedule SCHEDULE] [-time TIME] [-tags TAGS] [-text TEXT] [-question QUESTIONS] ... `  \n\npositional arguments:  \n> input                 one or more paths to files (or folders) to include in the post  \n\noptions:  \n> -h, --help            show this help message and exit  \n> -date DATE            schedule date (MM-DD-YYYY)  \n> -duration {1,3,7,30,99} duration in days (99 for 'No Limit') for a poll  \n> -expiration EXPIRATION  expiration in days (999 for 'No Limit')  \n> -performers PERFORMERS  performers to reference. adds \"@[...performers]\"  \n> -price PRICE          price to charge ($)  \n> -schedule SCHEDULE    schedule (MM-DD-YYYY:HH:MM:SS)  \n> -time TIME            time (HH:MM)  \n> -tags TAGS            tags (@[tag])  \n> -text TEXT            text to send  \n> -question QUESTIONS, -Q QUESTIONS   questions to ask  \n\n# Users\n\n`snarf users [-h]`  \n\noptions:  \n> -h, --help  show this help message and exit  \n"
  },
  {
    "path": "public/docs/menu.md",
    "content": "# Menu\n\n`snarf *args`\n\nPlease refer to the example config file provided for a complete listing of available options.\n\n## Actions\n\nAn action is performed by including the required combination of subcommands, arguments, and input. OnlySnarf actions can be fulfilled as a promptless script via:\n`snarf post /path/to/fileOrDirectory`\n\nFor more help with an action: `snarf post -h`\n\nUsers are easily referenced using keywords:  \n**All**: all users  \n**Recent**: users subscribed within last 5 days  \n**New**: users subscribed within last month who haven't been messaged  \n**Select**: selects User from list (currently unavailable)  \n**Username**: enter User by username  \n**Random**: select user at random  \n\n### Discount\nuser: \"all\" | \"recent\" | \"new\" | \"select\" | \"username\" | \"random\"  \namount (%): 0-55 | \"min\" | \"max\"  \nmonths: 1-12 | \"min\" | \"max\"  \n\n### Message\nuser: \"all\" | \"recent\" | \"new\" | \"select\" | \"username\" | \"random\"\ntext: \"\"  \n\n(optional)  \ninput: \"/path/to/fileoOrFolder\"  \n\n(when input is specified)  \nprice ($): \"0.00\" | \"min\" | \"max\"  \ntags: key, words -> #key #words  \nperformers: performerName1, performerName2 -> @performerName1 @performerName2  \n\nSchedule: (\"date\" & \"time\" or only \"schedule\")  \nschedule: \"mm/dd/YYYY:HH:MM\"  \ndate: \"mm/dd/YYYY\"  \ntime: \"HH:MM\"  \n\nMessage $USER the provided $TEXT with $TAGS and uploaded $IMAGE available for $PRICE.  \n\nIf schedule: schedules the message for the provided date and time.  \n\n### Post\ntext: \"\"  \n\n(optional)  \ninput: \"/path/to/fileoOrFolder\"  \ntags: key, words -> #key #words  \nperformers: performerName1, performerName2 -> @performerName1 @performerName2  \n\n**Schedule**: (when specificying \"date\" & \"time\" or only \"schedule\")  \nschedule: \"mm/dd/YYYY:HH:MM\"  \ndate: \"mm/dd/YYYY\"  \ntime: \"HH:MM\"  \n\n**Poll**:  \nquestions: \"your mom\", \"is very hot\", \"today\"  \nduration: 1, 3, 7, 99 or \"No limit\" | \"min\" | \"max\"  \nexpires: 1, 3, 7, 99 or \"No limit\" | \"min\" | \"max\"  \n\nUpload provided $INPUT with $TEXT, $TAGS, and provided list of $PERFORMERS.  \n\nIf schedule: schedules the post for the provided date and time.  \nIf poll: enters questions as provided in order, the duration, and expiration.  \n\n### Profile (currently not working)\n\nBackup:  \n**Content**: Downloads all posted content  \n**Messages**: Downloads (roughly) all messaged content  \n**Content & Messages**: both of above  \n\n### Users\n(none)\n\n## Args\n\nGeneral, debugging, and selenium related args apply to any action. Each action's available args are further described below.  \n\n### General\n\n-config, -C /path/to/file  \nThe path to the config file.  \n\n-login, -L [onlyfans|twitter]  \nThe method to use to log in.  \n\n-reduce  \nReduce the file size before uploading.  \n\n--username, --u \"\"  \nThe OnlyFans username to login as.  \n\n-phone ##########\nThe OnlyFans phone number associated with the account for verifying Twitter login.\n\n### Discount\n-amount #  \nThe amount in percent to discount.  \n\n-months #  \nThe number of months to specify for a schedule.  \n\n-user \"\"  \nThe user by username to specify for discounting.  \n\n-users \"user1,user2\"  \nThe users by username to specify for discounting.  \n\n### Message\n-date \"01/01/2000\"  \nThe date required for a scheduled message.  \n\n-performers \"name1\" -performers \"name2\" ...  \nPerformer usernames to reference. Adds to text with \"@\" symbols.  \n\n-price 0  \nThe price to specify for file uploads.  \n\n-schedule \"mm/dd/YYYY:HH:MM\"  \nSchedule message for upload via $date and $time.  \n\n-tags \"tag1\" -tags \"tag2\" ...  \nTags to become #tags when creating text.  \n\n-text \"\"  \nText to be entered.  \n\n-time \"HH:MM\"  \nTime for scheduled message.  \n\n-user \"\"  \nThe user by username to specify for messages.  \n\n-users \"user1,user2\"  \nThe users by username to specify for messages.  \n\n### Post\n-date \"01/01/2000\"  \nThe date required for a scheduled post.  \n\n-duration [0,3,7,99,min,max]  \nThe duration for a post.  \n\n-expiration [0,3,7,99,min,max]  \nThe expiration to use for a post.  \n\n-performers \"name1\" -performers \"name2\" ...  \nPerformer usernames to reference. Adds to text with \"@\" symbols.  \n\n-schedule \"mm/dd/YYYY:HH:MM\"  \nSchedule post for upload via $date and $time.  \n\n-tags \"tag1\" -tags \"tag2\" ...  \nTags to become #tags when creating text.  \n\n-text \"\"  \nText to be entered.  \n\n-time \"HH:MM\"  \nTime for scheduled posts.  \n\n-tweet  \nEnable Tweeting (if Twitter connected).  \n\n-question, -Q \"text\"  \nA question to include when posting a questions response. Can be provided in multiple up to 5. For example: `-question \"first\" -question \"second\" -question \"third\"`  \n\n### Users\n\n(none)  \n\n## more args\n\nSome args are hidden from the help command. All args are available via the config file and are (sometimes) further notated there as well.  \n\n### Selenium\n\n-browser, -B [auto|firefox|google|reconnect|remote]  \nBrowser to connect with.  \n\n-keep, -K  \nKeep the browser open when finished (allows for reconnect).  \n\n-save, -S  \nEnable saving users before exiting browser.  \n\n-upload-max-duration #  \nThe number of 10 minute intervals to wait while uploading a file.  \n\n### Debugging\n\n-debug, -D  \nTests configuration. Does not upload or remove from Google Drive.  \n\n-force-upload  \nEnable forcing upload despite long upload time.  \n\n-show, -SW  \nShow web browser.  \n\n-verbose  \nShows additional log output (up to 3).  \n\n-version  \nPrints the version  \n\nBasic debugging preface:  \n`snarf -debug -verbose -verbose -verbose -show -debug-delay`  \n\n## Config File Only\n\n### Debugging\n\n-debug-delay  \nDelays certain portions for visual monitoring.  \n\n-debug-firefox  \nEnable debugging of Firefox.  \n\n-debug-google  \nEnable debugging of google chrome.  \n\n-download-path \"\"  \nThe download path for files.  \n\n-image-download-max #  \nThe maximum number of files to download.  \n\n-image-upload-max #  \nThe maximum number of files to upload.  \n\n-recent-users-count #  \nThe number of users to count as \"recent\".  \n\n-skip-upload  \nSkip file uploads.  \n\n-skip-users \"user1,user2\"  \nSkip specific users by username or id.  \n\n-users-read #  \nThe number of users to count when reading messages.  \n\n## In Development Hell\n\nSync From: Gets / reads relevant settings from OnlyFans profile and saves locally.  \nSync To: Updates / writes relevant settings to OnlyFans profile from local save.  \n"
  },
  {
    "path": "setup.cfg",
    "content": "[metadata]\ndescription_file = README.md"
  },
  {
    "path": "setup.py",
    "content": "import setuptools\n\nwith open(\"README.md\", \"r\") as fh:\n    long_description = fh.read()\n\nsetuptools.setup(\n    name=\"OnlySnarf\",\n    version=\"4.6.2\",\n    author=\"Skeetzo\",\n    author_email=\"WebmasterSkeetzo@gmail.com\",\n    url = 'https://github.com/skeetzo/onlysnarf',\n    keywords = ['OnlyFans', 'OnlySnarf', 'selenium', 'snarf'],\n    description=\"OnlyFans Content Distribution Tool\",\n    long_description=long_description,\n    long_description_content_type=\"text/markdown\",\n    # packages=setuptools.find_packages(),\n    packages=[\"OnlySnarf\", \"OnlySnarf/classes\",\"OnlySnarf/conf\",\"OnlySnarf/elements\",\"OnlySnarf/lib\",\"OnlySnarf/util\",\"OnlySnarf/server\"],\n    include_package_data=True,\n    install_requires=[\n        'ffmpeg',\n        'inquirer',\n        'wget',\n        'selenium>=4',\n        'webdriver_manager==3.9.0',\n        'validators',\n        'flask'\n    ],\n    extras_require={\n        'dev': [\n            'pytest',\n            'flask-unittest'\n        ]\n    },\n    entry_points={\n        'console_scripts' : [\n            'snarf = OnlySnarf.snarf:main'\n        ]\n    },\n    classifiers=[\n        'Development Status :: 5 - Production/Stable',\n        'Intended Audience :: End Users/Desktop',\n        'Topic :: System :: Emulators',\n        'License :: OSI Approved :: MIT License',\n        'Programming Language :: Python :: 3.8',\n        \"Operating System :: OS Independent\"\n    ]\n)"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/api/__init__.py",
    "content": ""
  },
  {
    "path": "tests/api/test_api.py",
    "content": "import os\nos.environ['ENV'] = \"test\"\n\nimport flask_unittest\nimport flask.globals\n\nimport json\n\nfrom OnlySnarf.api import create_app\n\nclass TestAPI(flask_unittest.ClientTestCase):\n    # Assign the `Flask` app object\n    # app = Flask(__name__)\n    app = create_app()\n    app.debug = True\n    app.testing = True\n\n    def setUp(self, client):\n        # Perform set up before each test, using client\n        pass\n\n    def tearDown(self, client):\n        # Perform tear down after each test, using client\n        pass\n\n    def test_message(self, client):\n        mockMessage = {\n            \"text\":\"testes\",\n            \"user\":\"random\"\n        }\n        response = client.post(\"/message\", data=json.dumps(mockMessage))\n        assert response.status_code == 200\n\n    def test_post(self, client):\n        mockPost = {\n            \"text\":\"testes\"\n        }\n        response = client.post(\"/post\", data=json.dumps(mockPost))\n        assert response.status_code == 200\n"
  },
  {
    "path": "tests/selenium/__init__.py",
    "content": ""
  },
  {
    "path": "tests/selenium/browsers/__init__.py",
    "content": ""
  },
  {
    "path": "tests/selenium/browsers/test_brave.py",
    "content": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver import Driver\nfrom OnlySnarf.util.settings import Settings\n\nclass TestSeleniumBrave(unittest.TestCase):\n\n    def setUp(self):\n        config[\"debug_selenium\"] = True\n        config[\"keep\"] = False\n        # config[\"show\"] = True\n        Settings.set_debug(\"tests\")\n        self.driver = Driver()\n\n    def tearDown(self):\n        # config[\"debug_brave\"] = False\n        config[\"show\"] = False\n        self.driver.exit()\n\n    def test_brave(self):\n        config[\"browser\"] = \"brave\"\n        # config[\"debug_brave\"] = True\n        self.driver.init()\n        assert self.driver.browser, \"unable to launch brave\"\n\n############################################################################################\n\nif __name__ == '__main__':\n    unittest.main()"
  },
  {
    "path": "tests/selenium/browsers/test_chrome.py",
    "content": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver import Driver\nfrom OnlySnarf.util.settings import Settings\n\nclass TestSeleniumChrome(unittest.TestCase):\n\n    def setUp(self):\n        config[\"debug_selenium\"] = True\n        config[\"keep\"] = False\n        # config[\"show\"] = True\n        Settings.set_debug(\"tests\")\n        self.driver = Driver()\n\n    def tearDown(self):\n        config[\"debug_chrome\"] = False\n        config[\"show\"] = False\n        self.driver.exit()\n\n    def test_chrome(self):\n        config[\"browser\"] = \"chrome\"\n        config[\"debug_chrome\"] = True\n        self.driver.init()\n        assert self.driver.browser, \"unable to launch chrome\"\n\n############################################################################################\n\nif __name__ == '__main__':\n    unittest.main()"
  },
  {
    "path": "tests/selenium/browsers/test_chromium.py",
    "content": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver import Driver\nfrom OnlySnarf.util.settings import Settings\n\nclass TestSeleniumChromium(unittest.TestCase):\n\n    def setUp(self):\n        config[\"debug_selenium\"] = True\n        config[\"keep\"] = False\n        # config[\"show\"] = True\n        Settings.set_debug(\"tests\")\n        self.driver = Driver()\n\n    def tearDown(self):\n        # config[\"debug_chromium\"] = False\n        config[\"show\"] = False\n        self.driver.exit()\n\n    @unittest.skip(\"todo\")\n    def test_chromium(self):\n        config[\"browser\"] = \"chromium\"\n        # config[\"debug_chromium\"] = True\n        self.driver.init()\n        assert self.driver.browser, \"unable to launch chromium\"\n\n############################################################################################\n\nif __name__ == '__main__':\n    unittest.main()"
  },
  {
    "path": "tests/selenium/browsers/test_edge.py",
    "content": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver import Driver\nfrom OnlySnarf.util.settings import Settings\n\nclass TestSeleniumEdge(unittest.TestCase):\n\n    def setUp(self):\n        config[\"debug_selenium\"] = True\n        config[\"keep\"] = False\n        # config[\"show\"] = True\n        Settings.set_debug(\"tests\")\n        self.driver = Driver()\n\n    def tearDown(self):\n        # config[\"debug_edge\"] = False\n        config[\"show\"] = False\n        self.driver.exit()\n\n    # @unittest.skip(\"todo\")\n    def xtest_edge(self):\n        config[\"browser\"] = \"edge\"\n        # config[\"debug_edge\"] = True\n        self.driver.init()\n        assert self.driver.browser, \"unable to launch edge\"\n\n############################################################################################\n\nif __name__ == '__main__':\n    unittest.main()"
  },
  {
    "path": "tests/selenium/browsers/test_firefox.py",
    "content": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver import Driver\nfrom OnlySnarf.util.settings import Settings\n\nclass TestSeleniumFirefox(unittest.TestCase):\n\n    def setUp(self):\n        config[\"debug_selenium\"] = True\n        config[\"keep\"] = False\n        # config[\"show\"] = True\n        Settings.set_debug(\"tests\")\n        self.driver = Driver()\n\n    def tearDown(self):\n        config[\"debug_firefox\"] = False\n        config[\"show\"] = False\n        self.driver.exit()\n\n    def test_firefox(self):\n        config[\"browser\"] = \"firefox\"\n        config[\"debug_firefox\"] = True\n        self.driver.init()\n        assert self.driver.browser, \"unable to launch firefox\"\n\n############################################################################################\n\nif __name__ == '__main__':\n    unittest.main()"
  },
  {
    "path": "tests/selenium/browsers/test_ie.py",
    "content": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver import Driver\nfrom OnlySnarf.util.settings import Settings\n\nclass TestSeleniumIE(unittest.TestCase):\n\n    def setUp(self):\n        config[\"debug_selenium\"] = True\n        config[\"keep\"] = False\n        # config[\"show\"] = True\n        Settings.set_debug(\"tests\")\n        self.driver = Driver()\n\n    def tearDown(self):\n        # config[\"debug_ie\"] = False\n        config[\"show\"] = False\n        self.driver.exit()\n\n    # @unittest.skip(\"todo\")\n    def xtest_ie(self):\n        config[\"browser\"] = \"ie\"\n        # config[\"debug_ie\"] = True\n        self.driver.init()\n        assert self.driver.browser, \"unable to launch ie\"\n\n############################################################################################\n\nif __name__ == '__main__':\n    unittest.main()"
  },
  {
    "path": "tests/selenium/browsers/test_opera.py",
    "content": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver import Driver\nfrom OnlySnarf.util.settings import Settings\n\nclass TestSeleniumOpera(unittest.TestCase):\n\n    def setUp(self):\n        config[\"debug_selenium\"] = True\n        config[\"keep\"] = False\n        # config[\"show\"] = True\n        Settings.set_debug(\"tests\")\n        self.driver = Driver()\n\n    def tearDown(self):\n        # config[\"debug_opera\"] = False\n        config[\"show\"] = False\n        self.driver.exit()\n\n    # @unittest.skip(\"todo\")\n    def xtest_opera(self):\n        config[\"browser\"] = \"opera\"\n        config[\"debug_opera\"] = True\n        self.driver.init()\n        assert self.driver.browser, \"unable to launch opera\"\n\n############################################################################################\n\nif __name__ == '__main__':\n    unittest.main()"
  },
  {
    "path": "tests/selenium/reconnect/__init__.py",
    "content": ""
  },
  {
    "path": "tests/selenium/reconnect/test_brave.py",
    "content": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver import Driver\nfrom OnlySnarf.util.settings import Settings\n\nclass TestSeleniumReconnectBrave(unittest.TestCase):\n\n    def setUp(self):\n        config[\"debug_selenium\"] = True\n        config[\"keep\"] = True\n        # config[\"show\"] = True\n        Settings.set_debug(\"tests\")\n        self.driver = Driver()\n\n    def tearDown(self):\n        config[\"debug_selenium\"] = False\n        config[\"keep\"] = False\n        config[\"show\"] = False\n        self.driver.exit()\n\n    def test_reconnect_brave(self):\n        config[\"browser\"] = \"brave\"\n        self.driver.init()\n        self.driver.exit()\n        config[\"browser\"] = \"auto\"\n        self.driver.init()\n        assert self.driver.browser, \"unable to launch via reconnect brave\"\n\n############################################################################################\n\nif __name__ == '__main__':\n    unittest.main()"
  },
  {
    "path": "tests/selenium/reconnect/test_chrome.py",
    "content": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver import Driver\nfrom OnlySnarf.util.settings import Settings\n\nclass TestSeleniumReconnectChrome(unittest.TestCase):\n\n    def setUp(self):\n        config[\"debug_selenium\"] = True\n        config[\"keep\"] = True\n        # config[\"show\"] = True\n        Settings.set_debug(\"tests\")\n        self.driver = Driver()\n\n    def tearDown(self):\n        config[\"debug_selenium\"] = False\n        config[\"keep\"] = False\n        config[\"show\"] = False\n        self.driver.exit()\n    \n    def test_reconnect_chrome(self):\n        config[\"browser\"] = \"chrome\"\n        self.driver.init()\n        self.driver.exit()\n        config[\"browser\"] = \"auto\"\n        self.driver.init()\n        assert self.driver.browser, \"unable to launch via reconnect chrome\"\n\n############################################################################################\n\nif __name__ == '__main__':\n    unittest.main()"
  },
  {
    "path": "tests/selenium/reconnect/test_chromium.py",
    "content": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver import Driver\nfrom OnlySnarf.util.settings import Settings\n\nclass TestSeleniumReconnectChromium(unittest.TestCase):\n\n    def setUp(self):\n        config[\"debug_selenium\"] = True\n        config[\"keep\"] = True\n        # config[\"show\"] = True\n        Settings.set_debug(\"tests\")\n        self.driver = Driver()\n\n    def tearDown(self):\n        config[\"debug_selenium\"] = False\n        config[\"keep\"] = False\n        config[\"show\"] = False\n        self.driver.exit()\n\n    @unittest.skip(\"todo\")\n    def test_reconnect_chromium(self):\n        config[\"browser\"] = \"chromium\"\n        self.driver.init()\n        self.driver.exit()\n        config[\"browser\"] = \"auto\"\n        self.driver.init()\n        assert self.driver.browser, \"unable to launch via reconnect chromium\"\n\n############################################################################################\n\nif __name__ == '__main__':\n    unittest.main()"
  },
  {
    "path": "tests/selenium/reconnect/test_edge.py",
    "content": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver import Driver\nfrom OnlySnarf.util.settings import Settings\n\nclass TestSeleniumReconnectEdge(unittest.TestCase):\n\n    def setUp(self):\n        config[\"debug_selenium\"] = True\n        config[\"keep\"] = True\n        # config[\"show\"] = True\n        Settings.set_debug(\"tests\")\n        self.driver = Driver()\n\n    def tearDown(self):\n        config[\"debug_selenium\"] = False\n        config[\"keep\"] = False\n        config[\"show\"] = False\n        self.driver.exit()\n\n    @unittest.skip(\"todo\")\n    def test_reconnect_edge(self):\n        config[\"browser\"] = \"edge\"\n        self.driver.init()\n        self.driver.exit()\n        config[\"browser\"] = \"auto\"\n        self.driver.init()\n        assert self.driver.browser, \"unable to launch via reconnect edge\"\n\n############################################################################################\n\nif __name__ == '__main__':\n    unittest.main()"
  },
  {
    "path": "tests/selenium/reconnect/test_firefox.py",
    "content": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver import Driver\nfrom OnlySnarf.util.settings import Settings\n\nclass TestSeleniumReconnectFirefox(unittest.TestCase):\n\n    def setUp(self):\n        config[\"debug_selenium\"] = True\n        config[\"keep\"] = True\n        # config[\"show\"] = True\n        Settings.set_debug(\"tests\")\n        self.driver = Driver()\n\n    def tearDown(self):\n        config[\"debug_selenium\"] = False\n        config[\"keep\"] = False\n        config[\"show\"] = False\n        self.driver.exit()\n\n    def test_reconnect_firefox(self):\n        config[\"browser\"] = \"firefox\"\n        self.driver.init()\n        self.driver.exit()\n        config[\"browser\"] = \"auto\"\n        self.driver.init()\n        assert self.driver.browser, \"unable to launch via reconnect firefox\"\n\n############################################################################################\n\nif __name__ == '__main__':\n    unittest.main()"
  },
  {
    "path": "tests/selenium/reconnect/test_ie.py",
    "content": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver import Driver\nfrom OnlySnarf.util.settings import Settings\n\nclass TestSeleniumReconnectIE(unittest.TestCase):\n\n    def setUp(self):\n        config[\"debug_selenium\"] = True\n        config[\"keep\"] = True\n        # config[\"show\"] = True\n        Settings.set_debug(\"tests\")\n        self.driver = Driver()\n\n    def tearDown(self):\n        config[\"debug_selenium\"] = False\n        config[\"keep\"] = False\n        config[\"show\"] = False\n        self.driver.exit()\n\n    @unittest.skip(\"todo\")\n    def test_reconnect_ie(self):\n        config[\"browser\"] = \"ie\"\n        self.driver.init()\n        self.driver.exit()\n        config[\"browser\"] = \"auto\"\n        self.driver.init()\n        assert self.driver.browser, \"unable to launch via reconnect ie\"\n\n############################################################################################\n\nif __name__ == '__main__':\n    unittest.main()"
  },
  {
    "path": "tests/selenium/reconnect/test_opera.py",
    "content": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver import Driver\nfrom OnlySnarf.util.settings import Settings\n\nclass TestSeleniumReconnectOpera(unittest.TestCase):\n\n    def setUp(self):\n        config[\"debug_selenium\"] = True\n        config[\"keep\"] = True\n        # config[\"show\"] = True\n        Settings.set_debug(\"tests\")\n        self.driver = Driver()\n\n    def tearDown(self):\n        config[\"debug_selenium\"] = False\n        config[\"keep\"] = False\n        config[\"show\"] = False\n        self.driver.exit()\n    \n    @unittest.skip(\"todo\")\n    def test_reconnect_opera(self):\n        config[\"browser\"] = \"opera\"\n        self.driver.init()\n        self.driver.exit()\n        config[\"browser\"] = \"auto\"\n        self.driver.init()\n        assert self.driver.browser, \"unable to launch via reconnect opera\"\n\n############################################################################################\n\nif __name__ == '__main__':\n    unittest.main()"
  },
  {
    "path": "tests/selenium/test_browsers.py",
    "content": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver import Driver\nfrom OnlySnarf.util.settings import Settings\n\nclass TestSeleniumBrowsers(unittest.TestCase):\n\n    def setUp(self):\n        config[\"debug_selenium\"] = True\n        config[\"keep\"] = False\n        # config[\"show\"] = True\n        Settings.set_debug(\"tests\")\n        self.driver = Driver()\n\n    def tearDown(self):\n        config[\"debug_selenium\"] = False\n        config[\"show\"] = False\n        self.driver.exit()\n\n    def test_auto(self):\n        config[\"browser\"] = \"auto\"\n        self.driver.init()\n        assert self.driver.browser, \"unable to launch via auto\"\n\n############################################################################################\n\nif __name__ == '__main__':\n    unittest.main()"
  },
  {
    "path": "tests/selenium/test_reconnect.py",
    "content": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver import Driver\nfrom OnlySnarf.util.settings import Settings\n\nclass TestSeleniumReconnect(unittest.TestCase):\n\n    def setUp(self):\n        config[\"debug_selenium\"] = True\n        config[\"keep\"] = True\n        # config[\"show\"] = True\n        Settings.set_debug(\"tests\")\n        self.driver = Driver()\n\n    def tearDown(self):\n        config[\"debug_selenium\"] = False\n        config[\"keep\"] = False\n        config[\"show\"] = False\n        self.driver.exit()\n    \n    def test_reconnect(self):\n        config[\"browser\"] = \"auto\"\n        self.driver.init()\n        self.driver.exit()\n        config[\"browser\"] = \"auto\"\n        self.driver.init()\n        assert self.driver.browser, \"unable to launch via reconnect auto\"\n\n############################################################################################\n\nif __name__ == '__main__':\n    unittest.main()"
  },
  {
    "path": "tests/selenium/test_remote.py",
    "content": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\n# from OnlySnarf.util import defaults as DEFAULT\nfrom OnlySnarf.lib.driver import Driver\nfrom OnlySnarf.util.settings import Settings\n# from OnlySnarf.snarf import Snarf\n# from OnlySnarf.classes.user import User\n\nclass TestSeleniumRemote(unittest.TestCase):\n\n    def setUp(self):\n        config[\"debug_selenium\"] = True\n        config[\"keep\"] = False\n        Settings.set_debug(\"tests\")\n        self.driver = self.driver()\n\n    def tearDown(self):\n        self.driver.exit()\n\n    @unittest.skip(\"todo\")\n    def test_remote(self):\n        config[\"browser\"] = \"remote\"\n        self.driver.init()\n        assert self.driver.browser, \"unable to launch via remote\"\n    \n    @unittest.skip(\"todo\")\n    def test_remote_chrome(self):\n        config[\"browser\"] = \"remote-chrome\"\n        self.driver.init()\n        assert self.driver.browser, \"unable to launch via remote chrome\"\n\n    @unittest.skip(\"todo\")\n    def test_remote_firefox(self):\n        config[\"browser\"] = \"remote-firefox\"\n        self.driver.init()\n        assert self.driver.browser, \"unable to launch via remote firefox\"\n\n############################################################################################\n\nif __name__ == '__main__':\n    unittest.main()"
  },
  {
    "path": "tests/snarf/__init__.py",
    "content": ""
  },
  {
    "path": "tests/snarf/auth/__init__.py",
    "content": ""
  },
  {
    "path": "tests/snarf/auth/test_google.py",
    "content": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver import Driver\nfrom OnlySnarf.util.settings import Settings\n\nclass TestAuth(unittest.TestCase):\n\n    def setUp(self):\n        config[\"login\"] = \"google\"\n        Settings.set_debug(\"tests\")\n        self.driver = Driver()\n\n    def tearDown(self):\n        config[\"login\"] = \"auto\"\n        self.driver.exit()\n\n    @unittest.skip(\"todo\")\n    def test_login(self):\n        config[\"cookies\"] = False\n        assert self.driver.auth(), \"unable to login\"\n        config[\"cookies\"] = True # saves cookies for next test\n\n    @unittest.skip(\"todo\")\n    def test_login_via_cookies(self):\n        config[\"cookies\"] = True\n        config[\"debug_cookies\"] = True\n        assert self.driver.auth(), \"unable to login from cookies\"\n\n############################################################################################\n\nif __name__ == '__main__':\n    unittest.main()"
  },
  {
    "path": "tests/snarf/auth/test_onlyfans.py",
    "content": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver import Driver\nfrom OnlySnarf.util.settings import Settings\n\nclass TestAuth(unittest.TestCase):\n\n    def setUp(self):\n        config[\"login\"] = \"onlyfans\"\n        Settings.set_debug(\"tests\")\n        self.driver = Driver()\n\n    def tearDown(self):\n        config[\"login\"] = \"auto\"\n        self.driver.exit()\n\n    def test_login(self):\n        config[\"cookies\"] = False\n        assert self.driver.auth(), \"unable to login\"\n        config[\"cookies\"] = True # saves cookies for next test\n\n    def test_login_via_cookies(self):\n        config[\"cookies\"] = True\n        config[\"debug_cookies\"] = True\n        assert self.driver.auth(), \"unable to login from cookies\"\n\n############################################################################################\n\nif __name__ == '__main__':\n    unittest.main()"
  },
  {
    "path": "tests/snarf/auth/test_twitter.py",
    "content": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver import Driver\nfrom OnlySnarf.util.settings import Settings\n\nclass TestAuth(unittest.TestCase):\n\n    def setUp(self):\n        config[\"login\"] = \"twitter\"\n        Settings.set_debug(\"tests\")\n        self.driver = Driver()\n\n    def tearDown(self):\n        config[\"login\"] = \"auto\"\n        self.driver.exit()\n\n    def test_login(self):\n        config[\"cookies\"] = False\n        assert self.driver.auth(), \"unable to login\"\n        config[\"cookies\"] = True # saves cookies for next test\n\n    def test_login_via_cookies(self):\n        config[\"cookies\"] = True\n        config[\"debug_cookies\"] = True\n        assert self.driver.auth(), \"unable to login from cookies\"\n\n############################################################################################\n\nif __name__ == '__main__':\n    unittest.main()"
  },
  {
    "path": "tests/snarf/test_auth.py",
    "content": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver import Driver\nfrom OnlySnarf.util.settings import Settings\n\nclass TestAuth(unittest.TestCase):\n\n    def setUp(self):\n        config[\"login\"] = \"auto\"\n        # config[\"browser\"] = \"chrome\"\n        Settings.set_debug(\"tests\")\n        self.driver = Driver()\n\n    def tearDown(self):\n        self.driver.exit()\n\n    def test_login(self):\n        config[\"cookies\"] = False\n        assert self.driver.auth(), \"unable to login\"\n        config[\"cookies\"] = True # saves cookies for next test\n\n    def test_login_via_cookies(self):\n        config[\"cookies\"] = True\n        config[\"debug_cookies\"] = True\n        assert self.driver.auth(), \"unable to login from cookies\"\n\n############################################################################################\n\nif __name__ == '__main__':\n    unittest.main()"
  },
  {
    "path": "tests/snarf/test_discount.py",
    "content": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.util import defaults as DEFAULT\nfrom OnlySnarf.util.settings import Settings\nfrom OnlySnarf.snarf import Snarf\n\nclass TestSnarf(unittest.TestCase):\n\n    def setUp(self):\n        config[\"amount\"] = DEFAULT.DISCOUNT_MAX_AMOUNT/2 # 55 / 2 = 27 or 28 -> 25\n        config[\"months\"] = DEFAULT.DISCOUNT_MAX_MONTHS/2 # 12 / 2 = 6\n        config[\"user\"] = \"random\"\n        config[\"prefer_local\"] = True\n        Settings.set_debug(\"tests\")\n\n    def tearDown(self):\n        config[\"amount\"] = 0\n        config[\"months\"] = 0\n        config[\"user\"] = None\n        Snarf.close()\n\n    def test_discount(self):\n        config[\"prefer_local\"] = False\n        assert Snarf.discount(), \"unable to apply discount\"\n\n    def test_discount_max(self):\n        config[\"amount\"] = DEFAULT.DISCOUNT_MAX_AMOUNT # 55\n        config[\"months\"] = DEFAULT.DISCOUNT_MAX_MONTHS # 12\n        assert Snarf.discount(), \"unable to apply discount maximum\"\n\n    def test_discount_min(self):\n        config[\"amount\"] = DEFAULT.DISCOUNT_MIN_AMOUNT # 1\n        config[\"months\"] = DEFAULT.DISCOUNT_MIN_MONTHS # 1\n        assert Snarf.discount(), \"unable to apply discount minimum\"\n\n    # add a test for applying the same discount to an existing discount\n        \n############################################################################################\n\nif __name__ == '__main__':\n    unittest.main(warnings='ignore')"
  },
  {
    "path": "tests/snarf/test_expiration.py",
    "content": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.util import defaults as DEFAULT\nfrom OnlySnarf.util.settings import Settings\nfrom OnlySnarf.snarf import Snarf\n\nclass TestSnarf(unittest.TestCase):\n\n    def setUp(self):\n        config[\"expiration\"] = DEFAULT.EXPIRATION_MAX\n        config[\"text\"] = \"test balls\"\n        Settings.set_debug(\"tests\")\n\n    def tearDown(self):\n        Snarf.close()\n\n    def test_poll(self):\n        assert Snarf.post(), \"unable to post with expiration\"\n\n############################################################################################\n\nif __name__ == '__main__':\n    unittest.main(warnings='ignore')"
  },
  {
    "path": "tests/snarf/test_message.py",
    "content": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.util import defaults as DEFAULT\nfrom OnlySnarf.util.settings import Settings\nfrom OnlySnarf.snarf import Snarf\n\nclass TestSnarf(unittest.TestCase):\n\n    def setUp(self):\n        config[\"text\"] = \"test balls\"\n        config[\"user\"] = \"random\"\n        Settings.set_debug(\"tests\")\n        # config[\"skip_download\"] = False\n        # config[\"skip_upload\"] = True\n\n    def tearDown(self):\n        config[\"input\"] = []\n        config[\"price\"] = 0\n        # Snarf.close()\n\n    def test_message(self):\n        assert Snarf.message(), \"unable to send basic message\"\n\n    def test_message_files_local(self):\n        config[\"input\"] = [\"/home/skeetzo/Projects/onlysnarf/public/images/shnarf.jpg\", \"/home/skeetzo/Projects/onlysnarf/public/images/snarf.jpg\"]\n        assert Snarf.message(), \"unable to upload message files - local\"\n\n    def test_message_files_remote(self):\n        config[\"input\"] = [\"https://github.com/skeetzo/onlysnarf/blob/master/public/images/shnarf.jpg?raw=true\", \"https://github.com/skeetzo/onlysnarf/blob/master/public/images/snarf.jpg?raw=true\"]\n        assert Snarf.message(), \"unable to upload message files - remote\"\n\n    def test_message_price(self):\n        config[\"input\"] = [\"/home/skeetzo/Projects/onlysnarf/public/images/shnarf.jpg\"]\n        config[\"price\"] = DEFAULT.PRICE_MINIMUM\n        assert Snarf.message(), \"unable to set message price\"\n        Snarf.close()\n\n############################################################################################\n\nif __name__ == '__main__':\n    unittest.main(warnings='ignore')"
  },
  {
    "path": "tests/snarf/test_poll.py",
    "content": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.util import defaults as DEFAULT\nfrom OnlySnarf.util.settings import Settings\nfrom OnlySnarf.snarf import Snarf\n\nclass TestSnarf(unittest.TestCase):\n\n    def setUp(self):\n        config[\"duration\"] = DEFAULT.DURATION_ALLOWED[0]\n        config[\"questions\"] = [\"suck\",\"my\",\"dick\",\"please?\"]\n        config[\"text\"] = \"test balls\"\n        Settings.set_debug(\"tests\")\n\n    def tearDown(self):\n        config[\"duration\"] = None\n        config[\"questions\"] = []\n        Snarf.close()\n\n    def test_poll(self):\n        assert Snarf.post(), \"unable to post poll\"\n\n############################################################################################\n\nif __name__ == '__main__':\n    unittest.main(warnings='ignore')"
  },
  {
    "path": "tests/snarf/test_post.py",
    "content": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.util import defaults as DEFAULT\nfrom OnlySnarf.util.settings import Settings\nfrom OnlySnarf.snarf import Snarf\n\nclass TestSnarf(unittest.TestCase):\n\n    def setUp(self):\n        config[\"input\"] = [\"/home/skeetzo/Projects/onlysnarf/public/images/shnarf.jpg\", \"/home/skeetzo/Projects/onlysnarf/public/images/snarf.jpg\"]\n        config[\"price\"] = DEFAULT.PRICE_MINIMUM\n        config[\"text\"] = \"test balls\"\n        config[\"keywords\"] = [\"test\",\"ticles\"]\n        config[\"performers\"] = [\"yourmom\",\"yourdad\"]\n        Settings.set_debug(\"tests\")\n\n    def tearDown(self):\n        config[\"input\"] = []\n        config[\"performers\"] = []\n        config[\"price\"] = 0\n        config[\"keywords\"] = []\n        Snarf.close()\n\n    def test_post(self):\n        assert Snarf.post(), \"unable to post\"\n\n    @unittest.skip(\"todo\")\n    def test_post_files(self):\n        assert Snarf.post(), \"unable to upload post files\"\n\n    @unittest.skip(\"todo\")\n    def test_post_price(self):\n        assert Snarf.post(), \"unable to set post price\"\n\n    @unittest.skip(\"todo\")\n    def test_post_text(self):\n        assert Snarf.post(), \"unable to set post text\"\n\n############################################################################################\n\nif __name__ == '__main__':\n    unittest.main(warnings='ignore')"
  },
  {
    "path": "tests/snarf/test_schedule.py",
    "content": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\nimport datetime\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.util import defaults as DEFAULT\nfrom OnlySnarf.util.settings import Settings\nfrom OnlySnarf.snarf import Snarf\n\ntoday = datetime.datetime.now()\ntomorrow = today + datetime.timedelta(days=1, hours=13, minutes=10)\n\nclass TestSnarf(unittest.TestCase):\n\n    def setUp(self):\n        config[\"keep\"] = True\n        config[\"text\"] = \"test balls\"\n        config[\"schedule\"] = DEFAULT.SCHEDULE\n        config[\"date\"] = DEFAULT.DATE\n        config[\"time\"] = DEFAULT.TIME\n        Settings.set_debug(\"tests\")\n\n    def tearDown(self):\n        Snarf.close()\n\n    def test_schedule(self):\n        config[\"schedule\"] = tomorrow.strftime(DEFAULT.SCHEDULE_FORMAT)\n        assert Snarf.post(), \"unable to post schedule\"\n        \n    def test_schedule_date(self):\n        config[\"date\"] = tomorrow.strftime(DEFAULT.DATE_FORMAT)\n        assert Snarf.post(), \"unable to post schedule via date\"\n\n    def test_schedule_time(self):\n        config[\"time\"] = (today + datetime.timedelta(hours=1, minutes=30)).strftime(DEFAULT.TIME_FORMAT)\n        assert Snarf.post(), \"unable to post schedule via time\"\n\n    ## TODO:\n    # verify correct values by getting values of selected components:\n        # vdatetime-calendar__current--month\n    #     day, month, year: class=\"vdatetime-calendar__month__day vdatetime-calendar__month__day--selected\"\n    #     vdatetime-time-picker__item vdatetime-time-picker__item--selected\n\n    @unittest.skip(\"todo\")\n    def test_schedule_calendar_day(self):\n        pass\n\n    @unittest.skip(\"todo\")\n    def test_schedule_calendar_hour(self):\n        pass\n\n    @unittest.skip(\"todo\")\n    def test_schedule_calendar_minute(self):\n        pass\n    @unittest.skip(\"todo\")\n    def test_schedule_calendar_suffix(self):\n        pass\n\n############################################################################################\n\nif __name__ == '__main__':\n    unittest.main(warnings='ignore')"
  },
  {
    "path": "tests/snarf/test_users.py",
    "content": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\n# from OnlySnarf.util import defaults as DEFAULT\nfrom OnlySnarf.util.settings import Settings\nfrom OnlySnarf.snarf import Snarf\nfrom OnlySnarf.classes.user import User\n\nclass TestUsers(unittest.TestCase):\n\n    def setUp(self):\n        config[\"prefer_local\"] = False\n        Settings.set_debug(\"tests\")\n        \n    def tearDown(self):\n        config[\"prefer_local\"] = True\n        Snarf.close()\n\n    def test_get_users(self):\n        assert User.get_all_users(), \"unable to read users\"\n\n    # @unittest.skip(\"todo\")\n    # def test_read_users_locally(self):\n    #     assert User.read_users_local(), \"unable to read in users locally\"\n\n    # @unittest.skip(\"todo\")\n    # def test_write_users_locally(self):\n    #     assert User.write_users_local(), \"unable to write out users locally\"\n\n    # @unittest.skip(\"todo\")\n    # def test_get_following(self):\n    #     assert User.get_following(), \"unable to read followers\"\n\n    # @unittest.skip(\"todo\")\n    # def test_read_following_local(self):\n    #     assert User.read_following_local(), \"unable to read in followers locally\"\n\n    # @unittest.skip(\"todo\")\n    # def test_write_following_local(self):\n    #     assert User.write_following_local(), \"unable to write out followers locally\"\n\n############################################################################################\n\nif __name__ == '__main__':\n    unittest.main()"
  },
  {
    "path": "tests/test_profile.py",
    "content": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\n# from OnlySnarf.util import defaults as DEFAULT\nfrom OnlySnarf.lib.driver import Driver\nfrom OnlySnarf.util.settings import Settings\nfrom OnlySnarf.snarf import Snarf\n# from OnlySnarf.classes.user import User\n\nclass TestProfile(unittest.TestCase):\n\n    def setUp(self):\n        self.test_snarf = Snarf()\n        Settings.set_debug(\"tests\")\n        config[\"prefer_local\"] = False\n\n    def tearDown(self):\n        self.test_snarf.close()\n\n    @unittest.skip(\"todo\")\n    def test_profile_backup(self):\n        config[\"profile_method\"] = \"backup\"\n        assert self.test_snarf.profile(), \"unable to backup profile\"\n\n    @unittest.skip(\"todo\")\n    def test_profile_syncfrom(self):\n        config[\"profile_method\"] = \"syncfrom\"\n        assert self.test_snarf.profile(), \"unable to sync from profile\"\n\n    @unittest.skip(\"todo\")\n    def test_profile_syncto(self):\n        config[\"profile_method\"] = \"syncto\"\n        assert self.test_snarf.profile(), \"unable to sync to profile\"\n\n############################################################################################\n\nif __name__ == '__main__':\n    unittest.main()"
  },
  {
    "path": "tests/test_promotion.py",
    "content": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\n# from OnlySnarf.util import defaults as DEFAULT\nfrom OnlySnarf.lib.driver import Driver\nfrom OnlySnarf.util.settings import Settings\nfrom OnlySnarf.snarf import Snarf\n# from OnlySnarf.classes.user import User\n\nclass TestPromotion(unittest.TestCase):\n\n    def setUp(self):\n        Settings.set_debug(\"tests\")\n        config[\"prefer_local\"] = False\n        self.test_snarf = Snarf()\n\n    def tearDown(self):\n        self.test_snarf.close()\n\n    @unittest.skip(\"todo\")\n    def test_promotion_campaign(self):\n        config[\"promotion_method\"] = \"campaign\"\n        assert self.test_snarf.promotion(), \"unable to apply promotion: campaign\"\n\n    @unittest.skip(\"todo\")\n    def test_promotion_trial(self):\n        config[\"promotion_method\"] = \"trial\"\n        assert self.test_snarf.promotion(), \"unable to apply promotion: trial\"\n\n    @unittest.skip(\"todo\")\n    def test_promotion_user(self):\n        config[\"promotion_method\"] = \"user\"\n        assert self.test_snarf.promotion(), \"unable to apply promotion: user\"\n\n    @unittest.skip(\"todo\")\n    def test_promotion_grandfather(self):\n        config[\"promotion_method\"] = \"grandfather\"\n        assert self.test_snarf.promotion(), \"unable to apply promotion: grandfather\"\n\n############################################################################################\n\nif __name__ == '__main__':\n    unittest.main()"
  }
]