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 = { files: this.files }; ['dragenter', 'dragover', 'drop'].forEach(function (name) { var evt = document.createEvent('MouseEvent'); evt.initMouseEvent(name, !0, !0, window, 0, 0, 0, x, y, !1, !1, !1, !1, 0, null); evt.dataTransfer = dataTransfer; target.dispatchEvent(evt); }); setTimeout(function () { document.body.removeChild(input); }, 25); }; document.body.appendChild(input); return input; """ try: Settings.maybe_print("dragging and dropping...") Settings.dev_print("drop target: {}".format(drop_target.get_attribute("innerHTML"))) # BUG: requires double to register file upload file_input = drop_target.parent.execute_script(JS_DROP_FILE, drop_target, 0, 0) file_input.send_keys(path) file_input = drop_target.parent.execute_script(JS_DROP_FILE, drop_target, 50, 50) file_input.send_keys(path) Settings.debug_delay_check() return True except Exception as e: Settings.err_print(e) return False def enter_text(self, text): """ Enter the provided text into the page's text area Must be ran on a page with an OnlyFans text area. Parameters ---------- text : str The text to enter Returns ------- bool Whether or not entering the text was successful """ try: Settings.dev_print("entering text: "+text) # extra redundancy in action chain for getting the text entry area # for clearing text field with action chain: # https://stackoverflow.com/questions/45690688/clear-selenium-action-chains textEntry = self.browser.find_element(By.ID, "new_post_text_input") action = ActionChains(self.browser) action.move_to_element(textEntry) action.click(on_element = textEntry) action.double_click() action.click_and_hold() action.send_keys(Keys.CLEAR) action.send_keys(str(text)) action.perform() ## TODO ## check text was entered properly ## does not work # print(self.browser.find_element(By.ID, "new_post_text_input").get_attribute('innerHTML')) # Settings.debug_delay_check() # print(self.browser.find_element(By.ID, "new_post_text_input").get_attribute('innerHTML')) # if self.browser.find_element(By.ID, "new_post_text_input").get_attribute('innerHTML') != text: # Settings.dev_print("failed to enter text") # return False Settings.dev_print("successfully entered text") return True except Exception as e: Settings.dev_print(e) return False @staticmethod def error_checker(e): """ Custom error checker Parameters ---------- e : str Error text """ if "Unable to locate element" in str(e): Settings.warn_print("onlysnarf may require an update") if "Message: " in str(e): return Settings.dev_print(e) def error_window_upload(self): """Closes error window that appears during uploads for 'duplicate' files""" try: element = Element.get_element_by_name("errorUpload") error_buttons = self.browser.find_elements(By.CLASS_NAME, element.getClass()) Settings.dev_print("errors btns: {}".format(len(error_buttons))) if len(error_buttons) == 0: return True for butt in error_buttons: if butt.get_attribute("innerHTML").strip() == "Close" and butt.is_enabled(): Settings.maybe_print("upload error message, closing") butt.click() Settings.maybe_print("success: upload error message closed") time.sleep(0.5) return True return False except Exception as e: Driver.error_checker(e) return False ###################### ##### Expiration ##### ###################### def expires(self, expiration): """ Enters the provided expiration duration for a post Must be on home page Parameters ---------- expiration : int The duration (in days) until the post expires Returns ------- bool Whether or not entering the expiration was successful """ if str(expiration) == "0" or not expiration: return True try: Settings.print("Expiration:") Settings.print("- Period: {}".format(expiration)) # if expiration is 'no limit', then there's no expiration and hence no point here if expiration == 999: return True def enter_expiration(expires): # enter duration Settings.dev_print("entering expiration") action = ActionChains(self.browser) action.click(on_element=self.find_element_to_click("expiresAdd")) action.pause(int(1)) action.send_keys(Keys.TAB) action.send_keys(str(expires)) action.pause(int(1)) action.key_down(Keys.SHIFT).send_keys(Keys.TAB).key_up(Keys.SHIFT) action.pause(int(1)) action.send_keys(Keys.ENTER) action.perform() Settings.dev_print("successfully entered expiration") # not really necessary with 'Clear' button def cancel_expiration(): #icon-close elements = self.browser.find_elements(By.TAG_NAME, "use") element = [elem for elem in elements if '#icon-close' in str(elem.get_attribute('href'))][0] ActionChains(self.browser).move_to_element(element).click().perform() enter_expiration(expiration) Settings.debug_delay_check() Settings.dev_print("### Expiration Successful ###") return True except Exception as e: Driver.error_checker(e) Settings.err_print("failed to enter expiration") try: Settings.dev_print("canceling expiration") self.find_element_to_click("expiresCancel").click() Settings.dev_print("successfully canceled expiration") Settings.dev_print("### Expiration Successful Failure ###") except: Settings.dev_print("### Expiration Failure Failure") return False ###################################################################### def find_element_by_name(self, name): """ Find element on page by name Does not auth check or otherwise change the focus Parameters ---------- name : str The name of the element to reference from its /elements/element name Returns ------- Selenium.WebDriver.WebElement The located web element if found by id, class name, or css selector """ element = Element.get_element_by_name(name) if not element: Settings.err_print("unable to find element reference") return None # prioritize id over class name eleID = None try: eleID = self.browser.find_element(By.ID, element.getId()) except: eleID = None if eleID: return eleID for className in element.getClasses(): ele = None eleCSS = None try: ele = self.browser.find_element(By.CLASS_NAME, className) except: ele = None # try: eleCSS = self.browser.find_element(By.CSS_SELECTOR, className) # except: eleCSS = None Settings.dev_print("class: {} - {}:css".format(ele, eleCSS)) if ele: return ele # if eleCSS: return eleCSS raise Exception("unable to locate element") def find_elements_by_name(self, name): """ Find elements on page by name. Does not change window focus. Parameters ---------- name : str The name of the element to reference from its /elements/element name Returns ------- list A list of the located Selenium.WebDriver.WebElements as found by id, class name, or css selector. Elements must also be displayed """ element = Element.get_element_by_name(name) if not element: Settings.err_print("unable to find element reference") return [] eles = [] for className in element.getClasses(): eles_ = [] elesCSS_ = [] try: eles_ = self.browser.find_elements(By.CLASS_NAME, className) except: eles_ = [] # try: elesCSS_ = self.browser.find_elements(By.CSS_SELECTOR, className) # except: elesCSS_ = [] Settings.dev_print("class: {} - {}:css".format(len(eles_), len(elesCSS_))) eles.extend(eles_) # eles.extend(elesCSS_) eles_ = [] for i in range(len(eles)): # Settings.dev_print("ele: {} -> {}".format(eles[i].get_attribute("innerHTML").strip(), element.getText())) if eles[i].is_displayed(): Settings.dev_print("found displayed ele: {}".format(eles[i].get_attribute("innerHTML").strip())) eles_.append(eles[i]) if len(eles_) == 0: raise Exception("unable to locate elements: {}".format(name)) return eles_ def find_element_to_click(self, name, useId=False, offset=0): """ Find element on page by name to click Does not auth check or otherwise change the focus. Checks that located element is properly capable of being clicked. Parameters ---------- name : str The name of the element to click as referenced from its /elements/element name Returns ------- Selenium.WebDriver.WebElements The located web element that can be clicked """ Settings.dev_print("finding click: {}".format(name)) element = Element.get_element_by_name(name) if not element: Settings.err_print("unable to find element reference - {}".format(name)) return False elements = element.getClasses() if useId: elements = element.getIds() for className in elements: eles = [] elesCSS = [] try: eles = self.browser.find_elements(By.CLASS_NAME, className) except: eles = [] # try: elesCSS = self.browser.find_elements(By.CSS_SELECTOR, className) # except: elesCSS = [] Settings.dev_print("class: {} - {}:css".format(len(eles), len(elesCSS))) eles.extend(elesCSS) for i in range(len(eles)): i += offset if i > len(eles): i = len(eles) Settings.dev_print("ele: {} -> {}".format(eles[i].get_attribute("innerHTML").strip(), element.getText())) if (eles[i].is_displayed() and element.getText() and str(element.getText().lower()) == eles[i].get_attribute("innerHTML").strip().lower()) and eles[i].is_enabled(): Settings.dev_print("found matching ele") # Settings.dev_print("found matching ele: {}".format(eles[i].get_attribute("innerHTML").strip())) return eles[i] elif (eles[i].is_displayed() and element.getText() and str(element.getText().lower()) in eles[i].get_attribute("innerHTML").strip().lower()) and eles[i].is_enabled(): Settings.dev_print("found matching(ish) ele") # Settings.dev_print("found matching ele: {}".format(eles[i].get_attribute("innerHTML").strip())) return eles[i] elif (eles[i].is_displayed() and element.getText() and str(element.getText().lower()) in eles[i].get_attribute("innerHTML").strip().lower()): Settings.dev_print("found text ele") # Settings.dev_print("found text ele: {}".format(eles[i].get_attribute("innerHTML").strip())) return eles[i] elif eles[i].is_displayed() and not element.getText() and eles[i].is_enabled(): Settings.dev_print("found enabled ele") # Settings.dev_print("found enabled ele: {}".format(eles[i].get_attribute("innerHTML").strip())) return eles[i] if len(eles) > 0: return eles[offset] Settings.dev_print("unable to find element - {}".format(name)) raise Exception("unable to locate element - {}".format(name)) ###################################################################### @staticmethod def get_driver(): """ Return an existing driver, if not create one Returns ------- classes.driver The default driver object. """ if len(Driver.DRIVERS) > 0: return Driver.DRIVERS[0] return Driver() # waits for page load def get_page_load(self): """Attempt to generic page load""" time.sleep(2) try: WebDriverWait(self.browser, 60*3, poll_frequency=10).until(EC.visibility_of_element_located((By.CLASS_NAME, "main-wrapper"))) except Exception as e: Settings.dev_print(e) def handle_alert(self): """Switch to alert pop up""" try: alert_obj = self.browser.switch_to.alert or None if alert_obj: alert_obj.accept() except: pass ############## ### Go Tos ### ############## def go_to_home(self, force=False): """ Go to home page If already at home don't go unless forced Parameters ---------- force : bool Force page goto even if already at url """ def goto(): Settings.maybe_print("goto -> onlyfans.com") try: self.browser.set_page_load_timeout(10) self.browser.get(ONLYFANS_HOME_URL) except TimeoutException: Settings.dev_print("timed out waiting for page to check login element") except WebDriverException as e: Settings.dev_print("error fetching home page") Settings.err_print(e) # self.open_tab(ONLYFANS_HOME_URL) self.handle_alert() self.get_page_load() if force: return goto() if self.search_for_tab(ONLYFANS_HOME_URL): Settings.maybe_print("found -> /") return Settings.dev_print("current url: {}".format(self.browser.current_url)) if str(self.browser.current_url) == str(ONLYFANS_HOME_URL): Settings.maybe_print("at -> onlyfans.com") self.browser.execute_script("window.scrollTo(0, 0);") else: goto() def go_to_page(self, page): """ Go to page If already at page don't go Parameters ---------- page : str The url of the OnlyFans 'page' to go to """ self.auth() if self.search_for_tab(page): Settings.maybe_print("found -> {}".format(page)) return if str(self.browser.current_url) == str(page) or str(page) in str(self.browser.current_url): Settings.maybe_print("at -> {}".format(page)) self.browser.execute_script("window.scrollTo(0, 0);") else: Settings.maybe_print("goto -> {}".format(page)) self.open_tab(page) self.handle_alert() self.get_page_load() def go_to_profile(self): """Go to OnlyFans profile page""" self.auth() username = Settings.get_username() if str(username) == "": username = self.get_username() page = "{}/{}".format(ONLYFANS_HOME_URL, username) if self.search_for_tab(page): Settings.maybe_print("found -> /{}".format(username)) return if str(username) in str(self.browser.current_url): Settings.maybe_print("at -> {}".format(username)) self.browser.execute_script("window.scrollTo(0, 0);") else: Settings.maybe_print("goto -> {}".format(username)) # self.browser.get("{}{}".format(ONLYFANS_HOME_URL, username)) self.open_tab(page) # self.handle_alert() # self.get_page_load() # onlyfans.com/my/settings def go_to_settings(self, settingsTab): """ Go to settings tab on settings page If already at tab, stay Parameters ---------- settingsTab : str The name of the Settings tab to go to """ self.auth() if self.search_for_tab("{}{}".format(ONLYFANS_SETTINGS_URL, settingsTab)): Settings.maybe_print("found -> settings/{}".format(settingsTab)) return if str(ONLYFANS_SETTINGS_URL) in str(self.browser.current_url) and str(settingsTab) == "profile": Settings.maybe_print("at -> onlyfans.com/settings/{}".format(settingsTab)) self.browser.execute_script("window.scrollTo(0, 0);") else: if str(settingsTab) == "profile": settingsTab = "" Settings.maybe_print("goto -> onlyfans.com/settings/{}".format(settingsTab)) self.go_to_page("{}{}".format(ONLYFANS_SETTINGS_URL, settingsTab)) def search_for_tab(self, page): """ Search for (and goto if exists) tab in Driver.tabs cache Parameters ---------- page : str The url of the OnlyFans 'page' to go to Returns ------- bool Whether or not the tab exists """ original_handle = self.browser.current_window_handle Settings.dev_print("searching for page: {}".format(page)) Settings.dev_print("tabs: {}".format(self.tabs)) Settings.dev_print("handles: {}".format(self.browser.window_handles)) try: Settings.dev_print("checking tabs...") for page_, handle, value in self.tabs: Settings.dev_print("{} = {}".format(page_, page)) if str(page_) in str(page): self.browser.switch_to.window(handle) value += 1 Settings.dev_print("successfully located tab in cache: {}".format(page)) return True Settings.dev_print("checking handles...") for handle in self.browser.window_handles: Settings.dev_print(handle) self.browser.switch_to.window(handle) if str(page) in str(self.browser.current_url): Settings.dev_print("successfully located tab in handles: {}".format(page)) return True Settings.dev_print("failed to locate tab: {}".format(page)) self.browser.switch_to.window(original_handle) except Exception as e: # print(e) # if "Unable to locate window" not in str(e): Settings.dev_print(e) return False def open_tab(self, url): """ Open new tab of url Parameters ---------- url : str The url to open in a new tab Returns ------- bool Whether or not the tab was opened successfully """ Settings.maybe_print("tab -> {}".format(url)) # self.browser.find_element(By.TAG_NAME, 'body').send_keys(Keys.CONTROL + 't') # self.browser.get(url) # https://stackoverflow.com/questions/50844779/how-to-handle-multiple-windows-in-python-selenium-with-firefox-driver windows_before = self.browser.current_window_handle Settings.dev_print("current window handle is : %s" %windows_before) windows = self.browser.window_handles self.browser.execute_script('''window.open("{}","_blank");'''.format(url)) # self.browser.execute_script("window.open('{}')".format(url)) self.handle_alert() self.get_page_load() WebDriverWait(self.browser, 10).until(EC.number_of_windows_to_be(len(windows)+1)) windows_after = self.browser.window_handles new_window = [x for x in windows_after if x not in windows][0] # self.browser.switch_to.window(new_window)