Full Code of skeetzo/onlysnarf for AI

master eaecdf5fb025 cached
145 files
611.6 KB
140.5k tokens
803 symbols
1 requests
Download .txt
Showing preview only (651K chars total). Download the full file or copy to clipboard to get everything.
Repository: skeetzo/onlysnarf
Branch: master
Commit: eaecdf5fb025
Files: 145
Total size: 611.6 KB

Directory structure:
gitextract_pdkr1vka/

├── .gitignore
├── CHANGELOG.md
├── LICENSE.txt
├── MANIFEST.in
├── OnlySnarf/
│   ├── __init__.py
│   ├── __main__.py
│   ├── classes/
│   │   ├── __init__.py
│   │   ├── discount.py
│   │   ├── element.py
│   │   ├── file.py
│   │   ├── message.py
│   │   ├── poll.py
│   │   ├── profile.py
│   │   ├── promotion.py
│   │   ├── schedule.py
│   │   └── user.py
│   ├── conf/
│   │   ├── config.conf
│   │   ├── test-config.conf
│   │   └── users/
│   │       └── example-user.conf
│   ├── elements/
│   │   ├── __init__.py
│   │   ├── driver.py
│   │   ├── login.py
│   │   └── profile.py
│   ├── lib/
│   │   ├── __init__.py
│   │   ├── config.py
│   │   ├── driver.py
│   │   ├── ffmpeg.py
│   │   └── menu.py
│   ├── server/
│   │   └── api.py
│   ├── snarf.py
│   └── util/
│       ├── __init__.py
│       ├── args.py
│       ├── colorize.py
│       ├── config.py
│       ├── defaults.py
│       ├── logger.py
│       ├── optional_args.py
│       ├── settings.py
│       └── validators.py
├── Pipfile
├── README.md
├── bin/
│   ├── aws-setup.sh
│   ├── clean.sh
│   ├── demo-scripts.sh
│   ├── drivers/
│   │   ├── check-chrome.sh
│   │   ├── check-firefox.sh
│   │   ├── check.sh
│   │   ├── fix-chromedriver.sh
│   │   ├── fix-firefox-profile-error.sh
│   │   ├── install-chrome.sh
│   │   ├── install-chromedriver-aws.sh
│   │   ├── install-chromedriver-rpi.sh
│   │   ├── install-firefox.sh
│   │   ├── install-geckodriver-arm.sh
│   │   ├── install-geckodriver-rpi.sh
│   │   └── switch-firefox.sh
│   ├── install.sh
│   ├── run-tests.sh
│   ├── save.sh
│   ├── start-api-dev.sh
│   ├── start-api.sh
│   ├── test-all.sh
│   ├── test-api-remote.sh
│   ├── test-api.sh
│   ├── test.sh
│   ├── update-and-start.sh
│   ├── upload-test.sh
│   ├── upload.sh
│   └── virtualenv.sh
├── notes/
│   ├── Self-serving an ARM build
│   ├── adding-phantomjs.py
│   ├── animal.py
│   ├── docstrings.py
│   ├── login-state.py
│   ├── notes1.py
│   ├── notes2.py
│   ├── notes3.py
│   ├── notes4.py
│   ├── notes5.py
│   ├── notes6.py
│   ├── old/
│   │   ├── bot.py
│   │   ├── config.py
│   │   ├── file-old.py
│   │   ├── google-old.py
│   │   ├── remote.py
│   │   ├── removed-args.md
│   │   ├── removed-cron.py
│   │   ├── removed-readme.md
│   │   ├── removed-selenium-options.py
│   │   ├── removed_args.py
│   │   └── selectstuff.py
│   ├── onlysnarf_api.service
│   ├── scroll-notes.py
│   ├── selenium-notes.py
│   ├── selenium-notes1.py
│   ├── supervisor-api.txt
│   ├── testflask.py
│   ├── testflaskclient.py
│   ├── testrunners.py
│   ├── unittest.py
│   ├── unittestskips.py
│   ├── unittesttestrunners.py
│   └── windows-python-setup.txt
├── public/
│   └── docs/
│       ├── help.md
│       └── menu.md
├── setup.cfg
├── setup.py
└── tests/
    ├── __init__.py
    ├── api/
    │   ├── __init__.py
    │   └── test_api.py
    ├── selenium/
    │   ├── __init__.py
    │   ├── browsers/
    │   │   ├── __init__.py
    │   │   ├── test_brave.py
    │   │   ├── test_chrome.py
    │   │   ├── test_chromium.py
    │   │   ├── test_edge.py
    │   │   ├── test_firefox.py
    │   │   ├── test_ie.py
    │   │   └── test_opera.py
    │   ├── reconnect/
    │   │   ├── __init__.py
    │   │   ├── test_brave.py
    │   │   ├── test_chrome.py
    │   │   ├── test_chromium.py
    │   │   ├── test_edge.py
    │   │   ├── test_firefox.py
    │   │   ├── test_ie.py
    │   │   └── test_opera.py
    │   ├── test_browsers.py
    │   ├── test_reconnect.py
    │   └── test_remote.py
    ├── snarf/
    │   ├── __init__.py
    │   ├── auth/
    │   │   ├── __init__.py
    │   │   ├── test_google.py
    │   │   ├── test_onlyfans.py
    │   │   └── test_twitter.py
    │   ├── test_auth.py
    │   ├── test_discount.py
    │   ├── test_expiration.py
    │   ├── test_message.py
    │   ├── test_poll.py
    │   ├── test_post.py
    │   ├── test_schedule.py
    │   └── test_users.py
    ├── test_profile.py
    └── test_promotion.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage
log/
geckodriver.log

# production
/build
/dist
OnlySnarf.egg-info

# misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*

# python
/venv
__pycache__
.pytest_cache

# credentials
OnlySnarf/conf/users/alexdicksdown.conf


================================================
FILE: CHANGELOG.md
================================================
# Changelog  

**0.0.1 : 9/25/2018**
  - code organized
  **0.0.2 : 10/20/2018**
  - python package organized
  **0.0.3 : 1/14/2019**
  - sync with DBot updates; while loop upload
  **0.0.4 : 1/21/2019**
  - upload fix & hashtagging
  **0.0.5 : 1/29/2019**
  - demo
  **0.1.0 : 2/3/2019**
  - menu
  - package & setup.py
  **0.1.1 : 2/4/2019**
  - menu fixes
  **0.1.2 : 2/7/2019**
  - config.py
  - script names updated
  - readme updated
  **2/9/2019**
  - fuck you PyDrive
  **0.1.3 : 2/24/2019**
  - jpeg
  **0.1.4 : 3/4/2019**
  - mount path
  **0.1.5 : 3/7/2019**
  - updated send_post_button refs
  **0.1.6 : 3/19/2019**
  - module separation
  **0.1.7 : 3/28/2019**
  - settings.py
  - user.py
  **0.1.8 : 3/31/2019**
  - debugging
  - Drive API for mp4 downloads
  **0.2.0 : 4/10/2019**
  - User: read_chat
  **0.2.1 : 4/12/2019**
  - upload performer
  - upload scene
  **0.2.2 : 4/15/2019**
  - settings now actually updates
  - settings globals -> class
  **4/16/2019**
  - fucking default variables
  **1.0.0 : Production : 4/22/2019**
  - save image_name instead of path
  - uploaded to pip
  **1.0.1**
  - removed video.mp4
  **1.0.2**
  - minor adjustments
  **1.0.3 : 5/3/2019**
  - minor bug fixes
  **1.0.4 : 5/8/2019**
  - more minor bug fixes
  **1.1.0 : 5/12/2019**
  - added: (settings).MOUNT_DRIVE, ROOT_FOLDER, DRIVE_FOLDERS, CREATE_MISSING_FOLDERS
  - create Google folder structure programmatically
  - predefine Google root
  **5/14/2019**
  - replaced: settings.TYPE - settings.ACTION
  - added: settings profile -> skeetzo
  - updated: scenes to include trailer addition
  **1.1.1 : 5/23/2019**
  - fixed tweeting bug
  - todo priority queue
  **1.1.2 : 5/25/2019**
  - added: cron.py
  - |_ needs a menu system to be added
  **1.1.3 : 6/26/2019**
  - fixed file & directory uploads
  - removed config initialization from google.py
  - updated ReadMe
  **1.1.4**
  - fixed MANIFEST and credentials
  **1.1.5**
  - removed credentials
  **1.1.6**
  - relative imports -> absolute
  - fixed messaging: input price
  **1.1.7 : 7/3/2019**
  - collapsed upload_file & upload_directory into upload_to
  **1.1.8 : 7/19/2019**
  - fixed user scrape css
  **1.1.9 : 8/14/2019**
  - fixed user scrape css again
  **1.1.10 : 8/21/2019**
  - really really fixed user scrape css
  - removed apiclient from setup.py
  **1.2.0 : 9/2/2019**
  - fixed user scrape & cache
  - added promotions (unfinished)
  **1.2.1 : 9/4/2019**
  - removed innate debug profiles and added profile.conf
  - added google creds to onlysnarf-config
  - updated readme to reflect creds process
  **1.2.2 : 9/5/2019**
  - fixed user scrape & messaging
  - fixed messaging by username
  **1.3.0 : 9/8/2019**
  - finished testing promotions- unworkable w/o email or clipboard utility
  - finished testing messages & user selection
  - hidden unworking functions in menu w/o debug
  **1.3.1 : 9/9/2019**
  - error messages cleanup in user messaging
  **1.3.2 : 9/10/2019**
  - submit button works again
  - added: way to select google drive file to message
  **1.3.3 : 9/15/2019**
  - package install fix & dynamic version in menu
  **1.3.4 : 9/15/2019**
  - menu cleanup
  **1.4.0 : 9/15/2019**
  - chromedriver binary version set = 77.0.3865.40
  - added catch for image upload error
  **1.4.1 : 9/16/2019**
  - cleaned print & maybePrint outputs
  - cleaned up settings.py
  - cron cleanup
  **1.4.2 : 9/17/2019**
  - text fix
  - fixed verbose output
  **1.4.3 : 9/21/2019**
  - dbot issue
  **2.0.0 : 9/25/2019**
  - added functionality to choose instead of random
  **2.0.1**
  - oops
  **2.1.0 9/29/2019**
  - discount: all or select users x% for n months
  - fixed local user load
  - updated User(mess=mess) -> User(data)
  **2.1.1**
  - shameless 2.1.0 fix
  **2.2.0**
  - cleaned up & mostly fixed release via selection
  - cleaned up release via random
  - nonrandom uploads now confirm entered information
  - cleaned out user methods from driver -> static
  - updated download_performers and seperated randomizing selection
  **2.2.1**
  - release: keywords and performers can be deleted
  **2.2.2**
  - standalone script bug
  **2.2.3**
  - removed pointless User cache -> fixed User count bug
  - removed overwrite-local
  - added BROWSER.url checks
  **2.2.4**
  - user selections -> debug
  **2.3.0**
  - added: post
  **2.4.0**
  - settings cleanup
  - release -> upload
  - onlyfans var cleanup
  - added local input
  - added posts beginning -> needs configparser
  **2.5.0 : 10/5/2019**
  - configparser -> profile.conf updated
  - posts & text prompt
  - functionality to create a post w/ text
  |_ create multiple basic posts such as "greetings" or "going on holiday" or a trip, or question of what to post more of?
  |_ a menu of standardized posts like above
  |_ a menu of questions|greetings to message to users
  - added: easier way to select local file to upload
  **2.5.1**
  - more verbose cleanup
  **2.6.0**
  - config cleanup
  |_ profile.conf & posts.conf & config.json -> config.conf
  - added: cron feature for adding, deleting, listing crons
  - added: Twitter login prompt
  - added: `local` setting
  - added check for failed login
  - added post: "OnlySnarf Bot commands: !pic | !pic dick | !pic ass"
  **2.6.1**
  - creds cleanup
  **2.7.0**
  - menu sort
  - onlysnarfpy
  **2.7.1**
  - driver auth fix
  - cron fix
  **2.8.0 : 10/8/2019**
  - added Expiration
  - added Schedule
  - added: Poll
  - upload a gallery to a message
  **2.9.0**
  - a post that advertises custom requests
  - a post that advertises tipping price for messaging
  - a post that advertises prices for paid messages for individual photo requests
  - a post for requesting people to comment or dm me individuals they'd like to see me with
  - a post thanking followers for being followers
  **2.9.1**
  - args fix: keywords, performers, input
  **2.9.2**
  - text fix
  **2.9.3**
  - add video reduce to somewhere it can impact INPUT files
  **2.9.4**
  - input bug?
  **2.10.0**
  - discount css update
  - message image upload fix
  - added gifs
  **2.11.0**
  - OFKEYWORD - specifies random folder
  **2.11.1**
  - oops
  **2.11.2**
  - oops
  **2.11.3**
  - oopsies
  **2.11.4**
  - more oopsies
  **2.12.0**
  - added setting: skip-backup
  **2.12.1**
  - fixed BYKEYWORD bug preventing random upload
  - fixed messaging folder of images
  **2.13.0**
  - fixed: skip-backup
  - added: skip-delete-google
  **2.13.1**
  - fixed fixed: skip-backup
  - fixed: enter upload
  **2.13.2**
  - updated: google mimetypes
  **2.14.0**
  - added: NOTKEYWORD for excluding folders by keyword
  **2.14.1**
  - updated: upload_to_OnlyFans w/ more error checks in attempt to fix below bug
  - BUG: does not find "send_post_button" from chromebook ubuntu laptop using ChromeDriver 74.0.3729.6 | Google Chrome 74.0.3729.131
  - cleaned up settings.py comments
  - added: remember me upon login is checked, does nothing
  **2.14.2**
  - added: error_checker for not found elements
  **2.14.3**
  - fixed: user scrapes
  - updated: config.py, unable to test
  **2.14.4**
  - class reorg
  - added 'dynamic' element searching
  - fixed css elements for major functions
  - added element.py
  **2.15.0**
  - major functionality restored
  **2.15.1**
  - fixed menu
  - added -version flag
  **2.15.2**
  - settings options debugging
  - added: tabbing to inputs
  - undid tabbing to inputs -> unpredictable odd behavior
  - minor random debugging (??wtf?)
  - xmas scripts
  **2.15.3**
  - better sorted test scripts
  - changed error_window:filename fix to just closing window
  - fixed mimetype upload (sorta)
  **2.15.4**
  - updated test scripts output
  - added setting: verbosest
  - cleaned up driver&profile elements
  **2.15.5**
  - more settings prep
  - minor fixes to login
  **2.16.0**
  - fixed: post
  - updated menu.md
  **2.16.1**
  - fixed: browser closes now
  **2.16.2**
  - more Settings integration
  - updated method of messaging all, recent, favorite
  **2.16.3**
  - OnlySnarf classname -> Snarf
  - updated profile init
  **2.16.4**
  - minor bugs
  **2.16.5**
  - updated BYKEYWORD and NOTKEYWORD -> str != str
  **2.16.6**
  - fixed: message all price submit
  - fixed: random files now properly downloaded, again
  **2.16.7**
  - fixed: messageAll
  - fixed: go_to_page
  **2.16.8**
  - update: go_to_* auths first
  - added: following_get
  **2.16.9**
  - update: users_get, following_get -> speed +, reliability +
  - mostly functional
  **2.16.10** debugging pre 2.17.0
  - upload -> post
  - Message, File, Google_File, Google_Folder, Video, Image classes
  - ffmpeg.py
  - login function cleaned up / spawn_browser
  - settings -> argsparse
  - menu cleaned up
  - file system selection cleaned up
  - category
  **2.17.0**
  - massive spaghetti -> api overhaul
  - new menu
  **2.17.1**
  - fixed packaging
  **2.17.2**
  - fixed post/message w/o prompt
  **2.17.3**
  - fucking a
  **2.17.4**
  - fixed google uploads w/o prompt
  **2.17.5**
  - fixed video extensions
  **2.17.6**
  - increased UPLOAD_MAX_DURATION to 6 hrs
  - minor fixes to menu input
  **2.17.7**
  - fixed backup pathing
  - fixed file upload max
  **2.17.8**
  - added remote webdriver operations
  - added firefox
  - add performers; debugging
  **2.17.9**
  - debugged 2.17.8
  **2.17.10**
  - removed moviepy
  - exit(1) when missing driver
  **2.17.11**
  - fixed messaging a user
  - fixed performer operations
  **2.17.12**
  - minor fixes to launching firefox
  **2.17.13**
  - fixed post uploads with random content
  - updated onlysnarf-config
  **2.17.14**
  - herpderp
  **2.17.15**
  - herpaderpaderp
  - fixedfixed uploading
  **2.17.16**
  - herpaderpaderpa
  - fixed more uploading prompts
  **2.17.17**
  - debugged firefox
  - debugged Profile (a bit)
  - debugged backup content
  - args: added username_account to differentiate from twitter username for login
  - args: added source & destination
  - added remote ssh dir
  - added: login source [onlyfans|twitter]
  **2.17.18**
  - oops; fixed firefox "binary"
  **2.17.19**
  - User: following_get, following_write (still needs debugging)
  - remote: updated connection priorities; auto -> form -> twitter -> google
  - login: google; needs debugging
  **2.17.20**
  - oops, disabled auto_reconnect until debugging
  **2.17.21**
  - minor fixes to menu
  **2.18.0**
  - menu updates
  - profile / settings updates
  - cleaned up menu.md
  - updated profile: sync from, sync to, backup
  - updated login methods
  - added: delete-empty folders; properly remove empty folders that all images have been removed from when backing up / moving files
  - debugged: google login
  - debugged: following_write
  -> remote webserver behavior
  - added: session_id, session_url
  - added: remote-chrome, remote-firefox, auto-remote
  - added: reconnect
  - added: session_id & session_url -> session.json for reconnecting to existing browser sessions
  **2.18.1**
  - debugged: redundant category asking
  - debugging: local
  - updated: tests
  **2.18.2**
  - debugged: local
  - create-drive -> create-missing
  - debugging: remote
  **2.18.3**
  - failed expires/poll/schedule ends post
  - fixed date validator
  - debugged: schedule, date, time
  - debugged: post schedule
  - debugging promotion: updated promotion args
  - debugged: discount
  - debugging: promotion (mostly)
  **2.18.4**
  - debugged: promotion- free trial (ish)
  - debugging: promotion- campaign
  **2.18.5**
  - debugged: promotion- campaign
  - debugging: settings
  **2.19.0**
  - added: tabs behavior
  - added: cookies - wow i'm a fucking idiot for not adding this sooner
  - debugging: bot
  - properly tested: settings get
  - properly testedish: settings set
  **2.20.0**
  - added: bot functionality - menu prompt, tip parsing
  **2.20.1**
  - updated: saving session_id and session_url
  - more bot debugging
  - fixed bin/install-firefox.sh: update for processor
  **2.20.2**
  - more debugging
  - fixed: file input
  - debugging: grandfathered
  **3.0.0 : Bot Experiments : 9/21/2020**
  - major updates to browsers debugged
  - added: grandfather promotion
  - added: user lists (finally) - favorites, bookmarks, friends, etc
  - fixed: performer uploads
  - added: specify inner category for performers via 'category-performer'
  - added: fetch file by 'sort' - random|ordered
  -> Bot
  - bot functionality to check posts for tips
  - automatically heart / send dick pics to tips in messages
  **3.0.1**
  - added argument error catch
  **3.0.2**
  - documentation started
  **3.0.3**
  - selenium version 3.141.1 -> 3.141.59
  - bin/install-firefox version 26 -> 29
  **3.0.4**
  - jk no selenium bump...
  **4.0.0 : Flask & React : 3/24/2021**
  - flask-react integration and folder restructure
  **4.0.1 : 3/25/2021**
  - combined args: download_max & upload_max -> image-limit
  - added arg: delete (from delete_google)
  - removed: all cron references
  - changed: output print to log and uppercase to lowercase, except for menu cli
  **4.0.2 : 4/14/2021**
  - added test skeletons
  **4.0.3 : 12/6/2021**
  - removed react shit...
  - cleaned up dir structure; needs updates to package links
  **4.0.4 : 12/8/2021**
  - cleaned up snarf.py staticness
  - updated test_snarf
  **4.1.0 : Beginning Phase Out : 2/19/2022**
  - removed all the flask stuff that was being added
  - updated readme
  - dropped prices to free account
  - grandfathered everyone currently to a free amount a while ago
  - removed paid account $ structure
  - add flask gui for onlysnarf, etc -> submodules -> idea moved to next encompassing project -> ?
  - review setup / config
  - removed all email notifications implementations
  - added easier on off toggle states
  - checked DD writeup / ended project, elaborated on crypto payments and current market forewarning w/ fans
  - completely removed cron features
  - checked / cleaned content folders -> organize for free model funnel
  - cleaned up social links + snapchat
  - updated from.package imports to be shorter -> properly add to __init__.py files
  **4.1.1 : 3/10/2022**
  - take a look at AVN stars, maybe (re) set up profile, bio, socials etc and integrate ---> nah, too lazy
  **4.1.2 : 7/15/2022**
  - added docstring comments for menu.py
  - moved config baseDir -> "/HOME/$USER/.onlysnarf"
  - removed google & dropbox (finally)
  - fixed action: Settings -> now sets values again
  **4.1.3 : 8/23/2022**
  - begin testing finally yay
  - moved saving configs & user configs & session id & cookies to .onlysnarf
  - added method for reading profiles from conf/users / .onlysnarf/users
  **4.1.4 : 8/29/2022**
  - finished first login test
  - removed 'email' from config for fetching username for login
  **4.1.5 : 8/31/2022**
  - added 'debug-firefox' to args for enabling trace logging
  - added 'debug-selenium' to control logging
  - finished test_users
  - added temporary fix for boolean bug: using "True" and "False" strings instead of booleans
  **4.1.6 : 9/1/2022**
  - finished debugging test_discount
  **4.1.7 : 9/5/2022**
  - updates code and docstrings in messages.py; left off in file.py 
  - added classes for enums
  - added beginnings of IPFS 
  **4.1.8 : 9/7/2022**
  - more code cleanup; debugging process for messages & posts uploading files
  - switched git branch to development to break things less
  **4.1.9 : 9/8/2022 : God save the Queen**
  - added xmas test; need to add xmas shnarfs for testing
  - cleaned up more test code, still not much headway on uplading a file
  - began updates for rest of old sh test scripts into python test scripts
  **4.1.10 : 9/9/2022**
  - updated menu.md, updated removed_args.py
  - cleaned up args & commands & docs of such
  - add / ensure all default values to config.conf
  - cleand up config files and example
  - cleaned up dir structure references across project
  **4.2.0 : 9/12/2022**
    - finished debugging test_message & test_post; uploading files works again
    - tested changes made from removing / cleaning up args and commands
    - cleaned up tests (broke again)
  **4.2.1 : 9/13/2022**
    - mostly finished debugging (again): test_message
    - more finishing touches to uploading post & message (and rebroken fixed things)
    - major updates / fixes to browser creation flow / attempts to fix reconnect bug
    - fixed issue in lib/driver with media upload popup from multiple of the same file --> updated error window close
  **4.2.2 : 9/14/2022**
    - finished testing test_message and test_post (again)
    - added tests for selenium browser configurations
    - mostly finished updating expiration, poll, schedule new .get() return dict({})
    - mostly finished testing: test_discount (again), test_poll, test_schedule
    - major updates to classes/schedule & util/settings for proper datetime manipulation
    - added new tests for schedule variables
  **4.2.3 : 9/15/2022, 9/18/2022**
    - more updates to debugging schedule & poll
    - continued finalizing sufficient OK testing responses
  **4.2.4 : 9/19/2022**
    - more debugging schedule & poll, reconnect
    - added tests for trying different browsers, reconnecting, keeping open, remote sessions
    - schedule tests pass they just don't set the right hour
    - major snarf tests all OK (minus poll)
  **4.3.0 : 9/20/2022, 9/21/2022**
  - updated selenium, google chrome, & firefox geckodriver versions
  - driver updates to accomodate selenium version changes
  - changed Driver back to a basic class instead of all static, needs more debugging (again)
  - more individual tests for messages
  - mostly OK on basic tests
  - reconnect works again for chrome
  **4.3.1 : 9/22/2022**
  - more test debugging and finalizing basic OKs
  - browser reconnect reconnects to browser / retains session
  - debugging cookies somewhat saving login session
  - finished test for cookies; finished debugging cookies
  - finished debugging browser reconnect completely (maybe)
  **4.3.2 : 9/22/2022**
  - mostly finished debugging schedule
  - preparing for pypi upload version bump
  - almost done completely debugging basic snarf functionality
  **4.3.3 : 9/24/2022**
  - updated readme
  - update & test pypi upload process
  - updated / checked pypi config
  - mostly finished / updated tests: all OKs
  - reorganized tests for grouping
  **4.3.4 : 9/26/2022**
  - fixed driver: schedule hours not being set now work again
  - reorganized schedule in prep for individual component testing
  - finished debugging schedule (date & time)
  - fixed driver: poll button not being clicked and rest of poll functionality
  - finished debugging poll
  - updated cookie process to check if logged in from session data before overwriting existing cookies and re logging in
  - fixed message price not entering 
  **4.3.5 : 9/27/2022**
  - added text clear from post to message
  - added snarf pic to readme
  - completely finished debugging basic snarf functionality
  - ran full tests suite before final upload to pypi
  **4.3.6 : 10/2/2022**
  - update / check '-help' output; add to readme
  - ensure docs/menu.md is properly updated
  **4.3.7 : 10/4/2022**
  - added subcommands to -help
  - changed 'questions' to 'poll'
  - reorganize tests as necessary (none)
  **4.3.8 : 10/5/2022**
  - finished cleaning up class/user
  - cleaned up user class & simplified current methods for selecting user(s) aka removed prompts for now
  - restructured class/discount and how users are passed via args
  - updated message for new way of handling users passed via args
  **4.3.9: 10/6/2022**
  - added 'min' and 'max' to arg inputs: price, expiration, duration, amount, months, limit
  - changed 'poll' args back to 'question'
  - prepared commands for generating previews to record functionality with ala: "onlysnarf discount -user random"
  **4.3.10: 10/7/2022**
  - finished adding docstrings to classes/user.py
  - added subcommand for fetching users
  - reupdated menu.md w/ pruned config & args
  - updated help.md
  - more debugging for new subcommand structure
  **4.3.11: 10/8/2022**
  - updated method of importing config/args to allow for full subcommand testing via pytest by adding shim for args
  **4.3.12: 10/10/2022**
  - fixed tab handling in driver
  - debugged & tested newly added subcommand structure: discount, message, post --> snarf.py
  - beginning recordings for behavior previews
  **4.4.0: 10/11/2022**
  - fixed args & config overwrite direction
  - recorded new videos for demos
  - updated preview gifs of behavior for readme w/ OBS: discount, message, poll, post, schedule, users
  - cleaned up config files w/ final changes
  - added previews to readme
  - updated user config explainer to readme
  - cleaned up packages
  - cleaned up classes/files to keep up with gutting google, etc; removed Remote & Bot and saved in notes/old
  - doublechecked code for missing docstrings... aka finished cleaning up code (wow go me)
  - double checked / re-enabled performers & tags functionality
  - updated help.md and menu.md with new text changes
  - fixed driver & message actually sending... haha and discount applying.... woops
  - synced with main/master branch
  - uploaded working changes to pypi
  **4.4.1: 10/12/2022**
  - minor text changes
  - fixed file upload when posting (of course this would still be semi broken after publishing changes)
  **10/13/2022**
  - more minor text changes
  - changed text: bin/google* --> bin/chrome*
  **4.4.2: 10/14/2022**
  - fixed args validator for duration's "min" "max"
  - debugging project deployment & installer scripts for web browsers
  **4.4.3: 10/15/2022**
  - add a way for installation to work for webdrivers for pypi
  - added: webdriver_manager; cleaned up driver spawn code and packages : https://pypi.org/project/webdriver-manager/
  - debugging webdriver install processes on rpi4
  - added browser options to help with debugging on rpi: brave, chromium, ie, edge, and opera
  - added tests for new browser options
  **4.4.4: 10/20/2022**
  - continued debugging attempts for browsers on rpi4
  - added notes for debugging browsers
  **4.4.5: 10/20/2022**
  - added travis.cli config
  - connected travis to github
  - more driver debugging for added webmanager autoinstalls
  **4.4.6: 10/27/2022**
  - added travisci for testing python versions & os installs
  - more rpi debugging attempts; added attempt scripts
  **4.4.7: 3/17/2023**
  - upgraded selenium to 4.0
  - prep for project cleanup and python update
  - pruned prompts
  - fixed webdriver manager configurations for most browsers: brave, chrome, chromium, and firefox
  **3/18/2023**
  - fixed cookies for chrome but not firefox
  - rpi4 testing and prep for selenium cleanup
  - added a way for installation to work for webdrivers for pypi
  **4.4.8: 3/20/2023**
  - added check for rpi processor for chrome only
  - finished testing new browser changes
  - finished debugging web browser on rpi4
  - checked current instructions for installing from github
  - updated to python10
  - updated install scripts and organize by usability by platform; distinguish arm scripts for rpis
  - finished debugging new webdriver manager system
  **4.4.9: 3/21/2023**
  - fixed unknown bug when fetching random user
  - fixed applying discounts and updated min/max tests for discounts 
  - fixed messaging and posting 
  **3/22/2023**
  - fixed poll and schedule
  - pytest bug w/ final arg
  - updated any webscraping as necessary
  - added tests for alternate logins (that probably won't work anyways *cough* google)
  - begin prepping for merging new changes to main and publishing to pypi
  **4.4.10: 3/23/2023**
  - completely finished fixing schedule
  - super duper verified test results
  - full test coverage
  - merged w/ main
  - published changes to pypi
  **4.4.11**
  - fixed 'onlysnarf' cmd references
  - removed nonworking browser references in optional args
  - fixed discount bug
  **4.4.12: 3/24/2023**
  - RPi4 debugging
  - fixed element bug when posting
  - fixed users
  - fixed error message on close
  **4/15/2023**
  - cleaned up git repo size / long clone time
  **4.4.13: 4/17/2023**
  - Windows compatability testing
  - updated pathings for Windows
  - retested google login (remains disabled)
  **4.4.14 : 5/29/2023**
  - beginning readd of cli menu
  - switch from pyinquirer to inquirer
  **4.4.15 : 6/2/2023**
  - fixed cookies bug
  **4.4.16 : 7/5/2023**
  - update readme and help&menu docs / added personal touchups
  - fixed get random user for discount test 
  **4.5.0 : 7/11/2023**
  - added wget functionality to input for when a url is provided
  - cleaned up bin/test scripts
  - added basic api setup
  - added test scripts for flask & api
  - beginning modifications for receiving api calls
  **4.5.1 : 7/12/2023**
  - fixed package req: validators
  - added modifications for running via api
  **4.5.2 : 7/16/2023**
  - relocated api structure for testing
  - added tests for flask api
  - updates to tests, code flow for missing config / args values
  - added individual message funcationality tests 
    **7/17/2023**
  - continued debugging message tests
  - fixed random user functionality
  - updated driver.poll
  - fixed new message tests
  - added flask to package reqs
  - updated install script
  - updated api scripts to route through snarf
  **4.5.3,4,5,6 : 7/30/2023**
  - api debugging
  **4.5.7,8 : 8/2/2023**
  - fixed twitter login; added phone number to args&config
  - more api debugging w/ aws
  - updated date&time formats
  - debugged api: /message & /post
  **4.5.9 : 8/3/2023**
  - added update script meant to be run by systemd service script
  - added config script; requires testing
  **4.5.10 : 8/4/2023**
  - moved api & menu to cli
  - updates to config script
  - tested new config script
**4.6.0,1 : 8/7/2023**
  - minor version bump for working api & cli changes
**4.6.2 : 8-17-2024**
- adjusted manifest to include config files
- updated config subcommand to reset base config file via 'Reset'

------------------------------------------------------------------------------------

## TODO

- add cli args for config to autoconfigure more easily
- update 'snarf config' to interact with main config file and variables

- look into Marshmellow package for class / object cleanup


- add smart idea for getting statement information
- add better version notes to readme's list of "works on"
- re-add stuff for testing on multiple platforms ala mac ;) 

- double check how tags & performers are implemented in config and text config and then re-add to docs

- finish updating image/video downloading
- finish updating cli menu functionality
- finish updating profile class & menu

- add bypass for 2fa
https://www.geeksforgeeks.org/two-factor-authentication-using-google-authenticator-in-python/
https://stackoverflow.com/questions/55870489/how-to-handle-google-authenticator-with-selenium
https://stackoverflow.com/questions/8529265/google-authenticator-implementation-in-python

(review usability and code first)
-> OnlyFans: Promos
- clean up / fix & test
- add min/max to args & validators
- re-enable / add promo subcommands and config variables

-> OnlyFans: Profile
- new - setup - Twitter -> profile, banner; Price and Settings
- new - advertise
- new - posts - tweet to advertise new account, tweet to ask about what you should post, etc; recommend what to post
- need to add 'create' to Profile for asking for profile settings when syncing to
- add config for profile templates when testing profile features again
- add tests for profile integration / behavior
- re-enable / add profile subcommands and config variables

-> OnlyFans
- add quiz & target interactions (onlyfans buttons)
- add functionality that follows profiles that are free for a month
- update schedule, date, and time args to accept strings aka "1 day" or "1 day 2 hours"
- update time to accept strings that modify to add to current time aka "+2" or "2 hours" adds 2 hours to the current time

(once everything else in app works again)
- run new auth tests w/ appropriately connected accounts
-> Twitter
- actually test if tweeting behavior works in driver
- needs a dummy account to test actual tweeting w/
- tweet reminders from inlaid config behavior
- can enter and edit the final text that is tweeted
- can include media attachments
-- add checks for previously existing tweets
-- keep track of tweets (somehow)

-> Tests
- separate driver functions into individual components ala schedule --> individual steps; for easier testing (and to clean up the giant ass driver file)
- add tests for newly separated driver files / functions
- add tests for additional config variables such as browser and image/video options, limits
- finish adding tests for individual messaging circumstances: all, recent, favorite, renew on
- finish adding tests for individual message entry parts, individual post entry parts
- 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

(webdriver)
- (if necessary) finish integrating edge, ie, and opera
- figure out how to request specific webdriver versions installs to test v102 for edge

-> CLI Menu
(probably never)
- re-add menu system
- fix any new cli menu errors made while updating major processes
- re-enable prompting for discount amount&months in Settings or somewhere else (at some point)
- re-add removed user select code in notes/selectstuff.py (for menu prompts)
- re-enable menu command

## Fix / Debug

- 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

(unlikely to be fixed soon, if ever)
- google login: unsafe browser warning --> possibly end of usability --> should I just remove this? form login works, twitter login works (i think)
-- maybe just cut out / leave as is until can debug "unsafe browser" issue?

- debug: discover the cause of the super slow web scraping
-- probably not: debug_delay
---- possibly improved via recent updated coding? (4.3.10)

- figure out how to suppress the chrome stacktrace debugging messages
- fix driver.firefox: DeprecationWarning: service_log_path has been deprecated, please pass in a Service object

### Browser Changes

working: brave, chrome, chromium, firefox 
not working: edge, ie, opera

existing browsers: chrome, firefox
added new browsers: brave, chromium, ie, edge, and opera
other potential browsers: phantomjs (requires node), safari (requires python2.7)

https://pypi.org/project/webdriver-manager/
https://stackoverflow.com/questions/58686471/how-to-use-edge-chromium-webdriver-unknown-error-cannot-find-msedge-binary

notes:

#### edge:
requires: msedge-selenium-tools
- might only work for selenium v102
- might only work on Windows
"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."
https://stackoverflow.com/questions/72773330/when-running-selenium-edge-in-pyton-getting-sedgedriver-exe-unexpectedly-exite


#### ie:
- might only work on Windows
https://stackoverflow.com/questions/49787327/selenium-on-mac-message-chromedriver-executable-may-have-wrong-permissions

#### opera:
- might have a version limit requirement

updating permissions didn't work:
chown -R ubuntu /home/ubuntu/.wdm/drivers

what helps in general:
>> using correct webdriver options generator
>> specifying binary paths
>> correct permissions on binary paths

# API

note: the 

# Bugs

**4.1.4**
  - boolean checks from "Settings.is_" functions are failing: replaced with redundant string checks for if == "True"
**4.3.12**
  - message: drag&drop has decided to occasionally stop working; maybe a selenium version issue?
**4.4.0**
  - discount: amount&months still require 2 passes on average to update values correctly
**4.4.6**
  - 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
**4.4.9**
  - 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    
**4.5.2**
  - [fixed] message tests come up negative when they're all passing basic functionality



# Web Browser Versions
(no longer as relevant as of 4.4.7ish updates)

Version Check:
stable => Google Chrome 106.0.5249.40 beta
beta => Google Chrome 106.0.5249.40 beta
binary => Version: 106.0.5249.21.0
geckodriver => geckodriver 0.31.0 (b617178ef491 2022-04-06 11:57 +0000)


================================================
FILE: LICENSE.txt
================================================
MIT License

Copyright (c) 2018 Skeetzo

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: MANIFEST.in
================================================
# Config
include OnlySnarf/conf/*

exclude OnlySnarf/bin
exclude OnlySnarf/build
exclude OnlySnarf/dist
exclude OnlySnarf/log
exclude OnlySnarf/notes

# Include the README and CHANGELOG
include *.md

# Include the license file
include LICENSE.txt

================================================
FILE: OnlySnarf/__init__.py
================================================
from .snarf import Snarf

================================================
FILE: OnlySnarf/__main__.py
================================================
import sys
import os

def main(args=None):
    """The main routine."""
    if args is None:
        args = sys.argv[1:]
    os.system("python "+os.path.join(os.path.dirname(os.path.realpath(__file__)),'snarf.py')+" "+" ".join(args))

if __name__ == "__main__":
    try:
        main()
    except:
        print(sys.exc_info()[0])
        print("Shnarf!")
    finally:
        sys.exit(0)

================================================
FILE: OnlySnarf/classes/__init__.py
================================================


================================================
FILE: OnlySnarf/classes/discount.py
================================================
from ..lib.driver import Driver
from ..util.settings import Settings
from .user import User
##
from ..util.defaults import DISCOUNT_MAX_AMOUNT, DISCOUNT_MIN_AMOUNT, DISCOUNT_MAX_MONTHS, DISCOUNT_MIN_MONTHS

class Discount:

    """OnlyFans discount class"""

    def __init__(self, username, amount=None, months=None):

        """OnlyFans discount action."""

        self.amount = amount
        self.months = months
        self.username = username # the recipient username

    def apply(self):

        """
        Applies the discounted amount to the recipient username via Driver.discount_user

        If the targeted username is one of the matching keywords then all of the 
        matching recipients will be discounted. Values are determined by runtime args or prompted
        for.

        """

        Settings.maybe_print("discounting: {}".format(self.username))
        return Driver.discount_user(self.get())

    def get(self):
        """
        Get the discount's values in a dict.

        Returns
        -------
        dict
            A dict containing the values of the discount

        """

        return dict({
            "amount": self.get_amount(),
            "months": self.get_months(),
            "username": self.get_username()
        })

    def get_amount(self):

        """
        Populate and get the amount value

        If not found in args and prompt is enabled, ask for value.

        Returns
        -------
        int
            the discounted amount to apply

        """

        amount = self.amount or Settings.get_amount()
        if int(amount) > int(Settings.get_discount_max_amount()):
            Settings.warn_print("discount amount too high, max -> {}%".format(Settings.get_discount_max_months()))
            amount = int(Settings.get_discount_max_amount())
        elif int(amount) < int(Settings.get_discount_min_amount()):
            Settings.warn_print("discount amount too low, min -> {}%".format(Settings.get_discount_min_months()))
            amount = int(Settings.get_discount_min_amount())
        self.amount = amount
        return self.amount

    def get_months(self):

        """
        Populate and get the months value

        If not found in args and prompt is enabled, ask for value.

        Returns
        -------
        int
            the number of months to discount for

        """

        months = self.months or Settings.get_months()
        # check variable constraints
        if int(months) > int(Settings.get_discount_max_months()):
            Settings.warn_print("discount months too high, max -> {} months".format(Settings.get_discount_max_months()))
            months = int(Settings.get_discount_max_months())
        elif int(months) < int(Settings.get_discount_min_months()):
            Settings.warn_print("discount months too low, min -> {} months".format(Settings.get_discount_min_months()))
            months = int(Settings.get_discount_min_months())
        self.months = months
        return self.months

    def get_username(self):

        """
        Populate and get the username value

        If not found in args and prompt is enabled, ask for value.

        Returns
        -------
        str
            the username to discount

        """

        # if self.username: return self.username
        # self.username = Settings.get_user().username
        return self.username

    def grandfatherer(self, users=[]):

        """
        Executes the 'Grandfather' discount model

        If users is empty it is populated with users from the 'Grandfather' OnlyFans list in 
        the account. All 'Grandfather'ed users are provided with the max discount for the max months.

        Parameters
        ----------
        users : list
            list of users to 'Grandfather'

        """

        if len(users) == 0:
            users = User.get_users_by_list(name="grandfathered")
        print("Discount - Grandfathering: {} users".format(len(users)))
        self.months = DISCOUNT_MAX_MONTHS
        self.amount = DISCOUNT_MAX_AMOUNT
        # apply discount to all users
        for user in users:
            self.username = user.username
            print("Grandfathering: {}".format(self.username))
            try:
                Driver.get_driver().discount_user(discount=self)
            except Exception as e:
                print(e)

================================================
FILE: OnlySnarf/classes/element.py
================================================
# for easily interacting with changeable page elements

from ..util.settings import Settings
from ..elements.driver import ELEMENTS as driverElements
from ..elements.login import ELEMENTS as loginElements
from ..elements.profile import ELEMENTS as profileElements

ONLYFANS_ELEMENTS = []
ONLYFANS_ELEMENTS.extend(driverElements)
ONLYFANS_ELEMENTS.extend(loginElements)
ONLYFANS_ELEMENTS.extend(profileElements)

# represents elements the webdriver sortof looks for
# this class and the objects in th elements folder act as a half assed method for organizing the onlyfans interaction points
# it's an attempt to make things easier to parse but should be cleaned up at some point

class Element:
    def __init__(self, name=None, classes=[], text=[], id=[]):
        self.name = name
        self.classes = classes
        self.text = text
        self.id = id

    def getClass(self):
        if self.classes and len(self.classes) > 0:
            return self.classes[0]
        return ""

    def getClasses(self):
        return self.classes

    def getText(self):
        if self.text and len(self.text) > 0:
            return self.text[0]
        return ""

    def getTexts(self):
        return self.text

    def getId(self):
        if self.id and len(self.id) > 0:
            return self.id[0]

    def getIds(self):
        return self.id

    @staticmethod
    def get_element_by_name(name):
        Settings.dev_print("getting element: {}".format(name))
        if name == None:
            Settings.err_print("missing element name")
            return None
        global ONLYFANS_ELEMENTS
        for element in ONLYFANS_ELEMENTS:
            if str(element["name"]) == str(name): return Element(name=element["name"], classes=element["classes"], text=element["text"], id=element["id"])
        Settings.warn_print("missing element fetch - {}".format(name))
        return None

================================================
FILE: OnlySnarf/classes/file.py
================================================
import os, shutil, random, sys
from os import walk
##
from ..lib.ffmpeg import ffmpeg
from ..util.settings import Settings
import wget

###############################################################

class File():
    """File class for manipulating files."""

    ONE_GIGABYTE = 1000000000
    ONE_MEGABYTE = 1000000
    FIFTY_MEGABYTES = 50000000
    ONE_HUNDRED_KILOBYTES = 100000
    
    MIMETYPES_IMAGES = "(mimeType contains 'image/jpeg' or mimeType contains 'image/jpg' or mimeType contains 'image/png')"
    MIMETYPES_VIDEOS = "(mimeType contains 'video/mp4' or mimeType contains 'video/quicktime' or mimeType contains 'video/x-ms-wmv' or mimeType contains 'video/x-flv')"
    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')"
    MIMETYPES_IMAGES_LIST = ["image/jpeg","image/jpg","image/png"]
    MIMETYPES_VIDEOS_LIST = ["video/mp4","video/quicktime","video/x-ms-wmv","video/x-flv"]
    MIMETYPES_ALL_LIST = []
    MIMETYPES_ALL_LIST.extend(MIMETYPES_IMAGES_LIST)
    MIMETYPES_ALL_LIST.extend(MIMETYPES_VIDEOS_LIST)

    def __init__(self):
        """File object represents local image/video file"""

        # the path to the file locally
        self.path = ""
        # the file extension
        self.ext = ""
        # image|video, default image
        self.type = "image"
        ##
        # file title reference
        self.title = ""
        # file size
        self.size = 0

    ######################################################################################

    def check_size(self):
        """
        Check file size.

        Returns
        -------
        bool
            Whether or not the file exists by checking size

        """

        size = self.size
        if not size and not os.path.exists(self.get_path()):
            return False
        if size: return True
        size = os.path.getsize(self.get_path())
        Settings.maybe_print("file size: {}kb - {}mb".format(size/1000, size/1000000))
        if size <= File.ONE_HUNDRED_KILOBYTES:
            Settings.warn_print("tiny file size")
        elif size <= File.ONE_MEGABYTE:
            Settings.warn_print("small file size")
        elif size > 0:
            Settings.maybe_print("normal file size")
        else:
            Settings.err_print("empty file size")
            return False
        self.size = size
        return True

    ##############################

    def download(self):
        """Download a url. An input can only be a valid path or a valid url."""

        Settings.maybe_print("downloading file...")
        filename = wget.download(self.path, out=self.get_tmp())
        Settings.print("") # resume same line after wget download
        Settings.maybe_print("downloaded: "+filename)
        self.path = filename

    def get_ext(self):
        """Get the file's extension"""

        if self.ext != "": return self.ext
        self.get_title()
        return self.ext

    def get_path(self):
        """
        Get the file's path
        
        Returns
        -------
        str
            The file path

        """

        if self.path == "":
            Settings.err_print("missing file path")
            return  ""
        return str(self.path)

    def get_title(self):
        """
        Get the file's title from it's filename
        
        Returns
        -------
        str
            The file's title or filename without extension

        """

        if self.title != "": return self.title
        path = self.get_path()
        if str(path) == "": 
            Settings.err_print("missing file title!")
            return ""
        title, ext = os.path.splitext(path)
        self.ext = ext.replace(".","")
        self.title = "{}{}".format(os.path.basename(title), ext)
        return self.title

    @staticmethod
    def get_tmp():
        """Creates / gets the default temporary download directory"""

        download_path = Settings.get_download_path()
        if not os.path.exists(download_path):
            os.mkdir(download_path)
        return download_path

    def get_type(self):
        """
        Gets the file's type as an inner class of either Image or Video
        
        Returns
        -------
        Image|Video
            The file's type as an image or video class

        """

        if self.type: return self.type
        if str(self.get_ext()) in str(File.MIMETYPES_VIDEOS_LIST):
            self.type = Video()
        elif str(self.get_ext()) in str(File.MIMETYPES_IMAGES_LIST):
            self.type = Image()
        else: Settings.warn_print("unable to parse file type")
        return self.type

    def prepare(self):
        """
        Prepares the file for uploading.

        Runs the apppropriate file type method and downloads the file locally if necessary.

        Returns
        -------
        bool
            Whether or not the file is prepared

        """

        Settings.maybe_print("preparing file: {}".format(self.get_title()))
        # self.get_type().prepare()
        if not self.check_size():
            self.download()
        return self.check_size()

    @staticmethod
    def get_files_by_folder(path):
        """
        Get local files from the local folder path.

        Parameters
        ----------
        path : str
            Path to folder to get files of

        Returns
        -------
        list
            The files at the path

        """

        f = []
        for (dirpath, dirnames, filenames) in walk(path):
            f.extend(filenames)
            break
        return f

    def get_random_file(self):
        """Get random file from all files"""

        return random.choice(self.get_files())

    @staticmethod
    def get_images_of_folder(folder):
        """
        Get images of folder.

        Parameters
        ----------
        folder : str
            The folder path to get images from

        Returns
        -------
        list
            The discovered image files

        """

        Settings.dev_print("getting images of folder: {}".format(folder.get_title()))
        if not folder: return []
        imgs = []
        files = []
        valid_images = [".jpg",".gif",".png",".tga",".jpeg"]
        for f in os.listdir(folder.get_path()):
            ext = os.path.splitext(f)[1]
            if ext.lower() not in valid_images:
                continue
            file = File()
            setattr(file, "path", os.path.join(folder.get_path(),f))
            files.append(file)
            Settings.maybe_print("image path: {}".format(os.path.join(folder.get_path(),f)))
        return files

    @staticmethod
    def get_videos_of_folder(folder):
        """
        Get videos of folder.

        Parameters
        ----------
        folder : str
            The folder path to get videos from

        Returns
        -------
        list
            The discovered video files

        """

        Settings.dev_print("getting videos of folder: {}".format(folder.get_title()))
        if not folder: return []
        videos = []
        files = []

        ## TODO: change this to mimetypes

        valid_videos = [".mp4",".mov"]
        for f in os.listdir(folder.get_path()):
            ext = os.path.splitext(f)[1]
            if ext.lower() not in valid_videos:
                continue
            file = File()
            setattr(file, "path", os.path.join(folder.get_path(),f))
            files.append(file)
            Settings.maybe_print("video path: {}".format(os.path.join(folder.get_path(),f)))
        return files

    @staticmethod
    def get_folders_of_folder(folderPath):
        """
        Get folders of folder.

        Parameters
        ----------
        folderPath : str
            The folder path to get folders from

        Returns
        -------
        list
            The discovered folders

        """

        # os.walk(directory)
        # will yield a tuple for each subdirectory. Ths first entry in the 3-tuple is a directory name, so
        # [x[0] for x in os.walk(directory)]
        # should give you all of the subdirectories, recursively.
        # 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.
        # However, you could use it just to give you the immediate child directories:
        Settings.maybe_print("local walk: {}".format(folderPath))
        folders = []
        # Settings.print(os.walk(folderPath))
        for folder in next(os.walk(folderPath))[1]:
            Settings.maybe_print("folder: {}".format(folder))
            fol = Folder()
            setattr(fol, "path", os.path.join(folderPath, folder))
            folders.append(fol)
        return folders

    @staticmethod
    def remove_local():
        """
        Delete all local files.

        """

        try:
            Settings.maybe_print('deleting local files...')
            # delete /tmp
            tmp = File.get_tmp()
            if os.path.exists(tmp):
                shutil.rmtree(tmp)
                Settings.maybe_print('local files removed!')
            else:
                Settings.maybe_print('no local files found!')
        except Exception as e:
            Settings.dev_print(e)

    # def upload(self):
    #     """
    #     Process ran by a file after it has been uploaded.

    #     Ensures the file has been backed up and then deleted locally.
        
    #     Returns
    #     -------
    #     bool
    #         Whether or not the file was properly handled after its upload

    #     """
    #     if not self.prepare():
    #         Settings.err_print("unable to upload file - {}".format(self.get_title()))
    #         return False
    #     return True

######################################################################################################################
######################################################################################################################
######################################################################################################################

class Folder(File):
    def __init__(self):
        File.__init__(self)
        self.files = None

    def check_size(self):
        """
        Check the size of the files in the folder to check if the folder exists.

        Returns
        -------
        bool
            Whether or not the folder exists

        """

        for file in self.get_files():
            exists = file.check_size()
            if not exists: return False
        return True

    # def combine(self):
    #     if len(self.files) == 0: return
    #     Settings.dev_print("combining files: {}".format(len(self.files)))
    #     Settings.dev_print("combine path: {}".format(combinedPath))
    #     combinedPath = os.path.join(File.get_tmp(), "{}-combined".format(self.title))
    #     for file in files:
    #         shutil.move(file.get_path(), combinedPath)
    #         file.path = "{}/{}".format(combinedPath, self.title)
    #     self.combined = ffmpeg.combine(combinedPath)

    ##############################

    def get_files(self):
        """
        Get files from the folder.


        Returns
        -------
        list
            The discovered files

        """

        if not self.files and self.path:
            self.files = []
            files = File.get_files_by_folder(self.get_path())
            for file in files:
                file_ = File()
                setattr(file_, "path", os.path.join(self.get_path(), file))
                self.files.append(file_)
                Settings.maybe_print("local file found: {}".format(file_.get_title()))
        if Settings.get_title():
            for file in self.files:
                if str(Settings.get_title()) == str(file.get_title()):
                    self.files = [file]
                    break
        return self.files

    def get_title(self):
        """
        Get the title of the folder.

        Returns
        -------
        str
            The folder's title

        """

        if self.title: return self.title
        path = self.get_path()
        if str(path) == "": 
            Settings.err_print("missing file title")
            return ""
        title = os.path.basename(path)
        self.title = title
        return self.title

    def prepare():
        """
        Prepare the files in the folder for handling.

        Returns
        -------
        bool
            Whether or not the folder has been prepared successfully

        """

        Settings.maybe_print("preparing folder: {}".format(self.get_title()))
        prepared = False
        for file in self.get_files():
            prepared_ = file.prepare()
            if prepared_: prepared = prepared_
        return prepared

######################################################################################################################
######################################################################################################################
######################################################################################################################

class Image(File):
    def __init__(self):
        pass

    def prepare(self):
        """
        Prepare the image.

        Returns
        -------
        bool
            Whether or not the image has been prepared

        """

        Settings.maybe_print("preparing image: {}".format(self.get_title()))
        return super().prepare()

######################################################################################################################
######################################################################################################################
######################################################################################################################

class Video(File):
    def __init__(self):
        self.screenshots = []
        self.trimmed = ""
        self.split = ""

    #seconds off front or back
    def trim(self):
        """Trim the video file."""

        path = self.get_path()
        self.trimmed = ffmpeg.trim(path) 

    # into segments (60 sec, 5 min, 10 min)
    def split(self):
        """Split the video file."""

        path = self.get_path()
        self.split = ffmpeg.split(path)

    # unnecessary, handled by onlyfans
    # unless this somehow adds like more metadata
    def watermark(self):
        pass

    # cleanup & label appropriately (digital watermarking?)
    # def get_metadata(self):
        # pass

    # frames for preview gallery
    def get_frames(self):
        """Get frames from the video as screenshots."""

        path = self.get_path()
        self.screenshots = ffmpeg.frames(path)

    def prepare(self):
        """
        Prepare the video.

        Returns
        -------
        bool
            Whether or not the video has been prepared

        """

        Settings.maybe_print("preparing video: {}".format(self.get_title()))
        self.reduce()
        self.repair()
        self.watermark()
        return super().prepare()

    def reduce(self):
        """Reduce the video file."""

        if not Settings.is_reduce(): 
            Settings.maybe_print("skipping: video reduction")
            return
        path = self.get_path()
        if (int(os.stat(str(path)).st_size) < File.FIFTY_MEGABYTES or str(Settings.is_reduce()) == "False"):
            return
        Settings.dev_print("reduce: {}".format(self.get_title()))
        self.path = ffmpeg.reduce(path)
    
    # unnecessary
    # def repair(self):
    #     """Repair the video file."""
        
    #     if not Settings.is_repair():
    #         Settings.dev_print("skipping: video repair")
    #         return
    #     path = self.get_path()
    #     if Settings.is_repair():
    #         return
    #     Settings.dev_print("repair: {}".format(self.get_title()))
    #     self.path = ffmpeg.repair(path)



================================================
FILE: OnlySnarf/classes/message.py
================================================
import re
from datetime import datetime
from decimal import Decimal
from re import sub
##
from ..lib.driver import Driver
from .file import File, Folder
from .poll import Poll
from .user import User
from ..util.settings import Settings
from .schedule import Schedule

class Message():
    """OnlyFans message (and post) class"""

    def __init__(self, users=[]):
        """
        OnlyFans message and post object

        A post is just a message on a profile with different options made available. So all posts are messages, as all messages are messages.
            Squares and rectangles.

        """

        # universal message variables
        self.text = ""
        self.files = []
        self.performers = []
        self.price = 0 # $3 - $100
        self.keywords = []
        self.__initialized__ = False

    def init(self):
        """Initialize."""

        if self.__initialized__: return
        self.get_text()
        self.get_keywords()
        self.get_price()
        self.get_files()
        self.get_performers()
        self.__initialized__ = True

    @staticmethod
    def format_keywords(keywords):
        """
        Formats the list provided into a combined string with a # in front of each value.

        Parameters
        ----------
        keywords : list
            List of keywords as strings

        Returns
        -------
        str
            The generated keywords into a string
        """

        # ternary: a if condition else b
        return "#{}".format(" #".join(keywords)) if len(keywords) > 0 else ""

    @staticmethod
    def format_performers(performers):
        """
        Formats the list provided into a combined string with an @ in front of each value.
            A space is added before @ to close performer search modal (???).

        Parameters
        ----------
        performers : list
            List of performers usernames as strings

        Returns
        -------
        str
            The generated performers into a string
        """

        # ternary: a if condition else b
        return " @{} ".format(" @".join(performers)) if len(performers) > 0 else ""


    def format_text(self):
        """Formats self.text with the provided keywords and performers

        
        Returns
        -------
        str
            The generated text into a string. Example:
            "This is the text. @name, @name, and @name #keyword0 #keyword1"

        """

        return "{}{}{}".format(self.get_text(), Message.format_performers(self.get_performers()), Message.format_keywords(self.get_keywords())).strip()

    @staticmethod
    def is_tip(text):
        """
        Checks if the text contains a tip amount.

        Parameters
        ----------
        text : str
            The text to parse

        Returns
        -------
        bool
            Whether the text contains a tip or not
        int
            The tip amount contained, default 0

        """

        if re.search(r'I sent you a \$[0-9]*\.00 tip ♥', text):
            amount = re.match(r'I sent you a \$([0-9]*)\.00 tip ♥', text).group(1)
            Settings.maybe_print("message contains (tip): {}".format(amount))
            return True, int(amount)
        elif re.search(r"I\'ve contributed \$[0-9]*\.00 to your Campaign", text):
            amount = re.match(r'I\'ve contributed \$([0-9]*)\.00 to your Campaign', text).group(1)
            Settings.maybe_print("message contains (campaign): {}".format(amount))
            return True, int(amount)
        return False, 0

    def get_files(self):
        """
        Gets files from args specified source or prompts as necessary.

        Uses appropriate file select method as specified by runtime args:
        - remote (server, ipfs)
        - local

        Parameters
        ----------
        again : bool
            Whether or not it is the script user's first time around.

        Returns
        -------
        list
            Files in a list

        """

        if len(self.files) > 0:
            files_ = []
            for file in self.files[:int(Settings.get_upload_max())]:
                if not isinstance(file, File):
                    file_ = File()
                    setattr(file_, "path", file)
                    files_.append(file_)
                else:
                    files_.append(file)
            return files_
            # return self.files[:int(Settings.get_upload_max())]
        files = Settings.get_input_as_files()
        if len(files) > 0:
            Settings.dev_print("fetched input files for upload")
            self.files = files[:int(Settings.get_upload_max())] # reduce by max
            # self.files = files
            # return files
        return self.files
        # files = Folder.get_files()
        # if files is empty this all basically just skips to the end and returns blank 
        # filed = []
        # for file in files:
            # turn all folders into their files
            # if isinstance(file, Folder): filed.extend(file.get_files())
            # else:
                # filed.append(file)
                # TODO
                # this goes elsewhere
                # flag that the files include a performer
                # if hasattr(file, "performer"):
                    # self.performers.append(getattr("performer", file))

    def get_message(self):
        """
        Gets the message as a serialized JSON object.


        Returns
        -------
        Object
            The message as an object.

        """

        return dict({
            "text": self.format_text(),
            "files": self.get_files(),
            "performers": self.get_performers(),
            "price": self.get_price(),
            "keywords": self.get_keywords()
        })

    def get_performers(self):
        """
        Gets the performers for the text.

        Returns
        -------
        list
            The performers

        """

        if len(self.performers) > 0: return self.performers
        self.performers = Settings.get_performers()
        return self.performers

    def get_price(self):
        """
        Gets the price value if not none else sets it from args or prompts.


        Returns
        -------
        int
            The price

        """

        if self.price: return self.price
        price = Settings.get_price()
        if str(price) == "0": return 0
        priceMin = Settings.get_price_minimum()
        priceMax = Settings.get_price_maximum()
        if str(price) == "max": price = priceMax
        elif str(price) == "min": price = priceMin
        elif Decimal(sub(r'[^\d.]', '', str(price))) < Decimal(priceMin):
            Settings.warn_print("price too low: {} < {}".format(price, priceMin))
            Settings.maybe_print("adjusting price to minimum...")
            price = priceMin
        elif Decimal(sub(r'[^\d.]', '', str(price))) > Decimal(priceMax):
            Settings.warn_print("price too high: {} < {}".format(price, priceMax))
            Settings.maybe_print("adjusting price to maximum...")
            price = priceMax    
        self.price = price
        return self.price

    def get_keywords(self):
        """
        Gets the keywords for the text.

        Returns
        -------
        list
            The keywords

        """

        if len(self.keywords) > 0: return self.keywords
        self.keywords = Settings.get_keywords()
        return self.keywords

    def get_text(self, again=True):
        """
        Gets the text value if not none else sets it from args or prompts.


        Parameters
        ----------
        again : bool
            Whether or not it is the script user's first time around.

        Returns
        -------
        str
            The text to enter.

        """

        if self.text != "": return self.text
        # retrieve from args and return if exists
        text = Settings.get_text()
        if text != "": 
            self.text = text
            return text
        text = self.get_text_from_filename()
        if text != "":
            self.text = text
            return text
        self.text = text
        return self.text

    def get_text_from_filename(self):
        """Gets text from this object's file's title"""

        if not self.get_files(): return ""
        text = self.files[0].get_title()
        # if "_" in str(self.text):
        if re.match("[0-9]_[0-9]", text) is not None:
            texttext = self.files[0].get_parent()["title"]
        else:
            try: 
                int(text)
                # is a simple int
                if int(text) > 20:
                    text = self.files[0].get_parent()["title"]
            except Exception as e:
                # not a simple int
                # do nothing cause probably set already
                pass
        text = text.replace("_", " ")
        # redo keyword parsing (unsure if necessary call)
        text = self.update_keywords(text)
        return text

    def send(self, username, user_id=None):
        """
        Sends a message.


        Returns
        -------
        bool
            Whether or not sending the message was successful.

        """

        self.init()
        return User.message_user(self.get_message(), username, user_id=user_id)            

class Post(Message):
    """OnlyFans message (and post) class"""

    def __init__(self):
        """
        OnlyFans post object

        A post is just a message on a profile with different options made available. So all posts are messages, as all messages are messages.
            Squares and rectangles.

        """

        super().__init__(self)
        self.expiration = 0
        self.poll = None
        self.schedule = None

    # def __str__(self):
    #     return "fooPost"

    def init(self):
        """Initialize."""

        super().init()
        self.__initialized__ = False
        self.get_poll()
        self.get_schedule()
        self.get_expiration()
        self.__initialized__ = True

    def get_expiration(self, again=True):
        """
        Gets the expiration value if not none else sets it from args or prompts.
        
        Parameters
        ----------
        again : bool
            Whether or not it is the script user's first time around.


        Returns
        -------
        int
            The expiration as an int.

        """

        if self.expiration: return self.expiration
        # retrieve from args and return if exists
        expiration = Settings.get_expiration() or 0
        if expiration: 
            self.expiration = expiration
            return expiration
        self.expiration = expiration
        return self.expiration

    def get_poll(self, again=True):
        """
        Gets the poll value if not none else sets it from args or prompts.
        
        Parameters
        ----------
        again : bool
            Whether or not it is the script user's first time around.


        Returns
        -------
        Poll
            Poll object with proper values

        """

        # check if poll is ready
        if self.poll: return self.poll
        self.poll = Poll()
        return self.poll

    def get_post(self):
        """
        Gets the message as a serialized JSON object.


        Returns
        -------
        Object
            The message as an object.

        """

        return dict({
            "text": self.format_text(),
            "files": self.get_files(),
            "performers": self.get_performers(),
            "price": self.get_price(),
            "expiration": self.get_expiration(),
            "schedule": self.get_schedule(),
            "poll": self.get_poll(),
            "keywords": self.get_keywords()
        })

    def get_schedule(self):
        """
        Gets the schedule value if not none else sets it from args or prompts.

        Returns
        -------
        Schedule
            Schedule object with proper values.

        """

        if self.schedule: return self.schedule
        self.schedule = Schedule()
        return self.schedule

    def send(self):
        """
        Sends a post.


        Returns
        -------
        bool
            Whether or not sending the post was successful.

        """
        
        self.init()
        Settings.print("post > {}".format(self.get_text()))
        if not self.get_files() and self.get_text() == "":
            Settings.err_print("Missing files and text!")
            return False
        return Driver.post(self.get_post())
            


================================================
FILE: OnlySnarf/classes/poll.py
================================================
import re
from datetime import datetime
from ..lib.driver import Driver
from ..util.settings import Settings
from .user import User
##
from .file import File, Folder

class Poll:
    """OnlyFans Poll class"""

    def __init__(self):
        """OnlyFans Poll object"""

        # duration of poll
        self.duration = None
        # list of strings
        self.questions = []
        # prevents double prompts
        self.gotten = False

    def get(self):
        """
        Get the poll's values in a dict.

        Returns
        -------
        dict
            A dict containing the values of the poll

        """

        return dict({
            "duration": self.get_duration(),
            "questions": self.get_questions()
        })

    def get_duration(self):
        """
        Gets the duration value if not none else sets it from args or prompts.

        Returns
        -------
        int
            The duration as an int

        """

        if self.duration: return self.duration
        self.duration = Settings.get_duration()
        if int(self.duration) > 30: self.duration = "No limit"
        return self.duration

    def get_questions(self):
        """
        Gets the questions value if not none else sets it from args or prompts.

        Returns
        -------
        list
            The questions as strings in a list

        """

        if len(self.questions) > 0: return self.questions
        self.questions = Settings.get_questions()
        return self.questions

    def validate(self):
        """
        Determines whether or not the poll settings are valid.

        Returns
        -------
        bool
            Whether or not the poll is valid

        """

        Settings.dev_print("validating poll...")
        if len(self.get_questions()) > 0 and str(self.get_duration()) != "0":
            Settings.dev_print("valid poll!")
            return True
        Settings.dev_print("invalid poll!")
        return False

================================================
FILE: OnlySnarf/classes/profile.py
================================================
#!/usr/bin/python3
# Profile Settings
import json
import inquirer
##
from ..lib.driver import Driver
from ..util.settings import Settings
from .user import User

class Profile:
    TABS = ["profile", "advanced", "messaging", "notifications", "security", "story", "other"]

    # profile settings are either:
    #   enabled or disabled
    #   display text, variable type, variable name in settings
    def __init__(self):
        profile = Profile.fill_data()
        for key, value in profile.items():
            setattr(self, str(key), value)

    @staticmethod
    def ask_action():
        questions = [
            inquirer.List('action',
                message= "Please select an action:",
                choices= ['Back', 'Backup', 'Check', 'Posts', 'Setup', 'Sync']
            )
        ]
        answers = inquirer.prompt(questions)
        return answers["action"]

    # Backup

    @staticmethod
    def ask_backup():
        questions = [
            inquirer.List('backup',
                message= "Please select a backup action:",
                choices= ['Back', 'Content', 'Messages']
            )
        ]
        answers = inquirer.prompt(questions)
        return answers["backup"]

    @staticmethod
    def backup_menu():
        action = Profile.ask_backup()
        if (action == 'Back'): return
        elif (action == 'Content'): Profile.backup_content()
        elif (action == 'Messages'): Profile.backup_messages()

    @staticmethod
    def backup_content():
        print("Backing Up: Content")
        driver = Driver.get_driver()
        driver.download_content()
        ## TODO
        # Files.backup()
        print("Backed Up: Content")
        return True

    @staticmethod
    def backup_messages():
        print("Backing Up: Messages")
        # TODO: add user select
        # select user
        user = "all"
        # user = User.select_user()
        driver = Driver.get_driver()
        driver.download_messages(user)
        print("Backed Up: Messages")

    # new - advertise - tweet to advertise new account, tweet to ask about what you should post
    def advertise():
        pass

    def advertise_menu():
        pass

    # check settings for 'profile completion'
    # |- subscription price |- calculate recommended price from percentile count, posts numbers etc 
    # |-  Reward for subscriber referrals
    # |- about, location, website url, wishlist
    # |- if connected twitter/google
    # |- welcome message enabled
    # |- two step authentication
    # |- watermark enabled & custom text
    def check():
        if not Settings.is_debug():
            print("### Not Available ###")
            return
        print("Checking Profile Settings")
        profile = Profile.read_local() or Profile.create()
        desiredProfile = {
            "subprice":"avalue", # do manually to check price number as int
            "about":"avalue",
            "location":"avalue",
            "websiteURL":"avalue",
            "wishlist":"avalue",
            "twitter":"avalue",
            "google":"avalue",
            "welcomeMessage":"avalue",
            "twoStepAuth":True,
            "watermark":True,
            "watermarkPhoto":True,
            "watermarkVideo":True
        }
        # get profile settings
        # check against preferred settings
        # output message
        failed = False
        for key, value in profile.items():
            for key_, value_ in desiredProfile.items():
                Settings.dev_print("{}: {} = {}".format(key, value, value_))
                if value and str(value_) != "avalue":
                    if value != value_:
                        print("Warning: Unrecommended setting - {}".format(key))
                        failed = True
                elif not value or str(value) != str(value_):
                    print("Warning: Unrecommended setting - {}".format(key))
                    failed = True
        if failed:
            print("Error: Profile check failed!")
            return False
        print("Success! Profile check completed.")
        return True

    # update basic new profile settings w/ profile settings or prompt 
    # get Twitter profile & banner and use to update profile & banner
    # About, Price, Wishlist
    # watermark enabled & custom text == username
    def setup():
        if not Settings.is_debug():
            print("### Not Available ###")
            return
        print("Setting up basic profile settings")
        profile = Profile.read_local() or Profile.create()
        desiredProfile = {
            "subprice":"avalue", # do manually to check price number as int
            "about":"avalue",

            "welcomeMessage":"avalue",
            "watermark":True,
            "watermarkText":True
        }
        
        # compare to existing values to ignore already correctly set values
        for key, value in profile.items():
            for key_, value_ in desiredProfile.items():
                if str(key) == "subprice":
                    # do stuff
                    continue

                Settings.dev_print("{}: {} = {}".format(key, value, value_))
                setattr(profile, str(key), value_)

        # search for twitter banner
        twitterBanner = None
        # search for twitter profile photo
        twitterProfile = None
        # update both
        setattr(profile, "coverImage", twitterBanner)
        setattr(profile, "profilePhoto", twitterProfile)
        Profile.sync_to_profile(profile=profile)

    def posts_menu():
        if not Settings.is_debug():
            print("### Not Available ###")
            return
        action = Profile.ask_new()
        if (action == 'back'): return Profile.menu()
        elif (action == 'advertise'):
            Profile.advertise()
        # elif (action == 'new')
        # elif (action == 'new')

        Profile.menu()

    @staticmethod
    def menu():
        action = Profile.ask_action()
        if (action == 'Back'): return
        elif (action == 'Backup'): Profile.backup_menu()
        elif (action == 'Check'): Profile.check()
        elif (action == 'Posts'): Profile.posts_menu()
        elif (action == 'Setup'): Profile.setup()
        elif (action == 'Sync'): Profile.sync_from_profile()
        # elif (action == 'sync to'): Profile.sync_to_profile()
        
    @staticmethod
    def get_profile():
        print("Getting Profile")
        profile = Profile()
        for tab in Profile.TABS:
            profile.sync_from_tab(tab)
        return profile

    @staticmethod
    def sync_from_profile():
        # opens every settings tab in the browser from pages or all
        # gets necessary variables from browser
        # variables = get_settings_variables()
        print("Syncing from Profile")
        profile = Profile()
        for tab in Profile.TABS:
            profile.sync_from_tab(tab)
        print("Synced from Profile")
        Profile.write_local(profile)
        return True

    @staticmethod
    def sync_to_profile(profile=None):
        # syncs profile settings to onlyfans
        print("Syncing to Profile")
        if not profile:
            profile = Profile.read_local() or Profile.create()
        for tab in Profile.TABS:
            profile.sync_to_tab(tab)
        print("Synced to Profile")
        return True

    def sync_from_tab(self, tab):
        # syncs profile settings from the specificed tab
        Driver.sync_from_settings_page(profile=self, page=tab)

    def sync_to_tab(self, tab):
        # syncs profile settings to the specificed tab
        Driver.sync_to_settings_page(profile=self, page=tab)

    @staticmethod
    def get_country_list():
        return ["USA","Canada"]

    @staticmethod
    def get_variables_for_page(page):
        variables = get_settings_variables()
        vars_ = []
        for var in variables:
            if str(var[1]) == str(page):
                vars_.append(var)
        return vars_

    @staticmethod
    def fill_data():
        prof = {
            "coverImage": None,
            "profilePhoto": None,
            "displayName": "",
            "subscriptionPrice": "4.99",
            "about": "",
            "location": "",
            "websiteURL": None,
            "wishlist":None,
            "twitter":None,
            "google":None,
            "welcomeMessage":None,
            "twoStepAuth":False,
            "username": "",
            "email": "",
            "password": "",
            "emailNotifs": False,
            "emailNotifsNewReferral": False,
            "emailNotifsNewStream": False,
            "emailNotifsNewSubscriber": False,
            "emailNotifsNewTip": False,
            "emailNotifsRenewal": False,
            "emailNotifsNewLikes": False,
            "emailNotifsNewPosts": False,
            "emailNotifsNewPrivMessages": False,
            "siteNotifs": False,
            "siteNotifsNewComment": False,
            "siteNotifsNewFavorite": False,
            "siteNotifsDiscounts": False,
            "siteNotifsNewSubscriber": False,
            "siteNotifsNewTip": False,
            "toastNotifs": False,
            "toastNotifsNewComment": False,
            "toastNotifsNewFavorite": False,
            "toastNotifsNewSubscriber": False,
            "toastNotifsNewTip": False,
            "fullyPrivate": False,
            "enableComments": False,
            "showFansCount": False,
            "showPostsTip": False,
            "publicFriendsList": False,
            "ipCountry": Profile.get_country_list(),
            "ipIP": "",
            "watermark": True,
            "watermarkPhoto": False,
            "watermarkVideo": False,
            "watermarkText": "",
            "liveServer": "",
            "liveServerKey": ""
        }
        if prof.get("username") and str(prof.get("username")) != "" and prof.get("watermarkText") == "":
            prof.set("watermarkText", "OnlyFans.com/{}".format(prof.get("username")))
        return prof

    @staticmethod
    def read_local():
        Settings.maybe_print("Getting Local Profile")
        profile = None
        try:
            profile_ = {}
            with open(str(Settings.get_profile_path())) as json_file:  
                profile_ = json.load(json_file)['profile']
            Settings.maybe_print("Loaded Local Profile")
            profile = Profile()
            for key, value in profile_:
                setattr(profile, str(key), value)
        except Exception as e:
            Settings.dev_print(e)
        return profile

    @staticmethod
    def write_local(profile=None):
        if profile is None:
            profile = Profile.get_profile()
        print("Saving Profile Locally")
        Settings.maybe_print("local profile path: "+str(Settings.get_profile_path()))
        try:
            with open(str(Settings.get_profile_path()), 'w') as outfile:  
                json.dump({"profile":profile.__dict__}, outfile, indent=4, sort_keys=True)
        except FileNotFoundError:
            print("Error: Missing Profile File")
        except OSError:
            print("Error: Missing Profile Path")

    # returns list of settings and their classes
    # ["settingVariableName","pageProfile","inputType-text"]
    
def get_settings_variables():
    return [

        ### Profile ###

        ["coverImage","profile","file"],
        ["profilePhoto","profile","file"],
        ["username","profile","text"],
        ["displayName","profile","text"],
        ["subscriptionPrice","profile","text"],
        ["referralReward","dropdown"],
        ["about","profile","text"],
        ["location","profile","text"],
        ["websiteURL","profile","text"],

        #### Account / Advanced ###

        # id="input-email"
        ["email","advanced","text"],
        # id="old_password_input"
        ["password","advanced","text"],
        # id="new_password_input"
        ["newPassword","advanced","text"],
        # id="new_password2_input"
        ["confirmPassword","advanced","checkbox"],

        ### Chats / Messages ###
        # welcome message toggle
        # welcome message text
        # welcome message file
        # welcome message record voice, video
        # welcome message price
        # welcome message submit

        # hide outgoing message toggle
        # show full text of message in the notification email

        ### Notifications ###

        # id="push-notifications"
        ["emailNotifs","notifications","toggle"],
        # id="email-notifications"
        ["emailNotifsReferral","notifications","checkbox"],
        ["emailNotifsStream","notifications","toggle"],
        ["emailNotifsSubscriber","notifications","toggle"],
        ["emailNotifsTip","notifications","toggle"],
        ["emailNotifsRenewal","notifications","toggle"],
        # this is a dropdown
        ["emailNotifsLikes","notifications","dropdown"],
        ["emailNotifsPosts","notifications","toggle"],
        ["emailNotifsPrivMessages","notifications","toggle"],
        ["siteNotifs","notifications","toggle"],
        ["siteNotifsComment","notifications","toggle"],
        ["siteNotifsFavorite","notifications","toggle"],
        ["siteNotifsDiscounts","notifications","toggle"],
        ["siteNotifsSubscriber","notifications","toggle"],
        ["siteNotifsTip","notifications","toggle"],
        ["toastNotifsComment","notifications","toggle"],
        ["toastNotifsFavorite","notifications","toggle"],
        ["toastNotifsSubscriber","notifications","toggle"],
        ["toastNotifsTip","notifications","toggle"],

        ### Security ###

        ["fullyPrivate","security","checkbox"],
        ["enableComments","security","toggle"],
        ["showFansCount","security","toggle"],
        ["showPostsTip","security","toggle"],
        ["publicFriendsList","security","toggle"],
        ["ipCountry","security","list"],
        # id="input-blocked-ips"
        ["ipIP","security","list"],
        # id="hasWatermarkPhoto"
        ["watermarkPhoto","security","toggle"],
        # id="hasWatermarkVideo"
        ["watermarkVideo","security","toggle"],
        # placeholder="Watermark custom text"
        ["watermarkText","security","text"],

        ### Story ###
        # allow message replies - nobody
        # allow message replies - subscribers

        ### Other ###

        ["liveServer","other","text"],
        ["liveServerKey","other","text"],
        ["welcomeMessageToggle","other","toggle"],
        ["welcomeMessageText","other","text"],

    ]



================================================
FILE: OnlySnarf/classes/promotion.py
================================================
import re
from datetime import datetime
##

from ..lib.driver import Driver
from ..util import defaults as DEFAULT
from ..util.settings import Settings
from .file import File, Folder
from .user import User

class Promotion:
    """Promotion class"""

    def __init__(self):
        """Promotion object"""

        # the amount to discount
        self.amount = None
        # the number of trials to allow
        self.limit = None
        # the expiration of the trial
        self.expiration = None
        # the duration of the discount
        self.duration = None
        # the user to apply the promotion to
        self.user = None
        # the message to provide with the promotion
        self.message = None
        # prevents double prompts
        self.gotten = False

    @staticmethod
    def apply_to_user():
        """Applies promotion directly to user via their profile page
        
           Applying a discount to a user requires:
           - amount
           - duration
           - expiration
           - message
           - user

        """

        print("Promotion - Apply To User")
        p = Promotion()
        # ensure the promotion has non default values, return early if missing
        # p.get()
        gotten = p.get_amount()
        if not gotten: return False
        gotten = p.get_duration()
        if not gotten: return False
        gotten = p.get_expiration()
        if not gotten: return False
        gotten = p.get_message()
        if not gotten: return False
        gotten = p.get_user()
        if not gotten: return False
        # prompt skip
        from .driver import Driver
        # get default driver and apply the promotion directly
        Driver.promotion_user_directly(promotion=p)
        return True

    @staticmethod
    def create_campaign():
        """Creates a Promotional Campaign

           A campaign consists of:
           - amount
           - duration
           - expiration
           - limit
           - user
           - text

        """

        print("Promotion - Creating Campaign")
        p = Promotion()
        # ensure the promotion has non default values, return early if missing
        # p.get()
        gotten = p.get_amount()
        if not gotten: return False
        gotten = p.get_user()
        if not gotten: return False
        gotten = p.get_expiration()
        if not gotten: return False
        gotten = p.get_limit()
        if not gotten: return False
        gotten = p.get_duration()
        if not gotten: return False
        gotten = p.get_message()
        if not gotten: return False
        # prompt skip
        from .driver import Driver
        # get the default driver and enter the promotion campaign
        Driver.promotional_campaign(promotion=p)
        return True

    # requires the copy/paste and email steps
    @staticmethod
    def create_trial_link():
        """Creates a Promotional Trial Link

           A trial link consists of:
           - duration
           - expiration
           - limit
           - message
           - user
            
           Note: this creates a free trial link but does NOT send it to the user
           because it is incomplete. The copy/paste step to message to a user is nonfunctioning.           

        """

        print("Promotion - Creating Trial Link")
        p = Promotion()
        # ensure the promotion has non default values, return early if missing
        # p.get()
        gotten = p.get_duration()
        if not gotten: return False
        gotten = p.get_expiration()
        if not gotten: return False
        gotten = p.get_limit()
        if not gotten: return False
        gotten = p.get_message()
        if not gotten: return False
        gotten = p.get_user()
        if not gotten: return False
        # if not self.gotten: return
        # limit, expiration, months, user
        from .driver import Driver
        link = Driver.promotional_trial_link(promotion=p)
        # text = "Here's your free trial link!\n"+link
        # Settings.dev_print("Link: "+str(text))
        # Settings.send_email(email, text)
        return True

    def get(self):
        """Update the promotion object's default values"""

        return dict({
            "user": self.get_user(),
            "amount": self.get_amount(),
            "expiration": self.get_expiration(),
            "limit": self.get_limit(),
            "duraction": self.get_duration(),
            "message": self.get_message()
        })

    def get_amount(self):
        """
        Gets the amount value if not none else sets it from args or prompts.

        Returns
        -------
        int
            The amount as an int

        """

        if self.amount: return self.amount
        # retrieve from args and return if exists
        amount = Settings.get_amount() or None
        if amount: 
            self.amount = amount
            return amount
        # prompt skip
        if not Settings.confirm(amount): return self.get_amount()
        self.amount = amount
        return self.amount

    def get_expiration(self):
        """
        Gets the expiration value if not none else sets it from args or prompts.

        Returns
        -------
        int
            The expiration as an int

        """

        if self.expiration: return self.expiration
        # retrieve from args and return if exists
        expiration = Settings.get_expiration() or None
        if expiration: 
            self.expiration = expiration
            return expiration
        # prompt skip
        # confirm expiration
        if not Settings.confirm(expiration): return self.get_expiration()
        self.expiration = expiration
        return self.expiration

    def get_limit(self):
        """
        Gets the expiration value if not none else sets it from args or prompts.

        Returns
        -------
        int
            The expiration as an int

        """

        if self.limit: return self.limit
        # retrieve from args and return if exists
        limit = Settings.get_promotion_limit() or None
        if limit: 
            self.limit = limit
            return limit
        # prompt skip
        # confirm limit
        if not Settings.confirm(limit): return self.get_limit()
        self.limit = limit
        return self.limit

    def get_message(self):
        """
        Gets the message value if not none else sets it from args or prompts.

        Returns
        -------
        str
            The message as a str

        """

        if self.message != None: return self.message
        # retrieve from args and return if exists
        message = Settings.get_text() or None
        if message: 
            self.message = message
            return message
        # prompt skip
        # confirm message
        if not Settings.confirm(message): return self.get_text()
        self.message = message
        return self.message

    def get_duration(self):
        """
        Gets the duration value if not none else sets it from args or prompts.

        Returns
        -------
        int
            The duration as an int

        """

        if self.duration: return self.duration
        # retrieve from args and return if exists
        duration = Settings.get_promo_duration() or None
        if duration: 
            self.duration = duration
            return duration
        # duration skip
        # confirm duration
        if not Settings.confirm(duration): return self.get_duration()
        self.duration = duration
        return self.duration

    def get_user(self):
        """
        Populate and get the username value

        If not found in args and prompt is enabled, ask for value.

        Returns
        -------
        User
            the user to apply the promotion to

        """

        if self.user: return self.user
        user = User.select_user()
        self.user = user.username
        return self.user

    @staticmethod
    def grandfathered():
        """
        Executes the 'Grandfather' promotion model

        In groups of 5, existing users will be added to the 'Grandfathered' OnlyFans list and
        then provided with the max discount for the max months. If the process interrupts, 
        running again will continue to discount users not yet added to the list.

        """

        print("Promotion - Grandfather")
        # prompt skip
        Settings.maybe_print("getting users to grandfather")
        # get all users
        users = User.get_all_users()
        from .driver import Driver
        # get all users from logged in user's 'grandfathered' list
        users_, name, number = Driver.get_list(name="grandfathered")
        # remove all users that have already been grandfathered from the list of all users
        # users = [user for user in users if user not in users_] # i guess doesn't work?
        for i, user in enumerate(users[:]):
            for user_ in users_:
                for key, value in user_.items():
                    if str(key) == "username" and str(user.username) == str(value):
                        users.remove(user)

        def chunks(lst, n):
            """Yield successive n-sized chunks from lst."""
            for i in range(0, len(lst), n):
                yield lst[i:i + n]

        # get users in groups of 5 to allow performance over interrupts
        userChunks = chunks(users, 5)
        num = 1
        for userChunk in userChunks:
            print("Chunk: {}/{}".format(num, len(users)/5))
            num += 1
            # add users to 'grandfathered' list prior to discounting
            Settings.maybe_print("grandfathering: {}".format(len(userChunk)))
            try:
                successful = Driver.add_users_to_list(users=userChunk, number=number, name="grandfathered")
                # if successful then discount
                if not successful: return
                d = Discount() # discount will fill defaults with promotion values
                d.grandfatherer(users=userChunk)
            except Exception as e:
                Settings.dev_print(e)
        return True


================================================
FILE: OnlySnarf/classes/schedule.py
================================================
from datetime import datetime

from ..util import defaults as DEFAULT
from ..util.settings import Settings

class Schedule:

    def __init__(self):
        self._initialized_ = False
        self.date = None
        self.time = None
        ##
        self.hour = "00"
        self.minute = "00"
        self.year = "0"
        self.month = "0"
        self.day = "0"
        self.suffix = "am"
        ##
        self.init()

    def init(self):
        """Initialize the schedule's settings"""

        if self._initialized_: return
        Settings.dev_print("initiliazing schedule...")
        schedule = Settings.get_schedule()
        date = datetime.strptime(str(schedule), DEFAULT.SCHEDULE_FORMAT)
        self.year = date.year
        self.month = date.strftime("%B")
        self.day = date.day
        self.hour = date.hour
        self.minute = date.minute
        self.suffix = "am"
        if int(self.hour) > 12:
            self.suffix = "pm"
            self.hour = int(self.hour) - 12
        Settings.dev_print("year: {}".format(self.year))
        Settings.dev_print("month: {}".format(self.month))
        Settings.dev_print("day: {}".format(self.day))
        Settings.dev_print("hour: {}".format(self.hour))
        Settings.dev_print("minutes: {}".format(self.minute))
        Settings.dev_print("suffix: {}".format(self.suffix))
        Settings.dev_print("initiliazed schedule")
        self._initialized_ = True

    def get(self):
        """
        Get the schedule's values in a dict.

        Returns
        -------
        dict
            A dict containing the values of the schedule

        """

        return dict({
            "date": self.get_date(),
            "time": self.get_time(),
            "hour" : self.hour,
            "minute" : self.minute,
            "year" : self.year,
            "month" : self.month,
            "day" : self.day,
            "suffix" : self.suffix
        })

    def get_date(self):
        """
        Gets the date value if not none else sets it from args or prompts.

        Returns
        -------
        str
            The date as a valid date string

        """

        if self.date: return self.date
        self.date = Settings.get_date()
        return self.date

        # prompt skip
        # confirm date
        if not Settings.confirm(date): return self.get_date()
        self.date = date
        return self.date

    def get_time(self):
        """
        Gets the time value if not none else sets it from args or prompts.

        Returns
        -------
        str
            The time as a valid time string

        """

        if self.time: return self.time
        # retrieve from args and return if exists
        self.time = Settings.get_time()
        return self.time



        # # if time: 
        # time = datetime.strptime(str(time), DEFAULT.SCHEDULE_FORMAT)
        # # Settings.dev_print(time)
        # time = time.strftime("%I:%M %p")
        # # Settings.dev_print(time)
        # self.time = time
        # return self.time

        # retrieve time from schedule args and return if exists
        schedule = Settings.get_schedule() or None
        if schedule:
            time = datetime.strptime(str(schedule), DEFAULT.SCHEDULE_FORMAT)
            # Settings.dev_print(time)
            time = time.strftime("%I:%M %p")
            # Settings.dev_print(time)
            self.time = time
            return self.time
        # prompt skip
        # confirm time
        if not Settings.confirm(time): return self.get_time()
        self.time = time
        return self.time

    def validate(self):
        """
        Determines whether or not the schedule settings are valid.

        Returns
        -------
        bool
            Whether or not the schedule is valid

        """

        Settings.dev_print("validating schedule...")
        today = datetime.strptime(str(datetime.now().strftime(DEFAULT.SCHEDULE_FORMAT)), DEFAULT.SCHEDULE_FORMAT)
        # schedule = datetime.strptime(str(Settings.get_schedule().now().strftime(DEFAULT.SCHEDULE_FORMAT)), DEFAULT.SCHEDULE_FORMAT)
        schedule = Settings.get_schedule()
        if not schedule: return False
        if isinstance(schedule, str):
            schedule = datetime.strptime(schedule, DEFAULT.SCHEDULE_FORMAT)
        # should invalidate if all default settings
        if str(self.get_date()) == DEFAULT.DATE and (str(self.get_time()) == DEFAULT.TIME or str(self.get_time()) == DEFAULT.TIME_NONE):
            Settings.dev_print("invalid schedule! (default date and time)")
            return False
        # cannot post in the past
        # TODO: possibly add margin of error if necessary
        elif schedule <= today:
            Settings.dev_print("invalid schedule! (must be in future)")
            return False
        Settings.dev_print("valid schedule!")
        return True


================================================
FILE: OnlySnarf/classes/user.py
================================================
import json
import time
import os
import random
import threading
from datetime import datetime, timedelta
##
from ..util.colorize import colorize
from ..lib.driver import Driver
from ..util.settings import Settings

ALREADY_RANDOMIZED_USERS = []

class User:
    """OnlyFans users."""

    def __init__(self, data):
        """User object"""

        data = json.loads(json.dumps(data))
        self.name               =   data.get('name')                            or None
        self.username           =   str(data.get('username')).replace("@","")   or None
        self.id                 =   data.get('id')                              or None

        self.messages_parsed    =   data.get('messages_parsed')                 or []
        self.messages_sent      =   data.get('messages_sent')                   or []
        self.messages_received  =   data.get('messages_received')               or []
        self.messages           =   data.get('messages')                        or []

        self.sent_files         =   data.get('sent_files')                      or []
        self.isFavorite         =   data.get('isFavorite')                      or False
        # self.lists              =   data.get('lists')                           or []
        self.start_date         =   data.get('started')                         or None

        # BUG: fix empty array
        if len(self.sent_files) > 0 and self.sent_files[0] == "":
            self.sent_files = []

    def toJSON(self):
        """
        Dumps relevant user data to JSON.
        """

        return json.dumps({
            "name":str(self.name),
            "username":str(self.username),
            "id":str(self.id),
            "messages_parsed":str(self.messages_parsed),
            "messages_sent":str(self.messages_sent),
            "messages_received":str(self.messages_received),
            "messages":str(self.messages),
            "sent_files":str(self.sent_files),
            "isFavorite":str(self.isFavorite)
        })

    def equals(self, user):
        """
        Equals comparison checks usernames and ids.

        Parameters
        ----------
        classes.User
            The user to compare another user object against
        """

        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
        return False

    def get_id(self):
        """
        Get the provided ID of the User. Searches via username if necessary.

        Returns
        -------
        str
            The user id
        """

        if self.id: return self.id
        self.id = Driver.user_get_id(self.get_username())
        return self.id

    def get_username(self):
        """
        Get the username of the User.

        Returns
        -------
        str
            The username
        """

        if self.username: return self.username
        self.username = Driver.get_username(self.get_id())
        return self.username

    def message(self, message):
        """
        Message the user by their available username or id with the provided message.

        Parameters
        ----------
        message : Object
            The message to send as a serialized Message object from get_message.

        Returns
        -------
        bool
            Whether or not the message was successful
        """

        if not self.get_username() and not self.get_id(): return Settings.err_print("missing user identifiers!")
        if self.id:
            Settings.print("messaging user (id): {} ({}) - \"{}\"".format(self.username, self.id, message["text"]))
        else:
            Settings.print("messaging user: {} - \"{}\"".format(self.username, message["text"]))
        if not Driver.message(self.username, user_id=self.id): return False
        return self.message_send(message)

    def messages_read(self):
        """
        Read the chat of the user.
        """

        Settings.print("reading user chat: {} ({})".format(self.username, self.id))
        # messages, messages_received, messages_sent = Driver.read_user_messages(self.username, user_id=self.id)
        # self.messages = messages
        # self.messages_received = messages_received
        # self.messages_sent = messages_sent
        self.messages, self.messages_received, self.messages_sent = Driver.read_user_messages(self.username, user_id=self.id)
        # self.messages_and_timestamps = messages[1]
        Settings.maybe_print("chat read!")

    def message_send(self, message):
        """
        Complete the various components of sending a message to a user.
        
        Parameters
        ----------
        message : Object
            The message to send as a serialized Message object from get_message.

        Returns
        -------
        bool
            Whether or not the message was successful
        """

        Settings.print("entering message: (${}) {}".format(message["price"], message["text"]))
        try:
            driver = Driver.get_driver()
            def confirm_message(): return driver.message_confirm()
            # enter the text of the message
            def enter_text(text): return driver.message_text(text)
            # enter the price to send the message to the user
            def enter_price(price):
                if not price: return True
                return driver.message_price(price)
            def enter_files(files):
                # TODO: requires proper debugging
                # for file in files:
                    # enter files by filepath while checking for already sent files
                    # file_name = file.get_title()
                    # if str(file_name) in self.sent_files:
                    #     Settings.warn_print("file already sent to user: {} <-- {}".format(self.username, file_name))
                    #     Settings.maybe_print("skipping...")
                    #     continue
                    # self.sent_files.append(file_name)
                return driver.upload_files(files)
            if all([enter_text(message["text"]), enter_price(message["price"]), enter_files(message["files"])]): return confirm_message()
        except Exception as e:
            Settings.err_print("message failed!")
            Settings.dev_print(e)
        Settings.err_print("message somehow failed!")
        return False

    def update(self, user):
        for key, value in json.loads(user.toJSON()).items():
            # Settings.print("updating: {} = {}".format(key, value))
            setattr(self, str(key), value)

    #############
    ## Statics ##
    #############

    # TODO: update with more accurate "active"ness
    # gets users from local or refreshes from onlyfans.com
    @staticmethod
    def get_active_users():
        """
        Get active users.

        Returns
        -------
        list
            The active users

        """

        Settings.dev_print("getting active users...")
        active_users = []
        for user in User.get_all_users():
            if not User.skipUserCheck(user): continue
            active_users.append(user)
        Settings.maybe_print("active users: {}".format(len(active_users)))
        return active_users
    
    @staticmethod
    def get_all_users():
        """
        Get all users.

        Returns
        -------
        list
            The users

        """

        Settings.dev_print("getting all users...")
        users = []
        if Settings.is_prefer_local():
            users = User.read_users_local()
        if len(users) == 0:
            for user in Driver.users_get():
                if user is None: continue
                users.append(User(user))
        Settings.maybe_print("users: {}".format(len(users)))
        User.write_users_local(users=users)
        Settings.set_prefer_local(True)
        return users

    ## TODO
    # make this actually do something
    @staticmethod
    def get_favorite_users():
        """
        Get all favorite users.

        Returns
        -------
        list
            The favorite users

        """

        Settings.dev_print("getting favorite users...")
        users = []
        for user in User.get_all_users():
            if user.isFavorite:
                Settings.maybe_print("fav user: {}".format(user.username))
                users.append(user)
        return users

    @staticmethod
    def get_following():
        """
        Get all following.

        Returns
        -------
        list
            The users being followed

        """

        Settings.dev_print("getting following...")
        if Settings.is_prefer_local():
            users = User.read_following_local()
            if len(users) > 0: return users
        users = []
        for user in Driver.following_get():
            user = User(user)
            users.append(user)
        Settings.maybe_print("following: {}".format(len(users)))
        User.write_following_local(users=users)
        Settings.set_prefer_local(True)
        return users

    @staticmethod
    def get_never_messaged_users():
        """
        Get all users that have never been messaged before.

        Returns
        -------
        list
            The users that have not been messaged

        """

        Settings.dev_print("getting users that have never been messaged...")
        users = []
        for user in User.get_all_users():
            if len(user.messages_received) == 0:
                Settings.maybe_print("never messaged user: {}".format(user.username))
                users.append(user)
        return users

    @staticmethod
    def get_new_users():
        """
        Get all new users.

        Returns
        -------
        list
            The users that are new

        """

        Settings.dev_print("getting new users...")
        newUsers = []
        date_ = datetime.today() - timedelta(days=10)
        for user in User.get_all_users():
            if not user.start_date: continue
            started = datetime.strptime(str(user.start_date),"%b %d, %Y")
            # Settings.maybe_print("date: "+str(date_)+" - "+str(started))
            if started < date_: continue
            Settings.maybe_print("new user: {}".format(user.username))
            newUsers.append(user)
        return newUsers

    @staticmethod
    def get_random_user():
        """
        Get a random user.

        Returns
        -------
        classes.User
            A random user

        """

        Settings.dev_print("getting random user...")

        users = User.get_all_users_usernames()

        randomUser = None
        randomizedUsers = User.get_already_randomized_users()

        while randomUser not in randomizedUsers:
            randomUser = random.choice(users)
            if randomUser not in randomizedUsers:
                User.add_to_randomized_users(randomUser, users=randomizedUsers)
                randomizedUsers.append(randomUser)

        Settings.dev_print("random user: {}".format(randomUser))

        users = User.get_all_users()
        for user in users:
            if str(user.username) == str(randomUser):
                return user
        return User({"username":randomUser})

    @staticmethod
    def get_all_users_usernames():
        users = User.get_all_users()
        usernames = []
        for user in users:
            usernames.append(user.username)
        return usernames

    # return from json file 
    @staticmethod
    def get_already_randomized_users():
        Settings.dev_print("getting already randomized users...")
        users = []
        try:
            with open(str(Settings.get_users_path().replace("users.json","random_users.json"))) as json_file:  
                for user in json.load(json_file)['randomized_users']:
                    users.append(user)
            Settings.maybe_print("loaded randomized users")
        except Exception as e:
            Settings.dev_print(e)
        return users

    # add to json file
    @staticmethod
    def add_to_randomized_users(newUser, users=[]):
        data = {}
        data['randomized_users'] = []
        for user in users:
            data['randomized_users'].append(user)
        data['randomized_users'].append(newUser)
        try:
            with open(str(Settings.get_users_path().replace("users.json","random_users.json")), 'w') as outfile:  
                json.dump(data, outfile, indent=4, sort_keys=True)
        except FileNotFoundError:
            Settings.err_print("missing random users!")
        except OSError:
            Settings.err_print("missing random users path!")

    @staticmethod
    def get_recent_messagers():
        """
        Get users that have recently sent messages.

        Returns
        -------
        list
            The users that have recently sent messages

        """
        Settings.dev_print("getting recent users from messages...")
        users = []
        for user in Driver.messages_scan():
            users.append(User({"id":user}))
        return users

    ## TODO: maybe update this so it actually works?
    @staticmethod
    def get_recent_users():
        """
        Get recent users.

        Returns
        -------
        list
            The recent users

        """
        Settings.dev_print("getting recent users...")
        i = 0
        users = []
        for user in User.get_all_users():
            Settings.maybe_print("recent user: {}".format(user.username))
            users.append(user)
            i += 1
            if i == int(Settings.get_recent_user_count()): break
        return users

    @staticmethod
    def get_user_by_id(userid):
        """
        Get user by id.

        Returns
        -------
        int
            The user id

        """
        if not userid or userid == None:
            Settings.err_print("missing user id")
            return None
        for user in User.get_all_users():
            if str(user.id) == "@u"+str(userid) or str(user.id) == "@"+str(userid) or str(user.id) == str(userid):
                Settings.maybe_print("found user id: {}".format(userid))
                return user
        Settings.err_print("missing user by user id - {}".format(userid))
        return None

    @staticmethod
    def get_user_by_username(username):
        """
        Get user by username.

        Returns
        -------
        classes.User
            The user with the provided username

        """
        if not username or str(username) == "None":
            Settings.err_print("missing username!")
            return None
        for user in User.get_all_users():
            if str(user.username) == "@u"+str(username) or str(user.username) == "@"+str(username) or str(user.username) == str(username):
                Settings.maybe_print("found username: {}".format(username))
                return user
        Settings.err_print("missing user by username - {}".format(username))
        return None

    @staticmethod
    def get_users_by_list(number=None, name=None, ):
        """
        Get users by custom list.

        Returns
        -------
        list
            The users on the list

        """
        Settings.maybe_print("getting users by list: {} - {}".format(number, name))
        listUsers = []
        for user in Driver.get_list(number=number, name=name):
            Settings.maybe_print("user: {}".format(user.username))
            listUsers.append(user)
        return listUsers

    @staticmethod
    def message_user(message, username, user_id=None):

        """
        Message the user by their available username or id with the provided message data.

        Parameters
        ----------
        message : Object
            The message to send as a serialized Message object from get_message.
        """

        if str(username).lower() == "random":
            return User.get_random_user().message(message)
        else:
            return User({"username":username,"id":user_id}).message(message)

    @staticmethod
    def read_following_local():
        """
        Read the locally saved following file.

        Returns
        -------
        list
            The locally saved followers

        """
        Settings.dev_print("getting local following...")
        users = []
        try:
            with open(str(Settings.get_users_path().replace("users.json", "following.json"))) as json_file:  
                for user in json.load(json_file)['users']:
                    users.append(User(json.loads(user)))
            Settings.maybe_print("loaded local following")
        except Exception as e:
            Settings.dev_print(e)
        return users

    @staticmethod
    def read_users_local():
        """
        Read the locally saved users file.

        Returns
        -------
        list
            The locally saved users

        """
        Settings.dev_print("getting local users...")
        users = []
        try:
            with open(str(Settings.get_users_path())) as json_file:  
                for user in json.load(json_file)['users']:
                    users.append(User(json.loads(user)))
            Settings.maybe_print("loaded local users")
        except Exception as e:
            Settings.dev_print(e)
        return users

    @staticmethod
    def read_users_messages(users=[]):
        """
        Read all the users messages.

        Parameters
        ----------
        classes.User
            A list of users to read the messages of.

        """

        if len(users) == 0: users = User.get_all_users()
        Settings.print("updating chat logs: {}".format(len(users)))
        for user in users: user.messages_read()
        # User.write_users_local(users=users)
        return users
 
    @staticmethod
    def skipUserCheck(user):
        """
        Skip user if meets flags.

        Returns
        -------
        classes.User
            The same user provided (if not skipped)

        """
        if str(user.id).lower() in Settings.get_skipped_users() or str(user.username).lower() in Settings.get_skipped_users():
            Settings.maybe_print("skipping: {}".format(user.username))
            return None
        return user

    @staticmethod
    def write_users_local(users=None):
        """
        Write to local users file.

        """
        if users is None:
            users = User.get_all_users()
        if len(users) == 0:
            Settings.maybe_print("skipping: local users save - empty")
            return
        Settings.print("saving users...")
        Settings.dev_print("local users path: "+str(Settings.get_users_path()))
        # merge with existing user data
        existingUsers = User.read_users_local()
        for user in users:
            for u in existingUsers:
                if user.equals(u):
                    user.update(u)
        data = {}
        data['users'] = []
        for user in users:
            data['users'].append(user.toJSON())
        try:
            with open(str(Settings.get_users_path()), 'w') as outfile:  
                json.dump(data, outfile, indent=4, sort_keys=True)
        except FileNotFoundError:
            Settings.err_print("missing local users!")
        except OSError:
            Settings.err_print("missing local path!")

    @staticmethod
    def write_following_local(users=None):
        """
        Write to local followers file.

        """
        if users is None:
            users = User.get_following()
        if len(users) == 0:
            Settings.maybe_print("skipping: local following save - empty following")
            return
        Settings.print("saving following...")
        Settings.dev_print("local users path: "+str(Settings.get_users_path().replace("users.json", "following.json")))
        data = {}
        data['users'] = []
        for user in users:
            data['users'].append(user.toJSON())
        try:
            with open(str(Settings.get_users_path().replace("users.json", "following.json")), 'w') as outfile:  
                json.dump(data, outfile, indent=4, sort_keys=True)
        except FileNotFoundError:
            Settings.err_print("missing local following")
        except OSError:
            Settings.err_print("missing local path")

================================================
FILE: OnlySnarf/conf/config.conf
================================================
[ARGS]

## General ##
# auto, onlyfans, or twitter
#login = auto
#prefer_local = True
#save_users = False
#upload_max = 10
#upload_max_duration = 60 

## OnlySnarf ##
#amount = 0
#months = 0
#price = 
#date = 
#duration = 0
#expiration = 0 
#questions = 
#schedule = 
#tags = 
#text = 
#time = 
#tweeting = False
#user =  
#users =  
## fetches credentials from user config with provided username
#username = 
#phone =

## Selenium ##
# auto, brave, chrome, chromium, firefox; reconnect[-firefox, chrome, etc], remote[-firefox, chrome, etc]
#browser = auto
#cookies = True
#keep = False
#session_id = 
#session_url = 
## show browser window
#show = False

## Debugging ##
#debug = False
#debug_cookies = False
#debug_delay = False
#debug_firefox = False
#debug_google = False
#debug_selenium = False
#force_upload = False
#recent_users_count = 10
#skip_upload = False
#skipped_users = 
#users_read = 10
#reduce = False
#verbose = 1

[PATH]
#mount = $HOME/onlysnarf
#download = $HOME/onlysnarf/downloads 
#profile = $HOME/onlysnarf/profiles/$username
#users = $HOME/.onlysnarf/users.json

[POSTS]
#welcome = "Welcome to my OnlyFans!"
#ask = "What would you like to see me post more of? Comment below!"
#content_request = "Are there any specific content requests?"
#giggle = "teehee"
#no_customs = "I don\'t do customs, sorry!"
#social_media = "Be sure to check out my social medias!"
#thank_you = "Thank you all for your support!"
#reminder = "Reminder that you\'re awesome for being a subscriber!"
#commands = "OnlySnarf Bot Commands:\n\n!pic | !pic dick | !pic ass"
#more_soon = "Thanks for being followers! More coming soon!"
#customs = "I am currently accepting customs! Have a request? Message me directly! Prices vary :)"
#messages_tip = "I am 100%% more likely to notice your message and respond with a dick pic if you\'ve randomly tipped me"
#performers = "Be sure to comment below or message me directly which other performers you'd like to see me with most!"
#requests = "Be sure to message me w/ any requests for pics! Feet / Dick / etc. $5 for any quick pic, $10 if it requires setup"
#dick_pics = "Message me any particular dick pics you\'d like me to send! Tip for tip ;)"

[REMOTE]

## Selenium ##
#browser_host = 
#browser_port = 4444

================================================
FILE: OnlySnarf/conf/test-config.conf
================================================
[ARGS]

## General ##
# auto, onlyfans, or twitter
login = auto
prefer_local = True
save_users = False
upload_max = 10
upload_max_duration = 60 

## OnlySnarf ##
amount = 0
months = 0
price = 0
date = None
duration = 0
expiration = 0 
questions = []
schedule = None
tags = []
text = None
time = None
tweeting = False
user = None
users = None
## fetches credentials from user config with provided username
#username = alexdicksdown
#phone = 

## Selenium ##
# auto, brave, chrome, chromium, firefox, ie, edge, opera; reconnect[-firefox, chrome, etc], remote[-firefox, chrome, etc]
browser = auto
cookies = True
keep = True
session_id = None
session_url = None
## show browser window
show = True

## Debugging ##
debug = True
debug_cookies = False
debug_delay = True
debug_firefox = False
debug_google = False
debug_selenium = False
force_upload = False
recent_users_count = 10
skip_download = False
skip_upload = False
skipped_users = 
users_read = 10
reduce = False
verbose = 3

[PATH]
#mount = $HOME/onlysnarf
#download = $HOME/onlysnarf/downloads 
#profile = $HOME/onlysnarf/profiles/$username
#users = $HOME/.onlysnarf/users.json

[POSTS]
## these were all ideas that were never used, fyi
#welcome = "Welcome to my OnlyFans!"
#ask = "What would you like to see me post more of? Comment below!"
#content_request = "Are there any specific content requests?"
#giggle = "teehee"
#no_customs = "I don\'t do customs, sorry!"
#social_media = "Be sure to check out my social medias!"
#thank_you = "Thank you all for your support!"
#reminder = "Reminder that you\'re awesome for being a subscriber!"
#commands = "OnlySnarf Bot Commands:\n\n!pic | !pic dick | !pic ass"
#more_soon = "Thanks for being followers! More coming soon!"
#customs = "I am currently accepting customs! Have a request? Message me directly! Prices vary :)"
#messages_tip = "I am 100%% more likely to notice your message and respond with a dick pic if you\'ve randomly tipped me"
#performers = "Be sure to comment below or message me directly which other performers you'd like to see me with most!"
#requests = "Be sure to message me w/ any requests for pics! Feet / Dick / etc. $5 for any quick pic, $10 if it requires setup"
#dick_pics = "Message me any particular dick pics you\'d like me to send! Tip for tip ;)"

[REMOTE]

## Selenium ##
#browser_host = 
#browser_port = 4444

================================================
FILE: OnlySnarf/conf/users/example-user.conf
================================================
[ONLYFANS]
username = $USERNAME
password = $PASSWORD

# not working
[GOOGLE]
username = $UGOOGLE
password = $PGOOGLE

[TWITTER]
username = $UTWITTER
password = $PTWITTER

================================================
FILE: OnlySnarf/elements/__init__.py
================================================


================================================
FILE: OnlySnarf/elements/driver.py
================================================
# general driver elements

ELEMENTS = [

    {
        "name": "sendButton",
        "classes": ["g-btn.m-rounded"],
        "text": [],
        "id": []
    },
    
    {
        "name": "enterText",
        "classes": [],
        "text": [],
        "id": ["new_post_text_input"]
    },

    {
        "name": "enterMessage",
        "classes": ["b-chat__message__text"],
        "text": [],
        "id": []
    },

    {
        "name": "discountUserPromotion",
        "classes": ["g-btn.m-rounded.m-border.m-sm"],
        "text": [],
        "id": []
    },

    {
        "name": "tweet",
        "xpath": ["//label[@for='new_post_tweet_send']"],
        "classes": [],
        "text": [],
        "id": []
    },

    {
        "name": "pollInput",
        "xpath": ["//input[@class='form-control']"],
        "classes": [],
        "text": [],
        "id": []
    },

    {
        "name": "new_message",
        "classes": ["g-btn.m-rounded.b-chat__btn-submit"],
        "text": ["Send"],
        "id": []
    },

    ### upload
    # send
    {
        "name": "new_post",
        "classes": ["g-btn.m-rounded", "button.g-btn.m-rounded"],
        "text": ["Post"],
        "id": []
    },

    # record voice
    {
        "name": "recordVoice",
        "classes": [None],
        "text": [],
        "id": []
    },
    # post price
    {
        "name": "post_price",
        "classes": [None],
        "text": [],
        "id": []
    },
    # post price cancel
    {
        "name": "post_price_cancel",
        "classes": [None],
        "text": [],
        "id": []
    },
    # post price save
    {
        "name": "post_price_save",
        "classes": [None],
        "text": [],
        "id": []
    },
    # go live
    {
        "name": "go_live",
        "classes": [None],
        "text": [],
        "id": []
    },
    # upload image file
    {
        "name": "image_upload",
        "classes": ["attach_file"],
        "text": [],
        "id": ["attach_file_photo"]
    },
    # show more options # unnecessary w/ tabbing
    {
        "name": "moreOptions",
        "classes": ["button.g-btn.m-flat.b-make-post__more-btn"],
        "text": [],
        "id": []
    },
    
    # poll
    {
        "name": "poll",
        "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"],
        "text": [],
        "id": ["icon-quiz"]
    },
    # expire add
    {
        "name": "expiresAdd",
        "classes": ["b-make-post__expire-period-btn"],
        "text": ["Save"],
        "id": []
    },
    {
        "name": "expiresPeriods",
        "classes": ["b-tabs__nav__text", "b-make-post__expire__label"],
        "text": [],
        "id": []
    },
    {
        "name": "listSingleSave",
        "classes": ["g-btn.m-transparent-bg"],
        "text": ["Close"],
        "id": []
    },
    {
        "name": "expiresSave",
        "classes": ["g-btn.m-transparent-bg", "g-btn.m-rounded"],
        "text": ["Save"],
        "id": []
    },
    {
        "name": "expiresCancel",
        "classes": ["g-btn.m-flat.m-btn-gaps.m-reset-width", "g-btn.m-transparent-bg", "g-btn.m-rounded.m-border"],
        "text": ["Cancel"],
        "id": []
    },
    # poll cancel
    {
        "name": "pollCancel",
        "classes": ["b-dropzone__preview__delete"],
        "text": ["Cancel"],
        "id": []
    },
    # poll duration
    {
        "name": "pollDuration",
        "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"],
        "text": [],
        "id": []
    },
    # duration tabs
    {
        "name": "pollDurations",
        "classes": ["b-make-post__expire__label"],
        "text": [],
        "id": []
    },
    # poll save duration
    {
        "name": "pollSave",
        "classes": ["g-btn.m-flat.m-btn-gaps.m-reset-width"],
        "text": ["Save"],
        "id": []
    },
    # poll add question
    {
        "name": "pollQuestionAdd",
        "classes": ["g-btn.m-flat.new_vote_add_option", "button.g-btn.m-flat.new_vote_add_option"],
        "text": [],
        "id": []
    },

    # expiration
    {
        "name": "expirationAdd",
        "classes": ["g-btn.m-flat.b-make-post__expire-period-btn", "button.g-btn.m-flat.b-make-post__expire-period-btn"],
        "text": [],
        "id": []
    },
    # expiration periods (same for duration)
    {
        "name": "expirationPeriods",
        "classes": ["b-make-post__expire__label", "button.b-make-post__expire__label"],
        "text": [],
        "id": []
    },
    # expiration save
    {
        "name": "expirationSave",
        "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"],
        "text": ["Save"],
        "id": []
    },
    # expiration cancel
    {
        "name": "expirationCancel",
        "classes": ["g-btn.m-rounded.m-border", "button.g-btn.m-rounded.m-border"],
        "text": ["Cancel"],
        "id": []
    },
    
    {
        "name": "listSave",
        "classes": ["g-btn.m-rounded.m-sm-width"],
        "text": ["Add"],
        "id": []
    },

    ## price
    # price add
    {
        "name": "priceClick",
        "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"],
        "text": [],
        "id": []
    },
    {
        "name": "priceSave",
        "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"
        "text": ["Save"],
        "id": []
    },
    # price enter (adds .00)
    {
        "name": "priceEnter",
        "classes": ["form-control.g-input", ".form-control.g-input", "input.form-control.g-input", "input.form-control.g-input"],
        "text": ["Free"],
        "id": []
    },

    # schedule add
    {
        "name": "scheduleAdd",
        "classes": ["g-btn.m-flat.b-make-post__datepicker-btn", "button.g-btn.m-flat.b-make-post__datepicker-btn"],
        "text": [],
        "id": []
    },
    # schedule next month
    {
        "name": "scheduleNextMonth",
        "classes": ["vdatetime-calendar__navigation--next", "button.vdatetime-calendar__navigation--next"],
        "text": [],
        "id": []
    },
    # schedule date
    {
        "name": "scheduleDate",
        "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"],
        "text": [],
        "id": []
    },
    # schedule minutes
    {
        "name": "scheduleMinutes",
        "classes": ["vdatetime-time-picker__item", "button.vdatetime-time-picker__item", "vdatetime-time-picker__item.vdatetime-time-picker__item--selected"],
        "text": [],
        "id": []
    },
    # schedule hours
    {
        "name": "scheduleHours",  
        "classes": ["vdatetime-time-picker__list--hours", "vdatetime-time-picker__item.vdatetime-time-picker__item", "button.vdatetime-time-picker__item.vdatetime-time-picker__item"],
        "text": [],
        "id": []
    },
    # schedule days
    {
        "name": "scheduleDays",
        "classes": ["vdatetime-calendar__month__day", "button.vdatetime-calendar__month__day"],
        "text": [],
        "id": []
    },
    # schedule next
    {
        "name": "scheduleNext",
        "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"],
        "text": ["Next"],
        "id": []
    },
    # schedule save
    {
        "name": "scheduleSave",
        "classes": ["g-btn.m-transparent-bg", "g-btn.m-rounded", "button.g-btn.m-rounded"],
        "text": ["OK"],
        "id": []
    },
    # schedule cancel
    {
        "name": "scheduleCancel",
        "classes": ["g-btn.m-transparent-bg", "custom-datepicker-button-cancel", "button.g-btn.m-rounded"],
        "text": ["Cancel"],
        "id": []
    },
    # schedule am/pm
    {
        "name": "scheduleAMPM",
        "classes": ["vdatetime-time-picker__item.vdatetime-time-picker__item--selected"],
        "text": [],
        "id": []
    },

    ### message
    # message enter text
    {
        "name": "messageText",
        "classes": ["form-control.b-make-post__text-input", ".form-control.b-chat__message-input"],
        "text": [],
        "id": []
    },
    # message upload image
    {
        "name": "uploadMessageConfirm",
        "classes": ["g-btn.m-rounded.b-chat__btn-submit"],
        "text": [],
        "id": ["fileupload_photo"]
    },

    # upload error window close
    # tab probably closes error windows...
    {
        "name": "errorUpload",
        "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"],
        "text": ["Close"],
        "id": []
    },
    # messages all
    {
        "name": "messagesAll",
        "classes": ["b-chat__message__text"],
        "text": [],
        "id": []
    },
    # messages from user
    {
        "name": "messagesFrom",
        "classes": ["m-from-me","b-chat__message.m-from-me"],
        "text": [],
        "id": []
    },

    ## Users
    {
        "name": "usersUsernames",
        "classes": ["g-user-username"],
        "text": [],
        "id": []
    },
    # users
    {
        "name": "usersUsers",
        "classes": ["g-user-name__wrapper", "b-username"],
        "text": [],
        "id": ["profileUrl"]
    },
    # users started dates
    {
        "name": "usersStarteds",
        "classes": ["b-fans__item__list__item"],
        "text": [],
        "id": []
    },
    # users ids
    {
        "name": "usersIds",
        "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"],
        "text": [],
        "id": []
    },
    # users count
    {
        "name": "usersCount",
        "classes": ["l-sidebar__user-data__item__count", "b-tabs__nav__item.m-current"],
        "text": [],
        "id": []
    },
    {
        "name": "followingCount",
        "classes": ["b-tabs__nav__item.m-current"],
        "text": [],
        "id": []
    },
    # users discount buttons
    {
        "name": "discountUserButtons",
        "classes": ["g-btn.m-rounded.m-border.m-sm"],
        "text": [],
        "id": []
    },
    {
        "name": "newMessage",
        "classes": ["g-page__header__btn.b-chats__btn-new.has-tooltip"],
        "text": [],
        "href": ["/my/chats/send"],
        "id": [],
    },
    {
        "name": "messageAll",
        "classes": ["g-btn__text"],
        "text": ["Fans"],
        "id": [],
    },
    {
        "name": "messageRecent",
        "classes": ["g-btn__text"],
        "text": ["Recent"],
        "id": [],
    },
    {
        "name": "messageFavorite",
        "classes": ["g-btn__text"],
        "text": ["FAVORITE"],
        "id": [],
    },
    {
        "name": "messageRenewers",
        "classes": ["g-btn__text"],
        "text": ["Renew"],
        "id": [],
    },
    {
        "name": "promotionalTrial",
        "classes": ["g-btn.m-rounded.m-lg.m-flex.m-with-icon.m-uppercase"],
        "text": ["create new free trial link"],
        "id": [],
    },
    {
        "name": "promotionalCampaignAmount",
        "classes": ["form-control.b-fans__trial__select"],
        "text": ["promo-campaign-discount-percent-select"],
        "id": [],
    },
    {
        "name": "promotionalCampaign",
        "classes": ["g-btn.m-rounded.m-block.m-uppercase"],
        "text": [" Add a promotional campaign "],
        "id": [],
    },
    {
        "name": "promotionalTrialShow",
        "classes": ["g-box__header.m-icon-title.m-gray-bg"],
        "text": ["Free trial links"],
        "id": [],
    },
    {
        "name": "promotionalCopy",
        "classes": ["g-btn.m-rounded.m-uppercase"],
        "text": ["Copy link to profile"],
        "id": [],
    },
    {
        "name": "promotionalTrialCount",
        "classes": ["form-control.b-fans__trial__select"],
        "text": [],
        "id": ["trial-count-select"],
    },
    {
        "name": "promotionalTrialExpiration",
        "classes": [],
        "text": [],
        "id": ["trial-expiration-select"],
    },
    {
        "name": "promotionalTrialMessage",
        "classes": ["form-control.g-input"],
        "text": ["Type a message to users (optional)"],
        "id": [],
    },
    {
        "name": "promotionalTrialDuration",
        "classes": [],
        "text": [],
        "id": ["promo-campaign-period-select"],
    },
    {
        "name": "promotionalTrialConfirm",
        "classes": ["g-btn.m-rounded"],
        "text": ["Create"],
        "id": [],
    },
    {
        "name": "promotionalTrialCancel",
        "classes": ["g-btn.m-rounded.m-border"],
        "text": ["Cancel"],
        "id": [],
    },
    {
        "name": "promotionalTrialLink",
        "classes": ["g.btn.m-rounded"],
        "text": ["Copy trial link"],
        "id": [],
    },


    {
        "name": "postCancel",
        "classes": ["m-btn-clear-draft.g-btn.m-border.m-rounded.m-sm-width.m-reset-width"],
        "text": ["Clear"],
        "id": [],
    },


    {
        "name": "userOptions",
        "classes": ["btn.dropdown-toggle.btn-link"],
        "text": [],
        "id": ["__BVID__56__BV_toggle_"],
    },

    # save discount for user
    {
        "name": "discountUserButton",
        "classes": ["g-btn.m-flat.m-btn-gaps.m-reset-width", "g-btn.m-rounded"],
        "text": ["Apply"],
        "id": []
    },
    # discount save for user
    # {
    #     "name": "discountUsers",
    #     "classes": ["b-users__item.m-fans"],
    #     "text": ["Save"],
    #     "id": []
    # },

    {
        "name": "discountUser",
        "classes": ["b-tabs__nav__text"],
        "text": ["Discount"],
        "id": [],
    },

    {
        "name": "discountUserAmount",
        "classes": ["v-select__selection.v-select__selection--comma"],
        "text": ["% discount"],
        "id": [],
    },

    {
        "name": "discountUserMonths",
        "classes": ["v-select__selection.v-select__selection--comma"],
        "text": [" month"],
        "id": [],
    },

    {
        "name": "promotionalTrialExpirationUser",
        "classes": [],
        "text": [],
        "id": ["trial-expire-select"],
    },
    {
        "name": "promotionalTrialDurationUser",
        "classes": [],
        "text": [],
        "id": ["trial-period-select"],
    },
    {
        "name": "promotionalTrialMessageUser",
        "classes": ["form-control.g-input"],
        "text": [],
        "id": [],
    },
    {
        "name": "promotionalTrialApply",
        "classes": ["g-btn.m-rounded"],
        "text": ["Apply"],
        "id": [],
    },
    {
        "name": "promotionalTrialCancel",
        "classes": ["g-btn.m-rounded.m-border"],
        "text": ["Cancel"],
        "id": [],
    },
    {
        "name": "numberOfPosts",
        "classes": ["b-profile__actions__count"],
        "text": [],
        "id": [],
    }


]

================================================
FILE: OnlySnarf/elements/login.py
================================================
##
# unused, from Driver
##
# LOGIN_FORM = "b-loginreg__form"
# TWITTER_LOGIN0 = "//a[@class='g-btn m-rounded m-flex m-lg']"
# TWITTER_LOGIN1 = "//a[@class='g-btn m-rounded m-flex m-lg btn-twitter']"
# TWITTER_LOGIN2 = "//a[@class='btn btn-default btn-block btn-lg btn-twitter']"
# TWITTER_LOGIN3 = "//a[@class='g-btn m-rounded m-flex m-lg m-with-icon']"
# USERNAME_XPATH = "//input[@id='username_or_email']"
# PASSWORD_XPATH = "//input[@id='password']"
# SEND_BUTTON_XPATH = "//button[@type='submit' and @class='g-btn m-rounded']"
# SEND_BUTTON_CLASS2 = "button.g-btn.m-rounded"
# LIVE_BUTTON_CLASS = "b-make-post__streaming-link"
# DISCOUNT_INPUT = "form-control.b-fans__trial__select-wrapper"
# ONLYFANS_PRICE2 = "button.b-chat__btn-set-price"

ELEMENTS = [

    ### login
    {
        "name": "login",
        "classes": [],
        "text": [],
        "id": []
    },

    # username
    {
        "name": "loginUsername",
        "classes": [],
        "text": [],
        "id": []
    },

    # password
    {
        "name": "loginPassword",
        "classes": [],
        "text": [],
        "id": []
    },

    {
        "name": "loginCheck",
        "classes": ["b-make-post__streaming-link"],
        "text": [],
        "id": []
    },

    {
        "name": "rememberMe",
        "xpath": ["//input[@id='remember']"],
        "classes": [],
        "text": [],
        "id": []
    }
]

================================================
FILE: OnlySnarf/elements/profile.py
================================================
# profile settings elements

ELEMENTS = [
    ## Settings ##

    ## Account
    # cover image enter
    {
        "name": "coverImage",
        "classes": ["g-btn.m-rounded.m-sm.m-border"],
        "text": ["Upload cover image"],
        "id": []
    },
    # cover image cancel button
    {
        "name": "coverImageCancel",
        "classes": ["b-user-panel__del-btn.m-cover"],
        "text": [],
        "id": []
    },
    # profile photo
    {
        "name": "profilePhoto",
        "classes": ["g-btn.m-rounded.m-sm.m-border"],
        "text": ["Upload profile photo"],
        "id": []
    },
    # profile photo cancel button
    {
        "name": "profilePhotoCancel",
        "classes": ["b-user-panel__del-btn.m-avatar"],
        "text": [],
        "id": []
    },
    # username
    {
        "name": "username",
        "classes": [],
        "text": [],
        "id": ["input-login"]
    },
    # display name
    {
        "name": "displayName",
        "classes": [],
        "text": [],
        "id": ["input-name"]
    },
    # subscription price
    {
        "name": "subscriptionPrice",
        "classes": ["form-control.g-input"],
        "text": ["Free"],
        "id": []
    },
    # subscription bundle
    # TODO
    {
        "name": "subscriptionBundle",
        "classes": [None],
        "text": [],
        "id": []
    },
    # referral award enabled / disabled
    # TODO
    {
        "name": "referralReward",
        "classes": [None],
        "text": [],
        "id": []
    },

    # ADD reward for subscriber referrals
    # about
    {
        "name": "about",
        "classes": [],
        "text": [],
        "id": ["input-about"]
    },
    # location
    {
        "name": "location",
        "classes": [],
        "text": [],
        "id": ["input-location"]
    },
    # website url
    {
        "name": "websiteURL",
        "classes": [],
        "text": [],
        "id": ["input-website"]
    },

    ## Advanced
    # username
    # BLANK
    # username
    # {
    #     "name": "username",
    #     "classes": ["form-control.g-input"],
    #     "text": [],
    #     "id": []
    # },
    # email
    {
        "name": "email",
        "classes": ["form-control.g-input"],
        "text": [],
        "id": []
    },
    # connect other onlyfans accounts username enter area
    # BLANK
    # password
    {
        "name": "password",
        "classes": ["form-control.g-input"],
        "text": [],
        "id": []
    },
    # password 2x
    {
        "name": "newPassword",
        "classes": ["form-control.g-input"],
        "text": [],
        "id": []
    },
    # confirm new password
    {
        "name": "confirmPassword",
        "classes": ["form-control.g-input"],
        "text": [],
        "id": []
    },

    ## Messaging
    # all TODO

    {
        "name": "welcomeMessageToggle",
        "classes": [None],
        "text": [],
        "id": []
    },

    {
        "name": "welcomeMessageText",
        "classes": [None],
        "text": [],
        "id": []
    },

    {
        "name": "welcomeMessageUpload",
        "classes": [None],
        "text": [],
        "id": []
    },

    {
        "name": "welcomeMessageVoice",
        "classes": [None],
        "text": [],
        "id": []
    },

    {
        "name": "welcomeMessageVideo",
        "classes": [None],
        "text": [],
        "id": []
    },

    {
        "name": "welcomeMessagePrice",
        "classes": [None],
        "text": [],
        "id": []
    },

    {
        "name": "welcomeMessageSave",
        "classes": [None],
        "text": [],
        "id": []
    },

    {
        "name": "welcomeMessageHideToggle",
        "classes": [None],
        "text": [],
        "id": []
    },

    {
        "name": "showFullTextInEmailToggle",
        "classes": [None],
        "text": [],
        "id": []
    },

    ## Notifications
    # push notifications
    {
        "name": "pushNotifs",
        "classes": ["g-input__wrapper.m-checkbox__toggle"],
        "text": [],
        "id": ["push-notifications"]
    },
    # email notifications
    {
        "name": "emailNotifs",
        "classes": ["g-input__wrapper.m-checkbox__toggle"],
        "text": [],
        "id": ["email-notifications"]
    },
    # new referral email
    {
        "name": "emailNotifsReferral",
        "classes": ["b-input-radio"],
        "text": ["New Referral"],
        "id": []
    },
    # new stream email
    {
        "name": "emailNotifsStream",
        "classes": ["b-input-radio"],
        "text": ["New Stream"],
        "id": []
    },
    # new subscriber email
    {
        "name": "emailNotifsSubscriber",
        "classes": ["b-input-radio"],
        "text": ["New Subscriber"],
        "id": []
    },
    # new tip email
    {
        "name": "emailNotifsSubscriber",
        "classes": ["b-input-radio"],
        "text": ["New Tip"],
        "id": []
    },
    # new renewal email
    {
        "name": "emailNotifsSubscriber",
        "classes": ["b-input-radio"],
        "text": ["Renewal"],
        "id": []
    },

    {
        "name": "emailNotifsTip",
        "classes": ["b-input-radio"],
        "text": [],
        "id": []
    },
    #
    {
        "name": "emailNotifsRenewal",
        "classes": ["b-input-radio"],
        "text": [],
        "id": []
    },
    # new likes summary
    {
        "name": "emailNotifsLikes",
        "classes": [None],
        "text": [],
        "id": []
    },
    # new posts summary
    {
        "name": "emailNotifsPosts",
        "classes": [None],
        "text": [],
        "id": []
    },
    # new private message summary
    {
        "name": "emailNotifsPrivMessages",
        "classes": [None],
        "text": [],
        "id": []
    },
    # telegram bot button
    # BLANK
    # site notifications
    {
        "name": "siteNotifs",
        "classes": [None],
        "text": [],
        "id": []
    },
    # new comment notification
    {
        "name": "siteNotifsComment",
        "classes": [],
        "text": ["New comment"],
        "id": []
    },
    # new favorite notification
    {
        "name": "siteNotifsFavorite",
        "classes": [],
        "text": ["New favorite (like)"],
        "id": []
    },
    # discounts from users i've used to follow notification
    {
        "name": "siteNotifsDiscounts",
        "classes": [],
        "text": ["Discounts from users I used to follow"],
        "id": []
    },
    # new subscriber notification
    {
        "name": "siteNotifsSubscriber",
        "classes": [],
        "text": ["New Subscriber"],
        "id": []
    },
    # new tip notification
    {
        "name": "siteNotifsTip",
        "classes": [],
        "text": ["New Tip"],
        "id": []
    },
    # toast notification new comment
    {
        "name": "toastNotifsComment",
        "classes": [],
        "text": ["New comment"],
        "id": []
    },
    # toast notification new favorite
    {
        "name": "toastNotifsFavorite",
        "classes": [],
        "text": ["New favorite (like)"],
        "id": []
    },
    # toast notification new subscriber
    {
        "name": "toastNotifsSubscriber",
        "classes": [],
        "text": ["New Subscriber"],
        "id": []
    },
    # toast notification new tip
    {
        "name": "toastNotifsTip",
        "classes": [],
        "text": ["New Tip"],
        "id": []
    },

    ## Security

    # two step toggle
    # BLANK
    # fully private profile
    {
        "name": "fullyPrivate",
        "classes": [],
        "text": [],
        "id": ["is_private"]
    },
    # enable comments
    {
        "name": "enableComments",
        "classes": [],
        "text": [],
        "id": ["is_want_comments"]
    },
    # show fans count on profile
    {
        "name": "showFansCount",
        "classes": [],
        "text": [],
        "id": ["show_subscribers_count"]
    },
    # show posts tips summary
    {
        "name": "showPostsTip",
        "classes": [],
        "text": [],
        "id": ["show_posts_tips"]
    },
    # public friends list
    {
        "name": "publicFriendsList",
        "classes": [],
        "text": [],
        "id": ["show_friends_list"]
    },
    # geo blocking
    {
        "name": "ipCountry",
        "classes": ["multiselect__input"],
        "text": [],
        "id": []
    },
    # ip blocking
    {
        "name": "ipIP",
        "classes": [],
        "text": [],
        "id": ["input-blocked-ips"]
    },
    # watermarks photos
    {
        "name": "watermarkPhoto",
        "classes": [],
        "text": [],
        "id": ["hasWatermarkPhoto"]
    },
    # watermarks video
    {
        "name": "watermarkVideo",
        "classes": [],
        "text": [],
        "id": ["hasWatermarkVideo"]
    },
    # watermarks text
    {
        "name": "watermarkText",
        "classes": ["form-control.g-input"],
        "text": [],
        "id": []
    },
    ####### save changes may be the same for each
    ## Story
    # allow message replies - nobody
    {
        "name": "storyAllowRepliesNobody",
        "classes": [],
        "text": [],
        "id": ["allowNobody"]
    },
    # allow message replies - subscribers
    {
        "name": "storyAllowRepliesSubscribers",
        "classes": [],
        "text": [],
        "id": ["allowSubscribers"]
    },
    ## Other
    # obs server
    {
        "name": "liveServer",
        "classes": [],
        "text": [],
        "id": ["obsstreamingserver"]
    },
    # obs key
    {
        "name": "liveServerKey",
        "classes": [],
        "text": [],
        "id": ["streamingobskey"]
    },
    # welcome chat message toggle
    {
        "name": "welcomeMessageToggle",
        "classes": [],
        "text": [],
        "id": ["autoMessage"]
    },
    # then same pattern for message enter text or add stuff and price
    {
        "name": "welcomeMessageText",
        "classes": ["form-control.b-chat__message-input"],
        "text": [],
        "id": []
    },
    # save button for welcome chat message
    {
        "name": "welcomeMessageSave",
        "classes": ["g-btn.m-rounded.b-chat__btn-submit"],
        "text": [],
        "id": []
    },
    {
        "name": "profileSave",
        "classes": ["g-btn.m-rounded"],
        "text": ["Save changes"],
        "id": [],
    }

]


# # working
########################
# username
# displayName
# about
# location
# websiteURL

## security
# fullyPrivate
# enableComments
# showFansCount
# showPostsTip
# publicFriendsList
# ipCountry
# ipIP
# watermarkPhoto
# watermarkVideo
# watermarkText
# welcomeMessageToggle
## other
# liveServer
# liveServerKey

# # sorta working
########################
# coverImage
# profilePhoto
# password
# newPassword
# confirmPassword

# # all the notifs are probably false positives
# # are all b.input radio should maybe nth one found
# emailNotifsReferral
# emailNotifsStream
# emailNotifsSubscriber
# emailNotifsTip
# emailNotifsRenewal

# # not working
# ########################
# email
# emailNotifs
# emailNotifsPosts
# emailNotifsPrivMessages
# siteNotifs
# siteNotifsComment
# siteNotifsFavorite
# siteNotifsDiscounts
# siteNotifsSubscriber
# siteNotifsTip
# toastNotifsComment
# toastNotifsSubscriber
# toastNotifsTip
# welcomeMessageText


================================================
FILE: OnlySnarf/lib/__init__.py
================================================


================================================
FILE: OnlySnarf/lib/config.py
================================================
import os
import sys
import json
import time
import shutil
import inquirer
import fileinput
# from pathlib import Path
##
from ..util.settings import Settings
from ..util.colorize import colorize
from pathlib import Path

EMPTY_USER_CONFIG = Path(__file__).parent.joinpath("../conf/users/example-user.conf").resolve()
BASE_CONFIG = Path(__file__).parent.joinpath("../conf/config.conf").resolve()

class Config:

    def __init__(self):
        pass

    def add_user():
        username = input("OnlyFans username: ")
        # check if user already exists
        if str(username)+".conf" in Config.get_users():
            Settings.warn_print("user already exists!")
            return Config.main()
        Config.reset_user_config(username)
        Config.update_onlyfans_user(user=username)
        Config.update_google_user(user=username)
        Config.update_twitter_user(user=username)
        Config.main()


    def check_config(user):
        try:
            if not os.path.isfile(Settings.get_user_config_path(user)):
                Config.reset_user_config(user)
        except Exception as e:
            Config.reset_user_config(user)

    def display_user():
        Config.list_users()
        username = Config.list_user_menu()
        if (username == 'back'): return Config.main()
        Config.list_user_config(username)
        Config.main()

    def list_users():
        # list all user configs in conf/users
        for (dirpath, dirnames, filenames) in os.walk(os.path.join(Settings.get_base_directory(), "conf/users")):
            for filename in filenames:
                Settings.print("> "+filename)
            break

    def list_user_config(user):
        Settings.print("-- OnlySnarf Config --")
        Settings.print(colorize("Green", "green")+": configured")
        Settings.print(colorize("Blue", "blue")+": system defaults")
        Settings.print(colorize("Red", "red")+": missing")
        Settings.print("------------------------------")
        Settings.print(colorize("Config File", 'conf')+": "+colorize(user, 'green'))
        if str(Settings.get_username_onlyfans(user)) != "None":
            color = "green"
            if str(Settings.get_username_onlyfans(user)) == "$USERNAME":            
                color = "blue"
            Settings.print(colorize("OnlyFans Username", 'conf')+": "+colorize(Settings.get_username_onlyfans(user), color))
        else:
            Settings.print(colorize("OnlyFans Username", 'conf')+": "+colorize("N/A", 'red'))

        if str(Settings.get_password(user)) != "None":
            color = "green"
            if str(Settings.get_password(user)) == "$PASSWORD":            
                color = "blue"
            Settings.print(colorize("OnlyFans Password", 'conf')+": "+colorize("******", color))
        else:
            Settings.print(colorize("OnlyFans Password", 'conf')+": "+colorize("N/A", 'red'))

        if str(Settings.get_username_google(user)) != "None":
            color = "green"
            if str(Settings.get_username_google(user)) == "$UGOOGLE":            
                color = "blue"
            Settings.print(colorize("Google Username", 'conf')+": "+colorize(Settings.get_username_google(user), color))
        else:
            Settings.print(colorize("Google Username", 'conf')+": "+colorize("N/A", 'red'))

        if str(Settings.get_password_google(user)) != "None":
            color = "green"
            if str(Settings.get_password_google(user)) == "$PGOOGLE":            
                color = "blue"
            Settings.print(colorize("Google Password", 'conf')+": "+colorize("******", color))
        else:
            Settings.print(colorize("Google Password", 'conf')+": "+colorize("N/A", 'red'))

        if str(Settings.get_username_twitter(user)) != "None":
            color = "green"
            if str(Settings.get_username_twitter(user)) == "$UTWITTER":            
                color = "blue"
            Settings.print(colorize("Twitter Username", 'conf')+": "+colorize(Settings.get_username_twitter(user), color))
        else:
            Settings.print(colorize("Twitter Username", 'conf')+": "+colorize("N/A", 'red'))

        if str(Settings.get_password_twitter(user)) != "None":
            color = "green"
            if str(Settings.get_password_twitter(user)) == "$PTWITTER":            
                color = "blue"
            Settings.print(colorize("Twitter Password", 'conf')+": "+colorize("******", color))
        else:
            Settings.print(colorize("Twitter Password", 'conf')+": "+colorize("N/A", 'red'))
        Settings.print("------------------------------")

    def list_user_menu():
        options = ["back"]
        options.extend(Config.get_users())
        questions = [
            inquirer.List('list',
                message= "Please select a username for more info:",
                choices= options,
            )
        ]
        answers = inquirer.prompt(questions)
        return answers['list']

    def get_users():
        users = []
        for (dirpath, dirnames, filenames) in os.walk(os.path.join(Settings.get_base_directory(), "conf/users")):
            users.extend(filenames)
            break
        return users

    def ask_username():
        options = ["back"]
        options.extend(Config.get_users())
        if "example-user.conf" in options:
            options.remove("example-user.conf") # should not update the example / template file
        questions = [
            inquirer.List('username',
                message= "Please select a username:",
                choices= options,
            )
        ]
        answers = inquirer.prompt(questions)
        return answers['username']

    def update_menu():
        username = Config.ask_username()
        if (username == 'back'): return Config.main()
        Config.update_user_config(username.replace(".conf",""))
        Config.main()

    def user_header(user="default"):
        Settings.print("User:")
        if Settings.get_username_onlyfans(user) != "":
            Settings.print(" - Email = {}".format(Settings.get_username_onlyfans(user)))
        Settings.print(" - Username = {}".format(Settings.get_username(user)))
        pass_ = ""
        if str(Settings.get_password()) != "":
            pass_ = "******"
        Settings.print(" - Password = {}".format(pass_))
        if str(Settings.get_username_twitter(user)) != "":
            Settings.print(" - Twitter = {}".format(Settings.get_username_twitter(user)))
            pass_ = ""
            if str(Settings.get_password_twitter(user)) != "":
                pass_ = "******"
            Settings.print(" - Password = {}".format(pass_))
        Settings.print('\r')

    def remove_menu():
        username = Config.ask_username()
        if (username == 'back'): return Config.main()
        if input("ARE YOU SURE? N/y ") == "y":
            Config.remove_user(user=username)
        else:
            Settings.print("canceling deletion!")
        Config.main()

    def remove_user(user="default"):
        try:
            os.remove(Settings.get_user_config_path(user))
        except Exception as e:
            pass
        Settings.print("successfully removed {}!".format(user))

    def menu():
        questions = [
            inquirer.List('menu',
                message= "Please select an option:",
                choices= ['Add', 'Display', 'List', 'Update', 'Remove', 'Reset', 'Exit']
            )
        ]
        answers = inquirer.prompt(questions)
        return answers['menu']

    def main_menu():
        action = Config.menu()
        if (action == 'Add'): Config.add_user()
        elif (action == 'Display'): Config.display_user()
        elif (action == 'List'): Config.list_users()
        elif (action == 'Update'): Config.update_menu()
        elif (action == 'Remove'): Config.remove_menu()
        elif (action == 'Reset'): Config.reset_config()
        else: exit()
        Config.main()

    def main():
        time.sleep(1)
        try:
            Config.main_menu()
        except Exception as e:
            Settings.dev_print(e)

    def prompt_google(user):
        data = {}
        data['username'] = Settings.get_username_google(user)
        data['password'] = Settings.get_password_google(user)
        Settings.print("Username: "+data['username'])
        Settings.print("Password: "+data['password'])
        if data['username'] == "" or input("Update Google email? N/y ").lower() == "y":
            data['username'] = input('Google Email: ')
        if data['password'] == "" or input("Update Google password? N/y ").lower() == "y":
            data['password'] = input('Google Password: ')
        return data

    def prompt_onlyfans(user):
        data = {}
        data['username'] = Settings.get_username_onlyfans(user)
        data['password'] = Settings.get_password(user)
        Settings.print("Username: "+data['username'])
        Settings.print("Password: "+data['password'])
        if data['username'] == "" or input("Update OnlyFans email? N/y ").lower() == "y":
            data['username'] = input('OnlyFans Email: ')
        if data['password'] == "" or input("Update OnlyFans password? N/y ").lower() == "y":
            data['password'] = input('OnlyFans Password: ')
        return data

    def prompt_twitter(user):
        data = {}
        data['username'] = Settings.get_username_twitter(user)
        data['password'] = Settings.get_password_twitter(user)
        Settings.print("Username: "+data['username'])
        Settings.print("Password: "+data['password'])
        if data['username'] == "" or input("Update Twitter username? N/y ").lower() == "y":
            data['username'] = input('Twitter Username: ')
        if data['password'] == "" or input("Update Twitter password? N/y ").lower() == "y":
            data['password'] = input('Twitter Password: ')
        return data

    def reset_config():
        Settings.print("resetting configuration...")
        shutil.copyfile(BASE_CONFIG, os.path.join(Settings.get_base_directory(), "conf", "config.conf"))
        shutil.rmtree(os.path.join(Settings.get_base_directory(), "conf/users"))
        Path(os.path.join(Settings.get_base_directory(), "conf/users")).mkdir(parents=True, exist_ok=True)
        Settings.print("OnlySnarf config reset!")

    def reset_user_config(user="default"):
        Settings.dev_print("resetting user config files for {}...".format(user))
        if os.path.exists(Settings.get_user_config_path(user)):
            os.remove(Settings.get_user_config_path(user))
        else:
            Settings.dev_print("no user config exists to reset!")
        shutil.copyfile(EMPTY_USER_CONFIG, Settings.get_user_config_path(user))
        Settings.dev_print("successfully reset user config!")

    def update_user_config(user="default"):
        # save user settings in variables
        username = Settings.get_username_onlyfans(user)
        password = Settings.get_password(user)

        googleU = Settings.get_username_google(user)
        googleP = Settings.get_password_google(user)
        
        twitterU = Settings.get_username_twitter(user)
        twitterP = Settings.get_password_twitter(user)

        # reset user config
        Config.reset_user_config(user)

        onlyfans_data = Config.prompt_onlyfans(user)
        google_data = Config.prompt_google(user)
        twitter_data = Config.prompt_twitter(user)

        if onlyfans_data["username"] == "$USERNAME": onlyfans_data["username"] = username 
        if onlyfans_data["password"] == "$PASSWORD": onlyfans_data["password"] = password 
        if google_data["username"] == "$UGOOGLE": google_data["username"] = googleU 
        if google_data["password"] == "$PGOOGLE": google_data["password"] = googleP 
        if twitter_data["username"] == "$UTWITTER": twitter_data["username"] = twitterU 
        if twitter_data["password"] == "$PTWITTER": twitter_data["password"] = twitterP 

        Config.update_onlyfans_user(onlyfans_data, user)
        Config.update_google_user(google_data, user)
        Config.update_twitter_user(twitter_data, user)
        Settings.print("successfully updated user config for {}!".format(user))

    def update_onlyfans_user(data=None, user="default"):
        Config.check_config(user)
        if not data: data = Config.prompt_onlyfans(user)
        with fileinput.FileInput(Settings.get_user_config_path(user), inplace = True) as f:
            for line in f: 
                if data['username']:
                    line = line.replace("$USERNAME", data['username'])
                if data['password']:
                    line = line.replace("$PASSWORD", data['password'])
                print(line, end ='')
        Settings.print("OnlyFans user config updated!")

    def update_google_user(data=None, user="default"):
        Config.check_config(user)
        if not data: data = Config.prompt_google(user)
        with fileinput.FileInput(Settings.get_user_config_path(user), inplace = True) as f:
            for line in f: 
                if data['username']:
                    line = line.replace("$UGOOGLE", data['username'])
                if data['password']:
                    line = line.replace("$PGOOGLE", data['password'])
                print(line, end ='')
        Settings.print("Google user config updated!")

    def update_twitter_user(data=None, user="default"):
        Config.check_config(user)
        if not data: data = Config.prompt_twitter(user)
        with fileinput.FileInput(Settings.get_user_config_path(user), inplace = True) as f:
            for line in f: 
                if data['username']:
                    line = line.replace("$UTWITTER", data['username'])
                if data['password']:
                    line = line.replace("$PTWITTER", data['password'])
                print(line, end ='')
        Settings.print("Twitter user config updated!")


================================================
FILE: OnlySnarf/lib/driver.py
================================================
import re
import random
import os
import shutil
import json
import pathlib
import time
import wget
import pickle
from datetime import datetime, timedelta
from pathlib import Path
from selenium import webdriver
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.firefox.options import Options as FirefoxOptions
from selenium.webdriver.remote.file_detector import LocalFileDetector
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import NoSuchElementException
from selenium.common.exceptions import TimeoutException
from selenium.common.exceptions import WebDriverException
##
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.chrome.service import Service as BraveService
# chrome
from selenium.webdriver.chrome.service import Service as ChromeService
from webdriver_manager.chrome import ChromeDriverManager
# chromium
from webdriver_manager.core.utils import ChromeType
# brave
# use ChromeService
# firefox
from selenium.webdriver.firefox.service import Service as FirefoxService
from webdriver_manager.firefox import GeckoDriverManager
# ie
from selenium.webdriver.ie.service import Service as IEService
from webdriver_manager.microsoft import IEDriverManager
# edge
# from selenium.webdriver.edge.service import Service as EdgeService
# from webdriver_manager.microsoft import EdgeChromiumDriverManager
# from msedge.selenium_tools import Edge, EdgeOptions
# opera
from webdriver_manager.opera import OperaDriverManager
##
from ..classes.element import Element
from ..util.settings import Settings
#
from ..classes.file import File

###################
##### Globals #####
###################

# Urls
ONLYFANS_HOME_URL = "https://onlyfans.com"
ONLYFANS_HOME_URL2 = "https://onlyfans.com/"
ONLYFANS_NEW_MESSAGE_URL = "/my/chats/send"
ONLYFANS_CHAT_URL = "/my/chats/chat/"
ONLYFANS_SETTINGS_URL = "/my/settings/"
ONLYFANS_USERS_ACTIVE_URL = "/my/subscribers/active"
ONLYFANS_USERS_FOLLOWING_URL = "/my/subscriptions/active"
ONLYFANS_LISTS_URL = "/my/lists/"

class Driver:
    """Driver class for Selenium management"""

    BROWSER = None
    BROWSERS = []
    DRIVERS = []

    #
    DOWNLOADING = True
    DOWNLOADING_MAX = False
    DOWNLOAD_MAX_IMAGES = 1000
    DOWNLOAD_MAX_VIDEOS = 1000
    #
    MAX_TABS = 20
    NOT_INFORMED_KEPT = False # whether or not "Keep"ing the Driver.browser session has been printed once upon exit
    NOT_INFORMED_CLOSED = False # same dumb shit as above

    initialScrollDelay = 0.5
    scrollDelay = 0.5

    def __init__(self):

        # selenium web driver
        self.browser = None
        self.browsers = []

        # browser tabs cache
        self.tabs = []
        # OnlyFans discovered lists cache
        self.lists = []
        # save login state
        self.logged_in = False
        # web browser session id and url for reconnecting
        self.session_id = None
        self.session_url = None

        self._initialized_ = False

    def init(self):
        """
        Initiliaze the web driver aspect.


        """

        if self._initialized_: return
        self.browser = self.spawn_browser(Settings.get_browser_type())
        if self.browser:
            self.browsers.append(self.browser)
            ## Cookies
            if str(Settings.is_cookies()) == "True":
                self.cookies_load()
            self.tabs.append([self.browser.current_url, self.browser.current_window_handle, 0])
        self._initialized_ = True
        Driver.DRIVERS.append(self)

    def auth(self):
        """
        Authorization check

        Logs in with provided runtime creds if not logged in

        Returns
        -------
        bool
            Whether or not the login attempt was successful

        """

        self.init()
        if not self.login():
            if str(Settings.is_debug()) == "True":
                return False
            os._exit(1)
        if str(Settings.is_cookies()) == "True":
            self.cookies_save()
        return True

    ###################
    ##### Cookies #####
    ###################

    def cookies_load(self):
        """Loads existing web browser cookies from local source"""

        Settings.maybe_print("loading cookies...")
        try:
            if os.path.exists(Settings.get_cookies_path()):
                # must be at onlyfans.com to load cookies of onlyfans.com
                self.go_to_home()
                file = open(Settings.get_cookies_path(), "rb")
                cookies = pickle.load(file)
                file.close()
                Settings.dev_print("cookies: ")
                for cookie in cookies:
                    Settings.dev_print(cookie)
                    self.browser.add_cookie(cookie)
                Settings.maybe_print("successfully loaded cookies")
                self.refresh()
            else: 
                Settings.maybe_print("failed to load cookies, do not exist")
        except Exception as e:
            Settings.print("error loading cookies!")
            Settings.dev_print(e)

    def cookies_save(self):
        """Saves existing web browser cookies to local source"""

        Settings.maybe_print("saving cookies...")
        try:
            # must be at onlyfans.com to save cookies of onlyfans.com
            self.go_to_home()
            Settings.dev_print(self.browser.get_cookies())
            file = open(Settings.get_cookies_path(), "wb")
            pickle.dump(self.browser.get_cookies(), file) # "cookies.pkl"
            file.close()
            Settings.maybe_print("successfully saved cookies")
        except Exception as e:
            Settings.print("failed to save cookies!")
            Settings.dev_print(e)

    ####################
    ##### Discount #####
    ####################

    @staticmethod
    def discount_user(discount, reattempt=False):
        """
        Enter and apply discount to user

        Discount object requires:
        - duration (in months)
        - amount
        - username

        Parameters
        ----------
        discount : classes.Discount
            Discount object that contains or prompts for proper values

        Returns
        -------
        bool
            Whether or not the discount was applied successfully

        """

        if not discount:
            Settings.err_print("missing discount")
            return False

        # BUG
        # doesn't want to work with local variables
        Driver.originalAmount = None
        Driver.originalMonths = None
        try:
            driver = Driver.get_driver()
            driver.auth()
            months = int(discount["months"])
            amount = int(discount["amount"])
            username = str(discount["username"])
            Settings.print("discounting: {} {} for {} month(s)".format(username, amount, months))
            driver.go_to_page(ONLYFANS_USERS_ACTIVE_URL)
            end_ = True
            count = 0
            user_ = None
            Settings.maybe_print("searching for fan...")
            # scroll through users on page until user is found
            attempts = 0
            while end_:
                elements = driver.browser.find_elements(By.CLASS_NAME, "m-fans")
                for ele in elements:
                    username_ = ele.find_element(By.CLASS_NAME, "g-user-username").get_attribute("innerHTML").strip()
                    # if str(username) == str(username_).replace("@",""):
                    if username in username_:
                        driver.browser.execute_script("arguments[0].scrollIntoView();", ele)
                        user_ = ele
                        end_ = False
                if not end_: continue

                if len(elements) == int(count):
                    Driver.scrollDelay += Driver.initialScrollDelay
                    attempts+=1
                    if attempts == 5:
                        break

                Settings.print_same_line("({}/{}) scrolling...".format(count, len(elements)))
                count = len(elements)
                driver.browser.execute_script("window.scrollTo(0, document.body.scrollHeight);")
                time.sleep(Driver.scrollDelay)

            Settings.print("")
            Settings.dev_print("successfully found fans")
            if not user_:
                Settings.err_print("unable to find fan - {}".format(username))
                if not reattempt:
                    Settings.maybe_print("reattempting fan search...")
                    return Driver.discount_user(discount, reattempt=True)
                return False

            Settings.maybe_print("found: {}".format(username))
            ActionChains(driver.browser).move_to_element(user_).perform()
            Settings.dev_print("successfully moved to fan")
            Settings.dev_print("finding discount btn")
            buttons = user_.find_elements(By.CLASS_NAME, Element.get_element_by_name("discountUser").getClass())
            clicked = False
            for button in buttons:
                # print(button.get_attribute("innerHTML"))
                if "Discount" in button.get_attribute("innerHTML") and button.is_enabled() and button.is_displayed():
                    try:
                        Settings.dev_print("clicking discount btn")
                        button.click()
                        Settings.dev_print("clicked discount btn")
                        clicked = True
                        break
                    except Exception as e:
                        Driver.error_checker(e)
                        Settings.warn_print("unable to click discount btn for: {}".format(username))
                        return False
            if not clicked:
                Settings.warn_print("unable to find discount btn for: {}".format(username))
                return False
            time.sleep(1)

            def apply_discount():
                Settings.maybe_print("attempting discount entry...")
                Settings.dev_print("finding months and discount amount btns")
                ## amount
                discountEle = driver.browser.find_elements(By.CLASS_NAME, Element.get_element_by_name("discountUserAmount").getClass())[0]
                discountAmount = int(discountEle.get_attribute("innerHTML").replace("% discount", ""))
                if not Driver.originalAmount: Driver.originalAmount = discountAmount
                Settings.dev_print("amount: {}".format(discountAmount))
                Settings.dev_print("entering discount amount")
                if int(discountAmount) != int(amount):
                    up_ = int((discountAmount / 5) - 1)
                    down_ = int((int(amount) / 5) - 1)
                    Settings.dev_print("up: {}".format(up_))
                    Settings.dev_print("down: {}".format(down_))
                    action = ActionChains(driver.browser)
                    action.click(on_element=discountEle)
                    action.pause(1)
                    for n in range(up_):
                        action.send_keys(Keys.UP)
                        action.pause(0.5)
                    for n in range(down_):
                        action.send_keys(Keys.DOWN)
                        action.pause(0.5)                
                    action.send_keys(Keys.TAB)
                    action.perform()
                Settings.dev_print("successfully entered discount amount")
                ## months
                monthsEle = driver.browser.find_elements(By.CLASS_NAME, Element.get_element_by_name("discountUserMonths").getClass())[1]
                monthsAmount = int(monthsEle.get_attribute("innerHTML").replace(" months", "").replace(" month", ""))
                if not Driver.originalMonths: Driver.originalMonths = monthsAmount
                Settings.dev_print("months: {}".format(monthsAmount))
                Settings.dev_print("entering discount months")
                if int(monthsAmount) != int(months):
                    up_ = int(monthsAmount - 1)
                    down_ = int(int(months) - 1)
                    Settings.dev_print("up: {}".format(up_))
                    Settings.dev_print("down: {}".format(down_))
                    action = ActionChains(driver.browser)
                    action.click(on_element=monthsEle)
                    action.pause(1)
                    for n in range(up_):
                        action.send_keys(Keys.UP)
                        action.pause(0.5)
                    for n in range(down_):
                        action.send_keys(Keys.DOWN)
                        action.pause(0.5)
                    action.send_keys(Keys.TAB)
                    action.perform()
                Settings.dev_print("successfully entered discount months")
                discountEle = driver.browser.find_elements(By.CLASS_NAME, Element.get_element_by_name("discountUserAmount").getClass())[0]
                discountAmount = int(discountEle.get_attribute("innerHTML").replace("% discount", ""))
                monthsEle = driver.browser.find_elements(By.CLASS_NAME, Element.get_element_by_name("discountUserMonths").getClass())[1]
                monthsAmount = int(monthsEle.get_attribute("innerHTML").replace(" months", "").replace(" month", ""))
                return discountAmount, monthsAmount

            # discount method is repeated until values are correct because somehow it occasionally messes up...
            discountAmount, monthsAmount = apply_discount()
            while int(discountAmount) != int(amount) and int(monthsAmount) != int(months):
                # Settings.print("{} = {}    {} = {}".format(discountAmount, amount, monthsAmount, months))
                discountAmount, monthsAmount = apply_discount()

            Settings.debug_delay_check()
            ## apply
            Settings.dev_print("applying discount")
            buttons_ = driver.find_elements_by_name("discountUserButton")
            for button in buttons_:
                if not button.is_enabled() and not button.is_displayed(): continue
                if "Cancel" in button.get_attribute("innerHTML") and str(Settings.is_debug()) == "True":
                    Settings.print("skipping save discount (debug)")
                    button.click()
                    Settings.dev_print("successfully canceled discount")
                    Settings.dev_print("### Discount Successful ###")
                    return True
                elif "Cancel" in button.get_attribute("innerHTML") and int(discountAmount) == int(Driver.originalAmount) and int(monthsAmount) == int(Driver.originalMonths):
                    Settings.print("skipping existing discount")
                    button.click()
                    Settings.dev_print("successfully skipped existing discount")
                    Settings.dev_print("### Discount Successful ###")
                    # return True
                elif "Apply" in button.get_attribute("innerHTML"):
                    button.click()
                    Settings.print("discounted: {}".format(username))
                    Settings.dev_print("successfully applied discount")
                    Settings.dev_print("### Discount Successful ###")
                    return True
            Settings.dev_print("### Discount Failure ###")
            return False
        except Exception as e:
            Settings.print(e)
            Driver.error_checker(e)
            buttons_ = driver.find_elements_by_name("discountUserButton")
            for button in buttons_:
                if "Cancel" in button.get_attribute("innerHTML"):
                    button.click()
                    Settings.dev_print("### Discount Successful Failure ###")
                    return False
            Settings.dev_print("### Discount Failure ###")
            return False

    def download_content(self):
        """Downloads all content (images and video) from the user's profile page"""

        Settings.print("downloading content...")
        def scroll_to_bottom():
            try:
                # go to profile page and scroll to bottom
                self.go_to_profile()
                # count number of content elements to scroll to bottom
                num = self.browser.find_element(By.CLASS_NAME, "b-profile__sections__count").get_attribute("innerHTML")
                num = num.replace("K","00").replace(".","")
                Settings.maybe_print("content count: {}".format(num))
                for n in range(int(int(int(num)/5)+1)):
                    Settings.print_same_line("({}/{}) scrolling...".format(n,int(int(int(num)/5)+1)))
                    self.browser.execute_script("window.scrollTo(0, document.body.scrollHeight);")
                    time.sleep(1)
                Settings.print("")
            except Exception as e:
                Settings.print(e)
                Settings.err_print("failed to find content to scroll")
        scroll_to_bottom()
        imagesDownloaded = self.download_images()
        videosDownloaded = self.download_videos()
        Settings.print("downloaded content")
        Settings.print("count: {}".format(len(imagesDownloaded)+len(videosDownloaded)))

    def download_images(self, destination=None):
        """Downloads all images on the page"""

        downloaded = []
        downloadMe = []
        try:
            images_ = self.browser.find_elements(By.TAG_NAME, "img")
            images = []

            for image in images_:
                # print(image)
                # print(image.get_attribute("src"))
                if "thumbs.onlyfans.com" not in str(image.get_attribute("src")):
                    # print(image.get_attribute("src"))
                    images.append(image)

            end = len(images)
            if len(images) == 0:
                Settings.warn_print("no images found!")
                return downloaded
            if not destination: destination = os.path.join(Settings.get_download_path(), "images")
            Path(destination).mkdir(parents=True, exist_ok=True)
            i=0
            for j in range(end):
                try:
                    images_ = self.browser.find_elements(By.TAG_NAME, "img")
                    images = []

                    for image in images_:
                        if "thumbs.onlyfans.com" not in str(image.get_attribute("src")):
                            # print(image.get_attribute("src"))
                            images.append(image)

                    # click on each image
                    # download each image via class "pswp__img"
                    successful = self.move_to_then_click_element(images[j])

                    while not successful:
                        driver.browser.execute_script("window.scrollTo(0, document.body.scrollHeight);")
                        time.sleep(1)
                        successful = self.move_to_then_click_element(images[j])

                    time.sleep(1)
                    hdImages = self.browser.find_elements(By.CLASS_NAME, "pswp__img")
                    for image in hdImages:
                        downloadMe.append(image.get_attribute("src"))
                    # print(len(downloadMe))
                except Exception as err:
                    Settings.print("")            
                    Settings.warn_print(err)
                finally:
                    ActionChains(self.browser).send_keys(Keys.ESCAPE).perform()
                    i+=1
            Settings.print("")
        except Exception as err:
            Settings.err_print(err)

        # print(downloadMe)
        downloadMe = list(set(downloadMe)) # remove duplicates
        # print(downloadMe)

        i=1
        for src in downloadMe:
            # src = ""
            try:
                # if Driver.DOWNLOADING_MAX and i > Driver.DOWNLOAD_MAX_IMAGES: break
                # src = str(image.get_attribute("src"))
                # print(src)
                if not src or src == "" or src == "None" or "/thumbs/" in src or "_frame_" in src or "http" not in src: continue
                Settings.print_same_line("downloading image: {}/{}".format(i, len(images)))
                # Settings.print("Image: {}".format(src[:src.find(".jpg")+4]))
                # Settings.dev_print("image src: {}".format(src))
                    # while os.path.isfile("{}/{}.jpg".format(destination, i)):
                        # i+=1

                # TODO: maybe open image in new tab then download it

                wget.download(src, "{}/{}.jpg".format(destination, i), False)
                downloaded.append(i)
            except Exception as err:
                Settings.print("")            
                Settings.err_print(err)
                Settings.warn_print("skipped image: "+src)
            finally:
                i+=1

        return downloaded

    def download_messages(self, user="all", destination=None):
        """
        Downloads all content in messages with the user

        Parameters
        ----------
        user : str or classes.User
            The user to download message content from

        """

        Settings.print("downloading messages: {}".format(user))
        try:
            if str(user) == "all":
                # from OnlySnarf.classes.user import User
                from ..classes.user import User
                user = random.choice(User.get_all_users())
            self.message_user(user.username)
            time.sleep(1)
            contentCount = 0
            while True:
                self.browser.execute_script("document.querySelector('div[id=chatslist]').scrollTop=1e100")
                time.sleep(1)
                self.browser.execute_script("document.querySelector('div[id=chatslist]').scrollTop=1e100")
                time.sleep(1)
                self.browser.execute_script("document.querySelector('div[id=chatslist]').scrollTop=1e100")
                time.sleep(1)
                images = self.browser.find_elements(By.TAG_NAME, "img")
                videos = self.browser.find_elements(By.TAG_NAME, "video")
                # Settings.print((len(images)+len(videos)))
                if contentCount == len(images)+len(videos): break
                contentCount = len(images)+len(videos)
            # download all images and videos

            # TODO: download into correct user folders by username
            imagesDownloaded = self.download_images()
            videosDownloaded = self.download_videos()

            Settings.print("downloaded messages")
            Settings.print("count: {}".format(len(imagesDownloaded)+len(videosDownloaded)))
        except Exception as e:
            Settings.err_print(e)

    def download_videos(self, destination=None):
        """Downloads all videos on the page"""

        downloaded = []
        downloadMe = []
        try:
            # find all video elements on page
            # videos = self.browser.find_elements(By.TAG_NAME, "video")
            # videos = self.browser.find_elements(By.CLASS_NAME, "m-video-item")
            playButtons = self.browser.find_elements(By.CLASS_NAME, "b-photos__item__play-btn")
            end = len(playButtons)

            if len(playButtons) == 0:
                Settings.warn_print("no videos found!")
                return downloaded
            if not destination: destination = os.path.join(Settings.get_download_path(), "videos")
            Path(destination).mkdir(parents=True, exist_ok=True)
            i=0
            for j in range(end):
                src = ""
                playButtons = self.browser.find_elements(By.CLASS_NAME, "b-photos__item__play-btn")

                try:
                    # click on play button
                    # find new and only video ele on page
                    self.move_to_then_click_element(playButtons[i])

                    time.sleep(2)

                    video = self.browser.find_element(By.CLASS_NAME, "vjs-tech")
                    # try:
                    # except Exception as e:
                        # pass
                        # try:
                            # video = self.browser.find_element(By.TAG_NAME, "video")
                        # except Exception as e:
                            # pass

                    # if not video: continue

                    # if Driver.DOWNLOADING_MAX and i > Driver.DOWNLOAD_MAX_VIDEOS: break
                    src = str(video.get_attribute("src"))
                    if not src or src == "" or src == "None" or "http" not in src: continue
                    downloadMe.append(src)
                except Exception as e:
                    Settings.warn_print(e)
                finally:
                    # self.browser.switch_to.default_content()
                    ActionChains(self.browser).send_keys(Keys.ESCAPE).perform()
                    i+=1

            downloadMe = list(set(downloadMe)) # remove duplicates

            i=1
            for src in downloadMe:
                try:
                    Settings.print_same_line("downloading video: {}/{}".format(i, end))
                    # Settings.print("Video: {}".format(src[:src.find(".mp4")+4]))
                    # Settings.dev_print("video src: {}".format(src))
                    # while os.path.isfile("{}/{}.mp4".format(destination, i)):
                        # i+=1
                    wget.download(src, "{}/{}.mp4".format(destination, i), False)
                    downloaded.append(i)
                except Exception as e:
                    Settings.print("")            
                    Settings.err_print(e)
                    Settings.warn_print("skipped video: "+src)
                finally:
                    ActionChains(self.browser).send_keys(Keys.ESCAPE).perform()
                    i+=1
            Settings.print("")
        except Exception as e:
            Settings.err_print(e)
        return downloaded

    @staticmethod
    def drag_and_drop_file(drop_target, path):
        """
        Drag and drop the provided file path onto the provided element target.


        Parameters
        ----------
        drop_target : WebElement
            The web element to drop the file at path on

        path : str
            The file path to drag onto the web element

        Returns
        -------
        bool
            Whether or not dragging the file was successful

        """

        # https://stackoverflow.com/questions/43382447/python-with-selenium-drag-and-drop-from-file-system-to-webdriver
        JS_DROP_FILE = """
            var target = arguments[0],
                offsetX = arguments[1],
                offsetY = arguments[2],
                document = target.ownerDocument || document,
                window = document.defaultView || window;

            var input = document.createElement('INPUT');
            input.type = 'file';
            input.onchange = function () {
              var rect = target.getBoundingClientRect(),
                  x = rect.left + (offsetX || (rect.width >> 1)),
                  y = rect.top + (offsetY || (rect.height >> 1)),
                  dataTransfer = 
Download .txt
gitextract_pdkr1vka/

├── .gitignore
├── CHANGELOG.md
├── LICENSE.txt
├── MANIFEST.in
├── OnlySnarf/
│   ├── __init__.py
│   ├── __main__.py
│   ├── classes/
│   │   ├── __init__.py
│   │   ├── discount.py
│   │   ├── element.py
│   │   ├── file.py
│   │   ├── message.py
│   │   ├── poll.py
│   │   ├── profile.py
│   │   ├── promotion.py
│   │   ├── schedule.py
│   │   └── user.py
│   ├── conf/
│   │   ├── config.conf
│   │   ├── test-config.conf
│   │   └── users/
│   │       └── example-user.conf
│   ├── elements/
│   │   ├── __init__.py
│   │   ├── driver.py
│   │   ├── login.py
│   │   └── profile.py
│   ├── lib/
│   │   ├── __init__.py
│   │   ├── config.py
│   │   ├── driver.py
│   │   ├── ffmpeg.py
│   │   └── menu.py
│   ├── server/
│   │   └── api.py
│   ├── snarf.py
│   └── util/
│       ├── __init__.py
│       ├── args.py
│       ├── colorize.py
│       ├── config.py
│       ├── defaults.py
│       ├── logger.py
│       ├── optional_args.py
│       ├── settings.py
│       └── validators.py
├── Pipfile
├── README.md
├── bin/
│   ├── aws-setup.sh
│   ├── clean.sh
│   ├── demo-scripts.sh
│   ├── drivers/
│   │   ├── check-chrome.sh
│   │   ├── check-firefox.sh
│   │   ├── check.sh
│   │   ├── fix-chromedriver.sh
│   │   ├── fix-firefox-profile-error.sh
│   │   ├── install-chrome.sh
│   │   ├── install-chromedriver-aws.sh
│   │   ├── install-chromedriver-rpi.sh
│   │   ├── install-firefox.sh
│   │   ├── install-geckodriver-arm.sh
│   │   ├── install-geckodriver-rpi.sh
│   │   └── switch-firefox.sh
│   ├── install.sh
│   ├── run-tests.sh
│   ├── save.sh
│   ├── start-api-dev.sh
│   ├── start-api.sh
│   ├── test-all.sh
│   ├── test-api-remote.sh
│   ├── test-api.sh
│   ├── test.sh
│   ├── update-and-start.sh
│   ├── upload-test.sh
│   ├── upload.sh
│   └── virtualenv.sh
├── notes/
│   ├── Self-serving an ARM build
│   ├── adding-phantomjs.py
│   ├── animal.py
│   ├── docstrings.py
│   ├── login-state.py
│   ├── notes1.py
│   ├── notes2.py
│   ├── notes3.py
│   ├── notes4.py
│   ├── notes5.py
│   ├── notes6.py
│   ├── old/
│   │   ├── bot.py
│   │   ├── config.py
│   │   ├── file-old.py
│   │   ├── google-old.py
│   │   ├── remote.py
│   │   ├── removed-args.md
│   │   ├── removed-cron.py
│   │   ├── removed-readme.md
│   │   ├── removed-selenium-options.py
│   │   ├── removed_args.py
│   │   └── selectstuff.py
│   ├── onlysnarf_api.service
│   ├── scroll-notes.py
│   ├── selenium-notes.py
│   ├── selenium-notes1.py
│   ├── supervisor-api.txt
│   ├── testflask.py
│   ├── testflaskclient.py
│   ├── testrunners.py
│   ├── unittest.py
│   ├── unittestskips.py
│   ├── unittesttestrunners.py
│   └── windows-python-setup.txt
├── public/
│   └── docs/
│       ├── help.md
│       └── menu.md
├── setup.cfg
├── setup.py
└── tests/
    ├── __init__.py
    ├── api/
    │   ├── __init__.py
    │   └── test_api.py
    ├── selenium/
    │   ├── __init__.py
    │   ├── browsers/
    │   │   ├── __init__.py
    │   │   ├── test_brave.py
    │   │   ├── test_chrome.py
    │   │   ├── test_chromium.py
    │   │   ├── test_edge.py
    │   │   ├── test_firefox.py
    │   │   ├── test_ie.py
    │   │   └── test_opera.py
    │   ├── reconnect/
    │   │   ├── __init__.py
    │   │   ├── test_brave.py
    │   │   ├── test_chrome.py
    │   │   ├── test_chromium.py
    │   │   ├── test_edge.py
    │   │   ├── test_firefox.py
    │   │   ├── test_ie.py
    │   │   └── test_opera.py
    │   ├── test_browsers.py
    │   ├── test_reconnect.py
    │   └── test_remote.py
    ├── snarf/
    │   ├── __init__.py
    │   ├── auth/
    │   │   ├── __init__.py
    │   │   ├── test_google.py
    │   │   ├── test_onlyfans.py
    │   │   └── test_twitter.py
    │   ├── test_auth.py
    │   ├── test_discount.py
    │   ├── test_expiration.py
    │   ├── test_message.py
    │   ├── test_poll.py
    │   ├── test_post.py
    │   ├── test_schedule.py
    │   └── test_users.py
    ├── test_profile.py
    └── test_promotion.py
Download .txt
SYMBOL INDEX (803 symbols across 72 files)

FILE: OnlySnarf/__main__.py
  function main (line 4) | def main(args=None):

FILE: OnlySnarf/classes/discount.py
  class Discount (line 7) | class Discount:
    method __init__ (line 11) | def __init__(self, username, amount=None, months=None):
    method apply (line 19) | def apply(self):
    method get (line 33) | def get(self):
    method get_amount (line 50) | def get_amount(self):
    method get_months (line 74) | def get_months(self):
    method get_username (line 99) | def get_username(self):
    method grandfatherer (line 117) | def grandfatherer(self, users=[]):

FILE: OnlySnarf/classes/element.py
  class Element (line 17) | class Element:
    method __init__ (line 18) | def __init__(self, name=None, classes=[], text=[], id=[]):
    method getClass (line 24) | def getClass(self):
    method getClasses (line 29) | def getClasses(self):
    method getText (line 32) | def getText(self):
    method getTexts (line 37) | def getTexts(self):
    method getId (line 40) | def getId(self):
    method getIds (line 44) | def getIds(self):
    method get_element_by_name (line 48) | def get_element_by_name(name):

FILE: OnlySnarf/classes/file.py
  class File (line 10) | class File():
    method __init__ (line 27) | def __init__(self):
    method check_size (line 44) | def check_size(self):
    method download (line 75) | def download(self):
    method get_ext (line 84) | def get_ext(self):
    method get_path (line 91) | def get_path(self):
    method get_title (line 107) | def get_title(self):
    method get_tmp (line 129) | def get_tmp():
    method get_type (line 137) | def get_type(self):
    method prepare (line 156) | def prepare(self):
    method get_files_by_folder (line 176) | def get_files_by_folder(path):
    method get_random_file (line 198) | def get_random_file(self):
    method get_images_of_folder (line 204) | def get_images_of_folder(folder):
    method get_videos_of_folder (line 236) | def get_videos_of_folder(folder):
    method get_folders_of_folder (line 271) | def get_folders_of_folder(folderPath):
    method remove_local (line 304) | def remove_local():
  class Folder (line 343) | class Folder(File):
    method __init__ (line 344) | def __init__(self):
    method check_size (line 348) | def check_size(self):
    method get_files (line 376) | def get_files(self):
    method get_title (line 403) | def get_title(self):
    method prepare (line 423) | def prepare():
  class Image (line 445) | class Image(File):
    method __init__ (line 446) | def __init__(self):
    method prepare (line 449) | def prepare(self):
  class Video (line 467) | class Video(File):
    method __init__ (line 468) | def __init__(self):
    method trim (line 474) | def trim(self):
    method split (line 481) | def split(self):
    method watermark (line 489) | def watermark(self):
    method get_frames (line 497) | def get_frames(self):
    method prepare (line 503) | def prepare(self):
    method reduce (line 520) | def reduce(self):

FILE: OnlySnarf/classes/message.py
  class Message (line 13) | class Message():
    method __init__ (line 16) | def __init__(self, users=[]):
    method init (line 33) | def init(self):
    method format_keywords (line 45) | def format_keywords(keywords):
    method format_performers (line 64) | def format_performers(performers):
    method format_text (line 84) | def format_text(self):
    method is_tip (line 99) | def is_tip(text):
    method get_files (line 127) | def get_files(self):
    method get_message (line 179) | def get_message(self):
    method get_performers (line 199) | def get_performers(self):
    method get_price (line 214) | def get_price(self):
    method get_keywords (line 244) | def get_keywords(self):
    method get_text (line 259) | def get_text(self, again=True):
    method get_text_from_filename (line 289) | def get_text_from_filename(self):
    method send (line 312) | def send(self, username, user_id=None):
  class Post (line 327) | class Post(Message):
    method __init__ (line 330) | def __init__(self):
    method init (line 347) | def init(self):
    method get_expiration (line 357) | def get_expiration(self, again=True):
    method get_poll (line 383) | def get_poll(self, again=True):
    method get_post (line 405) | def get_post(self):
    method get_schedule (line 428) | def get_schedule(self):
    method send (line 443) | def send(self):

FILE: OnlySnarf/classes/poll.py
  class Poll (line 9) | class Poll:
    method __init__ (line 12) | def __init__(self):
    method get (line 22) | def get(self):
    method get_duration (line 38) | def get_duration(self):
    method get_questions (line 54) | def get_questions(self):
    method validate (line 69) | def validate(self):

FILE: OnlySnarf/classes/profile.py
  class Profile (line 10) | class Profile:
    method __init__ (line 16) | def __init__(self):
    method ask_action (line 22) | def ask_action():
    method ask_backup (line 35) | def ask_backup():
    method backup_menu (line 46) | def backup_menu():
    method backup_content (line 53) | def backup_content():
    method backup_messages (line 63) | def backup_messages():
    method advertise (line 74) | def advertise():
    method advertise_menu (line 77) | def advertise_menu():
    method check (line 88) | def check():
    method setup (line 132) | def setup():
    method posts_menu (line 166) | def posts_menu():
    method menu (line 180) | def menu():
    method get_profile (line 191) | def get_profile():
    method sync_from_profile (line 199) | def sync_from_profile():
    method sync_to_profile (line 212) | def sync_to_profile(profile=None):
    method sync_from_tab (line 222) | def sync_from_tab(self, tab):
    method sync_to_tab (line 226) | def sync_to_tab(self, tab):
    method get_country_list (line 231) | def get_country_list():
    method get_variables_for_page (line 235) | def get_variables_for_page(page):
    method fill_data (line 244) | def fill_data():
    method read_local (line 300) | def read_local():
    method write_local (line 316) | def write_local(profile=None):
  function get_settings_variables (line 332) | def get_settings_variables():

FILE: OnlySnarf/classes/promotion.py
  class Promotion (line 11) | class Promotion:
    method __init__ (line 14) | def __init__(self):
    method apply_to_user (line 33) | def apply_to_user():
    method create_campaign (line 66) | def create_campaign():
    method create_trial_link (line 103) | def create_trial_link():
    method get (line 141) | def get(self):
    method get_amount (line 153) | def get_amount(self):
    method get_expiration (line 175) | def get_expiration(self):
    method get_limit (line 198) | def get_limit(self):
    method get_message (line 221) | def get_message(self):
    method get_duration (line 244) | def get_duration(self):
    method get_user (line 267) | def get_user(self):
    method grandfathered (line 286) | def grandfathered():

FILE: OnlySnarf/classes/schedule.py
  class Schedule (line 6) | class Schedule:
    method __init__ (line 8) | def __init__(self):
    method init (line 22) | def init(self):
    method get (line 47) | def get(self):
    method get_date (line 69) | def get_date(self):
    method get_time (line 90) | def get_time(self):
    method validate (line 131) | def validate(self):

FILE: OnlySnarf/classes/user.py
  class User (line 14) | class User:
    method __init__ (line 17) | def __init__(self, data):
    method toJSON (line 39) | def toJSON(self):
    method equals (line 56) | def equals(self, user):
    method get_id (line 69) | def get_id(self):
    method get_username (line 83) | def get_username(self):
    method message (line 97) | def message(self, message):
    method messages_read (line 120) | def messages_read(self):
    method message_send (line 134) | def message_send(self, message):
    method update (line 177) | def update(self, user):
    method get_active_users (line 189) | def get_active_users():
    method get_all_users (line 209) | def get_all_users():
    method get_favorite_users (line 236) | def get_favorite_users():
    method get_following (line 256) | def get_following():
    method get_never_messaged_users (line 281) | def get_never_messaged_users():
    method get_new_users (line 301) | def get_new_users():
    method get_random_user (line 325) | def get_random_user():
    method get_all_users_usernames (line 358) | def get_all_users_usernames():
    method get_already_randomized_users (line 367) | def get_already_randomized_users():
    method add_to_randomized_users (line 381) | def add_to_randomized_users(newUser, users=[]):
    method get_recent_messagers (line 396) | def get_recent_messagers():
    method get_recent_users (line 414) | def get_recent_users():
    method get_user_by_id (line 435) | def get_user_by_id(userid):
    method get_user_by_username (line 456) | def get_user_by_username(username):
    method get_users_by_list (line 477) | def get_users_by_list(number=None, name=None, ):
    method message_user (line 495) | def message_user(message, username, user_id=None):
    method read_following_local (line 512) | def read_following_local():
    method read_users_local (line 534) | def read_users_local():
    method read_users_messages (line 556) | def read_users_messages(users=[]):
    method skipUserCheck (line 574) | def skipUserCheck(user):
    method write_users_local (line 590) | def write_users_local(users=None):
    method write_following_local (line 621) | def write_following_local(users=None):

FILE: OnlySnarf/lib/config.py
  class Config (line 17) | class Config:
    method __init__ (line 19) | def __init__(self):
    method add_user (line 22) | def add_user():
    method check_config (line 35) | def check_config(user):
    method display_user (line 42) | def display_user():
    method list_users (line 49) | def list_users():
    method list_user_config (line 56) | def list_user_config(user):
    method list_user_menu (line 112) | def list_user_menu():
    method get_users (line 124) | def get_users():
    method ask_username (line 131) | def ask_username():
    method update_menu (line 145) | def update_menu():
    method user_header (line 151) | def user_header(user="default"):
    method remove_menu (line 168) | def remove_menu():
    method remove_user (line 177) | def remove_user(user="default"):
    method menu (line 184) | def menu():
    method main_menu (line 194) | def main_menu():
    method main (line 205) | def main():
    method prompt_google (line 212) | def prompt_google(user):
    method prompt_onlyfans (line 224) | def prompt_onlyfans(user):
    method prompt_twitter (line 236) | def prompt_twitter(user):
    method reset_config (line 248) | def reset_config():
    method reset_user_config (line 255) | def reset_user_config(user="default"):
    method update_user_config (line 264) | def update_user_config(user="default"):
    method update_onlyfans_user (line 294) | def update_onlyfans_user(data=None, user="default"):
    method update_google_user (line 306) | def update_google_user(data=None, user="default"):
    method update_twitter_user (line 318) | def update_twitter_user(data=None, user="default"):

FILE: OnlySnarf/lib/driver.py
  class Driver (line 67) | class Driver:
    method __init__ (line 87) | def __init__(self):
    method init (line 105) | def init(self):
    method auth (line 123) | def auth(self):
    method cookies_load (line 149) | def cookies_load(self):
    method cookies_save (line 172) | def cookies_save(self):
    method discount_user (line 193) | def discount_user(discount, reattempt=False):
    method download_content (line 390) | def download_content(self):
    method download_images (line 416) | def download_images(self, destination=None):
    method download_messages (line 504) | def download_messages(self, user="all", destination=None):
    method download_videos (line 547) | def download_videos(self, destination=None):
    method drag_and_drop_file (line 623) | def drag_and_drop_file(drop_target, path):
    method enter_text (line 685) | def enter_text(self, text):
    method error_checker (line 734) | def error_checker(e):
    method error_window_upload (line 750) | def error_window_upload(self):
    method expires (line 774) | def expires(self, expiration):
    method find_element_by_name (line 839) | def find_element_by_name(self, name):
    method find_elements_by_name (line 877) | def find_elements_by_name(self, name):
    method find_element_to_click (line 919) | def find_element_to_click(self, name, useId=False, offset=0):
    method get_driver (line 981) | def get_driver():
    method get_page_load (line 998) | def get_page_load(self):
    method handle_alert (line 1005) | def handle_alert(self):
    method go_to_home (line 1018) | def go_to_home(self, force=False):
    method go_to_page (line 1054) | def go_to_page(self, page):
    method go_to_profile (line 1080) | def go_to_profile(self):
    method go_to_settings (line 1102) | def go_to_settings(self, settingsTab):
    method search_for_tab (line 1127) | def search_for_tab(self, page):
    method open_tab (line 1172) | def open_tab(self, url):
    method login (line 1218) | def login(self):
    method message (line 1495) | def message(username, user_id=None):
    method message_clear (line 1546) | def message_clear(self):
    method message_confirm (line 1597) | def message_confirm(self):
    method message_price (line 1629) | def message_price(self, price):
    method message_text (line 1671) | def message_text(self, text):
    method message_user_by_id (line 1700) | def message_user_by_id(self, user_id=None):
    method message_user (line 1729) | def message_user(self, username, user_id=None):
    method messages_scan (line 1776) | def messages_scan(num=0):
    method move_to_then_click_element (line 1814) | def move_to_then_click_element(self, element):
    method open_more_options (line 1863) | def open_more_options(self):
    method poll (line 1906) | def poll(self, poll):
    method post (line 2001) | def post(message):
    method promotional_campaign (line 2101) | def promotional_campaign(self, promotion=None):
    method promotional_trial_link (line 2226) | def promotional_trial_link(self, promotion=None):
    method promotion_user_directly (line 2353) | def promotion_user_directly(self, promotion=None):
    method read_user_messages (line 2445) | def read_user_messages(username, user_id=None):
    method refresh (line 2550) | def refresh(self):
    method reset (line 2560) | def reset(self):
    method schedule_open (line 2587) | def schedule_open(self):
    method schedule_date (line 2594) | def schedule_date(self, month, year):
    method schedule_day (line 2611) | def schedule_day(self, day):
    method schedule_save_date (line 2622) | def schedule_save_date(self):
    method schedule_hour (line 2628) | def schedule_hour(self, hour):
    method schedule_minutes (line 2641) | def schedule_minutes(self, minutes):
    method schedule_suffix (line 2653) | def schedule_suffix(self, suffix):
    method schedule_cancel (line 2665) | def schedule_cancel(self):
    method schedule_save (line 2672) | def schedule_save(self):
    method schedule (line 2680) | def schedule(self, schedule):
    method sync_from_settings_page (line 2772) | def sync_from_settings_page(self, profile=None, page=None):
    method sync_to_settings_page (line 2843) | def sync_to_settings_page(self, profile=None, page=None):
    method settings_save (line 2931) | def settings_save(self, page=None):
    method spawn_browser (line 2964) | def spawn_browser(self, browserType):
    method read_session_data (line 3260) | def read_session_data(self):
    method write_session_data (line 3273) | def write_session_data(self):
    method upload_files (line 3295) | def upload_files(self, files):
    method get_username (line 3396) | def get_username():
    method following_get (line 3427) | def following_get():
    method users_get (line 3472) | def users_get(page=ONLYFANS_USERS_ACTIVE_URL):
    method user_get_id (line 3519) | def user_get_id(username):
    method search_for_list (line 3556) | def search_for_list(self, name=None, number=None):
    method get_list (line 3588) | def get_list(name=None, number=None):
    method get_lists (line 3629) | def get_lists(self):
    method get_list_members (line 3693) | def get_list_members(self, list):
    method add_user_to_list (line 3717) | def add_user_to_list(self, username=None, listNumber=None):
    method add_users_to_list (line 3800) | def add_users_to_list(self, users=[], number=None, name=None):
    method exit (line 3894) | def exit(self):
    method exit_all (line 3921) | def exit_all():
  function parse_users (line 3929) | def parse_users(user_ids, starteds, users, usernames):

FILE: OnlySnarf/lib/ffmpeg.py
  function combine (line 12) | def combine(folderPath):
  function gifify (line 32) | def gifify(path):
  function frames (line 43) | def frames(path):
  function reduce (line 92) | def reduce(path):
  function split (line 139) | def split(path, segment):
  function thumbnail_fix (line 178) | def thumbnail_fix(path):
  function trim (line 202) | def trim(path):
  function watermark (line 234) | def watermark():
  function metadata (line 237) | def metadata():

FILE: OnlySnarf/lib/menu.py
  class Menu (line 22) | class Menu:
    method __init__ (line 31) | def __init__(self):
    method ask_action (line 34) | def ask_action():
    method action_menu (line 56) | def action_menu():
    method header (line 74) | def header():
    method settings_header (line 87) | def settings_header():
    method user_header (line 96) | def user_header():
    method menu (line 119) | def menu():
    method main_menu (line 148) | def main_menu():
    method main (line 161) | def main():
  function exit_handler (line 178) | def exit_handler():
  function main (line 191) | def main():

FILE: OnlySnarf/server/api.py
  function create_app (line 8) | def create_app():
  function main (line 62) | def main():

FILE: OnlySnarf/snarf.py
  class Snarf (line 18) | class Snarf:
    method __init__ (line 28) | def __init__(self):
    method close (line 34) | def close():
    method api (line 39) | def api():
    method config (line 43) | def config():
    method menu (line 47) | def menu():
    method discount (line 51) | def discount():
    method message (line 70) | def message():
    method post (line 89) | def post():
    method profile (line 103) | def profile():
    method promotion (line 130) | def promotion():
    method users (line 157) | def users():
  function exit_handler (line 176) | def exit_handler():
  function main (line 188) | def main():

FILE: OnlySnarf/util/colorize.py
  class fg (line 17) | class fg:
  class bg (line 28) | class bg:
  class style (line 39) | class style:
  function colorize (line 49) | def colorize(string, color):

FILE: OnlySnarf/util/logger.py
  class CustomFormatter (line 25) | class CustomFormatter(logging.Formatter):
    method format (line 46) | def format(self, record):

FILE: OnlySnarf/util/optional_args.py
  function apply_args (line 8) | def apply_args(parser):
  function apply_subcommand_args (line 128) | def apply_subcommand_args(parser):
  function apply_shim_args (line 281) | def apply_shim_args(parser):

FILE: OnlySnarf/util/settings.py
  class Settings (line 14) | class Settings:
    method debug_delay_check (line 25) | def debug_delay_check():
    method print (line 34) | def print(text):
    method print_same_line (line 38) | def print_same_line(text):
    method maybe_print (line 44) | def maybe_print(text):
    method dev_print (line 48) | def dev_print(text):
    method err_print (line 52) | def err_print(error):
    method warn_print (line 55) | def warn_print(error):
    method format_date (line 58) | def format_date(date):
    method format_time (line 64) | def format_time(time):
    method header (line 70) | def header():
    method menu (line 76) | def menu():
    method ensure_paths (line 82) | def ensure_paths():
    method get_action (line 93) | def get_action():
    method get_actions (line 96) | def get_actions():
    method get_amount (line 99) | def get_amount():
    method get_base_directory (line 102) | def get_base_directory():
    method get_browser_type (line 105) | def get_browser_type():
    method get_months (line 108) | def get_months():
    method get_category (line 111) | def get_category():
    method get_categories (line 119) | def get_categories():
    method get_cookies_path (line 125) | def get_cookies_path():
    method get_price (line 129) | def get_price():
    method get_price_minimum (line 134) | def get_price_minimum():
    method get_price_maximum (line 137) | def get_price_maximum():
    method get_default_greeting (line 140) | def get_default_greeting():
    method get_default_refresher (line 143) | def get_default_refresher():
    method get_discount_max_amount (line 146) | def get_discount_max_amount():
    method get_discount_min_amount (line 149) | def get_discount_min_amount():
    method get_discount_max_months (line 152) | def get_discount_max_months():
    method get_discount_min_months (line 155) | def get_discount_min_months():
    method get_download_max (line 158) | def get_download_max():
    method get_duration (line 161) | def get_duration():
    method get_promo_duration (line 166) | def get_promo_duration():
    method get_duration_allowed (line 171) | def get_duration_allowed():
    method get_duration_promo_allowed (line 174) | def get_duration_promo_allowed():
    method get_expiration (line 177) | def get_expiration():
    method get_promo_expiration (line 182) | def get_promo_expiration():
    method get_input (line 185) | def get_input():
    method get_input_as_files (line 193) | def get_input_as_files():
    method get_logs_path (line 213) | def get_logs_path(process):
    method get_message_choices (line 224) | def get_message_choices():
    method get_root_path (line 227) | def get_root_path():
    method get_sort_method (line 230) | def get_sort_method():
    method get_phone_number (line 233) | def get_phone_number():
    method get_performers (line 240) | def get_performers():
    method get_profile_path (line 248) | def get_profile_path():
    method get_recent_user_count (line 253) | def get_recent_user_count():
    method get_promotion_limit (line 256) | def get_promotion_limit():
    method get_promotion_method (line 259) | def get_promotion_method():
    method get_password (line 262) | def get_password(username=None):
    method get_password_google (line 269) | def get_password_google(username=None):
    method get_password_twitter (line 276) | def get_password_twitter(username=None):
    method get_download_path (line 283) | def get_download_path():
    method get_users_path (line 288) | def get_users_path():
    method get_config_path (line 293) | def get_config_path():
    method get_local_path (line 298) | def get_local_path():
    method get_reconnect_id (line 305) | def get_reconnect_id():
    method get_reconnect_url (line 308) | def get_reconnect_url():
    method get_remote_host (line 311) | def get_remote_host():
    method get_remote_port (line 314) | def get_remote_port():
    method get_remote_path (line 317) | def get_remote_path():
    method get_remote_username (line 320) | def get_remote_username():
    method get_remote_password (line 323) | def get_remote_password():
    method get_profile_method (line 326) | def get_profile_method():
    method get_date (line 329) | def get_date():
    method get_time (line 346) | def get_time():
    method get_schedule (line 362) | def get_schedule():
    method get_keywords (line 376) | def get_keywords():
    method get_text (line 386) | def get_text():
    method get_title (line 389) | def get_title():
    method get_skipped_users (line 392) | def get_skipped_users():
    method get_questions (line 395) | def get_questions():
    method get_upload_max (line 402) | def get_upload_max():
    method get_login_method (line 412) | def get_login_method():
    method get_upload_max_duration (line 417) | def get_upload_max_duration():
    method get_users (line 421) | def get_users():
    method get_user (line 442) | def get_user():
    method get_user_configs (line 445) | def get_user_configs():
    method get_user_config (line 449) | def get_user_config(username="default"):
    method get_user_config_path (line 467) | def get_user_config_path(username="default"):
    method get_username (line 471) | def get_username():
    method get_username_onlyfans (line 474) | def get_username_onlyfans(username=None):
    method get_username_google (line 481) | def get_username_google(username=None):
    method get_username_twitter (line 488) | def get_username_twitter(username=None):
    method get_remote_browser_host (line 495) | def get_remote_browser_host():
    method get_remote_browser_port (line 498) | def get_remote_browser_port():
    method get_profile (line 505) | def get_profile():
    method select_profile (line 508) | def select_profile():
    method get_verbosity (line 514) | def get_verbosity():
    method get_version (line 517) | def get_version():
    method get_user_num (line 520) | def get_user_num():
    method is_confirm (line 525) | def is_confirm():
    method is_cookies (line 528) | def is_cookies():
    method is_delete (line 531) | def is_delete():
    method is_delete_empty (line 534) | def is_delete_empty():
    method is_debug (line 537) | def is_debug(process=None):
    method is_debug_delay (line 545) | def is_debug_delay():
    method is_force_upload (line 548) | def is_force_upload():
    method is_keep (line 551) | def is_keep():
    method is_prefer_local (line 554) | def is_prefer_local():
    method is_prefer_local_following (line 557) | def is_prefer_local_following():
    method is_save_users (line 560) | def is_save_users():
    method is_reduce (line 563) | def is_reduce():
    method is_show_window (line 566) | def is_show_window():
    method is_tweeting (line 569) | def is_tweeting():
    method is_backup (line 572) | def is_backup():
    method is_skip_download (line 575) | def is_skip_download():
    method is_skip_upload (line 578) | def is_skip_upload():
    method set_bycategory (line 585) | def set_bycategory(cat):
    method set_category (line 588) | def set_category(cat):
    method set_cookies (line 591) | def set_cookies(value):
    method set_confirm (line 594) | def set_confirm(value):
    method set_email (line 597) | def set_email(email):
    method set_debug (line 600) | def set_debug(newValue):
    method set_username (line 607) | def set_username(username):
    method set_username_google (line 610) | def set_username_google(username):
    method set_username_twitter (line 613) | def set_username_twitter(username):
    method set_password (line 616) | def set_password(password):
    method set_password_google (line 619) | def set_password_google(password):
    method set_password_twitter (line 622) | def set_password_twitter(password):
    method set_prefer_local (line 625) | def set_prefer_local(buul):
    method set_prefer_local_following (line 628) | def set_prefer_local_following(buul):
  function delayForThirty (line 701) | def delayForThirty():

FILE: OnlySnarf/util/validators.py
  function valid_amount (line 19) | def valid_amount(s):
  function valid_date (line 29) | def valid_date(s):
  function valid_duration (line 35) | def valid_duration(s):
  function valid_promo_duration (line 45) | def valid_promo_duration(s):
  function valid_promo_expiration (line 53) | def valid_promo_expiration(s):
  function valid_expiration (line 60) | def valid_expiration(s):
  function valid_limit (line 69) | def valid_limit(s):
  function valid_month (line 79) | def valid_month(s):
  function valid_path (line 89) | def valid_path(s):
  function valid_url (line 101) | def valid_url(s):
  function valid_price (line 107) | def valid_price(s):
  function valid_schedule (line 115) | def valid_schedule(s):
  function valid_time (line 121) | def valid_time(s):

FILE: notes/animal.py
  class Animal (line 1) | class Animal:
    method __init__ (line 26) | def __init__(self, name, sound, num_legs=4):
    method says (line 42) | def says(self, sound=None):

FILE: notes/docstrings.py
  function my_function (line 23) | def my_function(my_arg, my_other_arg):
  class SimpleClass (line 115) | class SimpleClass:
    method say_hello (line 118) | def say_hello(self, name: str):
  class following (line 131) | class methods being documented, it’s now the module and any functions fo...
  function get_spreadsheet_cols (line 184) | def get_spreadsheet_cols(file_loc, print_cols=False):
  function main (line 210) | def main():

FILE: notes/notes1.py
  class HackerNewsSearchTest (line 11) | class HackerNewsSearchTest(unittest.TestCase):
    method setUp (line 13) | def setUp(self):
    method test_hackernews_search_for_testdrivenio (line 16) | def test_hackernews_search_for_testdrivenio(self):
    method test_hackernews_search_for_selenium (line 25) | def test_hackernews_search_for_selenium(self):
    method test_hackernews_search_for_testdriven (line 34) | def test_hackernews_search_for_testdriven(self):
    method test_hackernews_search_with_no_results (line 43) | def test_hackernews_search_with_no_results(self):
    method tearDown (line 52) | def tearDown(self):

FILE: notes/notes3.py
  function get_browser_and_wait (line 10) | def get_browser_and_wait(browser_data):

FILE: notes/notes4.py
  class TestExamples (line 16) | class TestExamples(unittest.TestCase):
    method setUp (line 18) | def setUp(self):
    method test_one (line 31) | def test_one(self):
    method test_two (line 43) | def test_two(self):
    method tearDown (line 53) | def tearDown(self):

FILE: notes/notes5.py
  function selenium (line 23) | def selenium(selenium):
  function test_one (line 28) | def test_one(selenium):
  function test_two (line 39) | def test_two(selenium):

FILE: notes/old/bot.py
  class Bot (line 27) | class Bot():
    method __init__ (line 33) | def __init__(self):
    method parse (line 45) | def parse(user=None):
    method parse_messages (line 81) | def parse_messages(self):
    method get_index (line 176) | def get_index():
    method prompt (line 185) | def prompt(user=None):
    method refresh (line 191) | def refresh(self):
    method refresher (line 195) | def refresher(self):
    method tipped (line 203) | def tipped(user=None, amount=None):

FILE: notes/old/config.py
  function checkBothCreds (line 14) | def checkBothCreds():
  function checkGoogle (line 19) | def checkGoogle():
  function checkOnlyFans (line 32) | def checkOnlyFans():
  function checkTwitter (line 40) | def checkTwitter():
  function createConfig (line 49) | def createConfig():
  function googleInstructions (line 75) | def googleInstructions():
  function setupConfig (line 79) | def setupConfig():
  function receiveGoogle (line 85) | def receiveGoogle():
  function receiveOnlyFans (line 91) | def receiveOnlyFans():
  function receiveTwitter (line 99) | def receiveTwitter():
  function refreshAll (line 106) | def refreshAll():
  function removeConfig (line 113) | def removeConfig():
  function removeGoogle (line 123) | def removeGoogle():
  function updateConfig (line 133) | def updateConfig():
  function updateOnlyFans (line 138) | def updateOnlyFans():
  function updateGoogle (line 152) | def updateGoogle():
  function updateTwitter (line 165) | def updateTwitter():
  function main (line 184) | def main():

FILE: notes/old/file-old.py
  class File (line 28) | class File():
    method __init__ (line 33) | def __init__(self):
    method backup (line 52) | def backup(self):
    method backup_text (line 67) | def backup_text(title):
    method backup_files (line 94) | def backup_files(files=[]):
    method check_size (line 115) | def check_size(self):
    method delete (line 143) | def delete(self):
    method delete_text (line 153) | def delete_text(title):
    method download_text (line 178) | def download_text(title):
    method get_ext (line 196) | def get_ext(self):
    method get_path (line 202) | def get_path(self):
    method get_title (line 218) | def get_title(self):
    method get_tmp (line 240) | def get_tmp():
    method get_type (line 256) | def get_type(self):
    method prepare (line 275) | def prepare(self):
    method remove_local (line 295) | def remove_local():
    method get_files (line 317) | def get_files():
    method get_files_by_folder (line 342) | def get_files_by_folder(path):
    method get_folder_by_name (line 364) | def get_folder_by_name(category, parent=None):
    method get_folders_of_folder_by_keywords (line 568) | def get_folders_of_folder_by_keywords(categoryFolder):
    method get_random_file (line 607) | def get_random_file():
    method get_images_of_folder (line 613) | def get_images_of_folder(folder):
    method get_videos_of_folder (line 649) | def get_videos_of_folder(folder):
    method get_folders_of_folder (line 685) | def get_folders_of_folder(folderPath):
    method get_files_by_category (line 722) | def get_files_by_category(cat, performer=None):
    method select_file (line 820) | def select_file(category, performer=None):
    method select_files (line 873) | def select_files():
    method select_file_upload_method (line 919) | def select_file_upload_method():
    method upload (line 971) | def upload(self):
  class Remote_File (line 1025) | class Remote_File(File):
    method __init__ (line 1026) | def __init__(self):
    method backup (line 1029) | def backup(self):
    method delete (line 1042) | def delete(self):
    method download (line 1049) | def download(self):
  class Folder (line 1062) | class Folder(File):
    method __init__ (line 1063) | def __init__(self):
    method backup (line 1067) | def backup(self):
    method check_size (line 1071) | def check_size(self):
    method download (line 1090) | def download(self):
    method get_files (line 1124) | def get_files(self):
    method get_title (line 1140) | def get_title(self):
    method prepare (line 1150) | def prepare():
  class Google_File (line 1160) | class Google_File(File):
    method __init__ (line 1162) | def __init__(self):
    method backup (line 1170) | def backup(self):
    method delete (line 1174) | def delete(self):
    method download_files (line 1179) | def download_files(files=[]):
    method download (line 1191) | def download(self):
    method get_ext (line 1202) | def get_ext(self):
    method get_id (line 1213) | def get_id(self):
    method get_file (line 1218) | def get_file(self):
    method get_files (line 1225) | def get_files():
    method get_files_by_category (line 1240) | def get_files_by_category(cat, performer=None):
    method get_random_file (line 1335) | def get_random_file():
    method get_mimetype (line 1344) | def get_mimetype(self):
    method get_parent (line 1349) | def get_parent(self):
    method get_path (line 1354) | def get_path(self):
    method get_title (line 1377) | def get_title(self):
    method prepare (line 1388) | def prepare(self):
    method select_file (line 1395) | def select_file(category, performer=None):
    method select_files (line 1426) | def select_files():
  class Google_Folder (line 1453) | class Google_Folder(Google_File):
    method __init__ (line 1454) | def __init__(self):
    method backup (line 1458) | def backup(self):
    method check_size (line 1462) | def check_size(self):
    method download (line 1468) | def download(self):
    method get_files (line 1502) | def get_files(self):
    method prepare (line 1517) | def prepare():
  class Image (line 1526) | class Image(File):
    method __init__ (line 1527) | def __init__(self):
    method prepare (line 1530) | def prepare(self):
  class Video (line 1536) | class Video(File):
    method __init__ (line 1537) | def __init__(self):
    method trim (line 1543) | def trim(self):
    method split (line 1548) | def split(self):
    method watermark (line 1554) | def watermark(self):
    method get_metadata (line 1558) | def get_metadata(self):
    method get_frames (line 1562) | def get_frames(self):
    method prepare (line 1566) | def prepare(self):
    method reduce (line 1573) | def reduce(self):
    method repair (line 1585) | def repair(self):

FILE: notes/old/google-old.py
  function authGoogle (line 58) | def authGoogle():
  function checkAuth (line 99) | def checkAuth():
  function backup_file (line 111) | def backup_file(file):
  function create_folders (line 153) | def create_folders():
  function delete_folder (line 180) | def delete_folder(folder):
  function cache_add (line 201) | def cache_add(folders):
  function cache_check (line 208) | def cache_check(folderName):
  function download_file (line 221) | def download_file(file):
  function get_files_by_folder_id (line 275) | def get_files_by_folder_id(folderID):
  function get_file (line 297) | def get_file(id_):
  function get_file_parent (line 318) | def get_file_parent(id_):
  function get_folder_by_name (line 341) | def get_folder_by_name(folderName, parent=None):
  function get_images_of_folder (line 382) | def get_images_of_folder(folder):
  function get_videos_of_folder (line 406) | def get_videos_of_folder(folder):
  function get_folders_of_folder_by_keywords (line 431) | def get_folders_of_folder_by_keywords(folder):
  function get_posted_folder_by_name (line 468) | def get_posted_folder_by_name(folderName, parent=None):
  function get_folders_of_folder (line 525) | def get_folders_of_folder(folder):
  function get_folder_root (line 560) | def get_folder_root():
  function upload_file (line 613) | def upload_file(file=None):
  function upload_gallery (line 647) | def upload_gallery(files=[]):

FILE: notes/old/remote.py
  function auth (line 8) | def auth():
  function backup_file (line 38) | def backup_file(file):
  function backup_files (line 60) | def backup_files(files):
  function upload_file (line 83) | def upload_file(file):
  function upload_files (line 104) | def upload_files(files):
  function delete_file (line 117) | def delete_file(file):
  function download_file (line 131) | def download_file(file):
  function prepare_dir (line 155) | def prepare_dir(sftp=None):
  function get_files (line 164) | def get_files(category=None, performer=None):
  function get_random_file (line 212) | def get_random_file(category=None, performer=None):
  function select_file (line 219) | def select_file(category, performer=None):
  function select_files (line 246) | def select_files():

FILE: notes/old/removed-cron.py
  function delete (line 8) | def delete(comment):
  function deleteAll (line 14) | def deleteAll():
  function disable (line 21) | def disable(comment):
  function enable (line 28) | def enable(comment):
  function create (line 35) | def create(comment, args=[], minute=None, hour=None):
  function list (line 51) | def list():
  function find (line 57) | def find(comment):
  function getAll (line 64) | def getAll():
  function discount (line 75) | def discount(user, amount=None, months=None):
  function message (line 85) | def message(user, text=None, image=None, price=None):
  function post (line 98) | def post(text):
  function upload (line 102) | def upload(opt):
  function uploadInput (line 106) | def uploadInput(type_, path):
  function checkScenes (line 111) | def checkScenes():
  function sendQueuedMessages (line 115) | def sendQueuedMessages():
  function test (line 122) | def test():

FILE: notes/old/selectstuff.py
  function select_user (line 14) | def select_user():
  function select_users (line 47) | def select_users():
  function select_username (line 60) | def select_username():
  function list_menu (line 87) | def list_menu():

FILE: notes/selenium-notes.py
  function run_browserstack (line 1) | def run_browserstack(self,os_name,os_version,browser,browser_version,rem...
  function remote (line 31) | def remote(self) -> None:
  function attach_to_session (line 52) | def attach_to_session(executor_url, session_id):
  function __init__ (line 74) | def __init__(self, command_executor = None, session_id = None, previous_...

FILE: notes/selenium-notes1.py
  function drag_and_drop_file (line 32) | def drag_and_drop_file(drop_target, path):

FILE: notes/testflask.py
  class TestFoo (line 4) | class TestFoo(flask_unittest.AppTestCase):
    method create_app (line 6) | def create_app(self):
    method setUp (line 10) | def setUp(self, app):
    method tearDown (line 14) | def tearDown(self, app):
    method test_foo_with_app (line 24) | def test_foo_with_app(self, app):
    method test_bar_with_app (line 30) | def test_bar_with_app(self, app):
    method test_baz_with_app (line 37) | def test_baz_with_app(self, app):

FILE: notes/testflaskclient.py
  class TestAPI (line 4) | class TestAPI(flask_unittest.ClientTestCase):
    method setUp (line 8) | def setUp(self, client):
    method tearDown (line 12) | def tearDown(self, client):
    method test_foo_with_client (line 22) | def test_foo_with_client(self, client):
    method test_bar_with_client (line 28) | def test_bar_with_client(self, client):

FILE: notes/unittest.py
  class TestSum (line 4) | class TestSum(unittest.TestCase):
    method test_sum (line 6) | def test_sum(self):
    method test_sum_tuple (line 9) | def test_sum_tuple(self):

FILE: notes/unittestskips.py
  class MyTestCase (line 1) | class MyTestCase(unittest.TestCase):
    method test_nothing (line 4) | def test_nothing(self):
    method test_format (line 9) | def test_format(self):
    method test_windows_support (line 14) | def test_windows_support(self):
    method test_maybe_skipped (line 18) | def test_maybe_skipped(self):

FILE: notes/unittesttestrunners.py
  function suite (line 1) | def suite():

FILE: tests/api/test_api.py
  class TestAPI (line 11) | class TestAPI(flask_unittest.ClientTestCase):
    method setUp (line 18) | def setUp(self, client):
    method tearDown (line 22) | def tearDown(self, client):
    method test_message (line 26) | def test_message(self, client):
    method test_post (line 34) | def test_post(self, client):

FILE: tests/selenium/browsers/test_brave.py
  class TestSeleniumBrave (line 9) | class TestSeleniumBrave(unittest.TestCase):
    method setUp (line 11) | def setUp(self):
    method tearDown (line 18) | def tearDown(self):
    method test_brave (line 23) | def test_brave(self):

FILE: tests/selenium/browsers/test_chrome.py
  class TestSeleniumChrome (line 9) | class TestSeleniumChrome(unittest.TestCase):
    method setUp (line 11) | def setUp(self):
    method tearDown (line 18) | def tearDown(self):
    method test_chrome (line 23) | def test_chrome(self):

FILE: tests/selenium/browsers/test_chromium.py
  class TestSeleniumChromium (line 9) | class TestSeleniumChromium(unittest.TestCase):
    method setUp (line 11) | def setUp(self):
    method tearDown (line 18) | def tearDown(self):
    method test_chromium (line 24) | def test_chromium(self):

FILE: tests/selenium/browsers/test_edge.py
  class TestSeleniumEdge (line 9) | class TestSeleniumEdge(unittest.TestCase):
    method setUp (line 11) | def setUp(self):
    method tearDown (line 18) | def tearDown(self):
    method xtest_edge (line 24) | def xtest_edge(self):

FILE: tests/selenium/browsers/test_firefox.py
  class TestSeleniumFirefox (line 9) | class TestSeleniumFirefox(unittest.TestCase):
    method setUp (line 11) | def setUp(self):
    method tearDown (line 18) | def tearDown(self):
    method test_firefox (line 23) | def test_firefox(self):

FILE: tests/selenium/browsers/test_ie.py
  class TestSeleniumIE (line 9) | class TestSeleniumIE(unittest.TestCase):
    method setUp (line 11) | def setUp(self):
    method tearDown (line 18) | def tearDown(self):
    method xtest_ie (line 24) | def xtest_ie(self):

FILE: tests/selenium/browsers/test_opera.py
  class TestSeleniumOpera (line 9) | class TestSeleniumOpera(unittest.TestCase):
    method setUp (line 11) | def setUp(self):
    method tearDown (line 18) | def tearDown(self):
    method xtest_opera (line 24) | def xtest_opera(self):

FILE: tests/selenium/reconnect/test_brave.py
  class TestSeleniumReconnectBrave (line 9) | class TestSeleniumReconnectBrave(unittest.TestCase):
    method setUp (line 11) | def setUp(self):
    method tearDown (line 18) | def tearDown(self):
    method test_reconnect_brave (line 24) | def test_reconnect_brave(self):

FILE: tests/selenium/reconnect/test_chrome.py
  class TestSeleniumReconnectChrome (line 9) | class TestSeleniumReconnectChrome(unittest.TestCase):
    method setUp (line 11) | def setUp(self):
    method tearDown (line 18) | def tearDown(self):
    method test_reconnect_chrome (line 24) | def test_reconnect_chrome(self):

FILE: tests/selenium/reconnect/test_chromium.py
  class TestSeleniumReconnectChromium (line 9) | class TestSeleniumReconnectChromium(unittest.TestCase):
    method setUp (line 11) | def setUp(self):
    method tearDown (line 18) | def tearDown(self):
    method test_reconnect_chromium (line 25) | def test_reconnect_chromium(self):

FILE: tests/selenium/reconnect/test_edge.py
  class TestSeleniumReconnectEdge (line 9) | class TestSeleniumReconnectEdge(unittest.TestCase):
    method setUp (line 11) | def setUp(self):
    method tearDown (line 18) | def tearDown(self):
    method test_reconnect_edge (line 25) | def test_reconnect_edge(self):

FILE: tests/selenium/reconnect/test_firefox.py
  class TestSeleniumReconnectFirefox (line 9) | class TestSeleniumReconnectFirefox(unittest.TestCase):
    method setUp (line 11) | def setUp(self):
    method tearDown (line 18) | def tearDown(self):
    method test_reconnect_firefox (line 24) | def test_reconnect_firefox(self):

FILE: tests/selenium/reconnect/test_ie.py
  class TestSeleniumReconnectIE (line 9) | class TestSeleniumReconnectIE(unittest.TestCase):
    method setUp (line 11) | def setUp(self):
    method tearDown (line 18) | def tearDown(self):
    method test_reconnect_ie (line 25) | def test_reconnect_ie(self):

FILE: tests/selenium/reconnect/test_opera.py
  class TestSeleniumReconnectOpera (line 9) | class TestSeleniumReconnectOpera(unittest.TestCase):
    method setUp (line 11) | def setUp(self):
    method tearDown (line 18) | def tearDown(self):
    method test_reconnect_opera (line 25) | def test_reconnect_opera(self):

FILE: tests/selenium/test_browsers.py
  class TestSeleniumBrowsers (line 9) | class TestSeleniumBrowsers(unittest.TestCase):
    method setUp (line 11) | def setUp(self):
    method tearDown (line 18) | def tearDown(self):
    method test_auto (line 23) | def test_auto(self):

FILE: tests/selenium/test_reconnect.py
  class TestSeleniumReconnect (line 9) | class TestSeleniumReconnect(unittest.TestCase):
    method setUp (line 11) | def setUp(self):
    method tearDown (line 18) | def tearDown(self):
    method test_reconnect (line 24) | def test_reconnect(self):

FILE: tests/selenium/test_remote.py
  class TestSeleniumRemote (line 12) | class TestSeleniumRemote(unittest.TestCase):
    method setUp (line 14) | def setUp(self):
    method tearDown (line 20) | def tearDown(self):
    method test_remote (line 24) | def test_remote(self):
    method test_remote_chrome (line 30) | def test_remote_chrome(self):
    method test_remote_firefox (line 36) | def test_remote_firefox(self):

FILE: tests/snarf/auth/test_google.py
  class TestAuth (line 9) | class TestAuth(unittest.TestCase):
    method setUp (line 11) | def setUp(self):
    method tearDown (line 16) | def tearDown(self):
    method test_login (line 21) | def test_login(self):
    method test_login_via_cookies (line 27) | def test_login_via_cookies(self):

FILE: tests/snarf/auth/test_onlyfans.py
  class TestAuth (line 9) | class TestAuth(unittest.TestCase):
    method setUp (line 11) | def setUp(self):
    method tearDown (line 16) | def tearDown(self):
    method test_login (line 20) | def test_login(self):
    method test_login_via_cookies (line 25) | def test_login_via_cookies(self):

FILE: tests/snarf/auth/test_twitter.py
  class TestAuth (line 9) | class TestAuth(unittest.TestCase):
    method setUp (line 11) | def setUp(self):
    method tearDown (line 16) | def tearDown(self):
    method test_login (line 20) | def test_login(self):
    method test_login_via_cookies (line 25) | def test_login_via_cookies(self):

FILE: tests/snarf/test_auth.py
  class TestAuth (line 9) | class TestAuth(unittest.TestCase):
    method setUp (line 11) | def setUp(self):
    method tearDown (line 17) | def tearDown(self):
    method test_login (line 20) | def test_login(self):
    method test_login_via_cookies (line 25) | def test_login_via_cookies(self):

FILE: tests/snarf/test_discount.py
  class TestSnarf (line 10) | class TestSnarf(unittest.TestCase):
    method setUp (line 12) | def setUp(self):
    method tearDown (line 19) | def tearDown(self):
    method test_discount (line 25) | def test_discount(self):
    method test_discount_max (line 29) | def test_discount_max(self):
    method test_discount_min (line 34) | def test_discount_min(self):

FILE: tests/snarf/test_expiration.py
  class TestSnarf (line 10) | class TestSnarf(unittest.TestCase):
    method setUp (line 12) | def setUp(self):
    method tearDown (line 17) | def tearDown(self):
    method test_poll (line 20) | def test_poll(self):

FILE: tests/snarf/test_message.py
  class TestSnarf (line 10) | class TestSnarf(unittest.TestCase):
    method setUp (line 12) | def setUp(self):
    method tearDown (line 19) | def tearDown(self):
    method test_message (line 24) | def test_message(self):
    method test_message_files_local (line 27) | def test_message_files_local(self):
    method test_message_files_remote (line 31) | def test_message_files_remote(self):
    method test_message_price (line 35) | def test_message_price(self):

FILE: tests/snarf/test_poll.py
  class TestSnarf (line 10) | class TestSnarf(unittest.TestCase):
    method setUp (line 12) | def setUp(self):
    method tearDown (line 18) | def tearDown(self):
    method test_poll (line 23) | def test_poll(self):

FILE: tests/snarf/test_post.py
  class TestSnarf (line 10) | class TestSnarf(unittest.TestCase):
    method setUp (line 12) | def setUp(self):
    method tearDown (line 20) | def tearDown(self):
    method test_post (line 27) | def test_post(self):
    method test_post_files (line 31) | def test_post_files(self):
    method test_post_price (line 35) | def test_post_price(self):
    method test_post_text (line 39) | def test_post_text(self):

FILE: tests/snarf/test_schedule.py
  class TestSnarf (line 14) | class TestSnarf(unittest.TestCase):
    method setUp (line 16) | def setUp(self):
    method tearDown (line 24) | def tearDown(self):
    method test_schedule (line 27) | def test_schedule(self):
    method test_schedule_date (line 31) | def test_schedule_date(self):
    method test_schedule_time (line 35) | def test_schedule_time(self):
    method test_schedule_calendar_day (line 46) | def test_schedule_calendar_day(self):
    method test_schedule_calendar_hour (line 50) | def test_schedule_calendar_hour(self):
    method test_schedule_calendar_minute (line 54) | def test_schedule_calendar_minute(self):
    method test_schedule_calendar_suffix (line 57) | def test_schedule_calendar_suffix(self):

FILE: tests/snarf/test_users.py
  class TestUsers (line 11) | class TestUsers(unittest.TestCase):
    method setUp (line 13) | def setUp(self):
    method tearDown (line 17) | def tearDown(self):
    method test_get_users (line 21) | def test_get_users(self):

FILE: tests/test_profile.py
  class TestProfile (line 12) | class TestProfile(unittest.TestCase):
    method setUp (line 14) | def setUp(self):
    method tearDown (line 19) | def tearDown(self):
    method test_profile_backup (line 23) | def test_profile_backup(self):
    method test_profile_syncfrom (line 28) | def test_profile_syncfrom(self):
    method test_profile_syncto (line 33) | def test_profile_syncto(self):

FILE: tests/test_promotion.py
  class TestPromotion (line 12) | class TestPromotion(unittest.TestCase):
    method setUp (line 14) | def setUp(self):
    method tearDown (line 19) | def tearDown(self):
    method test_promotion_campaign (line 23) | def test_promotion_campaign(self):
    method test_promotion_trial (line 28) | def test_promotion_trial(self):
    method test_promotion_user (line 33) | def test_promotion_user(self):
    method test_promotion_grandfather (line 38) | def test_promotion_grandfather(self):
Condensed preview — 145 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (666K chars).
[
  {
    "path": ".gitignore",
    "chars": 458,
    "preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 32788,
    "preview": "# 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 "
  },
  {
    "path": "LICENSE.txt",
    "chars": 1064,
    "preview": "MIT License\n\nCopyright (c) 2018 Skeetzo\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof"
  },
  {
    "path": "MANIFEST.in",
    "chars": 246,
    "preview": "# Config\ninclude OnlySnarf/conf/*\n\nexclude OnlySnarf/bin\nexclude OnlySnarf/build\nexclude OnlySnarf/dist\nexclude OnlySnar"
  },
  {
    "path": "OnlySnarf/__init__.py",
    "chars": 24,
    "preview": "from .snarf import Snarf"
  },
  {
    "path": "OnlySnarf/__main__.py",
    "chars": 387,
    "preview": "import sys\nimport os\n\ndef main(args=None):\n    \"\"\"The main routine.\"\"\"\n    if args is None:\n        args = sys.argv[1:]\n"
  },
  {
    "path": "OnlySnarf/classes/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "OnlySnarf/classes/discount.py",
    "chars": 4388,
    "preview": "from ..lib.driver import Driver\nfrom ..util.settings import Settings\nfrom .user import User\n##\nfrom ..util.defaults impo"
  },
  {
    "path": "OnlySnarf/classes/element.py",
    "chars": 1892,
    "preview": "# for easily interacting with changeable page elements\n\nfrom ..util.settings import Settings\nfrom ..elements.driver impo"
  },
  {
    "path": "OnlySnarf/classes/file.py",
    "chars": 16142,
    "preview": "import os, shutil, random, sys\nfrom os import walk\n##\nfrom ..lib.ffmpeg import ffmpeg\nfrom ..util.settings import Settin"
  },
  {
    "path": "OnlySnarf/classes/message.py",
    "chars": 12607,
    "preview": "import re\nfrom datetime import datetime\nfrom decimal import Decimal\nfrom re import sub\n##\nfrom ..lib.driver import Drive"
  },
  {
    "path": "OnlySnarf/classes/poll.py",
    "chars": 1986,
    "preview": "import re\nfrom datetime import datetime\nfrom ..lib.driver import Driver\nfrom ..util.settings import Settings\nfrom .user "
  },
  {
    "path": "OnlySnarf/classes/profile.py",
    "chars": 14542,
    "preview": "#!/usr/bin/python3\n# Profile Settings\nimport json\nimport inquirer\n##\nfrom ..lib.driver import Driver\nfrom ..util.setting"
  },
  {
    "path": "OnlySnarf/classes/promotion.py",
    "chars": 10171,
    "preview": "import re\nfrom datetime import datetime\n##\n\nfrom ..lib.driver import Driver\nfrom ..util import defaults as DEFAULT\nfrom "
  },
  {
    "path": "OnlySnarf/classes/schedule.py",
    "chars": 4891,
    "preview": "from datetime import datetime\n\nfrom ..util import defaults as DEFAULT\nfrom ..util.settings import Settings\n\nclass Schedu"
  },
  {
    "path": "OnlySnarf/classes/user.py",
    "chars": 20341,
    "preview": "import json\nimport time\nimport os\nimport random\nimport threading\nfrom datetime import datetime, timedelta\n##\nfrom ..util"
  },
  {
    "path": "OnlySnarf/conf/config.conf",
    "chars": 2249,
    "preview": "[ARGS]\n\n## General ##\n# auto, onlyfans, or twitter\n#login = auto\n#prefer_local = True\n#save_users = False\n#upload_max = "
  },
  {
    "path": "OnlySnarf/conf/test-config.conf",
    "chars": 2345,
    "preview": "[ARGS]\n\n## General ##\n# auto, onlyfans, or twitter\nlogin = auto\nprefer_local = True\nsave_users = False\nupload_max = 10\nu"
  },
  {
    "path": "OnlySnarf/conf/users/example-user.conf",
    "chars": 169,
    "preview": "[ONLYFANS]\nusername = $USERNAME\npassword = $PASSWORD\n\n# not working\n[GOOGLE]\nusername = $UGOOGLE\npassword = $PGOOGLE\n\n[T"
  },
  {
    "path": "OnlySnarf/elements/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "OnlySnarf/elements/driver.py",
    "chars": 15689,
    "preview": "# general driver elements\n\nELEMENTS = [\n\n    {\n        \"name\": \"sendButton\",\n        \"classes\": [\"g-btn.m-rounded\"],\n   "
  },
  {
    "path": "OnlySnarf/elements/login.py",
    "chars": 1401,
    "preview": "##\n# unused, from Driver\n##\n# LOGIN_FORM = \"b-loginreg__form\"\n# TWITTER_LOGIN0 = \"//a[@class='g-btn m-rounded m-flex m-l"
  },
  {
    "path": "OnlySnarf/elements/profile.py",
    "chars": 11335,
    "preview": "# profile settings elements\n\nELEMENTS = [\n    ## Settings ##\n\n    ## Account\n    # cover image enter\n    {\n        \"name"
  },
  {
    "path": "OnlySnarf/lib/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "OnlySnarf/lib/config.py",
    "chars": 13956,
    "preview": "import os\nimport sys\nimport json\nimport time\nimport shutil\nimport inquirer\nimport fileinput\n# from pathlib import Path\n#"
  },
  {
    "path": "OnlySnarf/lib/driver.py",
    "chars": 167866,
    "preview": "import re\nimport random\nimport os\nimport shutil\nimport json\nimport pathlib\nimport time\nimport wget\nimport pickle\nfrom da"
  },
  {
    "path": "OnlySnarf/lib/ffmpeg.py",
    "chars": 9500,
    "preview": "import ffmpeg\nimport datetime\nimport os\n##\nfrom ..util.settings import Settings\n\n##################\n##### FFMPEG #####\n#"
  },
  {
    "path": "OnlySnarf/lib/menu.py",
    "chars": 5097,
    "preview": "#!/usr/bin/python3\n\nimport os\nimport time\nimport random\nimport sys\nimport inquirer\n##\nfrom ..lib.driver import Driver\nfr"
  },
  {
    "path": "OnlySnarf/server/api.py",
    "chars": 2195,
    "preview": "import os\nimport json\nfrom flask import Flask, request\n\nfrom ..util.config import config\nfrom ..util.settings import Set"
  },
  {
    "path": "OnlySnarf/snarf.py",
    "chars": 5266,
    "preview": "#!/usr/bin/python3\n\nfrom .lib.driver import Driver\nfrom .lib.config import Config\nfrom .lib.menu import Menu\nfrom .util."
  },
  {
    "path": "OnlySnarf/util/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "OnlySnarf/util/args.py",
    "chars": 1327,
    "preview": "import argparse\nfrom typing import Dict, Any\n\nargs: Dict[str, Any] = {}\n\n###############################################"
  },
  {
    "path": "OnlySnarf/util/colorize.py",
    "chars": 1062,
    "preview": "\ncolors = {\n    'blue': '\\033[94m',\n    'magenta': '\\033[35m',\n\t'header': '\\033[48;1;34m',\n\t'teal': '\\033[96m',\n\t'pink':"
  },
  {
    "path": "OnlySnarf/util/config.py",
    "chars": 1591,
    "preview": "# parse config file while maintaining default values from args\n\nimport configparser\nimport os\n\n## SAME as Settings.get_b"
  },
  {
    "path": "OnlySnarf/util/defaults.py",
    "chars": 3338,
    "preview": "import os\nfrom datetime import datetime\nfrom pathlib import Path\n\n##\n# Defaults \n##\n\nACTIONS = [ \"Discount\", \"Message\", "
  },
  {
    "path": "OnlySnarf/util/logger.py",
    "chars": 2025,
    "preview": "import os\nimport logging\nfrom pathlib import Path\nfrom . import defaults as DEFAULT\nfrom .config import config\n\nloglevel"
  },
  {
    "path": "OnlySnarf/util/optional_args.py",
    "chars": 12695,
    "preview": "import argparse\nfrom .validators import valid_amount, valid_price, valid_path\nfrom .validators import valid_date, valid_"
  },
  {
    "path": "OnlySnarf/util/settings.py",
    "chars": 21582,
    "preview": "import pkg_resources\nimport time\nimport os, json, sys\nfrom datetime import datetime\nfrom pathlib import Path\n##\nfrom .co"
  },
  {
    "path": "OnlySnarf/util/validators.py",
    "chars": 3934,
    "preview": "import argparse, os\nimport validators\nfrom datetime import datetime\nfrom . import defaults as DEFAULT\n\n# Validators\n\n#\n#"
  },
  {
    "path": "Pipfile",
    "chars": 139,
    "preview": "[[source]]\nurl = \"https://pypi.org/simple\"\nverify_ssl = true\nname = \"pypi\"\n\n[packages]\n\n[dev-packages]\n\n[requires]\npytho"
  },
  {
    "path": "README.md",
    "chars": 4049,
    "preview": "<h1 align=\"center\">OnlySnarf</h1>\n<p align=\"center\"><img src=\"public/images/snarf-missionary.jpg\" alt=\"Shnarf\" width=\"40"
  },
  {
    "path": "bin/aws-setup.sh",
    "chars": 550,
    "preview": "#!/bin/bash\n\n# AWS Linux setup steps:\n\n# ssh keys\nssh-keygen -t ed25519 -C \"WebmasterSkeetzo@gmail.com\"\neval \"$(ssh-agen"
  },
  {
    "path": "bin/clean.sh",
    "chars": 402,
    "preview": "#!/usr/bin/env bash\n# git filter-branch -f --tree-filter 'rm -rf ./OnlySnarf/google_creds.txt' HEAD\n\nrm -rf dist/ build/"
  },
  {
    "path": "bin/demo-scripts.sh",
    "chars": 1005,
    "preview": "##################\n## Demo Scripts ##\n##################\n\n# Discount\nsnarf -debug discount -user random -amount max -mon"
  },
  {
    "path": "bin/drivers/check-chrome.sh",
    "chars": 243,
    "preview": "#!/usr/bin/env bash\necho \"Google Version Check:\"\ngoogle-chrome --version | (echo -n \"stable => \" && cat)\ngoogle-chrome-b"
  },
  {
    "path": "bin/drivers/check-firefox.sh",
    "chars": 120,
    "preview": "#!/usr/bin/env bash\necho \"Firefox Version Check:\"\ngeckodriver --version | head -n 1 | (echo -n \"geckodriver => \" && cat)"
  },
  {
    "path": "bin/drivers/check.sh",
    "chars": 54,
    "preview": "#!/usr/bin/env bash\npython -m build\ntwine check dist/*"
  },
  {
    "path": "bin/drivers/fix-chromedriver.sh",
    "chars": 538,
    "preview": "#!/usr/bin/env bash\n\n# didn't work\n\n# https://stackoverflow.com/questions/65617246/issues-running-selenium-with-chromedr"
  },
  {
    "path": "bin/drivers/fix-firefox-profile-error.sh",
    "chars": 462,
    "preview": "#!/bin/bash\n# https://support.mozilla.org/en-US/kb/install-firefox-linux#w_install-firefox-from-mozilla-builds-for-advan"
  },
  {
    "path": "bin/drivers/install-chrome.sh",
    "chars": 1400,
    "preview": "#!/usr/bin/env bash\n\nsudo apt-get remove google-chrome-stable --purge -y\nsudo apt-get remove google-chrome-beta --purge "
  },
  {
    "path": "bin/drivers/install-chromedriver-aws.sh",
    "chars": 448,
    "preview": "#!/bin/bash\n\n# install chromedriver on aws ec2\n\ncd tmp\nwget https://chromedriver.storage.googleapis.com/2.37/chromedrive"
  },
  {
    "path": "bin/drivers/install-chromedriver-rpi.sh",
    "chars": 54,
    "preview": "#!/bin/bash\nsudo apt-get install chromium-chromedriver"
  },
  {
    "path": "bin/drivers/install-firefox.sh",
    "chars": 398,
    "preview": "#!/usr/bin/env bash\n###############################\nVERSION=\"0.31.0\"\nBIT=\"64\"\n#\nwget \"https://github.com/mozilla/geckodr"
  },
  {
    "path": "bin/drivers/install-geckodriver-arm.sh",
    "chars": 358,
    "preview": "#!/usr/bin/env bash\n# https://raspberrypi.stackexchange.com/questions/63258/selenium-firefox-oserror-errno-8-exec-format"
  },
  {
    "path": "bin/drivers/install-geckodriver-rpi.sh",
    "chars": 313,
    "preview": "#!/bin/bash\n# doesn't work\n\nsudo apt-get upgrade\nsudo apt-get update\nsudo pip3 install selenium\nsudo apt-get install ice"
  },
  {
    "path": "bin/drivers/switch-firefox.sh",
    "chars": 378,
    "preview": "#!/bin/bash\nsudo snap remove firefox\nsudo add-apt-repository ppa:mozillateam/ppa\necho '\n\tPackage: *\n\tPin: release o=LP-P"
  },
  {
    "path": "bin/install.sh",
    "chars": 39,
    "preview": "#!/bin/bash\nyes | pip install onlysnarf"
  },
  {
    "path": "bin/run-tests.sh",
    "chars": 76,
    "preview": "#!/bin/bash\npython setup.py install\npytest tests/selenium\npytest tests/snarf"
  },
  {
    "path": "bin/save.sh",
    "chars": 118,
    "preview": "#!/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",
    "chars": 81,
    "preview": "#!/bin/sh\npython ./OnlySnarf/api/index.py -debug -verbose -verbose -verbose users"
  },
  {
    "path": "bin/start-api.sh",
    "chars": 324,
    "preview": "#!/bin/sh\n# -users arg is provided to squelch requirement from onlysnarf args\npython ./OnlySnarf/api/index.py -verbose u"
  },
  {
    "path": "bin/test-all.sh",
    "chars": 69,
    "preview": "#!/bin/bash\npytest tests/api\npytest tests/selenium\npytest tests/snarf"
  },
  {
    "path": "bin/test-api-remote.sh",
    "chars": 672,
    "preview": "#!/bin/bash\n\n# curl -X POST -H \"Content-Type: application/json\" -d '{\n#   \"text\": \"your mom\",\n#   \"input\": \"https://gith"
  },
  {
    "path": "bin/test-api.sh",
    "chars": 586,
    "preview": "#!/bin/bash\n\n# curl -X POST -H \"Content-Type: application/json\" -d '{\n#   \"text\": \"your mom\",\n#   \"schedule\": \"07/18/202"
  },
  {
    "path": "bin/test.sh",
    "chars": 2139,
    "preview": "#!/bin/bash\n# list of test scripts and other useful commands\n\ngit clone --depth 1  --branch development git@github.com:s"
  },
  {
    "path": "bin/update-and-start.sh",
    "chars": 104,
    "preview": "#!/bin/bash\n/usr/bin/pip install -U onlysnarf\n/usr/local/bin/snarf -debug -verbose -verbose -verbose api"
  },
  {
    "path": "bin/upload-test.sh",
    "chars": 132,
    "preview": "#!/usr/bin/env bash\nif [ -z \"$1\" ]; then\n\tset \"upload\"\nfi\nbin/save.sh\nwait\npython -m build\ntwine upload --verbose -r tes"
  },
  {
    "path": "bin/upload.sh",
    "chars": 131,
    "preview": "#!/usr/bin/env bash\nif [ -z \"$1\" ]; then\n\tset \"upload\"\nfi\npython -m pip freeze\nbin/save.sh\nwait\npython -m build\ntwine up"
  },
  {
    "path": "bin/virtualenv.sh",
    "chars": 605,
    "preview": "#!/bin/bash\n# basic setup script for python3 virtual environments\nsudo apt-get -y install python3-virtualenv python3-pip"
  },
  {
    "path": "notes/Self-serving an ARM build",
    "chars": 1072,
    "preview": "https://firefox-source-docs.mozilla.org/testing/geckodriver/ARM.html\nSelf-serving an ARM build¶\n\nMozilla announced the i"
  },
  {
    "path": "notes/adding-phantomjs.py",
    "chars": 1077,
    "preview": "# https://stackoverflow.com/questions/13287490/is-there-a-way-to-use-phantomjs-in-python\n\n# The easiest way to use Phant"
  },
  {
    "path": "notes/animal.py",
    "chars": 1639,
    "preview": "class Animal:\n    \"\"\"\n    A class used to represent an Animal\n\n    ...\n\n    Attributes\n    ----------\n    says_str : str"
  },
  {
    "path": "notes/docstrings.py",
    "chars": 6069,
    "preview": "\"\"\"\nSummary line.\n\nExtended description of function.\n\nParameters\n----------\narg1 : int\n    Description of arg1\narg2 : st"
  },
  {
    "path": "notes/login-state.py",
    "chars": 2536,
    "preview": "https://stackoverflow.com/questions/35641019/how-do-you-use-credentials-saved-by-the-browser-in-auto-login-script-in-pyt"
  },
  {
    "path": "notes/notes1.py",
    "chars": 1951,
    "preview": "# https://testdriven.io/blog/distributed-testing-with-selenium-grid/\n\n\nimport time\nimport unittest\n\nfrom selenium import"
  },
  {
    "path": "notes/notes2.py",
    "chars": 1541,
    "preview": "# https://crossbrowsertesting.com/blog/selenium/selenium-design-patterns/\n\n\nimport org.openqa.selenium.WebElement;\nimpor"
  },
  {
    "path": "notes/notes3.py",
    "chars": 1128,
    "preview": "\nbrowsers = [\n   {“platform”: “Windows 7 64-bit”, “browserName”: “Internet Explorer”,”version”: “10”, “name”: “Python Pa"
  },
  {
    "path": "notes/notes4.py",
    "chars": 1964,
    "preview": "# https://www.gridlastic.com/python-code-example.html\n\npip install pytest\npip install pytest-xdist\npip install pytest-re"
  },
  {
    "path": "notes/notes5.py",
    "chars": 1178,
    "preview": "pip install pytest-selenium\npip install pytest-variables\n\n\n JSON config file (capabilities.json) \n{ \"capabilities\": {\n\t\""
  },
  {
    "path": "notes/notes6.py",
    "chars": 412,
    "preview": "PROXY = \"hub_subdomain.gridlastic.com:8001\"; # hosted Squid proxy on your selenium grid hub\n#PROXY = \"your_gridlastic_co"
  },
  {
    "path": "notes/old/bot.py",
    "chars": 6881,
    "preview": "## unused ##\n\nimport time\nimport threading\nimport concurrent.futures\n##\nfrom OnlySnarf.driver import Driver\nfrom OnlySna"
  },
  {
    "path": "notes/old/config.py",
    "chars": 10450,
    "preview": "#!/usr/bin/python\n# setup & update script for config\n\nimport os\nimport sys\nimport json\nimport shutil\n##\nfrom OnlySnarf.l"
  },
  {
    "path": "notes/old/file-old.py",
    "chars": 47162,
    "preview": "import os, shutil, random, sys\nimport PyInquirer\nfrom PIL import Image\nfrom os import walk\n##\nfrom ..lib import google a"
  },
  {
    "path": "notes/old/google-old.py",
    "chars": 22327,
    "preview": "#!/usr/bin/python3\n\n# hide annoying google warning\nimport logging\nlogging.getLogger('googleapicliet.discovery_cache').se"
  },
  {
    "path": "notes/old/remote.py",
    "chars": 9494,
    "preview": "import pysftp, os\nimport PyInquirer\nimport random\n##\nfrom ..util.settings import Settings\n\n\ndef auth():\n\t\n\tglobal HOSTNA"
  },
  {
    "path": "notes/old/removed-args.md",
    "chars": 2551,
    "preview": "# Removed / Old\n\nThese args / flags have been deleted or have had their behavior's simplified elsewhere:\n\n-action [disco"
  },
  {
    "path": "notes/old/removed-cron.py",
    "chars": 5971,
    "preview": "from crontab import CronTab\nfrom .settings import Settings\n\n################\n##### Cron #####\n################\n\ndef dele"
  },
  {
    "path": "notes/old/removed-readme.md",
    "chars": 4021,
    "preview": "## Previews\n![preview](https://github.com/skeetzo/onlysnarf/blob/master/images/preview.jpeg)\n[Gallery](https://github.co"
  },
  {
    "path": "notes/old/removed-selenium-options.py",
    "chars": 1082,
    "preview": "\n# Chrome\noptions.add_argument(\"--disable-setuid-sandbox\")\noptions.add_argument(\"--disable-dev-shm-usage\") # overcome li"
  },
  {
    "path": "notes/old/removed_args.py",
    "chars": 7580,
    "preview": "###################################################################\n##                          DEPRECATED              "
  },
  {
    "path": "notes/old/selectstuff.py",
    "chars": 4846,
    "preview": "if str(username) == \"all\":\n    return User.get_all_users()\nelif str(username) == \"recent\":\n    return User.get_recent_us"
  },
  {
    "path": "notes/onlysnarf_api.service",
    "chars": 200,
    "preview": "[Unit]\nDescription=OnlySnarf API\nAfter=network.target\n\n[Service]\nWorkingDirectory=/home/snarf/\nExecStart=/home/snarf/upd"
  },
  {
    "path": "notes/scroll-notes.py",
    "chars": 518,
    "preview": "SCROLL_PAUSE_TIME = 0.5\n\n# Get scroll height\nlast_height = driver.execute_script(\"return document.body.scrollHeight\")\n\nw"
  },
  {
    "path": "notes/selenium-notes.py",
    "chars": 5260,
    "preview": "def run_browserstack(self,os_name,os_version,browser,browser_version,remote_project_name,remote_build_name):\n    \"Run th"
  },
  {
    "path": "notes/selenium-notes1.py",
    "chars": 1269,
    "preview": "# https://stackoverflow.com/questions/43382447/python-with-selenium-drag-and-drop-from-file-system-to-webdriver\n\n\nJS_DRO"
  },
  {
    "path": "notes/supervisor-api.txt",
    "chars": 227,
    "preview": "\nadd to supervisord.conf:\n\n[program:flask_app]\ncommand = FLASK_ENV=production python api/index.py users\ndirectory = /hom"
  },
  {
    "path": "notes/testflask.py",
    "chars": 1410,
    "preview": "import flask_unittest\nfrom flaskr.db import get_db\n\nclass TestFoo(flask_unittest.AppTestCase):\n\n    def create_app(self)"
  },
  {
    "path": "notes/testflaskclient.py",
    "chars": 1271,
    "preview": "import flask_unittest\nimport flask.globals\n\nclass TestAPI(flask_unittest.ClientTestCase):\n    # Assign the `Flask` app o"
  },
  {
    "path": "notes/testrunners.py",
    "chars": 2279,
    "preview": "Executing Test Runners\n\nThe Python application that executes your test code, checks the assertions, and gives you test r"
  },
  {
    "path": "notes/unittest.py",
    "chars": 1766,
    "preview": "import unittest\n\n\nclass TestSum(unittest.TestCase):\n\n    def test_sum(self):\n        self.assertEqual(sum([1, 2, 3]), 6,"
  },
  {
    "path": "notes/unittestskips.py",
    "chars": 749,
    "preview": "class MyTestCase(unittest.TestCase):\n\n    @unittest.skip(\"demonstrating skipping\")\n    def test_nothing(self):\n        s"
  },
  {
    "path": "notes/unittesttestrunners.py",
    "chars": 271,
    "preview": "def suite():\n    suite = unittest.TestSuite()\n    suite.addTest(WidgetTestCase('test_default_widget_size'))\n    suite.ad"
  },
  {
    "path": "notes/windows-python-setup.txt",
    "chars": 276,
    "preview": "\nhttps://stackoverflow.com/questions/4822400/register-an-exe-so-you-can-run-it-from-any-command-line-in-windows\n\nC:\\User"
  },
  {
    "path": "public/docs/help.md",
    "chars": 4382,
    "preview": "**Note**: General options go in front of the chosen subcommand. Options specific to the subcommand go after the subcomma"
  },
  {
    "path": "public/docs/menu.md",
    "chars": 6236,
    "preview": "# Menu\n\n`snarf *args`\n\nPlease refer to the example config file provided for a complete listing of available options.\n\n##"
  },
  {
    "path": "setup.cfg",
    "chars": 39,
    "preview": "[metadata]\ndescription_file = README.md"
  },
  {
    "path": "setup.py",
    "chars": 1398,
    "preview": "import setuptools\n\nwith open(\"README.md\", \"r\") as fh:\n    long_description = fh.read()\n\nsetuptools.setup(\n    name=\"Only"
  },
  {
    "path": "tests/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/api/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/api/test_api.py",
    "chars": 960,
    "preview": "import os\nos.environ['ENV'] = \"test\"\n\nimport flask_unittest\nimport flask.globals\n\nimport json\n\nfrom OnlySnarf.api import"
  },
  {
    "path": "tests/selenium/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/selenium/browsers/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/selenium/browsers/test_brave.py",
    "chars": 871,
    "preview": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver"
  },
  {
    "path": "tests/selenium/browsers/test_chrome.py",
    "chars": 873,
    "preview": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver"
  },
  {
    "path": "tests/selenium/browsers/test_chromium.py",
    "chars": 916,
    "preview": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver"
  },
  {
    "path": "tests/selenium/browsers/test_edge.py",
    "chars": 895,
    "preview": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver"
  },
  {
    "path": "tests/selenium/browsers/test_firefox.py",
    "chars": 879,
    "preview": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver"
  },
  {
    "path": "tests/selenium/browsers/test_ie.py",
    "chars": 883,
    "preview": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver"
  },
  {
    "path": "tests/selenium/browsers/test_opera.py",
    "chars": 899,
    "preview": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver"
  },
  {
    "path": "tests/selenium/reconnect/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/selenium/reconnect/test_brave.py",
    "chars": 985,
    "preview": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver"
  },
  {
    "path": "tests/selenium/reconnect/test_chrome.py",
    "chars": 993,
    "preview": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver"
  },
  {
    "path": "tests/selenium/reconnect/test_chromium.py",
    "chars": 1024,
    "preview": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver"
  },
  {
    "path": "tests/selenium/reconnect/test_edge.py",
    "chars": 1008,
    "preview": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver"
  },
  {
    "path": "tests/selenium/reconnect/test_firefox.py",
    "chars": 993,
    "preview": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver"
  },
  {
    "path": "tests/selenium/reconnect/test_ie.py",
    "chars": 1000,
    "preview": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver"
  },
  {
    "path": "tests/selenium/reconnect/test_opera.py",
    "chars": 1016,
    "preview": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver"
  },
  {
    "path": "tests/selenium/test_browsers.py",
    "chars": 837,
    "preview": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver"
  },
  {
    "path": "tests/selenium/test_reconnect.py",
    "chars": 976,
    "preview": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver"
  },
  {
    "path": "tests/selenium/test_remote.py",
    "chars": 1315,
    "preview": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\n# from OnlySnarf.util imp"
  },
  {
    "path": "tests/snarf/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/snarf/auth/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/snarf/auth/test_google.py",
    "chars": 974,
    "preview": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver"
  },
  {
    "path": "tests/snarf/auth/test_onlyfans.py",
    "chars": 922,
    "preview": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver"
  },
  {
    "path": "tests/snarf/auth/test_twitter.py",
    "chars": 921,
    "preview": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver"
  },
  {
    "path": "tests/snarf/test_auth.py",
    "chars": 924,
    "preview": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.lib.driver"
  },
  {
    "path": "tests/snarf/test_discount.py",
    "chars": 1486,
    "preview": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.util impor"
  },
  {
    "path": "tests/snarf/test_expiration.py",
    "chars": 701,
    "preview": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.util impor"
  },
  {
    "path": "tests/snarf/test_message.py",
    "chars": 1656,
    "preview": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.util impor"
  },
  {
    "path": "tests/snarf/test_poll.py",
    "chars": 821,
    "preview": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.util impor"
  },
  {
    "path": "tests/snarf/test_post.py",
    "chars": 1396,
    "preview": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\nfrom OnlySnarf.util impor"
  },
  {
    "path": "tests/snarf/test_schedule.py",
    "chars": 2011,
    "preview": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\nimport datetime\n\nfrom OnlySnarf.util.config import config\nfrom Only"
  },
  {
    "path": "tests/snarf/test_users.py",
    "chars": 1470,
    "preview": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\n# from OnlySnarf.util imp"
  },
  {
    "path": "tests/test_profile.py",
    "chars": 1209,
    "preview": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\n# from OnlySnarf.util imp"
  },
  {
    "path": "tests/test_promotion.py",
    "chars": 1451,
    "preview": "import os\nos.environ['ENV'] = \"test\"\nimport unittest\n\nfrom OnlySnarf.util.config import config\n# from OnlySnarf.util imp"
  }
]

About this extraction

This page contains the full source code of the skeetzo/onlysnarf GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 145 files (611.6 KB), approximately 140.5k tokens, and a symbol index with 803 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!